资讯专栏INFORMATION COLUMN

Java动态编程初探

赵连江 / 3457人阅读

摘要:动态编程使用场景通过配置生成代码,减少重复编码,降低维护成本。动态生成字节码操作字节码的工具有,其中有两个比较流行的,一个是,一个是。

作者简介

传恒,一个喜欢摄影和旅游的软件工程师,先后从事饿了么物流蜂鸟自配送和蜂鸟众包的开发,现在转战 Java,目前负责物流策略组分流相关业务的开发。

什么是动态编程

动态编程是相对于静态编程而言的,平时我们讨论比较多的静态编程语言例如Java, 与动态编程语言例如JavaScript相比,二者有什么明显的区别呢? 简单的说就是在静态编程中,类型检查是在编译时完成的,而动态编程中类型检查是在运行时完成的, 所谓动态编程就是绕过编译过程在运行时进行操作的技术。

动态编程使用场景

通过配置生成代码,减少重复编码,降低维护成本。

AOP的一种实现方式,方便实现性能监控和分析,日志,事务,权限校验等。

实现新语言的语义,例如Groovy使用ASM生成字节码。

单元测试中动态mock测试依赖。

在Java中有如下几种方式实现动态编程:

反射

我们常用到的动态特性主要是反射,在运行时查找对象的属性和方法,修改作用域,通过方法名称调用方法等。在线的应用不建议频繁使用反射,因为反射的性能开销较大。

动态代理

在java的java.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口,通过这个类和这个接口可以生成JDK动态代理类和动态代理对象。

动态编译

动态编译是从Java 6开始支持的,主要是通过一个JavaCompiler接口来完成的。通过这种方式我们可以直接编译一个已经存在的java文件,也可以在内存中动态生成Java代码,动态编译执行。

调用Java Script引擎

Java 6加入了对Script(JSR223)的支持。这是一个脚本框架,提供了让脚本语言来访问Java内部的方法。你可以在运行的时候找到脚本引擎,然后调用这个引擎去执行脚本,这个脚本API允许你为脚本语言提供Java支持。

动态生成字节码

操作java字节码的工具有BECL/ASM/CGLIB/Javassist,其中有两个比较流行的,一个是ASM,一个是Javassist。 ASM直接操作字节码指令,执行效率高,要求使用者掌握Java类字节码文件格式及指令,对使用者的要求比较高。 Javassist提供了更高级的API,执行效率相对较差,但无需掌握字节码指令的知识,对使用者要求较低,所以接下来我们重点讲讲Javassist。

Javassist

Javassist是一个开源的分析、编辑和创建Java字节码的类库。 它是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶滋) 所创建的,目前已经加入到开放源代码JBoss应用服务器项目,JBoss通过使用Javassist对字节码进行操作,实现动态AOP框架。

Javassist(Java Programming Assistant) 使对Java字节码的操作变得简单,它使Java程序能够在运行时定义新类,并且可以在JVM加载时修改类文件。 与其它类似的字节码编辑器不同,它提供两个级别的API:源级别和字节码级别。 如果用户使用源级别API,他们可以在不知道Java字节码规范的情况下编辑类文件。整个API仅使用Java语言的词汇表进行设计,你甚至可以使用Java源代码的方式插入字节码。 另外,用户也可以使用字节码级别的API去直接编辑类文件。

// ClassPool 是 CtClass 对象的容器,存储着CtClass的Hash表。它按需读取类文件来构造CtClass对象,并且保存CtClass对象以便之后使用
ClassPool classPool = ClassPool.getDefault();
// CtClass 表示一个class文件,一个 GtClass(compile-time class)对象用来处理一个class文件,下面是从classpath中查找该类
CtClass ctClass = classPool.get("test.config.ConfigHandle");
// 通知编辑器去寻找对应的包
classPool.importPackage("org.mockito.Mockito");
classPool.importPackage("test.adapter.ext.IDowngrade");
classPool.importPackage("test.utils.property.IProperties");
// 使用removeField() removeMethod() 去删除对应的属性和方法
ctClass.removeField(ctClass.getDeclaredField("serviceHandle"));
ctClass.removeField(ctClass.getDeclaredField("switchHandle"));
ctClass.removeField(ctClass.getDeclaredField("configHandle"));
// CtMethod 和 CtConstructor 提供了 setBody() 方法去修改方法体
CtConstructor ctConstructor = ctClass.getDeclaredConstructors()[0];
ctConstructor.setBody("{this.mySwitch = Mockito.mock(IDowngrade.class);
" +
    "            this.myConfig = Mockito.mock(IProperties.class);}");
// toClass() 请求当前线程的 ClassLoader 去加载 CtClass 所代表的类文件
ctClass.toClass();
//输出成二进制格式
//byte[] b = ctClass.toBytecode();
//输出class文件到目录中
//ctClass.writeFile("/tmp");

ClassPool是CtClass对象的容器,因为编译器在编译引用CtClass代表的Java类的源代码时,可能会引用CtClass对象,所以一旦一个CtClass被创建,它就被保存在ClassPool中。

如果事先知道要修改哪些类,修改类的最简单方法如下:

    调用 ClassPool.get() 获取 CtClass 对象

    修改对象

    调用 CtClass 对象的 writeFile() 或者 toBytecode() 获得修改过的类文件。

