在 《Spring AOP增强》一文中,演示了各种增强的实现方式。增强提供了连接点的方位信息:如织入方法前面、后面等。但是增强会被织入目标类的所有方法,如何像手术刀一样找到目标方法,进行增强的织入,就要配合切点。
切点进一步描述织入哪些类的哪些方法上。切点和增强组合起来我们称为切面。当然,增强本身就包含了一部分连接点信息,所以增强类本身就是一个普通的切面。但是这个普通切面还不够精确,如果需要更精确的增强织入,就需要配合切点,组成即另两种切面:切点切面 和引介切面。
在讨论切面之前,我们先看看切点。
切点的类型
Spring通过org.springframework.aop.Pointcut
接口描述切点。Pointcut由ClassFilter和MethodMatcher构成,它通过ClassFilter定位到某些特定的类上,通过MethodMatcher定位到某些特定的方法上,从而实现了精确的定位。
Spring支持两种方法匹配器:静态方法匹配器和动态方法匹配器,一般情况下,动态匹配不常使用。方法匹配器的类型由isRuntime()返回值决定默认是false表示静态方法匹配。
Spring中定义了6中类型的切点:
-
1.静态方法切点:
rg.springframework.aop.support.StaticMethodMatcherPointcut
是静态方法切点的抽象基类,默认情况下它匹配所有的类。
StaticMethodMatcherPointcut
包括两个主要的子类,分别是NameMatchMethodPointcut
和AbstractRegexpMethodPointcut
,前者提供简单字符串匹配方法签名,而后者使用正则表达式匹配方法签名。 -
2.动态方法切点:
-
3.注解切点:【下次讨论】
-
4.表达式切点:【下次讨论】
-
5.流程切点:
-
6.复合切点:
切面的类型
Spring使用org.springframework.aop.Advisor
接口表示切面的概念,一个切面同时包含横切代码和连接点信息。切面分为三类:一般切面、切点切面和引介切面。
-
Advisor:
代表一般切面,它仅包含一个Advice。所以Advice(增强) 本身就是一个简单的切面,只不过它代表的横切的连接点是所有目标类的所有方法。因为这个横切面太宽泛,所以一般不会直接使用。
-
PointcutAdvisor:
代表具有切点的切面,它包含Advice和Pointcut两个类。这是最常用的一种切面,也是本文讨论的重点。
它常用的实现类有:(后边也会围绕他们进行讨论)
-
IntroductionAdvisor:
代表引介切面。
这里我们主要讨论PointcutAdvisor
,它有两个常用的实现类:
StaticMethodMatcherPointcutAdvisor
:普通方法名匹配切面
regexpMethodPointcutAdvisor
:正则表达式匹配切面
普通方法名匹配切面
StaticMethodMatcherPointcutAdvisor
代表一个静态方法匹配切面,它通过StaticMethodMatcher Pointcut
(即静态方法切点)定义切点,通过类过滤和方法名匹配定义切点。
使用步骤:
-
1.定义目标对象和增强实现类:
目标对象:
//目标对象Waiter
public class Waiter {
public void greetTo(String name) {
System.out.println("waiter greet to " + name + "...");
}
public void serveTo(String name) {
System.out.println("waiter serving " + name + "...");
}
}
//目标对象Seller
public class Seller {
public void greetTo(String name) {
System.out.println("seller greet to " + name + "...");
}
}
对象Waiter中有两个方法:greetTo和serveTo.对象Seller中有一个同名的方法greetTo
增强实现类:这里定义一个前置增强。织入增强后,会在方法执行前打印一句话。
public class GreetingBeforeAdvice implements MethodBeforeAdvice {
public void before(Method method, Object[] args, Object obj) throws Throwable {
String clientName = (String) args[0];
System.out.println("How are you!Mr." + clientName + ".");
}
}
- 2.定义切面对象,这里只需要继承
StaticMethodMatcherPointcutAdvisor
,实现的两个方法分别用于过滤类和方法名,这样就能保证定位到具体类的具体方法上,进行增强的织入。
/**
* 定义一个切面
*/
public class StaticMethodMatcherAdvisor extends StaticMethodMatcherPointcutAdvisor {
/**
* 用于匹配方法
* @param method
* @param clazz
* @return
*/
public boolean matches(Method method, Class clazz) {
return "greetTo".equals(method.getName());
}
/**
* 用于匹配类
* @return
*/
public ClassFilter getClassFilter() {
return new ClassFilter() {
public boolean matches(Class clazz) {
return Waiter.class.isAssignableFrom(clazz);
}
};
}
}
- 3.测试验证:首先我们通过编码的方式实现。随后再演示如何通过配置文件实现。
public class StaticMethodMatcherAdvisorTest {
private Waiter waiter;
private Seller seller;
private ProxyFactory pfWaiter;
private ProxyFactory pfSaller;
private PointcutAdvisor advisor;//切面
private Advice advice;//增强
@BeforeClass
public void init(){
waiter = new Waiter();
seller = new Seller();
advisor = new StaticMethodMatcherAdvisor();
advice = new GreetingBeforeAdvice();
((StaticMethodMatcherAdvisor) advisor).setAdvice(advice);
//waiter的代理类
pfWaiter = new ProxyFactory();
pfWaiter.setTarget(waiter);
pfWaiter.addAdvisor(advisor);
//seller的代理类
pfSaller = new ProxyFactory();
pfSaller.setTarget(seller);
pfSaller.addAdvisor(advisor);
}
@Test
public void staticMethod() {
//获取代理进行方法调用
Waiter proxy = (Waiter) pfWaiter.getProxy();
proxy.greetTo("John");
proxy.serveTo("John");
//获取代理进行方法调用
Seller proxy2 = (Seller) pfSaller.getProxy();
proxy2.greetTo("John");
}
}
我们在测试类中首先定义了相关的目标对象,代理工厂和将要织入的增强。在初始化中,把增强和切面关联,同时构造了目标对象的代理工厂并将增强和期关联。最后,通过代理工厂获取代理类,进行调用。
最终打印结果如下:
How are you!Mr.John. //增强中的输出
waiter greet to John...
waiter serving John...
seller greet to John...
可以看到,只有Waiter中的greetTo方法被织入了增强,说明切面的过滤已经生效。
如何通过配置文件实现?
上面我们通过编码的方式实现目标对象、增强、切面还有代理类之间的相关装配。采用配置文件同样可以实现,本质上是一样,只不过把代码的逻辑采用了配置的方式实现。我们只需要将上面的第3步替换成以下两步:
1.编写配置文件:
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
<!-- 普通方法名匹配切面 -->
<bean id="waiterTarget" class="com.smart.advisor.Waiter" />
<bean id="sellerTarget" class="com.smart.advisor.Seller" />
<bean id="greetingAdvice" class="com.smart.advisor.GreetingBeforeAdvice" />
<bean id="greetingAdvisor" class="com.smart.advisor.StaticMethodMatcherAdvisor"
p:advice-ref="greetingAdvice" />
<bean id="parent" abstract="true"
class="org.springframework.aop.framework.ProxyFactoryBean"
p:interceptorNames="greetingAdvisor" p:proxyTargetClass="true" />
<bean id="waiter" parent="parent" p:target-ref="waiterTarget" />
<bean id="seller" parent="parent" p:target-ref="sellerTarget" />
</beans>
说明:我们首先配置了两个目标对象和增强类,紧接着我们配置了一个切面类,它指向:com.smart.advisor.StaticMethodMatcherAdvisor
,并通过advice-ref属性关联了上面配置的增强。
因为两个目标类使用相同的切面,所以配置了一个parent节点,用以简化。其中p:interceptorNames属性指明了切面,p:proxyTargetClass="true"表明使用CGLib代理。最后配置两个代理类,通过target-ref属性和目标对象进行关联。
- 使用配置文件:
public class StaticMethodMatcherAdvisorXMLTest {
private String configPath ="";
private ApplicationContext ctx =null;
@BeforeClass
public void init(){
configPath = "com/smart/advisor/beans.xml";
ctx = new ClassPathXmlApplicationContext(configPath);
}
@Test
public void staticMethod() {
Waiter waiter = (Waiter)ctx.getBean("waiter");
Seller seller = (Seller)ctx.getBean("seller");
waiter.greetTo("John");
waiter.serveTo("John");
seller.greetTo("John");
}
}
说明:通过ClassPathXmlApplicationContext
加载配置文件。获取对应的代理对象,进行方法调用。结果和上面通过代码完全一样。这种方式,代码比较简洁,对象的依赖关系,配置在xml中统一管理。
小结:
通过方法名匹配生成切面,可以对具体的方法进行增强织入,但是这种方式不够灵活,假设目标类中有多个方法,且命名有一定的规范,使用正则表达式匹配描述就灵活的多。
静态正则表达式方法匹配切面
下面直接使用RegexpMethodPointcutAdvisor,通过配置的方式为Waiter目标类定义一个切面。
1.在配置文件中增加正则表达式的切面配置和另一个代理对象:
<!-- 正则表达式方法名匹配切面 -->
<bean id="regexpAdvisor"
class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"
p:advice-ref="greetingAdvice">
<property name="patterns">
<list>
<value>.*greet.*</value>
</list>
</property>
</bean>
<bean id="waiter1" class="org.springframework.aop.framework.ProxyFactoryBean"
p:interceptorNames="regexpAdvisor" p:target-ref="waiterTarget"
p:proxyTargetClass="true" />
2.代码测试:
public class RegexpAdvisorTest {
private String configPath ="";
private ApplicationContext ctx =null;
@BeforeClass
public void init(){
configPath = "com/smart/advisor/beans.xml";
ctx = new ClassPathXmlApplicationContext(configPath);
}
@Test
public void regexpAdvisor() {
Waiter waiter = (Waiter)ctx.getBean("waiter1");
waiter.greetTo("John");
waiter.serveTo("John");
}
}
输出结果: 匹配规则生效,只有greetTo方法被织入增强
How are you!Mr.John.
waiter greet to John...
waiter serving John...
总结:
Spring中增强包含了要织入的代码和部分连接点信息,为了更精确的进行增强织入,需要配合切点精确定位。所以我们引出了切面的概念,切面就是增强和切点相结合。切点有6中类型,我们讨论最常用的静态方法切点,和与之对应的两个常用切面:StaticMethodMatcherAdvisor、RegexpMethodPointcutAdvisor。前者通过方法名和类过滤器,来确定对哪个类的哪个方法进行织入增强。后者定义了匹配的正则表达式规则,更加灵活。
同时我们采用代码和配置文件两种不同的方式实现了相同的逻辑,可以看出,两者的本质和逻辑几乎一致,只是不同的表现形式。这也是Spring一贯的风格,包容开放。我们似乎感觉到通过配置文件能简化代码,但是配置xml文件的工作依然繁琐无趣,而这正是SpringBoot目前给我们解决的一个痛点。