简介

模块采用 spring boot 下 spring mvc 的 ErrorController 或者 spring webflux 的 ErrorWebExceptionHandler ,进行的包装处理。

响应体结构

所有错误处理最终返回 ResponseVo 对象,结构如下:

{
    "success": false,
    "error": {
        "code": 100109,
        "message": "验证码错误",
        "longMessage": null,
        "source": null,
        "details": null
    },
    "data": null,
    "requestId": null
}

ErrorResponseVo 字段说明:

字段

说明

code

应用级错误码

message

错误消息

longMessage

错误消息扩展版本(可选)

source

错误来源信息(可选)

details

错误详情列表,包含字段级错误信息

ErrorDetail 字段说明:

字段

说明

field

字段名称(可选)

code

错误码

message

错误消息

仓库坐标

Maven

<dependency>
    <groupId>com.luamas.thinkbridge</groupId>
    <artifactId>error-spring-boot-starter</artifactId>
</dependency>

Gradle

implementation("com.luamas.thinkbridge:error-spring-boot-starter")

错误码定义

ErrorCode

实现 com.luamas.thinkbridge.common.base.exception.ErrorCode 接口

ErrorCode 接口定义了以下方法:

方法

说明

getValue()

获取错误码数值

getName()

获取错误码名称(用于国际化消息查找的key)

getInfo()

获取错误码描述信息

所有字段名需以 public static final 修饰,如:public static final ResultCode OK = new ResultCode(200, "base.OK", "OK");
package com.luamas.thinkbridge.common.base.exception;

/**
 * 基础错误类型,各个模块需要实现此接口,要求所有的字段名,以`public static final`修饰,如:<br/>
 * public static final BaseResultCode OK = new BaseResultCode(200, "base.OK", "OK");<br/>
 * public static final BaseResultCode UNAUTHORIZED = new BaseResultCode(401, "base.UNAUTHORIZED", "未授权: {0}");<br/>
 *
 * @author luamas
 */
public interface ErrorCode {
    int getValue();

    String getName();

    String getInfo();
}

示例

package com.luamas.thinkbridge.integration.demo;

import com.luamas.thinkbridge.common.auto.processor.AutoErrorCode;
import com.luamas.thinkbridge.common.base.exception.ErrorCode;

/**
 * 错误码范围100000~200000
 *
 * @author luamas
 */
@AutoErrorCode
public class ResultCode implements ErrorCode {
    public static final ResultCode CAPTCHA_ERROR = new ResultCode(100109, "integrationDemo.CAPTCHA_ERROR", "info");
    public static final ResultCode USER_NOT_EXIST = new ResultCode(100105, "【{0}】原生描述", "info");
    public static final ResultCode ERROR_DETAILS = new ResultCode(100106, "多个错误", "info");

    public final int value;
    private final String name;
    private final String info;

    private ResultCode(int value, String name, String info) {
        this.value = value;
        this.name = name;
        this.info = info;
    }

    @Override
    public int getValue() {
        return value;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getInfo() {
        return info;
    }
}

@AutoErrorCode 注解

使用 @AutoErrorCode 注解标注错误码类后,配合 thinkbridge-common-auto-processor 注解处理器,会在 class 路径下生成 META-INF/thinkbridge/error_code.factories 文件,用于收集所有错误码。

@AutoErrorCode
public class ResultCode implements ErrorCode {
    public static final ResultCode CAPTCHA_ERROR = new ResultCode(100109, "integrationDemo.CAPTCHA_ERROR", "info");
    public static final ResultCode USER_NOT_EXIST = new ResultCode(100105, "integrationDemo.USER_NOT_EXIST", "info");
}

获取所有错误码

Gradle 配置

这里必须使用thinkbridge的注解处理器,否则无法从 ErrorCodeService 获取到错误码
annotationProcessor("com.luamas.thinkbridge:thinkbridge-common-auto-processor")
compileOnly("com.luamas.thinkbridge:thinkbridge-common-auto-processor")

示例

定义错误码,并实现 ErrorCode 接口

package com.luamas.thinkbridge.integration.demo;

import com.luamas.thinkbridge.common.auto.processor.AutoErrorCode;
import com.luamas.thinkbridge.common.base.exception.ErrorCode;

/**
 * 错误码范围100000~200000
 *
 * @author luamas
 */
@AutoErrorCode
public class ResultCode implements ErrorCode {
    public static final ResultCode CAPTCHA_ERROR = new ResultCode(100109, "integrationDemo.CAPTCHA_ERROR", "info");
    public static final ResultCode USER_NOT_EXIST = new ResultCode(100105, "【{0}】原生描述", "info");
    public static final ResultCode ERROR_DETAILS = new ResultCode(100106, "多个错误", "info");

    public final int value;
    private final String name;
    private final String info;

    private ResultCode(int value, String name, String info) {
        this.value = value;
        this.name = name;
        this.info = info;
    }

    @Override
    public int getValue() {
        return value;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getInfo() {
        return info;
    }
}
所有实现 ErrorCode 接口,并被 @AutoErrorCode 进行注解,将会在class路径下产生 META-INF/thinkbridge/error_code.factories 文件来描述所有子类。

使用 ErrorCodeService 获取错误码

package com.luamas.thinkbridge.integration.demo.errorprocess;

import com.luamas.thinkbridge.common.base.exception.ErrorCode;
import com.luamas.thinkbridge.common.error.ErrorCodeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Locale;
import java.util.Map;

@RestController
@Tag(name = "错误码查询", description = "查询当前模块定义的所有错误码及国际化消息")
@RequestMapping("errorCode")
@RequiredArgsConstructor
public class ErrorCodeController {
    private final ErrorCodeService errorCodeService;

