在不少的情况下,我们需要对生产中的系统进行问题排查,但是又不能重启应用,java应用不同于数据库的存储过程,至少到目前为止,还不能原生的支持随时进行编译替换,从这种角度来说,数据库比java的动态性要好得多,而且其随时编译的性能也比其他解释性语言的性能要好的多。虽然如此,我们绝大部分应用都使用java编写,所以还是得尽可能的为随时问题排查做准备,尤其是对于提供行业应用托管的系统来说。

  在本文中,主要说下动态重新加载类的实现以及其他替换选择。

  默认情况下,当加载了一个类后,再遇到相同的类时,它不会再次加载它,而是跳过。

  如果JVM通过JPDA (Java Platform Debugger Architecture)选项启动,那么其中的类是可以重新加载的,只不过类的接口必须是相同或者兼容的。

  关于JPDA的详细信息,可以参考下列信息:

  • http://kyfxbl.iteye.com/blog/1697203
  • http://blog.sina.com.cn/s/blog_6e2d53050101j9wy.html
  • http://www.ibm.com/developerworks/cn/java/j-lo-jpda1/index.html
  • http://blog.csdn.net/sinat_18882775/article/details/51061946
  • http://docs.oracle.com/javase/7/docs/technotes/guides/jpda/jpda.html
  • http://bbs.pediy.com/showthread.php?p=1304860
  • http://jboss-javassist.github.io/javassist/html/javassist/util/HotSwapper.html
  • https://yq.aliyun.com/articles/20305

  不管使用其他什么方法,都是基于JPDA为基础,并在此基础上提供更加便利的方法,如同spring之于Lookup/JNDI机制。但是直接基于JPDA的效率就太低了,所以就广泛采用了字节码修改技术。

  最为流行的字节码操纵框架包括:

  由于我们在项目中适用javassist,所以这里以Javassist为例(cglib也不错)。Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。相对于ASM,它直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。

  在Javassist中,进行类表述的基本单元是CtClass(即“编译时的类”,compile time class)。组成程序的这些类会存储在一个ClassPool中,它本质上就是CtClass实例的一个容器。

  ClassPool的实现使用了一个HashMap,其中key是类的名称,而value是对应的CtClass对象。

  正常的Java类都会包含域、构造器以及方法。在CtClass中,分别与之对应的是CtField、CtConstructor和CtMethod。要定位某个CtClass,我们可以根据名称从ClassPool中获取,然后通过CtClass得到任意的方法,并做出我们的修改。如下所示:

  更详细的信息可参考http://jboss-javassist.github.io/javassist/。

  Javassist提供的javassist.util.HotSwapper(3.1之前则是javassist.tools.HotSwapper,BTrace也是使用HotSwapper机制)类能够更加方便的动态重新加载类。如下所示:

/**
*
*/
package com.ld.net.spider.example.standalone; /**
* @author zhjh256@163.com
* {@link} http://www.cnblogs.com/zhjh256
*/
public class Standard { /**
*
*/
public void doSomething() {
System.out.println("doSomething");
} }
package com.ld.net.spider.example.standalone;

import java.io.IOException;

