Java类加载

1 类加载机制

1.1 双亲委派机制

1.2 沙箱安全机制

保护JDK内部的类不会被应用覆盖,java不允许定义以java开头的类。

1.3 缓存机制

● 类加载器级别加载过的类加载对象缓存再次访问只需调用findLoadedClass()即可获取

● 缓存jvm层面缓存java CLassLoader对象存在一个classed属性可以查看当前类加载器加载了哪些

1.4 全盘委托机制

一个ClassLoader加载当前当前父类以及依赖所有都由这个CLassLoader加载同时这个类加载器加载当前父类以及依赖符合双亲委派机制

2 类加载器

类加载器继承体系

3 类加载过程

● 加载:通过类加载器将class文件加载到JVM中

● 验证:验证class文件是否符合JDK规范

● 准备:为静态变量分配内存并赋默认值,其中final修饰的静态变量在改阶段赋初值

● 解析:将符号引用解析为直接引用

● 初始化:静态变量赋初值,执行静态代码块(clinit方法),初始化当前类的父类

● 静态变量赋初值&final静态变量赋初值时机验证代码:

package com;
public class App {
    private  static App app = new App(5);
    private  static  int a = 10;
    private  final static  int b = 20;
    private int c;
    private int d;
    public App(int e){
        this.c = a-e;
        this.d = b-e;
    }
    public static void main(String[] args) {
        System.out.println(app.c);
        System.out.println(app.d);
    }
}

打印后输出:

c=-5:结果分析在静态变量app初始化时,静态变量a还未赋初值

d=15:结果分析在静态变量app初始化时,静态变量b已赋初值

通过字节码分析也可以看出来,

对于变量b的赋初值并未在初始化方法clinit方法中完成

对于变量a的赋初值是在创建完变量app后完成的

4 加载类的三种方式

● 使用类:创建类的对象,调用类的静态方法,访问类的静态属性

● 类加载器的loadClass()方法,该方法只会将类加载进内存,不会执行后续初始化逻辑

● Class.forName()方法,该方法会执行类的初始化方法

● 类加载器loadClass()&Class.forName()区别的验证代码

public class Person {
    public  static  Integer age =10;
    static {
        System.out.println(11111);
    }
}
class PersonTest{
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        ClassLoader classLoader = Test.class.getClassLoader();
        Class<?> aClass = classLoader.loadClass("com.load_class.Person");
//        aClass.newInstance();
        System.out.println("=====================");
        Class.forName("com.load_class.Person");
    }
}

5 通过类加载器引入外部jar

● 使用URLClassLoader

● 外部jar可以通过本地文件,远程服务器,classpath等方式指定

● 代码示例

public static void main(String[] args) throws Exception {
    URL url = new URL("file:D:\\workplat\\test\\target\\test-1.0-SNAPSHOT.jar");
    URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
    Object o = classLoader.loadClass("com.Test").newInstance();
    o.getClass().getMethod("sayHello").invoke(o);
}

6 通过自定义类加载器实现代码加密

● 准备测试类HelloTest

public class HelloTest {
    public  void sayHello(){
        System.out.println("hello test11");
    }
}

● 自定义类加密工具类

public class ClassFileTranformUtils {
    public static final byte[] MAGIC_HEADER = new byte[]{(byte) 0xfe, (byte) 0x62, (byte) 0x69, (byte) 0x6e};
    public static  Boolean tranformClassFile(String classFile){
        File file = new File(classFile);
        File outFile = new File(file.getParent(),file.getName().replace(".class",".gz"));
        try(
             FileInputStream is = new FileInputStream(classFile);
             FileOutputStream os = new FileOutputStream(outFile)
           ) {
            os.write(MAGIC_HEADER);
            byte[] buf = new byte[1024];
            int len = 0;
            while ((len=is.read(buf))!=-1){
                os.write(buf,0,len);
            }
        }catch (Exception e){
            return false;
        }
        return true;
    }

    public static void main(String[] args) {
        ClassFileTranformUtils.tranformClassFile("D:\\workplat\\test\\test1\\target\\classes\\com\\test\\HelloTest.class");
    }
}

● 自定义类加载器

public class CustomClasloader extends ClassLoader {
    private String classPath;

    public CustomClasloader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    public Class findClass(String name) throws ClassNotFoundException {
        byte[] b = loadClassData(name);
        return defineClass(name, b, 0, b.length);
    }

