Spring Boot实战指南:从零基础到企业级应用的完整开发之路

🚀 引言

Spring Boot作为Java生态系统中最受欢迎的框架之一,以其”约定优于配置”的理念和强大的自动配置能力,极大地简化了Spring应用的开发和部署。本文将带你从零开始,逐步掌握Spring Boot的核心技术,并通过实际项目案例,学会构建企业级应用。

无论你是Java初学者还是有经验的开发者,这份实战指南都将帮助你深入理解Spring Boot的精髓,掌握现代Java Web开发的最佳实践。

📋 目录

  1. Spring Boot基础概念
  2. 项目搭建与配置
  3. Web开发实战
  4. 数据访问层设计
  5. 安全认证与授权
  6. 缓存与性能优化
  7. 监控与运维
  8. 微服务架构
  9. 测试策略
  10. 部署与DevOps

🎯 Spring Boot基础概念

1. 核心特性

Spring Boot的核心优势在于其简化配置和快速开发的能力:

1
2
3
4
5
6
7
8
// 传统Spring应用需要大量XML配置
// Spring Boot只需要一个主类
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

2. 自动配置原理

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
// 自动配置示例
@Configuration
@ConditionalOnClass(DataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {

@Bean
@Primary
public DataSource dataSource(DataSourceProperties properties) {
return DataSourceBuilder.create()
.driverClassName(properties.getDriverClassName())
.url(properties.getUrl())
.username(properties.getUsername())
.password(properties.getPassword())
.build();
}
}

// 条件注解详解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {
Class<?>[] value() default {};
String[] name() default {};
}

3. Starter机制

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
<!-- pom.xml - Starter依赖管理 -->
<dependencies>
<!-- Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- JPA Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

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

<!-- Redis Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>

🏗️ 项目搭建与配置

1. 项目结构设计

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
src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── demo/
│ │ ├── DemoApplication.java
│ │ ├── config/
│ │ │ ├── DatabaseConfig.java
│ │ │ ├── SecurityConfig.java
│ │ │ └── RedisConfig.java
│ │ ├── controller/
│ │ │ ├── UserController.java
│ │ │ └── ProductController.java
│ │ ├── service/
│ │ │ ├── UserService.java
│ │ │ └── ProductService.java
│ │ ├── repository/
│ │ │ ├── UserRepository.java
│ │ │ └── ProductRepository.java
│ │ ├── entity/
│ │ │ ├── User.java
│ │ │ └── Product.java
│ │ ├── dto/
│ │ │ ├── UserDTO.java
│ │ │ └── ProductDTO.java
│ │ └── exception/
│ │ ├── GlobalExceptionHandler.java
│ │ └── BusinessException.java
│ └── resources/
│ ├── application.yml
│ ├── application-dev.yml
│ ├── application-prod.yml
│ └── static/
└── test/
└── java/
└── com/
└── example/
└── demo/
├── controller/
├── service/
└── repository/

2. 配置文件管理

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# application.yml - 主配置文件
spring:
profiles:
active: dev
application:
name: spring-boot-demo

# 数据库配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
username: ${DB_USERNAME:root}
password: ${DB_PASSWORD:password}
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000

# JPA配置
jpa:
hibernate:
ddl-auto: update
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
format_sql: true
use_sql_comments: true

# Redis配置
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1ms

# 缓存配置
cache:
type: redis
redis:
time-to-live: 600000
cache-null-values: false

# 服务器配置
server:
port: 8080
servlet:
context-path: /api
compression:
enabled: true
mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json
min-response-size: 1024

# 日志配置
logging:
level:
com.example.demo: INFO
org.springframework.security: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: logs/application.log
max-size: 10MB
max-history: 30

# 管理端点配置
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
metrics:
export:
prometheus:
enabled: true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# application-dev.yml - 开发环境配置
spring:
jpa:
show-sql: true
hibernate:
ddl-auto: create-drop

h2:
console:
enabled: true
path: /h2-console

logging:
level:
root: INFO
com.example.demo: DEBUG
org.springframework.web: DEBUG
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# application-prod.yml - 生产环境配置
spring:
jpa:
show-sql: false
hibernate:
ddl-auto: validate

datasource:
hikari:
maximum-pool-size: 50
minimum-idle: 10

logging:
level:
root: WARN
com.example.demo: INFO
file:
name: /var/log/spring-boot-demo/application.log

3. 配置类设计

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// DatabaseConfig.java - 数据库配置
@Configuration
@EnableJpaRepositories(basePackages = "com.example.demo.repository")
@EnableTransactionManagement
public class DatabaseConfig {

@Bean
@Primary
@ConfigurationProperties("spring.datasource")
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}

@Bean
@Primary
public DataSource dataSource() {
return dataSourceProperties()
.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}

@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}

// 数据库连接池监控
@Bean
@ConditionalOnProperty(name = "management.endpoints.web.exposure.include", havingValue = "metrics")
public MeterBinder hikariMetrics(DataSource dataSource) {
if (dataSource instanceof HikariDataSource) {
return new HikariCPMetrics((HikariDataSource) dataSource, "hikaricp", Collections.emptyList());
}
return registry -> {};
}
}

// RedisConfig.java - Redis配置
@Configuration
@EnableCaching
@ConditionalOnClass(RedisTemplate.class)
public class RedisConfig {

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);

// 设置序列化器
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(objectMapper);

template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);

template.afterPropertiesSet();
return template;
}

@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();

return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}

// Redis分布式锁
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://localhost:6379")
.setDatabase(0)
.setConnectionPoolSize(10)
.setConnectionMinimumIdleSize(2);

return Redisson.create(config);
}
}

🌐 Web开发实战

