G2rain 是一个面向企业级场景的开源平台,
在企业级SaaS平台的研发中,异常处理是保障系统稳定性、可维护性的核心环节。G2rain作为面向企业级场景的SaaS开源平台,围绕认证与授权、微前端框架等核心能力构建解决方案,其Java服务层设计严格遵循统一规范。异常处理机制作为微服务体系的“安全防线”,直接影响接口可用性、问题排查效率及用户体验。本文将聚焦G2rain微服务Java体系中的异常处理与错误码设计逻辑,拆解核心组件、实现细节及落地实践。
首先明确G2rain Java服务的基础技术选型:所有Java服务均基于JDK21构建,核心原因在于JDK21引入的虚拟线程特性,可轻松支撑十万甚至百万级别的并发请求,大幅提升平台高并发处理能力。基于JDK21的选型,Spring生态组件统一采用Spring Boot 4.0.x版本(对应Spring核心版本为7.0.x),该版本对JDK21的虚拟线程及其他新特性提供了完善的适配与升级支持,为异常处理规范的落地奠定了稳定的技术基座。
众所周知,Java方法仅支持单个返回值,这就给接口设计带来了核心难题:如何同时承载“正常业务数据”与“异常信息”?若通过自定义对象封装两种结果,会导致代码冗余、可读性差;若直接抛出异常,又会破坏业务逻辑的连贯性。G2rain在性能、复用性、可读性和可维护性之间反复权衡,最终确定了“分层异常处理”方案,核心原则如下:
G2rain在 com.g2rain.common.exception 包中提供了一套完整的异常处理解决方案,覆盖从错误定义、异常封装到响应转换的全链路,核心组件包括:
这套组件体系的核心优势在于“标准化”与“可扩展”:既避免了重复开发,又能通过接口抽象适配不同业务模块的个性化需求。
![]()
错误码是异常处理的“语言”,一套清晰的错误码体系能大幅提升问题排查效率。G2rain通过 ErrorCode 接口定义错误码的核心规范,所有错误码(系统级、业务级)均需实现此接口。
public interface ErrorCode { String code(); // 错误码(如system.40001) String messageTemplate(); // 错误消息模板(支持占位符,如“参数{0:paramName}不能为空”) String getMessage(Object... args); // 按索引参数填充消息模板 String getMessage(Map<String, Object> params); // 按键值参数填充消息模板}
核心设计思路:通过“错误码+消息模板+参数填充”的组合,实现错误信息的灵活定制。错误码需具备“自解释性”,消息模板支持占位符,可根据实际场景动态填充参数(如具体字段名、资源ID等)。
系统级错误码(如参数无效、资源不存在、系统内部错误)是所有模块共用的基础错误码,G2rain通过 SystemErrorCode 枚举实现,示例如下:
public enum SystemErrorCode implements ErrorCode { PARAM_INVALID("system.40000", "参数无效"), PARAM_REQUIRED("system.40001", "参数{0:paramName}不能为空"), RESOURCE_NOT_FOUND("system.40401", "资源{0:resource}(ID:{1:id})不存在"), SYSTEM_INTERNAL_ERROR("system.50001", "系统内部错误:{0:errorDetail}");}
错误码命名规范:采用“模块前缀.状态码+序号”的格式,如“system.40001”中,“system”表示系统模块,“40001”具体错误。这种格式能让开发者通过错误码快速定位错误类型与所属模块。
G2rain通过三级封装实现异常信息的精细化描述:BaseError封装基础错误信息,FieldError聚焦字段级错误,BusinessException作为顶层业务异常,整合两者形成完整异常上下文。
存储错误码、错误信息及参数上下文,是所有错误信息的基础载体:
public class BaseError { private String errorCode; // 错误码(如system.40001) private String errorMessage; // 填充后的错误信息 private Map<String, Object> keyArgs; // 键值参数(如{paramName: username}) private Object[] indexArgs; // 索引参数(如["username"]) // getter、setter及构造方法省略}
继承自BaseError,新增 field 字段,专门用于描述参数校验失败等场景的字段级错误:
public class FieldError extends BaseError { private String field; // 错误字段名(如username、email) // 构造方法:直接关联错误码与字段名 public FieldError(String field, ErrorCode errorCode) { super.setErrorCode(errorCode.code()); super.setErrorMessage(errorCode.messageTemplate()); this.field = field; } // 带参数的构造方法(支持字段错误的参数填充) public FieldError(String field, ErrorCode errorCode, Map<String, Object> keyArgs) { super.setErrorCode(errorCode.code()); super.setErrorMessage(errorCode.getMessage(keyArgs)); super.setKeyArgs(keyArgs); this.field = field; } // getter、setter省略}
继承自RuntimeException,是Service层抛出异常的唯一类型,包含BaseError基础错误信息和FieldError字段错误列表,支持多种构造方式以适配不同场景:
public class BusinessException extends RuntimeException { private final BaseError baseError; private final List<FieldError> fieldErrors; // 场景1:仅传递错误码(无参数) public BusinessException(ErrorCode errorCode); // 场景2:传递错误码+键值参数 public BusinessException(ErrorCode errorCode, Map<String, Object> keyArgs) ; // 场景3:传递错误码+索引参数 public BusinessException(ErrorCode errorCode, Object... indexArgs) ; // 场景4:传递错误码+字段错误列表(如多字段校验失败) public BusinessException(ErrorCode errorCode, List<FieldError> fieldErrors) ; // getter方法省略}
核心设计亮点:通过多构造方法覆盖不同异常场景,既支持简单的单错误码抛出,也支持多字段错误的批量抛出,满足业务开发的多样化需求。
Result类是所有接口的统一响应格式,同时承载正常业务数据与异常信息,通过静态工厂方法简化创建流程,核心定义如下:
public class Result<T> { private int status; // 状态码:200成功,500失败(适配HTTP状态码) private String errorCode; // 错误码(status=500时非空) private String errorMessage; // 错误信息(status=500时非空) private Map<String, Object> keyArgs; // 错误参数(用于前端自定义展示) private Object[] indexArgs; // 错误参数(索引式) private List<FieldError> fieldErrors; // 字段错误列表(参数校验场景) private T data; // 正常业务数据(status=200时非空) // 静态工厂方法:成功响应(无数据) public static <T> Result<T> success(); // 静态工厂方法:成功响应(带业务数据) public static <T> Result<T> success(T data); // 静态工厂方法:异常响应(直接传递错误码和信息) public static <T> Result<T> error(String errorCode, String errorMessage, Object... args); // 静态工厂方法:异常响应(传递ErrorCode接口,自动填充信息) public static <T> Result<T> error(ErrorCode errorCode, Object... args); // getter、setter省略}
与前文“分层异常处理”原则呼应:Controller层仅需调用 Result.success(data) 封装正常结果,异常结果则由全局处理器自动转换为 Result.error(...) 格式,无需手动处理。
G2rain要求每个业务模块单独定义错误码枚举(实现ErrorCode接口),确保错误码的模块隔离性与可维护性。以用户模块为例, UserErrorCode 枚举定义如下:
public enum UserErrorCode implements ErrorCode { USER_NOT_FOUND("user.40401", "用户{0:userId}不存在"), USER_ALREADY_EXISTS("user.40901", "用户名{0:username}已存在"), PASSWORD_INVALID("user.40001", "密码不符合要求:{0:rule}");}
模块级错误码规范:前缀为模块简称(如“user”代表用户模块),后续数字为错误码,确保错误码的唯一性与自解释性。
Service层在遇到异常场景时,直接通过 BusinessException 抛出异常,无需关注后续的响应封装。结合不同业务场景,常见用法如下:
@Servicepublic class UserServiceImpl implements UserService { @Override public UserVo getUserById(Long userId) { UserPo userPo = userMapper.selectById(userId); // 场景1:简单异常(无复杂参数) if (userPo == null) { throw new BusinessException(UserErrorCode.USER_NOT_FOUND, userId); } return userConverter.toVo(userPo); } @Override public void createUser(UserDto userDto) { // 场景2:带键值参数的异常 if (userMapper.existsByUsername(userDto.getUsername())) { Map<String, Object> keyArgs = Map.of("username", userDto.getUsername()); throw new BusinessException(UserErrorCode.USER_ALREADY_EXISTS, keyArgs); } // 场景3:多字段校验失败(抛出字段错误列表) List<FieldError> fieldErrors = new ArrayList<>(); if (StringUtils.isBlank(userDto.getPassword())) { fieldErrors.add(new FieldError("password", SystemErrorCode.PARAM_REQUIRED, Map.of("paramName", "password"))); } if (!Pattern.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", userDto.getEmail())) { fieldErrors.add(new FieldError("email", SystemErrorCode.PARAM_INVALID, Map.of("paramName", "email"))); } if (!fieldErrors.isEmpty()) { throw new BusinessException(SystemErrorCode.PARAM_INVALID, fieldErrors); } // 正常业务逻辑(省略) UserPo userPo = userConverter.toPo(userDto); userMapper.insert(userPo); }}
G2rain通过Spring MVC的 @ControllerAdvice 实现全局异常拦截,结合 ExceptionProcessor 完成异常到Result响应的转换,核心代码如下:
定义异常处理的核心逻辑,支持基础转换与国际化扩展:
// 接口定义public interface ExceptionProcessor { Result<Void> process(BusinessException ex, String locale); // 默认方法:基础转换(将BusinessException转为Result) default Result<Void> toResult(BusinessException ex) { BaseError baseError = ex.getBaseError(); Result<Void> result = Result.error( baseError.getErrorCode(), baseError.getErrorMessage() ); result.setKeyArgs(baseError.getKeyArgs()); result.setIndexArgs(baseError.getIndexArgs()); result.setFieldErrors(ex.getFieldErrors()); return result; }} // 默认实现(支持国际化)public record DefaultExceptionProcessor(ErrorMessageRegistry registry) implements ExceptionProcessor { @Override public Result<Void> process(BusinessException ex, String locale) { // 1. 先执行基础转换 Result<Void> result = toResult(ex); // 2. 若存在消息注册表,执行国际化替换(适配多语言) if (registry != null) { String errorCode = result.getErrorCode(); // 从注册表中获取对应语言的消息模板 String messageTemplate = registry.getMessage(errorCode, locale); if (messageTemplate != null) { // 根据异常中的参数重新填充模板 BaseError baseError = ex.getBaseError(); String errorMessage = baseError.getKeyArgs() != null ? MessageResolver.resolveByKey(messageTemplate, baseError.getKeyArgs()) : MessageResolver.resolveByIndex(messageTemplate, baseError.getIndexArgs()); result.setErrorMessage(errorMessage); } } return result; }}
@RestControllerAdvicepublic class GlobalExceptionHandler { private final ExceptionProcessor exceptionProcessor; // 构造注入处理器(支持自定义实现替换) public GlobalExceptionHandler(ExceptionProcessor exceptionProcessor) { this.exceptionProcessor = exceptionProcessor; } // 处理业务异常(主动抛出的BusinessException) @ExceptionHandler(BusinessException.class) public Result<Void> handleBusinessException( BusinessException ex, HttpServletRequest request) { // 从请求头中获取语言标识(如Accept-Language) String locale = getLocaleFromHeader(request); return exceptionProcessor.process(ex, locale); } // 处理通用异常(未捕获的其他Exception) @ExceptionHandler(Exception.class) public Result<Void> handleException(Exception ex) { // 将通用异常转为默认的系统内部错误 BusinessException be = ExceptionConverter.findBusinessExceptionOrDefault(ex); // 默认使用中文 locale return exceptionProcessor.process(be, "zh_CN"); } // 从请求头获取语言标识的工具方法 private String getLocaleFromHeader(HttpServletRequest request) { String acceptLanguage = request.getHeader("Accept-Language"); return acceptLanguage != null ? acceptLanguage.split(",")[0] : "zh_CN"; }}
核心逻辑说明:
支持“索引参数”与“键值参数”两种占位符解析,适配不同场景的错误消息填充,核心代码如下:
public class MessageResolver { private MessageResolver() { // 私有构造,防止实例化 } /** * 匹配 {index:key} 占位符的正则模式(如{0:username}、{1:id}) */ private static final Pattern INDEX_KEY_PATTERN = Pattern.compile( "\\{(\\d+):([a-zA-Z_$][a-zA-Z0-9_$]*(?:\\.[a-zA-Z_$][a-zA-Z0-9_$]*){0,5})}" ); /** * 按占位符索引替换模板中的值(如{0}→第一个参数) */ public static String resolveByIndex(String template, Object... args) { if (args == null || args.length == 0) { return template; } return resolveTemplate(template, mr -> { try { int idx = Integer.parseInt(mr.group(1)); // 索引不越界则替换,否则保留原占位符 return idx >= 0 && idx < args.length ? Objects.toString(args[idx], "") : mr.group(0); } catch (NumberFormatException e) { // 格式异常(如{abc:username}),保留原占位符 return mr.group(0); } }); } /** * 按占位符key替换模板中的值(如{username}→key为username的参数值) */ public static String resolveByKey(String template, Map<String, Object> params) { if (params == null || params.isEmpty()) { return template; } return resolveTemplate(template, mr -> { String key = mr.group(2); Object value = params.get(key); // 存在key则替换,否则保留原占位符 return value != null ? String.valueOf(value) : mr.group(0); }); } /** * 核心模板解析方法(通过Function灵活定义替换逻辑) */ private static String resolveTemplate(String template, Function<MatchResult, String> replacer) { return Strings.isNotBlank(template) ? INDEX_KEY_PATTERN.matcher(template).replaceAll(replacer) : template; }}
示例效果:
为简化参数校验与异常抛出代码,G2rain提供 Asserts 工具类,封装常见的条件检查逻辑,不满足条件时自动抛出BusinessException。核心功能与用法如下:
@Servicepublic class OrderServiceImpl implements OrderService { @Override public OrderVo createOrder(OrderDto orderDto) { // 1. 对象检查:订单DTO非空 Asserts.notNull(orderDto, SystemErrorCode.PARAM_REQUIRED, "orderDto"); // 2. 字符串检查:订单号非空且非空白 Asserts.notBlank(orderDto.getOrderNo(), SystemErrorCode.PARAM_REQUIRED, "orderNo"); // 3. 数值检查:金额大于0 Asserts.greaterThan(orderDto.getAmount(), 0, OrderErrorCode.INVALID_AMOUNT, "amount", orderDto.getAmount()); // 4. 条件检查:用户已登录(假设getLoginUserId()返回null表示未登录) Long userId = getLoginUserId(); Asserts.isTrue(userId != null, UserErrorCode.USER_NOT_LOGIN); // 5. 集合检查:商品列表非空 Asserts.notEmpty(orderDto.getProductIds(), SystemErrorCode.PARAM_REQUIRED, "productIds"); // 正常业务逻辑(省略) return orderConverter.toVo(orderMapper.insert(orderConverter.toPo(orderDto))); }}
核心价值:将“条件判断+异常抛出”的两行代码简化为一行,大幅减少冗余代码,提升代码可读性与开发效率。
对于企业级SaaS平台而言,稳定的异常处理机制是可持续交付的基础。G2rain这套异常处理规范,通过“标准化定义+分层处理+工具化简化”的设计思路,既解决了Java单返回值的核心痛点,又为平台的规模化扩展提供了支撑。后续,我们还将围绕G2rain的微服务治理、安全防护等核心能力,持续分享企业级SaaS平台的构建规范与实践经验。
代码地址: g2rain-common github仓库