Jackson 自定义注解扩展实战

1、简介

Jackson是一个json序列化工具, 并且作为SpringBoot默认的序列化和反序列化方式, 所以接口的请求体和响应体都是经过Jackson的处理, 并且Jackson是可以支持自定义序列化和反序列化的方式, 所以基于此我们可以扩展实现一些自定义序列化注解, 就像 @JsonFormat注解对时间格式处理一样。 那我们扩展自定义注解原理也很简单,主要是利用 @JsonSerialize注解和@JacksonAnnotationsInside注解去实现, @JacksonAnnotationsInside是一个组合注解,主要标记在用户的自定义注解上,那么这个用户自定义注解上标记的所有其他注解也会生效。

扩展注解如下:

  • @JKByteFormat 字节单位格式化
  • @JKDecimalFormat 小数格式化
  • @JKPercentFormat 百分数格式化
  • @JKEnumSerializer 枚举格式化。(支持枚举列表)
  • @JKEnumDeserializer 枚举反格式化

2、扩展注解基类实现

2.1、自定义序列化注解基类

主要是先把解析自定义注解和字段类型的逻辑抽象到基类父用。 实现了 ContextualSerializer接口的createContextual方法, 先判断是否标记了自定义注解,如果没有标记则走正常的序列化逻辑, 反之则走自定义的序列化逻辑。

java 复制代码
/**
 *
 * @param <A>                     自定义的序列化注解
 * @param <FieldType>             序列化的字段类型
 */
public abstract class BaseJKAnnotationSerializer<A extends Annotation,FieldType> extends JsonSerializer<FieldType> implements ContextualSerializer {

    /**
     *  自定义的注解
     */
    protected A annotation;

    /**
     *  序列化的字段类型
     */
    protected Class<?> fieldClass;

    /**
     *  动态决定序列化的方式,以及获取序列化的上下文,比如要序列化的字段的信息等等
     */
    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
        // 子类获取父亲标记的泛型参数
        Class<A> acls = getFatherParamType();

        annotation = property.getAnnotation(acls);
        this.fieldClass = property.getType().getRawClass();

        if (annotation == null || !isFilterSerializer(property)){
            return prov.findValueSerializer(property.getType(), property);
        }
        return  this;
    }

    /**
     *  判断此字段是否需要使用此序列化器
     */
    protected boolean isFilterSerializer(BeanProperty property){
        return true;
    }

    private Class<A> getFatherParamType() {
        Type superclass = getClass().getGenericSuperclass();
        Class<A> acls = null;
        if (superclass instanceof ParameterizedType){
            ParameterizedType parameterizedType = (ParameterizedType) superclass;
            Type actualTypeArgument = parameterizedType.getActualTypeArguments()[0];
            acls =  (Class<A>) actualTypeArgument;
        }
        return acls;
    }

    public static String smartScale(BigDecimal olBValue, int scale) {
        if (olBValue == null){
            return "";
        }

        if (olBValue.compareTo(BigDecimal.ZERO) == 0){
            return "0";
        }

        BigDecimal bValue = olBValue.setScale(scale, RoundingMode.HALF_UP);
        for (int i = scale + 1; i <= 8; i++) {
            if (bValue.compareTo(BigDecimal.ZERO) != 0){
                break;
            }
            bValue = olBValue.setScale(i, RoundingMode.HALF_UP);
        }

        return bValue.toString();
    }
}

2.2、自定义反序列化注解基类

主要是先把解析自定义注解和字段类型的逻辑抽象到基类父用。 实现了 ContextualDeserializer接口的createContextual方法, 先判断是否标记了自定义注解,如果没有标记则走正常的序列化逻辑, 反之则走自定义的序列化逻辑。

java 复制代码
/**
 *
 * @param <A>                   自定义反序列化注解
 * @param <FieldType>           处理的反序列化的字段类型
 */
public abstract class BaseJKAnnotationDeSerializer<A extends Annotation,FieldType> extends JsonDeserializer<FieldType> implements ContextualDeserializer {

    /**
     *  字段标记的注解
     */
    protected A annotation;

