程序员Java 杂谈

说说在 Spring 中如何创建切面(AOP)

2018-06-15  本文已影响158人  deniro

Spring 的增强提供了连接点的方位信息(织入方法前、方法后等),而切点则描述了增强需要织入到哪个类的哪一个方法上。

Pointcut 类关系图

Spring 通过 Pointcut 来描述切点,它是由 ClassFilter 和 MethodMatcher 组成的。ClassFilter 用于定位到特定的类,MethodMatcher 用于定位到特定的方法上。

ClassFilter 自定义了一个方法 matches(Class<?> clazz),clazz 表示被检查的类,这个方法用来判断是否是条件所要求的类。

Spring 支持两种方法匹配器:

  1. 静态方法匹配器 - 仅对方法签名(方法名、入参类型与顺序)进行匹配;仅判别一次。
  2. 动态方法匹配器 - 在运行期检查方法入参的值。每次调用方法都会进行判别,因此对性能有很大影响。

使用哪一种的方法匹配器是由 MethodMatcher 的 isRuntime() 方法的返回值决定的,true 表示使用动态方法匹配器。

从 Spring2.0+ 开始,支持注解切点(JDK5.0 +)与字符串表达式切点,它们使用的都是 AspectJ 切点表达式语言。

1 切点类型

Spring 提供 6 种类型的切点:

切点类型 说明
静态方法切点 org.springframework.aop.support.StaticMethodMatcherPointcut 是静态方法切点的抽象基类,默认情况下它匹配所有的类 。 StaticMethodMatcherPointcut 包括两个主要的子类,它们是 NameMatchMethodPointcutAbstractRegexpMethodPointcut ,前者提供简单字符串匹配方法签名,而后者是使用正则表达式匹配方法签名。
动态方法切点 org.springframework.aop.support.DynamicMethodMatcherPointcut 是动态方法切点的抽象基类,默认情况下它匹配所有的类 。
注解切点 org.springframework.aop.support.annotation.AnnotationMatchingPointcut 实现类表示注解切点 。 使用 AnnotationMatchingPointcut 支持在 Bean 中直接通过 JDK 5.0 注解标签来定义切点。
表达式切点 使用 org.springframework.aop.support.ExpressionPointcut 接口来支持 AspectJ 切点表达式语法。
流程切点 org.springframework.aop.support.ControlFlowPointcut 实现类来表示控制流程切点 。ControlFlowPointcut 是一种特殊的切点,它回根据程序执行堆栈的信息查找目标方法是否由某一个方法直接或间接发起的调用,以此判断是否为匹配的连接点。
复合切点 org.springframework.aop.support.ComposablePointcut 实现类是为创建多个切点而提供的操作类 。 它的所有方法都返回 ComposablePointcut 类,这样,我们就可以使用链接表达式对其进行操作啦O(∩_∩)O哈哈~

2 切面类型

由于增强既包含横切代码,又包含部分的连接点信息(方法前 、 方法后等的方位信息),所以我们可以仅通过增强类来生成一个切面 。 但切点仅代表目标类连接点的部分信息(即定位到哪个类的哪个方法),所以切点必须结合增强才能制作出一个切面 。 Spring 使用 org.springframework.aop.Advisor 接口表示切面,一个切面同时包含横切代码和连接点信息 。

Advisor 类图

切面可分为以下三类:

切面类型 说明
Advisor 一般切面,它仅包含一个 Advice ,因为 Advice 包含了横切代码和连接点的信息,所以 Advice 本身就是一个简单的切面,只是它代表的横切连接点是所有目标类的所有方法,这个横切面太宽泛,所以在实践中一般不会直接使用。
PointcutAdvisor 具有切点的切面,它包含 Advice 和 Pointcut 两个类,这样我们就可以通过类 、 方法名以及方法方位等信息灵活地定义出切面的连接点信息,从而提供更具适用性的切面。
IntroductionAdvisor 引介切面。它是对应引介增强的特殊的切面,应用于类,所以引介切点是使用 ClassFilter 来定义的。
PointcutAdvisor 类关系图

