为什么双亲委派?
::原因:修改java.lang.String类并且通过自定义类加载器加入到JVM中,等于覆盖了 String 类,造成不安全。::
为什么要破坏?
::不破坏行吗?可以: 增加代码Class.forName(“com.mysql.jdbc.Driver”);
即可;会以“双亲委派”原则加载具体jdbc实现::
::为什么要破坏?省略Class.forName(“com.mysql.jdbc.Driver”);
代码块的话,必须破坏双亲委派。::
破坏实现过程?
简要过程:通过扫描META-INF/services/java.sql.Driver文件,获取具体的实现名“com.mysql.jdbc.Driver”,然后使用线程上下文加载器加载。
具体过程:先加载DriverManager,DriverManager中会加载Driver实现类,由于DriverManager 由根加载器加载,加载Driver实现类时,只能由DriverManager的加载类或加载类的父类加载;而Driver的实现不能由根加载器加载(Driver的实现不在rt.jar中)。
什么是双亲委派?
首先,先要知道什么是类加载器。简单说,类加载器就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象。如果站在JVM的角度来看,只存在两种类加载器:
- 启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。
- 其他类加载器:由Java语言实现,继承自抽象类ClassLoader。
- (1)扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
(2)应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。
双亲委派的原因
修改java.lang.String类并且通过自定义类加载器加入到JVM中,等于覆盖了 String 类,造成不安全。
为什么需要双亲委派模型呢?假设没有双亲委派模型,试想一个场景:
黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。
而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类?;蛐砟慊嵯耄以谧远ㄒ宓睦嗉釉仄骼锩媲恐萍釉刈远ㄒ宓膉ava.lang.String类,不去通过调用父加载器不就好了吗?确实,这样是可行。但是,在JVM中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。
破坏双亲委派
使用线程上下文加载器,可以让父类加载器请求子类加载器去完成类加载的动作
JDBC 4.0 Drivers must include the file META-INF/services/java.sql.Driver. This file contains the name of the JDBC drivers implementation of java.sql.Driver. For example, to load the my.sql.Driver class, the META-INF/services/java.sql.Driver file would contain the entry:
my.sql.Driver
Applications no longer need to explicitly load JDBC drivers using Class.forName(). Existing programs which currently load JDBC drivers using Class.forName() will continue to work without modification.
When the method getConnection is called, the DriverManager will attempt to locate a suitable driver from amongst those loaded at initialization and those loaded explicitly using the same classloader as the current applet or application.
关于getContextClassLoader:
线程上下文类加载器是从jdk1.2开始引入的,类Thread中的getContextClassLoader()与setContextClassLoader(ClassLoader c1),分别用来获取和设置类加载器。
如果没有通过setContextClassLoader方法进行设置的话,线程将继承其父线程的上下文加载器,java应用运行时的初始线程的上下文类加载器是系统类加载器(这里是由Launcher类设置的)。在线程中运行的代码可以通过该类加载器来加载类和资源。
SPI(Service Provider Interface,服务提供者接口,指的是JDK提供标准接口,具体实现由厂商决定。例如sql),如上面的JDBC
父ClassLoader可以使用当前线程Thread.current.currentThread().getContextClassLoader()所指定的classLoader加载的类。这就改变了父ClassLoader不能使用子ClassLoader加载的类的情况,即改变了双亲委托模型。
线程上下文类加载器就是当前线程的CurrentClassloader。
在双亲委托模型下,类加载器是由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口是JAVA核心库提供的,而JAVA核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同的jar包(厂商提供),JAVA的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求。而通过给当前线程设置上下文类加载器,就可以设置的上下文类加载器来实现对于接口实现类的加载。
具体实现
JDBC破坏双亲委派
直接使用以下方式
Connection conn= DriverManager.getConnection(“jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK”, “root”, “”);
通过DriverManager类使用线程上下文类加载器去加载第三方jar包中的Driver类的
- 第一,获取线程上下文类加载器,从而也就获得了应用程序类加载器(也可能是自定义的类加载器)
- 第二,从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.jdbc.Driver”
- 第三,通过线程上下文类加载器去加载这个Driver类,从而避开了双亲委派模型的弊端
具体过程
Java本身有一套资源管理服务JNDI,是放置在rt.jar中,由启动类加载器加载的。
以对数据库管理JDBC为例,Java给数据库操作提供了一个Driver接口:
package com.mysql.jdbc;
public class Driver extends com.mysql.cj.jdbc.Driver {
public Driver() throws SQLException {
}
}
然后提供了一个DriverManager来管理这些Driver的具体实现:
DriverManager 类有两个功能:
- 通过DriverManager.registerDriver 方法可以注册Driver实现
- 通过静态初始化loadInitialDrivers()可以读取META-INF/services/java.sql.Driver文件注册
Driver实现
package java.sql;
public class DriverManager {
// List of registered JDBC drivers
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
//自动注册
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
String drivers;
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
......
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
//手动注册
public static synchronized void registerDriver(java.sql.Driver driver)
throws SQLException {
registerDriver(driver, null);
}
public static synchronized void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {
/* Register the driver if it has not already been added to our list */
if(driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
不破坏双亲委派模型的情况(不使用JNDI服务)
Class.forName=DriverManager.registerDriver(new Driver)
// 1.加载数据访问驱动
Class.forName("com.mysql.jdbc.Driver");
//2.连接到数据"库"上去
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
发现它调用了forName0方法,继续跟踪再看看
private static native Class<?> forName0(String name, boolean initialize,
ClassLoader loader,
Class<?> caller)
Native方法,源码也只能到此结束了.看下官方文档,怎么说吧.
https://docs.oracle.com/javase/7/docs/api/java/lang/Class.html#forName(java.lang.String)
发现官方文档,还是描述的很清楚的.
Returns the Class object associated with the class or interface with the given string name,
using the given class loader.
Given the fully qualified name for a class or interface (in the same format returned by getName)
this method attempts to locate, load, and link the class or interface.
The specified class loader is used to load the class or interface.
If the parameter loader is null, the class is loaded through the bootstrap class loader.
The class is initialized only if the initialize parameter is true and if it has not been
initialized earlier.
返回一个给定类或者接口的一个Class对象,如果没有给定classloader,那么会使用根类加载器.如果initalize这个参数传了true,那么给定的类如果之前没有被初始化过,那么会被初始化.我们在JDBC第一步的时候,传入的参数是com.mysql.jdbc.Driver. 也就是说这个类会被初始化.我们看一下这个类里面的内容.
package com.mysql.jdbc;
public class Driver extends com.mysql.cj.jdbc.Driver {
public Driver() throws SQLException {
}
}
package com.mysql.cj.jdbc;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
我们发现这个类也是超级简单的.一个构造函数和一个静态代码块.我们知道,类在初始化的时候,静态代码块的内容会被执行的.也就是说我们Class.forName和直接写DriverManager.registerDriver(new Driver)两者功能是等同的.
附件
ClassLoader类具体实现
public abstract class ClassLoader {
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) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
从上面代码可以明显看出,loadClass(String, boolean)函数即实现了双亲委派模型!整个大致过程如下:
- 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
- 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
- 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。
话句话说,如果自定义类加载器,就必须重写findClass方法!
引用
Java自定义类加载器与双亲委派模型 - Givefine - 博客园
以JDBC为例谈双亲委派模型的破坏 - weixin_33898233的博客 - CSDN博客
图解Tomcat类加载机制(阿里面试题) - aspirant - 博客园
双亲委派模式破坏-JDBC - 程序员大本营
Java界最神秘技术ClassLoader,吃透它看这一篇就够了