Solve the three serialization methods of enumeration classes in spring by annotating once

In daily projects, there are often many enumeration states, such as gender, account type, order status, etc. In the code writing stage, it is very convenient to use the enumeration class for this state type, but for convenience and For network transmission, mapping and storage of front-end or database, conventional numbers or specific characters are often used to identify states. We need to read and write enumerations as numbers or characters. If we use enumerations, we need to add conversions everywhere. If we do not use enumerations If you lift it, you will have a headache if you write the wrong state.

How to solve this trouble? Let's first look at the final effect:

只需一个注解,即可配置好让枚举在mybatis存储到数据库时采用数字id,json序列化传给前端时采用字符串,在controller的RequestParam 采用id反序列化为枚举If necessary, you can also set the serialized value in detail by implementing the specified interface

The most important thing is that everything is automated for the springboot project, and it can be used as a basic component to be introduced in each project and used out of the box

As we all know, in spring, there are 3 kinds of common serialization, namely mybatis, json, request

  • mybatis serialization
    • By TypeHandlercompleting the conversion between java types and database types

  • JSON serialization (take the spring default JSON component jackson as an example)
    • By JsonSerializerserializing the java class

    • By JsonDeserializerdeserializing to a java class

  • Serialization of controller methods in Spring Mvc
    • For the value @ResponseBodyof @RequestBodythe annotation, serialize/deserialize with RequestResponseBodyMethodProcessormatching appropriate ones , which are commonly used to serialize the return value of the request with the header as jsonHttpMessageConverterHttpMessageConverterMappingJackson2HttpMessageConverterAcceptapplication/json

    • For GETthe parameter method in the request (simple type @RequestParamcan be omitted), use RequestParamMethodArgumentResolverand call the java type that TypeConverterDelegatematches the appropriate ConversionServiceserialization to the corresponding method parameter


And when we implement this component, we need to start from these three serializations

1. First, let's write an annotation:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CustomSerializationEnum {
    Type myBatis();

    Type json() default Type.NAME;

    Type requestParam() default Type.NAME;

    enum Type {
    NAME,ID,CLASS,TO_STRING
    }
}
复制代码

2. This annotation is used to indicate how the enumeration class is serialized. We will set it in detail later. We will write an interface to set the serialized value in detail instead of directly using the original value of the enumeration. At the same time, In order to be more compatible with the adapter in step 3, we add methods to obtain the original class and original object

import org.springframework.core.annotation.AnnotationUtils;

public interface EnumSerialize<T extends Enum<T> & EnumSerialize<T>> {
    default String getSerializationName() {
        //noinspection unchecked
        return ((Enum<?>) this).name();
    }

    default Integer getSerializationId() {
        //noinspection unchecked
        return ((Enum<?>) this).ordinal();
    }
    /**
     * 获取原始类,专门给适配器使用的
     */
    default Class<T> getOriginalClass() {
        //noinspection unchecked
        return (Class<T>) this.getClass();
    }
    /**
     * 获取原始枚举对象,专门给适配器使用的
     */
    default Enum<T> getOriginalEnum() {
        //noinspection unchecked
        return (Enum<T>) this;
    }
    static <T extends Enum<T> & EnumSerialize<T>> CustomSerializationEnum getAnnotation(Class<T> enumClass) {
        return AnnotationUtils.findAnnotation(enumClass, CustomSerializationEnum.class);
    }
}
复制代码

3. So, everyone must have seen that it is the right way to implement this interface, so the question is, what to do with the enumeration class that does not implement this interface? We wrap it in an adapter:

@SuppressWarnings("rawtypes")
public final class EnumSerializeAdapter implements EnumSerialize {
    private final Enum<?> enumInstance;

    public Enum<?> getEnumInstance() {
        return enumInstance;
    }

    public EnumSerializeAdapter(Enum<?> enumInstance) {
        this.enumInstance = enumInstance;
    }

    @Override
    public String getSerializationName() {
        return enumInstance.name();
    }