    @Operation(summary = "获取所有错误码", description = "按语言获取模块内注册的所有错误码定义")
    @Parameter(name = "Accept-Language", description = "en或者zh_CN,默认使用浏览器自带的语言", in = ParameterIn.HEADER)
    @GetMapping
    public Map<String, List<ErrorCode>> test(Locale locale) {
        return errorCodeService.getI18nErrorCodeObjects(locale);
    }
}

错误消息国际化

错误处理支持消息国际化,默认会查找 i18n/error_message.properties 文件。

工作原理

  1. ErrorCode 实现类的 getMsgCode() 返回消息码

  2. 错误处理模块通过 I18nMessageSourceFactory 查找对应的消息

  3. 消息文件路径为 i18n/error_message 前缀的 properties 文件

  4. 支持 {0}, {1} 等占位符,通过 ErrorCode.getArgs() 传入参数

错误码定义

package com.luamas.thinkbridge.integration.demo;

import com.luamas.thinkbridge.common.auto.processor.AutoErrorCode;
import com.luamas.thinkbridge.common.base.exception.ErrorCode;

/**
 * 错误码范围100000~200000
 *
 * @author luamas
 */
@AutoErrorCode
public class ResultCode implements ErrorCode {
    public static final ResultCode CAPTCHA_ERROR = new ResultCode(100109, "integrationDemo.CAPTCHA_ERROR", "info");
    public static final ResultCode USER_NOT_EXIST = new ResultCode(100105, "【{0}】原生描述", "info");
    public static final ResultCode ERROR_DETAILS = new ResultCode(100106, "多个错误", "info");

    public final int value;
    private final String name;
    private final String info;

    private ResultCode(int value, String name, String info) {
        this.value = value;
        this.name = name;
        this.info = info;
    }

    @Override
    public int getValue() {
        return value;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getInfo() {
        return info;
    }
}

属性配置

error_message.properties(默认)

integrationDemo.CAPTCHA_ERROR=\u9A8C\u8BC1\u7801\u9519\u8BEF\uFF0C{0}
integrationDemo.CAPTCHA_ERROR=验证码错误,{0}

error_message_en.properties(英文)

integrationDemo.CAPTCHA_ERROR=captcha error\uFF0C{0}

error_message_zh_CN.properties(中文)

integrationDemo.CAPTCHA_ERROR=\u9A8C\u8BC1\u7801\u9519\u8BEF\uFF0C{0}

消息查找规则

查找键

说明

{msgCode}

优先使用 ErrorCode.getMsgCode() 的完整值

error.NOTFOUND

页面未找到的默认消息

error.ERROR

通用错误的默认消息

error.BEAUTIFY_MESSAGE

未知异常的美化消息

控制器示例

package com.luamas.thinkbridge.integration.demo.errorprocess;

import com.luamas.thinkbridge.common.base.exception.BusinessException;
import com.luamas.thinkbridge.common.base.vo.ErrorDetail;
import com.luamas.thinkbridge.common.base.vo.ResponseVo;
import com.luamas.thinkbridge.integration.demo.ResultCode;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@RestController
@Tag(name = "统一错误处理")
@RequestMapping("errorprocess")
public class ErrorProcessController {

    @GetMapping("test1")
    @ResponseBody
    @Parameter(name = "Accept-Language", description = "en或者zh_CN,默认使用浏览器自带的语言", in = ParameterIn.HEADER)
    @Operation(summary = "国际化错误信息")
    public ResponseVo<String> test1(@RequestParam(value = "userId", required = false) String userId) {
        throw BusinessException.of(ResultCode.CAPTCHA_ERROR, userId);
    }

    @GetMapping("test2")
    @ResponseBody
    @Parameter(name = "Accept-Language", description = "en或者zh_CN,默认使用浏览器自带的语言", in = ParameterIn.HEADER)
    @Operation(summary = "非国际化信息,全部统一返回中文信息,不建议")
    public ResponseVo<String> test2(@RequestParam(value = "userId", required = false) String userId) {
        throw BusinessException.of(ResultCode.USER_NOT_EXIST, userId);
    }

    @GetMapping("test3")
    @ResponseBody
    @Operation(summary = "未知异常,这里会触发一个告警")
    public ResponseVo<String> test3() {
        throw new RuntimeException("未捕获异常,会抛出一个默认值");
    }

    @PostMapping("test4")
    @ResponseBody
    @Operation(summary = "对象校验")
    public ResponseVo<String> test4(@Valid User user) {
        return ResponseVo.success();
    }

    @GetMapping("test5")
    @ResponseBody
    @Operation(summary = "自定义多错误异常")
    public ResponseVo<String> test5() {
        List<ErrorDetail> errorDetails = new ArrayList<>();
        errorDetails.add(ErrorDetail.of(1, "发生错误1"));
        errorDetails.add(ErrorDetail.of(2, "发生错误2"));
        errorDetails.add(ErrorDetail.of(3, "发生错误3"));
        throw BusinessException.ofDetails(ResultCode.ERROR_DETAILS, errorDetails);
    }


