
模块化介绍
这是jdk8的库:
这是jdk17的库:
从jdk9开始,就引入了模块化机制,旨在提高 Java 的可维护性、可扩展性和安全性,同时改善大型应用程序和库的构建和管理。模块化可以比作建造一座房子,将其分成多个功能明确的房间。每个房间(模块)负责特定的任务,如客厅用于接待、厨房用于做饭等,这样使得整个房子(软件项目)结构清晰、易于理解和维护。模块之间互不干扰,减少了功能冲突,允许独立开发和测试,提升了开发效率。此外,模块化还可以按需加载,节省资源,并使得维护工作变得更加简单,只需修复出现问题的模块,而不影响其他部分。
不过,jdk9时,模块化机制对我们进行反序列化没有实际影响,只是会打印警告而已,并不会直接报错中断。
模块化的强封装是从jdk17开始的。任何对java.*代码中的非public变量和方法进行反射会抛出InaccessibleObjectException异常。
但是每个模块中有一个module-info.java文件,里面定义了模块中的哪些包可以被外部访问,以及本模块依赖于哪些包。
1、export
通过 exports
声明的包中的公共类和接口可以被其他模块访问。
如果没有exports的话,你自己的类都没法去new
2、requires
表示依赖哪个模块。transitive表示隐式依赖。在这种情况下,任何使用了 java.desktop 模块的代码都能隐式的使用 java.xml 模块中的类。例如,如果 java.desktop 模块中某个方法返回了一个来自于 java.xml 模块中的类型,那么使用 java.desktop 模块的代码就间接依赖 java.xml 模块,如果没有 transitive 的话,那么使用 java.desktop 模块的模块就必须显式声明依赖 java.xml 模块才能正常编译,否则 java.xml 模块是不可见的。
3、opens
表示哪些包是对外开放的,即可以被反射调用。有to的话,就是指定对哪个模块开放。
1 | public class Failed { |
上面这段代码,在jdk8可以实现构造方法执行,但在jdk17中,会报错。
从抛出的异常中可以知道,java.base模块中的java.lang包没有对uname模块开放,也就是没有对我们自定义的类开放。
源码分析
去看java.lang.reflect.AccessibleObject#checkCanSetAccessible(java.lang.Class>, java.lang.Class>, boolean)
这里有三种方法能返回true:
1、调用类(自己的类)的模块和被调用类的模块名相同
2、调用类的模块和Object类的模块相同,即java.base
3、被调用类的模块是未命名的
这里我们会选择第一种,因为callerModule是通过caller.getModule获取的,我们也许能直接修改里面的module字段。方法二也可以,但是不通用,setAccessible能通过,但是其他反射调用,比如newInstance的时候就不一定行。
往下看,还有三种方法能返回true。前两个的先决条件是:被调用类是public,被调用类所在的包被exports了
1、被调用的方法或字段是public的
2、被调用的方法或字段是protect-static的,且调用者是被调用者的子类
3、被调用的包对调用者是open的
这三种都不考虑,因为这些都是直接写死在代码里的,没有操作空间。
绕过方法
patchModule
要用到Unsafe类,这个类提供了类似C语言的指针操作,能够直接修改对象中的字段值。
首先,Unsafe类是opens的:
所以咱们可以直接在自己的类中使用。
Unsafe类中有几个关键方法:
objectFieldOffset:获取字段的偏移量
getAndSetObject:获取一个对象在特定内存偏移量上的当前值,并将其替换为一个新值。
putObject:用于将一个对象(或引用)写入到指定对象的内存偏移量
putObject和getAndSetObject方法功能很相似,不过getAndSetObject
会返回被替换的旧值,当然里面细节也有所不同,比如getAndSetObject
会使用原子操作保障同时执行读取和写入。不过就我们修改类的Class对象的moudle属性来说,两者都可以。
这里callerModule是通过caller.getModule()获取的,而caller是一个Class对象。当调用类,也就是我们自己的类加载到JVM时,会产生一个Class对象,这个Class对象是唯一的,里面的Module字段也是唯一的。所以,我们能够保证修改成功。
最终代码:
1 | package com.test; |
增加虚拟机参数选项
由于上面的过程只是为了能够在本地调用其他模块,所以也可以通过添加虚拟机参数选项的方法实现。
需要注意的是,如果反序列化过程中,有第三方组件对jdk中的模块进行反射调用,就不能通过这种方式去修改。否则,虽然你本地可以打通,但是远程是打不通的,因为远程的JVM可没有打开这个选项。上面的patchModule也是同样的道理。必须在反序列化过程中,执行patchModule才行。
补充
java.lang.reflect.Constructor#newInstance–>java.lang.reflect.AccessibleObject#checkAccess–>java.lang.reflect.AccessibleObject#verifyAccess–>java.lang.reflect.AccessibleObject#slowVerifyAccess–>jdk.internal.reflect.Reflection#verifyModuleAccess–>java.lang.Module#isExported(java.lang.String, java.lang.Module)–>java.lang.Module#implIsExportedOrOpen–>java.lang.Module#isStaticallyExportedOrOpen–>java.lang.Module#allows
newInstance反射最终是这样绕过的,所以上面patchModule才选择使调用者和被调用者的模块一致,而不是简单地使调用者的模块为java.base
这里的绕过可以看这篇文章: