Featured image of post 类的动态加载

类的动态加载

类的动态加载

重写之前的Person类来理解

 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
38
39
40
41
42
43
44
45
46
47
48
package Serialize;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class Person implements Serializable {

    public String name;
    private int age;

    public static int id;
    static {
        System.out.println("静态代码块");
    }
    public static void staticAction(){
        System.out.println("静态方法");
    }
    {
        System.out.println("构造代码块");
    }

    public Person(){
        System.out.println("无参Person");
    }
    // 构造函数
    public Person(String name, int age){
        System.out.println("有参Person");
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString(){
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
        ois.defaultReadObject();
        Runtime.getRuntime().exec("calc");
    }

    public void action(String action){
        System.out.println(action);
    }
}

类加载会执行代码

初始化:静态代码块

实例化:构造代码块,无参构造函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package Serialize;

public class LoadClassTest {
    public static void main(String[] args) throws Exception {
//        new Person();//静态代码块  构造代码块  无参Person
//        Person.staticAction();//静态代码块 静态方法
//        Person.id =1;//静态代码块
//        Class c = Person.class;//只进行了类加载,没有初始化
    }
}

动态类加载方法

Class.forName可以选择初始化和不初始化,默认是初始化

1
2
3
4
5
6
@CallerSensitive
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName(className, caller);
    }

然后跟进可以看到forName0方法

1
2
3
4
5
6
7
@CallerSensitiveAdapter
    private static Class<?> forName(String className, Class<?> caller)
            throws ClassNotFoundException {
        ClassLoader loader = (caller == null) ? ClassLoader.getSystemClassLoader()
                                              : ClassLoader.getClassLoader(caller);
        return forName0(className, true, loader, caller);
    }

跟进

1
2
3
4
private static native Class<?> forName0(String name, boolean initialize,
                                            ClassLoader loader,
                                            Class<?> caller)
        throws ClassNotFoundException;

三个参数,类名,是否初始化,类加载器,第四个参数一般不填

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package Serialize;

public class LoadClassTest {
   public static void main(String[] args) throws Exception {
//        new Person();//静态代码块  构造代码块  无参Person
//        Person.staticAction();//静态代码块 静态方法
//        Person.id =1;//静态代码块
//        Class c = Person.class;//只进行了类加载,没有初始化
//        Class.forName("Serialize.Person");//静态代码块,因为默认初始化
       ClassLoader cl = ClassLoader.getSystemClassLoader();//获取系统的类加载器
       //Class.forName("Serialize.Person", false,LoadClassTest.class.getClassLoader());
       Class<?> c = Class.forName("Serialize.Person", false,cl);
       c.newInstance();

   }
}

那这个系统类加载器是什么,我们打印一下看

image-20250719125604401

可以看到是AppClassLoader

双亲委派模型

image-20250720112353044

双亲委派的意思是如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。

类加载器原理

可以用loadClass来进行类加载,没有初始化,我们跟到底层

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package Serialize;

public class LoadClassTest {
    public static void main(String[] args) throws Exception {
//        new Person();//静态代码块  构造代码块  无参Person
//        Person.staticAction();//静态代码块 静态方法
//        Person.id =1;//静态代码块
//        Class c = Person.class;//只进行了类加载,没有初始化
//        Class.forName("Serialize.Person");//静态代码块,因为默认初始化
        ClassLoader cl = ClassLoader.getSystemClassLoader();//获取系统的类加载器
        //Class.forName("Serialize.Person", false,LoadClassTest.class.getClassLoader());
//        Class<?> c = Class.forName("Serialize.Person", false,cl);
//        c.newInstance();
        Class<?> c = cl.loadClass("Serialize.Person");*

    }
}

然后在*这里下断点调试,这里要强制步过,才能到内部类里面

先是走到ClassLoader这个类里,

image-20250720133452932

调用的顺序

1
2
ClassLoader -> SecureClassLoader -> URLClassLoader -> AppClassLoader
loadClass -> findClass(重写的方法) -> defineClass (从字节码加载类)

动态加载字节码

利用URLClassLoader加载远程class文件

file协议

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package Serialize;

import java.net.URL;
import java.net.URLClassLoader;

public class LoadClassTest {
    public static void main(String[] args) throws Exception {
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///D:\\tmp\\classes\\")});
        Class<?> hello = urlClassLoader.loadClass("LoadTest");
        hello.newInstance();

    }
}

