Java运行时动态生成类实现过程详解

  最近一个项目中利用规则引擎,提供用户拖拽式的灵活定义规则。这就要求根据数据库数据动态生成对象处理特定规则的逻辑。如果手写不仅每次都要修改代码,还要每次测试发版,而且无法灵活根据用户定义的规则动态处理逻辑。所以想到将公共逻辑写到父类实现,将特定逻辑根据字符串动态生成子类处理。这就可以一劳永逸解决这个问题。

  那就着手从Java如何根据字符串模板在运行时动态生成对象。

  Java是一门静态语言,通常,我们需要的class在编译的时候就已经生成了,为什么有时候我们还想在运行时动态生成class呢?

  经过一番网上资料查找,由繁到简的方式总结如下:

一、利用JDK自带工具类实现

  现在问题来了,动态生成字节码,难度有多大?

  如果我们要自己直接输出二进制格式的字节码,在完成这个任务前,必须先认真阅读JVM规范第4章,详细了解class文件结构。估计读完规范后,两个月过去了。

  所以,第一种方法,自己动手,从零开始创建字节码,理论上可行,实际上很难。

  第二种方法,使用已有的一些能操作字节码的库,帮助我们创建class。

  目前,能够操作字节码的开源库主要有CGLib和Javassist两种,它们都提供了比较高级的API来操作字节码,最后输出为class文件。

比如CGLib,典型的用法如下:

Enhancer e = new Enhancer();
e.setSuperclass(...);
e.setStrategy(new DefaultGeneratorStrategy() {
  protected ClassGenerator transform(ClassGenerator cg) {
    return new TransformingGenerator(cg,
      new AddPropertyTransformer(new String[]{ "foo" },
          new Class[] { Integer.TYPE }));
  }});
Object obj = e.create();

比自己生成class要简单,但是,要学会它的API还是得花大量的时间,并且,上面的代码很难看懂对不对?

有木有更简单的方法?

有!

Java的编译器是javac,但是,在很早很早的时候,Java的编译器就已经用纯Java重写了,自己能编译自己,行业黑话叫“自举”。从Java 1.6开始,编译器接口正式放到JDK的公开API中,于是,我们不需要创建新的进程来调用javac,而是直接使用编译器API来编译源码。

使用起来也很简单:

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

这么写编译是没啥问题,问题是我们在内存中创建了Java代码后,必须先写到文件,再编译,最后还要手动读取class文件内容并用一个ClassLoader加载。

有木有更简单的方法?

有!

其实Java编译器根本不关心源码的内容是从哪来的,你给它一个String当作源码,它就可以输出byte[]作为class的内容。

所以,我们需要参考Java Compiler API的文档,让Compiler直接在内存中完成编译,输出的class内容就是byte[]。

Map<String, byte[]> results;
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager stdManager = compiler.getStandardFileManager(null, null, null);
try (MemoryJavaFileManager manager = new MemoryJavaFileManager(stdManager)) {
  JavaFileObject javaFileObject = manager.makeStringSource(fileName, source);
  CompilationTask task = compiler.getTask(null, manager, null, null, null, Arrays.asList(javaFileObject));
  if (task.call()) {
    results = manager.getClassBytes();
  }
}

上述代码的几个关键在于:

用MemoryJavaFileManager替换JDK默认的StandardJavaFileManager,以便在编译器请求源码内容时,不是从文件读取,而是直接返回String;

用MemoryOutputJavaFileObject替换JDK默认的SimpleJavaFileObject,以便在接收到编译器生成的byte[]内容时,不写入class文件,而是直接保存在内存中。

最后,编译的结果放在Map<String, byte[]>中,Key是类名,对应的byte[]是class的二进制内容。

为什么编译后不是一个byte[]呢?

因为一个.java的源文件编译后可能有多个.class文件!只要包含了静态类、匿名类等,编译出的class肯定多于一个。

如何加载编译后的class呢?

加载class相对而言就容易多了,我们只需要创建一个ClassLoader,覆写findClass()方法:

class MemoryClassLoader extends URLClassLoader {

