问题
流量
微服务化让我们的系统变得越来越多,往往在项目发布时,我们打出来的jar包或war包会非常的大,在利用CI&PI工具进行远程发布时,带宽可能会成为瓶颈。
磁盘空间
以springboot为例,当中间件运行多个springboot项目时,每个项目war包的WEB-INF/lib下都会有相同的一些springboot框架的jar包,对磁盘空间来说是浪费(当然这个影响是微不足道的)
思路
我们可以考虑把依赖的第三方jar包抽离,事先放入tomcat中间件中,在持续发布时,我们仅需要把项目本身的代码打成war包发布。当多个服务依赖的第三方jar包相同时,甚至可以将抽离的jar包作为共享。以springboot项目为例,springboot框架的jar包是每个springboot项目所依赖的,这时我们可以将springboot框架的jar包抽出来作为共享jar包,所有的springboot项目在打包时排除这些jar包,在中间件中发布时依赖共享jar包。
实现
Tomcat的类加载机制为我们提供了shared.loader配置,可以让同一个tomcat下运行的项目依赖一些共享的jar包。
以下讲解以tomcat-9.0.16
版本为例
- 在tomcat安装目录下创建share/lib文件夹,放入共享的jar包。
- 修改conf/catalina.properties配置
shared.loader="${catalina.base}/share/lib","${catalina.base}/share/lib/*.jar","${catalina.home}/share/lib","${catalina.home}/share/lib/*.jar"
- 利用maven-war-plugins插件在打包时排除共享jar包,打war包放入tomcat/webapp下
- 启动tomcat
很好,噩梦开始了
大多数情况下以上使用是没有问题的。这也是官方提供的用法,大家尽情使用。
一开始我也是兴高采烈地就这样操作了,然后就遇到了下面的问题:
Bean named 'myEntiy' is expected to be of type 'org.liuhao.kuangjia.vo.MyEntiy' but was actually of type 'org.liuhao.kuangjia.vo.MyEntiy'
当然还可能是下面这些错误:
java.lang.LinkageError: loader constraint violation: loader (instance of java/net/URLClassLoader) previously initiated loading for a different type with name xxx
org.liuhao.kuangjia.vo.MyEntiy Can not Cast to org.liuhao.kuangjia.vo.MyEntiy
当时我就懵逼了,明明是按照官方教程来的,咋会出现这种莫名其妙的错误。
出现以上问题的表面原因是因为我在项目中重写了jar
包中的MyEntiy
类,导致JVM中加载了两个相同的类。按理来说覆盖jar
包类是我们的常规操作,为什么会出现这种问题呢,这个就要涉及到tomcat设计的类加载机制问题了。
Tomcat类加载机制
先来看一张图
tomcat类加载模型
如上图所说,tomcat的中有以上6种类加载器。
Bootstrap —— 这个类加载器包含Java虚拟机提供的基本运行时类,以及系统扩展目录($JAVA_HOME/jre/lib/ext)中的JAR文件中的任何类。注意:一些JVM可能将其实现为多个类加载器,或者它可能根本不可见(作为类加载器)。
System —— 这个类加载器负责加载以下几个jar包
- $CATALINA_HOME/bin/bootstrap.jar
- $CATALINA_BASE/bin/tomcat-juli.jar or $CATALINA_HOME/bin/tomcat-juli.jar
- $CATALINA_HOME/bin/commons-daemon.jar
Common —— 这个类加载器加载common.loader
(conf/catalina.properties
)下的classes文件、资源文件和jar包,这些类对Tomcat内部类和所有Web应用程序都是可见的。
Server —— 这个类加载器加载server.loader
(conf/catalina.properties
)下的classes文件、资源文件和jar包,只对Tomcat内部可见,对web应用程序完全不可见。
Shared —— 这个类加载器加载shared.loader
(conf/catalina.properties
)下的classes文件、资源文件和jar包,对所有web应用程序可见,并可用于在所有web应用程序之间共享代码。但是,对此共享代码的任何更新都需要Tomcat重新启动
Webapp —— 为部署在单个Tomcat实例中的每个Web应用程序创建的类加载器。加载Web应用程序的/Web-INF/Class目录中的所有未打包类和资源,再加上Web应用程序/Web-INF/lib目录下JAR文件中的类和资源,对此web应用程序都是可见的,而对其他类则不可见。
更详细的介绍参考官网
要找到上面问题的根本原因,首先我们得了解一个词汇:双亲委派(即当类加载器在加载某个类时,先会把需要加载的类名交给父类加载器,如果父类加载器无法加载,才会由自身完成类加载。此模式是为了防止重复加载类以及恶意篡改核心类)
众所周知,JVM提供的类加载器都是遵循双亲委派的。值得注意的是,查看tomcat源码后可以发现WebappClassLoader是破坏了双亲委派原则的,关键代码如下
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 {
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
}
}
变量delegate
的默认值为false
,所以从上面代码我们可以发现,WebappClassLoader
在进行类加载时,会自身先尝试加载所需的类,如果自身无法完成类加载,才会委托给父类加载器进行加载。
到此,一切都真相大白了。因为我把项目依赖的jar包放入了shared.loader下,在容器初始化时,org.liuhao.kuangjia.vo.MyEntiy
由于被Spring Ioc初始化,此时调用org.liuhao.kuangjia.vo.MyEntiy
类的相关类是被SharedClassLoder加载的(因为相关jar包都在shared.loader
下),由于类加载器的传递性,会让SharedClassLoder
加载jar包里的org.liuhao.kuangjia.vo.MyEntiy
类,而由于我们项目中由重写了org.liuhao.kuangjia.vo.MyEntiy
类,在项目中调用org.liuhao.kuangjia.vo.MyEntiy
类时,WebappClassLoader
又会重新加载项目下的org.liuhao.kuangjia.vo.MyEntiy
类,此时JVM
会认为两个类是不同的(因为由不同的类加载器加载)。继而造成了上面我们提到的各种问题。
(我个人认为这算是一个BUG,这样的设计使得需要share框架时几乎无法满足要求。当然更可能是我水平有限不会用)
那我们在使用tomcat提供的共享类机制时,一定要覆盖共享类怎么办呢?以下两个办法:
- 在覆盖共享类时,要注意将调用此类的相关类都做覆盖(非常复杂繁琐)
- 修改tomcat源码,自己实现共享jar包功能(需要一些技术水平)
修改源码,拓展WebappClassLoader
终于进入正题了,领导提出了这种需求,你总不能跟他Bala一堆tomcat怎么怎么了,只能采用迂回战术,自己研究。
首先整理一下战略思路,问题出在SharedClassLoader和WebAppClassLoder的冲突上,那我们可以放弃使用SharedClassLoader,把共享类都让给WebAppCLassLoder去加载。研究了一下类加载部分的源码,发现可行!篇幅问题就不赘述我在这期间趟过的各种坑了,总之最后是成功的搞出来了,先看修改源码后的使用方式:
- 将修改后的catatlina模块源码打包为
catalina.jar
,替换tomcat/lib下的catalina.jar
- 在linux下创建/server/share_customer/lib文件夹,放入共享的jar包。
- 修改
conf/catalina.properties
配置,在share.loader=
下,增加以下配置
liuhao.loader=/server/share_customer/lib
- 利用maven-war-plugins插件在打包时排除共享jar包,打war包放入tomcat/webapp下
- 启动tomcat。
源码改动
1、StandardRoot
类下增加方法
protected void processWebInfYinHaiLib() throws LifecycleException {
String value = CatalinaProperties.getProperty("liuhao.loader");
if ((value != null) && (!value.equals(""))) {
Arrays.stream(value.split(",")).forEach(path -> {
File file = new File(path);
if(file.isDirectory()){
Arrays.stream(file.listFiles()).filter(pathname -> pathname.getName().endsWith(".jar")).forEach(shareJar -> {
try {
log.info("自定义共享jar包加入classes:"+shareJar.getName());
createWebResourceSet(ResourceSetType.CLASSES_JAR,
"/WEB-INF/classes", shareJar.toURI().toURL(), "/");
} catch (MalformedURLException e) {
e.printStackTrace();
}
});
}
});
}
}
2、StandardRoot
类修改startInternal方法
@Override
protected void startInternal() throws LifecycleException {
mainResources.clear();
main = createMainResourceSet();
mainResources.add(main);
for (List<WebResourceSet> list : allResources) {
// Skip class resources since they are started below
if (list != classResources) {
for (WebResourceSet webResourceSet : list) {
webResourceSet.start();
}
}
}
// This has to be called after the other resources have been started
// else it won't find all the matching resources
processWebInfLib();
//加入自定义共享包
processWebInfYinHaiLib();
// Need to start the newly found resources
for (WebResourceSet classResource : classResources) {
classResource.start();
}
cache.enforceObjectMaxSizeLimit();
setState(LifecycleState.STARTING);
}
3、修改WebappClassLoaderBase
类的start()
方法如下
/**
* Start the class loader.
*
* @exception LifecycleException if a lifecycle error occurs
*/
@Override
public void start() throws LifecycleException {
state = LifecycleState.STARTING_PREP;
WebResource[] classesResources = resources.getResources("/WEB-INF/classes");
for (WebResource classes : classesResources) {
if (classes.isDirectory() && classes.canRead()) {
localRepositories.add(classes.getURL());
}
}
WebResource[] jars = resources.listResources("/WEB-INF/lib");
for (WebResource jar : jars) {
if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
localRepositories.add(jar.getURL());
jarModificationTimes.put(
jar.getName(), Long.valueOf(jar.getLastModified()));
}
}
log.info("【装载自定义jar包,增加classpath路径】");
String value = CatalinaProperties.getProperty("liuhao.loader");
if ((value != null) && (!value.equals(""))) {
Arrays.stream(value.split(",")).forEach(path -> {
File file = new File(path);
if(file.isDirectory()){
Arrays.stream(file.listFiles()).filter(pathname -> pathname.getName().endsWith(".jar")).forEach(shareJar -> {
try {
localRepositories.add(shareJar.toURI().toURL());
} catch (MalformedURLException e) {
e.printStackTrace();
}
});
}
});
}
state = LifecycleState.STARTED;
}
自此就完全实现了jar包共享的功能,由于都由同一类加载器加载,并且保证了加载顺序,使得jar包类覆盖的操作可以顺利实现。
网友评论