背景

本文是《Java 后端从小白到大神》修仙系列之框架学习,Java框架之 SpringBoot 框架第三篇SpringBoot框架 可以说是微服务的基石,很多复杂的系统几乎都是通过微服务构造而来,若想详细学习请点击首篇博文开始,现在开始学习。

文章概览

  1. 全局异常处理与统一返回格式
  2. Spring Boot 中使用拦截器与过滤器
  3. Spring Boot 中的文件上传与下载
  4. Spring Boot 集成 Spring Security 入门
  5. Spring Boot 实现 AOP 切面编程

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
package com.example.demo.common;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 统一API响应格式封装
 */
@Data
@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 new ApiResponse<>(200, "操作成功", data, System.currentTimeMillis());
    }

    // 成功响应(无数据)
    public static <T> ApiResponse<T> success() {
        return new ApiResponse<>(200, "操作成功", null, System.currentTimeMillis());
    }

    // 失败响应(自定义错误码和消息)
    public static <T> ApiResponse<T> fail(int code, String message) {
        return new ApiResponse<>(code, message, null, System.currentTimeMillis());
    }

    // 失败响应(通用错误)
    public static <T> ApiResponse<T> fail(String message) {
        return new ApiResponse<>(500, message, null, System.currentTimeMillis());
    }

    // 失败响应(自定义异常)
    public static <T> ApiResponse<T> fail(BaseException e) {
        return new ApiResponse<>(e.getCode(), e.getMessage(), null, System.currentTimeMillis());
    }
}    

二、异常处理

基础异常类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.demo.exception;

import lombok.Getter;

/**
 * 基础异常类,所有自定义异常都应继承此类
 */
@Getter
public class BaseException extends RuntimeException {
    private final int code;

    public BaseException(int code, String message) {
        super(message);
        this.code = code;
    }

    public BaseException(int code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
    }
}    
业务异常
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.example.demo.exception;

/**
 * 业务异常,用于处理业务逻辑错误
 */
public class BusinessException extends BaseException {
    public BusinessException(String message) {
        super(40001, message);
    }

    public BusinessException(int code, String message) {
        super(code, message);
    }
}    
全局异常处理器
  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
package com.example.demo.exception;

import com.example.demo.common.ApiResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * 全局异常处理器
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * 处理自定义业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public ApiResponse<?> handleBusinessException(BusinessException e) {
        logger.error("业务异常: {}", e.getMessage());
        return ApiResponse.fail(e.getCode(), e.getMessage());
    }

    /**
     * 处理参数校验异常(MethodArgumentNotValidException)
     * 用于校验 @RequestBody 注解的参数
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResponse<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        Map<String, String> errors = new HashMap<>();
        for (FieldError error : e.getBindingResult().getFieldErrors()) {
            errors.put(error.getField(), error.getDefaultMessage());
        }
        logger.error("参数校验失败: {}", errors);
        return ApiResponse.fail(40002, "参数校验失败", errors);
    }

    /**
     * 处理参数绑定异常(BindException)
     * 用于校验 @ModelAttribute 注解的参数
     */
    @ExceptionHandler(BindException.class)
    public ApiResponse<?> handleBindException(BindException e) {
        Map<String, String> errors = new HashMap<>();
        for (FieldError error : e.getBindingResult().getFieldErrors()) {
            errors.put(error.getField(), error.getDefaultMessage());
        }
        logger.error("参数绑定失败: {}", errors);
        return ApiResponse.fail(40003, "参数绑定失败", errors);
    }

    /**
     * 处理请求参数格式错误(MethodArgumentTypeMismatchException)
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public ApiResponse<?> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
        logger.error("参数类型不匹配: {}", e.getMessage());
        return ApiResponse.fail(40004, "参数类型不匹配: " + e.getName());
    }

    /**
     * 处理请求体解析错误(HttpMessageNotReadableException)
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public ApiResponse<?> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
        logger.error("请求体解析失败: {}", e.getMessage());
        return ApiResponse.fail(40005, "请求体格式错误");
    }

    /**
     * 处理方法不支持异常(HttpRequestMethodNotSupportedException)
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ApiResponse<?> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        logger.error("不支持的请求方法: {}", e.getMessage());
        return ApiResponse.fail(40006, "不支持的请求方法: " + e.getMethod());
    }

    /**
     * 处理约束违反异常(ConstraintViolationException)
     * 用于校验 @PathVariable 和 @RequestParam 注解的参数
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public ApiResponse<?> handleConstraintViolationException(ConstraintViolationException e) {
        Map<String, String> errors = new HashMap<>();
        Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
        for (ConstraintViolation<?> violation : violations) {
            String propertyPath = violation.getPropertyPath().toString();
            String message = violation.getMessage();
            errors.put(propertyPath, message);
        }
        logger.error("参数约束校验失败: {}", errors);
        return ApiResponse.fail(40007, "参数约束校验失败", errors);
    }

    /**
     * 处理未定义的其他异常
     */
    @ExceptionHandler(Exception.class)
    public ApiResponse<?> handleException(Exception e) {
        logger.error("系统内部异常", e);
        return ApiResponse.fail(50000, "系统内部异常,请联系管理员");
    }
}    

