Spring AOP

AOP 是 OOP 的延续,是 Aspect Oriented Programming 的缩写,意思是面向切面编程。可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。AOP设计模式孜孜不倦追求的是调用者和被调用者之间的解耦,AOP 可以说也是这种目标的一种实现。我们现在做的一些非业务,如:日志、事务、安全等都会写在业务代码中(也即是说,这些非业务类横切于业务类),但这些代码往往是重复,复制——粘贴式的代码会给程序的维护带来不便,AOP 就实现了把这些业务需求与系统需求分开来做。这种解决的方式也称代理机制。

AOP 中几个概念

1、切面(Aspect)

官方的抽象定义为“一个关注点的模块化,这个关注点可能会横切多个对象”。“切面”在ApplicationContext 中<aop:aspect>来配置。
连接点(Joinpoint) :程序执行过程中的某一行为,例如,MemberService .get 的调用或者MemberService .delete 抛出异常等行为。
2、通知(Advice)
“切面”对于某个“连接点”所产生的动作。其中,一个“切面”可以包含多个“Advice”。
3、切入点(Pointcut)
匹配连接点的断言,在 AOP 中通知和一个切入点表达式关联。切面中的所有通知所关注的连接点,都由切入点表达式来决定。
4、目标对象(Target Object)
被一个或者多个切面所通知的对象。例如,AServcieImpl 和 BServiceImpl,当然在实际运行时,SpringAOP 采用代理实现,实际 AOP 操作的是 TargetObject 的代理对象。
5、AOP 代理(AOP Proxy)
在 Spring AOP 中有两种代理方式,JDK 动态代理和 CGLib 代理。默认情况下,TargetObject 实现了接口时,则采用 JDK 动态代理,例如,AServiceImpl;反之,采用 CGLib 代理,例如,BServiceImpl。强制使用 CGLib 代理需要将 <aop:config>的 proxy-target-class 属性设为 true。通知(Advice)类型:
6、前置通知(Before Advice)
在某连接点(JoinPoint)之前执行的通知,但这个通知不能阻止连接点前的执行。ApplicationContext中在<aop:aspect>里面使用<aop:before>元素进行声明。例如,TestAspect 中的 doBefore 方法。
7、后置通知(After Advice)
当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。ApplicationContext 中在<aop:aspect>里面使用<aop:after>元素进行声明。例如,ServiceAspect 中的 returnAfter 方法,所以 Teser 中调用 UserService.delete 抛出异常时,returnAfter 方法仍然执行。
8、返回后通知(After Return Advice)
在某连接点正常完成后执行的通知,不包括抛出异常的情况。ApplicationContext 中在<aop:aspect>里面使用<after-returning>元素进行声明。
9、环绕通知(Around Advice)
包围一个连接点的通知,类似 Web 中 Servlet 规范中的 Filter 的 doFilter 方法。可以在方法的调用前后完成自定义的行为,也可以选择不执行。ApplicationContext 中在<aop:aspect>里面使用<aop:around>元素进行声明。例如,ServiceAspect 中的 around 方法。
10、异常通知(After Throwing Advice)
在 方 法 抛 出 异 常 退 出 时 执 行 的 通 知 。 ApplicationContext 中 在 <aop:aspect> 里 面 使 用<aop:after-throwing>元素进行声明。例如,ServiceAspect 中的 returnThrow 方法。
注:可以将多个通知应用到一个目标对象上,即可以将多个切面织入到同一目标对象。
使用 Spring AOP 可以基于两种方式,一种是比较方便和强大的注解方式,另一种则是中规中矩的 xml配置方式。
先说注解,使用注解配置 Spring AOP 总体分为两步,第一步是在 xml 文件中声明激活自动扫描组件功能,同时激活自动代理功能(来测试 AOP 的注解功能):
<?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:context="http://www.springframework.org/schema/context"
	   xmlns:util="http://www.springframework.org/schema/util"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.0.xsd
	   http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">

	<context:component-scan base-package="com.gupaoedu"/>
	<context:annotation-config />

</beans>
第二步是为 Aspect 切面类添加注解:
//声明这是一个组件
@Component
//声明这是一个切面Bean,AnnotaionAspect是一个面,由框架实现的
@Aspect
public class AnnotaionAspect {

	private final static Logger log = Logger.getLogger(AnnotaionAspect.class);
	
	//配置切入点,该方法无方法体,主要为方便同类中其他方法使用此处配置的切入点
	//切点的集合,这个表达式所描述的是一个虚拟面(规则)
	//就是为了Annotation扫描时能够拿到注解中的内容
	@Pointcut("execution(* com.gupaoedu.vip.aop.service..*(..))")
	public void aspect(){}
	
	/*
	 * 配置前置通知,使用在方法aspect()上注册的切入点
	 * 同时接受JoinPoint切入点对象,可以没有该参数
	 */
	@Before("aspect()")
	public void before(JoinPoint joinPoint){
		log.info("before " + joinPoint);
	}
	
	//配置后置通知,使用在方法aspect()上注册的切入点
	@After("aspect()")
	public void after(JoinPoint joinPoint){
		log.info("after " + joinPoint);
	}
	
	//配置环绕通知,使用在方法aspect()上注册的切入点
	@Around("aspect()")
	public void around(JoinPoint joinPoint){
		long start = System.currentTimeMillis();
		try {
			((ProceedingJoinPoint) joinPoint).proceed();
			long end = System.currentTimeMillis();
			log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms!");
		} catch (Throwable e) {
			long end = System.currentTimeMillis();
			log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms with exception : " + e.getMessage());
		}
	}
	
	//配置后置返回通知,使用在方法aspect()上注册的切入点
	@AfterReturning("aspect()")
	public void afterReturn(JoinPoint joinPoint){
		log.info("afterReturn " + joinPoint);
	}
	
