一. 案例分析

1.  Tomcat:正统的类加载器架构

  主流的Java Web服务器,如Tomcat、Jetty、WebLogic、WebSphere或其他服务器,都实现了自己定义的类加载器(一般都不止一个)。因为一个功能健全的Web服务器,要解决如下问题:

  •   部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。服务器应当保证两个应用程序的类库可以互相独立使用。
  •   部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享。如果部分类库不能共享,虚拟机的方法区就会很容易出现过度膨胀的风险。
  •   服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响。基于安全考虑,服务器所使用的类库应该与应用程序的类库互相独立。
  •   支持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类库放置在这些目录中的含义分别是:

  •   放置在/common目录中: 类库可被Tomcat和所有的Web应用程序共同使用。
  •   放置在/server目录中: 类库可被Tomcat使用,对所有Web应用程序都不可见。
  •   放置在/shared目录中: 类库可被所有的Web应用程序共同使用,但对Tomcat不可见。
  •   放置在/WebApp/WEB-INF目录中: 类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。

      

  为了支持这套目录结构,并对目录里面的类库进行加载和隔离,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 项。

2.  OSGI:灵活的类加载器架构

  在OSGI里面,Bundle之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖。

  OSGI特点,要归功于它灵活的类加载架构。OSGI的Bundle类加载器之间只有规则,没有固定的委派关系。

3.  字节码生成技术与动态代理的实现

  相信许多Java开发人员都是用过动态代理,例如 java.lang.reflect.Proxy 或实现过 java.lang.reflect.InvocationHandler 接口。

  下面一个例子,在方法前面打印一句“welcome”。

  

public interface IHello {
void sayHello();
void sayHi();
} public class Hello implements IHello{ @Override
public void sayHello() {
System.out.println("hello world");
} @Override
public void sayHi() {
System.out.println("hi world");
}
} 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);
}
} public class DynamicProxyTest {
public static void main(String[] args) {
IHello hel = (IHello) new DynamicProxy().bind(new Hello());
hel.sayHello();
hel.sayHi();
}
}

运行结果

welcome
hello world
welcome
hi world

4.  Retrotranslator:跨越JDK版本

  把JDK1.5 中编写的代码放到 JDK1.4 或 1.3 的环境去部署使用。为了解决这个问题,一种名为“Java逆转移植”的工具(Java Backporting Tools)应运而生,Retrotranslator 是这类工具中较为出色的一个。

二. 实战:自己动手实现远程执行功能

  我们将使用前面学到的关于类加载及虚拟机执行子系统的知识去实现在服务端执行临时代码的能力。

1.   目标

  首先,在实现“在服务端执行临时代码”这个需求之前,先明确一下本次实战的具体目标,我们希望最终的产品是这样的:

  •   不依赖JDK版本,能在目前普遍使用的JDK中部署。
  •   不改变原有服务端程序的部署,不依赖任何第三方类库。
  •   不侵入原有程序,即无需改动原程序的任何代码,也不会对原有程序运行带来任何影响。
  •   “临时代码”应当具备足够的自由度,不需要依赖热定的类或实现特定的接口。
  •   “临时代码”的执行结果能返回到客户端,执行结果可以包括程序中输出的信息及抛出的异常等。

2.  思路

  在程序实现的过程中,我们需要解决以下3个问题:

  •   如何编译提交到服务器的Java代码?
  •   如何执行编译后的Java代码?
  •   如何收集Java代码的执行结果?