1. 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
// BaseController.java - 基础控制器
@RestController
public abstract class BaseController {

protected final Logger logger = LoggerFactory.getLogger(getClass());

// 统一响应格式
protected <T> ResponseEntity<ApiResponse<T>> success(T data) {
return ResponseEntity.ok(ApiResponse.success(data));
}

protected <T> ResponseEntity<ApiResponse<T>> success(T data, String message) {
return ResponseEntity.ok(ApiResponse.success(data, message));
}

protected ResponseEntity<ApiResponse<Void>> error(String message) {
return ResponseEntity.badRequest().body(ApiResponse.error(message));
}

protected ResponseEntity<ApiResponse<Void>> error(int code, String message) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error(code, message));
}
}

// ApiResponse.java - 统一响应格式
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
private int code;
private String message;
private T data;
private long timestamp;

public static <T> ApiResponse<T> success(T data) {
return ApiResponse.<T>builder()
.code(200)
.message("Success")
.data(data)
.timestamp(System.currentTimeMillis())
.build();
}

public static <T> ApiResponse<T> success(T data, String message) {
return ApiResponse.<T>builder()
.code(200)
.message(message)
.data(data)
.timestamp(System.currentTimeMillis())
.build();
}

public static <T> ApiResponse<T> error(String message) {
return ApiResponse.<T>builder()
.code(400)
.message(message)
.timestamp(System.currentTimeMillis())
.build();
}

public static <T> ApiResponse<T> error(int code, String message) {
return ApiResponse.<T>builder()
.code(code)
.message(message)
.timestamp(System.currentTimeMillis())
.build();
}
}

// UserController.java - 用户控制器
@RestController
@RequestMapping("/users")
@Validated
@Tag(name = "用户管理", description = "用户相关API")
public class UserController extends BaseController {

private final UserService userService;

public UserController(UserService userService) {
this.userService = userService;
}

@GetMapping
@Operation(summary = "获取用户列表", description = "分页获取用户列表")
public ResponseEntity<ApiResponse<PageResult<UserDTO>>> getUsers(
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "10") @Min(1) @Max(100) int size,
@RequestParam(required = false) String keyword) {

PageRequest pageRequest = PageRequest.of(page, size, Sort.by("createdAt").descending());
PageResult<UserDTO> result = userService.findUsers(pageRequest, keyword);
return success(result);
}

@GetMapping("/{id}")
@Operation(summary = "获取用户详情", description = "根据ID获取用户详情")
public ResponseEntity<ApiResponse<UserDTO>> getUser(
@PathVariable @NotNull Long id) {

UserDTO user = userService.findById(id);
return success(user);
}

@PostMapping
@Operation(summary = "创建用户", description = "创建新用户")
public ResponseEntity<ApiResponse<UserDTO>> createUser(
@RequestBody @Valid CreateUserRequest request) {

UserDTO user = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(user, "用户创建成功"));
}

@PutMapping("/{id}")
@Operation(summary = "更新用户", description = "更新用户信息")
public ResponseEntity<ApiResponse<UserDTO>> updateUser(
@PathVariable @NotNull Long id,
@RequestBody @Valid UpdateUserRequest request) {

UserDTO user = userService.updateUser(id, request);
return success(user, "用户更新成功");
}

@DeleteMapping("/{id}")
@Operation(summary = "删除用户", description = "软删除用户")
public ResponseEntity<ApiResponse<Void>> deleteUser(
@PathVariable @NotNull Long id) {

userService.deleteUser(id);
return success(null, "用户删除成功");
}

@PostMapping("/{id}/avatar")
@Operation(summary = "上传头像", description = "上传用户头像")
public ResponseEntity<ApiResponse<String>> uploadAvatar(
@PathVariable @NotNull Long id,
@RequestParam("file") MultipartFile file) {

if (file.isEmpty()) {
return error("文件不能为空");
}

String avatarUrl = userService.uploadAvatar(id, file);
return success(avatarUrl, "头像上传成功");
}

@GetMapping("/{id}/posts")
@Operation(summary = "获取用户文章", description = "获取用户发布的文章列表")
public ResponseEntity<ApiResponse<PageResult<PostDTO>>> getUserPosts(
@PathVariable @NotNull Long id,
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "10") @Min(1) @Max(50) int size) {

PageRequest pageRequest = PageRequest.of(page, size, Sort.by("createdAt").descending());
PageResult<PostDTO> result = userService.findUserPosts(id, pageRequest);
return success(result);
}
}

2. 数据传输对象设计

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// UserDTO.java - 用户数据传输对象
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UserDTO {
private Long id;

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

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

@Size(max = 100, message = "昵称长度不能超过100个字符")
private String nickname;

private String avatar;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updatedAt;

private Boolean active;

private Set<String> roles;

// 敏感信息不返回
@JsonIgnore
private String password;
}

// CreateUserRequest.java - 创建用户请求
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateUserRequest {

@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
private String username;

@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间")
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d@$!%*?&]{6,}$",
message = "密码必须包含至少一个大写字母、一个小写字母和一个数字")
private String password;

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

@Size(max = 100, message = "昵称长度不能超过100个字符")
private String nickname;

@Size(max = 11, message = "手机号长度不正确")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
}

// UpdateUserRequest.java - 更新用户请求
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UpdateUserRequest {

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

@Size(max = 100, message = "昵称长度不能超过100个字符")
private String nickname;

@Size(max = 11, message = "手机号长度不正确")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;

private Boolean active;
}

// PageResult.java - 分页结果
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PageResult<T> {
private List<T> content;
private int page;
private int size;
private long totalElements;
private int totalPages;
private boolean first;
private boolean last;
private boolean hasNext;
private boolean hasPrevious;

public static <T> PageResult<T> of(Page<T> page) {
return PageResult.<T>builder()
.content(page.getContent())
.page(page.getNumber())
.size(page.getSize())
.totalElements(page.getTotalElements())
.totalPages(page.getTotalPages())
.first(page.isFirst())
.last(page.isLast())
.hasNext(page.hasNext())
.hasPrevious(page.hasPrevious())
.build();
}
}

