背景

最近在读《代码精进之路 从码农到工匠》,书中有讲到异常规范和日志规范,大致是下面这几点:

  • 自定义BusinessException和SystemException,用于区分业务异常和系统异常,业务异常应该是warn级别,系统异常才可以记error
  • 异常日志监控:error级别即时报警,warn级别单位时间内数量超过预设阀值也要报警。由于监控报警所以日志等级要按照规范使用
  • 异常要使用AOP统一处理,而不应该使try-catch代码散落在项目中的各处。使得代码的可读性和简洁性降低

感觉说得很有道理,就规范进行了实践,对目前项目中的异常和日志进行了V1版本的优化,路漫漫其修远兮,不断的迭代吧。主要进行了两点,一是包装外部RPC接口优化,二是controller层web接口优化。优化点是将方法内进行try-catch处理及参数返回值打印的地方进行了统一处理。

RPC接口

原因

对于被调用的外部接口,最好不要直接调用,要在自己的项目内封装一层,主要做三件事:异常处理、参数及返回记录、特定逻辑处理。刚开始开发的时候不懂,都是直接调用,多个地方都调用后就会发现代码重复度很高,后面有改动的话,就要全局搜索改很多个地方,还是封装一层更为合理,磨刀不误砍柴工。

实现

自定义注解

用于标记rpc接口信息

