Java类加载的故事-修正终结版

2023-10-17 04:32

本文主要是介绍Java类加载的故事-修正终结版,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 故事起源:
    • 故事内容:
      • JAVA的类加载机制:
    • 故事背景:
    • 故事序幕:
    • 第一章:代码拆分
    • 第二章:class代码混淆
    • 第三章:实现热加载
    • 第四章:到底加载的是哪个类?
    • 第五章:实现同类多版本共存
    • 第六章:引入JAVA提供的SPI机制实现工资计算服务加载
    • 总结:

带你玩转不一样的JAVA.
== 楼兰:神秘Java宝藏 ==

之前发过的两篇类加载的故事由于当时实力不够,颇有错误,这次重新整理了一个修正终结版,并配合视频讲解。
博文配合视频:https://www.bilibili.com/video/BV11a4y1p7eP
如果觉得有帮助,烦请点赞鼓励下。

故事起源:

​ java指令到底干了些什么?我们写的java代码是如何被加载到jvm内存中执行的?

故事内容:

​ 回顾java类加载机制。 实战自定义的类加载器。实现自己的热加载。实现同类多个版本共存。

JAVA的类加载机制:

​ 1、java的类加载体系:

BootStrap Classloader > ExtClassLoader > AppClassLoader

​ 每种类加载器都有他自己的加载目录。

​ 2、双亲委派:一个java类加载进JVM内存的过程:

  • 每个类加载器对他加载过的类都有一个缓存。

  • 向上委托查找,向下委托加载。

​ 3、JDK的类加载对象:

ClassLoader -> SecureClassLoader ->  URLClassLoader -> ExtClassLoader,AppClassLoader

故事背景:

​ 有一个OA系统, 每个月需要定时的计算大家的工资。

故事序幕:

​ 有一个程序员,想要修改工资的计算方法。偷偷加工资。

​ 他偷偷的修改OA系统中计算工资的方法源码,给自己加了两成的工资。

第一章:代码拆分

​ 程序员偷偷加了工资,但是,肯定会被经理发现。OA系统的源码,经理也可以看到。

​ 把计算工资的方法,从OA系统的源码中抽出来,放到另外一个jar包中。

这样的jar包文件可以放在哪些地方?放到网络地址、maven仓库(drools规则引擎)

第二章:class代码混淆

​ 我们的jar包最终都可以通过反编译的方式,被发现。需要对jar包进行混淆。

​ 第一个想法:对class文件做手脚:

​ 修改.class的文件后缀,改为.myclass.

​ 自定义一个类加载,读取.myclass文件。

怎么实现一个自定义类加载? 1 继承一个系统类加载器 SecureClassLoader ; 2 覆盖父类的findClass方法, 在方法中,调用defineClass方法在JVM内存中定义一个类。

扩展:虽然改了文件后缀,但是文件的内容没有改。所以更安全的方式,是把class文件里面的内容也稍微做下改动。

​ 程序员可以通过简单修改二进制文件的方式,对class文件的内容做少量的修改,这样class文件的安全性得到进一步提高。

​ 最终这种处理方法还是要集成到jar包中。所以还是要实现一个从jar包中加载class类的自定义类加载器。

​ 第二种更加完善的方式:自定义一个类加载器,从jar包中去找到对应的class文件,加载到JVM中。

​ 把上面的两种方式结合起来,

通过这种方式,我们可以自定义class文件的加载逻辑,最终实现class文件的代码混淆。

代码的安全性得到进一步的提高。

第三章:实现热加载

​ 总公司临时需要核算工资。程序员需要赶紧将工资计算的方式还原回去。又希望在发工资的时候,将工资计算的方式改回来。

​ 这时候,程序员发现, 每次修改计算工资方法的jar包,都需要重启OA系统才能生效。这样显然更容易让别人起疑心。这时,程序员就需要实现热加载。我们计算工资方法的jar包,更新后,立即生效,不用重启OA系统。

​ 回到我们的问题:JAVA里的每一个类加载器,对他加载过的类,都会保留一个缓存。正是这个缓存,导致我们无法实现热加载。

​ 我们通过每一次new一个SalaryJarLoader的方式,实现了热加载。

​ 热加载既然很好用,为什么很少用到呢?因为热加载机制有一个加载的过程,很容易出错。还有个更大的问题,热加载必然产生非常多的垃圾对象。