3. 异常处理机制

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
// GlobalExceptionHandler.java - 全局异常处理器
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

// 处理参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationExceptions(
MethodArgumentNotValidException ex) {

Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});

log.warn("参数校验失败: {}", errors);
return ResponseEntity.badRequest()
.body(ApiResponse.<Map<String, String>>builder()
.code(400)
.message("参数校验失败")
.data(errors)
.timestamp(System.currentTimeMillis())
.build());
}

// 处理约束违反异常
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ApiResponse<Map<String, String>>> handleConstraintViolationException(
ConstraintViolationException ex) {

Map<String, String> errors = new HashMap<>();
ex.getConstraintViolations().forEach(violation -> {
String fieldName = violation.getPropertyPath().toString();
String errorMessage = violation.getMessage();
errors.put(fieldName, errorMessage);
});

log.warn("约束违反: {}", errors);
return ResponseEntity.badRequest()
.body(ApiResponse.<Map<String, String>>builder()
.code(400)
.message("参数约束违反")
.data(errors)
.timestamp(System.currentTimeMillis())
.build());
}

// 处理业务异常
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException ex) {
log.warn("业务异常: {}", ex.getMessage());
return ResponseEntity.status(ex.getHttpStatus())
.body(ApiResponse.<Void>builder()
.code(ex.getCode())
.message(ex.getMessage())
.timestamp(System.currentTimeMillis())
.build());
}

// 处理资源未找到异常
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ApiResponse<Void>> handleEntityNotFoundException(EntityNotFoundException ex) {
log.warn("资源未找到: {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.<Void>builder()
.code(404)
.message(ex.getMessage())
.timestamp(System.currentTimeMillis())
.build());
}

// 处理数据访问异常
@ExceptionHandler(DataAccessException.class)
public ResponseEntity<ApiResponse<Void>> handleDataAccessException(DataAccessException ex) {
log.error("数据访问异常", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.<Void>builder()
.code(500)
.message("数据访问异常,请稍后重试")
.timestamp(System.currentTimeMillis())
.build());
}

// 处理文件上传异常
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<ApiResponse<Void>> handleMaxUploadSizeExceededException(
MaxUploadSizeExceededException ex) {
log.warn("文件上传大小超限: {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
.body(ApiResponse.<Void>builder()
.code(413)
.message("文件大小超出限制")
.timestamp(System.currentTimeMillis())
.build());
}

// 处理HTTP方法不支持异常
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<ApiResponse<Void>> handleHttpRequestMethodNotSupportedException(
HttpRequestMethodNotSupportedException ex) {
log.warn("HTTP方法不支持: {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(ApiResponse.<Void>builder()
.code(405)
.message("HTTP方法不支持")
.timestamp(System.currentTimeMillis())
.build());
}

// 处理通用异常
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleGenericException(Exception ex) {
log.error("未处理的异常", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.<Void>builder()
.code(500)
.message("系统内部错误,请稍后重试")
.timestamp(System.currentTimeMillis())
.build());
}
}

// BusinessException.java - 业务异常
@Data
@EqualsAndHashCode(callSuper = true)
public class BusinessException extends RuntimeException {
private final int code;
private final HttpStatus httpStatus;

public BusinessException(String message) {
super(message);
this.code = 400;
this.httpStatus = HttpStatus.BAD_REQUEST;
}

public BusinessException(int code, String message) {
super(message);
this.code = code;
this.httpStatus = HttpStatus.BAD_REQUEST;
}

public BusinessException(int code, String message, HttpStatus httpStatus) {
super(message);
this.code = code;
this.httpStatus = httpStatus;
}

public static BusinessException notFound(String message) {
return new BusinessException(404, message, HttpStatus.NOT_FOUND);
}

public static BusinessException forbidden(String message) {
return new BusinessException(403, message, HttpStatus.FORBIDDEN);
}

public static BusinessException unauthorized(String message) {
return new BusinessException(401, message, HttpStatus.UNAUTHORIZED);
}
}

💾 数据访问层设计

1. 实体类设计

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
// BaseEntity.java - 基础实体类
@MappedSuperclass
@Data
@NoArgsConstructor
@AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

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

@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;

@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;

@CreatedBy
@Column(name = "created_by", updatable = false)
private String createdBy;

@LastModifiedBy
@Column(name = "updated_by")
private String updatedBy;

@Version
private Long version;

@Column(name = "deleted", nullable = false)
private Boolean deleted = false;
}

// User.java - 用户实体
@Entity
@Table(name = "users", indexes = {
@Index(name = "idx_username", columnList = "username", unique = true),
@Index(name = "idx_email", columnList = "email", unique = true),
@Index(name = "idx_created_at", columnList = "created_at")
})
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Where(clause = "deleted = false")
@SQLDelete(sql = "UPDATE users SET deleted = true WHERE id = ?")
public class User extends BaseEntity {

@Column(name = "username", nullable = false, unique = true, length = 50)
private String username;

@Column(name = "password", nullable = false)
private String password;

@Column(name = "email", nullable = false, unique = true, length = 100)
private String email;

@Column(name = "nickname", length = 100)
private String nickname;

@Column(name = "avatar")
private String avatar;

@Column(name = "phone", length = 11)
private String phone;

@Column(name = "active", nullable = false)
private Boolean active = true;

@Column(name = "last_login_at")
private LocalDateTime lastLoginAt;

@Column(name = "login_count", nullable = false)
private Integer loginCount = 0;

// 用户角色关联
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();

// 用户文章关联
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Post> posts = new ArrayList<>();

// 业务方法
public void addRole(Role role) {
this.roles.add(role);
role.getUsers().add(this);
}

public void removeRole(Role role) {
this.roles.remove(role);
role.getUsers().remove(this);
}

public boolean hasRole(String roleName) {
return roles.stream()
.anyMatch(role -> role.getName().equals(roleName));
}

public void updateLoginInfo() {
this.lastLoginAt = LocalDateTime.now();
this.loginCount++;
}
}

