日常源码解读技术架构

JarsLink | 阿里巴巴JarsLink动态加载与卸载原理

2018-05-01  本文已影响318人  Evan_Leung

1. 概述

本章节主要介绍JarsLinkd动态加载和卸载的实现机制,了解其设计思想和原理,会从以下几点入手:

1.模块加载/卸载的主要时序图
2.关键代码分析说明
3.问题分析

1.1 模块动态加载卸载主流程

AbstractModuleRefreshScheduler:模块管理,包含获取模块,执行模块里的方法
ModuleLoaderImpl:模块加载器
ModuleManagerImpl:模块管理者, 提供注册,移除和查找模块能力
SpringModule:集成Spring上下文的模块,从Spring上下中找Action
ConfigurableApplicationContext:Spring上下文


Load_Module_Sequence.png

2. 模块动态加载

2.1 模块加载源码分析

从上节模块加载主流程,我们可以大概了解到JarsLink加载的一个调用关系,接下来我们可以通过以上方法入口一步步进行分析
AbstractModuleRefreshScheduler:#refreshModuleConfigs()

2.1.1 AbstractModuleRefreshScheduler

从上文我们可以知道JarsLink模块加载是通过调度触发的,我们先看看AbstractModuleRefreshScheduler初始化时做了什么

  @Override
    public void afterPropertiesSet() throws Exception {
        //先刷新一次
        refreshModuleConfigs();
        scheduledExecutor = new ScheduledThreadPoolExecutor(1,
                new BasicThreadFactory.Builder().namingPattern("module_refresh-schedule-pool-%d").daemon(true).build());
        scheduledExecutor
                .scheduleWithFixedDelay(this, initialDelay, refreshDelay, TimeUnit.SECONDS);
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("AbstractModuleRefreshScheduler start");
        }
    }
  /**
     * 默认延迟执行,单位秒
     */
    private static final int DEFAULT_INITIAL_DELAY = 5;

    /**
     * 模块刷新默认间隔,单位秒
     */
    private static final int DEFAULT_REFRESH_DELAY = 60;

    /** 初始化的延迟时间 */
    private int initialDelay = DEFAULT_INITIAL_DELAY;

    /** 刷新间隔时间 */
    private int refreshDelay = DEFAULT_REFRESH_DELAY;
  /**
     * ScheduledExecutor 定时运行的方法
     * @see Runnable#run()
     */
    @Override
    public void run() {
        try {
            if (LOGGER.isInfoEnabled()) {
                LOGGER.info("Start module configs refresh");
            }
            refreshModuleConfigs();
            if (LOGGER.isInfoEnabled()) {
                LOGGER.info("Stop module configs refresh");
            }
        } catch (Throwable e) {
            LOGGER.error("Failed to refresh module configs", e);
        }
    }
  /**
     * 刷新ModuleConfig
     */
    private void refreshModuleConfigs() {
        // 查找状态为ENABLED的ModuleConfig,并以模块名作为Key,放到Map中
        Map<String, ModuleConfig> moduleConfigs = indexModuleConfigByModuleName(filterEnabledModule());

        // 转换Map的Value,提取Module的Version,Map的Key为DataProvider,Value为Version
        Map<String, String> configVersions = transformToConfigVersions(moduleConfigs);
        // 获取当前内存中,也就是ModuleManager已经加载的模板版本,同样Map的Key为name,Value为Version
        Map<String, String> moduleVersions = transformToModuleVersions(moduleManager.getModules());
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Config size: {}", configVersions.size());
            LOGGER.info("Module size: {}", moduleVersions.size());
            LOGGER.info("now in map {}", moduleVersions);
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Config versions: {}", configVersions);
            LOGGER.debug("Module versions: {}", moduleVersions);
        }
        // 找出配置与当前内存里配置的不同
        MapDifference<String, String> difference = Maps.difference(configVersions, moduleVersions);
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Version difference: {}", difference);
        }
        // 配置新增的
        putModules(moduleConfigs, configAdds(difference));
        // 配置版本与模块不同的
        putModules(moduleConfigs, configDifference(difference));
        // 模块多余的
        removeModules(modulesRedundant(difference));
    }

我们看看#putModules(moduleConfigs, configAdds(difference))这个方法怎样处理新增和发生变化的Jar