    @Getter
    @Setter
    static class User {
        @NotBlank
        private String username;
    }
}
消息码的命名建议使用 模块名.错误名 的格式,如 integrationDemo.CAPTCHA_ERROR,避免不同模块间的消息码冲突。

未知错误处理

当异常没有被任何 ErrorExceptionHandler 处理时,将进入未知错误处理流程。

默认输出为堆栈信息,生产环境中可能暴露敏感信息。

自定义处理

实现 com.luamas.thinkbridge.common.error.UnknownErrorResponseHandler 接口,将未知异常消息设置为固定信息,避免展示堆栈信息:

package com.luamas.thinkbridge.integration.demo.errorprocess;

import com.luamas.thinkbridge.common.error.ErrorResponseHandler;
import com.luamas.thinkbridge.common.error.ErrorResult;
import com.luamas.thinkbridge.common.error.UnknownErrorResponseHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Locale;

@Profile("errorprocess")
//@Component
@RequiredArgsConstructor
public class MyUnknownErrorResponseHandler implements UnknownErrorResponseHandler {
    private final ErrorResponseHandler<?> errorResponseHandler;
    @Override
    public ErrorResult<?> handle(Throwable throwable, HttpStatus status, Locale locale) {
        // 定义一个统一的异常码
        int code = 50000000;
        return errorResponseHandler.error("未知异常,debug可以打印此异常", null, code, status.value(), null, new HashMap<>(), locale);
    }
}

UnknownErrorResponseHandler 接口

方法

说明

handle(Throwable, HttpStatus, Locale)

处理未知异常,返回 ErrorResult 或 null

如果 handle() 返回 null,将使用默认的未知异常处理逻辑。建议在生产环境启用 unknownEnableBeautifyMessage=true 配置,配合自定义处理器使用。

自定义异常

如果不想用ThinkBridge定义好的 com.luamas.thinkbridge.common.base.exception 下的异常 ,只需要继承 com.luamas.thinkbridge.common.base.exception.AbstractException 并实现 com.luamas.thinkbridge.common.error.ErrorExceptionHandler。

ThinkBridgeException 接口定义了以下方法:getCode()、getMsgCode()、getArgs()、getMessage()、getHttpStatus()、getData()、getDetails()、getExtend()、isAlarmEnabled()。自定义异常时需确保正确实现这些方法。

ErrorExceptionHandler 接口

ErrorExceptionHandler 接口方法如下

方法

说明

supports(Throwable)

判断是否支持处理该异常

handle(Throwable, HttpStatus, Locale)

处理异常并返回 ErrorResult

package com.luamas.thinkbridge.common.error;

import org.springframework.http.HttpStatus;

import java.util.Locale;

public interface ErrorExceptionHandler {
    int DEFAULT_ERROR_CODE = 1500;
    /
     * 是否支持
     *
     * @param exception
     * @return
     */
    boolean supports(Throwable exception);

    /
     * 进行处理
     *
     * @param exception
     * @param status
     * @return
     */
    ErrorResult<?> handle(Throwable exception, HttpStatus status, Locale locale);
}

BaseErrorExceptionHandler 默认实现

BaseErrorExceptionHandler 是默认的异常处理器,处理流程如下:

  1. 判断异常是否为 TransmitException,如果是则直接透传原始响应

  2. 判断异常是否为 ThinkBridgeException,提取 code、msgCode、data、details、extend 等信息

  3. 将 details 放入 extend 中(key 为 THINK-BRIDGE-ERROR-DETAILS)

  4. 调用 ErrorResponseHandler 生成最终错误响应

package com.luamas.thinkbridge.common.error;

import com.luamas.thinkbridge.common.base.exception.ThinkBridgeException;
import com.luamas.thinkbridge.common.base.exception.TransmitException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

@RequiredArgsConstructor
public class BaseErrorExceptionHandler implements ErrorExceptionHandler {
    private final int defaultCode;
    private final ErrorResponseHandler<?> errorResponseHandler;

    @Override
    public boolean supports(Throwable exception) {
        return exception instanceof ThinkBridgeException;
    }

    @Override
    public ErrorResult<?> handle(Throwable exception, HttpStatus status, Locale locale) {
        if (exception instanceof TransmitException transmitException) {
            return new ErrorResult<>(transmitException, HttpStatus.resolve(transmitException.getHttpStatus()));
        }

        if (exception instanceof ThinkBridgeException thinkBridgeException) {
            return handleThinkBridgeException(thinkBridgeException, status, locale);
        }

        return null;
    }

    private ErrorResult<?> handleThinkBridgeException(ThinkBridgeException ex, HttpStatus status, Locale locale) {
        int httpStatus = ex.getHttpStatus();
        int code = ex.getCode() != 0 ? ex.getCode() : defaultCode;
        String message = ex.getMsgCode();
        Object data = ex.getData();
        Map<String, Object> extend = new HashMap<>(ex.getExtend());

        if (!ex.getDetails().isEmpty()) {
            extend.put(ErrorConst.ERROR_DETAILS, ex.getDetails());
        }

        return errorResponseHandler.error(message, ex.getArgs(), code, httpStatus, data, extend, locale);
    }
}

自定义异常示例

// 1. 自定义异常类
public class MyException extends AbstractException {
    protected MyException(int code, String msgCode, Object... args) {
        super(code, msgCode, args);
    }

    public static MyException of(ErrorCode errorCode, Object... args) {
        return new MyException(errorCode.getValue(), errorCode.getName(), args);
    }
}

// 2. 自定义异常处理器
@Component
public class MyExceptionHandler implements ErrorExceptionHandler {
    @Override
    public boolean supports(Throwable exception) {
        return exception instanceof MyException;
    }

