Hello,今天给各位童鞋们分享Spring AOP,赶紧拿出小本子记下来吧!
1、什么是AOP
面向切面编程(Aspect Oriented Programming)提供了另一种角度来思考程序的结构,通过这种方式弥补面向对象编程(Object Oriented Programming)的不足。除了类以外,AOP提供了切面,切面对关注点进行模块化,例如横切多个类型和对象的事务管理(这些关注点术语通常称作横切(crosscutting)关注点)。Spring AOP是Spring的一个重要组件,但是Spring IOC并不依赖于Spring AOP,这意味着你可以自由选择是否使用AOP,AOP提供了强大的中间件解决方案,这使得Spring IOC更加完善。我们可以通过AOP来实现日志监听,事务管理,权限控制等等。
1.1、AOP术语
切面(Aspect):一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是Java应用程序中一个关于横切关注点的很好的例子。在Spring AOP中,切面可以使用通过类(基于模式(XML)的风格)或者在普通类中以@Aspect注解(AspectJ风格)来实现。
连接点(Join point):程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中一个连接点总是代表一个方法的执行。个人理解:AOP拦截到的方法就是一个连接点。通过声明一个org.aspectj.lang.JoinPoint类型参数我们可以在通知(Advice)中获得连接点的信息。这个在稍后会给出案例。
通知(Advice):在切面(Aspect)的某个特定连接点上(Join point)执行的动作。通知的类型包括"around",“before”,"after"等等。通知的类型将在后面进行讨论。许多AOP框架,包括Spring 都是以拦截器作为通知的模型,并维护一个以连接点为中心的拦截器链。总之就是AOP对连接点的处理通过通知来执行。个人理解:Advice指当一个方法被AOP拦截到的时候要执行的代码。
切入点(Pointcut):匹配连接点(Join point)的断言。通知(Advice)跟切入点表达式关联,并在与切入点匹配的任何连接点上面运行。切入点表达式如何跟连接点匹配是AOP的核心,Spring默认使用AspectJ作为切入点语法。个人理解:通过切入点的表达式来确定哪些方法要被AOP拦截,之后这些被拦截的方法会执行相对应的Advice代码。
引入(Introduction):声明额外的方法或字段。Spring AOP允许你向任何被通知(Advice)对象引入一个新的接口(及其实现类)。个人理解:AOP允许在运行时动态的向代理对象实现新的接口来完成一些额外的功能并且不影响现有对象的功能。
目标对象(Target object):被一个或多个切面(Aspect)所通知(Advice)的对象,也称作被通知对象。由于Spring AOP是通过运行时代理实现的,所以这个对象永远是被代理对象。个人理解:所有的对象在AOP中都会生成一个代理类,AOP整个过程都是针对代理类在进行处理。
AOP代理(AOP proxy):AOP框架创建的对象,用来实现切面契约(aspect contract)(包括通知方法执行等功能),在Spring中AOP可以是JDK动态代理或者是CGLIB代理。
织入(Weaving):把切面(aspect)连接到其他的应用程序类型或者对象上,并创建一个被通知对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯AOP框架一样,在运行时完成织入。个人理解:把切面跟对象关联并创建该对象的代理对象的过程。
1.2、AOP框架
Spring AOP:Spring AOP使用纯Java实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类织入增强的代码。
AspectJ:AspectJ是一个基于Java的AOP框架,从Spring2.0开始,SpringAOP引入了对AspectJ的支持,AspectJ扩展了Java语言,提供了一个专门的编辑器,在编译时提供横向代码的织入。
2、动态代理
Spring中的AOP代理,可以是JDK动态代理,也可以是CGLIB代理。
2.1、JDK动态代理
JDK动态代理是通过java.lang.reflect.Proxy类来实现的,我们可以调用Proxy类的newProxyInstance()方法来创建代理对象。对于使用业务接口的类,Spring默认会使用JDK动态代理来实现AOP。
接下来通过一个案例演示Spring中的JDK动态代理
1.创建Java工程,导入Spring框架需要的JAR包并发布到类路径下,如下图所示:
2.创建接口,并编写添加和删除方法
package com.nynu.qdy.jdk;
public interface UserDao {
public void addUser();
public void deleteUser();
}
3.创建接口实现类,实现接口中的方法,并在方法中输出一条语句
package com.nynu.qdy.jdk;
//目标类
public class UserDaoImpl implements UserDao {
public void addUser() {
System.out.println("添加用户");
}
public void deleteUser() {
System.out.println("删除用户");
}
}
4.创建切面类MyAspect,并在该类中定义模拟权限检查和日志记录的方法。这两个方法表示切面中的通知
package com.nynu.qdy.aspect;
//切面类:可以存在多个通知Advice(即增强的方法)
public class MyAspect {
public void check_Permissions() {
System.out.println("模拟检查权限.....");
}
public void log() {
System.out.println("模拟记录日志....");
}
}
5.创建代理类,该类需要实现InvocationHandller接口,并编写代理方法。在代理方法中,需要通过Proxy类实现动态代理
jdkProxy类实现了InvocationHandler接口,并实现了接口中的invoke方法, 所有动态代理类所调用的方法都会交由该方法处理。在创建的代理方法createProxy()中,使用了Proxy类的newProxyInstance()方法来创建代理对象。newProxyInstance()方法中包含3个参数,其中第一个参数是当前类的类加载器,第2个参数表示的是被代理对象实现的所有接口,第3个参数this代表的就是代理类jdkProxy本身。在invoke()方法中,目标类方法执行的前后分别执行切面类中的check_Permissions()方法和log()方法。
6.测试类
创建代理对象和目标对象
从代理对象中获得对目标对象userDao增强后的对象
最后调用该对象中的添加和删除方法
package com.nynu.qdy.jdk;
public class jdkTest {
public static void main(String[] args) {
// 创建代理对象
JdkProxy jdkProxy = new JdkProxy();
// 创建目标对象
UserDao userDao = new UserDaoImpl();
// 从代理对象中获取增强后的目标对象
UserDao userDao1 = (UserDao) jdkProxy.createProxy(userDao);
// 执行方法
userDao1.addUser();
userDao1.deleteUser();
}
}
7.结果
模拟检查权限.....
添加用户
模拟记录日志....
模拟检查权限.....
删除用户
模拟记录日志....
从结果可以看出,userDao实例中的添加用户和删除用户的方法已被成功调用,并且在调用前后分别增加了权限检查和记录日志的功能。这种实现了接口的动态代理方式,就是Spring中的JDK动态代理。
2.2、CGLIB动态代理
JDK动态代理的使用非常简单,但它还有一定的局限性一使 用动态代理的对象必须实现一个或多个接口。如果要对没有实现接只的类进行代理,那么可以使用CGLIB代理。
CGLIB ( Code Generation Library )是一个高性能开源的代码生成包,它采用非常底层的字 节码技术,对指定的目标类生成一个子类, 并对子类进行增强。在Spring的核心包中已经集成了CGLIB所需要的包,所以开发中不需要另外导入JAR包。
下面通过一个案例来演示CGLIB动态代理的实现过程
1.创建目标类,目标类不需要实现任何接口,只需定义所需要的方法
package com.nynu.qdy.cglib;
//目标类
public class UserDao {
public void addUser() {
System.out.println("添加用户");
}
public void deleteUser() {
System.out.println("删除用户");
}
}
2.创建代理类,该代理类需要实现MethodInterceptor接口,并实现接口中的intercept()方法
在该代理方法中,首先创建了一个动态类对象Enhancer,它是CGLIB的核心类;然后调用了Enhancer类的setSuperclass()方法来确定目标对象;加下例调用了setCalllback()方法添加回调函数,其中的this代表的就是代理类CglibProxy本身;最后通过return语句将创建的代理类对象返回。intercept()方法会在程序执行目标方法时被调用,方法运行时将会执行切面类中的增强方法。
3.测试
创建测试类CglibTest,在该类的main方法中首先创建代理对象和目标对象,然后从代理对象中获得增强后的目标对象,最后调用对象中的添加和删除方法。
package com.nynu.qdy.cglib;
public class CglibTest {
public static void main(String[] args) {
// 创建代理对象
CglibProxy cglibProxy = new CglibProxy();
// 创建目标对象
UserDao userDao = new UserDao();
// 获取增强后的对象
UserDao userDao1 = (UserDao) cglibProxy.createProxy(userDao);
// 执行方法
userDao1.addUser();
userDao1.deleteUser();
}
}
4.结果
模拟检查权限.....
添加用户
模拟记录日志....
模拟检查权限.....
删除用户
模拟记录日志....
从结果可以看出,目标类UserDao中的方法被成功调用并增强了。这种没有实现接口的代理方式,就是CGLIB代理。
3、基于代理类的AOP实现
Spring中的AOP代理默认就是使用JDK动态代理的方式实现的。在Spring中,使用ProxyFactoryBean是创建AOP代理的最基本方式。
3.1、Spring的通知类型
Spring中的通知按在目标类方法的连接点位置,可以分为以下5种类型:
org.allane.intercept.Methodinterceptor (环绕通知)
在目标方法执行前后实施增强,可以应用于日志、事务管理等功能。
org.springframework.aop.MethodBeforeAdvice (前置通知)
在目标方法执行前实施增强,可以应用于权限管理等功能。
org.springframework aop .AfterReturningAdvice (后置通知)
在目标方法执行后实施增强,可以应用于关闭流、上传文件、删除临时文件等功能。
org.springframework.aop.ThrowsAdvice (异常通知)
在方法抛出异常后实施增强,可以应用于处理异常记录日志等功能。
org.springtramework.aop.IntroductionInterceptor (引介通知)
在目标类中添加一些新的方法和属性,可以应用于修改老版本程序(增强类)。
3.2、ProxyFactoryBean
ProxyFactoryBean是FactoryBean接口的实现类,FactoryBean负责实例化一个Bean,而ProxyFactoryBean负责为其他Bean创建代理实例。ProxyFactoryBean类中的常用配置属性如下边所示:
对ProxyFactoryBean类有了初步的了解后,接下来通过一个典型的环绕通知案例,来演示Spring使用ProxyFactroyBean创建AOP代理
1.创建Java工程,导入Spring框架需要的JAR包并发布到类路径下,如下图所示:
2.创建切面类,实现MethodInterceptor接口,并实现接口中的invoke()方法,来执行目标方法
为了演示效果,在目标方法前后分别执行了检查权限和记录日志的方法,这两个方法也就是增强的方法,也就是通知
3.创建配置文件,并指定代理对象
通过< bean >元素定义了目标类和切面,然后使用ProxyFactoryBean类定义了代理对象。在定义的代理对象中,分别通过< property >子元素指定了代理实现的接口、代理的目标对象、需要织入目标类的通知以及代理方式。
4.创建测试类
5.结果
模拟检查权限...
添加用户
模拟记录日志...
模拟检查权限...
删除用户
模拟记录日志...
4、AspectJ开发
AspectJ是一个基于Java语言的AOP框架,它提供了强大的AOP功能。
使用AspectJ实现AOP的方式:
基于XML的声明式AspectJ
基于注解的声明式AspectJ
4.1、基于XML的声明式AspectJ
基于XML的声明式AspectJ是指通过XML文件来定义切面、切入点以及通知,所有的切面、切入点和通知都必须定义在< aop:config >元素内。
- 配置切面
在Spring的配置文件中,配置切面使用的是<aop:aspect >元素,该元素会将一个已定 义好的Spring Bean转换成切面Bean,所以要在配置文件中先定义一个普通的Spring Bean (如上述代码中定义的myAspect)。 定义完成后,通过<aop:aspect >元素的ref 属性即可引用该Bean。
配置<aop:aspect >元素时,通常会指定id和ref两个属性,如下表所示。
2.配置切入点
在Spring的配置文件中,切入点是通过<aop:pointcut >元素来定义的。当<aop:pointcut >元素作为<aop:config >元素的子元素定义时,表示该切入点是全局切入点,它可被多个切面所共享;当<aop:pointcut >元素作为<aop:aspect >元素的子元素时,表示该切入点只对当前切面有效。
在定义<aop:pointcut >元素时,通常会指定id和expression两个属性,如下表所示。
在上述配置代码片段中,execution(* com.itheima.jdk.. (…))就是定义的切入点表达式,该切入点表达式的意思是匹配com.itheima.jdk包中任意类的任意方法的执行。其中execution()是 表达式的主体,第1个**表示的是返回类型,使用代表所有类型; com.itheima.jdk 表示的是需要拦截的包名,后面第2个表示的是类名,使用代表所有的类;第3个表示的是方法名,使用表示所有方法;后面(…)表示方法的参数,其中的“”表示任意参数。需要注意的是,第1个与包名之间有-个空格。
上面示例中定义的切入点表达式只是开发中常用的配置方式,而Spring AOP中切入点表达式的基本格式如下:
execution (modi fiers-pattern? ret-type-pattern declaring-type-pattern?name -pattern (param-pattern) throws-pattern?)
上述格式中,各部分说明如下。
modifiers- pattern:表示定义的目标方法的访问修饰符,如public、 private 等。
ret-type- pattern:表示定义的目标方法的返回值类型,如void、String 等。
delaring-type- pattern: 表示定义的目标方法的类路径,如com.itheima.jdk.UserDaolmpl. ,
name- -pattern: 表示具体需要被代理的目标方法,如add()方法。
param- pattern:表示需要被代理的目标方法包含的参数,本章示例中目标方法参数都为空。
throws- pattern:表示需要被代理的目标方法抛出的异常类型。
其中带有问号(? ) 的部分,如mdifers- patterm、 declaring-type- pattern和throws -pattern表示可配置项;而其他部分属于必须配置项。
3.配置通知
在配置代码中,分别使用<aop:aspeq >的子元素配置了5种常用通知,这5个子元素不支持使用子元素,但在使用时可以指定一些属性,如下表所示。
了解了如何在XML文件中配置切面、切入点和通知后,接下来通过一个案例来演示如何在Spring中基于XML的声明式Aspect J
1.创建切面类,并在类中分别定义不同类型的通知
在切面类中,分别定义了5种不同类型的通知,在通知中使用了JoinPoint 接口及其子接口ProceedingJoinPoint作为参数来获得目标对象的类名、目标方法名和目标方法参数等。
需要注意的是,环绕通知必须接收一个类型为ProceedingJoinPoint的参数,返回值也必须是Object类型,且必须抛出异常。异常通知中可以传入Throwable类型的参数来输出异常信息。
2.创建配置文件,并编写相关配置
3.创建测试类,并在类中对方法进行增强
4.结果
前置通知:模拟执行权限检查:
目标类是:com.nynu.qdy.jdk.UserDaoImpl@184cf7cf
,被植入增强的目标方法为: addUser
环绕开始:执行目标方法之前,模拟开启事物。。。。
添加用户
最终通知:模拟方法结束后的释放资源
环绕结束:执行目标方法之后。关闭模拟事物。。。
后置通知:模拟记录日志:
被植入增强处理的目标方法为: addUser
4.2、基于注解的声明式AspectJ
与基于代理类的AOP实现相比,基于XML的声明式ApectJ要便捷得多,但是它也存在着一些 缺点,那就是要在Spring文件中配置大量的代码信息。为了解决这个问题,AspectJ 框架为AOP的实现提供了一套注解,用以取代Spring配置文件中为实现AOP功能所配置的臃肿代码。
关于AspectJ注解的介绍,如下表所示:
为了使读者可以快速地掌握这些注解,接下来重新使用注解的形式来实现上一小节的案例,具体步骤如下
1.复制上一节切面类,并做相应修改
在切面类中,首先使用@Aspect注解定义了切面类,由于该类在Spring中是作为组件使用的,所以还需要添加@Component注解才能生效。然后使用了@Poincut注解来配置切入点表达式,并通过定义方法来表示切入点名称。接下来在每个通知相应的方法上添加了相应的注解,并将切入点名称“myPointCut"作为参数传递给需要执行增强的通知方法。如果需要其他参数(如异常通知的异常参数),可以根据代码提示传递相应的属性值。
2.目标类com.itheima.jdk.UserDaolmpl中,添加注解@Repository(“userDao”)
package com.nynu.qdy.jdk;
import org.springframework.stereotype.Repository;
@Repository("userDao")
public class UserDaoImpl implements UserDao {
public void addUser() {
System.out.println("添加用户");
}
public void deleteUser() {
System.out.println("删除用户");
}
}
3.创建配置文件applicationContext.xml
<beans xmlns:xsi="www.w3.org/2001/XMLSch…"
xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
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
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.3.xsd ">
<!-- 指定需要扫描的包,使注解生效 -->
<context:component-scan
base-package="com.nynu.qdy" />
<!-- 启动基于注解的声明式AspectJ支持 -->
<aop:aspectj-autoproxy/>
4.创建测试类
package com.nynu.qdy.aspectj.annotation;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.nynu.qdy.jdk.UserDao;
public class TestAnnotation {
@SuppressWarnings("resource")
public static void main(String[] args) {
String xmlPath = "com/nynu/qdy/aspectj/annotation/applicationContext.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
// 1 从容器中获得内容
UserDao userDao = (UserDao) applicationContext.getBean("userDao");
// 2 执行方法
userDao.addUser();
userDao.deleteUser();
}
}
5.结果
环绕开始:执行目标方法之前,模拟开启事物。。。。
前置通知:模拟执行权限检查:
目标类是:com.nynu.qdy.jdk.UserDaoImpl@71809907
,被植入增强的目标方法为: addUser
添加用户
环绕结束:执行目标方法之后。关闭模拟事物。。。
最终通知:模拟方法结束后的释放资源
后置通知:模拟记录日志:
被植入增强处理的目标方法为: addUser
环绕开始:执行目标方法之前,模拟开启事物。。。。
前置通知:模拟执行权限检查:
目标类是:com.nynu.qdy.jdk.UserDaoImpl@71809907
,被植入增强的目标方法为: deleteUser
删除用户
环绕结束:执行目标方法之后。关闭模拟事物。。。
最终通知:模拟方法结束后的释放资源
后置通知:模拟记录日志:
被植入增强处理的目标方法为: deleteUser
如果同一个连接点有多个通知需要执行,那么在同一切面中,目标方法之前的前置通知和环绕通知的执行顺序是未知的,目标方法之后的后置通知和环绕通知的执行顺序也是位置的。
好啦,今天的文章就到这里,希望能帮助到屏幕前迷茫的你们