AspectJ静态织入详解

目录

前言

AspectJ 的主要优势

成熟稳定

灵活性

性能优势

三种织入方式

CTW静态织入【compile-time weaving】

PCW编译后织入【post-compile weaving】

LTW类加载时织入【load-time weaving】

总结


前言

AOP编程想必大家都耳熟能详,在spring事务中就使用了AOP编程,默认是使用spring proxy,其底层是基于jdk、cglib的动态代理,当然spring事务也可以通过aspectj来实现。这篇文章就会介绍一个AOP编程范式框架AspectJ。

AspectJ 是一种强大的面向切面编程(AOP)框架,它通过在编译时、编译后、类加载时插入横切关注点(如日志记录、事务管理、性能监控等)来实现代码的模块化和解耦。

AspectJ 的主要优势

  • 成熟稳定

AspectJ 自 2001 年发展至今,已经是一个非常成熟和稳定的框架。通常在使用时,不需要过多担心插入的字节码正确性相关的问题

  • 灵活性

AspectJ 允许在多个位置插入自定义的代码,如方法调用的位置、方法体内部、读写变量的位置、静态代码块内部以及异常处理位置的前后。它还可以直接将原位置的代码替换为自定义的代码。

  • 性能优势

AspectJ 在编译时完成切面织入,因此在运行时不需要额外的动态代理机制,这使得其性能通常优于基于动态代理的 AOP 实现(如 Spring AOP)

三种织入方式

AspectJ有三种织入方式,分别是静态织入【compile-time weaving】、编译后织入【post-compile weaving】、类加载时织入【load-time weaving】,这三种方式其实都是在main方法主程序执行前去完成字节码的修改的,下面分别介绍下这三种方式

准备工作

准备一个maven的父子工程,如下

aspectj的pom依赖

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.7</version>
</dependency>

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.7</version>
</dependency>

CTW静态织入【compile-time weaving】

案例演示在aspectj-def模块中

先定义一个自定义注解

package com.tml.aspectj.def.aspect;

import java.lang.annotation.*;

