`
D-tune
  • 浏览: 77709 次
  • 性别: Icon_minigender_1
  • 来自: 上海浦东
文章分类
社区版块
存档分类
最新评论

Spring教程-第 5 章 Spring AOP: Spring之面向方面编程

阅读更多

5.1. 概念

面向方面编程 (AOP) 提供从另一个角度来考虑程序结构以完善面向对象编程(OOP)。 面向对象将应用程序分解成 各个层次的对象,而AOP将程序分解成各个方面 或者说 关注点 。 这使得可以模块化诸如事务管理等这些横切多个对象的关注点。(这些关注点术语称作 横切关注点。)

Spring的一个关键组件就是AOP框架。 Spring IoC容器(BeanFactory 和ApplicationContext)并不依赖于AOP, 这意味着如果你不需要使用,AOP你可以不用,AOP完善了Spring IoC,使之成为一个有效的中间件解决方案,。

AOP在Spring中的使用:

  • 提供声明式企业服务,特别是作为EJB声明式服务的替代品。这些服务中最重要的是 声明式事务管理,这个服务建立在Spring的事务管理抽象之上。
  • 允许用户实现自定义的方面,用AOP完善他们的OOP的使用。

这样你可以把Spring AOP看作是对Spring的补充,它使得Spring不需要EJB就能提供声明式事务管理;或者 使用Spring AOP框架的全部功能来实现自定义的方面。

如果你只使用通用的声明式服务或者预先打包的声明式中间件服务如pooling,你可以不直接使用 Spring AOP,并且跳过本章的大部分内容.

5.1.1. AOP概念

让我们从定义一些重要的AOP概念开始。这些术语不是Spring特有的。不幸的是,Spring的术语 不是特别地直观。而且,如果Spring使用自己的术语,这将使人更加迷惑。

  • 方面(Aspect): 一个关注点的模块化,这个关注点实现可能 另外横切多个对象。事务管理是J2EE应用中一个很好的横切关注点例子。方面用Spring的 Advisor或拦截器实现。
  • 连接点(Joinpoint): 程序执行过程中明确的点,如方法的调 用或特定的异常被抛出。
  • 通知(Advice): 在特定的连接点,AOP框架执行的动作。各种类 型的通知包括“around”、“before”和“throws”通知。通知类型将在下面讨论。许多AOP框架 包括Spring都是以拦截器做通知模型,维护一个“围绕”连接点的拦截器 链。
  • 切入点(Pointcut): 指定一个通知将被引发的一系列连接点 的集合。AOP框架必须允许开发者指定切入点:例如,使用正则表达式。
  • 引入(Introduction): 添加方法或字段到被通知的类。 Spring允许引入新的接口到任何被通知的对象。例如,你可以使用一个引入使任何对象实现 IsModified接口,来简化缓存。
  • 目标对象(Target Object): 包含连接点的对象。也被称作 被通知被代理对象。
  • AOP代理(AOP Proxy): AOP框架创建的对象,包含通知。 在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
  • 织入(Weaving): 组装方面来创建一个被通知对象。这可以在编译时 完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样, 在运行时完成织入。

各种通知类型包括:

  • Around通知: 包围一个连接点的通知,如方法调用。这是最 强大的通知。Aroud通知在方法调用前后完成自定义的行为。它们负责选择继续执行连接点或通过 返回它们自己的返回值或抛出异常来短路执行。
  • Before通知: 在一个连接点之前执行的通知,但这个通知 不能阻止连接点前的执行(除非它抛出一个异常)。
  • Throws通知: 在方法抛出异常时执行的通知。Spring提供 强类型的Throws通知,因此你可以书写代码捕获感兴趣的异常(和它的子类),不需要从Throwable 或Exception强制类型转换。
  • After returning通知: 在连接点正常完成后执行的通知, 例如,一个方法正常返回,没有抛出异常。

Around通知是最通用的通知类型。大部分基于拦截的AOP框架,如Nanning和JBoss4,只提供 Around通知。

如同AspectJ,Spring提供所有类型的通知,我们推荐你使用最为合适的通知类型来实现需 要的行为。例如,如果只是需要用一个方法的返回值来更新缓存,你最好实现一个after returning 通知而不是around通知,虽然around通知也能完成同样的事情。使用最合适的通知类型使编程模型变 得简单,并能减少潜在错误。例如你不需要调用在around通知中所需使用的的MethodInvocation的 proceed()方法,因此就调用失败。

切入点的概念是AOP的关键,使AOP区别于其它使用拦截的技术。切入点使通知独立于OO的 层次选定目标。例如,提供声明式事务管理的around通知可以被应用到跨越多个对象的一组方法上。 因此切入点构成了AOP的结构要素。

5.1.2. Spring AOP的功能

Spring AOP用纯Java实现。它不需要特别的编译过程。Spring AOP不需要控制类装载器层次, 因此适用于J2EE web容器或应用服务器。

Spring目前支持拦截方法调用。成员变量拦截器没有实现,虽然加入成员变量拦截器支持并不破坏 Spring AOP核心API。

成员变量拦截器在违反OO封装原则方面存在争论。我们不认为这在应用程序开发中是明智的。如 果你需要使用成员变量拦截器,考虑使用AspectJ。

Spring提供代表切入点或各种通知类型的类。Spring使用术语advisor来 表示代表方面的对象,它包含一个通知和一个指定特定连接点的切入点。

各种通知类型有MethodInterceptor (来自AOP联盟的拦截器API)和定义在org.springframework.aop包中的 通知接口。所有通知必须实现org.aopalliance.aop.Advice标签接口。 取出就可使用的通知有 MethodInterceptorThrowsAdviceBeforeAdviceAfterReturningAdvice。我们将在下面详细讨论这些通知类型。

Spring实现了AOP联盟的拦截器接口( http://www.sourceforge.net/projects/aopalliance). Around通知必须实现AOP联盟的org.aopalliance.intercept.MethodInterceptor 接口。这个接口的实现可以运行在Spring或其他AOP联盟兼容的实现中。目前JAC实现了AOP联盟的接 口,Nanning和Dynaop可能在2004年早期实现。

Spring实现AOP的途径不同于其他大部分AOP框架。它的目标不是提供及其完善的AOP实现( 虽然Spring AOP非常强大);而是提供一个和Spring IoC紧密整合的AOP实现,帮助解决企业应用 中的常见问题。因此,例如Spring AOP的功能通常是和Spring IoC容器联合使用的。AOP通知是用普通 的bean定义语法来定义的(虽然可以使用"autoproxying"功能);通知和切入点本身由Spring IoC 管理:这是一个重要的其他AOP实现的区别。有些事使用Spring AOP是无法容易或高效地实现,比如通知 非常细粒度的对象。这种情况AspectJ可能是最合适的选择。但是,我们的经验是Spring针对J2EE应 用中大部分能用AOP解决的问题提供了一个优秀的解决方案。

5.1.3. Spring中AOP代理

Spring默认使用JDK动态代理实现AOP代理。这使得任何接口或 接口的集合能够被代理。

Spring也可以是CGLIB代理。这可以代理类,而不是接口。如果业务对象没有实现一个接口, CGLIB被默认使用。但是作为一针对接口编程而不是类编程良好实践,业务对象 通常实现一个或多个业务接口。

也可以强制使用CGLIB:我们将在下面讨论,并且会解释为什么你会要这么做。

Spring 1.0后,Spring可能提供额外的AOP代理的类型,包括完全生成的类。这将不会影响 编程模型。

5.2. Spring的切入点

让我们看看Spring如何处理切入点这个重要的概念。

5.2.1. 概念

Spring的切入点模型能够使切入点独立于通知类型被重用。 同样的切入点有可能接受不同的 通知。

org.springframework.aop.Pointcut 接口是重要的接口, 用来指定通知到特定的类和方法目标。完整的接口定义如下:

public interface Pointcut {

    ClassFilter getClassFilter();

    MethodMatcher getMethodMatcher();

}

Pointcut接口分成两个部分有利于重用类和方法的匹配部分,并且组合细粒度的 操作(如和另一个方法匹配器执行一个”并“的操作)。

ClassFilter接口被用来将切入点限制到一个给定的目标类的集合。 如果matches()永远返回true,所有的目标类都将被匹配。

public interface ClassFilter {

    boolean matches(Class clazz);
}

MethodMatcher接口通常更加重要。完整的接口如下:

public interface MethodMatcher {

    boolean matches(Method m, Class targetClass);

    boolean isRuntime();

    boolean matches(Method m, Class targetClass, Object[] args);
}

matches(Method, Class) 方法被用来测试这个切入点是否匹 配目标类的给定方法。这个测试可以在AOP代理创建的时候执行,避免在所有方法调用时都需要进行 测试。如果2个参数的匹配方法对某个方法返回true,并且MethodMatcher的 isRuntime()也返回true,那么3个参数的匹配方法将在每次方法调用的时候被调用。这使 切入点能够在目标通知被执行之前立即查看传递给方法调用的参数。

大部分MethodMatcher都是静态的,意味着isRuntime()方法 返回false。这种情况下3个参数的匹配方法永远不会被调用。

如果可能,尽量使切入点是静态的,使当AOP代理被创建时,AOP框架能够缓存切入点的 测试结果。

5.2.2. 切入点的运算

Spring支持的切入点的运算有: 值得注意的是

并表示只要任何一个切入点匹配的方法。

交表示两个切入点都要匹配的方法。

并通常比较有用。

切入点可以用org.springframework.aop.support.Pointcuts 类的静态方法来组合,或者使用同一个包中的ComposablePointcut类。

5.2.3. 实用切入点实现

Spring提供几个实用的切入点实现。一些可以直接使用。另一些需要子类化来实现应用相 关的切入点。

5.2.3.1. 静态切入点

静态切入点只基于方法和目标类,而不考虑方法的参数。静态切入点足够满足大多数情况 的使用。Spring可以只在方法第一次被调用的时候计算静态切入点,不需要在每次方法调用 的时候计算。

让我们看一下Spring提供的一些静态切入点的实现。

5.2.3.1.1. 正则表达式切入点

一个很显然的指定静态切入点的方法是正则表达式。除了Spring以外,其它的AOP框架也实 现了这一点。org.springframework.aop.support.RegexpMethodPointcut 是一个通用的正则表达式切入点,它使用Perl 5的正则表达式的语法。

使用这个类你可以定义一个模式的列表。如果任何一个匹配,那个切入点将被计算成 true。(所以结果相当于是这些切入点的并集)。

用法如下:

<bean id="settersAndAbsquatulatePointcut" 
    class="org.springframework.aop.support.RegexpMethodPointcut">
    <property name="patterns">
        <list>
            <value>.*get.*</value>
            <value>.*absquatulate</value>
        </list>
    </property>
</bean>

RegexpMethodPointcut一个实用子类, RegexpMethodPointcutAdvisor, 允许我们同时引用一个通知。 (记住通知可以是拦截器,before通知,throws通知等等。)这简化了bean的装配,因为一个bean 可以同时当作切入点和通知,如下所示:

<bean id="settersAndAbsquatulateAdvisor" 
    class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
    <property name="interceptor">
        <ref local="beanNameOfAopAllianceInterceptor"/>
    </property>
    <property name="patterns">
        <list>
            <value>.*get.*</value>
            <value>.*absquatulate</value>
        </list>
    </property>
</bean>

RegexpMethodPointcutAdvisor可以用于任何通知类型。

RegexpMethodPointcut类需要Jakarta ORO正则表达式包。
5.2.3.1.2. 属性驱动的切入点

一类重要的静态切入点是元数据驱动的 切入点。 它使用元数据属性的值:典型地,使用源代码级元数据。

5.2.3.2. 动态切入点

动态切入点的演算代价比静态切入点高的多。它们不仅考虑静态信息,还要考虑方法的 参数。这意味着它们必须在每次方法调用的时候都被计算;并且不能缓存结果 ,因为参数是变化的。

这个主要的例子就是控制流切入点。

5.2.3.2.1. 控制流切入点

Spring的控制流切入点概念上和AspectJ的cflow 切入点一致,虽然没有其那么强大(当前没有办法指定一个切入点在另一个切入点后执行)。 一个控制流切入点匹配当前的调用栈。例如,连接点被 com.mycompany.web包或者 SomeCaller类中一个方法调用的时候,触发该切入点。控制流切入点的实现类是 org.springframework.aop.support.ControlFlowPointcut

注意
[Note]
控制流切入点是动态切入点中计算代价最高的。Java 1.4中, 它的运行开销是其他动态切入点的5倍。在Java 1.3中则超过10倍。

5.2.4. 切入点超类

Spring提供非常实用的切入点的超类帮助你实现你自己的切入点。

因为静态切入点非常实用,你很可能子类化StaticMethodMatcherPointcut,如下所示。 这只需要实现一个抽象方法(虽然可以改写其它的方法来自定义行为)。

class TestStaticPointcut extends StaticMethodMatcherPointcut {

    public boolean matches(Method m, Class targetClass) {
        // return true if custom criteria match
    }
}

当然也有动态切入点的超类。

Spring 1.0 RC2或以上版本,自定义切入点可以用于任何类型的通知。

5.2.5. 自定义切入点

因为Spring中的切入点是Java类,而不是语言特性(如AspectJ),因此可以定义自定义切入点, 无论静态还是动态。但是,没有直接支持用AspectJ语法书写的复杂的切入点表达式。不过, Spring的自定义切入点也可以任意的复杂。

后续版本的Spring可能象JA一样C提供”语义切入点“的支持:例如,“所有更改目标对象 实例变量的方法”。

5.3. Spring的通知类型

现在让我们看看Spring AOP是如何处理通知的。

5.3.1. 通知的生命周期

Spring的通知可以跨越多个被通知对象共享,或者每个被通知对象有自己的通知。这分别对应 per-classper-instance 通知。

Per-class通知使用最为广泛。它适合于通用的通知,如事务adisor。它们不依赖被代理 的对象的状态,也不添加新的状态。它们仅仅作用于方法和方法的参数。

Per-instance通知适合于导入,来支持混入(mixin)。在这种情况下,通知添加状态到 被代理的对象。

可以在同一个AOP代理中混合使用共享和per-instance通知。

5.3.2. Spring中通知类型

Spring提供几种现成的通知类型并可扩展提供任意的通知类型。让我们看看基本概念和 标准的通知类型。

5.3.2.1. Interception around advice

Spring中最基本的通知类型是interception around advice .

Spring使用方法拦截器的around通知是和AOP联盟接口兼容的。实现around通知的 类需要实现接口MethodInterceptor:

public interface MethodInterceptor extends Interceptor {
  
    Object invoke(MethodInvocation invocation) throws Throwable;
}

invoke()方法的MethodInvocation 参数暴露将被调用的方法、目标连接点、AOP代理和传递给被调用方法的参数。 invoke()方法应该返回调用的结果:连接点的返回值。

一个简单的MethodInterceptor实现看起来如下:

public class DebugInterceptor implements MethodInterceptor {

    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("Before: invocation=[" + invocation + "]");
        Object rval = invocation.proceed();
        System.out.println("Invocation returned");
        return rval;
    }
}

注意MethodInvocation的proceed()方法的调用。 这个调用会应用到目标连接点的拦截器链中的每一个拦截器。大部分拦截器会调用这个方法,并返回它的返回值。但是, 一个MethodInterceptor,和任何around通知一样,可以返回不同的值或者抛出一个异常,而 不调用proceed方法。但是,没有好的原因你要这么做。

MethodInterceptor提供了和其他AOP联盟的兼容实现的交互能力。这一节下面 要讨论的其他的通知类型实现了AOP公共的概念,但是以Spring特定的方式。虽然使用特定 通知类型有很多优点,但如果你可能需要在其他的AOP框架中使用,请坚持使用MethodInterceptor around通知类型。注意目前切入点不能和其它框架交互操作,并且AOP联盟目前也没有定义切入 点接口。

5.3.2.2. Before通知

Before通知是一种简单的通知类型。 这个通知不需要一个MethodInvocation对象,因为它只在进入一个方法 前被调用。

Before通知的主要优点是它不需要调用proceed() 方法, 因此没有无意中忘掉继续执行拦截器链的可能性。

MethodBeforeAdvice接口如下所示。 (Spring的API设计允许成员变量的before通知,虽然一般的对象都可以应用成员变量拦截,但Spring 有可能永远不会实现它)。

public interface MethodBeforeAdvice extends BeforeAdvice {

    void before(Method m, Object[] args, Object target) throws Throwable;
}

注意返回类型是void。 Before通知可以在连接点执行之前 插入自定义的行为,但是不能改变返回值。如果一个before通知抛出一个异常,这将中断拦截器 链的进一步执行。这个异常将沿着拦截器链后退着向上传播。如果这个异常是unchecked的,或者 出现在被调用的方法的签名中,它将会被直接传递给客户代码;否则,它将被AOP代理包装到一个unchecked 的异常里。

下面是Spring中一个before通知的例子,这个例子计数所有正常返回的方法:

public class CountingBeforeAdvice implements MethodBeforeAdvice {
    private int count;
    public void before(Method m, Object[] args, Object target) throws Throwable {
        ++count;
    }

    public int getCount() { 
        return count; 
    }
}
Before通知可以被用于任何类型的切入点。

5.3.2.3. Throws通知

如果连接点抛出异常,Throws通知 在连接点返回后被调用。Spring提供强类型的throws通知。注意这意味着 org.springframework.aop.ThrowsAdvice接口不包含任何方法: 它是一个标记接口,标识给定的对象实现了一个或多个强类型的throws通知方法。这些方法形式 如下:

afterThrowing([Method], [args], [target], subclassOfThrowable) 

只有最后一个参数是必需的。 这样从一个参数到四个参数,依赖于通知是否对方法和方法 的参数感兴趣。下面是throws通知的例子。

如果抛出RemoteException异常(包括子类), 这个通知会被调用

public  class RemoteThrowsAdvice implements ThrowsAdvice {

    public void afterThrowing(RemoteException ex) throws Throwable {
        // Do something with remote exception
    }
}

如果抛出ServletException异常, 下面的通知会被调用。和上面的通知不一样,它声明了四个参数,所以它可以访问被调用的方法,方法的参数 和目标对象:

public static class ServletThrowsAdviceWithArguments implements ThrowsAdvice {

    public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
        // Do something will all arguments
    }
}

最后一个例子演示了如何在一个类中使用两个方法来同时处理 RemoteExceptionServletException 异常。任意个数的throws方法可以被组合在一个类中。

public static class CombinedThrowsAdvice implements ThrowsAdvice {

    public void afterThrowing(RemoteException ex) throws Throwable {
        // Do something with remote exception
    }
 
    public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
        // Do something will all arguments
    }
}
Throws通知可被用于任何类型的切入点。

5.3.2.4. After Returning通知

Spring中的after returning通知必须实现 org.springframework.aop.AfterReturningAdvice 接口,如下所示:

public interface AfterReturningAdvice extends Advice {

    void afterReturning(Object returnValue, Method m, Object[] args, Object target) 
            throws Throwable;
}

After returning通知可以访问返回值(不能改变)、被调用的方法、方法的参数和 目标对象。

下面的after returning通知统计所有成功的没有抛出异常的方法调用:

public class CountingAfterReturningAdvice implements AfterReturningAdvice {
    private int count;

    public void afterReturning(Object returnValue, Method m, Object[] args, Object target) throws Throwable {
        ++count;
    }

    public int getCount() {
        return count;
    }
}

这方法不改变执行路径。如果它抛出一个异常,这个异常而不是返回值将被沿着拦截器链 向上抛出。

After returning通知可被用于任何类型的切入点。

5.3.2.5. Introduction通知

Spring将introduction通知看作一种特殊类型的拦截通知。

Introduction需要实现IntroductionAdvisor, 和IntroductionInterceptor接口:

public interface IntroductionInterceptor extends MethodInterceptor {

    boolean implementsInterface(Class intf);
}

继承自AOP联盟MethodInterceptor接口的 invoke()方法必须实现导入:也就是说,如果被调用的方法是在 导入的接口中,导入拦截器负责处理这个方法调用,它不能调用proceed() 方法。

Introduction通知不能被用于任何切入点,因为它只能作用于类层次上,而不是方法。 你可以只用InterceptionIntroductionAdvisor来实现导入通知,它有下面的方法:

public interface InterceptionIntroductionAdvisor extends InterceptionAdvisor {

    ClassFilter getClassFilter();

    IntroductionInterceptor getIntroductionInterceptor();

    Class[] getInterfaces();
}

这里没有MethodMatcher,因此也没有和导入通知关联的 切入点。只有类过滤是合乎逻辑的。

getInterfaces()方法返回advisor导入的接口。

让我们看看一个来自Spring测试套件中的简单例子。我们假设想要导入下面的接口到一个 或者多个对象中:

public interface Lockable {
    void lock();
    void unlock();
    boolean locked();
}

这个例子演示了一个mixin。我们想要能够 将被通知对象类型转换为Lockable,不管它们的类型,并且调用lock和unlock方法。如果我们调用 lock()方法,我们希望所有setter方法抛出LockedException异常。 这样我们能添加一个方面使的对象不可变,而它们不需要知道这一点:这是一个很好的AOP例 子。

首先,我们需要一个做大量转化的IntroductionInterceptor。 在这里,我们继承 org.springframework.aop.support.DelegatingIntroductionInterceptor 实用类。我们可以直接实现IntroductionInterceptor接口,但是大多数情况下 DelegatingIntroductionInterceptor是最合适的。

DelegatingIntroductionInterceptor的设计是将导入 委托到真正实现导入接口的接口,隐藏完成这些工作的拦截器。委托可以使用构造方法参数 设置到任何对象中;默认的委托就是自己(当无参数的构造方法被使用时)。这样在下面的 例子里,委托是DelegatingIntroductionInterceptor的子类 LockMixin。给定一个委托(默认是自身)的 DelegatingIntroductionInterceptor实例寻找被这个委托(而不 是IntroductionInterceptor)实现的所有接口,并支持它们中任何一个导入。子类如 LockMixin也可能调用suppressInterflace(Class intf) 方法隐藏不应暴露的接口。然而,不管IntroductionInterceptor 准备支持多少接口,IntroductionAdvisor将控制哪个接口将被实际 暴露。一个导入的接口将隐藏目标的同一个接口的所有实现。

这样,LockMixin继承DelegatingIntroductionInterceptor 并自己实现Lockable。父类自动选择支持导入的Lockable,所以我们不需要指定它。 用这种方法我们可以导入任意数量的接口。

注意locked实例变量的使用。这有效地添加额外的状态到目标 对象。

public class LockMixin extends DelegatingIntroductionInterceptor 
    implements Lockable {

    private boolean locked;

    public void lock() {
        this.locked = true;
    }

    public void unlock() {
        this.locked = false;
    }

    public boolean locked() {
        return this.locked;
    }

    public Object invoke(MethodInvocation invocation) throws Throwable {
        if (locked() && invocation.getMethod().getName().indexOf("set") == 0)
            throw new LockedException();
        return super.invoke(invocation);
    }

}

通常不要需要改写invoke()方法:实现 DelegatingIntroductionInterceptor就足够了,如果是导入的方法, DelegatingIntroductionInterceptor实现会调用委托方法, 否则继续沿着连接点处理。在现在的情况下,我们需要添加一个检查:在上锁 状态下不能调用setter方法。

所需的导入advisor是很简单的。只有保存一个独立的 LockMixin实例,并指定导入的接口,在这里就是 Lockable。一个稍微复杂一点例子可能需要一个导入拦截器(可以 定义成prototype)的引用:在这种情况下,LockMixin没有相关配置,所以我们简单地 使用new来创建它。

public class LockMixinAdvisor extends DefaultIntroductionAdvisor {

    public LockMixinAdvisor() {
        super(new LockMixin(), Lockable.class);
    }
}

我们可以非常简单地使用这个advisor:它不需要任何配置。(但是,有一点 必要的:就是不可能在没有IntroductionAdvisor 的情况下使用IntroductionInterceptor。) 和导入一样,通常 advisor必须是针对每个实例的,并且是有状态的。我们会有不同的的LockMixinAdvisor 每个被通知对象,会有不同的LockMixin。 advisor组成了被通知对象的状态的一部分。

和其他advisor一样,我们可以使用 Advised.addAdvisor() 方法以编程地方式使用这种advisor,或者在XML中配置(推荐这种方式)。 下面将讨论所有代理创建,包括“自动代理创建者”,选择代理创建以正确地处理导入和有状态的混入。

5.4. Spring中的advisor

在Spring中,一个advisor就是一个aspect的完整的模块化表示。 一般地,一个advisor包括通知和切入点。

撇开导入这种特殊情况,任何advisor可被用于任何通知。 org.springframework.aop.support.DefaultPointcutAdvisor 是最通用的advisor类。例如,它可以和MethodInterceptorBeforeAdvice或者ThrowsAdvice一起使 用。

Spring中可以将advisor和通知混合在一个AOP代理中。例如,你可以在一个代理配置中 使用一个对around通知、throws通知和before通知的拦截:Spring将自动创建必要的拦截器链。

5.5. 用ProxyFactoryBean创建AOP代理

如果你在为你的业务对象使用Spring的IoC容器(例如ApplicationContext或者BeanFactory), 你应该会或者你愿意会使用Spring的aop FactoryBean(记住,factory bean引入了一个间接层, 它能创建不同类型的对象).

在spring中创建AOP proxy的基本途径是使用org.springframework.aop.framework.ProxyFactoryBean. 这样可以对pointcut和advice作精确控制。但是如果你不需要这种控制,那些简单的选择可能更适合你。

5.5.1. 基本概要

ProxyFactoryBean,和其他Spring的 FactoryBean实现一样,引入一个间接的层次。如果你 定义一个名字为fooProxyFactoryBean, 引用foo的对象所看到的不是ProxyFactoryBean 实例本身,而是由实现ProxyFactoryBean的类的 getObject()方法所创建的对象。这个方法将创建一个包装了目标对象 的AOP代理。

使用ProxyFactoryBean或者其他IoC可知的类来创建AOP代理 的最重要的优点之一是IoC可以管理通知和切入点。这是一个非常的强大的功能,能够实 现其他AOP框架很难实现的特定的方法。例如,一个通知本身可以引用应用对象(除了目标对象, 它在任何AOP框架中都可以引用应用对象),这完全得益于依赖注入所提供的可插入性。

5.5.2. JavaBean的属性

类似于Spring提供的绝大部分FactoryBean实现一样, ProxyFactoryBean也是一个javabean,我们可以利用它的属性来:

  • 指定你将要代理的目标
  • 指定是否使用CGLIB

一些关键属性来自org.springframework.aop.framework.ProxyConfig :它是所有AOP代理工厂的父类。这些关键属性包括:

  • proxyTargetClass: 如果我们应该代理目标类, 而不是接口,这个属性的值为true。如果这是true,我们需要使用CGLIB。
  • optimize: 是否使用强优化来创建代理。不要使用 这个设置,除非你了解相关的AOP代理是如何处理优化的。目前这只对CGLIB代理有效;对JDK 动态代理无效(默认)。
  • frozen: 是否禁止通知的改变,一旦代理工厂已经配置。 默认是false。
  • exposeProxy: 当前代理是否要暴露在ThreadLocal中, 以便它可以被目标对象访问。(它可以通过MethodInvocation得到,不需要ThreadLocal)。 如果一个目标需要获得它的代理并且exposeProxy的值是ture,可以使用 AopContext.currentProxy()方法。
  • aopProxyFactory: 所使用的AopProxyFactory具体实现。 这个参数提供了一条途径来定义是否使用动态代理、CGLIB还是其他代理策略。默认实现将适当地选择动态 代理或CGLIB。一般不需要使用这个属性;它的意图是允许Spring 1.1使用另外新的代理类型。

其他ProxyFactoryBean特定的属性包括:

  • proxyInterfaces: 接口名称的字符串数组。如果这个 没有提供,CGLIB代理将被用于目标类。
  • interceptorNames: Advisor、interceptor或其他 被应用的通知名称的字符串数组。顺序是很重要的。这里的名称是当前工厂中bean的名称,包 括来自祖先工厂的bean的名称。
  • singleton: 工厂是否返回一个单独的对象,无论 getObject()被调用多少次。许多FactoryBean 的实现提供这个方法。默认值是true。如果你想要使用有状态的通知--例如,用于有状态的 mixin--将这个值设为false,使用prototype通知。

5.5.3. 代理接口

让我们来看一个简单的ProxyFactoryBean的实际例子。这个例子涉及到 :

  • 一个将被代理的目标bean,在这个例子里,这个bean的被定义为"personTarget".
  • 一个advisor和一个interceptor来提供advice.
  • 一个AOP代理bean定义,该bean指定目标对象(这里是personTarget bean), 代理接口,和使用的advice.
<bean id="personTarget" class="com.mycompany.PersonImpl">
    <property name="name"><value>Tony</value></property>
    <property name="age"><value>51</value></property>
</bean>

<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
    <property name="someProperty"><value>Custom string property value</value></property>
</bean>

<bean id="debugInterceptor" class="org.springframework.aop.interceptor.NopInterceptor">
</bean>

<bean id="person" 
    class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="proxyInterfaces"><value>com.mycompany.Person</value></property>

    <property name="target"><ref local="personTarget"/></property>
    <property name="interceptorNames">
        <list>
            <value>myAdvisor</value>
            <value>debugInterceptor</value>
        </list>
    </property>
</bean>

请注意:person bean的interceptorNames属性提供一个String列表, 列出的是该ProxyFactoryBean使用的,在当前bean工厂定义的interceptor或者advisor的 名字(advisor,interceptor,before,after returning,和throws advice 对象皆可)。 Advisor在该列表中的次序很重要。

你也许会对该列表为什么不采用bean的引用存有疑问。 原因就在于如果ProxyFactoryBean的singleton属性被设置为false, 那么bean工厂必须能返回多个独立的代理实例。 如果有任何一个advisor本身是prototype的,那么它就需要返回独立的实例, 也就是有必要从bean工厂获取advisor的不同实例,bean的引用在这里显然是不够的。

上面定义的“person”bean定义可以作为Person接口的实现来使用,如下所示:

Person person = (Person) factory.getBean("person");

在同一个IoC的上下文中,其他的bean可以依赖于Person接口,就象依赖于一个普通的java对象一样。

<bean id="personUser" class="com.mycompany.PersonUser">
    <property name="person"><ref local="person" /></property>
</bean>

在这个例子里,PersonUser类暴露了一个类型为Person的属性。 只要是在用到该属性的地方,AOP代理都能透明的替代一个真实的Person实现。 但是,这个类可能是一个动态代理类。也就是有可能把它类型转换为一个Advised接口 (该接口在下面的章节中论述) 。

5.5.4. 代理类

如果你需要代理的是类,而不是一个或多个接口,又该怎么办呢?

想象一下我们上面的例子,如果没有Person接口, 我们需要通知一个叫Person的类, 而且该类没有实现任何业务接口。在这种情况下,你可以配置Spring使用CGLIB代理, 而不是动态代理。你只要在上面的ProxyFactoryBean定义中把 它的proxyTargetClass属性改成true就行了。

只要你愿意,即使在有接口的情况下,你也可以强迫Spring使用CGLIB代理。

CGLIB代理是通过在运行期产生目标类的子类来进行工作的。 Spring可以配置这个生成的子类,来代理原始目标类的方法调用。这个子类是用 Decorator设计模式置入到advice中的。

CGLIB代理对于用户来说应该是透明的。然而,还有以下一些因素需要考虑:

  • Final方法不能被通知,因为不能被重写。
  • 你需要在你的classpath中包括CGLIB的二进制代码,而动态代理对任何JDK都是可用的.

CGLIB和动态代理在性能上有微小的区别,对Spring 1.0来说,后者稍快。 另外,以后可能会有变化。在这种情况下性能不是决定性因素

5.6. 便利的代理创建方式

通常,我们不需要ProxyFactoryBean的全部功能,因为我们常常只对一个方面感兴趣: 例如,事务管理。

当我们仅仅对一个特定的方面干兴趣时,我们可以使用许多便利的工厂来创建AOP代理。这些在其他 章节讨论,所以这里我们快速浏览一下它们。

5.6.1. TransactionProxyFactoryBean

用Spring提供的jPetStore的示例应用 演示了TransactionProxyFactoryBean的使用方式。

TransactionProxyFactoryBeanProxyConfig的子类, 因此基本配置信息是和ProxyFactoryBean共享的。 (见上面ProxyConfig的属性列表。)

下面的代码来自于JPetStore application,演示了ProxyFactoryBean是如何工作的。 跟ProxyFactoryBean一样,存在一个目标bean的定义。 类的依赖关系定义在代理工厂bean定义中(petStore),而不是普通Java对象(petStoreTarget)。

TransactionProxyFactoryBean需要设置一个target属性, 还需要设置“transactionAttributes”, “transactionAttributes”用来指定需要事务化 处理的方法,还有要求的传播方式和其他设置:

<bean id="petStoreTarget" class="org.springframework.samples.jpetstore.domain.logic.PetStoreImpl">
    <property name="accountDao"><ref bean="accountDao"/></property>
    <!-- Other dependencies omitted -->
</bean>

<bean id="petStore" 
    class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
    <property name="transactionManager"><ref bean="transactionManager"/></property>
    <property name="target"><ref local="petStoreTarget"/></property>
    <property name="transactionAttributes">
        <props>
            <prop key="insert*">PROPAGATION_REQUIRED</prop>
            <prop key="update*">PROPAGATION_REQUIRED</prop>
            <prop key="*">PROPAGATION_REQUIRED,readOnly</prop>
        </props>
    </property>
</bean>

TransactionProxyFactoryBean自动创建一个事务advisor, 该advisor包括一个基于事务属性的切入点。因此只有事务性的方法被通知。

TransactionProxyFactoryBean使用preInterceptors和 postInterceptors属性指定“pre”和“post”通知。它们将拦截器,通知和Advisor数组放置 在事务拦截器前后的拦截器链中。使用XML格式的bean定义中的<list>元素定义,就 象下面一样:

<property name="preInterceptors">
    <list>
        <ref local="authorizationInterceptor"/>
        <ref local="notificationBeforeAdvice"/>
    </list>
</property>
<property name="postInterceptors">
    <list>
        <ref local="myAdvisor"/>
    </list>
</property>

这些属性可以加到上面的“petStore”的bean定义里。一个通用用法是将事务和声明式 安全组合在一起使用:一个和EJB提供的类似的方法。

因为使用前拦截器和后拦截器时,用的是真正的实例引用,而不象在 ProxyFactoryBean中用的bean的名字,因此它们只能用于共享实例的通知。 因此它们不能用在有状态的通知中:例如,在mixin中。这和TransactionProxyFactoryBean的要求是 一致的。如果你需要更复杂的,可以定制的AOP,你可以考虑使用普通的ProxyFactoryBean, 或者是自动代理生成器(参考下面)。

尤其是如果我们将Spring的AOP在许多情况下看成是EJB的替代品,我们会发现大多数通知是很普通的, 可以使用共享实例。声明式的事务管理和安全检查是一个典型的例子。

TransactionProxyFactoryBean依赖于由它的transactionManager 属性指定的TransactionManager。 这种事务管理方式是可插拔的,基于JTA,JDBC或者其他事务管理策略皆可。 这与Spring的事务抽象层有关,而不在于AOP本身。我们将在下一章中讨论事务机制。

如果你只对声明性事务管理感兴趣,TransactionProxyFactoryBean是一个不错的解决办法, 并且比直接使用ProxyFactoryBean来得简单.

5.6.2. EJB 代理

其它有一些专门的代理用于创建EJB代理,使得EJB的“业务方法”的接口可以被调用代码直接使用。 调用代码并不需要进行JNDI查找或使用EJB的创建方法:这是在可读性和架构灵活性方面的重大提高。

进一步请参考本手册内的Spring的EJB业务。

5.7. 使用ProxyFactory以编程的方式创建AOP代理

使用Spring以编程的方式创建AOP代理也很简单。 这使得你不需要Spring的IoC就能够使用Spring的AOP。

下面的代码显示的用拦截器和advisor为目标对象创建代理。目标对象实现的接口将自动被代理:

ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addInterceptor(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();

第一步是创建类型为org.springframework.aop.framework.ProxyFactory 的对象。你可以和上面的例子一样用目标对象创建,或者在另一个构造函数中指定要被代理的接口。

你可以添加拦截器或advisor,在整个ProxyFactory的生命周期内操作它们。如果你添加 IntroductionInterceptionAroundAdvisor,你可以使代理实现附加接口。

ProxyFactory(它是从AdvisedSupport继承而来)也提供了一些实用方法,使你可以添加 其它通知类型,比如before通知和throws通知。AdvisedSupport是ProxyFactory和ProxyFactoryBean 的父类。

将AOP代理的创建和IoC框架结合起来在大多数应用中都是最好的实现方式。我们推荐你和一般情况一样, 不要将AOP配置信息放在Java代码里。

5.8. 操作被通知对象

无论你怎么创建AOP代理,你都可以使用org.springframework.aop.framework.Advised 接口来操作它们。任何AOP代理无论实现其它什么接口,都可以类型转换为这个接口。这个接口包括下列方法:

void addInterceptor(Interceptor interceptor) throws AopConfigException;

void addInterceptor(int pos, Interceptor interceptor) 
        throws AopConfigException;

void addAdvisor(Advisor advisor) throws AopConfigException;

void addAdvisor(int pos, Advisor advisor) throws AopConfigException;

int indexOf(Advisor advisor);

boolean removeAdvisor(Advisor advisor) throws AopConfigException;

void removeAdvisor(int index) throws AopConfigException;

boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException;

boolean isFrozen();

getAdvisors()方法为工厂中的每个advisor,拦截器或者其它通知类型返回一个Advisor。 如果你添加一个Advisor,使用当前索引返回的advisor就是你添加的对象。如果你添加拦截器或其它通知类型, Spring将当前对象和一个满足要求的切入点封装在一个advisor里。因此,如果你添加 MethodInterceptor, 使用当前索引返回的advisor是一个DefaultPointcutAdvisor,这个advisor返回 MethodInterceptor和满足所有类和方法的切入点。

addAdvisor()被用来添加Advisor。通常会是一个普通的 DefaultPointcutAdvisor,它可以和任何通知或切入点(除了引用)一起使用。

缺省情况下,在每次代理被创建的时候添加或删除advisor或拦截器。唯一的限制是不能添加或删除 引入advisor,因为工厂提供的已存在的代理不反映接口的变化。(你可以从工厂得到一个新的代理来避免这个问题)

是否建议在产品中修改业务对象的通知还值得怀疑,虽然毫无疑问存在合理的使用情况。但是, 在开发中这是非常有用的:例如,在测试中。我有时候发现以拦截器或其它通知的形式来添加测试代码非常有用, 这样就可以进入我想要测试的方法调用。(例如,通知可以进入为这个方法创建的事务中: 在为回滚事务作标记前,运行SQL检查数据库是否被正确更新。)

根据你创建代理的方式,你通常可以设置frozen标记,这样AdvisedisFrozen()就返回true,任何添加或删除通知都将导致 AopConfigException。这种冻结被通知对象状态的方法在一些情况下是非常有用的: 例如,为了阻止调用代码删除一个安全拦截器。如果已知运行时修改通知不被允许,这还可以被Spring 1.1用来 作优化。

5.9. 使用“autoproxy”功能

目前为止,我们已经讨论了使用ProxyFactoryBean或类似的工厂bean来显式创建AOP代理。

Spring也允许我们使用“autoproxy”的bean定义,它可以自动代理所选择的bean定义。这是建立在Spring的 “bean后处理器”机制上的,它能够在容器载入bean定义的时候修改任何bean定义。

在这个模型中,你可以在你的XML bean定义文件中建立特殊的bean定义,来配置自动代理机制。这允许你声明目标对象以使用自动代理功能: 你就可以不需要使用ProxyFactoryBean

有两种方法来实现自动代理:

  • 使用一个自动代理生成器,它引用当前上下文中的那些特殊bean
  • 有一个特殊的自动代理创建的情况值得单独考虑:由源代码级元数据驱动的自动代理创建

5.9.1. 自动代理的bean定义

org.springframework.aop.framework.autoproxy包提供了下列标准自动代理生成器。

5.9.1.1. BeanNameAutoProxyCreator

BeanNameAutoProxyCreator为名字符合某个值或统配符的bean自动创建AOP代理。

<bean id="jdkBeanNameProxyCreator" 
    class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames"><value>jdk*,onlyJdk</value></property>
    <property name="interceptorNames">
        <list>
            <value>myInterceptor</value>
        </list>
    </property>
</bean>

就和ProxyFactoryBean一样,有一个interceptorNames 属性,而不是一个拦截器列表,这个属性允许为prototype的advisor提供正确的行为。虽然名字叫 “拦截器”,但是也可以是advisor或任何通知类型。

就象一般的自动代理创建一样,使用BeanNameAutoProxyCreator的主要目的 是对多个对象使用相同的配置信息,并且减少配置的工作量。这在为多个对象使用声明式事务时是一个很流行的选择。

在上面的例子中,名字匹配的bean定义,如“jdkMyBean”和“onlyJdk”,是包含目标类的普通bean定义。 BeanNameAutoProxyCreator将自动创建AOP代理。相同的通知会被因用到所有匹配的bean。 注意,如果使用了advisor(而不是上面例子中的拦截器),切入点可能对不同的bean会不同。

5.9.1.2. DefaultAdvisorAutoProxyCreator

DefaultAdvisorAutoProxyCreator是一个更通用,更强大的自动代理生成器。它将 自动应用于当前上下文的符合条件的advisor,而不需要在自动代理advisor的bean定义中包含特定的bean名字。 它有助于配置的一致性,并避免象BeanNameAutoProxyCreator一样重复配置。

使用这个机制包括:

  • 指定一个DefaultAdvisorAutoProxyCreator的bean定义
  • 在相同或相关上下文中指定任何数目的Advisor。注意这些必须是Advisor, 而不仅仅是拦截器或其它通知。这是很必要的,因为必须有一个切入点来检查每个通知是否符合候选bean定义。

DefaultAdvisorAutoProxyCreator会自动计算每个advisor包含的的切入点,看看 是否有什么通知应该被引用到每个业务对象(比如例子中的“businessObject1”和“businessObject2”)。

这意味着任何数目的advisor都可以自动应用到每个业务对象。如果advisor中没有任何切入点符合业务对象的 方法,这个

分享到:
评论

相关推荐

    spring-aop.jar各个版本

    spring-aop-1.1.1.jar spring-aop-1.2.6.jar spring-aop-1.2.9.jar spring-aop-2.0.2.jar spring-aop-2.0.6.jar spring-aop-2.0.7.jar spring-aop-2.0.8.jar spring-aop-2.0.jar spring-aop-2.5.1.jar spring-aop-...

    Spring5 框架 ---- AOP ---- 代码

    Spring5 框架 ---- AOP ---- 代码 Spring5 框架 ---- AOP ---- 代码 Spring5 框架 ---- AOP ---- 代码 Spring5 框架 ---- AOP ---- 代码 Spring5 框架 ---- AOP ---- 代码 Spring5 框架 ---- AOP ---- 代码 Spring5 ...

    spring-aop-jar

    "spring-aop-jar"这个主题涉及到Spring框架中的核心组件之一——Spring AOP。这里我们将深入探讨Spring AOP、相关jar文件以及它们在实际开发中的作用。 首先,我们来看一下提供的文件: 1. aopalliance.jar:这是一...

    spring,spring-aop-5.3.22.jar+aop+IDEA本地包

    spring-aop-5.3.22.jar Spring AOP provides an Alliance-compliant aspect-oriented programming implementation allowing you to define method interceptors and pointcuts to cleanly decouple code that ...

    开发工具 spring-aop-4.3.6.RELEASE

    开发工具 spring-aop-4.3.6.RELEASE开发工具 spring-aop-4.3.6.RELEASE开发工具 spring-aop-4.3.6.RELEASE开发工具 spring-aop-4.3.6.RELEASE开发工具 spring-aop-4.3.6.RELEASE开发工具 spring-aop-4.3.6.RELEASE...

    spring-aop-5.0.10.RELEASE-API文档-中文版.zip

    赠送jar包:spring-aop-5.0.10.RELEASE.jar; 赠送原API文档:spring-aop-5.0.10.RELEASE-javadoc.jar; 赠送源代码:spring-aop-5.0.10.RELEASE-sources.jar; 赠送Maven依赖信息文件:spring-aop-5.0.10.RELEASE....

    spring.jar spring-agent.jar spring-aop.jar spring-beans.jar spring-hibernate3.jar spring-jdbc.jar spring-struts.jar spring-web.jar

    spring.jar spring-aop.jar spring-aop.jar spring-beans.jar spring-hibernate3.jar spring-jdbc.jar spring-struts.jar spring-web.jar

    spring-aop-5.2.0.RELEASE-API文档-中文版.zip

    赠送jar包:spring-aop-5.2.0.RELEASE.jar; 赠送原API文档:spring-aop-5.2.0.RELEASE-javadoc.jar; 赠送源代码:spring-aop-5.2.0.RELEASE-sources.jar; 赠送Maven依赖信息文件:spring-aop-5.2.0.RELEASE.pom;...

    spring-boot aop

    Spring Boot AOP(面向切面编程)是一种强大的设计模式,它允许我们在不修改现有代码的情况下,插入额外的功能或监控代码。在Spring框架中,AOP主要用于日志记录、事务管理、性能统计等场景。本示例是关于如何在...

    spring-aop-5.0.8.RELEASE-API文档-中英对照版.zip

    赠送jar包:spring-aop-5.0.8.RELEASE.jar; 赠送原API文档:spring-aop-5.0.8.RELEASE-javadoc.jar; 赠送源代码:spring-aop-5.0.8.RELEASE-sources.jar; 赠送Maven依赖信息文件:spring-aop-5.0.8.RELEASE.pom;...

    Spring AOP面向方面编程原理:AOP概念

    ### Spring AOP面向方面编程原理:AOP概念详解 #### 一、引言 随着软件系统的日益复杂,传统的面向对象编程(OOP)...对于希望深入了解Spring AOP原理与实践的读者来说,掌握以上概念将是开启面向方面编程之旅的第一步。

    spring aop注解方式、xml方式示例

    Spring AOP(面向切面编程)是Spring框架的重要组成部分,它提供了一种强大的方式来实现横切关注点,如日志、事务管理、性能监控等,而无需侵入业务代码。下面将详细介绍Spring AOP的注解方式和XML配置方式。 ### ...

    spring-aop.jar

    spring-aop.jarspring-aop.jarspring-aop.jarspring-aop.jarspring-aop.jarspring-aop.jarspring-aop.jarspring-aop.jarspring-aop.jarspring-aop.jar

    spring-aop-5.2.0.RELEASE.jar

    spring-aop-5.2.0.RELEASE

    spring-aop-5.3.10-API文档-中文版.zip

    赠送jar包:spring-aop-5.3.10.jar; 赠送原API文档:spring-aop-5.3.10-javadoc.jar; 赠送源代码:spring-aop-5.3.10-sources.jar; 赠送Maven依赖信息文件:spring-aop-5.3.10.pom; 包含翻译后的API文档:spring...

    spring-aop-6.0.2.jar

    spring-aop-6.0.2.jar

    spring-aop-5.3.12-API文档-中英对照版.zip

    赠送jar包:spring-aop-5.3.12.jar; 赠送原API文档:spring-aop-5.3.12-javadoc.jar; 赠送源代码:spring-aop-5.3.12-sources.jar; 赠送Maven依赖信息文件:spring-aop-5.3.12.pom; 包含翻译后的API文档:spring...

    spring-aop-3.0.xsd spring-beans-3.0 spring-context-3.0.xsd spring-mvc-3.1.xsd

    Spring AOP(面向切面编程)是 Spring 框架的一个重要部分,它允许开发者在不修改源代码的情况下,实现跨切面的关注点,如日志、事务管理等。`spring-aop-3.0.xsd` 是 Spring AOP 的 XML 配置文件,定义了与 AOP ...

    spring-aop-5.2.15.RELEASE-API文档-中文版.zip

    赠送jar包:spring-aop-5.2.15.RELEASE.jar; 赠送原API文档:spring-aop-5.2.15.RELEASE-javadoc.jar; 赠送源代码:spring-aop-5.2.15.RELEASE-sources.jar; 赠送Maven依赖信息文件:spring-aop-5.2.15.RELEASE....

    Spring框架(spring-framework-5.2.6.RELEASE)的jar包

    4. **AOP**:AOP模块提供了面向切面编程的实现,允许开发者定义“方面”,即关注点的模块化,如日志、事务管理等。这样可以将这些关注点与业务逻辑分离,提高代码的可读性和可维护性。 5. **Instrumentation**:这...

Global site tag (gtag.js) - Google Analytics