三、结合返回统一格式

Controller 直接返回 ApiResponse.success(data)

Controller 直接返回封装
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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
package com.example.demo.controller;

import com.example.demo.common.ApiResponse;
import com.example.demo.exception.BusinessException;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import java.util.HashMap;
import java.util.Map;

/**
 * 用户控制器示例
 */
@RestController
@RequestMapping("/api/users")
public class UserController {

    /**
     * 示例接口:获取用户信息
     */
    @GetMapping("/{id}")
    public ApiResponse<?> getUser(@PathVariable @Min(1) Long id) {
        // 模拟业务异常
        if (id <= 0) {
            throw new BusinessException("用户ID无效");
        }
        
        // 模拟查询用户
        Map<String, Object> user = new HashMap<>();
        user.put("id", id);
        user.put("name", "Test User");
        user.put("age", 30);
        
        return ApiResponse.success(user);
    }

    /**
     * 示例接口:创建用户
     */
    @PostMapping
    public ApiResponse<?> createUser(@RequestBody @Valid UserRequest request) {
        // 模拟创建用户
        return ApiResponse.success("用户创建成功");
    }

    /**
     * 示例接口:触发系统异常
     */
    @GetMapping("/error")
    public ApiResponse<?> triggerError() {
        // 模拟系统异常
        throw new RuntimeException("这是一个未处理的系统异常");
    }

    /**
     * 用户请求DTO
     */
    static class UserRequest {
        @NotBlank(message = "用户名不能为空")
        private String username;
        
        @Min(value = 18, message = "年龄不能小于18岁")
        private Integer age;

        public String getUsername() {
            return username;
        }

        public void setUsername(String username) {
            this.username = username;
        }

        public Integer getAge() {
            return age;
        }

        public void setAge(Integer age) {
            this.age = age;
        }
    }
}    

2. Spring Boot 中使用拦截器与过滤器

一、过滤器(Filter)

过滤器执行流程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
客户端请求 → Filter1 → Filter2 → ... → DispatcherServlet → 控制器处理 → 视图渲染 → ... → Filter2 → Filter1 → 响应客户端

--分割线--

Tomcat -> RequestLoggingFilter (通用 Filter)
Spring Security 内部的 SecurityFilterChain(由 SecurityConfig 生成)
JwtAuthenticationFilter(JWT 校验)
FilterSecurityInterceptor(权限判断)
DispatcherServlet -> Controller -> 返回响应
请求日志过滤器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.example.demo.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Enumeration;

/**
 * 请求日志过滤器:记录请求信息
 */
@WebFilter(urlPatterns = "/*", filterName = "requestLoggingFilter")
public class RequestLoggingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        // 记录请求信息
        logRequestInfo(httpRequest);
        
        // 继续处理请求
        chain.doFilter(request, response);
    }

    private void logRequestInfo(HttpServletRequest request) {
        System.out.println("=============== 请求开始 ===============");
        System.out.println("请求路径: " + request.getRequestURI());
        System.out.println("请求方法: " + request.getMethod());
        System.out.println("请求头信息:");
        
        // 打印所有请求头
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            System.out.println("  " + headerName + ": " + request.getHeader(headerName));
        }
        
        // 打印请求参数
        System.out.println("请求参数: " + request.getParameterMap());
        System.out.println("=============== 请求结束 ===============");
    }
}

二、拦截器(HandlerInterceptor)

拦截器执行流程:

1
请求进入 DispatcherServlet → 拦截器preHandle → 控制器处理 → 拦截器postHandle → 视图渲染 → 拦截器afterCompletion → DispatcherServlet 返回前端
权限拦截器
 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
