大家好,我是小丁,一名小小程序员。
Java程序员应该都知道类加载器,双亲委托加载。
idea运行:
public static void main(String[] args) {
System.out.println(OrderService.class.getClassLoader());
}
运行这行代码,输出:
sun.misc.Launcher$AppClassLoader@18b4aac2
所以,如果使用idea运行程序,使用的加载器是AppClassLoader。
命令行运行:
java AppStart
输出:
jdk.internal.loader.ClassLoaders$AppClassLoader@18ff02e4
打成可执行jar包运行:
######maven配置
org.apache.maven.pluginsgroupId>maven-assembly-plugin 3.3.0 com.dxc.project1.AppStart jar-with-dependencies
make-assembly package single
执行:
java -jar project.jar
输出:
jdk.internal.loader.ClassLoaders$AppClassLoader@18ff02e4
自定义类加载器
非springboot项目
新建自定义类加载器CustomClassLoader
package com.dxc.project1.service;
import java.io.*;
public class CustomClassLoader extends ClassLoader {
// 指定类文件的路径
private String classPath;
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
public Class findClass(String name) throws ClassNotFoundException {
byte[] classData = null;
try {
// 将包名转换为路径,例如 "com.example.MyClass" 转换为 "com/example/MyClass.class"
String path = classPath + "/" + name.replace('.', '/') + ".class";
InputStream inputStream = new FileInputStream(path);
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
int len;
byte[] buffer = new byte[1024];
while ((len = inputStream.read(buffer)) != -1) {
byteStream.write(buffer, 0, len);
}
classData = byteStream.toByteArray();
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("Class " + name + " not found.");
}
if (classData == null) {
throw new ClassNotFoundException("Class " + name + " not found.");
} else {
return defineClass(name, classData, 0, classData.length);
}
}
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
String classPath = "/Users/dxc/Desktop/project/ProGuardTest/project2/target/classes"; // 替换为你的类文件路径
CustomClassLoader customClassLoader = new CustomClassLoader(classPath);
try {
// 加载类并创建实例
Class clazz = customClassLoader.loadClass("com.dxc.project2.Student");
Object instance = clazz.getDeclaredConstructor().newInstance();
System.out.println(instance.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
输出:
com.dxc.project1.service.CustomClassLoader@5305068a
??注意:
com.dxc.project2.Student不是classpath所在路径的类,不然即使你使用自定义类加载器,最后还是会使用AppClassLoader。
springboot项目
我们新建个springboot项目,看看它的默认的加载器。
####maven配置
org.springframework.boot spring-boot-maven-plugin 2.3.3.RELEASE com.dxc.project1.AppStart true repackage
@SpringBootApplication
@ComponentScan(basePackages = {"com.dxc"})
public class AppStart {
public static void main(String[] args) {
SpringApplication.run(AppStart.class, args);
System.out.println(OrderService.class.getClassLoader());
}
}
idea运行输出:
sun.misc.Launcher$AppClassLoader@18b4aac2
jar包运行:java -jar project.jar
org.springframework.boot.loader.LaunchedURLClassLoader@51521cc1
所以idea运行,用到的加载器是AppClassLoader,但是打成可执行jar包运行,使用的加载器是LaunchedURLClassLoader,这个类加载器会打包到可执行jar包里。
对于springboot项目,如果我们要使用自定义类加载器方案来做代码的保护,则需要先对打包好的jar包进行加密,一般的逻辑是:
1. 解析jar包中的文件,如果是.class文件,而且是需要加密的,则对class文件进行加密。
2. 在自定义类加载器中,先读取文件的字节码,判断前面四个字节是不是Java的魔鬼数字(0xCAFEBABE),如果不是魔数,则这个文件是加密后的,则进行解密;是魔数,则不需要解密。
在网上都没找到开箱即用的适合于springboot项目的类加载器,因为如果你要通过自定义类加载器来解密class文件,你可能要按照LaunchedURLClassLoader重写新的一套类加载器,然后设置上下文加载器,但是这个改动量较大。
@SpringBootApplication
@ComponentScan(basePackages = {"com.dxc"})
public class AppStart {
public static void main(String[] args) {
Thread.currentThread().setContextClassLoader(new CustomClassLoader());
SpringApplication.run(AppStart.class, args);
}
}
所以,对于springboot工程,需要通过自定义类加载器的方式来进行代码保护,我不太建议完全按照LaunchedURLClassLoader的方式来重写,因为需要修改的东西太多了,感兴趣的可以参考文章:
https://www.cnblogs.com/Chary/p/18277547
我推荐的方法:改写LaunchedURLClassLoader源码,重写defineClass。
先看下spring-boot-maven-plugin插件的依赖:
查看本地库的org.springframework.boot ? spring-boot-loader-tools的内容:
参考文档:
https://gitee.com/liu1204/plugin-gradle-spring-boot/blob/master/README.md
所以,spring-boot-maven-plugin插件会将这个jar包内loader目录下的spring-boot-loader.jar解压到可执行jar的根目录下,来作为springboot的启动和加载程序。
所以,如果要改写LaunchedURLClassLoader,则下载spring-boot-loader源码,改写源码后打包成jar,替换掉spring-boot-loader-tools中的spring-boot-loader.jar即可(??注意版本对应)。
源码地址:
https://gitee.com/liu1204/plugin-gradle-spring-boot.git
代码修改:
通过LaunchedURLClassLoader源码,最终会调用的是:
这两个defineClass最终调用的是ClassLoader的
我们只要在LaunchedURLClassLoader中重写这个defineClass。但是因为这个函数是final,无法继承,这个方式就无法行得通。
建议的方式:
LaunchedURLClassLoader重写findClass,代码逻辑可以拷贝URLClassLoader的findClass,在获取字节码代码后:
byte[] b = res.getBytes();
增加判断和解密程序:
private static final int MAGIC_NUMBER = 0xCAFEBABE;
boolean isMagicNumber(byte[] buffer) {
// 检查缓冲区长度是否至少为4个字节
if (buffer == null || buffer.length < 4) {
return false;
}
// 将前四个字节转换为一个整数(大端字节序)
int magicNumberFromBuffer = ((buffer[0] & 0xFF) << 24) |
((buffer[1] & 0xFF) << 16) |
((buffer[2] & 0xFF) << 8) |
(buffer[3] & 0xFF);
// 比较转换后的整数和特定的“魔鬼数字”
return magicNumberFromBuffer == MAGIC_NUMBER;
}byte[] bytes = res.getBytes();
//增加代码进行解密
if (isMagicNumber(bytes)){
//解密
bytes = decrypt(bytes);
}...
所以,对于springboot程序,通过自定义加载器方式去做加解密,来保护代码,是一项吃力不讨好的事情,而且这块代码会直接暴露在jar包类,没有进行加密。
只要稍微资深的程序员,很容易逆向工程,得到解密后的class。此方案可以作为学习用,实际项目中用处不大。
END
这个方案虽然在表面上看起来颇为简单,但当我们深入探究其内部机制时,会发现其实它并不简单。
尽管如此,这个方案却为我们提供了一个宝贵的契机,使我们能够深入了解类加载器的核心原理,并学习如何根据实际需求去自定义类加载器。
尽管在实际项目中,我们可能并不会直接采用这个方案,但它所蕴含的知识点和技能,无疑对我们的编程能力和对Java深层次机制的理解有着极大的提升作用。
本文暂时没有评论,来添加一个吧(●'◡'●)