​
G2rain 是一个面向企业级场景的开源平台,致力于帮助企业或者中小团队完成一个通用可持续交付
的SaaS开发平台,企业可以在此平台上为客户提供SaaS业务能力。 该开源平台围绕应用化的思想,
提供了应用开发,上架SKU,交易支付,计量计费,财务记收等通用能力。 良好的支持了客户的使用,
运营的快捷交付,产研的开发效率以及销售交付团队的准确分成等功能。 使用的技术包括:

后端:Java(JDK21), Spring(Srping boot4.0), Spring cloud, Spring gateway, 
Mysql, Redis等常用组件

前端:Typescript, Vue3, qiankun(微前端), Vite5, Element plus等组件

代码位置:G2rain在Github

代码陆续发布中

​

在企业级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在性能、复用性、可读性和可维护性之间反复权衡,最终确定了“分层异常处理”方案,核心原则如下:

  1. Service层聚焦业务逻辑:出参仅为正常业务数据(如查询列表返回Vo列表),不掺杂任何异常相关字段;
  2. Controller层负责响应封装:调用Service层完成业务逻辑后,将正常结果封装为统一的Result结构返回;
  3. 异常统一捕获转换:Service层遇到非正常业务场景时,直接抛出RuntimeException(具体为自定义BusinessException),由全局拦截器捕获后,自动转换为标准化的异常Result响应。

一、异常处理核心组件概述

G2rain在com.g2rain.common.exception包中提供了一套完整的异常处理解决方案,覆盖从错误定义、异常封装到响应转换的全链路,核心组件包括:

  • 统一错误码规范(ErrorCode接口):定义错误码的核心结构,约束所有错误码的实现标准;
  • 统一异常封装(BusinessException、BaseError、FieldError):实现业务异常的精细化描述,支持全局异常与字段级异常;
  • 统一响应格式(Result类):承接正常与异常结果,为前端提供标准化的接口响应;
  • 异常转换处理器(ExceptionProcessor):实现异常到Result响应的自动转换,支撑国际化等扩展需求;
  • 国际化错误消息(ErrorMessageRegistry):管理多语言错误模板,适配全球化部署场景。

这套组件体系的核心优势在于“标准化”与“可扩展”:既避免了重复开发,又能通过接口抽象适配不同业务模块的个性化需求。

二、核心组件详细设计

2.1 错误码体系:标准化错误定义的基石

错误码是异常处理的“语言”,一套清晰的错误码体系能大幅提升问题排查效率。G2rain通过ErrorCode接口定义错误码的核心规范,所有错误码(系统级、业务级)均需实现此接口。

2.1.1 ErrorCode接口核心定义
public interface ErrorCode {
    String code();                    // 错误码(如system.40001)
    String messageTemplate();         // 错误消息模板(支持占位符,如“参数{0:paramName}不能为空”)
    String getMessage(Object... args); // 按索引参数填充消息模板
    String getMessage(Map<String, Object> params); // 按键值参数填充消息模板
}

核心设计思路:通过“错误码+消息模板+参数填充”的组合,实现错误信息的灵活定制。错误码需具备“自解释性”,消息模板支持占位符,可根据实际场景动态填充参数(如具体字段名、资源ID等)。

2.1.2 系统级错误码实现:SystemErrorCode枚举

系统级错误码(如参数无效、资源不存在、系统内部错误)是所有模块共用的基础错误码,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”具体错误。这种格式能让开发者通过错误码快速定位错误类型与所属模块。

2.2 异常封装类:精细化描述异常信息

G2rain通过三级封装实现异常信息的精细化描述:BaseError封装基础错误信息,FieldError聚焦字段级错误,BusinessException作为顶层业务异常,整合两者形成完整异常上下文。

2.2.1 BaseError:基础错误封装

存储错误码、错误信息及参数上下文,是所有错误信息的基础载体:

public class BaseError {
    private String errorCode;              // 错误码(如system.40001)
    private String errorMessage;          // 填充后的错误信息
    private Map<String, Object> keyArgs;  // 键值参数(如{paramName: username})
    private Object[] indexArgs;           // 索引参数(如["username"])

    // getter、setter及构造方法省略
}
2.2.2 FieldError:字段级错误封装

继承自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省略
}
2.2.3 BusinessException:顶层业务异常

继承自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方法省略
}

核心设计亮点:通过多构造方法覆盖不同异常场景,既支持简单的单错误码抛出,也支持多字段错误的批量抛出,满足业务开发的多样化需求。

