Jackson deserialisation / TypeReference用于动态加载的pojo类

时间:2017-08-05 05:24:52

标签: java json jackson jsonschema2pojo

我需要获取JSON输入Pojo实例并使用Jackson 2库,而readValue方法可以使用typeReferencing反序列化:

POJO_ClassName p = mapper.readValue(new TypeReference< POJO_ClassName >() {});

但问题是,当POJO在运行时动态创建和加载时,我如何获得JSONPOJO实例/对象,因为我没有完全限定的类(POJO_ClassName) )上述声明的名称?

注意:我使用jsonSchema2pojo库在运行时生成POJO类。

以下是代码段,我用于在运行时为POJO生成JSON 并尝试

  String classPath="com.EnrichmentService.Thread72"; 
     String classLocation = System.getProperty("user.dir")
                         + "/src/main/java"; JCodeModel codeModel = new JCodeModel();

     final RuleFactory ruleFactory = new RuleFactory(config,
                         new Jackson2Annotator(config), new SchemaStore());

     final SchemaMapper mapperSchema = new SchemaMapper(ruleFactory,
                         new SchemaGenerator());

     mapperSchema.generate(codeModel, "EsRootDoc",classPath, json);

     codeModel.build(new File(classLocation));  // generates pojo classes

     // Till above jsonSchema2Pojo pojo generation all Good !!
      // EsRootDoc instance is needed for further drools drl validations.

     com.EnrichmentService.Thread72.EsRootDoc p = mapper.readValue(new TypeReference<com.EnrichmentService.Thread72.EsRootDoc>() {}); 
// see alternative way as well in my 24Aug17 edit at the end of this question

但由于尚未生成com.EnrichmentService.Thread72.EsRootDoc,编译器会在找不到类时出错。

要点:

1)在运行时迭代生成相同的Pojo类,但每次都会改变JSON输入的不同属性。

