自定义类加载器

961 阅读5分钟

类加载器的作用

负责将.class文件加载在到内存中。

类加载过程

  • 加载

通过类的全名,把class字节码加载到内存中。

  • 验证

确保Class文件字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。

  • 准备

为静态变量分配内存,并设置默认初始化值。

  • 解析

将符号引用替换为直接引用。

  • 初始化

初始化类变量和其他资源。

类加载器

类加载器就是读取编译器生成的字节码文件,转化生成一个java.lang.Class类的一个实例。基本上所有的类加载器都是java.lang.ClassLoader类的一个实例。

  • 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库。
  • 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包。
  • 应用程序类加载器:负责加载ClassPath路径下的类包。
  • 自定义加载器:负责加载用户自定义路径下的类包。

双亲委派机制

类加载器收到类加载请求,会委托给父类加载器去执行,父类加载器还存在其父类加载器,则进一步向上委托,依次递归,直到顶层类加载器,如果顶层类加载器加载到该类,就成功返回class对象,否则委托给下级类加载器去执行,依次递归。双亲委派机制是为了避免重复加载和核心类篡改。

loadClass 方法的主要职责就是实现双亲委派机制:首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则委派给父加载器加载,这是一个递归调用,一层一层向上委派,最顶层的类加载器(启动类加载器)无法加载该类时,再一层一层向下委派给子类加载器加载。

public abstract class ClassLoader {

    private final ClassLoader parent;//父加载器
    
    public Class<?> loadClass(String name) {
  
        Class<?> c = findLoadedClass(name);
        if( c == null ){
          if (parent != null) {
              //委派父加载器加载
              c = parent.loadClass(name);
          }else {
              //查找Bootstrap加载器是否加载过
              c = findBootstrapClassOrNull(name);
          }
        }
        //调用findClass加载
        if (c == null) {
            c = findClass(name);
        } 
        return c;
    }
    //读取class文件到内存
    protected Class<?> findClass(String name){
       ...
       return defineClass(buf, off, len);
    }
    
    // 将字节数组转成Class对象
    protected final Class<?> defineClass(byte[] b, int off, int len){
       ...
    }
}

如何打破双亲委派机制

自定义ClassLoader,重写loadClass方法(只要不依次往上交给父加载器进行加载,就算是打破双亲委派机制)。

自定义加载器的使用场景

  • 解决依赖冲突
  • 热部署
  • 加密保护

自定义加载器简单实现

  • 继承ClassLoader
  • 重写findClass()方法

(1)先准备一个class文件,我暂时放在D盘。

public class TestClass {
    public void hello(){
        System.out.println("hello class !!!");
    }
}

(2)自定义ClassLoader,实现方式就是读取类文件流,然后调用defineClass()。

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class MyClassLoader extends ClassLoader {

    private String classpath;

    public MyClassLoader(String classpath) {
        this.classpath = classpath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            FileInputStream fis = new FileInputStream(classpath + File.separator + name.replace(".", File.separator).concat(".class"));
            byte[] bytes = new byte[fis.available()];
            fis.read(bytes);
            fis.close();
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }
}

(3)测试

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, MalformedURLException {
        MyClassLoader myClassLoader = new MyClassLoader("D:\");
        Class<?> aClass = myClassLoader.findClass("TestClass");
        Object object = aClass.newInstance();
        Method hello = aClass.getDeclaredMethod("hello");
        hello.invoke(object);
    }
}

(4)URLClassLoader

