One stop solution to various pain points of enumeration

If the variable value has only a limited number of optional values, it is a normal operation to define constants with enumeration classes.

However, in the business code, we do not want to rely on the coordination () for business operations, but to customize the number attributes to avoid the impact of increasing or decreasing the enumeration values.

@Getter
@AllArgsConstructor
public enum CourseType {

    PICTURE(102, "Image & Text"),
    AUDIO(103, "audio frequency"),
    VIDEO(104, "video"),
    ;

    private final int index;
    private final String name;
}

However, because of the use of custom digital attributes, the enumeration conversion function of many frameworks is no longer applicable. Therefore, we need to expand the corresponding transformation mechanism by ourselves, including:

  1. Spring MVC enumeration converter
  2. ORM enumeration mapping
  3. JSON serialization and deserialization

Custom spring MVC enumeration converter

Clear needs

Taking the CourseType above as an example, we hope to achieve the following results:

In the controller, we can directly use CourseType to receive the enumerated index value when the front end passes parameters. The framework is responsible for the conversion of index to CourseType.

@GetMapping("/list")
public void list(@RequestParam CourseType courseType) {
    // do something
}

Spring MVC comes with enumeration converter

Spring MVC comes with two enum related Converters:

  • org.springframework.core.convert.support.StringToEnumConverterFactory
  • org.springframework.boot.convert.StringToEnumIgnoringCaseConverterFactory

These two converters are transformed by calling the valueOf method of enumeration. Interested students can consult the source code by themselves.

Implement custom enumeration converter

Although these two converters can not meet our needs, they also bring us ideas. We can realize our needs by imitating these two converters:

  1. Implement the ConverterFactory interface, which requires us to return the Converter, which is a typical factory design pattern
  2. Implement the Converter interface, and complete the conversion from the custom digital attribute to the enumeration class

No more nonsense, source code:

/**
 * springMVC Converter of enumeration class
 * If a factory method (static method) in an enumeration class is marked as {@ link EnumConvertMethod}, the method is called and converted to an enumeration object
 */
@SuppressWarnings("all")
public class EnumMvcConverterFactory implements ConverterFactory<String, Enum<?>> {

    private final ConcurrentMap<Class<? extends Enum<?>>, EnumMvcConverterHolder> holderMapper = new ConcurrentHashMap<>();


    @Override
    public <T extends Enum<?>> Converter<String, T> getConverter(Class<T> targetType) {
        EnumMvcConverterHolder holder = holderMapper.computeIfAbsent(targetType, EnumMvcConverterHolder::createHolder);
        return (Converter<String, T>) holder.converter;
    }


    @AllArgsConstructor
    static class EnumMvcConverterHolder {
        @Nullable
        final EnumMvcConverter<?> converter;

        static EnumMvcConverterHolder createHolder(Class<?> targetType) {
            List<Method> methodList = MethodUtils.getMethodsListWithAnnotation(targetType, EnumConvertMethod.class, false, true);
            if (CollectionUtils.isEmpty(methodList)) {
                return new EnumMvcConverterHolder(null);
            }
            Assert.isTrue(methodList.size() == 1, "@EnumConvertMethod Only one factory method can be marked(Static method)upper");
            Method method = methodList.get(0);
            Assert.isTrue(Modifier.isStatic(method.getModifiers()), "@EnumConvertMethod Only factory methods can be marked(Static method)upper");
            return new EnumMvcConverterHolder(new EnumMvcConverter<>(method));
        }

    }

    static class EnumMvcConverter<T extends Enum<T>> implements Converter<String, T> {

        private final Method method;

        public EnumMvcConverter(Method method) {
            this.method = method;
            this.method.setAccessible(true);
        }

        @Override
        public T convert(String source) {
            if (source.isEmpty()) {
                // reset the enum value to null.
                return null;
            }
            try {
                return (T) method.invoke(null, Integer.valueOf(source));
            } catch (Exception e) {
                throw new IllegalArgumentException(e);
            }
        }

    }


}

  • Enummvcvconverterfactory: factory class, used to create enummvcvconverter

  • EnumMvcConverter: a custom enumeration converter to complete the conversion of custom numeric properties to enumeration classes

  • EnumConvertMethod: a user-defined annotation marked on the factory method of a user-defined enumeration class. It is used by enummvconverter to perform enumeration conversion

The specific source code of EnumConvertMethod is as follows:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnumConvertMethod {
}

How do you use it?

1. Register enummvccconverterfactory

@Configuration
public class MvcConfiguration implements WebMvcConfigurer {

    @Bean
    public EnumMvcConverterFactory enumMvcConverterFactory() {
        return new EnumMvcConverterFactory();
    }

    @Override
    public void addFormatters(FormatterRegistry registry) {
        // org.springframework.core.convert.support.GenericConversionService.ConvertersForPair.add
        // this.converters.addFirst(converter);
        // So what we customized will be put in the front
        registry.addConverterFactory(enumMvcConverterFactory());
    }
}

2. Provide a factory method in custom enumeration to complete the conversion of custom numeric attribute to enumeration class, and add @ EnumConvertMethod annotation on the factory method

