通过注解一次解决枚举类在spring中的3种序列化的方式

在日常的项目中,往往有很多枚举状态出现,比如性别、账户类型、订单状态等等,在代码编写阶段,对于这种状态类型,使用枚举类是非常方便的,但是由于为了方便与前端或数据库的网络传输、映射和存储,往往会采用约定数字或者特定字符来标识状态,我们有需要将枚举读写为数字或字符,如果用枚举,就要到处添加转换,如果不用枚举,就要头疼会不会写错状态。

怎么解决这个困扰呢?我们先来看最终效果:

只需一个注解,即可配置好让枚举在mybatis存储到数据库时采用数字id,json序列化传给前端时采用字符串,在controller的RequestParam 采用id反序列化为枚举 如果需要也可以通过实现指定接口来详细设定序列化的值

最重要的是一切对于springboot项目都是自动化的,完全可以作为基础组件来在各个项目中引入即可开箱即用

众所周知,在spring中,常见的序列化有3种,分别是 mybatis、json、request

  • mybatis 序列化
    • 通过TypeHandler完成java类型和数据库类型互相转换

  • json序列化(以spring默认json组件 jackson为例)
    • 通过JsonSerializer进行java类的序列化

    • 通过JsonDeserializer进行反序列化为java类

  • Spring Mvc 中 controller方法的序列化
    • 对于@ResponseBody@RequestBody注解的值,使用RequestResponseBodyMethodProcessor匹配合适的HttpMessageConverter进行序列化/反序列化,其中常用的HttpMessageConverterMappingJackson2HttpMessageConverter 用以对Accept头为application/json 的请求的返回值进行json序列化

    • 对于GET请求中参数方法(简单类型的@RequestParam可以省略),使用RequestParamMethodArgumentResolver并调用TypeConverterDelegate匹配合适的ConversionService进行序列化为方法参数对应的java类型


而我们实现这个组件,就需要从这三种序列化中入手

1.首先,我们先写一个注解:

@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. 这个注解用于指示这个枚举类如何序列化,稍后我们再详细设置,我们再写一个接口,用于可以详细设置序列化的值,而不是直接使用枚举的原始值,同时,为了更好兼容步骤3中的适配器,我们增加获取原始类和原始对象的方法

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. 那么,想必大家也看出来了,实现这个接口才是正路,那么问题来了,对于没有实现这接口的枚举类怎么办呢?我们把它包装在一个适配器中:

@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. 这样有了接口,有了适配器,我们就能给一开始注解CustomSerializationEnum中的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. 有了这些,我们就能够编写Json序列化器和mybatis需要的TypeHandler了:

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. 然后就是怎样自动扫描注解并将这些转换器注册进Spring中呢?

我们使用spring用来扫描@Component的组件ClassPathScanningCandidateComponentProvider来扫描我们的注解和接口实现类,使用ConverterRegistry 注册Converter,使用Jackson2ObjectMapperBuilderCustomizer 对spring全局的ObjectMapper注册模块,使用ConfigurationCustomizer 配置mybatis 的TypeHandler

@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. 好了,一切配置完毕,我们来测试吧!

一个有注解但是没有实现接口的枚举:

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

一个有注解也实现了接口的枚举:

@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;
    }
}
复制代码

一个实体类:

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

mybatis 的mapper就不贴了,我们直接看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;
    }
}
复制代码

点击controller左边的绿色小按钮,我们直接用idea发起http测试,这么简单的接口就用不着打开postman了


先post后get查询

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

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

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

我们发现数据已经正常存入数据库,并且也可以正常序列化为json了

在spring中加入如下配置即可查看mybatis执行的sql:

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]
复制代码

我们发现,存入数据库中的数据的确如注解所设置,是使用数字id进行存储

大功告成

Demo地址- gitee:custom-serialization-enum

猜你喜欢

转载自blog.csdn.net/qq_41701956/article/details/123452285