前言 cc链看到cc3,涉及到类加载,之前囫囵吞枣地学了些,还是不够牢固。于是写篇笔记给自己理清楚。参考了很多前辈的文章,感谢,如果有任何不对的,欢迎找我指正🙏
Java类加载机制 借用网上一张图
.java 编译后生成 .class文件保存在本地,如果我们要执行class文件,就需要经过一系列的生命周期和初始化操作,最后目的就是让JVM成功执行。
看这个流程图就知道类的加载就是由java类加载器实现的,作用将类文件进行动态加载到java虚拟机内存中运行。 
几个类加载器 BootstrapClassLoader 这个也叫引导类加载器,比较底层,由C++代码编写,属于JVM的一部分。不继承 java.lang.ClassLoader 类,也没有父加载器,主要负责加载核心 java 库(即 JVM 本身),存储在 /jre/lib/rt.jar 目录当中。(同时处于安全考虑,BootstrapClassLoader 只加载包名为 java、javax、sun 等开头的类)。
ExtensionsClassLoader 也叫拓展类加载器,由 sun.misc.Launcher$ExtClassLoader 类实现,用来在 /jre/lib/ext 或者 java.ext.dirs 中指明的目录加载 java 的扩展库。Java 虚拟机会提供一个扩展库目录,此加载器在目录里面查找并加载 java 类。
AppClassLoader App类加载器/系统类加载器(AppClassLoader),由 sun.misc.Launcher$AppClassLoader 实现,一般通过( java.class.path 或者 Classpath 环境变量)来加载 Java 类,也就是我们常说的 classpath 路径。通常我们是使用这个加载类来加载 Java 应用类,可以使用 ClassLoader.getSystemClassLoader() 来获取它。
UserDefineClassLoader 除了上面说的三种,用户还可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器。
我们介绍的几个ClassLoader之间有父子关系(不是继承),在Java.lang.ClassLoader里面定义了指向父加载器的常量 parent, 可以通过调用 getParent() 方法获取父加载器。可以看一个代码
1 2 3 4 5 6 7 public  class  ClassLoaderTest  {    public  static  void  main (String[] args)  {         System.out.println(ClassLoader.getSystemClassLoader());         System.out.println(ClassLoader.getSystemClassLoader().getParent());         System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());     } } 
打印出来是(这里的null其实就是BootstrapClassLoader,因为是c++实现的,所以无法在java中获取到相应的引用)
1 2 3 sun.misc.Launcher$AppClassLoader@5ce65a89 sun.misc.Launcher$ExtClassLoader@1edf1c96 null 
几个ClassLoader关系如下:
所有ClassLoader都继承自java.lang.ClassLoader 这个抽象类, ExtClassLoader 和 AppClassLoader 继承自 URLClassLoader, URLClassLoader既可以加载本地字节码,也可以加载远程字节码。
java.lang.ClassLoader 是所有 ClassLoader 的基石,在这个抽象类中定义了几个比较重要的方法
loadClass(): 基于双亲委派机制查找 Class, 调用父加载器的 loadClass 方法或自身的 findClass 方法 
findClass(): 根据名称和位置读取字节码, 并调用 defineClass 方法, 具体实现由子类重写 
defineClass(): 把 byte 数组形式的字节码转换成对应的 Class 对象 (真正加载字节码的地方) 
 
1 loadClass() -> findClass() -> defineClass() 
双亲委派机制 先说一下class有两种加载方式
隐式加载:通过 new 实例化类, 或通过 类名.方法名() 调用其静态方法, 或调用其静态属性
显式加载:通过反射的形式, 例如 Class.forName() 或者调用 ClassLoader 的 loadClass 方法
 
然后我们的类加载基于双亲委派机制:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载 
可以看一下实现代码,java.lang.ClassLoader的loadClass()方法 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 protected  Class<?> loadClass(String name, boolean  resolve)        throws  ClassNotFoundException     {         synchronized  (getClassLoadingLock(name)) {                          Class<?> c = findLoadedClass(name);              if  (c == null ) {                 long  t0  =  System.nanoTime();                 try  {                     if  (parent != null ) {                         c = parent.loadClass(name, false );                      } else  {                         c = findBootstrapClassOrNull(name);                      }                 } catch  (ClassNotFoundException e) {                                                           }                 if  (c == null ) {                                                               long  t1  =  System.nanoTime();                     c = findClass(name);                                           sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);                     sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);                     sun.misc.PerfCounter.getFindClasses().increment();                 }             }             if  (resolve) {                 resolveClass(c);             }             return  c;         }     } 
简单流程就是1、先检查类是否已经被加载过 2、若没有加载则调用父加载器的loadClass()方法进行加载 3、若父加载器为空则默认使用启动类加载器作为父加载器。 4、如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
不同场景下代码块加载顺序 基础认知
静态代码块:static{} 
构造代码块:{} 
无参构造器:ClassName() 
有参构造器:ClassName(String name) 
 
