1. 全局异常定义
首先自定义异常 code 和异常描述接口,并自定义业务异常编码枚举 BizErrorCode:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public interface ErrorCode { String getCode(); String getDescription(); }
public enum BizErrorCode implements ErrorCode {
SUCCESS("SUCCESS", "成功"), FAIL("FAIL", "失败"), SYSTEM_ERROR("SYSTEM_ERROR", "系统异常"), ;
private final String code; private final String description;
BizErrorCode(final String code, final String description) { this.code = code; this.description = description; }
@Override public String getCode() { return this.code; }
@Override public String getDescription() { return this.description; } }
|
自定义如下业务异常类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class BizException extends RuntimeException { protected final ErrorCode errorCode;
public BizException(ErrorCode errorCode) { super(errorCode.getDescription()); this.errorCode = errorCode; }
public BizException(ErrorCode errorCode, String detailedMessage) { super(detailedMessage); this.errorCode = errorCode; }
public ErrorCode getErrorCode() { return this.errorCode; } }
|
最后,需要将对 BizException 进行解析和二次处理,比如将异常封装为统一的响应 HttpResult 并响应给调用方。其中 handlerBizException 方法统一对抛出来的 BizException 进行处理并解析为 HttpResult 类响应给调用方。而 handlerException 则对所有异常进行兜底处理解析处理成 HttpResult 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
@ControllerAdvice @Slf4j public class GlobalExceptionHandler {
@ExceptionHandler(BizException.class) @ResponseBody public HttpResult handlerBizException(BizException e) { HttpResult httpResult; String message = e.getMessage(); if (StringUtils.isNotBlank(message)) { httpResult = HttpResult.build(new JSONObject(), e.getErrorCode().getCode(), message); } else { httpResult = HttpResult.fail(new JSONObject(), e.getErrorCode()); } return httpResult; }
@ExceptionHandler(Exception.class) @ResponseBody public HttpResult handlerException(Exception e) { String message = e.getMessage(); return HttpResult.fail(new JSONObject(), message); } }
|
2. 全局响应封装
GlobalExceptionHandler 会对异常解析成 HttpResult,最后以 Json 串响应给调用方,其中 HttpResult 定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
|
@Data @ToString public class HttpResult<T> implements Serializable { T data; String code; String msg; String errorMsg; String requestId;
private HttpResult(T data) { this(data, BizErrorCode.SUCCESS.getCode(), ""); }
private HttpResult(T data, String code, String msg) { this.errorMsg = ""; this.requestId = ""; this.data = data; this.code = code; this.msg = msg; }
public static <T> HttpResult<T> build(T data, String code, String msg) { return new HttpResult(data, code, msg); }
public static <T> HttpResult<T> succ(T data) { return new HttpResult(data); }
public static <T> HttpResult<T> fail(T data, ErrorCode err) { return new HttpResult(data, err.getCode(), err.getDescription()); }
public static <T> HttpResult<T> fail(T data, String msg) { return new HttpResult(data, BizErrorCode.FAIL.getCode(), msg); } }
|
如果需要对响应 HttpResult 进行额外的处理,比如加密等,可以在实现一个 ResponseAdvice 类并实现 ResponseBodyAdvice,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Slf4j @ControllerAdvice public class ResponseAdvice implements ResponseBodyAdvice { @Override public boolean supports(MethodParameter returnType, Class converterType) { return true; }
@Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType mediaType, Class converterType, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { return body; } }
|
ResponseBodyAdvice 接口是在 Controller 执行 return 之后,在 response 返回给客户端之前,执行的对 response 的一些处理,可以实现对 response 数据的一些统一封装或者加密等操作。该接口一共有两个方法:
- supports:判断是否要执行beforeBodyWrite方法,true为执行,false不执行 —— 通过supports方法,我们可以选择哪些类或哪些方法要对response进行处理,其余的则不处理。
- beforeBodyWrite:对 response 处理的具体执行方法。
如下,我们验证一下响应:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @RestController @RequestMapping("global/exception") public class GlobalExceptionController {
@RequestMapping("error") public void error() { int i = 1 / 0; }
@RequestMapping("bizError") public void bizError() { throw new BizException(BizErrorCode.SYSTEM_ERROR, "禁止访问"); }
@RequestMapping("right") public HttpResult<QueryOrderResponse> right() { QueryOrderResponse response = new QueryOrderResponse(); response.setOrderNo("456656565656565"); response.setUid("652326596"); return HttpResult.succ(response); } }
|
分别响应如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| { "data": {}, "code": "FAIL", "msg": "/ by zero", "errorMsg": "", "requestId": "" }
{ "data": {}, "code": "SYSTEM_ERROR", "msg": "禁止访问", "errorMsg": "", "requestId": "" }
{ "data": { "orderNo": "456656565656565", "uid": "652326596" }, "code": "SUCCESS", "msg": "", "errorMsg": "", "requestId": "" }
|
3. 响应配置国际化
假若需要给异常响应配置国际化,重写 BizErrorCode 如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
|
public enum BizErrorCode implements ErrorCode {
SUCCESS("SUCCESS", "成功"), FAIL("FAIL", "失败"), SYSTEM_ERROR("SYSTEM_ERROR", "系统异常"), INVALID_PARAMS("INVALID_PARAMS", "无效参数", "10001"), ;
private final String code; private final String description; private final String i18nCode;
BizErrorCode(final String code, final String description) { this(code, description, ""); }
BizErrorCode(final String code, final String description, String i18nCode) { this.code = code; this.description = description; this.i18nCode = i18nCode; }
@Override public String getCode() { return this.code; }
@Override public String getDescription() { return this.description; }
public String getI18nCode() { return i18nCode; }
public static BizErrorCode getByCode(String code) { for (BizErrorCode v : BizErrorCode.values()) { if (v.getCode().equals(code)) { return v; } } return null; } }
|
新增国际化资源文件,如下:
其中 messages_en_US.properties 和 messages_zh_CN.properties 的内容为:
1 2 3 4
| # messages_en_US.properties 10001=Invalid params # messages_zh_CN.properties 10001=无效参数
|
为了让配置文件生效,同时还需要进行如下配置:
1 2 3 4
| spring: messages: basename: i18n/messages
|
假若需要对系统抛出来的 BizException 异常做国际化处理,修改 GlobalExceptionHandler 类的 handlerBizException 方法如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| @ExceptionHandler(BizException.class) @ResponseBody public HttpResult handlerBizException(BizException e) { HttpResult httpResult; String message = e.getMessage(); if (MessageUtils.isEnLanguage()) { message = "System Error"; BizErrorCode bizErrorCode = BizErrorCode.getByCode(e.getErrorCode().getCode()); if (bizErrorCode != null && StringUtils.isNotBlank(bizErrorCode.getI18nCode())) { String i18nMessage = MessageUtils.getMessage(bizErrorCode.getI18nCode()); if (StringUtils.isNotBlank(i18nMessage)) { message = i18nMessage; } } } if (StringUtils.isNotBlank(message)) { httpResult = HttpResult.build(new JSONObject(), e.getErrorCode().getCode(), message); } else { httpResult = HttpResult.fail(new JSONObject(), e.getErrorCode()); } return httpResult; }
public class MessageUtils {
public static String getMessage(String code, Object... args) { MessageSource messageSource = SpringContextUtils.getBean(MessageSource.class); return messageSource.getMessage(code, args, LocaleContextHolder.getLocale()); }
public static boolean isEnLanguage() { Locale locale = LocaleContextHolder.getLocale(); return Locale.US.equals(locale); } }
@Component public class SpringContextUtils implements ApplicationContextAware { public static ApplicationContext applicationContext;
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringContextUtils.applicationContext = applicationContext; }
public static <T> T getBean(Class<T> requiredType) { return applicationContext.getBean(requiredType); } }
|
国际化语言的切换主要是因为有一个区域信息解析器在其作用,Spring 默认提供使用的是 LocaleResolver 的实现。下面我们自定义一个名为的 system-language
请求头参数,并根据其值是 CN
还是 EN
来切换不同的语言环境。定义类 CustomLocaleResolver 并实现 LocaleResolver 接口,并重写 resolveLocale 方法,具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class CustomLocaleResolver implements LocaleResolver {
private static final String LANGUAGE_EN = "EN";
@Override public Locale resolveLocale(HttpServletRequest request) { String language = Optional.ofNullable(request.getHeader("system-language")).orElse("CN"); if (LANGUAGE_EN.equalsIgnoreCase(language)) { return Locale.US; } return Locale.SIMPLIFIED_CHINESE; }
@Override public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) { } }
|
最后将我们自定义的区域信息解析器 CustomLocaleResolver 交由 Spring 托管,如下:
1 2 3 4 5 6 7
| @Configuration public class LocaleResolverConfiguration { @Bean public LocaleResolver localeResolver() { return new CustomLocaleResolver(); } }
|
最后,我们根据请求头配置 system-language
来切换不同语言环境一次得到不用语言的响应:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @RequestMapping("i18n/error") public void i18n() { throw new BizException(BizErrorCode.INVALID_PARAMS); }
{ "data": {}, "code": "INVALID_PARAMS", "msg": "无效参数", "errorMsg": "", "requestId": "" }
{ "data": {}, "code": "INVALID_PARAMS", "msg": "Invalid params", "errorMsg": "", "requestId": "" }
|