  Map<String, byte[]> classBytes = new HashMap<String, byte[]>();

  public MemoryClassLoader(Map<String, byte[]> classBytes) {
    super(new URL[0], MemoryClassLoader.class.getClassLoader());
    this.classBytes.putAll(classBytes);
  }

  @Override
  protected Class<?> findClass(String name) throws ClassNotFoundException {
    byte[] buf = classBytes.get(name);
    if (buf == null) {
      return super.findClass(name);
    }
    classBytes.remove(name);
    return defineClass(name, buf, 0, buf.length);
  }
}

总结以上,那么我们来编写一个Java脚本引擎吧:

https://github.com/barrywang88/compiler

https://github.com/barrywang88/compiler.git

二、利用三方Jar包实现

利用三方包com.itranswarp.compiler来实现:

1. 引入Maven依赖包:

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

2. 编写工具类

public class StringCompiler {
  public static Object run(String source, String...args) throws Exception {
    // 声明类名
    String className = "Main";
    String packageName = "top.fomeiherz";
    // 声明包名:package top.fomeiherz;
    String prefix = String.format("package %s;", packageName);
    // 全类名:top.fomeiherz.Main
    String fullName = String.format("%s.%s", packageName, className);

    // 编译器
    JavaStringCompiler compiler = new JavaStringCompiler();
    // 编译:compiler.compile("Main.java", source)
    Map<String, byte[]> results = compiler.compile(className + ".java", prefix + source);
    // 加载内存中byte到Class<?>对象
    Class<?> clazz = compiler.loadClass(fullName, results);
    // 创建实例
    Object instance = clazz.newInstance();
    Method mainMethod = clazz.getMethod("main", String[].class);
    // String[]数组时必须使用Object[]封装
    // 否则会报错:java.lang.IllegalArgumentException: wrong number of arguments
    return mainMethod.invoke(instance, new Object[]{args});
  }
}

3. 测试执行

public class StringCompilerTest {
  public static void main(String[] args) throws Exception {
    // 传入String类型的代码
    String source = "import java.util.Arrays;public class Main" +
        "{" +
        "public static void main(String[] args) {" +
        "System.out.println(Arrays.toString(args));" +
        "}" +
        "}";
    StringCompiler.run(source, "1", "2");
  }
}

三、利用Groovy脚本实现

以上两种方式尝试过,后来发现Groovy原生就支持脚本动态生成对象。

1. 引入Groovy maven依赖

<dependency>
  <groupId>org.codehaus.groovy</groupId>
  <artifactId>groovy-all</artifactId>
  <version>2.4.13</version>
 </dependency>

2. 直接上测试代码

