28. [Java]如何根据需要动态生成Java的class

28.1. 需求描述

现在有一个将excel导入Hive表的需求,导入的大致思路是这样的:

1、使用Java程序将excel文本内容读入内存,使用一个Java bean去接收每一行数据

2、使用spark将java bean转成dataframe

3、将得到的dataframe注册成临时表,使用sql将数据写入hive目标表

由于excel的内容里列数量是不固定的,在第一步中,如果每次都新建一个新的对象去接收,程序的维护未免太复杂,且不具备动态的扩展性,一句话说就是不够优雅,我不允许这样的事情发生!

于是,为了解决这个问题,咨询了同事还有一个类库可以解决这个问题,它就是:

javaassist

28.2. 操作起来!

既然内容是多变的,那么能不能根据实际excel的列数,动态去生成这个类,excel的表头列名就是对象的属性名,列有多少个,对象就有多少个属性!OK,下面邀请本次博客的重磅嘉宾:javaassist

28.2.1. 构建一个基础对象

maven中引入依赖:

<!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.30.2-GA</version>
</dependency>

下面我们从0开始,构造一个简单的对象,对象包括:

  • 无参构造

  • 有参构造

  • 两个属性

  • setter和getter方法

public class Person {
    private String name;
    private String sex;

    public void setName(String var1) {
        this.name = var1;
    }

    public String getName() {
        return this.name;
    }

    public void setSex(String var1) {
        this.sex = var1;
    }

    public String getSex() {
        return this.sex;
    }

    public Person(String var1, String var2) {
        this.sex = this.sex;
    }

    public Person() {
    }
}

28.2.2. 声明类

ClassPool classPool = ClassPool.getDefault();
CtClass row = classPool.makeClass("YOUR CLASS NAME");
row.setModifiers(Modifier.PUBLIC);

使用ClassPool创建了一个PUBLIC类方法,这时候主体已经有了

28.2.3. 声明私有属性

两个私有属性,分别是name和sex

新建两个Field对象,根据官网,新建一个属性需要new一个CtField对象,此时需要传入属性的类型,但是,默认的只有booleanType、charType、byteType、shortType、intType、longType、floatType、doubleType和voidType,如果需要使用String类型,则需要用CtClass cc = pool.get("java.lang.String");来得到

    /**
     * Creates a <code>CtField</code> object.
     * The created field must be added to a class
     * with <code>CtClass.addField()</code>.
     * An initial value of the field is specified
     * by a <code>CtField.Initializer</code> object.
     *
     * <p>If getter and setter methods are needed,
     * call <code>CtNewMethod.getter()</code> and 
     * <code>CtNewMethod.setter()</code>.
     *
     * @param type              field type
     * @param name              field name
     * @param declaring         the class to which the field will be added.
     *
     * @see CtClass#addField(CtField)
     * @see CtNewMethod#getter(String,CtField)
     * @see CtNewMethod#setter(String,CtField)
     * @see CtField.Initializer
     */
    public CtField(CtClass type, String name, CtClass declaring)
        throws CannotCompileException
    {
        this(Descriptor.of(type), name, declaring);
    }

先得到String的CtClass

CtClass stringClass = stringClass = classPool.get("java.lang.String");

再构建属性:

//参数依次是属性类型,属性名,累对象的CtClass
CtField a = new CtField(stringClass, colName, row);
a.setModifiers(Modifier.PRIVATE);

我们用得到类来添加该属性

row.addField(a);

如此往复,再添加另一个属性即可

28.2.4. 声明getter和setter

//setter方法
//setter方法是没有返回值的,所以在此设置为voidType,为了规范命名,将属性名转为驼峰命名格式
//setter方法是有参数的,在此即为属性的类型,为string
CtMethod setMethod = new CtMethod(CtClass.voidType, "set" + Camel.camel(colName), new CtClass[]{stringClass}, row);
setMethod.setModifiers(Modifier.PUBLIC);
setMethod.setBody("this." + colName + "=$1;"); //这里的$1即代表第一个参数
//getter方法
//getter方法是有返回值的,返回值的类型即是属性的类型,在此为string类型,同样属性名转驼峰格式
//getter方法是无参的
CtMethod getMethod = new CtMethod(stringClass, "get" + Camel.camel(colName), new CtClass[]{}, row);
getMethod.setModifiers(Modifier.PUBLIC);
getMethod.setBody("return " + colName + ";");