我们写一个LoadTest类然后先编译成.class文件,把他移到tmp目录下的classes目录,接着删掉这个类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package Serialize;

import java.io.IOException;

public class LoadTest {
    static{
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

然后运行测试能不能用file协议加载LoadTest.class然后弹计算器,这里有个坑点,我是新建了Serialize包,所以前下的路径也要包含Serialize

所以改完是这样

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package Serialize;

import java.net.URL;
import java.net.URLClassLoader;

public class LoadClassTest {
    public static void main(String[] args) throws Exception {
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///D:\\tmp\\classes\\")});
        Class<?> hello = urlClassLoader.loadClass("Serialize.LoadTest");
        hello.newInstance();

    }
}

成功弹计算器

http协议

在前下file协议写的目录下打开命令行用python起一个http服务监听

1
python -m http.server 2333

然后代码改为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package Serialize;

import java.net.URL;
import java.net.URLClassLoader;

public class LoadClassTest {
    public static void main(String[] args) throws Exception {
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://127.0.0.1:2333/")});
        Class<?> hello = urlClassLoader.loadClass("Serialize.LoadTest");
        hello.newInstance();

    }
}

image-20250720185901232

jar协议

也就是调用jar包

还是建议搞这个不要套软件包,不然很麻烦

先把class文件打包成jar包

1
jar -cvf LoadTest.jar LoadTest.class

然后把生成的jar包放在class文件同目录下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package Serialize;

import java.net.URL;
import java.net.URLClassLoader;

public class LoadClassTest {
    public static void main(String[] args) throws Exception {
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:file:///D:\\tmp\\classes\\LoadTest.jar!/")});
        Class<?> hello = urlClassLoader.loadClass("Serialize.LoadTest");
        hello.newInstance();

    }
}

http就改成

1
jar:http://127.0.0.1:2333/LoadTest.jar!/

用defineClass直接加载字节码

由于这个方法是私有的,所以需要用反射来调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package Serialize;

import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Paths;

public class LoadClassTest {
    public static void main(String[] args) throws Exception {
        ClassLoader cl = 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("D:\\tmp\\classes\\LoadTest.class"));
        Class c = (Class) method.invoke(cl,"Serialize.LoadTest",code,0,code.length);
        c.newInstance();
    }
}

这边报错说是没权限访问目录,我也不知道为什么,可能win的防护发力了吧

Unsafe加载字节码

新版jdk好像删除了defineClass方法,回退到1.8.0

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package Serialize;

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Paths;

public class LoadClassTest {
    public static void main(String[] args) throws Exception {
        ClassLoader cl = ClassLoader.getSystemClassLoader();//获取系统的类加载器
        byte[] code = Files.readAllBytes(Paths.get("D:\\tmp\\classes\\"));
        Class c = Unsafe.class;
        Field field =  c.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        Class c2 = (Class) unsafe.defineClass("Serialize.LoadTest",code,0,code.length,cl,null);
        c2.newInstance();
    }
}

TemplatesImpl 加载字节码

TemplatesImpl类内部还有TransletClassLoader类,继承ClassLoader

defineClass方法被重写了

1
2
3
Class<?> defineClass(final byte[] b) {
            return defineClass(null, b, 0, b.length);
        }

父类的defineClass方法是

1
2
3
4
5
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError
    {
        return defineClass(name, b, off, len, null);
    }

可以看到原来的protected变为default类型,在这个类里面追溯一下利用链

1
2
defineClass() <- defineTransletClasses() <- getTransletInstance() <- newTransformer()
<- getOutputProperties()

最前面的两个方法都是public类型,可以被外部调用

我们用newTransformer方法尝试加载字节码

因为getTransletInstance方法里面用到了AbstractTranslet,所以我们要继承AbstractTranslet

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package TemplateClassLoader;

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.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;

import java.io.IOException;


public class Test01 extends AbstractTranslet {
    public void transform(DOM document, SerializationHandler[] serializationHandler){}
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler serializationHandler) throws TransletException {}
    public Test01() throws IOException{
        super();
        Runtime.getRuntime().exec("calc");
    }
}

