Spring 框架配置使用指南

本篇用于记录 Spring 框架的配置使用方式,持续更新…

当前支持版本:4.x

一些需要注意的知识点:

  • BeanFactory 在初始化容器时并未实例化 bean,直到第一次访问时才真正实例化对应的 bean,而 ApplicationContext 则在初始化上下文时就完成所有 singleton bean 的实例化过程。

一. IoC 使用指南

1.1 基于 XML 的配置

1.1.1 bean的基本配置
1
<bean id="my-bean" name="myBean, myBean1, myBean2" class="org.zhenchao.bean.MyBean" />

id 的命名需要遵循 XML 对 id 的命名规范,但是 name 则相对要求不是那么严格,可以使用特殊字符。id 和 name 都可以指定多个名字,名字之间以 逗号、分号,或空格 进行分隔。id 必须是唯一的,但是 name 可以重复,如果多个 bean 配置了相同的 name,则以最后一个为准。

1.1.2 三种依赖注入方式

属性注入、构造方法注入,以及工厂方法注入。

  • 属性注入

即 setter 方法注入,配置如下:

1
2
3
<property name="age" value="26"/>
<property name="phone" value="18512345678"/>
<property name="email" value="zhenchao.wang@gmail.com"/>

注意:属性命名时,前两个字母要么全部大写,要么全部小写。 否则可能会抛出 org.springframework.beans.NotWritablePropertyException

  • 构造方法注入
1
2
3
4
<!--参数配置顺序并不能决定在构造方法中的匹配顺序-->
<constructor-arg index="0" name="id" type="long" value="100001"/>
<constructor-arg index="1" name="username" type="java.lang.String" value="zhenchao"/>
<constructor-arg index="2" name="password" type="java.lang.String" value="123456"/>

配置顺序与构造方法中参数定义顺序没有直接关系,所以大部分时候都可能会映射错误,这个时候我们可以凭借 index 标签和 type 标签从两个维度上做唯一性限制。

  • 工厂方法注入

工厂方法注入分为两种情况:非静态工厂和静态工厂。配置如下:

1
2
3
4
5
6
<!--非静态工厂注入-->
<bean id="my-bean-simple-factory" class="org.zhenchao.factory.MyBeanSimpleFactory"/>
<bean id="my-bean-1" factory-bean="my-bean-simple-factory" factory-method="create"/>

<!--静态工厂注入-->
<bean id="my-bean-2" class="org.zhenchao.factory.MyBeanStaticFactory" factory-method="create"/>

由上面可以看出非静态工厂需要先将工厂类定义成 bean,然后利用 factory-bean 标签引用进来,而静态工厂则直接以 class 标签声明,返回的 bean 就是目标对象。

1.1.3 注入参数说明
  • 注入参数包含特殊符号

如果注入参数携带特殊符号,比如 <script src="js/jquery-3.1.0.min.js"></script>,会破坏 XML 的结构,导致配置错误,这种情况下如果不希望对输入参数进行转义,可以采用 <![CDATA[]]> 标签,其作用是让 XML 解析器将标签内的字符串当做普通文本处理,如下:

1
2
3
4
<property name="shell">
<!--如果不希望转义特殊字符,则可以使用<![CDATA[]]>-->
<value><![CDATA[<script src="js/jquery-3.1.0.min.js"></script>]]></value>
</property>
  • 引用其他 Bean
1
2
3
4
5
<property name="refBean">
<!--<ref bean="ref-bean"/>-->
<ref local="ref-bean"/>
<!--<ref parent="ref-bean"/>-->
</property>

<ref bean="ref-bean"/> 表示可以引用同一容器或父容器中的 bean,这是最常用的形式。

<ref local="ref-bean"/> 表示只能引用同一配置文件中定义的 bean。

<ref parent="ref-bean"/> 表示引用父容器中的 bean。

  • 内部 Bean

内部 Bean 即使提供了 id、name、scope 属性,也会被忽略,scope 默认为 prototype 类型。

1
2
3
4
<property name="innerRefBean">
<!-- 内部bean,其id、name、scope属性配置会被忽略,scope默认为prototype -->
<bean class="org.zhenchao.bean.MyRefBean"/>
</property>
  • null 值
1
2
3
<property name="nill">
<value></value>
</property>

如果按照上述配置,nill 的值会被赋值空字符串,如果希望给 nill 赋值为 null,则需要如下配置:

1
2
3
4
<property name="nill">
<!-- null值注入,不然会默认注入空字符串 -->
<null/>
</property>
  • 级联属性

如果 bean 包含的属性是一个对象,那么可以直接通过级联属性配置对该对象的属性赋值:

1
2
<!--级联属性-->
<property name="refBean.name" value="cascade-property"/>
  • 集合属性
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
<property name="list">
<list>
<value>"aaa"</value>
<value>"bbb"</value>
<value>"ccc"</value>
</list>
</property>
<property name="set">
<set merge="true"> <!--集合合并,合pTagrent-bean中同名属性-->
<value>"111"</value>
<value>"222"</value>
<value>"333"</value>
</set>
</property>
<property name="map">
<map>
<entry key="key1" value="value1"/>
<entry key="key2" value="value2"/>
<entry key="key3" value="value3"/>
</map>
</property>
<property name="prop">
<props>
<prop key="key1">"value1"</prop>
<prop key="key2">"value2"</prop>
<prop key="key3">"value3"</prop>
</props>
</property>
  • 集合合并
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!--父 bean-->
<bean id="my-parent-bean" class="org.zhenchao.bean.MyParentBean" p:tag="ppp">
<property name="set">
<set>
<value>"000"</value>
<value>"111"</value>
<value>"222"</value>
</set>
</property>
</bean>

<!--子 bean-->
<bean id="my-bean" name="myBean" class="org.zhenchao.bean.MyBean" parent="my-parent-bean">
<property name="set">
<set merge="true"> <!--集合合并,合pTagrent-bean中同名属性-->
<value>"111"</value>
<value>"222"</value>
<value>"333"</value>
</set>
</property>
</bean>

集合合并是指按照上面的配置,子 bean 的 set 属性会合并父 bean 的 set 属性集合。

  • util 命名空间
1
2
3
4
5
6
7
8
9
10
11
12
13
<!--util命名空间配置集合-->
<util:list id="my-list" list-class="java.util.ArrayList" value-type="java.lang.Integer">
<value>1001</value>
<value>1002</value>
</util:list>
<util:set id="my-set" set-class="java.util.HashSet" value-type="java.lang.Long">
<value>123</value>
<value>234</value>
</util:set>
<util:map id="my-map" map-class="java.util.HashMap" key-type="java.lang.String" value-type="java.lang.Integer">
<entry key="aaa" value="100"/>
<entry key="bbb" value="200"/>
</util:map>

util 命名空间不是对于 bean 属性的配置,而是独立的声明 util 集合对象。

  • p 命名空间

p 命名空间可以简化属性的配置,配置示例如下:

1
2
<!--采用p标签设置属性-->
<bean id="ref-bean" class="org.zhenchao.bean.MyRefBean" p:name="my-ref-bean"/>
1.1.4 自动装配

Spring 支持的自动装配类型:

  1. byName,根据名称自动装配,假设 bean A 有一个名为 b 的属性,如果容器中刚好存在一个 bean 的名称为 b,则将该 bean 装配给 bean A 的 b 属性。
  2. byType,根据类型自动匹配,假设 bean A 有一个类型为 B 的属性,如果容器中刚好有一个 B 类型的 bean,则使用该 bean 装配 A 的对应属性。
  3. constructor,仅针对构造方法注入而言,类似于 byType,如果 bean A 有一个构造方法,构造方法包含一个 B 类型的入参,如果容器中有一个 B 类型的 bean,则使用该 bean 作为入参,如果找不到,则抛出异常。
  4. autodetect,根据 bean 的自省机制决定采用 byType 还是 constructor 进行自动装配,如果 bean 提供了默认的构造函数,则采用 byType,否则采用 constructor。

