利用 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
下找到的AttributeConverter
、Entity
等类名。
根据上面的提示,我们的目的就是想办法把自动生成的类塞到 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
,否则需要非常复杂繁琐且具有破坏性的操作才能够实现。
我们需要进行的步骤:
- 扫描
classpath
下所有的 <T extends Enum& ValueEnum > 类型的类 - 为其自动生成对应的
AttributeConverter
- 利用
PersistenceUnitPostProcessor
添加到PersistenceUnitInfo
中 - 添加
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.LocalContainerEntityManagerFactoryBean
、org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider
和 org.hibernate.boot.model.process.internal.ScanningCoordinator#applyScanResultsToManagedResources
。