Spring基础:切面

时间:2024-11-06 07:16:19

前言

在之前的文章中总结了 AOP 在 Spring 中的作用及地位,在分析 AOP 时涉及到切面的内容,这一节详细的分析切面的知识点。

正题

在开始文章前,有几个问题需要思考一下:

  • 切面到底是神马
  • 切面的构成
  • 切面有哪些实现类
  • 切面有哪些类型
1. 切面到底是神马

在 AOP 一文中提到什么是 AOP? AOP 可以使用 “面向切面编程”的译法。那切面就是把非业务逻辑相关的代码抽取出来定位到具体的连接点上的一种实现方式。

2. 切面的构成

切面由切点和增强(引介)组成,它既包括横切逻辑的定义,也包括连接点的定义。

2.1 切点的表示

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

可以看到 ClassFilter 只定义了一个方法 matches(Class clazz),其参数代表一个被检测类,该方法判别被检测的类是否匹配过滤条件。

Spring 支持两种方法匹配器:静态方法匹配器和动态方法匹配器。所谓静态方法匹配器,仅对方法名签名(包括方法名和入参类型及顺序)进行匹配;而动态方法匹配器会在运行期检查方法入参的值。静态匹配仅会判别一次,而动态匹配因为每次调用方法的入参都可能不一样,所以每次调用方法都必须判断,因此,动态匹配对性能的影响很大。一般情况下,动态匹配不常使用。方法匹配器的类型由 isRuntime() 方法的返回值决定,返回 false 表示是静态方法匹配器,返回 true 表示是动态方法匹配器。

Spring提供了6种类型的切点,下面分别针对它们的用途进行介绍。

  • 静态方法切点: 是静态方法切点的抽象基类,默认情况下它匹配所有的类。StaticMethodMatcherPointcut包括两个主要的子类,分别是NameMatchMethodPointcut 和 AbstractRegexpMethodPointcut,前者提供简单字符串匹配方法签名,而后者使用正则表达式匹配方法签名。
  • 动态方法切点: 是动态方法切点的抽象基类,默认情况下它匹配所有的类。
  • 注解切点: 实现类表示注解切点。使用 AnnotationMatchingPointcut 支持在 Bean 中直接通过 Java 5.0 注解标签定义的切点。
  • 流程切点: 实现类表示控制流程切点。ControlFlowPointcut 是一种特殊的切点,它根据程序执行堆栈的信息查看目标方法是否由某一个方法直接或间接发起调用,以此判断是否为匹配的连接点。
3. 切面有哪些实现类

由于增强既包括横切代码,又包括部分连接点信息(方法前、方法后主方位信息),所以可以仅通过增强类生成一个切面。但切面仅代表目标类连接点的部分信息(类和方法的定位),所以仅有切点无法制作出一个切面,必须结合增强才能制作出切面。Spring 使用 接口表示切面的概念,一个切面同时包含横切代码和连接点信息。切面可以分为 3 类:一般切面、切点切面和引介切面,可以通过 Spring 所定义的切面接口清楚地了解切面的分类。

  • Advisor:代表一般切面,仅包含一个 Advice。因为 Advice 包含了横切代码和连接点信息,所以 Advice 本身就是一个简单的切面,只不过它代表的横切的连接点是所有目标类的所有方法,因为这个横切面太广泛,所以一般不会直接使用。
  • PointcutAdvisor:代表具体切点的切面,包含 Advice和 Pointcut 两个类,这样就可以通过类、方法名及方法方位等信息灵活地定义切面的连接点,提供更具适用性的切面。
  • IntroductionAdvisor:代表引介切面,引介切面是对应引介增强的特殊切面,它应用于类层面上,所以引介切点使用 ClassFilter 进行定义。

下面再来看一下 PointcutAdvisor 的主要实现类体系:

PointcutAdvisor 主要有 6 个具体的实现类,分别介绍如下:

  • DefaultPointcutAdvisor:最常用的切面类型,它可以通过任意 Pointcut 和 Advice 定义一个切面,唯一不支持的是引介切面类型,一般可以通过扩展该类实现自定义的切面。
  • NameMatchMethodPointcutAdvisor:通过该类可以定义按方法名定义切点的切面。
  • RegexpMethodPointcutAdvisor:对于按正则表达式匹配方法名进行切点定义的切面,可以通过扩展该实现类进行操作。RegexpMethodPointcutAdvisor允许用户以正则表达式模式串定义方法匹配的切点,其内部通过 JDKRegexpMehtodPointcut 构造出正则表达式方法名切点。
  • StaticMethodMatcherPointcutAdvisor:静态方法匹配器切点定义的切面,默认情况下匹配所有的目标类。
  • AspectJDKExpressionPointcutAdvisor:用于 AspectJ 切点表达式定义切点的切面。
  • AspectJPointcutAdvisor:用于 AspectJ 语法定义切点的切面。

这些 Advisor 的实现类都可以在Pointcut中找到对应物,实际上,它们都是通过扩展对应的 Pointcut 实现类并实现 PointcutAdvisor 接口进行定义的。

4. 切面有哪些类型


