SpringMVC序列化技巧:通过自定义注解实现字段值自动转换

在现代的Web开发中,SpringMVC作为Spring框架的核心模块之一,广泛应用于构建高效、灵活的后端接口服务。随着业务需求的日益复杂,开发者常常需要在接口返回时对数据进行定制化的处理,例如格式化日期、脱敏敏感信息、动态调整字段值等。然而,传统的处理方式往往需要在业务逻辑中手动干预,这不仅增加了代码的复杂性,也降低了开发效率。

幸运的是,Jackson作为SpringMVC默认的JSON处理库,提供了强大的扩展机制,允许开发者通过自定义注解和序列化器来实现字段值的自动转换。通过这种方式,我们可以在不改变业务逻辑的前提下,灵活地控制接口返回的数据格式,从而实现更高效、更优雅的开发模式。

本文将深入探讨如何在SpringMVC中通过自定义注解和Jackson的序列化机制,实现字段值的自动转换。我们将从需求背景出发,逐步介绍自定义注解的设计思路、实现方法以及如何将其与Jackson的序列化流程无缝集成。通过实际案例,我们将展示如何利用这一技术解决常见的开发痛点,提升代码的可维护性和可扩展性。

无论你是希望优化现有接口的开发者,还是对SpringMVC和Jackson扩展机制感兴趣的架构师,本文都将为你提供实用的指导和启发。让我们一起探索SpringMVC与Jackson的强大功能,解锁接口开发的新境界。

一、需求背景

在实际开发中,我们常常遇到以下场景:

  1. 数据脱敏:某些字段(如手机号、身份证号)需要在返回时进行脱敏处理。
  2. 格式化输出:日期字段需要以特定格式返回,而默认的序列化方式可能不符合需求。
  3. 动态字段处理:根据业务逻辑动态调整字段值,例如根据用户权限显示不同的数据。
  4. 字段值转换:根据业务关键字段自动填充相关业务字段,例如根据用户id填充用户名称。

传统的解决方式是通过在业务逻辑中手动处理这些字段,但这会导致代码冗余且难以维护。通过自定义注解和Jackson的序列化机制,我们可以将这些逻辑封装到注解中,实现字段值的自动转换。

二、自定义注解的设计

为了实现字段值的自动转换,我们需要定义一个自定义注解,并通过Jackson的序列化机制将其应用到字段上。以下是实现步骤:

1. 定义自定义注解

首先,定义一个自定义注解,用于标记需要转换的字段。例如,我们定义一个 @Transfer 注解,用于指定字段的转换逻辑:

java 复制代码
package com.zwb.blog.common.anno;

import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
/**
 * JacksonAnnotationsInside:Jackson 库中的一个元注解,主要用于创建自定义组合注解。它的作用是指示 Jackson 在处理被该注解标记的自定义注解时,应该进一步解析自定义注解内部的其他注解
 *
 * 加上该注解后,在使用Transfer注解时,JsonSerialize注解也会生效,否则JsonSerialize注解不生效,如果不想用这个注解,也可以直接在字段上使用JsonSerialize注解
 */
@JacksonAnnotationsInside
/**
 * JsonSerialize:自定义对象的序列化行为
 * 指定自定义序列化逻辑实现类
 */
@JsonSerialize(using = TransferJsonSerialize.class)
public @interface Transfer {

    /**
     * 转换类型
     * 该参数决定具体使用的转换逻辑
     */
    TransferStrategyEnum transferStrategy();

    /**
     * 是否覆盖源字段
     * 值为true时,转换值将覆盖源字段值
     * 值为false时,源字段值不变,转换值将设置到目标转换字段中(目标转换字段不能为空,否则转换失败)
     */
    boolean overrideSelf() default true;

    /**
     * 目标转换字段名
     */
    String targetFieldName() default "";

}

2. 实现策略模式

策略模式允许我们根据不同的策略动态处理字段值。

java 复制代码
package com.zwb.blog.common.anno;

import org.springframework.util.CollectionUtils;
import java.util.HashMap;
import java.util.Map;

public class TransferContext {

    private static final Map<TransferStrategyEnum, TransferHandler> TRANSFER_HANDLER_MAP = new HashMap<>();