<beans/> 元素标签中的 default-autowire 属性可以配置全局自动匹配,default-autowire 默认值为 no,表示不启用自动装配。在实际开发中,XML 配置方式很少启用自动装配功能,而基于注解的配置方式默认采用 byType 自动装配策略。

1.1.5 方法注入
  • lookup-method

lookup 方法注入一般用于希望在一个 singleton bean 中获取一个 prototype bean 对象的场景,加入我们在一个 singleton bean 中寄希望每次调用 get 方法时获取一个 prototype bean 的新对象,因为外围 bean 是 singleton 的,其属性也都只有一份,所以不能每次都返回 prototype bean 类型属性的新对象,这个时候我们就可以使用 lookup-method 标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyBean {

private MyPrototypeBean prototypeBean;

public MyPrototypeBean getPrototypeBean() {
return this.prototypeBean;
}

public MyBean setPrototypeBean(MyPrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
return this;
}
}

MyBean 为 singleton bean,虽然 MyPrototypeBean 是 prototype 的,但是我们每次调用 getPrototypeBean() 仍然是返回同一个对象,这个时候就可以使用 lookup-method 标签:

1
2
<!--lookup-method-->
<lookup-method name="getPrototypeBean" bean="my-prototype-bean"/>

该标签让 getPrototypeBean() 方法每次都会去调用一遍 prototype bean,从而每次都生成新对象。

  • replaced-method

replaced-method 如其字面意思一样,可以替换一个方法的实现,如果希望替换 MyBean 的如下方法:

1
2
3
public void myOriginMethod() {
System.out.println("call my bean method");
}

我们首先需要定义一个实现了 org.springframework.beans.factory.support.MethodReplacer 接口的 bean,并实现 reimplement(Object obj, Method method, Object[] args) 方法,如下:

1
2
3
4
5
6
7
8
9
public class MyReplacedBean implements MethodReplacer {

@Override
public Object reimplement(Object obj, Method method, Object[] args) throws Throwable {
System.out.println("call replaced method");
return obj;
}

}

然后利用 replaced-method 配置:

1
2
3
<bean id="my-replacer" class="org.zhenchao.bean.MyReplacedBean"/>
<!--replaced-method-->
<replaced-method name="myOriginMethod" replacer="my-replacer"/>

这样在调用 myBean 的 myOriginMethod() 方法时,本质上是在调用 MethodReplacer 的 reimplement(Object obj, Method method, Object[] args) 方法。

1.1.6 bean 之间的关系
  • 继承

如果多个 bean 在配置上存在大量的重复,这个时候就可以考虑使用继承的配置,抽象出重复的属性配置在父 bean 中,而子 bean 则配置特有的属性,如下:

1
2
3
<bean name="myAbstractBean" class="org.zhenchao.bean.MyInheritBean" abstract="true" p:name="y450" p:price="4999"/>
<bean name="whitePc" parent="myAbstractBean" p:color="white"/>
<bean name="blackPc" parent="myAbstractBean" p:color="black"/>

通过 abstract="true" 将父 bean 置为抽象,然后在子 bean 中利用 parent 进行引用。

  • 依赖:depended-on

如果某个 bean 需要直接依赖于另外一个 bean,那么可以在 bean 中 ref 另外一个 bean,对于间接依赖来说,就不可以这样配置,这个时候我们可以利用 depended-on 来进行配置,如下:

1
2
<bean name="dpBeanA" class="org.zhenchao.bean.A" depends-on="dpBeanB"/>
<bean name="dpBeanB" class="org.zhenchao.bean.B"/>

A 可以不直接依赖于 B,但是利用 depended-on 标签可以让 B 先于 A 实例化,如果前置依赖于多个 bean,则可以通过 逗号、空格,或分号 进行分隔。

  • 引用:idref

Spring 提供了 idref 标签来引用一个bean,相对 ref 来说,Spring 在容器启动时就会检查引用的正确性,配置如下:

1
2
3
4
5
6
7
8
9
10
<!-- ref引用 -->
<!--<bean name="refBeanA" class="org.zhenchao.bean.A" p:b-ref="refBeanB"/>
<bean name="refBeanB" class="org.zhenchao.bean.B"/>-->
<!-- idref引用 -->
<bean name="refBeanA" class="org.zhenchao.bean.A">
<property name="b">
<idref bean="b"/>
</property>
</bean>
<bean id="b" class="org.zhenchao.bean.B"/>
1.1.7 Bean 的作用域
类型 应用场景 说明
singleton 普通 单例模式
prototype 普通 每次 getBean 时都返回一个新的实例
request WEB 每次 HTTP 请求时都会创建一个新的实例,仅适用于 WebApplicationContext
session WEB 同一个 Session 共享一个 Bean,仅适用于 WebApplicationContext
globalSession WEB 同一个全局 Session 共享一个 Bean,一般用于 Porlet 应用环境,仅适用于 WebApplicationContext

如果在 singleton 或 prototype bean 中引用了 request、session,或 globalSession 作用域的 bean,则需要做一些额外配置,因为后面三个是需要依赖于 WEB 环境的,仍然按照一般的方法引用,可能得到的对象不是我们所期望的,这个时候就需要依赖于Spring AOP,如下:

1
2
3
4
<bean id="my-request-bean" class="org.zhenchao.bean.MyRequestBean" scope="request">
<!-- 创建动态代理 -->
<aop:scoped-proxy>
</bean>

即在 request、session,或 globalSession 作用域的 bean 配置中添加 <aop:scoped-proxy/>,依赖于动态代理技术,当我们在调用引用了这些作用域 bean 的 singleton 或 prototype bean 时,可以得到正确作用域的对象。

1.1.8 FactoryBean

有时候采用配置的方式实例化一个 bean 可能会比较复杂,这个时候采用编码的方式可能是更好的选择,在这种情况下我们可以借助 org.springframework.beans.factory.FactoryBean<T> 来实现。该接口包含三个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface FactoryBean<T> {

/**
* 获取由 FactoryBean 创建的目标 bean 实例
*/
T getObject() throws Exception;

/**
* 返回目标 bean 类型
*/
Class<?> getObjectType();

/**
* 是否是单实例
*/
boolean isSingleton();

}

这里我们用 FactoryBean 来创建 MyBean 对象,首先我们需要实现一个创建目标实例的 MyFactoryBean,如下:

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
33
public class MyFactoryBean implements FactoryBean<MyBean> {

private String info;

@Override
public MyBean getObject() throws Exception {
MyBean myBean = new MyBean();
if (StringUtils.isNotBlank(info)) {
String[] elements = info.split(",");
myBean = new MyBean(Long.valueOf(elements[0]), elements[1], elements[2]);
}
return myBean;
}

@Override
public Class<?> getObjectType() {
return MyBean.class;
}

@Override
public boolean isSingleton() {
return false;
}

public String getInfo() {
return info;
}

public MyFactoryBean setInfo(String info) {
this.info = info;
return this;
}
}

配置如下:

1
<bean id="my-factory-bean" class="org.zhenchao.bean.MyFactoryBean" p:info="10001,zhenchao,123456"/>

我们 getBean("my-factory-bean") 返回的不是 MyFactoryBean 对象,而是 MyBean 对象,如果我们希望获取 MyFactoryBean 对象,则可以在获取的名字前面加 “&” ,即 getBean("&my-factory-bean"),UT 测试:

1
2
3
Assert.assertTrue(beanFactory.getBean("my-factory-bean") instanceof MyBean);  // true
Assert.assertFalse(beanFactory.getBean("&my-factory-bean") instanceof MyBean); // false
Assert.assertTrue(beanFactory.getBean("&my-factory-bean") instanceof FactoryBean); // true

1.2 基于注解的配置

