简单全链路应用
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());
  }
}
