Java 热部署深入探索Word格式.docx
- 文档编号:20250821
- 上传时间:2023-01-21
- 格式:DOCX
- 页数:8
- 大小:74.24KB
Java 热部署深入探索Word格式.docx
《Java 热部署深入探索Word格式.docx》由会员分享,可在线阅读,更多相关《Java 热部署深入探索Word格式.docx(8页珍藏版)》请在冰豆网上搜索。
自定义加载器仍然需要执行类加载的功能。
这里却存在一个问题,同一个类加载器无法同时加载两个相同名称的类,由于不论类的结构如何发生变化,生成的类名不会变,而classloader只能在虚拟机停止前销毁已经加载的类,这样classloader就无法加载更新后的类了。
这里有一个小技巧,让每次加载的类都保存成一个带有版本信息的class,比如加载Test.class时,保存在内存中的类是Test_v1.class,当类发生改变时,重新加载的类名是Test_v2.class。
但是真正执行加载class文件创建class的defineClass方法是一个native的方法,修改起来又变得很困难。
所以面前还剩一条路,那就是直接修改编译生成的class文件。
利用ASM修改class文件
可以修改字节码的框架有很多,比如ASM,CGLIB。
本文使用的是ASM。
先来介绍一下class文件的结构,class文件包含了以下几类信息,一个是类的基本信息,包含了访问权限信息,类名信息,父类信息,接口信息。
第二个是类的变量信息。
第三个是方法的信息。
ASM会先加载一个class文件,然后严格顺序读取类的各项信息,用户可以按照自己的意愿定义增强组件修改这些信息,最后输出成一个新的class。
首先看一下如何利用ASM修改类信息。
清单1.利用ASM修改字节码
ClassWritercw=newClassWriter(ClassWriter.<
em>
COMPUTE_MAXS<
/em>
);
ClassReadercr=null;
StringenhancedClassName=classSource.getEnhancedName();
try{
cr=newClassReader(newFileInputStream(
classSource.getFile()));
}catch(IOExceptione){
e.printStackTrace();
returnnull;
}
ClassVisitorcv=newEnhancedModifier(cw,
className.replace("
."
"
/"
),
enhancedClassName.replace("
));
cr.accept(cv,0);
ASM修改字节码文件的流程是一个责任链模式,首先使用一个ClassReader读入字节码,然后利用ClassVisitor做个性化的修改,最后利用ClassWriter输出修改后的字节码。
之前提过,需要将读取的class文件的类名做一些修改,加载成一个全新名字的派生类。
这里将之分为了2个步骤。
第一步,先将原来的类变成接口。
清单2.重定义的原始类
publicClass&
lt;
?
&
gt;
redefineClass(StringclassName){
ClassSourcecs=<
classFiles<
.get(className);
if(cs==null){
cr=newClassReader(newFileInputStream(cs.getFile()));
ClassModifiercm=newClassModifier(cw);
cr.accept(cm,0);
byte[]code=cw.toByteArray();
returndefineClass(className,code,0,code.length);
}
首先load原始类的class文件,此处定义了一个增强组件
ClassModifier,作用是修改原始类的类型,将它转换成接口。
原始类的所有方法逻辑都会被去掉。
第二步,生成的派生类都实现这个接口,即原始类,并且复制原始类中的所有方法逻辑。
之后如果该类需要更新,会生成一个新的派生类,也会实现这个接口。
这样做的目的是不论如何修改,同一个class的派生类都有一个共同的接口,他们之间的转换变得对外不透明。
清单3.定义一个派生类
//在class文件发生改变时重新定义这个类
privateClass&
redefineClass(StringclassName,ClassSourceclassSource){
classSource.update();
cr=newClassReader(
newFileInputStream(classSource.getFile()));
EnhancedModifierem=newEnhancedModifier(cw,className.replace("
ExtendModifierexm=newExtendModifier(em,className.replace("
cr.accept(exm,0);
classSource.setByteCopy(code);
Class&
clazz=defineClass(enhancedClassName,code,0,code.length);
classSource.setClassCopy(clazz);
returnclazz;
再次load原始类的class文件,此处定义了两个增强组件,一个是
EnhancedModifier,这个增强组件的作用是改变原有的类名。
第二个增强组件是
ExtendModifier,这个增强组件的作用是改变原有类的父类,让这个修改后的派生类能够实现同一个原始类(此时原始类已经转成接口了)。
自定义classloader还有一个作用是监听会发生改变的class文件,classloader会管理一个定时器,定时依次扫描这些class文件是否改变。
改变创建对象的行为
Java虚拟机常见的创建对象的方法有两种,一种是静态创建,直接new一个对象,一种是动态创建,通过反射的方法,创建对象。
由于已经在自定义加载器中更改了原有类的类型,把它从类改成了接口,所以这两种创建方法都无法成立。
我们要做的是将实例化原始类的行为变成实例化派生类。
对于第一种方法,需要做的是将静态创建,变为通过classloader获取class,然后动态创建该对象。
清单4.替换后的指令集所对应的逻辑
//原始逻辑
Greeterp=newGreeter();
//改变后的逻辑
IGreeterp=(IGreeter)MyClassLoader.<
getInstance<
().
findClass("
com.example.Greeter"
).newInstance();
这里又需要用到ASM来修改class文件了。
查找到所有new对象的语句,替换成通过classloader的形式来获取对象的形式。
清单5.利用ASM修改方法体
@Override
publicvoidvisitTypeInsn(intopcode,Stringtype){
if(opcode==Opcodes.<
NEW<
&
amp;
type.equals(className)){
List&
LocalVariableNode&
variables=node.localVariables;
StringcompileType=null;
for(inti=0;
i&
variables.size();
i++){
LocalVariableNodelocalVariable=variables.get(i);
compileType=<
formType<
(localVariable.desc);
if(matchType(compileType)&
!
valiableIndexUsed[i]){
valiableIndexUsed[i]=true;
break;
}
}
mv.visitMethodInsn(Opcodes.<
INVOKESTATIC<
<
CLASSLOAD_TYPE<
"
getInstance"
()L"
+<
+"
;
"
mv.visitLdcInsn(type.replace("
INVOKEVIRTUAL<
findClass"
(Ljava/lang/String;
)Ljava/lang/Class;
java/lang/Class"
newInstance"
()Ljava/lang/Object;
mv.visitTypeInsn(Opcodes.<
CHECKCAST<
compileType);
flag=true;
}else{
mv.visitTypeInsn(opcode,type);
对于第二种创建方法,需要通过修改
Class.forName()和
ClassLoader.findClass()的行为,使他们通过自定义加载器加载类。
使用JavaAgent拦截默认加载器的行为
之前实现的类加载器已经解决了热部署所需要的功能,可是JVM启动时,并不会用自定义的加载器加载classpath下的所有class文件,取而代之的是通过应用加载器去加载。
如果在其之后用自定义加载器重新加载已经加载的class,有可能会出现LinkageError的exception。
所以必须在应用启动之前,重新替换已经加载的class。
如果在jdk1.4之前,能使用的方法只有一种,改变jdk中classloader的加载行为,使它指向自定义加载器的加载行为。
好在jdk5.0之后,我们有了另一种侵略性更小的办法,这就是JavaAgent方法,JavaAgent可以在JVM启动之后,应用启动之前的短暂间隙,提供空间给用户做一些特殊行为。
比较常见的应用,是利用JavaAgent做面向方面的编程,在方法间加入监控日志等。
JavaAgent的实现很容易,只要在一个类里面,定义一个premain的方法。
清单6.一个简单的JavaAgent
publicclassReloadAgent{
publicstaticvoidpremain(StringagentArgs,Instrumentationinst){
GeneralTransformertrans=newGeneralTransformer();
inst.addTransformer(trans);
然后编写一个manifest文件,将
Premain-Class属性设置成定义一个拥有
premain方法的类名即可。
生成一个包含这个manifest文件的jar包。
manifest-Version:
1.0
Premain-Class:
com.example.ReloadAgent
Can-Redefine-Classes:
true
最后需要在执行应用的参数中增加
-javaagent参数,加入这个jar。
同时可以为Javaagent增加参数,下图中的参数是测试代码中testproject的绝对路径。
这样在执行应用的之前,会优先执行
premain方法中的逻辑,并且预解析需要加载的class。
图1.增加执行参数
这里利用
JavaAgent替换原始字节码,阻止原始字节码被Java虚拟机加载。
只需要实现
一个ClassFileTransformer的接口,利用这个实现类完成class替换的功能。
清单7.替换class
publicbyte[]transform(ClassLoaderparamClassLoader,StringparamString,
paramClass,ProtectionDomainparamProtectionDomain,
byte[]paramArrayOfByte)throwsIllegalClassFormatException{
StringclassName=paramString.replace("
if(className.equals("
com.example.Test"
)){
MyClassLoadercl=MyClassLoader.<
();
cl.defineReference(className,"
example.Greeter"
returncl.getByteCode(className);
}elseif(className.equals("
cl.redefineClass(className);
至此,所有的工作大功告成,欣赏一下hotswap的结果吧。
图2.Test执行结果
结束语
解决hotswap是个困难的课题,本文解决的仅仅是让新实例化的对象使用新的逻辑,并不能改变已经实例化对象的行为,如果JVM能够重新设计class的生命周期,支持运行时重新更新一个class,hotswap就会成为Java的一个闪亮新特性。
官方的JVM一直没有解决热部署这个问题,可能也是由于无法完全克服其中的诸多难点,希望未来的Jdk能解决这个问题,让Java应用对于更新更友好,避免不断重启应用浪费的时间。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Java 热部署深入探索 部署 深入 探索