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 .