    @Override
    public ErrorResult<?> handle(Throwable exception, HttpStatus status, Locale locale) {
        MyException ex = (MyException) exception;
        // 自定义处理逻辑
        return new ErrorResult<>(ResponseVo.error(ex.getCode(), ex.getMessage()), HttpStatus.resolve(ex.getHttpStatus()));
    }
}

自定义返回体

如果不想用ThinkBridge定义好的 com.luamas.thinkbridge.common.base.vo.ResponseVo ,只需要实现 com.luamas.thinkbridge.common.error.ErrorResponseHandler 泛型接口即可。

ErrorResponseHandler 接口

ErrorResponseHandler 接口方法如下

方法

说明

success(Object data)

构建成功响应

error(message, args, errorCode, statusCode, data, extend, locale)

构建错误响应(推荐使用)

旧版 error(String message, Object[] args, Integer errorCode, Integer statusCode, Object data, Locale locale) 方法已标记为 @Deprecated,请使用带 extend 参数的新方法。
package com.luamas.thinkbridge.common.error;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public interface ErrorResponseHandler<T> {
    ErrorResult<T> success(Object data);

    /
     * 返回ResponseVoResponseEntity
     *
     * @param message    消息
     * @param args       消息参数
     * @param errorCode  错误码
     * @param statusCode 状态码
     * @param data       数据,默认可以为空
     * @param locale 本地化
     * @return
     */
    @Deprecated
    default ErrorResult<T> error(String message, Object[] args, Integer errorCode, Integer statusCode, Object data, Locale locale) {
        return error(message, args, errorCode, statusCode, data, new HashMap<>(), locale);
    }

    /
     * 返回ResponseVoResponseEntity
     *
     * @param message    消息
     * @param args       消息参数
     * @param errorCode  错误码
     * @param statusCode 状态码
     * @param data       数据,默认可以为空
     * @param locale 本地化
     * @return
     */
    ErrorResult<T> error(String message, Object[] args, Integer errorCode, Integer statusCode, Object data, Map<String, Object> extend, Locale locale);
}

ThinkBridgeErrorResponseHandler 默认实现

默认实现 ThinkBridgeErrorResponseHandler 的处理逻辑:

  1. 通过 ErrorMessageSourceUtils 对消息进行国际化处理

  2. 如果 compatibility 为 true,对无法国际化的消息尝试使用 String.format 格式化

  3. 从 extend 中提取 details 错误详情列表

  4. 构建 ResponseVo.error() 响应体

  5. 如果 HTTP 状态码为 500,自动改为 200(避免触发前端异常处理)

package com.luamas.thinkbridge.common.error;

import com.luamas.thinkbridge.common.base.vo.ErrorDetail;
import com.luamas.thinkbridge.common.base.vo.ResponseVo;
import com.luamas.thinkbridge.common.spring.utils.ErrorMessageSourceUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;

import java.util.List;
import java.util.Locale;
import java.util.Map;

@Slf4j
@RequiredArgsConstructor
public class ThinkBridgeErrorResponseHandler implements ErrorResponseHandler<ResponseVo<?>> {
    /
     * 是否兼容处理
     */
    private final boolean compatibility;

    @Override
    public ErrorResult<ResponseVo<?>> success(Object data) {
        ResponseVo<?> responseVo = ResponseVo.success(data);
        return new ErrorResult<>(responseVo, HttpStatus.OK);
    }

    /
     * 返回ResponseVoResponseEntity
     *
     * @param message    消息
     * @param args       消息参数
     * @param errorCode  错误码
     * @param statusCode 状态码
     * @param data       数据,默认可以为空
     * @return
     */
    public ErrorResult<ResponseVo<?>> error(String message, Object[] args, Integer errorCode, Integer statusCode, Object data, Map<String, Object> extend, Locale locale) {
        HttpStatus status;
        ResponseVo<?> responseVo;
        try {
            message = ErrorMessageSourceUtils.getMessage(message, locale, args);
        } catch (Exception ex) {
            if (compatibility) {
                if (message.contains("%s")) {
                    try {
                        message = String.format(message, args);
                    } catch (Exception e) {
                        log.warn("Failed to format error message with String.format: {}", message, e);
                    }
                }
            }
        }
        if (compatibility) {
            if (message.contains("%s")) {
                try {
                    message = String.format(message, args);
                } catch (Exception e) {
                    log.warn("Failed to format error message with String.format: {}", message, e);
                }
            }
        }
        List<ErrorDetail> details = null;
        if (extend.containsKey(ErrorConst.ERROR_DETAILS)) {
            details = (List<ErrorDetail>) extend.get(ErrorConst.ERROR_DETAILS);
        }
        responseVo = ResponseVo.error(errorCode, message, data, details);
        if (500 == statusCode) {
            statusCode = 200;
        }
        status = HttpStatus.valueOf(statusCode);

        return new ErrorResult<>(responseVo, status);
    }
}

自定义返回体示例

@Component
public class MyErrorResponseHandler implements ErrorResponseHandler<MyResponseVo> {

    @Override
    public ErrorResult<MyResponseVo> success(Object data) {
        return new ErrorResult<>(MyResponseVo.success(data), HttpStatus.OK);
    }

    @Override
    public ErrorResult<MyResponseVo> error(String message, Object[] args, Integer errorCode, Integer statusCode, Object data, Map<String, Object> extend, Locale locale) {
        MyResponseVo responseVo = MyResponseVo.error(errorCode, message);
        return new ErrorResult<>(responseVo, HttpStatus.valueOf(statusCode));
    }
}

自定义默认错误码

如果不想用ThinkBridge定义好的 com.luamas.thinkbridge.common.error.DefaultExceptionConfigUtils 默认的异常错误码规则

ConfigExceptionClassData 属性

属性

说明

status

HTTP 状态码,不配置则使用传递来的 status

code

错误码,为空则使用全局默认错误码

message

错误消息,为空则默认使用 exception 的消息

extend

扩展数据,传递给下层使用,默认空

默认异常映射表

DefaultExceptionConfigUtils 内置了以下异常映射:

Security 相关

异常类

HTTP 状态码

错误码

AuthenticationException

401

1200

AccessDeniedException

403

1205

OAuth2AuthenticationException

401

1403

Spring MVC 相关

异常类

HTTP 状态码

错误码

HttpRequestMethodNotSupportedException

-

1001

HttpMediaTypeNotSupportedException

-

1003

HttpMediaTypeNotAcceptableException

-

1004

MissingPathVariableException

-

1005

MissingServletRequestParameterException

-

1006

ServletRequestBindingException

-

1007

ConversionNotSupportedException

-

1008

TypeMismatchException

-

1009

HttpMessageNotReadableException

-

1010

HttpMessageNotWritableException

-

1011

MethodArgumentNotValidException

200

1012

MissingServletRequestPartException

-

1013

BindException

200

1014

NoHandlerFoundException

404

1015

NoResourceFoundException

404

1016

DefaultExceptionConfigUtils 类如下

package com.luamas.thinkbridge.common.error;

import java.util.HashMap;
import java.util.Map;

/**
 * @author : luamas
 * @date : 2023/1/6 15:35
 */
public class DefaultExceptionConfigUtils {

