Spring AOP高级-AOP的其他扩展知识

从 0 开始深入学习 Spring 专栏目录总览

AOP 的高级部分,咱主要扩展一下 AOP 的一些其他的知识,这些内容在平时用的非常少,仅作了解即可。

1. AOP的引介【了解】

前面我们在学习 AOP 的术语时,就说过了,引介只是了解即可,现在用的实在是太少了。不过还是会有对此感兴趣的小伙伴,咱这里也讲解一下。

考虑到整体的难度和使用情况来看,小册只讲解基于 AspectJ 的引介,对于 SpringFramework 原生的引介小册不作讲解,感兴趣的小伙伴可以加群与我交流相关内容哈。

1.1 引介的作用和目标

AOP 的术语中咱提到,引介的作用是给目标对象所在的类,动态的添加属性和方法,这种增强的类型区别于方法级别的通知,它不会影响已有的方法,而是直接给类添加新的方法和属性。

不过话又说回来,如果手头的项目,源码都好好的,谁会闲的没事用这种东西呢?而且即便是因为工程依赖的 jar 包中的代码没办法修改,我们也能把那个类拷出来,自己再任意改造呀,所以这个引介通知的使用就越来越少了。

但是(话锋又一转)!这样直接改造框架的源码,回头每个项目都要这么搞,本身就很麻烦;如果每个项目对于既定源码的扩展内容都不一样,那可就没法搞了。所以,引介通知还是能起到作用的。

注意,引介作为一种特殊的 AOP 通知,它的作用对象是目标对象所属类而不是目标对象本身,这也就意味着引介的织入是对类织入,而不是对方法的逻辑织入。

1.2 SpringFramework中的引介

SpringFramework 中原生的引介通知,是通过 IntroductionInterceptor 来创建的,它本身扩展了 MethodInterceptor ,以及 DynamicIntroductionAdvice 接口,我们开发者可以通过实现 IntroductionInterceptor 的子接口 DelegatintIntroductionInterceptor ,来实现引介通知的编写。

不过由于这种使用方式太过于复杂,小册不作讲解。

1.3 AspectJ的引介

AspectJ 中的引介,有一个专门的注解 @DeclareParents 来很方便的实现目标对象所属类的属性和方法增强。它可以指定被增强的类要扩展什么接口,以及扩展的实现类。这种声明引介通知的方式相对比较简单,下面咱来学习这种编写方式。

本节源码位置:com.linkedbear.spring.aop.f_introduction 。

1.3.1 代码准备

这次的代码准备也不难,三个类就可以,分别是 Service 、切面类、配置类:

@Service
public class FinanceService {
    
    public void transfer(Long source, Long target, int money) {
        System.out.println("转账完成!");
        System.out.println(source + " 为 " + target + " 转钱" + money + "元!");
    }
}
@Component
@Aspect
public class IntroductionAspect {
    
    @Before("execution(* com..f_introduction.service.FinanceService.transfer(Long, Long, int))")
    public void beforePrintLog() {
        System.out.println("转账动作前置打印 。。。");
    }
}
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.linkedbear.spring.aop.f_introduction")
public class IntroductionConfiguration {
    
}

最后,编写测试启动类,驱动 IOC 容器后获取 FinanceService 并调用:

public class IntroductionApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(IntroductionConfiguration.class);
        FinanceService financeService = ctx.getBean(FinanceService.class);
        financeService.transfer(1L, 2L, 100);
    }
}

运行 main 方法,控制台可以正常打印 IntroductionAspect 中的前置通知,这样代码就准备好了。

1.3.2 需求说明

下面咱说一下要完成的需求哈。转账的动作中,金额一定不能是负的,而目前的代码中并没有这方面的校验逻辑。

相对简单的办法是,在前置通知中编写参数校验的逻辑即可,这个很好写,咱就不多说了。这里咱学习的是如何用引介的方式解决这个问题。

会有小伙伴说了,源码都在你手里,你加不就是了?

喂,你这不是砸场子吗?咱上面说的是,原有代码都不动了,那就不要改它了。。。

1.3.3 编写校验服务

首先,要用引介通知增强,首先需要一个新的接口 + 实现类,这里咱可以声明一个 MoneyValidator :

public interface MoneyValidator {
    boolean validate(int money);
}

紧接着,编写一个 MoneyValidator 的实现类:

@Component
public class MoneyValidatorImpl implements MoneyValidator {
    
    @Override
    public boolean validate(int money) {
        return money > 0;
    }
}

这样就有了一个金额的校验器。

1.3.4 @DeclareParents的使用

接下来就是给 FinanceService 织入引介通知了。首先咱要回到切面类中,在这里面添加一个 MoneyValidator 的成员,并标注 @DeclareParents 注解:

@Component
@Aspect
public class IntroductionAspect {
    
