异常处理其实一直都是项目开发中的大头,但关注异常处理的人一直都特别少。经常是简单的 try/catch 所有异常,然后简单的 printStackTrace ,最多使用 logger 来打印下日志或者重新抛出异常,还有的已经有自定义异常了,但是还是在 controller 捕获异常,需要 catch(异常1 )catch(异常2) 特别繁琐,而且容易漏。
其实 springmvc 在 ControllerAdvice 已经提供了一种全局处理异常的方式,并且我们还可以使用 aop 来统一处理异常,这样在任何地方我们都只需要关注自己的业务,而不用关注异常处理,而且抛出异常还可以利用 spring 的事务,它只有在检测到异常才会事务回滚。
这里使用 springmvc 的 ControllerAdvice 来做统一异常处理
import com.sanri.test.testmvc.dto.ResultEntity; import com.sanri.test.testmvc.exception.BusinessException; import com.sanri.test.testmvc.exception.RemoteException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.util.ArrayList; import java.util.List; @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { @Value("${project.package.prefix:com.sanri.test}") protected String packagePrefix; /** * 处理业务异常 * @param e * @return */ @ExceptionHandler(BusinessException.class) public ResultEntity businessException(BusinessException e){ printLocalStackTrack(e); return e.getResultEntity(); } @ExceptionHandler(RemoteException.class) public ResultEntity remoteException(RemoteException e){ ResultEntity parentResult = e.getParent().getResultEntity(); ResultEntity resultEntity = e.getResultEntity(); //返回给前端的是业务错误,但是需要在控制台把远程调用异常给打印出来 log.error(parentResult.getReturnCode()+":"+parentResult.getMessage() +" /n -| "+resultEntity.getReturnCode()+":"+resultEntity.getMessage()); printLocalStackTrack(e); //合并两个结果集返回 ResultEntity merge = ResultEntity.err(parentResult.getReturnCode()) .message(parentResult.getMessage()+" /n |- "+resultEntity.getReturnCode()+":"+resultEntity.getMessage()); return merge; } /** * 打印只涉及到项目类调用的异常堆栈 * @param e */ private void printLocalStackTrack(BusinessException e) { StackTraceElement[] stackTrace = e.getStackTrace(); List<StackTraceElement> localStackTrack = new ArrayList<>(); StringBuffer showMessage = new StringBuffer(); if (ArrayUtils.isNotEmpty(stackTrace)) { for (StackTraceElement stackTraceElement : stackTrace) { String className = stackTraceElement.getClassName(); int lineNumber = stackTraceElement.getLineNumber(); if (className.startsWith(packagePrefix)) { localStackTrack.add(stackTraceElement); showMessage.append(className + "(" + lineNumber + ")/n"); } } log.error("业务异常:" + e.getMessage() + "/n" + showMessage); } else { log.error("业务异常,没有调用栈 " + e.getMessage()); } } /** * 异常处理,可以绑定多个 * @return */ @ExceptionHandler(Exception.class) public ResultEntity result(Exception e){ e.printStackTrace(); return ResultEntity.err(e.getMessage()); } }
package com.sanri.test.testmvc.dto; import lombok.Data; import lombok.ToString; import java.io.Serializable; /** * 普通消息返回 * @param <T> */ @Data @ToString public class ResultEntity<T> implements Serializable { private String returnCode = "0"; private String message; private T data; public ResultEntity() { this.message = "ok"; } public ResultEntity(T data) { this(); this.data = data; } public static ResultEntity ok() { return new ResultEntity(); } public static ResultEntity err(String returnCode) { ResultEntity resultEntity = new ResultEntity(); resultEntity.returnCode = returnCode; resultEntity.message = "fail"; return resultEntity; } public static ResultEntity err() { return err("-1"); } public ResultEntity message(String msg) { this.message = msg; return this; } public ResultEntity data(T data) { this.data = data; return this; } }
自定义异常,就我目前的工作经历来看的话,异常一般就三种 。
对于业务异常来说,我们有时候需要对错误进行编号,因为前端需要拿到编号来做一些页面跳转的工作,而且客户在投诉错误的时候也可以告诉运营编号,然后可以做应对的措施;但绝大部分的时候是不需要错误编号的,这时可以随机生成一个编号。我们可以定一个号段来定义错误编号,比如 0 定义为正常,1~100 为通用错误, 101 ~1000 是 A 系统,1000 ~ 2000 是 B 系统,然后 10000 以上是随机代码等。
import com.sanri.test.testmvc.dto.ResultEntity; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; import java.util.List; /** * 系统业务异常(根异常),异常号段为 : * 0 : 成功 * 1 ~ 9999 内定系统异常段 * 10000 ~ 99999 自定义异常码段 * 100000 ~ Integer.MAX_VALUE 动态异常码段 */ public class BusinessException extends RuntimeException { protected ResultEntity resultEntity; protected static final int MIN_AUTO_CODE = 100000; public static BusinessException create(String message) { int value= (int) (MIN_AUTO_CODE + Math.round((Integer.MAX_VALUE - MIN_AUTO_CODE) * Math.random())); return create(value + "",message); } public static BusinessException create(String returnCode,String message){ if(StringUtils.isBlank(returnCode)){ return create(message); } BusinessException businessException = new BusinessException(); businessException.resultEntity = ResultEntity.err(returnCode).message(message); return businessException; } public static BusinessException create(ExceptionCause exceptionCause ,Object...args){ ResultEntity resultEntity = exceptionCause.result(); String message = resultEntity.getMessage(); if(ArrayUtils.isNotEmpty(args)){ String [] argsStringArray = new String [args.length]; for (int i=0;i<args.length;i++) { Object arg = args[i]; argsStringArray[i] = ObjectUtils.toString(arg); } String formatMessage = String.format(message, argsStringArray); resultEntity.setMessage(formatMessage); } BusinessException businessException = new BusinessException(); businessException.resultEntity = resultEntity; return businessException; } @Override public String getMessage() { return resultEntity.getMessage(); } public ResultEntity getResultEntity() { return resultEntity; } }
远程调用异常一般别人也会返回错误码和错误消息给我们,这时我们可以定义一个远程异常,把业务异常做为父级异常,这时候呈现的错误结构会是这样,举个例子
投保业务出错 -| E007 生效日期必须大于当前日期
代码如下,用到了建造者设计模式,如不知道这个设计模式,可以自行百度
import com.sanri.test.testmvc.dto.ResultEntity; import com.sun.deploy.net.proxy.RemoveCommentReader; public class RemoteException extends BusinessException{ private BusinessException parent; private RemoteException(BusinessException parent) { this.parent = parent; } /** * 创建远程异常 * @param parent * @param remoteCode * @param remoteMessage * @return */ public static RemoteException create(BusinessException parent,String remoteCode,String remoteMessage){ RemoteException remoteException = new RemoteException(parent); remoteException.resultEntity = ResultEntity.err(remoteCode).message(remoteMessage); return remoteException; } /** * 简易创建远程信息 * @param parent * @param remoteMessage * @return */ public static RemoteException create(BusinessException parent,String remoteMessage){ return create(parent,"remoteError",remoteMessage); } public static RemoteException create(String localMessage,String remoteCode,String remoteMessage){ return new Builder().localMessage(localMessage).remoteCode(remoteCode).remoteMessage(remoteMessage).build(); } public static RemoteException create(String localMessage,String remoteMessage){ return new Builder().localMessage(localMessage).remoteMessage(remoteMessage).build(); } public static class Builder{ private String localCode; private String localMessage; private String remoteCode; private String remoteMessage; public Builder localCode(String localCode){ this.localCode = localCode; return this; } public Builder localMessage(String localMessage){ this.localMessage = localMessage; return this; } public Builder remoteCode(String remoteCode){ this.remoteCode = remoteCode; return this; } public Builder remoteMessage(String remoteMessage){ this.remoteMessage = remoteMessage; return this; } public RemoteException build(){ BusinessException businessException = BusinessException.create(localCode, localMessage); RemoteException remoteException = RemoteException.create(businessException,remoteCode,remoteMessage); return remoteException; } } public BusinessException getParent() { return parent; } }
见过很多项目抛出新异常时使用了这样的方式 throw new BusinessException(...)
感觉特别不雅观。
BusinessException.create("姓名重复,请重新输入");
使用方法:
throw SystemMessage.NOT_LOGIN.exception();
代码定义:
import com.sanri.test.testmvc.dto.ResultEntity; public interface ExceptionCause<T extends Exception> { T exception(Object... args); ResultEntity result(); }
import com.sanri.test.testmvc.dto.ResultEntity; public enum SystemMessage implements ExceptionCause<BusinessException> { NOT_LOGIN(4001,"未登录或 session 失效"), PERMISSION_DENIED(4002,"没有权限"), DATA_PERMISSION_DENIED(4007,"无数据权限"), SIGN_ERROR(4003,"签名错误,你的签名串为 [%s]") ; ResultEntity resultEntity = new ResultEntity(); private SystemMessage(int returnCode,String message){ resultEntity.setReturnCode(returnCode+""); resultEntity.setMessage(message); } @Override public BusinessException exception(Object...args) { return BusinessException.create(this,args); } @Override public ResultEntity result() { return resultEntity; } /** * 自定义消息的结果返回 * @param args * @return */ public ResultEntity result(Object ... args){ String message = resultEntity.getMessage(); resultEntity.setMessage(String.format(message,args)); return resultEntity; } public String getReturnCode(){ return resultEntity.getReturnCode(); } }
assertLogin(); /** * 断言用户是否为登录状态 */ public void assertLogin(){ // 获取当前用户,从 session 或 redis 或 auth2 或 shiro 或 SSO 中获取 User user = xxx.get(); if(user == null){ throw SystemMessage.NOT_LOGIN.exception(); } }
@RestController public class ExceptionController { /** * 静态异常展示,固定错误码 */ @GetMapping("/staticException") public void staticException(){ throw SystemMessage.ACCESS_DENIED.exception("无权限"); } /** * 动态异常,前端不关注错误码 */ @GetMapping("/dynamicException") public void dynamicException(){ throw BusinessException.create("名称重复,请使用别的名字"); } /** * 第三方调用异常,需显示层级异常 */ @GetMapping("/remoteException") public void remoteException(){ //模拟远端错误 String remoteCode = "E007"; String remoteMessage = "生效日期必须大于当前日期"; throw RemoteException.create("某某业务调用错误",remoteCode,remoteMessage); } }
以上代码可以到我的 github 上下载相关项目,可以直接运行,拆箱即用。
https://gitee.com/sanri/example/tree/master/test-mvc创作不易,希望可以推广下我的小工具,很实用的解决项目中的一些麻烦的事情,欢迎来 github 点星,fork
github 地址: https://gitee.com/sanri/sanri-tools-maven
博客地址: https://blog.csdn.net/sanri1993/article/details/98664034