Spring源码分析(六)SpringAOP实例及标签的解析

栏目: Java · 发布时间: 5年前

内容简介:Spring的IOC和AOP称之为Spring框架的两个核心。AOP是什么?AOP原理是什么?本章节开始,我们就来看看SpringAOP到底是怎么玩转起来的?Aspect Oriented Programming,面向切面编程,是一种编程范例,旨在通过分离横切关注点来增加模块性,它通过在不修改代码本身的情况下向现有代码添加其他行为来实现。动态的将代码切入到类的指定方法或指定位置上的编程思想,就是面向切面编程。在系统中,肯定存在一些公共逻辑模块。比如日志的记录,事务的管理,请求的校验等。如果把这种逻辑模块的代

Spring的IOC和AOP称之为Spring框架的两个核心。AOP是什么?AOP原理是什么?本章节开始,我们就来看看SpringAOP到底是怎么玩转起来的?

AOP是什么?

定义

Aspect Oriented Programming,面向切面编程,是一种编程范例,旨在通过分离横切关注点来增加模块性,它通过在不修改代码本身的情况下向现有代码添加其他行为来实现。动态的将代码切入到类的指定方法或指定位置上的编程思想,就是面向切面编程。

使用

在系统中,肯定存在一些公共逻辑模块。比如日志的记录,事务的管理,请求的校验等。如果把这种逻辑模块的代码收到写到业务模块中,代码重复度就非常之高。这还不是唯一的问题,关键如果公共逻辑模块的代码要修改,必须要全部修改。这个根本不符合 码农 的科学发展观。AOP,可以帮助我们解决这些问题。

实现

AOP本身并不能解决这些问题,AOP就是一种思想,而解决问题依靠的是AOP具体的实现,也就是我们本章节所说的Spring AOP。不过,值得注意的是,在Spring2.0之后,开始集成aspectj。所以,我们所说的Spring AOP,其实就是Spring加Aspectj这种方式。

概念性知识

要熟悉Spring AOP,里面有些概念一定要先搞搞清楚才行。

  • Aspect 切面,将横切关注点设计为独立可重用的对象,这些对象称为切面。实际上就是一些功能增强的类或者对象的代表,比如:日志管理、事务管理、异常控制等。

  • Joinpoint 连接点,切面在应用程序执行时加入对象的业务流程中的特定点,称为连接点。它用来定义在目标程序的哪里通过AOP加入新的逻辑。通俗讲,就是对应的具体的被代理的方法 ,比如saveUser()。Joinpoint跟我们具体的被代理的方法一一对应

  • Pointcut 切点,匹配连接点的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行。它是joinpoint的集合。

  • Advice 通知/增强,在切面的某个特定的连接点上执行的动作。可以理解为它是一段程序代码,在代理类上的上面或者下面增加一些代码来实现增强。比如事务管理AOP,通知/增强对应的就是开启事务、关闭事务这些具体代码上的操作。

  • Advisor Advice和Pointcut组成的独立的单元,用来定义只有一个通知和一个切入点的切面。再通俗点来说,它是将Advice注入到程序中的Pointcut位置。Spring中的事务管理使用的就是advisor。

  • Introduction 引入,通过引入,可以在一个对象中加入新的方法和属性,而不用修改它的程序。这种方式很少用,基本也不太推荐用。自己定义的通知必须要实现MethodInterceptor。

实例

了解到上面的知识后,我们通过XML的配置方式具体来看一下Spring AOP的应用。

首先,定义一个切面的类。

public class UserAspect {

	public void beforeAdvice() {
		System.out.println("前置通知");
	}
	public void afterAdvice() {
		System.out.println("后置通知");
	}
	public void afterReturnAdvice() {
		System.out.println("返回通知");
	}
	public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {	
		System.out.println("环绕通知之前");
		Object result = joinPoint.proceed();
		System.out.println("环绕通知之后");
		return result;
	}
}
复制代码

其次,在Spring配置文件中先将这个类注册成Bean。再通过AOP的标签关联到一起。

<bean id="userAspect" class="com.viewscenes.netsupervisor.aspect.UserAspect"></bean>
	
