日常開發中,常用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方法
