An elegant way to implement global enumeration processing in Java applications

  • 2021-06-29 10:53:24
  • OfStack

Background description

To express an attribute with an optional set of scopes, we typically use two methods.Enumerated classes and data dictionaries have their own advantages.Enumerated classes are written in Java code, which makes it easy to write appropriate judgment logic. The code is readable and the attributes in enumerated classes can be predicted and determined in advance.Data dictionary, generally stored in the database, is not easy to write judgment and branching logic, because if the data changes, the corresponding code logic is likely to be invalid, strongly dependent on the correctness of the database data, the corresponding attributes in the data dictionary have little impact on the business, commonly used in daily development as classification, labeling, the amount of attributes can not be estimated.

At present, there is basically no good global solution to deal with enumeration classes, so I have written one on my own comprehensive data.

Code

The architecture is still improving, code 1 may run, but the configuration of enumeration has been completed, you can read and refer to: pretty-demo

Preface

When most companies process enumerations, they customize an enumeration conversion tool class or write a static method in the enumeration class to implement the Integer conversion enumeration.

For example:


//  Static method approach 
public enum GenderEnum {

  //  Code omission 
 
  public static GenderEnum get(int value) {
   for (GenderEnum item : GenderEnum.values()) {
   if (value == item.getValue()) {
     return item;
    }
   }
   return null;
  }
}
//  Tool class approach 
public class EnumUtil {

 public static <E extends Enumerable> E of(@Nonnull Class<E> classType, int value) {
  for (E enumConstant : classType.getEnumConstants()) {
   if (value == enumConstant.getValue()) {
    return enumConstant;
   }
  }
  return null;
 }

}

GenderEnum gender = EnumUtil.of(GenderEnum.class,1);

This can be cumbersome, or you need to manually write the corresponding static method, or you need to manually invoke a tool class for conversion.

Solution

For convenience, I have made a global enumeration value conversion scheme, which enables the front-end to pass int to the server, the server to automatically convert to enumeration classes, make corresponding business decisions, and then store in the database in the form of numbers;When we look up the data, we can also convert the number of the database into the java enumeration class. After processing the corresponding business logic, we transfer the display information of the enumeration and enumeration classes to the foreground together. The foreground does not need to maintain the corresponding relationship between the enumeration class and the display information, and the display information supports the internationalization process. The specific scheme is as follows:

1. Based on the principle that the Convention is greater than configuration, the rules for writing enumeration classes of Unity 1 are formulated.The general rules are as follows:

Each enumeration class has two fields: int value (Stored Database), String key (Find corresponding i18n text information via key).This section needs to be discussed in detail. Enumerated values usually have int values in the database and String values in the database. Each has its advantages and disadvantages.The advantage of storing int is its small size. If the enumeration values are regular, such as -1 is deleted, 0 is preprocessed, 1 is processed, and 2 is finished, then all of us can convert to status without using status in (0,1,2). > = 0;If String is stored, the advantage is high readability, the corresponding status can be directly understood from the database values, the disadvantage is the size of the point.Of course, these are relative. When int is saved, we can perfect the notes and make them readable.If int is replaced by String, the one point that occupies a lot of volume can actually be ignored. Enumeration classes need to inherit the System 1 interface and provide the appropriate methods for general processing of enumerations.

Here is an enumeration interface and an enumeration example:


public interface Enumerable<E extends Enumerable> {

 /**
  *  Get in i18n Corresponding in file  key
  * @return key
  */
 @Nonnull
 String getKey();

 /**
  *  Get the value that is ultimately saved to the database 
  * @return  value 
  */
 @Nonnull
 int getValue();

 /**
  *  Obtain  key  Corresponding Text Information 
  * @return  Text information 
  */
 @Nonnull
 default String getText() {
  return I18nMessageUtil.getMessage(this.getKey(), null);
 }
}
public enum GenderEnum implements Enumerable {

 /**  male  */
 MALE(1, "male"),

 /**  female  */
 FEMALE(2, "female");

 private int value;

 private String key;

 GenderEnum(int value, String key) {
  this.value = value;
  this.key = key;
 }

 @Override
 public String getKey() {
  return this.key;
 }

 @Override
 public int getValue() {
  return this.value;
 }
}

What we want to do is that each enumeration class we write needs to be written in this way, and enumeration classes defined by the specification are easy to write in Unit 1 below.

2. We analyze data entry and exit at the controller level to handle the conversion of enumeration classes and int values. In Spring MVC, the framework helps us to convert data types, so we use Spring MVC as our starting point.Foreground requests sent to the server, 1 generally have parameters in url and body, respectively, get requests and post requests with @RequestBody as the representative.

[Entry Reference] The get method is representative and the requested MediaType is "application/x-www-form-urlencoded". When int is converted to an enumeration, we register a new Converter and invoke the converter we set if spring MVC determines that a value is to be converted to an enumerated class object that we define.


@Configuration
public class MvcConfiguration implements WebMvcConfigurer, WebBindingInitializer {

 /**
  * [get] In the request, the int Value to Enum Class 
  * @param registry
  */
 @Override
 public void addFormatters(FormatterRegistry registry) {
  registry.addConverterFactory(new EnumConverterFactory());
 }
}

public class EnumConverterFactory implements ConverterFactory<String, Enumerable> {

 private final Map<Class, Converter> converterCache = new WeakHashMap<>();

 @Override
 @SuppressWarnings({"rawtypes", "unchecked"})
 public <T extends Enumerable> Converter<String, T> getConverter(@Nonnull Class<T> targetType) {
  return converterCache.computeIfAbsent(targetType,
    k -> converterCache.put(k, new EnumConverter(k))
  );
 }

