首页 前端知识 自定义Fastjson枚举类序列化、反序列化 --- 通过自定义Module方式实现

自定义Fastjson枚举类序列化、反序列化 --- 通过自定义Module方式实现

2024-09-01 00:09:30 前端知识 前端哥 646 696 我要收藏

自定义 Fastjson 通用枚举类型序列化与反序列化

在使用SpringMVC前端发送请求到后端的过程中几乎都会有枚举类型的参数需要传递,就用性别来看平时的使用过程(gender 1:男 2:女),然后再讨论怎么针对fastjson进行自定义通用枚举序列化与反序列的处理
注意:这里MessageConvert使用的是 FastJsonHttpMessageConverter

公共entity定义:

User类定义:

@Getter
@Setter
@ToString
public class User {
   private Integer id;
   private String name;
   private Integer gender;
}

性别枚举类定义:

@Getter
@RequiredArgsConstructor
public enum GenderEnum {
     MALE(1, "male", "男"),
     FEMALE(2, "female", "女");
   
     @JsonValue  // jackson 包中的注解
     private final Integer code;
     private final String name;
     private final String desc;
   
     @Override
     public String toString() {
         return "GenderEnum{" + "code=" + code + ", name='" + name + '\'' + ", desc='" + desc + '\'' + '}';
     }
}

一般前端发送请求到后端的方式有多种:POSTGET等,下面以POSTGET方式举例

POST方式

UserController:

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
     /**
      * application/json ==> {"name":"Bob","gender":2}
      */
     @PostMapping("/save")
     public void save(@RequestBody User user) {
         // 此处user对象中的gender是Integer类型
         log.info("save user:{}", user);
     }
}

GET方式:

UserController:

 @Slf4j
 @RestController
 @RequestMapping("/user")
 public class UserController {
     // GET /user/get/obj?name=Bob&gender=2
     @GetMapping("/get/obj")
     public User get(User user) {
         log.info("/get/obj user:{}", user);
         return user;
     }
 
     // GET /user/get/param?gender=1
     @GetMapping("/get/param")
     public Integer get(@RequestParam Integer gender) {
         log.info("/get/param gender:{}", gender);
         return gender;
     }    

     // GET /user/get/1
     @GetMapping("/get/{gender}")
     public Integer path(@PathVariable Integer gender) {
         log.info("/get/param gender:{}", gender);
         return gender;
     }
 }

从上述示例代码中可以看出,后端在接收性别参数时都是使用的Integer类型接收,然后再根据code值手动转换成对应的枚举类型,而通常情况下更期望是直接获取到转换后的枚举类型,
此时该如何实现自动根据枚举类型code值反序列化成对应的枚举对象?

自定义通用枚举类型序列化与反序列化器

公共类定义

package com.kws.annotation;
import java.lang.annotation.*;
/**
 * @author kws
 * @date 2024-01-24 13:59
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface EnumValueMarker {
}
package com.kws.annotation;

import com.fasterxml.jackson.annotation.JsonValue;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author kws
 * @date 2024-01-12 20:12
 */
public class EnumValueMarkerFinder {
//    public static final Class<? extends Annotation> ANNOTATION_CLASS = EnumValueMarker.class;
    public static final Class<? extends Annotation> ANNOTATION_CLASS = JsonValue.class;

    public static boolean hasAnnotation(Class<?> clazz) {
        try {
            return hasAnnotation(clazz, ANNOTATION_CLASS);
        } catch (Exception e) {
            return false;
        }
    }
    public static boolean hasAnnotation(Class<?> clazz, Class<? extends Annotation> annotationClass) {
        try {
            return findAnnotatedFields(clazz, annotationClass).size() > 0;
        } catch (Exception e) {
            return false;
        }
    }

    public static List<Field> findAnnotatedFields(Class<?> clazz, Class<? extends Annotation> annotationClass) {
        if (!clazz.isEnum()) {
            throw new RuntimeException("Class " + clazz.getName() + " is not an Enum");
        }
        return Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.isAnnotationPresent(annotationClass)).collect(Collectors.toList());
    }

    public static Field find(Class<?> clazz) {
        return find(clazz, ANNOTATION_CLASS);
    }

    public static Field find(Class<?> clazz, Class<? extends Annotation> annotationClass) {
        if (!clazz.isEnum()) {
            throw new RuntimeException("Class " + clazz.getName() + " is not an Enum");
        }

        List<Field> fields = Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.isAnnotationPresent(annotationClass)).collect(Collectors.toList());
        if (fields.isEmpty()) {
            throw new RuntimeException("Enum " + clazz.getName() + " has no field annotated with " + annotationClass.getName());
        }
        if (fields.size() > 1) {
            throw new RuntimeException(formatMsg(fields.get(0), fields.get(1)));
        }

        Field field = fields.get(0);
        if (field == null) {
            throw new RuntimeException("Enum " + clazz.getName() + " has no field annotated with " + ANNOTATION_CLASS.getName());
        }
        field.setAccessible(true);
        return field;
    }

    public static String formatMsg(Field field, Field field2) {
        return String.format("Multiple 'as-value' properties defined ([field %s#%s] vs [field %s#%s])", field.getDeclaringClass().getName(), field.getName(), field2.getDeclaringClass().getName(), field2.getName());
    }

    public static String formatMsg(Type type, String name, Object value) {
        return String.format("【%s#%s:%s is not exist】", type.getTypeName(), name, value);
    }
}

