动态选择从JSON创建对象的类

5
我有一个有趣的问题,但我在解决方案上遇到了困难。我的应用程序读取包含 json 对象的集合,并根据 json 中的字段将其反序列化为此或那个类类型。我无法控制 json 结构或其到达应用程序的方式。
我已经为应用程序可能收到的每种对象创建了模型,现在我正在尝试构建一个服务来提取“类型”字段,然后使用 ObjectMapper 将 json 反序列化为适当的 Model。
JSON 示例:
{
    "message_type" : "model1"
    "other data" : "other value"
    ...
}

模型:

public class Model1 {
    ...
}

public class Model2 {
    ...
}

服务?:

public class DynamicMappingService {

    public ???? mapJsonToObject(String json) {
        String type = pullTypeFromJson();

        ???
    }

    private String pullTypeFromJson() {...}
}

我不想要一个庞大的switch语句,它会说“如果类型值是这个,那么将其反序列化为那个”,但我很难想出一些干净的方法来处理这个问题。我想过可能会有一个通用的模型类,其中泛型参数是模型类型,唯一的字段是该模型类型的实例,但那似乎也不太对,并且我不确定它能给我带来什么好处。我还可以拥有某种空的抽象类,所有模型都继承自它,但那看起来也很可怕。我该怎么处理这个问题?附加分数,提供一个示例。

将标题更改为更好地反映我正在尝试解决的问题。 - b15
3
由于Model1Model2没有共同的父类(除了Object之外)也没有共同的接口,因此您的mapJsonToObject函数除了返回Object类型之外,无法获得适当的返回类型。 您想对返回的对象做什么? 由于您不知道它是什么类型,因此无法对某些超类型执行常规逻辑。 - roookeee
@roookeee 我需要进行一些处理,并将其重新序列化为稍微不同的模型。 - b15
2
但是你必须重新将 Model1Model2 序列化为其他类型,对吗?正如我所解释的那样-它们没有共同的超类型,因此只能使用类似于访问者模式的东西来实现。如果您有兴趣,我可以将一个示例添加为答案。 - roookeee
1
你需要知道你要映射到什么,否则你需要使用一个Map。据我所知,任何其他做这种事情的库/框架都需要你提供一个正确的类型,否则序列化/反序列化将失败。 - x80486
@roookeee 是的,每个模型都有自己的'输出'模型,需要重新序列化。我对访问者模式如何解决这个问题很感兴趣。 - b15
4个回答

3

我使用一个父接口Vehicle概念,其中包含两个类CarTruck。 在你的情况下,这意味着Model1Model2应该实现一个公共接口。

我的测试类:

import com.fasterxml.jackson.databind.ObjectMapper;

public class Tester {
    static ObjectMapper mapper=new ObjectMapper();

    public static void main(String[] args) throws IOException {
        Car car = new Car();
        car.setModel("sedan");
        String jsonCar=mapper.writeValueAsString(car);
        System.out.println(jsonCar);
        Vehicle c=mapper.readValue(jsonCar, Vehicle.class);
        System.out.println("Vehicle of type: "+c.getClass().getName());
        
        Truck truck=new Truck();
        truck.setPower(100);
        String jsonTruck=mapper.writeValueAsString(truck);
        System.out.println(jsonTruck);
        Vehicle t=mapper.readValue(jsonTruck, Vehicle.class);
        System.out.println("Vehicle of type: "+t.getClass().getName());
    }
}

你需要在某个地方存储一个类型字段值和相应类之间的映射关系。具体实现取决于你想要存放的位置。

1)父类型保存子类型列表:


import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonSubTypes({
    @JsonSubTypes.Type(value = Car.class, name = "car"),
    @JsonSubTypes.Type(value = Truck.class, name = "truck") }
)
@JsonTypeInfo(
          use = JsonTypeInfo.Id.NAME, 
          include = JsonTypeInfo.As.PROPERTY, 
          property = "type")
public interface Vehicle {
}

CarTruck的模型是简单的POJO,没有任何注释:

public class Car implements Vehicle {
    private String model;

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }
}

2) 单独的解析器保存映射关系:

Vehicle 包含额外的注释 @JsonTypeIdResolver

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;

