使用javassist运行时动态重新加载java类及其他替换选择

在不少的情况下,我们需要对生产中的系统进行问题排查,但是又不能重启应用,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之于IoC。但是直接基于JPDA的效率就太低了,所以就需要使用字节码修改技术了,

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

由于我们在项目中适用javassist,所以这里以Javassist为例。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)类能够更加方便的动态重新加载类。如下所示:

/**
 * 
 */
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定期检查某个参数或者环境变量,如果发生了变化则根据约定的规范动态加载指定接口的实现,在该动态加载的实现中实现调试的逻辑。

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

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值