利用 ByteBuddy 自动生成 AttributeConverter
in with 6 comment

利用 ByteBuddy 自动生成 AttributeConverter

in with 6 comment

利用 ByteBuddy 自动生成 AttributeConverter

在 JPA 的实际应用中,对于实体(Entity)的枚举字段需添加对应的转换器,保证数据库字段值不受枚举的名称及顺序影响。

原始做法

首先,我们提供一个样例枚举,如下所示:

public enum DataType {

    STRING(10),

    LONG(20),

    DOUBLE(30),

    BOOL(40);

    private final Integer value;

    DataType(Integer value) {
        this.value = value;
    }

    public Integer getValue() {
        return value;
    }
}

通常,我们会手动实现 javax.persistence.AttributeConverter,如下所示:

@Converter(autoApply = true)
public class DataTypeConverter implements AttributeConverter<DataType, Integer> {

    @Override
    public Integer convertToDatabaseColumn(DataType dataType) {
        return dataType == null ? null : dataType.getValue();
    }

    @Override
    public DataType convertToEntityAttribute(Integer dbData) {
        if (dbData == null) {
            return null;
        }
        return Arrays.stream(DataType.values())
            .filter(dataType -> dataType.value.equals(dbData))
            .findFirst()
            .orElseThrow();
    }
}

这样在应用启动过程中,Hibernate 会自动扫描所有的实现了 javax.persistence.AttributeConverter 接口并且添加了 javax.persistence.Converter 注解的类,对于设置了 autoApply=true 的转换器会自动应用,否则需要手动在实体的枚举类型的字段上需要加上 @Convert(converter = DateTypeConverter.class) 注解。

至此,一个枚举 AttrbiuteConverter 就应用成功了。

新方法:利用 ByteBuddy 自动生成枚举 AttributeConverter

若实体中枚举数量比较少,原始方法可行。但是考虑到未来业务的扩展,可能会忽略掉添加转换器,所以通过编程方式自动注入 AttrbiuteConverter 就很有必要了。

但是如何实现自动创建 AttributeConverter 和自动注入 AttributeConverter 呢?

自动创建 AttributeConverter

ByteBuddy 是一个代码生成和操作库,旨在运行时创建和修改 Java 应用中的类,并且不需要编译器的帮助。并且有丰富的 API 供我们使用,用它来自动创建 AttributeConverter 最好不过了。

首先,建议提供一个接口,用于约束枚举在数据库中的值,如下所示:

public interface ValueEnum<T extends Serializable> {

  /**
   * Get value.
   *
   * @return value
   */
  T getValue();

  /**
   * 数据库字段至枚举的转换工具。
   */
  static <V extends Serializable, E extends Enum<E> & ValueEnum<V>> E
    valueToEnum(Class<E> enumType, V value) {
    return Stream.of(enumType.getEnumConstants())
        .filter(item -> Objects.equals(item.getValue(), value))
        .findFirst()
        .orElseThrow(() -> new IllegalArgumentException("Unknown enum value: " + value
            + " for type: " + enumType.getSimpleName()));
  }
}

重构 DataType,使其实现 ValueEnum 接口,如下所示:

public enum DataType implements ValueEnum {

    STRING(10),

    LONG(20),

    DOUBLE(30),

    BOOL(40);

    private final Integer value;

    DataType(Integer value) {
        this.value = value;
    }

    @Override
    public Integer getValue() {
        return value;
    }
}

定义接管方法,主要用于 ByteBuddy 自动实现接口使用,如下所示:

public class AttributeConverterInterceptor {

    private AttributeConverterInterceptor() {
    }

  @RuntimeType
  public static <T extends Enum<T> & ValueEnum<V>,
      V extends Serializable> V convertToDatabaseColumn(T attribute) {
    return attribute == null ? null : attribute.getValue();
  }

  @RuntimeType
  public static <T extends Enum<T> & ValueEnum<V>,
      V extends Serializable> T convertToEntityAttribute(V dbData,
      @FieldValue("enumType") Class<T> enumType) {
    return dbData == null ? null : ValueEnum.valueToEnum(enumType, dbData);
  }
}

注意:其中的两个方法必须有 static 修饰,否则 ByteBuddy 无法找到匹配的方法。实际上这里完全可以写一个抽象类来解决此问题。

我们需要利用上面定义的接管方法来定义 AttributeConverter 自动生成器,如下所示:

public class AttributeConverterAutoGenerator {

  /**
   * Auto generation suffix.
   */
  public static final String AUTO_GENERATION_SUFFIX = "$AttributeConverterGeneratedByByteBuddy";

  private final ClassLoader classLoader;

  public AttributeConverterAutoGenerator(ClassLoader classLoader) {
    this.classLoader = classLoader;
  }

