Java高级——类加载及执行子系统的案例与实战

类加载及执行子系统的案例与实战

概述

Class文件以何种格式存储、字节码指令如何执行等都是由JVM控制

字节码生成与类加载器这两部分的功能,可由用户自定义,接下来将对一些实际应用进行介绍

类加载器案例

Tomcat

主流的Java Web服务器,如Tomcat、Jetty等自定义了类加载器(且不止一个),为了实现如下需求

  • 服务器上的两个Web应用的Java类库需要隔离(可能使用不同版本)或共享(避免重复加载同一类库)
  • 服务器需保证自身安全不受影响,也应与应用所使用的类库互相独立
  • JSP最终要编译成Class文件,且JSP支持热替换

由于上述问题,部署Web应用时需提供多个ClassPath存放第三方类库,其以lib或classes命名

  • /common中的类库可被Tomcat和所有的Web应用程序共享
  • /server中的类库只能被Tomcat使用
  • /shared中的类库可被所有的Web应用程序共享
  • /WebApp/WEB-INF的类库仅可被该Web应用程序使用

上图为Tomcat中的类加载器,按照双亲委派模型实现

  • Common、Catalina、Shared、Webapp加载器分别对应加载上面路径的类库
  • Common加载的类都可以被Catalina和Shared使用
  • Catalina和Shared加载的类相互隔离
  • WebApp可使用Shared加载的类,一个Web应用程序对应一个WebAppClassLoader,相互隔离
  • 一个JSP文件对应一个JasperLoader,当JSP文件被修改时,会生成新的JasperLoader替换,以此实现HotSwap

tomcat 6之后

  • /common、/server和/shared合并成/lib,相当于原来的/common
  • 只有指定tomcat/conf/catalina.properties中的server.loader和share.loader才会建立Catalina和Shared,否则使用Common

OSGi

OSGi(Open Service Gateway Initiative)是OSGi联盟制订的一个基于Java的动态模块化规范(JDK 9的JPMS是静态的模块系统)

OSGi中的每个模块(称为Bundle)与普通的Java类库类似,以JAR格式进行封装,内部存储的Java的Package和Class

Bundle可以声明它所依赖的Package(Import-Package),也可以声明它允许导出的Package(Export-Package),从传统的上层模块依赖底层模块转变为平级模块之间的依赖

OSGi的优点是可以实现模块热插拔,类加载器无固定的委派关系,对Package的类加载都会委派给发布它的Bundle类加载器去完成,若对于

  • Bundle A:发布了packageA,依赖java.*
  • Bundle B:依赖了packageA和packageC,依赖java.*
  • Bundle C:发布了packageC,依赖packageA

则它们的类加载关系如下

类加载的查找规则如下

  • java.*开头的类,委派给父类加载器加载
  • 否则,委派列表名单内的类,委派给父类加载器加载
  • 否则,Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
  • 否则,查找当前Bundle的Classpath,使用自己的类加载器加载
  • 否则,查找是否在自己的Fragment Bundle中,如果是则委派给Fragment Bundle的类加载器加载
  • 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
  • 否则,类查找失败

OSGi中的加载器不再是双亲委派模型的树形结构,而是网状结构

JDK7之前,加载需要锁定当前类加载器,若出现循环依赖可能导致死锁,而JDK7后将锁定对象降低为类名级别,从而避免死锁

字节码案例

动态代理

动态代理中的"动态",是相对与编写代理类的"静态"代理而言,其优势不仅是省去编码的工作量,而是实现了在原始类和接口未知时就确定代理类的代理行为

public class Test {
    
	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();
    }

}

如上代码通过代理,在hello world之前加上welcome

welcome
hello world

Proxy.newProxyInstance()方法返回一个实现了IHello的接口,并且代理了new Hello()实例行为的对象

代理通过调用sun.misc.ProxyGenerator::generateProxyClass()方法来生成字节码,在main中加入如下代码,可在磁盘中生成一个名为$Proxy0的代理类Class文件

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

将其反编译为如下

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 boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final void sayHello() throws  {
        try {
            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);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (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("Test$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());
        }
    }
}

代理类为接口及Object中继承的方法都生成了对应的实现并调用了InvocationHandler对象(即h)的invoke()方法,所以无论调用DynamicProxy的哪一个方法都会回调invoke()

Java逆向移植工具

当需要把高版本JDK编写的代码放到低版本JDK环境中去部署使用,可使用

  • Retrotranslator:将JDK5编译的Class文件转变为可在JDK1.4/3上部署的版本

  • Retrolambda:将JDK 8的Lambda表达式和try-resources语法转变为可以在JDK5、JDK 6、JDK 7中使用的形式

