1. 概述
Spring Boot 是 Spring 平台的一個帶有指導意見的增強,專注於約定優於配置——對於從最小的努力開始並創建獨立、生產級別的應用程序來説,這一點非常有用。
本教程是 Boot 的一個入門點,換句話説,它提供了一種簡單的方式來入門,使用一個基本的 Web 應用程序。
我們將涵蓋一些核心配置、前端、快速的數據操作以及異常處理。
2. 設置
首先,我們使用 Spring Initializr 生成項目的基礎。
生成的項目依賴於 Boot 父項目:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
<relativePath />
</parent>最初的依賴項將會相當簡單:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.5.4</version>
</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>
<version>3.5.4</version>
</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;
@RequestMapping("/")
public String homePage(Model model) {
model.addAttribute("appName", appName);
return "home";
}
}
<p>最後,這是我們的 <em >home.html</em >:</p>
<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>
<version>3.5.4</version>
</dependency>
現在我們可以注意到一個規律:大多數 Spring 庫都可以通過使用簡單的 Boot 啓動器輕鬆導入到我們的項目中。
一旦 spring-boot-starter-security 依賴項已添加到應用程序的類路徑中,則所有端點默認情況下都已通過使用 httpBasic 或 formLogin(基於 Spring Security 的內容協商策略)進行保護。
因此,如果啓動器已添加到類路徑中,我們通常應定義自己的自定義 Security 配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(expressionInterceptUrlRegistry ->
expressionInterceptUrlRegistry
.anyRequest()
.permitAll())
.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
}在我們的示例中,我們允許所有端點不受限制地訪問。
當然,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.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=
當然,就像安全一樣,持久性是一個比這裏這個基本集更廣泛的主題,也值得進一步探索。
7. Web 和控制器
接下來,讓我們看看 Web 層。我們首先將設置一個簡單的控制器,即 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. 錯誤處理
現在核心應用程序已準備就緒,讓我們重點關注使用 <em @ControllerAdvice</em> 實現的簡單集中式錯誤處理機制。
@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=/error29. 測試
最後,讓我們測試我們新的 Books API。
我們可以利用 @SpringBootTest 加載應用程序上下文並驗證應用程序運行時沒有錯誤:
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class SpringContextTest {
@Test
public void contextLoads() {
}
}接下來,讓我們添加一個 JUnit 測試,以驗證我們編寫的 API 調用,並使用 REST Assured。
首先,我們將添加 rest-assured 依賴項:
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.5.5</version
<scope>test</scope>
</dependency>現在我們可以添加測試:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SpringBootBootstrapLiveTest {
@LocalServerPort
private int port;
private String API_ROOT;
@BeforeEach
public void setUp() {
API_ROOT = "http://localhost:" + port + "/api/books";
RestAssured.port = port;
}
private Book createRandomBook() {
final Book book = new Book();
book.setTitle(randomAlphabetic(10));
book.setAuthor(randomAlphabetic(15));
return book;
}
private String createBookAsUri(Book book) {
final 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 介紹。當然,我們在這裏只是打下了基礎。這個框架遠比一篇簡單的介紹文章所能涵蓋的要多。
這就是為什麼我們網站上有不止一篇涵蓋 Spring Boot 的文章的原因。