PointcutAdvisor 有 6 个具体的实现类:

实现类 说明
DefaultPointcutAdvisor 最常用的切面类型,通过它可以设定任意的 Pointcut 和 Advice 定义一个切面,唯一不支持的是引介的切面类型,可以通过扩展该类实现自定义切面。
NameMatchMethodPointcutAdvisor 实现按方法名来定义切点的切面。
RegexpMethodPointcutAdvisor 使用正则表达式来匹配方法名,实现切点定义的切面 。 内部是通过 JdkRegexpMethodPointcut 类来构建出正则表达式方法名的切点的 。
StaticMethodMatcherPointcutAdvisor 使用静态方法匹配器来定义切点的切面。默认情况下,匹配所有的目标类。
AspecJExpressionPointcutAdvisor 使用 Aspecj 切点表达式来定义切点的切面 。
AspecJPointcutAdvisor 使用 AspecJ 语法来定义切点的切面 。

这些 Advisor 的实现类都是通过扩展对应 Pointcut 实现类并实现 PointcutAdvisor 接口来进行定义 。 此外, Advisor 都实现了 org.springframework.core.Ordered 接口, Spring 会根据 Advisor 定义的顺序来决定织入切面的顺序 。

3 静态方法名匹配

StaticMethodMatcherPointcutAdvisor 代表静态方法匹配切面,它通过 StaticMethodMatcherPointcut 来定义切点,并通过类和方法名来匹配所定义的切点。

假设有两个业务类 User 与 Charger,它们都定义了相同的方法。

User 类:

public class User {

    public void rent(String userId) {
        System.out.println("User:租赁【充电宝】");
    }
}

Charger 类:

public class Charger {

    public void rent(String userId) {
        System.out.println("Charger:【充电宝】被租赁");
    }
}

我们希望对 User 的 rent(String userId) 方法实施前置增强。

切面类:

public class RentAdvisor extends StaticMethodMatcherPointcutAdvisor {
    /**
     * 设定切点方法的匹配规则
     *
     * @param method
     * @param targetClass
     * @return
     */
    public boolean matches(Method method, Class<?> targetClass) {
        return "rent".equals(method.getName());//方法名为 rent
    }

    /**
     * 设定切点类的匹配规则
     *
     * @return
     */
    @Override
    public ClassFilter getClassFilter() {
        return new ClassFilter() {
            public boolean matches(Class<?> clazz) {
                return User.class.isAssignableFrom(clazz);//是 User 的类或子类
            }
        };
    }
}

要使用切面,我们还需要定义一个增强:

public class RentBeforeAdvice implements MethodBeforeAdvice {
    public void before(Method method, Object[] args, Object o) throws Throwable {
        System.out.println("准备租赁的用户 ID:" + args[0]);
    }
}

配置切面:

<?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-4.0.xsd">

    <bean id="user" class="net.deniro.spring4.aop.User"/>
    <bean id="charger" class="net.deniro.spring4.aop.Charger"/>

    <!-- 前置增强-->
    <bean id="rentBeforeAdvice" class="net.deniro.spring4.aop.RentBeforeAdvice"/>

    <!-- 切面-->
    <bean id="rentAdvisor" class="net.deniro.spring4.aop.RentAdvisor"
          p:advice-ref="rentBeforeAdvice"/>

    <!-- 通过父 Bean 来定义公共的配置信息-->
    <bean id="parentBean" abstract="true"
          class="org.springframework.aop.framework.ProxyFactoryBean"
          p:interceptorNames="rentAdvisor"
          p:proxyTargetClass="true"/>


    <!-- 代理-->
    <bean id="userProxy" parent="parentBean" p:target-ref="user"/>
    <bean id="chargerProxy" parent="parentBean" p:target-ref="charger"/>

