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

posted @ 2018-09-23 22:43  菩提树下的杨过  阅读(8666)  评论(1编辑  收藏  举报