上面自定义类加载器主要是用来读取类文件,然而可以直接使用URLClassLoader,URLClassLoader支持从jar文件和文件夹中获取class。

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, MalformedURLException {
        URLClassLoader ucl = new URLClassLoader(new URL[]{new URL("file:///D:\")});
        Class<?> aClass = ucl.loadClass("TestClass");
        Object object = aClass.newInstance();
        Method hello = aClass.getDeclaredMethod("hello");
        hello.invoke(object);
    }
}

加载不同版本的JDBC驱动jar包

项目需求在同一个运行环境中同时加载多个不同版本的驱动对象,来兼容多个版本的Mysql。

实现思想:通过URLClassLoader实现。

import java.net.URL;
import java.net.URLClassLoader;
import java.sql.Connection;
import java.sql.Driver;
import java.util.Properties;

public class MysqlTest {
    public static void main(String[] args) throws Exception {
        //com.mysql.jdbc.Driver >> mysql-connector-java-5.1.34.jar
        //com.mysql.cj.jdbc.Driver >> mysql-connector-java-8.0.15.jar
        String className = "com.mysql.jdbc.Driver";
        String jarName = "mysql-connector-java-5.1.34.jar";
        getConn(className, jarName);
    }
    private static Connection getConn(String className, String jarName) throws Exception{
        String userName = "root";
        String password = "root";
        String url = "jdbc:mysql://localhost:3306/cla";
        String jarFilePath = "jar:file:d://jar//"+jarName+"!/";
        URLClassLoader loader = new URLClassLoader(new URL[]{new URL(jarFilePath)}, null);
        Driver driver = (Driver) Class.forName(className, true, loader).newInstance();
        Connection connection = null;
        Properties info = new Properties();
        info.put("user", userName);
        info.put("password", password);
        connection = driver.connect(url, info);
        System.out.println(connection.getMetaData().getDatabaseProductVersion());
        System.out.println(connection.getMetaData().getDriverVersion());
        return connection;
    }
}

SPI机制

SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。

在Java中,SPI机制有一个非常典型的实现案例,就是数据库驱动java.jdbc.Driver。Java定义了一套JDBC的接口,当没有提供具体实现类,是由不同的厂商提供数据库实现包。

目的: ① 为了解耦。② 提高框架的扩展性。

简单使用SPI

(1)定义一个接口和实现类。

package com.syun.spi;

public interface Developer {
    void hello();
}
package com.syun.spi;

public class DeveloperA implements Developer{
    @Override
    public void hello() {
        System.out.println("hello developer a");
    }
}
package com.syun.spi;

public class DeveloperB implements Developer{
    @Override
    public void hello() {
        System.out.println("hello developer b");
    }
}

(2)在resources下新建META-INF/services文件夹,以接口的全限定名创建文件。

com.syun.spi.DeveloperA
com.syun.spi.DeveloperB

(3)测试。

public class Test {
    public static void main(String[] args) {
        ServiceLoader<Developer> serviceLoader = ServiceLoader.load(Developer.class);
        serviceLoader.forEach(Developer::hello);
    }
}

小问题

java.lang.SecurityException: Prohibited package name: java

由于定义了以 java 开始的包,编译时错误。可以定位到java.lang.ClassLoader.preDefineClass代码:

    private ProtectionDomain preDefineClass(String name,
                                            ProtectionDomain pd)
    {
        if (!checkName(name))
            throw new NoClassDefFoundError("IllegalName: " + name);

        // Note:  Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
        // relies on the fact that spoofing is impossible if a class has a name
        // of the form "java.*"
        if ((name != null) && name.startsWith("java.")) {
            throw new SecurityException
                ("Prohibited package name: " +
                 name.substring(0, name.lastIndexOf('.')));
        }
        if (pd == null) {
            pd = defaultDomain;
        }

        if (name != null) checkCerts(name, pd.getCodeSource());

        return pd;
    }

自定义加载器加载java.lang.String的问题

有一个博主尝试使用的自定义加载器加载java.lang.String过程<blog.csdn.net/li281037846…>。

  • 首先重写loadClass方法后,依旧报错,从ClassLoader的源码可以看出,只要加载java开头的包就会报错。所以真正原因是JVM安全机制,并不是因为双亲委派。
  • 然后修改ClassLoader类,删除preDefineClass()代码,依旧报错
  • 最后通过修改native代码中systemDictionary.cpp的resolve_from_stream()方法,重新编译JVM后成功