Spring Boot 概述

什么是 Spring Boot

Spring Boot 是由 Pivotal 团队提供的基于 Spring 框架的全新框架旨在简化 Spring 应用的初始搭建和开发过程

  • 核心功能
    • 自动配置根据 classpath 中的依赖自动配置 Spring 应用
    • 起步依赖提供一组便捷的依赖描述符
    • 内嵌服务器内置 TomcatJetty 或 Undertow
    • 生产就绪提供监控健康检查外部化配置等功能
  • 主要优势
    • 快速启动几分钟内即可运行 Spring 应用
    • 零配置无需 XML 配置开箱即用
    • 独立运行可打包为可执行 JAR 文件
    • 生态完善与 Spring 生态系统无缝集成

Spring Boot 的核心概念

自动配置

Spring Boot 根据 classpath 中的依赖自动配置 Bean

1
2
3
4
5
6
// 例如添加了 spring-boot-starter-web 依赖后
// Spring Boot 自动配置
// - DispatcherServlet
// - Tomcat 服务器
// - Jackson JSON 序列化
// - 静态资源处理

起步依赖Starter

起步依赖是一组方便的依赖描述符简化了依赖管理

Starter 功能
spring-boot-starter-web Web 开发Tomcat + Spring MVC
spring-boot-starter-data-jpa JPA 数据访问
spring-boot-starter-data-redis Redis 数据访问
spring-boot-starter-security Spring Security
spring-boot-starter-test 测试支持
spring-boot-starter-actuator 生产监控

外部化配置

支持多种配置方式优先级从高到低

  1. 命令行参数
  2. JNDI 属性
  3. JVM 系统属性
  4. 操作系统环境变量
  5. 随机值RandomValuePropertySource
  6. application-{profile}.properties/yml
  7. application.properties/yml
  8. @PropertySource 注解
  9. 默认属性

Spring Boot 的工作原理

启动流程

1
2
3
4
5
6
7
1. 创建 SpringApplication 实例
2. 运行 run() 方法
3. 准备环境Environment
4. 创建 ApplicationContext
5. 刷新上下文加载 Bean
6. 调用 CommandLineRunner 和 ApplicationRunner
7. 应用启动完成

自动配置原理

1
2
3
4
5
1. @SpringBootApplication 包含 @EnableAutoConfiguration
2. @EnableAutoConfiguration 导入 AutoConfigurationImportSelector
3. 读取 META-INF/spring.factories 中的自动配置类
4. 根据 @Conditional 条件判断是否生效
5. 注册符合条件的 Bean

快速开始

创建项目

使用 Spring Initializr

在线地址https://start.spring.io/

步骤

  1. 选择项目类型Maven/Gradle
  2. 选择语言Java/Kotlin/Groovy
  3. 填写项目信息GroupArtifactName
  4. 选择 Spring Boot 版本
  5. 添加依赖WebJPAMySQL 等
  6. 生成并下载项目

手动创建

1
2
3
4
5
6
# 使用 Maven 命令创建
mvn archetype:generate \
-DgroupId=com.example \
-DartifactId=myproject \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=false

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
myproject/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/myproject/
│ │ │ ├── MyProjectApplication.java # 启动类
│ │ │ ├── controller/ # 控制器
│ │ │ ├── service/ # 业务层
│ │ │ ├── mapper/ # 数据访问层
│ │ │ ├── entity/ # 实体类
│ │ │ └── config/ # 配置类
│ │ └── resources/
│ │ ├── application.yml # 配置文件
│ │ ├── static/ # 静态资源
│ │ └── templates/ # 模板文件
│ └── test/
└── pom.xml

启动类

1
2
3
4
5
6
7
8
9
10
11
12
package com.example.myproject;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyProjectApplication {

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

第一个 Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.myproject.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

@GetMapping("/hello")
public String hello() {
return "Hello, Spring Boot!";
}
}