1.2.1 Bean 的基本配置

Spring 提供了注解 @Component 来以注解的方式配置 bean,并提供 @Scope 来指定 bean 的作用域,@Component 注解定义如下:

1
2
3
4
5
6
7
8
9
10
11
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Component {

/**
* bean name
*/
String value() default "";

}

比如如下配置等效于 <bean id="a" class="org.zhenchao.bean.A"/>

1
2
3
@Scope("prototype")
@Component("a")
public class A { }

除了 @Component,Spring 还提供了 @Repository@Service,以及 @Controller,这些是 @Component 的特殊化注解,分别用于注解 DAO 层,Service 层,以及控制层,推荐使用后者。

我们需要配置扫描策略来对配置的注解进行扫描,即 <context:component-scan/>,并且可以在子命名空间下通过 <context:include-filter/><context:exclude-filter/> 来 include 和 exclude 扫描策略,component-scan 先 exclude 所有的黑名单,再 include 需要的白名单,这里有一个需要注意的配置项:use-default-filters,默认值为 true,表示会对 @Component@Repository@Service,以及 @Controller 注解的 bean 进行扫描,比如:

1
2
3
<context:component-scan base-package="org.zhenchao">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

这里因为 use-default-filters=true,所以 include 是不起作用的,需要将 use-default-filters 设置为 false 才生效:

1
2
3
<context:component-scan base-package="org.zhenchao" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
1.2.2 自动装配
  • @Authwired@Qualifier

Authwired 默认以 byType 匹配方式在容器中查找对应的 bean,如果找不到的话会抛 NoSuchBeanDefinitionException 异常,如果即使在找不到的情况下也不希望抛异常,可以设置 @Authwired(required=false)

如果容器中存在多个相同类型的 bean 时,我们可以采用 @Qualifier 来明确指定需要哪个 name 的 bean,比如:

1
2
3
4
5
@Component()
public class A {
@Autowired @Qualifier("b") // 明确指定使用name=b的bean
private B b;
}
  • @Resource@Inject

这两个注解的功能基本等同于 @Authwired,只不过是为了兼容 JSR 标准,@Resource 采用 byName 的方式注入,如果没有指定 name,则以标注的变量名或方法名作为 bean 的名称,而 @Inject 则采用 byType,类似于 @Authwired,只是没有 required 属性。一般推荐使用 @Authwired 注解,因为其功能更加强大一些。

1.2.3 生命过程

对于配置中的 init-method 和 destory-method 属性,Spring 也提供了相应的注解支持,分别是 @PostConstruct@PreDestroy,可以在一个 bean 中定义多个 @PostConstruct@PreDestroy 方法。

1.3 基于 java 类的配置

1.4 基于 Groovy DSL 的配置

二. 基于配置的 AOP 使用指南

Spring AOP 基于动态代理技术实现,采用了 jdk 原生的动态代理技术和 CGLib 的动态代理技术,后者相对于前者具备更高的运行效率,但缺点是后者在创建代理对象时需要更大的开销,所以 CGLib 一般适用于代理 singleton 对象。

2.1 基本概念

2.1.1 术语定义
  • 连接点(Joinpoint)

传统 AOP 对于连接点的定义比较宽泛,只要是具备边界性质的地方都可以作为连接点,比如类初始化前、类初始化后、方法调用前、方法调用后、方法抛出异常后等等。Spring AOP 目前仅支持方法级别的连接点,只能在方法调用前、方法调用后、方法抛出异常时,以及方法调用前后进行织入增强。一个连接点包含 执行点方位 两个属性,执行点就是具体的方法,而方位则表示前后信息。

  • 切点(Pointcut)

切点可以看做织入操作具体的织入点,连接点是客观存在的事物,而切点则是与织入规则相关的,可以看做是筛选连接点的查询条件,一个切点可以对应多个连接点,切点加上方位信息对应一个具体的连接点。

  • 增强(Advice)

增强是织入目标类连接点上的一段具体的程序代码,如果不使用 AOP,那么如果希望达到相同的效果,则增强部分的代码需要编写入具体的业务方法中。

  • 目标对象(Target)

增强织入的目标类,通过 AOP 的方式来往目标对象中切入增强逻辑。

  • 引介(Introduction)

引介是一种特殊的增强,它为类添加一些属性和方法

  • 织入(Weaving)

织入是将增强逻辑添加到目标对象上的过程,AOP 定义了三种织入方式:1)编译期织入;2)类装载期织入;3)动态代理织入。Spring 采用动态代理织入,而 AspectJ 则采用编译期和类装载器织入。

  • 代理(Proxy)

增强操作后得到的类可以看做是目标对象的一个代理,代理可以是与目标对象具备相同接口的类,也可以是目标对象的子类。

  • 切面(Aspect)

切面由切点和增强两个维度构成。

2.1.2 增强类型
  1. 前置增强
  2. 后置增强
  3. 环绕增强
  4. 异常抛出增强
  5. 引介增强

2.2 增强基本示例

这里以一般的切面类型来演示增强的使用,一般切面的缺点在于会对所有目标类的方法生效,不过这里我们关注的重点是各类增强的效果,所以暂时先不需要介意这些。首先定义我们的目标对象:

1
2
3
public interface AbstractAopBean {
void sayHello();
}
1
2
3
4
5
6
7
8
public class MyAopBean implements AbstractAopBean {

@Override
public void sayHello() {
System.out.println("hello, spring aop.");
}

}
2.2.1 前置增强

自定义前置增强需要实现 org.springframework.aop.MethodBeforeAdvice 接口,假设我们定义了自定义的前置增强如下:

1
2
3
4
5
6
7
8
public class MyMethodBeforeAdvice implements MethodBeforeAdvice {

@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("my method before advice, method name : " + method.getName());
}

}

如果以编码的方式调用,则我们需要借助于 ProxyFactory 类,调用方式如下:

1
2
3
4
5
6
7
MyAopBean myBean = new MyAopBean(); // 目标对象
MethodBeforeAdvice beforeAdvice = new MyMethodBeforeAdvice(); // 前置增强
ProxyFactory pf = new ProxyFactory(); // 代理工厂
pf.setTarget(myBean);
pf.addAdvice(beforeAdvice);
MyAopBean proxy = (MyAopBean) pf.getProxy();
proxy.sayHello();

而配置的方式则分为基于 jdk 原生动态代理和 CGLib 两种方式,前面我们也简单比较过二者的区别,而在这里配置上的区别主要源于 jdk 原生动态代理是基于接口的,所以代理类必须实现一个接口,配置如下:

1
2
3
4
5
6
<!--基于jdk原生动态代理的前置增强配置-->
<bean id="myBean" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="org.zhenchao.bean.AbstractAopBean"/> <!--因为基于JDK原生的动态代理,所以这里需要配置接口-->
<property name="interceptorNames" value="my-before-advice"/>
<property name="target" ref="my-aop-bean"/>
</bean>

而如果我们在配置时候,设置 proxyTargetClass=true,则可以不需要配置接口,因为这个使用的是基于 CGlib 的动态代理:

1
2
3
4
5
6
<!--基于CGLib动态代理的前置增强配置-->
<bean id="myBean2" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interceptorNames" value="my-before-advice"/>
<property name="target" ref="my-aop-bean"/>
<property name="proxyTargetClass" value="true"/>
</bean>

上述配置都是基于 ProxyFactoryBean,我们来看一下这个类几个常用配置属性:

  • target:目标对象
  • proxyInterfaces:目标对象所实现的接口,可以是多个
  • interceptorNames:目标对象 bean 名称列表,可以是多个
  • singleton:返回的代理对象是否是单例,默认为单例
  • optimize:为 true 时表示强制使用 CGLib 动态代理
  • proxyTargetClass:是否对类进行代理,而不是接口,为 true 时使用 CGLib 动态代理
2.2.2 后置增强