    private byte[] loadClassData(String name) throws ClassNotFoundException {
        String classFullName = this.classPath + name.replace(".", File.separator) + ".gz";
        File classFile = new File(classFullName);
        if (!classFile.exists()) {
            throw new ClassNotFoundException(name);
        }
        try (
                FileInputStream is = new FileInputStream(classFile);
                ByteArrayOutputStream os = new ByteArrayOutputStream();
        ) {
            is.skip(4);
            byte[] buf = new byte[1024];
            int len = 0;
            while ((len=is.read(buf))!=-1){
                os.write(buf,0,len);
            }
            return os.toByteArray();
        } catch (Exception e) {

        }
        return null;

    }
}

● 编写类加载器测试类Main

public class Main {
    public static void main(String[] args) throws Exception {
        CustomClasloader customClasloader = new CustomClasloader(
                "D:\\workplat\\test\\test1\\target\\classes\\",
                Main.class.getClassLoader().getParent());
        Object o = customClasloader.loadClass("com.test.HelloTest").newInstance();
        o.getClass().getMethod("sayHello").invoke(o);
    }
}

● 注意,自定义类加载器的父类加载器默认为AppclassLoader,如果测试类HelloTest与测试类Main在同一工程内,由于父类委托机制,此时运行的当前工程内的HelloTest类,而不是加密处理后的class类。

● 思考

○ 如何实现自动类加密打包?可通过maven插件

○ 如何读取jar包内的class文件并解密?参考URLClassLoader实现

7 自定义类加载器实现热加载

● 由于被加载过的类都会有缓存,因此备份加载过的类修改后重新部署上去不会实时生效,需要重新启动服务让类重新加载才会生效

● 自定义类加载器可以实现不重启服务,让修改的类实时生效

● 实现方式一:简单粗暴,每次使用类时都有新的类加载器对象去加载,这样缓存不会生效

● 实现方式二:后台增加定时任务探测对应文件是否修改过,如果修改过则替换类加载器为新的类加载器对象

8 自定义类加载器打破双亲委派

● 自定义类加载器实现loadClass()方法,读取文件方式

public class CustomClasloader extends ClassLoader {
    private String classPath;

    public CustomClasloader(String classPath, ClassLoader parent) {
        super(parent);
        this.classPath = classPath;
    }


    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                c = findClass(name);
                if (c == null) {
                    c = super.loadClass(name, resolve);

                }
            }
            return c;
        }
    }

    @Override
    public Class findClass(String name) throws ClassNotFoundException {
        byte[] b = loadClassData(name);
        if (b == null) {
            return null;
        }
        return defineClass(name, b, 0, b.length);
    }

    private byte[] loadClassData(String name) throws ClassNotFoundException {
        String classFullName = this.classPath + name.replace(".", File.separator) + ".gz";

        File classFile = new File(classFullName);
        if (!classFile.exists()) {
            return null;
        }
        try (
                FileInputStream is = new FileInputStream(classFile);
                ByteArrayOutputStream os = new ByteArrayOutputStream();
        ) {
            is.skip(4);
            byte[] buf = new byte[1024];
            int len = 0;
            while ((len = is.read(buf)) != -1) {
                os.write(buf, 0, len);
            }
            return os.toByteArray();
        } catch (Exception e) {

        }
        return null;

    }
}

9 ServiceLoader+类加载器实现外部jar方法调用

● jdk提供的SPI机制

● 在项目test1中添加接口HelloService

public interface HelloService {
    public  void sysHello(String name);
}

● 在项目test2添加test1依赖,并添加HelloService实现类


import com.service.HelloService;

/**
 * @author gzl
 * @Date 2024/12/28 11:01
 */
public class HelloServiceImpl implements HelloService {
    @Override
    public void sysHello(String name) {
        System.out.println("Hello test2 "+ name);
    }
}

● 在项目test2中添加SPI配置文件META-INF/services/com.service.HelloService

com.service.HelloServiceImpl

● 打包test2项目生成jar包

● 在项目test1中添加测试类Main

public class Main {
    public static void main(String[] args) throws Exception {
        URL url = new URL("file:D:\\workplat\\test\\test2\\target\\test2-1.0-SNAPSHOT.jar");
        URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
        ServiceLoader<HelloService> load = ServiceLoader.load(HelloService.class, classLoader);
        for (HelloService helloService : load) {
            helloService.sysHello("jack");
        }
    }
}