<aop:config>
	<aop:aspect id="userAspect" ref="userAspect">
		<aop:pointcut id="userPointcut" expression="(execution(* 
                              com.viewscenes.netsupervisor.service..*.*(..)))" />
		<aop:before method="beforeAdvice" pointcut-ref="userPointcut"/>
		<aop:after method="afterAdvice"  pointcut-ref="userPointcut"/>
		<aop:after-returning method="afterReturnAdvice" pointcut-ref="userPointcut"/>
		<aop:around method="aroundAdvice" pointcut-ref="userPointcut"/> 
	</aop:aspect>
</aop:config>
复制代码

最后,我们通过调用UserService中的方法来测试一下。

前置通知
环绕通知之前
----------根据ID删除用户信息------------
环绕通知之后
返回通知
后置通知
复制代码

XML标签的解析

不知诸位可否还有印象,Spring是怎么解析配置文件中的标签的呢?如果不记得,可以到 Spring源码分析(一)Spring的初始化和XML解析 回顾一下。

这里,我们直接来到 ConfigBeanDefinitionParser.parse() 方法。它位于 org.springframework.aop.config 包。大概可以分为两个步骤,注册入口类和解析子节点。

注册入口类

parse方法的开始就注册了一个类, AspectJAwareAdvisorAutoProxyCreator 。这个类相当重要,它是AOP的入口类。注册的过程就是把它封装成BeanDefinition对象,添加到beanDefinitionNames容器中。这个容器,我们已经很熟悉了,就是循环它来进行实例化和依赖注入。

//cls就是AspectJAwareAdvisorAutoProxyCreator.class
private static BeanDefinition registerOrEscalateApcAsRequired(Class<?> cls, 
								BeanDefinitionRegistry registry, Object source) {
	RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
	beanDefinition.setSource(source);
	beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
	beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
	
	//注册beanDefinition 将beanName加入到beanDefinitionNames容器中
	registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
	return beanDefinition;
}
复制代码

解析子节点

接下来是解析配置文件标签的地方,获取 <aop:config> 下的子标签。它的子标签只有三类: <aop:pointcut>、<aop:advisor>、<aop:aspect> 。下面的源码也正对应这三种类型。

List<Element> childElts = DomUtils.getChildElements(element);
for (Element elt: childElts) {
	String localName = parserContext.getDelegate().getLocalName(elt);
	if (POINTCUT.equals(localName)) {
		parsePointcut(elt, parserContext);
	}
	else if (ADVISOR.equals(localName)) {
		parseAdvisor(elt, parserContext);
	}
	else if (ASPECT.equals(localName)) {
		parseAspect(elt, parserContext);
	}
}
复制代码

pointcut的解析

pointcut解析其实很简单,把id和expression拿到,封装成BeanDefinition对象,它的类是 AspectJExpressionPointcut ,把表达式放入beanDefinition对象的propertyValues属性,最后同样是注册到beanDefinitionNames容器中。

private AbstractBeanDefinition parsePointcut(Element pointcutElement, ParserContext parserContext) {
	String id = pointcutElement.getAttribute(ID);
	String expression = pointcutElement.getAttribute(EXPRESSION);
	AbstractBeanDefinition pointcutDefinition = null;
	try {
		pointcutDefinition = createPointcutDefinition(expression);
		String pointcutBeanName = id;
		if (StringUtils.hasText(pointcutBeanName)) {
			//注册到beanDefinitionNames容器,id为beanName
			parserContext.getRegistry().registerBeanDefinition(pointcutBeanName, pointcutDefinition);
		}
	}
	return pointcutDefinition;
}

protected AbstractBeanDefinition createPointcutDefinition(String expression) {
	RootBeanDefinition beanDefinition = new RootBeanDefinition(AspectJExpressionPointcut.class);
	beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
	beanDefinition.setSynthetic(true);
	beanDefinition.getPropertyValues().add(EXPRESSION, expression);
	return beanDefinition;
}
复制代码

aspect的解析

aspect是一个切面。切面里面包含切入点和通知。引入类型先略过不表。

  • advice

获取aspect节点下的所有子节点,先过滤advice节点。然后解析生成AspectJPointcutAdvisor类的BeanDefinition对象。