2)甚至尝试过 Object pojo = mapper.readValue(json,Class.forName(&#34; com.EnrichmentService.Thread72.EsRootDoc&#34;));因为class.forName不会替换现有的类!

编辑8月24日 - 这是我的自定义类加载器:

注意:Indexer是在运行时加载动态EsRootDoc / POJO类的类。

 static class TestClassLoader extends ClassLoader {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                if (name.equals("com.EnrichmentService.Thread72.EsRootDoc")) {
                    try {
                        InputStream is = Indexer.class.getClassLoader().getResourceAsStream("com/EnrichmentService/Thread72/EsRootDoc.class");
                        byte[] buf = new byte[is.available()];
                        int len = is.read(buf);

                        Class<?> c=defineClass(name, buf, 0, len);
                        resolveClass(c);
                        return c;


                    } catch (IOException e) {
                        throw new ClassNotFoundException("", e);
                    }
                }
                return getParent().loadClass(name);
            }
        }

我尝试使用上面的TestClassLoader自定义类加载器作为替代方法是这样的:

Class cls = new      TestClassLoader().loadClass("com.EnrichmentService.Thread72.EsRootDoc");
    Object obj = cls.newInstance();
    cls.getMethod("getCrawlerSource").invoke(obj);
    p=mapper.readValue(json, cls);  // but here i am getting the same deserialization exception as earlier.

提到旧答案@ How to replace classes in a running application in java ?

Edit2:24Aug17  遇到异常stackTrace就在这里:https://pastebin.com/ckCu2uWx

3 个答案:

答案 0 :(得分:3)

你发现,你只能使用TypeReference编译时已知的类型(没有一些非常棘手的元编程)。

但是,[{1}}有很多替代重载,不需要readValue,以允许像你这样TypeReference不切实际的情况。

我认为您可以使用readValue(... , Class<T> valueType)

如果你有这些后期编译类的特殊类加载器,那么你可以从中获取一个TypeReference实例并将其传入,例如:

Class

另请参阅com.fasterxml.jackson.databind.type.TypeFactory以指定参数化通用类型,而不使用ClassLoader dynamicallyCompiledPojoLoader = ...; Class<?> pojoClass = dynamicallyCompiledPojoLoader.loadClass("..."); return mapper.readValue(..., pojoClass);

更新&#34; Edit2:24Aug17面临异常stackTrace就在这里&#34;

您当前的异常(https://pastebin.com/ckCu2uWx)不是类加载器问题,而是JSON模式不匹配问题。

异常消息的相关部分是:

TypeReference

所以杰克逊不喜欢这个&#34;爬虫&#34; JSON中的字段是一个对象,即以&#34; Can not deserialize instance of java.util.ArrayList out of START_OBJECT token ... through reference chain: com.EnrichmentService.Thread72.EsRootDoc["category"]->java.util.ArrayList[0]->com.EnrichmentService.Thread72.Category["crawler"]) &#34;开头,但Java属性&#34; crawler&#34;是一个ArrayList,即应该以&#34; {&#34;

开头

我不知道为什么[的POJO结构在这里是错误的,但它看起来不像是一个类加载器问题。

评论POJO类随每个请求更改

后更新

您已经说过1)POJO类结构因每个请求而异,2)您希望每次都为POJO使用相同的类名。

请参阅Java - how to load different versions of the same class?

您需要为每个请求使用新的类加载器,因为类会被类加载器缓存。到目前为止,您发布的代码表明您正在使用单个类加载器,并希望重新加载&#34;类别&#34;每个请求上课,但不会工作。

你说过:

  

我需要每次为基于drools的elasticsearch文档重新索引工作生成新类,并且这个drools设置需要pojo / Object类型实例。谢谢

...但我认为您应该考虑使用地图或类似输入到Drools而不是基于反射的POJO,因为您事先并不知道结构。正如您在此处所发现的,这不适合类/类加载器抽象。

参见例如

答案 1 :(得分:2)

Imo有两种方法可以解决这个问题:

  1. 创造&amp;在comile时编译类(例如使用maven和jaxb)
    1. 你做了类似的事情:

      String className = "com.EnrichmentService.Thread72.EsRootDoc";
      Class<?> clazz = Class.forName(className);
      Object object = clazz.getConstructor().newInstance();
      Object p = mapper.readValue(json, object.getClass());
      
    2. 如果该代码在mapper.readValue()之前失败,则会出现另一个问题(我的猜测是类加载)。

      更好的是泛型:

          String className = "com.EnrichmentService.Thread72.EsRootDoc";
          Class<?> clazz = Class.forName(className);
          // cannot use dynamically created classes in a static way, just to 
          // show the point
          // com.EnrichmentService.Thread72.EsRootDoc p = 
          //     getObjectFromMessageString(json, clazz);
          Object p = getObjectFromString(json, clazz);
      
          public static <T> T getObjectFromString(String json, Class<T> clazz) {
              return mapper.readValue(json, clazz);
          }
      

      编辑:

      我写了一些示例代码,它在运行时编译一个类,然后尝试转换为所述编译类的对象。输出就像我预期的那样:

      import java.io.File;
      import java.io.IOException;
      import java.io.InputStream;
      import java.nio.charset.StandardCharsets;
      import java.nio.file.Files;
      
      import javax.tools.JavaCompiler;
      import javax.tools.ToolProvider;
      
      import com.fasterxml.jackson.core.JsonParseException;
      import com.fasterxml.jackson.databind.JsonMappingException;
      import com.fasterxml.jackson.databind.ObjectMapper;
      
      public class JackonCustomClassTest {
          public static String CLASS_NAME = "EsRootDoc";
          public static String PACKAGE_NAME = "com.EnrichmentService.Thread72";
          public static String CANONICAL_NAME = PACKAGE_NAME + "." + CLASS_NAME;
      
          public static void main(String args[]) throws Exception {
              JackonCustomClassTest mtc = new JackonCustomClassTest();
              Class<?> c = null;
              String source = null;
              // compile class for the first time
              source = "package "+PACKAGE_NAME+"; public class "+CLASS_NAME+" { public "+CLASS_NAME+"() { }; public String toString() { return \"Name: not existing\" + \" - className: \" + getClass().getCanonicalName(); }; }";
              c = mtc.compileClass(CANONICAL_NAME, source);
      
              System.out.println("class test: " + c.newInstance().toString());
      
              // compile class for the second time
              source = "package "+PACKAGE_NAME+"; public class "+CLASS_NAME+" { private String name; public "+CLASS_NAME+"() { }; public String getName() { return name; }; public void setName(String name) { this.name = name; }; public String toString() { return \"Name: \" + name + \" - className: \" + getClass().getCanonicalName(); }; }";
              c = mtc.compileClass(CANONICAL_NAME, source);
      
              System.out.println("class test: " + c.newInstance().toString());
      
              mtc.runJackson(c);
          }
      
          private void runJackson(Class<?> clazz) throws JsonParseException, JsonMappingException, IOException {
              ObjectMapper m = new ObjectMapper();
              String string = "{ \"name\": \"asdf\" }";
              Object o = m.readValue(string, clazz);
              System.out.println("result of conversion: " + o); // Should print "Name: asdf"
          }
      
          public Class<?> compileClass(String fullyQualifiedClassName, String source) throws Exception {
              // Save source in .java file.
              File root = new java.io.File( "./target/test-classes/" );
              File sourceFile = new File(root, fullyQualifiedClassName.replace(".", "/") + ".java");
              sourceFile.getParentFile().mkdirs();
              Files.write(sourceFile.toPath(), source.getBytes(StandardCharsets.UTF_8));
      
              // Compile source file.
              JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
              compiler.run(null, null, null, sourceFile.getPath());
      
              // Load and instantiate compiled class.
              //          URLClassLoader classLoader = URLClassLoader.newInstance(new URL[] { root.toURI().toURL() });
              //          Class<?> cls = Class.forName(fullyQualifiedClassName, true, classLoader);
              Class<?> cls = new TestClassLoader().loadClass(fullyQualifiedClassName);
              return cls;
          }
      
          static class TestClassLoader extends ClassLoader {
              @Override
              public Class<?> loadClass(String name) throws ClassNotFoundException {
                  if (name.startsWith(PACKAGE_NAME)) {
                      try {
                          InputStream is = this.getClass().getClassLoader()
                                  .getResourceAsStream(name.replace(".",  "/") + ".class");
                          byte[] buf = new byte[is.available()];
                          int len = is.read(buf);
      
                          Class<?> c = defineClass(name, buf, 0, len);
                          resolveClass(c);
                          return c;
      
                      } catch (IOException e) {
                          throw new ClassNotFoundException("", e);
                      }
                  }
                  return getParent().loadClass(name);
              }
          }
      }
      

      编辑2:

      更新了试用TestClassLoader类的代码 - 仍然可以获得该类的正确(更新)版本。

答案 2 :(得分:1)

  

但是由于com.EnrichmentService.Thread72.EsRootDoc尚未生成,编译器会在找不到类时出错。

是的,当然。如果您没有在JVM中上课,则无法加载它们。

  

com.fasterxml.jackson.databind.JsonMappingException:无法反序列化

请给所有的堆栈跟踪。

  

1)在运行时迭代生成相同的Pojo类,但每次都会改变JSON输入的不同属性。

