Spring AOP 详解

一、SpringAOP介绍

Spring 有三大核心思想,其目的都是为了解耦。

我们日常开发中总能不知不觉用到其中两种,分别是控制反转(Inversion of Control, IOC)和依赖注入(Dependency Injection, DI)。

而面向切面编程(Aspect Oriented Programming, AOP)却时常被人所忽略,但它的作用却不可忽视。

AOP 的实现的目标是保证开发者在不修改原代码的前提下,去为系统中的业务组件添加某种通用功能。

在使用AOP时,我们能常常听到以下术语:Aspect、Pointcut、Advice、JoinPoint、Weaving。

  • Pointcut:切点,表示一组JoinPoint,它定义了Advice将要发生的地方,
  • JoinPoint:连接点,表示程序执行过程中能够插入切面的点,可以是方法的调用或异常的抛出。
  • Advice:增强,包括处理时机和处理内容。通俗的将就是什么时候该做什么事。
  • Aspect:切面,由同一类Pointcut和Advice组成。
  • Weaving:织入,就是通过动态代理,在目标对象中执行处理内容的过程。

如果术语描述的不太明白,请允许我在介绍AOP之前讲一个故事:

在很久很久以前,有一个国家叫M国,其国力昌盛,但皇帝大限将至,便将皇位传给太子。

不久后,皇帝驾崩,太子正式登基。但太子念其国力昌盛,整日无所作为,朝事荒废。

十几年后,边境事犯,皇帝欲选取文武兼备的人率军去平定叛乱,但此时朝廷人才寥落,只有一人能担此任。

皇帝为了犒劳该将军,在其出征之前,将其亲子委以朝廷命官,辅佐在自己身边。

经过数年苦战,该将军荣耀而归,平定叛乱。皇帝高兴万分,赏其千金,封为万户侯。

经此事后,皇帝重整朝廷,不久后,国家便重回巅峰。

听完如上故事,请思考并类比术语的含义:

Pointcut:文武兼备的人

JoinPoint:上文中的将军

Advice:将军出征前皇帝留任其亲子在身边、将军回来后皇帝对其进行赏赐。

Aspect:文武兼备的人出征这个事件

Weaving:对文武兼备的人出征前后干的事的过程。

本故事只能使你理解它这些术语的含义,并不能描述AOP的目标,也就是解耦,相信解耦大家都清除,这里就不说了。

接下来开始详细介绍AOP的概念以及使用,首先介绍Pointcut,下面是一段官方介绍:

@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature

上面的例子定义了一个名为'anyOldTransfer'的切入点,它将匹配任何名为'transfer'的方法的执行。

Spring AOP支持以下AspectJ切入点指示符(PCD),用于切入点表达式中:

切入点指示符 描述
execution 限制匹配连接点的方法
within 限制匹配连接点的包或者类
@within 限制匹配连接点的类带有指定注解
arg 限制匹配连接点的参数类型
@args 限制匹配连接点的参数带有指定注解
target 限制匹配连接点目标对象的类型
@target 与@within的功能类似,但注解的保留策略须为RUNTIME
this 限制匹配连接点的AOP代理类的类型
@annotation 限制匹配连接点的方法带有指定的注解
bean 限制连接点是指定的Bean,或一组命名Bean(使用通配符时)

看完上述切点表达式不理解的话很正常,下面给出详细介绍:

1、@Pointcut是创建切入点,切入点不用写代码,返回类型为void。

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
            throws-pattern?)
execution(方法修饰符(可选)  返回类型  类路径 方法名  参数  异常模式(可选))
  • 修饰符匹配(modifiers-pattern?)
  • 返回值匹配(ret-type-pattern)
  • 类路径匹配(declaring-type-pattern?)
  • 方法名匹配(name-pattern)
  • 参数匹配(param-pattern),可以指定具体参数类型,多个参数用","隔开。
  • 异常类型匹配(throws-pattern?)
  • 以上匹配中后面跟着"?"的表示是可选项。

多个匹配之间我们可以使用链接符 &&||来表示 “且”、“或”、“非”的关系。

