大家好,我是小丁,一名小小程序员。
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配置<plugin><groupId>org.apache.maven.pluginsgroupId><artifactId>maven-assembly-pluginartifactId><version>3.3.0version><configuration><archive><manifest><mainClass>com.dxc.project1.AppStartmainClass>manifest>archive><descriptorRefs><descriptorRef>jar-with-dependenciesdescriptorRef>descriptorRefs>configuration><executions><execution><id>make-assemblyid><phase>packagephase><goals><goal>singlegoal>goals>execution>executions>plugin>
执行:
java -jar project.jar
输出:
jdk.internal.loader.ClassLoaders$AppClassLoader@18ff02e4
自定义类加载器
非springboot项目
新建自定义类加载器CustomClassLoader
package com.dxc.project1.service;import java.io.*;
public CustomClassLoader extends ClassLoader { // 指定类文件的路径 private StringPath;
public CustomClassLoader(StringPath) {
this.classPath =Path; }
@Override public Class findClass(String name) throws ClassNotFoundException {
byte[]Data = null;
try { // 将包名转换为路径,例如 "com.example.MyClass" 转换为 "com/example/MyClass.class" String path =Path + "/" + 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,Data, 0,Data.length); } }}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
StringPath = "/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配置<plugin><groupId>org.springframework.bootgroupId><artifactId>spring-boot-maven-pluginartifactId><version>2.3.3.RELEASEversion><configuration><mainClass>com.dxc.project1.AppStartmainClass><fork>truefork>configuration><executions><execution><goals><goal>repackagegoal>goals>execution>executions>plugin>
@SpringBootApplication@ComponentScan(basePackages = {"com.dxc"})public 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 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深层次机制的理解有着极大的提升作用。