    public static Map<String, ConfigExceptionClassData> getConfigExceptions() {
        Map<String, ConfigExceptionClassData> map = new HashMap<>();
        // security
        map.put("org.springframework.security.core.AuthenticationException", new ConfigExceptionClassData(401, 1200, null, new HashMap<>()));
        map.put("org.springframework.security.access.AccessDeniedException", new ConfigExceptionClassData(403, 1205, null, new HashMap<>()));
        // oauth2
        map.put("org.springframework.security.oauth2.core.OAuth2AuthenticationException", new ConfigExceptionClassData(401, 1403, null, new HashMap<>()));

        // spring
        map.put("org.springframework.web.HttpRequestMethodNotSupportedException", new ConfigExceptionClassData(null, 1001, null, new HashMap<>()));
        map.put("org.springframework.web.HttpMediaTypeNotSupportedException", new ConfigExceptionClassData(null, 1003, null, new HashMap<>()));
        map.put("org.springframework.web.HttpMediaTypeNotAcceptableException", new ConfigExceptionClassData(null, 1004, null, new HashMap<>()));
        map.put("org.springframework.web.bind.MissingPathVariableException", new ConfigExceptionClassData(null, 1005, null, new HashMap<>()));
        map.put("org.springframework.web.bind.MissingServletRequestParameterException", new ConfigExceptionClassData(null, 1006, null, new HashMap<>()));
        map.put("org.springframework.web.bind.ServletRequestBindingException", new ConfigExceptionClassData(null, 1007, null, new HashMap<>()));
        map.put("org.springframework.beans.ConversionNotSupportedException", new ConfigExceptionClassData(null, 1008, null, new HashMap<>()));
        map.put("org.springframework.beans.TypeMismatchException", new ConfigExceptionClassData(null, 1009, null, new HashMap<>()));
        map.put("org.springframework.http.converter.HttpMessageNotReadableException", new ConfigExceptionClassData(null, 1010, null, new HashMap<>()));
        map.put("org.springframework.http.converter.HttpMessageNotWritableException", new ConfigExceptionClassData(null, 1011, null, new HashMap<>()));
        map.put("org.springframework.web.bind.MethodArgumentNotValidException", new ConfigExceptionClassData(200, 1012, "error.FAILVERIFY", new HashMap<>()));
        map.put("org.springframework.web.multipart.support.MissingServletRequestPartException", new ConfigExceptionClassData(null, 1013, null, new HashMap<>()));
        map.put("org.springframework.validation.BindException", new ConfigExceptionClassData(200, 1014, "error.FAILVERIFY", new HashMap<>()));




        // spring mvc
        map.put("org.springframework.web.servlet.NoHandlerFoundException", new ConfigExceptionClassData(404, 1015, null, new HashMap<>()));
        map.put("org.springframework.web.servlet.resource.NoResourceFoundException", new ConfigExceptionClassData(404, 1016, null, new HashMap<>()));
        return map;
    }
}

替换默认规则

使用 default-config-exceptions 会完全覆盖默认配置,使用 custom-config-exceptions 则是在默认配置基础上追加。
# 覆盖默认配置
think:
  error:
    default-config-exceptions:
      org.springframework.security.core.AuthenticationException:
          status: 401
          code: 1200
# 追加自定义配置(不影响默认配置)
think:
  error:
    custom-config-exceptions:
      java.sql.SQLException:
          status: 500
          code: 1500
          message: "数据库错误"

自定义错误码

如果项目中需要自己捕获一个异常,并返回统一的错误信息,可以通过配置异常映射来自定义错误码。

配置方式

defaultConfigExceptions(完全覆盖)

完全替换默认的异常映射配置:

think:
  error:
    default-config-exceptions:
      java.sql.SQLException:
        status: 500
        code: 1500
        message: "数据库错误"
使用 default-config-exceptions 会完全覆盖内置的默认异常映射,请确保包含所有需要的映射。

customConfigExceptions(追加配置)

在默认异常映射基础上追加自定义映射:

think:
  error:
    custom-config-exceptions:
      java.sql.SQLException:
        status: 500
        code: 1500
        message: "数据库错误"
      com.myapp.MyCustomException:
        status: 400
        code: 2001
        message: "自定义业务错误"
推荐使用 custom-config-exceptions,它不会覆盖默认配置,只在默认基础上追加。

映射属性

属性

说明

status

HTTP 状态码

code

应用级错误码

message

错误消息,支持国际化配置

未知异常配置

对未知异常可以进行以下配置

配置属性

属性

默认值

说明

unknownEnableBeautifyMessage

false

未知异常是否启用美化消息,生产建议启用

unknownCode

500

未知异常的错误码

unknownBeautifyMessage

error.BEAUTIFY_MESSAGE

未知异常的美化消息,支持国际化配置

defaultCode

1500

默认错误码

notfound

1504

页面未找到的错误码

defaultConfigExceptions

(内置)

默认异常映射配置,完全覆盖

customConfigExceptions

空

自定义异常映射配置,追加到默认配置

package com.luamas.thinkbridge.bootstarter.error.configuration;

import com.luamas.thinkbridge.common.error.ConfigExceptionClassData;
import com.luamas.thinkbridge.common.error.DefaultExceptionConfigUtils;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.HashMap;
import java.util.Map;

/
 * @author : luamas
 * @date : 2023/1/6 14:51
 */
@ConfigurationProperties(prefix = "think.error")
@Getter
@Setter
public class ThinkBridgeErrorProperties {
    /
     * 未知异常启用美化消息,生产建议启用,默认打印堆栈异常信息
     */
    private boolean unknownEnableBeautifyMessage = false;
    /
     * 未知异常的错误码,默认为500
     */
    private int unknownCode = 500;
    /
     * 未知异常的美化消息,支持国际化配置
     */
    private String unknownBeautifyMessage = "error.BEAUTIFY_MESSAGE";
    /
     * 默认错误码,默认为1500
     */
    private int defaultCode = 1500;
    /
     * 页面未找到的错误码,默认为1504
     */
    private int notfound = 1504;
    /
     * 异常相关错误码等配置
     */
    private Map<String, ConfigExceptionClassData> defaultConfigExceptions = DefaultExceptionConfigUtils.getConfigExceptions();
    /
     * 异常相关错误码等配置
     */
    private Map<String, ConfigExceptionClassData> customConfigExceptions = new HashMap<>();