    /**
     * 反序列化的字段类型
     */
    protected Class<?> fieldClass;

    /**
     * 反序列化的字段名
     */
    protected String fieldName;

    /**
     *  动态决定序列化的方式,以及获取序列化的上下文,比如要序列化的字段的信息等等
     */
    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext prov, BeanProperty property) throws JsonMappingException {
        // 子类获取父亲标记的泛型参数
        Type superclass = getClass().getGenericSuperclass();
        Class<A> acls = null;
        if (superclass instanceof ParameterizedType){
            ParameterizedType parameterizedType = (ParameterizedType) superclass;
            Type actualTypeArgument = parameterizedType.getActualTypeArguments()[0];
            acls =  (Class<A>) actualTypeArgument;
        }

        this.fieldClass = property.getType().getRawClass();
        this.fieldName = property.getName();

        annotation = property.getAnnotation(acls);
        if (annotation == null || !isFilterDeserializer(property)){
            return prov.findContextualValueDeserializer(property.getType(), property);
        }

        return this;
    }

    /**
     *  判断是否需要使用此反序列化器
     */
    protected boolean isFilterDeserializer(BeanProperty property){
        return true;
    }
}

3、注解扩展

3.1 @JKByteFormat 字节单位格式化

一般我们数据库存在的数据大小的单位都是字节,因为这样精度才是最完整的。 但是常常需求页面需要展示各种单位以及带单位符号。 比如按MB、KB展示, 所以扩展此注解直接标记即可前端直接展示不需要做额外处理。

java 复制代码
/**
 *  字节格式化
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JsonSerialize(using = JKByteFormatSerializer.class)
@JacksonAnnotationsInside
public @interface JKByteFormat {

    // 保留精度
    int scale() default 2;

    /**
     * 原数值类型
     */
    Unit type() default Unit.BYTE;

    /**
     * 转换后的数值类型
     */
    Unit convert();

    String unit() default "";

    enum Unit{
        BIT,
        BYTE,
        KB,
        MB,
        GB
        ;
    }
}
java 复制代码
public class JKByteFormatSerializer extends BaseJKAnnotationSerializer<JKByteFormat,Number>{

    @Override
    public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (value == null){
            return;
        }

        JKByteFormat.Unit type = this.annotation.type();
        JKByteFormat.Unit convert = this.annotation.convert();
        int scale = this.annotation.scale();
        String unitSymbol = this.annotation.unit();

        BigDecimal bigValue = new BigDecimal(value.toString());
        BigDecimal result = convertValue(bigValue, type, convert, scale);

        if (!unitSymbol.isEmpty()){
            gen.writeString(result + unitSymbol);
        }else {
            gen.writeNumber(result);
        }
    }

    private BigDecimal convertValue(BigDecimal bigValue,
                                    JKByteFormat.Unit type,
                                    JKByteFormat.Unit convert, int scale) {

        if (type.equals(convert)){
            return bigValue.setScale(scale, RoundingMode.HALF_UP);
        }

        if (type.equals(JKByteFormat.Unit.BYTE)){
            if (convert.equals(JKByteFormat.Unit.KB)){
                return bigValue.divide(BigDecimal.valueOf(1024), scale, RoundingMode.HALF_UP);
            }else if (convert.equals(JKByteFormat.Unit.MB)){
                return bigValue.divide(BigDecimal.valueOf(1024 * 1024), scale, RoundingMode.HALF_UP);
            }else if (convert.equals(JKByteFormat.Unit.GB)){
                return bigValue.divide(BigDecimal.valueOf(1024 * 1024 * 1024), scale, RoundingMode.HALF_UP);
            }
        }

        return null;
    }
}

3.2 @JKDecimalFormat 小数格式化

常常需求需要保留不同的小数位数,有些保留2位,有些3位, 甚至有些是能保留几位就保留几位因为经常四舍五入后存在精度丢失甚至丢成0了。所以扩展实现此注解动态保留小数精度

java 复制代码
/**
 *  小数精度转换
 */
