1. 概述

Spring Boot 使用约定优于配置的设计模式,通过最小化配置, 使开发人员能快速搭建Java开发环境,并创建独生产级应用程序。

本文我们将学习如何使用 Spring Boot 创建一个简单的 Web 应用程序,包括核心配置、前端开发、数据操作和异常处理。

2. Maven 配置

首先我们使用 Spring 官方提供的 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. Application 配置

定义Main Class,这是我们程序的入口:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@SpringBootApplication 注解是 @Configuration、*@EnableAutoConfiguration* 和 @ComponentScan 的组合。

最后,我们需要创建一个 application.properties 配置文件,目前只包含一项设置:

server.port=8081

默认服务器端口是8080,通过 server.port 我们修改为8081。当然还有其他很多配置暂时用不上,详情请参考Spring Boot官方属性配置说明。

4. 一个简单的 MVC View

对应前端,我们使用 Thymeleaf 模板引擎来实现页面的渲染。

首先添加 spring-boot-starter-thymeleaf Maven 依赖:

<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>

5. Spring Security

要想使用 Spring Security,我们需要添加下面依赖:

<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-security</artifactId> 
</dependency>

到目前为止,是不是发现 大多数 Spring 库都可以通过 spring-boot-starter-XXX 轻松集成到项目中。 不需要复杂繁琐的配置,这就是 Spring Boot 的强大之处。

添加 spring-boot-starter-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 是一个广泛的主题,无法通过几行配置轻松涵盖,感兴趣的朋友请移步至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 来实现,定义BookRepository:

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 指定需要扫描的repository包名
  • @EntityScan 指定实体类所在包名

为了方便演示,我们在这里使用 H2 作为数据库。它是一个内存数据库,可嵌入到我们的程序中,跟随我们的应用一起启动,这样我们就不用安装MySQL等外部数据库了。

一旦我们添加l H2 依赖,Spring Boot 会自动检测到它并完成自动装配,除了datasource属性外,我们不需要额外的配置:

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 持久化专题

7. MVC Controller

接下来,我们实现 BookController,实现基本的 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);
    }
}

注意我们使用的是 @RestController 注解,它是 @Controller@ResponseBody 的组合。它返回的是纯数据,通常为JSON格式,而非HTML页面。

在接口中我们直接暴露了Book实体,这作为演示是没问题的。但实际项目中,我们通常会将其转为DTO后传输,具体请参考实体与DTO的转换

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>

当然和Spring Boot其他一样,可以修改配置控制:

server.error.path=/error2

9. 测试

最后,让我们测试我们的Book接口。

我们可以使用 @SpringBootTest 来加载Spring 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");
    }
}

查询操作:

@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"));
}

最后,我们可以删除一本书:

@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 介绍。

当然,我们在这里只触及了表面。这个框架还有很多东西值得我们学习。

这就是为什么 我们不止一篇文章覆盖 Boot 在网站上。

当然,我们所有的示例代码都可以在 GitHub 上找到。


« 上一篇: Java Weekly, 第181期
» 下一篇: Liquibase回滚介绍