Spring Boot异常处理与跨域实战
·
🧩 一、统一异常处理:让错误有“标准答案”
❓ 为什么需要?
- 如果不处理,异常会直接抛给前端,返回 HTML 错误页或裸露的堆栈信息(不安全、不友好)。
- 每个 Controller 都写
try-catch重复且难以维护。
✅ 核心机制:@ControllerAdvice + @ExceptionHandler
在当前开发中本人倾向于使用
@RestControllerAdvice与@Exception的搭配
1. 定义统一返回格式
// Result.java
@Data
@AllArgsConstructor
public class Result<T> {
private int code;
private String msg;
private T data;
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}
public static <T> Result<T> error(int code, String msg) {
return new Result<>(code, msg, null);
}
}
2. 自定义业务异常(可选但推荐)
通过继承并实现某个类型的异常类拦截业务异常
// BizException.java
public class BizException extends RuntimeException {
private final int code;
public BizException(int code, String message) {
super(message);
this.code = code;
}
// getter...
}
3. 全局异常处理器
拦截所有的异常
@RestControllerAdvice // = @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {
// 捕获自定义业务异常
@ExceptionHandler(BizException.class)
public ResponseEntity<Result<?>> handleBiz(BizException e) {
return ResponseEntity.badRequest()
.body(Result.error(e.getCode(), e.getMessage()));
}
// 捕获参数校验异常(如 @Valid 失败)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Result<?>> handleValidation(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldError().getDefaultMessage();
return ResponseEntity.badRequest().body(Result.error(400, msg));
}
// 兜底:捕获所有未处理的异常(如 NullPointerException)
@ExceptionHandler(Exception.class)
public ResponseEntity<Result<?>> handleGeneral(Exception e) {
// 生产环境不要暴露具体异常信息!
return ResponseEntity.status(500).body(Result.error(500, "服务器内部错误"));
}
}
💡 关键点:
@RestControllerAdvice作用于 所有 Controller。- 异常处理方法按 异常类型精确匹配,找不到则走兜底。
- 生产环境务必隐藏真实异常堆栈,防止信息泄露。
🔒 二、Filter vs lnterceptor
| 特性 | 过滤器(Filter) | 拦截器(Interceptor) |
|---|---|---|
| 所属规范 | Java Servlet 规范 | Spring MVC 框架 |
| 作用范围 | 所有请求(包括静态资源 /static/、/favicon.ico) |
仅 Spring MVC 的 Controller 请求 |
| 能否注入 Spring Bean | ❌ 不能(非 Spring 管理) | ✅ 可以(是 Spring Bean) |
| 执行时机 | 在 DispatcherServlet 之前/之后 |
在 Controller 方法 调用前后(preHandle / postHandle / afterCompletion) |
| 典型用途 | 设置字符编码、CORS 预检、请求日志、安全头 | 登录鉴权(Token 校验)、权限控制、接口耗时统计 |
✅ 使用建议:
- 协议层/底层处理 → 用 Filter
(如:强制 UTF-8 编码、全局 CORS 响应头) - 业务逻辑层处理 → 用 Interceptor
(如:检查用户是否登录、是否有权限访问)
拦截器示例(登录校验):
在项目开发中,通常会使用这个拦截器模版,拦截除登录/注册意外的所有路径,获取token(Jwt令牌)后携带着进行访问
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("Authorization");
if (token == null || !isValid(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false; // 拦截,不放行
}
return true; // 放行
}
}
// 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/api/**") // 拦截哪些路径
.excludePathPatterns("/api/login"); // 排除哪些路径
}
}
🌐 三、CORS
❓ 什么是跨域?
浏览器出于安全考虑,禁止前端(如 http://localhost:3000)向不同源(协议/域名/端口任一不同)的后端(如 http://localhost:8080)发起 AJAX 请求。
✅ 解决方案
@CrossOrigin(开发调试用)。但是需在每个 Controller 上加,不适合生产。@RestController @CrossOrigin(origins = "http://localhost:3000") public class UserController { // 此 Controller 下所有接口都允许来自 localhost:3000 的跨域请求 }
全局配置
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // 匹配的路径
.allowedOrigins("http://localhost:3000", "https://yourdomain.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true); // 允许携带 Cookie
}
}
⚠️ 重要注意事项:
allowCredentials(true)时,allowedOrigins不能为"\*",必须指定具体域名。- 更优方案:Nginx 反向代理
将前端和后端部署在同一域名下(如nginx代理/api到后端),彻底规避跨域问题,更安全高效。
📤📥 四、文件上传与下载
1. 文件上传(MultipartFile)
后端代码:
@PostMapping("/upload")
public Result<String> upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
throw new IllegalArgumentException("文件为空");
}
try {
String filename = UUID.randomUUID() + "_" + file.getOriginalFilename();
Path path = Paths.get("/tmp/uploads/" + filename);
Files.createDirectories(path.getParent()); // 确保目录存在
file.transferTo(path.toFile());
return Result.success(filename);
} catch (IOException e) {
throw new RuntimeException("上传失败", e);
}
}
前端要求:
- 表单
enctype="multipart/form-data" - 使用
FormData对象提交(Axios/Vanilla JS)
配置最大文件大小(application.yml):
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
2. 文件下载
@GetMapping("/download/{filename}")
public void download(@PathVariable String filename, HttpServletResponse response) throws IOException {
Path file = Paths.get("/tmp/uploads/" + filename);
if (!Files.exists(file)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件不存在");
return;
}
// 设置响应头
response.setContentType("application/octet-stream"); // 二进制流
response.setHeader("Content-Disposition",
"attachment; filename=" + URLEncoder.encode(filename, "UTF-8"));
// 写入响应流
try (InputStream is = Files.newInputStream(file);
OutputStream os = response.getOutputStream()) {
is.transferTo(os);
}
}
💡 关键点:
- 上传:用
MultipartFile,注意空文件校验和存储路径安全。 - 下载:设置
Content-Type和Content-Disposition,用Files.copy()或transferTo()高效传输。 - 路径安全:避免用户通过
filename参数遍历服务器文件(如../../../etc/passwd),应对文件名做白名单或 UUID 映射。
✅ 总结:新手复盘 Checklist
| 模块 | 你是否能回答? |
|---|---|
| 统一异常 | 能否写出一个全局异常处理器?知道如何区分业务异常和系统异常? |
| Filter vs Interceptor | 能否说出两者区别?知道 Token 校验该用哪个? |
| 跨域 | 能否配置全局 CORS?知道 allowCredentials 的限制? |
| 文件操作 | 能否写出上传/下载接口?知道如何防路径遍历? |
🌟 记住:这些不是“魔法”,而是 Spring Boot 对 Servlet 规范和 Spring MVC 的封装与增强。理解其背后的 HTTP 协议、请求生命周期,才能真正掌握。
更多推荐




所有评论(0)