Spring 创建切面

版权声明:本文为博主原创文章,转载请指明文章出处! https://blog.csdn.net/u012385190/article/details/81952302

目录

 

1、概述

2、切点类型

3、切面类型

4、静态普通方法名匹配切面

5、静态正则表达式方法匹配切面

6、动态切面

7、流程切面

8、复合切点切面

9、总结


1、概述

在前面介绍各类增强时,大家可能没有注意到一个问题:增强被织入到目标类的所有方法中。假设我们希望有选择地织入到目标类某些特定的方法中,就需要使用切点进行目标连接点的定位了。描述连接点是进行AOP编程最主要的一项工作。增强提供了连接点方位信息:如织入到方法前面、后面等,而切点进一步描述织入到哪些类的哪些方法上。

Spring通过org.springframework.aop.Pointcut接口描述切点,Pointcut由ClassFilter和MethodMatcher构成。它通过ClassFilter定位到某些特定类上,通过MethodMatcher定位到某些特定方法上,这样Pointcut就拥有了描述某些类的某些特定方法的能力。Pointcut接口如下:

public interface Pointcut {
    Pointcut TRUE = TruePointcut.INSTANCE;
    //切点定位到特定类上
    ClassFilter getClassFilter();
    //切点定位到特定方法上
    MethodMatcher getMethodMatcher();
}

含义:切面由切点和增强(引介)组成,它既包括了横切逻辑的定义,也包括了连接点的定义,Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。

2、切点类型

Spring提供了六种类型切点:

静态方法切点:org.springframework.aop.support.StaticMethodMatcherPointcut是静态方法切点的抽象基类,默认情况下它匹配所有的类。 StaticMethodMatcherPointcut包括 两个主要的子类,分别是NameMatchMethodPointcut和AbstractRegexpMethodPointcut,前者提供简单字符串匹配方法签名,而后者使用正则表达式匹配方法签名;

动态方法切点:org.springframework.aop.support.DynamicMethodMatcherPointcut是动态方法切点的抽象基类,默认情况下它匹配所有的类。 DynamicMethodMatcherPointcut类已经过时,可以使用DefaultPointcutAdvisor和DynamicMethodMatcherPointcut动态方法匹配器替代之;

注解切点:org.springframework.aop.support.annotation.AnnotationMatchingPointcut实现类表示注解切点;

表达式切点:org.springframework.aop.support.ExpressionPointcut接口主要是为了支持AspectJ切点表达式语法而定义的接口;

流程切点:org.springframework.aop.support.ControlFlowPointcut实现类表示控制流程切点。ControlFlowPointcut是一种特殊的切点,它根据程序执行堆栈的信息查看目标方法是否由某一个方法直接或间接发起调用,以此判断是否为匹配的连接点;

复合切点:org.springframework.aop.support.ComposablePointcut实现类是为创建多个切点而提供的方便操作类。它所有的方法都返回ComposablePointcut类,这样,我们就可以使用链接表达式对其进行操作,形如:Pointcut pc=new ComposablePointcut().union(classFilter).intersection(methodMatcher).intersection(pointcut)

3、切面类型

切面就是由切点和增强组成。由于增强既包含横切代码,又包含部分的连接点信息(方法前、方法后等的方位信息),所以我们可以仅通过增强类生成一个切面。但切点仅代表目标类连接点的部分信息(类和方法的定位),所以仅有切点,我们无法制作出一个切面,必须结合增强才能制作出切面。

Spring使用org.springframework.aop.Advisor接口表示切面的概念,一个切面同时包含横切代码和连接点信息。

切面可以分为三类:一般切面、切点切面、引介切面,可以通过Spring定义的切面接口清楚的了解切面的分类。类图如下:

Advisor:代表一般切面,它仅包含一个Advice,我们说过,因为Advice包含了横切代码和连接点的信息,所以Advice本身就是一个简单的切面,只不过它代表的横切的连接点是所有目标类的所有方法,因为这个横切面太宽泛,所以一般不会直接使用;

PointcutAdvisor:代表具有切点的切面,它包含Advice和Pointcut两个类,这样我们就可以通过类、方法名以及方法方位等信息灵活地定义切面的连接点,提供更具适用性的切面;

IntroductionAdvisor:代表引介切面,是对应引介增强的特殊切面,它应用于类层面上;

