1. 程式人生 > >spring中的多執行緒aop方法攔截

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方法