1. Общ преглед
Spring Boot е умишлено допълнение към платформата Spring, фокусирано върху конфигурацията, което е изключително полезно за започване с минимални усилия и създаване на самостоятелни приложения от производствен клас.
Този урок е отправна точка за Boot - начин да започнете по прост начин, с основно уеб приложение.
Ще разгледаме някаква основна конфигурация, интерфейс, бърза манипулация на данни и обработка на изключения.
2. Настройка
Първо, нека използваме Spring Initializr, за да генерираме основата за нашия проект.
Генерираният проект разчита на родител на Boot:
org.springframework.boot spring-boot-starter-parent 2.2.2.RELEASE
Първоначалните зависимости ще бъдат доста прости:
org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-jpa com.h2database h2
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 :
org.springframework.boot spring-boot-starter-thymeleaf
Това позволява 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 :
Home Page Welcome to Our App
Обърнете внимание как използвахме свойство, което дефинирахме в нашите свойства - и след това го инжектирахме, за да можем да го покажем на нашата начална страница.
5. Сигурност
След това нека добавим защита към нашето приложение - като първо включим стартера за сигурност:
org.springframework.boot spring-boot-starter-security
Надяваме се, че досега забелязвате модел - повечето пролетни библиотеки лесно се импортират в нашия проект с помощта на прости начални стартиращи файлове .
След като зависимостта 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. Проста упоритост
Нека започнем с дефиниране на нашия модел данни - прост обект на книга :
@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 { List findByTitle(String title); }
И накрая, разбира се, трябва да конфигурираме новия ни слой на устойчивост:
@EnableJpaRepositories("com.baeldung.persistence.repo") @EntityScan("com.baeldung.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=
Of course, like security, persistence is a broader topic than this basic set here, and one you should certainly explore further.
7. Web and the Controller
Next, let's have a look at a web tier – and we'll start that by setting up a simple controller – the BookController.
We'll implement basic CRUD operations exposing Book resources with some simple validation:
@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); } }
Given this aspect of the application is an API, we made use of the @RestController annotation here – which equivalent to a @Controller along with @ResponseBody – so that each method marshalls the returned resource right to the HTTP response.
Just one note worth pointing out – we're exposing our Book entity as our external resource here. That's fine for our simple application here, but in a real-world application, you will likely want to separate these two concepts.
8. Error Handling
Now that the core application is ready to go, let's focus on a simple centralized error handling mechanism using @ControllerAdvice:
@ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler({ BookNotFoundException.class }) protected ResponseEntity 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 handleBadRequest( Exception ex, WebRequest request) { return handleExceptionInternal(ex, ex.getLocalizedMessage(), new HttpHeaders(), HttpStatus.BAD_REQUEST, request); } }
Beyond the standard exceptions we're handling here, we're also using a custom exception:
BookNotFoundException:
public class BookNotFoundException extends RuntimeException { public BookNotFoundException(String message, Throwable cause) { super(message, cause); } // ... }
This should give you an idea of what's possible with this global exception handling mechanism. If you'd like to see a full implementation, have a look at the in-depth tutorial.
Note that Spring Boot also provides an /error mapping by default. We can customize its view by creating a simple error.html:
Error Occurred [status] error message
Like most other aspects in Boot, we can control that with a simple property:
server.error.path=/error2
9. Testing
Finally, let's test our new Books API.
We can make use of @SpringBootTest to load the application context and verify there are no errors when running the app:
@RunWith(SpringRunner.class) @SpringBootTest public class SpringContextTest { @Test public void contextLoads() { } }
Next, let's add a JUnit test that verifies the calls to the API we're written, using RestAssured:
public class SpringBootBootstrapLiveTest { private static final String API_ROOT = "//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"); } }
First, we can try to find books using variant methods:
@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()); }
Next, we'll test creating a new book:
@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()); }
Update an existing book:
@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")); }
And delete a book:
@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. Conclusion
This was a quick but comprehensive intro to Spring Boot.
Разбира се, ние едва надраскахме повърхността тук - в тази рамка има много повече неща, които можем да обхванем в една статия.
Точно затова нямаме само една статия за Boot на сайта.
Пълният изходен код на нашите примери тук е, както винаги, върху GitHub.