AOP与JAVA动态代理
本文内容纲要:AOP与JAVA动态代理" title="动态代理">动态代理
1、AOP的各种实现
AOP就是面向切面编程,我们可以从以下几个层面来实现AOP
- 在编译期修改源代码
- 在运行期字节码加载前修改字节码
- 在运行期字节码加载后动态创建代理类的字节码
2、AOP各种实现机制的比较
以下是各种实现机制的比较:
类别 | 机制 | 原理 | 优点 | 缺点 |
---|---|---|---|---|
静态AOP | 静态织入 | 在编译期,切面直接以字节码的形式编译到目标字节码文件中 | 对系统无性能影响 | 灵活性不够 |
动态AOP | 动态代理 | 在运行期,目标类加载后,为接口动态生成代理类,将切面织入到代理类中 | 相对于静态AOP更加灵活 | 切入的关注点需要实现接口。 对系统有一点性能影响 |
动态字节码生成 | CGLIB | 在运行期,目标类加载后,动态构建字节码文件生成目标类的子类,将切面逻辑加入到子类中 | 没有接口也可以织入 | 扩展类的实例方法为final时,则无法进行织入 |
自定义类加载器 | 在运行期,目标加载前,将切面逻辑加到目标字节码里 | 可以对绝大部分类进行织入 | 代码中如果使用了其他类加载器,则这些类将不会被织入 | |
字节码转换 | 在运行期,所有类加载器加载字节码前进行拦截 | 可以对所有类进行织入 |
3、AOP里的公民
- Joinpoint:拦截点,如某个业务方法
- Pointcut:Joinpoint的表达式,表示拦截哪些方法。一个Pointcut对应多个Joinpoint
- Advice:要切入的逻辑
- Before Advice:在方法前切入
- After Advice:在方法后切入,抛出异常则不会切入
- After Returning Advice:在方法返回后切入,抛出异常则不会切入
- After Throwing Advice:在方法抛出异常时切入
- Around Advice:在方法执行前后切入,可以中断或忽略原有流程的执行
- 公民之间的关系
织入器通过在切面中定义pointcout来搜索目标(被代理类)的JoinPoint(切入点),然后把要切入的逻辑(Advice)织入到目标对象里,生成代理类
4、AOP的实现机制
- 动态代理
- 动态字节码生成
- 自定义类加载器
- 字节码转换
4.1 动态代理
静态代理:由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了
动态代理:即在运行期动态创建代理类,使用动态代理实现AOP需要4个角色:
- 被代理的类:即AOP里所说的目标对象
- 被代理类的接口
- 织入器:使用接口反射机制生成一个代理类,在这个代理类中织入代码
- InvocationHandler切面:切面,包含了Advice和Pointcut
4.1.1 动态代理的演示
例子演示的是在方法执行前织入一段记录日志的代码,其中
Business是代理类
LogInvocationHandler是记录日志的切面
IBusiness、IBusiness2是代理类的接口
Proxy.newProxyInstance是织入器
public interface IBusiness {
void doSomeThing();
}
public interface IBusiness2 {
void doSomeThing2();
}
public class Business implements IBusiness, IBusiness2 {
@Override
public void doSomeThing() {
System.out.println("执行业务逻辑");
}
@Override
public void doSomeThing2() {
System.out.println("执行业务逻辑2");
}
}
package aop;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
打印日志的切面
*/
public class LogInvocationHandler implements InvocationHandler {
private Object target;//目标对象
public LogInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//执行织入的日志,你可以控制哪些方法执行切入逻辑
if (method.getName().equals("doSomeThing2")) {
System.out.println("记录日志");
}
//执行原有逻辑
Object recv = method.invoke(target, args);
return recv;
}
}
package aop;
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
//需要代理的类接口,被代理类实现的多个接口都必须在这这里定义
Class[] proxyInterface = new Class[] {IBusiness.class, IBusiness2.class};
//构建AOP的Advice,这里需要传入业务类的实例
LogInvocationHandler handler = new LogInvocationHandler(new Business());
//生成代理类的字节码加载器
ClassLoader classLoader = Business.class.getClassLoader();
//织入器,织入代码并生成代理类
IBusiness2 proxyBusiness = (IBusiness2) Proxy.newProxyInstance(classLoader, proxyInterface, handler);
proxyBusiness.doSomeThing2();
((IBusiness)proxyBusiness).doSomeThing();
}
}
执行结果:
记录日志
执行业务逻辑2
执行业务逻辑
4.1.2 动态代理的原理
本节将结合动态代理的源代码讲解其实现原理
动态代理的核心其实就是代理对象的生成,即Proxy.newProxyInstance(classLoader, proxyInterface, handler)
让我们进入newProxyInstance方法观摩下,核心代码就三行:
//获取代理类 Class cl = getProxyClass(loader, interfaces);
//获取带有InvocationHandler参数的构造方法
Constructor cons = cl.getConstructor(constructorParams);
//把handler传入构造方法生成实例
return (Object) cons.newInstance(new Object[] { h });
getProxyClass(loader, interfaces)方法用于获取代理类,它主要做了三件事情:
- 在当前类加载器的缓存里搜索是否有代理类
- 没有则生成代理
- 并缓存在本地JVM里
查找代理类getProxyClass(loader, interfaces)方法:
1 // 缓存的key使用接口名称生成的List 2 Object key = Arrays.asList(interfaceNames);
3 synchronized (cache) {
4 do {
5 Object value = cache.get(key);
6 // 缓存里保存了代理类的引用
7 if (value instanceof Reference) {
8 proxyClass = (Class) ((Reference) value).get();
9 }
10 if (proxyClass != null) {
11 // 代理类已经存在则返回
12 return proxyClass;
13 } else if (value == pendingGenerationMarker) {
14 // 如果代理类正在产生,则等待
15 try {
16 cache.wait();
17 } catch (InterruptedException e) {
18 }
19 continue;
20 } else {
21 //没有代理类,则标记代理准备生成
22 cache.put(key, pendingGenerationMarker);
23 break;
24 }
25 } while (true);
26 }
生成加载代理类:
//生成代理类的字节码文件并保存到硬盘中(默认不保存到硬盘) proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);
//使用类加载器将字节码加载到内存中
proxyClass = defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);
代理类生成过程ProxyGenerator.generateProxyClass()方法的核心代码分析:
//添加接口中定义的方法,此时方法体为空 for (int i = 0; i < this.interfaces.length; i++) {
localObject1 = this.interfaces[i].getMethods();
for (int k = 0; k < localObject1.length; k++) {
addProxyMethod(localObject1[k], this.interfaces[i]);
}
}
//添加一个带有InvocationHandler的构造方法
MethodInfo localMethodInfo = new MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V", 1);
//循环生成方法体代码(省略)
//方法体里生成调用InvocationHandler的invoke方法代码。(此处有所省略)
this.cp.getInterfaceMethodRef("InvocationHandler", "invoke", "Object; Method; Object;")
//将生成的字节码,写入硬盘,前面有个if判断,默认情况下不保存到硬盘。
localFileOutputStream = new FileOutputStream(ProxyGenerator.access$000(this.val$name) + ".class");
localFileOutputStream.write(this.val$classFile);
通过以上分析,我们可以推出动态代理为我们生产了一个这样的代理类。把方法soSomeThing的方法体修改为调用LogInvocationHandler的invoke方法
代码如下:
public class ProxyBusiness implements IBusiness, IBusiness2 { private LogInvocationHandler h;
@Override
public void doSomeThing2() {
try {
Method m = (h.target).getClass().getMethod("doSomeThing", null);
h.invoke(this, m, null);
} catch (Throwable e) {
// 异常处理(略)
}
}
@Override
public boolean doSomeThing() {
try {
Method m = (h.target).getClass().getMethod("doSomeThing2", null);
return (Boolean) h.invoke(this, m, null);
} catch (Throwable e) {
// 异常处理(略)
}
return false;
}
public ProxyBusiness(LogInvocationHandler h) {
this.h = h;
}
//测试用
public static void main(String[] args) {
//构建AOP的Advice
LogInvocationHandler handler = new LogInvocationHandler(new Business());
new ProxyBusiness(handler).doSomeThing();
new ProxyBusiness(handler).doSomeThing2();
}
}
4.1.3 小结
从前两节的分析我们可以看出,动态代理在运行期通过接口动态生成代理类,这为其带来了一定的灵活性,但这个灵活性却带来了两个问题:
- 第一,代理类必须实现一个接口,如果没实现接口会抛出一个异常
- 第二,性能影响,因为动态代理是使用反射机制实现的,首先反射肯定比直接调用要慢,其次使用反射大量生成类文件可能引起full gc,因为字节码文件加载后会存放在JVM运行时方法区(或者叫永久代、元空间)中,当方法区满时会引起full gc,所以当你大量使用动态代理时,可以将永久代设置大一些,减少full gc的次数
4.2 CGLIB动态字节码生成
使用动态字节码生成技术实现AOP原理是在运行期间目标字节码加载后,生成目标类的子类,将切面逻辑加入到子类中,所以cglib实现AOP不需要基于接口
本节介绍如何使用cglib来实现动态字节码技术。
cglib是一个强大的、高性能的Code生成类库,它可以在运行期间扩展Java类和实现Java接口,它封装了Asm,所以使用cglib前需要引入Asm的jar
4.2.1 使用cglib实现AOP
1 package cglib; 2
3 /**
4 * 这个是没有实现接口的实现类
5 */
6 public class BookFacadeImpl {
7 public void addBook() {
8 System.out.println("增加图书的普通方法。。。");
9 }
10
11 public void deleteBook() {
12 System.out.println("删除图书的普通方法。。。");
13 }
14 }
1 package cglib;
2
3 import net.sf.cglib.proxy.Enhancer;
4 import net.sf.cglib.proxy.MethodInterceptor;
5 import net.sf.cglib.proxy.MethodProxy;
6
7 import java.lang.reflect.Method;
8
9 /**
10 * 使用cglib动态代理
11 */
12 public class BookFacadeCglib implements MethodInterceptor {
13
14 private Object target;
15
16 /**
17 * 创建代理对象
18 *
19 * @param target
20 * @return
21 */
22 public Object getInstance(Object target) {
23 this.target = target;
24 Enhancer enhancer = new Enhancer();
25 enhancer.setSuperclass(this.target.getClass());
26 //回调方法
27 enhancer.setCallback(this);
28 //创建代理
29 return enhancer.create();
30 }
31
32 //回调方法
33 @Override
34 public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
35 if (method.getName().equals("addBook")) {
36 System.out.println("记录增加图书的日志");
37 }
38 methodProxy.invokeSuper(obj, args);
39 return null;
40 }
41 }
package cglib;
/**
* 测试cglib字节码代理
*/
public class TestCglib {
public static void main(String[] args) {
BookFacadeCglib cglib = new BookFacadeCglib();
BookFacadeImpl bookFacade = (BookFacadeImpl) cglib.getInstance(new BookFacadeImpl());
bookFacade.addBook();
bookFacade.deleteBook();
}
}
执行结果:
记录增加图书的日志
增加图书的普通方法。。。
删除图书的普通方法。。。
4.3 自定义类加载器
如果我们实现了一个自定义类加载器,在类加载到JVM之前直接修改某些类的方法,并将切入逻辑织入到这个方法里,然后将修改后的字节码文件交给虚拟机运行,那岂不是更直接
Javassist是一个编辑字节码的框架,可以让你很简单地操作字节码。它可以在运行期定义或修改Class。使用Javassist实现AOP的原理是在字节码加载前直接修改需要切入的方法
这比使用cglib实现AOP更加高效,并且没有太多限制,实现原理如下图:
我们使用类加载器启动我们自定义的类加载器,在这个类加载器里加一个类加载监听器,监听器发现目标类被加载时就织入切入逻辑
4.3.1 Javassist实现AOP的代码
清单1:启动自定义的类加载器
//获取存放CtClass的容器ClassPool ClassPool cp = ClassPool.getDefault();
//创建一个类加载器
Loader cl = new Loader();
//增加一个转换器
cl.addTranslator(cp, new MyTranslator());
//启动MyTranslator的main函数
cl.run("jsvassist.JavassistAopDemo$MyTranslator", args);
清单2:类加载监听器
public static class MyTranslator implements Translator { public void start(ClassPool pool) throws NotFoundException, CannotCompileException {
}
/* *
* 类装载到JVM前进行代码织入
*/
public void onLoad(ClassPool pool, String classname) {
if (!"model$Business".equals(classname)) {
return;
}
//通过获取类文件
try {
CtClass cc = pool.get(classname);
//获得指定方法名的方法
CtMethod m = cc.getDeclaredMethod("doSomeThing");
//在方法执行前插入代码
m.insertBefore("{ System.out.println(\"记录日志\"); }");
} catch (NotFoundException e) {
} catch (CannotCompileException e) {
}
}
public static void main(String[] args) {
Business b = new Business();
b.doSomeThing2();
b.doSomeThing();
}
}
输出:
执行业务逻辑2
记录日志
执行业务逻辑
4.3.2 小结
从本节中可知,使用自定义的类加载器实现AOP在性能上有优于动态代理和cglib,因为它不会产生新类,但是它仍人存在一个问题,就是如果其他的类加载器来加载类的话,这些类就不会被拦截
4.4 字节码转换
自定义类加载器实现AOP只能拦截自己加载的字节码,那么有一种方式能够监控所有类加载器加载的字节码吗?
有,使用Instrumentation,它是Java5的新特性,使用Instrument,开发者可以构建一个字节码转换器,在字节码加载前进行转换
本节使用Instrumentation和javassist来实现AOP
4.4.1 构建字节码转换器
首先需要创建字节码转换器,该转换器负责拦截Business类,并在Business类的doSomeThing方法前使用javassist加入记录日志的代码
1 public class MyClassFileTransformer implements ClassFileTransformer { 2
3 /**
4 * 字节码加载到虚拟机前会进入这个方法
5 */
6 @Override
7 public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
8 ProtectionDomain protectionDomain, byte[] classfileBuffer)
9 throws IllegalClassFormatException {
10 System.out.println(className);
11 //如果加载Business类才拦截
12 if (!"model/Business".equals(className)) {
13 return null;
14 }
15
16 //javassist的包名是用点分割的,需要转换下
17 if (className.indexOf("/") != -1) {
18 className = className.replaceAll("/", ".");
19 }
20 try {
21 //通过包名获取类文件
22 CtClass cc = ClassPool.getDefault().get(className);
23 //获得指定方法名的方法
24 CtMethod m = cc.getDeclaredMethod("doSomeThing");
25 //在方法执行前插入代码
26 m.insertBefore("{ System.out.println(\"记录日志\"); }");
27 return cc.toBytecode();
28 } catch (NotFoundException e) {
29 } catch (CannotCompileException e) {
30 } catch (IOException e) {
31 //忽略异常处理
32 }
33 return null;
34 }
4.4.2 注册转换器
使用premain函数注册字节码转换器,该方法在main函数之前执行
public class MyClassFileTransformer implements ClassFileTransformer { public static void premain(String options, Instrumentation ins) {
//注册我自己的字节码转换器
ins.addTransformer(new MyClassFileTransformer());
}
}
4.4.3 配置和执行
需要告诉JVM在启动main函数之前,需要先执行premain函数。
首先,需要将premain函数所在的类打成jar包,并修改jar包里的META-INF\MANIFEST.MF文件
1 Manifest-Version: 1.0 2 Premain-Class: bci. MyClassFileTransformer
其次,在JVM的启动参数里加上-javaagent:D:\java\projects\opencometProject\Aop\lib\aop.jar
4.4.4 输出
执行main函数,你会发现切入的代码无侵入性的织入进去了
1 public static void main(String[] args) { 2 new Business().doSomeThing();
3 new Business().doSomeThing2();
4 }
5
输出:
1 model/Business 2 sun/misc/Cleaner
3 java/lang/Enum
4 model/IBusiness
5 model/IBusiness2
6 记录日志
7 执行业务逻辑
8 执行业务逻辑2
9 java/lang/Shutdown
10 java/lang/Shutdown$Lock
从输出中可以看到系统类加载器加载的类也经过了这里
5、AOP实战
5.1 AOP功能
- 性能监控:在方法调用前后记录调用时间,方法执行太长或超时报警
- 缓存代理:缓存某方法的返回值,下次执行该方法时,直接从缓存里获取
- 软件破解:使用AOP修改软件的验证类的判断逻辑
- 记录日志:在方法执行前后记录系统日志
- 工作流系统:工作流系统需要将业务代码和流程引擎代码混合在一起执行,那么我们可以使用AOP将其分离,并动态挂接业务
- 权限验证:方法执行前验证是否有权限执行当前方法,没有则抛出没有权限执行异常,由业务代码捕捉
5.2 Spring的AOP
Spring默认采取动态代理机制实现AOP,当动态代理不可用时(代理类无接口)会使用cglib机制
但Spring的AOP有一定的缺点:
第一,只能对方法进行切入,不能对接口、字段、静态代码块进行切入(切入接口的某个方法,则该接口下所有实现类的该方法都将被切入)
第二,同类中的互相调用方法将不会使用代理类。因为要使用代理类必须从Spring容器中获取Bean
第三,性能不是最好的。从前面几节得知,我们自定义的类加载器,性能优于动态代理和cglib
public IMsgFilterService getThis() {
return (IMsgFilterService) AopContext.currentProxy();
}
public boolean evaluateMsg () {
// 执行此方法将织入切入逻辑
return getThis().evaluateMsg(String message);
}
@MethodInvokeTimesMonitor("KEY_FILTER_NUM")
public boolean evaluateMsg(String message) {
public boolean evaluateMsg () {
// 执行此方法将不会织入切入逻辑
return evaluateMsg(String message);
}
@MethodInvokeTimesMonitor("KEY_FILTER_NUM")
public boolean evaluateMsg(String message) {
6、参考资料
http://www.iteye.com/topic/1116696
本文内容总结:AOP与JAVA动态代理
原文链接:https://www.cnblogs.com/xiaoxiao7/p/6057724.html
以上是 AOP与JAVA动态代理 的全部内容, 来源链接: utcz.com/z/362957.html