Spring核心技术--AOP

Spring核心技术–AOP

在OOP中, 模块化的单位是class, 在AOP中, 模块化的单位是aspect.

Spring IoC容器并不直接与AOP模块耦合, AOP模块是作为一个中间件方案提供给IoC容器使用的.

声明式事务管理就是AOP在Spring框架中的一个典型实现, 另外, 池化也是一个典型的应用.

对于Spring AOP编程来说, 推荐使用能够完成需求功能的最小Advice种类进行实现. 例如, 如果你想简单实现用返回值更新缓存, 那么应该使用@After Returning Advice而不是@Around Advice. 虽然Around advice更加强大, 也能完成相同的功能, 但是也更容易出错.

在Spring中, Pointcut + Advice = Spring AOP基本组件

Spring AOP的能力和局限性

只能用于方法级别的连接点, 不支持属性级别的拦截.

如果想实现属性级别的拦截, 考虑使用AspectJ.

Spring AOP与AspectJ并不是相互替代的关系, Spring AOP也不是用来取代AspectJ或者是其他AOP框架, Spring AOP只是为了更好的为Spring IoC容器提供一些通用问题的解决方案. 你可以同时使用AspectJ作为Spring AOP的补充.

注意: Spring AOP的使用借鉴了AspectJ的一些语法(例如使用了一些AspectJ同名的注解, 和切点解释, 匹配的方法, spring-aop模块默认已经添加了依赖, 无需自己手动整合), 实际实现与AspectJ本身大相径庭.

AOP Proxy类型

Spring AOP的实现方式是proxy-based AOP. 默认使用JDK动态代理作为AOP Proxy的实现方式. JDK动态代理能够代理被代理对象的所有接口.

Spring AOP也能使用CGLIB代理, 当一个business object中想被代理的某非final方法不是接口中的方法的时候, Spring AOP就会使用GCLIB的方式进行代理.

注意: 基于proxy的aop都只能够拦截被代理对象的外部调用, 也就是下例的方式不会被advice拦截:

public class SimplePojo implements Pojo {

    public void foo() {
        // this next method invocation is a direct call on the 'this' reference
        // 当proxy对象调用实际的被代理对象的foo方法时, this指针已经指向被代理对象, 所以bar方法不会被advice拦截
        this.bar();
    }

    public void bar() {
        // some logic...
    }
}

这种情况下只能使用AspectJ的方式通过weaving的方式实现AOP.

使用AspectJ风格进行Pointcut定义

在切点定义中, 可用的切点类型主要有以下几种.

切点类型简介
execution(最常用)匹配任意符合的方法
within匹配指定类型中的方法
this仅匹配动态代理类中的方法
target仅匹配被代理类中的方法
args仅匹配参数的运行时类型符合指定类型的方法
@target仅匹配含有指定注解的被代理类的方法
@args仅匹配参数运行时类型含有指定注解的方法
@within匹配含有指定注解的方法
@annotaion匹配标有指定注解的方法

注意: 在Spring AOP中, this指的是代理类的instance, target才是被代理类的instance.

组合切点

使用’&&’, ‘||’和’!’进行切点定义的组合, 实现复杂的切点定义.

下面是一个例子

// 匹配任意公有方法的切点
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}

// 匹配任意trading包中方法的切点
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}

// 组合切点: 匹配trading包中任意共有方法的切点
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}

按层定义可复用的切点定义

下面是一个符合这个规范的例子:

package com.xyz.someapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class SystemArchitecture {

    /**
     * A join point is in the web layer if the method is defined
     * in a type in the com.xyz.someapp.web package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.someapp.web..*)")
    public void inWebLayer() {}

    /**
     * A join point is in the service layer if the method is defined
     * in a type in the com.xyz.someapp.service package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.someapp.service..*)")
    public void inServiceLayer() {}

    /**
     * A join point is in the data access layer if the method is defined
     * in a type in the com.xyz.someapp.dao package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.someapp.dao..*)")
    public void inDataAccessLayer() {}

    /**
     * A business service is the execution of any method defined on a service
     * interface. This definition assumes that interfaces are placed in the
     * "service" package, and that implementation types are in sub-packages.
     *
     * If you group service interfaces by functional area (for example,
     * in packages com.xyz.someapp.abc.service and com.xyz.someapp.def.service) then
     * the pointcut expression "execution(* com.xyz.someapp.service.*.*(..))"
     * could be used instead.
     *
     * Alternatively, you can write the expression using the 'bean'
     * PCD, like so "bean(*Service)". (This assumes that you have
     * named your Spring service beans in a consistent fashion.)
     */
    @Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
    public void businessService() {}

    /**
     * A data access operation is the execution of any method defined on a
     * dao interface. This definition assumes that interfaces are placed in the
     * "dao" package, and that implementation types are in sub-packages.
     */
    @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
    public void dataAccessOperation() {}

}

一些切点定义的例子

这里我们以execution类型的切点定义为例, 通用的模式是:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
            throws-pattern?)

其中, 必选字段是

  • ret-type-pattern
  • name-pattern
  • param-pattern

表达式支持’*’, ‘..’等通配符, 下面是一些符合上述定义的例子:

// 所有public方法
execution(public * *(..))

// 所有以set开头的方法
execution(* set*(..))

// AccoutService接口中定义的所有方法
execution(* com.xyz.service.AccountService.*(..))

// service包中定义的所有方法
execution(* com.xyz.service.*.*(..))

// service及其子包中定义的所有方法
execution(* com.xyz.service..*.*(..))

