SpringBoot开发详解(六)-- 异常统一管理以及AOP的使用
使用切面管理异常的原因:
今天的内容干货满满哦~并且是我自己在平时工作中的一些问题与解决途径,对实际开发的作用很大,好,闲言少叙,让我们开始吧~~
我们先看一张错误信息在APP中的展示图:
是不是体验很差,整个后台错误信息都在APP上打印了。
作为后台开发人员,我们总是在不停的写各种接口提供给前端调用,然而不可避免的,当后台出现BUG时,前端总是丑陋的讲错误信息直接暴露给用户,这样的用户体验想必是相当差的(不过后台开发一看就知道问题出现在哪里)。同时,在解决BUG时,我们总是要问前端拿到参数去调适,排除各种问题(网络,Json体错误,接口名写错……BaLa……BaLa……BaLa)。在不考虑前端容错的情况下。我们自己后台有没有优雅的解决这个问题的方法呢,今天这篇我们就来使用AOP统一对异常进行记录以及返回。
SpringBoot引入AOP
在SpringBoot中引入AOP是一件很方便的事,和其他引入依赖一样,我们只需要在POM中引入starter就可以了:
1 <!--spring切面aop依赖--> 2 <dependency> 3 <groupId>org.springframework.boot</groupId> 4 <artifactId>spring-boot-starter-aop</artifactId> 5 </dependency>
返回体报文定义
接下来我们先想一下,一般我们返回体是什么样子的呢?或者你觉得一个返回的报文应该具有哪些特征。
-
成功标示:可以用boolean型作为标示位。
-
错误代码:一般用整型作为标示位,罗列的越详细,前端的容错也就能做的更细致。
-
错误信息:使用String作为错误信息的描述,留给前端是否展示给用户或者进入其他错误流程的使用。
-
结果集:在无错误信息的情况下所得到的正确数据信息。一般是个Map,前端根据Key取值。
以上是对一个返回体报文一个粗略的定义了,如果再细致点,可以使用签名进行验签功能活着对明文数据进行对称加密等等。这些我们今天先不讨论,我们先完成一个能够使用的接口信息定义。
我们再对以上提到这些信息做一个完善,去除冗余的字段,对差不多的类型进行合并于封装。这样的想法下,我们创建一个返回体报文的实体类。
1 public class Result<T> { 2 3 // error_code 状态值:0 极为成功,其他数值代表失败 4 private Integer status; 5 6 // error_msg 错误信息,若status为0时,为success 7 private String msg; 8 9 // content 返回体报文的出参,使用泛型兼容不同的类型 10 private T data; 11 12 public Integer getStatus() { 13 return status; 14 } 15 16 public void setStatus(Integer code) { 17 this.status = code; 18 } 19 20 public String getMsg() { 21 return msg; 22 } 23 24 public void setMsg(String msg) { 25 this.msg = msg; 26 } 27 28 public T getData(Object object) { 29 return data; 30 } 31 32 public void setData(T data) { 33 this.data = data; 34 } 35 36 public T getData() { 37 return data; 38 } 39 40 @Override 41 public String toString() { 42 return "Result{" + 43 "status=" + status + 44 ", msg='" + msg + '\'' + 45 ", data=" + data + 46 '}'; 47 }
现在我们已经有一个返回体报文的定义了,那接下来我们可以来创建一个枚举类,来记录一些我们已知的错误信息,可以在代码中直接使用。
1 public enum ExceptionEnum { 2 UNKNOW_ERROR(-1,"未知错误"), 3 USER_NOT_FIND(-101,"用户不存在"), 4 ; 5 6 private Integer code; 7 8 private String msg; 9 10 ExceptionEnum(Integer code, String msg) { 11 this.code = code; 12 this.msg = msg; 13 } 14 15 public Integer getCode() { 16 return code; 17 } 18 19 public String getMsg() { 20 return msg; 21 } 22 }
我们在这里把对于不再预期内的错误统一设置为-1,未知错误。以避免返回给前端大段大段的错误信息。
接下来我们只需要创建一个工具类在代码中使用:
1 public class ResultUtil { 2 3 /** 4 * 返回成功,传入返回体具体出參 5 * @param object 6 * @return 7 */ 8 public static Result success(Object object){ 9 Result result = new Result(); 10 result.setStatus(0); 11 result.setMsg("success"); 12 result.setData(object); 13 return result; 14 } 15 16 /** 17 * 提供给部分不需要出參的接口 18 * @return 19 */ 20 public static Result success(){ 21 return success(null); 22 } 23 24 /** 25 * 自定义错误信息 26 * @param code 27 * @param msg 28 * @return 29 */ 30 public static Result error(Integer code,String msg){ 31 Result result = new Result(); 32 result.setStatus(code); 33 result.setMsg(msg); 34 result.setData(null); 35 return result; 36 } 37 38 /** 39 * 返回异常信息,在已知的范围内 40 * @param exceptionEnum 41 * @return 42 */ 43 public static Result error(ExceptionEnum exceptionEnum){ 44 Result result = new Result(); 45 result.setStatus(exceptionEnum.getCode()); 46 result.setMsg(exceptionEnum.getMsg()); 47 result.setData(null); 48 return result; 49 } 50 }
以上我们已经可以捕获代码中那些在编码阶段我们已知的错误了,但是却无法捕获程序出的未知异常信息。我们的代码应该写得漂亮一点,虽然很多时候我们会说时间太紧了,等之后我再来好好优化。可事实是,我们再也不会回来看这些代码了。项目总是一个接着一个,时间总是不够用的。如果真的需要你完善重构原来的代码,那你一定会非常痛苦,死得相当难看。所以,在第一次构建时,就将你的代码写完善了。
一般系统抛出的错误是不含错误代码的,除去部分的404,400,500错误之外,我们如果想把错误代码定义的更细致,就需要自己继承RuntimeException这个类后重新定义一个构造方法来定义我们自己的错误信息:
1 public class DescribeException extends RuntimeException{ 2 3 private Integer code; 4 5 /** 6 * 继承exception,加入错误状态值 7 * @param exceptionEnum 8 */ 9 public DescribeException(ExceptionEnum exceptionEnum) { 10 super(exceptionEnum.getMsg()); 11 this.code = exceptionEnum.getCode(); 12 } 13 14 /** 15 * 自定义错误信息 16 * @param message 17 * @param code 18 */ 19 public DescribeException(String message, Integer code) { 20 super(message); 21 this.code = code; 22 } 23 24 public Integer getCode() { 25 return code; 26 } 27 28 public void setCode(Integer code) { 29 this.code = code; 30 } 31 }
同时,我们使用一个Handle来把Try,Catch中捕获的错误进行判定,是一个我们已知的错误信息,还是一个未知的错误信息,如果是未知的错误信息,那我们就用log记录它,便于之后的查找和解决:
1 @ControllerAdvice 2 public class ExceptionHandle { 3 4 private final static Logger LOGGER = LoggerFactory.getLogger(ExceptionHandle.class); 5 6 /** 7 * 判断错误是否是已定义的已知错误,不是则由未知错误代替,同时记录在log中 8 * @param e 9 * @return 10 */ 11 @ExceptionHandler(value = Exception.class) 12 @ResponseBody 13 public Result exceptionGet(Exception e){ 14 if(e instanceof DescribeException){ 15 DescribeException MyException = (DescribeException) e; 16 return ResultUtil.error(MyException.getCode(),MyException.getMessage()); 17 } 18 19 LOGGER.error("【系统异常】{}",e); 20 return ResultUtil.error(ExceptionEnum.UNKNOW_ERROR); 21 } 22 }
这里我们使用了 @ControllerAdvice ,使Spring能加载该类,同时我们将所有捕获的异常统一返回结果Result这个实体。
此时,我们已经完成了对结果以及异常的统一返回管理,并且在出现异常时,我们可以不返回错误信息给前端,而是用未知错误进行代替,只有查看log我们才会知道真实的错误信息。
可能有小伙伴要问了,说了这么久,并没有使用到AOP啊。不要着急,我们继续完成我们剩余的工作。
我们使用接口若出现了异常,很难知道是谁调用接口,是前端还是后端出现的问题导致异常的出现,那这时,AOP就发挥作用了,我们之前已经引入了AOP的依赖,现在我们编写一个切面类,切点如何配置不需要我多说了吧:
1 @Aspect 2 @Component 3 public class HttpAspect { 4 5 private final static Logger LOGGER = LoggerFactory.getLogger(HttpAspect.class); 6 7 @Autowired 8 private ExceptionHandle exceptionHandle; 9 10 @Pointcut("execution(public * com.zzp.controller.*.*(..))") 11 public void log(){ 12 13 } 14 15 @Before("log()") 16 public void doBefore(JoinPoint joinPoint){ 17 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); 18 HttpServletRequest request = attributes.getRequest(); 19 20 //url 21 LOGGER.info("url={}",request.getRequestURL()); 22 //method 23 LOGGER.info("method={}",request.getMethod()); 24 //ip 25 LOGGER.info("id={}",request.getRemoteAddr()); 26 //class_method 27 LOGGER.info("class_method={}",joinPoint.getSignature().getDeclaringTypeName() + "," + joinPoint.getSignature().getName()); 28 //args[] 29 LOGGER.info("args={}",joinPoint.getArgs()); 30 } 31 32 @Around("log()") 33 public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { 34 Result result = null; 35 try { 36 37 } catch (Exception e) { 38 return exceptionHandle.exceptionGet(e); 39 } 40 if(result == null){ 41 return proceedingJoinPoint.proceed(); 42 }else { 43 return result; 44 } 45 } 46 47 @AfterReturning(pointcut = "log()",returning = "object")//打印输出结果 48 public void doAfterReturing(Object object){ 49 LOGGER.info("response={}",object.toString()); 50 } 51 }
我们使用@Aspect来声明这是一个切面,使用@Pointcut来定义切面所需要切入的位置,这里我们是对每一个HTTP请求都需要切入,在进入方法之前我们使用@Before记录了调用的接口URL,调用的方法,调用方的IP地址以及输入的参数等。在整个接口代码运作期间,我们使用@Around来捕获异常信息,并用之前定义好的Result进行异常的返回,最后我们使用@AfterReturning来记录我们的出參。
以上全部,我们就完成了异常的统一管理以及切面获取接口信息,接下来我们心新写一个ResultController来测试一下:
1 @RestController 2 @RequestMapping("/result") 3 public class ResultController { 4 5 @Autowired 6 private ExceptionHandle exceptionHandle; 7 8 /** 9 * 返回体测试 10 * @param name 11 * @param pwd 12 * @return 13 */ 14 @RequestMapping(value = "/getResult",method = RequestMethod.POST) 15 public Result getResult(@RequestParam("name") String name, @RequestParam("pwd") String pwd){ 16 Result result = ResultUtil.success(); 17 try { 18 if (name.equals("zzp")){ 19 result = ResultUtil.success(new UserInfo()); 20 }else if (name.equals("pzz")){ 21 result = ResultUtil.error(ExceptionEnum.USER_NOT_FIND); 22 }else{ 23 int i = 1/0; 24 } 25 }catch (Exception e){ 26 result = exceptionHandle.exceptionGet(e); 27 } 28 return result; 29 } 30 }
在上面我们设计了一个controller,如果传入的name是zzp的话,我们就返回一个用户实体类,如果传入的是pzz的话,我们返回一个没有该用户的错误,其他的,我们让他抛出一个by zero的异常。
我们用POSTMAN进行下测试:
我们可以看到,前端收到的返回体报文已经按我们要求同意了格式,并且在控制台中我们打印出了调用该接口的一些接口信息,我们继续测试另外两个会出现错误情况的请求:
我们可以看到,如是我们之前在代码中定义完成的错误信息,我们可以直接返回错误码以及错误信息,如果是程序出现了我们在编码阶段不曾预想到的错误,则统一返回未知错误,并在log中记录真实错误信息。
以上就是我们统一管理结果集以及使用切面来记录接口调用的一些真实情况,在平时的使用中,大家要清楚切点的优先级以及在不同的切点位置该使用哪些注解来帮助我们完成开发,并且在切面中,如果遇到同步问题该如何解决等等。
转自:https://blog.csdn.net/qq_31001665/article/details/71357825