运行项目

1
2
3
4
5
6
7
8
9
# 使用 Maven
mvn spring-boot:run

# 打包后运行
mvn clean package
java -jar target/myproject-0.0.1-SNAPSHOT.jar

# 访问
curl http://localhost:8080/hello

配置文件

application.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 服务器配置
server.port=8080
server.servlet.context-path=/api

# 数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# JPA 配置
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

# 日志配置
logging.level.root=INFO
logging.level.com.example=DEBUG
logging.file.name=logs/app.log

application.yml推荐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
server:
port: 8080
servlet:
context-path: /api

spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver

jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL5InnoDBDialect

jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8

logging:
level:
root: INFO
com.example: DEBUG
file:
name: logs/app.log

多环境配置

创建不同环境的配置文件

1
2
3
4
5
src/main/resources/
├── application.yml # 公共配置
├── application-dev.yml # 开发环境
├── application-test.yml # 测试环境
└── application-prod.yml # 生产环境

application-dev.yml

1
2
3
4
5
6
7
8
9
spring:
datasource:
url: jdbc:mysql://localhost:3306/dev_db
username: dev_user
password: dev_pass

logging:
level:
com.example: DEBUG

application-prod.yml

1
2
3
4
5
6
7
8
9
spring:
datasource:
url: jdbc:mysql://prod-server:3306/prod_db
username: prod_user
password: ${DB_PASSWORD} # 从环境变量读取

logging:
level:
com.example: WARN

激活指定环境

1
2
3
4
5
6
7
8
9
10
# 方式一在 application.yml 中指定
spring:
profiles:
active: dev

# 方式二命令行参数
java -jar app.jar --spring.profiles.active=prod

# 方式三环境变量
export SPRING_PROFILES_ACTIVE=test

自定义配置

定义配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {

private String name;
private String version;
private List<String> admins;
private Map<String, String> settings;
}

配置文件

1
2
3
4
5
6
7
8
9
app:
name: My Application
version: 1.0.0
admins:
- admin1@example.com
- admin2@example.com
settings:
theme: dark
language: zh-CN

使用配置

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class ConfigController {

@Autowired
private AppProperties appProperties;

@GetMapping("/config")
public AppProperties getConfig() {
return appProperties;
}
}

Web 开发

RESTful API

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@RestController
@RequestMapping("/api/users")
public class UserController {

@Autowired
private UserService userService;

// 查询列表
@GetMapping
public ResponseEntity<List<User>> list() {
List<User> users = userService.findAll();
return ResponseEntity.ok(users);
}

// 查询详情
@GetMapping("/{id}")
public ResponseEntity<User> detail(@PathVariable Integer id) {
User user = userService.findById(id);
return ResponseEntity.ok(user);
}

// 新增
@PostMapping
public ResponseEntity<User> create(@RequestBody User user) {
userService.save(user);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}

// 更新
@PutMapping("/{id}")
public ResponseEntity<Void> update(@PathVariable Integer id, @RequestBody User user) {
user.setId(id);
userService.update(user);
return ResponseEntity.noContent().build();
}

// 删除
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Integer id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
}

统一返回结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
private Integer code;
private String message;
private T data;

public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}

public static <T> Result<T> error(Integer code, String message) {
return new Result<>(code, message, null);
}
}

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
return Result.error(500, e.getMessage());
}
}

参数验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Data
public class UserDTO {

@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")
private String username;

@Email(message = "邮箱格式不正确")
private String email;

@Min(value = 18, message = "年龄必须大于等于18")
private Integer age;
}

@PostMapping("/users")
public Result<User> create(@Valid @RequestBody UserDTO userDTO) {
User user = userService.save(userDTO);
return Result.success(user);
}

数据访问

JPA

添加依赖

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Entity
@Table(name = "user")
@Data
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

@Column(nullable = false, length = 50)
private String username;

@Column(length = 100)
private String email;