    @Override
    public Integer getSerializationId() {
        return enumInstance.ordinal();
    }

    @Override
    public String toString() {
        return enumInstance.toString();
    }

    @Override
    public Class<?> getOriginalClass() {
        return enumInstance.getClass();
    }

    @Override
    public Enum<?> getOriginalEnum() {
        return enumInstance;
    }
}

复制代码

4. With the interface and the adapter in this way, we can add specific functions to the annotations CustomSerializationEnumat the beginning:Type


import sun.misc.SharedSecrets;

import java.lang.annotation.*;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/**
 * <H1>指示枚举类自定义序列化的方法,被注解类必须为 <span style="color:#c7c7c7">枚举类</span></H1><br>
 * <H1>被注解类通常实现 <span style="color:#c7c7c7">{@link EnumSerialize}</span>,用以提供更丰富的序列化选择,否则会包装成<span style="color:#c7c7c7">{@link EnumSerializeAdapter}</span></H1>
 *
 * @author muyuanjin
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CustomSerializationEnum {
    Type myBatis();

    Type json() default Type.NAME;

    Type requestParam() default Type.NAME;

    enum Type {

        NAME {
            @Override
            public String getSerializedValue(EnumSerialize<?> serializationEnum) {
                return serializationEnum.getSerializationName();
            }
        },
        ID {
            @Override
            public Integer getSerializedValue(EnumSerialize<?> serializationEnum) {
                return serializationEnum.getSerializationId();
            }
        },
        CLASS {
            @Override
            public String getSerializedValue(EnumSerialize<?> serializationEnum) {
                return serializationEnum.getOriginalClass().getCanonicalName() + ":" + serializationEnum.getOriginalEnum().name();
            }
        },
        TO_STRING {
            @Override
            public Object getSerializedValue(EnumSerialize<?> serializationEnum) {
                return serializationEnum.toString();
            }
        };

        Type() {
        }

        static final Map<Class<? extends EnumSerialize<?>>, Map<Object, Enum<?>>> DESERIALIZE_MAP = new ConcurrentHashMap<>();

        public abstract Object getSerializedValue(EnumSerialize<?> serializationEnum);

        @SuppressWarnings("unchecked")
        public <T extends Enum<T> & EnumSerialize<T>> T getDeserializeObj(Class<T> enumClass, Object serializedValue) {
            if (enumClass == null || serializedValue == null) {
                return null;
            }
            return (T) DESERIALIZE_MAP.computeIfAbsent(enumClass, t -> new ConcurrentHashMap<>())
                    .computeIfAbsent(serializedValue.toString(),
                            t -> Arrays.stream(SharedSecrets.getJavaLangAccess().getEnumConstantsShared(enumClass)).filter(Objects::nonNull)
                                    .filter(e -> {
                                        //noinspection ConstantConditions
                                        if (e instanceof EnumSerialize) {
                                            return getSerializedValue(e).toString().equals(serializedValue.toString());
                                        } else if (e.getClass().isEnum()) {
                                            return getSerializedValue(new EnumSerializeAdapter(e)).toString().equals(serializedValue.toString());
                                        }
                                        return false;
                                    }).findFirst().orElse(null)
                    );
        }
    }
}
复制代码

5. With these, we can write the Json serializer and the TypeHandler required by mybatis:

public class CustomSerializationEnumJsonSerializer<T extends Enum<T> & EnumSerialize<T>> extends JsonSerializer<T> {
    private final CustomSerializationEnum.Type type;

    public CustomSerializationEnumJsonSerializer(Pair<Class<Enum<?>>, Set<EnumSerialize<T>>> enumSerialize) {
        //noinspection unchecked,rawtypes
        CustomSerializationEnum annotation = EnumSerialize.getAnnotation((Class) enumSerialize.getKey());
        //找不到注解就默认使用name序列化
        type = annotation == null ? CustomSerializationEnum.Type.NAME : annotation.json();
    }

    @Override
    public void serialize(T value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        Object serializedValue;
        //noinspection ConstantConditions
        if (value instanceof EnumSerialize) {
            serializedValue = type.getSerializedValue(value);
        } else {
            //noinspection ConstantConditions
            serializedValue = type.getSerializedValue(new EnumSerializeAdapter(value));
        }
        serializers.findValueSerializer(serializedValue.getClass()).serialize(serializedValue, gen, serializers);
    }
}
复制代码
public class CustomSerializationEnumJsonDeserializer<T extends Enum<T> & EnumSerialize<T>> extends JsonDeserializer<T> {

    private final CustomSerializationEnum.Type type;
    private final Class<T> clazz;

    public CustomSerializationEnumJsonDeserializer(Pair<Class<Enum<?>>, Set<EnumSerialize<T>>> enumSerialize) {
        //noinspection unchecked,rawtypes
        clazz = (Class) enumSerialize.getKey();
        //找不到注解就默认使用name序列化
        CustomSerializationEnum annotation = EnumSerialize.getAnnotation(clazz);
        type = annotation == null ? CustomSerializationEnum.Type.NAME : annotation.json();
    }

    @Override
    public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        return type.getDeserializeObj(clazz, p.getText());
    }
}
复制代码
public class CustomSerializationEnumTypeHandler<T extends Enum<T> & EnumSerialize<T>> extends BaseTypeHandler<T> {
    private final CustomSerializationEnum.Type type;
    private final Class<T> clazz;

    public CustomSerializationEnumTypeHandler(Pair<Class<Enum<?>>, Set<EnumSerialize<T>>> enumSerialize) {
        //noinspection unchecked,rawtypes
        clazz = (Class) enumSerialize.getKey();
        CustomSerializationEnum annotation = EnumSerialize.getAnnotation(clazz);
        type = annotation == null ? CustomSerializationEnum.Type.NAME : annotation.myBatis();
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
        Object serializedValue;
        //noinspection ConstantConditions
        if (parameter instanceof EnumSerialize) {
            serializedValue = type.getSerializedValue(parameter);
        } else {
            //noinspection ConstantConditions
            serializedValue = type.getSerializedValue(new EnumSerializeAdapter(parameter));
        }
        if (serializedValue instanceof String) {
            ps.setString(i, (String) serializedValue);
        } else if (serializedValue instanceof Integer) {
            ps.setInt(i, (Integer) serializedValue);
        }
    }

    @Override
    public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return type.getDeserializeObj(clazz, rs.getObject(columnName));
    }

    @Override
    public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return type.getDeserializeObj(clazz, rs.getObject(columnIndex));
    }

    @Override
    public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return type.getDeserializeObj(clazz, cs.getObject(columnIndex));
    }
}
复制代码

6. Then how to automatically scan annotations and register these converters with Spring?

We use @Componentthe components that spring uses to scan ClassPathScanningCandidateComponentProviderto scan our annotations and interface implementation classes, use the ConverterRegistry registration Converter, use Jackson2ObjectMapperBuilderCustomizer the Spring global ObjectMapper registration module, and ConfigurationCustomizerconfigure the TypeHandler of mybatis

@Slf4j
@Configuration(proxyBeanMethods = false)
public class TestConfig {
    private final Map<Class<Enum<?>>, Set<EnumSerialize<?>>> enumSerializes;
    //在spring 配置文件中使用custom-serialization-enum.path即可配置扫描路径,没有配置就使用com.muyuanjin作为默认值
    //如果需要作为基础组件在多个项目中使用,就不是这样配置了,但是原理是一样的
    public TestConfig(@Value("${custom-serialization-enum.path:com.muyuanjin}") String path) {
        final ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
        enumSerializes = getEnumSerializes(provider, path);
        enumSerializes.putAll(getAnnotatedEnums(provider, path));
    }

    @Autowired
    @SuppressWarnings({"unchecked", "rawtypes"})
    void registryConverter(ConverterRegistry converterRegistry) {
        for (Map.Entry<Class<Enum<?>>, Set<EnumSerialize<?>>> classSetEntry : enumSerializes.entrySet()) {
            Class clazz = classSetEntry.getKey();
            CustomSerializationEnum annotation = EnumSerialize.getAnnotation(clazz);
            //找不到注解就默认使用name序列化
            CustomSerializationEnum.Type type = annotation == null ? CustomSerializationEnum.Type.NAME : annotation.requestParam();
            converterRegistry.addConverter(String.class, clazz, t -> type.getDeserializeObj(clazz, t));
        }
    }

    @Bean
    @SuppressWarnings({"unchecked", "rawtypes"})
    public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
        return builder -> builder.modules(new SimpleModule() {
            {
                for (Map.Entry<Class<Enum<?>>, Set<EnumSerialize<?>>> classSetEntry : enumSerializes.entrySet()) {
                    Class clazz = classSetEntry.getKey();
                    addDeserializer(clazz, new CustomSerializationEnumJsonDeserializer(new Pair<>(classSetEntry.getKey(), classSetEntry.getValue())));
                    addSerializer(clazz, new CustomSerializationEnumJsonSerializer(new Pair<>(classSetEntry.getKey(), classSetEntry.getValue())));
                }
            }
        });
    }

    @Bean
    @SuppressWarnings({"unchecked", "rawtypes"})
    ConfigurationCustomizer mybatisConfigurationCustomizer() {
        return t -> {
            for (Map.Entry<Class<Enum<?>>, Set<EnumSerialize<?>>> classSetEntry : enumSerializes.entrySet()) {
                Class clazz = classSetEntry.getKey();
                t.getTypeHandlerRegistry().register(clazz, new CustomSerializationEnumTypeHandler(new Pair<>(classSetEntry.getKey(), classSetEntry.getValue())));
            }
        };
    }

    /**
     * 扫描对应路径所有实现了EnumSerialize接口的类,排除掉EnumSerializeAdapter之后,返回类和对应类的枚举实例
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    @SneakyThrows(ClassNotFoundException.class)
    private static Map<Class<Enum<?>>, Set<EnumSerialize<?>>> getEnumSerializes(ClassPathScanningCandidateComponentProvider provider, String path) {
        provider.resetFilters(false);
        provider.addIncludeFilter(new AssignableTypeFilter(EnumSerialize.class));
        final Set<BeanDefinition> components = provider.findCandidateComponents(path);
        final Map<Class<Enum<?>>, Set<EnumSerialize<?>>> enumSerializes = new HashMap<>();
        for (final BeanDefinition component : components) {
            final Class<?> cls = Class.forName(component.getBeanClassName());
            if (cls.equals(EnumSerializeAdapter.class)) {
                continue;
            }
            if (cls.isEnum()) {
                for (Enum<?> anEnum : SharedSecrets.getJavaLangAccess().getEnumConstantsShared((Class) cls)) {
                    enumSerializes.computeIfAbsent((Class<Enum<?>>) cls, t -> new HashSet<>()).add((EnumSerialize<?>) anEnum);
                }
            } else {
                throw new UnsupportedOperationException("Class:" + cls.getCanonicalName() + "is not enum! " + "The class that implements the "EnumSerialize" must be an enumeration class.");
            }
        }
        return enumSerializes;
    }
    /**
     * 扫描对应路径所有被CustomSerializationEnum注解并且没有实现的EnumSerialize的类,排除掉,返回类和对应类的和被是适配器包装后的枚举实例
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    @SneakyThrows(ClassNotFoundException.class)
    private static Map<Class<Enum<?>>, Set<EnumSerialize<?>>> getAnnotatedEnums(ClassPathScanningCandidateComponentProvider provider, String path) {
        provider.resetFilters(false);
        provider.addIncludeFilter(new AnnotationTypeFilter(CustomSerializationEnum.class));
        provider.addExcludeFilter(new AssignableTypeFilter(EnumSerialize.class));
        final Set<BeanDefinition> components = provider.findCandidateComponents(path);
        final Map<Class<Enum<?>>, Set<EnumSerialize<?>>> enumSerializes = new HashMap<>();
        for (final BeanDefinition component : components) {
            final Class<?> cls = Class.forName(component.getBeanClassName());
            if (cls.isEnum()) {
                for (Enum<?> anEnum : SharedSecrets.getJavaLangAccess().getEnumConstantsShared((Class) cls)) {
                    enumSerializes.computeIfAbsent((Class<Enum<?>>) cls, t -> new HashSet<>()).add(new EnumSerializeAdapter(anEnum));
                }
            } else {
                throw new UnsupportedOperationException("Class:" + cls.getCanonicalName() + "is not enum! " + "The class annotated by "CustomSerializationEnum" must be an enumeration class.");
            }
        }
        return enumSerializes;
    }
}
复制代码

7. Ok, everything is configured, let's test it!

An annotated enum that does not implement an interface:

@CustomSerializationEnum(myBatis = CustomSerializationEnum.Type.ID, 
json = CustomSerializationEnum.Type.NAME, 
requestParam = CustomSerializationEnum.Type.ID)
public enum AccountType {
    BUILT_IN, ORDINARY, GUEST
}
复制代码

An annotated enum that also implements the interface:

@CustomSerializationEnum(myBatis = CustomSerializationEnum.Type.ID, 
json = CustomSerializationEnum.Type.NAME)
public enum Gender implements EnumSerialize<Gender> {
    MALE("男"),
    FEMALE("女"),
    UNKNOWN("未知") {
        @Override
        public String getSerializationName() {
            return "秀吉";
        }

        @Override
        public Integer getSerializationId() {
            return 114514;
        }
    };

    private final String name;

    Gender(String name) {
        this.name = name;
    }

    @Override
    public String getSerializationName() {
        return name;
    }
}
复制代码

An entity class:

@Data
public class UserEntity {
    private String username;
    private String password;
    private Gender gender;
    private AccountType accountType;
}
复制代码

The mapper of mybatis will not be posted, let's look directly at the controller:

@RestController
@RequiredArgsConstructor
public class TestController {
    private final UserMapper userMapper;

    @GetMapping
    public List<UserEntity> get() {
        return userMapper.select();
    }

    @PostMapping
    public void creat(@RequestBody UserEntity userEntity) {
        userMapper.insert(userEntity);
    }

    @GetMapping("/gender")
    public Gender gender(Gender gender) {
        return gender;
    }

    @GetMapping("/accountType")
    public AccountType accountType(AccountType accountType) {
        return accountType;
    }
}
复制代码

Click the small green button on the left side of the controller, we directly use the idea to initiate the http test, so there is no need to open postman for such a simple interface


Post first and then get query

###
POST http://localhost:8085/
Content-Type: application/json

{
  "username": "test",
  "password": "content",
  "gender": "秀吉",
  "accountType": "ORDINARY"
}

###
GET http://localhost:8085/?username=test
复制代码

We found that the data has been stored in the database normally, and can also be serialized to json normally

Add the following configuration to spring to view the sql executed by mybatis:

mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
复制代码
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5ff500a6] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1368709612 wrapping conn0: url=jdbc:h2:mem:5ff25d54-569b-488c-8073-1fef973eadf3 user=SA] will not be managed by Spring
==>  Preparing: insert into `user` (`username`,`password`,gender,account_type) values (?,?,?,?)
==> Parameters: test(String), content(String), 114514(Integer), 1(Integer)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5ff500a6]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@487dd9b] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@643544028 wrapping conn0: url=jdbc:h2:mem:5ff25d54-569b-488c-8073-1fef973eadf3 user=SA] will not be managed by Spring
==>  Preparing: select * from `user`
==> Parameters: 
<==    Columns: USERNAME, PASSWORD, GENDER, ACCOUNT_TYPE
<==        Row: test, content, 114514, 1
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@487dd9b]
复制代码

We found that the data stored in the database is indeed as set by the annotation, and is stored using a digital id

You're done !

Demo address - gitee: custom-serialization-enum

Guess you like

Origin blog.csdn.net/qq_41701956/article/details/123452285