Spring学习 - 03 Spring AOP

第三章 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。

  1. 创建项目,导入Spring框架所需的JAR包
  2. 创建接口UserDao,并编写添加和删除的方法。
//UserDao.java
package com.itheima.jdk;
public interface UserDao {
	public void addUser();
	public void deleteUser();
}

  1. 创建实现类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("删除用户");
    }
}
  1. 创建切面类MyAspect,在该类中定义一个模拟权限检查的方法和一个模拟记录日志的方法,这两个方法就表示切面中的通知。
//MyAspect.java
//切面类:可以存在多个通知 Advice (即增强的方法)
public class MyAspect{
    public void check_Permissions(){
        System.out.println("模拟检查权限");
    }
    public void log(){
        System.out.prinln("模拟记录日志");
    }
}
  1. 创建代理类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()方法。

  1. 创建测试类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();
	}
}

输出如下:

模拟检查权限…
添加用户
模拟记录日志…
模拟检查权限…
删除用户
模拟记录日志…

  1. 总结

​ userDao实例中的添加用户和删除用户的方法已被成功调用,并且在调用前后分别增加了检查权限和记录日志的功能。这种实现了接口的代理方式,就是Spring中的JDK动态代理

3.2.2 CGLIB代理

JDK 动态代理的对象必须实现一个或多个接口 。 如果要对没有实现接口的类进行代理,那么可以使用 CGLIB代理。

CGLIB(CodeGenerationLibrary)是一个高性能开源的代码生成包,它采用非常底层的字节码技术,对指定的目标类生成一个子类,并对子类进行增强。在Spring的核心包中已经集成了CGLIB所需要的包,所以开发中不需要另外导入JAR包。

  1. 创建目标类UserDao,UserDao不需要实现任何接口,只需要定义两个方法
package com.itheima.cglib;

public class UserDao {
	//没有接口的目标类
	public void addUser() {
		System.out.println("添加用户");
	}
	public void deleteUser() {
		System.out.println("删除用户");
	}
}

  1. 创建代理类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()方法会在程序执行目标方法时被调用,方法运行时将会执行切面类中的增强方法。

  1. 创建测试类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代理的过程

  1. 导包,除了核心包外还需要spring-aop-4.3.6RELEASE.jaraopalliance-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 下载。
  2. 创建切面类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;
	}

}
  1. 创建配置文件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>

  1. 创建测试类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();
	}
}

3.4 AspectJ开发

上一篇:Spring AOP解决了面向对象不能解决的什么问题


下一篇:spring框架-控制反转思想