@Target({ElementType.ANNOTATION_TYPE,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JsonSerialize(using = JKDecimalScaleSerializer.class)
@JacksonAnnotationsInside
public @interface JKDecimalFormat {

    /**
     * 保留精度
     */
    int scale() default 2;

    /**
     *  智能保留精度
     */
    boolean smartScale() default true;
}
java 复制代码
public class JKDecimalScaleSerializer extends BaseJKAnnotationSerializer<JKDecimalFormat,Number> {

    @Override
    public void serialize(Number number, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        if (number == null){
            return;
        }

        int scale = annotation.scale();
        boolean smartScale = annotation.smartScale();

        BigDecimal value = null;
        if (fieldClass == BigDecimal.class){
            value = (BigDecimal) number;
        }else if (fieldClass == Float.class) {
            value = BigDecimal.valueOf((Float) number);
        }else if (fieldClass == Double.class) {
            value = BigDecimal.valueOf((Double) number);
        }else {
            jsonGenerator.writeString(number.toString());
            return;
        }

        if (smartScale){
            String str = smartScale(value, scale);
            jsonGenerator.writeNumber(new BigDecimal(str));
        }else {
            jsonGenerator.writeNumber(value.setScale(scale, RoundingMode.HALF_UP));
        }
    }
}

3.3 @JKPercentFormat 百分数格式化

java 复制代码
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JsonSerialize(using = JKPercentFormatSerializer.class)
@JacksonAnnotationsInside
public @interface JKPercentFormat {

    /**
     * 保留小数位数
     */
    int scale() default 2;

    /**
     * 单位符号
     */
    boolean withSymbol() default false;
}
java 复制代码
public class JKPercentFormatSerializer extends BaseJKAnnotationSerializer<JKPercentFormat,Number> {

    @Override
    public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (value == null){
            return;
        }

        BigDecimal bigValue = null;
        if (value instanceof BigDecimal) {
            bigValue = (BigDecimal) value;
        } else if (value instanceof Float){
            bigValue = BigDecimal.valueOf((Float) value);
        }else if (value instanceof Double){
            bigValue = BigDecimal.valueOf((Double) value);
        }else if (value instanceof Long){
            bigValue = BigDecimal.valueOf((Long) value);
        }else if (value instanceof Integer){
            bigValue = BigDecimal.valueOf((Integer) value);
        }else {
            gen.writeString(value.toString());
            return;
        }

        int scale = this.annotation.scale();
        boolean withSymbol = this.annotation.withSymbol();
        bigValue = bigValue.multiply(new BigDecimal(100)).setScale(scale, RoundingMode.HALF_UP);

        String plainString = bigValue.toPlainString();
        if (withSymbol && StringUtils.hasText(plainString)) {
            gen.writeString(plainString + "%");
        }else {
            gen.writeNumber(bigValue);
        }
    }
}

3.4 枚举扩展

我们知道在Jackson中,默认对枚举的序列化和反序列化都是根据 java.lang.Enum#toString的返回值进行处理的, 而这个方法默认实现就是返回枚举常量的名字。 所以在接口中如果我们要用枚举类来接收请求参数需要使用枚举常量的名字去请求, 同理在接口的返回值中也是用枚举常量的名字去返回, 但是经常我们在不同的接口需要返回枚举常量的其他字段值, 所以我们需要扩展自定义枚举处理注解去帮我们抉择应该使用枚举常量的哪个字段去处理。

为了方便指定不同的枚举方式,创建一个枚举

java 复制代码
public enum JKEnumMode {
    /**
     *  根据名字
     */
    NAME,

    /**
     *  根据指定字段
     */
    FIELD,

    /**
     *  根据 toString方法返回值
     */
    TO_STRING,

    /**
     *  根据基础枚举接口的方式值
     */
    BASE_ENUM_KEY0, // 对应 BaseJKEnumKey接口的getEnumVey0方法返回值
    BASE_ENUM_KEY1   // 对应 BaseJKEnumKey接口的getEnumVey1方法返回值
}

基础枚举接口,当我们的枚举需要使用不同的字段去序列化时实现此接口即可

java 复制代码
public interface BaseJKEnumKey {

    Object getEnumVey0();

    default Object getEnumVey1(){
        return null;
    }
}

3.4.1 枚举序列化 @JKEnumSerializer

java 复制代码
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
@JsonSerialize(using = JKEnumFormatSerializer.class)
@JacksonAnnotationsInside
public @interface JKEnumSerializer {

    /**
     *  序列化方式
     */
    JKEnumMode mode() default JKEnumMode.NAME;

    /**
     *  当序列化方式为FIELD生效,指定使用的字段名的返回值进行序列化
     */
    String fieldName() default "";
}

具体枚举序列化逻辑,支持单一枚举对象和枚举列表的序列化

java 复制代码
public class JKEnumFormatSerializer extends BaseJKAnnotationSerializer<JKEnumSerializer,Object> {

    /**
     * 只对枚举类型或者枚举List使用此序列化器
     */
    @Override
    protected boolean isFilterSerializer(BeanProperty property) {
        if (Enum.class.isAssignableFrom(fieldClass)){
            return true;
        }

        if (List.class.isAssignableFrom(fieldClass)){
            //  获取泛型参数
            JavaType type = property.getType();
            Class<?> rawClass = type.getContentType().getRawClass();
            if (Enum.class.isAssignableFrom(rawClass)){
                return true;
            }else {
                return false;
            }

        }
        return false;
    }

    @Override
    public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (value == null){
            return;
        }

        if (value instanceof Enum){
            //单一枚举
            Object result = getEnumValue((Enum)value);
            gen.writeObject(result);
        }else if (List.class.isAssignableFrom(value.getClass())){
            // 枚举列表
            List<Enum> valueList = (List<Enum>) value;
            List<Object> result = new ArrayList<>();
            for (Enum tmp : valueList) {
                Object enumValue = getEnumValue(tmp);
                result.add(enumValue);
            }
            gen.writeObject(result);
        }
    }

    private Object getEnumValue(Enum value) {
        Object result = null;
        JKEnumMode mode = annotation.mode();
        if (mode.equals(JKEnumMode.FIELD)){
            String fieldName = annotation.fieldName();
            // 获取枚举类的字段值
            Field field = ReflectionUtils.findField(value.getClass(), fieldName);
            if (field == null) {
                throw new IllegalArgumentException(value.getClass().getSimpleName() + "枚举类中不存在该字段" + fieldName);
            }
            field.setAccessible(true);
            Object fieldValue = ReflectionUtils.getField(field, value);
            result = fieldValue != null ? fieldValue.toString() : null;
        }else if (mode.equals(JKEnumMode.TO_STRING)) {
            result = value.toString();
        } else if (mode.equals(JKEnumMode.BASE_ENUM_KEY0)) {
            result =  ((BaseJKEnumKey) value).getEnumVey0();
        } else if (mode.equals(JKEnumMode.BASE_ENUM_KEY1)) {
            result =  ((BaseJKEnumKey) value).getEnumVey1();
        }else if (mode.equals(JKEnumMode.NAME)) {
           result = value.name();
        }
        return result;
    }
}

