1、概览 {#1概览}
Spring Boot 是一个基于 Spring 的框架,旨在简化 Spring 应用的配置和开发过程,通过自动配置和约定大于配置的原则,使开发者能够快速搭建独立、生产级别的应用程序。
本文将带你了解 Spring Boot 的核心内容,从基本的项目创建开始,内容包括了:应用配置、Thymeleaf 视图配置、Spring Security 配置、持久层配置、Web 层 Controller 配置以及异常处理和测试。
2、设置 {#2设置}
首先,使用 Spring Initializr 生成基础项目。
生成的项目依赖于 Spring Boot Parent 项目:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<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、应用配置 {#3应用配置}
创建 Main Application 类:
@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。更多的可配置属性,可以参阅 中文文档。
4、MVC 视图 {#4mvc-视图}
使用 Thymeleaf 添加一个简单的 HTML 前端页面。
首先,需要在 pom.xml
中添加 spring-boot-starter-thymeleaf
依赖:
<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 和一个基本主页:
@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>
在 Controller 中注入了 properties 中定义的属性,然后渲染在主页上。
5、安全 {#5安全}
接下来,为应用添加安全配置,首先添加 Security Starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
你可能已经注意到了,只需要简单的导入 starter
依赖,Spring Boot 就会依据约定自动进行配置。
一旦在应用的 classpath 上添加了 spring-boot-starter-security
依赖,所有端点都会根据 Spring Security 的内容协商策略使用 httpBasic 或 formLogin 进行默认安全保护。
通常,需要自定义安全配置:
@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 是一个广泛的主题,不是几行配置就能轻易涵盖的。因此,推荐你阅读 Spring Security 中文文档。
6、持久层 {#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;
}
然后后是实体类对应的 Repository,这里使用了 Spring Data:
public interface BookRepository extends CrudRepository<Book, Long> {
List<Book> findByTitle(String title);
}
最后,需要配置持久层:
@EnableJpaRepositories("com.baeldung.persistence.repo") // Repository 接口所在包
@EntityScan("com.baeldung.persistence.model") // 实体类所在包
@SpringBootApplication
public class Application {
...
}
@EnableJpaRepositories
指定 Repository 接口所在包。@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=
当然,就像 Spring Security 一样,持久层也是一个比这里的基本内容更广泛的话题,推荐你阅读 Spring Data Jpa 中文文档。
7、Web 层和 Controller {#7web-层和-controller}
首先创建一个简单的 Controller:BookController
。
使用一些简单的验证来实现对 Book
资源的基本 CRUD 操作。
@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 响应。
如上,Controller 直接将 Book
实体作为资源公开,这对于简单的应用来说没有问题,但在实际应用中,可能需要把实体类转换为其他的 POJO 对象。
8、异常处理 {#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>
可以通过一个简单的属性来控制它:
server.error.path=/error2
9、测试 {#9测试}
最后,来测试一下 Book API。
使用 @SpringBootTest
来加载 Application Context,并验证运行应用时是否出现异常:
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringContextTest {
@Test
public void contextLoads() {
}
}
接下来,添加一个 JUnit 测试,使用 REST Assured 来验证 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");
}
}
首先,尝试使检索 Book:
@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());
}
接着,创建 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());
}
然后,更新 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"));
}
最后,删除 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、总结 {#10总结}
本文是对 Spring Boot 快速而全面的介绍。
当然,我们只是触及了皮毛。这个框架的内容远非一篇介绍文章所能涵盖。所以,推荐你阅读 Spring Boot 中文文档。
Ref:https://www.baeldung.com/spring-boot-start