然后把这个编译成class文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package TemplateClassLoader;

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 Test02 {
    public static void main(String[] args) throws Exception {
        byte[] code = Files.readAllBytes(Paths.get("D:\\tmp\\classes\\TemplateClassLoader\\Test01.class"));
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl,"_name","calc");
        setFieldValue(templatesImpl,"_bytecodes",new byte[][]{code});
        setFieldValue(templatesImpl,"_tfactory",new TransformerFactoryImpl());
        templatesImpl.newTransformer();
    }
    public  static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
        Field field = target.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(target,value);
    }
}

这里同样出现了高版本jdk的问题,需要加参数强制开放 java.xml 模块

  • 允许未命名模块(ALL-UNNAMED)访问 java.xml 模块中的 com.sun.org.apache.xalan.internal.xsltc.trax 包。
1
2
--add-exports java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED
--add-exports java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime=ALL-UNNAMED
  • 不仅导出(exports),还开放(opens)反射访问权限(适用于 setAccessible(true) 的情况)。
1
2
--add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED
--add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime=ALL-UNNAMED

这样就能成功弹出计算器了

分析一下链子

getTransletInstance方法

 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
private Translet getTransletInstance()
        throws TransformerConfigurationException {
        try {
            if (_name == null) return null;

            if (_class == null) defineTransletClasses();

            // The translet needs to keep a reference to all its auxiliary
            // class to prevent the GC from collecting them
            AbstractTranslet translet = (AbstractTranslet)
                    _class[_transletIndex].getConstructor().newInstance();
            translet.postInitialization();
            translet.setTemplates(this);
            translet.setOverrideDefaultParser(_overrideDefaultParser);
            translet.setAllowedProtocols(_accessExternalStylesheet);
            if (_auxClasses != null) {
                translet.setAuxiliaryClasses(_auxClasses);
            }

            return translet;
        }
        catch (InstantiationException | IllegalAccessException |
                NoSuchMethodException | InvocationTargetException e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString(), e);
        }
    }

这里需要_name参数不为空才能进defineTransletClasses方法

defineTransletClasses方法

 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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
private void defineTransletClasses()
        throws TransformerConfigurationException {

        if (_bytecodes == null) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
            throw new TransformerConfigurationException(err.toString());
        }

        TransletClassLoader loader =
                new TransletClassLoader(ObjectFactory.findClassLoader(),
                            _tfactory.getExternalExtensionsMap());

        try {
            final int classCount = _bytecodes.length;
            _class = new Class<?>[classCount];

            if (classCount > 1) {
                _auxClasses = new HashMap<>();
            }

            // create a module for the translet

            String mn = "jdk.translet";

            String pn = _tfactory.getPackageName();
            assert pn != null && pn.length() > 0;

            ModuleDescriptor descriptor =
                ModuleDescriptor.newModule(mn, Set.of(ModuleDescriptor.Modifier.SYNTHETIC))
                                .requires("java.xml")
                                .exports(pn, Set.of("java.xml"))
                                .build();

            Module m = createModule(descriptor, loader);

            // the module needs access to runtime classes
            Module thisModule = TemplatesImpl.class.getModule();
            // the module also needs permission to access each package
            // that is exported to it
            PermissionCollection perms =
                new RuntimePermission("*").newPermissionCollection();
            Arrays.asList(Constants.PKGS_USED_BY_TRANSLET_CLASSES).forEach(p -> {
                thisModule.addExports(p, m);
                perms.add(new RuntimePermission("accessClassInPackage." + p));
            });

            CodeSource codeSource = new CodeSource(null, (CodeSigner[])null);
            ProtectionDomain pd = new ProtectionDomain(codeSource, perms,
                                                       loader, null);

            // java.xml needs to instantiate the translet class
            thisModule.addReads(m);

            for (int i = 0; i < classCount; i++) {
                _class[i] = loader.defineClass(_bytecodes[i], pd);
                final Class<?> superClass = _class[i].getSuperclass();

                // Check if this is the main class
                if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                    _transletIndex = i;
                }
                else {
                    _auxClasses.put(_class[i].getName(), _class[i]);
                }
            }

            if (_transletIndex < 0) {
                ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
                throw new TransformerConfigurationException(err.toString());
            }
        }
        catch (ClassFormatError e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
            throw new TransformerConfigurationException(err.toString(), e);
        }
        catch (LinkageError e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString(), e);
        }
    }

