Nop入门:极简AOP实现
讲解视频:哔哩哔哩_bilibili
AOP(Aspect Oriented Programming)本质上是一个非常简单的概念(定位+局部逻辑修改),但是它的实现却往往并不简单。作为Spring框架的一个核心功能,SpringAOP中引入的异常复杂的切点定义语法。但是在实际使用场景中,唯一得到广泛应用的切点(Pointcut)定义方法就是使用注解(Annotation)。
另外一方面,SpringAOP会使用动态代码生成或者动态代理类,这会影响到应用的启动速度或者运行时性能,对于GraalVM原生编译也不友好。Nop平台采用代码生成的方式实现了一个最简单的AOP机制,具有最优的性能且完全满足我们一般应用的需求,全部代码仅有不到2000行。
NopIoC容器内置了对AOP的支持,这也简化了NopIoC容器的实现,使得它不必像Spring那样为了支持AOP被迫引入多级对象缓存(NopIoC仅使用了一级对象缓存)。
本文将简单介绍一下NopAOP的实现原理和使用方法,详细介绍参见aop.md
一. 注册用于AOP切点的注解类
NopAOP并不使用动态字节码生成技术,而是直接生成代理类的源码,因此它需要在编译的时候知道需要为哪些类生成代理类。具体的做法就是扫描所有类的方法,识别哪些方法使用了特殊的注解,并为这些类生成代理类。
例如在nop-quarkus-demo
项目中,我们增加了一个SendEmail的注解类和/_vfs/nop/aop/nop-quarkus-demo.annotations
文件。
io.nop.demo.annotations.SendEmail
-
AOP的应用需要程序的结构空间存在一种稳定的定位坐标体系。
-
复杂的Pointcut相当于是通过动态计算得到的一种不稳定的定位坐标。例如如果pointcut是匹配函数名的前缀或者后缀,那么当程序员不按照特定的命名规范编写代码时,编译器不会报任何错误,但是AOP却拦截不到对应的方法。
-
使用自定义的注解相当于是在程序的结构空间提供一种专用于AOP的稳定坐标。对比之下,方法名可能因为各种情况而改变,无法保证稳定。
-
Nop平台在生成AOP代理类时会扫描
/nop/aop/
目录下的所有后缀名为annotations的文件。一般情况下这个文件名会与所在的模块名同名,避免不同模块定义的文件名发生冲突。
二. 生成AOP代理类
Java生态中用于编译期代码生成的标准技术方案是APT(Annotation Processing Tool),它是 Java编译器的一个功能,用于在编译期间执行对源代码中注解(Annotations)的处理。APT 可以读取和分析源代码中的注解,并生成新的代码(如类、接口、枚举等)或者生成额外的文件(如XML、配置文件等)。但是使用这个技术需要了解一定的APT的相关知识,所编写的代码还不能独立于Java编译过程来使用,因此NopAOP没有使用APT技术,而是直接提供了一个GenAopProxy帮助类使用Java反射技术获取类的方法信息,然后生成AOP代理类。
在nop-entropy项目的根pom文件中预定义了exec-maven-plugin插件的参数,设置了aop执行阶段,它会负责执行AOP代码生成工作。
<!-- nop-entropy的pom文件 -->
<pom>...<pluginManagement><plugins><plugin><groupId>org.codehaus.mojo</groupId><artifactId>exec-maven-plugin</artifactId><version>3.0.0</version><executions><execution><id>aop</id><phase>compile</phase><goals><goal>java</goal></goals><configuration><arguments><argument>${project.basedir}</argument><argument>aop</argument></arguments></configuration></execution></executions><configuration><classpathScope>compile</classpathScope><includePluginDependencies>true</includePluginDependencies><includeProjectDependencies>true</includeProjectDependencies><addResourcesToClasspath>true</addResourcesToClasspath><mainClass>io.nop.codegen.task.CodeGenTask</mainClass><cleanupDaemonThreads>false</cleanupDaemonThreads></configuration></plugin></plugins></pluginManagement>
</pom>
在需要生成代理类的模块中,我们可以从nop-entropy的根pom继承,并引入exec-maven-plugin插件即可。
<pom><parent><groupId>io.github.entropy-cloud</groupId><artifactId>nop-entropy</artifactId><version>2.0.0-SNAPSHOT</version></parent><build><plugins><plugin><groupId>org.codehaus.mojo</groupId><artifactId>exec-maven-plugin</artifactId></plugin></plugins></build>
</pom>
在示例应用中,我们为DemoBizModel的testMethod1方法增加@SendEmail
注解。
@BizModel("Demo")
public class DemoBizModel {@BizQuery@SendEmailpublic void testMethod1(@RequestBean MyRequest request) {System.out.println("doSomething");}
}
然后执行mvn package或者mvn install指令,通过exec-maven-plugin插件会自动生成一个对应的DemoBizModel__aop类。
package io.nop.demo.biz;@io.nop.api.core.annotations.aop.AopProxy({io.nop.demo.annotations.SendEmail.class})
public class DemoBizModel__aop extends io.nop.demo.biz.DemoBizModel implements io.nop.core.reflect.aop.IAopProxy {private io.nop.core.reflect.aop.IMethodInterceptor[] $$interceptors;@Overridepublic void $$aop_interceptors(io.nop.core.reflect.aop.IMethodInterceptor[] interceptors) {this.$$interceptors = interceptors;}private static io.nop.core.reflect.IFunctionModel $$testMethod1_0;static {try {$$testMethod1_0 = io.nop.core.reflect.impl.MethodModelBuilder.from(io.nop.demo.biz.DemoBizModel.class, io.nop.demo.biz.DemoBizModel.class.getDeclaredMethod("testMethod1", io.nop.demo.biz.MyRequest.class));} catch (Exception e) {e.printStackTrace();}}public DemoBizModel__aop() {super();}@Overridepublic void testMethod1(final io.nop.demo.biz.MyRequest arg0) {if (this.$$interceptors == null || this.$$interceptors.length == 0) {super.testMethod1(arg0);return;}io.nop.core.reflect.aop.CallableMethodInvocation $$methodInv = new io.nop.core.reflect.aop.CallableMethodInvocation(this,new java.lang.Object[]{arg0}, $$testMethod1_0, () -> {super.testMethod1(arg0);return null;});io.nop.core.reflect.aop.AopMethodInvocation $$inv = new io.nop.core.reflect.aop.AopMethodInvocation($$methodInv, this.$$interceptors);try {$$inv.proceed();} catch (java.lang.Exception e) {throw io.nop.api.core.exceptions.NopException.adapt(e);}}
}
- 生成的代理类上通过
@AopProxy
注解来快速获取实际用到的注解类 - NopAOP不使用Java内置的Method类,而是使用IFunctionModel接口,使得在Java反射系统之外也可以复用这些Interceptor
三. 增加IMethodInterceptor实现类
IMethodInterceptor接口类似于Spring中使用的MethodInterceptor接口,只是使用IFunctionModel代替Java反射模型。
public class SendEmailInterceptor implements IMethodInterceptor {@Overridepublic Object invoke(IMethodInvocation inv) throws Exception {if (inv.getArguments().length <= 0)return inv.proceed();Object arg = inv.getArguments()[0];if (arg instanceof MyRequest) {System.out.println("sendEmail:message=" + ((MyRequest) arg).getMessage());}return inv.proceed();}
}
四. 在IoC容器中注册Interceptor,并声明Pointcut
<bean id="sendEmailInterceptor" class="io.nop.demo.interceptors.SendEmailInterceptor"><ioc:pointcut annotations="io.nop.demo.annotations.SendEmail"/></bean>
- NopIoC为Spring1.0的语法扩展了
<ioc:pointcut>
节点,通过它表示Interceptor所作用的AOP切点。 - 在bean容器初始化的时候会检查容器中是否有可应用的Interceptor,如果有,则用代理类的Constructor代替原始类的Constructor。
基于可逆计算理论设计的低代码平台NopPlatform已开源:
- gitee: canonical-entropy/nop-entropy
- github: entropy-cloud/nop-entropy
- 开发示例:docs/tutorial/tutorial.md
- 可逆计算原理和Nop平台介绍及答疑_哔哩哔哩_bilibili