《Spring实战》读书笔记3-SpringAOP

1. 参考

2. AOP术语

  1. 通知(active)

    切面的工作被称为通知,比如抄表员去每家每户抄电表的数据,这个动作(活动)就是通知

    通知一共包含以下五种:

    • 前置通知(Before):在目标方法被调用之前调用通知功能;
    • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
    • 返回通知(After-returning):在目标方法成功执行之后调用通知;
    • 异常通知(After-throwing):在目标方法抛出异常后调用通知;
    • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
  1. 连接点(Join point)

    可以被插入的时机 就是连接点。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

  2. 切点(Poincut)

    如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了 “何处”。切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。有些AOP框架允许我们创建动态的切点,可以根据运行时的决策(比如方法的参数值)来决定是否应用通知。

  3. 切面(Aspect)

    当抄表员开始一天的工作时,他知道自己要做的事情(报告用电量)和从哪些房屋收集信息。因此,他知道要完成工作所需要的一切东西。

    切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。

  4. 引入(Introduction)

    引入允许我们向现有的类添加新方法或属性 。例如,我们可以创建一个Auditable通知类,该类记录了对象最后一次修改时的状态。这很简单,只需一个方法,setLastModified(Date),和一个实例变量来保存这个状态。然后,这个新方法和实例变量就可以被引入到现有的类中,从而可以在无需修改这些现有的类的情况下,让它们具有新的行为和状态。

  5. 织入(Weaving)

    织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:

    • 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
    • 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面。
    • 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。

在AOP术语中这些关注点被称为横切关注点(crosscutting)。如下图所示:

通知(Advice)的类型:

类型 执行点
Before 在主方法调用之前执行
After 通知在主方法完成之后执行,不管主方法的调用结果如何
After-returnning 通知在主方法正常返回后执行。比如在不抛出异常时正常返回
After-throwing 通知在主方法抛出异常后执行
Around 通知包装了主方法,提供在方法调用一直或之后提供一些功能

3. 代码示例

首先,我们定义被代理的接口和实现:

清单1. 被代理接口和实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// MyBean.java
package org.archerie.aop.bean;

public interface MyBean {

void sayHello(String msg);
}

// MyBeanImpl.java
package org.archerie.aop.bean;

import org.springframework.stereotype.Component;

// 使用spring @Component注解,加载到spring上下文中
@Component
public class MyBeanImpl implements MyBean {

public void sayHello(String msg) {
System.out.println(msg);
}
}

接下来,我们就开始定义我们的切面。如下:

清单2. 切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package org.archerie.aop.aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class AdviceExampleAspect {

// 定义切点
@Pointcut("execution(** org.archerie..*Bean.*(..))")
public void beanPointCut() {}

@Before("beanPointCut()")
public void silenceCellPhone() {
System.out.println("手机静音!");
}

@Before("execution(** org.archerie..MyBean.sayHello(String)) && args(msg)")
public void printMsg(String msg) {
System.out.println("MyBean将要说的是:" + msg);
}

@After("beanPointCut()")
public void applause() {
System.out.println("鼓掌!鼓掌!");
}

}

现在,我们就需要定义我们的spring配置文件了。我们选择Java配置的方式来配置Spring。

清单3. Spring配置

1
2
3
4
5
6
7
8
9
10
11
12
package org.archerie.aop.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = {"org.archerie.aop"})
public class AopJavaConfig {

}

以上,切面就准备好了,现在就差使用了。下面,使用JUnit来进行测试:

清单4. JUnit测试切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package org.archerie.aop;

import org.archerie.aop.bean.MyBean;
import org.archerie.aop.bean.MyOtherBean;
import org.archerie.aop.config.AopJavaConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AopJavaConfig.class)
public class AspectTest {

@Autowired
MyBean myBean;

@Autowired
MyOtherBean otherBean;

@Test
public void testAspect() {
myBean.sayHello("我要把你做成一个玩偶!");
}
}

以上,就是完成的代码示例。现在运行查看结果:

MyBean将要说的是:我要把你做成一个玩偶!
手机静音!
我要把你做成一个玩偶!
鼓掌!鼓掌!

4. 术语详解

Spring AOP主要由连接点切入点通知组成切面。而切入点是用来选择某一范围的连接点的,所以我们首先讨论如何定义切入点。这里要说明一点的就是:Spring AOP只支持一种Join point,就是方法的执行。所以,以下切入点(Pointcut)只能选取方法执行的连接点(Join Point)。看我::happy:

4.1. 切入点(Pointcuts)

以下是SpringAOP支持的AspectJ切入点:

AspectJ表达式 描述
execution 匹配方法执行的连接点,这是使用Spring AOP时主要使用的切入点
within 匹配特定类型中的连接点(在Spring AOP中则限制为匹配类型中的方法执行)
this 匹配Spring AOP代理对象中的连接点(在Spring AOP中为方法的执行),注意匹配的是spring aop代理对象为指定的类型。this表达式必须使用完整的限定类名,不能使用通配符。
target 匹配目标对象中的连接点(在Spring AOP中为方法的执行),但是目标对象得为特定的类型。target表达式必须使用完整的限定类名,不能使用通配符。
args 匹配参数为特定类型实例的连接点(Spring AOP中为方法的执行)
@target 匹配特定的连接点(Spring AOP中为方法执行),执行方法的类拥有指定类型的注解
@args 匹配特定的连接点(Spring AOP中为方法的执行),运行时传入的参数必须拥有特定类型的注解
@within 用于匹配拥有特定注解的类型中的连接点
@annotation 用于匹配拥有特定注解的连接点(Spring AOP中为方法的执行)
bean Spring AOP扩展的切入点,可以匹配特定的类名中的连接点(方法的执行)
4.1.1 通配符(Wildcards)

在使用切入点表达式的时候,有些时候我们可以使用通配符:*、..、+。

符号 含义
.. 在类型匹配时,匹配任何以 .,以 . 结尾的包名。在方法定义时匹配任意数量的参数。
+ 匹配给定类型的任意子类型。
* 匹配数量的任意字符,除了*字符。
4.1.2 类型(Type)指示符

通过类型来过滤方法,比如接口类名或者是包名。Spring提供within切入点,使用方式如下。type name可以被替换为package name或者class name

1
within(<type name>)

以下是一些例子:

  • within(com.xyz.web..*):匹配com.xyz.web包下面的所有类中方法的执行,而且因为使用了 .. 通配符,所以可以匹配com.xyz.web的所有子包。*通配符匹配所有的类名,所以可以匹配所有类中方法的执行。
  • with(com.xyz.web.*):匹配com.xyz.web包下面所有类中方法的执行。因为没有使用..通配符,所有只是匹配到web包,不包括子包。*一样匹配所有的类名。
  • with(com.xyz.service.AccountService):匹配AccountService类下面所有方法的执行。
  • with(com.xyz.interface.MyServiceInterface+):匹配所有实现了MyServiceInterface接口的类中的所有方法的执行。
  • with(com.xyz.service.MyBaseService+):匹配MyBaseService类和它的子类。
4.1.3 方法(Method)指示符

匹配特定方法的执行,可以使用execution关键字。execution表达式的格式如下:

1
2
3
execution(modifiers-pattern? ret-type-pattern 
declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)123

其中,所有的部分除了返回值类型(ret-type-pattern)、方法名(name-pattern)和参数(param-pattern)都是可选的。 修饰符(modifiers-pattern) publicprotectedprivate,也可以使用*匹配所有的修饰符。返回值类型(ret-type-pattern) 匹配特定的返回类型。大多数情况都是使用*通配符匹配所有的返回类型。方法名(name-pattern) 匹配执行方法的名称,可以使用*通配符匹配任意数量够的字符。如果要匹配特定类中方法的执行,就必须指定 类名(declaring-type-pattern) 部分,这部分使用的格式参考 4.1.2 类型指示符参数列表(param-pattern) 部分,指定方法的参数必须满足的格式。()匹配没有参数的方法,(..)匹配任意数量的参数。当然你也可以使用*匹配任意一个参数的类型,比如(\*, String)匹配第二个参数为String类型,第一个参数为任意类型的情况。异常列表(throws-pattern) 匹配全限定类名异常类型,如果有多个异常,使用,分割,比如throws java.lang.IllegalArgumentException, java.lang.ArrayIndexOutOfBoundsException