4.1 静态普通方法名匹配切面

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

public class Waiter {
    public void greetTo(String name) {
        ("Waiter green to " + name + "...");
    }

    public void serveTo(String name) {
        ("Waiter serving " + name + "...");
    }
}

public class Seller {
    public void greetTo(String name) {
        ("Seller green to " + name + "...");
    }
}

Waiter和Seller拥有相同的greetTo方法,希望通过 StaticMethodMatcherPointcutAdvisor 定义一个切面,在 Waiter##greetTo 方法调用前织入一个增强。

public class GreetingAdvisor extends StaticMethodMatcherPointcutAdvisor {
    public boolean matches(Method method, Class<?> aClass) { //①
        return "greetTo".equals(());
    }

    @Override
    public ClassFilter getClassFilter() { //②
        return new ClassFilter() {
            public boolean matches(Class<?> aClass) {
                return (aClass);
            }
        };
    }
}

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

当然,Advisor 还需要一个增强类的配合。接下类定义一个前置增强:

public class GreetingBeforeAdvice implements MethodBeforeAdvice {
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        String clientName = (String) objects[0];
        ("how are you! Mr." + clientName);
    }
}

下面使用 Spring 配置来定义切面:

<bean  class=""/>
<bean  class=""
      p:advice-ref="greetingAdvice"/>①

<bean  class=""/>
<bean  class=""/>

<bean  abstract="true"
      class=""
      p:interceptorNames="greetingAdvisor"
      p:proxyTargetClass="true"/>

<bean  parent="parent" p:target-ref="waiterTarget"/>
<bean  parent="parent" p:target-ref="sellerTarget"/>
在 ① 处将 greetingAdvice 增强装配到 greetingAdvisor 切面中。StaitcMatcherPointcutAdvisor 除具有 advice 属性外,还可以定义另外两个属性。
  • classFilter:类匹配过滤器,在 GreetingAdvisor 中用编码的方式设定了 classFilter。
  • order:切面织入时的顺序,该属性用于定义 Ordered 接口表示的顺序。

由于需要分别为 waiter 和 seller 两个 Bean 定义代理器,且两者有很多公共的配置信息,所以使用一个父 <bean> 简化配置,通过引用父 <bean> 轻松地定义两个织入切面的代理。

运行结果:

how are you! 
Waiter green to John...

Seller green to John...
4.2 静态正则表达式方法匹配切面

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

<bean  class=""
      p:advice-ref="greetingAdvice">
    <property name="patterns">
        <list>
            <value>.*greetTo.*</value>②
        </list>
    </property>
</bean>

<bean  class=""
      p:target-ref="waiterTarget"
      p:interceptorNames="regexpAdvisor"
      p:proxyTargetClass="true">
</bean>

在 ② 处定义了一个匹配模式串“.*greet.*”,该模式匹配 () 方法。值得注意的是,匹配模式串的是目标方法的全限定名,即带类名的方法名。

运行结果:

how are you! 
Waiter green to John...

Waiter serving John...

可见 () 方法被织入了切面,而 () 方法没有被织入切面。除了使用 patterns 和 advice 属性外,还有另外两个属性,分别介绍如下:

  • pattern:如果只有一个匹配模式,则可以使用该属性进行配置。patterns属性用于定义多个匹配模式串,这些匹配模式串之间是“或”的关系。
  • order:切面在织入时对应的顺序。

只要程序的类包具有良好的命名规范,就可以使用简单的正则表达式描述出目标方法。由于需要使用全限定名来定义方法名,所以不但方法名需要具有良好的规范性,包名也需要具体良好的规范性。对包名、类名、方法名按其功用进行规范命名并不是一件坏事,相反,规范命名可以增强程序的可读性和团队开发的协作性,降低沟通成本,是值得实践和提倡的编程方法。

4.3 动态切面

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

public class GreetingDynamicPointcut extends DynamicMethodMatcherPointcut {
    private static List<String> specialClientList = new ArrayList<String>();
    static {
        ("John");
        ("Tom");
    }

    @Override
    public ClassFilter getClassFilter() {
        return new ClassFilter() {
            public boolean matches(Class<?> aClass) {
                ("调用getClassFilter()对" + () + "做静态检查.");
                return (aClass);
            }
        };
    }

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

    /**
     * 对方法进行动态切点检查
     */
    public boolean matches(Method method, Class<?> aClass, Object[] objects) {
        ("调用matches(method, class)" + () + "." + () + "做动态检查.");
        String clientName = (String) objects[0];
        return (clientName);
    }
}

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

在编写好动态切点后,就可以着手在Spring配置文件中装配出一个动态切面,代码如下:

<bean  class="">
    <property name="pointcut">
        <bean class=""/>①
    </property>
    <property name="advice">
        <bean class=""/>②
    </property>
</bean>

<bean  class=""
      p:target-ref="waiterTarget"
      p:interceptorNames="dynamicAdvisor"
      p:proxyTargetClass="true">
</bean>

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

接下来定义一个测试类:

public class BeanFactoryTest {

    @Test
    public void getBean() throws IOException {
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] resource = ("classpath:com/smart/");

        DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
        (resource);
        Waiter waiter = (Waiter) ("waiter");
        ("John");
        ("Pitty");
        ("John");
    }
}

运行结果:

调用getClassFilter()对做静态检查.
调用matches(method, class)做静态检查.
调用getClassFilter()对做静态检查.
调用matches(method, class)做静态检查.
调用getClassFilter()对做静态检查.
调用matches(method, class)做静态检查.
调用getClassFilter()对做静态检查.
调用matches(method, class)做静态检查.
调用getClassFilter()对做静态检查.

调用matches(method, class)做静态检查.
调用matches(method, class)做动态检查.
how are you! 
Waiter green to John...

调用getClassFilter()对做静态检查.
调用matches(method, class)做静态检查.
Waiter serving John...

调用matches(method, class)做动态检查.
Waiter green to Pitty...
Waiter serving Pitty...

通过以上输出信息,对照 DynamicMethodMatcherPointcut 切点类,可以很容易发现,Spring 会在创建代理织入切面时,对目标类的所有方法进行静态切点检查;在生成织入切面的代理对象后,第一次调用代理类的每一个方法时都会进行一次静态切点检查,如果本次检查就能从候选者列表中将该方法排除,则以后对该方法的调用就不再执行静态切点检查;对于那些在静态切点检查时匹配的方法,在后续调用该方法时,将执行动态切点检查。

如果将 getClassFilter() 方法和 matches(Method, Class) 方法注释掉,重新运行代码结果如下:

调用matches(method, class)做动态检查.
how are you! 
Waiter green to John...
调用matches(method, class)做动态检查.
how are you! 
Waiter serving John...
调用matches(method, class)做动态检查.
Waiter green to Pitty...
调用matches(method, class)做动态检查.
Waiter serving Pitty...

可以发现,每次调用代理对象的任何一个方法,都会执行动态切点检查,这将导致很大的性能问题。所以,在定义动态切点时,切勿忘记同时覆盖 getClassFilter() 方法和 matches(Method, Class) 方法,通过静态切点检查排除大部分方法。

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

4.4 流程切面

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

public class WaiterDelegate {
    private Waiter waiter;

    public void service(String clientName) {
        (clientName);
        (clientName);
    }

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

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

<bean  class=""/>
<bean  class=""/>
<bean  class="">
    <constructor-arg type="" value=""/>
    <constructor-arg type="" value="service"/>
</bean>

<bean  class=""
      p:pointcut-ref="controlFlowPointcut"
      p:advice-ref="greetingAdvice"/>

<bean  class=""
      p:target-ref="waiterTarget"
      p:interceptorNames="controllFlowAdvisor"
      p:proxyTargetClass="true">
</bean>

ControlFlowPointcut 有两个构造函数,分别是 ControlFlowPointcut(Class clazz) 和 ControlFlowPointcut(Class clazz, String methodName)。第一个构造函数指定一个类作为流程切点;而第二个构造函数指定一个类和一个方法作为流程切点。在这里,指定 WaiterDelegate#service() 方法作为切点,表示所有通过该方法直接或间接发起的调用匹配切点。

PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resource = ("classpath:com/smart/");

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
(resource);
Waiter waiter = (Waiter) ("waiter");
("John");

WaiterDelegate wd = new WaiterDelegate();
(waiter);
("John");

运行上面的代码,在控制台输出一下信息:

Waiter green to John...①
②
how are you! 
Waiter green to John...
how are you! 
Waiter serving John...

① 处的信息直接调用 greetTo() 方法的输出,此时增强没有起作用;② 处通过 WaiterDelegate#service() 调用 Waiter 的 greetTo() 和 serveTo() 方法输出,这时发现 Waiter 的两个方法都织入了增强。

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

4.5 引介切面

引介切面是引介增强的封装器,通过引介切面,可以更容易地为现有对象添加任何接口的实现。下图是引介增强的类继承关系图:


IntroductionAdvisor 接口同时继承 Advisor 和 IntroductionInfo 接口,IntroductionInfo 接口描述了目标类需要实现的新接口。IntroductionAdvisor 和 PointcutAdvisor 接口不同,它仅有一个类过滤器 ClassFilter 而没有 MethodMatcher,这是因为引介切面的切点是类级别的,而 Pointcut 的切点是方法级别的。

IntroductionAdvisor 有两个实现类,分别是 DefaultIntroductionAdvisor 和 DeclareParentsAdvisor,前者是引介切面最常用的实现类,后者用于实现使用 AspectJDK 语言的 DeclareParent 注解表示的引介切面。

下面通过 DefaultIntroductionAdvisor 来增加引介增强配置切面,会发现这种方式比前面的方式更简洁、更清晰。

<bean  class="">
    <constructor-arg>
        <bean class=""/>
    </constructor-arg>
</bean>

<bean  class=""
      p:target-ref="waiterTarget"
      p:interceptorNames="introductionAdvisor"
      p:proxyTargetClass="true"/>
虽然引介切面和其他切面由很多的不同,但却可以采用相似的Spring配置方式配置引介切面。