@Test
  public void testGroovyClasses() throws Exception {
    //groovy提供了一种将字符串文本代码直接转换成Java Class对象的功能
    GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
    //里面的文本是Java代码,但是我们可以看到这是一个字符串我们可以直接生成对应的Class<?>对象,而不需要我们写一个.java文件
    Class<?> clazz = groovyClassLoader.parseClass("package com.xxl.job.core.glue;\n" +
        "\n" +
        "public class Main {\n" +
        "\n" +
        "  public int age = 22;\n" +
        "  \n" +
        "  public void sayHello() {\n" +
        "    System.out.println(\"年龄是:\" + age);\n" +
        "  }\n" +
        "}\n");
    Object obj = clazz.newInstance();
    Method method = clazz.getDeclaredMethod("sayHello");
    method.invoke(obj);

    Object val = method.getDefaultValue();
    System.out.println(val);
  }

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 用Rational Rose逆向工程(java)生成类图(教程和错误解决)

    拿到了项目框架工程代码却没有uml图,那么方法之间的调用关系功能流转就不容易看出来,那么如何产生类图呢,记忆里方法有下:1.rose逆向工程,2.together rose有了,together前天才卸掉,那么就用ROSE,摸索后得出以下步骤,知道的就不用看了,很粗糙的:) 1.新建一个ROSE工程,选择J2EE模板,2.如图 3.在出现的界面中,CLASSPATH中加入源代码的目录,加入支持的包,点'ADD RECURSIVE'点 'SELECT ALL'' 点REVERSE ',完成后没任何

  • Java使用Random类生成随机数示例

    本文实例讲述了Java使用Random类生成随机数.分享给大家供大家参考,具体如下: 一 代码 import java.util.Random; class RandomDie { private int sides; private Random generator; public RandomDie(int s) { sides = s; generator = new Random( ); } public int cast( ) { return 1 + generator.nextIn

  • Java生成图形验证码工具类

    生成验证码效果 ValidateCode.java 验证码生成类 package cn.dsna.util.images; import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.FileOutputStream; import java.io.IOException; import java.io.Ou

  • java实现验证码类生成中文验证码

    复制代码 代码如下: package xwcms.net.service;import java.awt.Color;import java.awt.Font;import java.awt.Graphics;import java.awt.Graphics2D;import java.awt.image.BufferedImage;import java.io.IOException;import java.util.Random;import javax.imageio.ImageIO;im

  • java数据库唯一id生成工具类

    本文实例为大家分享了java数据库唯一id生成工具类的具体代码,供大家参考,具体内容如下 import java.io.File; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Map; import java.util.Random; import java.util.UUID; import org.springframework.dao.EmptyResultDataAccessExce

  • java类中生成jfreechart,返回图表的url地址 代码分享

    web.xml中设置: 复制代码 代码如下: <servlet> <servlet-name>DisplayChart</servlet-name> <servlet-class>org.jfree.chart.servlet.DisplayChart</servlet-class> </servlet > <servlet-mapping> <servlet-name>DisplayChart</ser

  • 利用java生成二维码工具类示例代码

    二维码介绍 二维条形码最早发明于日本,它是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的,在代码编制上巧妙地利用构成计算机内部逻辑基础的"0"."1"比特流的概念,使用若干个与二进制相对应的几何形体来表示文字数值信息,通过图象输入设备或光电扫描设备自动识读以实现信息自动处理. 如下为java生成二维码工具类,可以选择生成文件,或者直接在页面生成,话不多说了,来一起看看详细的示例代码吧. 示例代码 import java.aw

  • java随机生成字符串(字符随机生成类 生成随机字符组合)

    复制代码 代码如下: package com.phyl.password; import java.util.ArrayList;import java.util.Arrays;import java.util.Random;/** * 字符随机生成类 * @author ASUS * */public class PassWord { /**  * 密码类型枚举  * @author ASUS  */ public static enum TYPE {  /**   * 字符型   */  L

  • Java运行时动态生成类实现过程详解

    最近一个项目中利用规则引擎,提供用户拖拽式的灵活定义规则.这就要求根据数据库数据动态生成对象处理特定规则的逻辑.如果手写不仅每次都要修改代码,还要每次测试发版,而且无法灵活根据用户定义的规则动态处理逻辑.所以想到将公共逻辑写到父类实现,将特定逻辑根据字符串动态生成子类处理.这就可以一劳永逸解决这个问题. 那就着手从Java如何根据字符串模板在运行时动态生成对象. Java是一门静态语言,通常,我们需要的class在编译的时候就已经生成了,为什么有时候我们还想在运行时动态生成class呢? 经过一

  • Java运行时动态生成对象的方法小结

    目录 一.利用JDK自带工具类实现 二.利用三方Jar包实现 三.利用Groovy脚本实现 最近一个项目中利用规则引擎,提供用户拖拽式的灵活定义规则.这就要求根据数据库数据动态生成对象处理特定规则的逻辑.如果手写不仅每次都要修改代码,还要每次测试发版,而且无法灵活根据用户定义的规则动态处理逻辑.所以想到将公共逻辑写到父类实现,将特定逻辑根据字符串动态生成子类处理.这就可以一劳永逸解决这个问题. 那就着手从Java如何根据字符串模板在运行时动态生成对象. Java是一门静态语言,通常,我们需要的c

  • Java运行时环境之ClassLoader类加载机制详解

    背景:听说ClassLoader类加载机制是进入BAT的必经之路. ClassLoader总述: 普通的Java开发其实用到ClassLoader的地方并不多,但是理解透彻ClassLoader类的加载机制,无论是对我们编写更高效的代码还是进BAT都大有裨益:而从"黄埔军校"出来的我对ClassLoader的理解都是借鉴了很多书籍和博客,站在了各大博主的肩膀上,感谢你们!上菜,Classloader最主要的作用就是将Java字节码文件(后缀为.class)加载到JVM中,JVM在启动时

  • Java 中DateUtils日期工具类的实例详解

    Java 中DateUtils日期工具类的实例详解 介绍 在java中队日期类型的处理并不方便,通常都需要借助java.text.SimpleDateFormat类来实现日期类型 和字符串类型之间的转换,但是在jdk1.8之后有所改善,jdk1.7以及之前的版本处理日期类型并不方便, 可以借助Joda Time组件来处理,尤其是日期类型的一些数学操作就更是不方便. java代码 /** * * 日期工具类 java对日期的操作一直都很不理想,直到jdk1.8之后才有了本质的改变. * 如果使用的

  • java接口语法以及与类的关系详解

    1.语法: public interface 接口名{} public class 类名 implements 接口名{} interface +接口名 implements: 实现 创建接口对象: Jumping p=new Cat(); 接口对象可以访问: Cat类中的成员变量和成员方法 但接口要写跟类一样的定义方法,接口对象才能引用类的方法 在interface里面的变量都是public static final 的.所以你可以这样写: public static final int i=

  • Java在运行时识别类型信息的方法详解

    前言 在日常的学习工作当中,有一些知识是我们在读书的时候就能够习得:但有一些知识不是的,需要在实践的时候才能得到真知--这或许就是王阳明提倡的"知行合一". 在Java中,并不是所有的类型信息都能在编译阶段明确,有一些类型信息需要在运行时才能确定,这种机制被称为RTTI,英文全称为Run-Time Type Identification,即运行时类型识别,有没有一点"知行合一"的味道?运行时类型识别主要由Class类实现. 01 Class类 在Java中,我们常用

  • Java 用Prometheus搭建实时监控系统过程详解

    上帝之火 本系列讲述的是开源实时监控告警解决方案Prometheus,这个单词很牛逼.每次我都能联想到带来上帝之火的希腊之神,普罗米修斯.而这个开源的logo也是火,个人挺喜欢这个logo的设计. 本系列着重介绍Prometheus以及如何用它和其周边的生态来搭建一套属于自己的实时监控告警平台. 本系列受众对象为初次接触Prometheus的用户,大神勿喷,偏重于操作和实战,但是重要的概念也会精炼出提及下.系列主要分为以下几块 Prometheus各个概念介绍和搭建,如何抓取数据(本次分享内容)

  • Java中mybatis关于example类的使用详解

    这几天刚接触example,很多内容都是破碎的,写一篇博文加深理解. 一.什么是example类 mybatis-generator会为每个字段产生如上的Criterion,如果表的字段比较多,产生的Example类会十分庞大.理论上通过example类可以构造你想到的任何筛选条件.在mybatis-generator中加以配置,配置数据表的生成操作就可以自动生成example了.具体配置可以参考MBG有关配置.      下面是mybatis自动生成example的使用. 二.了解exampl

  • java使用xfire搭建webservice服务的过程详解

    前言 以前用的都是 apache 的cxf来搞webservice,今天做项目发现这个项目用的是 xfire,于是搭一个,写个demo用一下,在此记录一下过程. 搭建过程 本文使用的是maven形式的web工程.不知道如何搭建web工程的看上一篇博文. 引入xfire的依赖 <dependency> <groupId>org.codehaus.xfire</groupId> <artifactId>xfire-all</artifactId> &

  • java动态口令登录实现过程详解

    1.实现一个ItsClient 客户端用来实例化调用验证功能 public class ItsClient { private static final String routing = "/v1.0/sectoken/otp_validation"; // ! HTTPS消息验证地址 private String httpsVerifyUrl = ""; // ! otp ipAddr private String ipAddr = ""; /

随机推荐