Java项目基础架构(二)| 通用响应与异常

Java项目基础架构(二)| 通用响应与异常

  • 一、方案概述
  • 二、核心依赖
  • 三、完整代码实现
    • [1. 错误码枚举(BizErrorCodeEnum)](#1. 错误码枚举(BizErrorCodeEnum))
    • [2. 统一返回类(Result<T>)](#2. 统一返回类(Result<T>))
    • [3. 分页数据类(PageData<T>,支持MyBatis-Plus)](#3. 分页数据类(PageData<T>,支持MyBatis-Plus))
    • [4. 自定义业务异常(BusinessException)](#4. 自定义业务异常(BusinessException))
    • [5. 全局异常处理器(GlobalExceptionHandler)](#5. 全局异常处理器(GlobalExceptionHandler))
    • [6. MyBatis-Plus配置(分页插件)](#6. MyBatis-Plus配置(分页插件))
  • 四、使用示例(完整业务流程)
    • [1. 实体类(User)](#1. 实体类(User))
    • [2. Mapper层(UserMapper)](#2. Mapper层(UserMapper))
    • [3. Service层(UserService)](#3. Service层(UserService))
    • [4. Controller层(UserController)](#4. Controller层(UserController))
  • 五、接口返回示例
    • [1. 普通接口(查询用户ID=1)](#1. 普通接口(查询用户ID=1))
    • [2. 分页接口(查询第1页,每页2条,关键词"张")](#2. 分页接口(查询第1页,每页2条,关键词“张”))

一、方案概述

本方案整合了"统一返回格式""全局异常处理"和"MyBatis-Plus分页插件",解决Java项目中接口格式混乱、异常处理分散、分页逻辑重复的问题,符合大厂编码规范,适配90%以上的业务场景。

二、核心依赖

  1. Spring Boot Starter Web(基础Web功能)
  2. Lombok(简化POJO代码)
  3. MyBatis-Plus Starter(ORM框架,含分页插件)
  4. MySQL Connector(数据库驱动,根据实际数据库调整)

三、完整代码实现

1. 错误码枚举(BizErrorCodeEnum)

java 复制代码
/**
 * 全局业务错误码枚举
 * 分类:200=成功,4xx=业务错误,5xx=系统错误
 * 新增错误码只需添加枚举项,无需修改其他代码
 */
public enum BizErrorCodeEnum {
    SUCCESS(200, "操作成功"),
    BUSINESS_ERROR(400, "业务参数错误"),
    PARAM_VALID_ERROR(4001, "参数校验失败"),
    RESOURCE_NOT_FOUND(404, "资源不存在"),
    SYSTEM_ERROR(500, "系统繁忙,请稍后再试");

    private final int code;
    private final String defaultMessage;

    BizErrorCodeEnum(int code, String defaultMessage) {
        this.code = code;
        this.defaultMessage = defaultMessage;
    }

    public int getCode() {
        return code;
    }

    public String getDefaultMessage() {
        return defaultMessage;
    }
}

2. 统一返回类(Result)

java 复制代码
import lombok.Data;
import java.io.Serializable;

/**
 * 所有Controller接口统一返回格式
 * 泛型<T>支持任意数据类型(单条实体、列表、分页数据等)
 * 字段说明:
 * - code:错误码(200=成功)
 * - message:提示消息
 * - data:业务数据(失败时为null)
 * - timestamp:响应时间戳
 * - success:是否成功(前端直接判断)
 */
@Data
public class Result<T> implements Serializable {
    private static final long serialVersionUID = 1L;

    private int code;
    private String message;
    private T data;
    private Long timestamp;
    private boolean success;

    // 私有构造,强制通过静态方法创建
    private Result() {
        this.timestamp = System.currentTimeMillis();
    }

    /**
     * 成功响应(带业务数据)
     */
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(BizErrorCodeEnum.SUCCESS.getCode());
        result.setMessage(BizErrorCodeEnum.SUCCESS.getDefaultMessage());
        result.setData(data);
        result.setSuccess(true);
        return result;
    }

    /**
     * 失败响应(使用默认错误消息)
     */
    public static <T> Result<T> fail(BizErrorCodeEnum errorEnum) {
        Result<T> result = new Result<>();
        result.setCode(errorEnum.getCode());
        result.setMessage(errorEnum.getDefaultMessage());
        result.setData(null);
        result.setSuccess(false);
        return result;
    }

    /**
     * 失败响应(自定义错误消息)
     */
    public static <T> Result<T> fail(BizErrorCodeEnum errorEnum, String customMessage) {
        Result<T> result = new Result<>();
        result.setCode(errorEnum.getCode());
        result.setMessage(customMessage);
        result.setData(null);
        result.setSuccess(false);
        return result;
    }
}

3. 分页数据类(PageData,支持MyBatis-Plus)

java 复制代码
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.Data;
import java.io.Serializable;
import java.util.List;

/**
 * 分页数据封装类,作为Result的data字段使用
 * 支持两种构建方式:
 * 1. 手动传参(of(list, pageNum, pageSize, total))
 * 2. 从MyBatis-Plus的IPage对象转换(of(iPage))
 */
@Data
public class PageData<T> implements Serializable {
    private static final long serialVersionUID = 1L;

    private List<T> list; // 当前页数据列表
    private int pageNum; // 当前页码(从1开始)
    private int pageSize; // 每页大小
    private long total; // 总记录数
    private int totalPage; // 总页数(自动计算)

    private PageData() {}

    /**
     * 方式1:手动构建分页数据
     */
    public static <T> PageData<T> of(List<T> list, int pageNum, int pageSize, long total) {
        // 参数校验,避免非法值
        if (pageNum < 1) pageNum = 1;
        if (pageSize < 1) pageSize = 10;
        if (total < 0) total = 0;

        PageData<T> pageData = new PageData<>();
        pageData.setList(list);
        pageData.setPageNum(pageNum);
        pageData.setPageSize(pageSize);
        pageData.setTotal(total);
        // 计算总页数:总记录数÷每页大小,向上取整
        pageData.setTotalPage(total == 0 ? 0 : (int) Math.ceil((double) total / pageSize));
        return pageData;
    }

    /**
     * 方式2:从MyBatis-Plus的IPage对象构建(推荐)
     * 自动提取分页参数,无需手动计算total和totalPage
     */
    public static <T> PageData<T> of(IPage<T> iPage) {
        if (iPage == null) {
            return of(List.of(), 1, 10, 0); // 空分页默认值
        }
        return of(
            iPage.getRecords(), // 从IPage获取当前页数据
            (int) iPage.getCurrent(), // 当前页码(转int)
            (int) iPage.getSize(), // 每页大小(转int)
            iPage.getTotal() // 总记录数
        );
    }
}

4. 自定义业务异常(BusinessException)

java 复制代码
import lombok.Getter;

/**
 * 业务逻辑异常类,业务校验失败时抛出
 * 携带错误码枚举,便于全局异常处理器统一处理
 */
@Getter
public class BusinessException extends RuntimeException {
    private final BizErrorCodeEnum errorEnum;

    /**
     * 使用枚举默认消息
     */
    public BusinessException(BizErrorCodeEnum errorEnum) {
        super(errorEnum.getDefaultMessage());
        this.errorEnum = errorEnum;
    }

    /**
     * 自定义错误消息(覆盖枚举默认值)
     */
    public BusinessException(BizErrorCodeEnum errorEnum, String customMessage) {
        super(customMessage);
        this.errorEnum = errorEnum;
    }
}

5. 全局异常处理器(GlobalExceptionHandler)

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.stream.Collectors;

/**
 * 全局异常处理器,拦截所有@RestController的异常
 * 功能:
 * 1. 统一返回Result格式,避免Controller层写try-catch
 * 2. 区分业务异常和系统异常,记录不同级别日志
 * 3. 隐藏系统异常细节,防止敏感信息泄露
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理自定义业务异常(优先级最高)
     */
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e) {
        // 记录WARN日志,无需打印堆栈(非系统错误)
        log.warn("业务异常:{}(错误码:{})", e.getMessage(), e.getErrorEnum().getCode());
        // 返回统一失败格式,使用自定义消息(如果有)
        return Result.fail(e.getErrorEnum(), e.getMessage());
    }

    /**
     * 处理参数校验异常(如@Valid注解触发)
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidException(MethodArgumentNotValidException e) {
        // 提取所有参数错误信息,用","分隔(例:name:不能为空,age:必须≥18)
        String errorMsg = e.getBindingResult().getFieldErrors().stream()
                .map(err -> err.getField() + ":" + err.getDefaultMessage())
                .collect(Collectors.joining(","));
        // 记录WARN日志,便于定位参数问题
        log.warn("参数校验失败:{}", errorMsg);
        // 返回参数错误提示,附带详细信息
        return Result.fail(BizErrorCodeEnum.PARAM_VALID_ERROR, errorMsg);
    }

    /**
     * 兜底处理所有系统异常(如NullPointerException、SQL异常)
     */
    @ExceptionHandler(Exception.class)
    public Result<Void> handleSystemException(Exception e) {
        // 记录ERROR日志,必须打印完整堆栈(便于排查系统问题)
        log.error("系统异常", e);
        // 返回通用提示,隐藏系统异常细节
        return Result.fail(BizErrorCodeEnum.SYSTEM_ERROR);
    }
}

6. MyBatis-Plus配置(分页插件)

java 复制代码
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
// 导入动态数据源自动配置
//@Import(DynamicDataSourceAutoConfiguration.class)
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分页插件不指定固定数据库类型,会根据动态数据源自动适配
        PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor();
        // 明确指定使用 MySQL
        // paginationInterceptor.setDbType(com.baomidou.mybatisplus.annotation.DbType.MYSQL);
        // 开启溢出分页处理
        paginationInterceptor.setOverflow(true);
        interceptor.addInnerInterceptor(paginationInterceptor);
        return interceptor;
    }
}

四、使用示例(完整业务流程)

1. 实体类(User)

java 复制代码
import lombok.Data;
import java.io.Serializable;

/**
 * 用户实体类,与数据库表user对应
 */
@Data
public class User implements Serializable {
    private static final long serialVersionUID = 1L;

    private Long id; // 主键ID
    private String name; // 用户名
    private String email; // 邮箱
    private Integer age; // 年龄
}

2. Mapper层(UserMapper)

java 复制代码
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

/**
 * MyBatis-Plus Mapper接口,继承BaseMapper即可使用内置方法
 * 无需编写分页查询SQL,BaseMapper自带selectPage方法
 */
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

3. Service层(UserService)

java 复制代码
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.stereotype.Service;

/**
 * 业务层,处理用户相关逻辑
 */
@Service
public class UserService {

    private final UserMapper userMapper;

    // 构造方法注入Mapper
    public UserService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    /**
     * 根据ID查询用户(普通查询)
     * @param id 用户ID
     * @return User 查找到的用户,未找到返回null
     */
    public User getUserById(Long id) {
        // 业务校验:ID不能为空
        if (id == null) {
            throw new BusinessException(BizErrorCodeEnum.PARAM_VALID_ERROR, "用户ID不能为空");
        }
        // 使用MyBatis-Plus内置方法查询
        User user = userMapper.selectById(id);
        // 校验数据是否存在
        if (user == null) {
            throw new BusinessException(BizErrorCodeEnum.RESOURCE_NOT_FOUND, "用户不存在");
        }
        return user;
    }

    /**
     * 分页查询用户(结合MyBatis-Plus分页插件)
     * @param pageNum 当前页码
     * @param pageSize 每页大小
     * @param keyword 搜索关键词(模糊查询用户名/邮箱)
     * @return IPage<User> 分页结果(包含数据列表、总记录数等)
     */
    public IPage<User> getUserPage(int pageNum, int pageSize, String keyword) {
        // 1. 创建MyBatis-Plus Page对象,指定分页参数
        Page<User> page = new Page<>(pageNum, pageSize);

        // 2. 构建查询条件(示例:模糊查询用户名和邮箱)
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        if (keyword != null && !keyword.isEmpty()) {
            queryWrapper.like("name", keyword)
                        .or()
                        .like("email", keyword);
        }

        // 3. 调用selectPage方法,自动分页(需配置分页插件)
        return userMapper.selectPage(page, queryWrapper);
    }
}

4. Controller层(UserController)

java 复制代码
import org.springframework.web.bind.annotation.*;

/**
 * 控制层,对外提供HTTP接口
 * 所有接口统一返回Result格式,无需处理异常
 */
@RestController
@RequestMapping("/api/user")
public class UserController {

    private final UserService userService;

    // 构造方法注入Service
    public UserController(UserService userService) {
        this.userService = userService;
    }

    /**
     * 接口1:根据ID查询用户(普通接口)
     * 请求方式:GET
     * 请求路径:/api/user/1
     * 返回格式:Result<User>
     */
    @GetMapping("/{id}")
    public Result<User> getUser(@PathVariable Long id) {
        // 调用Service层,无需try-catch(异常由全局处理器处理)
        User user = userService.getUserById(id);
        // 使用Result.success()封装数据,直接返回
        return Result.success(user);
    }

    /**
     * 接口2:分页查询用户(结合MyBatis-Plus)
     * 请求方式:GET
     * 请求路径:/api/user/page?pageNum=1&pageSize=10&keyword=张
     * 返回格式:Result<PageData<User>>
     */
    @GetMapping("/page")
    public Result<PageData<User>> getUserPage(
            @RequestParam(defaultValue = "1") int pageNum, // 默认第1页
            @RequestParam(defaultValue = "10") int pageSize, // 默认每页10条
            @RequestParam(required = false) String keyword) { // 可选搜索词

        // 1. 调用Service层,获取MyBatis-Plus分页结果
        IPage<User> iPage = userService.getUserPage(pageNum, pageSize, keyword);

        // 2. 用PageData.of(iPage)快速转换,自动填充分页信息
        PageData<User> pageData = PageData.of(iPage);

        // 3. 返回统一Result格式,包含分页数据
        return Result.success(pageData);
    }
}

五、接口返回示例

1. 普通接口(查询用户ID=1)

  • 请求:GET /api/user/1
  • 成功响应:
json 复制代码
{
  "code": 200,
  "message": "操作成功",
  "data": {
    "id": 1,
    "name": "张三",
    "email": "zhangsan@xxx.com",
    "age": 25
  },
  "timestamp": 1733300000000,
  "success": true
}
  • 失败响应(ID不存在):
json 复制代码
{
  "code": 404,
  "message": "用户不存在",
  "data": null,
  "timestamp": 1733300000000,
  "success": false
}

2. 分页接口(查询第1页,每页2条,关键词"张")

  • 请求:GET /api/user/page?pageNum=1&pageSize=2&keyword=张
  • 响应:
json 复制代码
{
  "code": 200,
  "message": "操作成功",
  "data": {
    "list": [
      {
        "id": 1,
        "name": "张三",
        "email": "zhangsan@xxx.com",
        "age": 25
      },
      {
        "id": 3,
        "name": "张三丰",
        "email": "zhangsanfeng@xxx.com",
        "age": 100
      }
    ],
    "pageNum": 1,
    "pageSize": 2,
    "total": 2,
    "totalPage": 1
  },
  "timestamp": 1733300000000,
  "success": true
}
相关推荐
毕设源码-钟学长1 小时前
【开题答辩全过程】以 个人理财系统界面化设为例,包含答辩的问题和答案
java
LQxdp1 小时前
复现-[Java Puzzle #2 WP] HEAD权限绕过与字符截断CRLF
java·开发语言·漏洞复现·java 代码审计
克喵的水银蛇1 小时前
Flutter 弹性布局实战:快速掌握 Row/Column/Flex 核心用法
开发语言·javascript·flutter
sztian681 小时前
JavaScript---BOM对象、JS执行机制、location对象
开发语言·前端·javascript
CoderYanger1 小时前
动态规划算法-斐波那契数列模型:2.三步问题
开发语言·算法·leetcode·面试·职场和发展·动态规划·1024程序员节
小坏讲微服务1 小时前
SpringBoot4.0整合Scala完整使用
java·开发语言·spring boot·后端·scala·mybatis
泉城老铁1 小时前
windows服务器mysql数据库备份脚本
java·后端·mysql
神奇的板烧1 小时前
Java泛型不变性引发的类型转换问题及解决方案
java·c#