    public static void register(TransferHandler transferHandler) {
        TRANSFER_HANDLER_MAP.put(transferHandler.getTransferStrategyEnum(), transferHandler);
    }

    /**
     * 数据转换逻辑
     */
    public static Object transfer(TransferStrategyEnum transferStrategyEnum, Object object) {
        if (object == null || CollectionUtils.isEmpty(TRANSFER_HANDLER_MAP)) {
            return null;
        }

        TransferHandler transferHandler = TRANSFER_HANDLER_MAP.get(transferStrategyEnum);

        if (transferHandler == null) {
            return null;
        }
        return transferHandler.getData(object);
    }
}
java 复制代码
package com.zwb.blog.common.anno;

import javax.annotation.PostConstruct;

public interface TransferHandler {

    @PostConstruct
    default void init() {
        /**
         * 注册策略对象
         */
        TransferContext.register(this);
    }

    /**
     * 根据转换字段对象获取转换值
     *
     * @param object 转换字段对象
     * @return 转换对象值
     */
    Object getData(Object object);

    /**
     * 当前Handler的转换类型
     *
     * @return
     */
    TransferStrategyEnum getTransferStrategyEnum();
}
java 复制代码
package com.zwb.blog.common.anno;

import org.springframework.stereotype.Component;

@Component
public class AdminUsernameTransferHandler implements TransferHandler {

    @Override
    public Object getData(Object object) {
        //执行转换逻辑......
        return "123123";
    }

    @Override
    public TransferStrategyEnum getTransferStrategyEnum() {
        return TransferStrategyEnum.ADMIN_USERNAME;
    }


}

3. 实现自定义序列化器

接下来,实现一个自定义序列化器,通过策略模式动态处理标记了 @Transfer 注解的字段。

java 复制代码
package com.zwb.blog.common.anno;

import cn.hutool.core.util.ReflectUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.lang.reflect.Field;

/**
 * JsonSerializer:是 Jackson 库中的一个核心抽象类,用于自定义对象的序列化逻辑。
 * 它允许开发者实现自己的序列化规则,从而控制对象在被序列化为 JSON 数据时的具体表现形式。
 * 简单来说,JsonSerializer 是一个接口,通过实现这个接口,可以定义如何将 Java 对象转换为 JSON 格式。
 */
@Slf4j
@NoArgsConstructor
@AllArgsConstructor
public class TransferJsonSerialize extends JsonSerializer<Object> implements ContextualSerializer {

    private boolean overrideSelf;

    private String targetFieldName;

    private TransferStrategyEnum transferStrategyEnum;

    /**
     * 自定义序列化逻辑
     *
     * @param object             这是需要序列化的对象。是你自定义的对象类型,它可能是一个简单的类或者是一个复杂的嵌套对象。(这里代表使用了该注解的字段实体对象)
     * @param jsonGenerator      这是Jackson提供的用于生成JSON内容的生成器。通过它,你可以手动构建JSON字段和值。
     * @param serializerProvider 提供了序列化时需要用到的上下文信息,比如其他序列化器,配置选项等。
     * @throws IOException
     */
    @Override
    public void serialize(Object object, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        //填充源字段值
        jsonGenerator.writeObject(object);
        //执行转换逻辑,获取转换值
        Object transfer = TransferContext.transfer(transferStrategyEnum, object);
        if (transfer == null) {
            log.error("transfer value is null");
            return;
        }

        if (this.overrideSelf) {
            //填充当前字段
            jsonGenerator.writeObject(transfer);
            return;
        }

        //获取映射字段对象
        Field field = ReflectUtil.getField(jsonGenerator.getCurrentValue().getClass(), targetFieldName);
        //通过反射将转换至映射到对应字段中
        ReflectUtil.setFieldValue(jsonGenerator.getCurrentValue(), field, transfer);
    }

