简单全链路应用
Spring Boot 快速开始
Spring Boot 是 Spring 平台上有一定约束的,基于约定的配置的补充,对于以最小的努力入门和创建独立的生产级应用程序非常有用。通过 Spring Boot CLI 能够直接运行如下的代码:
@RestController
@EnableAutoConfiguration
public class Example {
@RequestMapping("/")
String home() {
return "Hello World!";
}
public static void main(String[] args) throws Exception {
SpringApplication.run(Example.class, args);
}
}
我们可以在 Spring Initializr 站点中快速生成 Spring Boot 项目模板,或者使用 Spring Boot CLI 来直接运行本地代码,而不需要关心具体的 Spring 项目配置。创建之后的项目会依赖于 Boot 父项目:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</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>
应用配置
我们可以先创建简单的主应用类:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
很多时候,也可以在配置文件中引入 XML 的配置文件:
@SpringBootApplication
@EnableConfigurationProperties(ServiceProperties.class)
@ImportResource("integration-context.xml")
public class SampleIntegrationApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(SampleIntegrationApplication.class, args);
}
}
@SpringBootApplication 等价于 @Configuration, @EnableAutoConfiguration, 以及 @ComponentScan 的综合体。最后,我们可以定义简单的应用属性文件 application.properties:
server.port=8081
更多 Spring 内置的配置参数项可以参考 Spring Boot properties available。
MVC
接下来我们可以通过 Thymeleaf 来添加基础的 MVC 特性,我们可以添加 spring-boot-starter-thymeleaf 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
然后在 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>
现代开发中很多的不是直接返回页面,而是以接口方式与前端进行交互,譬如这里简单的 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 响应。
Security(安全)
接下来我们可以为我们的应用添加安全控制,首先需要将 spring-boot-starter-security 添加到项目依赖中;
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
该依赖添加完毕后,所有端点都将使用 httpBasic 或 formLogin 进行保护。这就是为什么,如果我们在类路径上有启动器,通常应该通过扩展 WebSecurityConfigurerAdapter 类来定义自己的自定义安全性配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll().and().csrf().disable();
}
}
简单持久化(Persistence)
我们可以利用 Spring Data 进行快速地持久化操作:
@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 的 CrudRepository:
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 {
...
}
这里我们可以使用内存数据库 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=
异常处理(Error Handling)
在基础应用之上,我们还需要为应用添加异常处理的能力,这里基于 @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
);
}
}
这里的异常类型是允许我们进行业务化定制的:
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
Testing(测试)
最后我们可以测试新的 Books 接口,这里主要使用 @SpringBootTest 来加载应用上下文:
@RunWith(SpringRunner.class)
@SpringBootTest
public class Chapter11ApplicationTests {
private MockMvc mvc;
@Before
public void setUp() throws Exception {
mvc = MockMvcBuilders.standaloneSetup(new HelloController()).build();
}
@Test
public void getHello() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/hello").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string(equalTo("Hello World")));
}
}
也可以单独使用 JUnit 来以黑盒方式测试 API:
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 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 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());
}
}