0.前言
本文主要想阐述的问题如下:
- 什么动态代理(AOP)以及如何用JDK的Proxy和InvocationHandler实现自己的代理?
- 什么是Spring动态代理(AOP)?
- Spring AOP注解实现
1.动态代理(AOP)
1.1 AOP
- 什么是AOP?
AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。- 为什么需要用AOP?
OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP技术利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。- 什么是切面(Aspect)?
所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。- 使用切面(Aspect)技术有什么好处?
使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
1.2 代理模式
代理模式是AOP的基础,也是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。
使用代理模式必须要让代理类和目标类实现相同的接口,客户端通过代理类来调用目标方法,代理类会将所有的方法调用分派到目标对象上反射执行,还可以在分派过程中添加"前置通知"和后置处理(如在调用目标方法前校验权限,在调用完目标方法后打印日志等)等功能。
如上图所示:
1.委托对象和代理对象都共同实现的了同一个接口。
2.委托对象中存在的方法在代理对象中也同样存在。代理模式分为两种:
- 静态代理:代理类是在编译时就实现好的。也就是说 Java 编译完成后代理类是一个实际的 class 文件。
- 动态代理:代理类是在运行时生成的,也就是说 Java 编译完之后并没有实际的 class 文件,而是在运行时动态生成的类字节码,并加载到JVM中。
1.2 静态代理实现
//客户端 public class Client { public static void main(String args[]) { Target subject = new Target(); Proxy p = new Proxy(subject); p.request(); } } //委托对象和代理对象都共同实现的接口 interface Interface { void request(); } //委托类 class Target implements Interface { public void request() { System.out.println("request"); } } //代理类 class Proxy implements Interface { private Interface subject; public Proxy(Interface subject) { this.subject = subject; } public void request() { System.out.println("PreProcess"); subject.request(); System.out.println("PostProcess"); } }
1.3 Java 实现动态代理
Java实现动态代理的大致步骤如下:
- 定义一个委托类和公共接口
//公共接口 public interface IHello { void sayHello(); } //委托类 class Hello implements IHello { public void sayHello() { System.out.println("Hello world!!"); } }
- 通过实现InvocationHandler接口来自定义自己的InvocationHandler,指定运行时将生成的代理类需要完成的具体任务
//自定义InvocationHandler public class HWInvocationHandler implements InvocationHandler { // 目标对象 private Object target; public HWInvocationHandler(Object target) { this.target = target; } public Object invoke(Object proxy, Method method, Object[] args) >throws Throwable { System.out.println("------插入前置通知代码-------------"); // 执行相应的目标方法 Object rs = method.invoke(target, args); System.out.println("------插入后置处理代码-------------"); return rs; } }
- 生成代理对象,这个可以分为四步:
(1)通过Proxy.getProxyClass获得动态代理类
(2)通过反射机制获得代理类的构造方法,方法签名为getConstructor(InvocationHandler.class)
(3)通过构造函数获得代理对象并将自定义的InvocationHandler实例对象传为参数传入
(4)通过代理对象调用目标方法public class Client { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, >InvocationTargetException, InstantiationException { // 生成Proxy的class文件 System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); // 获取动态代理类 Class<?> proxyClazz = Proxy.getProxyClass(IHello.class.getClassLoader(), IHello.class); // 获得代理类的构造函数,并传入参数类型InvocationHandler.class Constructor<?> constructor = proxyClazz.getConstructor(InvocationHandler.class); // 通过构造函数来创建动态代理对象,将自定义的InvocationHandler实例传入 IHello iHello = (IHello) constructor.newInstance(new HWInvocationHandler(new Hello())); // 通过代理对象调用目标方法 iHello.sayHello(); } }
Proxy类中还有个将2~4步骤封装好的简便方法来创建动态代理对象,其方法签名为:newProxyInstance(ClassLoader loader,Class<?>[] instance, InvocationHandler h),如下例:
public class Client2 { public static void main(String[] args) throws NoSuchMethodException, >IllegalAccessException, InvocationTargetException, InstantiationException { //生成$Proxy0的class文件 System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); IHello ihello = (IHello) >Proxy.newProxyInstance(IHello.class.getClassLoader(), //加载接口的类加载器 new Class[]{IHello.class}, //一组接口 new HWInvocationHandler(new Hello())); //自定义的>InvocationHandler ihello.sayHello(); } }
这个静态函数的第一个参数是类加载器对象(即哪个类加载器来加载这个代理类到 JVM 的方法区),第二个参数是接口(表明你这个代理类需要实现哪些接口),第三个参数是调用处理器类实例(指定代理类中具体要干什么)
以上就是对代理类如何生成,代理类方法如何被调用的分析!在很多框架都使用了动态代理如Spring,HDFS的RPC调用等等。
2.Spring动态代理
2.1 Spring AOP实现的原理
Spring中AOP代理由Spring的IOC容器负责生成、管理,其依赖关系也由IOC容器负责管理。因此,AOP代理可以直接使用容器中的其它bean实例作为目标,这种关系可由IOC容器的依赖注入提供。Spirng的AOP的动态代理实现机制有两种,分别是:
- JDK动态代理:JDK动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。这个在之前已经介绍过了。
- CGLib动态代理:cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
2.2 如何选择的使用代理机制
- 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
- 如果目标对象实现了接口,可以强制使用CGLIB实现AOP
- 如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换
2.3 AOP基本概念
在写Spring AOP之前先简单介绍下几个概念:
- 切面(Aspect) :通知和切入点共同组成了切面,时间、地点和要发生的“故事”。
- 连接点(Joinpoint) :程序能够应用通知的一个“时机”,这些“时机”就是连接点,例如方法被调用时、异常被抛出时等等。
- 通知(Advice) :通知定义了切面是什么以及何时使用。描述了切面要完成的工作和何时需要执行这个工作。
- 切入点(Pointcut) :通知定义了切面要发生的“故事”和时间,那么切入点就定义了“故事”发生的地点,例如某个类或方法的名称。
- 目标对象(Target Object) :即被通知的对象。
- 织入(Weaving):把切面应用到目标对象来创建新的代理对象的过程,织入一般发生在如下几个时机:
1)编译时:当一个类文件被编译时进行织入,这需要特殊的编译器才能做到,例如AspectJ的织入编译器;
2)类加载时:使用特殊的ClassLoader在目标类被加载到程序之前增强类的字节代码;
3)运行时:切面在运行的某个时刻被织入,SpringAOP就是以这种方式织入切面的,原理是使用了JDK的动态代理。AOP通知类型:
- @Before 前置通知(Before advice) :在某连接点(JoinPoint)之前执行的通知,但这个通知不能阻止连接点前的执行。
- @After 后通知(After advice) :当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
- @AfterReturning 返回后通知(After return advice) :在某连接点正常完成后执行的通知,不包括抛出异常的情况。
- @Around 环绕通知(Around advice) :包围一个连接点的通知,类似Web中Servlet规范中的Filter的doFilter方法。可以在方法的调用前后完成自定义的行为,也可以选择不执行。
- @AfterThrowing 抛出异常后通知(After throwing advice) : 在方法抛出异常退出时执行的通知。
3.Spring AOP注解实现
对于AOP编程,我们只需要做三件事:
- 定义普通业务组件
- 定义切入点,一个切入点可能横切多个业务组件
- 定义增强处理,增强处理就是在AOP框架为普通业务组件织入的处理动作
首先我们定义一个接口:它只完成增加用户的功能。
public interface UserDao { public void add(User user); }
其次,我们定义一个接口实现类:它实现了用户的添加功能。
@Component("u") public class UserDaoImpl implements UserDao { @Override public void add(User user) { System.out.println("add user!"); } }
然后,定义一个service类,他会调用UserDao的add方法
@Component public class UserService { private UserDao userDao; public void add(User user) { userDao.add(user); } public UserDao getUserDao() { return userDao; } @Resource(name = "u") public void setUserDao(UserDao userDao) { this.userDao = userDao; } }
定义一下横切关注点的类:我们这里列举了各种情况,在方法执行之前,之后,成功等等情况都有涉及
@Aspect @Component public class LogInterceptor { // @Pointcut("execution(public * com.syf.dao.impl..*.*(..))") @Pointcut("execution(public * com.syf.service..*.add(..))") public void myMethod() { }; // @Before("execution(public void // com.syf.dao.impl.UserDaoImpl.add(com.syf.model.User))") // @Before("execution(public * com.syf.dao.impl..*.*(..))") @Before("myMethod()") public void before() { System.out.println("method start"); } // @After("execution(public * com.syf.dao.impl..*.*(..))") @After("myMethod()") public void after() { System.out.println("method end"); } // @AfterReturning("execution(public * com.syf.dao.impl..*.*(..))") @AfterReturning("myMethod()") public void afterReturning() { System.out.println("method after returning"); } @Around("myMethod()") public void aroundMethod(ProceedingJoinPoint pjp) throws Throwable { System.out.println("around start method"); pjp.proceed(); System.out.println("around end method"); } }
Spring 的配置文件如下。通过aop命名空间的<aop:aspectj-autoproxy />声明自动为spring容器中那些配置@aspectJ切面的bean创建代理,织入切面
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"> <context:annotation-config></context:annotation-config> <context:component-scan base-package="com.syf">></context:component-scan> <aop:aspectj-autoproxy /> </beans>
编写测试类对其进行测试:
public class UserServiceTest { @Test public void testAdd() throws Exception{ @SuppressWarnings("resource") ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml"); UserService svc = (UserService) applicationContext.getBean("userService"); User u = new User(); u.setId(1); u.setName("name"); svc.add(u); } }
打印出的log证明,在add方法执行前后等情况下,切面均有被织入,Spring
AOP代理实现成功:around start method method start add user! //add 方法实现的内容 around end method method end method after returning
所以进行AOP编程的关键就是定义切入点和定义增强处理,一旦定义了合适的切入点和增强处理,AOP框架将自动生成AOP代理,即:代理对象的方法=增强处理+被代理对象的方法。
4.代码
本文中所涉及的代码在github上都有,可以点击以下链接:
GIthub地址