Jackson 自定义注解扩展实战

2023年 9月 12日 62.7k 0

1、简介

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

扩展注解如下:

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

2、扩展注解基类实现

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

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

/**
 *
 * @param                      自定义的序列化注解
 * @param              序列化的字段类型
 */
public abstract class BaseJKAnnotationSerializer extends JsonSerializer implements ContextualSerializer {

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

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

    /**
     *  动态决定序列化的方式,以及获取序列化的上下文,比如要序列化的字段的信息等等
     */
    @Override
    public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
        // 子类获取父亲标记的泛型参数
        Class 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 getFatherParamType() {
        Type superclass = getClass().getGenericSuperclass();
        Class acls = null;
        if (superclass instanceof ParameterizedType){
            ParameterizedType parameterizedType = (ParameterizedType) superclass;
            Type actualTypeArgument = parameterizedType.getActualTypeArguments()[0];
            acls =  (Class) 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  createContextual(DeserializationContext prov, BeanProperty property) throws JsonMappingException {
        // 子类获取父亲标记的泛型参数
        Type superclass = getClass().getGenericSuperclass();
        Class acls = null;
        if (superclass instanceof ParameterizedType){
            ParameterizedType parameterizedType = (ParameterizedType) superclass;
            Type actualTypeArgument = parameterizedType.getActualTypeArguments()[0];
            acls =  (Class) 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展示, 所以扩展此注解直接标记即可前端直接展示不需要做额外处理。

/**
 *  字节格式化
 */
@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
        ;
    }
}

public class JKByteFormatSerializer extends BaseJKAnnotationSerializer{

    @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了。所以扩展实现此注解动态保留小数精度

/**
 *  小数精度转换
 */
@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;
}
public class JKDecimalScaleSerializer extends BaseJKAnnotationSerializer {

    @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 百分数格式化

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

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

    /**
     * 单位符号
     */
    boolean withSymbol() default false;
}

public class JKPercentFormatSerializer extends BaseJKAnnotationSerializer {

    @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的返回值进行处理的, 而这个方法默认实现就是返回枚举常量的名字。 所以在接口中如果我们要用枚举类来接收请求参数需要使用枚举常量的名字去请求, 同理在接口的返回值中也是用枚举常量的名字去返回, 但是经常我们在不同的接口需要返回枚举常量的其他字段值, 所以我们需要扩展自定义枚举处理注解去帮我们抉择应该使用枚举常量的哪个字段去处理。

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

public enum JKEnumMode {
    /**
     *  根据名字
     */
    NAME,

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

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

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

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

public interface BaseJKEnumKey {

    Object getEnumVey0();

    default Object getEnumVey1(){
        return null;
    }
}

3.4.1 枚举序列化 @JKEnumSerializer

@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 "";
}

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

public class JKEnumFormatSerializer extends BaseJKAnnotationSerializer {

    /**
     * 只对枚举类型或者枚举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 valueList = (List) value;
            List 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

@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;

}
public class JKEnumFormatDeserializer extends BaseJKAnnotationDeSerializer {

    private Class 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)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 strings = JSON.parseArray(s, String.class);
            List 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 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[]) 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标记上我们的扩展注解即可,然后进行序列化和反序列化的测试

@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 value6 = Collections.singletonList(AnalysisEnum.FAIL);

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

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

    @JKEnumDeserializer
    private AnalysisEnum value9;

}

@Getter
public enum AnalysisEnum implements BaseJKEnumKey {

    SUCCESS("sc","成功"),

    FAIL("fl","失败");

    AnalysisEnum(String key, String desc) {
        this.key = key;
        this.desc = desc;
    }


    private String key;
    private String desc;

    @Override
    public String toString() {
        return "[" + this.key + "," + this.desc + "]";
    }

    @Override
    public Object getEnumVey0() {
        return key;
    }

    @Override
    public Object getEnumVey1() {
        return desc;
    }


}
    @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();
    }

序列化结果:

{
    "value0": 1024000.333333,
    "value02": "0.0037(MB)",
    "value021": 0.0467,
    "value3": 0.12,
    "value4": "sc",
    "value5": "成功",
    "value6": [
        "失败"
    ],
    "value7": [
        "SUCCESS"
    ],
    "value8": null,
    "value9": null
}

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论