第二篇:核心篇 — Spring Boot 常用开发能力

目标 :掌握企业项目中最常用的后端开发能力
前置要求:完成入门篇


目录

  1. 参数接收
  2. 数据校验
  3. 统一返回结果
  4. 全局异常处理
  5. 日志体系
  6. 多环境配置
  7. 拦截器
  8. 过滤器
  9. 文件上传与下载
  10. [接口文档 Swagger/OpenAPI](#接口文档 Swagger/OpenAPI)
  11. [跨域处理 CORS](#跨域处理 CORS)
  12. 接口版本管理
  13. 面试高频题

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 字段,减少响应体大小。


上一篇:01_入门篇 | 下一篇:03_数据篇


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
相关推荐
RainCity11 小时前
Java Swing 自定义组件库分享(三)
java·笔记
泰式大师11 小时前
从“记忆”到“项目 Wiki”:我在 SkillLite 里实现了一套 Markdown-only LLM Wiki 自动维护机制
后端
凤凰院凶涛QAQ11 小时前
《C++转java快速入手系列》类与对象篇
java·开发语言·c++
Devin~Y11 小时前
大厂Java面试实录:Spring Boot/Cloud + Redis/Kafka + JWT + RAG/Agent(小Y翻车版)
java·spring boot·redis·spring cloud·kafka·spring security·jwt
渐儿11 小时前
案例2:内存管理与性能优化
后端
一叶之政11 小时前
C++ 系统学习日记・第 09 天|指针全解:定义 + 内存 + 空 / 野指针 + const 修饰 + 数组 + 函数
后端
风曦Kisaki11 小时前
# Linux服务Day1:模板机制作、FTP与NTP服务配置全解析
后端
渐儿11 小时前
案例3:文件系统与数据持久化
后端
渐儿11 小时前
Git 高阶使用与实战场景指南
后端
渐儿11 小时前
AI算法基础常识 - 从原理到应用
后端