3.4.2 枚举反序列化 @JKEnumDeserializer

java 复制代码
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
@JsonDeserialize(using = JKEnumFormatDeserializer.class)
@JacksonAnnotationsInside
public @interface JKEnumDeserializer {

    /**
     *  序列化方式
     */
    JKEnumMode mode() default JKEnumMode.NAME;

    /**
     *  当序列化方式为FIELD生效,指定使用的字段名的返回值进行序列化
     */
    String fieldName() default "";

    /**
     *  是否允许反序列为空
     */
    boolean nullable() default true;

}
java 复制代码
public class JKEnumFormatDeserializer extends BaseJKAnnotationDeSerializer<JKEnumDeserializer,Object> {

    private Class<Enum> listEnumClass;

    /**
     * 只有当字段类型为枚举类或者枚举List的时候才使用此反序列化器
     */
    @Override
    protected boolean isFilterDeserializer(BeanProperty property) {
        if (Enum.class.isAssignableFrom(fieldClass)){
            return true;
        }

        if (List.class.isAssignableFrom(fieldClass)){
            //  获取泛型参数
            JavaType type = property.getType();
            Class<?> rawClass = type.getContentType().getRawClass();
            if (Enum.class.isAssignableFrom(rawClass)){
                listEnumClass = (Class<Enum>)rawClass;
                return true;
            }else {
                return false;
            }

        }
        return false;
    }

    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        if (!StringUtils.hasText(p.getText())) {
            return null;
        }