// service包中定义的所有方法
within(com.xyz.service.*)

// service及其子包中定义的所有方法
within(com.xyz.service..*)

// 所有实现了AccountService接口的代理类instance的所有方法
this(com.xyz.service.AccountService)

// 所有实现了AccountService接口的被代理类instance的所有方法
target(com.xyz.service.AccountService)

// 所有接受单个java.io.Serializable类型参数的方法(这个接口只要是参数的接口之一即可)
// 注意: 如果是execution(* *(java.io.Serializable)), 要求参数类型声明必须严格是java.io.Serializable. args只要是Serializable的就可以了, 匹配面更广
args(java.io.Serializable)

如何定义好的切点

我们定义的任何切点都会在运行时被AspectJ重写和优化, 但即使如此, 我们也应该通过更明确的显式定义, 加速AspectJ切点匹配的时间和空间消耗.

前面介绍过的切点基本上可以分为三类:

  1. 按类型定义的切点: 如execution类型.
  2. 按范围定义的切点: 如within类型.
  3. 上下文相关的切点: 如this, target.

一个好的切点定义应该尽量包括1, 2两种类型的定义, 再退一步, 至少要包含第2种定义, 因为within类型能够快速抛弃掉不符合的大量切点, 极大加速运行时解析速度.

使用AspectJ风格进行Advice定义

一个Advice与一个Pointcut绑定, 构成AOP的基本组件.

以下是一些不同类型的Advice定义:

@AfterReturning(
    pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
    returning="retVal") // returning能够对被代理类的对象的方法执行返回结果进行绑定
public void doAccessCheck(Object retVal) {
    // ...
}

@AfterThrowing(
    pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
    throwing="ex") // 可以根据方法参数指定拦截的异常类型
public void doRecoveryActions(DataAccessException ex) {
    // ...
}

// 使用@Around advice, 一般是有@Before和@After共享状态的需要时启用
// 下面是一个使用@Around实现的方法执行耗时计算.
// Spring的缓存机制也是使用这种Advice实现的.
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
    // start stopwatch
    // 执行方法(甚至可以不执行, 决定权完全掌握在应用编写者手中!)
    Object retVal = pjp.proceed();
    // stop stopwatch
    return retVal;
}

在Advice中获取连接点(方法)状态

在Spring AOP中, 仅支持方法作为连接点, 所以Advice获取的总是方法的状态(包括参数, 方法签名, 类名等等).

Spring中要想获取连接点的状态, 必须将连接点作为Advice方法的第一个参数传入, 主要用两个类型描述:

  • org.aspectj.lang.JoinPoint
  • ProceedingJoinPoint: @Around Advice使用

上述类型有很多实用的方法, 如:

  • getArgs()
  • getThis()
  • getTarget()
  • getSignature()
  • toString

获取和使用连接点(方法)参数

下面是一个例子:

@Around("execution(List<Account> find*(..)) && " +
        "com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
        "args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
        String accountHolderNamePattern) throws Throwable {
    String newPattern = preProcess(accountHolderNamePattern);
    return pjp.proceed(new Object[] {newPattern});
}

Advice执行顺序

当多个Advice作用于同一个连接点的时候, 我们可以通过Order来显式指定Advice优先级.

举个例子:

  • @Before: 优先级高的, 先运行;
  • @After: 优先级高的, 后运行;

使用AspectJ风格进行introductions定义

introductions允许一个aspect为连接点方法对应的this对象(proxy对象)引入一个新的接口, 并给出默认的实现, 作为proxy对象的增强实现.

简单的说, 就是能够动态为this对象(proxy对象)扩展某接口的功能.

下面是一个用例:

@Aspect
public class UsageTracking {

    // @DeclareParents为value对应的所有类型的代理对象引入了新的接口实现, 也就是说对于指定类型, 我们可以使用UsageTracked usageTracked = (UsageTracked) context.getBean("myService"); 去获取它们了. 注意: 它们的类型声明中本没有实现UsageTracked接口
    @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
    public static UsageTracked mixin;

    @Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
    public void recordUsage(UsageTracked usageTracked) {
        usageTracked.incrementUseCount();
    }

}

案例解析

案例1: 使用AOP实现失败自动重试

需求说明: 对于一些会由于并发问题(例如死锁)导致的服务执行失败, 如果操作是幂等性的, 我们希望程序能够透明的重试, 而不是抛给用户一个PessimisticLockingFailureException.

// 定义一个幂等操作标识, 用于辅助切面识别
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}

@Aspect // 实现Ordered接口能够让我们的类中定义一个order属性, 执行的时候, 会按照order属性的顺序进行执行(值越大,优先级越低, 相同值的执行顺序随机. 与servlet执行顺序策略相似)
public class ConcurrentOperationExecutor implements Ordered {

    private static final int DEFAULT_MAX_RETRIES = 2;

    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    @Around("com.xyz.myapp.SystemArchitecture.businessService() && " + "@annotation(com.xyz.myapp.service.Idempotent)")
    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            }
            catch(PessimisticLockingFailureException ex) { // 如果出现了并发问题, 先捕捉, 不抛出, 尝试重试
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries); // 如果还可以重试

        // 达到了最大重试次数, 抛出异常
        throw lockFailureException;
    }

}

@Configuratin
public class AspectConfig {
    // 手动进行Aspect的初始化
    @Bean
    public ConcurrentOperationExecutor config() {
        return new ConcurrentOperationExecutor(3, 100);
    }
}

小结

以上.

参考链接:


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值