自定义后置增强则需要实现 org.springframework.aop.MethodBeforeAdvice 接口,如下:

1
2
3
4
5
6
7
public class MyMethodAfterAdvice implements AfterReturningAdvice {

@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("my method after advice, method name : " + method.getName());
}
}

而配置上则与前置增强相同,不再重复撰述。

2.2.3 环绕增强

环绕增强,顾名思义就是围绕在一个方法的前后,自定义环绕增强需要实现 org.aopalliance.intercept.MethodInterceptor 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyMethodEncircleAdvice implements MethodInterceptor {

@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
String methodName = methodInvocation.getMethod().getName();
// 前置环绕
System.out.println("my method encircle advice before, method name : " + methodName);
// 以反射的方式调用目标方法
Object obj = methodInvocation.proceed();
// 后置环绕
System.out.println("my method encircle advice after, method name : " + methodName);
return obj;
}

}

我们需要在实现的 invoke 方法中通过反射的方式调用目标方法,并在调用前后实现自己的环绕逻辑,配置方式相同。

2.2.4 异常抛出增强

异常抛出增强在方法抛出异常时触发,自定义异常抛出增强需要实现 org.springframework.aop.ThrowsAdvice 接口,需要注意的是,这是一个标记接口,没有生命任何方法,主要是考虑到异常抛出有多种类型,所以将方法的定义交给开发者,这样开发者就可以按照异常类型来重载多个方法,不过方法的定义的方法签名需要满足:

void afterThrowing(Method method, Object[] args, Object target, Throwable e)

其中 Method method, Object[] args, Object target 这三个参数要么全有,要么全无,而最后一个参数则可以重载为各种异常类型,一个自定义的异常增强类示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyExceptionAdvice implements ThrowsAdvice {

/**
* 处理 SQLException
*/
public void afterThrowing(Method method, Object[] args, Object target, SQLException e) {
System.out.println("after throwing invoke sql exception");
}

/**
* 处理 RunTimeException
*/
public void afterThrowing(Method method, Object[] args, Object target, RuntimeException e) {
System.out.println("after throwing invoke run time exception");
}

}

配置上相同,不再重复撰述。

2.2.5 引介增强

引介增强不同于前面介绍的 4 种增强,前面所述的增强都是方法级别的,围绕方法进行织入,而引介增强则是类级别的,用于为目标类创建新的属性和方法。Spring 通过 IntroductionInterceptor 提供引介增强支持,不过这是一个标记接口,Spring 提供了 DelegatingIntroductionInterceptor 实现类,一般我们只需要继承该实现类即可。

假设我们现在有一个目标类,我们希望通过引介增强在不更改目标类实现的基础上实现一个接口,用于实现日志打印的开关功能,我们先定义一个接口:

1
2
3
4
5
6
public interface LoggerEnable {
/**
* 控制日志打印的开关
*/
void enable(boolean enable);
}

然后定义引介增强类,需要继承 DelegatingIntroductionInterceptor 类,并实现 LoggerEnable 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyIntroductionAdvice extends DelegatingIntroductionInterceptor implements LoggerEnable {

private ThreadLocal<Boolean> enable = new ThreadLocal<>();

@Override
public void enable(boolean enable) {
this.enable.set(enable);
}

@Override
public Object invoke(MethodInvocation mi) throws Throwable {
if (null != enable.get() && enable.get()) {
System.out.println("my introduction advice, enable logger");
}
return super.invoke(mi);
}
}

我们在自定义的引介增强中实现了接口中定义的方法,并基于该方法来对我们目标类中的方法进行增强,相关配置如下:

1
2
3
4
5
6
7
8
9
<!--引介增强-->
<bean id="my-introduction-advice" class="org.zhenchao.advice.MyIntroductionAdvice"/>

<bean id="myBean3" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interfaces" value="org.zhenchao.bean.LoggerEnable"/> <!--引介增强实现的接口-->
<property name="target" ref="my-aop-bean"/> <!--目标对象-->
<property name="interceptorNames" value="my-introduction-advice"/> <!--引介增强-->
<property name="proxyTargetClass" value="true"/> <!--引介增强需要强制使用CGLib来做动态代理-->
</bean>

调用示例:

1
2
3
4
5
6
7
8
MyAopBean myBean = (MyAopBean) applicationContext.getBean("myBean3");
myBean.sayHello();

// 开启日志打印
LoggerEnable loggerEnable = (LoggerEnable) myBean;
loggerEnable.enable(true);

myBean.sayHello();

引介增强使用过程中一个需要注意的地方是,由于基于 CGLib,所以创建 singleton 的代理对象性能会更加高一些,但是这样就可能会带来线程安全的问题,使用过程中一定要注意。

2.3 创建切面

上一小节中演示了 AOP 的简单示例,但是这些示例都存在一个共同的缺点,就是我们的增强被应用到了所有目标类的所有方法上,在具体开发中这显然是我们不想要的,我们需要对增强适用的方法进行灵活的控制,这个时候我们就需要对候选类和方法进行筛选。

Spring 提供了 ClassFilter 和 MethodMatcher 供我们使用,前者用于筛选类,后者匹配目标方法。对于类过滤器 ClassFilter,仅包含一个匹配方法 boolean matches(Class<?> clazz),用于匹配目标类,而对于方法匹配器而言,Spring 提供了两种方法匹配器:静态方法匹配器动态方法匹配器

  • 静态方法匹配器

针对方法的签名进行匹配,且仅会匹配一次。

  • 动态方法匹配器

动态方法匹配器会在运行期检查方法入参的值,所以每次调用方法都会进行匹配。

由于动态方法对于性能影响较大,所以一般不推荐使用,方法匹配器的具体选择由 MethodMatcher 的 boolean isRuntime() 方法返回值决定,false 表示使用静态方法匹配器,true 表示使用动态方法匹配器。

2.3.1 切点类型
  1. 静态方法切点
  2. 动态方法切点
  3. 流程切点
  4. 复合切点
  5. 注解切点
  6. 表达式切点
2.3.2 切面类型

切面由切点和增强组成,既包含横切的逻辑,也包含连接点的定义。切面可以分为三类:

  • 一般切面(Advisor)

一般切面仅包含一个 Advice 类,代表目标类的所有方法,前面示例中都可以称作一般切面,因为作用面太宽泛,一般不会直接使用。

  • 切点切面(PointcutAdvisor)

切点切面包含 Advice 和 Pointcut 两个类,从而可以对目标类、方法,以及方位信息进行筛选。PointcutAdvisor 主要有 6 个实现类:

DefaultPointcutAdvisor:最常用的切面类型,可以通过任意 Pointcut 和 Advice 定义一个切面,唯一不支持的是引介切面类型,一般可以通过扩展该类实现自定义切面。

NameMatchMethodPointcutAdvisor:按方法名定义切点的切面。

RegexpMethodPointcutAdvisor:基于正则表达式匹配方法名进行切面定义。

StaticMethodMatcherPointcutAdvisor:静态方法匹配器切点定义的切面。

AspectJExpressionPointcutAdvisor:基于 AspectJ 切点表达式定义切点的切面。

AspectJPointcutAdvisor:基于 AspectJ 语法定义切点的切面。

  • 引介切面(IntroductionAdvisor)

引介切面是对应引介增强的特殊切面,应用于类层面。

2.4 切面基本示例

2.4.1 静态方法名匹配切面:StaticMethodMatcherPointcutAdvisor

我们可以基于方法名称来筛选我们的目标方法,自定义静态方法名匹配切面需要继承 org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyStaticMethodMatcherPointcutAdvisor extends StaticMethodMatcherPointcutAdvisor {

@Override
public boolean matches(Method method, Class<?> targetClass) {
return "sayHello".equals(method.getName());
}

@Override
public ClassFilter getClassFilter() {
// 可以覆盖 getClassFilter 方法来过滤类
return (clazz) -> MyAopBean.class.isAssignableFrom(clazz);
}
}

