美文网首页后端之路源码
Tomcat共享Jar包部署Springboot(非shared

Tomcat共享Jar包部署Springboot(非shared

作者: liurenhao | 来源:发表于2019-07-25 15:17 被阅读80次

    问题

     流量

     微服务化让我们的系统变得越来越多,往往在项目发布时,我们打出来的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版本为例

    1. 在tomcat安装目录下创建share/lib文件夹,放入共享的jar包。
    2. 修改conf/catalina.properties配置
      shared.loader="${catalina.base}/share/lib","${catalina.base}/share/lib/*.jar","${catalina.home}/share/lib","${catalina.home}/share/lib/*.jar"
    3. 利用maven-war-plugins插件在打包时排除共享jar包,打war包放入tomcat/webapp下
    4. 启动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去加载。研究了一下类加载部分的源码,发现可行!篇幅问题就不赘述我在这期间趟过的各种坑了,总之最后是成功的搞出来了,先看修改源码后的使用方式:

    1. 将修改后的catatlina模块源码打包为catalina.jar,替换tomcat/lib下的catalina.jar
    2. 在linux下创建/server/share_customer/lib文件夹,放入共享的jar包。
    3. 修改conf/catalina.properties配置,在share.loader=下,增加以下配置
      liuhao.loader=/server/share_customer/lib
    4. 利用maven-war-plugins插件在打包时排除共享jar包,打war包放入tomcat/webapp下
    5. 启动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包类覆盖的操作可以顺利实现。

    相关文章

      网友评论

        本文标题:Tomcat共享Jar包部署Springboot(非shared

        本文链接:https://www.haomeiwen.com/subject/hqjolctx.html