但是在使用 XML 文件配置时,这些符号有特殊的含义,所以我们使用 “and”、“or”、“not”来表示。

示例:

// 匹配AccountService的任意方法
execution(* com.xyz.service.AccountService.*(..))
// 匹配服务包下的任意方法
execution(* com.xyz.service.*.*(..))
//匹配服务包或其子包下的任意方法
execution(* com.xyz.service..*.*(..))
// 匹配位于service包下任意类型
within(com.xyz.service.*)
// 匹配代理实现AccountSercice接口的任意类
this(com.xyz.service.AccountService)
// 匹配目标对象实现AccountService接口的任意类
target(com.xyz.service.AccountService)

2、Advice是增强通知,其有五种通知类型,分别如下:

  • @Before,在目标方法调用前执行
  • @After,在目标方法调用后执行
  • @AfterReturning,在目标方法返回后调用
  • @AfterThrowing,在目标方法抛出异常后调用
  • @Around,将目标方法封装起来

 首先介绍一下@AfterReturning,官网带参数示例介绍如下:

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

@Aspect
public class AfterReturningExample {

    @AfterReturning(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }

}

returning属性中使用的名称必须与通知方法中的参数名称相对应。

当方法执行返回时,该返回值将作为相应的参数值传递到通知方法。

另外returning子句也限制了只能匹配返回指定类型的值,如果是Object类型,将可以匹配任何返回值。

 其次是@AfterThrowing的带参数示例:

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

@Aspect
public class AfterThrowingExample {

    @AfterThrowing(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }

}

throwing属性中使用的名称必须和通知方法中的参数名称相对应。

当方法抛出异常时,该异常将作为相应的参数传递给通知方法。

另外throwing子句也限制了只能匹配到指定异常类型。

接下来介绍@Around示例:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;

@Aspect
public class AroundExample {

    @Around("com.xyz.myapp.SystemArchitecture.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }

}
@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});
}

@Around可以在方法执行之前和之后工作,并能决定该方法何时执行以及如何调用。

该通知方法的第一个参数必须为ProceedingJoinPoint类型。

在通知方法中,调用ProceedingJoinPoint的proceed方法来执行匹配的方法。

proceed方法也可能被调用解析Object[]数组,它被用于匹配方法执行的参数。

通知方法返回的值将是匹配方法调用者看到的返回值。

最后,任何通知方法都可以将JoinPoint作为它的第一个参数,@Around除外,它第一个参数必须是ProceedingJoinPoint。

JoinPoint可以提供许多有用的参数,比如getArgs、getThis、getTarget等等。

目前我们已经看到了如何绑定返回值或异常值。如果要使参数用于通知接收,可以使用绑定形式的args。

如果在args表达式中使用参数名替代类型名称,则在调用通知方法时,将相应参数的值作为参数值传递就可以了。

示例如下:

@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
    // ...
}

arg(account,..)切入点表达式有两个作用,第一它将匹配限制为方法需要有至少一个参数。

第二它限制了参数的类型为Account,并且使该对象可以用于通知方法。

另外它也可以用如下方式表示:

@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
    // ...
}

代理对象、目标对象、和注解等都可以像如下示例使用:

首先定义一个注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
    AuditCode value();
}

然后是与@Auditable相匹配的通知:

@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
    AuditCode code = auditable.value();
    // ...
}

通知参数和泛型:(不适用于集合泛型)

public interface Sample<T> {
    void sampleGenericMethod(T param);
    void sampleGenericCollectionMethod(Collection<T> param);
}
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
    // Advice implementation
}

通知方法中的参数绑定依赖于切入点表达式中使用的名称。

因为参数名称无法通过java反射活动,因此Sping AOP使用如下策略确定参数名称:

@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code and bean
}

如果通知方法第一个参数是JoinPoint,ProceedingJoinPoint等,则可以省去参数。

@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
    // ... use jp
}

JoinPoint、ProceedingJoinPoint类型特别方便,可以通过它的getArgs方法获取参数。

当多个通知都希望在同一连接点运行时,除非另外指定,否则执行顺序是不确定的。