需要强制我们实现的方法是 boolean matches(Method method, Class<?> targetClass),我们可以在这个方法中基于方法名对目标方法进行匹配,这里我们仅匹配目标方法名为 “sayHello” 的方法,我们还可以覆盖 getClassFilter 方法对目标类进行筛选。

配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--目标对象-->
<bean id="my-aop-bean" class="org.zhenchao.bean.MyAopBean"/>
<!--前置增强-->
<bean id="my-before-advice" class="org.zhenchao.advice.MyMethodBeforeAdvice"/>

<!--静态方法匹配器切面-->
<bean id="my-static-method-advisor" class="org.zhenchao.advisor.MyStaticMethodMatcherPointcutAdvisor">
<property name="advice" ref="my-before-advice"/>
<!--<property name="classFilter" value="筛选目标类"/>-->
<!--<property name="order" value="设置切面织入顺序"/>-->
</bean>
<bean id="myBean4" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interceptorNames" value="my-static-method-advisor"/> <!--指定切面-->
<property name="target" ref="my-aop-bean"/> <!--目标对象-->
<property name="proxyTargetClass" value="true"/>
</bean>
2.4.2 静态正则表达式方法匹配切面:RegexpMethodPointcutAdvisor

如果我们的方法命名比较符合规范,那么大多数时候基于正则表达式进行匹配的切面会更加适用,Spring 提供了 org.springframework.aop.support.RegexpMethodPointcutAdvisor 正则匹配切面,大部分情况下该类已经足够使用,不需要我们再自定义扩展,假设我们需要对目标对象所有以 “say” 开头的方法进行增强,则我们可以直接配置如下(不需要自己再扩展实现):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--静态正则表达式方法匹配器切面-->
<bean id="my-regexp-advisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice" ref="my-before-advice"/>
<property name="patterns">
<list>
<value>.*say.*</value> <!--匹配的是方法全限定名,即“类名.方法名”-->
</list>
</property>
<!--<property name="pattern" value="如果只有一个匹配模式,则可以使用该配置"/>-->
<!--<property name="order" value="设置切面织入顺序"/>-->
</bean>
<bean id="myBean5" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interceptorNames" value="my-regexp-advisor"/> <!--指定切面-->
<property name="target" ref="my-aop-bean"/> <!--目标对象-->
<property name="proxyTargetClass" value="true"/>
</bean>

需要注意的一点是 匹配模式串匹配的是目标方法类的全限定名,即 类名.方法名

2.4.3 动态切面

前面所讲述的静态切面均可以在编译期筛选筛选连接点,而一些场景下可能只有到运行期才能对筛选条件进行判定,这个时候我们就不得不使用动态切面,虽然动态切面在每次调用时都要检查,降低了运行效率,但是我们可以结合静态切面和动态切面来避免一些不必要的检查,同时又能满足需要动态代理才能实现的功能。

我们可以借助 org.springframework.aop.support.DynamicMethodMatcherPointcut 和 org.springframework.aop.support.DefaultPointcutAdvisor 来实现动态切面,自定义的动态切面需要继承 DynamicMethodMatcherPointcut 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyDynamicMethodMatcherPointcut extends DynamicMethodMatcherPointcut {

@Override
public ClassFilter getClassFilter() { // 静态切点检查
return (Class<?> clazz) -> MyAopBean.class.isAssignableFrom(clazz);
}

@Override
public boolean matches(Method method, Class<?> targetClass) { // 静态切点检查
return method.getName().startsWith("say");
}

@Override
public boolean matches(Method method, Class<?> targetClass, Object[] args) { // 动态切点检查
if(ArrayUtils.isEmpty(args)) return false;
return "zhenchao".equalsIgnoreCase(String.valueOf(args[0]));
}

}

该动态切面检查如果传递的参数值是 “zhenchao”,则织入增强,虽然该类只强制要求我们实现 boolean matches(Method method, Class<?> targetClass, Object[] args) 方法,但是为了提高效率,避免不必要的检查,最佳实践是同时覆盖前面两个方法,Spring 在执行时候的检查机制是:在创建代理时对目标类的每个连接点使用静态切点检查,如果仅凭静态检查就可以排除的连接点,则不再进行动态检查,这样就可以基于静态切面避免许多不必要的动态检查

动态切面的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
<!--动态切面, 基于 DefaultPointcutAdvisor-->
<bean id="dynamic-advisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="pointcut">
<bean class="org.zhenchao.advisor.MyDynamicMethodMatcherPointcut"/>
</property>
<property name="advice" ref="my-before-advice"/>
</bean>
<bean id="myBean6" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interceptorNames" value="dynamic-advisor"/> <!--指定切面-->
<property name="target" ref="my-aop-bean"/> <!--目标对象-->
<property name="proxyTargetClass" value="true"/>
</bean>
2.4.4 流程切面(动态)

流程一般都牵涉到前后的继承关系,流程切面也不例外,他可以描述为如果某个动作是由特定流程触发的,则织入增强,假设我们定义了一个 MyAopBeanDelegate 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyAopBeanDelegate {

private AbstractAopBean aopBean;

public void say() {
aopBean.sayHello();
aopBean.sayByeBye();
}

public MyAopBeanDelegate setAopBean(AbstractAopBean aopBean) {
this.aopBean = aopBean;
return this;
}
}

我们希望如果 sayHello 和 sayByeBye 方法是在 MyAopBeanDelegate 中触发的则织入增强。自定义流程切面基于 org.springframework.aop.support.DefaultPointcutAdvisor 和 org.springframework.aop.support.ControlFlowPointcut 两个类实现,仅需要配置即可,无需自定义逻辑,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 流程切面 -->
<bean id="flow-pointcut" class="org.springframework.aop.support.ControlFlowPointcut">
<constructor-arg type="java.lang.Class" value="org.zhenchao.bean.MyAopBeanDelegate"/>
<constructor-arg type="java.lang.String" value="say"/>
</bean>
<bean id="flow-advisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="pointcut" ref="flow-pointcut"/>
<property name="advice" ref="my-before-advice"/>
</bean>
<bean id="myBean7" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interceptorNames" value="flow-advisor"/> <!--指定切面-->
<property name="target" ref="my-aop-bean"/> <!--目标对象-->
<property name="proxyTargetClass" value="true"/>
</bean>

其中 ControlFlowPointcut 包含两个构造方法:ControlFlowPointcut(Class clazz) 和 ControlFlowPointcut(Class clazz, String methodName),前者以类作为切点,后者以类的特定方法作为切点。需要注意的是 流程切面也属于动态切面,所以要注意性能问题。

2.4.5 复合切点切面

前面我们所举例的切面都只包含一个切点,但是一些场景下我们可能需要多个切点才能满足需求,比如针对 2.4.4 中的流程切面,如果我们希望对由 MyAopBeanDelegate 的 say 方法触发的仅 sayHello 方法织入增强,这个时候我们虽然可以重新定义切点,将需求实现在一个切点里面,但是 Spring 提供了复合切点 org.springframework.aop.support.ComposablePointcut,可以将多个切点以 交集或并集 的方式组合起来,从而可以复用已有的切点定义。

ComposablePointcut 本身就是一个切点,包含的构造方法如下:

  1. ComposablePointcut():构造一个匹配所有类的所有方法的复合切点
  2. ComposablePointcut(Pointcut pointcut):基于给定的切点来创建一个复合切点
  3. ComposablePointcut(ClassFilter classFilter):构造一个匹配特定类所有方法的复合切点
  4. ComposablePointcut(MethodMatcher methodMatcher):构造一个匹配所有类特定方法的复合切点
  5. ComposablePointcut(ClassFilter classFilter, MethodMatcher methodMatcher):构造一个匹配特定类特定方法的复合切点

