Spring Boot 最全面的入门教程

1、概览

Spring Boot 是一个基于 Spring 的框架,旨在简化 Spring 应用的配置和开发过程,通过自动配置和约定大于配置的原则,使开发者能够快速搭建独立、生产级别的应用程序。

本文将带你了解 Spring Boot 的核心内容,从基本的项目创建开始,内容包括了:应用配置、Thymeleaf 视图配置、Spring Security 配置、持久层配置、Web 层 Controller 配置以及异常处理和测试。

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、应用配置

创建 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 视图

使用 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、安全

接下来,为应用添加安全配置,首先添加 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 的内容协商策略使用 httpBasicformLogin 进行默认安全保护。

通常,需要自定义安全配置:

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

然后后是实体类对应的 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

首先创建一个简单的 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、异常处理

使用 @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、测试

最后,来测试一下 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、总结

本文是对 Spring Boot 快速而全面的介绍。

当然,我们只是触及了皮毛。这个框架的内容远非一篇介绍文章所能涵盖。所以,推荐你阅读 Spring Boot 中文文档


Ref:https://www.baeldung.com/spring-boot-start