        if (Enum.class.isAssignableFrom(fieldClass)){
            String enumValue = p.getText();
            Class<?> enumClass = this.fieldClass;
            Enum<?> result = getEnumValue(enumValue, enumClass);

            if (!this.annotation.nullable() && result == null){
                throw new IllegalArgumentException("从参数 【"+enumValue+"】无法反序列化成字段【" + fieldClass.getSimpleName()+" "+fieldName+"】,请重新传参");
            }
            return result;
        }

        if (List.class.isAssignableFrom(fieldClass)){
            TreeNode treeNode = p.getCodec().readTree(p);
            String s = treeNode.toString();
            List<String> strings = JSON.parseArray(s, String.class);
            List<Enum> valueList = new ArrayList<>();
            for (String element : strings) {
                valueList.add(getEnumValue(element,listEnumClass));
            }
            return valueList;
        }

        return null;
    }

    private Enum<?> getEnumValue(String enumValue, Class<?> enumClass) {
        JKEnumMode mode = this.annotation.mode();
        Enum<?> result = null;
        if (mode.equals(JKEnumMode.FIELD)){
            result = getEnumForCustomField(enumValue, enumClass);
        } else if (mode.equals(JKEnumMode.TO_STRING)) {
            result = getEnumForToString(enumValue, enumClass);
        } else if (mode.equals(JKEnumMode.BASE_ENUM_KEY0)){
            result = getEnumForBasEnumKey(enumValue, enumClass, e -> e.getEnumVey0() == null ? null : e.getEnumVey0().toString());
        } else if (mode.equals(JKEnumMode.BASE_ENUM_KEY1)){
            result = getEnumForBasEnumKey(enumValue, enumClass, e -> e.getEnumVey1() == null ? null : e.getEnumVey1().toString());
        }else {
            result =  getEnum(enumValue,enumClass);
        }
        return result;
    }

    private Enum<?> getEnumForBasEnumKey(String enumValue, Class<?> enumClass, Function<BaseJKEnumKey,String> getEnum) {
        if (!BaseJKEnumKey.class.isAssignableFrom(enumClass)){
            throw new IllegalArgumentException("对于枚举类"+ enumClass.getSimpleName()+"当指定为根据自定义枚举key序列时,请实现BaseJKEnumKey接口");
        }
        return findEnumConstant(enumValue,enumClass, e -> getEnum.apply(((BaseJKEnumKey)e)));
    }

    private Enum<?> getEnumForToString(String enumValue, Class<?> enumClass) {
       return findEnumConstant(enumValue,enumClass, Enum::toString);
    }

    public Enum<?> findEnumConstant(String enumValue, Class<?> enumClass, Function<Enum<?>,String> getEnum){
        Enum<?> result = null;
        Enum<?>[] enumConstants = (Enum<?>[]) enumClass.getEnumConstants();
        for (Enum<?> tmp : enumConstants) {
            String value = getEnum.apply(tmp);
            if (enumValue.equals(value)) {
                result = tmp;
                break;
            }
        }
        return result;
    }

    private Enum<?> getEnumForCustomField(String enumValue, Class<?> enumClass) {
        String fieldName = annotation.fieldName();
        // 获取枚举类的字段值
        Field field = ReflectionUtils.findField(enumClass, fieldName);
        if (field == null) {
            throw new IllegalArgumentException(enumClass.getSimpleName() + "枚举类中不存在该字段" + fieldName);
        }
        field.setAccessible(true);
        return findEnumConstant(enumValue,enumClass,e -> {
            Object filedValue = ReflectionUtils.getField(field, e);
            return filedValue == null ? null : filedValue.toString();
        });
    }

    public  Enum getEnum(String enumValue, Class<?> enumClass) {
        return findEnumConstant(enumValue,enumClass, Enum::name);
    }
}

