日常开发中,常用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方法