    public Map<String, ConfigExceptionClassData> getConfigExceptions() {
        Map<String, ConfigExceptionClassData> map = new HashMap<>();
        map.putAll(defaultConfigExceptions);
        map.putAll(customConfigExceptions);
        return map;
    }
}

配置示例

# 下面演示了对所有SQL错误的拦截
think:
  error:
    unknown-enable-beautify-message: false # 未知异常启用美化消息,生产建议启用,默认打印堆栈异常信息
    unknown-code: 500 # 未知异常的错误码,默认为500
    unknown-beautify-message: "error.BEAUTIFY_MESSAGE" # 未知异常的美化消息,支持国际化配置,默认:"error.BEAUTIFY_MESSAGE"
    default-code: 1500 # 默认错误码,默认为1500
    notfound: 1504 # 页面未找到的错误码,默认为1504
think:
  error:
    # 启用未知异常美化消息
    unknown-enable-beautify-message: true
    # 未知异常错误码
    unknown-code: 500
    # 默认错误码
    default-code: 1500
    # 页面未找到错误码
    notfound: 1504

异常使用

Thinkbridge中大部分异常全部实现了 com.luamas.thinkbridge.common.base.exception.ThinkBridgeException 接口。

ThinkBridgeException 接口定义了以下方法:

方法

说明

getCode()

异常码

getMsgCode()

异常消息码(用于国际化查找)

getArgs()

异常消息参数

getMessage()

异常消息(已国际化处理)

getHttpStatus()

HTTP 状态码,默认 200

getData()

业务数据,默认 null

getDetails()

错误详情列表,默认空列表

getExtend()

扩展数据,默认空 Map

isAlarmEnabled()

是否触发告警,默认 true

目前包含了以下常用异常:

BusinessException

com.luamas.thinkbridge.common.base.exception.BusinessException

统一的业务异常类,通过链式 API 控制行为,替代了旧版多个分散的异常类。

基本用法

// 基本业务异常
throw BusinessException.of(ResultCode.CAPTCHA_ERROR, userId);

// 带业务数据
throw BusinessException.ofData(ResultCode.XXX, dataObj);

// 带错误详情列表
throw BusinessException.ofDetails(ResultCode.ERROR_DETAILS, errorDetails);

链式配置

// 关闭告警
throw BusinessException.of(ResultCode.XXX, args).noAlarm();

// 自定义 HTTP 状态码
throw BusinessException.of(ResultCode.XXX, args).httpStatus(400);

// 设置扩展数据
throw BusinessException.of(ResultCode.XXX, args)
    .extend(Map.of("key1", "value1"));

// 添加单条扩展数据
throw BusinessException.of(ResultCode.XXX, args)
    .putExtend("key1", "value1");

// 组合使用
throw BusinessException.of(ResultCode.XXX, args)
    .noAlarm()
    .httpStatus(400)
    .putExtend("key1", "value1");

属性说明

属性

说明

httpStatus

HTTP 状态码,默认 200

data

业务数据,通过 ofData() 设置

details

错误详情列表,通过 ofDetails() 设置

alarmEnabled

是否触发告警,默认 true,通过 noAlarm() 关闭

extend

扩展数据,通过 putExtend() 设置

旧版异常迁移对照

旧版

新版

BusinessException

BusinessException.of(errorCode, args)

BusinessDataException

BusinessException.ofData(errorCode, data, args)

BusinessErrorDetailsException

BusinessException.ofDetails(errorCode, details, args)

NoRollbackException

由事务管理器根据异常类型判断,不再需要

NoRollbackDataException

由事务管理器根据异常类型判断,不再需要

NoneAlarmException

BusinessException.of(errorCode, args).noAlarm()

IdempotentException

BusinessException.of(errorCode, args).noAlarm()

SuccessException

使用正常的返回值替代,不再支持

ErrorDetail

com.luamas.thinkbridge.common.base.vo.ErrorDetail

错误详情对象,用于在异常中携带多个字段级错误信息。

基本用法

// 使用错误码和消息创建
ErrorDetail detail1 = ErrorDetail.of(1, "发生错误1");

// 使用字段名、错误码和消息创建
ErrorDetail detail2 = ErrorDetail.of("fieldName", 2, "字段错误");

// 使用 ErrorCode 创建(支持国际化)
ErrorDetail detail3 = ErrorDetail.of(ResultCode.CAPTCHA_ERROR, userId);

配合 BusinessException 使用

List<ErrorDetail> errorDetails = new ArrayList<>();
errorDetails.add(ErrorDetail.of(1, "发生错误1"));
errorDetails.add(ErrorDetail.of(2, "发生错误2"));
throw BusinessException.ofDetails(ResultCode.ERROR_DETAILS, errorDetails);

代码如下:

package com.luamas.thinkbridge.common.base.exception;

import com.luamas.thinkbridge.common.base.vo.ErrorDetail;

import java.util.List;
import java.util.Map;

/
 * 统一业务异常类,通过属性控制行为:
 * <ul>
 *   <li>httpStatus: HTTP 状态码,默认 200</li>
 *   <li>data: 业务数据,null 表示无数据</li>
 *   <li>details: 错误详情列表,null 表示无详情</li>
 *   <li>alarmEnabled: 是否触发告警,默认 true</li>
 *   <li>extend: 扩展数据</li>
 * </ul>
 *
 * @author luamas
 /
public class BusinessException extends AbstractException {

