虚拟机实践
在Class文件格式与执行引擎这部分中,用户的程序能直接影响的内容并不太多,Class文件以何种格式存储,类型何时加载、如何连接,以及虚拟机如何执行字节码指令等都是由虚拟机直接控制的行为,用户程序无法对其进行改变。能通过程序进行操作的,主要是 字节码生成 与 类加载器 这两部分的功能,但仅仅在如何处理这两点上,就已经出现了许多值得欣赏和借鉴的思路,这些思路后来成为了许多常用功能和程序实现的基础
类加载器
Tomcat
主流的Java Web服务器,如 Tomcat 、 Jetty 、 WebLogic 、 WebSphere 等,都 实现了自己定义的类加载器 (一般都不止一个)。因为一个功能健全的Web服务器,要解决如下几个问题:
- 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离
两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库可以互相独立使用
部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享
用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,如果把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费 这主要倒不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到服务器内存,如果类库不能共享,虚拟机的方法区就会很容易出现过度膨胀
- 服务器需要尽可能地 保证自身的安全不受部署的Web应用程序影响
有许多主流的Java Web服务器自身也是使用Java语言来实现的 因此,服务器本身也有类库依赖的问题 一般来说,基于安全考虑,服务器所使用的类库应该与应用程序的类库互相独立
- 支持 JSP 应用的Web服务器,大多数都需要 支持热部署 功能
JSP文件最终要编译成Java Class才能由虚拟机执行,但JSP文件由于其纯文本存储的特性,运行时修改的概率远远大于第三方类库或程序自身的Class文件 ASP、PHP和JSP这些网页应用也把修改后无须重启作为一个很大的优势来看待,因此主流的Web服务器都会支持JSP生成类的热替换 当然也有非主流的,如运行在生产模式(Production Mode)下的Web Logic服务器默认就不会处理JSP文件的变化
由于存在上述问题,在部署Web应用时,单独的一个ClassPath就无法满足需求了,所以各种Web服务器都地提供了好几个ClassPath路径供用户存放第三方类库,这些路径一般都以 lib 或 classes 命名。被放置到不同路径中的类库,具备不同的访问范围和服务对象,通常,每一个目录都会有一个相应的自定义类加载器去加载放置在里面的Java类库。以Tomcat服务器为例,看一看Tomcat具体是如何规划用户类库结构和类加载器的
在Tomcat目录结构中,有3组目录 /common/* 、 /server/* 和 /shared/* )可以存放Java类库,另外还可以加上Web应用程序自身的目录 /WEB-INF/* ,一共4组,把Java类库放置在这些目录中的含义分别如下:
- 放置在 /common 目录中:类库可 被Tomcat和所有的Web应用程序 共同使用
- 放置在 /server 目录中:类库可 被Tomcat使用 ,对所有的Web应用程序都不可见
- 放置在 /shared 目录中:类库可被 所有的Web应用程序共同使用 ,但对Tomcat自己不可见
- 放置在 /WebApp/WEB-INF 目录中:类库 仅仅可以被此Web应用程序 使用,对Tomcat和其他Web应用程序都不可见
为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的 双亲委派模型 来实现,其关系如图所示:
最上层的3个类加载器是JDK默认提供的类加载器,这3个加载器的作用已经介绍过了。 、 、和 则是Tomcat自己定义的类加载器,
- CommonClassLoader : 加载 /common/*
- CatalinaClassLoader : 加载 /server/*
- SharedClassLoader : 加载 /shared/*
- WebappClassLoader : 加载 /WebApp/WEB-INF/* 中的Java类库
- JsperLoader : 加载 /WebApp/WEB-INF/* 中的Jsp文件
WebApp类加载器和Jsp类加载器通常会存在多个实例:
- 每一个Web应用程序对应一个WebApp类加载器
- 每一个JSP文件对应一个Jsp类加载器
从图中委派关系可以看出:
- CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用
- CatalinaClassLoader 和 SharedClassLoader 自己能加载的类则与对方相互隔离
- WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,各个 WebAppClassLoader 实例之间相互隔离
- JasperLoader 的加载范围仅仅是这个JSP文件所编译出来的那一个Class,它出现的目的就是 为了被丢弃 :
- 当服务器检测到JSP文件被修改时,会替掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的Hot Swap功能
对于Tomcat的6.x版本的默认配置下,/common、/server和/shared三个目录已经合并到一起变成/lib目录了,这个目录里的类库相当于以前/common目录中类库的作用 只有指定了catalina.properties配置文件的server.loader和share.loader项后才会真正建立CatalinaClassLoader和SharedClassLoader的实例,否则会用到这两个类加载器的地方会用CommonClassLoader的实例代替 这是Tomcat设计团队为了简化大多数的部署场景所做的一项改进,如果默认设置不能满足需要,用户可以通过修改配置文件指定server.loader和share.loader的方式重新启用Tomcat5.x的加载器架构
总结:Tomcat加载器的实现 清晰易懂 ,并且采用了官方推荐的 正统 的使用类加载器的方式
如果有10个Web应用程序都是用Spring来进行组织和管理的话,可以把Spring放到Common或Shared目录下让这些程序共享 Spring要对用户程序的类进行管理,自然要能访问到用户程序的类,而用户的程序显然是放在/WebApp/WEB-INF目录中的,那么被CommonClassLoader或SharedClassLoader加载的Spring如何访问不在其加载范围内的用户程序? 可以尝试的答案是:使用线程上下文加载器
OSGi
OSGi 是OSGi联盟制定的一个基于Java语言的动态模块化规范,这个规范最初由Sun、IBM、爰立信等公司联合发起,目的是使服务提供商通过住宅网关为各种家用智能设备提供各种服务,后来这个规范在Java的其他技术领域也有相当不错的发展,现在已经成为Java世界中“事实上”的模块化标准,并且已经有了Equinox、Felix等成熟的实现。OSGi在Java程序员中最著名的应用案例就是 Eclipse ,另外还有许多大型的软件平台和中间件服务器都基于或声明将会基于OSGi规范来实现,如IBM Jazz平台 、 GlassFish服务器 、 jBoss OSGi 等
OSGi中的每个模块(称为 Bundle )与普通的Java类库区别并不太大,两者一般都以 JAR 格式进行封装,并且内部存储的都是Java Package 和 Class 。但是一个Bundle可以 声明它所依赖的Java Package (通过 Import-Package 描述),也可以声明它 允许导出发布的Java Package (通过 Export-Package 描述)。在OSGi里面, Bundle之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖 (至少外观上如此),而且 类库的可见性能得到非常精确的控制 ,一个模块里只有被Export过的Package才可能由外界访问,其他的Package和Class将会隐藏起来。除了更精确的模块划分和可见性控制外,基于OSGi的程序很可能(只是很可能,并不是一定会)可以实现 模块级的热插拔 功能,当程序升级更新或调试除错时,可以只停用、重新安装然后启用程序的其中一部分,这对企业级程序开发来说是一个非常有诱惑力的特性
OSGi之所以能有上述“诱人”的特点,要归功于它 灵活的类加载器架构 。OSGi的Bundle类加载器之间只有规则,没有固定的委派关系:
- 某个Bundle声明了一个它依赖的Package,如果有其他Bundle声明发布了这个Package,那么 所有对这个 Package的类加载动作 都会委派给发布它的Bundle类加载器 去完成
- 不涉及某个具体的Package时,各个Bundle加载器都是平级关系,只有具体使用某个Package和Class的时候,才会 根据 Package导入导出 定义来构造Bundle间的委派和依赖
- 一个Bundle类加载器为其他Bundle提供服务时,会 根据 Export-Package 列表严格控制访问范围
- 如果一个类存在于Bundle的类库中但是没有被Export,那么这个Bundle的类加载器能找到这个类,但不会提供给其他Bundle使用,而且OSGi平台也不会把其他Bundle的类加载请求分配给这个Bundle来办理
假设存在 Bundle A 、 Bundle B 、 Bundle C 三个模块,并且这三个Bundle定义的依赖关系如下:
- Bundle A: 声明发布了 package A ,依赖了 java.* 的包
- Bundle B: 声明依赖了 package A 和 package C ,同时也依赖了 java.* 的包
- Bundle C: 声明发布了 package C ,依赖了 package A
那么,这三个Bundle之间的类加载器及父类加载器之间的关系如图所示:
图中的类加载器都没有指明具体的加载器实现,只是一个体现了加载器之间关系的概念模型,并且只是体现了OSGi中 最简单的加载器委派关系 。一般来说,在OSGi中,加载一个类可能发生的查找行为和委派关系会复杂得多,类加载时可能进行的查找规则如下:
- 以 java.* 开头的类,委派给 父类加载器 加载
- 委派列表名单内 的类,委派给 父类加载器 加载
- Import列表 中的类,委派给 Export这个类的Bundle的类加载器 加载
- 查找 当前Bundle的Classpath ,使用 自己的类加载器 加载
- 查找是否在 自己的Fragment Bundle 中,如果是,则委派给 Fragment Bundle的类加载器 加载
- 查找 Dynamic Import列表的Bundle ,委派给 对应Bundle的类加载器 加载
- 类查找失败
在OSGi里面,加载器之间的关系不再是 双亲委派模型的树形结构 ,而是已经进一步发展成了一种更为复杂的、 运行时才能确定的网状结构
这种网状的类加载器架构在带来更好的灵活性的同时,也可能会产生许多新的隐患。在高并发环境下经常出现死锁 如果出现了Bundle A依赖Bundle B的Package B,而Bundle B又依赖了Bundle A的Package A,这两个Bundle进行类加载时就很容易发生死锁 当Bundle A加载Package B的类时,首先需要锁定当前类加载器的实例对象(java.lang.ClassLoader.loadClass()是一个synchronized方法) 然后把请求委派给Bundle B的加载器处理,如果这时候Bundle B也正好想加载Package A的类,它也先锁定自己的加载器再去请求Bundle A的加载器处理 这样,两个加载器都在等待对方处理自己的请求,而对方处理完之前自己又一直处于同步锁定的状态,因此它们就互相死锁,永远无法完成加载请求了 Equinox的BugList中有关于这类问题的Bug,也提供了一个以牺牲性能为代价的解决方案: 用户可以用osgi.classloader.singleThreadLoads参数来按单线程串行化的方式强制进行类加载动作 在JDK1.7中,为非树状继承关系下的类加载器架构进行了一次专门的升级,目的是从底层避免这类死锁出现的可能
总结:OSGi描绘了一个很美好的模块化开发的目标,而且定义了实现这个目标所需要的各种服务,同时也有成熟框架对其提供实现支持:
- 对于单个虚拟机下的应用,从开发初期就建立在OSGi上是一个很不错的选择,这样便于 约束依赖
- 但并非所有的应用都适合采用OSGi作为基础架构,OSGi在提供强大功能的同时,也引入了 额外的复杂度 ,带来了 线程死锁 和 内存泄漏 的风险
字节码
在Java里面使用字节码生成的例子:
- 如Web服务器中的JSP编译器
- 编译时植入的AOP框架
- 动态代理技术,
- 使用反射的时候虚拟机都有可能会在运行时生成字节码来提高执行速度
动态代理的实现
字节码生成并不是什么高深的技术,也先不必去想诸如Javassist 、 CGLib 、 ASM之类的字节码类库,因为JDK里面的javac命令就是字节码生成技术的“老祖宗” javac也是一个由Java语言写成的程序,它的代码存放在OpenJDK的langtools/src/share/classes/com/sun/tools/javac目录中 要深入了解字节码生成,阅读javac的源码是个很好的途径
即使没有直接使用过 Java.lang.reflect.Proxy 或实现过 java.lang.reflect.InvocationHandler 接口,应该也用过Spring来做过Bean的组织管理。如果使用过Spring,那大多数情况都会用过动态代理,因为如果Bean是面向接口编程,那么在Spring内部都是通过 动态代理 的方式来对Bean进行增强的
动态代理中所谓的 动态 ,是针对使用Java代码实际编写了代理类的 静态 代理而言的,它的优势不在于省去了编写代理类那一点工作量,而是实现了可以 在原始类和接口还未知的时候,就确定代理类的代理行为 ,当代理类与原始类脱离直接联系后,就可以很灵活地重用于不同的应用场景之中
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class DynamicProxyTest { interface IHello { void sayHello(); } static class Hello implements IHello { @Override public void sayHello() { System.out.println("hello world"); } } static class DynamicProxy implements InvocationHandler { Object originObj; Object bind(Object obj) { this.originObj = obj; return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("welcome"); return method.invoke(originObj, args); } } public static void main(String[] args) { IHello hello = (IHello) new DynamicProxy().bind(new Hello()); hello.sayHello(); } }
welcome hello world
上述代码里,唯一的 黑厘子 就是 Proxy.newProxyInstance() 方法,除此之外再没有任何特殊之处。这个方法返回一个实现了 IHello 的接口,并且代理了 newHello() 实例行为的对象。跟踪这个方法的源码,可以看到程序进行了 验证 、 优化 、 缓存 、 同步 、 生成字节码 、 显式类加载 等操作,前面的步骤并不是关注的重点,而最后它调用了 sun.misc.ProxyGenerator.generateProxyClass() 方法来完成生成字节码的动作,这个方法可以在 运行时产生一个描述代理类的字节码byte[]数组 。如果想看一看这个在运行时产生的代理类中写了些什么,可以在main()方法中加入下面这句:
System.getProperties().put ("sun.misc.ProxyGenerator.saveGeneratedFiles" ,"true");
加入这句代码后再次运行程序,磁盘中将会产生一个名为 $Proxy0.class 的代理类Class文件,反编译后可以看见:
import DynamicProxyTest.IHello; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.lang.reflect.UndeclaredThrowableException; final class $Proxy0 extends Proxy implements IHello { private static Method m1; private static Method m3; private static Method m2; private static Method m0; public $Proxy0(InvocationHandler var1) throws { super(var1); } public final void sayHello() throws { try { //super.h实际是Proxy.newProxyInstance()这个方法的第三个绑定的参数 //也就是new DynamicProxy()生成的实例变量 super.h.invoke(this, m3, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final String toString() throws { try { return (String)super.h.invoke(this, m2, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } static { try { m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); m3 = Class.forName("DynamicProxyTest$IHello").getMethod("sayHello"); m2 = Class.forName("java.lang.Object").getMethod("toString"); m0 = Class.forName("java.lang.Object").getMethod("hashCode"); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError(var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError(var3.getMessage()); } } }
$Proxy0 这个代理类为传入接口中的每一个方法,以及从 java.lang.Object 中继承来的 equals() 、 hashCode() 、 toString() 方法都生成了对应的实现,并且统一调用了 InvocationHandler对象的 invoke() 方法 : 代码中的 this.h 就是父类Proxy中保存的 InvocationHandler 实例变量)来实现这些方法的内容,各个方法的区别不过是 传入的参数和Method对象有所不同 而已,所以无论调用动态代理的哪一个方法,实际上都是在 执行InvocationHandler.invoke() 中的代理逻辑
这个例子中并没有讲到generateProxyClass()方法具体是如何产生代理类“$Proxy0.class”的字节码的,大致的生成过程其实就是根据Class文件的格式规范去拼装字节码 但在实际开发中,以byte为单位直接拼装出字节码的应用场合很少见,这种生成方式也只能产生一些高度模板化的代码 对于用户的程序代码来说,如果有要大量操作字节码的需求,还是使用封装好的字节码类库比较合适 如果对动态代理的字节码拼装过程很感兴趣,可以在OpenJDK的jdk/src/share/classes/sun/misc目录下找到sun.misc.ProxyGenerator的源码
Retrotranslator
在Java世界里,每一次JDK大版本的发布,都伴随着一场大规模的技术革新,而对Java程序编写习惯改变最大的,无疑是JDK1.5的发布 自动装箱、泛型、动态注解、枚举、变长参数、遍历循环(foreach循环)…… 在没有这些语法特性的年代,Java程序也照样能写,但是现在看来,上述每一种语法的改进几乎都是“必不可少”的 但因为要保护现有投资、维持程序结构稳定等,必须使用1.5以前版本的JDK 为了把JDK1.5中编写的代码放到JDK1.4或1.3的环境中去部署使用,一种名为Java逆向移植的工具应运而生, 而Retrotranslator是这类工具中较出色的一个
Retrotranslator 的作用是将JDK 1.5编译出来的Class文件转变为可以在JDK 1.4或 1.3上部署的版本,它可以很好地支持自动装箱、泛型、动态注解、枚举、变长参数、遍历循环、静态导入这些语法特性,甚至还可以支持JDK1.5中新增的集合改进、并发包以及对泛型、注解等的反射操作
首先要弄清楚JDK升级中会提供哪些新的功能。JDK每次升级新增的功能大致可以分为以下4类:
- 编译器 :
- 自动装箱拆箱 :实际上就是编译器在程序中使用到包装对象的地方自动插入了很多 Integer.valueOf() 、 Float.valueOf() 之类的代码
- 变长参数 :在编译之后就 自动转化成了一个数组 来完成参数传递
- 泛型 :信息则在 编译阶段就已经擦除掉了 (但是在 元数据中还保留 着),相应的地方被编译器自动插入了 类型转换代码
- Java API 的代码增强:
- JDK 1.2时代引入的 java.util.Collections 等一系列 集合类
- JDK 1.5时代引入的 java.util.concurrent 并发包 等
- 字节码 中进行支持的改动,字节码指令集一直处于相对比较稳定的状态,这种需要在字节码层面直接进行的改动是比较少见的:
- JDK 1.7里面新加入的语法特性: 动态语言支持 ,就需要在虚拟机中新增一条 invokedynamic 字节码指令来实现相关的调用功能
- 虚拟机内部 ,这类改动对于程序员编写代码基本是透明的,但会对程序运行时产生影响:
- JDK 1.5中实现的 JSR-133 规范重新定义的 Java内存模型
- CMS收集器 之类的改动
上述4类新功能中,Retrotranslator只能模拟前两类,对于后面两类直接在虚拟机内部实现的改进,一般所有的逆向移植工具都是无能为力的,至少不能完整地或者在可接受的效率上完成全部模拟,否则虚拟机设计团队也没有必要舍近求远地改动处于JDK底层的虚拟机。在可以模拟的两类功能中,第二类模拟相对更容易实现一些,如JDK 1.5引入的java.util.concurrent包,实际是由多线程大师DougLea开发的一套并发包,在JDK1.5出现之前就已经存在(那时候名字叫做dl.util.concurrent),所以要在旧的JDK中支持这部分功能,以独立类库的方式便可实现。Retrotranslator中附带了一个名叫 backport-util-concurrent.jar 的类库来代替JDK 1.5的并发包
至于JDK在 编译阶段进行处理的那些改进 ,Retrotranslator则是使用 ASM框架直接对字节码 进行处理。由于组成Class文件的字节码指令数量并没有改变,所以无论是JDK 1.3、JDK 1.4还是JDK 1.5,能用字节码表达的语义范围应该是一致的。当然,肯定不可能简单地把Class的文件版本号从49.0改回48.0就能解决问题了,虽然字节码指令的数量没有变化,但是元数据信息和一些语法支持的内容还是要做相应的修改。以枚举为例,在JDK 1.5中增加了 enum 关键字,但是Class文件常量池的 CONSTANT_Class_info 类型常量并没有发生任何语义变化,仍然是代表一个类或接口的符号引用,没有加入枚举,也没有增加过 CONSTANT_Enum_info 之类的 枚举符号引用 常量。所以使用 enum 关键字定义常量,虽然从Java语法上看起来与使用class关键字定义类、使用 interface 关键字定义接口是同一层次的,但实际上这是由Javac编译器做出来的假象,从字节码的角度来看, 枚举仅仅是一个继承于 java.lang.Enum 、自动生成了 values() 和 valueOf() 方法的普通类 而已
Retrotranslator对枚举所做的主要处理:
- 枚举类的父类从 java.lang.Enum 替换为它运行时类库中包含的 net.sf.retrotranslator.runtime.java.lang.Enum_
- 在类和字段的访问标志中抹去 ACC_ENUM 标志位
当然,这只是处理的总体思路,具体的实现要比上面说的复杂得多 可以想象既然两个父类实现都不一样,values()和valueOf()的方法自然需要重写 常量池需要引入大量新的来自父类的符号引用,这些都是实现细节
下图是一个使用JDK 1.5编译的枚举类与被Retrotranslator转换处理后的字节码的对比图:
服务器执行临时代码
排查问题的过程中,想查看内存中的一些参数值,却又没有方法把这些值输出到界面或日志中,又或者定位到某个缓存数据有问题,但缺少缓存的统一管理界面,不得不重启服务才能清理这个缓存。类似的需求有一个共同的特点,那就是只要在服务中执行一段程序代码,就可以定位或排除问题,但就是偏偏找不到可以让服务器执行临时代码的途径,这时候就会希望Java服务器中也有提供类似Groovy Console的功能
JDK 1.6之后提供了Compiler API,可以动态地编译Java程序,虽然这样达不到动态语言的灵活度,但让服务器执行临时代码的需求就可以得到解决了。在JDK 1.6之前,也可以通过其他方式来做到,譬如写一个JSP文件上传到服务器,然后在浏览器中运行它,或者在服务端程序中加入一个Bean ShellScript、JavaScript等的执行引擎(如Mozilla Rhino)去执行动态脚本。这里将使用前面学到的关于 类加载 及 虚拟机执行子系统 的知识去实现在 服务端执行临时代码 的功能
目标
- 不依赖JDK版本,能在目前还普遍使用的JDK中部署,也就是使用JDK 1.4 ~ JDK 1.7都可以运行
- 不改变原有服务端程序的部署,不依赖任何第三方类库
- 不侵入原有程序,即无须改动原程序的任何代码,也不会对原有程序的运行带来任何影响
- 考到 Bean ShellScript 或 JavaScript 等脚本编写起来不太方便,“临时代码”需要直接支持Java语言
- “临时代码”应当具备足够的自由度, 不需要 依赖特定的类或实现特定的接口。这里写的是 不需要 而不是 不可以 ,当“临时代码”需要引用其他类库时也没有限制,只要服务端程序能使用的,临时代码应当都能直接引用
- “临时代码”的 执行结果能返回到客户端 ,执行结果可以包括程序中输出的信息及输出的异常等
设计
需要解决以下3个问题:
- 如何编译提交到服务器的Java代码:
- 使用 tools.jar 包(在Sun JDK/lib目录下)中的 com.sun.tools.javac.Main 类来编译Java文件,这其实和使用Javac命令编译是一样的。缺点是引入了额外的JAR包,而且把程序 绑死 在Sun的JDK上了,要部署到其他公司的JDK中还得把tools.jar带上
- 直接在客户端编译好,把字节码而不是Java代码传到服务端
- 如何执行编译之后的Java代码:
- 类加载器加载这个类生成一个Class对象,然后反射调用一下某个方法就可以了(因为不实现任何接口, 可以直接调用这个编写类的 main() 方法)
- 一段程序往往不是编写、运行一次就能达到效果,同一个类可能要反复地修改、提交、执行。既然提交的是临时代码,那提交的Java类在执行完后就应当能卸载和回收
- 提交上去的类要能访问服务端的其他类库才行
- 如何收集Java代码的执行结果:
- 把程序往 标准输出 (Systemout)和 标准错误输出 (Systemerr)中打印的信息收集起来,但 标准输出设备是整个虚拟机进程全局共享的资源
- 如果使用System.setOut()/System.setErr()方法把输出流重定向到自己定义的PrintStream对象上固然可以收集输出信息,但也会对原有程序产生影响: 会把其他线程向标准输出中打印的信息也收集了
- 直接在执行的类中 把对 System.out的符号引用 替换为我们准备的 PrintStream 的符号引用
- 把程序往 标准输出 (Systemout)和 标准错误输出 (Systemerr)中打印的信息收集起来,但 标准输出设备是整个虚拟机进程全局共享的资源
实现
HotSwapClassLoader:同一个类的代码可以被多次加载
/** * 为了多次载入执行类而加入的加载器<br> * 把defineClass方法开放出来,只有外部显式调用的时候才会使用到loadByte方法 * 由虚拟机调用时,仍然按照原有的双亲委派规则使用loadClass方法进行类加载 * * @author zzm */ public class HotSwapClassLoader extends ClassLoader { public HotSwapClassLoader() { super(HotSwapClassLoader.class.getClassLoader()); } public Class loadByte(byte[] classByte) { return defineClass(null, classByte, 0, classByte.length); } }
HotSwapClassLoader所做的事情仅仅是 公开父类( java.lang.ClassLoader )中的 protected方法 defineClass ()
- 外部手工调用loadByte()方法的话将会把提交执行的Java类的 byte[]数组 转变为 Class对象
- HotSwapClassLoader中并 没有重写 loadClass() 或 findClass() 方法,这个类加载器的类查找范围与它的父类加载器是完全一致的,在被虚拟机调用时,它会按照双亲委派模型交给父类加载。构造函数中指定为加载HotSwapClassLoader类的类加载器也为父类加载器,这一步是实现 提交的执行代码可以访问服务端引用类库 的关键
/** * 修改Class文件,暂时只提供修改常量池常量的功能 * @author zzm */ public class ClassModifier { /** * Class文件中常量池的起始偏移 */ private static final int CONSTANT_POOL_COUNT_INDEX = 8; /** * CONSTANT_Utf8_info常量的tag标志 */ private static final int CONSTANT_Utf8_info = 1; /** * 常量池中11种常量所占的长度,CONSTANT_Utf8_info型常量除外,因为它不是定长的 */ private static final int[] CONSTANT_ITEM_LENGTH = { -1, -1, -1, 5, 5, 9, 9, 3, 3, 5, 5, 5, 5 }; private static final int u1 = 1; private static final int u2 = 2; private byte[] classByte; public ClassModifier(byte[] classByte) { this.classByte = classByte; } /** * 修改常量池中CONSTANT_Utf8_info常量的内容 * @param oldStr 修改前的字符串 * @param newStr 修改后的字符串 * @return 修改结果 */ public byte[] modifyUTF8Constant(String oldStr, String newStr) { int cpc = getConstantPoolCount(); int offset = CONSTANT_POOL_COUNT_INDEX + u2; for (int i = 0; i < cpc; i++) { int tag = ByteUtils.bytes2Int(classByte, offset, u1); if (tag == CONSTANT_Utf8_info) { int len = ByteUtils.bytes2Int(classByte, offset + u1, u2); offset += (u1 + u2); String str = ByteUtils.bytes2String(classByte, offset, len); if (str.equalsIgnoreCase(oldStr)) { byte[] strBytes = ByteUtils.string2Bytes(newStr); byte[] strLen = ByteUtils.int2Bytes(newStr.length(), u2); classByte = ByteUtils.bytesReplace(classByte, offset - u2, u2, strLen); classByte = ByteUtils.bytesReplace(classByte, offset, len, strBytes); return classByte; } else { offset += len; } } else { offset += CONSTANT_ITEM_LENGTH[tag]; } } return classByte; } /** * 获取常量池中常量的数量 * @return 常量池数量 */ public int getConstantPoolCount() { return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2); } }
ClassModifier:将 java.lang.System 替换为自己定义的 HackSystem 类的过程,它直接修改符合Class文件格式的 byte[]数组中的常量池 部分,将常量池中指定内容的CONSTANT_UtfB_info常量替换为新的字符串
/** * Bytes数组处理工具 * @author */ public class ByteUtils { public static int bytes2Int(byte[] b, int start, int len) { int sum = 0; int end = start + len; for (int i = start; i < end; i++) { int n = ((int) b[i]) & 0xff; n <<= (--len) * 8; sum = n + sum; } return sum; } public static byte[] int2Bytes(int value, int len) { byte[] b = new byte[len]; for (int i = 0; i < len; i++) { b[len - i - 1] = (byte) ((value >> 8 * i) & 0xff); } return b; } public static String bytes2String(byte[] b, int start, int len) { return new String(b, start, len); } public static byte[] string2Bytes(String str) { return str.getBytes(); } public static byte[] bytesReplace(byte[] originalBytes, int offset, int len, byte[] replaceBytes) { byte[] newBytes = new byte[originalBytes.length + (replaceBytes.length - len)]; System.arraycopy(originalBytes, 0, newBytes, 0, offset); System.arraycopy(replaceBytes, 0, newBytes, offset, replaceBytes.length); System.arraycopy(originalBytes, offset + len, newBytes, offset + replaceBytes.length, originalBytes.length - offset - len); return newBytes; } }
ByteUtils : 实现了将byte[]与int和String互相转换,以及把对byte[]数据的替换操作
/** * 为JavaClass劫持java.lang.System提供支持 * 除了out和err外,其余的都直接转发给System处理 * * @author zzm */ public class HackSystem { public final static InputStream in = System.in; private static ByteArrayOutputStream buffer = new ByteArrayOutputStream(); public final static PrintStream out = new PrintStream(buffer); public final static PrintStream err = out; public static String getBufferString() { return buffer.toString(); } public static void clearBuffer() { buffer.reset(); } public static void setSecurityManager(final SecurityManager s) { System.setSecurityManager(s); } public static SecurityManager getSecurityManager() { return System.getSecurityManager(); } public static long currentTimeMillis() { return System.currentTimeMillis(); } public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length) { System.arraycopy(src, srcPos, dest, destPos, length); } public static int identityHashCode(Object x) { return System.identityHashCode(x); } // 下面所有的方法都与java.lang.System的名称一样 // 实现都是字节转调System的对应方法 // 因版面原因,省略了其他方法 }
JavaClassExecuter : 是提供给外部调用的入口,调用前面几个支持类组装逻辑,完成类加载工作
- execute ()方法:
- 用输入的符合Class文件格式的byte[]数组替换java.lang.System的符号引用后
- 使用 HotSwapClassLoader 加载生成一个 Class 对象
- 由于每次执行execute()方法都会 生成一个新的类加载器实例 ,因此同一个类可以实现 重复加载
- 反射调用 这个Class对象的 main ()方法,如果期间出现任何异常,将异常信息打印到HackSystemout中
- 把缓冲区中的信息、作为方法的结果返回
/** * JavaClass执行工具 * * @author zzm */ public class JavaClassExecuter { /** * 执行外部传过来的代表一个Java类的Byte数组<br> * 将输入类的byte数组中代表java.lang.System的CONSTANT_Utf8_info常量修改为劫持后的HackSystem类 * 执行方法为该类的static main(String[] args)方法,输出结果为该类向System.out/err输出的信息 * @param classByte 代表一个Java类的Byte数组 * @return 执行结果 */ public static String execute(byte[] classByte) { HackSystem.clearBuffer(); ClassModifier cm = new ClassModifier(classByte); byte[] modiBytes = cm.modifyUTF8Constant("java/lang/System", "org/fenixsoft/classloading/execute/HackSystem"); HotSwapClassLoader loader = new HotSwapClassLoader(); Class clazz = loader.loadByte(modiBytes); try { Method method = clazz.getMethod("main", new Class[] { String[].class }); method.invoke(null, new String[] { null }); } catch (Throwable e) { e.printStackTrace(HackSystem.out); } return HackSystem.getBufferString(); } }