  public <T> Class<?> generate(Class<T> clazz) {
    try {
      return new ByteBuddy()
          .with(new NamingStrategy.AbstractBase() {
            @Override
            protected String name(TypeDescription superClass) {
              return clazz.getName() + AUTO_GENERATION_SUFFIX;
            }
          })
          .subclass(
              parameterizedType(AttributeConverter.class, clazz, Integer.class).build())
          .annotateType(ofType(Converter.class).define("autoApply", true).build())
          .constructor(isDefaultConstructor())
          .intercept(MethodCall.invoke(Object.class.getDeclaredConstructor())
              .andThen(ofField("enumType").setsValue(clazz)))
          .defineField("enumType", Class.class, Modifier.PRIVATE | Modifier.FINAL)
          .method(named("convertToDatabaseColumn"))
          .intercept(to(AttributeConverterInterceptor.class))
          .method(named("convertToEntityAttribute"))
          .intercept(to(AttributeConverterInterceptor.class))
          .make()
          .load(this.classLoader, ClassLoadingStrategy.Default.INJECTION.allowExistingTypes())
          .getLoaded();
    } catch (NoSuchMethodException e) {
      // should never happen
      throw new RuntimeException("Failed to get declared constructor.", e);
    }
  }

  public static boolean isGeneratedByByteBuddy(String className) {
    return StringUtils.endsWith(className, AUTO_GENERATION_SUFFIX);
  }

}

对于自动生成出来的 AttributeConverter 可自行写测试类验证是否符合预期。

自动注入 AttributeConverter

上面我们完成了自动生成 AttributeConverter,但问题转换为如何让 Hibernate 知道自动生成的 AttributeConverter 并应用呢?

有兴趣的读者可以去研究一下 javax.persistence.spi.PersistenceUnitInfo#getManagedClassNames,该方法维护了在 classpath 下找到的 AttributeConverterEntity 等类名。

根据上面的提示,我们的目的就是想办法把自动生成的类塞到 managedClassNames 中。经过一番调试后发现,我们只需要新增一个 org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor 处理器就可以操作 PersistenceUnitInfo 了。

Hibernate 版本不低于:5.4.29
Spring Boot 版本不低于:2.5.0-M3
这里得益于两个代码提交
1). https://github.com/hibernate/hibernate-orm/commit/f03dd44107c7c0cf287022d6b02c06690715570f
2). https://github.com/spring-projects/spring-boot/pull/25443/files
第 1)个主要解决:加载 ByteBuddy 生成的类的问题,若采用默认的扫描 classpath 方式判断类名是否存在,这对 ByteBuddy 生成的类是无效的,因为 ByteBuddy 是运行时生成的类,无法在 classpath 中找到;
第 2)个主要解决:更加方便的设置 PersistenceUnitPostProcessor,否则需要非常复杂繁琐且具有破坏性的操作才能够实现。

我们需要进行的步骤:

这里我们用到了 Spring 提供的工具 org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#findCandidateComponents,指定 basePackage 可查找到想要的类信息,同时可提前设置查找过滤。

PersistenceUnitPostProcessor 定义如下所示:

public class AutoGenerateConverterPersistenceUnitPostProcessor
    implements PersistenceUnitPostProcessor {

  private static final String PACKAGE_TO_SCAN = "your.package.name";

  @Override
  public void postProcessPersistenceUnitInfo(MutablePersistenceUnitInfo pui) {
    AttributeConverterAutoGenerator generator = new AttributeConverterAutoGenerator(ClassUtils.getDefaultClassLoader());

    findValueEnumClasses()
        .stream()
        .map(generator::generate)
        .map(Class::getName)
        .forEach(pui::addManagedClassName);
  }

  private Set<Class<?>> findValueEnumClasses() {
    ClassPathScanningCandidateComponentProvider scanner =
        new ClassPathScanningCandidateComponentProvider(false);
    scanner.addIncludeFilter(new AssignableTypeFilter(ValueEnum.class));
    return scanner.findCandidateComponents(PACKAGE_TO_SCAN)
        .stream()
        .filter(bd -> bd.getBeanClassName() != null)
        .map(bd -> ClassUtils.resolveClassName(bd.getBeanClassName(), null))
        .collect(Collectors.toUnmodifiableSet());
  }
}

最后我们需要在配置类中定义 EntityManagerFactoryBuilderCustomizer,以此将此前定义的 PersistenceUnitPostProcessor 设置到系统中,如下所示:

@Configuration(proxyBeanMethods = false)
public class JpaAutoConfiguration {

  @Bean
  EntityManagerFactoryBuilderCustomizer entityManagerFactoryBuilderCustomizer(
      ConfigurableListableBeanFactory factory) {
    return builder -> builder.setPersistenceUnitPostProcessors(
        new AutoGenerateConverterPersistenceUnitPostProcessor(factory));
  }
}

至此,自动生成 AttributeConverter 就完成了,可以放心摒弃掉手动添加的 AttributeConverter

最后

以上方法看似简单,实际则需要经过大量的调试,并修改 Spring Boot 和 Hibernate 源码才能够方便地实现。

有兴趣的读者可以关注 org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBeanorg.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProviderorg.hibernate.boot.model.process.internal.ScanningCoordinator#applyScanResultsToManagedResources