    protected BusinessException(int code, String msgCode, Object... args) {
        super(code, msgCode, args);
    }

    protected BusinessException(Throwable cause, int code, String msgCode, Object... args) {
        super(cause, code, msgCode, args);
    }

    /
     * 创建业务异常
     */
    public static BusinessException of(ErrorCode errorCode, Object... args) {
        return new BusinessException(errorCode.getValue(), errorCode.getName(), args);
    }

    public static BusinessException of(Throwable cause, ErrorCode errorCode, Object... args) {
        return new BusinessException(cause, errorCode.getValue(), errorCode.getName(), args);
    }

    public static BusinessException of(int code, String msgCode, Object... args) {
        return new BusinessException(code, msgCode, args);
    }

    /
     * 创建带业务数据的业务异常
     */
    public static BusinessException ofData(ErrorCode errorCode, Object data, Object... args) {
        BusinessException ex = new BusinessException(errorCode.getValue(), errorCode.getName(), args);
        ex.data = data;
        return ex;
    }

    /
     * 创建带错误详情的业务异常
     */
    public static BusinessException ofDetails(ErrorCode errorCode, List<? extends ErrorDetail> details, Object... args) {
        BusinessException ex = new BusinessException(errorCode.getValue(), errorCode.getName(), args);
        ex.details = details;
        return ex;
    }

    /
     * 关闭告警
     */
    public BusinessException noAlarm() {
        this.alarmEnabled = false;
        return this;
    }

    /
     * 设置 HTTP 状态码
     */
    public BusinessException httpStatus(int httpStatus) {
        this.httpStatus = httpStatus;
        return this;
    }

    /
     * 设置扩展数据
     */
    public BusinessException extend(Map<String, Object> extend) {
        this.extend = extend;
        return this;
    }

    /
     * 添加扩展数据
     */
    public BusinessException putExtend(String key, Object value) {
        this.extend.put(key, value);
        return this;
    }

    // ========== 兼容旧版 API (不推荐使用) ==========

