之前有一篇文章讲了Java的Gson、FastJson等解析json常用类,与Python的json模块比较,繁琐之处是要定义各种实体类。那么,Java中有没有自动定义实体类的方法呢?
数据接入是我在大数据工作中的一部分,定长、csv、json是比较常见的几种数据格式。通常我都是使用Flume来完成数据的接入,根据对端数据源配置source,在数据源配置Interceptor(拦截器),将channel设置为kafka(通常是memory,为了流计算所以放在kafka),sink设置成文件等待后续调度入库。
Flume提供的的source、channel、sink基本可以覆盖日常需求,通过conf配置就可以轻松使用。而Interceptor很多时候都需要自己开发打包,放到Flume的lib中,然后在conf配置中绑定在source中,这样就能在进入channel之前对数据进行处理。
随着接入的Json数据越来越多,每接入一种格式的json,都要定义一个实体类,然后定义一个Interceptor,来将Json解析成CSV。
后来有一天就想着,能不能开发一个适配性Json的Interceptor,在配置文件中配置字段名称,就自动生成实体类,然后自动在Gson中引入这个实体类,对json数据进行解析。
说起自动生成类,就想起了我在2017年自学Java时学到的javassist类。
javassist提供了动态生成class的功能,接下来就看看如何使用javassist来创建一个类。
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.2-GA</version>
</dependency>
既然是创建class,那么一定会有个类,来表示正在构建的class对象,这样才能对class进行添加字段和方法等操作,这个类就是CtClass,即compile-time class,编译时class文件。
接着就是看看如何都创建CtClass对象。
从注释中看,CtClass是从ClassPool中获取。
ClassPool就是存放被JVM加载后class的容器,我们通常使用ClassPool.getDefault() 来获取CLASSPATH的class。
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get("java.lang.String");
System.out.println(ctClass);
我们来获取String的CtClass。
可以看到String的字段被封装成CtMethod类型,constructor封装成CTConstructor,同样字段会被封装成CtField。
接下来就是构建一个json数据对应的实体类。调用CtPool的makeClass() 方法,就可以新建一个空的CtClass,然后添加字段和方法。
Flume主要是从外部配置参数,然后通过脚本启动,所以我将json实体类的字段名称都通过参数配置。日常开发中比较常见的json主要有两种格式:
这里就先写简单类型的实体类如何构建。
先根据要处理的数据定制规范。
String json = "{\"name\": \"Tom\", \"age\": \"10\", \"time\": \"2024\" }";
String className = "A";
String fields = "name,age,time";
String connector = "|";
代码中json是要处理的json数据,className、fields、connector是外部传入的参数,分别代表着要创建类的名称、字段和toString方法的连接符。
根据上面讲的方法,在初始化了ctClass之后,就要添加字段和toString。
public static CtClass generateClass(String className, String fields, String connector) throws CannotCompileException {
ClassPool pool = ClassPool.getDefault();
// 初始化CtClas
CtClass ctClass = pool.makeClass(className);
for (String field : fields.split(",")) {
// 添加字段
ctClass.addField(CtField.make("private String " + field + ";", ctClass));
}
// 添加toString方法
ctClass.addMethod(CtMethod.make("public String toString() { String result = " + fields.replace(",", "+ \"" + connector + "\" +") + "; return result;}", ctClass));
return ctClass;
}
通过ctClass.addField添加字段,参数类型是CtField,通过CtField.make可以直接构建。分割之后遍历传入的fields,直接将字段添加到ctClass中,这里我直接默认为String类型,如果想做其他类型,可以对传入参数精准到数据类型。
因为结果要输出csv格式,所以最后一定要添加toString方法,来定义输出的格式。使用addMethod就可以直接添加一个toString方法。connector是每个字段的分隔符,通过对fields.replace将name,age,time换成了 name + "|" + age + "|" + time,用于构建toString代码。
最后返回构建完成的ctClass。
这里以Gson为例,在调用fromJson解析json串的时候,第一个参数是json字符串,第二个参数是Class对象。
如何能通过CtClass获取到Class,这里肯定首先想到类加载器ClassLoader,在Class源码注释中,也提供了这个方法。
JVM使用ClassLoder.defineClass,将编译后的字节形式的class文件加载构建成Class对象。而Ctclass中的toBytecode就可以将ctClass转换成class文件。
所以这里就定义一个loadClass方法,来生成Class。
public static Class loadClass(CtClass ctClass, String className) {
Class c = null;
byte[] byteClass;
try {
byteClass = ctClass.toBytecode();
// 反射
Class<?> clazz = Class.forName("java.lang.ClassLoader");
Method defineClass = clazz.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
c = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), className, byteClass, 0, byteClass.length);
} catch (Exception e) {
e.printStackTrace();
}
return c;
}
因为ClassLoder.defineClass是非public方法,所以我们只能通过反射来调用这个方法,最后生成一个class对象。
这样,Gson需要的Class参数也生成了,现在进行测试。
对上面定义的json进行测试:
String json = "{\"name\": \"Tom\", \"age\": \"10\", \"time\": \"2024\" }";
String className = "A";
String fields = "name,age,time";
String connector = "|";
CtClass ctClass = generateClass(className, fields, connector);
Gson gson = new Gson();
Object o = gson.fromJson(json, loadClass(ctClass, className));
System.out.println(o);
结果输出:
更换一个其他字段的json进行测试:
String json = "{\"a\": \"11\", \"b\": \"10\", \"c\": \"22\", \"d\": \"22\"}";
String className = "A";
String fields = "a,b,c,d,e";
String connector = ",";
CtClass ctClass = generateClass(className, fields, connector);
Gson gson = new Gson();
Object o = gson.fromJson(json, loadClass(ctClass, className));
System.out.println(o);
结果输出:
经过测试,对于简单类型的json可以做到配置化解析。
对于复杂类型json解析配置化解析,后面也可以写一写。我在开发list类型的json解析类时,使用javassist就遇到了编译时问题,后面在研究一下吧。
当然,我在18年开始使用flume,不仅仅是做一些数据接入,也做过大数据量的应用场景,有着使用flume从Kafka落地到HDFS超过8000亿条/天(存储500T/天)的实践,所以还略有一些心得,所以对flume有兴趣的可以一起学习!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。