Spring LTW+DDD实现业务数据变化的自动跟踪与回退
Posted coding涛
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring LTW+DDD实现业务数据变化的自动跟踪与回退相关的知识,希望对你有一定的参考价值。
背景
我们的应用系统中会保存各类业务数据,这些业务数据在新增后会经常变化。用户通过各类业务操作来修改业务数据。对于一些比较重要的业务数据,出于审计或其他因素的考虑,经常需要跟踪记录变更过程,记录的信息主要包括:变更时间、变更动作名、变更前的值,变更后的值、变更用户等。另外,有些业务数据的变化需要经过审批才能生效。若审批拒绝,还需要支持操作的回退。
一种业务数据一般对于一个或多个实体类。为了避免变更过程记录代码与业务处理代码耦合,我们可以使用AOP面向切面编程,在实体的赋值方法中织入切面,来实现变更过程的自动跟踪记录。
传统方式实现
传统的面向过程的开发模式中,实体被设计成贫血模型,即只有getter/setter方法,实体的变更是直接调用setter方法完成。这种情况下,如果使用AOP在setter方法中织入切面,会有以下两个问题:
1)记录下的变更前后的数据,没有业务意义。我们只能知道变更前后的值,很难获取到变更对应的业务动作。例如:电商系统中,用户修改了商品的价格。如果单纯地拦截setter方法,则无法知道用户修改商品价格的业务背景,是商户为了纠正错误的价格还是商户要促销或者要涨价等。
2)实体的变更都对应一个有意义的业务动作,这个业务动作往往包含多个字段的修改。而拦截setter方法只能获取到分隔的变更记录,很难将多条字段的变更记录关联起来。例如电商系统中,订单由待发货变成已发货,如果分开单独记录订单状态和订单物流号两个字段的变化,没有很大意义。
因此,传统面向过程的开发模式很难通过AOP做到变更过程记录代码与业务代码分离。
DDD方式实现
DDD领域驱动设计中,实体是充血模型,它含有具有业务意义的实体方法。实体方法可以分为两类:Query类型方法和Command类型方法。其中Query方法不对实体进行修改,实体的修改都通过Command方法完成,实体的setter方法一般会设置成private,避免被外界直接调用。这里注意不能删掉setter方法,因为在Java中,很多框架反射时需要使用到setter方法,例如fastjson、BeanUtils工具类。
基于DDD的实体,有一种简单的实体变化跟踪方式:在Command方法中发布变更事件。例如:
这里,在实体修改前,通过SerializationUtils工具类保存实体的所有值,然后,开始修改赋值,修改完成后,发布变更事件。在这里实体继承了Spring的AbstractAggregateRoot类,调用这个类的registerEvent方法完成事件的发布。注意:registerEvent发布的事件只有在save执行时才发布。这也符合我们的要求,即实体变更保存成功后才发布变更事件。
这种方式虽然简单,通过发布事件耦合度也很弱。但还是将变更记录代码与业务代码耦合了。实体只有在发布事件后才能跟踪到变化过程,如果不发布,则无法跟踪。如果需要实现“实体变更跟踪功能的动态控制,即在不变更系统的情况下,随时开启或关闭实体变更跟踪记录功能”,这种方式无法做到。
另一种方式就是基于AOP编程,自动在实体的Command方法中织入切面。
Java中的AOP编程
在Java 语言中,AOP功能从切面织入方式角度,可以分为如下3种方式:
1)CTW(Compile Time Weaving)编译期织入:在Java类的编译期,采用特殊的编译器,将切面织入到Java类中;
2)LTW(Load Time Weaving)类加载期织入:通过特殊的类加载器,在类字节码加载到JVM时,替换字节码,织入切面;
3)RTW(RunTime Weaving)运行期织入:采用CGLib工具或JDK动态代理进行切面的织入。
AspectJ和Spring都提供了AOP功能。其中AspectJ是最早提供AOP功能的java框架。Spring2.0开始支持AOP,并且配置方式也兼容AspectJ的配置。这两个框架对AOP的支持情况如下:
框架名 | CTW | LTW | RTW |
AspectJ | 支持 | 支持 | 不支持 |
Spring | 不支持 | 支持 | 支持 |
其中,
AspectJ的“CTW编译期织入”方式实现起来略有点麻烦,需要按照AspectJ的规定的语法规则编写一个aj文件,然后通过AspectJ的特殊的编译工具AJDT编译。
Spring的“RTW运行期织入”方式是基于CGLib或JDK动态代理实现,但它只能完成Bean的切面织入,而我们的实体往往是通过new方法实例化的,它不是受Spring管理的bean,Spring无法动态织入切面。
因此,要实现我们的要求,就得通过LTW方式织入切面。AspectJ和Spring都支持LTW。
LTW实现原理
JDK5.0 新增了 java.lang.instrument 包,通过这个包里面的类可以实现对 JVM 底层组件进行访问。借此向 JVM 注册类文件转换器,在类加载时对类文件的字节码进行转换,织入我们定义的切面,从而实现 AOP 功能。
java.lang.instrument 包主要包括以下两个接口:
1、ClassFileTransformer类字节码转换器。JVM启动,用于转换指定类的字节码。
2、Instrumentation仪器组件类。jvm中的一个内部组件,用于注册并持有ClassTransformer类字节码转换器。通过它,我们可以使用独立于应用程序之外的代理(agent)程序来监测和修改运行在JVM上的应用程序。
LTW过程
AspectJ LTW 切面织入过程
JVM启动时,通过 JVM 的 javaagent 代理参数获取到Instrumentation,然后通过JDK动态代理,向Instrumentation中注册ClassFileTransformer;类加载时,ClassFileTransformer读取 AspectJ 的配置文件,即类路径下的 META-INF/aop.xml 文件,获取切面,接着对类进行字节码转换,织入切面。
AspectJ LTW配置比较简单:
1、新增@AspectJ切面配置类
2、在classpath下添加META-INF/aop.xml文件,配置需要织入切面的实体类的范围和切面处理类
3、jvm启动时,添加启动参数:-javaagent:"D:/aspectjweaver.jar"(请改成实际jar文件路径)
Spring LTW切面织入过程
Spring LTW官方文档地址: https://docs.spring.io/spring/docs/5.1.8.RELEASE/spring-framework-reference/core.html#aop-aj-ltw
Spring LTW 基本原理与AspectJ相同,配置也一样,都是使用classpath下的META-INF/aop.xml文件配置切面定义类和需要织入切面的类的范围。
不同的地方在于“向Instrumentation中注册ClassFileTransformer”的方式不同:
1)AspectJ LTW是通过JDK动态代理实现,它需要借助jvm启动代理参数javaagent,这种方式比较粗放,它对整个JVM有效,如果一个web服务器部署多个应用,那么一个应用的AspectJ LTW配置会影响同一个服务器的其他应用。
2)Spring LTW 则自定义定义了一个LoadTimeWeaver接口,用于控制ClassFileTransformer的注册。
在不同的启动环境下,使用的LoadTimeWeaver实现类不同。Spring提供了以下环境的LoadTimeWeaver接口实现类:
启动环境 | LoadTimeWeaver 实现类 |
Apache Tomcat | TomcatLoadTimeWeaver |
GlassFish (使用EAR包部署) | GlassFishLoadTimeWeaver |
Red Hat’s JBoss AS 或WildFly | JBossLoadTimeWeaver |
IBM’s WebSphere | WebSphereLoadTimeWeaver |
Oracle’s WebLogic | WebLogicLoadTimeWeaver |
带-javaagent : spring-instrument.jar参数启动JVM | InstrumentationLoadTimeWeaver |
带addTransformer方法的其他环境 | ReflectiveLoadTimeWeaver |
另外,spring提供了一个DefaultContextLoadTimeWeaver类,这个类会自动检测当前环境并自动选择使用哪个LoadTimeWeaver。当我们未指定LoadTimeWeaver实现类时,会默认使用DefaultContextLoadTimeWeaver类来检测。
如果Spring内置的LoadTimeWeaver接口实现类不满足要求,则可以自定义类转换器,并使用如下方式配置:
@Configuration @EnableLoadTimeWeaving publicclass AppConfig implements LoadTimeWeavingConfigurer { @Override public LoadTimeWeaver getLoadTimeWeaver() { MyLoadTimeWeaver ltw = new MyLoadTimeWeaver(); ltw.addClassTransformer(myClassFileTransformer); // ... returnltw; } } |
Spring LTW将LTW过程控制只在ClassLoader内有效。因此不存在像AspectJ LTW那样的“一个应用的LTW配置影响同一个服务器的其他应用”的问题,切面织入过程的控制上比AspectJ更精细化。
Spring LTW配置过程
1、开启Spring LTW功能
在任意配置类上,使用@EnableLoadTimeWeaving注解。它包含一个属性aspectjWeaving,属性值列表如下:(一般使用默认值即可)
注解值 | 备注 |
ENABLED | 开启LTW |
DISABLED | 关闭LTW |
AUTODETECT(默认) | 检测META-INF/aop.xml是否存在,如果存在则开启LTW功能;否则关闭LTW功能 |
2、新增@Aspec切面配置类(与AspectJ相同)
3、创建META-INF/aop.xml文件(与AspectJ相同)
Spring LTW配置时,需要注意的问题
1、在SpringBoot环境下,使用Spring LTW功能时,如果使用了内嵌的服务器,比如使用内嵌的Tomcat服务器。此时,LoadTimeWeaver实现类并不是TomcatLoadTimeWeaver类,因为启动方式并不是通过Tomcat服务器启动,而是通过jvm直接启动。因此,SpringBoot如果是通过main启动,则启动参数要添加-javaagent:D:/path/spring-instrument.jar参数,此时对应的LoadTimeWeaver实现类是InstrumentationLoadTimeWeaver。具体过程可以通过调试DefaultContextLoadTimeWeaver类的setBeanClassLoader方法看到。
2、@AspectJ切面类不能添加@Component注解,即不能为Spring Bean。
3、aop.xml文件中,weaver节点配置切面织入范围时,包的路径中,必须包含了@AspectJ切面类,否则会报找不到aspectOf方法的错误,例如:
java.lang.NoSuchMethodError: com.***.aspectOf()Lcom/**;
4、weaver节点配置时,如果需要扫描某个包及其子包,不能配置为:“com.test.*”,而应该为:“com.test..*”。注意,使用两个点好表示当前包及其子包。
5、关于jar包依赖,只需要添加Spring AOP功能相关的jar包即可。如果是SpringBoot,只需要添加AOP功能。网上有些文章把jar包和编译脚本写得十分复杂,误导了很多读者。
以上是关于Spring LTW+DDD实现业务数据变化的自动跟踪与回退的主要内容,如果未能解决你的问题,请参考以下文章