@JsonTypeInfo(
          use = JsonTypeInfo.Id.NAME, 
          include = JsonTypeInfo.As.PROPERTY, 
          property = "type")
@JsonTypeIdResolver(JsonResolver.class)
public interface Vehicle {
}

JsonResolver 类保存类型字段值和类之间的映射关系。
import java.util.HashMap;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import com.fasterxml.jackson.databind.DatabindContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase;

public class JsonResolver extends TypeIdResolverBase {

    private static Map<String,Class<?>> ID_TO_TYPE=new HashMap<>();
    static {
        ID_TO_TYPE.put("car",Car.class);
        ID_TO_TYPE.put("truck",Truck.class);
    }
    public JsonResolver() {
        super();
    }

    @Override
    public Id getMechanism() {
        return null;
    }

    @Override
    public String idFromValue(Object value) {
        return value.getClass().getSimpleName();
    }

    @Override
    public String idFromValueAndType(Object value, Class<?> arg1) {
        return idFromValue(value);
    }

    @Override
    public JavaType typeFromId(DatabindContext context, String id) {
        return context.getTypeFactory().constructType(ID_TO_TYPE.get(id));
    }
}

3)json中包含完整的类名:

如果您接受序列化的json包含完整的Java类名,则无需解析器,但需指定use = JsonTypeInfo.Id.CLASS

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;

@JsonTypeInfo(
          use = JsonTypeInfo.Id.CLASS, 
          include = JsonTypeInfo.As.PROPERTY, 
          property = "type")
public interface Vehicle {
}

解决方案3是最容易实现的,但我个人不喜欢在我的数据中使用完整的Java类名。如果您开始重构Java包,这可能会带来潜在风险。


这很不错!我之前不知道Jackson有这个功能。对我来说,解决方案1似乎是最好的,即使“type”是类名,但它并不是。我只需要在model.getClass().getName()上使用switch,并从那里将对象转换为特定的实现。 - b15
当JSON中的“type”没有相应的@JsonSubTypes.Type时,这将会发生什么? - b15
1
一个简单的测试告诉我它抛出了异常:Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id 'bike' into a subtype of [simple type, class be.test.json.Vehicle]: known type ids = [Vehicle, car, truck] at [Source: {"type":"bike","model":"electric"}; line: 1, column: 9] - Conffusion
1
@b15 还发现了 https://dev59.com/GV4c5IYBdhLWcg3wz9BG,这可能是定义默认实现的解决方案。这取决于您的业务逻辑,但也许最好只是捕获异常并在 catch 块中进行必要的操作。您收到了无法理解的数据,所以... - Conffusion

1
您可以在这里使用访问者模式:
class ScratchStackOverflowQuestion57102092 {
    interface Reserializer {
        void accept(Model1 model1);
        void accept(Model2 model2);
    }

    interface Reserializeable {
        void visit(Reserializer reserializer);
    }

    class Model1 implements Reserializeable {
        @Override
        public void visit(Reserializer reserializer) {
            reserializer.accept(this);
        }
    }

    class Model2 implements Reserializeable {
        @Override
        public void visit(Reserializer reserializer) {
            reserializer.accept(this);
        }
    }

    public class ReserializerImpl implements Reserializer {

        @Override
        public void accept(Model1 model1) {
            //TODO: reserialize and push the new object somewhere
        }

        @Override
        public void accept(Model2 model2) {
            //TODO: reserialize and push the new object somewhere
        }
    }

    public class JsonConversion {
        //TODO: instantiate etc
        private Reserializer reserializer;

        public void handleJson(String json) {
            //TODO: use some framework like Jackson which can read type hints from JSON fields
            Reserializeable serialized = mapFromJson(json);
            serialized.visit(reserializer);
        }

    }
}

这是一个简化的示例,展示如何实现你想要的功能,但它目前缺少以下特性:
  • 它不返回任何内容,因为你必须拥有另一个访问者模式并为每个重新序列化的对象实现一个接收器
  • 你仍然需要实现/找到一个库,从接收到的json中读取类型提示(如代码注释所述,jackson可以做到这一点)