可以通过org.springframework.core.Ordered的Order注解来确定顺序,值越低,优先级越高。

3、引入(Introductions)

这个注解感觉用起来作用不大,就不介绍了。 

二、SpringAOP 不生效的原因

Spring的声明式事务和切面都是通过aop进行动态代理实现的,所以直接通过this来调用方法的话,将不会触发事务和切面。

示例:

@Service
public class AuthUserTagLkService extends ServiceImpl<AuthUserTagLkMapper, AuthUserTagLk> {
//添加认证用户标签
    @AuthUserTag(action = Action.ADD)
    @Transactional
    public void addAuthUserTag(CommonAuthUserTagQuery query){
        List<Integer> authUserIds = query.getAuthUserIds();
        List<AuthUserTagLk> list = new ArrayList<>();
        for (Integer authUserId : authUserIds) {
            AuthUserTagLk authUserTagLk = new AuthUserTagLk();
            authUserTagLk.setTagId(query.getTagId());
            authUserTagLk.setAuthUserId(authUserId);
            authUserTagLk.setCreateUser(query.getLoginUserId());
            list.add(authUserTagLk);
        }
        saveBatch(list);
    }//删除认证用户标签
    @AuthUserTag(action = Action.DEL)
    @Transactional
    public void delAuthUserTag(CommonAuthUserTagQuery query){
        if (!Objects.isNull(query.getTagId())){
            QueryWrapper<AuthUserTagLk> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("tag_id",query.getTagId()).eq("auth_user_id",query.getAuthUserIds());
            remove(queryWrapper);
        }else {
            removeBatchByIds(query.getTagLKIds());
        }
    }

    //更新认证用户标签
    @AuthUserTag(action = Action.UPD)
    @Transactional
    public void updAuthUserTag(CommonAuthUserTagQuery query){
        //这个只需要把参数传进来,由切面执行后面调用即可,因为更新标签名称本质上详情表不会改变。
    }

    public void operateAuthUserTag(Action action,CommonAuthUserTagQuery commonAuthUserTagQuery) {
        switch (action){
            case ADD:
                addAuthUserTag(commonAuthUserTagQuery);
                break;
            case DEL:
                delAuthUserTag(commonAuthUserTagQuery);
                break;
            case UPD:
                updAuthUserTag(commonAuthUserTagQuery);
            default:
                break;
        }
    }
}

切面方法:

  @AfterReturning("@annotation(com.upchina.customerManager.annotation.AuthUserTag)")
    public void syncTag2WaterDropletWX(JoinPoint joinPoint) {
        //获取切入点所在目标对象
        Object target = joinPoint.getTarget();
        //获取修饰符+包名+类名+方法名
        Signature signature = joinPoint.getSignature();
        //获取方法上的注解
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        AuthUserTag authUserTag = method.getAnnotation(AuthUserTag.class);
        //获取切入点方法上的参数列表
        Object[] args = joinPoint.getArgs();
    }

我们在 operateAuthUserTag 方法中直接调用了 addAuthUserTag、delAuthUserTag、updAuthUserTag 等方法,

此时@AuthUserTag注解的切面以及事务都不会生效,原因是调用这些方法是通过this调用的,而不是通过Spring管理的Bean来调用,也就是调用的不是代理之后的方法。

解决方案:

@Service
public class AuthUserTagLkService extends ServiceImpl<AuthUserTagLkMapper, AuthUserTagLk> {

    @Resource
    @Lazy
    private AuthUserTagLkService authUserTagLkService;

   public void operateAuthUserTag(Action action,CommonAuthUserTagQuery commonAuthUserTagQuery) { switch (action){ case ADD: authUserTagLkService.addAuthUserTag(commonAuthUserTagQuery); break; case DEL: authUserTagLkService.delAuthUserTag(commonAuthUserTagQuery); break; case UPD: authUserTagLkService.updAuthUserTag(commonAuthUserTagQuery); default: break; } } }

 

posted @ 2021-05-10 11:28  M-Anonymous  阅读(182)  评论(0编辑  收藏  举报