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

news2024/11/25 0:34:34

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

  • 概述
  • 类加载器案例
    • Tomcat
    • OSGi
  • 字节码案例
    • 动态代理
    • Java逆向移植工具
  • 实战——远程执行功能
    • 目标
    • 思路
    • 实现
    • 验证(未完成,不会写JSP)

概述

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>");
%>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/994901.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

手写数据库连接池

数据库连接是个耗时操作.对数据库连接的高效管理影响应用程序的性能指标. 数据库连接池正是针对这个问题提出来的. 数据库连接池负责分配,管理和释放数据库连接.它允许应用程序重复使用一个现有的数据路连接,而不需要每次重新建立一个新的连接,利用数据库连接池将明显提升对数…

文本生成模型如何解码

文章目录 解码方法Greedy SearchBeam SearchsamplingTemperature Samplingtop-k samplingTop-p (nucleus) samplingContrastive search 总结相关资源 语言模型如何对于一个给定输入生成相应的输出呢&#xff1f;答案是使用解码策略(decoding strategy)。这里对现有的解码策略做…

在vx1000中对目标属性值的函数修改方法

通过在函数中编辑GFX对象属性值&#xff0c;实现Y坐标相反的操作方法 有时需要对目标属性的x 、y坐标做负方向转换&#xff0c;就需要以下方法来实现 return input.ProY*(-1); return input.C0*(-1);

验收测试的内容和流程有哪些?

验收测试 信息化项目验收确认测试内容一般包括&#xff1a;测试(复核 ),资料评审 ,质量鉴定三部分。 (一)验收评测工作主要包括 :文档分析 ,方案制定 ,现场测试 ,问题单提交 ,测试报告 ; (二)验收测试内容主要包括 :检查 "合同 " 或"验收标准 "要求的所…

【Redis】2、Redis持久化和性能管理

Redis 高可用 在web服务器中&#xff0c;高可用是指服务器可以正常访问的时间&#xff0c;衡量的标准是在多长时间内可以提供正常服务&#xff08;99.9%、99.99%、99.999%等等&#xff09;。 但是在Redis语境中&#xff0c;高可用的含义似乎要宽泛一些&#xff0c;除了保证提供…

C++之构造函数列表使用默认值(一百九十一)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

发布订阅机制和点对点机制

【Go项目】25. 在 gin 中引入 WebSocket 和 Hub_哔哩哔哩_bilibili gorilla/websocket: Package gorilla/websocket is a fast, well-tested and widely used WebSocket implementation for Go. (github.com) 1.订阅发布机制 引用上面链接的内容 发布订阅的基本工作原理 在分…

AQS源码剖析,完整流程解读

目录 1 AQS是什么2 AQS加锁流程3 结构4 AQS方法概览5 AQS源码剖析5.1 加锁方法5.2 释放锁5.3 await等待5.4 signal唤醒 1 AQS是什么 ​ AQS即AbstractQueuedSynchronizer缩写&#xff0c;翻译为抽象队列同步器&#xff0c;是一种用来构建锁和同步器的框架。 平时使用较多的Ree…

【C++】常用排序算法

0.前言 1.sort #include <iostream> using namespace std;// 常用排序算法 sort #include<vector> #include<algorithm>//利用仿函数 打印输出 class myPrint { public:void operator()(int val){cout << val << " ";} };//利用普通函…

车载网络测试 - UDS诊断篇 - CANTP常用缩写

CANTP层规范常用缩写 缩写英文全称中文注释BRSbit rate switch比特率开关BSBlockSize块大小CAN controller area network控制器局域网CAN_DL CAN frame data link layer data length in bytesCAN 帧数据链路层数据长度&#xff08;以字节为单位&#xff09;CAN FDcontroller a…

[kingbase运维之奇怪的现象]

#[kingbase运维之奇怪的现象] ##奇怪的现象 某银行数据中心应用反馈&#xff0c;业务接口日志记录了很多执行慢的SQL&#xff0c;出现的时间是随机的&#xff0c;单独在数据库客户端工具执行会很快返回结果。根据之前的经验推断是业务代码传入的参数类型与数据库表结构字段定义…