​ 在ClassLoader的loadClass方法中,还传入了一个Boolean的resolve参数,这个是干什么的?

一个类的类加载过程通常分为 加载、连接、初始化 三个部分,具体的行为在java虚拟机规范中都有详细的定义,这里只是大致的说明一下。

  • 加载Loading: 这个过程是Java将字节码数据从不同的数据源读取到JVM中,并映射成为JVM认可的数据结构。而如果输入的Class不符合JVM的规范,就会抛出异常。这个阶段是用户可以参与的阶段,我们自定义的类加载器,就是工作在这个过程。
  • 连接Linking:这个是核心的步骤。又可以大致分为三个小阶段:1、验证:检查JVM加载的字节信息是否符合Java虚拟机规范,否则就会报错。这一阶段是JVM的安全大门,防止黑客大神的恶意信息或者不合规信息危害JVM的正常运行。2、准备:这一阶段创建类或接口的静态变量,并给这些静态变量赋一个初始值(不是最终指定的值),这一部分的作用更大的是预分配内存。3、解析:这一步主要是将常量池中的符号引用替换为直接引用。例如我们有个类A调用了类B的方法,这些在代码层次还好只是一些对计算机没有意义的符号引用,在这一阶段就会转换成计算机所能理解的堆栈、引用等这些直接引用。
  • 初始化Initialization:这一步才是真正去执行类初始化的代码逻辑。包括执行static静态代码块,给静态变量赋值等。

实际上resolve这个参数就是表示需不需要进行连接阶段。从这里也能看出热加载机制的另一个很大的问题:热加载机制将一些在编译阶段就可以检查出来的问题全都延迟到了运行时,这对整个程序的安全性是一个很大的威胁。

第四章:到底加载的是哪个类?

程序员在某一次调试的过程当中,不小心在OA系统里留下了一个SalaryCaler类。这时,每次加载的都是OA系统内的这个SalayrCaler类,而不是我们预期的jar包里的计算类。这样就导致了我们之前的热加载机制全部失败了。

经过分析,问题就出在了双亲委派机制。

通过打破双亲委派机制,我们就实现了工资计算类优先从jar包中加载,而不取OA系统内的SalaryCaler类。

我们来想一下,我们这种方式有什么问题?

​ 我们把com.roy这样的包名,硬编码方式写到SalaryJarLoader中,这肯定是给系统以后的扩展留下了一个很大的隐患。所以,我们接下来,必须要找到一个方式,把com.roy这样的硬编码从程序中移除。

第五章:实现同类多版本共存

经过之前的分享,我们知道了在AppClassLoader和SalaryJarLoader的缓存中,都有一个com.roy.SalaryCaler。那我们可不可以把这两个类都拿出来?同时打印原价计算的salary和修改后的salary。

当程序员想要加载出SalaryJarLoader中的SalaryCaler类时,出现了一个神奇的异常

com.roy.SalaryCaler cannot be cast to com.roy.SalaryCaler

SalaryCaler我是谁?谁是我?我怎么不能转换成我自己?

其实这就是我们打破双亲委派机制之后,出现的问题。问题的根源就在于打破双亲委派机制后,AppClassLoader和SalaryJarLoader都分别加载出了一个SalaryCaler的类,而两个ClassLoader中的SalaryCaler类是无法进行类型转换的。

既然类型无法强转,那我们就只能通过反射的方式,来执行SalaryJarLoader中的SalaryCaler类的cal方法。通过这样的方式,我们实现了同类多版本共存。

​ 但是这跟我们上一章节提到的消灭com.roy硬编码,有什么关系呢?

​ 我们可以覆盖父类的双亲委派机制。优先从本地目录加载类,本地目录加载不到,再走双亲委派机制进行加载。通过这种方式,就解决了我们上一章节留下的要消灭com.roy硬编码的问题。

​ 但是我们又遇到一个让人非常不爽的问题:工资计算类到现在只能通过反射的方式去操作,而没办法声明成一个正常的类。

第六章:引入JAVA提供的SPI机制实现工资计算服务加载

​ 目的:是要让工资计算类SalaryClaer能够像一个正常类一样声明、使用。所谓的这个正常声明,其实就是说要把SalaryClaer转换成一个由AppClassLaoder加载出来的对象。

​ 分析: 就只能使用java的多态来表示这个问题。就是在OA系统里声明一个接口,而SalaryJarLoader提供接口的实现类。

