统一响应与全局异常处理¶
让所有接口返回统一格式,让所有异常集中处理——这是后端工程化的基本功。
统一响应:Result¶
前端最怕接口返回格式不统一(这个返回 {data},那个返回 {result})。约定一个统一结构:
用 Result<T> 包装,Controller 返回 Result.success(data) 即可:
package com.javaglm.task.common;
/**
* 统一响应结构(第 24 章)。
* 所有接口都返回 { code, message, data },前端只认这套结构。
* 对标前端 axios 封装里 res.data.code / res.data.data 的统一约定。
*/
public class Result<T> {
private int code;
private String message;
private T data;
public Result() {
}
/** 成功并带数据。 */
public static <T> Result<T> success(T data) {
Result<T> r = new Result<>();
r.code = 200;
r.message = "success";
r.data = data;
return r;
}
/** 成功无数据。 */
public static <T> Result<T> success() {
return success(null);
}
/** 失败。 */
public static <T> Result<T> error(int code, String message) {
Result<T> r = new Result<>();
r.code = code;
r.message = message;
return r;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
前端 axios 封装里就能统一判断 res.data.code === 200,逻辑极其清爽。
业务异常:BizException¶
业务出错(余额不足、任务不存在)不该用返回码 if/else 判断,而是直接抛异常:
package com.javaglm.task.common;
/**
* 业务异常(第 24 章)。
* 继承 RuntimeException(非受检),不用每个方法都 throws。
* 抛出后由全局异常处理器统一接住,转成友好响应。
*/
public class BizException extends RuntimeException {
private final int code;
public BizException(String message) {
this(400, message);
}
public BizException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}
全局异常处理器:@RestControllerAdvice¶
抛出的异常谁来接?全局异常处理器,集中转成 Result:
package com.javaglm.task.config;
import com.javaglm.task.common.BizException;
import com.javaglm.task.common.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器(第 24 章)。
* @RestControllerAdvice:全局拦截所有 Controller 抛出的异常,统一转成 Result 返回。
* 好处:Controller / Service 里只管"抛异常","怎么变成友好响应"集中在这里处理。
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/** 业务异常:返回业务错误码 + 消息。 */
@ExceptionHandler(BizException.class)
public Result<?> handleBiz(BizException e) {
return Result.error(e.getCode(), e.getMessage());
}
/** 参数校验失败:@Valid 触发的,提取第一条错误消息。 */
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValid(MethodArgumentNotValidException e) {
FieldError fe = e.getBindingResult().getFieldError();
String msg = fe != null ? fe.getDefaultMessage() : "参数校验失败";
return Result.error(400, msg);
}
/** 兜底:其他未预料异常。细节只写日志,不返回给前端(防信息泄漏)。 */
@ExceptionHandler(Exception.class)
public Result<?> handleOther(Exception e) {
log.error("未预期异常", e);
return Result.error(500, "服务器内部错误");
}
}
@RestControllerAdvice 让这个类全局拦截所有 Controller 抛出的异常。每种异常写一个 @ExceptionHandler 方法:
BizException→ 返回业务码 + 消息;MethodArgumentNotValidException(参数校验失败)→ 返回 400 + 校验消息;Exception(兜底)→ 返回 500。
兜底异常要脱敏
Exception 兜底分支只返回固定的"服务器内部错误",不把 e.getMessage() 返回给前端。因为未预期异常可能含 SQL 语句、文件路径、类名等内部信息,泄漏出去是安全隐患。真正细节用日志 log.error("未预期异常", e) 记录下来供排查。而 BizException 的 message 是我们自己写的、安全的,可以放心返回前端。
整体效果¶
Service 里随便抛 BizException,Controller 不用 try/catch,前端永远收到统一的 {code, message, data}。例如 TaskServiceImpl 修改任务时校验归属:
package com.javaglm.task.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.javaglm.task.common.BizException;
import com.javaglm.task.dto.TaskRequest;
import com.javaglm.task.entity.Task;
import com.javaglm.task.mapper.TaskMapper;
import com.javaglm.task.service.TaskService;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
/**
* 任务服务实现(第 28 章)。
* 继承 ServiceImpl<TaskMapper, Task>:自动拥有 CRUD,Mapper 也自动注入。
* 业务逻辑写在这里(校验归属、组装数据),Controller 保持"瘦"。
*/
@Service
public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task> implements TaskService {
@Override
public IPage<Task> pageTasks(long current, long size, Long userId, Integer status) {
LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Task::getUserId, userId);
if (status != null) {
wrapper.eq(Task::getStatus, status);
}
wrapper.orderByDesc(Task::getCreateTime);
return this.page(new Page<>(current, size), wrapper);
}
@Override
public Task createTask(TaskRequest req, Long userId) {
Task task = new Task();
task.setTitle(req.getTitle());
task.setDescription(req.getDescription());
task.setStatus(req.getStatus() == null ? 0 : req.getStatus());
task.setUserId(userId);
task.setCreateTime(LocalDateTime.now());
task.setUpdateTime(LocalDateTime.now());
this.save(task);
return task;
}
@Override
public Task updateTask(Long id, TaskRequest req, Long userId) {
Task task = this.getById(id);
if (task == null || !task.getUserId().equals(userId)) {
throw new BizException(404, "任务不存在或无权操作");
}
task.setTitle(req.getTitle());
task.setDescription(req.getDescription());
if (req.getStatus() != null) {
task.setStatus(req.getStatus());
}
task.setUpdateTime(LocalDateTime.now());
this.updateById(task);
return task;
}
@Override
public void deleteTask(Long id, Long userId) {
Task task = this.getById(id);
if (task == null || !task.getUserId().equals(userId)) {
throw new BizException(404, "任务不存在或无权操作");
}
this.removeById(id); // @TableLogic:实际执行逻辑删除
}
}
看 updateTask / deleteTask:归属不对直接 throw new BizException(404, ...),由全局处理器转成友好响应。
flowchart LR
S["Service: throw new BizException"] --> G["全局异常处理器接住"]
G --> R["返回 Result.error → JSON"]
:octicons-arrow-left-16: 上一章:参数接收与校验 | 下一章:配置管理