本系列是用来记录《深刻理解Java虚拟机》这本书的读书笔记。方便本身查看,也方便你们查阅。java
欲速则不达,欲达则欲速!程序员
第九章 类加载与执行子系统的案例与实战web
代码编译的结果从本地机器码转变为字节码是存储格式发展的一小步,倒是编程语言发展的一大步。编程
1、案例分析swift
一、tomcat:正统的类加载器架构数组
主流的java web服务器,如tomcat、jetty、weblogic、websphere等服务器,都实现了自定义的类加载。由于一个功能健全的web服务器,要解决如下问题:浏览器
(1)部署在同一个服务器上的两个web应用程序所使用的java类库能够实现相互隔离。服务器应当保证两个应用程序的类库能够互相独立使用。缓存
(2)部署在同一个服务器上的两个web应用程序所使用的java类库能够互相分享。若是部分类库不能分享,虚拟机的方法区就会很容易出现过分膨胀的风险。tomcat
(3)服务器须要尽量的保证自身的安全不受部署的web应用程序影响。基于安全考虑,服务器所使用的类库应该与应用程序的类库互相独立。安全
(4)支持JSP应用的web服务器,大多数须要支持hotswap功能。咱们知道,JSP文件最终要编译成java class才能由虚拟机执行,但JSP文件因为其纯文本存储的特性,运行时修改的几率远远大于第三方类库或程序自身class文件。
因为存在上述问题,在部署web应用时,单独的一个classpath就没法知足需求了,因此各类web服务器都不约而同的提供了好几个classpath路径供影虎存放第三方类库,这些路径通常以lib或classes命名。不一样路径的类库,具有不一样的访问范围和服务对象。
在tomcat目录结构中,有3种目录( “/common/*”、“/server/*”和“/shared/*” )能够存放java类库,另外加上web应用程序自身目录 “WEB-INF/*”,一共4组,把java类库放置在这些目录中的含义分别是:
为了支持这套目录结构,并对目录里面的类库进行加载和隔离,tomcat自定义多个类加载器,这些类加载器按照经典的双亲委派模型来实现。
从图中的委派关系能够看出, CommonClassLoader 能加载的类均可以被 CatelinaClassLoader和SharedClassLoader使用 , 而CatelinaClassLoader和SharedClassLoader本身能加载的类则与对方相互隔离。
WebAppClassLoader可使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
而JasperLoader的加载范围则仅仅是这个JSP文件所编译出来的哪个Class,它出现的目的就是为了被抛弃:当服务器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并经过再创建一个新的Jsp类加载器来实现JSP文件的HotSwap功能。
注意:对于Tomcat6.x的版本,只有指定了 tomcat/conf/catalina.properties 配置文件的 server.loader 和 share.loader 项才会真正创建对应的 *ClassLoader实例,不然会用到这两个类加载器的地方使用CommonClassLoader 的实例来代替,默认配置中没有设置这两个loader 项。
二、OSGi:灵活的类加载器架构
(1)OSGi概述
在OSGi里,Bundle之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖。
OSGi特色,要归功于它灵活的类加载架构。OSGi的Bundle类加载器之间只有规则,没有固定的委派关系。
java程序社区流传着这么一个观点:“学习J2EE规范,去看JBoss源码;学习类加载器,就去看OSGi源码”。尽管J2EE规范和类加载器的知识并非一个对等的概念。不过既然这个观点能在程序员中流传开来,也从侧面说明了OSGi对类加载器的运用确实有其独到之处。
OSGi(open service gateway initiative)是OSGI联盟制定的一个基于java语言的动态模块化规范,最著名的应用案例就是eclipse IDE。
(2)引入OSGi的一个重要理由:
OSGi之因此能有上述诱人的特色,要归功于它灵活的类加载器结构。OSGi的Bundle类加载器之间只有规则,没有固定的委派关系。例如,某个Bundel声明了一个它依赖的Package,若是有其余Bundle声明发布了这个Package后,那个对这个Package的全部类加载动做都会给发布它的Bundle类加载器去完成。不涉及某个具体的Package时,各个Bundle加载器都是平级的关系,只有具体使用到某个Package和Class的时候,才会根据Package导入导出定义来构造Bundle间的委派。
另外,一个Bundle类加载器为其它Bundle提供服务时,会根据 Export-Package 列表严格控制访问范围。若是一个类存在于Bundle的类加载器能找到这个类,但不会提供给其它bundle使用,并且OSGi平台也不会把其它Bundle的类加载请求分配给这个Bundle来处理。
(3)代码实例
咱们能够举一个更具体一点的简单例子,加入存在BundleA,BundleB和BundleC三个模块,而且这三个Bundle定义的依赖关系为:
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 originalObj; Object bind(Object originalObj){ this.originalObj = originalObj; return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(), originalObj.getClass(), getInterfaces(),this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("welcome"); return method.invoke(originalObj, args); } } public static void main(String[] args) { IHello hello = (IHello)new DynamicProxy().bind(new Hello()); hello.sayHello(); } }
上述代码中,惟一的“黑匣子”就是Proxy.newProxyInstance()方法,除此以外再没有任何特殊之处。这个方法返回一个实现了IHello的接口。而且代理了new Hello()实例行为的对象。跟踪这个方法的源码,能够看到程序进行了验证,优化,缓存,同步,生成字节码和显式类加载等操做,前面的步骤并非咱们关注的重点,而最后它调用了sun.misc.ProxyGenerator.generateProxyClass()方法来完成生成字节码的动做.。
三、字节码生成技术与动态代理的实现
相信许多Java开发人员都是用过动态代理,例如 java.lang.reflect.Proxy 或实现过 java.lang.reflect.InvocationHandler 接口。
下面一个例子,在方法前面打印一句“welcome”。
package jvm; public interface IHello { void sayHello(); void sayHi(); }
package jvm; public class Hello implements IHello{ @Override public void sayHello() { System.out.println("hello world"); } @Override public void sayHi() { System.out.println("hi world"); } }
package jvm; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class DynamicProxy implements InvocationHandler { Object originalObj; Object bind(Object originalObj) { this.originalObj = originalObj; return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(), originalObj.getClass().getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("welcome"); return method.invoke(originalObj,args); } }
package jvm; public class test { public static void main(String[] args) { IHello hel = (IHello) new DynamicProxy().bind(new Hello()); hel.sayHello(); hel.sayHi(); } }
运行结果
四、Retrotranslator:跨越JDK版本
把JDK1.5 中编写的代码放到 JDK1.4 或 1.3 的环境去部署使用。为了解决这个问题,一种名为“Java逆转移植”的工具(Java Backporting Tools)应运而生,Retrotranslator 是这类工具中较为出色的一个。
2、实战:本身动手实现远程执行功能
咱们将使用前面学到的类加载及虚拟机执行子系统的知识去实如今服务端执行临时代码的能力。
一、目标
首先,在实现“在服务端执行临时代码”这个需求以前,先明确一下本次实战的具体目标,咱们但愿最终的产品是这样的:
(1)不依赖JDK版本,能在目前普通使用的JDK中部署。
(2)不改变原有服务端程序的部署,不依赖任何第三方类库。
(3)不侵入原有程序,即无需改变原程序的任何代码,也不会对原程序运行带来任何影响。
(4)“临时代码”应当具有足够的自由度,不须要依赖特定的类或特定的接口。
(5)“临时代码”的执行结果能返回客户端,执行结果能够包括程序中输出的信息及抛出的异常等。
二、思路
在程序实现的过程当中,咱们须要解决如下3个问题:
(1) 如何编译提交到服务器的Java代码?
(2) 如何执行编译后的Java代码?
(3) 如何收集Java代码的执行结果?
三、实现
第一个类用于实现“同一个类的代码能够被屡次加载”这个需求,具体代码以下:
package org.swift.framework.RemotePlugin; /** * 为了屡次载入执行类而加入的加载器 * 把defineClass方法开放出来,只有外部显式调用的时候才会使用到loadByte方法 * 由虚拟机调用时,仍然按照原有的双亲委派规则使用loadClass方法进行加载 * zww */ public class HotSwapClassLoader extends ClassLoader { public HotSwapClassLoader() { super(HotSwapClassLoader.class.getClassLoader()); //使用父类的加载器 } public Class loadByte(byte[] classByte) { return defineClass(null, classByte, 0, classByte.length); } }
HotSwapClassLoader 所作的事情仅仅是公开父类中的defineClass() ,这个类加载器的类查找范围与它的父类加载器是彻底一致的。
第二个类实现将 java.lang.System 替换为咱们本身定义的HackSystem 类的过程,它直接修改符合Class 文件格式的 byte[] 数组中的常量池部分,将常量池中指定内容的 CONSTANT_Utf8_info 常量替换为新的字符串。
package org.swift.framework.RemotePlugin; /** * 修改Class文件,暂时只提供修改常量池常量的功能 */ 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; } public byte[] modifyUTF8Constant(String oldStr, String newStr) { int cpc = getConstantPoolCount(); //常量的数量 int offset = CONSTANT_POOL_COUNT_INDEX + u2; //CONSTANT_POOL 起始位置 for (int i = 0; i < cpc; i++) { int tag = ByteUtils.bytes2Int(classByte, offset, u1); //获取常量型 if (tag == CONSTANT_Utf8_info) { //判断常量型类型是不是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 */ private int getConstantPoolCount() { return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2); } }
ByteUtils 工具的实现:
package org.swift.framework.RemotePlugin; 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++) { // 由于当系统检测到byte可能会转化成int或者说byte与int类型进行运算的时候, // 就会将byte的内存空间高位补1(也就是按符号位补位)扩充到32位 // 若是b[i]为负数时:例如:10000001 & 11111111 ==》 1111111111111111111111111 10000001 & 11111111 = 000000000000000000000000 10000001 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) { // || |~offset| ~len || || byte[] newBytes = new byte[originalBytes.length - len + replaceBytes.length]; 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; } }
通过ClassModifier 处理后的 byte[] 数据才会传给 HotSwapClassLoader.loadByte() 方法进行类加载
最后一个类就是前面提到的代替 java.lang.System 的 HackSystem ,这个类除了把 out 和 err 两个静态变量修改了,其余都来自于 System类的 public方法。
package org.swift.framework.RemotePlugin; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.PrintStream; /** * 为JavaClass 劫持 java.lang.System 提供支持 * 除了 out 和 err 外,其他的都直接转发给 System 处理 */ 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(); } //下面全部的方法都与 java.lang.System 的名称同样 //实现都是字节调System的对应方法 //因版面缘由,省略其余方法 }
至此,4个支持类已经讲解完毕,咱们来看看最后一个类 JavaClassExecuter ,它是提供外部调用的入口
package org.swift.framework.RemotePlugin; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * JavaClass 执行工具 */ public class JavaClassExecuter { /** * 执行外部传过来的表明一个Java类的byte数组 * 将输入类byte数组中表明 java.lang.System的CONTANT_Utf8_info常量修改成劫持后的HackSystem类 * 执行方法为该类的 main 方法,输出结构为该类向System.out/err输出的信息 * @param classByte * @return */ public static String execute(byte[] classByte) { HackSystem.clearBuffer(); ClassModifier classModifier = new ClassModifier(classByte); //修改Class字节码,把HackSystem 替代 System byte[] modiBytes = classModifier.modifyUTF8Constant("java.lang.System", "org.swift.framework.RemotePlugin.HackSystem"); HotSwapClassLoader loader = new HotSwapClassLoader(); Class clazz = loader.loadByte(modiBytes); try { //调用其main方法 Method method = clazz.getMethod("main", new Class[] { String[].class}); method.invoke(null, new String[] {null}); } catch (Exception e) { e.printStackTrace(HackSystem.out); } return HackSystem.getBufferString(); } }
四、验证
任意写一个Jaca类,只须要向外System.out 信息便可,同事放到指定路径 C://TestClass.class ,而后创建一个Jsp 文件,在浏览器能够看到这个类的运行结果。
package org.swift.framework.RemotePlugin; public class TestClass { public static void main(String[] args) { System.out.println("this is a test class"); } }
<%@ page import="java.lang.*" %> <%@ page import="java.io.*" %> <%@ page import="org.swift.framework.RemotePlugin.*" %> <% InputStream is = new FileInputStream("C:/TestClass.class"); byte[] b = new byte[is.available()]; is.read(b); is.close(); out.println("<textarea style='width:1000;height:800'>"); out.println(JavaClassExecuter.execute(b)); out.println("</textarea>"); %>
其中主要须要学习的就是对 class文件的内容进行修改替换,并能够正常提供使用。
3、总结
本书中第6~9章介绍了class文件格式,类加载及虚拟机执行引擎及部份内容,这些内容时虚拟机必不可少的组成部分,了解了虚拟机如何执行程序,才能更好的理解怎样才能写出优秀的代码。
关于虚拟机执行子系统的介绍到此为止就结束了,经过这4章的讲解,咱们描绘了一个虚拟机应该如何运行class文件的概念模型。对于具体到某个虚拟机的实现,为了使实现简单清晰,或者为了更快的运行速度,在虚拟机内部的运做跟概念模型可能会有很是大的差别,但从最终的执行结果来看应该是一致的。
推荐博文
鸣谢:特别感谢做者周志明提供的技术支持!