​ 方案:引入java提供的SPI机制实现服务加载。

​ 我们就通过JAVA的SPI机制ServiceLoader.load(SalaryCalService.class,classloader); 实现了把工资计算类声明成一个对象的方式。

​ 通过调整线程向下文类加载器的方式,实现了工资计算类的稳定加载。

总结:

​ JAVA类加载机制,是从JDK源码向JVM底层学习的一个门户。

​ 第四章、第五章实现的加载流程,模拟的tomcat的类加载机制。

​ 第六章,SPI机制。 JDBC、 ShardingSphere、Dubbo

快乐学习,爱上JAVA。

这篇关于Java类加载的故事-修正终结版的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/222974

相关文章

Java实现字节字符转bcd编码

《Java实现字节字符转bcd编码》BCD是一种将十进制数字编码为二进制的表示方式,常用于数字显示和存储,本文将介绍如何在Java中实现字节字符转BCD码的过程,需要的小伙伴可以了解下... 目录前言BCD码是什么Java实现字节转bcd编码方法补充总结前言BCD码(Binary-Coded Decima

SpringBoot全局域名替换的实现

《SpringBoot全局域名替换的实现》本文主要介绍了SpringBoot全局域名替换的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录 项目结构⚙️ 配置文件application.yml️ 配置类AppProperties.Ja

Java使用Javassist动态生成HelloWorld类

《Java使用Javassist动态生成HelloWorld类》Javassist是一个非常强大的字节码操作和定义库,它允许开发者在运行时创建新的类或者修改现有的类,本文将简单介绍如何使用Javass... 目录1. Javassist简介2. 环境准备3. 动态生成HelloWorld类3.1 创建CtC

JavaScript中的高级调试方法全攻略指南

《JavaScript中的高级调试方法全攻略指南》什么是高级JavaScript调试技巧,它比console.log有何优势,如何使用断点调试定位问题,通过本文,我们将深入解答这些问题,带您从理论到实... 目录观点与案例结合观点1观点2观点3观点4观点5高级调试技巧详解实战案例断点调试:定位变量错误性能分

Java实现将HTML文件与字符串转换为图片

《Java实现将HTML文件与字符串转换为图片》在Java开发中,我们经常会遇到将HTML内容转换为图片的需求,本文小编就来和大家详细讲讲如何使用FreeSpire.DocforJava库来实现这一功... 目录前言核心实现:html 转图片完整代码场景 1:转换本地 HTML 文件为图片场景 2:转换 H

Java使用jar命令配置服务器端口的完整指南

《Java使用jar命令配置服务器端口的完整指南》本文将详细介绍如何使用java-jar命令启动应用,并重点讲解如何配置服务器端口,同时提供一个实用的Web工具来简化这一过程,希望对大家有所帮助... 目录1. Java Jar文件简介1.1 什么是Jar文件1.2 创建可执行Jar文件2. 使用java

SpringBoot实现不同接口指定上传文件大小的具体步骤

《SpringBoot实现不同接口指定上传文件大小的具体步骤》:本文主要介绍在SpringBoot中通过自定义注解、AOP拦截和配置文件实现不同接口上传文件大小限制的方法,强调需设置全局阈值远大于... 目录一  springboot实现不同接口指定文件大小1.1 思路说明1.2 工程启动说明二 具体实施2

Java实现在Word文档中添加文本水印和图片水印的操作指南

《Java实现在Word文档中添加文本水印和图片水印的操作指南》在当今数字时代,文档的自动化处理与安全防护变得尤为重要,无论是为了保护版权、推广品牌,还是为了在文档中加入特定的标识,为Word文档添加... 目录引言Spire.Doc for Java:高效Word文档处理的利器代码实战:使用Java为Wo

SpringBoot日志级别与日志分组详解

《SpringBoot日志级别与日志分组详解》文章介绍了日志级别(ALL至OFF)及其作用,说明SpringBoot默认日志级别为INFO,可通过application.properties调整全局或... 目录日志级别1、级别内容2、调整日志级别调整默认日志级别调整指定类的日志级别项目开发过程中,利用日志

Java中的抽象类与abstract 关键字使用详解

《Java中的抽象类与abstract关键字使用详解》:本文主要介绍Java中的抽象类与abstract关键字使用详解,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧... 目录一、抽象类的概念二、使用 abstract2.1 修饰类 => 抽象类2.2 修饰方法 => 抽象方法,没有