• spring中的多线程aop方法拦截


    日常开发中,常用spring的aop机制来拦截方法,记点日志、执行结果、方法执行时间啥的,很是方便,比如下面这样:(以spring-boot项目为例)

    一、先定义一个Aspect

    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;
    
    @Aspect
    @Component("logAspect")
    public class LogAspect {
    
    
        @Pointcut("execution(* com.cnblogs.yjmyzz..service..*(..))")
        private void logPointCut() {
        }
    
        @Around("logPointCut()")
        public Object doAround(ProceedingJoinPoint pjp) {
            Object result = null;
            StringBuilder sb = new StringBuilder();
            long start = 0;
            try {
                //记录线程id、方法签名
                sb.append("thread:" + Thread.currentThread().getId() + ", method:" + pjp.getSignature() + ",");
                //记录参数
                if (pjp.getArgs() != null) {
                    sb.append("args:");
                    for (int i = 0; i < pjp.getArgs().length; i++) {
                        sb.append("[" + i + "]" + pjp.getArgs()[i] + ",");
                    }
                }
                start = System.currentTimeMillis();
                result = pjp.proceed();
                //记录返回结果
                sb.append("result:" + result);
            } catch (Throwable e) {
                sb.append(",error:" + e.getMessage());
                throw e;
            } finally {
                long elapsedTime = System.currentTimeMillis() - start;
                //记录执行时间
                sb.append(",elapsedTime:" + elapsedTime + "ms");
                System.out.println(sb.toString());
                return result;
            }
        }
    
    }
    

      

    二、定义一个service

    import org.springframework.stereotype.Service;
    
    @Service("sampleService")
    public class SampleService {
    
        public String hello(String name) {
            return "你好," + name;
        }
    
    }
    

      

    三、跑一把

    @SpringBootApplication
    @EnableAspectJAutoProxy
    @ComponentScan(basePackages = {"com.cnblogs.yjmyzz"})
    public class AopThreadApplication {
    
        public static void main(String[] args) throws InterruptedException {
            ApplicationContext context = SpringApplication.run(AopThreadApplication.class, args);
            SampleService sampleService = context.getBean(SampleService.class);
    
            System.out.println("main thread:" + Thread.currentThread().getId());
    
            System.out.println(sampleService.hello("菩提树下的杨过"));
            System.out.println();
    
        }
    }
    

    输出:

    main thread:1
    thread:1, method:String com.cnblogs.yjmyzz.aop.thread.service.SampleService.hello(String),args:[0]菩提树下的杨过,result:你好,菩提树下的杨过,elapsedTime:6ms
    你好,菩提树下的杨过
    

    第2行即aop拦截后输出的内容。但有些时候,我们会使用多线程来调用服务,这时候aop还能不能拦到呢?

    四、多线程

    4.1 场景1:Runnable中传入了Spring上下文

    public class RunnableA implements Runnable {
    
        private ApplicationContext context;
    
        public RunnableA(ApplicationContext context) {
            this.context = context;
        }
    
        @Override
        public void run() {
            SampleService sampleService = context.getBean(SampleService.class);
            System.out.println("thread:" + Thread.currentThread().getId() + "," + sampleService.hello("菩提树下的杨过-2"));
        }
    }
    

    把刚才的main方法,改成用线程池调用(即:多线程)

        public static void main(String[] args) throws InterruptedException {
            ApplicationContext context = SpringApplication.run(AopThreadApplication.class, args);
    
            System.out.println("main thread:" + Thread.currentThread().getId());
            System.out.println();
    
            ExecutorService executorService = Executors.newSingleThreadExecutor();
            executorService.submit(new RunnableA(context));
        }
    

    输出如下:

    main thread:1
    thread:23, method:String com.cnblogs.yjmyzz.aop.thread.service.SampleService.hello(String),args:[0]菩提树下的杨过-2,result:你好,菩提树下的杨过-2,elapsedTime:4ms
    thread:23,你好,菩提树下的杨过-2
    

    很明显,仍然正常拦截到了,而且从线程id上看,确实是一个新线程。

    4.2 场景2:Runnable中没传入Spring上下文

    public class RunnableB implements Runnable {
    
        public RunnableB() {
        }
    
        @Override
        public void run() {
            SampleService sampleService = new SampleService();
            System.out.println("thread:" + Thread.currentThread().getId() + "," + sampleService.hello("菩提树下的杨过-2"));
        }
    }
    

    与RunnableA的区别在于,完全与spring上下文没有任何关系,服务实例是手动new出来的。

    修改main方法:

        public static void main(String[] args) throws InterruptedException {
            ApplicationContext context = SpringApplication.run(AopThreadApplication.class, args);
    
            System.out.println("main thread:" + Thread.currentThread().getId());
            System.out.println();
    
            ExecutorService executorService = Executors.newSingleThreadExecutor();
            executorService.submit(new RunnableB());
        }
    

    输出:

    main thread:1
    thread:22,你好,菩提树下的杨过-2
    

    全都是手动new出来的对象,与spring没半毛钱关系,aop不起作用也符合预期。这种情况下该怎么破?

    轮到CGLib出场了,其实spring的aop机制,跟它就有密切关系,大致原理:CGLib会从被代理的类,派生出一个子类,然后在子类中覆写所有非final的public方法,从而达到"方法增强"的效果。为此,我们需要写一个代理类:

    import net.sf.cglib.proxy.MethodInterceptor;
    import net.sf.cglib.proxy.MethodProxy;
    import org.apache.commons.lang3.ArrayUtils;
    
    import java.lang.reflect.Method;
    
    public class AopProxy implements MethodInterceptor {
    
        private final static int MAX_LEVEL = 3;
        private final static String DOT = ".";
    
        public static String getMethodName(Method method) {
            if (method == null) {
                return null;
            }
            String[] arr = method.toString().split(" ");
            String methodName = arr[2].split("\(")[0] + "()";
            String[] arr2 = methodName.split("\.");
            if (arr2.length > MAX_LEVEL) {
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < arr2.length; i++) {
                    if (i <= MAX_LEVEL) {
                        sb.append(arr2[i].substring(0, 1) + DOT);
                    } else {
                        sb.append(arr2[i] + DOT);
                    }
                }
                String temp = sb.toString();
                if (temp.endsWith(DOT)) {
                    temp = temp.substring(0, temp.length() - 1);
                }
                return temp;
            }
            return methodName;
        }
    
        @Override
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
            StringBuilder sb = new StringBuilder();
            Object result = null;
            long start = System.currentTimeMillis();
            boolean hasError = false;
            try {
                sb.append("thread[" + Thread.currentThread().getId() + "] " + getMethodName(method) + " =>args:");
                if (ArrayUtils.isNotEmpty(objects)) {
                    for (int i = 0; i < objects.length; i++) {
                        sb.append("[" + i + "]" + objects[i].toString() + ",");
                    }
                } else {
                    sb.append("null,");
                }
                result = methodProxy.invokeSuper(o, objects);
                sb.append(" result:" + result);
            } catch (Exception e) {
                sb.append(", error:" + e.getMessage());
                hasError = true;
            } finally {
                long execTime = System.currentTimeMillis() - start;
                sb.append(", execTime:" + execTime + " ms");
            }
            System.out.println(sb.toString());
            return result;
        }
    }
    

    关键点都在intercept方法里,被代理的类有方法调用时,在intercept中处理拦截逻辑,为了方便使用这个代理类,再写一个小工具:

    import net.sf.cglib.proxy.Enhancer;
    
    public class ProxyUtils {
    
        /**
         * 创建代理对象实例
         *
         * @param type
         * @param <T>
         * @return
         */
        public static <T> T createProxyObject(Class<T> type) {
            AopProxy factory = new AopProxy();
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(type);
            enhancer.setCallback(factory);
            //注意:被代理的类,必须有默认无参的空构造函数
            T instance = (T) enhancer.create();
            return instance;
        }
    }
    

    有了它就好办了:

    public class RunnableB implements Runnable {
    
        public RunnableB() {
        }
    
        @Override
        public void run() {
            //注:这里改成用CGLib来创建目标的代理类实例
            SampleService sampleService = ProxyUtils.createProxyObject(SampleService.class);
            System.out.println("thread:" + Thread.currentThread().getId() + "," + sampleService.hello("菩提树下的杨过-2"));
        }
    }
    

    手动new的地方,改成用ProxyUtils生成代理类实例,还是跑刚才的main方法:

    main thread:1
    thread[24] c.c.y.a.thread.service.SampleService.hello() =>args:[0]菩提树下的杨过-2, result:你好,菩提树下的杨过-2, execTime:9 ms
    thread:24,你好,菩提树下的杨过-2
    

    第2行的输出,便是AopProxy类拦截的输出,成功拦截,皆大欢喜! 

    注意事项:

    1. 被代理的类,不能是内部类(即嵌套在类中的类),更不能是final类

    2. 要拦截的方法,不能是private方法或final方法

    附示例源码: https://github.com/yjmyzz/aop-multi-thread-demo

  • 相关阅读:
    解决tomcat CNTA-2020-0004漏洞
    简单理解程序如何控制硬件的
    【Spring】Spring Session的简单搭建与源码阅读
    【Spring】Spring,我的零散使用杂记
    CORS跨域、Cookie传递SessionID实现单点登录后的权限认证的移动端兼容性测试报告
    【Fiddler】Fiddler抓包
    一起学习Maven
    分布式系统接口调用回滚方案
    【FastDFS】FastDFS在CentOS的搭建
    【Java】Java日志框架Logback的简单例子
  • 原文地址:https://www.cnblogs.com/yjmyzz/p/how-to-use-aop-in-multi-thread-spring-application.html
Copyright © 2020-2023  润新知