在开始本篇博文前,我们先思考一个问题:
Question: Java 语音在设计时没有开放类的理念,也就是说 Java 并不是动态语言,一旦类编译完成了,很难再为该类添加新的功能;那么如何实现像 Ruby 一样,在不直接修改对象或类的定义就能够为对象或类增加新的方法。
Answer: 首先我们直到我们使用动态代理时候可以为对象拥有的方法添加新的功能,而 spring 的AOP 就是基于java的 动态代理,既然切面能够为现有的方法增加额外的功能,那么如何给一个对象增加新的方法呢?
对于动态代理推荐两篇博文:
接下来,我们基于《Spring in Action (第四版) 》来对AOP做一个详细的介绍;
==================================================================
一:什么是面向切面编程
在模块化开发过程中,每一个模块可以有两部分组成:核心功能(为特定业务领域提供功能)和辅助功能(安全,日志,事物);这些辅助功能虽然不是服务模块的核心代码,但是它又遍布在程序的各个角落,因为每个模块都要涉及到安全、事物等辅助功能;我们可以将这些分布在应用程序的各个角落,与业务逻辑相切的功能称为横切性关注点;
如果我们要重用通用的功能,最常见的技术是继承或者委托(组合),但是如果整个应用都使用相同的基类,首先 Java 是单继承,其次,继承形成的体系相对脆弱;如果使用委托,又会涉及到对委托对象的复杂的调用。最终我们可以引入 AOP 的技术来解决上述问题;
二:AOP术语
通知(Advice)
通知 定义了切面是什么以及何时使用。也就是说 Advice 由两部分组成,除了描述切面要完成的工作,通知还要解决何时执行这个工作的问题(它应该应用在某个方法被调用之前?之后?之前之后都调用?还是抛出异常时再调用?)。
Spring 的切面有5种类型的通知:
- 前置通知(Before): 在目标方法被调用之前调用通知的功能;
- 后置通知(After): 在目标方法完成之后调用通知,此时不关心方法的输出是什么;
- 返回通知(After-returning): 在目标方法成功执行之后调用通知;
- 异常通知(After-throwing): 在目标方法抛出异常后调用通知;
- 环绕通知(Around): 在目标方法调用之前和调用之后执行自定义的行为;
连接点(Join point)
连接点可以看成是应用通知的时机,也就是连接点是在应用执行过程中能够插入切面的一个点;这个点可以是调用方法时,抛出异常时,修改字段时等。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为;
切点(Poincut)
切点定义了这个切面在何处执行,也就是这个切面在应用程序中的那些连接点上执行,限定了连接点的范围;切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或者正则表达式定义所匹配的类和方法名称来指定这些切点;
切面(Aspect)
切面是通知(Advice)和切点(Poincut)的结合。
对上面上个概念我们用一个简易图进行说明切面 切点 连接点 和应用程序执行流程的关系如下:
引入(Introduction)
引入允许我们向现有的类添加新的属性或方法。例如,我们创建了一个Auditable通知类,该类记录了对象最后一次修改时的状态,我们只需在这个类中定义一个字段保持修改时间,提供一个方法来修改时间即可;然后将这个新的方法和实例变量引入到现有的类中。从而在无需修改这些现有类的情况下,让它们具有新的行为和状态;
织入(Weaving)
把切面应用到目标对象并创建新的代理对象的过程;切面在指定的连接点被织入到目标对象中,在目标对象的生命周期中有多个点可以进行织入;
- 编译期:切面在目标类被编译是被织入。这种方式需要特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的;
- 类加载期:切面在目标类加载到JVM 时被织入,这种方式需要特殊的类加载器(ClassLoader),
- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP 容器会为目标对象动态创建一个代理对象。 Spring AOP 就是采用的这种方式织入切面的。
三:Spring 对AOP的支持
Spring AOP 构建在动态代理基础上,因此 Spring 对 AOP 的支持局限于方法的拦截;如果我们的 AOP 需要超过了简单的方法调用(如构造器或者属性拦截),那么我们可以使用 AspectJ 来实现切面;
Spring AOP 在运行时通知对象:
通过在代理类中包裹切面,Spring 在运行期把切面织入到 Spring 管理的 bean 中。代理类封装了目标类,并拦截 被通知方法 (目标类的方法)的调用,在调用目标 bean 方法之前或之后执行切面逻辑,同时可以转发给真正的目标bean。Spring 在应用需要被代理的bean时,才会创建代理对象。因此Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入 Spring AOP 的切面。
通过切点来选择连接点:
切点用于准确的定位应该在什么地方应用切面的通知。通知和切点是切面的最基本元素。如何编写切点具有重要的意义,Spring AOP 所支持的AspectJ切点指示器如下
arg() | 限定连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的bean引用为指定类型的类 |
target | 限制连接点匹配目标对象为指定类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型 |
@annotation | 限定匹配带有指定注解的连接点 |
创建切面:
(1)使用注解创建切面:
示例背景:对于一场演出,从演出的角度来看,观众是非常重要的,但对于演出本身的功能来讲,观众并不是核心,这是一个单独的关注点。因此我们可以把观众定义成一个切面,并将其应用到演出上。
示例代码:
先定义一个借口和相应的实现;
public interface Performance {
public void perform();
public void show();
}
public class PerformanceImpl implements Performance {
@Override
public void perform() {
// TODO Auto-generated method stub
System.out.println("....perform method is running...");
}
@Override
public void show() {
// TODO Auto-generated method stub
System.out.println("....show method is running...");
}
}
使用注解 将一个POJO类定义成一个切面;
package qian.aspectj;
import org.aspectj.lang.ProceedingJoinPoint;
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;
/**
* @author Qian
* 功能:
* Step1:使用注解定义一个类为切面
* Aspect:[Advice(DoWhat, WhenDo), Poincut]
* Step2:如何让这个标满了注解的POJO类变成切面;
* 第一种:使用JavaConfig
* 第二种:xml中配置;
*/
//声明这个类为一个切面;
@Aspect
public class Audience {
//因为Before AfterReturning AfterThrowing 后的相同切点表达方式存在重复
//使用注解@Pointcut 定义命名的切点;
@Pointcut("execution(** qian.aspectj.Performance.perform(..))")
public void performance(){
//此方法只为@Pointcut注解依附;
}
@Before("performance()")
//@Before("execution(* qian.aspectj.Performance.perform(..))")
//method_intro:手机静音(表演之前)
public void silenceCellPhones(){
System.out.println("Silencing cell phones ");
}
@Before("performance()")
//@Before("execution(* qian.aspectj.Performance.perform(..))")
//method_intro: 就坐(表演之前)
public void takeSeats(){
System.out.println(" Taking seats");
}
@AfterReturning("performance()")
//@AfterReturning("execution(* qian.aspectj.Performance.perform(..))")
//method_intro: 鼓掌(表演之后)
public void applause(){
System.out.println("Good CLAP CLAP CLAP...");
}
@AfterThrowing("performance()")
//@AfterThrowing("execution(* qian.aspectj.Performance.perform(..))")
//method_intro: 退钱(表演失败之后)
public void demandRefund(){
System.out.println("Demanding a refund!!!");
}
@Around("execution(** qian.aspectj.PerformanceImpl.show(..))")
//method_intro: 观看节目整个过程
public void watchShowing(ProceedingJoinPoint jp){
try {
System.out.println("Silencing cell phones ");
System.out.println(" Taking seats");
jp.proceed();
System.out.println("Good CLAP CLAP CLAP...");
} catch (Throwable e) {
System.out.println("Demanding a refund!!!");
}
}
}
使用JavaConfig的形式使用注解:主类和配置文件;
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig{
//声明Audience bean
@Bean
public Audience getAudience(){
return new Audience();
}
public static void main(String[] args){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext2.xml");
Performance pf = (Performance) context.getBean("performanceImpl");
pf.perform();
System.out.println("===============");
pf.show();
}
}
Spring 的applicationContext.xml配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
...>
<!-- Step1: 启动包扫描 -->
<context:component-scan base-package="qian.aspectj"/>
<bean id="performanceImpl" class="qian.aspectj.PerformanceImpl"/>
</beans>
都使用xml文件:
public class ConcertConfig{
public static void main(String[] args){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext2.xml");
Performance pf = (Performance) context.getBean("performanceImpl");
pf.perform();
System.out.println("===============");
pf.show();
}
}
applicationContext.xml文件,通知 Spring 我们使用了注解,让其帮我们自动创建和在何时的时机使用这个切面;
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:p="http://www.springframework.org/schema/p" xmlns:util="http://www.springframework.org/schema/util" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- Step1: 启动包扫描 -->
<context:component-scan base-package="qian.aspectj"/>
<!-- Step2: 启动AspectJ自动代理 -->
<aop:aspectj-autoproxy/>
<!-- 声明Audience bean -->
<bean class="qian.aspectj.Audience"/>
<bean id="performanceImpl" class="qian.aspectj.PerformanceImpl"/>
</beans>
在上面的示例代码中,我们需要注意以下几点:
- 我们在定义一个POJO类为一个切面时,注意几个概念和连接点选择范围的定义;Aspect【Advice(What:切面要做什么也就是各种方法,When:何时做before after...),Poincut:在那些连接点上使用切面】
- 当切面类中的多个方法匹配相同的连接点时,我们可以将这些链接点限定的范围pointcut 抽取处理依附到一个空方法上,在其他需要使用这个Pointcut时,我们只需使用这个方法名+括号;
- Pointcut中连接点的限定可以限定到接口的方法上,也可以限定到实现的方法上,所以我们可以限定到接口上,这也是声明式事物中常用到的;
- 在使用环绕通知时,我们要在方法定义上接收 ProceedingJoinPoint 对象为参数,从而在通知中通过它来调用被通知的方法,我们在通知方法中可以做自己的事情,当要把通知权交给被通知的方法时,只要调用ProceedingJoinPoint对象的proceed方法即可;
- 如果在环绕通知中忘记调用proceed 方法,那么通知实际上会阻塞对被通知方法的调用。
思考:如果切面所通知的方法有参数时,切面能访问和使用传递给被通知方法的参数嘛?
定义一个带参的接口及其实现Performance.playTrack(int):
public void playTrack(int trackNumber);
在切面中声明要提供给通知方法的参数。
//声明这个类为一个切面;
@Aspect
public class Audience {
private Map<Integer,Integer> trackCounts = new HashMap<>();
@Before("execution(* qian.aspectj.Performance.playTrack(int)) && args(trackNumber)")
public void countTrack(int trackNumber){
int currentCount = getPlayCount(trackNumber);
trackCounts.put(trackNumber, currentCount+1);
}
public int getPlayCount(int trackNumber){
return trackCounts.containsKey(trackNumber)? trackCounts.get(trackNumber):0;
}
}
思考:通过注解如何引入新的功能?
在 Spring 中,切面只是实现了它们所包装 bean 相同接口的代理,如果除了实现这些接口(也就是包装bean所实现的接口),代理也能暴露新的接口的话,会怎么样?这样的话切面所通知的bean 看起来像是实现了新的接口,即便底层实现类并没有实现这些接口也无所谓。
第一步:我们先定义一个接口及其实现;
public interface HelpPerformance {
public void helpPerform();
}
public class HelpPerformanceImpl implements HelpPerformance {
@Override
public void helpPerform() {
System.out.println("helpPerform().....");
}
}
第二步:让接口Performance的全部或者部分实现同时实现HelpPerformance接口,
@Aspect
public class HelpPerformanceAspect {
@DeclareParents(value="qian.aspectj.Performance+",
defaultImpl=HelpPerformanceImpl.class)
public static HelpPerformance helpPerformance;
}
@DeclareParents注解由三部分组成:
- value: 指定那些类型的bean 要引入该接口。本例中所有实现Performance接口的类型。标记符后面的加号 表示 Performance 的所有 子类型,而不是Performance本身。
- defaultImpl: 指定了为引入功能提供实现的类,也就是HelpPerformance的实现类;
- @DeclareParents 注解所标注的静态属性指明了要引入了接口,接口类型为HelpPerformance接口;
第三步:在application中对切面进行 bean声明;
<!-- Step1: 启动包扫描 -->
<context:component-scan base-package="qian.aspectj"/>
<!-- Step2: 启动AspectJ自动代理 -->
<aop:aspectj-autoproxy/>
<!-- 声明HelpPerformanceAspect bean -->
<bean class="qian.aspectj.HelpPerformanceAspect"/>
第四步:测试类中,我们就可以将Performance的实现接口PerfomanceImpl既可以向上转型为Performance类型,也可以向上转型为HelpPerformance类型;
public class ConcertConfig{
public static void main(String[] args){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Performance pf = (Performance) context.getBean("performanceImpl");
HelpPerformance hp = (HelpPerformance) context.getBean("performanceImpl");
Audience audience = (Audience) context.getBean("audience");
pf.perform();
hp.helpPerform();
}
(2)使用配置文件XML声明切面
当我们需要声明切面,但是又不能为通知类添加注解(无法获得通知类的源码)的时候,那必须要用XML配置了。
Spring AOP配置元素能够以非侵入的方式声明切面:
<aop:config> | 顶层的AOP配置元素, |
<aop:aspect> | 定义一个切面 |
<aop:before> | 定义AOP前置通知 |
<aop:after> | 定义AOP后置通知 |
<aop:after-returning> | 定义AOP返回通知 |
<aop:after-throwing> | 定义AOP异常通知 |
<aop:around> | 定义AOP环绕通知 |
<aop:advisor> | 定制AOP通知器 |
<aop:pointcut> | 定义一个切点 |
<aop:declare-parents> | 以透明的方式为被通知的对象引入额外的接口 |
<aop:aspectj-autoproxy> | 启用@AspectJ注解驱动的切面 |
本例还以上面注解的示例,来使用XML配置的方式来声明,
(1)为Audience类声明前置通知和后置通知和环绕通知
<beans>
<bean id="performanceImpl" class="qian.aspectj.PerformanceImpl"/>
<bean id="" class="qian.aspectj.HelpPerformanceImpl"/>
<bean id="audience" class="qian.aspectj.Audience"/>
<!-- -->
<aop:config>
<aop:aspect ref="audience">
<aop:before method="silenceCellPhones" pointcut="execution(* qian.aspectj.Performance.perform(..))"/>
<aop:before method="takeSeats" pointcut="execution(* qian.aspectj.Performance.perform(..))"/>
<aop:after-returning method="applause" pointcut="execution(* qian.aspectj.Performance.perform(..))"/>
<aop:after-throwing method="demandRefund" pointcut="execution(* qian.aspectj.Performance.perform(..))"/>
<aop:around method="watchShowing" pointcut="execution(* qian.aspectj.Performance.perform(..))"/>
</aop:aspect>
</aop:config>
</beans>
( 2 ) 将切面何时执行标签中相同的
pointcut="execution(* qian.aspectj.Performance.perform(..))"
向上抽取成一个<aop:pointcut>标签
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut id="performance" expression="execution(* qian.aspectj.Performance.perform(..))"/>
<aop:before method="silenceCellPhones" pointcut-ref="performance"/>
<aop:before method="takeSeats" pointcut-ref="performance"/>
<aop:after-returning method="applause" pointcut-ref="performance"/>
<aop:after-throwing method="demandRefund" pointcut-ref="performance"/>
<aop:around method="watchShowing" pointcut-ref="performance"/>
</aop:aspect>
</aop:config>
(3):为通知传递参数问题:注意将&& 替换成 and
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut id="performance" expression="execution(* qian.aspectj.Performance.perform(..))"/>
<aop:before method="silenceCellPhones" pointcut-ref="performance"/>
<aop:before method="takeSeats" pointcut-ref="performance"/>
<aop:after-returning method="applause" pointcut-ref="performance"/>
<aop:after-throwing method="demandRefund" pointcut-ref="performance"/>
<aop:around method="watchShowing" pointcut-ref="performance"/>
<aop:pointcut id="trackPlayed" expression="execution(* qian.aspectj.Performance.playTrack(int)) and args(trackNumber)"/>
<aop:before method="countTrack" pointcut-ref="trackPlayed"/>
</aop:aspect>
</aop:config>
( 4 ): 通过切面引入新的功能
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut id="performance" expression="execution(* qian.aspectj.Performance.perform(..))"/>
<aop:before method="silenceCellPhones" pointcut-ref="performance"/>
<aop:before method="takeSeats" pointcut-ref="performance"/>
<aop:after-returning method="applause" pointcut-ref="performance"/>
<aop:after-throwing method="demandRefund" pointcut-ref="performance"/>
<aop:around method="watchShowing" pointcut-ref="performance"/>
<!-- 为通知传递参数 -->
<aop:pointcut id="trackPlayed"
expression="execution(* qian.aspectj.Performance.playTrack(int)) and args(trackNumber)"/>
<aop:before method="countTrack" pointcut-ref="trackPlayed"/>
</aop:aspect>
<!-- 引入新功能 -->
<aop:aspect>
<aop:declare-parents
types-matching="qian.aspectj.Performance+"
implement-interface="qian.aspectj.HelpPerformance"
default-impl="qian.aspectj.HelpPerformanceImpl"/>
</aop:aspect>
</aop:config>
(5)Spring AOP中 <aop: aspect> 与 <aop:advisor>的区别分析
< aop:aspect>:定义切面(切面包括 通知 和 切点)
< aop:advisor>:定义通知器(通知器跟切面一样,也包括通知和切点)
< aop:aspect>定义切面时,只需要定义一般的bean就行,而定义< aop:advisor>中引用的通知时,通知必须实现Advice接口。
import java.lang.reflect.Method;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
public class PerformanceHelper implements MethodBeforeAdvice, AfterReturningAdvice{
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("before..");
}
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("afterReturning...");
}
}
<bean id="performanceImpl" class="qian.aspectj.PerformanceImpl"/>
<!-- 声明切面bean -->
<bean id="audience" class="qian.aspectj.Audience"/>
<bean id="performanceHelper" class="qian.aspectj.PerformanceHelper"/>
<aop:config>
<!-- ***使用advisor来声明切面*** -->
<aop:pointcut id="performance" expression="execution(* qian.aspectj.Performance.perform(..))"/>
<aop:advisor advice-ref="performanceHelper" pointcut-ref="performance"/>
<!-- 使用aspect来声明切面 -->
<aop:aspect ref="audience">
<!-- 为通知传递参数 -->
<aop:pointcut id="trackPlayed"
expression="execution(* qian.aspectj.Performance.playTrack(int)) and args(trackNumber)"/>
<aop:before method="countTrack" pointcut-ref="trackPlayed"/>
</aop:aspect>
<!-- 引入新功能 -->
<aop:aspect>
<aop:declare-parents
types-matching="qian.aspectj.Performance+"
implement-interface="qian.aspectj.HelpPerformance"
default-impl="qian.aspectj.HelpPerformanceImpl"/>
</aop:aspect>
</aop:config>
使用场景的区别:
< aop:advisor>大多用于事务管理。
例如在Spring + Hibernate 中使用的声明式事物;
配置事务的转播特性
<!--Step 3: 配置事物属性 ,需要事物管理器-->
<!-- **myTxAdvice** -->
<!-- step 2 中的myTransactionManager名称 -->
<tx:advice id="myTxAdvice" transaction-manager="myTransactionManager">
<tx:attributes>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<tx:method name="modify*" propagation="REQUIRED"/>
<tx:method name="find*" propagation="REQUIRED" read-only="true"/>
</tx:attributes>
</tx:advice>
配置事务的切点,并将事务属性和切点管理起来
<!--Step 4: 配置事物切点,并把事物属性和切点关联起来 -->
<aop:config>
<aop:pointcut expression="execution(* com.spring.trans.manager.*.*(..))" id="myPointcut"/>
<!-- step 3: myTxAdvice -->
<aop:advisor advice-ref="myTxAdvice" pointcut-ref="myPointcut"/>
</aop:config>
至此 面向切面的Spring 的基础内容完结;