Spring Boot 教程 – 快速启动一个简单应用

更新于 2025-12-30

baeldung 2017-06-12

1. 概述

Spring Boot 是 Spring 平台的一个“约定优于配置”的扩展,旨在以最少的配置快速构建独立、生产级的应用程序。

本教程是 Spring Boot 的入门指南,通过一个简单的 Web 应用带你快速上手。

我们将涵盖核心配置、前端页面、基本数据操作以及异常处理等内容。


2. 项目搭建

首先,使用 Spring Initializr 生成项目基础结构。

生成的项目会继承 Spring Boot 父 POM:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.4</version>
    <relativePath />
</parent>

初始依赖项非常简单:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.5.4</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>3.5.4</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>

3. 应用配置

接下来,创建主应用类:

@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 改为 8081。当然,Spring Boot 还支持大量其他配置属性。


4. 简单的 MVC 视图

现在,我们使用 Thymeleaf 添加一个简单的前端页面。

首先,在 pom.xml 中添加 Thymeleaf 依赖:

<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    <version>3.5.4</version>
</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
public class SimpleController {
    @Value("${spring.application.name}")
    String appName;

    @RequestMapping("/")
    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>

注意:我们在页面中使用了 application.properties 中定义的属性,并通过 @Value 注入到控制器中。


5. 安全性

接下来,为应用添加安全控制。首先引入 Spring Security Starter:

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

可以看到一个规律:大多数 Spring 功能都可以通过简单的 Starter 依赖轻松集成。

一旦 spring-boot-starter-security 在类路径中,所有端点默认都会被保护(根据内容协商策略,使用 HTTP Basic 或表单登录)。

因此,通常我们需要自定义安全配置:

@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 是一个庞大而复杂的主题,远不止这几行配置。建议深入学习相关文档。


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 的能力:

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:扫描 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=

同样,数据持久化是一个更广泛的主题,值得深入探索。


7. Web 层与控制器

现在来看 Web 层。我们创建一个 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);
    }
}

由于这是一个 API,我们使用了 @RestController 注解(相当于 @Controller + @ResponseBody),使每个方法直接将返回对象序列化为 HTTP 响应体。

注意:在此简单应用中,我们直接暴露 Book 实体作为 API 资源。但在真实项目中,通常应将实体与 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>

也可以通过配置修改错误路径:

server.error.path=/error2

9. 测试

最后,测试我们的 Books API。

首先使用 @SpringBootTest 加载应用上下文,确保应用能正常启动:

@ExtendWith(SpringExtension.class)
@SpringBootTest
public class SpringContextTest {

    @Test
    public void contextLoads() {
    }
}

接下来,使用 REST Assured 编写 API 集成测试。

先添加依赖:

<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <version>5.5.5</version>
    <scope>test</scope>
</dependency>

编写测试类:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SpringBootBootstrapLiveTest {

    @LocalServerPort
    private int port;
    private String API_ROOT;

    @BeforeEach
    public void setUp() {
        API_ROOT = "http://localhost:" + port + "/api/books";
        RestAssured.port = port;
    }

    private Book createRandomBook() {
        final Book book = new Book();
        book.setTitle(randomAlphabetic(10));
        book.setAuthor(randomAlphabetic(15));
        return book;
    }

    private String createBookAsUri(Book book) {
        final 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());
}

创建测试

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

更新测试

@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 入门指南。

当然,我们只是浅尝辄止。Spring Boot 的功能远不止于此,还有大量高级特性值得深入学习和探索。