AOP
AOP(Aspect Oriented Programming),即面向切面编程。面向切面是什么意思呢?
首先我们看一张图。
在OOP中,允许开发者定义横向的层次,例如图中的CourseService等。但是不允许开发者定义纵向的关系,例如日志功能。日志功能往往是散布在所有的对象层次当中。而AOP就是实现纵向功能的一种技术。
AOP将影响多个类的公共行为封装到一个可重用的模块中,并命名为切面,就如图中的纵向功能,纵切了多个类,所以形象的称为切面。AOP减少了系统的重复的代码,降低了模块的耦合程度,有利于维护。
如何实现面向切面的功能?
如果是你,你会用什么样的思路来实现这个功能呢?我举个例子,例如是日志功能,日志功能需要提供什么样的功能呢?需要在调用这个业务之前做日志功能,完成调用之后也要写日志,概括的来说就是需要在调用功能的前后来写日志。在扩展来说,就是在调用某个功能的前后完成某些操作。这不就是JAVA设计模式中的代理模式吗?Spring AOP也是采用代理模式的方式来实现的。
AOP的核心概念:
1.切面(aspect)
官方的抽象定义为“一个关注点的模块化,这个关注点可能会横切多个对象”。
2.连接点(joinpoint)
程序执行过程中的某一行为
3.切入点(pointcut)
对连接点进行拦截的定义。也就是上面的调用的某个功能,对其进行拦截。
4.通知(advice)
拦截到连接点之后要执行的代码,可能是之前执行,也可能是之后执行。
5.目标对象(target)
代理的目标对象
6.织入(weave)
将切面应用到目标对象并导致代理对象创建的过程
7.引入(introduction)
在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段
其中advice可以分为5种类型
1.Before advice
在某连接点(JoinPoint)之前执行的通知,但这个通知不能阻止连接点前的执行。
2.After return advice
在某连接点正常完成后执行的通知,不包括抛出异常的情况。
3.After throwing advice
在方法抛出异常退出时执行的通知。
4.After advice
当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。所以也称为finally advice
5.Around advice
包围一个连接点的通知,即自定义之前执行还是之后执行。
Spring AOP API的方式实现:(这是最原始的方式,后来的无论是基于XML的还是注解,基础都是这个方式)
举个起床的例子。我们有一个起床的接口,一个human的实现类。
public interface Iawake {
public void awake();
}
public class Human implements Iawake {
public void awake() {
System.out.println("I am awake");
}
}
创建一个advice类,分别继承MethodBeforeAdvice, AfterReturningAdvice接口。
public class ApiAdvice implements MethodBeforeAdvice, AfterReturningAdvice {
public void afterReturning(Object returnType, Method mtd, Object[] arg, Object target) throws Throwable {
System.out.println("Brush my teeth.");
}
public void before(Method mtd, Object[] arg, Object target) throws Throwable {
System.out.println("The clock rings.");
}
}
并在XML配置文件中对两个实现类创建bean对象。创建通知的操作就完成了。
然后进行XML的配置工作。- -名称略长。
首先配置一个切入点。这里切入点用正则的匹配方式,匹配所有以awake结尾的方法。
<bean id="awakePointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<property name="pattern" value=".*awake" />
</bean>
切入点仅仅表示了什么地方切入,我们还需要将他和advice结合起来,组合成advisor,详细的说明在哪,什么时候,做什么。
<bean id="myAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="advice" ref="apiAdvice" />
<property name="pointcut" ref="awakePointcut" />
</bean>
通知者advisor也声明了,那么我们要如何使用呢?这时就应该产生代理对象了。<bean id="humanProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="human" />
<property name="interceptorNames" value="myAdvisor" />
<property name="proxyInterfaces" value="spring.aopTest.api.Iawake" />
</bean>
这里不要被语法给迷惑了,使用humanProxy拿到的并不是ProxyFactoryBean的实例,而是ProxyFactoryBean的getObject方法返回的实例,也就是下面配置的target对应的实例。也就是说,humanProxy返回的是human这个bean对应的类的实例。
当我们获取humanProxy的bean的时候,并调用awake方法的时候,就会调用响应的advice得到结果了。
public void TestAPI() {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring-aopApi.xml");
Iawake iawake = (Iawake)ctx.getBean("humanProxy");
iawake.awake();
ctx.destroy();
}
结果:
The clock rings.
I am awake
Brush my teeth.
有没有觉得略复杂,Spring提供了一种更为简便的方法,使用自动代理。
<bean id="apiAdvice" class="spring.aopTest.api.ApiAdvice"></bean>
<bean id="myAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice" ref="apiAdvice" />
<property name="pattern" value=".*awake" />
</bean>
<bean id="human" class="spring.aopTest.api.Human"></bean>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />
这样只需要声明org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator就可以为匹配的bean自动的创建代理。结果是一致的。
当然,advice还可以实现很多其他的类,来分别对应不同的操作时间。这里不一一说明了。
Spring AOP XML配置:
XML的配置,相对于API的方式来说,简单了很多。
例子是一样的,都是有一个Iawake接口和一个Human实现类。
我们还要创建一个advice类,里面具体需要什么方法我们再说。
现在我们去xml文件中配置切面。首先我们为advice类声明为一个bean
<bean id="XmlAspect" class="spring.aopTest.XmlAdvice"></bean>
接着我们创建一个切面
<aop:config>
<aop:aspect id="XmlAspectAOP" ref="XmlAspect">
</aop:aspect>
</aop:config>
这个切面是advice类的引用。我们在这个切面中配置它的切入点,并且配置before标签。就是当匹配到切入点的时候这个切面会在之前这个切入点之前执行before里的方法。
<aop:config>
<aop:aspect id="XmlAspectAOP" ref="XmlAspect">
<aop:pointcut expression="execution(* spring.aopTest.Human.*(..))"
id="humanPointcut" />
<aop:before method="before" pointcut-ref="humanPointcut" />
</aop:aspect>
</aop:config>
pointcut中的expression就是匹配的条件,这里的意思是匹配spring.aopTest的Human类下的任意方法。
这里还需要在advice类中增加before方法。
public class XmlAdvice {
public void before() {
System.out.println("The clock rings.");
}
}
就是我们醒之前,闹钟要响。
我们看如果运行到Human下的awake方法的时候,也就是切入点的时候,会发生什么事呢?
The clock rings. <-
切入点之前的before
I am awake <-
切入点的执行
这样,我们就成功在某个切入点的前面执行了某种操作,就完成了AOP。
我们还可以在切入点的后面执行某种操作,使用<aop:after> <aop:after-returning> <aop:after-throwing><aop:around>方法。分别对应不同的情况。
我认为使用XML的方式可以更好的理解AOP的概念。并且更加容易配置和明白并且灵活简短。
Spring AOP 注解方式(AspectJ):
使用注解的方式相对于XML配置的方式来说更加的简便!!
还是使用同样的例子。
创建一个Aspect类替代原来的Advice类。
我们可以使用@Aspect注解来标识这是一个切面,就像在XML中的一样。
很显然,切面中存在很多自对象,例如pointcut,advice等等。
在注解的方式中,使用@Pointcut标注这是一个pointcut,使用@Before,@AfterReturning等来标识各种advice等等。
例如:
@Component
@Aspect
public class AnnotationAspect {
@Pointcut("execution(* spring.aopTest.annotation.Human.*(..))")
public void Pointcut1() {}
@Before("Pointcut1()")
public void Before() {
System.out.println("The clock rings.");
}
@AfterReturning("Pointcut1()")
public void AfterReturning() {
System.out.println("Brush my teeth.");
}
}
这样是不是也非常的清晰并且简单呢?
这样,切面就定义好了。
我们只需要在xml配置文件中加上:
<context:component-scan base-package="spring.aopTest.annotation"/>
<aop:aspectj-autoproxy proxy-target-class="true"/>
加上注解的扫描,并且将自动代理打开,就能轻松的完成AOP的功能了。这里需要注意的是,我们要么在Aspect类中使用@Component注解,要么在需要在配置文件中显示的声明这个Aspect类的bean。
因为@Aspect注解不能够通过类路径自动检测发现,也就是说component-scan这个扫描并不会读取@Aspect这个注解,所以需要使用@Component或者自己声明bean。
@pointcut中的匹配可以使用很多方式,如within,execution等等,还可以携带参数args,annotaion等等。这个还需要慢慢的去使用探索。