// Role.java - 角色实体
@Entity
@Table(name = "roles")
@Data
@EqualsAndHashCode(callSuper = true, exclude = {"users", "permissions"})
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Role extends BaseEntity {

@Column(name = "name", nullable = false, unique = true, length = 50)
private String name;

@Column(name = "description")
private String description;

@ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY)
private Set<User> users = new HashSet<>();

@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "role_permissions",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id")
)
private Set<Permission> permissions = new HashSet<>();
}

// Post.java - 文章实体
@Entity
@Table(name = "posts", indexes = {
@Index(name = "idx_title", columnList = "title"),
@Index(name = "idx_status", columnList = "status"),
@Index(name = "idx_created_at", columnList = "created_at")
})
@Data
@EqualsAndHashCode(callSuper = true, exclude = {"author", "tags", "comments"})
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Where(clause = "deleted = false")
@SQLDelete(sql = "UPDATE posts SET deleted = true WHERE id = ?")
public class Post extends BaseEntity {

@Column(name = "title", nullable = false, length = 200)
private String title;

@Column(name = "content", columnDefinition = "TEXT")
private String content;

@Column(name = "summary", length = 500)
private String summary;

@Column(name = "cover_image")
private String coverImage;

@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
private PostStatus status = PostStatus.DRAFT;

@Column(name = "view_count", nullable = false)
private Integer viewCount = 0;

@Column(name = "like_count", nullable = false)
private Integer likeCount = 0;

@Column(name = "comment_count", nullable = false)
private Integer commentCount = 0;

@Column(name = "published_at")
private LocalDateTime publishedAt;

// 作者关联
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id", nullable = false)
private User author;

// 标签关联
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "post_tags",
joinColumns = @JoinColumn(name = "post_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private Set<Tag> tags = new HashSet<>();

// 评论关联
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Comment> comments = new ArrayList<>();

public enum PostStatus {
DRAFT, PUBLISHED, ARCHIVED
}

// 业务方法
public void publish() {
this.status = PostStatus.PUBLISHED;
this.publishedAt = LocalDateTime.now();
}

public void incrementViewCount() {
this.viewCount++;
}

public void incrementLikeCount() {
this.likeCount++;
}

public void incrementCommentCount() {
this.commentCount++;
}
}

2. Repository层设计

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
// UserRepository.java - 用户仓库接口
@Repository
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {

// 基础查询方法
Optional<User> findByUsername(String username);

Optional<User> findByEmail(String email);

boolean existsByUsername(String username);

boolean existsByEmail(String email);

List<User> findByActiveTrue();

// 自定义查询方法
@Query("SELECT u FROM User u WHERE u.username LIKE %:keyword% OR u.email LIKE %:keyword% OR u.nickname LIKE %:keyword%")
Page<User> findByKeyword(@Param("keyword") String keyword, Pageable pageable);

@Query("SELECT u FROM User u JOIN u.roles r WHERE r.name = :roleName")
List<User> findByRoleName(@Param("roleName") String roleName);

@Query("SELECT u FROM User u WHERE u.createdAt BETWEEN :startDate AND :endDate")
List<User> findByCreatedAtBetween(@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate);

// 统计查询
@Query("SELECT COUNT(u) FROM User u WHERE u.active = true")
long countActiveUsers();

@Query("SELECT COUNT(u) FROM User u WHERE u.createdAt >= :date")
long countNewUsersAfter(@Param("date") LocalDateTime date);

// 更新操作
@Modifying
@Query("UPDATE User u SET u.active = :active WHERE u.id = :id")
int updateUserStatus(@Param("id") Long id, @Param("active") Boolean active);

@Modifying
@Query("UPDATE User u SET u.lastLoginAt = :loginTime, u.loginCount = u.loginCount + 1 WHERE u.id = :id")
int updateLoginInfo(@Param("id") Long id, @Param("loginTime") LocalDateTime loginTime);

// 原生SQL查询
@Query(value = "SELECT * FROM users u WHERE u.login_count > :minCount ORDER BY u.login_count DESC LIMIT :limit",
nativeQuery = true)
List<User> findTopActiveUsers(@Param("minCount") Integer minCount, @Param("limit") Integer limit);
}

// PostRepository.java - 文章仓库接口
@Repository
public interface PostRepository extends JpaRepository<Post, Long>, JpaSpecificationExecutor<Post> {

// 基础查询
List<Post> findByStatus(Post.PostStatus status);

Page<Post> findByStatusOrderByCreatedAtDesc(Post.PostStatus status, Pageable pageable);

List<Post> findByAuthorId(Long authorId);

Page<Post> findByAuthorIdOrderByCreatedAtDesc(Long authorId, Pageable pageable);

// 复杂查询
@Query("SELECT p FROM Post p WHERE p.title LIKE %:keyword% OR p.content LIKE %:keyword% OR p.summary LIKE %:keyword%")
Page<Post> findByKeyword(@Param("keyword") String keyword, Pageable pageable);

@Query("SELECT p FROM Post p JOIN p.tags t WHERE t.name IN :tagNames GROUP BY p HAVING COUNT(t) = :tagCount")
List<Post> findByAllTags(@Param("tagNames") List<String> tagNames, @Param("tagCount") long tagCount);

@Query("SELECT p FROM Post p WHERE p.status = 'PUBLISHED' AND p.publishedAt BETWEEN :startDate AND :endDate")
List<Post> findPublishedBetween(@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate);

// 统计查询
@Query("SELECT COUNT(p) FROM Post p WHERE p.status = :status")
long countByStatus(@Param("status") Post.PostStatus status);

@Query("SELECT p.author.id, COUNT(p) FROM Post p WHERE p.status = 'PUBLISHED' GROUP BY p.author.id ORDER BY COUNT(p) DESC")
List<Object[]> findTopAuthors(Pageable pageable);

// 热门文章
@Query("SELECT p FROM Post p WHERE p.status = 'PUBLISHED' ORDER BY p.viewCount DESC, p.likeCount DESC")
List<Post> findHotPosts(Pageable pageable);

// 相关文章推荐
@Query("SELECT p FROM Post p JOIN p.tags t WHERE t IN (SELECT t2 FROM Post p2 JOIN p2.tags t2 WHERE p2.id = :postId) AND p.id != :postId AND p.status = 'PUBLISHED' GROUP BY p ORDER BY COUNT(t) DESC")
List<Post> findRelatedPosts(@Param("postId") Long postId, Pageable pageable);
}

// 自定义Repository实现
@Repository
public class UserRepositoryCustomImpl implements UserRepositoryCustom {

@PersistenceContext
private EntityManager entityManager;

@Override
public Page<User> findUsersWithDynamicQuery(UserSearchCriteria criteria, Pageable pageable) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> root = query.from(User.class);

List<Predicate> predicates = new ArrayList<>();

// 动态构建查询条件
if (StringUtils.hasText(criteria.getKeyword())) {
String keyword = "%" + criteria.getKeyword() + "%";
Predicate usernamePredicate = cb.like(root.get("username"), keyword);
Predicate emailPredicate = cb.like(root.get("email"), keyword);
Predicate nicknamePredicate = cb.like(root.get("nickname"), keyword);
predicates.add(cb.or(usernamePredicate, emailPredicate, nicknamePredicate));
}

if (criteria.getActive() != null) {
predicates.add(cb.equal(root.get("active"), criteria.getActive()));
}

if (criteria.getStartDate() != null && criteria.getEndDate() != null) {
predicates.add(cb.between(root.get("createdAt"), criteria.getStartDate(), criteria.getEndDate()));
}

if (criteria.getRoles() != null && !criteria.getRoles().isEmpty()) {
Join<User, Role> roleJoin = root.join("roles");
predicates.add(roleJoin.get("name").in(criteria.getRoles()));
}

query.where(predicates.toArray(new Predicate[0]));

// 排序
if (pageable.getSort().isSorted()) {
List<Order> orders = new ArrayList<>();
pageable.getSort().forEach(order -> {
if (order.isAscending()) {
orders.add(cb.asc(root.get(order.getProperty())));
} else {
orders.add(cb.desc(root.get(order.getProperty())));
}
});
query.orderBy(orders);
}

// 执行查询
TypedQuery<User> typedQuery = entityManager.createQuery(query);
typedQuery.setFirstResult((int) pageable.getOffset());
typedQuery.setMaxResults(pageable.getPageSize());

List<User> users = typedQuery.getResultList();

// 计算总数
CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
Root<User> countRoot = countQuery.from(User.class);
countQuery.select(cb.count(countRoot));
countQuery.where(predicates.toArray(new Predicate[0]));
Long total = entityManager.createQuery(countQuery).getSingleResult();

return new PageImpl<>(users, pageable, total);
}
}

