3 Spring AOP

3.1Spring AOP简介

3.1.1什么是AOP

AOP是面向切面编程的缩写(Aspect-Oriented programming),我们在编写传统业务处理代码时,都会进行事务处理,权限检查,日志记录等。为了实现日志记录功能,我们会将同样的代码分散到各个方法中,当我们需要修改其中的业务逻辑时会非常麻烦。AOP采用横向抽取机制,这样编码人员只需要关注核心功能的实现,提高代码的可维护性,也提高了开发效率。一个简单的例子如下:

目前主流的AOP框架有两个,分别为Spring AOP和AspectJ。
Spring AOP使用纯Java实现,需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类织入要增强的代码。
AspectJ是一个基于Java语言的AOP框架,扩展了Java语言,提供了专门的编译器,在编译时提供横向代码织入。

3.1.2AOP术语

术语 解释
切面 用于横向插入系统功能的(比如封装了事务处理,权限检查等功能的类)
连接点 方法的调用 (添加用户,删除用户)
切入点 需要处理的连接点
通知/增强处理 切面类中的一个具体方法,比如事务处理
目标对象 需要被增强的对象
代理 将通知应用到目标对象,被动态创建的对象
织入 将亲切面代码插入到目标对象,生成代理对象的过程

3.2动态代理

3.2.1JDK动态代理

JDK动态代理是通过java.lang.reflect.Proxy类来实现,调用Proxy类的newProxyInstance()方法来创建对象。
首先创建接口和对应的实现类,将此实现类作为目标类。

public interface UserDao {
	public void addUser() ;
	public void deleteUser() ;
}
public class UserDaoImpl implements UserDao {
	public void addUser() {
		// TODO Auto-generated method stub
		System.out.println("添加用户");
	}
	public void deleteUser() {
		// TODO Auto-generated method stub
		System.out.println("添加用户");
	}
}

编写切面类和其中的通知即增强方法

//切面类
public class MyAspect {
	//增强处理
	public void check_before() {
		System.out.println("模拟权限检查");		
	}
	public void check_after() {
		System.out.println("模拟记录日志");		
	}

}

最重要的就是代理类,代理类需要实现InnovationHandler接口,实现接口中的invoke方法,所有动态代理类都会交由这个方法来处理。这个类还包括一个创建代理的方法,获取类加载器,然后获取所有的接口对象,调用Proxy.newProxyInstance方法,传入类加载器和接口对象和自身。

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();
		//使用代理类返回被代理的对象
		return Proxy.newProxyInstance(classLoader, clazz, this);
	}
	/*
	 * 所有动态代理类的方法调用,都会交由invoke方法处理
	 * proxy被代理后的对象
	 * method将要被执行的方法
	 * args执行方法需要的参数
	 * */
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		// TODO Auto-generated method stub
		//声明切面类
		MyAspect myAspect = new MyAspect();
		//前增强
		myAspect.check_before();
		//目标类上调用方法,并传入参数
		Object obj = method.invoke(userDao, args);
		//后增强
		myAspect.check_after();
		return obj;
	}
}

调用测试

public static void main(String[] args) {
	JdkProxy jdkProxy = new JdkProxy();
	UserDao userDao = new UserDaoImpl();
	UserDao userDao2 =(UserDao) jdkProxy.createProxy(userDao);
	userDao2.addUser();
	userDao2.deleteUser();
}

运行结果展示

3.2.2CGLIB代理

JDK动态代理,代理的对象必须实现一个或多个接口,因此要对没有实现接口的类进行代理可以使用CGLIB代理,这种代理无需导入额外的JAR包。
前面的准备工作和JDK动态代理同,创建切面和目标类
这里直接给出代理类

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 org.springframework.remoting.RemoteTimeoutException;

public class CgligProxy implements MethodInterceptor {
	//代理方法
	public Object createProxy(Object target) {
		//创建动态类对象
		Enhancer enhancer =new Enhancer();
		//确定需要增强的类,设置父类
		enhancer.setSuperclass(target.getClass());
		//增加回调函数
		enhancer.setCallback(this);
		//返回创建的代理类
		return enhancer.create();
	}
	
	/*args拦截方法的参数
	 * method拦截的方法
	 * methodProxy方法的代理对象,用于执行父类的方法
	 * */
	@Override
	public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
		// TODO Auto-generated method stub
		MyAspect myAspect = new MyAspect();
		myAspect.check_before();
		Object obj = methodProxy.invokeSuper(proxy, args);
		myAspect.check_after();
		return obj;
	}

}

该代理类也同样包含两个方法,一个是代理方法,一个是实现接口MethodInterceptor的方法intercept。在代理方法中,先定义了一个动态类对象,确定需要增强的类,设置父类,增加回调函数,最后return代理对象。intercept方法会在程序执行目标方法调用.
运行结果如下