在创造的类上添加这两个方法:

row.addMethod(setMethod);
row.addMethod(getMethod);

28.2.5. 创建构造方法

在此创建一个有两个参数的有参构造和一个无参构造

1、有参构造

CtClass[] ctClasses = new CtClass[2];
Arrays.fill(ctClasses, stringClass);
CtConstructor constructor = new CtConstructor(ctClasses, row);
constructor.setModifiers(Modifier.PUBLIC);//有参构造方法是PUBLIC修饰的

constructor.setBody("this." + colName + "= " + colName + ";");

2、无参构造

CtConstructor noArgConstructor = CtNewConstructor.make("public " + cName + "(){}", row);
row.addConstructor(noArgConstructor);

将以上方法整合起来就得到了一个构建的类,该类是可以在运行时调用的

28.3. 封装

为了后续方便使用,将这些方法封装起来,做成工具使用,下面新建一个工具类,允许传入属性列表,并且默认全部都是string类型,如果需要其他类型,在此基础上修改即可:

import com.svw.usually.util.Camel;
import javassist.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Arrays;

public class ClassMaker {

    private ArrayList<String> columns = new ArrayList<String>();
    private String className = null;

    public ClassMaker(ArrayList<String> columns, String className) {
        this.columns = columns;
        this.className = className;
    }

    private static final Logger LOG = LoggerFactory.getLogger(ClassMaker.class);

    public Class<?> makeClass() throws CannotCompileException {

        ClassPool classPool = ClassPool.getDefault();
        CtClass row = classPool.makeClass(className);
        row.setModifiers(Modifier.PUBLIC);

        CtClass stringClass = null;
        try {
            stringClass = classPool.get("java.lang.String");
        } catch (NotFoundException e) {
            throw new RuntimeException(e);
        }

        CtClass[] ctClasses = new CtClass[columns.size()];
        Arrays.fill(ctClasses, stringClass);
        CtConstructor constructor = new CtConstructor(ctClasses, row);
        constructor.setModifiers(Modifier.PUBLIC);
        for (String colName : columns) {
            LOG.info("Adding property name: {}", colName);
            CtField a = new CtField(stringClass, colName, row);
            a.setModifiers(Modifier.PRIVATE);
            row.addField(a);

            //set方法
            CtMethod setMethod = new CtMethod(CtClass.voidType, "set" + Camel.camel(colName), new CtClass[]{stringClass}, row);
            setMethod.setModifiers(Modifier.PUBLIC);
            setMethod.setBody("this." + colName + "=$1;");
            //get方法
            CtMethod getMethod = new CtMethod(stringClass, "get" + Camel.camel(colName), new CtClass[]{}, row);
            getMethod.setModifiers(Modifier.PUBLIC);
            getMethod.setBody("return " + colName + ";");

            row.addMethod(setMethod);
            row.addMethod(getMethod);
            constructor.setBody("this." + colName + "= " + colName + ";");
        }
        row.addConstructor(constructor);
        String[] pts = className.split(".");
        String cName = pts[pts.length - 1];
        CtConstructor noArgConstructor = CtNewConstructor.make("public " + cName + "(){}", row);
        row.addConstructor(noArgConstructor);
        
        return row.toClass();
    }
}

Camel

public class Camel {
    public static String camel(String name) {
        if (name == null) {
            return null;
        }
        String head = name.substring(0, 1).toUpperCase();

        if (name.length() >= 2) {
            return head + name.substring(1, name.length());
        } else return head;
    }
}

28.4. 后续

以上都只是最简单的用法,由于时间问题,先更新这些,如果后续还花更多时间探索或者解锁了其他玩法,再在这里更新!