2.3 Result类:统一响应格式的载体

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(...) 格式,无需手动处理。

三、统一错误码与异常处理落地实践

3.1 错误码定义:模块级枚举实现

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”代表用户模块),后续数字为错误码,确保错误码的唯一性与自解释性。

3.2 异常抛出:Service层的标准化用法

Service层在遇到异常场景时,直接通过BusinessException抛出异常,无需关注后续的响应封装。结合不同业务场景,常见用法如下:

@Service
public 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);
    }
}

3.3 异常捕获与转换:全局处理器的核心作用

G2rain通过Spring MVC的@ControllerAdvice实现全局异常拦截,结合ExceptionProcessor完成异常到Result响应的转换,核心代码如下:

3.3.1 ExceptionProcessor接口与实现

定义异常处理的核心逻辑,支持基础转换与国际化扩展:

// 接口定义
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;
    }
}
3.3.2 全局异常处理器(GlobalExceptionHandler)
@RestControllerAdvice
public 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";
    }
}

核心逻辑说明:

  1. 精准拦截:通过@ExceptionHandler分别拦截BusinessException(业务异常)与Exception(通用异常),确保异常不泄露到前端;
  2. 国际化支持:从请求头获取语言标识,通过ErrorMessageRegistry加载对应语言的错误模板,实现多语言适配;
  3. 扩展性强:ExceptionProcessor采用接口设计,若业务需要自定义异常处理逻辑(如特殊错误码的额外处理),只需实现接口并注入Spring容器即可。

3.4 消息解析工具:MessageResolver

支持“索引参数”与“键值参数”两种占位符解析,适配不同场景的错误消息填充,核心代码如下:

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;
    }
}

示例效果:

  • 索引参数解析:模板“用户{0:userId}不存在”+参数[10086] → 结果“用户10086不存在”;
  • 键值参数解析:模板“用户名{0:username}已存在”+参数{username: admin} → 结果“用户名admin已存在”。

四、辅助工具类:Asserts参数断言

为简化参数校验与异常抛出代码,G2rain提供Asserts工具类,封装常见的条件检查逻辑,不满足条件时自动抛出BusinessException。核心功能与用法如下:

4.1 核心功能分类

  • 对象检查:校验对象非空(如用户、订单等);
  • 条件检查:校验布尔条件为true(如年龄≥18);
  • 字符串检查:校验字符串非空、非空白(如用户名、邮箱);
  • 集合检查:校验集合非空(如用户列表、权限列表);
  • 数值检查:校验数值符合范围(如金额>0、年龄在18-100之间)。

4.2 常见用法示例

@Service
public 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)));
    }
}

核心价值:将“条件判断+异常抛出”的两行代码简化为一行,大幅减少冗余代码,提升代码可读性与开发效率。

五、异常处理类关系与核心优势总结

5.1 核心类关系梳理

  • ErrorCode接口是错误定义的标准,由SystemErrorCode(系统级)、UserErrorCode(业务级)等枚举实现;
  • BaseError与FieldError是错误信息的载体,FieldError继承自BaseError,支持字段级错误描述;
  • BusinessException整合BaseError与FieldError,是Service层抛出的标准异常;
  • ExceptionProcessor负责将BusinessException转换为Result响应,GlobalExceptionHandler提供全局拦截能力;
  • Asserts工具类基于上述组件,简化参数校验与异常抛出流程。

5.2 核心优势

  1. 标准化:统一错误码格式、异常封装、响应格式,降低跨团队协作成本;
  2. 高效率:通过Asserts工具类、静态工厂方法简化开发,减少冗余代码;
  3. 可扩展:支持国际化、自定义异常处理器,适配企业级平台的多样化需求;
  4. 易排查:错误码具备自解释性,结合参数上下文,可快速定位问题根源;
  5. 低耦合:分层处理逻辑(Service抛异常、Controller管响应、处理器做转换),职责清晰,便于维护。

对于企业级SaaS平台而言,稳定的异常处理机制是可持续交付的基础。G2rain这套异常处理规范,通过“标准化定义+分层处理+工具化简化”的设计思路,既解决了Java单返回值的核心痛点,又为平台的规模化扩展提供了支撑。后续,我们还将围绕G2rain的微服务治理、安全防护等核心能力,持续分享企业级SaaS平台的构建规范与实践经验。

代码地址:g2rain-common在Github

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