为什么你不使用地图?你为什么不在所有领域使用一个大班(其中一些将是空的)?

  

是的,EsRootDoc类在运行时迭代生成,类更改以及每次迭代中每个输入json的更改

如果你在多个线程中执行此操作,只需同步它们,例如:

final String ConcurrentHashMap<String, Class> dynamicClasses = new ConcurrentHashMap();

Object doInThreadOne(String json, String className) {
    return mapper.readObject(json, dynamicClasses.get(className))

void doInAnotherThread(String className) {
    dynamicClasses.put(className, reload(className));
}

如果您需要更强的一致性,则可以按类名使用同步:

static final String className = "com.EnrichmentService.Thread72.EsRootDoc";

final String Map<String, Class> dynamicClasses = new HashMap();

Object doInThreadOne(String json) {
    synchronized(className) {
        return mapper.readObject(json, dynamicClasses.get(className))
    }

void doInAnotherThread(String className) {
    synchronized(className) {
        dynamicClasses.put(className, reload(className));
    }
}

方法Class.forName使用调用者的类加载器。你使用不同的类加载器,这可能是理由。有重载,你可以通过classloader。

更新stackrace及其他信息。

您应该将@JsonIgnoreProperties(ignoreUnknown = true)添加到Crawler,因为它具有字段subCategory,这在Pojo中不存在。