Перейти к основному содержимому

Учебное пособие по Spring Boot — загрузите простое приложение

· 8 мин. чтения

1. Обзор

Spring Boot — это продуманное дополнение к платформе Spring, ориентированное на соглашение, а не на конфигурацию, — очень полезное средство для начала работы с минимальными усилиями и создания автономных приложений производственного уровня.

Этот учебник является отправной точкой для Boot , другими словами, это простой способ начать работу с базовым веб-приложением.

Мы рассмотрим базовую конфигурацию, внешний интерфейс, быструю обработку данных и обработку исключений.

2. Настройка

Во-первых, давайте воспользуемся Spring Initializr для создания базы для нашего проекта.

Сгенерированный проект зависит от родителя Boot:

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
<relativePath />
</parent>

Начальные зависимости будут довольно простыми:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>

3. Конфигурация приложения

Далее мы настроим простой основной класс для нашего приложения:

@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

Обратите внимание, как мы используем @SpringBootApplication в качестве основного класса конфигурации приложения. За кулисами это эквивалентно @Configuration , @EnableAutoConfiguration и @ComponentScan вместе.

Наконец, мы определим простой файл application.properties , который пока имеет только одно свойство:

server.port=8081

server.port изменяет порт сервера с 8080 по умолчанию на 8081; конечно, есть много других доступных свойств Spring Boot .

4. Простой вид MVC

Давайте теперь добавим простой внешний интерфейс, используя Thymeleaf.

Во- первых, нам нужно добавить зависимость spring-boot-starter-thymeleaf в наш pom.xml :

<dependency> 
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Это включает Thymeleaf по умолчанию. Дополнительная настройка не требуется.

Теперь мы можем настроить его в нашем application.properties :

spring.thymeleaf.cache=false
spring.thymeleaf.enabled=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

spring.application.name=Bootstrap Spring Boot

Далее мы определим простой контроллер и базовую домашнюю страницу с приветственным сообщением:

@Controller
public class SimpleController {
@Value("${spring.application.name}")
String appName;

@GetMapping("/")
public String homePage(Model model) {
model.addAttribute("appName", appName);
return "home";
}
}

Наконец, вот наш home.html :

<html>
<head><title>Home Page</title></head>
<body>
<h1>Hello !</h1>
<p>Welcome to <span th:text="${appName}">Our App</span></p>
</body>
</html>

Обратите внимание, как мы использовали свойство, которое мы определили в наших свойствах, а затем внедрили его, чтобы мы могли показать его на нашей домашней странице.

5. Безопасность

Далее давайте добавим безопасность в наше приложение, сначала включив стартер безопасности:

<dependency> 
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

К настоящему времени мы можем заметить закономерность: большинство библиотек Spring легко импортируются в наш проект с помощью простых стартеров Boot.

Как только зависимость spring-boot-starter-security находится в пути к классам приложения, все конечные точки по умолчанию защищены с использованием либо httpBasic , либо formLogin на основе стратегии согласования содержимого Spring Security.

Вот почему, если у нас есть стартер в пути к классам, мы обычно должны определять нашу собственную пользовательскую конфигурацию безопасности, расширяя класс WebSecurityConfigurerAdapter :

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.permitAll()
.and().csrf().disable();
}
}

В нашем примере мы разрешаем неограниченный доступ ко всем конечным точкам.

Конечно, Spring Security — это обширная тема, и ее не так просто осветить в паре строк конфигурации. Таким образом, мы определенно поощряем более глубокое изучение темы .

6. Простое упорство

Давайте начнем с определения нашей модели данных, простой сущности Book :

@Entity
public class Book {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;

@Column(nullable = false, unique = true)
private String title;

@Column(nullable = false)
private String author;
}

и его репозиторий, хорошо использующий Spring Data здесь:

public interface BookRepository extends CrudRepository<Book, Long> {
List<Book> findByTitle(String title);
}

Наконец, нам, конечно же, нужно настроить наш новый уровень сохраняемости:

@EnableJpaRepositories("com.foreach.persistence.repo") 
@EntityScan("com.foreach.persistence.model")
@SpringBootApplication
public class Application {
...
}