因此,你可能需要稍微调整给定的代码 :)
编辑:由于广大需求,提供了完整的实现,通过另一个访问者启用动态处理重新序列化的对象(仅缺少在JSON中考虑类型提示的Jackson使用)。对于近十几个类,我感到抱歉,没有更短的方式。请参见函数exampleUsage(),了解如何使用此方法/如何为不同的重新转换对象定义处理程序:
class ScratchStackOverflowQuestion57102092_V2 {
//////////////////////////////// INPUTS //////////////////////////////
    interface Reserializer {
        void accept(Model1 model1);
        void accept(Model2 model2);
    }

    interface Reserializeable {
        void visit(Reserializer reserializer);
    }


    class Model1 implements Reserializeable {
        @Override
        public void visit(Reserializer reserializer) {
            reserializer.accept(this);
        }
    }

    class Model2 implements Reserializeable {
        @Override
        public void visit(Reserializer reserializer) {
            reserializer.accept(this);
        }
    }

//////////////////////////////// RECONVERSION /////////////////////////

    interface ReconvertedVisitor {
        void accept(ReconvertedModel1 reconverted);
        void accept(ReconvertedModel2 reconverted);
    }

    interface ReconvertedModel {
        void visit(ReconvertedVisitor visitor);
    }

    //Some dummy object as an example
    class ReconvertedModel1 implements ReconvertedModel{

        @Override
        public void visit(ReconvertedVisitor visitor) {
            visitor.accept(this);
        }
    }

    //Some dummy object as an example
    class ReconvertedModel2 implements ReconvertedModel{
        @Override
        public void visit(ReconvertedVisitor visitor) {
            visitor.accept(this);
        }
    }

////////////////////////////// IMPLEMENTATIONS ///////////////////////////////
    public class ReserializerImpl implements Reserializer {

        private final ReconvertedVisitor visitor;

        public ReserializerImpl(ReconvertedVisitor visitor) {
            this.visitor = visitor;
        }

        @Override
        public void accept(Model1 model1) {
            //TODO: do some real conversion
            ReconvertedModel1 reserializeResult = new ReconvertedModel1();
        }

        @Override
        public void accept(Model2 model2) {
            //TODO: do some real conversion
            ReconvertedModel2 reserializeResult = new ReconvertedModel2();
        }
    }

    public class JsonConversion {

        public void handleJson(String json, ReconvertedVisitor handler) {
            //TODO: use some framework like Jackson which can read type hints from JSON fields
            Reserializeable serialized = mapFromJson(json);
            ReserializerImpl reserializer = new ReserializerImpl(handler);
            serialized.visit(reserializer);
        }
    }

    public void exampleUsage() {
        //Just some sample, you could delegate to different objects in each accept
        class PrintingReconvertedVisitor implements ReconvertedVisitor {

            @Override
            public void accept(ReconvertedModel1 reconverted) {
                System.out.println(reconverted);
            }

            @Override
            public void accept(ReconvertedModel2 reconverted) {
                System.out.println(reconverted);
            }
        }
        JsonConversion conversion = new JsonConversion();
        conversion.handleJson("TODO: SOME REAL JSON HERE", new PrintingReconvertedVisitor());
    }
}

我对类的命名不太满意,也许可以将Reserializer重新命名为ModelVisitor或其他适当的名称。

客户端代码是什么意思?我不太明白@daniu的意思。 - roookeee
你会如何实际调用反序列化 JSON 的操作呢?这是 OP 的问题。 - daniu
你只需要调用 JsonConversion.handleJson,它会自动调用 ReseriliazerImpl 中的“正确”反序列化器。正如我在回答中所解释的那样:目前它没有返回类型,并且还必须使用访问者模式来处理不同的接收器/业务逻辑。如果您愿意,我可以扩展我的答案以进行澄清 :) - roookeee
请问,您是否会为每种类型创建一个模型对象并调用visit来处理它们? - daniu
@daniu,给你。正如OP所解释的,每个输入模型都有一个独特的重新转换对象(我在我的帖子中坚持了他的术语)。 - roookeee
嗯...看起来我不得不想一下,使用switch语句是不是在这种情况下较小的邪恶。 - b15

1
你有两个不同的问题:创建一个未知类型的对象,然后进行有意义的操作。如果你创建的类型确实取决于json中节点的值,那么你将无法避免从该字符串创建类的映射。一个巨大的if-then-else块将在两到三个类之外没有用处。