HDD-FAT32 ZIP-FAT32 HDD-FAT16 ZIP-FAT16 HDD-NTFS

FAT32、FAT16指的是分区格式&#xff0c; FAT16单个文件最大2G FAT32单个文件最大4G NTFS单个文件大于4G HDD是硬盘启动 ZIP是软盘启动 U盘选HDD HDD-NTFS

buuctf crypto 【还原大师】解题记录

1.打开题目就能直接看到密文 2.感觉爆破直接能解&#xff0c;试试爆破&#xff08;参考文章&#xff1a;[buuctf] crypto全解——前84道&#xff08;不建议直接抄flag&#xff09;_buuctf crypto_咸鱼壹号的博客-CSDN博客&#xff09; import hashlib k TASC?O3RJMV?WDJKX?…

建筑模板9层板和7层板的区别

建筑模板是建筑施工过程中不可或缺的一环&#xff0c;而在建筑模板的选择中&#xff0c;常见的有9层板和7层板两种选项。它们在结构、特性和应用方面存在一些区别。下面将详细探讨9层板和7层板之间的区别。 首先&#xff0c;9层板和7层板的名称源自其板材的层数。9层板由9层木片…

Docker容器技术实战-1

1.docker容器 docker就好比传统的货运集装箱 每个虚拟机都有独立的操作系统&#xff0c;互不干扰&#xff0c;在这个虚拟机里可以跑任何东西 如应用 文件系统随便装&#xff0c;通过Guest OS 做了一个完全隔离&#xff0c;所以安全性很好&#xff0c;互不影响 容器 没有虚拟化…

Tomcat配置ssl、jar包

Tomcat配置ssl 部署tomcat服务&#xff0c;项目做到用https访问&#xff0c;使用nginx去做&#xff0c;访问任意一个子网站&#xff0c;都是https 或者 医美项目需要 上传jdk 456 tomcat war包 [nginx-stable] namenginx stable repo baseurlhttp://nginx.org/packages/…

AI绘画:StableDiffusion实操教程-斗破苍穹-云韵-常服(附高清图下载)

前段时间我分享了StableDiffusion的非常完整的教程&#xff1a;“AI绘画&#xff1a;Stable Diffusion 终极宝典&#xff1a;从入门到精通 ” 不久前&#xff0c;我与大家分享了StableDiffusion的全面教程&#xff1a;“AI绘画&#xff1a;Stable Diffusion 终极宝典&#xff…

用Navicat备份Mysql演示系统数据库的时候出:Too Many Connections

今天用Navicat进行数据备份的时候&#xff0c;发现由于数据库连接数目过多导致连接锁定&#xff0c;这种情况在多人协同开发的场景中很常见。当然我这里也因为多个应用使用了数据库连接&#xff0c;所以出现了Too Many Connections。 可能是超过最大连接数了。 1、进入Navicat…

【JAVA-Day03】JDK安装与IntelliJ IDEA安装、配置环境变量

JDK安装与IntelliJ IDEA安装、配置环境变量 一、JDK 版本介绍1.1 JDK 版本选择JDK 8JDK 11JDK 16JDK 171.2 JDK 下载1.3 JDK 安装1.4 配置环境变量1.5 验证 JDK 安装 二、开发利器——IntelliJ IDEA 的安装2.1 IntelliJ IDEA下载2.2 IntelliJ IDEA安装2.3 IntelliJ IDEA启动2.4…

编译原理:编译原理简明教程知识点梳理(应对考试版)

前言 姜老师是一个好老师&#xff0c;编译原理没有过是我的问题&#xff0c;我爱姜老师。 写这篇博客涉及到好多符号&#xff0c;可以参考这篇文章latex数学公式详细教程 因为打字过于麻烦&#xff0c;很多内容用平板的手写截图&#xff0c;还有电脑截图替代&#xff0c;不太习…