Обратите внимание, что мы используем следующее:

  • @EnableJpaRepositories для сканирования указанного пакета на наличие репозиториев.
  • @EntityScan для получения наших сущностей JPA

Для простоты мы используем здесь базу данных H2 в памяти. Это сделано для того, чтобы у нас не было никаких внешних зависимостей при запуске проекта.

Как только мы включим зависимость H2, Spring Boot автоматически обнаружит ее и настроит наше постоянство без необходимости дополнительной настройки, кроме свойств источника данных:

spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:bootapp;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=

Конечно, как и безопасность, постоянство — это более широкая тема, чем этот базовый набор здесь, и ее, безусловно, следует исследовать дальше .

7. Веб и контроллер

Далее давайте взглянем на веб-уровень. И мы начнем с настройки простого контроллера BookController .

Мы реализуем базовые операции CRUD, раскрывающие ресурсы Book , с помощью простой проверки:

@RestController
@RequestMapping("/api/books")
public class BookController {

@Autowired
private BookRepository bookRepository;

@GetMapping
public Iterable findAll() {
return bookRepository.findAll();
}

@GetMapping("/title/{bookTitle}")
public List findByTitle(@PathVariable String bookTitle) {
return bookRepository.findByTitle(bookTitle);
}

@GetMapping("/{id}")
public Book findOne(@PathVariable Long id) {
return bookRepository.findById(id)
.orElseThrow(BookNotFoundException::new);
}

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Book create(@RequestBody Book book) {
return bookRepository.save(book);
}

@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
bookRepository.findById(id)
.orElseThrow(BookNotFoundException::new);
bookRepository.deleteById(id);
}

@PutMapping("/{id}")
public Book updateBook(@RequestBody Book book, @PathVariable Long id) {
if (book.getId() != id) {
throw new BookIdMismatchException();
}
bookRepository.findById(id)
.orElseThrow(BookNotFoundException::new);
return bookRepository.save(book);
}
}

Учитывая, что этот аспект приложения представляет собой API, мы использовали здесь аннотацию @ RestController , которая эквивалентна @Controller вместе с @ResponseBody , чтобы каждый метод маршалировал возвращенный ресурс прямо в HTTP-ответ.

Обратите внимание, что здесь мы выставляем нашу сущность Book как внешний ресурс. Это нормально для этого простого приложения, но в реальном приложении мы, вероятно, захотим разделить эти две концепции .

8. Обработка ошибок

Теперь, когда основное приложение готово к работе, давайте сосредоточимся на простом централизованном механизме обработки ошибок с помощью @ControllerAdvice :