先写一个Dog.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package  com;public  class  Dog  {    public  static  int  staticVar;     public  int  instanceVar;     static  {         System.out.println("静态代码块" );     }     {         System.out.println("构造代码块" );     }     Dog(){         System.out.println("无参构造器" );     }     Dog(int  instanceVar){         System.out.println("有参构造器" );     }     public  static  void  staticAction () {         System.out.println("静态方法" );     } } 
场景一:实例化对象 1 2 3 4 5 6 7 package  com;public  class  Main  {    public  static  void  main (String[] args)  {         Dog  dog  =  new  Dog ();     } } 
打印
所以当我们使用new来实例化对象的时候,会先调用静态代码块,再调用动态代码块,再根据不同的实例化方法调用不同的构造器
场景二:调用静态方法 1 2 3 4 5 public  class  Main  {    public  static  void  main (String[] args)  {         Dog.staticAction();     } } 
打印
所以当我们不实例化对象直接调用静态方法,会先调用类中的静态代码块,然后调用静态方法
场景三:对类中的静态成员赋值 1 2 3 4 5 public  class  Main  {    public  static  void  main (String[] args)  {         Dog.staticVar = 1 ;     } } 
打印
所以对静态成员变量赋值前,会调用静态代码块
场景四:使用class获取类 1 2 3 4 5 public  class  Main  {    public  static  void  main (String[] args)  {         Class  c  =  Dog.class;     } } 
没有输出任何东西
场景五:使用forName获取类 1 2 3 4 5 public  class  Main  {    public  static  void  main (String[] args)  throws  ClassNotFoundException{         Class.forName("com.Dog" );     } } 
打印 
还有
1 2 3 4 Class.forName("com.Dog" , true , ClassLoader.getSystemClassLoader()); Class.forName("com.Dog" , false , ClassLoader.getSystemClassLoader()); 
利用URLClassLoader加载class文件 URLClassLoader 实际上是我们平时默认使用的 AppClassLoader 的父类,所以,我们解释 URLClassLoader 的工作过程实际上就是在解释默认的 Java 类加载器的工作流程。
先新建一个calc类,注意这里没有声明Calc再哪个包,就是自由的
1 2 3 4 5 6 7 8 9 10 11 import  java.io.IOException;public  class  Calc  {    static  {         try  {             Runtime.getRuntime().exec("open -a Calculator" );         } catch  (IOException e){             e.printStackTrace();         }     } } 
构建生成Calc.class文件,把它放在目录下
file协议 1 2 3 4 5 6 7 8 9 10 11 12 13 package com; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; public class URLLoader {     public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {         URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///Users/lingtian/Downloads/")});         Class calc = urlClassLoader.loadClass("Calc");         calc.newInstance();     } } 
http协议 在Clac.class所在目录开一个http服务
1 2 3 4 5 6 7 8 9 10 11 12 13 package  com;import  java.net.MalformedURLException;import  java.net.URL;import  java.net.URLClassLoader;public  class  URLLoader  {    public  static  void  main (String[] args)  throws  MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {         URLClassLoader  urlClassLoader  =  new  URLClassLoader (new  URL []{new  URL ("http://localhost:8000/" )});         Class  calc  =  urlClassLoader.loadClass("Calc" );         calc.newInstance();     } } 
服务器这边也可以看到接收到请求
jar+file 1 jar -cvf Calc.jar Clac.class 
生成一个jar文件
1 2 3 URLClassLoader  urlClassLoader  =  new  URLClassLoader (new  URL []{new  URL ("jar:file:///Users/lingtian/Downloads/Calc.jar!/" )});Class  calc  =  urlClassLoader.loadClass("Calc" );calc.newInstance(); 
就是调用前面多一个jar:,总体逻辑是一样的,要注意包声明的位置,需要对得上
jar+http 1 2 3 URLClassLoader  urlClassLoader  =  new  URLClassLoader (new  URL []{new  URL ("jar:http://127.0.0.1:8000/Calc.jar!/" )});Class  calc  =  urlClassLoader.loadClass("Calc" );calc.newInstance(); 
利用 ClassLoader#defineClass 直接加载字节码 默认的ClassLoader#defineClass方法是native,逻辑在JVM里面的C语言
看一下defineclass的调用模式,name就是类名,b为字节码数组,off为偏移量,len为字节码长度
1 protected  final  Class<?> defineClass(String name, byte [] b, int  off, int  len)
因为是保护的属性,无法在外部直接访问,所以我们需要反射调用defineClass() 方法进行字节码的加载
1 2 3 4 5 6 7 8 9 10 public  class  URLLoaderTest  {    public  static  void  main (String[] args)  throws  IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {         ClassLoader  classLoader  =  ClassLoader.getSystemClassLoader();          Method  method  =  ClassLoader.class.getDeclaredMethod("defineClass" , String.class, byte [].class, int .class, int .class);         method.setAccessible(true );          byte [] code = Files.readAllBytes(Paths.get("/Users/lingtian/Downloads/Calc.class" ));          Class  c  =  (Class) method.invoke(classLoader, "Calc" , code, 0 , code.length);          c.newInstance();      } } 
使用ClassLoader#defineClass直接加载字节码有个优点就是不需要出网也可以加载字节码。
利用Unsafe 加载字节码 Unsafe也有defineClass(),本质和上一个差不多
1 2 3 4 5 6 7 8 9 10 11 12 13 public  static  void  main (String[] args)  throws  IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {        byte [] code = Files.readAllBytes(Paths.get("/Users/lingtian/Downloads/Calc.class" ));         ClassLoader  classLoader  =  ClassLoader.getSystemClassLoader();         Class<Unsafe> unsafeClass = Unsafe.class;         Field  unsafeField  =  unsafeClass.getDeclaredField("theUnsafe" );         unsafeField.setAccessible(true );         Unsafe  classUnsafe  =  (Unsafe) unsafeField.get(null );         Method  defineClassMethod  =  unsafeClass.getMethod("defineClass" , String.class, byte [].class,int .class, int .class, ClassLoader.class, ProtectionDomain.class);         Class  cc  =  (Class) defineClassMethod.invoke(classUnsafe, "Calc" , code, 0 , code.length, classLoader, null );         cc.newInstance();     } 
TemplatesImpl加载字节码 为什么要用这个 为什么用这个:在实际场景中,因为defineClass方法作用域却是不开放的,所以我们很很难直接利用到它,但是我们的TemplatesImpl里的作用域是default
目标,调用到里面的TemplatesImpl有一个内部类,TransletClassLoader,里面调用了defineClass方法(重写),而且这个内部类继承了ClassLoader,而且这个重写的defineClass方法没有写明作用域,就默认是default,所以可以被类外部调用
调用链 确定了目标,接下来确定一下调用链
1 2 3 4 5 TransletClassLoader#defineClass() 	-> TemplatesImpl#defineTransletClasses() 	 -> TemplatesImpl#getTransletInstance() 	 	-> TemplatesImpl#newTransformer() 	 		-> TemplatesImpl#getOutputProperties() 
倒推前面一两个属性都是private,直到newTransformer和getOutputProperties才是public
注意点1 这里有一个点,就是在找defineTransletClasses上一个调用方法的时候,其实是有三个
但是这里getTransletClasses()和 getTransletIndex() 都在调用defineTransletClasses()后没别的动作,那么最后加载的类就无法被实例化/初始化
咱们getTransletInstance()在取得了_class后会对数组里面对应的队形进行无参数 初始化,满足了我们的要求
poc 确定了调用链,先构造一个准备被调用的类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package  com;import  com.sun.org.apache.xalan.internal.xsltc.DOM;import  com.sun.org.apache.xalan.internal.xsltc.TransletException;import  com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import  com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import  com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import  com.sun.org.apache.xml.internal.serializer.SerializationHandler;import  java.io.IOException;public  class  TemplatesBytes  extends  AbstractTranslet  {    public  TemplatesBytes ()  throws  IOException{          super ();         System.out.println("Hello TemplatesBytes" );     }     @Override      public  void  transform (DOM document, SerializationHandler[] handlers)  throws  TransletException {}     @Override      public  void  transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler)  throws  TransletException {} } 
这里这个类需要继承com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
然后因为AbstractTranslet这个类里面有一些抽象方法,所以需要重写,然后就直接写我们要做的事情,这里控制打印一下就好
POC: 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package  com;import  com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import  com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import  java.lang.reflect.Field;import  java.nio.file.Files;import  java.nio.file.Paths;public  class  TemplatesRce  {    public  static  void  main (String[] args)  throws  Exception{         byte [] code = Files.readAllBytes(Paths.get("/Users/lingtian/Downloads/TemplatesBytes.class" ));         TemplatesImpl  templates  =  new  TemplatesImpl ();         setFieldValue(templates, "_name" , "Calc" );         setFieldValue(templates, "_bytecodes" , new  byte [][] {code});         setFieldValue(templates, "_tfactory" , new  TransformerFactoryImpl ());         templates.getOutputProperties();     }     public  static  void  setFieldValue (Object obj, String fieldName, Object value)  throws  Exception{         Field  field  =  obj.getClass().getDeclaredField(fieldName);         field.setAccessible(true );         field.set(obj, value);     } } 
setFieldValue这个好高明,直接定义一个方法调用,避免冗余,主要逻辑就是获取成员变量然后修改里面的值就好
注意点2 链子一开始可以是newTransformer()也可以是getOutputProperties(),因为都是public,多一个getOutputProperties去调用也没什么影响,直接newTransformer也是可以的,不用纠结这个,直接修改代码里面的这个就行
参考 b站:白日梦组长
https://drun1baby.top/2022/06/03/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%9F%BA%E7%A1%80%E7%AF%87-05-%E7%B1%BB%E7%9A%84%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD/ 
https://exp10it.io/2022/11/java-classloader/ 
https://xz.aliyun.com/news/8556 
https://www.anquanke.com/post/id/260902 
https://www.cnblogs.com/hollischuang/p/14260801.html