第三章 Spring AOP
3.1 Spirng AOP简介
3.1.1 什么是AOP
概念:AOP 的全称是 Aspect-Oriented Programming ,即面向切面编程(也称面向方面编程)。 它
是面向对象编程 (OOP) 的一种补充,目前已成为一种比较成熟的编程方式 。
-
横向抽取机制:将分散在各个方法中的重复代码提取出来,然后在程序编译或运行时,再将这些提取出来的代码应用到需要执行的地方 ,而传统的 OOP只能实现父子关系的纵向的重用。
-
**地位:**虽然AOP是一种新的编程思想,但不是OOP的替代品,而是其的延伸和补充。
-
意义:AOP 的使用,使开发人员在编写业务逻辑时可以专心于核心业务,而不用过多地关注于其
他业务逻辑的实现,这不但提高了开发效率,而且增强了代码的可维护性 。 -
最流行的两个AOP框架:
-
Spring AOP
- 纯Java实现,不需要专门的编译过程和类加载器,在运行期间通过代理的方式向目标类织入增强的代码。
-
AspectJ
- 基于Java语言的AOP框架,从Spring2.0开始,SpringAOP引入了对AspectJ的支持,AspectJ扩展了Java语言,提供了一个专门的编译器,在编译时提供横向代码的织入。
-
3.1.2 AOP术语
在学习使用 AOP 之前,首先要了解一下 AOP 的专业术语 。 这些术语包括 Aspect 、Joinpoint 、 Pointcut 、 Advice 、 Target Object 、 Proxy 和 Weaving ,对于这些专业术语的解释,具体如下 。
-
Aspect(切面):在实际应用中,切面通常是指封装的用于横向插入系统功能(如事务、曰志等)的类,该类要被Spring容器识别为切面,需要在配置文件中通过
<bean>
元素指定。 - Joinpoint(连接点):在程序执行过程中的某个阶段点,它实际上是对象的一个操作,例如方法的调用或异常的抛出。在SpringAOP中,连接点就是指方法的调用。
- Pointcut (切入点):是指切面与程序流程的交叉点,即那些需要处理的连接点,通常在程序中,切入点指的是类或者方法名,如某个通知要应用到所有以 add 开头 的方法中,那么所有满足这一规则的方法都是切入点 。
- Advice(通知/增强处理):AOP框架在特定的切入点执行的增强处理,即在定义好的切入点处所要执行的程序代码。可以将其理解为切面类中的方法,它是切面的具体实现。
- TargetObject(目标对象):是指所有被通知的对象,也称为被增强对象。如果AOP框架采用的是动态的AOP实现,那么该对象就是一个被代理对象。
- Proxy(代理):将通知应用到目标对象之后,被动态创建的对象。
- Weaving (织入):将切面代码插入到目标对象上,从而生成代理对象的过程。
3.2 动态代理
3.2.1 JDK动态代理
JDK动态代理是通过java.lang.eflect.Proxy类来实现的,我们可以调用Proxy类的newProxylnstance()方法来创建代理对象。对于使用业务接口的类,Spring默认会使用JDK动态代理来实现AOP。
- 创建项目,导入Spring框架所需的JAR包
- 创建接口UserDao,并编写添加和删除的方法。
//UserDao.java
package com.itheima.jdk;
public interface UserDao {
public void addUser();
public void deleteUser();
}
- 创建实现类UserDaoImpl,分别实现接口中的方法并添加输出语句。
//UserDaoImpl.java
//目标类
@Repository("userDao")
import org.springframework.stereotype.Repository;
public class UserDaoImpl implements UserDao{
public void addUser(){
System.out.println("添加用户");
}
public void deletUser(){
System.out.println("删除用户");
}
}
- 创建切面类MyAspect,在该类中定义一个模拟权限检查的方法和一个模拟记录日志的方法,这两个方法就表示切面中的通知。
//MyAspect.java
//切面类:可以存在多个通知 Advice (即增强的方法)
public class MyAspect{
public void check_Permissions(){
System.out.println("模拟检查权限");
}
public void log(){
System.out.prinln("模拟记录日志");
}
}
- 创建代理类JdkProxy,需要实现InvocationHandler接口,并编写代理方法。在代理方法中,需要对Proxy类实现动态代理。
package com.itheima.jdk;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import com.itheima.aspect.MyAspect;
/**
* JDK代理类
*/
public class JdkProxy implements InvocationHandler{
// 声明目标类接口
private UserDao userDao;
// 创建代理方法
public Object createProxy(UserDao userDao) {
this.userDao = userDao;
// 1.类加载器
ClassLoader classLoader = JdkProxy.class.getClassLoader();
// 2.被代理对象实现的所有接口
Class[] clazz = userDao.getClass().getInterfaces();
// 3.使用代理类,进行增强,返回的是代理后的对象
return Proxy.newProxyInstance(classLoader,clazz,this);
}
/*
* 所有动态代理类的方法调用,都会交由invoke()方法去处理
* proxy 被代理后的对象
* method 将要被执行的方法信息(反射)
* args 执行方法时需要的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// 声明切面
MyAspect myAspect = new MyAspect();
// 前增强
myAspect.check_Permissions();
// 在目标类上调用方法,并传入参数
Object obj = method.invoke(userDao, args);
// 后增强
myAspect.log();
return obj;
}
}
JdkProxy类实现了InvocationHandler接口,并实现了接口中的invoke()方法,所有动态代理类所调用的方法都会交由该方法处理。在创建的代理方法createProxy()中,使用了Proxy类的newProxylnstance()方法来创建代理对象。newProxylnstance()方法中包含3个参数,其中第1个参数是当前类的类加载器,第2个参数表示的是被代理对象实现的所有接口,第3个参数this代表的就是代理类JdkProxy本身。在invoke()方法中,目标类方法执行的前后,会分别执行切面类中的check_PermissionsO方法和log()方法。
- 创建测试类JdkTest,该类中的main()方法中创建代理对象和目标对象,然后从代理对象中获得对目标对象userDao增强后的对象,最后调用该对象中的添加和删除方法。
package com.itheima.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();
}
}
输出如下:
模拟检查权限…
添加用户
模拟记录日志…
模拟检查权限…
删除用户
模拟记录日志…
- 总结
userDao实例中的添加用户和删除用户的方法已被成功调用,并且在调用前后分别增加了检查权限和记录日志的功能。这种实现了接口的代理方式,就是Spring中的JDK动态代理
3.2.2 CGLIB代理
JDK 动态代理的对象必须实现一个或多个接口 。 如果要对没有实现接口的类进行代理,那么可以使用 CGLIB代理。
CGLIB(CodeGenerationLibrary)是一个高性能开源的代码生成包,它采用非常底层的字节码技术,对指定的目标类生成一个子类,并对子类进行增强。在Spring的核心包中已经集成了CGLIB所需要的包,所以开发中不需要另外导入JAR包。
- 创建目标类UserDao,UserDao不需要实现任何接口,只需要定义两个方法
package com.itheima.cglib;
public class UserDao {
//没有接口的目标类
public void addUser() {
System.out.println("添加用户");
}
public void deleteUser() {
System.out.println("删除用户");
}
}
- 创建代理类CglibProxy,需要实现MethodInterceptor接口,并实现intercept方法
package com.itheima.cglib;
import java.lang.reflect.Method;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import com.itheima.aspect.MyAspect;
public class CglibProxy implements MethodInterceptor {
//CGLIB代理类
//代理方法
public Object createProxy(Object target) {
//创建一个动态对象
Enhancer enhancer = new Enhancer();
//确定需要增强的类,设置其父类
enhancer.setSuperclass(target.getClass());
//添加回调函数
enhancer.setCallback(this);
//返回创建的代理类
return enhancer.create();
}
/**
* proxy CGlib根据指定父类生成的代理对象
* method 拦截的方法
* args 拦截方法的数组
* methodProxy方法的代理对象,用于执行父类的方法
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//创建切面类对象
MyAspect myAspect = new MyAspect();
//前增强
myAspect.check_Permissions();
//目标方法执行
Object obj = methodProxy.invokeSuper(proxy, args);
//后增强
myAspect.log();
return obj;
}
}
首先创建了一个动态类对象Enhancer,它是CGLIB的核心类;然后调用了Enhancer类的setSuperclass()方法来确定目标对象;接下来调用了setCallback()方法添加回调函数,其中的this代表的就是代理类CglibProxy本身;最后通过return语句将创建的代理类对象返回。intercept()方法会在程序执行目标方法时被调用,方法运行时将会执行切面类中的增强方法。
- 创建测试类CglibTest,在该类的 main()方法中首先创建代理对象和目标对象,然后从代理对象中获得增强后的目标对象,最后调用对象的添加和删除方法
package com.itheima.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();
}
}
输出结果
检查权限中…
添加用户
写入日志中…
检查权限中…
删除用户
写入日志中…
3.3 基于代理类的AOP实现
实际上, Spring中的 AOP 代理默认就是使用 JDK 动态代理的方式来实现的 。 在 Spring 中,使用 ProxyFactoryBean 是创建 AOP 代理的最基本方式 。
3.3.1 Spring的通知类型
Spring中的通知按照在目标类的方法的连接点位置,分为五种类型
-
环绕通知:org.aopalliance.intercept.MethodInterceptor
- 在目标方法执行前后实施增强,可以用于日志、事物管理等功能。
-
前置通知:org.springframework.aop.MethodBeforeAdvice
- 在目标方法执行前实施增强,可以应用于权限管理等功能。
-
后置通知:org.springframework.aop.AfterReturningAdvice
- 在目标方法执行后实施增强,可以应用于关闭流、上传文件、删除临时文件等功能。
-
异常通知:org.springframework.aop.ThrowsAdvice
- 在方法抛出异常后实施增强,可以应用于处理异常记录曰志等功能。
-
引介通知:org.springframework.aop.IntroductionInterceptor
- 在目标类中添加一些新的方法和属性,可以应用于修改老版本程序(增强类)。
3.3.2 ProxyFactoryBean
ProxyFactoryBean 是 FactoryBean 接口的实现类, FactoryBean 负责实例化一个 Bean ,而ProxyFactoryBean 负责为其他 Bean 创建代理实例 。 在 Spring 中,使用 ProxyFactoryBean 是创建 AOP 代理的基本方式 。
ProxyFactoryBean的常用属性
属性名 | 描述 |
---|---|
taget | 代理的目标 |
proxyInterfaces | 代理实现的接口 |
proxyTargetClass | 是否对类代理而不是接口,设置为true时使用CGLIB代理 |
interceptorNames | 需要织入目标的Advice |
singleton | 返回的代理是否为单实例,默认为true返回单实例 |
optimize | 设置为true时强制使用CGLIB |
Spring使用ProxyFactoryBean创建AOP代理的过程
- 导包,除了核心包外还需要
spring-aop-4.3.6RELEASE.jar
和aopalliance-1.0.jar
- spring-aop- 4.3.6RELEASE .jar: 是 Spring 为 AOP 提供的实现包, Spring包中提供
- aopalliance-1.0.jar:是AOP联盟提供的规范包,该JAR包可以通过"http://mvnrepository.com/artifact/aopalliance/aopalliance/1.0 下载。
- 创建切面类MyAspet,需要实现org.aopalliance.intercept.MehodInterceptor接口
package com.itheima.factorybean;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
//切面类
public class MyAspect implements MethodInterceptor {
public void check_Permissions() {
System.out.println("检查权限中...");
}
public void log() {
System.out.println("写入日志中...");
}
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
check_Permissions();
//执行目标方法
Object obj = mi.proceed();
log();
return obj;
}
}
- 创建配置文件applicationContext.xml,指定代理对象
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 1 目标类 -->
<bean id="userDao" class="com.itheima.jdk.UserDaoImpl" />
<!-- 2 切面 -->
<bean id="myAspect" class="com.itheima.aspectj.xml.MyAspect" />
<!-- 3 aop编程 -->
<aop:config>
<!-- 配置切面 -->
<aop:aspect ref="myAspect">
<!-- 3.1 配置切入点,通知最后增强哪些方法 -->
<aop:pointcut expression="execution(* com.itheima.jdk.*.*(..))"
id="myPointCut" />
<!-- 3.2 关联通知Advice和切入点pointCut -->
<!-- 3.2.1 前置通知 -->
<aop:before method="myBefore" pointcut-ref="myPointCut" />
<!-- 3.2.2 后置通知,在方法返回之后执行,就可以获得返回值
returning属性:用于设置后置通知的第二个参数的名称,类型是Object -->
<aop:after-returning method="myAfterReturning"
pointcut-ref="myPointCut" returning="returnVal" />
<!-- 3.2.3 环绕通知 -->
<aop:around method="myAround" pointcut-ref="myPointCut" />
<!-- 3.2.4 抛出通知:用于处理程序发生异常-->
<!-- * 注意:如果程序没有异常,将不会执行增强 -->
<!-- * throwing属性:用于设置通知第二个参数的名称,类型Throwable -->
<aop:after-throwing method="myAfterThrowing"
pointcut-ref="myPointCut" throwing="e" />
<!-- 3.2.5 最终通知:无论程序发生任何事情,都将执行 -->
<aop:after method="myAfter" pointcut-ref="myPointCut" />
</aop:aspect>
</aop:config>
</beans>
- 创建测试类ProxyFactoryBeanTest,通过Spring容器获取代理对象的实例
package com.itheima.factorybean;
import org.apache.catalina.User;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.itheima.jdk.UserDao;
public class ProxyFactoryBeanTest {
public static void main(String[] args) {
String xmlPath = "com/itheima/factorybean/applicationContext.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
//从Spring容器获得内容
UserDao userDao = (UserDao) applicationContext.getBean("userDaoProxy");
//执行方法
userDao.addUser();
userDao.deleteUser();
}
}