示例如下:

  • execution(public * *(..)):匹配所有的public方法的执行。
  • execution(* set*(..)):匹配所有方法名以set开头的方法的执行。
  • execution(* com.xyz.service.AccountService.*(..)):匹配AccountService接口下所有方法的执行。
  • execution(* com.xyz.service.*.*(..)):匹配包com.xyz.service下所有类(或接口)下的所有方法的执行。
  • execution(* com.xyz.service..*.*(..)):匹配包com.xyz.service和其子包中的类(或接口)下的所有方法的执行。
  • execution(* *(.., String):匹配所有最后一个参数为String的方法的执行。
  • execution(* ..Sample+.sampleGenericCollectionMethod(*)):匹配任意以.Sample结尾的包,以及其子包中的sampleGenericCollectionMethod方法的执行,且具有唯一的任意类型的参数。
  • execution(* *(*, String, ..):匹配第一个参数为任意类型,第二个参数为String,后面可拥有任意个参数的方法的执行。
4.1.4 其他的切入点指示符
  • bean(*Service):所有bean名称以Service结尾的bean。
  • @annotation(org.springframework.transaction.annotation.Transactional):匹配所有连接点(Spring AOP中方法的执行)拥有@Transaction注解在方法上。
  • this(com.xyz.service.AccountService):匹配实现了AccountService接口的代理类中的所有连接点(Spring AOP中方法的执行)
  • target(com.xyz.service.AccountService):匹配实现了AccountService接口的目标类的所有连接点(Spring AOP中方法的执行)
4.1.5 组合多个切入点

很多时候可能一个切入点并不能满足我们的需求,这时候就需要组合使用切入点来限制匹配的切入点。在Spring AOP中可以使用and(&&)、or(||)和not(!)。既可以使用文字的形式也可以使用符号的形式。比如 execution(* concert.Performance.perform(..)) && within(concert.*))

4.2 定义切面(@Aspect)

现在,我们可以使用我们学到的来定义切面了。在Spring中可以使用XML和注解的方式来定义切面,本文将只讨论使用注解定义切面的方式。Spring支持使用AspectJ的注解@Aspect来定义切面,就像清单2所使用的那样。但是,如果我们要让Spring知道我们定义了一个切面的话,还必须把这个切面声明为一个Bean。所以我们使用@Component注解,这样Spring就会识别它,并把它当做切面来看待了。

1
2
3
@Component
@Aspect
public class AdviceExampleAspect {}

4.3 定义切点(@Pointcut)

切点(Pointcut)定义使用@Pointcut注解。注解中我们就可以使用4.1 切点中的表达式来选择连接点。定义切点时,可以组合多个切点。

1
2
3
4
5
6
7
8
9
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}

@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}

// 组合切点
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}

4.4 定义通知

清单2中我们已经定义了@Before@After通知,也可以使用其他类型的通知。这里主要介绍如何使用@Around注解,定义 环绕(Around) 通知类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package org.archerie.aop.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class AdviceAroundAspect {

@Pointcut("execution(** org.archerie..*Bean.*(..))")
public void beanPointCut() {}

@Around("beanPointCut()")
public void watchBean(ProceedingJoinPoint jp) {
try {
System.out.println("手机静音!");
jp.proceed();
System.out.println("鼓掌!鼓掌!");
} catch (Throwable e) {
System.out.println("投诉!投诉!");
}
}
}

在Around通知类型中,我们可以定义具体方法运行前或者后的逻辑。

0%