3.2.3JDK动态代理和CGLIB动态代理区别

JDK动态代理 CGLIB动态代理
使用动态代理对象必须实现一个或多个接口 不需要
JDK动态代理类实现了InvocationHandler接口,重写的invoke方法。 CGLIB代理类实现了MethodInterceptor接口,重写的intercept方法
JDK动态代理的基础是反射机制(method.invoke(对象,参数))Proxy.newProxyInstance() 原理是对指定的目标生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。
它生成类的速度很快,但是运行时因为是基于反射,调用后续的类操作会很慢. 生成类的速度慢,但是后续执行类的操作时候很快.

3.3基于代理类的AOP实现

在Spring中使用ProxyFactoryBean是创建AOP代理的最基本方式

3.3.1Spring的通知类型

根据Spring中的通知在目标类方法连接点的位置,可分为以下类型

名称 说明
org.springframework.aop.MethodBeforeAdvice(前置通知) 在方法之前自动执行的通知称为前置通知,可以应用于权限管理等功能。
org.springframework.aop.AfterReturningAdvice(后置通知) 在方法之后自动执行的通知称为后置通知,可以应用于关闭流、上传文件、删除临时文件等功能。
org.aopalliance.intercept.MethodInterceptor(环绕通知) 在方法前后自动执行的通知称为环绕通知,可以应用于日志、事务管理等功能。
org.springframework.aop.ThrowsAdvice(异常通知) 在方法抛出异常时自动执行的通知称为异常通知,可以应用于处理异常记录日志等功能。
org.springframework.aop.IntroductionInterceptor(引介通知) 在目标类中添加一些新的方法和属性,可以应用于修改旧版本程序(增强类)。

3.3.2ProxyFactoryBean

ProxyFactoryBean是FactoryBean接口的实现类,FactoryBean负责实例化一个Bean,ProxyFactoryBean负责为其他的Bean创建代理实例。
PeoxyfactoryBean类的可配置属性

属性名称 描述
target 代理的目标对象
proxyInterfaces 代理要实现的接口
proxyTargetClass 是否对类代理而不是接口,设置为true时,使用CGLIb代理,默认JDk代理
interceptorNames 需要织入目标的Advices
首先重新编写MyAspect类,模拟环绕通知,所以实现MethodInterceptor接口
//切面类
public class MyAspect implements MethodInterceptor {
	//增强处理
	public void check_before() {
		System.out.println("模拟权限检查");		
	}
	public void check_after() {
		System.out.println("模拟记录日志");		
	}
	@Override
	public Object invoke(MethodInvocation mi) throws Throwable {
		// TODO Auto-generated method stub
		check_before();
		Object obj = mi.proceed();
		check_after();
		return obj;
	}

}

编写配置文件,在配置文件中需要先定义目标对象然后定义切面对象,最后定义代理对象,在代理对中设置属性值,值得注意的是属性设置目标对象时是用ref而不是value

<!-- 1 目标类 -->
	<bean id="userDao" class="com.itheimafactorybean.UserDaoImpl" />
	<!-- 2 切面类 -->
	<bean id="myAspect" class="com.itheima.factorybean.MyAspect" />
	<!-- 3 使用Spring代理工厂定义一个名称为userDaoProxy的代理对象 -->
	<bean id="userDaoProxy" 
            class="org.springframework.aop.framework.ProxyFactoryBean">
		<!-- 3.1 指定代理实现的接口-->
		<property name="proxyInterfaces" 
                      value="com.itheima.factorybean.UserDao" />
		<!-- 3.2 指定目标对象 -->
		<property name="target" ref="userDao" />
		<!-- 3.3 指定切面,织入环绕通知 -->
		<property name="interceptorNames" value="myAspect" />
		<!-- 3.4 指定代理方式,true:使用cglib,false(默认):使用jdk动态代理 -->
		<property name="proxyTargetClass" value="true" />
	</bean>

编写测试类,运行即可

3.4AspectJ开发

使用AspectJ实现AOP有基于XML声明式AspectJ和基于注解声明式AspectJ两种方式

3.4.1基于XML声明式AspectJ

这种方式通过xml文件来定义切面,切入点和通知,配置文件beans下面可以包含多个config元素,config标签下又可以包含属性和子元素,子元素包括pointcut和advisor和aspect。这三个元素必须按照顺序
首先编写切面类