</beans>

使用切面 rentAdvisor 的 advice-ref 属性来指定前置增强。

StaticMethodMatcherPointcutAdvisor 除了 advice 属性之外还有两个属性:

属性 说明
classFilter 类匹配过滤器。(在 RentAdvisor 中采用编码方式实现了这个过滤器)
order 切面织入的顺序。

这里还需要了一个父 Bean 来简化配置。

单元测试:

User user = (User) context.getBean("userProxy");
Charger charger = (Charger) context.getBean("chargerProxy");

String userId="001";
user.rent(userId);
charger.rent(userId);

输出结果:

准备租赁的用户 ID:001
User:租赁【充电宝】
Charger:【充电宝】被租赁

从输出结果中可以看出, User 方法被织入增强。

4 静态正则表达式方法名匹配

StaticMethodMatcherPointcutAdvisor 仅能通过方法名来定义切点,这种方式不够灵活,如果目标类中包含多个方法,而且它们满足一定的命名规范,那么使用正则表达式就很方便 。

配置:

<!-- 静态正则表达式方法名匹配-->
<bean id="regexpAdvisor"
      class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"
      p:advice-ref="rentBeforeAdvice">
    <!-- 匹配模式-->
    <property name="patterns">
        <list>
            <!-- 匹配字符串-->
            <value>.*rent.*</value>
        </list>
    </property>
</bean>

<!-- 代理-->
<bean id="userProxy2"
      class="org.springframework.aop.framework.ProxyFactoryBean"
      p:interceptorNames="rentAdvisor"
      p:target-ref="user"
      p:proxyTargetClass="true"/>

单元测试:

User user = (User) context.getBean("userProxy2");

String userId="002";
user.rent(userId);

输出结果:

准备租赁的用户 ID:002
User:租赁【充电宝】

RegexpMethodPointcutAdvisor 除了 pattern 和 advice 属性之外 ,还有另外两个属性:

属性 说明
pattern 只定义一个匹配模式串。
paterns 定义多个匹配模式串;这些匹配模式串之间是 “ 或 ” 的关系 。
order 切面织入时对应的顺序。

正则表达式语法请参见 说说正则表达式的基础语法

只要应用类包具有良好的命名规范,那么就可以使用简单的正则表达式来描述目标方法。好的命名规范既可以增强程序的可读性与团队开发的协作性,有能够降低沟通成本,所以是值得推广的好的编程实践方法。


推荐一款正则表达式工具 RegexBuddy,它是一款全球知名的正则式测试工具,支持多平台规则测试,支持正则式分组测试以及对相关字符进行高亮显示。具体请参见 正则表达式工具 RegexBuddy 使用指南

5 动态切面

可以使用 DefaultPointcutAdvisor 和 DynamicMethodMatcherPointcut 来创建动态切面。

DynamicMethodMatcherPointcut 是一个抽象类,它继承的抽象类 DynamicMethodMatcher 将 isRuntime() 标识为 final 并返回 true ,这样其子类就一定是一个动态切点 。 DynamicMethodMatcherPointcut 默认匹配所有的类和方法,所以需要扩展该类以编写出符合要求的动态切点:

public class RentDynamicPointcut extends DynamicMethodMatcherPointcut {

    private static List<String> SPECIAL_USER_IDS = new ArrayList();

    static {
        SPECIAL_USER_IDS.add("001");
    }


    /**
     * 对类做静态切点检查
     *
     * @return
     */
    @Override
    public ClassFilter getClassFilter() {
        return new ClassFilter() {
            public boolean matches(Class<?> clazz) {
                System.out.println("对 " + clazz.getName() + " 做静态切点检查。");
                return User.class.isAssignableFrom(clazz);
            }
        };
    }