package com.example.demo.interceptor;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 权限拦截器:验证请求是否有权限访问
 */
@Component
public class AuthInterceptor implements HandlerInterceptor {

    // 请求处理前执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        // 从请求头中获取token
        String token = request.getHeader("Authorization");
        
        // 验证token(示例逻辑,实际应根据业务需求实现)
        if (token == null || !token.startsWith("Bearer ")) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("未授权访问");
            return false;
        }
        
        // 提取并验证token有效性
        String tokenValue = token.substring(7);
        if (!validateToken(tokenValue)) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.getWriter().write("无效的访问令牌");
            return false;
        }
        
        // 将用户信息存入request,供后续处理使用
        request.setAttribute("userId", extractUserId(tokenValue));
        
        // 允许请求继续
        return true;
    }

    // 请求处理后执行,但在视图渲染前
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception {
        // 可以在这里添加渲染前的处理逻辑
    }

    // 整个请求完成后执行(视图渲染后)
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
                                Exception ex) throws Exception {
        // 可以在这里添加资源清理等操作
    }

    // 验证token有效性(示例方法)
    private boolean validateToken(String token) {
        // 实际应调用认证服务验证token
        return token.equals("valid_token");
    }

    // 从token中提取用户ID(示例方法)
    private String extractUserId(String token) {
        // 实际应从token中解析用户信息
        return "12345";
    }
}

三、注册拦截器

Web MVC配置类
 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
package com.example.demo.config;

import com.example.demo.interceptor.AuthInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Web MVC配置类:注册拦截器和过滤器
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Bean
    public AuthInterceptor authInterceptor() {
        return new AuthInterceptor();
    }

    // 注册拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册权限拦截器
        registry.addInterceptor(authInterceptor())
                .addPathPatterns("/api/**")  // 拦截所有/api/开头的请求
                .excludePathPatterns("/api/public/**");  // 排除公共接口
        
        // 可以注册多个拦截器,按顺序执行
    }
}


### 配置说明

1. **过滤器配置**
   - 使用 `@WebFilter` 注解定义过滤器
   - 使用 `@ServletComponentScan` 启用Servlet组件扫描
   - 可以通过 `FilterRegistrationBean` 进行更灵活的配置如指定顺序

2. **拦截器配置**
   - 实现 `HandlerInterceptor` 接口
   - 通过 `WebMvcConfigurer` 注册拦截器
   - 可以指定拦截路径和排除路径

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
package com.example.demo.controller;

import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

/**
 * 示例控制器
 */
@RestController
@RequestMapping("/api")
public class ExampleController {

    /**
     * 公共接口,不需要认证
     */
    @GetMapping("/public/hello")
    public Map<String, String> publicHello() {
        Map<String, String> result = new HashMap<>();
        result.put("message", "公共接口,无需认证");
        return result;
    }

    /**
     * 需要认证的接口
     */
    @GetMapping("/secure/hello")
    public Map<String, String> secureHello(@RequestHeader("Authorization") String token,
                                           @RequestAttribute("userId") String userId) {
        Map<String, String> result = new HashMap<>();
        result.put("message", "认证通过的接口");
        result.put("token", token);
        result.put("userId", userId);
        return result;
    }
}
应用启动类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

/**
 * 应用启动类
 */
@SpringBootApplication
@ServletComponentScan  // 启用Servlet组件扫描(用于过滤器)
public class Application {

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

四、使用示例

  1. 访问公共接口(无需认证)

    1
    
    curl http://localhost:8080/api/public/hello
    

    响应

    1
    2
    3
    
    {
        "message": "公共接口,无需认证"
    }
    
  2. 访问需要认证的接口(未提供token)

    1
    
    curl http://localhost:8080/api/secure/hello
    

    响应

    未授权访问
    
  3. 访问需要认证的接口(提供有效token)

    1
    
    curl -H "Authorization: Bearer valid_token" http://localhost:8080/api/secure/hello
    

    响应

    1
    2
    3
    4
    5
    