这里的_bytecodes不能为空,而且_tfactorygetExternalExtensionsMap方法跟进后是TransformerFactoryImpl类,也就是说_tfactory需要一个TransformerFactoryImpl对象

BCEL ClassLoader 加载字节码

BCEL 的全名应该是 Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为被 Apache Xalan 所使用,而 Apache Xalan 又是 Java 内部对于 JAXP 的实现,所以 BCEL 也被包含在了 JDK 的原生库中。

我们可以通过 BCEL 提供的两个类 RepositoryUtility 来利用: Repository 用于将一个Java Class 先转换成原生字节码,当然这里也可以直接使用javac命令来编译 java 文件生成字节码; Utility 用于将原生的字节码转换成BCEL格式的字节码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package Serialize;

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;


public class BCELClassLoader {
    public static void main(String[] args) throws Exception {
        Class calc = Class.forName("Serialize.LoadTest");
        JavaClass javaClass = Repository.lookupClass(calc);
        String code = Utility.encode(javaClass.getBytes(), true);
        System.out.println(code);
    }
}

这里同样高版本jdk会报错

1
2
--add-opens java.xml/com.sun.org.apache.bcel.internal=ALL-UNNAMED
--add-opens java.xml/com.sun.org.apache.bcel.internal.classfile=ALL-UNNAMED

image-20250721133112981

可以看到输出了一串BCEL字节码

我们可以直接利用这一串代码来加载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package Serialize;

import com.sun.org.apache.bcel.internal.util.ClassLoader;


public class BCELRCE {
    public static void main(String[] args) throws Exception {
        new ClassLoader().loadClass("$$BCEL$$"+"$l$8b$I$A$A$A$A$A$A$ffmQMo$d3$40$Q$7d$9b8$b1cl$92$3aM$K$e5$a3$d0$82H$7b$c0$3f$c0$R$X$daJU$5d$8aHT$ce$9b$cd$w$dd$d4$b1$pg$83$w$fe$Q$e7$5e$C$e2$c0$P$e0GU$9d5$nD$w$xy$c6$f3v$de$db$b7$3b$bfo$7f$fe$Cp$88$d7$$J$u$db$b0$3cTPeh$8c$f9$X$k$s$3c$j$85$e7$83$b1$U$9a$a1$daU$a9$d2$ef$Y$ca$9d$fd$L$X$Oj6$5c$P$P$e01l$fck$ff4O$b5$9aH$Gw$q$f5$aahu$f6$e3$7b$3d$91$83$87$M$96$e0$890z$N$P$h$I$I$90$d7R0$bc$e9$ac1z$3aW$e9$uZ$X$f9$98gB$cef$91$8dM$86f$81$ab$y$3c9$3f$ba$Wr$aaU$96$bah$a2$eda$cb$dc$a7$3e$r$be$eei$$$ae$fa9$X$d2$c6c$86$a0$ts$c5$T$f5U$86q$c6$87$7d9$a3kZ$ef$b3$n$Z$ae$c7$w$95$l$e6$93$81$cc$fb$7c$90$Q$S$c4$Z9$bd$e0D$a1z$JZ$faR$cd$Y6$e3$fbR$R$83$d3$V$c9$f2$d1$98y$84$f8$3f$$$a9$cd$_$7c$9d$f1$e9R$d4$ede$f3$5c$c8ce$K$ff$af$de$5bC$c6K$3c$a2I$99U$C3$b3$a2$b8MUH$99Q$ae$i$7c$H$bb$v$b6$9fP$ac$fe$B$f1$94$a2$b7$fc$7f$86$e7$94$j$ec$ac$c8$bc$Q$D$82$l$b0$D$7f$81$fa$e7opN$P$Wh$dd$Ux$8d$b8M$94$L$c56$v$Yv$cdL$9e$a2$P$97$c6$e6$afN$f0a$e1$F$d9$Ev$e9$b3Q$8am4$z$da$d8$xL$bd$ba$D$h$bf$abCs$C$A$A").newInstance();
    }
}

在前面加上$$BCEL$$,是因为在 ClassLoader#loadClass() 中,其会判断类名是否是 $$BCEL$$ 开头,如果是的话,将会对这个字符串进行 decode

使用 Hugo 构建
主题 StackJimmy 设计