 protected class EnumConverter<T extends Enumerable> implements Converter<Integer, T> {

  private final Class<T> enumType;

  public EnumConverter(@Nonnull Class<T> enumType) {
   this.enumType = enumType;
  }

  @Override
  public T convert(@Nonnull Integer value) {
   return EnumUtil.of(this.enumType, value);
  }
 }
}

On behalf of post, int is converted to an enumeration.We have an agreement with the foreground (applicationType in Ajax) that all data in body must be in json format.Similarly, the MediaType requested for the parameter corresponding to background @RequestBody is "application/json", spring MVC uses Jackson2HttpMessageConverter by default for data in Json format.There are @JsonCreator and @JsonValue annotations available when Jackson is converted to an entity, but it still feels a bit cumbersome.For processing all 1, we need to modify Jackson's support for serialization and deserialization of enumeration classes.The configuration is as follows:


@Configuration
@Slf4j
public class JacksonConfiguration {

 /**
  * Jackson Converter 
  * @return
  */
 @Bean
 @Primary
 @SuppressWarnings({"rawtypes", "unchecked"})
 public MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter() {
  final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
  ObjectMapper objectMapper = converter.getObjectMapper();
  // Include.NON_EMPTY  Attribute is   Empty ( "" )   Or  NULL  Without serialization, the json There is no such field.This saves traffic on the mobile side 
  objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
  //  When deserializing, encountering extra fields does not fail, ignore 
  objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
  //  Allow special characters and escape characters 
  objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
  //  Allow single quotation marks 
  objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
  SimpleModule customerModule = new SimpleModule();
  customerModule.addDeserializer(String.class, new StringTrimDeserializer(String.class));
  customerModule.addDeserializer(Enumerable.class, new EnumDeserializer(Enumerable.class));
  customerModule.addSerializer(Enumerable.class, new EnumSerializer(Enumerable.class));
  objectMapper.registerModule(customerModule);
  converter.setSupportedMediaTypes(ImmutableList.of(MediaType.TEXT_HTML, MediaType.APPLICATION_JSON));
  return converter;
 }

}
public class EnumDeserializer<E extends Enumerable> extends StdDeserializer<E> {

 private Class<E> enumType;

 public EnumDeserializer(@Nonnull Class<E> enumType) {
  super(enumType);
  this.enumType = enumType;
 }

 @Override
 public E deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
  return EnumUtil.of(this.enumType, jsonParser.getIntValue());
 }

}

When we query the results and show them to the foreground, we add the @ResponseBody comment to the result set, which calls the serialization method of Jackson, so we add the sequence configuration of the enumeration class.If we simply convert the enumeration to int for the foreground, the foreground needs to maintain the relationship between int for this enumeration class and the corresponding presentation information.So this piece we will return the value and display information 1 together to the front desk to ease the pressure on the work of the front desk.


//  Register enumeration class serialization processing class 
customerModule.addSerializer(Enumerable.class, new EnumSerializer(Enumerable.class));

public class EnumSerializer extends StdSerializer<Enumerable> {

 public EnumSerializer(@Nonnull Class<Enumerable> type) {
  super(type);
 }

 @Override
 public void serialize(Enumerable enumerable, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
  jsonGenerator.writeStartObject();
  jsonGenerator.writeNumberField("value", enumerable.getValue());
  jsonGenerator.writeStringField("text", enumerable.getText());
  jsonGenerator.writeEndObject();
 }
}

This completes the configuration of both input and output parameters, and we can ensure that all ints passed from the foreground to the background are automatically converted to enumeration classes.If the returned data has an enumeration class, the enumeration class also contains values and presentation text, which is convenient and simple.

3. Storage layer conversions about enumeration classes.The ORM framework selected here is Mybatis, but if you look at the official website, only two scenarios are provided for the information on the official website. There is no common enumeration solution by enumerating the transformations of the hidden fields name and ordinal.However, by looking through the issue and release records in github, we find that the corresponding custom enumeration processing configuration is available in version 3.4.5, which does not require too much configuration. We directly increase the dependency of mybatis-spring-boot-starter and configure the corresponding Yaml file directly to achieve the function.


application.yml
--
mybatis:
 configuration:
  default-enum-type-handler: github.shiyajian.pretty.config.enums.EnumTypeHandler

public class EnumTypeHandler<E extends Enumerable> extends BaseTypeHandler<E> {

  private Class<E> enumType;

  public EnumTypeHandler() { /* instance */ }


  public EnumTypeHandler(@Nonnull Class<E> enumType) {
    this.enumType = enumType;
  }

  @Override
  public void setNonNullParameter(PreparedStatement preparedStatement, int i, E e, JdbcType jdbcType) throws SQLException {
    preparedStatement.setInt(i, e.getValue());
  }

  @Override
  public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
    int value = rs.getInt(columnName);
    return rs.wasNull() ? null : EnumUtil.of(this.enumType, value);
  }

  @Override
  public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    int value = rs.getInt(columnIndex);
    return rs.wasNull() ? null : EnumUtil.of(this.enumType, value);
  }

  @Override
  public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
    int value = cs.getInt(columnIndex);
    return cs.wasNull() ? null : EnumUtil.of(this.enumType, value);
  }

}

This completes the enumeration class conversion from the foreground page to the business code to the database, from the database query to the business code to the page.Enumeration classes do not need to be handled manually anymore throughout the project.Our development process is much simpler.

epilogue

A good plan does not require much superb technology, such as various reflections, various design modes, as long as the design is reasonable, it is simple and easy to use, similar to the mortise and tenon in ancient China.

summary


Related articles: