动态编译生成Java类

3,173 阅读3分钟

这是我参与8月更文挑战的第23天,活动详情查看:8月更文挑战

动态创建bean,前面一篇介绍了通过cglib来创建的方式,虽然实现了动态创建java bean,但是有一个问题,java bean中的field name和我们预期的不太一致

接下来我们介绍一种直接通过拼接java代码,然后再将其编译成class并加载,从而实现动态类的创建

1. java代码动态编译创建

接下来我们主要借助jdk自带的JavaCompiler来实现java源码的编译,生成class,然后交由ClassLoader来加载类

如果我们现在有个java文件,希望在项目运行时,动态编译并加载类,一般的操作流程如下

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int compilationResult = compiler.run(null, null, null, '/path/Test.java');

相比于存在的java文件,我们更多的场景则是针对String的java源码,基于它来创建java类

测试则需要针对上面的逻辑进行扩展;如果不想关心细节,直接使用开源的三方包来实现

<dependency>
    <groupId>com.itranswarp</groupId>
    <artifactId>compiler</artifactId>
    <version>1.0</version>
</dependency>

同样我们来实现一个根据Map,来生成java bean的方式

基于javacode来生成源码

/**
 * 编译java代码,并生成对象
 *
 * @param packageName 包名
 * @param className   类名
 * @param javaCode    源码
 * @return
 * @throws Exception
 */
public Object createBean(String packageName, String className, String javaCode) throws Exception {
    JavaStringCompiler compiler = new JavaStringCompiler();
    Map<String, byte[]> result = compiler.compile(className + ".java", "package " + packageName + ";\n" + javaCode);
    Class clz = compiler.loadClass(packageName + "." + className, result);
    return clz.newInstance();
}

接下来我们需要实现的是根据map来生成对应的java code

private String genCode(String clzName, Map<String, Object> map) {
    StringBuilder builder = new StringBuilder("public class ").append(clzName).append("{\n");
    for (Map.Entry<String, Object> obj: map.entrySet()) {
        String fieldType;
        if (obj.getValue() instanceof List) {
            fieldType = "java.util.List";
        } else if (obj.getValue() instanceof Set) {
            fieldType = "java.util.Set";
        } else if (obj.getValue() instanceof  Map) {
            fieldType = "java.util.Map";
        } else {
            fieldType = obj.getValue().getClass().getName().replace("$", ".");
        }
        builder.append("\tpublic ").append(fieldType).append(" ").append(obj.getKey()).append(";\n");
    }
    builder.append("}");
    return builder.toString();
}

注意上面的实现,现在只是最基础的实现,需要注意,如果value的类型,是一个内部类,就可能有坑(比如上面针对容器类进行了特殊处理)

最后测试一下

@Test
public void testGenMapBean() throws Exception {
    Map<String, Object> map = new HashMap<>();
    map.put("hello", "world");
    map.put("key", 12);
    map.put("list", Arrays.asList(1, 2, 3));

    String packageName = "com.git.hui.dynamic";
    String clzName = "MBean1";
    String code = genCode(clzName, map);
    Object bean = createBean(packageName, clzName, code);
    // 初始化bean的成员
    for(Map.Entry<String, Object> entry: map.entrySet()) {
        Field field = bean.getClass().getField(entry.getKey());
        field.set(bean, entry.getValue());
    }
    System.out.println("---------------- java code  ---------------\n" + code + "\n------------------");
    System.out.println(JSON.toJSONString(bean));
}

上面给出了一个根据Map生成bean对象,并初始化成员值的实例

输出如下

---------------- java code  ---------------
public class MBean1{
	public java.lang.String hello;
	public java.util.List list;
	public java.lang.Integer key;
}
------------------
{"hello":"world","key":12,"list":[1,2,3]}

输出和我们预期一致,直接通过组装java代码,然后来生成对象

那么这种方式应用场景在什么地方呢?

  • 比如我之前做过一个动态脚本执行的框架,可以在控制台上写java代码,写完之后直接交给框架来运行;当然这种选择groovy来做脚本可能更优雅一些

II. 其他

1. 一灰灰Blogliuyueyi.github.io/hexblog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

2. 声明

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

  • 微博地址: 小灰灰Blog
  • QQ: 一灰灰/3302797840
  • 微信公众号:一灰灰blog