    {
        "message": "认证通过的接口",
        "token": "Bearer valid_token",
        "userId": "12345"
    }
    

五、过滤器与拦截器的区别

特性 过滤器(Filter) 拦截器(Interceptor)
实现接口 javax.servlet.Filter org.springframework.web.servlet.HandlerInterceptor
介入时机 在Servlet容器处理请求前/后 在Spring MVC处理请求前/后/完成后
依赖 依赖Servlet API 依赖Spring MVC框架
应用场景 日志记录、字符编码转换、请求过滤等底层操作 权限验证、请求参数处理、事务管理等业务层面操作
访问Spring Bean 不能直接访问 可以直接访问Spring Bean
执行顺序 按注册顺序执行 按注册顺序执行

3. Spring Boot 中的文件上传与下载

一、上传接口与下载接口

上传接口与下载接口
  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
package com.example.demo.controller;

import com.example.demo.common.ApiResponse;
import com.example.demo.exception.BusinessException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.UUID;

/**
 * 文件上传与下载控制器
 */
@RestController
@RequestMapping("/api/files")
public class FileController {

    // 上传文件存储目录
    @Value("${file.upload-dir:uploads}")
    private String uploadDir;

    /**
     * 单文件上传接口
     */
    @PostMapping("/upload")
    public ApiResponse<String> uploadFile(@RequestParam("file") MultipartFile file) {
        if (file.isEmpty()) {
            throw new BusinessException("上传文件不能为空");
        }
        
        try {
            // 生成唯一文件名,防止文件覆盖
            String fileName = UUID.randomUUID().toString() + "_" + file.getOriginalFilename();
            Path targetLocation = Paths.get(uploadDir).resolve(fileName);
            
            // 创建存储目录(如果不存在)
            Files.createDirectories(Paths.get(uploadDir));
            
            // 保存文件
            Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);
            
            // 返回文件访问URL(实际项目中可能需要根据部署环境调整)
            String fileUrl = "/api/files/download?fileName=" + fileName;
            return ApiResponse.success("文件上传成功", fileUrl);
            
        } catch (IOException e) {
            throw new BusinessException("文件上传失败: " + e.getMessage());
        }
    }

    /**
     * 多文件上传接口
     */
    @PostMapping("/upload/multiple")
    public ApiResponse<String> uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) {
        if (files == null || files.length == 0) {
            throw new BusinessException("上传文件不能为空");
        }
        
        try {
            for (MultipartFile file : files) {
                if (!file.isEmpty()) {
                    // 处理每个文件(逻辑同上)
                    String fileName = UUID.randomUUID().toString() + "_" + file.getOriginalFilename();
                    Path targetLocation = Paths.get(uploadDir).resolve(fileName);
                    Files.createDirectories(Paths.get(uploadDir));
                    Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);
                }
            }
            return ApiResponse.success("多文件上传成功");
        } catch (IOException e) {
            throw new BusinessException("文件上传失败: " + e.getMessage());
        }
    }

    /**
     * 文件下载接口
     */
    @GetMapping("/download")
    public ResponseEntity<Resource> downloadFile(@RequestParam("fileName") String fileName,
                                                 HttpServletRequest request) {
        try {
            // 构建文件路径
            Path filePath = Paths.get(uploadDir).resolve(fileName).normalize();
            Resource resource = new UrlResource(filePath.toUri());
            
            // 检查文件是否存在
            if (!resource.exists()) {
                throw new BusinessException("文件不存在");
            }
            
            // 确定文件内容类型
            String contentType = null;
            try {
                contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
            } catch (IOException ex) {
                // 如果无法确定内容类型,使用默认值
                contentType = "application/octet-stream";
            }
            
            // 如果内容类型仍未确定,使用默认值
            if (contentType == null) {
                contentType = "application/octet-stream";
            }
            
            // 构建响应
            return ResponseEntity.ok()
                    .contentType(MediaType.parseMediaType(contentType))
                    .header(HttpHeaders.CONTENT_DISPOSITION, 
                            "attachment; filename=\"" + resource.getFilename() + "\"")
                    .body(resource);
                    
        } catch (MalformedURLException e) {
            throw new BusinessException("文件路径无效");
        }
    }

    /**
     * 在线预览文件接口
     */
    @GetMapping("/preview")
    public ResponseEntity<Resource> previewFile(@RequestParam("fileName") String fileName,
                                                HttpServletRequest request) {
        try {
            // 构建文件路径
            Path filePath = Paths.get(uploadDir).resolve(fileName).normalize();
            Resource resource = new UrlResource(filePath.toUri());
            
            // 检查文件是否存在
            if (!resource.exists()) {
                throw new BusinessException("文件不存在");
            }
            
            // 确定文件内容类型
            String contentType = null;
            try {
                contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
            } catch (IOException ex) {
                contentType = "application/octet-stream";
            }
            
            if (contentType == null) {
                contentType = "application/octet-stream";
            }
            
            // 构建响应(使用 inline 表示浏览器应尝试直接显示内容)
            return ResponseEntity.ok()
                    .contentType(MediaType.parseMediaType(contentType))
                    .header(HttpHeaders.CONTENT_DISPOSITION, 
                            "inline; filename=\"" + resource.getFilename() + "\"")
                    .body(resource);
                    
        } catch (MalformedURLException e) {
            throw new BusinessException("文件路径无效");
        }
    }
}