ComposablePointcut 提供的集合运算如下:

  1. union(ClassFilter other):将复合切点与一个 ClassFilter 对象执行并集运算,返回一个新的复合切点
  2. intersection(ClassFilter other):将复合切点与一个 ClassFilter 对象执行交集运算,返回一个新的复合切点
  3. union(MethodMatcher other):将复合切点与一个 MethodMatcher 对象执行并集运算,返回一个新的复合切点
  4. intersection(MethodMatcher other):将复合切点与一个 MethodMatcher 对象执行交集运算,返回一个新的复合切点
  5. union(Pointcut other):将复合切点与另外一个切点执行并集运算,返回一个新的复合切点
  6. intersection(Pointcut other):将复合切点与另外一个切点执行交集运算,返回一个新的复合切点

ComposablePointcut 没有提供对两个切点进行交并运算的方法,如果希望对两个切点进行交并运算,可以使用工具类 org.springframework.aop.support.Pointcuts。

接下来我们来举例说明,先定义一个复合切点:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyComposablePointcut {

public Pointcut getIntersectionPointcut() {
// 流式切点
Pointcut flowPointcut = new ControlFlowPointcut(MyAopBeanDelegate.class, "say");
// 方法名匹配切点
NameMatchMethodPointcut namePointcut = new NameMatchMethodPointcut();
namePointcut.addMethodName("sayHello");
return Pointcuts.intersection(flowPointcut, namePointcut);
}

}

上述复合切点对于流式切点和方法名匹配切点进行做了交集运算,相关配置如下:

1
2
3
4
5
6
7
8
9
10
11
<!--复合切面-->
<bean id="cp" class="org.zhenchao.advisor.MyComposablePointcut"/>
<bean id="composable-advisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="pointcut" value="#{cp.intersectionPointcut}"/><!-- 引用MyComposablePointcut.getIntersectionPointcut所返回的复合切点-->
<property name="advice" ref="my-before-advice"/>
</bean>
<bean id="myBean8" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interceptorNames" value="composable-advisor"/> <!--指定切面-->
<property name="target" ref="my-aop-bean"/> <!--目标对象-->
<property name="proxyTargetClass" value="true"/>
</bean>
2.4.6 引介切面

引介切面可以更加容易的为现有对象添加任何接口的实现,IntroductionAdvisor 为引介切面提供了支持,它仅包含一个类过滤器,因为引介是作用于类层面而不是方法层面。IntroductionAdvisor 由两个实现类,分别是 DefaultIntroductionAdvisor 和 DeclareParentsAdvisor,前者是引介切面最常用的类,后者则用于实现使用 AspectJ 语言的 DeclareParent 注解表示的引介切面。

DefaultIntroductionAdvisor 主要包含 3 个构造方法:

  1. DefaultIntroductionAdvisor(Advice advice):基于一个增强创建的引介切面,引介切面将为目标对象新增增强对象中所有接口的实现。
  2. DefaultIntroductionAdvisor(DynamicIntroductionAdvice advice, Class<?> intf):通过一个增强和一个指定的接口类创建引介切面,仅为目标对象新增 intf 接口的实现。
  3. DefaultIntroductionAdvisor(Advice advice, IntroductionInfo introductionInfo):通过一个增强和一个 IntroductionInfo 创建引介切面,目标对象需要实现哪些接口,由 IntroductionInfo 的 getInterfaces() 方法表示。

使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
<!--引介切面-->
<bean id="introduction-advisor" class="org.springframework.aop.support.DefaultIntroductionAdvisor">
<constructor-arg>
<bean class="org.zhenchao.advice.MyIntroductionAdvice"/>
</constructor-arg>
</bean>
<bean id="myBean9" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interceptorNames" value="introduction-advisor"/> <!--指定切面-->
<property name="target" ref="my-aop-bean"/> <!--目标对象-->
<property name="proxyTargetClass" value="true"/>
</bean>

2.5 自动代理创建

前面我们在创建代理时都需要借助于 ProxyFactoryBean 类,如果配置繁多,那么这样的配置往往显得比较冗余,这个时候我们就可以使用 Spring 的自动代理创建机制,该机制基于 BeanPostProcessor 实现。自动代理创建器可以分为 3 类:

  1. BeanNameAutoProxyCreator:基于 Bean 名称自动代理创建器
  2. DefaultAdvisorAutoProxyCreator:基于 Advisor 匹配机制自动代理创建器
  3. AnnotationAwareAspectJAutoProxyCreator:为包含 AspectJ 注解的 Bean 自动创建代理实例

下面演示第一种的用法,第二种个人觉得会降低代码的阅读性,第三种则留到后面讲解基于注解的 AOP 时再来讨论。

假设我们有两个 bean,如果我们希望对这两个 bean 织入增强,如果按照前面的方式我们需要分别为这两个 bean 配置对应的 ProxyFactoryBean,而基于 BeanNameAutoProxyCreator,我们只需要统一如下配置即可:

1
2
3
4
5
6
7
8
<!--基于bean名称的自动代理创建器-->
<bean id="left" class="org.zhenchao.bean.MyAopBeanLeft"/>
<bean id="right" class="org.zhenchao.bean.MyAopBeanRight"/>
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames" value="left,right"/>
<property name="interceptorNames" value="my-before-advice"/>
<property name="optimize" value="true"/>
</bean>

我们在 getBean 的时候,还是以 left 和 right 作为名称。

三. 基于注解的 AOP 使用指南

由前面的使用我们也感受到基于配置的注解在使用上还是比较繁琐的,这也是 Spring AOP 在刚刚推出时被人们所诟病的地方,不过好在现在我们可以通过注解的方式来使用 AOP,相对于配置的方式要简洁许多。需要注意的是,在基于注解使用 Spring AOP 之前,我们需要引入下面这些依赖库:

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-asm</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
</dependency>

下面通过一个简单的例子来初体验一番,假设目标类为 org.zhenchao.bean.MyAspectJBean,我们希望为目标对象中所有以“say”开头的任意方法织入增强(不管返回类型和参数类型),则切面定义如下:

1
2
3
4
5
6
7
8
9
@Aspect /*标注为一个切面*/
public class DemoAspect {

@Before("execution(* say*(..))") /*匹配以say开头的任意方法(任意入参和返回值)*/
public void advice() {
System.out.println("Hello, this is my first annotation aop.");
}

}

上述代码中 @Aspect 标注这是一个切面,@Before 标注这是一个前置增强方法,对所有以“say”开头的方法进行前置增强,配置:

1
2
3
4
<bean id="aspectJBean" class="org.zhenchao.bean.MyAspectJBean"/>
<bean class="org.zhenchao.aspect.DemoAspect"/>
<!--自定代理创建器-->
<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator"/>

这里我们使用了 AnnotationAwareAspectJAutoProxyCreator 来自动创建代理,Spring 也提供了 aop 命名空间来简化配置:

1
2
3
<aop:aspectj-autoproxy/>
<bean class="org.zhenchao.aspect.DemoAspect"/>
<bean id="aspectJBean2" class="org.zhenchao.bean.MyAspectJBean"/>

<aop:aspectj-autoproxy/> 做了与 AnnotationAwareAspectJAutoProxyCreator 相同的工作,它包含一个 proxy-target-class 属性,默认为 false,表示使用 JDK 原生的动态代理支持,如果设置为 true 则强制使用 CGLib,不过即使设置成 false,如果目标类没有声明接口,也会自动使用 CGLib。

3.1 @AspectJ 语法基础

Spring 仅支持方法级别的连接点,所以对于 AspectJ 中定义的切点语言仅部分支持。前面我们的示例中配置了一个简单的切点表达式 execution(* say*(..)),我们称 execution 为函数,而匹配串 * say*(..) 称为函数的入参。

Spring 目前支持 9 个切点表达式函数,大致可分为 4 种类型,如下表:

image

上面这些函数中部分是支持通配符的,入参通配符类型:

