业务开发过程中涉及大量的异常处理,通常采用@ControllerAdvice搭配@ExceptionHandler来处理各种异常,但是抛出异常的过程任然避免不了大量的if...else判断,刚好今天看到一篇文章(为什么不建议用try catch处理异常?)采用自定义Assert+枚举+自定义异常的方式优雅的解决了该问题,现整理实现思路:
- 异常最重要的两个信息code和message,为了表示不同的业务异常信息,采用枚举的方式是个不错的选择。因为不同的业务异常有不同的划分方式,所以我们采用面向接口编程的思想,将IResponseEnum作为BaseException的组合元素之一。
public interface IResponseEnum { int getCode(); String getMessage(); }
- 全局异常引入IResponseEnum作为自己的属性,保证抛出的异常信息全部为服务自己定义。
/** * @ClassName: BaseException * @Author: whp * @Description: 基础异常类 * @Date: 2022/3/4 11:20 * @Version: 1.0 */ @Getter public class BaseException extends RuntimeException{ protected IResponseEnum responseEnum; protected Object[] args; public BaseException(IResponseEnum responseEnum){ super(responseEnum.getMessage()); this.responseEnum=responseEnum; } public BaseException(int code,String msg){ super(msg); this.responseEnum=new IResponseEnum() { @Override public int getCode() { return code; } @Override public String getMessage() { return msg; } }; } public BaseException(IResponseEnum responseEnum,Object[] args,String message){ super(message); this.responseEnum=responseEnum; this.args=args; } public BaseException(IResponseEnum responseEnum,Object[] args,String message,Throwable cause){ super(message,cause); this.responseEnum=responseEnum; this.args=args; } }
- 定义Assert,一共有两个作用:1>添加判断逻辑,例如:判断对象是否为空,AssertTrue判断是否为真等;2> 抛出自定义异常。
/** * @ClassName: Assert * @Author: whp * @Description: 自定义断言 * @Date: 2022/3/4 11:25 * @Version: 1.0 */ public interface Assert { BaseException newException(Object ... args); BaseException newException(Throwable t,Object... args); default void assertNotNull(Object obj){ if(obj==null){ throw newException(obj); } } default void assertNotNull(Object obj,Object... args){ if(obj==null){ throw newException(args); } } }
- 定义BusinessExceptionAssert同时实现Assert判断逻辑和枚举类区分不同业务异常逻辑。
/** * @ClassName: BusinessExceptionAssert * @Author: whp * @Description: 业务异常断言 * @Date: 2022/3/4 11:33 * @Version: 1.0 */ public interface BusinessExceptionAssert extends IResponseEnum,Assert { @Override default BaseException newException(Object ... orgs){ String msg= MessageFormat.format(this.getMessage(),orgs); return new BusinessException(this,orgs,msg); } @Override default BaseException newException(Throwable t,Object... orgs){ String msg=MessageFormat.format(this.getMessage(),orgs); return new BusinessException(this,orgs,msg,t); } }
- 使用@Controlleradvice统一处理抛出的异常类:
*/ @ControllerAdvice @ResponseBody @Slf4j public class ExceptionHandlerAdvice { private static final String ENV_PROD="online"; @Value("${spring.profiles.active}") private String profile; @ExceptionHandler(value = BaseException.class) public Response commonException(BaseException exception, WebRequest request){ return new Response().failure().code(exception.getResponseEnum().getCode()).message(exception.getMessage()); } /** * Controller上一层相关异常 * * @param e 异常 * @return 异常结果 */ @ExceptionHandler({ NoHandlerFoundException.class, HttpRequestMethodNotSupportedException.class, HttpMediaTypeNotSupportedException.class, MissingPathVariableException.class, MissingServletRequestParameterException.class, TypeMismatchException.class, HttpMessageNotReadableException.class, HttpMessageNotWritableException.class, HttpMediaTypeNotAcceptableException.class, ServletRequestBindingException.class, ConversionNotSupportedException.class, MissingServletRequestPartException.class, AsyncRequestTimeoutException.class }) @ResponseBody public Response handleServletException(Exception e) { log.error(e.getMessage(), e); int code = ServletResponseEnum.SEVER_EXCEPTION.getCode(); try { ServletResponseEnum servletExceptionEnum = ServletResponseEnum.valueOf(e.getClass().getSimpleName()); return new Response().failure().code(servletExceptionEnum.getCode()).message(servletExceptionEnum.getMessage()); } catch (IllegalArgumentException e1) { log.error("class [{}] not defined in enum {}", e.getClass().getName(), ServletResponseEnum.class.getName()); } return new Response().failure().code(code).message(ServletResponseEnum.SEVER_EXCEPTION.getMessage()); } }
当然,全局异常的应用也要按照不同的服务进行区分,例如在API层异常结果是要返回给用户的,需要将异常信息展示为对用户友好的语言。服务间调用则需要让对应开发能够快速明确问题。相关代码实现可以参考:GitHub - whpHarper/spring-boot-study: 企业级spring boot框架
问题思考:
1. 全局异常可以分为业务到达接口前异常(例如:NoHandlerFoundException状态码对应404,HttpRequestMethodNotSupportedException状态码对应405)、参数校验异常、自定义业务异常等,各类异常如何返回客户端或对应调用方,需要合理规划下。
2. 业务异常定义构造函数携带参数throwable的目的是什么?
答:可以封装将不必要的异常不在接口返回,容易追踪最开始问题点。