private Integer age;

@CreationTimestamp
private LocalDateTime createTime;

@UpdateTimestamp
private LocalDateTime updateTime;
}

Repository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface UserRepository extends JpaRepository<User, Integer> {

// 方法名查询
List<User> findByUsername(String username);

List<User> findByAgeGreaterThan(Integer age);

// JPQL 查询
@Query("SELECT u FROM User u WHERE u.email LIKE %:email%")
List<User> findByEmailContaining(@Param("email") String email);

// 原生 SQL 查询
@Query(value = "SELECT * FROM user WHERE username = :username", nativeQuery = true)
User findByUsernameNative(@Param("username") String username);
}

Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
@Transactional
public class UserService {

@Autowired
private UserRepository userRepository;

public List<User> findAll() {
return userRepository.findAll();
}

public User findById(Integer id) {
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("用户不存在"));
}

public User save(User user) {
return userRepository.save(user);
}

public void delete(Integer id) {
userRepository.deleteById(id);
}
}

MyBatis

添加依赖

1
2
3
4
5
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>

Mapper 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Mapper
public interface UserMapper {

@Select("SELECT * FROM user WHERE id = #{id}")
User findById(Integer id);

@Insert("INSERT INTO user(username, email, age) VALUES(#{username}, #{email}, #{age})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);

@Update("UPDATE user SET username=#{username}, email=#{email} WHERE id=#{id}")
int update(User user);

@Delete("DELETE FROM user WHERE id = #{id}")
int delete(Integer id);
}

配置文件

1
2
3
4
5
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.entity
configuration:
map-underscore-to-camel-case: true

安全认证

Spring Security

添加依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

基本配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/home")
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
);

return http.build();
}

@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder().encode("123456"))
.roles("USER")
.build();

UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder().encode("admin123"))
.roles("ADMIN")
.build();

return new InMemoryUserDetailsManager(user, admin);
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

JWT 认证

添加依赖

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>

JWT 工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Component
public class JwtUtil {

private static final String SECRET_KEY = "your-secret-key";
private static final long EXPIRATION_TIME = 7 * 24 * 60 * 60 * 1000; // 7天

public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}

public String extractUsername(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody()
.getSubject();
}

public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
}

测试

单元测试

添加依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

Service 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@SpringBootTest
class UserServiceTest {

@Autowired
private UserService userService;

@MockBean
private UserRepository userRepository;

@Test
void testFindById() {
User mockUser = new User();
mockUser.setId(1);
mockUser.setUsername("张三");

when(userRepository.findById(1)).thenReturn(Optional.of(mockUser));

User user = userService.findById(1);

assertNotNull(user);
assertEquals("张三", user.getUsername());
}

@Test
void testSave() {
User user = new User();
user.setUsername("李四");
user.setEmail("lisi@example.com");

when(userRepository.save(any(User.class))).thenReturn(user);

User savedUser = userService.save(user);

assertNotNull(savedUser);
assertEquals("李四", savedUser.getUsername());
}
}

Controller 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@WebMvcTest(UserController.class)
class UserControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private UserService userService;

