在日常的项目中,往往有很多枚举状态出现,比如性别、账户类型、订单状态等等,在代码编写阶段,对于这种状态类型,使用枚举类是非常方便的,但是由于为了方便与前端或数据库的网络传输、映射和存储,往往会采用约定数字或者特定字符来标识状态,我们有需要将枚举读写为数字或字符,如果用枚举,就要到处添加转换,如果不用枚举,就要头疼会不会写错状态。
怎么解决这个困扰呢?我们先来看最终效果:
只需一个注解,即可配置好让枚举在mybatis存储到数据库时采用数字id,json序列化传给前端时采用字符串,在controller的RequestParam 采用id反序列化为枚举 如果需要也可以通过实现指定接口来详细设定序列化的值
最重要的是一切对于springboot项目都是自动化的,完全可以作为基础组件来在各个项目中引入即可开箱即用
众所周知,在spring中,常见的序列化有3种,分别是 mybatis、json、request
- mybatis 序列化
- json序列化(以spring默认json组件 jackson为例)
- Spring Mvc 中 controller方法的序列化
-
对于@ResponseBody 、@RequestBody 注解的值,使用RequestResponseBodyMethodProcessor 匹配合适的HttpMessageConverter 进行序列化/反序列化,其中常用的HttpMessageConverter 为MappingJackson2HttpMessageConverter 用以对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进行存储的
大功告成!
|