当前位置:

首页 > 编程开发 > Java如何实现方法调用日志记录?

Java如何实现方法调用日志记录?

1.背景SpringBoot项目中,之前都是在controller方法的第一行手动打印log,return之前再打印返回值。有多个返回点时,就需要出现多少重复代码,过多的非业务代码显得十分凌乱。本文将采用AOP配置自定义注解实现入参、出参的日志打印(方法的入参和返回值都采用fastjson序列化)。2.设计思路将特定包下所有的controller生成代理类对象,并交由Spring容器管理,并重写invoke方法进行增强(入参、出参的打印).3.核心代码3.1自定义注解@Target(ElementType

本文目录

      1.背景   

      SpringBoot项目中,之前都是在controller方法的第一行手动打印 log,return之前再打印返回值。有多个返回点时,就需要出现多少重复代码,过多的非业务代码显得十分凌乱。

      本文将采用AOP 配置自定义注解实现 入参、出参的日志打印(方法的入参和返回值都采用 fastjson 序列化)。

      2.设计思路

      将特定包下所有的controller生成代理类对象,并交由Spring容器管理,并重写invoke方法进行增强(入参、出参的打印).

      3.核心代码

      3.1 自定义注解

      @Target(ElementType.TYPE)
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      @Import({InteractRecordBeanPostProcessor.class})
      public @interface EnableInteractRecord {
      
          /**
           * app对应controller包名
           */
          String[] basePackages() default {};
      
          /**
           * 排除某些包
           */
          String[] exclusions() default {};
      
      }

      3.2 实现BeanFactoryPostProcessor接口

      作用:获取EnableInteractRecord注解对象,用于获取需要创建代理对象的包名,以及需要排除的包名

      @Component
      public class InteractRecordFactoryPostProcessor implements BeanFactoryPostProcessor {
      
          private static Logger logger = LoggerFactory.getLogger(InteractRecordFactoryPostProcessor.class);
      
          private EnableInteractRecord enableInteractRecord;
      
          @Override
          public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
              try {
                  String[] names = beanFactory.getBeanNamesForAnnotation(EnableInteractRecord.class);
                  for (String name : names) {
                      enableInteractRecord = beanFactory.findAnnotationOnBean(name, EnableInteractRecord.class);
                      logger.info("开启交互记录 ", enableInteractRecord);
                  }
              } catch (Exception e) {
                  logger.error("postProcessBeanFactory() Exception ", e);
              }
          }
      
          public EnableInteractRecord getEnableInteractRecord() {
              return enableInteractRecord;
          }
      
      }

      3.3 实现MethodInterceptor编写打印日志逻辑

      作用:进行入参、出参打印,包含是否打印逻辑

      @Component
      public class ControllerMethodInterceptor implements MethodInterceptor {
          private static Logger logger = LoggerFactory.getLogger(ControllerMethodInterceptor.class);
          // 请求开始时间
          ThreadLocal startTime = new ThreadLocal<>();
          private String localIp = "";
      
          @PostConstruct
          public void init() {
              try {
                  localIp = InetAddress.getLocalHost().getHostAddress();
              } catch (UnknownHostException e) {
                  logger.error("本地IP初始化失败 : ", e);
              }
          }
      
          @Override
          public Object invoke(MethodInvocation invocation) {
              pre(invocation);
              Object result;
              try {
                  result = invocation.proceed();
                  post(invocation, result);
                  return result;
              } catch (Throwable ex) {
                  logger.error("controller 执行异常: ", ex);
                  error(invocation, ex);
              }
      
              return null;
      
          }
      
          public void error(MethodInvocation invocation, Throwable ex) {
              String msgText = ex.getMessage();
              logger.info(startTime.get() + " 异常,请求结束");
              logger.info("RESPONSE : " + msgText);
              logger.info("SPEND TIME : " + (System.currentTimeMillis() - startTime.get()));
          }
      
          private void pre(MethodInvocation invocation) {
              long now = System.currentTimeMillis();
              startTime.set(now);
              logger.info(now + " 请求开始");
              ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
              HttpServletRequest request = attributes.getRequest();
      
              logger.info("URL : " + request.getRequestURL().toString());
              logger.info("HTTP_METHOD : " + request.getMethod());
              logger.info("REMOTE_IP : " + getRemoteIp(request));
              logger.info("LOCAL_IP : " + localIp);
              logger.info("METHOD : " + request.getMethod());
              logger.info("CLASS_METHOD : " + getTargetClassName(invocation) + "." + invocation.getMethod().getName());
      
              // 获取请求头header参数
              Map map = new HashMap();
              Enumeration headerNames = request.getHeaderNames();
              while (headerNames.hasMoreElements()) {
                  String key = (String) headerNames.nextElement();
                  String value = request.getHeader(key);
                  map.put(key, value);
              }
              logger.info("HEADERS : " + JSONObject.toJSONString(map));
              Date createTime = new Date(now);
              // 请求报文
              Object[] args = invocation.getArguments();// 参数
              String msgText = "";
              Annotation[][] annotationss = invocation.getMethod().getParameterAnnotations();
      
              for (int i = 0; i < args.length; i++) {
                  Object arg = args[i];
                  if (!(arg instanceof ServletRequest)
                          && !(arg instanceof ServletResponse)
                          && !(arg instanceof Model)) {
                      RequestParam rp = null;
                      Annotation[] annotations = annotationss[i];
                      for (Annotation annotation : annotations) {
                          if (annotation instanceof RequestParam) {
                              rp = (RequestParam) annotation;
                          }
                      }
                      if (msgText.equals("")) {
                          msgText += (rp != null ? rp.value() + " = " : " ") + JSONObject.toJSONString(arg);
                      } else {
                          msgText += "," + (rp != null ? rp.value() + " = " : " ") + JSONObject.toJSONString(arg);
                      }
                  }
              }
              logger.info("PARAMS : " + msgText);
          }
      
          private void post(MethodInvocation invocation, Object result) {
              logger.info(startTime.get() + " 请求结束");
              if (!(result instanceof ModelAndView)) {
                  String msgText = JSONObject.toJSONString(result);
                  logger.info("RESPONSE : " + msgText);
              }
              logger.info("SPEND TIME : " + (System.currentTimeMillis() - startTime.get()));
      
          }
      
      
          private String getRemoteIp(HttpServletRequest request) {
              String remoteIp = null;
              String remoteAddr = request.getRemoteAddr();
              String forwarded = request.getHeader("X-Forwarded-For");
              String realIp = request.getHeader("X-Real-IP");
              if (realIp == null) {
                  if (forwarded == null) {
                      remoteIp = remoteAddr;
                  } else {
                      remoteIp = remoteAddr + "/" + forwarded.split(",")[0];
                  }
              } else {
                  if (realIp.equals(forwarded)) {
                      remoteIp = realIp;
                  } else {
                      if (forwarded != null) {
                          forwarded = forwarded.split(",")[0];
                      }
                      remoteIp = realIp + "/" + forwarded;
                  }
              }
              return remoteIp;
          }
      
          private String getTargetClassName(MethodInvocation invocation) {
              String targetClassName = "";
              try {
                  targetClassName = AopTargetUtils.getTarget(invocation.getThis()).getClass().getName();
              } catch (Exception e) {
                  targetClassName = invocation.getThis().getClass().getName();
              }
              return targetClassName;
          }
      
      }

      AopTargetUtils:

      public class AopTargetUtils {  
        
            
          /** 
           * 获取 目标对象 
           * @param proxy 代理对象 
           * @return  
           * @throws Exception 
           */  
          public static Object getTarget(Object proxy) throws Exception {  
                
              if(!AopUtils.isAopProxy(proxy)) {
                  return proxy;//不是代理对象  
              }  
                
              if(AopUtils.isJdkDynamicProxy(proxy)) {
                  return getJdkDynamicProxyTargetObject(proxy);  
              } else { //cglib  
                  return getCglibProxyTargetObject(proxy);  
              }  
                
                
                
          }  
        
        
          private static Object getCglibProxyTargetObject(Object proxy) throws Exception {  
              Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");  
              h.setAccessible(true);
              Object dynamicAdvisedInterceptor = h.get(proxy);  
                
              Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");  
              advised.setAccessible(true);  
                
              Object target = ((AdvisedSupport)advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();
                
              return getTarget(target);
          }  
        
        
          private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {  
              Field h = proxy.getClass().getSuperclass().getDeclaredField("h");  
              h.setAccessible(true);  
              AopProxy aopProxy = (AopProxy) h.get(proxy);
                
              Field advised = aopProxy.getClass().getDeclaredField("advised");  
              advised.setAccessible(true);  
                
              Object target = ((AdvisedSupport)advised.get(aopProxy)).getTargetSource().getTarget();
                
              return getTarget(target); 
          }  
            
      }

      3.4 实现BeanPostProcessor接口

      作用:筛选出需要生成代理的类,并生成代理类,返回给Spring容器管理。

      public class InteractRecordBeanPostProcessor implements BeanPostProcessor {
      
          private static Logger logger = LoggerFactory.getLogger(InteractRecordBeanPostProcessor.class);
      
          @Autowired
          private InteractRecordFactoryPostProcessor interactRecordFactoryPostProcessor;
      
          @Autowired
          private ControllerMethodInterceptor controllerMethodInterceptor;
      
          private String BASE_PACKAGES[];//需要拦截的包
      
          private String EXCLUDING[];// 过滤的包
      
          //一层目录匹配
          private static final String ONE_REGEX = "[a-zA-Z0-9_]+";
      
          //多层目录匹配
          private static final String ALL_REGEX = ".*";
      
          private static final String END_ALL_REGEX = "*";
      
          @PostConstruct
          public void init() {
              EnableInteractRecord ir = interactRecordFactoryPostProcessor.getEnableInteractRecord();
              BASE_PACKAGES = ir.basePackages();
              EXCLUDING = ir.exclusions();
          }
      
          @Override
          public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
              try {
                  if (interactRecordFactoryPostProcessor.getEnableInteractRecord() != null) {
                      // 根据注解配置的包名记录对应的controller层
                      if (BASE_PACKAGES != null && BASE_PACKAGES.length > 0) {
                          Object proxyObj = doEnhanceForController(bean);
                          if (proxyObj != null) {
                              return proxyObj;
                          }
                      }
                  }
              } catch (Exception e) {
                  logger.error("postProcessAfterInitialization() Exception ", e);
              }
              return bean;
          }
      
          @Override
          public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
              return bean;
          }
      
          private Object doEnhanceForController(Object bean) {
              String beanPackageName = getBeanPackageName(bean);
              if (StringUtils.isNotBlank(beanPackageName)) {
                  for (String basePackage : BASE_PACKAGES) {
                      if (matchingPackage(basePackage, beanPackageName)) {
                          if (EXCLUDING != null && EXCLUDING.length > 0) {
                              for (String excluding : EXCLUDING) {
                                  if (matchingPackage(excluding, beanPackageName)) {
                                      return bean;
                                  }
                              }
                          }
                          Object target = null;
                          try {
                              target = AopTargetUtils.getTarget(bean);
                          } catch (Exception e) {
                              logger.error("AopTargetUtils.getTarget() exception", e);
                          }
                          if (target != null) {
                              boolean isController = target.getClass().isAnnotationPresent(Controller.class);
                              boolean isRestController = target.getClass().isAnnotationPresent(RestController.class);
                              if (isController || isRestController) {
                                  ProxyFactory proxy = new ProxyFactory();
                                  proxy.setTarget(bean);
                                  proxy.addAdvice(controllerMethodInterceptor);
                                  return proxy.getProxy();
                              }
                          }
                      }
                  }
      
              }
              return null;
          }
      
          private static boolean matchingPackage(String basePackage, String currentPackage) {
              if (StringUtils.isEmpty(basePackage) || StringUtils.isEmpty(currentPackage)) {
                  return false;
              }
              if (basePackage.indexOf("*") != -1) {
                  String patterns[] = StringUtils.split(basePackage, ".");
                  for (int i = 0; i < patterns.length; i++) {
                      String patternNode = patterns[i];
                      if (patternNode.equals("*")) {
                          patterns[i] = ONE_REGEX;
                      }
                      if (patternNode.equals("**")) {
                          if (i == patterns.length - 1) {
                              patterns[i] = END_ALL_REGEX;
                          } else {
                              patterns[i] = ALL_REGEX;
                          }
                      }
                  }
                  String basePackageRegex = StringUtils.join(patterns, "\\.");
                  Pattern r = Pattern.compile(basePackageRegex);
                  Matcher m = r.matcher(currentPackage);
                  return m.find();
              } else {
                  return basePackage.equals(currentPackage);
              }
          }
      
          private String getBeanPackageName(Object bean) {
              String beanPackageName = "";
              if (bean != null) {
                  Class beanClass = bean.getClass();
                  if (beanClass != null) {
                      Package beanPackage = beanClass.getPackage();
                      if (beanPackage != null) {
                          beanPackageName = beanPackage.getName();
                      }
                  }
              }
              return beanPackageName;
          }
      
      }

      3.5 启动类配置注解

      @EnableInteractRecord(basePackages = “com.test.test.controller”,exclusions = “com.test.demo.controller”)

      以上即可实现入参、出参日志统一打印,并且可以将特定的controller集中管理,并不进行日志的打印(及不进生成代理类)。

      4.出现的问题(及其解决办法)

      实际开发中,特定不需要打印日志的接口,无法统一到一个包下。大部分需要打印的接口,和不需要打印的接口,大概率会参杂在同一个controller中,根据以上设计思路,无法进行区分。

      解决办法:

      自定义排除入参打印注解

      @Target(ElementType.METHOD)
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      public @interface ExcludeReqLog {
      }

      自定义排除出参打印注解

      @Target(ElementType.METHOD)
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      public @interface ExcludeRespLog {
      }

      增加逻辑

      // 1.在解析requestParam之前进行判断
              Method method = invocation.getMethod();
              Annotation[] declaredAnnotations = method.getDeclaredAnnotations();
              boolean flag = true;
              for (Annotation annotation : declaredAnnotations) {
                  if (annotation instanceof ExcludeReqLog) {
                      flag = false;
                  }
              }
              if (!flag) {
                  logger.info("该方法已排除,不打印入参");
                  return;
              }
      // 2.在解析requestResp之前进行判断
              Method method = invocation.getMethod();
              Annotation[] declaredAnnotations = method.getDeclaredAnnotations();
              boolean flag = true;
              for (Annotation annotation : declaredAnnotations) {
                  if (annotation instanceof ExcludeRespLog) {
                      flag = false;
                  }
              }
              if (!flag) {
                  logger.info("该方法已排除,不打印出参");
                  return;
              }

      使用方法

      // 1.不打印入参
          @PostMapping("/uploadImg")
          @ExcludeReqLog
          public Result> uploadIdeaImg(@RequestParam(value = "imgFile", required = false) MultipartFile[] imgFile) {
              return demoService.uploadIdeaImg(imgFile);
          }
      //2.不打印出参
          @PostMapping("/uploadImg")
          @ExcludeRespLog 
          public Result> uploadIdeaImg(@RequestParam(value = "imgFile", required = false) MultipartFile[] imgFile) {
              return demoService.uploadIdeaImg(imgFile);
          }
      本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
      作者最新文章
      编程开发 Java
      相关文章 更多
      Java单元测试入门:使用JUnit 5编写第一个测试用例
      Java单元测试入门:使用JUnit 5编写第一个测试用例

      面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

      Java变量未初始化报错解析:局部变量与成员变量默认值差异
      Java变量未初始化报错解析:局部变量与成员变量默认值差异

      深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

      Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
      Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

      详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

      Java中final与static final常量的定义及初始化规则
      Java中final与static final常量的定义及初始化规则

      深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

      Java抽象方法与接口方法的区别及选型建议
      Java抽象方法与接口方法的区别及选型建议

      深入对比Java中抽象类抽象方法与接口方法的语法演进、设计意图及运行期差异,并提供清晰的架构选型决策流程与实战范例。

      Java实现HTTP请求与JSON解析的完整代码示例
      Java实现HTTP请求与JSON解析的完整代码示例

      本文提供Java标准库HttpClient发起HTTP GET/POST请求并结合Jackson进行JSON数据反序列化与序列化的完整代码示例与工程实践指南。

      Java递归出现StackOverflowError的原因分析及栈空间优化方案
      Java递归出现StackOverflowError的原因分析及栈空间优化方案

      详细分析Java递归调用出现StackOverflowError的原因,结合代码说明线程栈、栈帧和递归深度之间的关系,并介绍终止条件排查、迭代改写、减少栈帧开销以及通过-Xss调整JVM栈空间的方法。

      Java空指针异常(NPE)的代码排查与修复实例
      Java空指针异常(NPE)的代码排查与修复实例

      针对Java中常见的NullPointerException(NPE),本文从堆栈日志定位、典型代码触发场景、具体修复代码对比到防御体系进行系统讲解,提供实用且易上手的排查与修复方案。

      selenium上传文件教程本地文件路径设置方法
      selenium上传文件教程本地文件路径设置方法

      深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

      大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
      大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

      针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

      查看更多
      精品专题 更多
      装机必备
      装机必备

      正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

      Windows
      Windows

      正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

      PDF教程
      PDF教程

      正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

      Mac软件 更多
      讯飞智作 网页版
      讯飞智作 网页版
      Windows/ macOS/ Linux

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版
      Windows/ macOS/ Linux

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      Aperty macOS版
      Aperty macOS版
      macOS

      Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

      WINDOWS 更多
      讯飞智作 网页版
      讯飞智作 网页版
      Windows/ macOS/ Linux

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版
      Windows/ macOS/ Linux

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      PixTV 网页版
      PixTV 网页版
      Windows

      PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。