下面我们分析切点切面接口PointcutAdvisor,主要有6个具体的实现类,分别介绍如下:

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

2、NameMatchMethodPointcutAdvisor:通过该类可以定义按方法名定义切点的切面;

3、RegexpMethodPointcutAdvisor:对于按正则表达式匹配方法名进行切点定义的切面,该类已经是功能齐备的实现类,一般情况下无须扩展该类,直接配置即可使用;

4、StaticMethodMatcherPointcutAdvisor:静态方法匹配器切点定义的切面,默认情况下,匹配所有的目标类;

5、AspecJExpressionPointcutAdvisor:用于Aspecj切点表达式定义切点的切面;

6、AspecJPointcutAdvisor:用于AspecJ语法定义切点的切面。

下面具体讲解PointcutAdvisor下常用的几个类 。

4、静态普通方法名匹配切面

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

业务类Waiter

/**
 * 服务员业务类
 */
public class Waiter {
    public void greetTo(String name) {
        System.out.println("Waiter Greet to " + name);
    }

    public void serverTo(String name) {
        System.out.println("Waiter Server to " + name);
    }
}

业务类Seller

public class Seller {
    /**
     * 和Waiter类中的同名的方法,目的是为了验证仅仅织入了Waiter类中的greetTo方法
     */
    public void greetTo(String name) {
        System.out.println("Seller Greet to " + name);
    }
}

现在我们希望通过StaticMethodMatcherPointcutAdvisor定义一个切面,在Waiter#greetTo()方法调用前织入一个增强,即连接点为Waiter#greetTo()方法调用前的位置。

/**
 * 静态普通方法名匹配切面
 */
public class GreetingAdvisor extends StaticMethodMatcherPointcutAdvisor {
    /**
     * 重写matches方法,切点方法匹配规则:方法名为greetTo
     */
    public boolean matches(Method method, Class<?> aClass) {
        return "greetTo".equals(method.getName());
    }

    /**
     * 默认情况下,匹配所有的类,重写getClassFilter,定义匹配规则切点类型匹配规则,为Waiter的类或者之类
     */
    public ClassFilter getClassFilter() {
        return new ClassFilter() {
            public boolean matches(Class<?> clazz) {
                return Waiter.class.isAssignableFrom(clazz);
            }
        };
    }
}

StaticMethodMatcherPointcutAdvisor 抽象类唯一需要定义的是matches()方法,在默认情况下,该切面匹配所有的类,这里通过覆盖getClassFilter()方法,让它仅匹配Waiter类及其子类。

当然,Advisor还需要一个增强类的配合,我们来定义一个前置增强

/**
 * 前置增强
 */
public class GreetBeforeAdivce implements MethodBeforeAdvice {
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        // 输出切点
        String clientName = (String) objects[0];
                System.out.println("How are you " + clientName + " ?(切点方法为:" + method + ")");
    }
}

我们使用Spring配置来定义切面等信息

<!-- 配置切面:静态方法匹配切面 start -->
    <!-- Waiter目标类 -->
    <bean id="waiterTarget" class="demo04.advisor.Waiter"/>
    <!-- Seller目标类 -->
    <bean id="sellerTarget" class="demo04.advisor.Seller"/>

    <!-- 前置增强 -->
    <bean id="greetBeforeAdvice" class="demo04.advisor.GreetBeforeAdivce"/>

    <!-- 切面 -->
    <bean id="greetAdvicesor" class="demo04.advisor.GreetingAdvisor"
          p:advice-ref="greetBeforeAdvice"/> <!-- 向切面注入一个前置增强 -->

    <!-- 通过父bean,配置公共的信息 -->
    <bean id="parent" abstract="true"
          class="org.springframework.aop.framework.ProxyFactoryBean"
          p:interceptorNames="greetAdvicesor"
          p:proxyTargetClass="true"/>

    <!-- waiter代理 -->
    <bean id="waiter2" parent="parent" p:target-ref="waiterTarget"/>
    <!-- seller代理 -->
    <bean id="seller" parent="parent" p:target-ref="sellerTarget"/>
    <!-- 配置切面:静态方法匹配切面 end -->

测试类