* 匹配任意字符,但只能匹配上下文中一个元素
.. 匹配任意字符,可以匹配上下文中多个元素,但在表示类时必须和 * 联合使用,而在表示入参时可以单独使用
+ 表示按类型匹配指定类的所有类(类本身、继承的类等),必须跟在类名后面,如 org.zhenchao.My+。

函数对于通配符的支持可以概括为:

  • 支持所有的通配符

execution、within

  • 仅支持“+”的通配符

args、this、target

  • 不支持通配符

@args@within@target@annotation

切点函数之间可以进行逻辑运算,组成复合切点,Spring 支持以下切点运算符:

  • &&:与,等效操作符:and
  • ||:或,等效操作符:or
  • !:非,等效操作符:not

使用 not 时需要在开头留一个空格,否则会解析错误,这应该是 Spring 的一个 bug。

3.2 增强类型

所有增强注解都是方法级别的,且都作用于运行期。

  • 前置增强:@Before
  • value:定义切点
  • argNames:由于无法通过反射获取方法参数名称,如果需要获取参数名称信息,就需要借助该属性,注意参数名称必须完全一致,多个参数名称以逗号分隔
  • 后置增强:@AfterReturning
  • value:定义切点
  • pointcut:切点信息,如果显式指定将覆盖 value 的配置
  • returning:将目标对象方法的返回值绑定给增强的方法
  • argNames:如前所述
  • 环绕增强:@Around
  • value:定义切点
  • argNames:如前所述
  • 异常抛出增强:@AfterThrowing
  • value:定义切点
  • pointcut:如前所述
  • throwing:将抛出的异常,绑定到增强方法中
  • argNames:如前所述
  • Final 增强:@After

不管方法是正常退出还是异常退出,该增强都会执行。

  • value:定义切点
  • argNames:如前所述
  • 引介增强:@DeclareParents
  • value:定义切点,表示在哪个目标类上织入引介增强
  • defaultImpl:默认的接口实现类

3.3 切点函数示例

3.3.1 @annotation

假设我们定义了一个注解:

1
2
3
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodLabel {}

并且我们基于 @annotation 定义了一个切面:

1
2
3
4
5
6
7
8
9
@Aspect
public class MyAspect {

@After("@annotation(org.zhenchao.annotation.MethodLabel)")
public void aspect() {
System.out.println("this is a final after aspect");
}

}

其语义就是只要某个方法被 MethodLabel 注解了,那么该方法在调用时就会被织入上面的增强。

3.3.2 execution

execution 是最常用的切面函数,其语法如下:

execution(<修饰符匹配> <返回类型匹配> <方法名匹配>(<参数匹配>) <异常匹配>)

其中 返回类型、方法名,以及参数 匹配模式是必须的。

举例说明:

  • 通过方法签名定义切点

execution(public * *(..))

匹配所有目标类的 public 方法(任意返回类型和参数签名)

execution(* *To(..))

匹配目标类的所有以 To 为后缀的方法(任意返回类型和参数签名)

  • 通过类定义切点

execution(* org.zhenchao.Demo.*(..))

匹配 Demo 接口的所有方法(必须是在 Demo 中声明的,继承类扩展的不认)

execution(* org.zhenchao.Demo+.*(..))

匹配 Demo 接口及其实现类的所有方法(这些方法可以不是在 Demo 接口中声明的)

  • 通过类包定义切点

在类名模式串中,“.*” 表示包下的所有类,而 “..*” 表示包及其子包下的所有类。

execution(* org.zhenchao.*(..))

匹配 org.zhenchao 包下所有类的所有方法

execution(* org.zhenchao..*(..))

匹配 org.zhenchao 包,以及子包下的所有方法。当“..”出现在类名中时,后面必须跟“*”

execution(* org..*.*Dao.find*(..))

匹配 org 包及其子包下 Dao 类的所有以 find 开头的方法。

  • 通过方法入参定义切点

在方法入参匹配模式中,“*”表示任意类型参数,而“..”表示任意类型参数且参数个数不限。

execution(* foo(String, int))

匹配 foo(String, int) 方法,如果是 java.lang 下面的类型或基本类型则无需写全限定名。

execution(* foo(String, *))

第一个参数为 String,且只有两个参数

execution(* foo(String, ..))

第一个参数为 String,可以有多个参数

execution(* foo(Object+))

匹配仅用一个入参,且参数类型是 Object 类型及其子类型。

3.3.3 args 和 @args

args 和 @args 都是对运行时候的参数类型进行匹配,区别在于前者的入参类型是类名,而后者的入参类型必须是注解类的类名。需要注意的一点是,虽然 args 允许在类名后面追加“+”,但是并不会起作用。

假设我们已经定义了 ClassA、ClassB、ClassC 三个类,并且 C 继承 B, B 又继承 A。

先来看一下 args 的使用,假设我们希望对所有入参为 org.zhenchao.pojo.CLassA 类型及其子类型的所有方法进行增强,则可以定义切点:

1
2
3
4
@AfterReturning("args(org.zhenchao.pojo.CLassA)")
public void argsAspect() {
System.out.println("this is a after returning args aspect");
}

这样就会拦截所有类型为 ClassA 的方法,并且适用于里氏替换。

而对于 @args 来说,情况就稍微要复杂一些,@args 的入参是修饰类的注解类名,假设这里的入参是注解:

1
2
3
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ClassLabel {}

那么下面的切点会匹配所有入参类型被 ClassLabel 注解的类:

1
2
3
4
@After("@args(org.zhenchao.annotation.ClassLabel)")
public void argsAnnotationAspect() {
System.out.println("this ia a args annotation aspect");
}

具体举例来说,假设该注解修饰了 ClassB:

  • 对于 foo(ClassB b) 方法来说,如果我们传入的是 ClassB 或 ClassC 都会被增强
  • 对于 foo(ClassA a) 方法来说,只有传入 ClassB 类型会被增强
  • 对于 foo(ClassC c) 方法来说,不会被增强
3.3.4 within

within 所指定的最小连接点是类级别的,所以实际上 execution 在功能上已经覆盖了 within。

示例如下:

within(org.zhenchao.Demo)

匹配 Demo 类的所有方法,但是不包括 Demo 的子类。

within(org.zhenchao.*)

匹配 org.zhenchao 包下面所有类的方法,不包含子包。

within(org.zhenchao..*)

匹配 org.zhenchao 包及其子包下面的所有方法。

3.3.5 @within@target

@within@target 也都是用于筛选目标类,并且入参是注解类型,区别在于前者作用于被注解类及其子类的所有方法,而后者只作用于目标类。需要注意的一点是 他们都作用于类,如果注解在接口上则均不生效

3.3.6 targrt 和 this

target 用于匹配目标类及其子类的所有方法(包括子类中不在目标类中声明的方法),示例:

target(org.zhenchao.Demo)

匹配 Demo 及其子类中所有的方法,包括子类中新增的方法。

一般情况下 this 和 target 是等价的,而且区别在于通过引介切面产生代理对象时的具体表现。this 会匹配引介引入的方法,而 target 则不会,所以 this 匹配的方法是 target 的超集。

3.4 @AspectJ 高级特性

3.4.1 切点复合运算

基于逻辑运算 “与、或、非” 我们可以将多个切点复合来实现一些复杂筛选逻辑,示例:

within(org.zhenchao.*) && execution(* say*(..))

匹配 org.zhenchao 包下面(不包括子包) 所有以 say 开头的任意方法。

!target(org.zhenchao.Demo) && execution(* *(..))

匹配除 org.zhenchao.Demo 类以外的所有方法。

target(org.zhenchao.A) || target(org.zhenchao.B)

匹配类 A 或 B 的所有方法

3.4.2 对切点进行命名

