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%以上的业务场景。
二、核心依赖
- Spring Boot Starter Web(基础Web功能)
- Lombok(简化POJO代码)
- MyBatis-Plus Starter(ORM框架,含分页插件)
- 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
}