public class StaticMethodMatcherPointcutAdvisorTest {
    @Test
    public void test() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans/beans.xml");
        Waiter waiter = ctx.getBean("waiter2", Waiter.class);
        Seller seller = ctx.getBean("seller", Seller.class);
        waiter.greetTo("JayChou");
        waiter.serverTo("JayChou");
        seller.greetTo("JayChou");
    }
}

运行结果

How are you JayChou ?(切点方法为:public void demo04.advisor.Waiter.greetTo(java.lang.String))
Waiter Greet to JayChou
Waiter Server to JayChou
Seller Greet to JayChou

我们可以看到切面仅仅织入了Waiter.greetTo()方法调用前的连接点上, Waiter.serverTo()和Seller.greetTo()方法并没有织入切面。

5、静态正则表达式方法匹配切面

在 StaticMethodMatcherPointcutAdvisor中,仅能通过方法名定义切点,这种描述方式不够灵活。假设目标类中有多个方法。且它们都满足一定的命名规范,使用正则表达式进行匹配描述就要灵活多了。RegexpMethodPointcutAdvisor 是正则表达式方法匹配的切面实现类,该类已经是功能齐备的实现类,一般情况下无须扩展该类。如下配置即可

    <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"/>

6、动态切面

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

/**
 * 会先执行静态切点检查,再执行动态切点检查
 */
public class GreetingDynamicPointcut extends DynamicMethodMatcherPointcut {
    private static List<String> specialClientList = asList("JayChou", "LiHong");

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

    /**
     * 对方法进行动态切点检查
     * 如果返回false,那么该切点即不匹配
     * 思考:既然先匹配静态方法,那么为啥还需要动态匹配?
     * 原因:可以在运行期根据方法入参的值来判断增强是否需要织入目标类的连接点上
     */
    public boolean matches(Method method, Class<?> aClass, Object[] objects) {
        String clientName = (String) objects[0];
        System.out.println("调用了" + aClass.getName() + "." + method.getName() + "." + clientName + "做动态检查.");
        return specialClientList.contains(clientName);
    }

}

如上,GreetingDynamicPointcut 类既有用于静态切点检查的方法,又有用于动态检查的方法。由于动态切点检查会对性能造成很大的影响,所以应当尽量避免在运行时每次都对目标类的各个方法进行动态检查。Spring 采用这样的机制:在创建代理时对目标类的每个连接点使用静态切点检查,如果仅通过静态切点检查就可以知道连接点是不匹配的,则在运行时就不再进行动态检查;如果静态切点检查是匹配的,则在运行时才进行动态切点检查。在动态切点类中定义静态切点检查的方法可以避免不必要的动态检查操作,从而极大地提高运行效率。所以如果自定义动态切面,可以先进行静态切点和类检查,再进行动态切点检查会极大的提高效率。

进行xml文件配置,如下

    <!-- 配置切面:动态态方法匹配切面 start -->
    <bean id="dynamicAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="pointcut"> <!--① 注入动态切点-->
            <bean class="demo04.advisor.GreetingDynamicPointcut"/>
        </property>
        <property name="advice"><!--② 注入增强-->
            <bean class="demo04.advisor.GreetBeforeAdivce"/>
        </property>
    </bean>

    <bean id="waiter3" class="org.springframework.aop.framework.ProxyFactoryBean"
          p:target-ref="waiterTarget"
          p:interceptorNames="dynamicAdvisor"
          p:proxyTargetClass="true" />
    <!-- 配置切面:动态态方法匹配切面 end -->

动态切面的配置和静态切面的配置没什么区别。使用 DefaultPointcutAdvisor 定义切面,在 ① 处使用内部 Bean 方式注入动态切点 GreetingDynamicPointcut,在 ② 处注入增强。此外,DefaultPointcutAdvisor 还有一个 order 属性,用于定义切面的织入顺序。

测试类,如下

public class DynamicMethodMatcherPointcutTest {
    @Test
    public void test() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans/beans.xml");
        Waiter waiter = ctx.getBean("waiter3", Waiter.class);
        Seller seller = ctx.getBean("seller", Seller.class);
        waiter.greetTo("JayChou");
        waiter.greetTo("JayChou2");
        waiter.greetTo("JayChou3");
        waiter.serverTo("JayChou");
        waiter.serverTo("JayChou2");
        waiter.serverTo("JayChou3");
        seller.greetTo("LiHong");
    }
}

执行结果