import com.sun.jdi.connect.IllegalConnectorArgumentsException;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;
import javassist.util.HotSwapper; public class SpiderStandaloneMainAnb {
public static void main(String[] args) {
// ApplicationContext context = new ClassPathXmlApplicationContext("classpath*:spider-base-service.xml");
// System.out.println("---" + context.getApplicationName());
Standard standard = new Standard();
standard.doSomething();
ClassPool pool = ClassPool.getDefault();
try {
CtClass clazz = pool.get("com.ld.net.spider.example.standalone.Standard");
CtMethod cm = clazz.getDeclaredMethod("doSomething");
cm.insertAt(1,"{System.out.println(\"hello HotSwapper.\");}"); // clazz完全可以是全新的,这里只是为了测试方便而已
HotSwapper swap = new HotSwapper(8000);
swap.reload("com.ld.net.spider.example.standalone.Standard", clazz.toBytecode());
standard.doSomething();
} catch (CannotCompileException | IOException | IllegalConnectorArgumentsException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

Listening for transport dt_socket at address: 8000
doSomething

=======
hello HotSwapper.
doSomething

  注意:需要把tools.jar加到classpath,同时增加JVM选项

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000

  动态创建构造器。我们知道java反射的性能比较低,对于field较多的pojo,按照一个个属性去反射的性能就更低了,有了动态更改字节码的技术,这个问题就可以不复存在,我们可以在运行时动态根据pojo的定义创建相应字段作为参数的构造器,然后在反射时就只需要调用一次完成对象的构造,而不是和原来一样先构造一个空的对象,然后一个个setter去回调,如下所示:

/**
* @author zhjh256@163.com
* {@link} http://www.cnblogs.com/zhjh256
*/
public class Hello {
private String value;
private String value2;
public void say() {
System.out.println(value + "," + value2);
}
} import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;
import javassist.Modifier; /**
*
*/ /**
* @author zhjh256@163.com
* {@link} http://www.cnblogs.com/zhjh256
*/
public class Test {
public static void main(String[] args) throws Exception {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("Hello");
CtConstructor constructor = new CtConstructor(new CtClass[] { cp.get(String.class.getName()),cp.get(String.class.getName()) }, cc);
constructor.setModifiers(Modifier.PUBLIC);
constructor.setBody("{this.value=$1;this.value2=$2;}");
cc.addConstructor(constructor);
CtMethod m = cc.getDeclaredMethod("say");
m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
Class<?> c = cc.toClass();
Hello h = (Hello) c.getConstructor(String.class, String.class).newInstance(new Object[] { "javassist", "param2" });
h.say();
}
}
Hello.say():
javassist,param2

  虽然Javassist能够提供动态重新加载类的功能,不过由于它要求启用JPDA,一定程度上会损耗不少性能、留下了潜在的安全漏洞(因为是公开的规范,更容易被攻击),  同时,它仍然要求在系统设计的时候将Javassist纳入体系。所以应该来说,除非为了设计IDE,更加合适的设计应该是这样的,而不是借助于JPDA的架构:

  • 1、在关键服务环节(最好按照最小粒度模块为单位以最小化影响)预留空的AOP点和指定接口;
  • 2、设置一个watcher定期检查某个参数或者环境变量,如果发生了变化则根据约定的规范动态加载指定接口的实现,在该动态加载的实现中实现调试的逻辑。

  不过,在这里最重要的是绝大部分系统都是现成的,而不是全新开发的系统。这就使得现有的三方类库可能存在各种不兼容或者过新的情况,如何在不影响开发的情况下,将所有这些类库和配置参数等透明的切换并且让所有开发都心悦诚服的调整通常是其中最难的部分。

  除此之外,还可以在程序启动前创建全新的类(我们在一些需要高度灵活的场景下就通过从特定位置)。如下:

        ClassPool pool = ClassPool.getDefault();
// 创建类
CtClass ct = pool.makeClass("com.zhi.Person");
// 让类实现Cloneable接口
ct.setInterfaces(new CtClass[] { pool.makeInterface("java.lang.Cloneable") }); // 添加一个int类型的共有属性
CtField fieldId = new CtField(CtClass.intType, "id", ct);
fieldId.setModifiers(AccessFlag.PUBLIC);
ct.addField(fieldId); // 添加一个默认构造器
CtConstructor constructor1 = CtNewConstructor.make("public Person(){this.id=1;}", ct);
ct.addConstructor(constructor1); // 添加方法
CtMethod helloM = CtNewMethod
.make("public void hello(String des){System.out.println(\"执行hello方法,\"+des+\",我的id是\"+this.id);}", ct);
ct.addMethod(helloM); // 将生成的.class文件保存到磁盘
ct.writeFile(); // 加载目标类,可用ct.toClass()或new Loader(pool).loadClass()
Class<?> clazz = ct.toClass();
// Class<?> clazz = new Loader(pool).loadClass("com.zhi.Person"); // 输出类基本信息
System.out.println("包名:" + clazz.getPackageName());
System.out.println("类名:" + clazz.getName());
System.out.println("简要类名:" + clazz.getSimpleName());
System.out.println("限定符:" + Modifier.toString(clazz.getModifiers()));
System.out.println("继承类:" + clazz.getSuperclass().getName());
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println("属性名称:" + field.getName() + ",属性类型:" + field.getType() + ",限定符:"
+ Modifier.toString(field.getModifiers()));
} // 构造一个对象,并执行hello方法
Object ob = clazz.getDeclaredConstructor().newInstance();
clazz.getMethod("hello", String.class).invoke(ob, "张三");

  参考:http://www.javassist.org/

使用javassist运行时动态重新加载java类及其他替换选择的更多相关文章

  1. 在Android的App中动态的加载Java类

    原文的地址:http://yenliangl.blogspot.com/2009/11/dynamic-loading-of-classes-in-your.html 我正在编写一个应用程序能够加载别 ...

  2. 未能加载文件或程序集“BLL”或它的某一个依赖项。生成此程序集的运行时比当前加载的运行时新,无法加载此程序集。

    今天使用VS2012创建项目的时候,考虑到项目中代码的重用性以及清晰简洁性,搭建了一个三层架构,但是在项目运行的时候,总是报错: “未能加载文件或程序集“BLL”或它的某一个依赖项.生成此程序集的运行 ...

  3. vs2015运行时提示未加载vcruntime140.adm64.pb

    后调试查看发现 vs2015运行时提示未加载vcruntime140.adm64.pb 解决方案:去微软官网下载安装 vc_redist.exe ,安装就可以了.有64位版和32位版,根据计算机配置进 ...

  4. 《深入理解Java虚拟机》- Java虚拟机是如何加载Java类的?

    Java虚拟机是如何加载Java类的?  这个问题也就是面试常问到的Java类加载机制.在年初面试百战之后,菜鸟喜鹊也是能把这流程倒背如流啊!但是,也只是字面上的背诵,根本就是像上学时背书考试一样. ...

  5. 自己动手实现springboot运行时执行java源码(运行时编译、加载、注册bean、调用)

    看来断点.单步调试还不够硬核,根本没多少人看,这次再来个硬核的.依然是由于apaas平台越来越流行了,如果apaas平台选择了java语言作为平台内的业务代码,那么不仅仅面临着IDE外的断点.单步调试 ...

  6. 03 Java 虚拟机是如何加载 Java 类的

    Java 引用类型 Java 中的引用类型细分为四种:类,接口,数组类和泛型参数. 因为泛型参数会在编译过程中被擦除,所以 Java 虚拟机实际上只有前三种.数组类是由 Java 虚拟机直接生成的,其 ...

  7. IDEA 配置Jrebet 自动加载Java类,

    官方文档地址: http://manuals.zeroturnaround.com/jrebel/ide/intellij.html#installation 过期激活 https://blog.cs ...

  8. linux动态库加载RPATH, RUNPATH

    摘自http://gotowqj.iteye.com/blog/1926771 linux动态库加载RPATH, RUNPATH 链接动态库 如何程序在连接时使用了共享库,就必须在运行的时候能够找到共 ...

  9. linux动态库加载的秘密

    摘自http://gotowqj.iteye.com/blog/1926734 摘自http://www.360doc.com/content/14/0313/13/12747488_36024641 ...

随机推荐

  1. 01--css编码技巧--css揭秘

    一 尽量减少代码重复 1.按钮 #btn { padding: .3em .8em; border: 1px solid #446d88; background: #58a linear-gradie ...

  2. import,include

    1. #import导入头文件,即:导入头文件中的内容到当前类 2. #import ""导⼊自定义类,#import <>导入类库中的头文件. 3.功能类似C语言中的 ...

  3. 网络叠加模式VLAN、VxLAN、GRE

    什么是叠加网络 1.一个数据包(或帧)封装在另一个数据包内;被封装的包转发到隧道端点后再被拆装. 2.叠加网络就是使用这种所谓"包内之包"的技术安全地将一个网络隐藏在另一个 网络中 ...

  4. 自定义配置文件的使用及加载-txt

    [Game] Version=1 [Login] Account = 阿斗阿斯顿撒 Password =我去饿我去恶趣味 Success = 成哥 Faild = 失败 [Job] Job1 = 战士 ...

  5. TCP/IP协议中backlog参数

    TCP建立连接是要进行三次握手,但是否完成三次握手后,服务器就处理(accept)呢? backlog其实是一个连接队列,在Linux内核2.2之前,backlog大小包括半连接状态和全连接状态两种队 ...

  6. 【原生js】原生js的省市区三级联动

    html: <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" c ...

  7. [读书笔记]python3.5实现socket通讯(TCP)

    TCP连接: tcp是面向连接的一个协议,意味着,客户端和服务器开发发送数据之前,需要先握手创建一个TCP连接.TCP连接的一端与客户端套接字相互联系,另一端与服务器套接字相联系.当创建该TCP连接的 ...

  8. Linux下制作静(动)态库

    关键命令: 动态库制作命令 gcc xxx.c -fPIC -shared -o libxxx.so 静态库制作命令 gcc -c xxx.c ar crv libxxx.a xxx.o 例: //h ...

  9. MSSQL2008 中文乱码问题 (引自ljg888的专栏)

    PHP向MSSQL2008中写入数据,中文乱码   首先:查看SQLserver编码格式的SQL语句为:     SELECT  COLLATIONPROPERTY('Chinese_PRC_Stro ...

  10. pymongo数据报表脚本

    最近打算将平时自己的一些小工具整理整理,由于一直使用python写脚本,而且数据库一直使用MongoDB,所以直接使用pymonogo驱动数据库做一些报表生成的小脚本,此次的脚本主要针对每个月中公司业 ...