简单全链路应用

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());
  }
}

打包部署

Links

上一页
下一页