@Target({ElementType.ANNOTATION_TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface TradeLog {
}

定义一个切面

package com.tml.aspectj.def.aspect;

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

@Aspect
public class TradeLogAspect {

    @Pointcut("@annotation(com.tml.aspectj.def.aspect.TradeLog) && execution(* *(..))")
    private void tradeLogPoint() {

    }

    @Around("tradeLogPoint()")
    public Object tradeLogWithAspectJ(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        System.out.println("TradeLogAspect.tradeLogPoint begin");
        Object proceed = joinPoint.proceed();
        System.out.println("TradeLogAspect.tradeLogPoint end, --costTime :" + (System.currentTimeMillis() - startTime));
        return proceed;
    }
}

定义一个目标类

package com.tml.aspectj.def.service;

import com.tml.aspectj.def.aspect.TradeLog;

public class DefService {

    @TradeLog
    public String  sayHello(String name) throws InterruptedException {
        Thread.sleep(1500);
        return "Hello " + name;
    }
}

 测试类

package com.tml.aspectj.def.service;


public class Main {
    public static void main(String[] args) throws InterruptedException {
        DefService defService = new DefService();
        String s = defService.sayHello("tml");
        System.out.println(s);

    }
}

 直接运行测试类,结果如下

运行结果并没有增强,切面的逻辑没有执行到,为什么呢?

在CTW模式下,编译器是需要采用一种特有的编译器ajc,这里没有使用ajc编译器,当然没有生效。于是,在pom中增加aspectj-maven-plugin,如下

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.14.0</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <complianceLevel>1.8</complianceLevel>
        <showWeaveInfo>true</showWeaveInfo>
        <Xlint>ignore</Xlint>
        <encoding>UTF-8</encoding>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
            </goals>
        </execution>
</executions>

</plugin>

   执行maven compile后

看下class文件和之前有什么不同

可以看到编译之后的class文件中有ajc的字眼,这里就表示静态织入成功了,运行一下,看看结果

通过运行结果发现,环绕通知已经切入生效,达到预期。 

当然,切面的定义和切面的使用,并不一定在同一个子模块中,假设我切面的定义是在aspectj-def模块中,而目标类是在aspectj-user中,这个又需要怎么配置呢?

先在aspectj-user中定义一个新的目标类

package com.tml.aspectj.user.service;

import com.tml.aspectj.def.aspect.TradeLog;

public class UserService {

    @TradeLog
    public String  sayHello(String name) throws InterruptedException {
        Thread.sleep(1500);
        return "你好 " + name;
    }
}

接着在aspectj-user中定义一个新的测试类

package com.tml.aspectj.user.service;


public class Main {
    public static void main(String[] args) throws InterruptedException {
        UserService userService = new UserService();
        String s = userService.sayHello("tml");
        System.out.println(s);

    }
}

 aspectj-user子工程的pom

            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>aspectj-maven-plugin</artifactId>
                <version>1.14.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <complianceLevel>1.8</complianceLevel>
                    <showWeaveInfo>true</showWeaveInfo>
                    <Xlint>ignore</Xlint>
                    <encoding>UTF-8</encoding>
                    <aspectLibraries>
                        <aspectLibrary>
                            <groupId>com.tml</groupId>
                            <artifactId>aspectj-def</artifactId>
                        </aspectLibrary>
                    </aspectLibraries>

                

                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>

            </plugin>

和上一个工程不同之处,在于插件中增加了aspectLibraries的配置,maven compile后,看下目标类的class文件结构

和刚刚的class文件结构不一样,多了一个 UserService$AjcClosure1的class文件

运行看一下结果,如下

环绕通知也执行了,这种情况的静态织入可能是最容易出错的,aspectj-maven插件配置稍微不对,切面就不会生效。 

PCW编译后织入【post-compile weaving】

编译后织入,值得是需要为另外一个模块或者jar中的class进行增强,比如我需要在aspectj-user中使用一个新的切面来增强DefService

在aspectj-user中定义一个新的切面

package com.tml.aspectj.user.service;

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

@Aspect
public class PrintAspect {

    @Pointcut("execution(* com.tml.aspectj.def.service.*.*(..))")
    private void print() {

    }

    @Before("print()")
    public void before(JoinPoint joinPoint) {
        System.out.println("当前类className"+joinPoint.getTarget().getClass());
    }
}

 这里的切入点表达式,刚好能命中aspectj-def中的DefService

aspectj-user模块中新增一个测试类

package com.tml.aspectj.user.service;

import com.tml.aspectj.def.service.DefService;

public class Main1 {
    public static void main(String[] args) throws InterruptedException {
        DefService defService = new DefService();
        String s = defService.sayHello("aspectj");
        System.out.println(s);

    }
}

aspectj-user的pom配置

         <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>aspectj-maven-plugin</artifactId>
                <version>1.14.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <complianceLevel>1.8</complianceLevel>
                    <showWeaveInfo>true</showWeaveInfo>
                    <Xlint>ignore</Xlint>
                    <encoding>UTF-8</encoding>
                    <aspectLibraries>
                        <aspectLibrary>
                            <groupId>com.tml</groupId>
                            <artifactId>aspectj-def</artifactId>
                        </aspectLibrary>
                    </aspectLibraries>

                    <weaveDependencies>
                        <dependency>
                            <groupId>com.tml</groupId>
                            <artifactId>aspectj-def</artifactId>
                        </dependency>
                    </weaveDependencies>

                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>

            </plugin>

相比跨模块的静态织入,这里多了weaveDependencies的配置

执行maven compile后,看下class文件的结构 

这个是aspectj-user模块的class文件结构,可以看到这里把aspectj-def中的class文件也拷贝过来了

运行看下结果

可以看到DefService上的两个切面都执行了,因为环绕通知的优先级高于前置通知,所以执行结果如上 ,执行结果也是符合预期

LTW类加载时织入【load-time weaving】

LTW方式织入,其实就已经不在依赖ajc的编译器了,他采用的方式是javaagent,这种方式可能在实际应用中使用比较少。javaagent执行时机是在main主程序之前,也就是说他会额外增加项目的启动时间。

这里我们在aspectj-def模块中去验证

首先去掉aspectj-maven插件,意思是不再使用ajc来进行编译织入

接着在resource、META-INF目录下,创建一个aop.xml文件,文件的内容如下

<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>

    <weaver>
        <!-- 只织入特定包下的类 -->
        <include within="com.tml.aspectj.def..*"/>
    </weaver>

    <aspects>
        <!-- 织入这个切面 -->
        <aspect name="com.tml.aspectj.def.aspect.TradeLogAspect"/>
    </aspects>

</aspectj>

编译好aspectj-def,看下class文件,并没有ajc的增强

项目运行前,增加jvm启动参数,如下

 -javaagent:C:\org\aspectj\aspectjweaver\1.9.7\aspectjweaver-1.9.7.jar

运行Main结果如下,符合预期,结果是进行了增强

TradeLogAspect.tradeLogPoint begin
TradeLogAspect.tradeLogPoint end, --costTime :1512
Hello tml 

总结

通过上面的多个用例,aspectj的三种织入方式都有了一个基本的了解。

其中,跨模块之间的CTW可能是最为重要的,实际工程应用中也是最常见的,需要尤为注意。

最后,相关的测试代码已经上传到github,如果感兴趣可以前往https://github.com/tianmlin19/aspecj-demo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值