创建

你可以创建一个注册表单例来维护这个映射。

public enum ClassByNodeMapping {
    INSTANCE;
    final Map<String, Class<?>> mapping = new HashMap<>();

    public void addMapping(String nodeValue, Class<?> clazz) {
        mapping.put(nodeValue, clazz);
    }
    public Class<?> getMapping(String nodeValue) {
        return mapping.get(nodeValue);
    }
}

您可以按班级填写此表格:
@Data
class Model1 {
    static {
        ClassByNodeMapping.INSTANCE.addMapping("model1", Model1.class);
    }
    private String model1Value;
}

但即便如此,在注册之前,您仍需要实例化Model1(以确保类已被加载)。因此,您的客户端代码将如下所示:

class Scratch {
    static {
        // need to instantiate the models once to trigger registration
        Model1 model = new Model1();
        Model2 model2 = new Model2();
    }
    private static final ObjectMapper mapper = new ObjectMapper()
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

    public static void main(String[] args) throws IOException {
        String json1 = "{\"type\": \"model1\", \"model1Value\": \"m1\"}";
        String json2 = "{\"type\": \"model2\", \"model2Value\": \"m2\"}";

        System.out.println(deserialize(json1));
        System.out.println(deserialize(json2));
    }

    private static Object deserialize(String json) throws IOException {
        JsonNode jsonNode = mapper.readTree(json);
        Class<?> type = ClassByNodeMapping.INSTANCE.getMapping(jsonNode.get("type").textValue());
        return mapper.readValue(json, type);
    }
}

处理

现在你已经创建了对象,但由于它们是Object类型,所以你不能对它们做太多事情。在你的问题评论中,你说了类似于“重新序列化”的话,我不确定那是什么意思;这里的一般概念是消费 - 对象被创建,然后进行某些操作。你可以在此处使用其他答案中的访问者模式,但我觉得有点混乱。

你可以通过包含处理程序来扩展映射。通常,你会想要将两者分开,但在这种情况下,可能需要保留类型安全性。

enum ClassByNodeMapping {
    INSTANCE;
    final Map<String, Class<?>> mapping = new HashMap<>();
    final Map<Class<?>, Consumer<?>> handlerMapping = new HashMap<>();

    public <T>void addMapping(String nodeValue, Class<T> clazz, Consumer<T> handler) {
        mapping.put(nodeValue, clazz);
        handlerMapping.put(clazz, handler);
    }
    public Class<?> getMapping(String nodeValue) {
        return mapping.get(nodeValue);
    }
    public Consumer<Object> getHandler(Class<?> clazz) {
        return (Consumer<Object>) handlerMapping.get(clazz);
    }
}

这会使你的注册码长成这个样子。
@Data
class Model1 {
    static {
        // you'd probably not want to do the registration here,
        // assuming your handler code is outside
        ClassByNodeMapping.INSTANCE.addMapping("model1", Model1.class, Model1::handle);
    }
    private String model1Value;

    private static void handle(Model1 m1) {
        System.out.printf("handling as model1: %s%n", m1);
    }
}

你的(测试)客户端代码

public static void main(String[] args) throws IOException {
    String json1 = "{\"type\": \"model1\", \"model1Value\": \"m1\"}";
    String json2 = "{\"type\": \"model2\", \"model2Value\": \"m2\"}";

    handle(json1);
    handle(json2);
}

private static void handle(String json) throws IOException {
    JsonNode jsonNode = mapper.readTree(json);
    Class<?> type = ClassByNodeMapping.INSTANCE.getMapping(jsonNode.get("type").textValue());
    Consumer<Object> handler = ClassByNodeMapping.INSTANCE.getHandler(type);
    Object o = mapper.readValue(json, type);
    handler.accept(o);
}

注意事项

我认为整个概念都是代码异味。我一定会努力让传入通道只包含一种数据类型——无论是单独的REST端点、Kafka主题还是其他什么。


是的,我们正在与第三方合作,他们会将不同类型的事件一次性打包发送给我们,所以我无法做任何处理。 - b15
@b15,你可以创建一个仅分析类型字段并将对比的 JSON 部分转发到适当端点的分销商。 - daniu

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接