Automate attribute converter (#1325)

* Deprecate AbstractConverter

* Remove unused enum and attribute converter

* Add AttributeConverterApplyTest

* Add JpaConfiguration

* Add AttributeConverterAutoGenerator

* Integrate automate-attribute-converter

* Rename JpaConfiguration

* Remove useless attribute converters

* Exclude property enums for auto-generating

* Refine JournalType definition

* Fix an error about existing injected type
pull/1331/head
John Niang 2021-03-27 23:05:27 +08:00 committed by GitHub
parent e6b32ac8c2
commit 8472a7b3d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 307 additions and 184 deletions

View File

@ -10,6 +10,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@ -19,6 +20,7 @@ import org.springframework.web.client.RestTemplate;
import run.halo.app.cache.AbstractStringCacheStore;
import run.halo.app.cache.InMemoryCacheStore;
import run.halo.app.cache.LevelCacheStore;
import run.halo.app.config.attributeconverter.AttributeConverterAutoGenerateConfiguration;
import run.halo.app.config.properties.HaloProperties;
import run.halo.app.repository.base.BaseRepositoryImpl;
import run.halo.app.utils.HttpClientUtils;
@ -35,6 +37,7 @@ import run.halo.app.utils.HttpClientUtils;
@EnableConfigurationProperties(HaloProperties.class)
@EnableJpaRepositories(basePackages = "run.halo.app.repository", repositoryBaseClass =
BaseRepositoryImpl.class)
@Import(AttributeConverterAutoGenerateConfiguration.class)
public class HaloConfiguration {
private final HaloProperties haloProperties;

View File

@ -0,0 +1,20 @@
package run.halo.app.config.attributeconverter;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilderCustomizer;
import org.springframework.context.annotation.Bean;
/**
* Jpa configuration.
*
* @author johnniang
*/
public class AttributeConverterAutoGenerateConfiguration {
@Bean
EntityManagerFactoryBuilderCustomizer entityManagerFactoryBuilderCustomizer(
ConfigurableListableBeanFactory factory) {
return builder -> builder.setPersistenceUnitPostProcessors(
new AutoGenerateConverterPersistenceUnitPostProcessor(factory));
}
}

View File

@ -0,0 +1,71 @@
package run.halo.app.config.attributeconverter;
import static net.bytebuddy.description.annotation.AnnotationDescription.Builder.ofType;
import static net.bytebuddy.description.type.TypeDescription.Generic.Builder.parameterizedType;
import static net.bytebuddy.implementation.FieldAccessor.ofField;
import static net.bytebuddy.implementation.MethodDelegation.to;
import static net.bytebuddy.matcher.ElementMatchers.isDefaultConstructor;
import static net.bytebuddy.matcher.ElementMatchers.named;
import java.lang.reflect.Modifier;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.NamingStrategy;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.MethodCall;
import org.apache.commons.lang3.StringUtils;
/**
* Attribute converter auto generator.
*
* @author johnniang
*/
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);
}
}

View File

@ -0,0 +1,27 @@
package run.halo.app.config.attributeconverter;
import net.bytebuddy.implementation.bind.annotation.FieldValue;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import run.halo.app.model.enums.ValueEnum;
/**
* Attribute Converter Interceptor.
*
* @author johnniang
*/
public class AttributeConverterInterceptor {
private AttributeConverterInterceptor() {
}
@RuntimeType
public static <T extends Enum<T> & ValueEnum<V>, V> V convertToDatabaseColumn(T attribute) {
return attribute == null ? null : attribute.getValue();
}
@RuntimeType
public static <T extends Enum<T> & ValueEnum<V>, V> T convertToEntityAttribute(V dbData,
@FieldValue("enumType") Class<T> enumType) {
return dbData == null ? null : ValueEnum.valueToEnum(enumType, dbData);
}
}

View File

@ -0,0 +1,55 @@
package run.halo.app.config.attributeconverter;
import static java.util.stream.Collectors.toUnmodifiableSet;
import java.util.Set;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo;
import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor;
import org.springframework.util.ClassUtils;
import run.halo.app.model.enums.ValueEnum;
import run.halo.app.model.properties.PropertyEnum;
/**
* Attribute converter persistence unit post processor.
*
* @author johnniang
*/
class AutoGenerateConverterPersistenceUnitPostProcessor implements PersistenceUnitPostProcessor {
private static final String PACKAGE_TO_SCAN = "run.halo.app";
private final ConfigurableListableBeanFactory factory;
public AutoGenerateConverterPersistenceUnitPostProcessor(
ConfigurableListableBeanFactory factory) {
this.factory = factory;
}
@Override
public void postProcessPersistenceUnitInfo(MutablePersistenceUnitInfo pui) {
var generator = new AttributeConverterAutoGenerator(factory.getBeanClassLoader());
findValueEnumClasses()
.stream()
.map(generator::generate)
.map(Class::getName)
.forEach(pui::addManagedClassName);
}
private Set<Class<?>> findValueEnumClasses() {
var scanner = new ClassPathScanningCandidateComponentProvider(false);
// include ValueEnum class
scanner.addIncludeFilter(new AssignableTypeFilter(ValueEnum.class));
// exclude PropertyEnum class
scanner.addExcludeFilter(new AssignableTypeFilter(PropertyEnum.class));
return scanner.findCandidateComponents(PACKAGE_TO_SCAN)
.stream()
.filter(bd -> bd.getBeanClassName() != null)
.map(bd -> ClassUtils.resolveClassName(bd.getBeanClassName(), null))
.collect(toUnmodifiableSet());
}
}