二、配置文件(application.properties)

1
2
3
4
5
6
7
8
9
# 文件上传配置
spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=100MB
spring.servlet.multipart.file-size-threshold=2MB
spring.servlet.multipart.location=${java.io.tmpdir}

# 自定义配置
file.upload-dir=./uploads  # 实际项目中建议使用绝对路径

三、文件存储服务

文件存储服务
 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
package com.example.demo.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.UUID;

/**
 * 文件存储服务
 */
@Service
public class FileStorageService {

    @Value("${file.upload-dir}")
    private String uploadDir;

    /**
     * 初始化存储目录
     */
    public void init() {
        try {
            Files.createDirectories(Paths.get(uploadDir));
        } catch (IOException e) {
            throw new RuntimeException("无法创建存储目录", e);
        }
    }

    /**
     * 存储文件
     */
    public String storeFile(MultipartFile file) {
        // 正常化文件名
        String originalFilename = StringUtils.cleanPath(file.getOriginalFilename());
        
        try {
            // 检查文件是否为空
            if (file.isEmpty()) {
                throw new RuntimeException("上传文件为空");
            }
            
            // 防止路径遍历攻击
            if (originalFilename.contains("..")) {
                throw new RuntimeException("文件名包含非法路径序列 " + originalFilename);
            }
            
            // 生成唯一文件名
            String fileName = UUID.randomUUID().toString() + "_" + originalFilename;
            Path targetLocation = Paths.get(uploadDir).resolve(fileName);
            
            // 存储文件
            Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);
            
            return fileName;
        } catch (IOException e) {
            throw new RuntimeException("存储文件失败: " + originalFilename, e);
        }
    }

    /**
     * 加载文件作为资源
     */
    public Resource loadFileAsResource(String fileName) {
        try {
            Path filePath = Paths.get(uploadDir).resolve(fileName).normalize();
            Resource resource = new UrlResource(filePath.toUri());
            
            if (resource.exists()) {
                return resource;
            } else {
                throw new RuntimeException("文件不存在: " + fileName);
            }
        } catch (MalformedURLException e) {
            throw new RuntimeException("文件路径无效: " + fileName, e);
        }
    }
}

4. Spring Boot 集成 Spring Security 入门

一、引入依赖

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

二、基本配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Configuration
public class SecurityConfig {
  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeRequests()
      .anyRequest().authenticated()
      .and().formLogin();
    return http.build();
  }
}

三、自定义登录用户

1
2
3
4
@Bean
public UserDetailsService userDetailsService() {
  return new InMemoryUserDetailsManager(User.withUsername("admin").password("{noop}1234").roles("ADMIN").build());
}

5. Spring Boot 实现 AOP 切面编程

一、AOP 依赖

spring-boot-starter-aop 默认包含在 Web 项目中。

二、自定义切面

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Aspect
@Component
public class LogAspect {
  @Pointcut("@annotation(com.example.demo.annotation.Loggable)")
  public void logPointcut() {}

  @Around("logPointcut()")
  public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    // 打印日志
    return joinPoint.proceed();
  }
}

三、自定义注解

1
2
3
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {}

6. 综合上述内容练习

将上述内容综合起来,创建一个小项目。