调用了demo04.advisor.Waiter.greetTo做静态检查.
调用了demo04.advisor.Waiter.greetTo.JayChou做动态检查.
How are you JayChou ?(切点方法为:public void demo04.advisor.Waiter.greetTo(java.lang.String))
Waiter Greet to JayChou
调用了demo04.advisor.Waiter.greetTo.JayChou2做动态检查.
Waiter Greet to JayChou2
调用了demo04.advisor.Waiter.greetTo.JayChou3做动态检查.
Waiter Greet to JayChou3
调用了demo04.advisor.Waiter.serverTo做静态检查.
Waiter Server to JayChou
Waiter Server to JayChou2
Waiter Server to JayChou3
Seller Greet to LiHong

通过以上输出信息,发现Spring 会在创建代理织入切面时,对目标类的所有方法进行静态切点检查;在生成织入切面的代理对象后,第一次调用代理类的每一个方法时都会进行一次静态切点检查,以后该方法的调用就不再执行静态切点检查;对于那些在静态切点检查时匹配的方法,在后续调用该方法时,将继续会执行动态切点检查。

动态代理的“动态”是相对于那些编译期生成代理 Class 文件和类加载期生成代理 Class 文件而言的。动态代理是运行时动态产生的代理。在 Spring 中,不管是静态切面还是动态切面,都是通过动态代理技术实现的。所谓静态切面,是指在生成代理对象时就确定了增强是否需要织入目标类的连接点上;而动态切面是指必须在运行期根据方法入参的值来判断增强是否需要织入目标类的连接点上。

7、流程切面

Spring 的流程切面由 DefaultPointcutAdvisor 和 ControlFlowPointcut 实现。流程切点代表由某个方法直接或间接发起调用的其他方法。来看下面的实例,假设通过一个 WaiterDelegate 类代理 Waiter 所有的方法,代码如下:

public class WaiterDelegate {
    private Waiter waiter;

    public void waiterService(String clientName) {
        waiter.greetTo(clientName);
        waiter.serverTo(clientName);
    }

    public void setWaiter(Waiter waiter) {
        this.waiter = waiter;
    }
}

如果希望所有由 WaiterDelegate#waiterService() 方法发起调用的其他方法都织入 GreetingBeforeAdvice 增强,就必须使用流程切面来完成目标。下面使用 DefaultPointcutAdvisor 配置一个流程切面来完成这一需求:

<!-- 配置切面:流程切面配置 start -->
    <!--切点配置-->
    <bean id="controlFlowPointcut" class="org.springframework.aop.support.ControlFlowPointcut">
        <!--指定一个类作为流程切点-->
        <constructor-arg type="java.lang.Class" value="demo04.advisor.WaiterDelegate"/>
        <!--指定一个方法作为流程切点-->
        <constructor-arg type="java.lang.String" value="waiterService"/>
    </bean>
    <!--配置增强-->
    <bean id="controllFlowAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"
          p:pointcut-ref="controlFlowPointcut"
          p:advice-ref="greetBeforeAdvice"/>
    <!--配置代理类-->
    <bean id="waiter4" class="org.springframework.aop.framework.ProxyFactoryBean"
          p:target-ref="waiterTarget"
          p:interceptorNames="controllFlowAdvisor"
          p:proxyTargetClass="true">
    </bean>
    <!-- 配置切面:流程切面配置 end -->

测试类

public class DefaultPointcutAdvisorTest {
    @Test
    public void test() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans/beans.xml");
        Waiter waiter = ctx.getBean("waiter4", Waiter.class);
        waiter.greetTo("JayChou");
        waiter.serverTo("JayChou");
        WaiterDelegate wd = new WaiterDelegate();
        wd.setWaiter(waiter);
        wd.waiterService("LiHong");
    }
}

运行结果

Waiter Greet to JayChou
Waiter Server to JayChou
How are you LiHong ?(切点方法为:public void demo04.advisor.Waiter.greetTo(java.lang.String))
Waiter Greet to LiHong
How are you LiHong ?(切点方法为:public void demo04.advisor.Waiter.serverTo(java.lang.String))
Waiter Server to LiHong

流程切面和动态切面从某种程度上说可以算是一类切面,因为二者都需要在运行期判断动态环境。对于流程切面来说,代理对象在每次调用目标类方法时,都需要判断方法调用堆栈中是否有满足流程切点要求的方法。因此,和动态切面一样,流程切面对性能的影响也很大。