//获取aspect节点的子节点 
NodeList nodeList = aspectElement.getChildNodes();
boolean adviceFoundAlready = false;
for (int i = 0; i < nodeList.getLength(); i++) {
	Node node = nodeList.item(i);
	//判断是不是advice节点。
	if (isAdviceNode(node, parserContext)) {
		if (!adviceFoundAlready) {
			adviceFoundAlready = true;
			//aspectName就切面的ref,Bean的名字
			beanReferences.add(new RuntimeBeanReference(aspectName));
		}
		//解析advice 生成AspectJPointcutAdvisor类的BeanDefinition对象。
		AbstractBeanDefinition advisorDefinition = parseAdvice(
				aspectName, i, aspectElement, (Element) node, parserContext, beanDefinitions, beanReferences);
		beanDefinitions.add(advisorDefinition);
	}
}

private boolean isAdviceNode(Node aNode, ParserContext parserContext) {
	String name = parserContext.getDelegate().getLocalName(aNode);
	return (BEFORE.equals(name) || AFTER.equals(name) || 
		AFTER_RETURNING_ELEMENT.equals(name) ||
		AFTER_THROWING_ELEMENT.equals(name) || AROUND.equals(name));
}
复制代码

parseAdvice方法注册很多类,最后串联到一块来,一个一个来看。

首先,创建了方法工厂bean。注册了MethodLocatingFactoryBean类,往propertyValues中添加了两个属性,targetBeanName切面的Bean、methodName通知的方法名。

RootBeanDefinition methodDefinition = new RootBeanDefinition(MethodLocatingFactoryBean.class);
//aspectName切面类的Bean  methodName方法名称 比如before
methodDefinition.getPropertyValues().add("targetBeanName", aspectName);
methodDefinition.getPropertyValues().add("methodName", adviceElement.getAttribute("method"));
methodDefinition.setSynthetic(true);
复制代码

然后,创建实例工厂的定义。注册了SimpleBeanFactoryAwareAspectInstanceFactory类,这个类实现了BeanFactoryAware接口。这样的话,在实例化的时候会调用到setBeanFactory方法,可以拿到BeanFactory。有个getAspectInstance方法,根据切面名字就可以拿到切面类的实例。

//注册SimpleBeanFactoryAwareAspectInstanceFactory实例的BeanDefinition
RootBeanDefinition aspectFactoryDef =
		new RootBeanDefinition(SimpleBeanFactoryAwareAspectInstanceFactory.class);
aspectFactoryDef.getPropertyValues().add("aspectBeanName", aspectName);


//类的属性和方法
public class SimpleBeanFactoryAwareAspectInstanceFactory implements 
								AspectInstanceFactory, BeanFactoryAware {
	
	private String aspectBeanName;
	private BeanFactory beanFactory;
	
	public void setAspectBeanName(String aspectBeanName) {
		this.aspectBeanName = aspectBeanName;
	}

	public void setBeanFactory(BeanFactory beanFactory) {
		this.beanFactory = beanFactory;
		if (!StringUtils.hasText(this.aspectBeanName)) {
			throw new IllegalArgumentException("'aspectBeanName' is required");
		}
	}
	public Object getAspectInstance() {
		return this.beanFactory.getBean(this.aspectBeanName);
	}
}
复制代码

其次,注册切入点。它把上面这两个BeanDefinition当做参数传了过去,最后放入新建的BeanDefinition对象中。这个新建的BeanDefinition对象,是根据advice类型而创建的,当然了,也是五个类型,对应五个类的实例。下面还有三个步骤:设置propertyValues、解析advcie里的pointcut属性、设置bean的参数列表。

