概述
持续创造,加速生长!这是我参加「日新计划 6 月更文挑战」的第5天,点击检查活动详情
前面经过类加载器系列(二)——从源码视点理解双亲派遣模型讲解了类加载器的双亲派遣模型,提到了自界说类加载器,那么咱们现在讲解下怎么自界说类加载器。
自界说类加载器场景
java内置的类加载器不满足于咱们加载类的需求,这种状况下需求咱们自界说一个类加载器,通常有一下几种状况:
- 扩展类加载源
通常状况下咱们写的java类文件存放在classpath下,由使用类加载器AppClassLoader加载。咱们能够自界说类加载器从数据库、网络等其他地方加载咱们咱们的类。
- 阻隔类
比如tomcat这种web容器中,会部署多个使用程序,比如使用程序A依靠了一个三方jar的1.0.0版别, 使用程序B依靠了同一个三方jar的版别1.1.0, 他们使用了同一个类User.class,
这个类在两个版别jar中有内容上的差别,假如不做阻隔处理的话,程序A加载了1.0.0版别中的User.class
, 此刻程序B也去加载时,发现已经有了User.class,
它实践就不会去加载1.1.0版别中的User.class,终究导致严峻的结果。所以阻隔类在这种状况还是很有必要的。
为了完成阻隔性,优先加载 Web 使用自己界说的类,所以没有遵照双亲派遣的约好,每一个使用自己的类加载器——WebAppClassLoader
担任加载本身的目录下的class文件,加载不到时再交给CommonClassLoader
加载,这和双亲派遣刚好相反。
- 避免源码泄露
某些状况下,咱们的源码是商业机密,不能外泄,这种状况下会进行编译加密。那么在类加载的时分,需求进行解密复原,这种状况下就要自界说类加载器了。
完成办法
Java供给了抽象类java.lang.ClassLoader
,一切用户自界说的类加载器都应该继承ClassLoader
类。
在自界说ClassLoader
的子类时分,咱们常见的会有两种做法:
● 重写loadClass()办法
● 重写findClass()办法
loadClass() 和 findClass()
检查源码,咱们发现loadClass()终究调用的还是findClass()办法。
那咱们该用那种办法呢?
主要依据实践需求来,
- 假如想打破双亲派遣模型,那么就重写整个loadClass办法
loadClass()
中封装了双亲派遣模型的核心逻辑,假如咱们确实有需求,需求打破这样的机制,那么就需求重写loadClass()
办法。
- 假如不想打破双亲派遣模型,那么只需求重写findClass办法即可
可是大部分状况下,咱们建议的做法还是重写findClass()
自界说类的加载办法,依据指定的类名,返回对应的Class目标的引证。由于恣意打破双亲派遣模型或许容易带来问题,咱们重写findClass()
是在双亲派遣模型的框架下进行小范围的改动。
自界说一个类加载器
需求: 加载本地磁盘D盘目录下的class文件。
分析: 该需求只是从其他一个额外途径下加载class文件,不需求打破双亲派遣模型,能够直接界说loadClass()
办法。
public class FileReadClassLoader extends ClassLoader {
private String dir;
public FileReadClassLoader(String dir) {
this.dir = dir;
}
public FileReadClassLoader(String dir, ClassLoader parent) {
super(parent);
this.dir = dir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 读取class
byte[] bytes = getClassBytes(name);
// 将二进制class转换为class目标
Class<?> c = this.defineClass(null, bytes, 0, bytes.length);
return c;
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
private byte[] getClassBytes(String name) throws Exception {
// 这儿要读入.class的字节,因此要使用字节流
FileInputStream fis = new FileInputStream(new File(this.dir + File.separator + name + ".class"));
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024);
while (true) {
int i = fc.read(by);
if (i == 0 || i == -1)
break;
by.flip();
wbc.write(by);
by.clear();
}
fis.close();
return baos.toByteArray();
}
}
测验:
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
// 创立自界说的类加载器
FileReadClassLoader fileReadClassLoader = new FileReadClassLoader("D:\classes");
// 加载类
Class<?> account = fileReadClassLoader.loadClass("Account");
Object o = account.newInstance();
ClassLoader classLoader = o.getClass().getClassLoader();
System.out.println("加载当前类的类加载器为:" + classLoader);
System.out.println("父类加载器为:" + classLoader.getParent());
}
SpringBoot自界说ClassLoader
springboot想必大家都使用过,它也用到了自界说的类加载器。springboot终究打成一个fat jar,经过java -jar xxx.jar
,就能够快速便利的发动使用。
fat jar目录结构:
├───BOOT-INF
│ ├───classes
│ │ │ application.properties
│ │ │
│ │ └───com
│ │ └───alvin
│ │ │ Application.class
│ │
│ └───lib
│ .......(省掉)
│ spring-aop-5.1.2.RELEASE.jar
│ spring-beans-5.1.2.RELEASE.jar
│ spring-boot-2.1.0.RELEASE.jar
│ spring-boot-actuator-2.1.0.RELEASE.jar
│
├───META-INF
│ │ MANIFEST.MF
│ │
│ └───maven
│ └───com.gpcoding
│ └───spring-boot-exception
│ pom.properties
│ pom.xml
│
└───org
└───springframework
└───boot
└───loader
│ ExecutableArchiveLauncher.class
│ JarLauncher.class
│ LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
│ LaunchedURLClassLoader.class
│ Launcher.class
│ MainMethodRunner.class
│ PropertiesLauncher$1.class
│ PropertiesLauncher$ArchiveEntryFilter.class
│ PropertiesLauncher$PrefixMatchingArchiveFilter.class
│ PropertiesLauncher.class
│ WarLauncher.class
│
├───archive
│ Archive$Entry.class
│ 。。。(省掉)
│
├───data
│ RandomAccessData.class
│ 。。。(省掉)
├───jar
│ AsciiBytes.class
│ 。。。(省掉)
└───util
SystemPropertyUtils.class
从目录结构咱们看出:
|--BOOT-INF
|--BOOT-INF\classes 该文件下的文件是咱们最终需求履行的代码
|--BOOT-INF\lib 该文件下的文件是咱们最终需求履行的代码的依靠
|--META-INF
|--MANIFEST.MF 该文件指定了版别以及Start-Class,Main-Class
|--org 该文件下的文件是一个spring loader文件,使用类加载器首要会加载履行该目录下的代码
显然,这样的目录结构,需求咱们经过自界说类加载器,去加载其间的类。
检查目录结构中的MANIFEST.MF文件,如下:
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: springboot-01-helloworld
Implementation-Version: 1.0-SNAPSHOT
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.alvinlkk.HelloWorldApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.5.6
Created-By: Maven JAR Plugin 3.2.2
Main-Class: org.springframework.boot.loader.JarLauncher
能够看到,实践的发动的进口类为org.springframework.boot.loader.JarLauncher
,在解压的目录中可见。
为了便利检查源码,咱们需求在工程中引进下面的依靠:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<version>2.7.0</version>
</dependency>
履行逻辑如下:
-
Lanuncher#launch()
办法中创立了LaunchedURLClassLoader
类加载器,设置到线程上下文类加载器中。 - 经过反射获取带有
SpringBootApplication
注解的发动类,履行main办法。
LaunchedURLClassLoader解析
org.springframework.boot.loader.LaunchedURLClassLoader
是 spring-boot-loader
中自界说的类加载器,完成对 jar 包中 BOOT-INF/classes 目录下的类和 BOOT-INF/lib 下第三方 jar 包中的类的加载。
LaunchedURLClassLoader
重写了loadClass
办法,打破了双亲派遣模型。
/**
* 重写类加载器中加载 Class 类目标办法
*/
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 假如名称是以org.springframework.boot.loader.jarmode最初的直接加载类
if (name.startsWith("org.springframework.boot.loader.jarmode.")) {
try {
Class<?> result = loadClassInLaunchedClassLoader(name);
if (resolve) {
resolveClass(result);
}
return result;
}
catch (ClassNotFoundException ex) {
}
}
if (this.exploded) {
return super.loadClass(name, resolve);
}
Handler.setUseFastConnectionExceptions(true);
try {
try {
// 判断这个类是否有对应的package包
// 没有的话会从一切 URL(包含内部引进的一切 jar 包)中找到对应的 Package 包并进行设置
definePackageIfNecessary(name);
}
catch (IllegalArgumentException ex) {
// Tolerate race condition due to being parallel capable
if (getPackage(name) == null) {
// This should never happen as the IllegalArgumentException indicates
// that the package has already been defined and, therefore,
// getPackage(name) should not return null.
throw new AssertionError("Package " + name + " has already been defined but it could not be found");
}
}
// 调用父类的加载器
return super.loadClass(name, resolve);
}
finally {
Handler.setUseFastConnectionExceptions(false);
}
}
/**
* Define a package before a {@code findClass} call is made. This is necessary to
* ensure that the appropriate manifest for nested JARs is associated with the
* package.
* @param className the class name being found
*/
private void definePackageIfNecessary(String className) {
int lastDot = className.lastIndexOf('.');
if (lastDot >= 0) {
// 获取包名
String packageName = className.substring(0, lastDot);
// 没有找到对应的包名,则进行解析
if (getPackage(packageName) == null) {
try {
// 遍历一切的 URL,从一切的 jar 包中找到这个类对应的 Package 包并进行设置
definePackage(className, packageName);
}
catch (IllegalArgumentException ex) {
// Tolerate race condition due to being parallel capable
if (getPackage(packageName) == null) {
// This should never happen as the IllegalArgumentException
// indicates that the package has already been defined and,
// therefore, getPackage(name) should not have returned null.
throw new AssertionError(
"Package " + packageName + " has already been defined but it could not be found");
}
}
}
}
}
private void definePackage(String className, String packageName) {
try {
AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
// 把类途径解析成类名并加上 .class 后缀
String packageEntryName = packageName.replace('.', '/') + "/";
String classEntryName = className.replace('.', '/') + ".class";
// 遍历一切的 URL(包含使用内部引进的一切 jar 包)
for (URL url : getURLs()) {
try {
URLConnection connection = url.openConnection();
if (connection instanceof JarURLConnection) {
JarFile jarFile = ((JarURLConnection) connection).getJarFile();
// 假如这个 jar 中存在这个类名,且有对应的 Manifest
if (jarFile.getEntry(classEntryName) != null && jarFile.getEntry(packageEntryName) != null
&& jarFile.getManifest() != null) {
// 界说这个类对应的 Package 包
definePackage(packageName, jarFile.getManifest(), url);
return null;
}
}
}
catch (IOException ex) {
// Ignore
}
}
return null;
}, AccessController.getContext());
}
catch (java.security.PrivilegedActionException ex) {
// Ignore
}
}
总结
本文阐述怎么创立自界说类加载器,以及在什么场景下创立自界说类加载器,一起经过springboot发动拆创立的类加载器加深咱们的理解。
参阅
www.jianshu.com/p/9c07ced8d…
www.cnblogs.com/lifullmoon/…
www.cnblogs.com/xrq730/p/48…