前面示例中我们定义的切点都是作用在目标方法中,这种切点被称之为匿名切点,如果我们希望对切点进行重用的话,那么我们可以专门用一个类去定义切点,并对切点进行命名,这样在需要的时候就可以基于切点名称来引用切点,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Pointcuts {

@Pointcut("@annotation(org.zhenchao.annotation.MethodLabel)")
public void methodLabel() {
}

@Pointcut("args(org.zhenchao.pojo.CLassA)")
public void argsClassA() {
}

@Pointcut("@args(org.zhenchao.annotation.ClassLabel)")
public void argsClassLabel() {
}

}

示例中我们利用 Pointcuts 类来封装了多个切点,其中 @Pointcut 是切点命名的注解,命名的切点使用类方法作为切点的名称,并且利用方法的修饰符来作为切点的修饰符,从而定义切点的使用范围,仅利用这两项信息,所以命名切点所附属的方法一般都是 void 返回类型,并且方法体为空,具体使用方式可以匿名切点相似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Aspect
public class MyAspect {

@After("org.zhenchao.pointcut.Pointcuts.methodLabel()")
public void annotationAspect() {
System.out.println("this is a final after annotation aspect");
}

@AfterReturning("org.zhenchao.pointcut.Pointcuts.argsClassA()")
public void argsAspect() {
System.out.println("this is a after returning args aspect");
}

@After("org.zhenchao.pointcut.Pointcuts.argsClassLabel()")
public void argsAnnotationAspect() {
System.out.println("this ia a args annotation aspect");
}

}
3.4.3 增强的织入顺序

如果一个连接点匹配多个切点,那么我们这些切点的织入顺序可以概括如下

  1. 同一个切面类中定义的切点按照从上到下的原则织入
  2. 不同切面之间,如果都实现了 Orderd 接口,则按照顺序号有小到大顺序织入,切面内还是遵从 1 中的规则
  3. 如果没有实现 Ordered 接口,则织入顺序是不确定的
3.4.4 访问连接点信息

如果我们希望在切点函数中拿到连接点的信息,那么需要将切点函数的 第一个入参 声明为 org.aspectj.lang.JoinPoint 类型,如果是环绕增强则需要声明为 org.aspectj.lang.ProceedingJoinPoint 类型。

  • org.aspectj.lang.JoinPoint
  • getArgs():获取连接点方法运行时的参数列表
  • getSignature():获取连接点方法签名对象
  • getTarget():获取连接点所在的目标对象
  • getThis():获取代理对象本身
  • org.aspectj.lang.ProceedingJoinPoint
  • proceed() throws Throwable:通过反射执行目标对象连接点处的方法
  • proceed(Object[] args) throws Throwable:通过反射执行目标对象连接点处的方法,使用新的入参替换原来的入参
3.4.5 绑定连接点的运行信息
  • 获取连接点方法的入参

如果我们希望在切点函数中拿到连接点方法的入参信息,那么就需要进行绑定,前面介绍过的切点函数 args、this、target、@args、@within、@target、以及 @annotation,除了可以指定类名外,还可以指定参数名。

  • args:绑定连接点方法的入参
  • @annotation:绑定连接点方法的注解对象
  • @args:绑定连接点方法入参的注解

下面示例中演示了 args 绑定入参的用法:

1
2
3
4
@Before("execution(* org.zhenchao.bean.MyAspectJBean.say*(String)) && args(name)")
public void say(JoinPoint point, String name) {
System.out.println("before aspect, method:" + point.getSignature() + ", name=" + name);
}

上面的切点配置中,我们通过 args(name) 拿到了目标方法的 name 参数,args 根据增强方法对应参数名称 name 的类型知道这个参数是 String 类型,同时增强方法又通过 args 拿到 name 参数对应的值。

  • 获取连接点所在的目标对象
  • this 或 target:绑定被代理的对象实例

使用方式类似于获取运行入参:

1
2
3
4
@Before("execution(* org.zhenchao.bean.MyAspectJBean.say*(String)) && this(bean)")
public void sayWithObj(JoinPoint point, MyAspectJBean bean) {
System.out.println("before aspect, method:" + point.getSignature() + ", obj=" + bean.getClass().getName());
}
  • 获取类注解的对象
  • @within@target:可以将目标类的注解对象绑定到增强方法中。
  • 获取返回值

在 AfterReturning 增强中,可以通过其 returning 属性拿到方法的返回值。

  • 获取抛出的异常

在 AfterThrowing 增强中,可以通过 throwing 属性拿到方法抛出的异常对象。

四. 基于 Schema 的 AOP 使用指南

上一小节中介绍了基于注解配置的 AOP 使用方式,Spring 也为我们提供了基于 aop 命名空间配置的方式使用 AOP,实际上我们在上一节中已经初步接触到了,比如:<aop:aspectj-autoproxy proxy-target-class="true"/>

下面先来一个示例感受一下,假设我们已经定义了一个增强类:

1
2
3
4
5
6
7
public class MyAdvice {

public void before() {
System.out.println("schema aop, this is a before advice");
}

}

如果希望将 before 增强织入以 say 开头的所有方法,则可以按照如下配置:

1
2
3
4
5
6
7
<bean id="my-advice" class="org.zhenchao.schema.advice.MyAdvice"/>

<aop:config proxy-target-class="true">
<aop:aspect ref="my-advice">
<aop:before pointcut="execution(* say*(..))" method="before"/>
</aop:aspect>
</aop:config>

4.1 对切点进行命名

上面的例子中使用的切点称为匿名切点,如果我们希望对切点进行命名,然后重用的话,可以做如下配置:

1
2
3
4
5
6
7
8
9
<aop:config proxy-target-class="true">
<!--该切点作用于本config-->
<aop:pointcut id="my-config-advice-before" expression="execution(* say*(..))"/>
<aop:aspect ref="my-advice">
<!--该切点作用于本aspect-->
<aop:pointcut id="my-aspect-advice-before" expression="execution(* say*(..))"/>
<aop:before pointcut-ref="my-aspect-advice-before" method="before"/>
</aop:aspect>
</aop:config>

需要注意的是如果在 <aop:config /> 下面定义子标签,则需要保证配置顺序:

<aop:pointcut/><aop:advisor/><aop:aspect/>

在实际开发中推荐都按照这样的配置顺序来。

4.2 各增强类型配置

首先定义一个切点类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyAdvice {

public void before() {}

public void afterReturning(int returnValue) {}

public void aroundMethod(ProceedingJoinPoint joinPoint) {}

public void afterThrowing(Exception e) {}

public void finalAfter() {}

}

各类型增强配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<aop:config proxy-target-class="true">
<aop:aspect ref="my-advice">
<aop:pointcut id="my-advice-pointcut" expression="execution(* say*(..))"/>
<!--前置增强-->
<aop:before pointcut-ref="my-advice-pointcut" method="before"/>
<!--后置增强-->
<aop:after-returning pointcut-ref="my-advice-pointcut" method="afterReturning" returning="returnValue"/>
<!--环绕增强-->
<aop:around pointcut-ref="my-advice-pointcut" method="aroundMethod"/>
<!--异常抛出增强-->
<aop:after-throwing pointcut-ref="my-advice-pointcut" method="afterThrowing" throwing="e"/>
<!--final after 增强-->
<aop:after pointcut-ref="my-advice-pointcut" method="finalAfter"/>
<!--引介增强-->
<aop:declare-parents types-matching="org.zhenchao.bean.MySchemaBean+" implement-interface="org.zhenchao.bean.LoggerEnable"/>
</aop:aspect>
</aop:config>

上述配置中我们还绑定了一些连接点的信息,比如返回值,异常对象等,对于其他的连接信息的绑定,也是基于第三节中的切面函数进行绑定,不再重复撰述。


转载声明 : 版权所有,商业转载请联系作者,非商业转载请注明出处
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议
Powered by hexo & Theme by hiero   Copyright © 2015-2019 浙ICP备 16010916  号,指 · 间 All Rights Reserved.