    /
     * @deprecated 使用 {@link #of(ErrorCode, Object...)} 替代
     */
    @Deprecated
    public static BusinessException newInstance(ErrorCode errorCode, Object... args) {
        return of(errorCode, args);
    }

    /
     * @deprecated 使用 {@link #of(Throwable, ErrorCode, Object...)} 替代
     */
    @Deprecated
    public static BusinessException newInstance(Throwable cause, ErrorCode errorCode, Object... args) {
        return of(cause, errorCode, args);
    }

    /*
     * @deprecated 使用 {@link #of(int, String, Object...)} 替代
     */
    @Deprecated
    public static BusinessException newInstance(int code, String msgCode, Object... args) {
        return of(code, msgCode, args);
    }
}

UnauthorizedException

com.luamas.thinkbridge.common.base.exception.UnauthorizedException

返回 HTTP 状态码 401,使用 of() 创建实例

throw UnauthorizedException.of(ResultCode.XXX, args);

代码如下:

package com.luamas.thinkbridge.common.base.exception;

/
 * 没有授权,此异常会抛出401
 *
 * @author luamas
 */
public class UnauthorizedException extends AbstractException {

    protected UnauthorizedException(int code, String msgCode, Object... args) {
        super(code, msgCode, args);
        this.httpStatus = 401;
    }

    protected UnauthorizedException(Throwable cause, int code, String msgCode, Object... args) {
        super(cause, code, msgCode, args);
        this.httpStatus = 401;
    }
    public static UnauthorizedException of(ErrorCode errorCode, Object... args) {
        return new UnauthorizedException(errorCode.getValue(), errorCode.getName(), args);
    }

    public static UnauthorizedException of(Throwable cause, ErrorCode errorCode, Object... args) {
        return new UnauthorizedException(cause, errorCode.getValue(), errorCode.getName(), args);
    }

    public static UnauthorizedException of(int code, String msgCode, Object... args) {
        return new UnauthorizedException(code, msgCode, args);
    }

    /
     * @deprecated 使用 {@link #of(ErrorCode, Object...)} 替代
     */
    @Deprecated
    public static UnauthorizedException newInstance(ErrorCode errorCode, Object... args) {
        return of(errorCode, args);
    }

    /
     * @deprecated 使用 {@link #of(Throwable, ErrorCode, Object...)} 替代
     */
    @Deprecated
    public static UnauthorizedException newInstance(Throwable cause, ErrorCode errorCode, Object... args) {
        return of(cause, errorCode, args);
    }

    /
     * @deprecated 使用 {@link #of(int, String, Object...)} 替代
     */
    @Deprecated
    public static UnauthorizedException newInstance(int code, String msgCode, Object... args) {
        return of(code, msgCode, args);
    }
}

ForbiddenException

com.luamas.thinkbridge.common.base.exception.ForbiddenException

返回 HTTP 状态码 403,使用 of() 创建实例

throw ForbiddenException.of(ResultCode.XXX, args);

代码如下:

package com.luamas.thinkbridge.common.base.exception;

/
 * 没有授权,此异常会抛出403
 *
 * @author luamas
 /
public class ForbiddenException extends AbstractException {
    protected ForbiddenException(int code, String msgCode, Object... args) {
        super(code, msgCode, args);
        this.httpStatus = 403;
    }

    protected ForbiddenException(Throwable cause, int code, String msgCode, Object... args) {
        super(cause, code, msgCode, args);
        this.httpStatus = 403;
    }

    public static ForbiddenException of(ErrorCode errorCode, Object... args) {
        return new ForbiddenException(errorCode.getValue(), errorCode.getName(), args);
    }

    public static ForbiddenException of(Throwable cause, ErrorCode errorCode, Object... args) {
        return new ForbiddenException(cause, errorCode.getValue(), errorCode.getName(), args);
    }

    /
     * @deprecated 使用 {@link #of(ErrorCode, Object...)} 替代
     */
    @Deprecated
    public static ForbiddenException newInstance(ErrorCode errorCode, Object... args) {
        return of(errorCode, args);
    }

    /*
     * @deprecated 使用 {@link #of(Throwable, ErrorCode, Object...)} 替代
     */
    @Deprecated
    public static ForbiddenException newInstance(Throwable cause, ErrorCode errorCode, Object... args) {
        return of(cause, errorCode, args);
    }
}

TransmitException

com.luamas.thinkbridge.common.base.exception.TransmitException

用来转义其他的 HTTP 错误为 TransmitException 异常,进行统一处理(如 feign 即可按照原始的 http 值进行转义抛出)。

TransmitException 不实现 ThinkBridgeException 接口,它直接继承 RuntimeException,用于透传原始的 ResponseVo 响应体。

基本用法

// 转发异常,保留原始 HTTP 状态码和响应体
throw TransmitException.of(httpStatus, responseVo);

// 仅传入 ResponseVo,默认 HTTP 状态码 200
throw TransmitException.of(responseVo);

// 自定义消息
throw TransmitException.of("自定义消息", httpStatus, responseVo);

代码如下:

package com.luamas.thinkbridge.common.base.exception;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.luamas.thinkbridge.common.base.vo.ResponseVo;
import lombok.Getter;
import lombok.Setter;

/
 * 异常转发
 *
 * @author luamas
 */
@Getter
@Setter
public class TransmitException extends RuntimeException {
    @JsonIgnore
    private Integer httpStatus;
    @JsonUnwrapped
    private ResponseVo<?> response;

    protected TransmitException(String message, Integer httpStatus, ResponseVo<?> response) {
        super(message);
        this.httpStatus = httpStatus;
        this.response = response;
    }

    public static TransmitException of(String message, Integer httpStatus, ResponseVo<?> response) {
        return new TransmitException(message, httpStatus, response);
    }

    public static TransmitException of(Integer httpStatus, ResponseVo<?> response) {
        return of(response.getError().getMessage(), httpStatus, response);
    }

    public static TransmitException of(ResponseVo<?> response) {
        return of(200, response);
    }

    /
     * @deprecated 使用 {@link #of(String, Integer, ResponseVo)} 替代
     */
    @Deprecated
    public static TransmitException newInstance(String message, Integer httpStatus, ResponseVo<?> response) {
        return of(message, httpStatus, response);
    }

    /
     * @deprecated 使用 {@link #of(Integer, ResponseVo)} 替代
     */
    @Deprecated
    public static TransmitException newInstance(Integer httpStatus, ResponseVo<?> response) {
        return of(httpStatus, response);
    }

    /
     * @deprecated 使用 {@link #of(ResponseVo)} 替代
     */
    @Deprecated
    public static TransmitException newInstance(ResponseVo<?> response) {
        return of(response);
    }
}

InternalException

com.luamas.thinkbridge.common.base.exception.InternalException

内部错误异常,返回 HTTP 状态码 500。适用于服务端内部逻辑错误,如数据库异常、第三方服务调用失败等场景。

基本用法

// 使用 ErrorCode 创建
throw InternalException.of(ResultCode.XXX, args);

// 带原始异常创建
throw InternalException.of(cause, ResultCode.XXX, args);

代码如下:

package com.luamas.thinkbridge.common.base.exception;

/
 * 内部错误异常,所有业务异常都必须继承于此异常
 *
 * @author luamas
 /
public class InternalException extends AbstractException {
    /
     * 建议使用of(int code, String msgCode, Object... args)
     * @param code 异常码
     * @param msgCode 异常消息码
     * @param args 异常消息参数
     */
    protected InternalException(int code, String msgCode, Object... args) {
        super(code, msgCode, args);
        this.httpStatus = 500;
    }
    /
     * 建议使用of(Throwable cause, int code, String msgCode, Object... args)
     * @param cause 异常原因
     * @param code 异常码
     * @param msgCode 异常消息码
     * @param args 异常消息参数
     */
    protected InternalException(Throwable cause, int code, String msgCode, Object... args) {
        super(cause, code, msgCode, args);
        this.httpStatus = 500;
    }

    public static InternalException of(ErrorCode errorCode, Object... args) {
        return new InternalException(errorCode.getValue(), errorCode.getName(), args);
    }

    public static InternalException of(Throwable cause, ErrorCode errorCode, Object... args) {
        return new InternalException(cause, errorCode.getValue(), errorCode.getName(), args);
    }

    /
     * @deprecated 使用 {@link #of(ErrorCode, Object...)} 替代
     */
    @Deprecated
    public static InternalException newInstance(ErrorCode errorCode, Object... args) {
        return of(errorCode, args);
    }

    /*
     * @deprecated 使用 {@link #of(Throwable, ErrorCode, Object...)} 替代
     */
    @Deprecated
    public static InternalException newInstance(Throwable cause, ErrorCode errorCode, Object... args) {
        return of(cause, errorCode, args);
    }
}