1. 如何打破双亲委派模型
我们知道类的加载方式默认是双亲委派,如果我们有一个类想要通过自定义的类加载器来加载这个类,而不是通过系统默认的类加载器,说白了就是不走双亲委派那一套。即:
- 自定义类加载器 ,重写loadclass方法。典型的打破双亲委派模型的框架和中间件有tomcat与osgi
- SPI机制绕开loadclass 方法。当前线程设定关联类加载器
当然这里要注意一下,Object.class这是对象的顶级类,改变类的类加载器的时候要注意,如果全部改了,Object.class就找不到了,加载不了了。所以呢,这里重写的时候,要注意分类解决,把你想要通过自定义类加载器加载的和想通过默认类加载器加载的分隔开。
如果不想打破双亲委派模型,就重写ClassLoader类中的findClass()方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。
2. 自定义打破双亲委派
2.1 直接自定义类加载器加载
如下是一个自定义的类加载器TestClassLoader,并重写了findClass和loadClass:
public class TestClassLoader extends ClassLoader {
public TestClassLoader(ClassLoader parent) {
super(parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 1、获取class文件二进制字节数组
byte[] data = null;
try {
System.out.println(name);
String namePath = name.replaceAll("\\.", "\\\\");
String classFile = "C:\\study\\myStudy\\ZooKeeperLearning\\zkops\\target\\classes\\" + namePath + ".class";
ByteArrayOutputStream baos = new ByteArrayOutputStream();
FileInputStream fis = new FileInputStream(new File(classFile));
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fis.read(bytes)) != -1) {
baos.write(bytes, 0, len);
}
data = baos.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
// 2、字节码加载到 JVM 的方法区,
// 并在 JVM 的堆区建立一个java.lang.Class对象的实例
// 用来封装 Java 类相关的数据和方法
return this.defineClass(name, data, 0, data.length);
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException{
Class<?> clazz = null;
// 直接自己加载
clazz = this.findClass(name);
if (clazz != null) {
return clazz;
}
// 自己加载不了,再调用父类loadClass,保持双亲委托模式
return super.loadClass(name);
}
}
测试:初始化自定义的类加载器,需要传入一个parent,指定其父类加载器,那就先指定为加载TestClassLoader的类加载器为TestClassLoader的父类加载器吧:
public static void main(String[] args) throws Exception {
// 初始化TestClassLoader,被将加载TestClassLoader类的类加载器设置为TestClassLoader的parent
TestClassLoader testClassLoader = new TestClassLoader(TestClassLoader.class.getClassLoader());
System.out.println("TestClassLoader的父类加载器:" + testClassLoader.getParent());
// 加载 Demo
Class clazz = testClassLoader.loadClass("study.stefan.classLoader.Demo");
System.out.println("Demo的类加载器:" + clazz.getClassLoader());
}
运行如下测试代码,发现报错了:
找不到java\lang\Object.class
,我加载study.stefan.classLoader.Demo
类和Object有什么关系呢?
转瞬想到java中所有的类都隐含继承了超类Object,加载study.stefan.classLoader.Demo
,也会加载父类Object。Object和study.stefan.classLoader.Demo
并不在同个目录,那就找到Object.class的目录(将jre/lib/rt.jar解压),修改TestClassLoader#findClass
如下:
遇到前缀为java.的就去找官方的class文件。
运行测试代码:
还是报错了!!!
报错信息为:Prohibited package name: java.lang
。
跟了下异常堆栈:
TestClassLoader#findClass
最后一行代码调用了java.lang.ClassLoader#defineClass
,
java.lang.ClassLoader#defineClass
最终调用了如下代码:
看意思是java禁止用户用自定义的类加载器加载java.开头的官方类,也就是说只有启动类加载器BootstrapClassLoader才能加载java.开头的官方类。
得出结论,因为java中所有类都继承了Object,而加载自定义类study.stefan.classLoader.Demo,之后还会加载其父类,而最顶级的父类Object是java官方的类,只能由BootstrapClassLoader加载
2.2 跳过AppClassLoader和ExtClassLoader
既然如此,先将study.stefan.classLoader.Demo
交由BootstrapClassLoader加载即可。
由于java中无法直接引用BootstrapClassLoader,所以在初始化TestClassLoader时,传入parent为null,也就是TestClassLoader的父类加载器设置为BootstrapClassLoader:
package com.stefan.DailyTest.classLoader;
public class Test {
public static void main(String[] args) throws Exception {
// 初始化TestClassLoader,并将加载TestClassLoader类的类加载器
// 设置为TestClassLoader的parent
TestClassLoader testClassLoader = new TestClassLoader(null);
System.out.println("TestClassLoader的父类加载器:" + testClassLoader.getParent());
// 加载 Demo
Class clazz = testClassLoader.loadClass("com.stefan.DailyTest.classLoader.Demo");
System.out.println("Demo的类加载器:" + clazz.getClassLoader());
}
}
双亲委派的逻辑在 loadClass,由于现在的类加载器的关系为TestClassLoader —>BootstrapClassLoader,所以TestClassLoader中无需重写loadClass。
运行测试代码:
成功了,Demo类由自定义的类加载器TestClassLoader加载的,双亲委派模型被破坏了。
如果不破坏双亲委派,那么Demo类处于classpath下,就应该是AppClassLoader加载的,所以真正破坏的是AppClassLoader这一层的双亲委派
2.3 自定义类加载器加载扩展类
假设classpath下由上述TestClassLoader加载的类中用到了<JAVA_HOME>\lib\ext下的扩展类,那么这些扩展类也会由TestClassLoader加载,但是会报类文件找不到的情况。
但是自定义类加载器也是能加载<JAVA_HOME>\lib\ext下的扩展类的,只要自定义类加载器能找准扩展类的类路径。
以扩展目录com.sun.crypto.provider下的类举例:
- Demo中随便引用一个扩展类:
import com.sun.crypto.provider.ARCFOURCipher;
public class Demo {
public Demo() {
ARCFOURCipher arcfourCipher = new ARCFOURCipher();
System.out.println("ARCFOURCipher.getClassLoader=" + arcfourCipher.getClass().getClassLoader());
}
}
- 修改TestClassLoader#findClass:
- 测试代码中需要调用一下Demo类的构造器
- 运行测试代码,自定义类加载器成功加载了扩展类。
由上得出结论,<JAVA_HOME>\lib\ext下的扩展类是没有强制只有ExtClassLoader能加载,自定义类加载器也能加载
2.4 一个比较完整的自定义类加载器
一般情况下,自定义类加载器都是继承URLClassLoader,具有如下类关系图:
public class TestClassLoader extends URLClassLoader {
public TestClassLoader(ClassLoader parent) {
super(new URL[0], parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 1、先自己的路径找
Class<?> clazz = null;
try {
clazz = findClassInternal(name);
} catch (Exception e) {
// Ignore
}
if (clazz != null) {
return clazz;
}
// 在 父类路径 找
return super.findClass(name);
}
private Class<?> findClassInternal(String name) throws IOException {
byte[] data = null;
try {
String dir = "C:\\study\\myStudy\\ZooKeeperLearning\\zkops\\target\\classes\\";
String namePath = name.replaceAll("\\.", "\\\\");
String classFile = dir + namePath + ".class";
ByteArrayOutputStream baos = new ByteArrayOutputStream();
FileInputStream fis = new FileInputStream(new File(classFile));
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fis.read(bytes)) != -1) {
baos.write(bytes, 0, len);
}
data = baos.toByteArray();
// 字节码加载到 JVM 的方法区,
// 并在 JVM 的堆区建立一个java.lang.Class对象的实例
// 用来封装 Java 类相关的数据和方法
return this.defineClass(name, data, 0, data.length);
} catch (Exception e) {
throw e;
}
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException{
// 1、先委托给ext classLoader 加载
ClassLoader classLoader = getSystemClassLoader();
while (classLoader.getParent() != null) {
classLoader = classLoader.getParent();
}
Class<?> clazz = null;
try {
clazz = classLoader.loadClass(name);
} catch (ClassNotFoundException e) {
// Ignore
}
if (clazz != null) {
return clazz;
}
// 2、自己加载
clazz = this.findClass(name);
if (clazz != null) {
return clazz;
}
// 3、自己加载不了,再调用父类loadClass,保持双亲委托模式
return super.loadClass(name);
}
}
3. tomcat是如何打破双亲委派的
Tomcat中可以部署多个web项目,为了保证每个web项目互相独立,所以不能都由AppClassLoader加载,所以自定义了类加载器WebappClassLoader,WebappClassLoader继承自URLClassLoader,重写了findClass和loadClass,并且WebappClassLoader的父类加载器设置为AppClassLoader。
WebappClassLoader.loadClass中会先在缓存中查看类是否加载过,没有加载,就交给ExtClassLoader,ExtClassLoader再交给BootstrapClassLoader加载;都加载不了,才自己加载;自己也加载不了,就遵循原始的双亲委派,交由AppClassLoader递归加载。
/**
* Load the class with the specified name, searching using the following
* algorithm until it finds and returns the class. If the class cannot
* be found, returns <code>ClassNotFoundException</code>.
* <ul>
* <li>Call <code>findLoadedClass(String)</code> to check if the
* class has already been loaded. If it has, the same
* <code>Class</code> object is returned.</li>
* <li>If the <code>delegate</code> property is set to <code>true</code>,
* call the <code>loadClass()</code> method of the parent class
* loader, if any.</li>
* <li>Call <code>findClass()</code> to find this class in our locally
* defined repositories.</li>
* <li>Call the <code>loadClass()</code> method of our parent
* class loader, if any.</li>
* </ul>
* If the class was found using the above steps, and the
* <code>resolve</code> flag is <code>true</code>, this method will then
* call <code>resolveClass(Class)</code> on the resulting Class object.
*
* @param name The binary name of the class to be loaded
* @param resolve If <code>true</code> then resolve the class
*
* @exception ClassNotFoundException if the class was not found
*/
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class<?> clazz = null;
// Log access to stopped class loader
checkStateForClassLoading(name);
// (0) Check our previously loaded local class cache 本地缓存
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);//链接classloader todo
return clazz;
}
// (0.1) Check our previously loaded class cache
clazz = findLoadedClass(name);//校验jvm 的appclassloader的缓存中是否存在
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
// (0.2) Try loading the class with the system class loader, to prevent
// the webapp from overriding Java SE classes. This implements
// SRV.10.7.2
String resourceName = binaryNameToPath(name, false);
ClassLoader javaseLoader = getJavaseClassLoader();//系统的bootstrap classloader 的classloader
boolean tryLoadingFromJavaseLoader;
try {
// Use getResource as it won't trigger an expensive
// ClassNotFoundException if the resource is not available from
// the Java SE class loader. However (see
// https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 for
// details) when running under a security manager in rare cases
// this call may trigger a ClassCircularityError.
// See https://bz.apache.org/bugzilla/show_bug.cgi?id=61424 for
// details of how this may trigger a StackOverflowError
// Given these reported errors, catch Throwable to ensure any
// other edge cases are also caught
URL url;
if (securityManager != null) {
PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
url = AccessController.doPrivileged(dp);
} else {
url = javaseLoader.getResource(resourceName);
}
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
// Swallow all exceptions apart from those that must be re-thrown
ExceptionUtils.handleThrowable(t);
// The getResource() trick won't work for this class. We have to
// try loading it directly and accept that we might get a
// ClassNotFoundException.
tryLoadingFromJavaseLoader = true;
}
if (tryLoadingFromJavaseLoader) {
try {
//利用javaser的加载方式 ,即双亲委派的模式,核心类库
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// (0.5) Permission to access this class when using a SecurityManager
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = "Security Violation, attempt to use " +
"Restricted Class: " + name;
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
//是否使用委托方式,即利用父类加载器加载该类的方式
boolean delegateLoad = delegate || filter(name, true);
// (1) Delegate to our parent if requested
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// (2) Search local repositories
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
//不使用父类加载器的方式,直接重写findclass,
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// (3) Delegate to parent unconditionally
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
}
throw new ClassNotFoundException(name);
}
protected void checkStateForClassLoading(String className) throws ClassNotFoundException {
// It is not permitted to load new classes once the web application has
// been stopped.
try {
checkStateForResourceLoading(className);
} catch (IllegalStateException ise) {
throw new ClassNotFoundException(ise.getMessage(), ise);
}
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
if (log.isDebugEnabled())
log.debug(" findClass(" + name + ")");
checkStateForClassLoading(name);
// (1) Permission to define this class when using a SecurityManager
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
if (log.isTraceEnabled())
log.trace(" securityManager.checkPackageDefinition");
securityManager.checkPackageDefinition(name.substring(0,i));
} catch (Exception se) {
if (log.isTraceEnabled())
log.trace(" -->Exception-->ClassNotFoundException", se);
throw new ClassNotFoundException(name, se);
}
}
}
// Ask our superclass to locate this class, if possible
// (throws ClassNotFoundException if it is not found)
Class<?> clazz = null;
try {
if (log.isTraceEnabled())
log.trace(" findClassInternal(" + name + ")");
try {
if (securityManager != null) {
PrivilegedAction<Class<?>> dp =
new PrivilegedFindClassByName(name);
clazz = AccessController.doPrivileged(dp);
} else {
//从本地的具体类名查找 内部方式 ;//webapps lib
clazz = findClassInternal(name);
}
} catch(AccessControlException ace) {
log.warn("WebappClassLoader.findClassInternal(" + name
+ ") security exception: " + ace.getMessage(), ace);
throw new ClassNotFoundException(name, ace);
} catch (RuntimeException e) {
if (log.isTraceEnabled())
log.trace(" -->RuntimeException Rethrown", e);
throw e;
}
if ((clazz == null) && hasExternalRepositories) {
try {
//调用父类的加载方式
clazz = super.findClass(name);
} catch(AccessControlException ace) {
log.warn("WebappClassLoader.findClassInternal(" + name
+ ") security exception: " + ace.getMessage(), ace);
throw new ClassNotFoundException(name, ace);
} catch (RuntimeException e) {
if (log.isTraceEnabled())
log.trace(" -->RuntimeException Rethrown", e);
throw e;
}
}
if (clazz == null) {
if (log.isDebugEnabled())
log.debug(" --> Returning ClassNotFoundException");
throw new ClassNotFoundException(name);
}
} catch (ClassNotFoundException e) {
if (log.isTraceEnabled())
log.trace(" --> Passing on ClassNotFoundException");
throw e;
}
// Return the class we have located
if (log.isTraceEnabled())
log.debug(" Returning class " + clazz);
if (log.isTraceEnabled()) {
ClassLoader cl;
if (Globals.IS_SECURITY_ENABLED){
cl = AccessController.doPrivileged(
new PrivilegedGetClassLoader(clazz));
} else {
//如果父类再加载不到的化,
cl = clazz.getClassLoader();
}
log.debug(" Loaded by " + cl.toString());
}
return clazz;
}
Web应用默认的类加载顺序是(打破了双亲委派规则):
- 先从JVM的BootStrapClassLoader中加载。
- 加载Web应用下/WEB-INF/classes中的类。
- 加载Web应用下/WEB-INF/lib/*.jap中的jar包中的类。
- 加载上面定义的System路径下面的类。
- 加载上面定义的Common路径下面的类。
如果在配置文件中配置了``,那么就是遵循双亲委派规则,加载顺序如下:
- 先从JVM的BootStrapClassLoader中加载。
- 加载上面定义的System路径下面的类。
- 加载上面定义的Common路径下面的类。
- 加载Web应用下/WEB-INF/classes中的类。
- 加载Web应用下/WEB-INF/lib/*.jap中的jar包中的类。
2.1 Tomcat对用户类库与类加载器的规划
在其目录结构下有三组目录(“/common/”、“/server/”、“/shared/”)可以存放Java类库,另外还可以加上Web应用程序本身的目录“/WEB-INF/”,一共4组,把Java类库放置在这些目录中的含义分别如下:
- 放置在/commom目录中:类库可被Tomcat和所有的Web应用程序共同使用
- 放置在/server目录中:类库可被Tomcat使用,对所有的Web应用程序都不可见
- 放置在/shared目录中:类库可被所有的Web应用程序所共同使用,但对Tomcat自己不可见
- 放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见
为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,所下图:
最上面的三个类加载器是JDK默认提供的类加载器,这三个加载器的的作用之前也说过,这里不再赘述了,可以在简单了解。而CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebAppClassLoader则是Tomcat自己定义的类加载器,他们分别加载/common/、/server/、/shared/和/WebApp/WEB-INF/中的Java类库。其中WebApp类加载器和jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个jsp文件对应一个Jsp类加载器
从上图的委派关系可以看出,CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的哪一个Class,它出现的目的就是为了被丢弃:当服务器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过在建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能
4. OSGI是如何打破双亲委派的
既然说到OSGI,就要来解释一下OSGi是什么,以及它的作用
OSGi(Open Service Gateway Initiative):是OSGi联盟指定的一个基于Java语言的动态模块化规范,这个规范最初是由Sun、IBM、爱立信等公司联合发起,目的是使服务提供商通过住宅网管为各种家用智能设备提供各种服务,后来这个规范在Java的其他技术领域也有不错的发展,现在已经成为Java世界中的“事实上”的模块化标准,并且已经有了Equinox、Felix等成熟的实现。OSGi在Java程序员中最著名的应用案例就是Eclipse IDE
OSGi中的每一个模块(称为Bundle)与普通的Java类库区别并不大,两者一般都以JAR格式进行封装,并且内部存储的都是Java Package和Class。但是一个Bundle可以声明它所依赖的Java Package(通过Import-Package描述),也可以声明他允许导出发布的Java Package(通过Export-Package描述)。在OSGi里面,Bundle之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖(至少外观上如此),而且类库的可见性能得到精确的控制,一个模块里只有被Export过的Package才可能由外界访问,其他的Package和Class将会隐藏起来。除了更精确的模块划分和可见性控制外,引入OSGi的另外一个重要理由是,基于OSGi的程序很可能可以实现模块级的热插拔功能,当程序升级更新或调试除错时,可以只停用、重新安装然后启动程序的其中一部分,这对企业级程序开发来说是一个非常有诱惑性的特性
OSGi之所以能有上述“诱人”的特点,要归功于它灵活的类加载器架构。OSGi的Bundle类加载器之间只有规则,没有固定的委派关系。例如,某个Bundle声明了一个它依赖的Package,如果有其他的Bundle声明发布了这个Package,那么所有对这个Package的类加载动作都会为派给发布他的Bundle类加载器去完成。不涉及某个具体的Package时,各个Bundle加载器是平级关系,只有具体使用某个Package和Class的时候,才会根据Package导入导出定义来构造Bundle间的委派和依赖
另外,一个Bundle类加载器为其他Bundle提供服务时,会根据Export-Package列表严格控制访问范围。如果一个类存在于Bundle的类库中但是没有被Export,那么这个Bundle的类加载器能找到这个类,但不会提供给其他Bundle使用,而且OSGi平台也不会把其他Bundle的类加载请求分配给这个Bundle来处理
一个例子:假设存在BundleA、BundleB、BundleC三个模块,并且这三个Bundle定义的依赖关系如下:
- BundleA:声明发布了packageA,依赖了java.*的包
- BundleB:声明依赖了packageA和packageC,同时也依赖了Java.*的包
- BundleC:声明发布了packageC,依赖了packageA
那么,这三个Bundle之间的类加载器及父类加载器之间的关系如下图:
由于没有涉及到具体的OSGi实现,所以上图中的类加载器没有指明具体的加载器实现,只是一个体现了加载器之间关系的概念模型,并且只是体现了OSGi中最简单的加载器委派关系。一般来说,在OSGi中,加载一个类可能发生的查找行为和委派关系会比上图中显示的复杂,类加载时的查找规则如下:
- 以java.*开头的类,委派给父类加载器加载
- 否则,委派列表名单内的类,委派给父类加载器加载
- 否则,Import列表中的类,委派给Export这个类的Bundle的类加载器加载
- 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
- 否则,查找是否在自己的Fragment Bundle中,如果是,则委派给Fragment bundle的类加载器加载
- 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载
- 否则,查找失败
从之前的图可以看出,在OSGi里面,加载器的关系不再是双亲委派模型的树形架构,而是已经进一步发展成了一种更复杂的、运行时才能确定的网状结构
3.1 相关面试题:一个类的静态块是否可能被执行两次
一个自于网易面试官的一个问题,一个类的静态块是否可能被执行两次。
答案:如果一个类,被两个 osgi的bundle加载, 然后又有实例被初始化,其静态块会被执行两次
5. SPI机制绕开
SPI机制绕开参见:https://www.jianshu.com/p/22d9c0a2d2e9