    @DeclareParents(value = "", defaultImpl = )
    private MoneyValidator moneyValidator;

这个 @DeclareParents 注解有两个参数,value 是即将增强到原有目标类的全限定名,defaultImpl 是引介接口的默认实现类。所以我们可以在这里面这样声明:

@Component
@Aspect
public class IntroductionAspect {
    
    @DeclareParents(value = "com.linkedbear.spring.aop.f_introduction.service.FinanceService", 
                    defaultImpl = MoneyValidatorImpl.class)
    private MoneyValidator moneyValidator;

但是话又说回来,现在的 FinanceService 是个类,那可以,如果这是个接口呢(FinanceServiceImpl implements FinanceService)?这次该怎么写呢?

AspectJ 当然也考虑到了这一点,只需要在整个接口的全限定名后面带一个 + 就可以了:

@DeclareParents(value = "com.linkedbear.spring.aop.f_introduction.service.FinanceService+", // ←看这里
                defaultImpl = MoneyValidatorImpl.class)
private MoneyValidator moneyValidator;

这样就代表,对于这个 FinanceService 接口下面的所有实现类,全部织入引介通知

1.3.5 编写校验逻辑

剩下的就是使用引介过去的 MoneyValidatorImpl 的逻辑了,这个逻辑也非常的简单,咱先理一下思路哈:首先把方法的请求参数先拿出来,然后拿到目标对象的代理对象(注意此处必须要拿到代理对象,原始目标对象压根就没实现 MoneyValidator 接口),强转为 MoneyValidator 类型,就可以调用它的 validate 方法了。如果 validate 方法返回 true ,则接下来的方法可以执行;如果返回 false ,则代表 money 参数不合法,抛出参数不合法的异常即可。

用代码编写也很简单:

@Before("execution(* com..f_introduction.service.FinanceService.transfer(Long, Long, int))")
public void beforePrintLog(JoinPoint joinPoint) {
    int money = (int) joinPoint.getArgs()[2];
    MoneyValidator validator = (MoneyValidator) joinPoint.getThis();
    if (validator.validate(money)) {
        System.out.println("转账动作前置打印 。。。");
    } else {
        throw new IllegalArgumentException("转账金额不合法!");
    }
}

1.3.6 测试运行

修改一下 main 方法的测试代码,我们把正常数据和错误数据都执行一下:

public static void main(String[] args) throws Exception {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(IntroductionConfiguration.class);
    FinanceService financeService = ctx.getBean(FinanceService.class);
    financeService.transfer(1L, 2L, 100);
    System.out.println("------------------------------");
    financeService.transfer(1L, 2L, -1);
}

运行 main 方法,控制台可以打印出 100 的成功,和 -1 的异常:

转账动作前置打印 。。。
转账完成!
1 为 2 转钱100元!
------------------------------
Exception in thread "main" java.lang.IllegalArgumentException: 转账金额不合法!

由此可以完成引介通知的增强。

2. LoadTimeWeawer【了解】

还记得在第 22 章,配置元信息中提到的 <context:load-time-weaver/> 这个标签吗?这个标签的作用是修改代理对象的构建时机。与之相匹配的注解是 @EnableLoadTimeWeaving 。

2.1 AOP增强的时机

之前咱一开始讲解 AOP 的时候,说到 SpringFramework 的 AOP 底层是使用运行时动态代理的技术实现,其实这话并不绝对(所以咱一开始说的是可以,而不是一定),因为从原生的 AOP 设计角度来看,通知的织入是有三种时机的,它们分别是:

  • 字节码编译织入:在 javac 的动作中,使用特殊的编译器,将通知直接织入到 Java 类的字节码文件中
  • 类加载时期织入:在类加载时期,使用特殊的类加载器,在目标类的字节码加载到 JVM 的时机中,将通知织入进去;
  • 运行时创建对象织入:在目标对象的创建时机,使用动态代理技术将通知织入到目标对象中,形成代理对象。

所以你看,我们前面编写的所有 AOP 的实例,全部都是基于运行时创建代理对象的方式织入通知的。除此之外,还有上面的两种方式可以选择,只是我们几乎都不用了。

2.2 AspectJ对于增强的时机

AspectJ 作为很早就出现的 AOP 框架,它可以说是非常强大了,以上三种方式它都有提供方案 / 支持:

  • 对于字节码的编译期织入,它可以利用它自己定义的 AspectJ 语言编写好切面,并借助 Maven 等项目管理工具,在工程的编译期使用特殊的编译器(ajc等),将切面类中定义的通知织入到 Java 类中;
  • 对于类加载时期的织入,它的机制就是 LoadTimeWeaving (刚好就是字面意思);
  • 对于运行时创建对象的织入,它在早期有整合一个叫 AspectWerkz 框架,也是在运行时动态代理产生代理对象,只不过我们现在学习的是 Spring 整合 AspectJ ,那最终还是用基于 SpringFramework 底层的动态代理搞定了。

2.3 AspectJ的LoadTimeWeaving

下面咱还是通过一个简单的示例,来了解一下 LoadTimeWeaving 这个机制。

本节源码位置:com.linkedbear.spring.aop.g_weawer 。

2.3.1 代码准备

还是跟之前的套路一样,一个 Service 一个 Aspect 一个 Configuration :

@Service
public class UserService {
    