private AbstractBeanDefinition createAdviceDefinition(
	Element adviceElement, ParserContext parserContext, String aspectName, int order,
			RootBeanDefinition methodDef, RootBeanDefinition aspectFactoryDef,
			List<BeanDefinition> beanDefinitions, List<BeanReference> beanReferences) {

	//getAdviceClass 根据advice的类型创建不同类型的BeanDefinition
	//BEFORE前置通知 					AspectJMethodBeforeAdvice.class
	//AFTER后置通知 					AspectJAfterAdvice.class
	//AFTER_RETURNING_ELEMENT返回后通知 AspectJAfterReturningAdvice.class
	//AFTER_THROWING_ELEMENT异常通知  	AspectJAfterThrowingAdvice.class
	//AROUND环绕通知					AspectJAroundAdvice.class

	RootBeanDefinition adviceDefinition = new RootBeanDefinition(
							   getAdviceClass(adviceElement, parserContext));
	adviceDefinition.setSource(parserContext.extractSource(adviceElement));
	//1、设置propertyValues
	adviceDefinition.getPropertyValues().add(ASPECT_NAME_PROPERTY, aspectName);
	adviceDefinition.getPropertyValues().add(DECLARATION_ORDER_PROPERTY, order);

	//2、解析advcie里的pointcut属性
	//pointcut分为两种。一种是pointcut-ref引用类型,一种是pointcut表达式类型
	//如果是引用类型,返回字符串  
	//如果是表达式类型,则创建AspectJExpressionPointcut类型的Bean,将表达式放入propertyValues属性。
	Object pointcut = parsePointcutProperty(adviceElement, parserContext);
	if (pointcut instanceof BeanDefinition) {
		cav.addIndexedArgumentValue(POINTCUT_INDEX, pointcut);
		beanDefinitions.add((BeanDefinition) pointcut);
	}
	else if (pointcut instanceof String) {
		RuntimeBeanReference pointcutRef = new RuntimeBeanReference((String) pointcut);
		cav.addIndexedArgumentValue(POINTCUT_INDEX, pointcutRef);
		beanReferences.add(pointcutRef);
	}

	//3、设置bean的参数列表。adviceDefinition对象有一个构造函数参数值,放入了三个属性
	ConstructorArgumentValues cav = adviceDefinition.getConstructorArgumentValues();
	cav.addIndexedArgumentValue(METHOD_INDEX, methodDef);
	cav.addIndexedArgumentValue(POINTCUT_INDEX, pointcutRef);
	cav.addIndexedArgumentValue(ASPECT_INSTANCE_FACTORY_INDEX, aspectFactoryDef);
        
	return adviceDefinition;
}
复制代码

最后,配置advisor。创建AspectJPointcutAdvisor类实例的BeanDefinition对象,还是那个构造函数参数值,把上一步返回的adviceDefinition当做参数放入genericArgumentValues。

RootBeanDefinition advisorDefinition = new RootBeanDefinition(AspectJPointcutAdvisor.class);
advisorDefinition.setSource(parserContext.extractSource(adviceElement));
//构造函数参数值 adviceDef就是上一步返回的adviceDefinition
advisorDefinition.getConstructorArgumentValues().addGenericArgumentValue(adviceDef);
复制代码

最后的最后,注册advisorDefinition到容器中并返回。

parserContext.getReaderContext().registerWithGeneratedName(advisorDefinition);
return advisorDefinition;
复制代码

一定要记得,这一系列操作都是在循环体里完成的。所以,有几个通知的类型,就会生成几个advisorDefinition对象。处理完,添加到循环体开头定义的List中。

  • pointcut

刚才在解析advice已经解析了pointcut,这里又有一个呢?advice里的pointcut是独立使用的,只能作用于当前的advice。但是在aspect里面也可以单独定义pointcut,可以作用于所有的advice。解析过程是一样的,不再赘述。

advisor的解析

advisor可以理解为是只有一个通知和一个切入点的切面。它的解析也比较简单。创建一个DefaultBeanFactoryPointcutAdvisor类实例的BeanDefinition的对象,把通知的BeanName和Order放入propertyValues,再把这个BeanDefinition对象注册到容器中。然后解析pointcut,过程一样。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

JavaScript入门经典

JavaScript入门经典

Paul Wilton、Jeremy McPeak / 施宏斌 / 清华大学出版社 / 2009-2 / 98.00元

《Java Script入门经典(第3版)》首先介绍了J avaScript的基本语法,并介绍了如何发挥JavaScript中对象的威力。《Java Script入门经典(第3版)》还介绍了如何操纵最新版本浏览器所提供的BOM对象。在《Java Script入门经典(第3版)》的高级主题中,将介绍如何使用cookie,以及如何应用DHTML技术使Web页面焕发动感和活力。另外,《Java Scri......一起来看看 《JavaScript入门经典》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

html转js在线工具
html转js在线工具

html转js在线工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具