目标 :掌握企业项目中最常用的后端开发能力
前置要求:完成入门篇
目录
- 参数接收
- 数据校验
- 统一返回结果
- 全局异常处理
- 日志体系
- 多环境配置
- 拦截器
- 过滤器
- 文件上传与下载
- [接口文档 Swagger/OpenAPI](#接口文档 Swagger/OpenAPI)
- [跨域处理 CORS](#跨域处理 CORS)
- 接口版本管理
- 面试高频题
1. 参数接收
五种参数接收方式
java
@RestController
@RequestMapping("/api/demo")
public class ParamController {
// 1. 路径参数 @PathVariable:/users/123
@GetMapping("/users/{id}")
public String pathVar(@PathVariable Long id) {
return "id=" + id;
}
// 2. 查询参数 @RequestParam:/users?name=zhang&age=18
@GetMapping("/users")
public String requestParam(
@RequestParam String name,
@RequestParam(required = false, defaultValue = "0") Integer age) {
return name + ":" + age;
}
// 3. 请求体 @RequestBody(JSON)
@PostMapping("/users")
public String requestBody(@RequestBody @Valid UserDTO dto) {
return dto.toString();
}
// 4. 请求头 @RequestHeader
@GetMapping("/header")
public String header(@RequestHeader("Authorization") String token) {
return "token=" + token;
}
// 5. 对象参数接收(表单/查询参数 → 对象)
@GetMapping("/search")
public String modelAttr(SearchQuery query) {
// ?keyword=java&page=1&size=10 → 自动绑定到 SearchQuery 对象
return query.toString();
}
}
参数接收对比表
| 注解 | 数据来源 | Content-Type | 常用场景 |
|---|---|---|---|
@PathVariable |
URL 路径 | 任意 | GET/DELETE 资源 ID |
@RequestParam |
URL 查询串 | 任意 | 分页、搜索条件 |
@RequestBody |
请求体 | application/json | POST/PUT JSON 数据 |
@RequestHeader |
请求头 | 任意 | Token、签名 |
| 无注解(对象) | URL 查询串 | form | 查询条件对象 |
2. 数据校验
引入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
常用校验注解
| 注解 | 说明 |
|---|---|
@NotNull |
不为 null |
@NotBlank |
字符串不为空(去空格后长度 > 0) |
@NotEmpty |
集合/字符串不为空 |
@Size(min,max) |
长度/元素数量范围 |
@Min / @Max |
数值范围 |
@Email |
邮箱格式 |
@Pattern(regexp) |
正则表达式 |
@Positive |
正数 |
@Future / @Past |
时间在未来/过去 |
使用示例
java
@Data
public class CreateUserDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度为 2-20 位")
private String username;
@NotBlank(message = "密码不能为空")
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{6,20}$",
message = "密码须包含大小写字母和数字,长度 6-20 位")
private String password;
@NotBlank @Email(message = "邮箱格式不正确")
private String email;
@NotNull @Min(value = 1, message = "年龄最小为 1")
@Max(value = 150, message = "年龄最大为 150")
private Integer age;
}
// Controller 中使用 @Valid 触发校验
@PostMapping("/users")
public Result<User> create(@RequestBody @Valid CreateUserDTO dto) {
return Result.success(userService.create(dto));
}
自定义校验注解
java
// 1. 定义注解
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 2. 实现验证器
public class PhoneValidator implements ConstraintValidator<Phone, String> {
private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value == null || PHONE_PATTERN.matcher(value).matches();
}
}
// 3. 使用
@Phone
private String phone;
分组校验
java
// 创建时必填,更新时非必填
public interface CreateGroup {}
public interface UpdateGroup {}
@NotBlank(groups = CreateGroup.class, message = "创建时用户名必填")
@Size(min = 2, max = 20)
private String username;
// Controller 使用 @Validated(CreateGroup.class)
@PostMapping
public Result<User> create(@RequestBody @Validated(CreateGroup.class) UserDTO dto) {...}
3. 统一返回结果
企业项目中,所有接口的返回格式必须统一,方便前端处理。
标准 Result 结构
json
{
"code": 200,
"message": "操作成功",
"data": { ... },
"timestamp": 1714000000000
}
Result 实现(参见 common 模块)
java
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Result<T> {
private Integer code;
private String message;
private T data;
private Long timestamp;
public static <T> Result<T> success(T data) { ... }
public static <T> Result<T> fail(String message) { ... }
}
使用示例
java
// 成功
return Result.success(user); // 有数据
return Result.success("删除成功", null); // 带消息
return Result.success(); // 无数据
// 失败
return Result.fail("用户不存在");
return Result.fail(ResultCode.UNAUTHORIZED);
4. 全局异常处理
@RestControllerAdvice + @ExceptionHandler
java
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
// 1. 业务异常(主动抛出)
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusiness(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
return Result.fail(e.getCode(), e.getMessage());
}
// 2. 参数校验失败(@Valid 触发)
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Void> handleValidation(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors()
.stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining("; "));
return Result.fail(1001, msg);
}
// 3. 兜底异常
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Void> handleAll(Exception e) {
log.error("系统异常: ", e);
return Result.fail("服务器内部错误,请联系管理员");
}
}
异常处理原则
业务可预期异常 → 抛 BusinessException → 全局处理器捕获 → 返回业务错误码
系统未知异常 → 兜底处理器捕获 → 返回 500 → 打印完整堆栈
5. 日志体系
Spring Boot 默认日志框架
SLF4J(门面)+ Logback(实现)
└── spring-boot-starter-logging(自动引入)
使用
java
// 推荐:Lombok @Slf4j 注解
@Slf4j
@Service
public class UserService {
public void createUser(User user) {
log.debug("调试信息: {}", user); // 开发环境
log.info("用户创建: id={}", user.getId());
log.warn("警告: 用户已存在");
log.error("错误: ", exception); // 异常必须传对象,不要 toString
}
}
日志级别配置
yaml
logging:
level:
root: INFO # 全局级别
com.example: DEBUG # 包级别(覆盖全局)
org.hibernate.SQL: DEBUG # SQL 日志
file:
name: logs/app.log # 日志文件路径
pattern:
console: "%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
logback-spring.xml 高级配置
xml
<configuration>
<!-- 按天滚动,保留 30 天 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="FILE"/>
</root>
</configuration>
6. 多环境配置
Profile 配置文件
resources/
├── application.yml ← 公共配置
├── application-dev.yml ← 开发环境
├── application-test.yml ← 测试环境
└── application-prod.yml ← 生产环境
主配置文件激活
yaml
# application.yml
spring:
profiles:
active: dev # 激活 dev 环境
# 也可通过命令行参数激活(优先级更高)
# java -jar app.jar --spring.profiles.active=prod
环境差异配置示例
yaml
# application-dev.yml
spring:
datasource:
url: jdbc:h2:mem:devdb
jpa:
show-sql: true
logging:
level:
com.example: DEBUG
---
# application-prod.yml
spring:
datasource:
url: jdbc:mysql://prod-db:3306/mydb
jpa:
show-sql: false
logging:
level:
com.example: INFO
@Profile 条件 Bean
java
// 只在 dev 环境创建的 Bean
@Bean
@Profile("dev")
public DataInitializer devDataInitializer() {
return new DataInitializer();
}
// 生产环境排除
@Component
@Profile("!prod")
public class MockPaymentService implements PaymentService { ... }
7. 拦截器
与过滤器的区别
| 特性 | 拦截器(Interceptor) | 过滤器(Filter) |
|---|---|---|
| 规范 | Spring MVC | Servlet |
| 执行位置 | DispatcherServlet 之后 | DispatcherServlet 之前 |
| Spring 访问 | ✅ 可访问 Spring Bean | ❌ 不能(需手动获取) |
| 返回值处理 | ✅ 可修改返回结果 | ❌ |
| 常用场景 | 登录校验、权限、日志 | 字符集、CORS、请求包装 |
实现拦截器
java
@Slf4j
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res,
Object handler) throws Exception {
String token = req.getHeader("Authorization");
if (token == null || !JwtUtil.isValid(token.replace("Bearer ", ""))) {
res.setStatus(401);
res.setContentType("application/json;charset=UTF-8");
res.getWriter().write("{\"code\":401,\"message\":\"请先登录\"}");
return false; // 拦截请求
}
return true; // 放行
}
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res,
Object handler, Exception ex) {
// 清理 ThreadLocal,防止内存泄漏
UserContext.clear();
}
}
// 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/api/**")
.excludePathPatterns("/api/auth/login", "/api/auth/register");
}
}
8. 过滤器
java
@Slf4j
@Component
@Order(1) // 多个过滤器时的执行顺序
public class RequestWrapperFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpReq = (HttpServletRequest) request;
long start = System.currentTimeMillis();
log.info("Filter 开始: {} {}", httpReq.getMethod(), httpReq.getRequestURI());
// 放行(执行后续 Filter 和 Servlet)
chain.doFilter(request, response);
log.info("Filter 结束,耗时: {}ms", System.currentTimeMillis() - start);
}
}
9. 文件上传与下载
配置文件大小限制
yaml
spring:
servlet:
multipart:
max-file-size: 50MB # 单文件最大
max-request-size: 100MB # 整个请求最大
文件上传接口
java
@PostMapping("/upload")
public Result<String> upload(@RequestParam("file") MultipartFile file) throws IOException {
if (file.isEmpty()) {
return Result.fail("文件不能为空");
}
// 校验文件类型
String originalName = file.getOriginalFilename();
String ext = originalName.substring(originalName.lastIndexOf(".")).toLowerCase();
if (!List.of(".jpg", ".png", ".pdf").contains(ext)) {
return Result.fail("不支持的文件类型: " + ext);
}
// 生成唯一文件名(避免覆盖)
String newName = UUID.randomUUID() + ext;
Path savePath = Paths.get("uploads", newName);
Files.createDirectories(savePath.getParent());
file.transferTo(savePath.toFile());
return Result.success("上传成功", "/files/" + newName);
}
文件下载接口
java
@GetMapping("/download/{fileName}")
public ResponseEntity<Resource> download(@PathVariable String fileName) throws IOException {
Path filePath = Paths.get("uploads").resolve(fileName).normalize();
Resource resource = new UrlResource(filePath.toUri());
if (!resource.exists()) {
return ResponseEntity.notFound().build();
}
String contentType = Files.probeContentType(filePath);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
}
10. 接口文档 Swagger/OpenAPI
引入 SpringDoc(Spring Boot 3.x 推荐)
xml
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
全局配置
java
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.info(new Info()
.title("Spring Boot Demo API")
.description("六篇知识体系示例接口文档")
.version("v1.0.0")
.contact(new Contact().name("开发团队")))
.addSecurityItem(new SecurityRequirement().addList("Bearer Token"))
.components(new Components().addSecuritySchemes("Bearer Token",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
}
接口注解
java
@Tag(name = "用户管理", description = "用户 CRUD 接口")
@RestController
public class UserController {
@Operation(summary = "创建用户", description = "邮箱全局唯一")
@ApiResponse(responseCode = "200", description = "创建成功")
@ApiResponse(responseCode = "400", description = "参数错误")
@PostMapping("/users")
public Result<User> create(
@Parameter(description = "用户信息", required = true)
@RequestBody @Valid CreateUserDTO dto) { ... }
}
访问地址
Swagger UI: http://localhost:8080/swagger-ui/index.html
OpenAPI JSON: http://localhost:8080/v3/api-docs
11. 跨域处理 CORS
方式一:全局配置(推荐)
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOriginPatterns("http://localhost:3000", "https://yourdomain.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
方式二:单接口注解
java
@CrossOrigin(origins = "http://localhost:3000")
@GetMapping("/public-api")
public Result<String> publicApi() { ... }
12. 接口版本管理
URL 版本号(最常用)
java
@RequestMapping("/api/v1/users") // v1 版本
@RestController
public class UserV1Controller { ... }
@RequestMapping("/api/v2/users") // v2 版本(新增字段)
@RestController
public class UserV2Controller { ... }
Header 版本号
java
@GetMapping(value = "/users", headers = "API-Version=1")
public Result<UserV1VO> getUserV1() { ... }
@GetMapping(value = "/users", headers = "API-Version=2")
public Result<UserV2VO> getUserV2() { ... }
13. 面试高频题
Q1:拦截器和过滤器的区别?
过滤器是 Servlet 规范,在 DispatcherServlet 之前执行,可过滤任何请求;拦截器是 Spring MVC 特有,在 Controller 执行前后触发,可访问 Spring Bean,适合做认证授权。
Q2:全局异常处理 @RestControllerAdvice 的原理?
基于 Spring AOP 代理,通过
HandlerExceptionResolverComposite捕获 Controller 层异常,按@ExceptionHandler匹配的异常类型分发处理。
Q3:Spring Boot 如何实现多环境配置?
通过
spring.profiles.active激活对应的application-{profile}.yml文件。优先级:命令行参数 > 环境变量 > application-{profile}.yml > application.yml。
Q4:日志框架 SLF4J 和 Logback 是什么关系?
SLF4J 是门面(接口),Logback 是实现。Spring Boot 默认使用 Logback 作为 SLF4J 的实现,开发者针对 SLF4J 编码,底层可以无缝切换到 Log4j2。
Q5:文件上传时如何防止目录穿越攻击?
使用
Paths.get(uploadDir).resolve(filename).normalize()规范化路径后,检查是否以 uploadDir 开头;避免直接使用用户上传的文件名。
Q6:如何在 Spring Boot 中实现接口限流?
单机用 Guava
RateLimiter;集群用 Redis + Lua 脚本实现令牌桶;也可用 Sentinel、Gateway 限流组件。
Q7:@Valid 和 @Validated 的区别?
@Valid是 JSR-303 标准注解,不支持分组;@Validated是 Spring 扩展,支持分组校验(Groups)。在 Controller 方法参数上两者都可用,推荐统一用@Valid,分组时用@Validated。
Q8:如何处理统一返回结果中的 null 字段?
在 Result 类上添加
@JsonInclude(JsonInclude.Include.NON_NULL),序列化时自动忽略 null 字段,减少响应体大小。
14. AOP 实战:操作日志与接口耗时(专家必知)
知识点 1:为什么用 AOP 做操作日志
手动在每个 Service 方法里写日志记录代码,会导致:
- 代码冗余:每个方法都有相同的日志逻辑
- 侵入性强:业务代码和日志代码混杂
- 难以维护:修改日志格式需要改动所有地方
AOP 的解决方案:一处定义,处处生效,业务代码零感知。
知识点 2:操作日志注解实现
java
// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLog {
String module() default ""; // 模块名(如"用户管理")
String operation() default ""; // 操作描述(如"创建用户")
boolean logParams() default true; // 是否记录请求参数
boolean logResult() default false; // 是否记录返回结果(敏感接口不记录)
}
// 操作日志实体
@Data
@TableName("t_operation_log")
public class OperationLog {
private Long id;
private String username; // 操作人
private String module; // 模块
private String operation; // 操作描述
private String method; // 方法全限定名
private String requestUrl; // 请求URL
private String requestMethod; // HTTP方法
private String requestParams; // 请求参数(JSON)
private String responseResult; // 返回结果(JSON)
private Long costTime; // 耗时(ms)
private String ipAddress; // 客户端IP
private Integer status; // 0成功 1失败
private String errorMessage; // 异常信息
private LocalDateTime createTime;
}
// AOP 切面实现
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class OperationLogAspect {
private final OperationLogService operationLogService;
private final ObjectMapper objectMapper;
// 切点:标注了 @OperationLog 的方法
@Pointcut("@annotation(com.example.annotation.OperationLog)")
public void logPointcut() {}
@Around("logPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
OperationLog logEntity = new OperationLog();
logEntity.setCreateTime(LocalDateTime.now());
// 获取方法上的注解
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
com.example.annotation.OperationLog annotation =
method.getAnnotation(com.example.annotation.OperationLog.class);
logEntity.setModule(annotation.module());
logEntity.setOperation(annotation.operation());
logEntity.setMethod(method.getDeclaringClass().getName() + "." + method.getName());
// 获取请求信息
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
logEntity.setRequestUrl(request.getRequestURI());
logEntity.setRequestMethod(request.getMethod());
logEntity.setIpAddress(getClientIp(request));
// 获取当前用户
// logEntity.setUsername(SecurityUtils.getCurrentUsername());
// 记录请求参数
if (annotation.logParams()) {
logEntity.setRequestParams(objectMapper.writeValueAsString(joinPoint.getArgs()));
}
Object result = null;
try {
result = joinPoint.proceed();
logEntity.setStatus(0); // 成功
// 记录返回结果
if (annotation.logResult() && result != null) {
logEntity.setResponseResult(objectMapper.writeValueAsString(result));
}
return result;
} catch (Throwable e) {
logEntity.setStatus(1); // 失败
logEntity.setErrorMessage(e.getMessage());
throw e;
} finally {
logEntity.setCostTime(System.currentTimeMillis() - startTime);
// 异步保存(避免日志记录影响主业务性能)
operationLogService.saveAsync(logEntity);
log.info("[{}] {} 耗时: {}ms 状态: {}",
logEntity.getModule(), logEntity.getOperation(),
logEntity.getCostTime(), logEntity.getStatus() == 0 ? "成功" : "失败");
}
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty()) {
ip = request.getRemoteAddr();
}
// X-Forwarded-For 可能包含多个IP(代理链),取第一个
return ip != null && ip.contains(",") ? ip.split(",")[0].trim() : ip;
}
}
// 使用示例
@RestController
@RequiredArgsConstructor
public class UserController {
@OperationLog(module = "用户管理", operation = "创建用户")
@PostMapping("/api/users")
public Result<User> createUser(@RequestBody @Valid CreateUserDTO dto) {
return Result.success(userService.createUser(dto));
}
@OperationLog(module = "用户管理", operation = "删除用户", logResult = false)
@DeleteMapping("/api/users/{id}")
public Result<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return Result.success();
}
}
15. ThreadLocal 使用规范与内存泄漏防范
知识点 1:ThreadLocal 的正确使用场景
ThreadLocal 用于在同一线程的不同方法间传递上下文数据(如当前用户信息),无需通过方法参数层层传递。
java
// 用户上下文持有者(全局唯一)
public class UserContext {
private static final ThreadLocal<UserInfo> USER_HOLDER = new ThreadLocal<>();
public static void set(UserInfo user) {
USER_HOLDER.set(user);
}
public static UserInfo get() {
return USER_HOLDER.get();
}
// 必须在请求结束时清理!
public static void clear() {
USER_HOLDER.remove();
}
}
// 在拦截器中设置和清理(标准模式)
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) {
// 解析 Token,设置用户上下文
String token = request.getHeader("Authorization");
UserInfo user = jwtUtil.parseToken(token);
UserContext.set(user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
// 请求结束,必须清理!防止内存泄漏
UserContext.clear();
}
}
// 在 Service 中直接获取,无需传参
@Service
public class OrderService {
public Order createOrder(CreateOrderDTO dto) {
UserInfo currentUser = UserContext.get(); // 直接获取当前用户
log.info("用户 {} 创建订单", currentUser.getUsername());
// ...
}
}
知识点 2:内存泄漏原因与防范
text
内存泄漏根因:
Thread(线程池线程,长期存活)
└── ThreadLocalMap
└── Entry(弱引用 Key → ThreadLocal,强引用 Value → UserInfo)
当 ThreadLocal 变量被 GC 回收(Key 变为 null),Value 仍被强引用
→ 线程池中的线程不销毁 → UserInfo 对象永久无法回收 → 内存泄漏
防范规则:
1. 必须在 finally 块或请求结束时调用 ThreadLocal.remove()
2. 使用 InheritableThreadLocal 时注意子线程传递问题
3. 线程池 + TransmittableThreadLocal(阿里开源)解决线程池场景下的上下文传递
16. 参数校验深度详解
知识点 1:JSR-303 常用校验注解
java
@Data
public class CreateUserRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在 2-20 之间")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotBlank(message = "密码不能为空")
@Size(min = 8, max = 32, message = "密码长度必须在 8-32 之间")
private String password;
@NotNull(message = "年龄不能为空")
@Min(value = 18, message = "年龄不能小于 18")
@Max(value = 120, message = "年龄不能大于 120")
private Integer age;
@NotNull(message = "生日不能为空")
@Past(message = "生日必须是过去的时间")
private LocalDate birthday;
@DecimalMin(value = "0.0", message = "余额不能为负")
@DecimalMax(value = "1000000.0", message = "余额上限 100 万")
private BigDecimal balance;
// 嵌套对象校验,需要 @Valid
@Valid
@NotNull
private AddressDTO address;
}
@Data
public class AddressDTO {
@NotBlank(message = "省份不能为空")
private String province;
@NotBlank(message = "城市不能为空")
private String city;
}
知识点 2:分组校验(不同操作用不同规则)
java
// 定义分组接口
public interface ValidationGroups {
interface Create {} // 创建时校验
interface Update {} // 更新时校验
}
// 实体类中按分组配置规则
@Data
public class UserRequest {
// 更新时必须有 id,创建时不需要
@NotNull(groups = ValidationGroups.Update.class, message = "更新时 ID 不能为空")
@Null(groups = ValidationGroups.Create.class, message = "创建时不能传 ID")
private Long id;
// 创建时必填,更新时可选
@NotBlank(groups = ValidationGroups.Create.class, message = "创建时用户名必填")
@Size(min = 2, max = 20, groups = {ValidationGroups.Create.class, ValidationGroups.Update.class})
private String username;
@NotBlank(groups = ValidationGroups.Create.class, message = "创建时密码必填")
private String password;
}
// Controller 中使用 @Validated 指定分组
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public Result<UserDTO> create(
@Validated(ValidationGroups.Create.class) @RequestBody UserRequest request) {
// ...
}
@PutMapping("/{id}")
public Result<UserDTO> update(@PathVariable Long id,
@Validated(ValidationGroups.Update.class) @RequestBody UserRequest request) {
// ...
}
}
知识点 3:自定义校验注解
java
// 场景:校验手机号格式(中国大陆手机号)
// 1. 定义注解
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
@Documented
public @interface Phone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 2. 实现校验逻辑
public class PhoneValidator implements ConstraintValidator<Phone, String> {
private static final Pattern PHONE_PATTERN =
Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isBlank()) return true; // 空值交给 @NotBlank 处理
return PHONE_PATTERN.matcher(value).matches();
}
}
// 3. 使用
@Data
public class RegisterRequest {
@NotBlank
@Phone // 自定义注解
private String phone;
}
知识点 4:编程式校验(Service 层)
java
@Service
@RequiredArgsConstructor
public class UserService {
private final Validator validator; // javax.validation.Validator
public void processUser(User user) {
// 手动触发校验
Set<ConstraintViolation<User>> violations = validator.validate(user);
if (!violations.isEmpty()) {
String message = violations.stream()
.map(v -> v.getPropertyPath() + ": " + v.getMessage())
.collect(Collectors.joining(", "));
throw new BusinessException(400, "数据校验失败: " + message);
}
// 正常业务逻辑...
}
}
17. 异步编程:@Async 与线程池
知识点 1:@Async 原理和使用
@Async 通过 AOP 代理将方法提交到线程池异步执行,调用方立即返回,不等待方法完成。
注意:@Async 失效场景与 @Transactional 相同------同类内部调用绕过代理。
java
// 1. 启用异步支持
@SpringBootApplication
@EnableAsync
public class MyApplication { ... }
// 2. 配置线程池(不配置则使用 SimpleAsyncTaskExecutor,不推荐!)
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 核心线程数(常驻)
executor.setMaxPoolSize(20); // 最大线程数
executor.setQueueCapacity(100); // 任务队列容量
executor.setKeepAliveSeconds(60); // 非核心线程空闲存活时间
executor.setThreadNamePrefix("async-"); // 线程名前缀(便于日志定位)
// 拒绝策略:队列满且线程数已达上限时,由调用者线程执行(不丢任务)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
// 3. 使用 @Async
@Service
@Slf4j
public class EmailService {
// 异步发送邮件(指定线程池)
@Async("taskExecutor")
public void sendEmail(String to, String subject, String content) {
log.info("发送邮件到 {} [线程: {}]", to, Thread.currentThread().getName());
// 模拟耗时操作
try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
log.info("邮件发送完成: {}", to);
}
// 异步方法可以有返回值(Future)
@Async("taskExecutor")
public CompletableFuture<String> processData(Long id) {
log.info("异步处理数据 ID: {}", id);
// 处理逻辑...
String result = "处理结果: " + id;
return CompletableFuture.completedFuture(result);
}
}
// 4. 调用示例
@Service
@RequiredArgsConstructor
public class OrderService {
private final EmailService emailService;
public Order createOrder(CreateOrderDTO dto) {
Order order = saveOrder(dto);
// 异步发送确认邮件(不阻塞下单主流程)
emailService.sendEmail(dto.getUserEmail(), "订单确认", "您的订单已创建成功");
// 异步处理,可以等待结果
CompletableFuture<String> future = emailService.processData(order.getId());
// 此时主线程可以做其他事情...
// 需要结果时:String result = future.get(5, TimeUnit.SECONDS);
return order;
}
}
知识点 2:CompletableFuture 并发编排
java
@Service
@RequiredArgsConstructor
public class OrderDetailService {
private final UserFeignClient userClient;
private final ProductFeignClient productClient;
private final InventoryFeignClient inventoryClient;
/**
* 组装订单详情页数据
* 三个接口互相独立,可以并发查询
*/
public OrderDetailDTO getOrderDetail(Long orderId) {
// 并发查询(三个请求同时发出)
CompletableFuture<UserDTO> userFuture =
CompletableFuture.supplyAsync(() -> userClient.getUser(1L));
CompletableFuture<ProductDTO> productFuture =
CompletableFuture.supplyAsync(() -> productClient.getProduct(2L));
CompletableFuture<Integer> stockFuture =
CompletableFuture.supplyAsync(() -> inventoryClient.getStock(2L));
// 等待所有完成并组装结果
CompletableFuture.allOf(userFuture, productFuture, stockFuture).join();
// 顺序执行(依赖上一步结果)
// CompletableFuture<String> chain = CompletableFuture
// .supplyAsync(() -> "step1")
// .thenApply(result -> result + " -> step2") // 同步处理
// .thenApplyAsync(result -> result + " -> step3") // 异步处理
// .exceptionally(e -> "error: " + e.getMessage()); // 异常处理
return OrderDetailDTO.builder()
.user(userFuture.join())
.product(productFuture.join())
.stock(stockFuture.join())
.build();
}
}
18. 定时任务详解
知识点 1:@Scheduled 注解详解
java
@Component
@Slf4j
public class ScheduledTasks {
// 固定延迟:上次执行完毕后,再等待 5 秒才执行下次
// 适合:任务本身耗时不固定,需要任务完成后再计时
@Scheduled(fixedDelay = 5000)
public void fixedDelayTask() {
log.info("fixedDelay 任务执行: {}", LocalDateTime.now());
}
// 固定速率:每隔 5 秒执行一次(从上次开始时间算)
// 适合:需要严格按时间间隔执行(如心跳检测)
// 注意:若任务执行超过间隔时间,下次立即执行(可能并发)
@Scheduled(fixedRate = 5000)
public void fixedRateTask() {
log.info("fixedRate 任务执行: {}", LocalDateTime.now());
}
// 初始延迟:应用启动后延迟 10 秒再开始执行
@Scheduled(fixedRate = 60000, initialDelay = 10000)
public void withInitialDelay() {
log.info("延迟启动的定时任务");
}
// Cron 表达式:最灵活的方式
// 格式:秒 分 时 日 月 星期
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨 2 点
public void dailyCleanup() {
log.info("每日数据清理任务");
}
@Scheduled(cron = "0 0 9-18 * * MON-FRI") // 工作日 9-18点每整点
public void workHoursReport() {
log.info("工作时间报表");
}
// 从配置文件读取 Cron(支持动态修改)
@Scheduled(cron = "${app.tasks.report.cron:0 0 8 * * ?}")
public void configurableCron() {
log.info("可配置的定时任务");
}
}
Cron 表达式速查:
text
秒 分 时 日 月 周
0 0 2 * * ? → 每天凌晨 2:00:00
0 0/30 * * * ? → 每30分钟
0 0 0 1 * ? → 每月1日凌晨
0 0 8 ? * MON → 每周一早上8点
0 0 0 L * ? → 每月最后一天凌晨
知识点 2:定时任务的生产问题------分布式环境重复执行
text
问题:3个实例部署,每个实例都会在凌晨2点执行清理任务,导致重复执行!
解决方案1:Quartz(分布式任务调度框架)
解决方案2:ShedLock(基于数据库/Redis 的分布式锁)→ 推荐,接入成本低
解决方案3:XXL-JOB / PowerJob(专业分布式调度平台)
ShedLock 接入(最简单):
xml
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>5.10.0</version>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-redis-spring</artifactId>
<version>5.10.0</version>
</dependency>
java
@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
public class ShedLockConfig {
@Bean
public LockProvider lockProvider(RedisConnectionFactory connectionFactory) {
return new RedisLockProvider(connectionFactory, "production");
}
}
@Component
@Slf4j
public class DistributedScheduledTask {
// 多实例中只有一个实例执行,其他实例跳过
@Scheduled(cron = "0 0 2 * * ?")
@SchedulerLock(
name = "daily-cleanup-task", // 锁名称(全局唯一)
lockAtLeastFor = "5m", // 最少持锁5分钟(防止任务太快完成又被抢)
lockAtMostFor = "10m" // 最多持锁10分钟(防止宕机锁永远不释放)
)
public void dailyCleanup() {
log.info("分布式清理任务执行 [实例: {}]", InetAddress.getLocalHost().getHostName());
// 只有获得锁的实例才会执行到这里
}
}
19. 文件上传与下载
知识点 1:本地文件上传
java
@RestController
@RequestMapping("/api/files")
@Slf4j
public class FileController {
@Value("${file.upload-dir:/tmp/uploads}")
private String uploadDir;
// 单文件上传
@PostMapping("/upload")
public Result<String> upload(@RequestParam("file") MultipartFile file) throws IOException {
// 1. 校验文件
if (file.isEmpty()) throw new BusinessException(400, "文件不能为空");
long maxSize = 10 * 1024 * 1024; // 10MB
if (file.getSize() > maxSize) throw new BusinessException(400, "文件大小不能超过 10MB");
String originalName = file.getOriginalFilename();
String extension = StringUtils.getFilenameExtension(originalName);
Set<String> allowedExtensions = Set.of("jpg", "jpeg", "png", "gif", "pdf");
if (!allowedExtensions.contains(extension.toLowerCase())) {
throw new BusinessException(400, "不支持的文件类型: " + extension);
}
// 2. 生成唯一文件名(防止重名覆盖)
String fileName = UUID.randomUUID() + "." + extension;
// 3. 按日期分目录存储
String datePath = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
Path dirPath = Paths.get(uploadDir, datePath);
Files.createDirectories(dirPath);
// 4. 保存文件
Path filePath = dirPath.resolve(fileName);
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
String fileUrl = "/files/" + datePath + "/" + fileName;
log.info("文件上传成功: {} → {}", originalName, filePath);
return Result.success(fileUrl);
}
// 多文件上传
@PostMapping("/upload/batch")
public Result<List<String>> uploadBatch(
@RequestParam("files") List<MultipartFile> files) throws IOException {
List<String> urls = new ArrayList<>();
for (MultipartFile file : files) {
// 复用单文件上传逻辑
urls.add(upload(file).getData());
}
return Result.success(urls);
}
// 文件下载
@GetMapping("/download/{date}/{filename}")
public ResponseEntity<Resource> download(
@PathVariable String date,
@PathVariable String filename,
HttpServletRequest request) throws IOException {
Path filePath = Paths.get(uploadDir, date, filename);
Resource resource = new UrlResource(filePath.toUri());
if (!resource.exists() || !resource.isReadable()) {
throw new BusinessException(404, "文件不存在");
}
// 探测文件 MIME 类型
String contentType = request.getServletContext()
.getMimeType(resource.getFile().getAbsolutePath());
if (contentType == null) contentType = "application/octet-stream";
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
// inline:浏览器预览;attachment:强制下载
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + URLEncoder.encode(filename, StandardCharsets.UTF_8) + "\"")
.body(resource);
}
}
yaml
# application.yml
spring:
servlet:
multipart:
enabled: true
max-file-size: 10MB # 单文件最大 10MB
max-request-size: 50MB # 整个请求最大 50MB(多文件)
file-size-threshold: 2MB # 超过 2MB 写临时文件(避免内存溢出)
file:
upload-dir: /data/uploads
20. 接口文档:SpringDoc OpenAPI
知识点:自动生成 Swagger UI
xml
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
java
// 全局配置
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("订单服务 API")
.version("v1.0")
.description("订单服务的 REST API 文档")
.contact(new Contact().name("开发团队").email("dev@example.com")))
.addSecurityItem(new SecurityRequirement().addList("Bearer Token"))
.components(new Components()
.addSecuritySchemes("Bearer Token",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
}
// Controller 文档注解
@RestController
@RequestMapping("/api/v1/users")
@Tag(name = "用户管理", description = "用户的增删改查接口")
public class UserController {
@Operation(
summary = "根据 ID 查询用户",
description = "通过用户 ID 获取用户详细信息,需要登录"
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "查询成功"),
@ApiResponse(responseCode = "404", description = "用户不存在")
})
@GetMapping("/{id}")
public Result<UserDTO> getUser(
@Parameter(description = "用户 ID", example = "1") @PathVariable Long id) {
return Result.success(userService.findById(id));
}
}
// DTO 文档注解
@Data
@Schema(description = "创建用户请求体")
public class CreateUserRequest {
@Schema(description = "邮箱", example = "user@example.com", requiredMode = Schema.RequiredMode.REQUIRED)
@Email @NotBlank
private String email;
@Schema(description = "密码(8-32位)", example = "MyPass@123")
@NotBlank @Size(min = 8, max = 32)
private String password;
}
yaml
# 访问地址:http://localhost:8080/swagger-ui.html
springdoc:
swagger-ui:
path: /swagger-ui.html # 自定义访问路径
tags-sorter: alpha # 按字母排序标签
operations-sorter: method # 按 HTTP 方法排序
api-docs:
path: /api-docs # JSON 文档地址
packages-to-scan: com.example.controller # 扫描范围
# 生产环境关闭(安全)
# springdoc.swagger-ui.enabled: false
# springdoc.api-docs.enabled: false