@Test
void testList() throws Exception {
List<User> users = Arrays.asList(
new User(1, "张三", "zhangsan@example.com"),
new User(2, "李四", "lisi@example.com")
);

when(userService.findAll()).thenReturn(users);

mockMvc.perform(get("/api/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(2))
.andExpect(jsonPath("$[0].username").value("张三"));
}

@Test
void testCreate() throws Exception {
User user = new User();
user.setUsername("王五");
user.setEmail("wangwu@example.com");

when(userService.save(any(User.class))).thenReturn(user);

mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.username").value("王五"));
}
}

部署

打包

1
2
3
4
5
# 打包为 JAR
mvn clean package

# 跳过测试
mvn clean package -DskipTests

运行

1
2
3
4
5
6
7
8
# 直接运行
java -jar target/myproject-0.0.1-SNAPSHOT.jar

# 指定端口
java -jar app.jar --server.port=9090

# 指定环境
java -jar app.jar --spring.profiles.active=prod

Docker 部署

Dockerfile

1
2
3
4
5
6
7
8
9
FROM openjdk:11-jre-slim

WORKDIR /app

COPY target/myproject-0.0.1-SNAPSHOT.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

构建和运行

1
2
3
4
5
# 构建镜像
docker build -t myproject:latest .

# 运行容器
docker run -d -p 8080:8080 myproject:latest

最佳实践

项目结构规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
com.example.project/
├── common/ # 通用模块
│ ├── constant/ # 常量定义
│ ├── exception/ # 异常定义
│ ├── result/ # 统一返回结果
│ └── utils/ # 工具类
├── config/ # 配置类
├── controller/ # 控制层
├── service/ # 业务层
│ ├── impl/ # 业务实现
│ └── dto/ # 数据传输对象
├── mapper/ # 数据访问层
├── entity/ # 实体类
└── ProjectApplication.java # 启动类

日志规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
@Service
public class UserService {

public User findById(Integer id) {
log.info("查询用户ID: {}", id);
try {
User user = userMapper.findById(id);
log.debug("查询结果: {}", user);
return user;
} catch (Exception e) {
log.error("查询用户失败ID: {}", id, e);
throw new BusinessException("查询用户失败");
}
}
}

异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Data
@AllArgsConstructor
public class BusinessException extends RuntimeException {
private Integer code;
private String message;
}

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
return Result.error(e.getCode(), e.getMessage());
}

@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常", e);
return Result.error(500, "系统异常请联系管理员");
}
}

常见问题

端口冲突

1
2
3
4
5
6
问题Port 8080 was already in use

解决方案
1. 修改端口server.port=8081
2. 关闭占用端口的进程
3. 使用随机端口server.port=0

数据库连接失败

1
2
3
4
5
6
问题Cannot load driver class: com.mysql.cj.jdbc.Driver

解决方案
1. 检查 MySQL 驱动依赖是否正确
2. 确认数据库 URL用户名密码正确
3. 检查数据库服务是否启动

报错处理

💗💗 Spring Boot 报错BeanCreationException

1
2
3
4
5
6
7
8
9
10
11
12
错误信息
Error creating bean with name 'xxx': Injection of autowired dependencies failed

错误原因
1. 依赖的 Bean 不存在
2. 循环依赖
3. 构造方法参数不匹配

解决方案
1. 检查 @Component@Service 等注解
2. 使用 @Lazy 解决循环依赖
3. 确认构造方法参数正确

💗💗 Spring Boot 报错Whitelabel Error Page

1
2
3
4
5
6
7
8
9
10
11
12
错误信息
This application has no explicit mapping for /error

错误原因
1. 请求路径错误
2. Controller 未正确配置
3. 包扫描范围不对

解决方案
1. 检查 @RequestMapping 路径
2. 确认 Controller 上有 @RestController 或 @Controller
3. 确保 Controller 在启动类的子包下

学习资源

  • 视频
    • 尚硅谷 SpringBoot 教程https://www.bilibili.com/video/BV19K4y1L7MT
  • 官方文档
    • Spring Boot 官方文档https://spring.io/projects/spring-boot
    • Spring Boot GitHubhttps://github.com/spring-projects/spring-boot
  • 书籍
    • Spring Boot 实战Craig Walls 著
    • Spring Boot 编程思想小马哥著
  • 教程
    • Spring Boot 入门教程https://www.runoob.com/w3cnote/spring-boot-tutorial.html
    • Baeldung Spring Boot 教程https://www.baeldung.com/category/spring-boot/
  • 社区
    • Stack Overflow Spring Boot 标签https://stackoverflow.com/questions/tagged/spring-boot
    • Spring 中文社区https://spring.io/projects