杰克逊反序列化/动态加载的 pojo 类的类型引用



我需要获取JSON输入 Pojo 实例,我正在使用Jackson 2库,下面readValue方法可以使用类型引用进行反序列化:

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

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

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

这是我用来在运行时生成JSONPOJO的代码片段 并尝试

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("com.EnrichmentService.Thread72.EsRootDoc"));因为class.forName不会替换现有的类!

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

注意:索引器是在运行时加载动态 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.

参考了一个旧answer@如何在java中替换正在运行的应用程序中的类?

编辑2: 24Aug17面临异常 堆栈跟踪在这里: https://pastebin.com/ckCu2uWx

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

  1. 在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());
    

如果该代码在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 类 - 仍然获得该类的正确(更新)版本。

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

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

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

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

ClassLoader dynamicallyCompiledPojoLoader = ...;
Class<?> pojoClass = dynamicallyCompiledPojoLoader.loadClass("...");
return mapper.readValue(..., pojoClass);

另请参阅 com.fasterxml.jackson.databind.type.TypeFactory 以指定参数化的泛型类型而不使用TypeReference

"编辑2:24Aug17 面临异常堆栈跟踪在这里"之后的更新

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

异常消息的相关部分是:

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"])
所以 Jackson 不高兴 JSON 中的 "crawler" 字段是一个对象,即以 "{

" 开头,但 Java 属性 "crawler" 是一个 ArrayList,即应该以 "[" 开头

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

评论 POJO 类随每个请求而更改后更新

您说过 1) POJO 类结构随每个请求而变化,2) 您希望每次都对 POJO 使用相同的类名。

参见 Java - 如何加载同一类的不同版本?

您需要为每个请求使用一个新的类加载器,因为类加载器会缓存类。到目前为止,您发布的代码表明您正在使用单个类加载器,并希望在每个请求上重新加载"Category"类,这不起作用。

你说过:

我每次都需要为基于流口水的 elasticsearch 文档重新索引工作生成新类,这种流口水设置需要 pojo/Object 类型实例。

。但我认为您应该考虑使用 Map 或类似的输入来流口水,而不是基于反射的 POJO,因为您事先不知道结构。正如你在这里发现的,这不适合类/类加载器抽象。

参见

例如
  • https://groups.google.com/forum/#!topic/drools-usage/CbuSO-V-w_g
  • 将Java POJO转换为Drools DRL,反之亦然
  • https://groups.google.com/forum/#!topic/drools-usage/0BIXF3Tg5pw

但作为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 使用调用方的类加载器。您使用不同的类加载器,这可能是原因。有重载,您可以在其中传递类加载器。

更新堆栈和其他信息。

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

最新更新