4、测试

新建一个DTO标记上我们的扩展注解即可,然后进行序列化和反序列化的测试

java 复制代码
@Data
public class TestDTO {

    @JKDecimalFormat(scale = 6)
    private BigDecimal value0 = new BigDecimal(1024000.3333333);

    @JKByteFormat(convert = JKByteFormat.Unit.MB,unit = "(MB)", scale = 4)
    private Object value02 = new Double(3900);

    @JKByteFormat(convert = JKByteFormat.Unit.MB, scale = 4)
    private Long value021 = new Long(49000);

    @JKPercentFormat
    private BigDecimal value3 = new BigDecimal(0.0012);

    @JKEnumSerializer(mode = JKEnumMode.FIELD,fieldName = "key")
    private AnalysisEnum value4 = AnalysisEnum.SUCCESS;

    @JKEnumSerializer(mode = JKEnumMode.BASE_ENUM_KEY1)
    private AnalysisEnum value5 = AnalysisEnum.SUCCESS;

    @JKEnumSerializer(mode = JKEnumMode.BASE_ENUM_KEY1)
    private List<AnalysisEnum> value6 = Collections.singletonList(AnalysisEnum.FAIL);

    /**
     *  反序列化
     */
    @JKEnumDeserializer(mode = JKEnumMode.FIELD,fieldName = "key")
    private List<AnalysisEnum> value7 = Collections.singletonList(AnalysisEnum.SUCCESS);

    @JKEnumDeserializer(mode = JKEnumMode.BASE_ENUM_KEY1,nullable = false)
    private AnalysisEnum value8;

    @JKEnumDeserializer
    private AnalysisEnum value9;

}
java 复制代码
    @Test
    public void test1() throws JsonProcessingException, InterruptedException {
        ObjectMapper objectMapper = new ObjectMapper();

		// 序列化
        String s = objectMapper.writeValueAsString(new TestDTO());
        System.out.println(s);

        // 反序列化
        String json = "{\"value7\":[\"fl\",\"sc\"],\"value8\":\"成功\",\"value9\":\"FAIL\"}";
        TestDTO testDTO = objectMapper.readValue(json, TestDTO.class);

        System.out.println();
    }

序列化结果:

json 复制代码
{
    "value0": 1024000.333333,
    "value02": "0.0037(MB)",
    "value021": 0.0467,
    "value3": 0.12,
    "value4": "sc",
    "value5": "成功",
    "value6": [
        "失败"
    ],
    "value7": [
        "SUCCESS"
    ],
    "value8": null,
    "value9": null
}
相关推荐
AAA修煤气灶刘哥21 分钟前
ES 聚合爽到飞起!从分桶到 Java 实操,再也不用翻烂文档
后端·elasticsearch·面试
爱读源码的大都督32 分钟前
Java已死?别慌,看我如何用Java手写一个Qwen Code Agent,拯救Java
java·人工智能·后端
勇往直前plus1 小时前
Sentinel微服务保护
java·spring boot·微服务·sentinel
星辰大海的精灵1 小时前
SpringBoot与Quartz整合,实现订单自动取消功能
java·后端·算法
天天摸鱼的java工程师1 小时前
RestTemplate 如何优化连接池?—— 八年 Java 开发的踩坑与优化指南
java·后端
一乐小哥1 小时前
一口气同步10年豆瓣记录———豆瓣书影音同步 Notion分享 🚀
后端·python
LSTM971 小时前
如何使用C#实现Excel和CSV互转:基于Spire.XLS for .NET的专业指南
后端
三十_1 小时前
【NestJS】构建可复用的数据存储模块 - 动态模块
前端·后端·nestjs
武子康1 小时前
大数据-91 Spark广播变量:高效共享只读数据的最佳实践 RDD+Scala编程
大数据·后端·spark
努力的小郑1 小时前
MySQL索引(二):覆盖索引、最左前缀原则与索引下推详解
后端·mysql