如果需要定义一个新类,只需要

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("HelloWorld");
冻结classes

如果一个 CtClass 对象通过 writeFile(), toClass(), toBytecode()被转换成一个类文件,该CtClass对象会被冻结起来,不允许再修改,因为一个类只能被JVM加载一次。

CtClasss cc = ...;
    :
cc.writeFile();
cc.defrost();
cc.setSuperclass(...);    // 类已经被解冻
Class 搜索路径:

通过 ClassPool.getDefault() 获取的ClassPool默认使用JVM的类搜索路径。如果程序运行在JBoss或者Tomcat等Web服务器上,ClassPool可能无法找到用户自己定义的类,因为这种Web服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool必须添加额外的类搜索路径。

pool.insertClassPath(new ClassClassPath(this.getClass())); // 当前的类使用的类路径,注册到类搜索路径
pool.insertClassPath("/usr/local/javalib"); // 添加目录 /usr/local/javalib 到类搜索路径
ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist.");
pool.insertClassPath(cp); // 注册URL到搜索路径

在Java中,多个类加载器是可以共存的。每个类加载器创建了自己的命名空间,不同的类加载器可以加载具有相同类名的不同类文件,被加载的类也会被视为不同的类。此功能使我们能够在单个JVM上面运行多个应用程序,即使这些程序包含具有相同名称的类。

注意,JVM不允许动态重新加载类,一旦类加载器加载了一个类,就不能再在运行时重新加载该类的其它版本。因此,在JVM加载类之后,就不能再更改该类的定义。 但是,JPDA(Java平台调试器架构)提供有限的重新加载类的能力,如果相同的类文件由两个不同的类加载器加载,则JVM内会创建两个具有相同名称但是定义的不同的类。由于两个类不相同,所以一个类的实例不能被分配给另一个类的变量,两个类之间的转换操作也会失败并且抛出一个ClassCastException异常。

总结

Javassist比我们在本文中所讨论的功能要丰富得多,作为jboss的一个子项目,其主要的优点在于简单和快速,可以直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。如果你不是很了解虚拟机指令,可以采用javassist。

参考文档:

www.javassist.org/tutorial/tu…

en.wikipedia.org/wiki/Javass…





阅读博客还不过瘾?

欢迎大家扫二维码通过添加群助手,加入交流群,讨论和博客有关的技术问题,还可以和博主有更多互动

博客转载、线下活动及合作等问题请邮件至 shadowfly_zyl@hotmail.com 进行沟通

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/7354.html

相关文章

  • 初探Kotlin+SpringBoot联合编程

    摘要:是一门最近比较流行的静态类型编程语言,而且和一样同属系。这个生成的构造函数是合成的,因此不能从或中直接调用,但可以使用反射调用。 showImg(https://segmentfault.com/img/remote/1460000012958496); Kotlin是一门最近比较流行的静态类型编程语言,而且和Groovy、Scala一样同属Java系。Kotlin具有的很多静态语言...

    xiaokai 评论0 收藏0
  • Android Flutter实践内存初探

    摘要:摘要实践内存初探闲鱼技术匠修我们想使用来统一移动开发并做了一些实践。将内存管理分为新生代和老年代。在标记阶段,所有线程参与并发的完成对回收对象的标记,降低标记阶段耗时。的首帧渲染耗时较高,在版本有明显感受,大概会黑屏秒,版本会好很多。 摘要: Android Flutter实践内存初探 闲鱼技术-匠修我们想使用Flutter来统一移动App开发并做了一些实践。移动设备上的资源有限,通常...

    dabai 评论0 收藏0
  • Python Metaclass 初探

    摘要:以我们的程序为例,就是以为产生了一个名为的新类型,改类型的实现由给出,而就包含了通过返回的这个方法。从中找到这些类并一一执行测试。 先以一个大牛的一段关于Python Metapgramming的著名的话来做开头: Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder ...

    nidaye 评论0 收藏0
  • 云智慧压测实战分享之JMeter工具使用初探

    摘要:有了测试脚本,通过线程组来模拟真实用户对服务器的访问压力。不同的是,这些类型的线程执行测试结束后执行定期的线程组。线程组中包含的线程数量在测试执行过程中是不会发生改变的。逻辑控制器元件只对其子节点中的取样器和逻辑控制器作用。 工欲善其事必先利其器,要保证移动应用产品在上线之后能稳定运行于各种复杂环境,仅仅进行功能测试是远远不够的,压力测试越来越被应用开发商所重视。而压力测试从传统的内部...

    venmos 评论0 收藏0
  • [Java并发-6]“管程”-java管程初探

    摘要:语言在之前,提供的唯一的并发原语就是管程,而且之后提供的并发包,也是以管程技术为基础的。但是管程更容易使用,所以选择了管程。线程进入条件变量的等待队列后,是允许其他线程进入管程的。并发编程里两大核心问题互斥和同步,都可以由管程来帮你解决。 并发编程这个技术领域已经发展了半个世纪了。有没有一种核心技术可以很方便地解决我们的并发问题呢?这个问题, 我会选择 Monitor(管程)技术。Ja...

    Steve_Wang_ 评论0 收藏0

发表评论

0条评论

赵连江

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<