欢迎光临散文网 会员登陆 & 注册

终于有人把Java类加载器讲清楚了!(2022最新180分钟超详细讲解)

2022-03-01 17:06 作者:房顶上的铝皮水塔  | 我要投稿

代码拆分

通过将处理逻辑的代码转移到jar包中,然后通过UrlClassLoader加载某一个位置的URL(jar包所在的位置),加载生成Class对象,然后通过反射的方式调用方法。

代码混淆

url可以指定一个网络地址,不一定是一个本地的目录。jar包中的class文件可以被反编译。

修改后缀名

(可以反编译class文件,但是无法反编译myclass) -> 自定义类加载器

如何自定义一个类加载器

1. 自定义的SalaryClassloader集成SecureClassloader

自定义一个SalaryClassloader用于加载字节码的二进制文件:

2. 重写findClass方法

将这个字节码处理为一个byte数组,然后调用defineClass,返回成Class文件。

3. 存在的问题

myclass中的文件内容没有被改变,可以稍作处理。比如可以生成一种新的class文件,在文件的第一个位置放入一个值(在读取的时候也要做对应的处理)。这样可以避免文件被反编译


实现一个加载Jar包的类加载器

实现一个SalaryJarLoader ,具体的加载逻辑和直接加载文件,读取文件字节流的方式类似。


实现热加载


生成一个Jar文件修改工资计算的方式,但是再次生成Jar文件覆盖的时候,修改并没有生效。每次都需要重启OA系统。


1. 为什么不会生效

因为会从缓存中加载


2.如何修改 :在方法中生成ClassLoader,而不是在最开始执行生成一个。这样就可以每次清除ClassLoader中的缓存

3. 热加载机制为什么很少使用?

  • 热加载机制可能出现问题
  • 热加载机制会产生很多的垃圾对象
  • 默认不会执行链接


打破双亲委派

出现的问题:如果我们在OASystem的项目中也添加了一个SalaryCaler类,在后续加载的过程中,不会加载我们定义在jar中的类。

  • 为什么会失败?

原因在双亲委派机制上。我们定义的SalaryJarLoader的默认parent是AppClassLoader,AppClassLoader在加载的过程中会首先加载我们定义在OASystem中的SalaryCaler。当SalaryJarLoader在准备加载SalaryCaler时,会在AppClassLoader中找到缓存。这样一来就不会加载我们定义在jar中的SalaryCaler (这两个类的全限定名称是相同的,但是在不同的project中)


解决方式1:通过限定名称做分支,首先加载自身的SalaryCaler:


解决方式2:解决硬编码的问题


在上一节中,虽然可以使用全限定名称让SalaryJarLoader优先加载定义在jar中的Java文件,而不是首先查看Parent是否加载过。但是使用全限定名称进行分支判断的方式存在扩展性的问题。


所以可以重写ClassLoader#loadClass方法,优先执行SalaryJarLoader#findClass方法,找到并且返回jar中的Class文件,如果没有,再去AppClassLoader中加载。

保存多个版本的代码


使用new创建实例和使用ClassLoader加载一个外部jar包类中的类

会抛出异常:

这是因为两个类的名字确实相同,但是本质上不是一个类。(可能是为了规避潜在的类型问题)


只能使用反射的方式:

通过类加载机制构造一个Class对象,然后通过newInstance创建一个Object对象,然后使用Class#getMethod获取Method,传入方法名、参数、实例对象 反射调用Method


所以为了同时保存多个版本的代码,我们首先需要将定义在OASystem同一个project中的SalaryCaler提出,形成一个jar包,并且通过ClassLoader+反射,构造两个类。



SPI机制完成进一步的优化


通过ClassLoader实现的反射的方式可以建立一个Object,但是这种方法不够优雅,我们期望能够建立一个Interface,然后通过实现接口的方式提供服务。


jdk提供的一种机制。resources下面建立META-INF文件夹,其中再建立services文件夹,在建立一个文件定义加载的内容。

  • Jar中:实现一个SalaryCalerService的实现类

在客户端(也就是需要通过SPI机制注入服务的OASystem类)定义文件内容:

如果有多个实现类,也需要写。

因为ServiceLoader#load返回的是当前的interface的所有实现类,所以需要使用迭代器模式。默认会从AppClassLoader中加载,如果需要加载我们ClassLoader,使用load的重载即可。



总结

Java类加载机制的模型:

Java的类加载机制分成三个部分:加载-链接-初始化 每一个步骤的主要流程如下:

1. 加载过程 Classloader#loadClass


加载过程也是我们可以干预的过程。在加载器中就体现在ClassLoader#loadClass。

2. 双亲委派机制

双亲委派机制不是说一个加载器是另外一个加载的父节点,而是像链表一样的持有和传递的关系。

通常我们写的类都会使用AppClassLoader进行加载。



3. 自定义一个类加载器


类似于课程中的方法,如果要自定义一个类加载器,因为我们默认使用了双亲委派机制,loadClass的过程不需要我们考虑,我们需要重写findClass方法。在findClass方法中我们需要读取某一个地方的Class字节流文件。因为这个字节流文件可能是进行加密处理过,我们可以在这里使用某种方式,还原成原本的Class二进制的byte数组,然后传递给defineClass。通过defineClass可以返回一个Class对象。


4. 热加载机制

如果我们想要临时修改Class文件的执行行为,我们直接修改Class文件行不通。

  • 文件在ClassLoader执行时已经读取了,后面不会再通过这个路径继续读取
  • 文件已经被读取到ClassLoader中存在缓存

解决问题1: 在后续第二次执行时,再次读取Class文件的路径

解决问题2:需要重新初始化ClassLoader对象


5. 更优雅的实现

一个接口存在多个实现类。我们需要借助SPI机制,也就是ServiceLoader接口,对于特定的接口,我们通过META-INF会定义他的实现类名等等。然后我们还可以再ServiceLoader#load时传入自定义的ClassLoader对象,修改读取Class实现类的行为。















终于有人把Java类加载器讲清楚了!(2022最新180分钟超详细讲解)的评论 (共 条)

分享到微博请遵守国家法律