/**
     * 根据dataProviders指定的ModuleConfig初始化模块,并放入ModuleManager中
     *
     * @param moduleConfigs
     * @param moduleNames
     */
    private void putModules(Map<String, ModuleConfig> moduleConfigs, Set<String> moduleNames) {
        for (String name : moduleNames) {
            ModuleConfig moduleConfig = moduleConfigs.get(name);
            try {
                if (isFailedVersion(moduleConfig)) {
                    if (LOGGER.isInfoEnabled()) {
                        LOGGER.info("this version is failed, ignore.{}", moduleConfig);
                    }
                    continue;
                }
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Load module config: {}", moduleConfig);
                }
                Module module = moduleLoader.load(moduleConfig);
                Module removed = moduleManager.register(module);
                destroyQuietly(removed);
                moduleManager.getErrorModuleContext().remove(name.toUpperCase(Locale.CHINESE));
                moduleManager.getErrorModuleContext().remove(name.toUpperCase(Locale.CHINESE) + "_ERROR");
            } catch (Exception e) {
                moduleManager.getErrorModuleContext().put(name.toUpperCase(Locale.CHINESE) + "_ERROR",
                        ToStringBuilder.reflectionToString(e));
                moduleManager.getErrorModuleContext().put(name.toUpperCase(Locale.CHINESE),
                        moduleConfig.getVersion());
                LOGGER.error("Failed to load module config: " + moduleConfig, e);
            } catch (Error e) {
                LOGGER.error("Failed to load module config: " + moduleConfig, e);
            }
        }
    }

2.1.2 ModuleLoaderImpl

ModuleLoaderImpl是模块加载器的实现,主要负责模块加载,接下来我们看看上文moduleLoader.load(moduleConfig)方法做了什么

  @Override
    public Module load(ModuleConfig moduleConfig) {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Loading module: {}", moduleConfig);
        }
        List<String> tempFileJarURLs = moduleConfig.getModuleUrlPath();
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Local jars: {}", tempFileJarURLs);
        }

        ConfigurableApplicationContext moduleApplicationContext = loadModuleApplication(moduleConfig, tempFileJarURLs);

        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Loading module  complete:{}", moduleConfig);
        }
        return new SpringModule(moduleConfig, moduleConfig.getVersion(), moduleConfig.getName(), moduleApplicationContext);
    }

接下来我们看看loadModuleApplication方法主要做了什么,这个方法是实现动态加载的核心

/**
     * 根据本地临时文件Jar,初始化模块自己的ClassLoader,初始化Spring Application Context,同时要设置当前线程上下文的ClassLoader问模块的ClassLoader
     *
     * @param moduleConfig
     * @param tempFileJarURLs
     * @return
     */
    private ClassPathXmlApplicationContext loadModuleApplication(ModuleConfig moduleConfig, List<String> tempFileJarURLs) {
        ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
        //获取模块的ClassLoader
        ClassLoader moduleClassLoader = new ModuleClassLoader(moduleConfig.getModuleUrl(), applicationContext.getClassLoader(),
                getOverridePackages(moduleConfig));

        try {
            //把当前线程的ClassLoader切换成模块的
            Thread.currentThread().setContextClassLoader(moduleClassLoader);
            ModuleApplicationContext moduleApplicationContext = new ModuleApplicationContext(applicationContext);
            Properties properties = getProperties(moduleConfig);
            moduleApplicationContext.setProperties(properties);
            moduleApplicationContext.setClassLoader(moduleClassLoader);
            moduleApplicationContext.setConfigLocations(findSpringConfigs(tempFileJarURLs, moduleClassLoader,
                    getExclusionConfigeNameList(properties)));
            moduleApplicationContext.refresh();
            return moduleApplicationContext;
        } catch (Throwable e) {
            CachedIntrospectionResults.clearClassLoader(moduleClassLoader);
            throw Throwables.propagate(e);
        } finally {
            //还原当前线程的ClassLoader
            Thread.currentThread().setContextClassLoader(currentClassLoader);
        }
    }

2.1.3 ModuleManagerImpl

ModuleManagerImpl主要用于模块管理,本文只关注模块注册
上文提到过模块加载时,如果模块有发生变化,新增Jar或者版本发生变化,都会用到register

  public Module register(Module module) {
        checkNotNull(module, "module is null");
        String name = module.getName();
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Put Module: {}-{}", name, module.getVersion());
        }

        return modules.put(name.toUpperCase(Locale.CHINESE), module);
    }

3. 模块动态卸载

卸载模块需要满足三个条件

所以需要做到三点卸载实例,卸载类和卸载类加载器,整个模块的卸载顺序如下:


