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实现删除文件中的指定内容

《Java实现删除文件中的指定内容》在日常开发中,经常需要对文本文件进行批量处理,其中,删除文件中指定内容是最常见的需求之一,下面我们就来看看如何使用java实现删除文件中的指定内容吧... 目录1. 项目背景详细介绍2. 项目需求详细介绍2.1 功能需求2.2 非功能需求3. 相关技术详细介绍3.1 Ja

springboot项目中整合高德地图的实践

《springboot项目中整合高德地图的实践》:本文主要介绍springboot项目中整合高德地图的实践,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一:高德开放平台的使用二:创建数据库(我是用的是mysql)三:Springboot所需的依赖(根据你的需求再

spring中的ImportSelector接口示例详解

《spring中的ImportSelector接口示例详解》Spring的ImportSelector接口用于动态选择配置类,实现条件化和模块化配置,关键方法selectImports根据注解信息返回... 目录一、核心作用二、关键方法三、扩展功能四、使用示例五、工作原理六、应用场景七、自定义实现Impor

SpringBoot3应用中集成和使用Spring Retry的实践记录

《SpringBoot3应用中集成和使用SpringRetry的实践记录》SpringRetry为SpringBoot3提供重试机制,支持注解和编程式两种方式,可配置重试策略与监听器,适用于临时性故... 目录1. 简介2. 环境准备3. 使用方式3.1 注解方式 基础使用自定义重试策略失败恢复机制注意事项

SpringBoot整合Flowable实现工作流的详细流程

《SpringBoot整合Flowable实现工作流的详细流程》Flowable是一个使用Java编写的轻量级业务流程引擎,Flowable流程引擎可用于部署BPMN2.0流程定义,创建这些流程定义的... 目录1、流程引擎介绍2、创建项目3、画流程图4、开发接口4.1 Java 类梳理4.2 查看流程图4

一文详解如何在idea中快速搭建一个Spring Boot项目

《一文详解如何在idea中快速搭建一个SpringBoot项目》IntelliJIDEA作为Java开发者的‌首选IDE‌,深度集成SpringBoot支持,可一键生成项目骨架、智能配置依赖,这篇文... 目录前言1、创建项目名称2、勾选需要的依赖3、在setting中检查maven4、编写数据源5、开启热

Java对异常的认识与异常的处理小结

《Java对异常的认识与异常的处理小结》Java程序在运行时可能出现的错误或非正常情况称为异常,下面给大家介绍Java对异常的认识与异常的处理,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参... 目录一、认识异常与异常类型。二、异常的处理三、总结 一、认识异常与异常类型。(1)简单定义-什么是

SpringBoot项目配置logback-spring.xml屏蔽特定路径的日志

《SpringBoot项目配置logback-spring.xml屏蔽特定路径的日志》在SpringBoot项目中,使用logback-spring.xml配置屏蔽特定路径的日志有两种常用方式,文中的... 目录方案一:基础配置(直接关闭目标路径日志)方案二:结合 Spring Profile 按环境屏蔽关

Java使用HttpClient实现图片下载与本地保存功能

《Java使用HttpClient实现图片下载与本地保存功能》在当今数字化时代,网络资源的获取与处理已成为软件开发中的常见需求,其中,图片作为网络上最常见的资源之一,其下载与保存功能在许多应用场景中都... 目录引言一、Apache HttpClient简介二、技术栈与环境准备三、实现图片下载与保存功能1.

SpringBoot排查和解决JSON解析错误(400 Bad Request)的方法

《SpringBoot排查和解决JSON解析错误(400BadRequest)的方法》在开发SpringBootRESTfulAPI时,客户端与服务端的数据交互通常使用JSON格式,然而,JSON... 目录问题背景1. 问题描述2. 错误分析解决方案1. 手动重新输入jsON2. 使用工具清理JSON3.