🔐 安全认证与授权

1. Spring Security配置

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
// SecurityConfig.java - 安全配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@Slf4j
public class SecurityConfig {

private final UserDetailsService userDetailsService;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
private final JwtTokenProvider jwtTokenProvider;

public SecurityConfig(UserDetailsService userDetailsService,
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
JwtAccessDeniedHandler jwtAccessDeniedHandler,
JwtTokenProvider jwtTokenProvider) {
this.userDetailsService = userDetailsService;
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
this.jwtTokenProvider = jwtTokenProvider;
}

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

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRF
.csrf(csrf -> csrf.disable())

// 配置会话管理
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

// 配置异常处理
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
)

// 配置请求授权
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/posts/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)

// 添加JWT过滤器
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

// JwtTokenProvider.java - JWT令牌提供者
@Component
@Slf4j
public class JwtTokenProvider {

@Value("${app.jwt.secret}")
private String jwtSecret;

@Value("${app.jwt.expiration:86400}")
private int jwtExpirationInSeconds;

@Value("${app.jwt.refresh-expiration:604800}")
private int refreshExpirationInSeconds;

private Key key;

@PostConstruct
public void init() {
byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
this.key = Keys.hmacShaKeyFor(keyBytes);
}

public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date expiryDate = new Date(System.currentTimeMillis() + jwtExpirationInSeconds * 1000L);

return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}

public String generateRefreshToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date expiryDate = new Date(System.currentTimeMillis() + refreshExpirationInSeconds * 1000L);

return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.claim("type", "refresh")
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}

public Long getUserIdFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();

return Long.parseLong(claims.getSubject());
}

public boolean validateToken(String authToken) {
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(authToken);
return true;
} catch (SecurityException ex) {
log.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
log.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
log.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
log.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
log.error("JWT claims string is empty");
}
return false;
}
}

// JwtAuthenticationFilter.java - JWT认证过滤器
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenProvider tokenProvider;
private final UserDetailsService userDetailsService;

public JwtAuthenticationFilter(JwtTokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
this.userDetailsService = ApplicationContextProvider.getBean(UserDetailsService.class);
}

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

String jwt = getJwtFromRequest(request);

if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromToken(jwt);

