基础知识
AOP的3个实现层面
AOP就是面向切面编程,可以从3个层面来实现AOP
- 编译期:修改源代码。
- 字节码加载前:修改字节码。
- 字节码加载后:动态创建代理类的字节码。
AOP的3个实现层面
实现机制比较
AOP的基础
Joinpoint
拦截点,如某个业务方法。Pointcut
Joinpoint的表达式,表示拦截哪些方法。一个Pointcut对应多个Joinpoint。Advice
要切入的逻辑。
- Before Advice:在方法前切入。
- After Advice:在方法后切入,抛出异常时也会切入。
- After Returning Advice:在方法返回后切入,抛出异常则不会切入。
- After Throwing Advice:在方法抛出异常时切入。
- Around Advice:在方法执行前后切入,可以中断或忽略原有流程的执行。
JoinPoint、Pointcut、Advice之间的关系
织入器通过在切面中定义pointcut来搜索目标(被代理类)的JoinPoint(切入点),然后把要切入的逻辑(Advice)织入到目标对象里,生成代理类。动态字节码生成
原理
运行期间目标字节码加载后,生成目标类的子类,将切面逻辑加入到子类中。所以使用Cglib实现AOP不需要基于接口,Cglib底层是实现ASM的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39public class CglibAopDemo {
public static void main(String[] args) {
byteCodeGe();
}
public static void byteCodeGe() {
// 创建一个织入器
Enhancer enhancer = new Enhancer();
// 设置父类
enhancer.setSuperclass(Business.class);
// 设置需要织入的逻辑-InvocationHandler
enhancer.setCallback(new LogIntercept());
// 创建子类对象
IBusiness2 newBusiness = (IBusiness2) enhancer.create();
newBusiness.doSomeThing2();
}
/**
* 记录日志
*
* 类似InvocationHandler,织入新的逻辑
*
*/
public static class LogIntercept implements MethodInterceptor {
@Override
public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy)
throws Throwable {
// 执行原有逻辑
Object rev = proxy.invokeSuper(target, args);
// 执行织入的日志
if (method.getName().equals("doSomeThing2")) {
System.out.println("记录日志");
}
return rev;
}
}
}
字节码修改
在类加载到JVM之前直接修改某些类的方法,并将切入逻辑织入到这个方法里,然后将修改后的字节码文件交给虚拟机运行。
Javassist是一个编辑字节码的框架,可以很简单地操作字节码。它可以在运行期定义或修改Class。使用Javassist实现AOP的原理是在字节码加载前直接修改需要切入的方法。这比使用Cglib实现AOP更加高效(不用生成代理类),并且没太多限制。1
2
3
4
5
6
7
8
9
10
11
12
13public static void aop() throws NotFoundException, CannotCompileException, InstantiationException,
IllegalAccessException {
// 获取存放CtClass的容器ClassPool
ClassPool cp = ClassPool.getDefault();
// 获取class
CtClass cc = cp.get("model.Business");
// 获得指定方法名的方法
CtMethod m = cc.getDeclaredMethod("doSomeThing");
// 在方法执行前插入代码
m.insertBefore("{ System.out.println(\"记录日志\"); }");
// 执行方法
((Business) cc.toClass().newInstance()).doSomeThing();
}
字节码转换器
使用Instrumentation
,它是Java 5提供的新特性,使用Instrumentation
,可以构建一个字节码转换器,在字节码加载前进行转换。本节使用Instrumentation
和Javassist来实现AOP。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42/**
* jdk自带的字节码转换器
*/
public class MyClassFileTransformer implements ClassFileTransformer {
/**
* 字节码加载到虚拟机前会进入这个方法
*/
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println(className);
// 如果加载Business类才拦截
if (!"model/Business".equals(className)) {
return null;
}
// javassist的包名是用点分割的,需要转换下
if (className != null && className.indexOf("/") != -1) {
className = className.replaceAll("/", ".");
}
try {
CtClass cc = ClassPool.getDefault().get(className);
CtMethod m = cc.getDeclaredMethod("doSomeThing");
m.insertBefore("{ System.out.println(\"记录日志\"); }");
return cc.toBytecode();
} catch (NotFoundException e) {
} catch (CannotCompileException e) {
} catch (IOException e) {
// 忽略异常处理
}
return null;
}
/**
* 注册转换器
*/
public static void premain(String options, Instrumentation ins) {
// 注册我自己的字节码转换器
ins.addTransformer(new MyClassFileTransformer());
}
}
1 | public static void main(String[] args) { |
配置
需要告诉JVM在启动main()
之前,需要先执行premain()
(注册转换器)。首先需要将premain()
所在的类打成jar。并修改该jar里的META-INF\MANIFEST.MF
文件。1
2Manifest-Version: 1.0
Premain-Class: bci.MyClassFileTransformer
然后在JVM的启动参数里加上:1
-javaagent:D:\java\projects\opencometProject\Aop\lib\aop.jar
执行结果
1 | model/Business |