pom文件
  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
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.2</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.example</groupId>
    <artifactId>filterandinterceptor</artifactId>
    <version>1.0-SNAPSHOT</version>

    <name>filterandinterceptor</name>
    <!-- FIXME change it to the project's website -->
    <url>http://www.example.com</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>18</maven.compiler.source>
        <maven.compiler.target>18</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- Spring Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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

        <!-- Validation -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- File Upload -->
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.5</version>
        </dependency>

        <!-- Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be
            moved to parent pom) -->
            <plugins>
                <!-- clean lifecycle, see
                https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
                <plugin>
                    <artifactId>maven-clean-plugin</artifactId>
                    <version>3.1.0</version>
                </plugin>
                <!-- default lifecycle, jar packaging: see
                https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
                <plugin>
                    <artifactId>maven-resources-plugin</artifactId>
                    <version>3.0.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.0</version>
                </plugin>
                <plugin>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>2.22.1</version>
                </plugin>
                <plugin>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>3.0.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-install-plugin</artifactId>
                    <version>2.5.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-deploy-plugin</artifactId>
                    <version>2.8.2</version>
                </plugin>
                <!-- site lifecycle, see
                https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
                <plugin>
                    <artifactId>maven-site-plugin</artifactId>
                    <version>3.7.1</version>
                </plugin>
                <plugin>
                    <artifactId>maven-project-info-reports-plugin</artifactId>
                    <version>3.0.0</version>
                </plugin>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <excludes>
                            <exclude>
                                <groupId>org.projectlombok</groupId>
                                <artifactId>lombok</artifactId>
                            </exclude>
                        </excludes>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>
application.properties配置文件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 服务器配置
# 服务器端口
server.port=8080

# 文件上传配置Spring Boot 自带
spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=100MB
spring.servlet.multipart.file-size-threshold=2MB

# 文件上传路径配置Spring Boot 使用
spring.servlet.multipart.location=./uploads

# 自定义目录初始化配置你的 FileStorageInitializer 使用
file.upload-dir=./uploads

# 日志配置可选
logging.level.root=INFO
logging.level.com.example=DEBUG

logging.level.org.springframework.security.web.FilterChainProxy=DEBUG
统一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
package com.example.demo.common;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 统一API响应格式
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> implements Serializable {
    private static final long serialVersionUID = 1L;

    private int code;           // 状态码
    private String message;     // 消息
    private T data;             // 数据
    private LocalDateTime timestamp; // 时间戳

    // 成功响应(带数据)
    public static <T> ApiResponse<T> success(String message, T data) {
        return new ApiResponse<>(200, message, data, LocalDateTime.now());
    }

    // 成功响应(不带数据)
    public static <T> ApiResponse<T> success(String message) {
        return new ApiResponse<>(200, message, null, LocalDateTime.now());
    }

    // 失败响应
    public static <T> ApiResponse<T> fail(int code, String message) {
        return new ApiResponse<>(code, message, null, LocalDateTime.now());
    }

    // 失败响应(默认错误码)
    public static <T> ApiResponse<T> fail(String message) {
        return new ApiResponse<>(500, message, null, LocalDateTime.now());
    }
}
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
package com.example.config;

import com.example.filter.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable()) // 关闭跨站请求伪造(CSRF)保护
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 禁用Session
                // 配置请求授权规则
                .authorizeRequests(auth -> auth
                        .requestMatchers(
                                "/api/public/**",
                                "/api/auth/**",
                                "/api/files/public/**",
                                "/api/files/download/**")
                        .permitAll()
                        .anyRequest().authenticated())
                .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
拦截器配置
 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
package com.example.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.example.interceptor.AuthInterceptor;