UserDetails userDetails = userDetailsService.loadUserByUsername(userId.toString());
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(request, response);
}

private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}

2. 用户认证服务

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
// AuthController.java - 认证控制器
@RestController
@RequestMapping("/auth")
@Validated
@Tag(name = "认证管理", description = "用户认证相关API")
public class AuthController extends BaseController {

private final AuthService authService;

public AuthController(AuthService authService) {
this.authService = authService;
}

@PostMapping("/login")
@Operation(summary = "用户登录", description = "用户名密码登录")
public ResponseEntity<ApiResponse<AuthResponse>> login(
@RequestBody @Valid LoginRequest request) {

AuthResponse response = authService.login(request);
return success(response, "登录成功");
}

@PostMapping("/register")
@Operation(summary = "用户注册", description = "注册新用户")
public ResponseEntity<ApiResponse<UserDTO>> register(
@RequestBody @Valid RegisterRequest request) {

UserDTO user = authService.register(request);
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(user, "注册成功"));
}

@PostMapping("/refresh")
@Operation(summary = "刷新令牌", description = "使用刷新令牌获取新的访问令牌")
public ResponseEntity<ApiResponse<AuthResponse>> refresh(
@RequestBody @Valid RefreshTokenRequest request) {

AuthResponse response = authService.refreshToken(request.getRefreshToken());
return success(response, "令牌刷新成功");
}

@PostMapping("/logout")
@Operation(summary = "用户登出", description = "用户登出")
public ResponseEntity<ApiResponse<Void>> logout(HttpServletRequest request) {
String token = getJwtFromRequest(request);
if (token != null) {
authService.logout(token);
}
return success(null, "登出成功");
}

@GetMapping("/me")
@Operation(summary = "获取当前用户信息", description = "获取当前登录用户的详细信息")
public ResponseEntity<ApiResponse<UserDTO>> getCurrentUser(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
UserDTO user = authService.getCurrentUser(userPrincipal.getId());
return success(user);
}

private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}

// AuthService.java - 认证服务
@Service
@Transactional
@Slf4j
public class AuthService {

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider tokenProvider;
private final RedisTemplate<String, Object> redisTemplate;
private final UserMapper userMapper;

public AuthService(UserRepository userRepository,
PasswordEncoder passwordEncoder,
AuthenticationManager authenticationManager,
JwtTokenProvider tokenProvider,
RedisTemplate<String, Object> redisTemplate,
UserMapper userMapper) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.authenticationManager = authenticationManager;
this.tokenProvider = tokenProvider;
this.redisTemplate = redisTemplate;
this.userMapper = userMapper;
}

public AuthResponse login(LoginRequest request) {
// 认证用户
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);

// 生成令牌
String accessToken = tokenProvider.generateToken(authentication);
String refreshToken = tokenProvider.generateRefreshToken(authentication);

// 更新登录信息
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
User user = userRepository.findById(userPrincipal.getId())
.orElseThrow(() -> BusinessException.notFound("用户不存在"));

user.updateLoginInfo();
userRepository.save(user);

// 缓存刷新令牌
String refreshTokenKey = "refresh_token:" + userPrincipal.getId();
redisTemplate.opsForValue().set(refreshTokenKey, refreshToken, Duration.ofDays(7));

log.info("用户 {} 登录成功", request.getUsername());

return AuthResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.tokenType("Bearer")
.expiresIn(86400)
.user(userMapper.toDTO(user))
.build();
}

public UserDTO register(RegisterRequest request) {
// 检查用户名和邮箱是否已存在
if (userRepository.existsByUsername(request.getUsername())) {
throw new BusinessException("用户名已存在");
}

if (userRepository.existsByEmail(request.getEmail())) {
throw new BusinessException("邮箱已存在");
}

// 创建用户
User user = User.builder()
.username(request.getUsername())
.password(passwordEncoder.encode(request.getPassword()))
.email(request.getEmail())
.nickname(request.getNickname())
.active(true)
.build();

User savedUser = userRepository.save(user);
log.info("用户注册成功: {}", savedUser.getUsername());

return userMapper.toDTO(savedUser);
}

public AuthResponse refreshToken(String refreshToken) {
if (!tokenProvider.validateToken(refreshToken)) {
throw BusinessException.unauthorized("刷新令牌无效");
}

Long userId = tokenProvider.getUserIdFromToken(refreshToken);

// 验证刷新令牌是否在缓存中
String refreshTokenKey = "refresh_token:" + userId;
String cachedToken = (String) redisTemplate.opsForValue().get(refreshTokenKey);

if (!refreshToken.equals(cachedToken)) {
throw BusinessException.unauthorized("刷新令牌无效");
}

// 生成新的访问令牌
User user = userRepository.findById(userId)
.orElseThrow(() -> BusinessException.notFound("用户不存在"));

UserPrincipal userPrincipal = UserPrincipal.create(user);
Authentication authentication = new UsernamePasswordAuthenticationToken(
userPrincipal, null, userPrincipal.getAuthorities());

String newAccessToken = tokenProvider.generateToken(authentication);

return AuthResponse.builder()
.accessToken(newAccessToken)
.refreshToken(refreshToken)
.tokenType("Bearer")
.expiresIn(86400)
.user(userMapper.toDTO(user))
.build();
}

public void logout(String token) {
// 将令牌加入黑名单
String blacklistKey = "blacklist_token:" + token;
redisTemplate.opsForValue().set(blacklistKey, "true", Duration.ofDays(1));

// 清除刷新令牌
Long userId = tokenProvider.getUserIdFromToken(token);
String refreshTokenKey = "refresh_token:" + userId;
redisTemplate.delete(refreshTokenKey);

log.info("用户 {} 登出成功", userId);
}