    /**
     * 该方法用于确定是否需要一个不同的(或不同配置的)序列化器来序列化指定属性的值。
     * 请注意,通常调用该方法的实例是共享的,因此该方法<b>不应</b>修改此实例,而应构造并返回一个新实例。
     * 只有当此实例已经适合使用时,才应原样返回此实例。
     *
     * @param prov     序列化器提供者,用于访问配置和其他序列化器
     * @param property 代表属性的方法或字段(用于访问要序列化的值)。
     *                 通常应该是可用的;但在某些情况下,调用者可能无法提供它,并传递 null
     *                 (在这种情况下,通常实现会将 'this' 序列化器按原样传递)
     * @return 用于序列化指定属性值的序列化器;
     * 可以是此实例或新实例。
     * @throws JsonMappingException
     */
    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
        Transfer annotation = property.getAnnotation(Transfer.class);

        if (annotation != null) {
            /**
             * Jackson 在序列化过程中,会为同一个类型的属性复用相同的 JsonSerializer 实例。如果多个线程同时序列化相同类型的属性,它们可能会共享同一个序列化器实例。
             * 
             * 这意味着,即使这些字段是实例变量,而不是静态变量(线程共享变量),如果多个线程同时使用同一个 TransferJsonSerialize 实例,
             * 它们将会同时修改 overrideSelf、targetFieldName 和 transferStrategyEnum 的值,从而引发竞态条件。
             */
            //初始化序列化器上下文。将自定义注解Transfer中的参数初始化到序列化器的字段中,以供执行自定义序列化逻辑时使用
            //每次都返回一个新的序列化器,避免线程安全问题
            return new TransferJsonSerialize(annotation.overrideSelf(), annotation.targetFieldName(), annotation.transferStrategy());
        } else {
            //如果需要序列化的字段上没有该注解,则返回一个新的默认序列化器,该序列化器会替代当前序列化器对实体进行序列化
            return prov.findValueSerializer(property.getType(), property);
        }
    }
}

4. 使用自定义注解

现在,我们可以在实体类中使用 @Transfer 注解来标记需要转换的字段

java 复制代码
package com.zwb.blog.model.admin.vo;

import com.zwb.blog.common.anno.Transfer;
import com.zwb.blog.common.anno.TransferStrategyEnum;
import lombok.*;

import java.io.Serializable;

@Data
public class AdminUserVO implements Serializable {

    @Transfer(transferStrategy = TransferStrategyEnum.ADMIN_USERNAME,overrideSelf = false,targetFieldName = "userNameTransfer")
    private Integer id;

    private String userName;

    private String userNameTransfer;
    
}

三、测试效果

为了验证自定义注解的效果,可以编写一个简单的测试接口(从数据库查询出数据记录或整理一条测试数据,转换为AdminUserVO返回即可。这里不方便将测试代码放出来)。使用postman调用测试接口后,结果就是userNameTransfer的值会自动填充为"123123"

四、总结

通过自定义注解和Jackson的序列化机制,结合策略模式,我们可以在SpringMVC接口中实现字段值的自动转换,而无需在业务逻辑中手动处理。这种方式不仅提高了代码的可维护性和可扩展性,还使得字段处理逻辑更加清晰和集中。

希望本文的介绍能够帮助你在实际开发中更好地利用SpringMVC和Jackson的强大功能,提升开发效率。如果你对自定义注解或Jackson的扩展机制有更多疑问,欢迎在评论区留言,我会尽力解答。

相关推荐
Ai 编码助手5 小时前
在 Go 语言中如何高效地处理集合
开发语言·后端·golang
小丁爱养花5 小时前
Spring MVC:HTTP 请求的参数传递2.0
java·后端·spring
Channing Lewis5 小时前
什么是 Flask 的蓝图(Blueprint)
后端·python·flask
轩辕烨瑾7 小时前
C#语言的区块链
开发语言·后端·golang
栗豆包8 小时前
w175基于springboot的图书管理系统的设计与实现
java·spring boot·后端·spring·tomcat
萧若岚9 小时前
Elixir语言的Web开发
开发语言·后端·golang
Channing Lewis9 小时前
flask实现重启后需要重新输入用户名而避免浏览器使用之前已经记录的用户名
后端·python·flask
Channing Lewis9 小时前
如何在 Flask 中实现用户认证?
后端·python·flask
一只爱吃“兔子”的“胡萝卜”10 小时前
2.Spring-AOP
java·后端·spring
AI向前看10 小时前
PHP语言的软件工程
开发语言·后端·golang