四、Spring AOP面向切面编程
4.1 Spring AOP概述
AOP是Spring 的重要特性,AOP是通过预编译方式和运行期间动态代理实现程序功能,也就是说可以在不修改源代码的情况下,给程序统一添加功能。
AOP定义:
AOP即面向切面编程。AOP主张将程序中相同的业务逻辑进行横向隔离,并将重复的业务逻辑抽取到一个独立的模块中,以达到提高程序可重用性和开发效率的目的。
AOP中常用术语:
-
切面:是指关注点(指类中重复的代码)形成的类,通常是指封装的、用于横向插入系统的功能类。在实际开发中,该类被Spring容器识别为切面,需要在配置文件中通过
<bean>
元素指定。 -
**连接点:**程序执行过程中某个特定的节点,例如,某方法调用时或处理异常时。在Spring AOP中,一个连接点通常是一个方法的执行。
-
切入点: 当某个连接点满足预先指定的条件时,AOP就能够定位到这个连接点,在连接点处插入切面,该连接点也就变成了切入点。
-
**通知/增强处理:**就是插入的切面程序代码。可以将通知/增强处理理解为切面中的方法
-
**目标对象:**是指被插入切面的方法
-
**织入:**将切面代码插入到目标对象上,从而生成代理对象的过程。
-
**代理:**将通知应用到目标对象之后,程序动态创建的通知对象,就称为代理。
-
**引介:**是一种特殊的通知,它可为目标对象添加一些属性和方法。这样,即使一个业务类原本没有实现某一个接口,通过AOP的引介功能,也可以动态地为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。
4.2 Spring AOP的实现机制
Spring AOP实现时需要创建一个代理对象,根据代理对象的创建方式,可以将AOP实现机制分为两种:一种是JDK动态代理;一种是CGLib动态代理。
动态代理是指在程序运行时创建一个代理对象,而不是在编译时生成的静态代理。代理对象可以代替目标对象,并控制对目标对象方法的调用。
4.2.1 JDK动态代理
默认情况下,Spring AOP使用JDK动态代理,JDK动态代理是通过java.lang.reflect.Proxy 类实现的,可以调用Proxy类的**newProxyInstance()**方法创建代理对象。JDK动态代理可以实现无侵入式的代码扩展,并且可以在不修改源代码的情况下,增强某些方法。
- **要求:**代理的目标对象必须实现某些接口
- **实现:**为UserDao中的方法通过JDK动态代理添加权限检查和日志方法
public interface UserDao {
public void addUser();
public void deleteUser();
}
public class UserDaoImpl implements UserDao {
public void addUser() {
System.out.println("添加用户"); }
public void deleteUser() {
System.out.println("删除用户"); }
}
// 切面类:存在多个通知Advice(增强的方法)
public class MyAspect {
public void check_Permissions(){
System.out.println("模拟检查权限...");
}
public void log(){
System.out.println("模拟记录日志...");
}
}
创建代理类MyProxy,该类需要实现InvocationHandler接口设置代理类的调用处理程序。在代理类中,通过newProxyInstance()生成代理方法。
public class MyProxy implements InvocationHandler {
//声明目标接口
private UserDao userDao;
/**
创建代理对象:
Proxy.newProxyInstance(classLoader, interfaces, this)
ClassLoader loader:类加载器,将此代理对象加载到JDK中
Class<?>[] interfaces:目标对象所实现的所有接口
InvocationHandler h:方法拦截,所有动态代理类的方法调用,都会交由h.invoke()方法去处理
*/
public Object createProxy(UserDao userDao) {
this.userDao = userDao;
//获取类加载器
ClassLoader classLoader = MyProxy.class.getClassLoader();
//获取目标对象的所有接口
Class<?>[] interfaces = userDao.getClass().getInterfaces();
for (Class<?> anInterface : interfaces) {
System.out.println(anInterface);
}
return Proxy.newProxyInstance(classLoader, interfaces, this);
}
/**
所有动态代理类的方法调用,都会交由invoke()方法去处理
proxy:代理对象
method:将要被执行的方法详细(反射)原先目标对象的方法
args:执行方法所需的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//创建切面对象
MyAspect myAspect = new MyAspect();
//前增强
myAspect.permission();
//目标对象上调用方法
Object invoke = method.invoke(userDao, args);
//后增强
myAspect.log();
return invoke;
}
}
public class JDKTest {
public static void main(String[] args) {
MyProxy myProxy = new MyProxy();
UserDao userDao = new UserDaoImpl();
UserDao proxy = (UserDao) myProxy.createProxy(userDao);
proxy.addUser();
System.out.println();
proxy.deleteUser();
}
}
4.2.2 CGLib动态代理
JDK动态代理存在缺陷,它只能为接口创建代理对象,当需要为类创建代理对象时,就需要使用CGLib(Code Generation Library)动态代理,CGLib动态代理不要求目标类实现接口,它采用底层的字节码技术,通过继承的方式动态创建代理对象。
public class CglibProxy implements MethodInterceptor {
//代理方法
public Object createProxy(Object target) {
//创建一个动态对象类
Enhancer enhancer = new Enhancer();
//确定需要增强的类设为父类
enhancer.setSuperclass(target.getClass());
//添加回调函数
enhancer.setCallback(this);
//返回创建的代理类
return enhancer.create();
}
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//创建切面对象
MyAspect myAspect = new MyAspect();
//前增强
myAspect.permission();
//目标对象上调用方法
Object invoke = methodProxy.invokeSuper(proxy, args);
//后增强
myAspect.log();
return invoke;
}
}
4.3 基于XML的AOP实现
因为Spring AOP中的代理对象由IoC容器自动生成,所以开发者无须过多关注代理对象生成的过程,只需选择连接点、创建切面、定义切点并在XML文件中添加配置信息即可。
元素 | 描述 |
---|---|
< aop:config> | Spring AOP配置的根元素 |
- 配置切面
元素 | 描述 |
---|---|
<aop:aspect> | 配置切面 |
属性名称 | 描述 |
id | 用于定义该切面的唯一标识 |
ref | 用于引用普通的Spring Bean |
<bean id="id" class="">
<aop:aspect id="" ref="id">
在Spring的配置文件中,配置切面使用的是<aop:aspect>
元素,该元素会将一个已定义好的Spring Bean转换成切面Bean,因此,在使用<aop:aspect>
元素之前,要在配置文件中先定义一个普通的Bean。
- 配置切入点
当切入点作为 <aop:config>
子元素时,切入点可被多个切面共享
当切入点作为<aop:aspect>
子元素时,为局部切入点
元素 | 描述 |
---|---|
<aop:pointcut> | 配置切入点 |
属性名称 | 描述 |
id | 用于指定切入点的唯一标识 |
expression | 用于指定切入点关联的切入点表达式 |
//切入点表达式
//execution(修饰符?返回值类型 类路径?方法名(参数)异常类型?)
execution(* com.itheima.jdk.*.*(..))
*:返回值为任意类型
com.tyut.jdk.*:拦截com.tyut.jdk包下的所有类(目标方法)
*(..):所有方法,任意参数
- 配置通知
元素 | 描述 |
---|---|
<aop:before> | 配置前置通知,在目标方法执行前实施增强,可以应用于权限管理等功能 |
<aop:after > | 配置后置通知,在目标方法执行后实施增强,可以应用于关闭流、上传文件、删除临时文件等功能 |
<aop:around> | 配置环绕通知,在目标方法执行前后实施增强,可以应用于日志、事务管理等功能 |
<aop:after-returning> | 配置返回通知,在目标方法成功执行之后调用通知 |
<aop:after-throwing> | 配置异常通知,在方法抛出异常后实施增强,可以应用于处理异常记录日志等功能 |
属性 | 说明 |
pointcut | 该属性用于指定一个切入点表达式,Spring将在匹配该表达式的连接点时织入该通知。 |
pointcut-ref | 该属性指定一个已经存在的切入点名称。 |
method | 该属性指定一个方法名,指定将切面Bean中的该方法转换为增强处理。 |
throwing | 该属性只对<after-throwing> 元素有效,它用于指定一个形参名,异常通知方法可以通过该形参访问目标方法所抛出的异常。 |
returning | 该属性只对<after-returning> 元素有效,它用于指定一个形参名,后置通知方法可以通过该形参访问目标方法的返回值。 |
- 相关依赖
<!-- aspectjrt包的依赖
aspectjrt 提供了 AspectJ 所需的核心运行时支持。
aspectjweaver 负责实际的编织过程,确保切面被应用到目标类
-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
- 目标类
public interface UserDao {
public void addUser();
public void deleteUser();
}
public class UserDaoImpl implements UserDao {
@Override
public void addUser() {
System.out.println("添加用户");
int x = 1 / 0;
}
@Override
public void deleteUser() {
System.out.println("删除用户");
}
}
- 切面类
public class XmlAdvice {
//前置通知
public void before(JoinPoint joinPoint) {
System.out.println("这是前置通知");
System.out.println("目标类" + joinPoint.getTarget());
System.out.println("目标方法为" + joinPoint.getSignature().getName());
}
//返回通知
public void afterReturning(JoinPoint joinPoint) {
System.out.println("返回通知方法(未异常)");
System.out.println("目标方法为" + joinPoint.getSignature().getName());
}
//环绕通知
public Object around(ProceedingJoinPoint point) throws Throwable {
System.out.println("这是环绕通知前半部分");
//调用目标方法
Object proceed = point.proceed();
System.out.println("这是环绕通知后半部分");
return proceed;
}
//异常通知
public void afterException() {
System.out.println("异常通知");
}
//后置通知
public void after() {
System.out.println("这是后置通知");
}
}
- 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.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 注册Bean -->
<bean name="userDao" class="com.tyut.example07.UserDaoImpl"/>
<bean name="xmlAdvice" class="com.tyut.example07.XmlAdvice"/>
<!-- 配置Spring AOP -->
<aop:config>
<!-- 设置切入点 -->
<aop:pointcut id="pointcut" expression="execution(* com.tyut.example07.UserDaoImpl.*(..))"/>
<!-- 设置切面 -->
<aop:aspect id="xmlAspect" ref="xmlAdvice">
<aop:before method="before" pointcut-ref="pointcut"/>
<aop:after-returning method="afterReturning" pointcut-ref="pointcut"/>
<aop:around method="around" pointcut-ref="pointcut"/>
<aop:after-throwing method="afterException" pointcut-ref="pointcut"/>
<aop:after method="after" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
</beans>
4.4 基于注解的AOP实现
元素 | 描述 |
---|---|
@Aspect | 配置切面 |
@Pointcut | 配置切点 |
@Before | 配置前置通知 |
@After | 配置后置通知 |
@Around | 配置环绕方式 |
@AfterReturning | 配置返回通知 |
@AfterThrowing | 配置异常通知 |
@Aspect
public class XmlAdvice {
//配置切点
@Pointcut("execution(* com.tyut.example08.UserDao.*(..))")
public void pointcut() {
}
//前置通知
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
System.out.println("这是前置通知");
System.out.println("目标类" + joinPoint.getTarget());
System.out.println("目标方法为" + joinPoint.getSignature().getName());
}
//返回通知
@After("pointcut()")
public void afterReturning(JoinPoint joinPoint) {
System.out.println("返回通知方法(未异常)");
System.out.println("目标方法为" + joinPoint.getSignature().getName());
}
//环绕通知
@Around("pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
System.out.println("这是环绕通知前半部分");
//调用目标方法
Object proceed = point.proceed();
System.out.println("这是环绕通知后半部分");
return proceed;
}
//异常通知
@AfterThrowing("pointcut()")
public void afterException() {
System.out.println("异常通知");
}
//后置通知
@After("pointcut()")
public void after() {
System.out.println("这是后置通知");
}
}
<?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.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 注册Bean -->
<bean name="userDao" class="com.tyut.example08.UserDaoImpl"/>
<bean name="xmlAdvice" class="com.tyut.example08.XmlAdvice"/>
<!-- 开启Aop自动代理 -->
<aop:aspectj-autoproxy/>
</beans>