3.  实现

  第一个类用于实现“同一个类的代码可以被多次加载”这个需求,具体代码如下:

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, , 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 = ; /**
* CONSTANT_Utf8_info 常量的tag标志
*/
private static final int CONSTANT_Utf8_info = ; /**
* 常量池中11种常量所占的长度,CONSTANT_Utf8_info型常量除外,因为不是定长的
*/
private static final int[] CONSTANT_ITEM_LENGTH = {-, -, -, , , , , , , , , , }; private static final int u1 = ;
private static final int u2 = ; 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 = ; 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 = ;
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) * ;
sum = n + sum;
}
return sum;
} public static byte[] int2Bytes(int value, int len) {
byte[] b = new byte[len];
for (int i = ; i < len; i++) {
b[len - i - ] = (byte) ((value >> * 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, , newBytes, , offset); //替换位置之前
System.arraycopy(replaceBytes, , 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();
} }

4.  验证

  任意写一个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文件的内容进行修改替换,并可以正常提供使用。

【JVM.8】类加载及执行子系统的案例与实战的更多相关文章

  1. 《深入理解Java虚拟机》-----第9章 类加载及执行子系统的案例与实战

    概述 在Class文件格式与执行引擎这部分中,用户的程序能直接影响的内容并不太多, Class文件以何种格式存储,类型何时加载.如何连接,以及虚拟机如何执行字节码指令等都是由虚拟机直接控制的行为,用户 ...

  2. 【深入理解JAVA虚拟机】第三部分.虚拟机执行子系统.4.类加载及执行子系统的案例与实战

    1.概述 在Class文件格式与执行引擎这部分中 : 用户不能控制的:Class文件以何种格式存储,类型何时加载. 如何连接,以及虚拟机如何执行字节码指令等都是由虚拟机直接控制的行为 用户能控制的:字 ...

  3. JVM性能优化系列-(3) 虚拟机执行子系统

    3. 虚拟机执行子系统 3.1 Java跨平台的基础 Java刚诞生的宣传口号:一次编写,到处运行(Write Once, Run Anywhere),其中字节码是构成平台无关的基石,也是语言无关性的 ...

  4. Java JVM——2.类加载器子系统

    概述 类加载器子系统在Java JVM中的位置 类加载器子系统的具体实现 类加载器子系统的作用 ① 负责从文件系统或者网络中加载.class文件,Class 文件在文件开头有特定的文件标识. ② Cl ...

  5. JVM 的执行子系统

    JVM 的执行子系统. 一.Class类文件结构 1. JVM的平台无关性 与平台无关性是建立在操作系统上,虚拟机厂商提供了许多可以运行在各种不同平台的虚拟机,它们都可以载入和执行字节码,从而实现程序 ...

  6. JVM类加载以及执行的实战

    前几篇文章主要是去理解JVM类加载的原理和应用,这一回讲一个可以自己动手的例子,希望能从头到尾的理解类加载以及执行的整个过程. 这个例子是从周志明的著作<深入理解Java虚拟机>第9章里抄 ...

  7. JVM学习第三天(JVM的执行子系统)之开篇Class类文件结构

    虽然这几天 很忙,但是学习是不能落下的,也不能推迟,因为如果推迟了一次,那么就会有无数次;加油,come on! Java跨平台的基础: 各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节 ...

  8. JVM系列之三:类装载器子系统

    0. JVM架构图 Java虚拟机主要分为五大模块:类装载器子系统.运行时数据区.执行引擎.本地方法接口和垃圾收集模块. 1. 类的加载 虚拟机类装载器子系统:虚拟机把描述类的数据从class文件加载 ...

  9. jvm (一)jvm结构 & 类加载 & 双亲委托模型

    参考文档: jvm内幕-java虚拟机详解:http://www.importnew.com/17770.html 常量池:https://www.jianshu.com/p/c7f47de2ee80 ...

随机推荐

  1. MVP架构分析与搭建

    一个项目的核心就是架构 1.什么是MVP:MVP是一种项目架构设计模式. 其实MVP的本质就是将view和model完全隔离出来,通过Presenter (主持人) 统一调度管理.

  2. (网页)javaScript增删改查(转)

    转自CSDN: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> ...

  3. JavaScript大杂烩15 - 使用JQuery(下)

    前面我们总结了使用各种selector拿到了jQuery对象了,下面就是对这个对象执行指定的行为了. 2. 操作对象 - 行为函数action 执行jQuery内置的行为函数的时候,JQuery自动遍 ...

  4. Excel实用录入技巧

    一.文本录入技巧 输入开头为0的序号 当直接输入单元格中的数字第一个为0时系统会默认去掉 只需要经单元格格式改为文本或者在单元格输入前使用英文状态下的单引号(‘) 例如:'0001 >>& ...

  5. Centos7查询开机启动项服务

    问题描述: 最近安装了zabbix设置了一些开机启动服务 例如:zabbix-server.service,httpd.service,mariadb.service,或者系统的firework.se ...

  6. gcc库链接

    转载于https://blog.csdn.net/zhangdaisylove/article/details/45721667 1.库的分类 库有静态库和动态库,linux下静态库为.a,动态库为. ...

  7. January 18th, 2018 Week 03rd Thursday

    To strive, to seek, to find, and not to yield. 去奋斗,去寻觅,去探索,但绝不屈服. Strive for our dreams, seek the ve ...

  8. java.sql.SQLSyntaxErrorException: ORA-00904: "column": 标识符无效

    java.sql.SQLSyntaxErrorException: ORA-00904: "column": 标识符无效 首先查看无效的列是不是orcale关键字 , 如果不是 , ...

  9. Unity3D中自带事件函数的执行顺序

    在Unity3D脚本中,有几个Unity3D自带的事件函数按照预定的顺序执行作为脚本执行.其执行顺序如下: 编辑器(Editor) Reset:Reset函数被调用来初始化脚本属性当脚本第一次被附到对 ...

  10. ECstore后台报表显示空白问题解决办法

    执行如下sql语句: INSERT INTO `sdb_ectools_analysis` (`id`, `service`, `interval`, `modify`) VALUES (1, 'b2 ...