@Getter
@AllArgsConstructor
public enum CourseType {

    PICTURE(102, "Image & Text"),
    AUDIO(103, "audio frequency"),
    VIDEO(104, "video"),
    ;

    private final int index;
    private final String name;

    private static final Map<Integer, CourseType> mappings;

    static {
        Map<Integer, CourseType> temp = new HashMap<>();
        for (CourseType courseType : values()) {
            temp.put(courseType.index, courseType);
        }
        mappings = Collections.unmodifiableMap(temp);
    }

    @EnumConvertMethod
    @Nullable
    public static CourseType resolve(int index) {
        return mappings.get(index);
    }
}

Custom ORM enumeration mapping

What's the problem

Take the above CourseType enumeration as an example. Generally, the data of business code should be persisted in DB. Suppose that there is a course metadata table to record the type of the current course. Our entity object may be as follows:

@Getter
@Setter
@Entity
@Table(name = "course_meta")
public class CourseMeta {
    private Integer id;

    /**
     * Course type, {@ link CourseType}
     */
    private Integer type;
}

In the above way, javadoc annotation is used to tell the user that the value type of type is related to CourseType.

However, we want to avoid comments by making the code more explicit.

Therefore, can ORM directly map the type of Integer type to CourseType enumeration when mapping? The answer is feasible.

AttributeConverter

Our current system uses the Spring Data JPA framework, which is a further encapsulation of JPA. Therefore, this article only provides solutions in JPA environment.

In the JPA specification, the javax.persistence.AttributeConverter interface is provided to extend the mapping of object attributes and database field types.

public class CourseTypeEnumConverter implements AttributeConverter<CourseType, Integer> {

    @Override
    public Integer convertToDatabaseColumn(CourseType attribute) {
        return attribute.getIndex();
    }

    @Override
    public CourseType convertToEntityAttribute(Integer dbData) {
        return CourseType.resolve(dbData);
    }
}

How does it work? There are two ways

  1. Register the AttributeConverter into the global JPA container, which needs to be used with javax.persistence.Converter
  2. The second way is to use with javax.persistence.Convert, and specify AttributeConverter where necessary, which will not take effect globally at this time

In this paper, we choose the second method, where you need to specify the AttributeConverter. The specific code is as follows:

@Getter
@Setter
@Entity
@Table(name = "ourse_meta")
public class CourseMeta {
    private Integer id;

    @Convert(converter = CourseTypeEnumConverter.class)
    private CourseType type;
}

JSON serialization

So far, we have solved the support of spring MVC and ORM for custom enumeration. Is that enough? What else is the problem?

The enumeration converter of spring MVC can only support the parameter conversion of GET request. If the front end submits the POST request in JSON format, it is not supported.

In addition, when outputting VO to the front-end, by default, you still need to manually map the enumeration type to Integer type, instead of directly using enumeration output in VO.

@Data
public class CourseMetaShowVO {
    private Integer id;
    private Integer type;

    public static CourseMetaShowVO of(CourseMeta courseMeta) {
        if (courseMeta == null) {
            return null;
        }
        CourseMetaShowVO vo = new CourseMetaShowVO();
        vo.setId(courseMeta.getId());
        // Manual conversion enumeration
        vo.setType(courseMeta.getType().getIndex());
        return vo;
    }
}

@JsonValue and @ JsonCreator

Jackson is a very powerful JSON serialization tool. Spring MVC also uses Jackson as its JSON converter by default.

Jackson has provided us with two comments that just solve the problem.

  • @JsonValue: when serializing, only the value annotated by @ JsonValue annotation will be serialized
  • @JsonCreator: when deserializing, call the constructor or factory method of the @ JsonCreator annotation to create the object

The final code is as follows:

@Getter
@AllArgsConstructor
public enum CourseType {

    PICTURE(102, "Image & Text"),
    AUDIO(103, "audio frequency"),
    VIDEO(104, "video"),
    ;

    @JsonValue
    private final int index;
    private final String name;

    private static final Map<Integer, CourseType> mappings;

    static {
        Map<Integer, CourseType> temp = new HashMap<>();
        for (CourseType courseType : values()) {
            temp.put(courseType.index, courseType);
        }
        mappings = Collections.unmodifiableMap(temp);
    }

    @EnumConvertMethod
    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    @Nullable
    public static CourseType resolve(int index) {
        return mappings.get(index);
    }
}

Extending swagger's support for enumeration

After the above-mentioned custom converters, some of the pain points of using enumeration in the code are basically solved. But do you think that's enough?

Now most of the code is using swagger to write documents. I wonder if you have such pain points:

When writing a document, you need to tell the front-end enumeration types what values they have. After each increase of values, you need to not only change the code, but also find out where the corresponding values are used, and then modify the swagger document.

Anyway, I don't think it's good for Xiaohei to do so. Is there any way to let the swagger framework help us automatically list all enumeration values? Of course, there is!

How to do it? emmm... We'll see next issue~~

Tags: Java Spring JSON Attribute Database

Posted on Tue, 05 May 2020 22:59:49 -0700 by mrhappiness