一、AOP
1、介绍
AOP(Aspect Oriented Programming),面向切面编程。它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
AOP是对所有对象或者是一类对象编程,核心是在不增加代码的基础上,还增加新功能。实际上在开发框架本身用的多,在实际项目中,用的不多。
切面(aspect):要实现的交叉功能,是系统模块化的一个切面或领域。如日志记录。
连接点:应用程序执行过程中插入切面的地点,可以是方法调用,异常抛出,或者要修改的字段。
通知(增强):切面的实际实现,他通知系统新的行为。如在日志通知包含了实现日志功能的代码,如向日志文件写日志。通知在连接点插入到应用系统中。
切入点:定义了通知应该应用在哪些连接点,通知可以应用到AOP框架支持的任何连接点。连接点(静态)-->切入点(动态)。
引入:为类添加新方法和属性。
目标对象:被通知的对象(被代理的对象)。既可以是你编写的类也可以是第三方类。
代理对象:将通知应用到目标对象后创建的对象,应用系统的其他部分不用为了支持代理对象而改变。
织入:将切面应用到目标对象从而创建一个新代理对象的过程。织入发生在目标对象生命周期的多个点上:
①编译期:切面在目标对象编译时织入,这需要一个特殊的编译器。
②类装载期:切面在目标对象被载入JVM时织入,这需要一个特殊的类载入器。
③运行期:切面在应用系统运行时织入。
原理:AOP的核心思想为设计模式的动态代理模式。
2、案例
注意:接下来介绍的案例,更像是AOP的静态代理实现,个人理解。
定义目标对象(被代理的对象)
1 // 定义一个接口 2 public interface ITeacher { 3 void teach(); 4 int add(int i, int j); 5 } 6 7 // 定义目标对象 8 public class Teacher implements ITeacher { 9 @Override 10 public void teach() { 11 System.out.println("老师正在上课"); 12 } 13 14 @Override 15 public int add(int i, int j) { 16 int add = i + j; 17 System.out.println("执行目标方法:老师正在做加法,结果为:" + add); 18 // int throwable = 10 / 0; 测试异常通知 19 return add; 20 } 21 22 // 目标对象自己的方法,此方法不是接口所以无法代理 23 public void sayHello() { 24 System.out.println("老师会说hello"); 25 } 26 27 }
编写一个通知(前置通知)
1 // 前置通知 2 public class MyMethodBeforeAdvice implements MethodBeforeAdvice { 3 4 // method:被 代理对象 调用的方法(目标方法) 5 // objects:被 代理对象 调用的方法入参(参数) 6 // target:目标对象 7 @Override 8 public void before(Method method, Object[] objects, Object target) throws Throwable { 9 System.out.println("前置通知=====1======函数名:" + method.getName()); 10 System.out.println("前置通知=====2======参数值:" + JSON.toJSONString(objects)); 11 System.out.println("前置通知=====3======对象值:" + JSON.toJSONString(target)); 12 } 13 14 }
配置 application.xml
1 <!-- 配置目标对象 --> 2 <bean id="teacher" class="com.lx.spring.day1.Teacher"/> 3 4 <!-- 配置前置通知 --> 5 <bean id="myMethodBeforeAdvice" class="com.lx.spring.day1.MyMethodBeforeAdvice"/> 6 7 <!-- 配置代理对象 --> 8 <bean id="proxyFactoryBean" class="org.springframework.aop.framework.ProxyFactoryBean"> 9 <!-- 1.指定要代理的目标对象 --> 10 <property name="target" ref="teacher"/> 11 12 <!-- 2.指定要代理的接口集 --> 13 <property name="proxyInterfaces"> 14 <list> 15 <value>com.lx.spring.day1.ITeacher</value> 16 </list> 17 </property> 18 19 <!-- 3.指定要织入的通知 --> 20 <property name="interceptorNames"> 21 <list> 22 <value>myMethodBeforeAdvice</value> 23 </list> 24 </property> 25 </bean>
1 // 测试类 2 public class Main { 3 public static void main(String[] args) { 4 ApplicationContext app = new ClassPathXmlApplicationContext("app1.xml"); 5 // 获取代理对象 6 ITeacher iTeacher = (ITeacher) app.getBean("proxyFactoryBean"); 7 // 通过代理对象执行目标对象的方法 8 int add = iTeacher.add(1, 2); 9 } 10 } 11 12 // 前置通知=====1======函数名:add 13 // 前置通知=====2======参数值:[1,2] 14 // 前置通知=====3======对象值:{} 15 // 执行目标方法:老师正在做加法,结果为:3
原理剖析:案例中,重点在于理解ProxyFactoryBean这个Bean到底做了什么?有一个前置通知,在目标方法前调用(从打印结果也能看出)。
那么,用静态代理模式不难理解,简单理解上面的实现如下,详细的请查看源码。
1 // 代理对象,静态代理 2 public class ProxyFactoryBean implements ITeacher { // 2.指定要代理的接口集,即要实现哪些接口 3 // 目标对象,通过接口来聚合 4 private Teacher target; 5 // 前置通知 6 private MethodBeforeAdvice methodBeforeAdvice; 7 8 // 3.指定要织入的通知,即在执行目标对象方法要执行什么代码 9 public void setMethodBeforeAdvice(MethodBeforeAdvice methodBeforeAdvice) { 10 this.methodBeforeAdvice = methodBeforeAdvice; 11 } 12 13 public ProxyFactoryBean(Teacher teacher) { // 1.指定要代理的目标对象 14 this.target = teacher; 15 } 16 17 @Override 18 public void teach() { 19 20 } 21 22 // 代理方法的编写 23 @Override 24 public int add(int i, int j) { 25 try { 26 // 1.如果设置了前置通知,执行前置通知 27 if (methodBeforeAdvice != null) { 28 Method method = ITeacher.class.getMethod("add", int.class, int.class); 29 List<Object> objectList = new ArrayList<>(); 30 objectList.add(i); 31 objectList.add(j); 32 Object[] objects = objectList.toArray(); 33 34 methodBeforeAdvice.before(method, objects, target); 35 } 36 37 // 2.执行目标方法 38 return target.add(i, j); 39 40 // 3.如果设置了后置通知,执行后置通知 41 } catch (Throwable e) { 42 e.printStackTrace(); 43 } 44 return 0; 45 } 46 }
1 // 测试类 2 public class Main { 3 public static void main(String[] args) { 4 // 创建代理对象,同时将创建的目标对象作为参数传递,即为谁代理. 5 ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean(new Teacher()); 6 // 设置一个前置通知 7 proxyFactoryBean.setMethodBeforeAdvice(new MyMethodBeforeAdvice()); 8 // 通过代理对象执行目标对象的方法. 9 proxyFactoryBean.add(1, 4); 10 } 11 } 12 13 // 前置通知=====1======函数名:add 14 // 前置通知=====2======参数值:[1,3] 15 // 前置通知=====3======对象值:{} 16 // 执行目标方法:老师正在做加法,结果为:4
3、通知类别
除了案例中使用的前置通知,Spring中还提供了如下几种通知:
通知类型 | 接口 | 描述 |
前置通知 | org.springframework.aop.MethodBeforeAdvice | 在目标方法前调用 |
后置通知 | org.springframework.aop.AfterReturningAdvice | 在目标方法后调用 |
环绕通知 | org.aopalliance.intercept.MethodInterceptor | 拦截对目标方法调用 |
异常通知 | org.springframework.aop.ThrowsAdvice | 目标方法抛出异常时调用 |
引入通知 | org.springframework.aop.support.NameMatchMethodPointcutAdvisor | 可以指定切入点 |
前置通知:接口提供了获得目标方法,参数和目标对象的机会。该接口唯一能阻止目标方法被调用的途径是抛出异常或(System.exit())。
后置通知:同前置通知类似。
环绕通知:该通知能够控制目标方法是否真的被调用。通过invocation.proceed()方法来调用。该通知可以控制返回的对象。可以返回一个与proceed()方法返回对象完全不同的对象。但要谨慎使用。特别注意:配置通知时,指定要织入的通知,环绕顺序不同,会影响执行顺序。
异常通知:该接口为标识性(tag)接口,没有任何方法,但实现该接口的类必须要有如下形式的方法:
void afterThrowing(Throwable throwable);
void afterThrowing(Method m,Object[] os,Object target,Exception e);
第一个方法只接受一个参数:需要抛出的异常。
第二个方法接受异常、被调用的方法、参数以及目标对象
引入通知:以前定义的通知类型是在目标对象的方法被调用的周围织入。引入通知给目标对象添加新的方法和属性。
几种通知:
1 // 前置通知(上面已介绍过) 2 public class MyMethodBeforeAdvice implements MethodBeforeAdvice { 3 // 目标方法、参数、目标对象 4 @Override 5 public void before(Method method, Object[] objects, Object target) throws Throwable { 6 System.out.println("前置通知=====1======函数名:" + method.getName()); 7 System.out.println("前置通知=====2======参数值:" + JSON.toJSONString(objects)); 8 System.out.println("前置通知=====3======对象值:" + JSON.toJSONString(target)); 9 } 10 } 11 12 // 后置通知 13 public class MyAfterReturningAdvice implements AfterReturningAdvice { 14 // object:方法的返回值 15 // method:目标方法 16 // objects:参数 17 // target:目标对象.注:该对象由前置通知传入的 target 18 @Override 19 public void afterReturning(Object object, Method method, Object[] objects, Object target) throws Throwable { 20 System.out.println("后置通知=====0======返回值:" + object); 21 System.out.println("后置通知=====1======函数名:" + method.getName()); 22 System.out.println("后置通知=====2======参数值:" + JSON.toJSONString(objects)); 23 System.out.println("后置通知=====3======对象值:" + JSON.toJSONString(target)); 24 } 25 } 26 27 // 环绕通知 28 public class MyMethodInterceptor implements MethodInterceptor { 29 // object:目标方法的返回值 30 @Override 31 public Object invoke(MethodInvocation invocation) throws Throwable { 32 System.out.println("============环绕前=============="); 33 Object object = invocation.proceed(); // 这里会切入目标方法 34 System.out.println("============环绕后=============="); 35 return object; 36 } 37 } 38 39 // 异常通知 40 public class MyThrowsAdvice implements ThrowsAdvice { 41 42 public void afterThrowing(Method method, Object[] objects, Object target, Exception e) { 43 System.out.println("异常通知=====1======函数名:" + method.getName()); 44 System.out.println("异常通知=====2======参数值:" + JSON.toJSONString(objects)); 45 System.out.println("异常通知=====3======对象值:" + JSON.toJSONString(target)); 46 System.out.println("异常通知=====4======异常值:" + JSON.toJSONString(e.getMessage())); 47 } 48 }
经过上面案例及四种通知的说明,可以看到,ProxyFactoryBean是一个在BeanFactory中显式创建代理对象的中心类,可以给它一个要代理的目标对象、一个要代理的接口集、一个要织入的通知,他将创建一个崭新的代理对象。
引入通知(切点):前四种通知已经指明了在目标方法前,还是后,还是环绕调用。如果不能表达在什么地方应用通知的话,通知将毫无用处,这就是切入点的用处。
切入点决定了一个特定的类的特定方法是否满足一定的规则。若符合,通知就应用到该方法上。引入通知可以指定切入点(即指定在哪些方法上织入通知)。
注:引入通知并不像前4个介绍的那样是一个通知。而重点思想在于:①在哪里(切点,或者说方法)引入?②引入一个什么样的通知?
规则如下:
符号 | 描述 | 示例 | 匹配 | 不匹配 |
. | 匹配任何单个字符 | setFoo. | setFooB | setFooBar setFooB |
+ | 匹配前一个字符一次或多次 | setFoo.+ | setFooBar setFooB | setFoo |
* | 匹配前一个字符0次或多次 | setFoo.* | setFoosetFooB, setFooBar | |
\ | 匹配任何正则表达式符号 | \.setFoo. | bar.setFoo | setFoo |
下面给出一份完整的配置文件 application.xml
1 <!-- 配置目标对象 --> 2 <bean id="teacher" class="com.lx.test.Teacher"/> 3 4 <!-- 配置前置通知 --> 5 <bean id="myMethodBeforeAdvice" class="com.lx.advice.MyMethodBeforeAdvice"/> 6 <!-- 配置后置通知 --> 7 <bean id="myAfterReturningAdvice" class="com.lx.advice.MyAfterReturningAdvice"/> 8 <!-- 配置环绕通知 --> 9 <bean id="myMethodInterceptor" class="com.lx.advice.MyMethodInterceptor"/> 10 <!-- 配置异常通知 --> 11 <bean id="myThrowsAdvice" class="com.lx.advice.MyThrowsAdvice"/> 12 13 <!-- 配置引入通知 --> 14 <bean id="pointcutAdvisor" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor"> 15 <property name="advice" ref="myMethodBeforeAdvice"/> 16 <property name="mappedNames"> 17 <list> 18 <value>add*</value> <!-- 对以add开头的函数织入前置通知 --> 19 <value>del*</value> 20 <value>teach*</value> 21 </list> 22 </property> 23 </bean> 24 25 <!-- 配置代理对象 --> 26 <bean id="proxyFactoryBean" class="org.springframework.aop.framework.ProxyFactoryBean"> 27 <!-- 指定要代理的目标对象 --> 28 <property name="target" ref="teacher"/> 29 30 <!-- 指定要代理的接口集 --> 31 <property name="proxyInterfaces"> 32 <list> 33 <value>com.lx.test.ITeacher</value> 34 </list> 35 </property> 36 37 <!-- 指定要织入的通知 --> 38 <property name="interceptorNames"> 39 <list> 40 <value>myMethodBeforeAdvice</value> 41 <value>myAfterReturningAdvice</value> 42 <value>myMethodInterceptor</value> 43 <value>myThrowsAdvice</value> 44 45 <value>pointcutAdvisor</value> 46 </list> 47 </property> 48 </bean>
1 // 测试类 2 public class Main { 3 public static void main(String[] args) { 4 ApplicationContext app = new ClassPathXmlApplicationContext("beans.xml"); 5 ITeacher iTeacher = (ITeacher) app.getBean("proxyFactoryBean"); 6 int add = iTeacher.add(11, 22); 7 } 8 } 9 10 // 前置通知=====1======函数名:add 11 // 前置通知=====2======参数值:[11,22] 12 // 前置通知=====3======对象值:{} 13 // ============环绕前============== 14 // 前置通知=====1======函数名:add 15 // 前置通知=====2======参数值:[11,22] 16 // 前置通知=====3======对象值:{} 17 // 执行目标方法:老师正在做加法,结果为:33 18 // ============环绕后============== 19 // 后置通知=====0======返回值:33 20 // 后置通知=====1======函数名:add 21 // 后置通知=====2======参数值:[11,22] 22 // 后置通知=====3======对象值:{} 23 24 // row14、15、16是由于配置了引入通知又执行了一次前置通知
4、小结
Spring在运行时通知对象,在运行期创建代理,不需要特殊的编译器。Spring有两种代理方式:
若目标对象实现了若干接口:Spring使用JDK的java.lang.reflect.Proxy类代理。该类让Spring动态产生一个新类,它实现了所需的接口,织入了通知和代理对目标对象的所有请求。
若目标对象没有实现任何接口:Spring使用CGLIB库生成目标对象的子类。使用该方式时需要注意:
①对接口创建代理优于对类创建代理,因为会产生更加松耦合的系统。对类代理是让遗留系统或无法实现接口的第三方类库同样可以得到通知,这种方式应该是备用方案。
②标记为final的方法不能够被通知。Spring是为目标类产生子类,任何需要被通知的方法都需要被复写,将通知织入。final方法是不允许重写的。
Spring实现了aop联盟接口。Spring只支持方法连接点,不提供属性接入点。Spring的观点是属性拦截破坏了封装。面向对象的概念是对象自己处理工作,其他对象只能通过方法调用的得到的结果。
问:说Spring的aop中,当你通过代理对象去实现aop的时候,获取的ProxyFactoryBean是什么类型?
答:返回的是一个代理对象,如果目标对象实现了接口,则Spring使用jdk动态代理技术;如果目标对象没有实现接口,则Spring使用CGLIB技术。