一. 案例分析

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. python 遇到的小坑

    由于前端资源紧缺,我的后端系统迟迟等不来它的前端,没办法只好自己来写了.从html,js入门学起,然后照着vue.js的官方教程写了几个实例,从github上clone了一个不错的vue.js模版,填 ...

  2. 洗礼灵魂,修炼python(46)--巩固篇—如虎添翼的property

    @property 在前面装饰器一章中,提过一句话,装饰器也可以用于类中,确实可以的,并且python的类也内置了一部分装饰器.并且在前两章的hasattr等四个内置方法中,也说过其用法很类似装饰器, ...

  3. 上下文管理器——with语句的实现

    前言 with语句的使用给我们带来了很多的便利,最常用的可能就是关闭一个文件,释放一把锁. 既然with语句这么好用,那我也想让我自己写的代码也能够使用with语句,该怎么实现? 下面具体介绍怎样实现 ...

  4. Linux之特殊的环境变量IFS以及如何删除带有空格的目录

    1.IFS是什么? Linux下有一个特殊的环境变量叫做IFS,叫做内部字段分隔符(internal field separator).IFS环境变量定义了bash shell用户字段分隔符的一系列字 ...

  5. Linux永久修改系统时间

    1.date 查看系统时间 2.hwclock --show 查看硬件的时间 3.hwclock --set --date '2017-08-16 17:17:00' 设置硬件时间为17年8月16日1 ...

  6. Class doesn't contain any JAX-RS annotated method

    项目中使用了Jersey RESTful风格的注解 , 根据错误提示就知道了 , 在类中没有带注解的方法 ,所以只要在方法上添加 @path() 注解就行了,至少要有一个方法带有Jersey注解

  7. 17秋 软件工程 团队第五次作业 Alpha Scrum1

    题目:团队作业--Alpha冲刺 17秋 软件工程 团队第五次作业 Alpha Scrum1 各个成员在 Alpha 阶段认领的任务 伟航:督促和监督团队进度,协调组内合作 港晨:APP前端页面编写: ...

  8. ABP模块运行解析

    从官方创建一份ASP.NET CORE 2.0的项目,并加入源码调试,可以看出如下图的加载顺序 1.ABP是通过什么样的机制加载的 既然ABP中模块需要添加DLL到引用中,又要加入DependsOn在 ...

  9. ABAP CDS 替换对象(Replacement Objects)引起的数据错误

    最近遇到了一个诡异的问题:从CDS视图中取得的数据,和从透明表中取得的数据,会有不同的值.在这里记录下问题的表现和解决方案,以供参考. 系统版本:S/4HANA OP1610 涉及表:MCHB 本文链 ...

  10. git命令行解决冲突文件步骤

    原文https://blog.csdn.net/zwl18210851801/article/details/79106448 亲测有用,解决git冲突的好办法 方法一(推荐使用): git pull ...