    /**
     * 对方法进行静态切点检查
     *
     * @param method
     * @param targetClass
     * @return
     */
    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        System.out.println("对 " + targetClass.getName() + " 的 " + method.getName() + " " +
                "做静态切点检查。");
        return "rent".equals(method.getName());
    }

    /**
     * 对方法进行动态切点检查
     * @param method
     * @param targetClass
     * @param args
     * @return
     */
    public boolean matches(Method method, Class<?> targetClass, Object... args) {
        System.out.println("对 " + targetClass.getName() + " 的 " + method.getName() + " " +
                "做动态切点检查。");
        String userId = (String) args[0];
        return SPECIAL_USER_IDS.contains(userId);
    }
}

RentDynamicPointcut 类既有对方法进行静态切点检查,又有对方法进行动态切点检查 。

由于动态切点检查会对性能造成很大的影响,所以应当尽量避免在运行时每次都对目标类的各个方法进行动态检查 。

Spring 在创建代理时对目标类的每个连接点都使用静态切点检查,如果仅通过静态切点检查发现连接点不匹配,那么在运行时就不再进行动态切点检查 ;如果在静态切点检查发现连接点匹配,则再进行动态切点检查。

配置:

<!-- 动态切面-->
<bean id="dynamicAdvisor"
      class="org.springframework.aop.support.DefaultPointcutAdvisor">
    <property name="pointcut">
        <bean class="net.deniro.spring4.aop.RentDynamicPointcut"/>
    </property>
    <property name="advice">
        <bean class="net.deniro.spring4.aop.RentBeforeAdvice"/>
    </property>
</bean>

<!-- 代理-->
<bean id="userProxy3"
      class="org.springframework.aop.framework.ProxyFactoryBean"
      p:interceptorNames="dynamicAdvisor"
      p:target-ref="user"
      p:proxyTargetClass="true"/>

单元测试:

User user = (User) context.getBean("userProxy3");
String userId="001";
user.rent(userId);
userId="002";
user.rent(userId);

输出结果:

对 net.deniro.spring4.aop.User 做静态切点检查。
对 net.deniro.spring4.aop.User 的 rent 做静态切点检查。
对 net.deniro.spring4.aop.User 做静态切点检查。
对 net.deniro.spring4.aop.User 的 toString 做静态切点检查。
对 net.deniro.spring4.aop.User 做静态切点检查。
对 net.deniro.spring4.aop.User 的 clone 做静态切点检查。
对 net.deniro.spring4.aop.User 做静态切点检查。
对 net.deniro.spring4.aop.User 的 rent 做静态切点检查。
对 net.deniro.spring4.aop.User 的 rent 做动态切点检查。
准备租赁的用户 ID:001
User:租赁【充电宝】
对 net.deniro.spring4.aop.User 的 rent 做动态切点检查。
User:租赁【充电宝】

从输出中可以看出:


Spring 的静态切面指的是:在生成代理对象时,就确定了是否把增强织入目标类的连接点;而动态切面指的是:在运行期根据方法入参的值,来确定增强是否织入目标类的连接点。这两种切面都是通过动态代理技术实现的。

6 流程切面

Spring 的流程切面是由 DefaultPointcutAdvisor 和 ControlFlowPointcut 实现的 。流程切点指的是由某个方法直接或者间接发起调用的其他方法。

假设我们希望通过一个 UserDelegate 类的某个方法调用 User 中的 rent() 方法与 back() 方法:

public class UserDelegate {
    private User user;

    public void service(String userId) {
        user.rent(userId);
        user.back(userId);
    }

    public void setUser(User user) {
        this.user = user;
    }
}

现在使用流程切面,让 UserDelegate.service 方法内部调用的其他类方法都织入增强:

<!-- 流程切点-->
<bean id="controlFlowPointcut"
      class="org.springframework.aop.support.ControlFlowPointcut">
    <!-- 指定类-->
    <constructor-arg type="java.lang.Class" value="net.deniro.spring4.aop.UserDelegate"/>
    <!-- 指定方法-->
    <constructor-arg type="java.lang.String" value="service"/>
