上篇讲到了代理模式出现的原因,实现方式以及跟其他相似设计模式的区别。传送门@_@ http://blog.csdn.net/wonking666/article/details/79497547
1.静态代理的不足
设计模式里面的代理模式,代理类是需要手动去写的。但是手写代理的问题颇多
1.如果不同类型的目标对象需要执行同样一套代理的逻辑,比如说在方法调用前后打印参数和结果,那么仍然需要为每一个类型写一个代理类,将会产生大量的样板代码,书写起来非常枯燥
2.一个类中会有多个方法,对于不需要拦截的方法,我们还是要手动调一下目标对象对应的方法,虽然只有一行代码,但写多了还是感觉蠢蠢哒
3.还有,手写的代理类是静态代理,编译之后就代码就定死了,没办法做到动态的变化,缺少一些灵动
2.对现存问题的思考
回想我们使用代理类最初的目的,就只是为了拦截某个方法的调用,在其前后执行一些额外逻辑,额外逻辑和目标方法并不具有强关联。方法调用会形成一个调用栈,可以把代理想象为是在这个栈上切了一刀,在切口上做的逻辑,我们叫切面逻辑
于是聪明的程序员想到,将纯净的切面逻辑抽出来,再定义一套切入规则,然后让工具自动生成代理类,岂不是美滋滋
所以说代理几乎天生是用来做切面的,二者有着剪不断理还乱的关系
PS: 从分析来看,AOP只需要关注3点即可:切谁?什么时候切?在切口上做什么?
最讨厌那些整一大套花里胡哨的理论东西了,什么Joinpoint,Pointcut,Advice,Before/After/Around Advice,取的名字这么不形象,故作高深只会误导吃瓜群众,哼,我掀了你的小板凳 (╯—﹏—)╯(┷━━━┷
3.如何自动生成代理
我们知道,一个类从编写,到运行时调用,中间大概会经过这几个步骤
所以生成代理可以有三个思路,一,在编译期修改源代码;二,在字节码加载前修改字节码;三,在字节码加载后动态创建代理类的字节码
类别 | 机制 | 原理 | 优点 | 缺点 | 技术 |
静态AOP | 静态织入 | 在编译期,切面直接以字节码的形式编译到目标字节码文件中 | 对系统无性能影响 | 灵活性不够 | AspectJ |
动态AOP | 动态代理 | 在运行期,目标类加载后,为接口动态生成代理类,将切面植入到代理类中 | 相对于静态AOP更加灵活 | 切入的关注点需要实现接口。对系统有一点性能影响 | JDK dynamic proxy |
动态字节码生成 | 在运行期,目标类加载后,动态构建字节码文件生成目标类的子类,将切面逻辑加入到子类中 | 没有接口也可以织入 | 扩展类的实例方法为final时,则无法进行织入 | cglib | |
自定义类加载器 | 在运行期,目标加载前,将切面逻辑加到目标字节码里 | 可以对绝大部分类进行织入 | 代码中如果使用了其他类加载器,则这些类将不会被织入 | ||
字节码转换 | 在运行期,所有类加载器加载字节码前,前进行拦截 | 可以对所有类进行织入 |
4.AspectJ生成静态代理
AspectJ 是 Java 语言的一个 AOP 实现,其主要包括两个部分:第一个部分定义了如何表达、定义 AOP 编程中的语法规范,通过这套语言规范,我们可以方便地用 AOP 来解决 Java 语言中存在的交叉关注点问题;另一个部分是工具部分,包括编译器、调试工具等
下载、安装 AspectJ 比较简单,读者登录 AspectJ 官网(http://www.eclipse.org/aspectj),即可下载到一个可执行的 JAR 包,使用 java -jar aspectj-1.x.x.jar 命令、多次单击“Next”按钮即可成功安装 AspectJ
AspectJ 的用法非常简单,就像我们使用 JDK 编译、运行 Java 程序一样,下面是一个简单的示例
编写一个POJO业务类
public class HelloAspectJ { public void sayHello() { System.out.println("Hello AspectJ"); } public static void main(String[] args) { HelloAspectJ h = new HelloAspectJ(); h.sayHello(); } }
使用AspectJ编写一个Aspect
public aspect TestAspect{ void around():call(void Hello.sayHello()){ System.out.println("开始事务 ..."); proceed(); System.out.println("事务结束 ..."); } }
使用AspectJ的编译器编译
ajc -d . Hello.java TxAspect.aj
注意:在定义切面的时候我们使用了 aspect 关键字而非class,这个并不是Java里面的关键字,所以无法用javac来编译,只能用ajc来编译
运行Java程序
java test.HelloAspectJ
5.JDK动态代理生成
以一个简单的事务实现原理作为示例
首先定义接口
public interface IUserDao { boolean login(String name, String password) throws RuntimeException; }
给个接口的实现
public class UserDaoImpl implements IUserDao { @Override public boolean login(String name, String password) throws RuntimeException{ return "wonking".equals(name) && "666".equals(password); } }
使用JDK的Proxy.newProxyInstance生成代理
public class ProxyFactory { private ProxyFactory(){ } public static <T> Object getProxyInstance(final T t){ return Proxy.newProxyInstance(t.getClass().getClassLoader(), t.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("-----begin transaction-----"); Object returnValue=null; try{ returnValue=method.invoke(t, args); }catch (Exception e){ System.out.println("-----do rollback-----"); } System.out.println("-----finish transaction-----"); return returnValue==null ? false : returnValue; } }); } }
使用代理
public class ApplicationProxy { public static void main(String[] args) { UserDaoImpl target=new UserDaoImpl(); IUserDao proxy= (IUserDao) ProxyFactory.getProxyInstance(target); Object result=proxy.login(null,"666"); } }
注:后面会单独开一篇文章讲解Proxy.newProxyInstance(ClassLoader loader, Class<?> interface, InvocationHandler h)背后的原理
6.cglib动态字节码生成
public class CglibProxyTest { public static void main(String[] args) { //创建一个织入器 Enhancer enhancer = new Enhancer(); //设置父类 enhancer.setSuperclass(IUserDao.class); //设置需要织入的逻辑 enhancer.setCallback(new LogIntercept()); //使用织入器创建子类 IUserDao dao = (IUserDao) enhancer.create(); dao.login(null,"666"); } public static class LogIntercept implements MethodInterceptor { @Override public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable { //执行原有逻辑,注意这里是invokeSuper Object rev = proxy.invokeSuper(target, args); //执行织入的日志 if (method.getName().equals("doSomeThing2")) { System.out.println("记录日志"); } return rev; } } }
7.自定义类加载器
如果我们实现了一个自定义类加载器,在类加载到JVM之前直接修改某些类的方法,并将切入逻辑织入到这个方法里,然后将修改后的字节码文件交给虚拟机运行,那岂不是更直接Javassist是一个编辑字节码的框架,可以让你很简单地操作字节码。它可以在运行期定义或修改Class。使用Javassist实现AOP的原理是在字节码加载前直接修改需要切入的方法。这比使用Cglib实现AOP更加高效,并且没太多限制。
我们使用系统类加载器启动我们自定义的类加载器,在这个类加载器里加一个类加载监听器,监听器发现目标类被加载时就织入切入逻辑
代码:类加载监听器
public class ClassLoaderListener implements Translator { public void start(ClassPool pool) throws NotFoundException, CannotCompileException { } public void onLoad(ClassPool pool, String classname) { if (!"test$UserDaoImpl".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) { UserDaoImpl dao = new UserDaoImpl(); dao.login("wonking","666"); } }
代码:类加载器启动器
public class ClassLoaderBootstrap { public static void main(String[] args) { //获取存放CtClass的容器ClassPool ClassPool cp = ClassPool.getDefault(); //创建一个类加载器 Loader cl = new Loader(); try { //增加一个转换器 cl.addTranslator(cp, new ClassLoaderListener()); //启动MyTranslator的main函数 cl.run("jsvassist.JavassistAopDemo$MyTranslator", args); } catch (NotFoundException e) { e.printStackTrace(); } catch (CannotCompileException e) { e.printStackTrace(); } catch (Throwable throwable) { throwable.printStackTrace(); } } }
8.自定义字节码转换器
首先需要创建字节码转换器,该转换器负责拦截UserDaoImpl类,并在UserDaoImpl类的login方法前使用javassist加入记录日志的代码
代码:字节码转换器
public class ClassByteTranslator implements ClassFileTransformer { /** * 字节码加载到虚拟机前会进入这个方法 */ @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println(className); //如果加载UserDaoImpl类才拦截 if (!"test.UserDaoImpl".equals(className)) { return null; } 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) { //ignore } return null; }
//注册字节码转换器 public static void premain(String options, Instrumentation ins) { ins.addTransformer(new ClassByteTranslator()); }}
配置&执行
需要告诉JVM在启动main函数之前,需要先执行premain函数。首先需要将premain函数所在的类打成jar包。并修改该jar包里的META-INF\MANIFEST.MF 文件
代码:修改MANIFEST.MF
-
Manifest-Version: 1.0
-
Premain-Class: test.ClassByteTranslator
然后在JVM的启动参数里加上。-javaagent:D:\java\projects\opencometProject\Aop\lib\aop.jar
代码:main函数
public class ClassLoaderTransBootstrap { public static void main(String[] args) { new UserDaoImpl().login("wonking","666"); } }
执行main函数,你会发现切入的代码无侵入性的织入进去了