import java.lang.annotation.*;/*** 调用外部接口检查** @yx8102 2020/5/15*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RpcCheck {// 服务描述String serviceNameCN() default "外部服务";// 服务名称String serviceNameEN() default "EXTERNAL_SERVICE";// 方法描述String methodNameCN() default "外部方法";// 方法名称String methodNameEN() default "EXTERNAL_METHOD";// 是否打印入参boolean printParam() default true;
}

切面

进行日志、异常、耗时统一处理

import com.alibaba.fastjson.JSON;
import com.google.common.base.Stopwatch;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;/*** 外部接口调用check** @yx8102 2020/5/15*/
@Slf4j
@Aspect
@Component
public class RpcCheckAspect {@SneakyThrows@Around("@annotation(rpcCheck)")public Object around(ProceedingJoinPoint point, RpcCheck rpcCheck) {Object result;try {// 拼装接口入参, 入参名称-入参值mapMap<String, Object> paramMap = new HashMap<>();Object[] paramValueArr = point.getArgs();String[] paramNameArr = ((MethodSignature) point.getSignature()).getParameterNames();for (int i = 0; i < paramValueArr.length; i++) {Object paramValue = paramValueArr[i];if (Objects.isNull(paramValue) || paramValue instanceof HttpServletRequest || paramValue instanceof HttpServletResponse) {continue;}if (paramValue instanceof MultipartFile) {paramValue = ((MultipartFile) paramValue).getSize();}paramMap.put(paramNameArr[i], paramValue);}log.info("调用[服务]:{} {} {} {},[参数]:{}", rpcCheck.serviceNameCN(), rpcCheck.serviceNameEN(), rpcCheck.methodNameCN(), rpcCheck.methodNameEN(), rpcCheck.printParam() ? JSON.toJSONString(paramMap) : point.getArgs().length);Stopwatch stopwatch = Stopwatch.createStarted();result = point.proceed();log.info("调用[服务]:{} {} {} {},[返回]:{}", rpcCheck.serviceNameCN(), rpcCheck.serviceNameEN(), rpcCheck.methodNameCN(), rpcCheck.methodNameEN(), JSON.toJSONString(result));log.info("调用[服务]:{} {} {} {},[耗时]:{}", rpcCheck.serviceNameCN(), rpcCheck.serviceNameEN(), rpcCheck.methodNameCN(), rpcCheck.methodNameEN(), stopwatch.elapsed(TimeUnit.MILLISECONDS));} catch (NullPointerException e) {log.error("调用[服务]:{} {} {} {} 异常", rpcCheck.methodNameCN(), rpcCheck.serviceNameEN(), rpcCheck.serviceNameCN(), rpcCheck.methodNameEN(), e);throw new SystemException(String.format("[服务]: %s,调用发生异常(无可用服务): %s", rpcCheck.methodNameEN(), e.getMessage()));} catch (Exception e) {log.error("调用[服务]:{} {} {} {} 异常", rpcCheck.methodNameCN(), rpcCheck.serviceNameEN(), rpcCheck.serviceNameCN(), rpcCheck.methodNameEN(), e);throw new SystemException(String.format("[服务]: %s,调用发生异常: %s", rpcCheck.methodNameEN(), e.getMessage()));}if (Objects.isNull(result)) {throw new SystemException(String.format("[服务]: %s, 返回为null", rpcCheck.methodNameEN()));}return result;}}

Web接口

原因

因为请求的出口是controller层,所以在这一层增加切面进行统一处理。

实现

自定义注解

用于标记需要个性化处理的接口,比如文件上传等不需要打印入参的接口

import java.lang.annotation.*;/*** controller接口check** @yx8102 2020/5/19*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebCheck {/*** 无异常时,是否打印入参, 默认否* @return*/boolean printParam() default false;/*** 是否打印返回值, 默认否* @return*/boolean printReturn() default false;/*** 是否打印耗时, 默认否* @return*/boolean printTime() default false;
}

切面

进行异常处理、日志打印

import com.alibaba.fastjson.JSON;
import com.google.common.base.Stopwatch;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;/*** controller 接口日志、异常统一处理** @xyang010 2020/5/18*/
@Aspect
@Slf4j
@Component
public class WebCheckAspect {@Pointcut("execution(public * com.yx.controller..*.*(..)))")public void controller() {}@SneakyThrows@Around("controller()")public Object around(ProceedingJoinPoint point) {// 接口调用计时Stopwatch stopwatch = Stopwatch.createStarted();// 接口正常返回Object result = null;// 接口异常返回MyResult<Void> errorResult = new MyResult<>(ResultCodes.UNKNOW_ERROR, ResultCodes.UNKNOW_ERROR.getText());// controller请求路径String targetClassPath = Objects.nonNull(point.getTarget().getClass().getAnnotation(RequestMapping.class)) ? point.getTarget().getClass().getAnnotation(RequestMapping.class).value()[0] : "";// controller功能描述String targetClassDesc = Objects.nonNull(point.getTarget().getClass().getAnnotation(Api.class)) ? point.getTarget().getClass().getAnnotation(Api.class).value() : "";// 接口Method targetMethod  = ((MethodSignature) point.getSignature()).getMethod();// 接口功能描述String targetMethodDesc = Objects.nonNull(targetMethod.getAnnotation(ApiOperation.class)) ? targetMethod.getAnnotation(ApiOperation.class).value() : "";// 接口请求路径String methodPost = Objects.nonNull(targetMethod.getAnnotation(PostMapping.class)) ? targetMethod.getAnnotation(PostMapping.class).value()[0] : "";String methodGet = Objects.nonNull(targetMethod.getAnnotation(GetMapping.class)) ? targetMethod.getAnnotation(GetMapping.class).value()[0] : "";String methodRequest = Objects.nonNull(targetMethod.getAnnotation(RequestMapping.class)) ? targetMethod.getAnnotation(RequestMapping.class).value()[0] : "";String methodPath = methodPost + methodGet + methodRequest;// 接口打印信息配置WebCheck webCheck = targetMethod.getAnnotation(WebCheck.class);// 无异常时,是否打印入参boolean printParam = Objects.nonNull(webCheck) && webCheck.printParam();// 是否打印返回值boolean printReturn = Objects.nonNull(webCheck) && webCheck.printReturn();// 是否打印接口耗时boolean printTime = Objects.nonNull(webCheck) && webCheck.printTime();// 拼装接口入参, 入参名称-入参值mapMap<String, Object> paramMap = new HashMap<>();Object[] paramValueArr = point.getArgs();String[] paramNameArr = ((MethodSignature) point.getSignature()).getParameterNames();for (int i = 0; i < paramValueArr.length; i++) {Object paramValue = paramValueArr[i];if (Objects.isNull(paramValue) || paramValue instanceof HttpServletRequest || paramValue instanceof HttpServletResponse) {continue;}if (paramValue instanceof MultipartFile) {paramValue = ((MultipartFile) paramValue).getSize();}paramMap.put(paramNameArr[i], paramValue);}try {log.info("[接口] {} {} {} {}, " + (printParam ? "[参数] {}" : ">>>>>>>> start"), targetClassDesc, targetMethodDesc, targetClassPath, methodPath, (printParam ? JSON.toJSONString(paramMap) : ""));result = point.proceed();} catch (BusinessException e) {log.warn("[接口][业务异常] {} {} {} {}, [参数] {}", targetClassDesc, targetMethodDesc, targetClassPath, methodPath, JSON.toJSONString(paramMap), e);errorResult.initializeFail(ResultCodes.BUSINESS_ERROR.getCode(), targetMethodDesc + ": " + e.getMessage());} catch (SystemException e) {log.error("[接口][系统异常] {} {} {} {}, [参数] {}", targetClassDesc, targetMethodDesc, targetClassPath, methodPath, JSON.toJSONString(paramMap), e);errorResult.initializeFail(ResultCodes.INNER_ERROR.getCode(), targetMethodDesc + ": " + e.getMessage());} catch (Exception e) {log.error("[接口][未知异常] {} {} {} {}, [参数] {}", targetClassDesc, targetMethodDesc, targetClassPath, methodPath, JSON.toJSONString(paramMap), e);errorResult.initializeFail(ResultCodes.UNKNOW_ERROR.getCode(), targetMethodDesc + ": " + e.getMessage());}if (printReturn) {log.info("[接口] {} {} {} {}, [返回] {}", targetClassDesc, targetMethodDesc, targetClassPath, methodPath, JSON.toJSONString(Objects.nonNull(result) ? result : errorResult));}if (printTime) {log.info("[接口] {} {} {} {}, [耗时] {} {}", targetClassDesc, targetMethodDesc, targetClassPath, methodPath, stopwatch.elapsed(TimeUnit.MILLISECONDS), "ms");}if (!printReturn && !printReturn) {log.info("[接口] {} {} {} {}, >>>>>>>> end", targetClassDesc, targetMethodDesc, targetClassPath, methodPath);}return Objects.nonNull(result) ? result : errorResult;}}

建议

最好再使用@RestControllerAdvice + @ExceptionHandler 进行controller异常兜底,因为框架层的异常返回,上面aop是无法拦截的。

示例代码

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MultipartException;/*** 全局异常处理类** @yx8102 2019/10/28*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public MyResult<Void> exceptionHandler(Exception e) {MyResult<Void> result = new MyResult<>();result.initializeFail(ResultCodes.INNER_ERROR.getCode(), e.getMessage());log.error("系统异常. result {}", JSON.toJSONString(result), e);return result;}
}

参考

Spring aop获取目标对象,方法,接口上的注解

spring aop获取目标对象的方法对象(包括方法上的注解)

Spring AOP 之二:Pointcut注解表达式

AOP Aspect 统一日志、异常处理、数据格式

Spring AOP获取拦截方法的参数名称跟参数值

使用@ControllerAdvice/@RestControllerAdvice + @ExceptionHandler注解实现全局处理Controller层的异常

异常 try – finally 注意的地方

解决AOP切面在嵌套方法调用时不生效问题

spring Aop嵌套调用的解决办法

【Java】使用AOP进行异常处理与日志记录相关推荐

  1. ASP.NET Core 异常处理与日志记录

    ASP.NET Core 异常处理与日志记录 参考文章: (1)ASP.NET Core 异常处理与日志记录 (2)https://www.cnblogs.com/vipyoumay/p/783806 ...

  2. 基于 abp vNext 和 .NET Core 开发博客项目 - 异常处理和日志记录

    基于 abp vNext 和 .NET Core 开发博客项目 - 异常处理和日志记录 转载于:https://github.com/Meowv/Blog 在开始之前,我们实现一个之前的遗留问题,这个 ...

  3. 从零开始编写自己的C#框架(20)——框架异常处理及日志记录

    最近很忙,杂事也多,所以开发本框架也是断断续续的,终于在前两天将前面设定的功能都基本完成了,剩下一些小功能遗漏的以后发现再补上.接下来的章节主要都是讲解在本框架的基础上进行开发的小巧. 本框架主要有四 ...

  4. java aop注解日志记录_spring aop通过注解实现日志记录

    首先是几个概念:连接点(Joinpoint).切点(Pointcut).增强(Advice).切面(Aspect) 另外也要使用到注解. 需求:通过注解定义LogEnable.然后程序运行能够识别定义 ...

  5. Asp.NetCore依赖注入和管道方式的异常处理及日志记录

    前言     在业务系统,异常处理是所有开发人员必须面对的问题,在一定程度上,异常处理的能力反映出开发者对业务的驾驭水平:本章将着重介绍如何在 WebApi 程序中对异常进行捕获,然后利用 Nlog ...

  6. 使用AOP+Annotation实现操作日志记录

    先创建注解 OperInfo 1 @Target({ElementType.TYPE, ElementType.METHOD}) 2 @Retention(RetentionPolicy.RUNTIM ...

  7. Java=微信支付详解与日志记录详解

    一.二维码: (1)什么是二维码 二维码又称QR Code,QR全称Quick Response,是一个近几年来移动设备上超流行的一种编码方式,它比传统的Bar Code条形码能存更多的信息,也能表示 ...

  8. java log日志函数_Java 中的 AWS Lambda 函数日志记录 - AWS Lambda

    AWS 文档中描述的 AWS 服务或功能可能因区域而异.要查看适用于中国区域的差异,请参阅中国的 AWS 服务入门. 本文属于机器翻译版本.若本译文内容与英语原文存在差异,则一律以英文原文为准. Ja ...

  9. Spring Cloud 设置Feign的日志记录级别

    <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-c ...

  10. 日志记录的问题-翻译

    该篇为英文,自己google翻译. 原文地址:https://blog.codinghorror.com/the-problem-with-logging/ The Problem With Logg ...

最新文章

  1. 基于DirectUI搭建Windows窗体程序
  2. Windows——系统盘隐藏分区功能
  3. mysql添加两种数据类型_MySQL入门(二) 数据库数据类型详解
  4. CH - 0601 Genius ACM(倍增+归并排序)
  5. 电脑的ip地址经常变化_电脑网络:分分钟通俗了解网关、DNS、子网掩码、MAC地址、DHCP...
  6. Qt网络编程——TCP
  7. 前端面试之 判断 true == true 需要进行哪几步操作?
  8. multisim页面不够大_multisim小常识
  9. Pem私钥pkcs1和pkcs8之间互转
  10. 为什么仿宋字体打印出楷体_请问仿宋体和楷体有什么区别?
  11. 三乘三魔方教程,按步骤来肯定能搞出来
  12. 利用Android SAF(存储访问框架)进行游戏反和谐(伊甸园的骄傲)/Android data目录的访问限制
  13. java程序员创业需要_java程序员出路有哪些
  14. 计算投资指数基金的预期收益率
  15. ABR算法研究综述 | A Survey on Bitrate Adaptation Schemes for Streaming Media Over HTTP(IEEE COMST‘18)阅读笔记
  16. excel计算二元线性回归_谁说菜鸟不会数据分析(高级篇)及竞争力 excel②
  17. Linux编辑器-gcc/g++使用
  18. 电脑重装系统以后出现 error: unknown filesystem怎么办?
  19. ps命令 proc文件系统
  20. 'internalField' 和'boundaryField'的区别?【翻译】

热门文章

  1. JS设计模式(二)-Revealing Module(揭示模式)
  2. 乔布斯斯坦福毕业演讲,这是我听过最精彩的毕业演讲!
  3. 先手获胜逻辑题_最获胜的A / B测试结果是否令人误解?
  4. 湖南科技大学EDA作业
  5. 消除macbook桌面快捷键图标上的箭头
  6. 苹果计算机怎样恢复桌面,mac桌面整理_使Mac桌面恢复整洁的四种技巧
  7. RQ dashboard使用
  8. 摄影测量后方交会算法C#实现
  9. 太帅了!钟楚曦这件老爹裤A到炸裂,一般人可穿不出这种范
  10. 发了两个月传单,他转行做了程序员