</bean>

<!-- 流程切面-->
<bean id="controlFlowAdvisor"
      class="org.springframework.aop.support.DefaultPointcutAdvisor"
      p:pointcut-ref="controlFlowPointcut"
      p:advice-ref="rentBeforeAdvice"/>

<!-- 代理类-->
<bean id="userProxy4"
      class="org.springframework.aop.framework.ProxyFactoryBean"
      p:interceptorNames="controlFlowAdvisor"
      p:target-ref="user"
      p:proxyTargetClass="true"/>

ControlFlowPointcut 有两个构造函数:

构造函数 说明
ControlFlowPointcut(Class<?> clazz) 指定一个类作为流程切点。
ControlFlowPointcut(Class<?> clazz, String methodName) 指定一个类和一个方法作为流程切点。

单元测试:

User user = (User) context.getBean("userProxy4");

System.out.println("增强前-------------");
String userId = "001";
user.rent(userId);
user.back(userId);

System.out.println("增强后-------------");
UserDelegate delegate = new UserDelegate();
delegate.setUser(user);
delegate.service(userId);

输出结果:

增强前-------------
User:租赁【充电宝】
User:归还【充电宝】
增强后-------------
准备租赁的用户 ID:001
User:租赁【充电宝】
准备租赁的用户 ID:001
User:归还【充电宝】


流程切面和动态切面都需要在运行期进行动态判断 。 于流程切面的代理对象在每次调用目标类方法时,都需要判断方法调用堆栈中是否有符合流程切点要求的方法,所以它对性能的影响也很大。

7 复合切点切面

有时候,一个切点可能难以描述出目标连接点的信息。比如之前流程切点的例子中,我们希望 在 UserDelegate#service() 方法发起的调用并且被调用的方法是 User#rent() 方法时,才织入增强,那么这个切点就是复合切点,因为它是由两个切点共同确定的 。

Spring 的 ComposablePointcut 可以将多个切点以并集或者交集的方式组合起来,从而提供切点之间的复合运算功能 。

ComposablePointcut 实现了 Pointcut 接口,所以它本身也是一个切点,它有以下这些构造函数:

构造函数 说明
ComposablePointcut() 匹配所有类所有方法。
ComposablePointcut(Pointcut pointcut) 匹配特定切点。
ComposablePointcut(ClassFilter classFilter) 匹配特定类所有方法。
ComposablePointcut(MethodMatcher methodMatcher) 匹配所有类特定方法。
ComposablePointcut(ClassFilter classFilter, MethodMatcher methodMatcher) 匹配特定类特定方法。

ComposablePointcut 提供了 3 个交集运算方法:

方法 说明
ComposablePointcut intersection(ClassFilter other) 复合切点和一个 ClassFilter 对象进行交集运算。
ComposablePointcut intersection(MethodMatcher other) 复合切点和一个 MethodMatcher 对象进行交集运算。
ComposablePointcut intersection(Pointcut other) 复合切点和一个切点对象进行交集运算。

ComposablePointcut 还提供了 3 个并集运算方法:

方法 说明
ComposablePointcut union(ClassFilter other) 复合切点和一个 ClassFilter 对象进行并集运算。
ComposablePointcut union(MethodMatcher other) 复合切点和一个 MethodMatcher 对象进行并集运算。
ComposablePointcut union(Pointcut other) 复合切点和一个切点对象进行并集运算。

如果需要对两个切点做交集或并集运算,那么可以使用org.springframework.aop.support.Pointcuts 工具类,它包含以下两个静态方法:

方法 说明
Pointcut union(Pointcut pc1, Pointcut pc2) 对两个切点进行交集运算。
Pointcut intersection(Pointcut pc1, Pointcut pc2) 对两个切点进行并集运算。

定义复合切点:

public class RentComposablePointcut {