	//配置抛出异常后通知,使用在方法aspect()上注册的切入点
	@AfterThrowing(pointcut="aspect()", throwing="ex")
	public void afterThrow(JoinPoint joinPoint, Exception ex){
		log.info("afterThrow " + joinPoint + "\t" + ex.getMessage());
	}
	
}
测试代码
@ContextConfiguration(locations = {"classpath*:application-context.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
public class AnnotationTest {
	@Autowired MemberService memberService;
//	@Autowired ApplicationContext app;
	
	
	@Test
//	@Ignore
	public void test(){
		System.out.println("=====这是一条华丽的分割线======");
		
//		AnnotaionAspect aspect = app.getBean(AnnotaionAspect.class);
//		System.out.println(aspect);
		memberService.save(new Member());
//
		System.out.println("=====这是一条华丽的分割线======");
		try {
			memberService.delete(1L);
		} catch (Exception e) {
			//e.printStackTrace();
		}
	}
	
}
控制台输出如下:
=====这是一条华丽的分割线======
com.gupaoedu.aop.aspect.AnnotaionAspect@6ef714a0
[INFO ] [13:04:46] com.gupaoedu.aop.aspect.AnnotaionAspect - before execution(void
com.gupaoedu.aop.service.MemberService.save(Member))
[INFO ] [13:04:46] com.gupaoedu.aop.aspect.ArgsAspect - beforeArgUser execution(void
com.gupaoedu.aop.service.MemberService.save(Member))
[INFO ] [13:04:46] com.gupaoedu.aop.aspect.AnnotaionAspect - save member Method . . .
[INFO ] [13:04:46] com.gupaoedu.aop.aspect.AnnotaionAspect - around execution(void
com.gupaoedu.aop.service.MemberService.save(Member)) Use time : 38 ms!
[INFO ] [13:04:46] com.gupaoedu.aop.aspect.AnnotaionAspect - after execution(void
com.gupaoedu.aop.service.MemberService.save(Member))
[INFO ] [13:04:46] com.gupaoedu.aop.aspect.AnnotaionAspect - afterReturn execution(void
com.gupaoedu.aop.service.MemberService.save(Member))
=====这是一条华丽的分割线======
[INFO ] [13:04:46] com.gupaoedu.aop.aspect.AnnotaionAspect - before execution(boolean
com.gupaoedu.aop.service.MemberService.delete(long))
[INFO ] [13:04:46] com.gupaoedu.aop.aspect.ArgsAspect - beforeArgId execution(boolean
com.gupaoedu.aop.service.MemberService.delete(long)) ID:1
[INFO ] [13:04:46] com.gupaoedu.aop.aspect.AnnotaionAspect - delete Method . . .
[INFO ] [13:04:46] com.gupaoedu.aop.aspect.AnnotaionAspect - around execution(boolean
com.gupaoedu.aop.service.MemberService.delete(long)) Use time : 3 ms with exception : spring aop ThrowAdvice
演示
[INFO ] [13:04:46] com.gupaoedu.aop.aspect.AnnotaionAspect - after execution(boolean
com.gupaoedu.aop.service.MemberService.delete(long))
[INFO ] [13:04:46] com.gupaoedu.aop.aspect.AnnotaionAspect - afterReturn execution(boolean
com.gupaoedu.aop.service.MemberService.delete(long))
可以看到,正如我们预期的那样,虽然我们并没有对 MemberService 类包括其调用方式做任何改变,但是 Spring 仍然拦截到了其中方法的调用,或许这正是 AOP 的魔力所在。再简单说一下 xml 配置方式,其实也一样简单:
<bean id="xmlAspect" class="com.gupaoedu.vip.aop.aspect.XmlAspect"></bean>
	 <!--AOP配置 -->
	<aop:config>
		 <!--声明一个切面,并注入切面Bean,相当于@Aspect -->
		<aop:aspect ref="xmlAspect" >
			 <!--配置一个切入点,相当于@Pointcut -->
			<aop:pointcut expression="execution(* com.gupaoedu.vip.aop.service..*(..))" id="simplePointcut"/>
			 <!--配置通知,相当于@Before、@After、@AfterReturn、@Around、@AfterThrowing -->
			<aop:before pointcut-ref="simplePointcut" method="before"/>
			<aop:after pointcut-ref="simplePointcut" method="after"/>
			<aop:after-returning pointcut-ref="simplePointcut" method="afterReturn"/>
			<aop:after-throwing pointcut-ref="simplePointcut" method="afterThrow" throwing="ex"/>
			<aop:around pointcut-ref="simplePointcut"  method="around"/>
		</aop:aspect>
	</aop:config>
个人觉得不如注解灵活和强大,你可以不同意这个观点,但是不知道如下的代码会不会让你的想法有所改善:
//配置切入点,该方法无方法体,主要为方便同类中其他方法使用此处配置的切入点
	//切点的集合,这个表达式所描述的是一个虚拟面(规则)
	//就是为了Annotation扫描时能够拿到注解中的内容
	@Pointcut("execution(* com.gupaoedu.vip.aop.service..*(..))")
	public void aspect(){}
	
	/*
	 * 配置前置通知,使用在方法aspect()上注册的切入点
	 * 同时接受JoinPoint切入点对象,可以没有该参数
	 */
	@Before("aspect()")
	public void before(JoinPoint joinPoint){
		log.info("before " + joinPoint);
	}
	
	//配置后置通知,使用在方法aspect()上注册的切入点
	@After("aspect()")
	public void after(JoinPoint joinPoint){
		log.info("after " + joinPoint);
	}
	
	//配置环绕通知,使用在方法aspect()上注册的切入点
	@Around("aspect()")
	public void around(JoinPoint joinPoint){
		long start = System.currentTimeMillis();
		try {
			((ProceedingJoinPoint) joinPoint).proceed();
			long end = System.currentTimeMillis();
			log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms!");
		} catch (Throwable e) {
			long end = System.currentTimeMillis();
			log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms with exception : " + e.getMessage());
		}
	}
	
	//配置后置返回通知,使用在方法aspect()上注册的切入点
	@AfterReturning("aspect()")
	public void afterReturn(JoinPoint joinPoint){
		log.info("afterReturn " + joinPoint);
	}
	
	//配置抛出异常后通知,使用在方法aspect()上注册的切入点
	@AfterThrowing(pointcut="aspect()", throwing="ex")
	public void afterThrow(JoinPoint joinPoint, Exception ex){
		log.info("afterThrow " + joinPoint + "\t" + ex.getMessage());
	}
以下是 MemberService 的代码:
@Service
public class MemberService {

	private final static Logger log = Logger.getLogger(AnnotaionAspect.class);
	
	public Member get(long id){
		log.info("getMemberById method . . .");
		return new Member();
	}
	
	
	public Member get(){
		log.info("getMember method . . .");
		return new Member();
	}
	
	public void save(Member member){
		log.info("save member method . . .");
	}
	
	public boolean delete(long id) throws Exception{
		log.info("delete method . . .");
		throw new Exception("spring aop ThrowAdvice演示");
	}
	
}
应该说学习 Spring AOP 有两个难点,第一点在于理解 AOP 的理念和相关概念,第二点在于灵活掌握和使用切入点表达式。概念的理解通常不在一朝一夕,慢慢浸泡的时间长了,自然就明白了,下面我们简单地介绍一下切入点表达式的配置规则吧。通常情况下,表达式中使用”execution“就可以满足大部分的要求。表达式格式如下:
modifiers-pattern:方法的操作权限
ret-type-pattern:返回值
declaring-type-pattern:方法所在的包
name-pattern:方法名
parm-pattern:参数名
throws-pattern:异常
其 中 , 除 ret-type-pattern 和 name-pattern 之 外 , 其 他 都 是 可 选 的 。 上 例 中 , execution(*com.spring.service.*.*(..))表示 com.spring.service 包下,返回值为任意类型;方法名任意;参数不作限制的所有方法。最后说一下通知参数,可以通过 args 来绑定参数,这样就可以在通知(Advice)中访问具体参数了。例如,<aop:aspect>配置如下:
<aop:config>
<aop:aspect ref="xmlAspect">
<aop:pointcut id="simplePointcut"
expression="execution(* com.gupaoedu.aop.service..*(..)) and args(msg,..)" />
<aop:after pointcut-ref="simplePointcut" Method="after"/>
</aop:aspect>
</aop:config>
上面的代码 args(msg,..)是指将切入点方法上的第一个 String 类型参数添加到参数名为 msg 的通知的入参上,这样就可以直接使用该参数啦。
在上面的 Aspect 切面 Bean 中已经看到了,每个通知方法第一个参数都是 JoinPoint。其实,在 Spring中,任何通知(Advice)方法都可以将第一个参数定义为 org.aspectj.lang.JoinPoint 类型用以接受当前连接点对象。JoinPoint 接口提供了一系列有用的方法, 比如 getArgs() (返回方法参数)、getThis() (返回代理对象)、getTarget() (返回目标)、getSignature() (返回正在被通知的方法相关信息)和 toString() (打印出正在被通知的方法的有用信息)。

寻找入口

Spring 的 AOP 是通过接入 BeanPostProcessor 后置处理器开始的,它是 Spring IOC 容器经常使用到的一个特性,这个 Bean 后置处理器是一个监听器,可以监听容器触发的 Bean 声明周期事件。后置处理器向容器注册以后,容器中管理的 Bean 就具备了接收 IOC 容器事件回调的能力。
BeanPostProcessor 的使用非常简单,只需要提供一个实现接口 BeanPostProcessor 的实现类,然后在 Bean 的配置文件中设置即可.

1、BeanPostProcessor 源码
public interface BeanPostProcessor {
	//为在 Bean 的初始化前提供回调入口
	@Nullable
	default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}
	//为在 Bean 的初始化之后提供回调入口
	@Nullable
	default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}
}
这两个回调的入口都是和容器管理的 Bean 的生命周期事件紧密相关,可以为用户提供在 Spring IOC容器初始化 Bean 。
2、AbstractAutowireCapableBeanFactory 类对容器生成的 Bean 添加后置处理器
BeanPostProcessor 后置处理器的调用发生在 Spring IOC 容器完成对 Bean 实例对象的创建和属性的依赖注入完成之后,在对Spring依赖注入的源码分析过程中我们知道,当应用程序第一次调用 getBean()方法(lazy-init 预实例化除外)向 Spring IOC 容器索取指定 Bean 时触发 Spring IOC 容器创建 Bean 实例 对 象 并 进 行 依 赖 注 入 的 过 程 , 其 中 真 正 实 现 创 建 Bean 对 象 并 进 行 依 赖 注 入 的 方 法 是AbstractAutowireCapableBeanFactory 类的 doCreateBean()方法,主要源码如下:
//真正创建 Bean 的方法
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
	//创建 Bean
	Object exposedObject = bean;
	try {
		//对 Bean 属性进行依赖注入
		populateBean(beanName, mbd, instanceWrapper);
		//在对 Bean 实例对象生成和依赖注入完成以后,开始对 Bean 实例对象
		//进行初始化 ,为 Bean 实例对象应用 BeanPostProcessor 后置处理器
		exposedObject = initializeBean(beanName, exposedObject, mbd);
	}
	catch (Throwable ex) {
		if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
			throw (BeanCreationException) ex;
		}
		else {
			throw new BeanCreationException(
			mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
		}
	}
	...
	//为应用返回所需要的实例对象
	return exposedObject;
}
从上面的代码中我们知道,为 Bean 实例对象添加 BeanPostProcessor 后置处理器的入口的是initializeBean()方法。
3、initializeBean()方法为容器产生的 Bean 实例对象添加 BeanPostProcessor 后置处理器
同样在 AbstractAutowireCapableBeanFactory 类中,initializeBean()方法实现为容器创建的 Bean实例对象添加 BeanPostProcessor 后置处理器,源码如下:
//初始容器创建的 Bean 实例对象,为其添加 BeanPostProcessor 后置处理器
protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
//JDK 的安全机制验证权限
if (System.getSecurityManager() != null) {
//实现 PrivilegedAction 接口的匿名内部类
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
invokeAwareMethods(beanName, bean);
return null;
}, getAccessControlContext());
}
else {
//为 Bean 实例对象包装相关属性,如名称,类加载器,所属容器等信息
invokeAwareMethods(beanName, bean);
}
Object wrappedBean = bean;
//对 BeanPostProcessor 后置处理器的 postProcessBeforeInitialization
//回调方法的调用,为 Bean 实例初始化前做一些处理
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}
//调用 Bean 实例对象初始化的方法,这个初始化方法是在 Spring Bean 定义配置
//文件中通过 init-Method 属性指定的
try {
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(
(mbd != null ? mbd.getResourceDescription() : null),
beanName, "Invocation of init Method failed", ex);
}
//对 BeanPostProcessor 后置处理器的 postProcessAfterInitialization
//回调方法的调用,为 Bean 实例初始化之后做一些处理
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}
return wrappedBean;
}
@Override
//调用 BeanPostProcessor 后置处理器实例对象初始化之前的处理方法
public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)
throws BeansException {
Object result = existingBean;
//遍历容器为所创建的 Bean 添加的所有 BeanPostProcessor 后置处理器
for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
//调用 Bean 实例所有的后置处理中的初始化前处理方法,为 Bean 实例对象在
//初始化之前做一些自定义的处理操作
Object current = beanProcessor.postProcessBeforeInitialization(result, beanName);
if (current == null) {
return result;
}
result = current;
}
return result;
}
@Override
//调用 BeanPostProcessor 后置处理器实例对象初始化之后的处理方法
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
throws BeansException {
Object result = existingBean;
//遍历容器为所创建的 Bean 添加的所有 BeanPostProcessor 后置处理器
for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
//调用 Bean 实例所有的后置处理中的初始化后处理方法,为 Bean 实例对象在
//初始化之后做一些自定义的处理操作
Object current = beanProcessor.postProcessAfterInitialization(result, beanName);
if (current == null) {
return result;
}
result = current;
}
return result;
}
BeanPostProcessor 是一个接口,其初始化前的操作方法和初始化后的操作方法均委托其实现子类来实现,在 Spring 中,BeanPostProcessor 的实现子类非常的多,分别完成不同的操作,如:AOP 面向切面编程的注册通知适配器、Bean 对象的数据校验、Bean 继承属性、方法的合并等等,我们以最简单的AOP 切面织入来简单了解其主要的功能。下面我们来分析其中一个创建 AOP 代理对象的子类AbstractAutoProxyCreator 类。该类重写了 postProcessAfterInitialization()方法。
01-20 12:28