import jakarta.validation.constraints.NotNull;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private final AuthInterceptor authInterceptor;

    public WebMvcConfig(AuthInterceptor authInterceptor) {
        this.authInterceptor = authInterceptor;
    }

    @Override
    public void addInterceptors(@NotNull InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/api/secure/**")
                .excludePathPatterns("/api/public/**");
    }
}
认证控制器
 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

package com.example.controller;

import com.example.common.ApiResponse;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * 认证控制器
 */
@RestController
public class AuthController {

    /**
     * 用户登录接口
     */
    @PostMapping("/api/auth/login")
    public ApiResponse<?> login(@RequestParam String username, @RequestParam String password) {
        // 在实际应用中,这里应该验证用户名和密码
        // 这里简化处理,假设用户名是"admin",密码是"password"
        if (!"admin".equals(username) || !"password".equals(password)) {
            return ApiResponse.fail(401, "用户名或密码错误");
        }

        // 生成token(实际应用中应该使用JWT等方式生成)
        String token = "valid_token";

        // 返回包含token的响应
        Map<String, String> result = new HashMap<>();
        result.put("token", token);
        result.put("token_type", "Bearer");

        return ApiResponse.success("登录成功", result);
    }
}
示例控制器
 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
package com.example.controller;

import com.example.common.ApiResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api")
public class DemoController {

    @GetMapping("/public/hello")
    public ApiResponse<?> publicHello() {
        return ApiResponse.success("公共接口,无需认证");
    }

    @GetMapping("/secure/hello")
    public ApiResponse<?> secureHello(HttpServletRequest request) {
        String userId = (String) request.getAttribute("userId");
        String token = request.getHeader("Authorization");

        Map<String, Object> result = new HashMap<>();
        result.put("message", "认证通过的接口");
        result.put("token", token);
        result.put("userId", userId);

        return ApiResponse.success("认证成功", result);
    }
}
文件上传与下载控制器
 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
package com.example.controller;

import com.example.common.ApiResponse;
import com.example.exception.BusinessException;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.*;
import java.util.UUID;

@RestController
@RequestMapping("/api/files")
public class FileController {

    @Value("${file.upload-dir:uploads}")
    private String uploadDir;

    @PostMapping("/public/upload")
    public ApiResponse<String> publicUploadFile(@RequestParam("file") MultipartFile file) {
        return processFileUpload(file);
    }

    @PostMapping("/secure/upload")
    public ApiResponse<String> secureUploadFile(@RequestParam("file") MultipartFile file) {
        return processFileUpload(file);
    }

    private ApiResponse<String> processFileUpload(MultipartFile file) {
        if (file.isEmpty()) {
            throw new BusinessException("上传文件不能为空");
        }

        try {
            String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
            Path targetLocation = Paths.get(uploadDir).toAbsolutePath().normalize().resolve(fileName);
            Files.createDirectories(targetLocation.getParent());
            Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);

            String fileUrl = "/api/files/download?fileName=" + fileName;
            return ApiResponse.success("文件上传成功", fileUrl);

        } catch (IOException e) {
            throw new BusinessException("文件上传失败: " + e.getMessage());
        }
    }

    @GetMapping("/public/download")
    public ResponseEntity<Resource> publicDownload(@RequestParam String fileName, HttpServletRequest request) {
        return processDownload(fileName, request);
    }

    @GetMapping("/secure/download")
    public ResponseEntity<Resource> secureDownload(@RequestParam String fileName, HttpServletRequest request) {
        return processDownload(fileName, request);
    }

    private ResponseEntity<Resource> processDownload(String fileName, HttpServletRequest request) {
        try {
            Path filePath = Paths.get(uploadDir).toAbsolutePath().normalize().resolve(fileName);
            Resource resource = new UrlResource(filePath.toUri());

            if (!resource.exists()) {
                throw new BusinessException("文件不存在");
            }

            String contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
            contentType = contentType == null ? "application/octet-stream" : contentType;

            return ResponseEntity.ok()
                    .contentType(MediaType.parseMediaType(contentType))
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
                    .body(resource);

        } catch (MalformedURLException e) {
            throw new BusinessException("文件路径无效");
        } catch (IOException e) {
            throw new BusinessException("文件读取失败");
        }
    }
}
业务异常类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.exception;

import lombok.Getter;

/**
 * 业务异常类
 */
@Getter
public class BusinessException extends RuntimeException {
    private final int code;

    public BusinessException(String message) {
        super(message);
        this.code = 500;
    }

    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }
}
全局异常处理器
 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
package com.example.exception;

import com.example.common.ApiResponse;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

import java.util.HashMap;
import java.util.Map;

/**
 * 全局异常处理器
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public ApiResponse<?> handleBusinessException(BusinessException e) {
        return ApiResponse.fail(e.getCode(), e.getMessage());
    }

    /**
     * 处理方法参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResponse<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        Map<String, String> errors = new HashMap<>();
        e.getBindingResult().getFieldErrors()
                .forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
        return ApiResponse.fail(400, "参数校验失败");
    }

    /**
     * 处理表单参数校验异常
     */
    @ExceptionHandler(BindException.class)
    public ApiResponse<?> handleBindException(BindException e) {
        Map<String, String> errors = new HashMap<>();
        e.getBindingResult().getFieldErrors()
                .forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
        return ApiResponse.fail(400, "参数校验失败");
    }

    /**
     * 处理HTTP请求方法不支持异常
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ApiResponse<?> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        return ApiResponse.fail(405, "不支持的HTTP请求方法: " + e.getMethod());
    }

    /**
     * 处理HTTP消息不可读异常
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public ApiResponse<?> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
        return ApiResponse.fail(400, "请求体格式错误");
    }

    /**
     * 处理文件上传大小超出限制异常
     */
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ApiResponse<?> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e) {
        return ApiResponse.fail(413, "上传文件大小超出限制");
    }

    /**
     * 处理未捕获的异常
     */
    @ExceptionHandler(Exception.class)
    public ApiResponse<?> handleException(Exception e) {
        e.printStackTrace(); // 打印堆栈信息,便于调试
        return ApiResponse.fail("系统内部错误,请联系管理员");
    }
}
请求日志过滤器
 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
