如何在不使用公共setter的情况下将JSON反序列化到私有字段?
针对你遇到的「不想用公共Setter,又要避免违反开闭原则(OCP)、里氏替换原则(LSP)」的问题,以下是几种可行方案:
一、Jackson 直接操作私有字段(最简方案)
Jackson本身支持通过反射访问私有字段,完全不需要公共Setter,两种实现方式:
1. 给私有字段加@JsonProperty注解
修改Food类,直接在私有字段上标记注解,指定JSON映射关系,保留必要的Getter即可:
public class Food { @JsonProperty private String name; @JsonProperty private double calories; @JsonProperty("serving_size_g") // 直接对应JSON字段名,替代@JsonAlias private double servingSize; // 仅保留业务需要的Getter,无任何Setter public String getName() { return name; } public double getCalories() { return calories; } public double getServingSize() { return servingSize; } }
原有的getFood方法不需要修改,ObjectMapper会自动通过反射将JSON值注入私有字段。
2. 全局配置ObjectMapper允许访问私有字段
如果不想给每个字段加注解,可全局开启私有字段访问权限:
ObjectMapper objectMapper = new ObjectMapper(); // 允许Jackson直接访问所有字段(包括私有) objectMapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
此时字段名默认和JSON键名匹配,需要别名的字段仍可单独加@JsonProperty("serving_size_g")。
二、Gson 反射映射(告别自定义TypeAdapter)
Gson默认支持反射访问私有字段,无需写那种带switch的TypeAdapter,只需用@SerializedName指定字段映射:
修改Food类:
public class Food { @SerializedName("name") private String name; @SerializedName("calories") private double calories; @SerializedName("serving_size_g") private double servingSize; // 仅保留Getter public String getName() { return name; } public double getCalories() { return calories; } public double getServingSize() { return servingSize; } }
反序列化代码直接用Gson即可:
private Food getFood(String foodName) throws IOException { String nutritionJsonString = apiConnector.getNutritionAsStringFromAPI(foodName); return new Gson().fromJson(nutritionJsonString, Food.class); }
Gson会自动通过反射给私有字段赋值,完全不需要Setter,也不会出现违反OCP的switch逻辑。
三、Builder模式 + 序列化器(灵活扩展,完全符合OCP)
如果需要更强的扩展性,同时避免反射,Builder模式是最优选择——新增字段只需修改Builder,无需改动反序列化核心逻辑:
1. 实现Food的Builder模式
public class Food { // 私有不可变字段,保证线程安全和封装性 private final String name; private final double calories; private final double servingSize; // 私有构造方法,只能通过Builder创建对象 private Food(Builder builder) { this.name = builder.name; this.calories = builder.calories; this.servingSize = builder.servingSize; } // 仅保留Getter public String getName() { return name; } public double getCalories() { return calories; } public double getServingSize() { return servingSize; } // Builder内部类 public static class Builder { private String name; private double calories; private double servingSize; public Builder name(String name) { this.name = name; return this; } public Builder calories(double calories) { this.calories = calories; return this; } public Builder servingSize(double servingSize) { this.servingSize = servingSize; return this; } public Food build() { return new Food(this); } } }
2. Jackson适配Builder
Jackson支持直接反序列化到Builder,只需两步:
- 在
Food类上添加@JsonDeserialize(builder = Food.Builder.class)注解 - 在Builder的每个方法上添加
@JsonProperty对应JSON字段:
public static class Builder { private String name; private double calories; private double servingSize; @JsonProperty("name") public Builder name(String name) { this.name = name; return this; } @JsonProperty("calories") public Builder calories(double calories) { this.calories = calories; return this; } @JsonProperty("serving_size_g") public Builder servingSize(double servingSize) { this.servingSize = servingSize; return this; } public Food build() { return new Food(this); } }
反序列化代码无需修改,Jackson会自动调用Builder方法构建对象,新增字段时只需在Builder中添加对应方法和注解,完全符合OCP。
3. Gson适配Builder
Gson可通过自定义通用BuilderTypeAdapter实现,避免重复代码,同时完整实现序列化/反序列化(符合LSP):
// 通用Builder类型适配器 public class BuilderTypeAdapter<T, B> extends TypeAdapter<T> { private final Supplier<B> builderSupplier; private final Function<B, T> buildFunction; private final Map<String, BiConsumer<B, Object>> fieldSetters; public BuilderTypeAdapter(Supplier<B> builderSupplier, Function<B, T> buildFunction, Map<String, BiConsumer<B, Object>> fieldSetters) { this.builderSupplier = builderSupplier; this.buildFunction = buildFunction; this.fieldSetters = fieldSetters; } @Override public void write(JsonWriter out, T value) throws IOException { // 完整实现序列化逻辑,符合里氏替换原则 Food food = (Food) value; out.beginObject(); out.name("name").value(food.getName()); out.name("calories").value(food.getCalories()); out.name("serving_size_g").value(food.getServingSize()); out.endObject(); } @Override public T read(JsonReader in) throws IOException { B builder = builderSupplier.get(); in.beginObject(); while (in.hasNext()) { String fieldName = in.nextName(); BiConsumer<B, Object> setter = fieldSetters.get(fieldName); if (setter != null) { switch (in.peek()) { case STRING: setter.accept(builder, in.nextString()); break; case NUMBER: setter.accept(builder, in.nextDouble()); break; default: in.skipValue(); } } else { in.skipValue(); } } in.endObject(); return buildFunction.apply(builder); } }
然后给Food类添加@JsonAdapter注解:
@JsonAdapter(Food.FoodTypeAdapter.class) public class Food { // 原有Food和Builder代码不变 public static class FoodTypeAdapter extends BuilderTypeAdapter<Food, Builder> { public FoodTypeAdapter() { super(Builder::new, Builder::build, Map.of( "name", (builder, val) -> builder.name((String) val), "calories", (builder, val) -> builder.calories((Double) val), "serving_size_g", (builder, val) -> builder.servingSize((Double) val) )); } } }
新增字段时只需在Map.of中添加对应映射,无需修改read方法的核心逻辑,完美符合OCP;同时write方法完整实现序列化,不会违反LSP。
内容的提问来源于stack exchange,提问作者SaftwareEnginnering

