DDD实践:实现基于快照机制的变更追踪

2023年 8月 22日 47.5k 0

王有志,一个分享硬核Java技术的互金摸鱼侠
加入Java人的提桶跑路群:共同富裕的Java人

去年我们在重构项目中落地了DDD,当时花了点时间研究了下阿里巴巴大淘宝技术发布的《阿里技术专家详解DDD系列》,其中第三讲《阿里技术专家详解DDD系列 第三讲 - Repository模式》中提到了一项技术--变更追踪。

简单来说,变更追踪是记录对象进行业务操作后发生的改变,通过这些改变来决定如何更新数据库,文章中提到了两种实现变更追踪方案:

  • 基于Snapshot的方案:当数据从DB里取出来后,在内存中保存一份snapshot,然后在数据写入时和snapshot比较。常见的实现如Hibernate。
  • 基于Proxy的方案:当数据从DB里取出来后,通过weaving的方式将所有setter都增加一个切面来判断setter是否被调用以及值是否变更,如果变更则标记为Dirty。在保存时根据Dirty判断是否需要更新。常见的实现如Entity Framework。
  • 不过由于只给出了Snapshot方案的部分实现代码,导致很多读者对产生了疑惑。

    图1:评论.png

    我们在工程实践中借鉴了Snapshot方案的设计,并根据自身的业务情况做出了一些调整,下面就和大家分享我们在工程中的实践。

    叠“BUFF”:

    • 今天的主题是实现变更追踪而不是DDD,所以尽量不要把DDD的“战火”引过来;
    • 以下代码未经过严格的测试,可能存在BUG,欢迎大家批评指正和讨论。

    开始前的准备工作

    聚合与Repository接口的定义

    正式开始前,我们先做一些简单的准备工作,主要是DDD设计中的接口定义,首先是定义接口Aggregate和Identifier:

    public interface Aggregate extends Serializable {
    	ID getId();
    }
    
    public interface Identifier extends Serializable {
    	Serializable value();
    }
    

    接着定义Repository接口并提供3个基础能力:

    public interface Repository {
    
    	/**
       * 保存
    	 * @param aggregateRoot
    	 * @throws IllegalAccessException
    	 */
    	void save(T aggregateRoot) throws IllegalAccessException;
    
    	/**
       * 删除
    	 * @param aggregateRoot
    	 */
    	void remove(T aggregateRoot);
    
    	/**
       * 查询
    	 * @param identifier
    	 * @return
    	 */
    	T find(ID identifier);
    }
    

    Repository是Service(业务逻辑)与DAO(Data Access Object,数据访问对象)间的“桥梁”,用于隔离业务逻辑与数据库之间的依赖,帮助我们屏蔽在数据库发生变更时对业务逻辑产生的影响,这点是DDD设计相关的内容,我们在这里不过多的讨论。

    领域对象与Repository服务的定义

    我们定义一个简单书籍和图片的实体:

    @Getter
    @Setter
    public class Book implements Aggregate {
    
    	private BookId bookId;
    
    	private String bookName;
    
    	private String bookDesc;
    
    	private Long words;
    
    	private List images;
    
    	private List contents;
    
    	@Override
    	public BookId getId() {
    		return this.bookId;
    	}
    }
    
    @Getter
    @Setter
    public class BookId implements Identifier {
    
    	private Long bookId;
    
    	@Override
    	public Serializable value() {
    		return this.bookId;
    	}
    }
    
    @Getter
    @Setter
    public class Image implements Aggregate {
    
    	private ImageId imageId;
    
    	private String imageUrl;
    
    	@Override
    	public ImageId getId() {
    		return this.imageId;
    	}
    }
    
    @Getter
    @Setter
    public class ImageId implements Identifier {
    
    	private long imageId;
    
    	@Override
    	public Serializable value() {
    		return this.imageId;
    	}
    }
    

    在有些DDD的实践规范中,实体中是不允许出现Getter方法和Setter方法的,这里为了方便提供测试数据,直接使用了lombok的注解添加Getter方法和Setter方法。

    最后我们来定义实体Book的Repository服务:

    public interface BookRepository extends Repository {
    
    }
    
    public class BookRepositoryImpl implements BookRepository {
    
    	@Override
    	public void save(Book aggregateRoot) {
    		// 实现保存逻辑
    	}
    
    	@Override
    	public void remove(Book aggregateRoot) {
    		// 实现删除逻辑
    	}
    
    	@Override
    	public Book find(BookId identifier) {
    		Book book = new Book();
    		// 实现查询逻辑
    		return book;
    	}
    }
    

    BookRepository接口的意义是方便自定义Repository方法,BookRepositoryImpl是BookRepository具体的实现,这里我们只使用3个基础功能即可,具体的实现逻辑是调用DAO实现增删改查,并借助Convert工具实现DO与实体的转换,我们这里就省略这部分内容了,实际上是我懒得写了。

    变更追踪的实现

    RepositorySupport的实现

    变更追踪的核心是在调用Repository的基础能力时进行实体对象的追踪,并在保存时对比实体对象的变化,具体的执行逻辑如下:

    • 调用Repository#find时,复制实体对象的快照,添加的变更追踪的容器中;
    • 调用Repository#save时,对比当前实体对象与快照,返回两者间的差异;
    • 调用Repository#remove时,删除变更追踪容器中实体对象的快照。

    在我们的工程实践中,核心设计采用了阿里巴巴在《阿里技术专家详解DDD系列 第三讲 - Repository模式》给出的方案,但在具体的实现细节上,我们做了一些调整,接下来就和大家分享下我们的设计。

    首先来实现通用支撑类RepositorySupport,提供可复用的变更追踪能力:

    public abstract class RepositorySupport implements Repository {
    	
    	private final AggregateTracingManager aggregateTracingManager;
    
    	public RepositorySupport() {
    		this.aggregateTracingManager = new ThreadLocalTracingManager();
    	}
    
    	/**
       * 由继承RepositorySupport的子类实现
    	 */
    	protected abstract T onSelect(ID id);
    	protected abstract void onInsert(T aggregate);
    	protected abstract void onUpdate(T aggregate, AggregateDifference aggregateDifference);
    	protected abstract void onDelete(T aggregate);
    	
    	/**
       * 主动追踪
    	 * @param id
    	 * @return
    	 */
    	public void attach(T aggregate) {
    		this.aggregateTracingManager.attach(aggregate);
    	}
    
    	/**
       * 差异对比
    	 * @param aggregate
    	 * @return
       * @throws IllegalAccessException
    	 */
    	protected AggregateDifference different(T aggregate) throws IllegalAccessException {
    		return this.aggregateTracingManager.different(aggregate);
    	}
    	
    	/**
       * 解除追踪
    	 * @param id
    	 * @return
    	 */
    	public void detach(T aggregate) {
    		this.aggregateTracingManager.detach(aggregate);
    	}
    
    	@Override
    	public T find(ID identifier) {
    		T aggregate = this.onSelect(identifier);
    		if (aggregate != null) {
    			this.aggregateTracingManager.attach(aggregate);
    		}
    		return aggregate;
    	}
    
    	@Override
    	public void save(T aggregate) throws IllegalAccessException {
    		AggregateDifference aggregateDifference = this.aggregateTracingManager.different(aggregate);
    		if (DifferenceTypeEnum.ADDED.equals(aggregateDifference.getDifferentType())) {
    			this.onInsert(aggregate);
    		} else {
    			this.onUpdate(aggregate, aggregateDifference);
    		}
    		this.aggregateTracingManager.merge(aggregate);
    	}
    
    	@Override
    	public void remove(T aggregate) {
    		this.onDelete(aggregate);
    		this.aggregateTracingManager.detach(aggregate);
    	}
    }
    

    我们依次对通用支撑类RepositorySupport中的成员变量和方法进行说明。

    首先是RepositorySupport中唯一的成员变量AggregateTracingManager,该类的功能是完成变更追踪快照的管理,包括对象追踪,差异对比和解除追踪等。

    接着是继承RepositorySupport的实现类需要重写的方法:

    • RepositorySupport#onSelect,由RepositorySupport中实现的Repository#find调用,与直接实现Repository#find相同,通过DAO查询数据,并转换为实体对象;
    • RepositorySupport#onInsert,由RepositorySupport中实现的Repository#save调用,与直接实现Repository#save类似,通过DAO保存数据,此时为新增数据的保存;
    • RepositorySupport#onUpdate,由RepositorySupport中实现的Repository#save调用,与直接实现Repository#save类似,通过DAO保存数据,此时为修改数据的保存;
    • RepositorySupport#onDelete,由RepositorySupport中实现的Repository#remove调用,与直接实现Repository#remove相同,通过DAO删除数据。

    接着是Repository中定义的提供变更追踪能力的方法:

    • RepositorySupport#attach,主动追踪,当实体的Repository接口中自定义查询方法时,实现类可以通过该方法实现对象的变更追踪;
    • RepositorySupport#different,差异对比,当实体的Repository接口中自定义保存方法时,实现类可以通过该方法获取当前实体对象与快照的差异;
    • RepositorySupport#detach,解除追踪,当实体的Repository接口中自定义删除方法时,实现类可以通过该方法解除对象的变更追踪。

    最后是RepositorySupport中对Repository接口的实现,实现中确定了RepositorySupport#onSelectRepositorySupport#onInsertRepositorySupport#onUpdateRepositorySupport#onDelete方法的调用时机,并通过AggregateTracingManager来管理追踪对象:

    • RepositorySupport#find的实现中,通过RepositorySupport#onSelect查询实体对象,并决定是否调用AggregateTracingManager#attach进行变更追踪;
    • RepositorySupport#save的实现中,调用AggregateTracingManager#different获取当前实体对象与快照间的差异,并根据差异的类型选择执行RepositorySupport#onInsertRepositorySupport#onUpdate,最后调用AggregateTracingManager#merge将变更后的对象合并到变更追踪容器中;
    • RepositorySupport#remove的实现中,调用RepositorySupport#onDelete删除数据,并调用AggregateTracingManager#detach解除对象的追踪。

    AggregateTracingManager的实现

    AggregateTracingManager提供了管理变更追踪的能力,接口设计如下:

    public interface AggregateTracingManager {
    
    	/**
       * 变更追踪
    	 * @param aggregate
    	 */
    	void attach(T aggregate);
    
    	/**
       * 解除追踪
    	 * @param aggregate
    	 */
    	void detach(T aggregate);
    
    	/**
       * 对比差异
    	 * @param aggregate
    	 * @return
    	 */
    	AggregateDifference different(T aggregate) throws IllegalAccessException;
    
    	/**
       * 合并变更
    	 * @param aggregate
    	 */
    	void merge(T aggregate);
    }
    

    接着提供一个AggregateTracingManager的实现类,我们的工程中同样选择了ThreadLocal来实现线程隔离:

    public class ThreadLocalTracingManager implements AggregateTracingManager {
    
    	private final ThreadLocal context;
    
    	public ThreadLocalTracingManager() {
    		this.context = ThreadLocal.withInitial(MapContext::new);
    	}
    
    	@Override
    	public void attach(T aggregate) {
    		this.context.get().tracing(aggregate.getId(), aggregate);
    	}
    
    	@Override
    	public void detach(T aggregate) {
    		this.context.get().remove(aggregate.getId());
    	}
    
    	@Override
    	public AggregateDifference different(T aggregate) throws IllegalAccessException {
    		T snapshot = this.context.get().find(aggregate.getId());
    		return DifferentUtils.different(snapshot, aggregate);
    	}
    
    	@Override
    	public void merge(T aggregate) {
    		attach(aggregate);
    	}
    }
    

    最后是定义变更追踪中用于存储快照的容器TraceContext接口:

    public interface TraceContext {
    
    	void add(ID id, T aggregate);
    
    	T find(ID id);
    
    	void remove(ID id);
    }
    

    TraceContext的功能比较简单,提供了3个方法:

    • void add(ID id, T aggregate),添加追踪对象;
    • T find(ID id),获取追踪对象的快照;
    • void remove(ID id),删除追踪对象。

    这里我提供一个使用HashMap做存储容器的简单实现:

    public class MapContext implements TraceContext {
    
    	private final Map snapshots;
    
    	public MapContext() {
    		this.snapshots = new HashMap();
    	}
    
    	@Override
    	public void add(ID id, T aggregate) {
    		T snapshot = SnapshotUtils.snapshot(aggregate);
    		this.snapshots.put(aggregate.getId(), snapshot);
    	}
    
    	@Override
    	public T find(ID id) {
    		for (Map.Entry entry : this.snapshots.entrySet()) {
    			ID entryId = entry.getKey();
    			if (id.getClass().equals(entryId.getClass()) && entryId.value().equals(id.value())) {
    				return entry.getValue();
    			}
    		}
    		return snapshots.get(id);
    	}
    
    	@Override
    	public void remove(ID id) {
    		this.snapshots.remove(id);
    	}
    }
    

    至此,我们已经完成了变更追踪的整体框架。实际上我们在工程中实现的AggregateTracingManager和TraceContext会更加复杂,并添加了一些具有我司特色的功能,这里大家可以根据各自的情况做出不同的实现。

    变更追踪中的工具类实现

    由于《阿里技术专家详解DDD系列 第三讲 - Repository模式》文中的重点是介绍变更追踪这项技术,因此忽略了几个较为关键的工具类的实现,导致很多人在落地这项技术上遇到了困境,这里我结合工程中的实践,结合我个人的思考,给大家提供一个设计思路。

    SnapshotUtils的实现

    SnapshotUtils用于实现Aggregate的拷贝,因为在MapContext#find方法的实现中是通过类型与值的对比来获取对象,因此我们在SnapshotUtils的实现中只需要实现深拷贝即可:

    public class SnapshotUtils {
    
    	@SuppressWarnings("unchecked")
    	public static  T snapshot(T aggregate) throws IOException, ClassNotFoundException {
    		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    		ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
    		objectOutputStream.writeObject(aggregate);
    
    		ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
    		ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
    		T snapshot = (T) objectInputStream.readObject();
    
    		objectOutputStream.close();
    		byteArrayOutputStream.close();
    
    		objectInputStream.close();
    		byteArrayInputStream.close();
    		return snapshot;
    	}
    }
    

    据我推测阿里巴巴大淘宝技术在文中使用的SnapshotUtils中除了Identifier外的其余字段是深拷贝,我们的实践中允许Identifier也进行深拷贝,所以可以通过序列化与反序列化的方式进行深拷贝。

    除了序列化的方式外,还有很多其他的方式可以实现深拷贝,我见过使用JSON工具来回倒腾实现深拷贝,或者可以使用BeanUtil等等。

    Tips:有些工具的使用是有前提的,比如需要Getter和Setter方法,又或者使用序列化的方式需要继承Serializable接口。

    使用Java Objec Diff实现DiffUtils

    DiffUtils用于实现两个Java对象间的对比,因为此类需求较少所以市面上可供使用的开源工具并不是很多,相对来说Java Objec Diff是使用较为广泛的开源项目,不过该项目最新版本是2018年更新的0.95版本,作者应该是停止维护Java Object Diff了,或是由于该项目属于工具类项目,目前已经达到了较为完备的状态,不需要进行太多的维护工作了。

    我们先来使用Java Objec Diff项目实现一个简单的Java对象对比工具,引入Java Objec Diff的依赖:

    
    	de.danielbechler
    	java-object-diff
    	0.95
    
    

    基于Java Objec Diff项目构建DiffUtils,这里给出一个简单的实现:

    public class DiffUtils {
    
    	public static EntityDiff diff(Object snapshot, Object obj) {
    		DiffNode diffNode = ObjectDifferBuilder.buildDefault().compare(obj, snapshot);
    
    		if (!diffNode.hasChanges()) {
    			return EntityDiff.EMPTY;
    		}
    
    		EntityDiff entityDiff = new EntityDiff();
    		entityDiff.setHasChanges(true);
    		diffNode.visit((node, visit) -> {
    			boolean hasChanges = node.hasChanges();
    			Object objValue = node.canonicalGet(obj);
    			Object snapshotValue = node.canonicalGet(snapshot);
    			// 处理其他的逻辑和构建EntityDiff对象
    		});
    
    		return entityDiff;
    	}
    }
    
    @Getter
    @Setter
    public class EntityDiff {
    
    	public static final EntityDiff EMPTY = new EntityDiff();
    
    	private boolean hasChanges;
    
    	// 省略其余属性的实现
    
    	public EntityDiff() {
    
    	}
    }
    

    EntityDiff的结构可以根据自身工程的需求进行定制化,我这里只是为了展示如何通过Java Objec Diff项目构建DiffUtils。

    具有我司特色的DifferentUtils

    接下来就该我来献丑了。

    图2:献丑了.png

    因为我们有一些定制化的需求(具体原因已经记不得了),所以当时没有选择使用Java Objec Diff项目而是实现了具有我司特色的Java对象的对比工具类DifferentUtils。

    首先是我们定义的4种差异状态:

    public enum DifferenceType {
    
    	/**
         * 新增
         */
    	ADDED(),
    
    	/**
         * 删除
         */
    	REMOVED(),
    
    	/**
         * 修改
         */
    	MODIFIED(),
    
    	/**
         * 无变化
         */
    	UNTOUCHED()
    }
    

    接着我们对结果进行了封装,分为两层,第一层是标记Aggregate差异的AggregateDifference:

    @Getter
    @Setter
    public class AggregateDifference {
    
    	/**
       * 快照对象
    	 */
    	private T snapshot;
    
    	/**
       * 追踪对象
       */
    	private T aggregate;
    
    	/**
       * 差异类型
       */
    	private DifferenceType differentType;
    
    	/**
       * 字段差异
       */
    	private Map fieldDifferences;
    }
    

    第二层是比较Aggregate字段差异的FieldDifference:

    @Getter
    @Setter
    public class FieldDifference {
    
    	/**
    	 * 字段名
    	 */
    	private String name;
    
    	/**
    	 * 字段类型
    	 */
    	private Type type;
    
    	/**
    	 * 快照值
    	 */
    	private Object snapshotValue;
    
    	/**
    	 * 当前值
    	 */
    	private Object tracValue;
    
    	/**
    	 * 差异类型
    	 */
    	private DifferenceType differenceType;
    }
    

    以及3个实现类,标记Java中原生类型的JavaTypeFieldDifference,标记集合类型的CollectionFieldDifference,以及标记实现Aggregate接口的AggregareFieldDifference:

    public class JavaTypeFieldDifference extends FieldDifference {
    }
    
    @Getter
    @Setter
    public class CollectionFieldDifference extends FieldDifference {
    
    	/**
    	 * 集合元素差异
    	 */
    	private List elementDifference;
    
    	public CollectionFieldDifference(String name, Type type, Object snapshotValue, Object tracValue) {
    		super(name, type, snapshotValue, tracValue);
    		this.elementDifference = new ArrayList();
    	}
    	public CollectionFieldDifference(String name, Type type, Object snapshotValue, Object tracValue, DifferenceType differenceType) {
    		super(name, type, snapshotValue, tracValue, differenceType);
    		this.elementDifference = new ArrayList();
    	}
    }
    
    @Getter
    @Setter
    public class AggregareFieldDifference extends FieldDifference {
    
    	private Map fieldDifferences;
    
    	private final Identifier identifier;
    
    	public AggregareFieldDifference(String name, Type type, Object snapshotValue, Object tracValue, DifferenceType differenceType, Identifier identifier) {
    		super(name, type, snapshotValue, tracValue, differenceType);
    		this.identifier = identifier;
    		this.fieldDifferences = new HashMap();
    	}
    }
    

    可以看到,我们在工程实践中并不支持Map类型的字段进行对比,这是因为在我们落地的DDD工程规范中,实现Aggregate接口的类中不允许出现Map类型的字段,只允许Java的8种基础类型(包装类型),String,List,值对象以及实体。

    准备工作完成后,我们开始实现DifferentUtils,首先定义方法声明,与上面的DiffUtils#diff存在一些差异,主要在泛型的使用上:

    public class DifferentUtils {
    	public static  AggregateDifference different(T snapshot, T aggregate) throws IllegalAccessException {
    		// 待实现
    	}
    }
    

    接着我们处理两个入参可能为null的情况进行处理,总计有4种情况:

    • snapsho == null && aggregate == null,此时认为是DifferenceType.UNTOUCHED
    • snapshot == null && aggregate != null,此时认为是DifferenceType.ADDED
    • snapshot != null && aggregate == null,此时认为是DifferenceType.REMOVED
    • snapshot != null && aggregate != null,这种情况需要对比字段的差异。

    此时我们可以得到用于入参为null时,返回DifferenceType的方法:

    private static DifferenceType basicDifferentType(Object snapshot, Object aggregate) {
    	if (snapshot == null && aggregate == null) {
    		return DifferenceType.UNTOUCHED;
    	}
    	if (snapshot == null) {
    		return DifferenceType.ADDED;
    	}
    	if (aggregate == null) {
    		return DifferenceType.REMOVED;
    	}
    	return null;
    }
    

    我们直接在DifferentUtils#different中调用DifferentUtils#basicDifferentType,并补充snapshot和aggregate均不为null时的处理:

    public static  AggregateDifference different(T snapshot, T aggregate) throws IllegalAccessException {
    	DifferenceType basicDifferenceType = basicDifferentType(snapshot, aggregate);
    	if (basicDifferenceType != null) {
    		return new AggregateDifference(snapshot, aggregate, basicDifferenceType);
    	}
    
    	Field[] fields = ReflectionUtils.getFields(aggregate);
    	// 标记Aggregate
    	DifferenceType aggregateDifferentType = aggregateDifferentType(fields, snapshot, aggregate);
    	// 构建AggregateDifference对象
    	AggregateDifference aggregateDifference = new AggregateDifference(snapshot, aggregate, aggregateDifferentType);
    	Map fieldDifferences = aggregateDifference.getFieldDifferences();
    	// 对比字段差异
    	setDifferences(snapshot, aggregate, fields, fieldDifferences);
    	return aggregateDifference
    }
    

    DifferentUtils#aggregateDifferentType方法,该方法用于对Aggregate进行标记:

    public static  DifferenceType aggregateDifferentType(Field[] fields, T snapshot, T aggregate) throws IllegalAccessException {
      DifferenceType differenceType = basicDifferentType(snapshot, aggregate);
      if (differenceType != null) {
    	  return differenceType;
      }
    
      boolean unchanged = true;
      for (Field field : fields) {
    	  field.setAccessible(true);
    
    		// 处理需要跳过的情形
    		if (shouldSkipClass(field.getType())) {
    			continue;
    		}
    
    	  if (Collection.class.isAssignableFrom(field.getType())) {
    			ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
    			Class parameterizedClass = (Class) parameterizedType.getActualTypeArguments()[0];
    			if (Aggregate.class.isAssignableFrom(parameterizedClass) || Map.class.isAssignableFrom(parameterizedClass)) {
    				continue;
    			}
    		}
    
    		// 对比字段差异
    		Object aggregateValue = field.get(aggregate);
    		Object snapshotValue = field.get(snapshot);
    		if (snapshotValue == null && aggregateValue == null) {
    			continue;
    		} else if (snapshotValue == null) {
    			unchanged = false;
    			continue;
    		}
    		unchanged = snapshotValue.equals(aggregateValue) & unchanged;
    	}
      return unchanged ? DifferenceType.UNTOUCHED : DifferenceType.MODIFIED;
    }
    
    private static boolean shouldSkipClass(Class clazz) {
    	return Identifier.class.isAssignableFrom(clazz) || Aggregate.class.isAssignableFrom(clazz) || Map.class.isAssignableFrom(clazz);
    }
    

    因为该方法需要在其它位置复用,所以开始时先调用了DifferentUtils#aggregateDifferentType处理null的状态;接着是跳过需要特殊处理的类型,这些类型要么是单独处理,要么是不需要处理,以及当字段的类型为Collection时,某些泛型类型也不需要处理;最后是通过Object#equals方法进行对比,并返回相应的修改状态。

    DifferentUtils#setDifferences的实现,该方法遍历Aggregate的字段,并对比每个字段的差异:

    private static  void setDifferences(T snapshot, T aggregate, Field[] fields, Map fieldDifferences) throws IllegalAccessException {
      for (Field field : fields) {
    	  if (Identifier.class.isAssignableFrom(field.getType())) {
    			continue;
    		}
    	
    		String filedName = ReflectionUtils.getFieldName(field);
    		field.setAccessible(true);
    
    		Object snapshotValue = snapshot == null ? null : field.get(snapshot);
    		Object aggregateValue = aggregate == null ? null : field.get(aggregate);
    		if (snapshotValue == null && aggregateValue == null) {
    			continue;
    		}
    	  // 对比每个字段的差异
    		FieldDifference fieldDifference = compareFiled(field, snapshotValue, aggregateValue);
    		fieldDifferences.put(filedName, fieldDifference);
    	}
    }
    

    DifferentUtils#compareFiled的实现,该方法将字段进行分类对比:

    @SuppressWarnings("unchecked")
    private static  FieldDifference compareFiled(Field field, Object snapshotValue, Object aggregateValue) throws IllegalAccessException {
      ComparableType comparableType = ComparableType.comparableType(aggregateValue == null ? snapshotValue : aggregateValue);
      if (ComparableType.AGGREGATE_TYPE.equals(comparableType)) {
    	  return compareAggregateType(field, (T) snapshotValue, (T) aggregateValue);
      } else if (ComparableType.COLLECTION_TYPE.equals(comparableType)) {
    	  return compareCollectionType(field, snapshotValue, aggregateValue);
      } else if (ComparableType.JAVA_TYPE.equals(comparableType)) {
    	  return compareJavaType(field, snapshotValue, aggregateValue);
      } else {
    	  throw new UnsupportedOperationException();
      }
    }
    
    /**
     * 可比较的字段类型
     */
    enum ComparableType {
    	AGGREGATE_TYPE(),
    	COLLECTION_TYPE(),
    	JAVA_TYPE(),
    	OTHER_TYPE();
    	
    	public static ComparableType comparableType(@NonNull Object obj) {
    		if (obj instanceof Aggregate) {
    			return AGGREGATE_TYPE;
    		} else if (obj instanceof Collection) {
    			return COLLECTION_TYPE;
    		} else if (obj instanceof Map) {
    			return OTHER_TYPE;
    		} else {
    			return JAVA_TYPE;
    		}
    	}
    }
    

    DifferentUtils#compareJavaType的实现,该方法对比了Java类型字段的差异:

    private static FieldDifference compareJavaType(Field field, Object snapshotValue, Object aggregateValue) {
    	String filedName = ReflectionUtils.getFieldName(field);
    	Type type = field.getGenericType();
    	DifferenceType differenceType = javaDifferentType(snapshotValue, aggregateValue);
    	return new JavaTypeFieldDifference(filedName, type, snapshotValue, aggregateValue, differenceType);
    }
    
    public static DifferenceType javaDifferentType(Object snapshot, Object aggregate) {
    	DifferenceType differenceType = basicDifferentType(snapshot, aggregate);
    	if (differenceType != null) {
    		return differenceType;
    	}
    
    	if (snapshot.equals(aggregate)) {
    		return DifferenceType.UNTOUCHED;
    	} else {
    		return DifferenceType.MODIFIED;
    	}
    }
    

    DifferentUtils#compareAggregateType的实现,该方法对比实现Aggregate接口的类型的字段进行对比,通过递归不断向下深入直到类型为Java类型:

    private static  FieldDifference compareAggregateType(Field field, T snapshotValue, T aggregateValue) throws IllegalAccessException {
      String filedName = ReflectionUtils.getFieldName(field);
      Type type = field.getGenericType();
    
      Aggregate notNullValue = snapshotValue == null ? aggregateValue : snapshotValue;
      Field[] entityFields = ReflectionUtils.getFields(notNullValue);
      Identifier id = notNullValue.getId();
    
      DifferenceType differenceType = aggregateDifferentType(entityFields, snapshotValue, aggregateValue);
      AggregareFieldDifference aggregareFieldDifference = new AggregareFieldDifference(filedName, type, snapshotValue, aggregateValue, differenceType, id);
      Map fieldDifferences = aggregareFieldDifference.getFieldDifferences();
      setDifferences(snapshotValue, aggregateValue, entityFields, fieldDifferences);
      return aggregareFieldDifference;
    }
    

    DifferentUtils#compareCollectionType的实现,该方法用于对比集合类型的

    @SuppressWarnings("unchecked")
    private static  FieldDifference compareCollectionType(Field field, Object snapshotValue, Object aggregateValue) throws IllegalAccessException {
      String filedName = ReflectionUtils.getFieldName(field);
      Type type = field.getGenericType();
    
      ParameterizedType parameterizedType = (ParameterizedType) type;
      Class genericityClass = (Class) parameterizedType.getActualTypeArguments()[0];
    
      // 处理泛型为Java类型的集合
      if (!Aggregate.class.isAssignableFrom(genericityClass) && !Map.class.isAssignableFrom(genericityClass)) {
    	  Collection snapshotValues = (Collection) snapshotValue;
    	  Collection aggregateValues = (Collection) aggregateValue;
    	  DifferenceType differenceType = collectionDifferentType(genericityClass, snapshotValues, aggregateValues);
    	  return new CollectionFieldDifference(filedName, type, snapshotValue, aggregateValue, differenceType);
      }
    
      // 处理泛型为实现Aggreagte接口的类型的集合
      Collection snapshotValues = (Collection) snapshotValue;
      Collection aggregateValues = (Collection) aggregateValue;
    
      Map snapshotMap = snapshotValues.stream().collect(Collectors.toMap(snapshot -> snapshot.getId().value(), snapshot -> snapshot));
      Map aggregateMap = aggregateValues.stream().collect(Collectors.toMap(aggregate -> aggregate.getId().value(), aggregate -> aggregate));
    
      CollectionFieldDifference collectionFieldDifference = new CollectionFieldDifference(filedName, type, snapshotValue, aggregateValue);
    
      boolean unchanged = true;
      // snapshotMap与aggregateMap的交集,snapshotMap对aggregateMap的补集
      for (Serializable key : snapshotMap.keySet()) {
    	  T snapshotElement = snapshotMap.get(key);
    	  T aggregateElement = aggregateMap.get(key);
    	  FieldDifference fieldDifferent = compareFiled(field, snapshotElement, aggregateElement);
    	  unchanged = DifferenceType.UNTOUCHED.equals(fieldDifferent.getDifferenceType()) & unchanged;
    	  collectionFieldDifference.getElementDifference().add(fieldDifferent);
      }
      // aggregateMap对snapshotMap的补集
      for (Serializable key : aggregateMap.keySet()) {
    	  if (snapshotMap.get(key) != null) {
    		  continue;
    	  }
    	  T aggregateElement = aggregateMap.get(key);
    	  FieldDifference fieldDifferent = compareFiled(field, null, aggregateElement);
    	  unchanged = DifferenceType.UNTOUCHED.equals(fieldDifferent.getDifferenceType()) & unchanged;
    	  collectionFieldDifference.getElementDifference().add(fieldDifferent);
      }
      if (unchanged) {
    	  collectionFieldDifference.setDifferenceType(DifferenceType.UNTOUCHED);
      } else {
    	  collectionFieldDifference.setDifferenceType(DifferenceType.MODIFIED);
      }
      return collectionFieldDifference;
    }
    
    public static DifferenceType collectionDifferentType(Class typeArguments, Collection snapshot, Collection aggregate) {
      if (CollectionUtils.isEmpty(snapshot) && CollectionUtils.isEmpty(aggregate)) {
    		return DifferenceType.UNTOUCHED;
    	}
    	if (CollectionUtils.isEmpty(snapshot)) {
    		return DifferenceType.ADDED;
    	}
    	if (CollectionUtils.isEmpty(aggregate)) {
    		return DifferenceType.REMOVED;
    	}
    	if (specialHandingClass(typeArguments)) {
    		return snapshot.size() == aggregate.size() ? DifferenceType.UNTOUCHED : DifferenceType.MODIFIED;
    	}
    	return snapshot.equals(aggregate) ? DifferenceType.UNTOUCHED : DifferenceType.MODIFIED;
    }
    
    private static boolean specialHandingClass(Class clazz) {
    	return shouldSkipClass(clazz) || Collection.class.isAssignableFrom(clazz);
    }
    

    我们将Collection类型的字段分为两类,泛型为Java类型的和泛型为实现Aggregate接口的。当集合的泛型为Java类型时,只需要使用Object#equals方法进行对比即可;当集合的泛型为Collection或Aggregate时(集合的泛型不应该出现Map或Identifier),先对数量进行对比,标记整体的变化,接着来对比每个Aggregate的差异,并进行标记。

    我的想法是,先将List转换为Map,Map的key存储Id,value存储对象本身,这样可以得到两个Map:

    • Map snapshotMap
    • Map aggregateMap

    先遍历snapshotMap,取出aggregateMap中Id与之对应的对象进行比较,并一一标记,这里处理的是snapshotMap与aggregateMap的交集,以及snapshotMap对aggregateMap的补集(即snapshotMap中有而aggregateMap中无的),实际上,我们这里处理的是snapshotMap的全集;再遍历aggregateMap,跳过snapshotMap中Id与之对应的对象,这里我们处理的是aggregateMap对snapshotMap的补集(即aggregateMap中有而snapshotMap中无的);这样,我们就处理完了两个集合中的元素,最后再根据每个元素对比的结果标记集合的差异类型即可。

    好了,以上就是具有我司特色的DifferentUtils工具类的实现,因为没有研究过Java Object Diff的源码,因此不太清楚自己与大佬的差距究竟有多远,欢迎大家提出自己的想法一起讨论。

    Tips:鉴于保密的原因,DifferentUtils及相关类都经过不同程度的修改,且修改后的实现并没有经过严格的评审和测试,可能会出现各种各样的BUG~~

    ReflectionUtils的实现

    变更追踪的实现中还有一个反射相关的工具类ReflectionUtils,该工具类的实现可大可小,往小了可以像我下面实现的这样:

    public class ReflectionUtils {
    
    	public static Field[] getFields(Object obj) {
    		return obj.getClass().getDeclaredFields();
    	}
    
    	public static String getFieldName(Field field) {
    		return field.getName();
    	}
    }
    

    往大了可以加入缓存等优化措施,例如ReflectionUtils#getFields加入缓存Map

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论