1. 预备知识-动态代理
1.1 什么是动态代理
动态代理利用Java的反射技术(Java Reflection)生成字节码,在运行时创建一个实现某些给定接口的新类(也称"动态代理类")及其实例。
1.2 动态代理的优势
动态代理的优势是实现无侵入式的代码扩展,也就是方法的增强;让你可以在不用修改源码的情况下,增强一些方法;在方法的前后你可以做你任何想做的事情(甚至不去执行这个方法就可以)
spring中的AOP是动态代理使用的经典场景。
1.3 基于JDK动态代理实现
在基于JDK的动态代理的实现中有两个重要的类:InvocationHandler, Proxy
- InvocationHandler
是代理实例的调用处理程序实现的接口。每个代理实例都具有一个关联的调用处理程序。对代理实例调用方法时,将对方法调用进行编码并将其指派到它的调用处理程序的 invoke 方法。 - Proxy
JDK中动态生成代理类的工具类
一个动态代理的示例:
定义一个接口(基于JDK的动态代理只能使用接口)
public interface ISubject { void hello(String param); }
为接口定义实现类
public class SubjectImpl implements ISubject { @Override public void hello(String param) { System.out.println("hello " + param); } }
实现一个代理类:
public class JDKProxy implements InvocationHandler { private Object target; public JDKProxy(Object target) { this.target = target; } //创建代理 public Object newProxy() { return (ISubject)Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("---------- 在业务方法调用之前可以进行前置增强 ------------"); //利用反射机制调用方法,invoke为返回值,如果没有返回null Object invoke = method.invoke(target, args); System.out.println("---------- 在业务方法调用之前可以进行后置增强 ------------"); return invoke; } }
编写代理类实际的调用,利用Proxy类创建代理之后的Subject类
public class JDKProxyDemo { public static void main(String[] args) { ISubject subject = new SubjectImpl(); JDKProxy subjectProxy = new JDKProxy(subject); ISubject proxyInstance = (ISubject)subjectProxy.newProxy(); proxyInstance.hello("world"); } }
运行结果:
---------- 在业务方法调用之前可以进行前置增强 ------------ hello world ---------- 在业务方法调用之前可以进行后置增强 ------------
2. AOP
2.1 基本概念
- 连接点 (Joinpoint)
程序执行过程中明确的点,如方法的调用,或者异常的抛出. - 目标(Target)
被通知(被代理)的对象,如上例中的SubjectImpl - 通知(Advice)
在某个特定的连接点上执行的动作,同时Advice也是程序代码的具体实现,例如一个实现日志记录的代码(通知有些书上也称为处理),可以理解为AOP真正要实现的功能 - 代理(Proxy)
将通知应用到目标对象后创建的对象(代理=目标+通知),请注意:只有代理对象才有AOP功能,而AOP的代码是写在通知的方法里面的,如上例中的JDKProxy - 切入点(Pointcut)
多个连接点的集合,定义了通知应该应用到那些连接点。也将Pointcut理解成一个条件 ,此条件决定了容器在什么情况下将通知和目标组合成代理返回给外部程序 - 适配器(Advisor)
适配器=通知(Advice)+切入点(Pointcut)
AOP运行原理:目标对象只负责业务逻辑,通知只负责AOP增强逻辑(如日志,数据验证等),而代理对象则将业务逻辑而AOP增强代码组织起来(组织者)
2.2 AOP带来的好处
AOP是公用的框架代码放置的理想地方,将公共代码与业务代码分离,使我们在处理业务时可以专心的处理业务。
伪代码:
public void doSameBusiness (long lParam,String sParam){ // 记录日志 log.info("调用 doSameBusiness方法,参数是:"+lParam); // 输入合法性验证 if (lParam<=0){ throws new IllegalArgumentException("xx应该大于0"); } if (sParam==null || sParam.trim().equals("")){ throws new IllegalArgumentException("xx不能为空"); } // 异常处理 try{ 真正的业务处理 }catch(...){ }catch(...){ } // 事务控制 tx.commit(); }
通过使用AOP我们可以将日志记录,数据合法性验证,异常处理等功能放入AOP中,那么在编写业务时就可以专心实现真正的业务逻辑代码。
3. Spring AOP
在spring中org.springframework.aop.framework.ProxyFactoryBean用来创建代理对象,在一般情况下它需要注入一下三个属性:
- proxyInterfaces 代理应该实现的接口列表(List)
- interceptorNames 需要应用到目标对象上的通知Bean的名字
- target 目标对象 (Object)
准备工作:创建一个IBookService接口及其实现类,用于演示spring AOP开发示例:
public interface IBookService { // 购书 public boolean buy(String userName, String bookName, Double price); // 发表书评 public void comment(String userName, String comments); }
public class BookServiceImpl implements IBookService { private Logger logger = LoggerFactory.getLogger(this.getClass()); public BookServiceImpl() { super(); } public boolean buy(String userName, String bookName, Double price) { //logger.info("userName={},bookName={},price={}", userName, bookName, price); // 通过控制台的输出方式模拟购书 logger.info(userName + " buy " + bookName + ", spend " + price); return true; } public void comment(String userName, String comments) { logger.info(userName + " say:" + comments); } }
将service配置到spring配置文件中,以便于被spring管理,按自己的实际情况配置
<!-- 目标 --> <bean id="bookServiceTarget" class="com.zking.sp02.impl.BookServiceImpl"/>
3.1 前置通知
前置通知需要实现org.springframework.aop.MethodBeforeAdvice,前置通知将在目标对象调用前调用。示例实现购书系统AOP方式实现日志,简单打印调用的方法及参数
1)首先实现一个前置通知类,实现接口MethodBeforeAdvice,并实现接口中的before方法
public class MyMethodBeforeAdvice implements MethodBeforeAdvice { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public void before(Method method, Object[] args, Object target) throws Throwable { String s = "[前置通知]: " + this.getClass() + "." + method.getName() + "将被调用,参数为:" + Arrays.toString(args); logger.info(s); } }
2)将实现的前置通知配置到Spring.xml中,一遍与被spring管理。需要根据自己的实际情况配置。
<bean id="myMethodBeforeAdvice" class="com.zking.springdemo.aop.MyMethodBeforeAdvice"/>
3)现在需要解决如何将通知和目标联系起来,需要一个组织者 - 代理
<bean id="bookService" class="org.springframework.aop.framework.ProxyFactoryBean"> <!-- 配置代理目标 --> <property name="target" ref="bookServiceTarget"/> <!-- 配置拦截器列表,拦截器就是通知 --> <property name="interceptorNames"> <list> <value>myMethodBeforeAdvice</value> </list> </property> <!-- 代理要实现的接口,代理类与被代理类需要实现相同接口 --> <property name="proxyInterfaces"> <list> <value>com.zking.springdemo.aop.IBookService</value> </list> </property> </bean>
写一个测试类,测试前置通知
public class Demo { public static void main(String[] args) { ApplicationContext <u>cxt</u> = new ClassPathXmlApplicationContext("/spring.xml"); IBookService bookService = (IBookService)cxt.getBean("bookService"); System.out.println(bookService.getClass().getName()); bookService.buy("zs", "hlm", 10D); } }
3.2 后置通知
在连接点正常完成后执行的通知。定义的后置通知类需要实org.springframework.aop.AfterReturningAdvice
示例:在线购书系统中,要求不修改BookServiceImpl代码的情况下增加如下功能:对买书的用户进行返利:每买本书返利10元,简单打印类似于“[后置通知] 返利10元”即可。开发步骤与前置通知类似
1) 编写一个后置通知实现类
public class MyAfterReturnAdvice implements AfterReturningAdvice { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { logger.info("[后置通知]: 返利10元"); } }
2) 将后置通知实现类配置到spring配置文件中,以便于spring管理
<bean id="myAfterReturnAdvice" class="com.zking.springdemo.aop.MyAfterReturnAdvice"/>
3)解决如何将通知和目标联系起来,在实现前置通知时已经配置了代理对象,现在只要将后置通知也配置到拦截器列表当中即可。
<!-- 配置拦截器列表,拦截器就是通知 --> <property name="interceptorNames"> <list> <!-- 前置通知 --> <value>myMethodBeforeAdvice</value> <!-- 后置通知 --> <value>myAfterReturnAdvice</value> </list> </property>
运行上例已经实现的测试类,查看后置通知的运行效果。
3.3 环绕通知
包围一个连接点的通知,最大特点是可以修改返回值,由于它在方法前后都加入了自己的逻辑代码,因此功能很强大。自定义的环绕通知需要实现org.aopalliance.intercept.MethodInterceptor接口。
示例:在环绕通知中输出日志和返回值
1)实现一个环绕通知,该类要实现MethodInterceptor接口
public class MyMethodInterceptor implements MethodInterceptor { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public Object invoke(MethodInvocation invocation) throws Throwable { //获取目标对象 Object target = invocation.getThis(); //获取参数 Object[] args = invocation.getArguments(); //获取方法 Method method = invocation.getMethod(); logger.info("[环绕通知] 前:将调用{}.{}方法,参数为{}", target.getClass(), method.getName(), Arrays.toString(args)); //调用目标对象上的方法 Object val = invocation.proceed(); logger.info("[环绕通知] 后,已调用{}.{}, 返回值:{}", target.getClass(), method.getName(), val); return val; } }
2)在spring的配置文件中配置环绕通知,以便于spring管理
<bean id="myMethodInterceptor" class="com.zking.springdemo.aop.MyMethodInterceptor"/>
3)解决如何将通知和目标联系起来,在实现前置通知时已经配置了代理对象,现在只要将环绕通知也配置到拦截器列表当中即可。
<!-- 配置拦截器列表,拦截器就是通知 --> <property name="interceptorNames"> <list> <!--前置通知--> <value>myMethodBeforeAdvice</value> <!--后置通知--> <value>myAfterReturnAdvice</value> <!--环绕通知--> <value>myMethodInterceptor</value> </list> </property>
运行上例已经实现的测试类,查看后置通知的运行效果。
3.4 异常通知
异常通知需要实现ThrowsAdvice接口,这个通知会在方法抛出异常退出时执行,与以上演示的前置、后置、环绕通知不同,主要有一下特点:
- 这个接口里面没有定义方法,要求我们的类必须实现afterThrows这个方法
- 以异常类型作为参数,无返回值
示例
1)定义一个自定义异常,继承RuntimeException运行时异常
public class PriceException extends RuntimeException { public PriceException() { super(); } public PriceException(String message, Throwable cause) { super(message, cause); } public PriceException(String message) { super(message); } public PriceException(Throwable cause) { super(cause); } }
2)创建异常通知类,该类实现ThrowsAdvice接口,并实现afterThrowing方法
public class MyThrowsAdvice implements ThrowsAdvice { private Logger logger = LoggerFactory.getLogger(this.getClass()); //以异常类型作为参数,无返回值 public void afterThrowing(PriceException e) { logger.info("程序发生了PriceException异常"); } }
剩下的步骤是将异常通知配置到spring配置文件中,及在代理配置中加入异常通知的配置,可参考上面的环绕通知等示例。
3.5 适配器
适配器, 通过正则表达式来定义方法切入点,也就是说定义哪些方法将被拦截器处理。适配器=通知(Advice)+切入点(Pointcut)。
在配置适配器时需要使用org.springframework.aop.support.RegexpMethodPointcutAdvisor
配置适配器示例:
<!-- 配置适配器 --> <bean id="myAdisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <!-- 定义正则表达式,定义需要拦截的方法名 ,本例定义了所有以buy结尾的方法 --> <property name="patterns"> <list> <value>.*buy</value> </list> </property> <!-- 定义由那个通知(或者叫拦截器)来处理匹配的方法 --> <property name="advice"> <ref bean="myAfterReturnAdvice"/> </property> </bean>
在代理中使用刚刚配置的适配器
<!-- 将直接使用后置拦截器 ,改为使用适配器 --> <!-- <value>myMethodAfterReturnAdvice</value> --> <!-- 通过适配器使用后置拦截器 --> <value>myAdisor</value>
修改代理中的拦截器列表(spring配置文件,代理部分),将配置器直接配置在拦截器列表中即可。