    public void get(String id) {
        System.out.println("获取id为" + id + "的用户。。。");
    }
}
@Component
@Aspect
public class LogAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.g_weawer.service.UserService.*(..))")
    public void beforePrint() {
        System.out.println("LogAspect 前置通知 ......");
    }
}
@Configuration
@ComponentScan("com.linkedbear.spring.aop.g_weawer")
//@EnableAspectJAutoProxy
@EnableLoadTimeWeaving
public class LoadTimeWeavingConfiguration {
    
}

注意!此处不再使用 @EnableAspectJAutoProxy 注解,它是启用运行时的动态代理织入通知,而开启类加载时期的织入就需要使用另外的注解了,也就是上面提到的 @EnableLoadTimeWeaving 注解(或者在 xml 中声明 <context:load-time-weaver/> 标签)。

最后,编写测试启动类,套路还是都一样:

public class LoadTimeWeavingApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(LoadTimeWeavingConfiguration.class);
        UserService userService = ctx.getBean(UserService.class);
        userService.get("aaa");
    }
}

2.3.2 只声明注解并不会生效

此时运行 main 方法,控制台会抛出一个异常:

Caused by: java.lang.IllegalStateException: ClassLoader [sun.misc.Launcher$AppClassLoader] does NOT provide an 'addTransformer(ClassFileTransformer)' method. Specify a custom LoadTimeWeaver or start your Java virtual machine with Spring's agent: -javaagent:spring-instrument-{version}.jar

大概翻译一下,说是如果使用类加载器阶段的通知织入,要么自定义一个 LoadTimeWeaver ,要么导个 jar 包,而这个 jar 包叫 spring-instrument 。

这个家伙我们没见过,但它说了,那咱就导吧:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-instrument</artifactId>
    <version>${spring.framework.version}</version>
</dependency>

由于一开始我把工程中 SpringFramework 的版本全部定义了 5.2.8.RELEASE ,所以这里也就是 5.2.8 了。

导入之后还不行,注意异常的提示中还需要一个 vm 启动参数,叫 -javaagent ,那好吧,我们也把它加上:

-javaagent:E:/maven/repository/org/springframework/spring-instrument/5.2.8.RELEASE/spring-instrument-5.2.8.RELEASE.jar

注意 jar 包的位置要使用绝对路径,且小伙伴要记得修改这个 jar 包的路径呀。

这样声明好之后,重新运行 main 方法后发现还是不生效,控制台依然没有打印切面日志。。。

2.3.3 aop.xml

在 SpringFramework 整合 AspectJ 的规则中,规定了一点:如果要使用类加载级别的 AOP ,需要在 resources 的 META-INF 下编写一个 aop.xml 的配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
    <!-- 要织入切面的目标类 -->
    <weaver>
        <include within="com.linkedbear.spring.aop.g_weaving..*"/>
    </weaver>
    <!-- 切面类 -->
    <aspects>
        <aspect name="com.linkedbear.spring.aop.g_weaving.aspect.LogAspect"/>
    </aspects>
</aspectj>

注意,weaver 中包含的类要把切面类一起包含进去!否则无法正常织入切面。

这样写完之后,就算是终于都搞完了。重新运行 main 方法,LogAspect 的通知就被织入到 UserService 中了:

LogAspect 前置通知 ......
获取id为aaa的用户。。。

2.3.4 不好使?

可能有的小伙伴在实际编码时,会遇到按照小册的步骤一步一步来,但最后仍然没有打印切面日志!这种情况就需要另加一个步骤了:

在 vm-options 中再加入一行 javaagent :

-javaagent:E:\maven\repository\org\aspectj\aspectjweaver\1.9.5\aspectjweaver-1.9.5.jar

这样再执行 main 方法,就可以成功打印切面日志了。

不过这样写完之后,控制台会报一个警告:

[AppClassLoader@18b4aac2] error at com\linkedbear\spring\aop\g_weaving\aspect\LogAspect.java::0 class com.linkedbear.spring.aop.g_weaving.aspect.LogAspect is already woven and has not been built in reweavable mode [Xlint:nonReweavableTypeEncountered]

这个警告的意思也很明确,LogAspect 这个切面已经被使用过了,已经织入成功了,所以就不要再搞了。。。

出现这个问题的原因,是因为上面的 javaagent 与 @EnableLoadTimeWeaving 同时存在了,所以导致通知织入了两次。解决方法很简单,把注解配置类上的 @EnableLoadTimeWeaving 注解删掉即可。

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » Spring AOP高级-AOP的其他扩展知识