@Transactional(readOnly = true)
public UserDTO getCurrentUser(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> BusinessException.notFound("用户不存在"));
return userMapper.toDTO(user);
}
}

🚀 缓存与性能优化

1. 多级缓存策略

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// CacheConfig.java - 缓存配置
@Configuration
@EnableCaching
public class CacheConfig {

@Bean
@Primary
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 配置序列化方式
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();

// 不同缓存的配置
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
cacheConfigurations.put("users", config.entryTtl(Duration.ofHours(1)));
cacheConfigurations.put("posts", config.entryTtl(Duration.ofMinutes(30)));
cacheConfigurations.put("hot_posts", config.entryTtl(Duration.ofMinutes(10)));
cacheConfigurations.put("user_posts", config.entryTtl(Duration.ofMinutes(15)));

return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.withInitialCacheConfigurations(cacheConfigurations)
.build();
}

@Bean
public CacheManager caffeineCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(5))
.recordStats());
return cacheManager;
}

@Bean
public CacheManager compositeCacheManager() {
CompositeCacheManager cacheManager = new CompositeCacheManager();
cacheManager.setCacheManagers(Arrays.asList(
caffeineCacheManager(),
cacheManager(null) // Redis缓存管理器
));
cacheManager.setFallbackToNoOpCache(false);
return cacheManager;
}
}

// CacheService.java - 缓存服务
@Service
@Slf4j
public class CacheService {

private final RedisTemplate<String, Object> redisTemplate;
private final Cache caffeineCache;

public CacheService(RedisTemplate<String, Object> redisTemplate,
CacheManager caffeineCacheManager) {
this.redisTemplate = redisTemplate;
this.caffeineCache = caffeineCacheManager.getCache("local");
}

// 多级缓存获取
public <T> T getFromMultiLevelCache(String key, Class<T> type, Supplier<T> dataLoader) {
// 1. 先从本地缓存获取
T value = caffeineCache.get(key, type);
if (value != null) {
log.debug("从本地缓存获取数据: {}", key);
return value;
}

// 2. 从Redis缓存获取
value = (T) redisTemplate.opsForValue().get(key);
if (value != null) {
log.debug("从Redis缓存获取数据: {}", key);
// 回写到本地缓存
caffeineCache.put(key, value);
return value;
}

// 3. 从数据源获取
value = dataLoader.get();
if (value != null) {
log.debug("从数据源获取数据: {}", key);
// 写入Redis缓存
redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(30));
// 写入本地缓存
caffeineCache.put(key, value);
}

return value;
}

// 缓存预热
@EventListener(ApplicationReadyEvent.class)
public void warmUpCache() {
log.info("开始缓存预热...");

// 预热热门文章
CompletableFuture.runAsync(() -> {
try {
// 模拟预热逻辑
List<String> hotPostIds = Arrays.asList("1", "2", "3", "4", "5");
hotPostIds.forEach(id -> {
String key = "post:" + id;
// 这里应该调用实际的数据加载逻辑
redisTemplate.opsForValue().set(key, "hot_post_data_" + id, Duration.ofHours(1));
});
log.info("热门文章缓存预热完成");
} catch (Exception e) {
log.error("缓存预热失败", e);
}
});
}

// 缓存失效
public void evictCache(String pattern) {
Set<String> keys = redisTemplate.keys(pattern);
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
log.info("清除缓存: {} 个key", keys.size());
}
}
}

2. 性能监控与优化

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// PerformanceMonitor.java - 性能监控
@Component
@Slf4j
public class PerformanceMonitor {

private final MeterRegistry meterRegistry;
private final Timer.Sample sample;

public PerformanceMonitor(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.sample = Timer.start(meterRegistry);
}

@EventListener
public void handleMethodExecution(MethodExecutionEvent event) {
Timer.Sample.stop(meterRegistry.timer("method.execution.time",
"class", event.getClassName(),
"method", event.getMethodName()));
}

// 数据库连接池监控
@Bean
public MeterBinder hikariMetrics(DataSource dataSource) {
if (dataSource instanceof HikariDataSource) {
return new HikariCPMetrics((HikariDataSource) dataSource, "hikaricp", Collections.emptyList());
}
return registry -> {};
}

// JVM监控
@Bean
public MeterBinder jvmMetrics() {
return new JvmMetrics();
}

// 缓存监控
@Bean
public MeterBinder cacheMetrics(CacheManager cacheManager) {
return new CacheMetrics(cacheManager, "cache", Collections.emptyList());
}
}

// PerformanceAspect.java - 性能切面
@Aspect
@Component
@Slf4j
public class PerformanceAspect {

private final MeterRegistry meterRegistry;

public PerformanceAspect(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}

@Around("@annotation(Timed)")
public Object timeMethod(ProceedingJoinPoint joinPoint) throws Throwable {
Timer.Sample sample = Timer.start(meterRegistry);

try {
Object result = joinPoint.proceed();
sample.stop(Timer.builder("method.execution")
.tag("class", joinPoint.getTarget().getClass().getSimpleName())
.tag("method", joinPoint.getSignature().getName())
.tag("status", "success")
.register(meterRegistry));
return result;
} catch (Exception e) {
sample.stop(Timer.builder("method.execution")
.tag("class", joinPoint.getTarget().getClass().getSimpleName())
.tag("method", joinPoint.getSignature().getName())
.tag("status", "error")
.register(meterRegistry));
throw e;
}
}

@Around("execution(* com.example.demo.service.*.*(..))")
public Object monitorServiceMethods(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();

long startTime = System.currentTimeMillis();

try {
Object result = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - startTime;

// 记录执行时间
meterRegistry.timer("service.method.execution",
"class", className,
"method", methodName)
.record(executionTime, TimeUnit.MILLISECONDS);

// 慢查询告警
if (executionTime > 1000) {
log.warn("慢方法执行: {}.{} 耗时: {}ms", className, methodName, executionTime);
}

return result;
} catch (Exception e) {
meterRegistry.counter("service.method.error",
"class", className,
"method", methodName)
.increment();
throw e;
}
}
}

