动态选择要从json创建对象的类

时间:2019-07-18 20:20:06

标签: java json design-patterns

我有一个有趣的问题,我在想出一个干净的解决方案时遇到麻烦。我的应用程序根据json本身的字段读取需要反序列化为该或该类类型的json对象的集合。我无法控制json结构或它如何到达我的应用程序。

我已经为可能要进入应用程序的每种类型的对象创建了模型,并且达到了要构建一个提取“类型”字段然后使用ObjectMapper反序列化的服务的地步。 json转换为适当的模型。

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语句显示“如果类型值是这个,然后反序列化为那个”,但是我正在努力提出一些干净的方法。我认为也许是通用模型类,其中通用参数是Model Type,唯一字段是该模型类型的实例,但这似乎也不对,我不确定买什么。我还可以拥有所有模型都可以扩展的某种空抽象类,但这似乎也很可怕。我该如何处理?加分为例。

4 个答案:

答案 0 :(得分: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读取类型提示的库(例如代码注释状态,杰克逊可以做到这一点)

因此,您可能需要对给定的代码进行一些调整:)

编辑:由于流行的需求,一个完整的实现可以通过另一个访问者动态处理重新序列化的对象(它只缺少在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或其他合适的名称。

答案 1 :(得分:1)

您在这里有两个不同的问题:创建一个未知类型的对象,然后对其进行有意义的处理。如果您创建的类型确实取决于json中节点的值,那么您将无法创建从该String到它将创建的类的映射。一个巨大的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主题还是其他类型。

答案 2 :(得分:1)

我使用具有两个类Car和Truck的父接口Vehicle的概念。 在您的情况下,这意味着Model1和Model2应该实现一个公共接口。

我的考试班:

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 {
}

汽车和卡车的模型是简单的POJO,没有任何注释:

public class Car implements Vehicle {
    private String model;

    public String getModel() {
        return model;
    }

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

2)一个单独的解析器保存该映射:

车辆包含额外的注释@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软件包,则可能存在潜在风险。

答案 3 :(得分:0)

为此,您需要Factory Method或/和Abstract Factory