在现代的Web开发中,SpringMVC作为Spring框架的核心模块之一,广泛应用于构建高效、灵活的后端接口服务。随着业务需求的日益复杂,开发者常常需要在接口返回时对数据进行定制化的处理,例如格式化日期、脱敏敏感信息、动态调整字段值等。然而,传统的处理方式往往需要在业务逻辑中手动干预,这不仅增加了代码的复杂性,也降低了开发效率。
幸运的是,Jackson作为SpringMVC默认的JSON处理库,提供了强大的扩展机制,允许开发者通过自定义注解和序列化器来实现字段值的自动转换。通过这种方式,我们可以在不改变业务逻辑的前提下,灵活地控制接口返回的数据格式,从而实现更高效、更优雅的开发模式。
本文将深入探讨如何在SpringMVC中通过自定义注解和Jackson的序列化机制,实现字段值的自动转换。我们将从需求背景出发,逐步介绍自定义注解的设计思路、实现方法以及如何将其与Jackson的序列化流程无缝集成。通过实际案例,我们将展示如何利用这一技术解决常见的开发痛点,提升代码的可维护性和可扩展性。
无论你是希望优化现有接口的开发者,还是对SpringMVC和Jackson扩展机制感兴趣的架构师,本文都将为你提供实用的指导和启发。让我们一起探索SpringMVC与Jackson的强大功能,解锁接口开发的新境界。
一、需求背景
在实际开发中,我们常常遇到以下场景:
- 数据脱敏:某些字段(如手机号、身份证号)需要在返回时进行脱敏处理。
- 格式化输出:日期字段需要以特定格式返回,而默认的序列化方式可能不符合需求。
- 动态字段处理:根据业务逻辑动态调整字段值,例如根据用户权限显示不同的数据。
- 字段值转换:根据业务关键字段自动填充相关业务字段,例如根据用户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的扩展机制有更多疑问,欢迎在评论区留言,我会尽力解答。