跳转至

统一响应与全局异常处理

让所有接口返回统一格式,让所有异常集中处理——这是后端工程化的基本功。

统一响应:Result

前端最怕接口返回格式不统一(这个返回 {data},那个返回 {result})。约定一个统一结构:

{ "code": 200, "message": "success", "data": {...} }

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;
    }
}
// Service 里直接抛,不用层层 return 错误码
if (task == null) {
    throw new BizException(404, "任务不存在");
}

全局异常处理器:@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: 上一章:参数接收与校验 | 下一章:配置管理