package com.example.filter;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * 请求日志过滤器
 */
@Slf4j
@Component
public class RequestLoggingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        // 记录请求信息
        log.info("请求路径: {}", httpRequest.getRequestURI());
        log.info("请求方法: {}", httpRequest.getMethod());
        log.info("请求IP: {}", httpRequest.getRemoteAddr());
        
        // 继续处理请求
        chain.doFilter(request, response);
    }
}
认证拦截器
 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
package com.example.interceptor;

import com.example.exception.BusinessException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

/**
 * 认证拦截器
 */
@Component
public class AuthInterceptor implements HandlerInterceptor {

    private static final String AUTH_HEADER = "Authorization";
    private static final String TOKEN_PREFIX = "Bearer ";

    @Override
    public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
            @NonNull Object handler)
            throws Exception {

        System.out.println("AuthInterceptor 执行了,请求路径:" + request.getRequestURI());
        // 获取请求头中的Authorization字段
        String authHeader = request.getHeader(AUTH_HEADER);

        // 验证token
        if (authHeader == null || !authHeader.startsWith(TOKEN_PREFIX)) {
            throw new BusinessException(401, "未授权访问");
        }

        String token = authHeader.substring(TOKEN_PREFIX.length());

        // 在实际应用中,这里应该调用认证服务验证token的有效性
        // 这里简化处理,假设token为"valid_token"时有效
        if (!"valid_token".equals(token)) {
            throw new BusinessException(401, "无效的token");
        }

        // 将用户信息存入request,供后续处理使用
        request.setAttribute("userId", "12345");

        return true;
    }
}
启动类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.example;

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

/**
 * 应用启动类
 */
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

一些疑问点:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
后端数据如何返回前端:

Controller 方法返回 ApiResponse 对象
Spring MVC 发现返回类型是对象,并且有 @ResponseBody(或 @RestController)
调用 HttpMessageConverter(如 MappingJackson2HttpMessageConverter)
用 Jackson 序列化 ApiResponse 对象,生成 JSON 字符串
将 JSON 字符串写入 HTTP 响应体
浏览器或 Postman 收到响应,拿到 JSON 数据

请求参数区别总结:

类型 示例 URL Spring 注解 数据位置
表单数据 POST /submit,body: name=Tom&age=20 @RequestParam 请求体(body)
查询参数(URL) GET /user?name=Tom&age=20 @RequestParam URL 查询字符串
路径参数 GET /user/123 @PathVariable URL 路径部分
JSON 请求体 POST /user,body: {"name":"Tom","age":20} @RequestBody 请求体(body)
文件上传 POST /upload,multipart 表单 @RequestParam + MultipartFile 表单 body

逻辑分析:

第一步:过滤器(Filter)

  • 所有请求都先经过 过滤器链

  • 配置的 JwtAuthenticationFilter 会判断:

    • token 是否存在、是否合法?
    • 如果路径是 /api/auth/login,就 放行(在 SecurityConfig 中已经 permitAll())。
    • 因为这个接口不需要登录,因此不做 token 校验。

第二步:拦截器(HandlerInterceptor)

  • 定义的拦截器(如 AuthInterceptor)是注册到 WebMvcConfig 的。
  • 它默认会拦截除 /api/auth/** 以外的路径,所以 不会拦截 /api/auth/login
  • 所以这个请求也跳过拦截器。

第三步:控制器(Controller)

  • 请求来到 AuthController,用户名和密码在方法里写死校验。
  • 校验成功后,生成一个包含 "userId": "12345" 的 JWT。

总结

SpringBoot 框架是微服务生态的基石,必知必会。