• 如何低侵入的记录调用日志


    前言

    前阵子朋友他老大叫他实现这么一个功能,就是低侵入的记录接口每次的请求响应日志,然后并统计每次请求调用的成功、失败次数以及响应耗时,当时朋友的实现思路是在每个业务的controller的方法上加一个自定义注解,然后写一个aop,以这个自定义注解为pointcut来记录日志。

    这种AOP+注解来实现日志记录,应该是很常见的实现方式。然而朋友在落地的时候,发现项目要加自定义注解的地方太多。后面我就跟他说,那就不写注解,直接以形如下

    execution(* com.github.lybgeek.logaop.service..*.*(..))
    

    这样不行吗?他说他这个功能他老大是希望给各个项目组使用,像我上面的方法,估计行不通,我就问他说为啥行不通,他说各个项目的包名都不一样,如果我那种思路,他就说这样在代码里poincut不得要这么写

    execution(* com.github.lybgeek.a.service..*.*(..) 
    || * com.github.lybgeek.b.service..*.*(..) || * com.github.lybgeek.c.service..*.*(..) )
    

    这样每次新加要日志记录,都得改切面代码,还不如用自定注解来的好。听完他的解释,我一脸黑人问号脸。于是就趁着5.1假期期间,写个demo实现上面的需求

    业务场景

    低侵入的记录接口每次的请求响应日志,然后并统计每次请求调用的成功、失败次数以及响应耗时

    这个业务需求应该算是很简单,实现的难点就在于低侵入,提到低侵入,我首先想到是使用者无需写代码,或者只需写少量代码或者仅需简单配置一下,最好能做到业务无感知。

    实现手段

    我这边提供2种思路

    • javaagent + byte-buddy
    • springboot自动装配 + AOP

    javaagent

    1、什么是javaagent

    javaagent是一个简单优雅的java agent,利用java自带的instrument特性+javassist/byte-buddy字节码可以实现对类的拦截或者增强。

    javaAgent 是运行在 main方法之前的拦截器,它内定的方法名叫 premain ,也就是说先执行 premain 方法然后再执行 main 方法

    2、如何实现一个javaagent

    • a、必须实现premain方法

    示例:

    public class AgentDemo {
    
        public static void premain(String agentArgs, Instrumentation inst) {
            System.out.println("agentArgs : " + agentArgs);
            inst.addTransformer(new DefineTransformer(),true);
        }
    
        static class DefineTransformer implements ClassFileTransformer {
    
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                System.out.println("premain load Class:" + className);
                return classfileBuffer;
            }
        }
    }
    
    • b、在META-INF目录添加MANIFEST.MF文档,内容形如下
    Manifest-Version: 1.0
    Implementation-Version: 0.0.1-SNAPSHOT
    Premain-Class: com.github.lybgeek.agent.ServiceLogAgent
    Can-Redefine-Classes: true
    

    其中Premain-Class是必选项。MANIFEST.MF可以利用maven插件进行生成,插件如下

     <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>3.2.0</version>
                    <configuration>
                        <archive>
                            <manifest>
                                <addClasspath>true</addClasspath>
                            </manifest>
                            <manifestEntries>
                                <Premain-Class>com.github.lybgeek.agent.ServiceLogAgent</Premain-Class>
                                <Agent-Class>com.github.lybgeek.agent.ServiceLogAgent</Agent-Class>
                                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                <Can-Retransform-Classes>true</Can-Retransform-Classes>
                            </manifestEntries>
                        </archive>
                    </configuration>
                </plugin>
    

    3、业务代码如何使用javagent

    java -javaagent:agentjar文件的位置 [= 传入 premain的参数 ] -jar 要运行的jar文件
    

    :-javaagent一定要在-jar之前,不然不会生效

    byte-buddy

    1、什么是byte-buddy

    Byte Buddy是一个JVM的运行时代码生成器,你可以利用它创建任何类,且不像JDK动态代理那样强制实现一个接口。Byte Buddy还提供了简单的API,便于手工、通过Java Agent,或者在构建期间修改字节码

    2、byte-buddy教程

    注: 如果再介绍byte-buddy使用,则篇幅会比较长,因此提供以下2个byte-buddy学习链接,感兴趣的朋友可以点击查看

    https://blog.gmem.cc/byte-buddy-study-note

    https://notes.diguage.com/byte-buddy-tutorial/

    如何利用javaagent + byte-buddy实现低侵入记录日志

    1、编写agent入口类

    public class ServiceLogAgent {
    
    
        public static String base_package_key = "agent.basePackage";
    
        public static void premain(String agentArgs, Instrumentation inst) {
            System.out.println("loaded agentArgs :" + agentArgs);
            Properties properties = PropertiesUtils.getProperties(agentArgs);
            ServiceLogHelperFactory serviceLogHelperFactory = new ServiceLogHelperFactory(properties);
            serviceLogHelperFactory.getServiceLogHelper().initTable();
    
            AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {
                return builder
                        .method(ElementMatchers.<MethodDescription>any()) // 拦截任意方法
                        .intercept(MethodDelegation.to(new ServiceLogInterceptor(serviceLogHelperFactory))); // 委托
            };
    
            AgentBuilder.Listener listener = new AgentBuilder.Listener() {
                private Log log = LogFactory.getLog(AgentBuilder.Listener.class);
    
                @Override
                public void onDiscovery(String s, ClassLoader classLoader, JavaModule javaModule, boolean b) {
                }
    
                @Override
                public void onTransformation(TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule, boolean b, DynamicType dynamicType) {
                }
    
                @Override
                public void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule, boolean b) {
                }
    
                @Override
                public void onError(String s, ClassLoader classLoader, JavaModule javaModule, boolean b, Throwable throwable) {
                    log.error(throwable.getMessage(),throwable);
                }
    
                @Override
                public void onComplete(String s, ClassLoader classLoader, JavaModule javaModule, boolean b) {
                }
    
            };
    
            new AgentBuilder
                    .Default()
                    // 指定需要拦截的类
                    .type(ElementMatchers.nameStartsWith(properties.getProperty(base_package_key)))
                    .and(ElementMatchers.isAnnotatedWith(Service.class))
                    .transform(transformer)
                    .with(listener)
                    .installOn(inst);
        }
    
    
    }
    

    2、编写拦截器

    public class ServiceLogInterceptor {
        private Log log = LogFactory.getLog(ServiceLogInterceptor.class);
    
    
        private ServiceLogHelperFactory serviceLogHelperFactory;
    
        public ServiceLogInterceptor(ServiceLogHelperFactory serviceLogHelperFactory) {
            this.serviceLogHelperFactory = serviceLogHelperFactory;
        }
    
        @RuntimeType
        public Object intercept(@AllArguments Object[] args, @Origin Method method, @SuperCall Callable<?> callable) {
            long start = System.currentTimeMillis();
            long costTime = 0L;
            String status = ServiceLog.SUCEESS;
            Object result = null;
            String respResult = null;
            try {
                // 原有函数执行
                result = callable.call();
                respResult = JsonUtils.object2json(result);
            } catch (Exception e){
                log.error(e.getMessage(),e);
                status = ServiceLog.FAIL;
                respResult = e.getMessage();
            } finally{
                costTime = System.currentTimeMillis() - start;
                saveLog(args, method, costTime, status, respResult);
            }
            return result;
        }
    
        private void saveLog(Object[] args, Method method, long costTime, String status, String respResult) {
            if(!isSkipLog(method)){
                ServiceLog serviceLog = serviceLogHelperFactory.createServiceLog(args,method);
                serviceLog.setCostTime(costTime);
                serviceLog.setRespResult(respResult);
                serviceLog.setStatus(status);
                ServiceLogHelper serviceLogHelper = serviceLogHelperFactory.getServiceLogHelper();
                serviceLogHelper.saveLog(serviceLog);
            }
        }
    
    
        private boolean isSkipLog(Method method){
            ServiceLogProperties serviceLogProperties = serviceLogHelperFactory.getServiceLogProperties();
            List<String> skipLogServiceNameList = serviceLogProperties.getSkipLogServiceNameList();
            if(!CollectionUtils.isEmpty(skipLogServiceNameList)){
                String currentServiceName = method.getDeclaringClass().getName() + ServiceLogProperties.CLASS_METHOD_SPITE + method.getName();
                return skipLogServiceNameList.contains(currentServiceName);
            }
            return false;
        }
    
    
    
    }
    
    

    3、通过maven将agent打包成jar

    4、效果演示

    首先idea在启动类的vm参数,加入形如下内容

     -javaagent:F:springboot-learningspringboot-agentspringboot-javaagent-log	argetagent-log.jar=F:springboot-learningspringboot-agentspringboot-javaagent-log	argetclassesagent.properties
    

    效果图
    image.png
    image.png
    image.png

    如何利用自动装配+AOP实现低侵入记录日志

    注: 其实朋友那种方式也差不多可以了,只需把poincut的外移到配置文件文件即可

    1、编写切面

    @Slf4j
    public class ServiceLogAdvice implements MethodInterceptor {
    
        private LogService logService;
    
        public ServiceLogAdvice(LogService logService) {
            this.logService = logService;
        }
    
        @Override
        public Object invoke(MethodInvocation invocation)  {
    
            long start = System.currentTimeMillis();
            long costTime = 0L;
            String status = ServiceLog.SUCEESS;
            Object result = null;
            String respResult = null;
            try {
                // 原有函数执行
                result = invocation.proceed();
                respResult = JSON.toJSONString(result);
            } catch (Throwable e){
                log.error(e.getMessage(),e);
                status = ServiceLog.FAIL;
                respResult = e.getMessage();
            } finally{
                costTime = System.currentTimeMillis() - start;
                saveLog(invocation.getArguments(), invocation.getMethod(), costTime, status, respResult);
            }
            return result;
    
        }
    
        private void saveLog(Object[] args, Method method, long costTime, String status, String respResult) {
                ServiceLog serviceLog = ServiceLog.builder()
                                        .serviceName(method.getDeclaringClass().getName())
                                        .costTime(costTime)
                                        .methodName(method.getName())
                                        .status(status)
                                        .reqArgs(JSON.toJSONString(args))
                                        .respResult(respResult).build();
               logService.saveLog(serviceLog);
        }
    }
    
    

    2、注入切面bean

     @Bean
        @ConditionalOnMissingBean
        public AspectJExpressionPointcutAdvisor serviceLogAspectJExpressionPointcutAdvisor(AopLogProperties aopLogProperties) {
            AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
            advisor.setExpression(aopLogProperties.getPointcut());
            advisor.setAdvice(serviceLogAdvice());
            return advisor;
        }
    

    3、编写自动装配类

    @Configuration
    @EnableConfigurationProperties(AopLogProperties.class)
    @ConditionalOnProperty(prefix = "servicelog",name = "enabled",havingValue = "true",matchIfMissing = true)
    public class AopLogAutoConfiguration {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Bean
        @ConditionalOnMissingBean
        public LogService logService(){
            return new LogServiceImpl(jdbcTemplate);
        }
    
        @Bean
        @ConditionalOnMissingBean
        public ServiceLogAdvice serviceLogAdvice(){
            return new ServiceLogAdvice(logService());
        }
    
        @Bean
        @ConditionalOnMissingBean
        public AspectJExpressionPointcutAdvisor serviceLogAspectJExpressionPointcutAdvisor(AopLogProperties aopLogProperties) {
            AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
            advisor.setExpression(aopLogProperties.getPointcut());
            advisor.setAdvice(serviceLogAdvice());
            return advisor;
        }
    
    
    }
    
    

    4、编写spring.factories

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=
    	com.github.lybgeek.logaop.config.AopLogAutoConfiguration
    

    5、效果演示

    在业务代码做如下配置

    • 5.1 在pom.xml引入starter
      <dependency>
                <groupId>com.github.lybgeek</groupId>
                <artifactId>aoplog-springboot-starter</artifactId>
                <version>0.0.1-SNAPSHOT</version>
            </dependency>
    
    • 5.2 在yml文件中配置pointcut
    servicelog:
      pointcut: execution(* com.github.lybgeek.mock.service.client..*.*(..))
      enabled: true
    
    • 5.3 效果图

    image.png
    在这里插入图片描述

    总结

    以上主要列举了通过javaagent和aop加自动装配2两种方式来实现低侵入记录日志。其实这两种实现在一些开源的方案用得挺多的,比如byte-buddy在skywalking和arthas就有使用到,比如MethodInterceptor 在spring事务中就有用到。所以多看些源码,在设计方案时,有时候会产生意想不到的火花

    demo链接

    https://github.com/lyb-geek/springboot-learning/tree/master/springboot-agent

  • 相关阅读:
    BUUCTF | [极客大挑战 2019]PHP
    BUUCTF | [极客大挑战 2019]Secret File
    [网鼎杯 2018] Fakebook 复现
    [强网杯2019 随便注]总结
    [Flask(Jinja2)服务端模板注入漏洞(SSTI)]学习简记
    [Flask框架]学习简记
    ADO.NET基础
    SQL Server基础
    AdventureWorks2012下载链接
    SQL Server2012从入门到精通
  • 原文地址:https://www.cnblogs.com/linyb-geek/p/14732680.html
Copyright © 2020-2023  润新知