3. 异步处理与线程池优化

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// AsyncConfig.java - 异步配置
@Configuration
@EnableAsync
public class AsyncConfig {

@Bean("taskExecutor")
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("async-task-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}

@Bean("emailExecutor")
public TaskExecutor emailExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("email-task-");
executor.initialize();
return executor;
}

@Bean
public AsyncUncaughtExceptionHandler asyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
}

// AsyncService.java - 异步服务
@Service
@Slf4j
public class AsyncService {

@Async("taskExecutor")
public CompletableFuture<String> processDataAsync(String data) {
log.info("开始异步处理数据: {}", data);

try {
// 模拟耗时操作
Thread.sleep(2000);
String result = "处理结果: " + data.toUpperCase();
log.info("异步处理完成: {}", result);
return CompletableFuture.completedFuture(result);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return CompletableFuture.failedFuture(e);
}
}

@Async("emailExecutor")
public void sendEmailAsync(String to, String subject, String content) {
log.info("开始发送邮件: {} -> {}", subject, to);

try {
// 模拟邮件发送
Thread.sleep(1000);
log.info("邮件发送成功: {}", to);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("邮件发送失败", e);
}
}

// 批量异步处理
public CompletableFuture<List<String>> processBatchAsync(List<String> dataList) {
List<CompletableFuture<String>> futures = dataList.stream()
.map(this::processDataAsync)
.collect(Collectors.toList());

return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList()));
}
}

📊 监控与运维

1. 应用监控配置

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// MetricsConfig.java - 指标配置
@Configuration
public class MetricsConfig {

@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}

@Bean
public CountedAspect countedAspect(MeterRegistry registry) {
return new CountedAspect(registry);
}

// 自定义指标
@Bean
public MeterBinder customMetrics() {
return registry -> {
// 业务指标
Gauge.builder("business.active.users")
.description("当前活跃用户数")
.register(registry, this, obj -> getActiveUserCount());

Gauge.builder("business.total.posts")
.description("文章总数")
.register(registry, this, obj -> getTotalPostCount());
};
}

private double getActiveUserCount() {
// 实际业务逻辑
return 100.0;
}

private double getTotalPostCount() {
// 实际业务逻辑
return 500.0;
}
}

// HealthIndicator.java - 健康检查
@Component
public class CustomHealthIndicator implements HealthIndicator {

private final UserRepository userRepository;
private final RedisTemplate<String, Object> redisTemplate;

public CustomHealthIndicator(UserRepository userRepository,
RedisTemplate<String, Object> redisTemplate) {
this.userRepository = userRepository;
this.redisTemplate = redisTemplate;
}

@Override
public Health health() {
Health.Builder builder = new Health.Builder();

try {
// 检查数据库连接
long userCount = userRepository.count();
builder.withDetail("database.user.count", userCount);

// 检查Redis连接
redisTemplate.opsForValue().set("health.check", "ok", Duration.ofSeconds(10));
String redisStatus = (String) redisTemplate.opsForValue().get("health.check");

if ("ok".equals(redisStatus)) {
builder.withDetail("redis.status", "UP");
builder.up();
} else {
builder.withDetail("redis.status", "DOWN");
builder.down();
}

} catch (Exception e) {
builder.withDetail("error", e.getMessage());
builder.down();
}

return builder.build();
}
}

2. 日志配置与管理

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
47
48
49
50
51
52
53
54
55
<!-- logback-spring.xml - 日志配置 -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<springProfile name="dev">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>

<logger name="com.example.demo" level="DEBUG"/>
</springProfile>

<springProfile name="prod">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/error.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</springProfile>
</configuration>

🏗️ 最佳实践总结

1. 架构设计原则

  • 单一职责原则:每个类和方法只负责一个功能
  • 依赖注入:使用构造器注入,避免字段注入
  • 配置外部化:使用配置文件管理环境相关配置
  • 异常处理:统一异常处理机制,提供友好的错误信息

2. 性能优化策略

  • 数据库优化:合理使用索引,避免N+1查询
  • 缓存策略:多级缓存,缓存预热和失效机制
  • 异步处理:耗时操作异步化,提高响应速度
  • 连接池优化:合理配置数据库和Redis连接池

3. 安全最佳实践

  • 输入验证:严格的参数校验和数据验证
  • 权限控制:基于角色的访问控制(RBAC)
  • 安全传输:HTTPS和JWT令牌机制
  • 敏感信息保护:密码加密,敏感数据脱敏

4. 运维监控要点

  • 健康检查:应用和依赖服务的健康状态监控
  • 性能指标:响应时间、吞吐量、错误率监控
  • 日志管理:结构化日志,日志聚合和分析
  • 告警机制:关键指标异常时及时告警

🎯 总结

Spring Boot以其强大的自动配置能力和丰富的生态系统,为Java开发者提供了高效的开发体验。通过本文的实战指南,我们学习了从项目搭建到生产部署的完整流程,掌握了企业级应用开发的核心技术。

在实际项目中,要根据具体需求选择合适的技术栈,注重代码质量和系统性能,建立完善的监控和运维体系。Spring Boot的魅力在于其简化了复杂性,让开发者能够专注于业务逻辑的实现,这正是现代软件开发所追求的目标。

持续学习新技术,关注社区动态,在实践中不断提升技能,才能在快速发展的技术领域中保持竞争力。

/Users/codeicee/Downloads/project/myself/znb-hexo-blog/source/_posts/spring-boot实战指南.md