@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {

@ExceptionHandler({ BookNotFoundException.class })
protected ResponseEntity<Object> handleNotFound(
Exception ex, WebRequest request) {
return handleExceptionInternal(ex, "Book not found",
new HttpHeaders(), HttpStatus.NOT_FOUND, request);
}

@ExceptionHandler({ BookIdMismatchException.class,
ConstraintViolationException.class,
DataIntegrityViolationException.class })
public ResponseEntity<Object> handleBadRequest(
Exception ex, WebRequest request) {
return handleExceptionInternal(ex, ex.getLocalizedMessage(),
new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
}
}

Помимо стандартных исключений, которые мы здесь обрабатываем, мы также используем пользовательское исключение BookNotFoundException :

public class BookNotFoundException extends RuntimeException {

public BookNotFoundException(String message, Throwable cause) {
super(message, cause);
}
// ...
}

Это дает нам представление о возможностях этого глобального механизма обработки исключений. Чтобы увидеть полную реализацию, ознакомьтесь с подробным руководством .

Обратите внимание, что Spring Boot по умолчанию также предоставляет сопоставление /error . Мы можем настроить его представление, создав простой файл error.html :

<html lang="en">
<head><title>Error Occurred</title></head>
<body>
<h1>Error Occurred!</h1>
<b>[<span th:text="${status}">status</span>]
<span th:text="${error}">error</span>
</b>
<p th:text="${message}">message</p>
</body>
</html>

Как и большинство других аспектов в Boot, мы можем управлять этим с помощью простого свойства:

server.error.path=/error2

9. Тестирование

Наконец, давайте протестируем наш новый API книг.

Мы можем использовать @SpringBootTest для загрузки контекста приложения и проверки отсутствия ошибок при запуске приложения:

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringContextTest {

@Test
public void contextLoads() {
}
}

Далее добавим тест JUnit, который проверяет вызовы написанного нами API, используя REST Assured .

Во- первых, мы добавим гарантированную зависимость:

<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>

И теперь мы можем добавить тест:

public class SpringBootBootstrapLiveTest {

private static final String API_ROOT
= "http://localhost:8081/api/books";

private Book createRandomBook() {
Book book = new Book();
book.setTitle(randomAlphabetic(10));
book.setAuthor(randomAlphabetic(15));
return book;
}

private String createBookAsUri(Book book) {
Response response = RestAssured.given()
.contentType(MediaType.APPLICATION_JSON_VALUE)
.body(book)
.post(API_ROOT);
return API_ROOT + "/" + response.jsonPath().get("id");
}
}

Во-первых, мы можем попробовать найти книги, используя вариантные методы:

@Test
public void whenGetAllBooks_thenOK() {
Response response = RestAssured.get(API_ROOT);

assertEquals(HttpStatus.OK.value(), response.getStatusCode());
}

@Test
public void whenGetBooksByTitle_thenOK() {
Book book = createRandomBook();
createBookAsUri(book);
Response response = RestAssured.get(
API_ROOT + "/title/" + book.getTitle());

assertEquals(HttpStatus.OK.value(), response.getStatusCode());
assertTrue(response.as(List.class)
.size() > 0);
}
@Test
public void whenGetCreatedBookById_thenOK() {
Book book = createRandomBook();
String location = createBookAsUri(book);
Response response = RestAssured.get(location);

assertEquals(HttpStatus.OK.value(), response.getStatusCode());
assertEquals(book.getTitle(), response.jsonPath()
.get("title"));
}

@Test
public void whenGetNotExistBookById_thenNotFound() {
Response response = RestAssured.get(API_ROOT + "/" + randomNumeric(4));

assertEquals(HttpStatus.NOT_FOUND.value(), response.getStatusCode());
}

Далее мы протестируем создание новой книги:

@Test
public void whenCreateNewBook_thenCreated() {
Book book = createRandomBook();
Response response = RestAssured.given()
.contentType(MediaType.APPLICATION_JSON_VALUE)
.body(book)
.post(API_ROOT);

assertEquals(HttpStatus.CREATED.value(), response.getStatusCode());
}

@Test
public void whenInvalidBook_thenError() {
Book book = createRandomBook();
book.setAuthor(null);
Response response = RestAssured.given()
.contentType(MediaType.APPLICATION_JSON_VALUE)
.body(book)
.post(API_ROOT);

assertEquals(HttpStatus.BAD_REQUEST.value(), response.getStatusCode());
}

Затем мы обновим существующую книгу:

@Test
public void whenUpdateCreatedBook_thenUpdated() {
Book book = createRandomBook();
String location = createBookAsUri(book);
book.setId(Long.parseLong(location.split("api/books/")[1]));
book.setAuthor("newAuthor");
Response response = RestAssured.given()
.contentType(MediaType.APPLICATION_JSON_VALUE)
.body(book)
.put(location);

assertEquals(HttpStatus.OK.value(), response.getStatusCode());

response = RestAssured.get(location);

assertEquals(HttpStatus.OK.value(), response.getStatusCode());
assertEquals("newAuthor", response.jsonPath()
.get("author"));
}

И мы можем удалить книгу:

@Test
public void whenDeleteCreatedBook_thenOk() {
Book book = createRandomBook();
String location = createBookAsUri(book);
Response response = RestAssured.delete(location);

assertEquals(HttpStatus.OK.value(), response.getStatusCode());

response = RestAssured.get(location);
assertEquals(HttpStatus.NOT_FOUND.value(), response.getStatusCode());
}

10. Заключение

Это было быстрое, но всестороннее введение в Spring Boot.

Конечно, мы едва поцарапали поверхность здесь. В этой структуре гораздо больше, чем мы можем охватить в одной вводной статье.

Именно поэтому у нас на сайте есть не одна статья, посвященная Boot .

Как всегда, полный исходный код наших примеров здесь закончился на GitHub .