//环绕通知必须接收一个ProceedingJoinPoint的参数,返回值必须是Object类型,且必须抛出异常
public class MyAspect {
	public void myBefore(JoinPoint joinPoint) {
		System.out.println("前置通知,模拟权限检查");
		System.out.println(" 目标类"+joinPoint.getTarget());
		System.out.println(" 被织入增强处理的目标方法"+joinPoint.getSignature().getName());
	}
	public void myAfterreturn(JoinPoint joinPoint) {
		System.out.println("后置通知,模拟日志记录");
		System.out.println(" 目标类"+joinPoint.getTarget());
		System.out.println(" 被织入增强处理的目标方法"+joinPoint.getSignature().getName());
	}
	public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
		//
		System.out.println("环绕通知,模拟开启事务");
		Object obj = proceedingJoinPoint.proceed();
		System.out.println("环绕通知,模拟关闭事务");
		return obj;
	}
	public void myAfterThrowing(JoinPoint joinPoint,Throwable e) {
		System.out.println("异常通知,出错了"+e.getMessage());
	}
	//最终通知
	public void myAfter() {
		System.out.println("最终通知模拟资源释放");
	}
}

编写xml配置文件

	    <!-- 1 目标类 -->
		<bean id="userDao" class="com.itheima.factorybean.UserDaoImpl" />
		<!-- 2 切面 -->
		<bean id="myAspect" class="com.itheima.aspectj.xml.MyAspect" />
		<!-- aop编程 -->
		<aop:config>
        <!-- 配置切面 -->
		<aop:aspect ref="myAspect">
		  <!-- 3.1 配置切入点,通知最后增强哪些方法 -->
		  <aop:pointcut expression="execution(* com.itheima.factorybean.*.*(..))"
				                                      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="myAfterreturn"
				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>

注意点

  1. 后置通知只有在目标方法成功执行后才会被织入,而最终方法无论目标方法如何结束都会被执行
  2. 在配置切面过程中,要注意严格使用空格,尤其在name属性中
  3. 在配置切点中定义切入点表达式,定义格式为exection(返回类型此处要有空格包路径 类名 方法名 (参数)
    运行测试


异常测试

3.4.2基于注解声明式AspectJ

同样为了解决xm文件中的内容过于臃肿,采用注解的方式解决
关于AspectJ的属性介绍如下

名称 描述
@Aspect 用于定义一个切面。
@Before 用于定义前置通知
@AfterReturning 用于定义后置通知。
@Around 用于定义环绕通知。
@AfterThrowing 用于定义抛出通知
@After 用于定义最终final通知。

为了实现注解,首先需要在 UserDaoImpl添加注解
@Repository("userDao")
修改切面类

package com.itheima.aspectj.annotion;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
//环绕通知必须接收一个ProceedingJoinPoint的参数,返回值必须是Object类型,且必须抛出异常
@Aspect
@Component
public class MyAspect {
	//定义一个切入点表达式
	@Pointcut("execution(* com.itheima.aspectj.annotion.*.*(..))")
	//使用一个返回值为void,方法体为空的方法来命名切入点
	//相当于id
	private void myPointcut() {
		
	}
	@Before("myPointcut()")
	public void myBefore(JoinPoint joinPoint) {
		System.out.println("前置通知,模拟权限检查");
		System.out.println(" 目标类"+joinPoint.getTarget());
		System.out.println(" 被织入增强处理的目标方法"+joinPoint.getSignature().getName());
	}
	@AfterReturning("myPointcut()")
	public void myAfterreturn(JoinPoint joinPoint) {
		System.out.println("后置通知,模拟日志记录");
		System.out.println(" 目标类"+joinPoint.getTarget());
		System.out.println(" 被织入增强处理的目标方法"+joinPoint.getSignature().getName());
	}
	@Around("myPointcut()")
	public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
		//
		System.out.println("环绕通知,模拟开启事务");
		Object obj = proceedingJoinPoint.proceed();
		System.out.println("环绕通知,模拟关闭事务");
		return obj;
	}
	@AfterThrowing(value="myPointcut()",throwing="e")
	public void myAfterThrowing(JoinPoint joinPoint,Throwable e) {
		System.out.println("异常通知,出错了"+e.getMessage());
	}
	//最终通知
	@After("myPointcut()")
	public void myAfter() {
		System.out.println("最终通知模拟资源释放");
	}
}

值得注意的是
首先使用@Aspect注解定义了切面类,由于该类在Spring中作为组件使用,所以需要添加@Component才可以生效,然后使用@PointCut注解来配置切入点表达式,并通过定义方法来表示切入点名称。接下来在方法中将切入点作为参数传递给需要执行增强的通知方法。注意:参数中为“pointCut()”,有括号。
然后修改xml文档,开启注解和需要扫描的包

      <!-- 指定需要扫描的包,使注解生效 -->
      <context:component-scan base-package="com.itheima" />
      <!-- 启动基于注解的声明式AspectJ支持 -->
      <aop:aspectj-autoproxy />

编写测试类,运行如下

posted @ 2021-02-01 11:02  我就是隔壁老张  阅读(106)  评论(0编辑  收藏  举报