实现思路:

  1. 自定义注解用于标识枚举字段code值(可以使用Jackson自带的@JsonValue注解,也可以单独自定义注解),注解标识的字段类型非固定类型,可为IntegerLongString等其他基本类型或其他类型(其他类型请多测试)

  2. 实现自定义序列化类:FastJsonEnumSerializer.java

    @Slf4j
    public class FastJsonEnumSerializer implements ObjectSerializer {
        public FastJsonEnumSerializer() {}
        @Override
        public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) throws IOException {
            if (object == null) {
                return;
            }
            try {
                // 获取枚举类字段加了@JsonValue注解或自定义注解的字段,因为需要序列化的就是当前属性的值
                Field field = EnumValueMarkerFinder.find(object.getClass());
                Object val = field.get(object);
                log.info("==> serialize {}#{}【{}】", fieldType != null ? fieldType.getTypeName() : object.getClass().getName(), field.getName(), val);
                serializer.write(val);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
    }
    
  3. 实现自定义反序列化类:FastJsonEnumDeserializer.java

    @Slf4j
    public class FastJsonEnumDeserializer implements ObjectDeserializer {
    
        @SuppressWarnings("unchecked")
        @Override
        public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
            try {
                Field field = EnumValueMarkerFinder.find((Class<T>) type);
    
                Object value = parser.parse();
                String name = field.getName();
                log.info("==> deserialize {}#{}【{}】", type.getTypeName(), name, value);
    
                // 根据code值反序列化成对应的枚举类
                PropertyDescriptor propertyDescriptor = BeanUtils.getPropertyDescriptor((Class<?>) type, name);
                Method readMethod = Optional.ofNullable(propertyDescriptor).map(PropertyDescriptor::getReadMethod).orElseThrow(() -> new RuntimeException("获取属性" + name + "失败,缺少get方法"));
                for (T enumConstant : ((Class<T>) type).getEnumConstants()) {
                    Object codeValue = readMethod.invoke(enumConstant);
                    if (value.equals(codeValue)) {
                        return enumConstant;
                    }
                }
                log.warn("==> deserialized failed. {}", EnumValueMarkerFinder.formatMsg(type, name, value));
            } catch (Exception e) {
                log.error("==> deserialized failed.", e);
                throw new JSONException("getEnumValue error", e);
            }
            return null;
        }
    
        @Override
        public int getFastMatchToken() {
            return 0;
        }
    }
    
  4. 上述步骤中已经完成了fastjson枚举类型序列化与反序列化的自定义功能,那么该如何生效呢?

  5. 接下来我们通过自定义一个Module:CustomizeEnumModule.java并实现com.alibaba.fastjson.spi.Module接口中的createDeserializercreateSerializer方法来将刚才定义的反序列化与序列化功能接入fastjson

    public class CustomizeEnumModule implements Module {
        public CustomizeEnumModule() {
        }
    
        @Override
        public ObjectDeserializer createDeserializer(ParserConfig config, Class type) {
            return isCustomizedEnum(type) ? new FastJsonEnumDeserializer() : null;
        }
    
        @Override
        public ObjectSerializer createSerializer(SerializeConfig config, Class type) {
            return isCustomizedEnum(type) ? new FastJsonEnumSerializer() : null;
        }
    
        // 是否为枚举类型且当前枚举类型属性中有加了@JsonValue注解或自定义注解的属性
        private boolean isCustomizedEnum(Class<?> type) {
            return type.isEnum() && EnumValueMarkerFinder.hasAnnotation(type);
        }
    }
    
  6. Module已经定义完成,接下来就是如何将自定义的Module应用到fastjson的序列化与反序列化中了?

  7. 自定义FastJsonEnumConfiguration配置类,用来注册自定义的Module,及将自定义的序列化与反序列化器注册进去

    @Slf4j
    @Configuration(proxyBeanMethods = false)
    public class FastJsonEnumConfiguration {
    
        @Configuration(proxyBeanMethods = false)
        @ConditionalOnClass({ObjectSerializer.class, ObjectDeserializer.class})
        public static class CustomizeEnumGlobalConfig {
            @PostConstruct
            public void init() {
                if (log.isDebugEnabled()) {
                    log.debug("====== Register global customized enum serializer and deserializer ======");
                }
                SerializeConfig serializeConfig = SerializeConfig.getGlobalInstance();
                ParserConfig parserConfig = ParserConfig.getGlobalInstance();
    
                // 在这里
                CustomizeEnumModule customizeEnumModule = new CustomizeEnumModule();
                serializeConfig.register(customizeEnumModule);
                parserConfig.register(customizeEnumModule);
            }
        }
    }
    
  8. 完成上述步骤后调整User对象的定义,可以使用GenderEnum枚举类来接收性别参数而不需要再使用Integer类型接收参数了
    User定义如下:

    @Getter
    @Setter
    @ToString
    public class User {
        private Integer id;
        private String name;
        private GenderEnum gender;
    
        public User() {}
    
        public User(String name, GenderEnum gender) {
            this.name = name;
            this.gender = gender;
        }
    }
    
  9. UserController.java

    @Slf4j
    @RestController
    @RequestMapping("/user")
    public class UserController {
        // application/json ===> {"name":"Bob","gender":2}
        @PostMapping("/save")
        public void save(@RequestBody User user) {
            // 现在user对象中的性别属性已经是枚举类型数据了
            log.info("save user:{}", user);
        }
    }
    
  10. 目前针对POST方式application/json的请求已经都可以自动将参数中的枚举类型进行自动完成序列化与反序列化了,
    但是在同事的提醒下,发现GET方法的请求参数还不行,再继续完成对GET方式json枚举类型参数的序列化与反序列化

  11. 自定义CustomConverterFactory.java并实现org.springframework.core.convert.converter.ConditionalConverter接口和org.springframework.core.convert.converter.ConverterFactory接口

    @Slf4j
    public class CustomConverterFactory implements ConditionalConverter, ConverterFactory<String, Enum<?>> {
    
        @Override
        public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
            return EnumValueMarkerFinder.hasAnnotation(targetType.getType());
        }
    
        @Override
        public <T extends Enum<?>> Converter<String, T> getConverter(Class<T> targetType) {
            if (!targetType.isEnum()) {
                return null;
            }
            try {
                Field field = EnumValueMarkerFinder.find(targetType);
                return new CustomEnumConvert<>(targetType, field);
            } catch (Exception e) {
                log.error("字段属性转换失败", e);
            }
            return null;
        }
    
        @RequiredArgsConstructor
        private static class CustomEnumConvert<T extends Enum<?>> implements Converter<String, T>{
            private final Class<T> enumType;
            private final Field field;
    
            @Override
            public T convert(String source) {
                try {
                    PropertyDescriptor propertyDescriptor = BeanUtils.getPropertyDescriptor(enumType, field.getName());
                    Method readMethod = Optional.ofNullable(propertyDescriptor).map(PropertyDescriptor::getReadMethod).orElseThrow(() -> new RuntimeException("获取属性" + field.getName() + "失败,缺少get方法"));
                    for (T enumConstant : enumType.getEnumConstants()) {
                        Object codeValue = readMethod.invoke(enumConstant);
                        if (codeValue != null && source.equals(codeValue.toString())) {
                            return enumConstant;
                        }
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                return null;
            }
        }
    }
    
  12. UserController.java中get请求示例代码:

        @Slf4j
        @RestController
        @RequestMapping("/user")
        public class UserController {
        
            //GET /user/get/obj?name=Bob&gender=3
            @GetMapping("/get/obj")
            public User get(User user) {
                log.info("/get/obj user:{}", user);
                return user;
            }
        
            // GET /user/get/param?gender=1
            @GetMapping("/get/param")
            public GenderEnum get(@RequestParam GenderEnum gender) {
                log.info("/get/param GenderEnum:{}", gender);
                return gender;
            }
        
            @GetMapping("/get/{gender}")
            public GenderEnum path(@PathVariable GenderEnum gender) {
                log.info("/get/param GenderEnum:{}", gender);
                return gender;
            }
        }
    
  13. 完整代码已发布github
    github: enum-mapping
    模块为:enum-mapping-mvc-fastjson

转载请注明出处或者链接地址:https://www.qianduange.cn//article/17376.html
标签
评论
发布的文章

Spring MVC-JSON

2024-06-02 09:06:53

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!