8、复合切点切面

比如上面流程切面的例子中,只需要对WaiterDelegate#waiterService()中的greetTo方法进行切点,用户可以可以通过一个切点同时满足以上两个匹配条件的连接点,而更好的方法是使用Spring提供的ComposablePointcut,通过多个切点的并集或者交集的方式组合起来,提供了切点之间的复合运算。

/**
 * 复合切点切面
 */
public class GreetingComposablePointcut {
    public Pointcut getIntersectionPointcut(){
        // 创建一个复合切点
        ComposablePointcut cp = new ComposablePointcut();
        // 创建一个流程切点
        Pointcut pt1 = new ControlFlowPointcut(WaiterDelegate.class,"waiterService");
        // 创建一个方法名切点
        NameMatchMethodPointcut pt2 = new NameMatchMethodPointcut();
        pt2.addMethodName("greetTo");
        // 将两个切点进行交集操作
        return cp.intersection(pt1).intersection((Pointcut)pt2);
    }
}

xml配置

    <!-- 配置切面:复合切点切面配置 start -->
    <bean id="gcp" class="demo04.advisor.GreetingComposablePointcut" />
    <!-- 引用gcp.getIntersectionPointcut()方法所返回的复合切点 -->
    <bean id="composableAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"
          p:pointcut="#{gcp.intersectionPointcut}"
          p:advice-ref="greetBeforeAdvice" />

    <!-- p:interceptorNames指定使用的复合切面 -->
    <bean id="waiter5" class="org.springframework.aop.framework.ProxyFactoryBean"
          p:interceptorNames="composableAdvisor"
          p:target-ref="waiterTarget"
          p:proxyTargetClass="true" />
    <!-- 配置切面:复合切点切面配置 end -->

测试类

public class GreetingComposablePointcutTest {
    @Test
    public void test() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans/beans.xml");
        Waiter waiter = ctx.getBean("waiter5", Waiter.class);
        waiter.greetTo("JayChou");
        waiter.serverTo("JayChou");
        WaiterDelegate wd = new WaiterDelegate();
        wd.setWaiter(waiter);
        wd.waiterService("LiHong");
    }
}

运行结果

Waiter Greet to JayChou
Waiter Server to JayChou
How are you LiHong ?(切点方法为:public void demo04.advisor.Waiter.greetTo(java.lang.String))
Waiter Greet to LiHong
Waiter Server to LiHong


9、总结

1、Spring通过org.springframework.aop.Pointcut接口描述切点,Pointcut由ClassFilter和MethodMatcher构成。它通过ClassFilter定位到某些特定类上,通过MethodMatcher定位到某些特定方法上,这样Pointcut就拥有了描述某些类的某些特定方法的能力。

2、切点类型共有6种

静态方法切点:org.springframework.aop.support.StaticMethodMatcherPointcut
动态方法切点:org.springframework.aop.support.DynamicMethodMatcherPointcut
注解切点:org.springframework.aop.support.annotation.AnnotationMatchingPointcut
表达式切点:org.springframework.aop.support.ExpressionPointcut
流程切点:org.springframework.aop.support.ControlFlowPointcut
复合切点:org.springframework.aop.support.ComposablePointcut

3、Spring使用org.springframework.aop.Advisor接口表示切面的概念,一个切面同时包含横切代码和连接点信息。
切面可以分为三类:一般切面(Advisor)、切点切面(PointcutAdvisor)、引介切面(IntroductionAdvisor)

4、切点切面接口PointcutAdvisor,主要有6个具体的实现类:

DefaultPointcutAdvisor:最常用的切面类型,它可以通过任意Pointcut和Advice定义一个切面;
NameMatchMethodPointcutAdvisor:通过该类可以定义按方法名定义切点的切面;
RegexpMethodPointcutAdvisor:对于按正则表达式匹配方法名进行切点定义的切面; 
StaticMethodMatcherPointcutAdvisor:静态方法匹配器切点定义的切面,默认情况下,匹配所有的目标类;
AspecJExpressionPointcutAdvisor:用于Aspecj切点表达式定义切点的切面。
AspecJPointcutAdvisor:用于AspecJ语法定义切点的切面。

猜你喜欢

转载自blog.csdn.net/u012385190/article/details/81952302
今日推荐