    public Pointcut getIntersectionPointcut() {
        ComposablePointcut pointcut = new ComposablePointcut();

        //流程切点
        ControlFlowPointcut pointcut1 = new ControlFlowPointcut(UserDelegate.class, "service");

        //方法名切点
        NameMatchMethodPointcut pointcut2 = new NameMatchMethodPointcut();
        pointcut2.addMethodName("rent");

        //交集操作
        return pointcut.intersection((Pointcut) pointcut1).intersection((Pointcut) pointcut2);
    }
}

配置:

<!-- 复合切点-->
<bean id="composablePointcut" class="net.deniro.spring4.aop.RentComposablePointcut"/>
<!-- 复合切面-->
<bean id="composableAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"
      p:pointcut="#{composablePointcut.intersectionPointcut}"
      p:advice-ref="rentBeforeAdvice"/>

<!-- 代理类-->
<bean id="userProxy5"
      class="org.springframework.aop.framework.ProxyFactoryBean"
      p:interceptorNames="composableAdvisor"
      p:target-ref="user"
      p:proxyTargetClass="true"/>

这里在复合切面中,通过 p:pointcut#{} 引用了 composablePointcut#getIntersectionPointcut() 方法来获得复合切点。

单元测试:

User user = (User) context.getBean("userProxy5");

System.out.println("增强前-------------");
String userId = "001";
user.rent(userId);
user.back(userId);

System.out.println("增强后-------------");
UserDelegate delegate = new UserDelegate();
delegate.setUser(user);
delegate.service(userId);

输出结果:

增强前-------------
User:租赁【充电宝】
User:归还【充电宝】
增强后-------------
准备租赁的用户 ID:001
User:租赁【充电宝】
User:归还【充电宝】

8 引介切面

引介切面是引介增强的封装器,通过引介切面可以很容易的为现有对象添加任何接口的实现。

引介切面类关系图

IntroductionAdvisor 仅有一个类过滤器 ClassFilter,因为引介切面是类级别的。

IntroductionAdvisor 接口的两个实现类:

描述
DefaultIntroductionAdvisor 默认实现类。
DeclareParentsAdvisor 实现使用 AspectJ 语言的 DeclareParent 注解表示的引介切面。

DefaultIntroductionAdvisor 拥有三个构造函数:

构造函数 说明
DefaultIntroductionAdvisor(Advice advice) 通过一个增强创建的引介切面,它将为目标对象中的所有接口的实现方法注入增强。
DefaultIntroductionAdvisor(Advice advice, IntroductionInfo introductionInfo) 通过一个增强和一个 IntroductionInfo 创建引介切面,目标对象需要实现的方法由 IntroductionInfo 对象的 getInterfaces() 方法返回。
DefaultIntroductionAdvisor(DynamicIntroductionAdvice advice, Class<?> intf) 通过增强与指定的接口类来创建引介切面,这个切面将对目标对象的所有接口实现进行增强。

配置:

<!-- 引介切面-->
<bean id="introductionAdvisor"
      class="org.springframework.aop.support.DefaultIntroductionAdvisor">
    <constructor-arg>
        <bean class="net.deniro.spring4.aop.RentBeforeAdvice"/>
    </constructor-arg>
</bean>
<!-- 代理类-->
<bean id="userProxy6"
      class="org.springframework.aop.framework.ProxyFactoryBean"
      p:interceptorNames="introductionAdvisor"
      p:target-ref="user"
      p:proxyTargetClass="true"/>

单元测试:

User user = (User) context.getBean("userProxy6");
String userId = "001";
user.rent(userId);
user.back(userId);

输出结果:

准备租赁的用户 ID:001
User:租赁【充电宝】
准备租赁的用户 ID:001
User:归还【充电宝】

从输出结果中可以看出,引介切面对所有的方法都实施了增强【因为使用了
DefaultIntroductionAdvisor(Advice advice) 构造函数】。

上一篇下一篇

猜你喜欢

热点阅读