687474703a2f2f69666576652e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031372f30382f536e697032303137303830365f32382e706e67.png

3.1 模块动态卸载源码分析

JarsLink动态卸载Jar跟动态加载一样,都是通过定时调度来检测是否存在旧Jar包,然后通过就是关闭spring上下文、清理类加载器、通过moduleManager从内存移除等操作实现。
因为上文动态加载部分已经把大部分模块已经讲解了,接下来主要是重点讲解涉及到卸载部分。

3.1.1 SpringModule

JarsLink卸载Jar主要通过调用SpringModule类中的destroy()方法处理

  @Override
    public void destroy() {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Close application context: {}", applicationContext);
        }
        //close spring context
        closeQuietly(applicationContext);
        //clean classloader
        clear(applicationContext.getClassLoader());
    }
  /**
     * 关闭Spring上下文
     * @param applicationContext
     */
    private static void closeQuietly(ConfigurableApplicationContext applicationContext) {
        checkNotNull(applicationContext, "applicationContext is null");
        try {
            applicationContext.close();
        } catch (Exception e) {
            LOGGER.error("Failed to close application context", e);
        }
    }
   /**
     * 清除类加载器
     *
     * @param classLoader
     */
    public static void clear(ClassLoader classLoader) {
        checkNotNull(classLoader, "classLoader is null");
        //Introspector缓存BeanInfo类来获得更好的性能。卸载时刷新所有Introspector的内部缓存。
        Introspector.flushCaches();
        //从已经使用给定类加载器加载的缓存中移除所有资源包
        ResourceBundle.clearCache(classLoader);
        //Clear the introspection cache for the given ClassLoader
        CachedIntrospectionResults.clearClassLoader(classLoader);
        LogFactory.release(classLoader);
    }

4. 彩蛋

4.1 JarsLink如何打破双亲委托机制去动态加载类

双亲委托机制
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载

在JVM中,默认的类加载器都是遵循双亲委托机制,这样可以防止内存中出现多份同样的字节码 ,但是对于JarsLink来说,在运行时加载父加载器已经加载过的类,实现模块升级依赖包不需要系统发布,是怎么实现的呢?
JarsLink是通过重写ClassLoader的loadClass方法,从而实现对模块特定的类突破双亲委托加载,可以动态加载和覆盖

    /**
     * 覆盖双亲委派机制
     *
     * @see ClassLoader#loadClass(String, boolean)
     */
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class<?> result = null;
        synchronized (ModuleClassLoader.class) {
            if (isEligibleForOverriding(name)) {
                if (LOGGER.isInfoEnabled()) {
                    LOGGER.info("Load class for overriding: {}", name);
                }
                result = loadClassForOverriding(name);
            }
            if (result != null) {
                //链接类
                if (resolve) {
                    resolveClass(result);
                }
                return result;
            }
        }
        //使用默认类加载方式
        return super.loadClass(name, resolve);

    }

4.2 JarsLink如何实现类隔离和实例隔离

JarsLink通过独立的类加载器和Spring上下文实现
类隔离:框架为每个模块的Class使用单独的ClassLoader来加载,每个模块可以依赖同一种框架的不同的版本。
实例隔离:框架为每个模块创建了一个独立的Spring上下文,来加载模块中的BEAN,实例化失败不会影响其他模块。

上面章节也提到过,JarsLink是通过ModuleLoaderImpl的load(ModuleConfig moduleConfig) 方法进行模块加载,而在ModuleLoaderImpl中loadModuleApplication(ModuleConfig moduleConfig, List tempFileJarURLs)方法是真正实现类隔离和实例隔离的核心,为每个模块的Class都有自己的类加载器和类实例有独立的IOC容器


微信截图_20180415012356.png
类隔离.png

4.3 动态加载新版本的Jar会对正在运行的Jar有影响吗

从4.2可以得知,每一个模块的Class都有独立的类加载器和上下文,当我们更新的Jar包,重新加载,运行时的类和新加载的类使用的是不同的IOC容器和类加载器,可以理解为是两个独立的副本,互不干涉。

而模块卸载也需要满足模块里的实例对象没有被引用、模块里的Class没有被引用、类加载器没有被引用三个条件,这样保证了运行中的代码不受影响。


白天上班,休息写写博客,更多内容,请关注公众号Eshare分享!


qrcode_for_gh_8b8c2e52934a_430.jpg
上一篇下一篇

猜你喜欢

热点阅读