每次JDK升级新增功能可分为五大类

  • 增强Java类库,如concurren、invoke包
  • 改进前端编译器,称为语法糖,如自动装箱拆箱
  • 字节码改动,如invokedynamic
  • JDK整体结构改动,如模块化系统
  • JVM内部改动,如更换垃圾收集器

对于第一类,可使用其他可代替的包去实现,如Retrotranslator中存在backport-util-concurrent.jar代替concurren

对于第二类,JDK在编译阶段进行的改进,Retrotranslator使用ASM框架对字节码进行处理,如Retrotranslator将枚举的父类Enum转为自身类库的net.sf.retrotranslator.runtime.java.lang.Enum_,再去掉ACC_ENUM标志位

对于第三类,invokedynamic实现Lambda,Retrolambda生成一组匿名内部类来替代Lambda

而第四第五类,使用逆向移植工具比较难处理

实战------远程执行功能

有时候需要在服务中执行一小段程序代码定位或排除问题(如查看内存的参数值),但却没有让服务器执行临时代码的途径,可通过

  • BTrace这类JVMTI工具去动态修改程序中某一部分的运行代码
  • JDK 6的Compiler API可以动态地编译Java程序
  • 写一个JSP文件在服务器浏览器中运行它,或者在服务端程序中新增BeanShell Script、JavaScript等的执行引擎去执行动态脚本
  • 在应用程序中内置动态执行的功能

目标

实现在服务端执行临时代码

  • 不依赖于JDK版本(1.4以上)
  • 不改变原有服务端程序的部署,不依赖第三方类库
  • 无须改动原程序代码,也不会影响源程序运行
  • 临时代码支持Java
  • 不需要依赖特定的类或实现特定的接口
  • 执行结果能返回到客户端

思路

如何编译提交到服务器的Java代码?

  • JDK6后可使用Compiler API,JDK6前可使用tool.jar,但是引入依赖
  • 直接把编译好的字节码而不是Java代码上传

如何执行编译之后的java代码

  • 让类加载器加载这个类生成一个Class对象,然后通过反射调用

如何收集Java代码的执行结果

  • 使用System.out和System.err将输出重定向,但会收集到其他应用信息
  • 可将System.out的符号引用替换为自定义PrintStream的符号引用

实现

HotSwapClassLoader用于实现用一个类的代码可以被多次加载的需求,用loadByte()方法公开defineClass(),把byte[]数组转变为Class对象

public class HotSwapClassLoader extends ClassLoader {
    public HotSwapClassLoader() {
        super(HotSwapClassLoader.class.getClassLoader());
    }
    public Class loadByte(byte[] classByte) {
        return defineClass(null, classByte, 0, classByte.length);
    }
}

ClassModifier的modifyUTF8Constant()方法将Class数组流的常量替换为指定常量,从而实现将System替换为自定义的HackSystem

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;
        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;
    }
    public int getConstantPoolCount() {
        return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2);
    }
}

ByteUtils用于对byte[]数组进行替换

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);
        return newBytes;
    }
}

HackSystem将out和err改为用PrintStream对象,以及增加了读取、清理ByteArrayOutputStream中内容的getBufferString()和clearBuffer(),其余方法调用原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();
    }
    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);
    }
}

JavaclassExecuter组合前面的类完成类加载

public class JavaclassExecuter {
        public static String execute(byte[] classByte) {
            HackSystem.clearBuffer();
            ClassModifier cm = new ClassModifier(classByte);
            byte[] modiBytes = cm.modifyUTF8Constant("java/lang/System", "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();
        }
    }

验证(未完成,不会写JSP)

任意书写一个TestClass类,向System.out输出信息,再建立个JSP文件用于再浏览器中看TestClass的运行结果

<%@ page import="java.lang.*" %>
<%@ page import="java.io.*" %>
<%@ page import="org.fenixsoft.classloading.execute.*" %>
<%
    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>");
%>
相关推荐
Swift社区1 小时前
在 Swift 中实现字符串分割问题:以字典中的单词构造句子
开发语言·ios·swift
没头脑的ht1 小时前
Swift内存访问冲突
开发语言·ios·swift
没头脑的ht1 小时前
Swift闭包的本质
开发语言·ios·swift
wjs20242 小时前
Swift 数组
开发语言
吾日三省吾码2 小时前
JVM 性能调优
java
stm 学习ing3 小时前
FPGA 第十讲 避免latch的产生
c语言·开发语言·单片机·嵌入式硬件·fpga开发·fpga
湫ccc3 小时前
《Python基础》之字符串格式化输出
开发语言·python
弗拉唐3 小时前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
oi774 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器