Spring AOP
1、Spring AOP简介
1)、AOP的全称是Aspect-Oriented Programming,即面向切面编程(也称面向方面编程)。它是面向对象编程(OOP)的一种补充,目前已成为一种比较成熟的编程方式。
2)、AOP采取横向抽取机制,将分散在各个方法中的重复代码提取出来,然后在程序编译或运行时,再将这些提取出来的代码应用到需要执行的地方。这种采用横向抽取机制的方式,采用传统的OOP思想显然是无法办到的,因为OOP只能实现父子关系的纵向的重用(纵向继承体系重复性代码)。虽然AOP是一种新的编程思想,但却不是OOP的替代品,它只是OOP的延伸和补充。 经典应用:事务管理。实现原理:AOP底层将采用代理机制进行实现。
3)、类与切面的关系:
AOP的使用,使开发人员在编写业务逻辑时可以专心于核心业务,而不用过多的关注于其他业务逻辑的实现,这不但提高了开发效率,而且增强了代码的可维护性。
4)、目前最流行的AOP框架有2个:Spring AOP和AspectJ(一个基于Java语言的AOP框架)。
5)、AOP术语:Aspect、Joinpoint、Pointcut、Advice、Target Object、Proxy和Weaving。
a)、Aspect(切面,切入点和通知的结合):该类要被Spring容器识别为切面,需要在配置文件中通过<bean>元素指定。
b)、Joinpoint(连接点,即那些可能被拦截到的方法):实际上是对象的一个操作,例如方法的调用或异常的抛出。在Spring AOP中,连接点就是指方法的调用。
c)、Pointcut(切入点,已被增强的连接点,是连接点的子集):通常在程序中,切入点指的是类或者方法名,如某个通知要应用到所有以add开头的方法中,那么所有满足这一规则的方法都是切入点。
d)、Advice(通知/增强处理,增强代码,例如:before、after等):AOP框架在特定的切入点执行增强处理,即在定义好的切入点处所要执行的程序代码。可以将其理解为切面类中的方法,它是切面的具体实现。
e)、Target Object(目标对象,需要被代理的类):是指所有被通知的对象,也称为被增强对象。如果AOP框架采用的是动态的AOP实现,那么该对象就是一个被代理对象。
f)、Proxy(代理):将通知应用到目标对象之后,被动态创建的对象。
g)、Weaving(织入):将切面代码(增强advice应用到)插入到目标对象target上,从而生成代理对象的过程。
2、动态代理
1)、JDK动态代理(实现接口的代理方式)
a)、JDK动态代理是通过java.lang.reflect.Proxy 类来实现的,我们可以调用Proxy类的newProxyInstance()方法来创建代理对象。对于使用业务接口的类,Spring默认会使用JDK动态代理来实现AOP。
b)、src->com.itheima.jdk
①UserDao.java
1 package com.itheima.jdk; 2 3 public interface UserDao { 4 public void addUser(); // 添加方法 5 public void deleteUser(); // 删除方法 6 }
②目标类:UserDaoImpl.java(接口的实现类、即切入点)
1 package com.itheima.jdk; 2 3 public class UserDaoImpl implements UserDao { 4 // 本案例中会将实现类UserDaoImpl作为 目标类 ,对其中的方法进行增强处理 5 6 public void addUser() { 7 System.out.println("添加用户"); 8 } 9 10 public void deleteUser() { 11 System.out.println("删除用户"); 12 } 13 }
c)、src->com.itheima.aspect
③切面类:MyAspect.java(存放多个通知advice,即在切入点处增强)
1 package com.itheima.aspect; 2 3 // 切面类:可以存在多个通知Advice(即增强方法) 4 public class MyAspect { 5 // 切面类中的这2个方法就是通知 6 public void check_Permissions() { 7 System.out.println("模拟检查权限..."); 8 } 9 public void log() { 10 System.out.println("模拟记录日志..."); 11 } 12 }
d)、src->com.itheima.jdk
④代理类:JdkProxy.java(编写工厂生成代理)
1 package com.itheima.jdk; 2 import java.lang.reflect.InvocationHandler; 3 import java.lang.reflect.Method; 4 import java.lang.reflect.Proxy; 5 import com.itheima.aspect.MyAspect; 6 /** 7 * JDK代理类 8 */ 9 public class JdkProxy implements InvocationHandler{ // 需要实现InvocationHandler接口,并编写代理方法 10 // 1、声明目标类接口对象userDao 11 private UserDao userDao; 12 13 // 2、创建代理方法 14 public Object createProxy(UserDao userDao) { 15 this.userDao = userDao; 16 17 // 1)、类加载器:当前类名.class.getClassLoader(); 18 // 目标类实例.getClass().getClassLoader(); 19 ClassLoader classLoader = JdkProxy.class.getClassLoader(); 20 21 // 2)、被代理对象实现的所有接口 22 Class[] clazz = userDao.getClass().getInterfaces(); 23 24 // 3)、使用代理类,进行增强,返回的是代理后的对象 25 return Proxy.newProxyInstance(classLoader,clazz,this); 26 /* 参数1:classLoader(当前类的类加载器):动态代理类,运行时创建,任何类都需要类加载器将其加载到内存 27 * 参数2:interfaces:被代理对象需要实现的所有接口 28 * 方式1:目标类实例.getClass().getInterfaces(); 注意:只能获得自己的接口,不能获得父类接口 29 * 方式2:new Class[]{UserDao.class} 30 * 参数3:InvocationHandler 处理类(接口),必须进行实现类,一般采用匿名内部类,this就是JdkProxy本身 31 */ 32 } 33 /* 34 * 所有动态代理类的方法调用,都会交由invoke()方法去处理,执行一次方法,则调用一次invoke()方法 35 * proxy: 被代理后的对象 this 36 * method: 将要被执行的方法信息(反射) 37 * args: 执行方法时需要的参数 38 */ 39 @Override // 实现了接口中的invoke()方法 40 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 41 // 1、声明切面(通知) 42 MyAspect myAspect = new MyAspect(); 43 44 // 2、前增强(目标类) 45 myAspect.check_Permissions(); 46 47 // 3、在目标类上调用方法,并传入参数, method.invoke(目标类对象, 方法实际参数); 48 Object obj = method.invoke(userDao, args); 49 50 // 4、后增强(目标类) 51 myAspect.log(); 52 53 return obj; 54 } 55 }
⑤测试类:JdkTest.java
1 package com.itheima.jdk; 2 public class JdkTest{ 3 public static void main(String[] args) { 4 // 1、创建代理对象 5 JdkProxy jdkProxy = new JdkProxy(); 6 7 // 2、创建目标对象 8 UserDao userDao= new UserDaoImpl(); 9 10 // 3、从代理对象中获取增强后的目标对象 11 UserDao userDao1 = (UserDao) jdkProxy.createProxy(userDao); 12 13 // 4、执行方法 14 userDao1.addUser(); 15 userDao1.deleteUser(); 16 } 17 }
⑥运行结果:
2)、CGLIB代理
a)、如果想代理没有实现接口的类,那么可以使用CGLIB代理。使用动态代理的对象必须实现一个或多个接口。
b)、CGLIB(Code Generation Library)是一个高性能开源的代码生成包,它采用非常底层的字节码技术,对指定的目标类生成一个子类,并对子类进行增强。
c)、src->com.itheima.cglib
①UserDao.java
1 package com.itheima.cglib; 2 //目标类:UserDao 3 public class UserDao { 4 public void addUser() { 5 System.out.println("添加用户"); 6 } 7 public void deleteUser() { 8 System.out.println("删除用户"); 9 } 10 }
②代理类:CglibProxy.java
1 package com.itheima.cglib; 2 import java.lang.reflect.Method; 3 import org.springframework.cglib.proxy.Enhancer; 4 import org.springframework.cglib.proxy.MethodInterceptor; 5 import org.springframework.cglib.proxy.MethodProxy; 6 import com.itheima.aspect.MyAspect; 7 8 // 代理类CglibProxy 9 public class CglibProxy implements MethodInterceptor{ // 需要实现MethodInterceptor接口,并实现接口中的intercept方法 10 // 代理方法 11 public Object createProxy(Object target) { 12 // 1、创建一个动态类对象,它是CGLIB的核心类 13 Enhancer enhancer = new Enhancer(); 14 15 // 2、确定需要增强的类,设置其父类CglibProxy 16 enhancer.setSuperclass(target.getClass()); 17 18 // 3、添加回调函数,this就是代理类 19 enhancer.setCallback(this); 20 21 // 4、返回创建的代理类 22 return enhancer.create(); 23 } 24 /** 25 * proxy CGlib根据指定父类生成的代理对象 26 * method 拦截的方法 27 * args 拦截方法的参数数组 28 * methodProxy 方法的代理对象,用于执行父类的方法 29 */ 30 31 @Override 32 public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { 33 // 1、创建切面类对象(通知) 34 MyAspect myAspect = new MyAspect(); 35 36 // 2、前增强(目标类) 37 myAspect.check_Permissions(); 38 39 // 3、目标方法执行 40 Object obj = methodProxy.invokeSuper(proxy, args); 41 42 // 4、后增强(目标类) 43 myAspect.log(); 44 return obj; 45 } 46 }
③测试类:CglibTest.java
1 package com.itheima.cglib; 2 // 测试类 3 public class CglibTest { 4 public static void main(String[] args) { 5 // 1、创建代理对象 6 CglibProxy cglibProxy = new CglibProxy(); 7 8 // 2、创建目标对象 9 UserDao userDao = new UserDao(); 10 11 // 3、获取增强后的目标对象 12 UserDao userDao1 = (UserDao)cglibProxy.createProxy(userDao); 13 14 // 4、执行方法 15 userDao1.addUser(); 16 userDao1.deleteUser(); 17 } 18 }
④运行结果:
3、基于代理类的AOP实现
1)、ProxyFactoryBean是FactoryBean接口的实现类,FactoryBean负责实例化一个Bean,而ProxyFactoryBean负责为其他Bean创建代理实例。在Spring中,使用ProxyFactoryBean是创建AOP代理的最基本方式。
2)、Spring的通知类型:Spring按照通知在目标类方法的连接点位置,可以分为5种类型,具体如下:
org.springframework.aop.MethodBeforeAdvice(前置通知)
在目标方法执行前实施增强,可以应用于权限管理等功能。
org.springframework.aop.AfterReturningAdvice(后置通知)
在目标方法执行后实施增强,可以应用于关闭流、上传文件、删除临时文件等功能。
org.aopalliance.intercept.MethodInterceptor(环绕通知)
在目标方法执行前后实施增强,可以应用于日志、事务管理等功能。
org.springframework.aop.ThrowsAdvice(异常抛出通知)
在方法抛出异常后实施增强,可以应用于处理异常记录日志等功能。
org.springframework.aop.IntroductionInterceptor(引介通知)
在目标类中添加一些新的方法和属性,可以应用于修改老版本程序。
3)、ProxyFactoryBean类中的常用可配置属性如下:
4)、src->com.itheima.factorybean
①切面类:MyAspect.java
1 package com.itheima.factorybean; 2 import org.aopalliance.intercept.MethodInterceptor; 3 import org.aopalliance.intercept.MethodInvocation; 4 // 切面类MyAspect 5 public class MyAspect implements MethodInterceptor { // 实现该接口,并且实现接口中的invoke()方法 6 7 @Override 8 public Object invoke(MethodInvocation mi) throws Throwable { 9 10 check_Permissions(); 11 // 执行目标方法 12 Object obj = mi.proceed(); 13 log(); 14 return obj; 15 } 16 public void check_Permissions(){ // 以下2个是增强的方法,也就是通知 17 System.out.println("模拟检查权限..."); 18 } 19 public void log(){ 20 System.out.println("模拟记录日志..."); 21 } 22 }
②配置文件:applicationContext.xml
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://www.springframework.org/schema/beans 5 http://www.springframework.org/schema/beans/spring-beans-4.3.xsd"> 6 7 <!-- 1、 目标类 --> 8 <bean id="userDao" class="com.itheima.jdk.UserDaoImpl" /> 9 10 <!-- 2、 切面类 --> 11 <bean id="myAspect" class="com.itheima.factorybean.MyAspect" /> 12 13 <!-- 3、使用Spring代理工厂定义一个名称为userDaoProxy的代理对象 --> 14 <bean id="userDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> 15 16 <!-- 3.1 、指定代理实现的接口--> 17 <property name="proxyInterfaces" value="com.itheima.jdk.UserDao" /> 18 19 <!-- 3.2 、指定目标对象 --> 20 <property name="target" ref="userDao" /> 21 22 <!-- 3.3 、指定切面,织入环绕通知 --> 23 <property name="interceptorNames" value="myAspect" /> 24 25 <!-- 3.4 指定代理方式,true:使用cglib,false(默认):使用jdk动态代理 --> 26 <property name="proxyTargetClass" value="true" /> 27 </bean> 28 </beans>
③测试类:ProxyFactoryBeanTest.java
1 package com.itheima.factorybean; 2 import org.springframework.context.ApplicationContext; 3 import org.springframework.context.support.ClassPathXmlApplicationContext; 4 import com.itheima.jdk.UserDao; 5 // 测试类 6 public class ProxyFactoryBeanTest { 7 public static void main(String args[]) { 8 String xmlPath = "com/itheima/factorybean/applicationContext.xml"; 9 ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath); 10 // 从Spring容器获得内容 11 UserDao userDao = (UserDao) applicationContext.getBean("userDaoProxy"); 12 // 执行方法 13 userDao.addUser(); 14 userDao.deleteUser(); 15 } 16 }
④运行结果:
4、AspectJ开发
1)、使用AspectJ实现AOP有两种方式:一种是基于XML的声明式AspectJ,另一种是基于注解的声明式AspectJ。
2)、基于XML的声明式AspectJ
a)、基于XML的声明式AspectJ是指通过XML文件来定义切面、切入点及通知,所有的切面、切入点和通知都必须定义在<aop:config>元素内。
b)、<aop:config>元素及其子元素如下:
c)、XML文件中常用元素的配置方式如下:
1 <!--定义切面Bean --> 2 <bean id="myAspect" class="com.itheima.aspectj.xml.MyAspect" /> 3 <aop:config> 4 <!-- 配置切面 --> 5 <aop:aspect id="aspect" ref="myAspect"> 6 <!-- 配置切入点 --> 7 <aop:pointcut expression="execution(* com.itheima.jdk.*.*(..))“ id="myPointCut" /> 8 <aop:before method="myBefore" pointcut-ref="myPointCut" /> 9 <aop:after-returning method="myAfterReturning“ pointcut-ref="myPointCut" returning="returnVal" /> 10 <aop:around method="myAround" pointcut-ref="myPointCut" /> 11 <aop:after-throwing method="myAfterThrowing“ pointcut-ref="myPointCut" throwing="e" /> 12 <aop:after method="myAfter" pointcut-ref="myPointCut" /> 13 </aop:aspect> 14 </aop:config>
d)、配置切面:在Spring的配置文件中,配置切面使用的是<aop:aspect>元素,该元素会将一个已定义好的Spring Bean转换成切面Bean,所以要在配置文件中先定义一个普通的Spring Bean(如myAspect)。配置<aop:aspect>元素时,通常会指定id和ref两个属性:
e)、配置切入点:当<aop:pointcut>元素作为<aop:config>元素的子元素定义时,表示该切入点是全局切入点,它可被多个切面所共享;当<aop:pointcut>元素作为<aop:aspect>元素的子元素时,表示该切入点只对当前切面有效。在定义<aop:pointcut>元素时,通常会指定id和expression两个属性:
f)、切入点表达式:execution(* com.itheima.jdk.*.*(..)) 是定义的切入点表达式,该切入点表达式的意思是匹配com.itheima.jdk包中任意类的任意方法的执行。
其中execution()是表达式的主体,第1个*表示的是返回类型,使用*代表所有类型;com.itheima.jdk表示的是需要拦截的包名,后面第2个*表示的是类名,使用*代表所有的类;第3个*表示的是方法名,使用*表示所有方法;后面(..)表示方法的参数,其中的".."表示任意参数。注意:第1个*与包名之间有一个空格。
g)、切入点表达式的基本格式:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
带有问号(?)的部分表示可配置项,而其他部分属于必须配置项。
h)、配置通知:使用<aop:aspect>的子元素可以配置5种常用通知,这5个子元素不支持使用子元素,但在使用时可以指定一些属性,其常用属性及其描述如下:
i)、src->com.itheima.aspectj.xml
①切面类:MyAspect.java
1 package com.itheima.aspectj.xml; 2 import org.aspectj.lang.JoinPoint; 3 import org.aspectj.lang.ProceedingJoinPoint; 4 /** 5 *切面类,在此类中编写通知 6 */ 7 public class MyAspect { 8 // 前置通知 9 public void myBefore(JoinPoint joinPoint) { //使用JoinPoint接口及其子接口ProceedingJoinPoint 10 System.out.print("前置通知 :模拟执行权限检查...,"); 11 System.out.print("目标类是:"+joinPoint.getTarget() ); 12 System.out.println(",被织入增强处理的目标方法为:"+joinPoint.getSignature().getName()); 13 } 14 // 后置通知 15 public void myAfterReturning(JoinPoint joinPoint) { 16 System.out.print("后置通知:模拟记录日志...," ); 17 System.out.println("被织入增强处理的目标方法为:" + joinPoint.getSignature().getName()); 18 } 19 /** 20 * 环绕通知 21 * ProceedingJoinPoint 是JoinPoint子接口,表示可以执行目标方法 22 * 1.必须是Object类型的返回值 23 * 2.必须接收一个参数,类型为ProceedingJoinPoint 24 * 3.必须throws Throwable 25 */ 26 public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { 27 // 开始 28 System.out.println("环绕开始:执行目标方法之前,模拟开启事务..."); 29 // 执行当前目标方法 30 Object obj = proceedingJoinPoint.proceed(); 31 // 结束 32 System.out.println("环绕结束:执行目标方法之后,模拟关闭事务..."); 33 return obj; 34 } 35 // 异常通知 36 public void myAfterThrowing(JoinPoint joinPoint, Throwable e) { 37 System.out.println("异常通知:" + "出错了" + e.getMessage()); 38 } 39 // 最终通知 40 public void myAfter() { 41 System.out.println("最终通知:模拟方法结束后的释放资源..."); 42 } 43 }
②配置文件:applicationContext.xml
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:aop="http://www.springframework.org/schema/aop" 5 xsi:schemaLocation="http://www.springframework.org/schema/beans 6 http://www.springframework.org/schema/beans/spring-beans-4.3.xsd 7 http://www.springframework.org/schema/aop 8 http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"> 9 <!-- 1 、目标类 --> 10 <bean id="userDao" class="com.itheima.jdk.UserDaoImpl" /> 11 12 <!-- 2 、切面 --> 13 <bean id="myAspect" class="com.itheima.aspectj.xml.MyAspect" /> 14 15 <!-- 3 、aop编程 --> 16 <aop:config> 17 <!-- 配置切面 --> 18 <aop:aspect ref="myAspect"> 19 <!-- 3.1 、配置切入点,通知最后增强哪些方法 --> 20 <aop:pointcut expression="execution(* com.itheima.jdk.*.*(..))" id="myPointCut" /> 21 22 <!-- 3.2 、关联通知Advice和切入点pointCut --> 23 <!-- 3.2.1 、前置通知 --> 24 <aop:before method="myBefore" pointcut-ref="myPointCut" /> 25 26 <!-- 3.2.2 后置通知,在方法返回之后执行,就可以获得返回值 27 returning属性:用于设置后置通知的第二个参数的名称,类型是Object 28 --> 29 <aop:after-returning method="myAfterReturning" 30 pointcut-ref="myPointCut" returning="returnVal" /> 31 32 <!-- 3.2.3 环绕通知 --> 33 <aop:around method="myAround" pointcut-ref="myPointCut" /> 34 35 <!-- 3.2.4 抛出通知:用于处理程序发生异常--> 36 <!-- * 注意:如果程序没有异常,将不会执行增强 --> 37 <!-- * throwing属性:用于设置通知第二个参数的名称,类型Throwable --> 38 <aop:after-throwing method="myAfterThrowing" 39 pointcut-ref="myPointCut" throwing="e" /> 40 41 <!-- 3.2.5 最终通知:无论程序发生任何事情,都将执行 --> 42 <aop:after method="myAfter" pointcut-ref="myPointCut" /> 43 </aop:aspect> 44 </aop:config> 45 </beans>
③测试类:TestXmlAspectj.java
1 package com.itheima.aspectj.xml; 2 import org.springframework.context.ApplicationContext; 3 import org.springframework.context.support.ClassPathXmlApplicationContext; 4 import com.itheima.jdk.UserDao; 5 // 测试类 6 public class TestXmlAspectj { 7 public static void main(String args[]) { 8 String xmlPath = "com/itheima/aspectj/xml/applicationContext.xml"; 9 ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath); 10 // 1 、从spring容器获得内容 11 UserDao userDao = (UserDao) applicationContext.getBean("userDao"); 12 // 2、 执行方法 13 userDao.addUser(); 14 } 15 }
④运行结果:
3)、基于注解的声明式AspectJ
a)、AspectJ框架为AOP的实现提供了一套注解,用以取代Spring配置文件中为实现AOP功能所配置的臃肿代码。AspectJ的注解及其描述如下所示:
b)、src->com.itheima.aspectj.annotation