View File

@ -47,7 +47,7 @@ public class Journal extends BaseEntity {
private Long likes;
@Column(name = "type")
@ColumnDefault("1")
@ColumnDefault("0")
private JournalType type;
@Override

View File

@ -10,12 +10,12 @@ public enum JournalType implements ValueEnum<Integer> {
/**
* Public type.
*/
PUBLIC(1),
PUBLIC(0),
/**
* Intimate type.
*/
INTIMATE(0);
INTIMATE(1);
private final int value;

View File

@ -1,36 +0,0 @@
package run.halo.app.model.enums;
/**
* Post type.
*
* @author johnniang
*/
@Deprecated
public enum PostType implements ValueEnum<Integer> {
/**
*
*/
POST(0),
/**
*
*/
PAGE(1),
/**
*
*/
JOURNAL(2);
private final Integer value;
PostType(Integer value) {
this.value = value;
}
@Override
public Integer getValue() {
return value;
}
}

View File

@ -11,6 +11,13 @@ import org.springframework.util.Assert;
*/
public interface ValueEnum<T> {
/**
* Gets enum value.
*
* @return enum value
*/
T getValue();
/**
* Converts value to corresponding enum.
*
@ -20,7 +27,7 @@ public interface ValueEnum<T> {
* @param <E> enum generic
* @return corresponding enum
*/
static <V, E extends ValueEnum<V>> E valueToEnum(Class<E> enumType, V value) {
static <V, E extends Enum<E> & ValueEnum<V>> E valueToEnum(Class<E> enumType, V value) {
Assert.notNull(enumType, "enum type must not be null");
Assert.notNull(value, "value must not be null");
Assert.isTrue(enumType.isEnum(), "type must be an enum type");
@ -30,12 +37,4 @@ public interface ValueEnum<T> {
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("unknown database value: " + value));
}
/**
* Gets enum value.
*
* @return enum value
*/
T getValue();
}

View File

@ -1,40 +0,0 @@
package run.halo.app.model.enums.converter;
import java.lang.reflect.Type;
import java.util.Objects;
import javax.persistence.AttributeConverter;
import run.halo.app.model.enums.ValueEnum;
import run.halo.app.utils.ReflectionUtils;
/**
* Abstract converter.
*
* @param <E> enum generic
* @param <V> value generic
* @author johnniang
* @date 12/6/18
*/
public abstract class AbstractConverter<E extends ValueEnum<V>, V>
implements AttributeConverter<E, V> {
private final Class<E> clazz;
@SuppressWarnings("unchecked")
protected AbstractConverter() {
Type enumType = Objects.requireNonNull(
ReflectionUtils
.getParameterizedTypeBySuperClass(AbstractConverter.class, this.getClass())
).getActualTypeArguments()[0];
this.clazz = (Class<E>) enumType;
}
@Override
public V convertToDatabaseColumn(E attribute) {
return attribute == null ? null : attribute.getValue();
}
@Override
public E convertToEntityAttribute(V dbData) {
return dbData == null ? null : ValueEnum.valueToEnum(clazz, dbData);
}
}

View File

@ -1,15 +0,0 @@
package run.halo.app.model.enums.converter;
import javax.persistence.Converter;
import run.halo.app.model.enums.AttachmentType;
/**
* Attachment type converter
*
* @author johnniang
* @date 3/27/19
*/
@Converter(autoApply = true)
public class AttachmentTypeConverter extends AbstractConverter<AttachmentType, Integer> {
}

View File

@ -1,15 +0,0 @@
package run.halo.app.model.enums.converter;
import javax.persistence.Converter;
import run.halo.app.model.enums.CommentStatus;
/**
* PostComment status converter.
*
* @author johnniang
* @date 3/27/19
*/
@Converter(autoApply = true)
public class CommentStatusConverter extends AbstractConverter<CommentStatus, Integer> {
}

View File

@ -1,15 +0,0 @@
package run.halo.app.model.enums.converter;
import javax.persistence.Converter;
import run.halo.app.model.enums.DataType;
/**
* Data type converter.
*
* @author johnniang
* @date 4/10/19
*/
@Converter(autoApply = true)
public class DataTypeConverter extends AbstractConverter<DataType, Integer> {
}

View File

@ -1,15 +0,0 @@
package run.halo.app.model.enums.converter;
import javax.persistence.Converter;
import run.halo.app.model.enums.LogType;
/**
* Log type converter.
*
* @author johnniang
* @date 3/27/19
*/
@Converter(autoApply = true)
public class LogTypeConverter extends AbstractConverter<LogType, Integer> {
}

View File

@ -1,15 +0,0 @@
package run.halo.app.model.enums.converter;
import javax.persistence.Converter;
import run.halo.app.model.enums.PostStatus;
/**
* PostStatus converter.
*
* @author johnniang
* @date 3/27/19
*/
@Converter(autoApply = true)
public class PostStatusConverter extends AbstractConverter<PostStatus, Integer> {
}

View File

@ -1,16 +0,0 @@
package run.halo.app.model.enums.converter;
import javax.persistence.Converter;
import run.halo.app.model.enums.PostType;
/**
* PostType converter.
*
* @author johnniang
* @date 3/27/19
*/
@Converter(autoApply = true)
@Deprecated
public class PostTypeConverter extends AbstractConverter<PostType, Integer> {
}

View File

@ -281,7 +281,8 @@ public interface OptionService extends CrudService<Option, Integer> {
* @return an optional value enum value
*/
@NonNull
<V, E extends ValueEnum<V>> Optional<E> getValueEnumByProperty(@NonNull PropertyEnum property,
<V, E extends Enum<E> & ValueEnum<V>> Optional<E> getValueEnumByProperty(
@NonNull PropertyEnum property,
@NonNull Class<V> valueType, @NonNull Class<E> enumType);
/**
@ -296,7 +297,8 @@ public interface OptionService extends CrudService<Option, Integer> {
* @return value enum value or null if the default value is null
*/
@Nullable
<V, E extends ValueEnum<V>> E getValueEnumByPropertyOrDefault(@NonNull PropertyEnum property,
<V, E extends Enum<E> & ValueEnum<V>> E getValueEnumByPropertyOrDefault(
@NonNull PropertyEnum property,
@NonNull Class<V> valueType, @NonNull Class<E> enumType, @Nullable E defaultValue);
/**

View File

@ -28,7 +28,6 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import run.halo.app.cache.AbstractStringCacheStore;
import run.halo.app.config.properties.HaloProperties;
import run.halo.app.event.options.OptionUpdatedEvent;
import run.halo.app.exception.MissingPropertyException;
import run.halo.app.model.dto.OptionDTO;
@ -379,14 +378,16 @@ public class OptionServiceImpl extends AbstractCrudService<Option, Integer>
}
@Override
public <V, E extends ValueEnum<V>> Optional<E> getValueEnumByProperty(PropertyEnum property,
public <V, E extends Enum<E> & ValueEnum<V>> Optional<E> getValueEnumByProperty(
PropertyEnum property,
Class<V> valueType, Class<E> enumType) {
return getByProperty(property).map(value -> ValueEnum
.valueToEnum(enumType, PropertyEnum.convertTo(value.toString(), valueType)));
}
@Override
public <V, E extends ValueEnum<V>> E getValueEnumByPropertyOrDefault(PropertyEnum property,
public <V, E extends Enum<E> & ValueEnum<V>> E getValueEnumByPropertyOrDefault(
PropertyEnum property,
Class<V> valueType, Class<E> enumType, E defaultValue) {
return getValueEnumByProperty(property, valueType, enumType).orElse(defaultValue);
}

View File

@ -0,0 +1,46 @@
package run.halo.app.attributeconverter;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;
import run.halo.app.config.attributeconverter.AttributeConverterAutoGenerateConfiguration;
/**
* Attribute converter apply result test.
*
* @author johnniang
*/
@DataJpaTest
@Import(AttributeConverterAutoGenerateConfiguration.class)
class AttributeConverterApplyTest {
@Autowired
EntityManager entityManager;
@Autowired
CityRepository cityRepository;
@Test
void shouldAutoAppliedForAttributeConverter() {
final var city = new City();
city.setName("ChengDu");
city.setLevel(CityLevel.CITY);
cityRepository.save(city);
final var cityId = city.getId();
Query nativeQuery =
entityManager.createNativeQuery("select level from city where id = " + cityId);
Object level = nativeQuery.getSingleResult();
Assertions.assertEquals(CityLevel.CITY.getValue(), level);
}
@SpringBootApplication
static class Application {
}
}

View File

@ -0,0 +1,26 @@
package run.halo.app.attributeconverter;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import lombok.Data;
/**
* City entity.
*
* @author johnniang
*/
@Entity
@Data
class City {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private CityLevel level;
}

View File

@ -0,0 +1,29 @@
package run.halo.app.attributeconverter;
import run.halo.app.model.enums.ValueEnum;
/**
* City level.
*
* @author johnniang
*/
enum CityLevel implements ValueEnum<Integer> {
PROVINCE(123),
CITY(456),
DISTRICT(789);
private final int value;
CityLevel(int value) {
this.value = value;
}
@Override
public Integer getValue() {
return value;
}
}

View File

@ -0,0 +1,11 @@
package run.halo.app.attributeconverter;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* City operation repository.
*
* @author johnniang
*/
interface CityRepository extends JpaRepository<City, Integer> {
}