转载

用JaVers比较对象

JaVers 是一个轻量级的对象比较/审计框架,非常易于使用。当前的JaVers版本3是用Java 8编写的,只能运行在JRE 8或以上版本。2.9.2是最后一个和Java 7兼容的版本。源代码 在此 。

简介

如果你需要实时比较生产环境的处理结果和备份环境的处理结果,或是在新系统中重放生产环境的请求,或者像代码一样对对象进行版本管理,那么JaVers就可以成为你的好朋友。它不仅可以比较对象,也可以将比较结果存储在数据库中以便审计。审计是这样的一种需求:

作为用户,我希望知道谁改变了状态,

是什么时候改变的,以及原先的状态是什么。

本文仅关注于比较部分,对审计部分就不具体展开了。

快速一览

新建Maven工程,往pom.xml中增加dependency,最后的pom.xml看起来就像这样:

<?xml version="1.0" encoding="UTF-8"?>
<projectxmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.ggg.javers</groupId>
  <artifactId>helloJaVers</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>helloJaVers</name>

  <dependencies>
    <dependency>
      <groupId>org.javers</groupId>
      <artifactId>javers-core</artifactId>
      <version>3.9.6</version>
    </dependency>
    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>25.0-jre</version>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.16.20</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>

</project>

假设我们有一个名为 Staff 的POJO如下:

@ToString
@Builder
@Data
public class Staff{
    private String name;
    private int age;
    private Double height;
    private BigDecimal salary;
    private Staff manager;
    private List<String> hobbies;
    private Map<String, String> phones;
}

main 函数中如下编写:

Staff tommy = Staff.builder()
                   .name("Tommy")
                   .age(18)
                   .height(180d)
                   .salary(new BigDecimal("10000"))
                   .hobbies(Lists.newArrayList("film", "game"))
                   .phones(ImmutableMap.of("home", "1234", "office", "4321"))
                   .manager(Staff.builder().name("ok").build())
                   .build();
Staff ggg = Staff.builder()
                 .name("ggg")
                 .age(17)
                 .height(180.000000001d)
                 .hobbies(Lists.newArrayList("game", "music", "travel"))
                 .phones(ImmutableMap.of("mobile", "4321", "home", "1235"))
                 .manager(Staff.builder().name("ok").build())
                 .build();

Javers javers = JaversBuilder.javers().build();
Diff diff = javers.compare(tommy, ggg);
System.out.println(diff);

即可得到以下输出结果:

Diff:
* changes on org.ggg.javers.Staff/ :
  - 'age' changed from '18' to '17'
  - 'height' changed from '180.0' to '180.000000001'
  - 'hobbies' collection changes :
    0. 'film' changed to 'game'
    1. 'game' changed to 'music'
    2. 'travel' added
  - 'name' changed from 'Tommy' to 'ggg'
  - 'phones' map changes :
    'home' -> '1234' changed to '1235'
    'mobile' -> '4321' added
    'office' -> '4321' removed
  - 'salary' changed from '10000' to ''

大部分的代码都是我们创建对象所用的,可见JaVers非常易于使用。

对象类型

根据DDD,JaVers把要比较的对象分为三种类型:实体(Entity)、值对象(Value Object)和值(Value)。每种类型都有不同的比较方法。值最简单,就是看它们是否是 Object.equals() 的。实体和值对象都是按属性依次比较。它们俩的区别是实体拥有标识(Entity Id),而值对象并没有。标识不同的实体就会被认为是不同的对象,从而不再继续比较其余的字段。从DDD的角度上严格来说值对象不能单独存在,需要依附于实体。好在JaVers并不教条,值对象也可以用来单独比较。上面的代码其实就是把Staff对象当作值对象来比较。如果我们在 Staff 类中,给 name 添加一个 @Id 的注解(所有的注解都在 org.javers.core.metamodel.annotation 包里),那么比较结果就会不同:

Diff:
* new object: org.ggg.javers.Staff/ggg
* object removed: org.ggg.javers.Staff/Tommy

只有当 name 属性相同的时候,这两个对象才会被当成同一实体,从而依次比较其余属性。如果没有权限修改实体以增加 @Id 注解,也可以用这样的方法来注册,效果相同:

Javers javers = JaversBuilder.javers()
                             .registerEntity(new EntityDefinition(Staff.class, "name"))
                             .build();

如果 registerEntity 的属性和 @Id 注解都存在,那么以 registerEntity 所注册的属性为准。

JaVers完全兼容 Groovy ,可以参考其 文档 来了解用例。

自定义比较方式

忽略属性

从业务上说,有些属性新、旧系统本来就不一样,或者是不关心,这时候我们可以在比较中“忽略”这些属性。如果有权限修改要比较的对象类,可以简单地在属性上面增加一个 @DiffIgnores ,比较的时候就会将其忽略。 @DiffIgnores 相当于黑名单, @DiffInclude 起到了白名单的效果。如果没有修改类的权限,那么也可以用这样的方法来注册,效果相同:

Javers javers = JaversBuilder.javers()
                             .registerValueObject(new ValueObjectDefinition(Staff.class, Lists.newArrayList("hobbies", "phones")))
                             .build();

这里我们注册的是个值对象ValueObject,与上一个例子的实体Entity的区别就是有没有标识属性。

比较算法

一开始细心的读者们就可能注意到了, Lists.newArrayList("film", "game")Lists.newArrayList("game", "music", "travel") 的比较结果居然是:

- 'hobbies' collection changes :
  0. 'film' changed to 'game'
  1. 'game' changed to 'music'
  2. 'travel' added

这当然也是可以配置的:

Javers javers = JaversBuilder.javers()
                             .withListCompareAlgorithm(ListCompareAlgorithm.LEVENSHTEIN_DISTANCE)
                             .build();

这样的话,结果就变成了:

- 'hobbies' collection changes :
  2. 'travel' added
  1. 'music' added
  0. 'film' removed

值得注意的是,这种算法在列表元素过多的时候可能会影响性能。

定制比较

我们可以注册自定义的比较器,例如,如果在业务上认为两个 Double 类型的 1.0000000011 相等,这时候我们可以注册一个如下的类:

public class CustomDoubleIgnorePrecisionComparatorimplements CustomPropertyComparator<Double,ValueChange>{

    private static final double DELTA = 0.00001;

    public ValueChange compare(final Double left, final Double right, final GlobalId affectedId, final Property property){
        final double diff = Math.abs(left - right);
        if (diff <= DELTA) return null;

        return new ValueChange(affectedId, property.getName(), left, right);
    }

}

GlobalId 是当前比较对象的标识,如值对象的 org.ggg.javers.Staff/ 或是以name为标识的实体的 org.ggg.javers.Staff/gggProperty 是当前比较的属性。可以通过这两个值来设置比较属性的黑名单或是白名单。然后注册进JaVers就好了:

Javers javers = JaversBuilder.javers()
                             .registerCustomComparator(new CustomDoubleIgnorePrecisionComparator(), Double.class)
                             .build();

需要注意的是, Doubledouble 是不同的,如果 Staff 中的 heightdouble 类型,那么需要在调用 registerCustomComparator 时传入 double.class 。自定义的比较在许多场合都比较有用,比如String类型的不同格式的日期等。

关联字段比较

关联字段就是说,如果几个字段之间有关联,我们就认为它们一样。比如说我们虚构一个需求:对于一个拥有 int xint yRectangle 类来说,如果 x * y 也就是面积相等,我们就认为它们相等。在这种情况下,JaVers似乎并没有原生提供关联字段比较的办法。有一种办法是新建一个包装类,比如说 RectangleWrapper ,里面有一个 Rectangle rectangle 和一个 int area 字段,分别赋值为要比较的对象和其 x * y 。注册 Javers 的时候,把 Rectangle 类的 xy 忽略即可。如果有更复杂的需求,例如当面积不同时需要报 xy 不同而不是 area 不同,那也可以通过生成多个 Javers 对象,并多次调用来解决。 Javers#compare 方法返回的是一个 Diff 对象,从中可以很方便地查看某些字段是否存在变化。就是要多写点代码咯。

原文  http://qinghua.github.io/javers/
正文到此结束
Loading...