● 如果使用自定义类加载器加载外部jar时,会使SPI失效,因为外部我们只通过自定义加载器去加载了class类,其内部的resources文件没有加载进来,解决的方式有以下两种

○ 使用java -cp {libFile}将外部jar包添加新classpath路径

○ 参照URLClassLoader将resources下的配置文件加载进来

10 SpringFactoriesLoader+类加载器实现外部jar方法调用

● SpringFactoriesLoader是spring提供的spi机制

● 在项目test1中添加接口HelloService

public interface HelloService {
    public  void sysHello(String name);
}

● 在项目test2添加test1依赖,并添加HelloService实现类


import com.service.HelloService;

/**
 * @author gzl
 * @Date 2024/12/28 11:01
 */
public class HelloServiceImpl implements HelloService {
    @Override
    public void sysHello(String name) {
        System.out.println("Hello test2 "+ name);
    }
}

● 在项目test2中添加SPI配置文件META-INF/spring.factories

com.service.HelloService=com.service.impl.HelloServiceImpl

● 打包test2项目生成jar包

● 在项目test1自定义类加载器实现外部jar读取

public class CustomClasloader extends URLClassLoader {
    private String classPath;

    public CustomClasloader(String classPath) throws MalformedURLException {
        super(new URL[]{new URL("file:"+classPath)});
        this.classPath=classPath;
    }

//
//    @Override
//    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//        if (name.startsWith("java")){
//            return super.loadClass(name, resolve);
//        }
//        synchronized (getClassLoadingLock(name)) {
//            // First, check if the class has already been loaded
//            Class<?> c = findLoadedClass(name);
//            if (c == null) {
//                c = findClass(name);
//                if (c == null) {
//                    c = super.loadClass(name, resolve);
//
//                }
//            }
//            return c;
//        }
//    }

    @Override
    public Class findClass(String name) throws ClassNotFoundException {
        byte[] b = loadClassData(name);
        if (b == null) {
            return null;
        }
        return defineClass(name, b, 0, b.length);
    }

    private byte[] loadClassData(String name) throws ClassNotFoundException {
        String classFullName = name.replace(".", "/") + ".class";

        try (
                InputStream resource = getResourceAsStream(classFullName);
                ByteArrayOutputStream os = new ByteArrayOutputStream();
        ) {
            if (resource==null){
                return null;
            }
//            is.skip(4);
            byte[] buf = new byte[1024];
            int len = 0;
            while ((len = resource.read(buf)) != -1) {
                os.write(buf, 0, len);
            }
            return os.toByteArray();
        } catch (Exception e) {

        }
        return null;
    }
}

● 在项目1添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>2.7.18</version>
</dependency>

● 在项目test1中添加测试类Main

public class Main {
    public static void main(String[] args) throws Exception {
        CustomClasloader classLoader = new CustomClasloader("D:\\workplat\\test\\test2\\target\\test2-1.0-SNAPSHOT.jar");
        List<HelloService> helloServices = SpringFactoriesLoader.loadFactories(HelloService.class, classLoader);
        for (HelloService helloService : helloServices) {
            helloService.sysHello("jack");
        }
    }
}

注意:如果修改loadClass()改变双亲委派机制的默认行为,先执行findClass()方法,未找到类时再次父类加载器中寻找,此时自动义类加载加载HelloServiceImpl时,会回调找父类HelloService找父类的逻辑因为优先进findClass()方法,导致HelloService被自定义类加载器再加载一遍,而不是使用AppClassLoader中的HelloService,从而导致报错

Caused by: java.lang.IllegalArgumentException: Class [com.service.impl.HelloServiceImpl] is not assignable to factory type [com.service.HelloService]
	at org.springframework.core.io.support.SpringFactoriesLoader.instantiateFactory(SpringFactoriesLoader.java:177)
	... 2 more

想要解决以上问题有两种方式:

● 方式一:只需要让回调找父类时父类不被自定义类加载即可,即重写自定义类加载器getResource()方法

@Override
public URL getResource(String name) {
    return findResource(name);
}

● 方式二:在测试类Main上再封装一层,让自定义类加载器加载测试类Main,这样保证了HelloServiceImplHelloService同时被自定义类加载器加载

11 实现同名类多版本共存

实现思路:创建新的类加载器对象去加载不同路径的类,即可以实现同名类多版本共存

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值