我是靠谱客的博主 风趣纸飞机,最近开发中收集的这篇文章主要介绍java自定义插件实现,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

一、背景

算法RD以前用python实现特征预处理,改完代码重新运行就生效了,现在改成java(为了借助java更强大生态,同时融入公司java体系),每次修改,都要打包、上线,迭代效率特别低。

二、需求分析

python编程,不需要显式编译,现改现运行,python解释器会先把源代码编译成.pyc,然后解释执行,两步一起做了,用户无感知。而java源代码需要先在一个地方编译成.class字节码,然后丢到另一个地方的JVM去解释执行,两步是分开的。

需求很简单:java插件修改完代码,重新打包上传到插件目录,线上代码自动生效。

需求可分解为:

  1. 插件
  2. 热加载

三、需求拆解

在实现这个需求时,我做了很多调研,先梳理一些知识点。

双亲委派

  1. 子类先委托父类加载
  2. 父类加载器有自己的加载范围,范围内没有找到,则不加载,并返回给子类
  3. 子类在收到父类无法加载的时候,才会自己去加载

类加载器体系

  1. 启动类加载器(BootstrapClassLoader):C++实现,在java里无法获取,只负责加载<JAVA_HOME>/lib下的类。
  2. 扩展类加载器(ExtClassLoader): Java实现,可以在java里获取,只负责加载<JAVA_HOME>/lib/ext下的类。
  3. 系统类加载器/应用程序类加载器(AppClassLoader):是与我们接触对多的类加载器,我们写的代码默认就是由它来加载,ClassLoader.getSystemClassLoader返回的就是它。
  4. 自定义类加载器(CustomClassLoader)

v2-26332f01bc80217ceac1ea5a7eab87c8_b.jpg
双亲委派流程图(左找classloader,右尝试加载类)

双亲委派最重要的作用是为了安全,保证核心API的.class不被篡改,比如jdk的String类只有BootstrapClassLoader能加载,如果AppClassLoader也能加载,JVM中会有多个String类的class实例,有很大安全隐患。

SPI

java做插件,标准做法基本是用SPI(Service Provider Interface)实现,由框架层制定接口,允许第三方为这些接口提供实现。比如

  1. JDBC:java.sql.Driver是jdk定义的,mysql产商提供mysql-connector-java具体实现。
  2. dubbo:extension机制,协议扩展、路由扩展等。

双亲委派的可见性原则

  • AppClassLoader是可以读取到由ExtClassLoader和BootstrapClassLoader加载进来的Class的;
  • ExtClassLoader是可以读取到由BootstrapClassLoader加载进来的Class的;

v2-aaf81c472941fa9fa71fccc30d75dab9_b.jpg
双亲委派可见性(网上引用)

为什么SPI要打破双亲委派?

如果接口定义A和接口实现B,都是被AppClassLoader加载,自然没必要打破。但有些情况下,A和B不是同一个类加载器加载,比如第一个例子,JDBC接口定义是在<JAVA_HOME>/lib包,它只能被BootstrapClassLoader加载的,而BootstrapClassLoader不能加载mysql的实现类,只能由AppClassLoader加载。根据双亲委派的可见性原则,BootstrapClassLoader加载的DriverManager是不可能拿到AppClassLoader加载的实现类,所以要想办法打破。

怎么打破双亲委派?

  1. 上下文类加载器
  2. 自定义类加载器

SPI打破双亲委派,一般通过上下文类加载器

eg1:jdk的ServiceLoader

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

eg2:dubbo扩展获取classloader源码

public static ClassLoader getClassLoader(Class<?> clazz) {
        ClassLoader cl = null;
        try {
            cl = Thread.currentThread().getContextClassLoader();
        } catch (Throwable ex) {
         // Cannot access thread context ClassLoader - falling back to system class loader...
        }
        if (cl == null) {
            // No thread context class loader -> use class loader of this class.
            cl = clazz.getClassLoader();
            if (cl == null) {
                // getClassLoader() returning null indicates the bootstrap ClassLoader
                try {
                    cl = ClassLoader.getSystemClassLoader();
                }
                catch (Throwable ex) {
                    // Cannot access system ClassLoader - oh well, maybe the caller can live with null...
                }
            }
        }

        return cl;
    }

要实现热加载,一般需要自定义类加载器

因为热加载的本质是引用不同的class实例,而唯一确定一个class实例唯一性是通过如下2点,只要一致,就是同一个class实例。

  1. 类全限定名
  2. 类加载器

类全限定名很好理解,比如:java.sql.Driver,通常它不被反复修改。

所以只能从类加载器下手,每次热部署时,new一个自定义类加载器来重新加载类,这样在方法区有2个class实例,使用新的class代替旧的class使用。

四、实现代码

原理:每次插件jar上传,DefaultTransformFactory自动监控到jar包更新,重新new一个TransformClassLoader来加载新的类放在pluginClassMap,插件使用方只需要调用getTransformerClassMap获取pluginClassMap来使用。

待改进:

  1. 这里没考虑插件的卸载,理论上存在class实例和自定义类加载器实例会无限创建,引起元空间内存溢出(方法区)的隐患。不过我们在调用方做到了对class引用不扩散,以及每次重启jvm会重置,影响不大。

闲话:

  1. 这里只有部分核心代码,实现上只用了自定义类加载器,就解决了插件+热加载的需求,上面分析SPI的篇幅,只是调研总结的资料。tomcat应用部署隔离,其实就是自定义类加载器+SPI结合使用的场景,我们这个需求没那么复杂。
  2. 热部署确实很热,延伸到OSGi(Java动态化模块化系统)、蚂蚁金服JarsLink,据说蚂蚁金服做到了上线无需发版,着实厉害。
  3. 刚看到github上专门有做插件的开源项目pf4j,后面有空也要研究一下。

接入方调用代码块

//通过插件name找到class
Map<String, Class<? extends BaseTransformer>> transformPluginMap = transformFactory.getTransformerClassMap();
if (transformPluginMap != null && transformPluginMap.size() > 0) {
    Iterator<Map.Entry<String, Class<? extends BaseTransformer>>> iterator = transformPluginMap.entrySet().iterator();
    while (iterator.hasNext()) {
        Map.Entry<String, Class<? extends BaseTransformer>> entry = iterator.next();
        if (name2Transformer.containsKey(entry.getKey())) {
            log.info("name={}`transformer={} already in name2Transformer", entry.getKey(), entry.getValue());
            continue;
        }
        name2Transformer.put(entry.getKey(), entry.getValue());
        log.info("name={}`transformer={} inserted into name2Transformer", entry.getKey(), entry.getValue());
    }
}

自定义类加载器TransformClassLoader

/**
 * Created by itbasketplayer on 2020/7/16.
 */
public class TransformClassLoader extends ClassLoader {

    private final static Logger logger = LoggerFactory.getLogger(TransformClassLoader.class);

    private HashSet dynaclazns; // 需要由该类加载器直接加载的类全限定名

    public TransformClassLoader(String jarfileDir) {
        super(null); // 指定父类加载器为 null
        if (StringUtils.isEmpty(jarfileDir)) {
            throw new IllegalArgumentException("basePath can not be empty!");
        }
        File dir = new File(jarfileDir);
        if (!dir.exists()) {
            throw new IllegalArgumentException("basePath not exists:" + jarfileDir);
        }
        if (!dir.isDirectory()) {
            throw new IllegalArgumentException("basePath must be a directory:" + jarfileDir);
        }
        dynaclazns = new HashSet();
        loadClassFromJarfileDir(jarfileDir);
    }

    private void loadClassFromJarfileDir(String jarfileDir) {
        File[] files = new File(jarfileDir).listFiles();
        if (files != null) {
            for (File file : files) {
                scanJarFile(file);
            }
        }
    }

    private void scanJarFile(File file) {
        if (file.exists()) {
            if (file.isFile() && file.getName().endsWith(".jar")) {
                try {
                    readJAR(new JarFile(file));
                } catch (IOException e) {
                    logger.error("", e);
                }
            } else if (file.isDirectory()) {
                for (File f : file.listFiles()) {
                    scanJarFile(f);
                }
            }
        }
    }

    private void readJAR(JarFile jar) throws IOException {
        Enumeration<JarEntry> en = jar.entries();
        while (en.hasMoreElements()) {
            JarEntry je = en.nextElement();
            je.getName();
            String name = je.getName();
            if (name.endsWith(".class")) {
                String className = name.replace("\", ".")
                        .replace("/", ".")
                        .replace(".class", "");
                InputStream input = null;
                ByteArrayOutputStream baos = null;
                try {
                    input = jar.getInputStream(je);
                    baos = new ByteArrayOutputStream();
                    int bufferSize = 1024;
                    byte[] buffer = new byte[bufferSize];
                    int bytesNumRead = 0;
                    while ((bytesNumRead = input.read(buffer)) != -1) {
                        baos.write(buffer, 0, bytesNumRead);
                    }
                    defineClass(className, baos.toByteArray(), 0, baos.toByteArray().length);
                    dynaclazns.add(className);
                } catch (Exception e) {
                    logger.error("", e);
                } finally {
                    if (baos != null) {
                        baos.close();
                    }
                    if (input != null) {
                        input.close();
                    }
                }
            }
        }
    }

    protected Class loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        Class cls = null;
        cls = findLoadedClass(name);
        if (!this.dynaclazns.contains(name) && cls == null)
            cls = getSystemClassLoader().loadClass(name);
        if (cls == null)
            throw new ClassNotFoundException(name);
        if (resolve)
            resolveClass(cls);
        return cls;
    }

}


默认插件工厂DefaultTransformFactory

/**
 * 默认插件工厂
 * Created by itbasketplayer on 2020/7/16.
 */
public class DefaultTransformFactory extends AbstractTransformFactory {

    private final static Logger logger = LoggerFactory.getLogger(DefaultTransformFactory.class);

    private TransformClassLoader classLoader;
    private AppConfig config;

    private volatile boolean reloading = false;
    private int pluginHistoryCount = 0;
    private int classLoaderHistoryCount = 0;

    public DefaultTransformFactory(final String pluginPath, final PluginDbInfo pluginDbInfo) {
        if (pluginPath == null || pluginPath.length() == 0) {
            throw new IllegalArgumentException("pluginPath can not be empty!");
        }
        final MysqlConfigParser mysqlConfigParser = new MysqlConfigParser(pluginDbInfo);
        try {
            this.config = mysqlConfigParser.parseConfig();
        } catch (Exception e) {
            throw new IllegalArgumentException("parseConfig error!");
        }
        this.config.setPluginPath(pluginPath);
        init(this.config);

        //文件监控,实现热更新
        ExecutorService schedulerExecuter = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable runnable) {
                Thread thread = Executors.defaultThreadFactory().newThread(runnable);
                thread.setName("TransformFileListener-Thread");
                thread.setDaemon(false);
                return thread;
            }
        });
        schedulerExecuter.submit(new Runnable() {
            @Override
            public void run() {
                TransformFileWatch transformFileWatch = new TransformFileWatch(pluginPath, 5);
                logger.info("begin transformFileWatch...");
                transformFileWatch.monitor(new TransformFileListener(new TransformFileListenerCallback() {
                    @Override
                    public void onFileChange(File file) {
                        try {
                            if (reloading) {
                                logger.info("正在更新jar,请勿太频繁更新");
                                return;
                            }
                            reloading = true;
                            config = mysqlConfigParser.parseConfig();
                            config.setPluginPath(pluginPath);
                            init(config);
                        } catch (Exception e) {
                            logger.error("", e);
                        } finally {
                            reloading = false;
                        }
                    }
                }));
            }
        });
    }

    private void init(AppConfig config) {
        long startTime = System.currentTimeMillis();
        ConcurrentHashMap<String, PluginDefinition> newPluginDefinitionMap = new ConcurrentHashMap();
        //加载pluginPath下的jar
        classLoader = new TransformClassLoader(config.getPluginPath());
        classLoaderHistoryCount++;
        //注册pluginName,此时并未初始化plugin实例,plugin实例将在getPlugin时初始化
        List<PluginDefinition> pds = config.getPlugins();
        if (pds != null && pds.size() > 0) {
            for (PluginDefinition pd : pds) {
                newPluginDefinitionMap.put(pd.getPluginName(), pd);
            }
        }
        pluginDefinitionMap.putAll(newPluginDefinitionMap);
        Map<String, Class<? extends BaseTransformer>> newPluginClassMap = initPluginClassMap(newPluginDefinitionMap);
        pluginHistoryCount += newPluginClassMap.size();
        pluginClassMap.putAll(newPluginClassMap);
        logger.info("init spends={}ms classLoader={} `classLoaderHistoryCount={} `pluginHistoryCount={}", System.currentTimeMillis() - startTime, classLoader.toString(), classLoaderHistoryCount, pluginHistoryCount);
        logger.info("pluginDefinitionMapSize={} `pluginClassSize={} `pluginClassMap={}", pluginDefinitionMap.size(), pluginClassMap.size(), pluginClassMap);
    }

    private Map<String, Class<? extends BaseTransformer>> initPluginClassMap(Map<String, PluginDefinition> newPluginDefinitionMap) {
        ConcurrentHashMap<String, Class<? extends BaseTransformer>> newPluginClassMap = new ConcurrentHashMap<>();
        for (Map.Entry<String, PluginDefinition> entry : newPluginDefinitionMap.entrySet()) {
            Class<? extends BaseTransformer> plugin = createPlugin(entry.getValue().getPluginClass());
            if (plugin == null) {
                logger.error("initPluginClassMap pluginName={} can not create a plugin", entry.getKey());
                continue;
            }
            newPluginClassMap.put(entry.getKey(), plugin);
        }
        return newPluginClassMap;
    }

    @Override
    protected Class<? extends BaseTransformer> loadPluginClass(String clazz) throws ClassNotFoundException {
        return (Class<? extends BaseTransformer>) classLoader.loadClass(clazz);
    }

}


插件工厂接口TransformFactory

/**
 * Created by itbasketplayer on 2020/7/10.
 */
public interface TransformFactory {

    /**
     * 通过插件全限定名获取插件实例
     * 注意使用规范,每次获取实例都从此接口获取,并使用final
     * 1、保证jar更新后能使用新的实例
     * 2、旧引用不扩散,避免引发内存泄露
     *
     * @param transformName
     * @return
     */
    Class<? extends BaseTransformer> getTransformerClass(String transformName);

    Map<String, Class<? extends BaseTransformer>> getTransformerClassMap();
}


插件工厂抽象类AbstractTransformFactory

/**
 * 插件工厂抽象类
 * Created by itbasketplayer on 2020/7/10.
 */
public abstract class AbstractTransformFactory implements TransformFactory {

    private static Logger logger = LoggerFactory.getLogger(AbstractTransformFactory.class);

    protected ConcurrentHashMap<String, PluginDefinition> pluginDefinitionMap = new ConcurrentHashMap<>();
    protected ConcurrentHashMap<String, Class<? extends BaseTransformer>> pluginClassMap = new ConcurrentHashMap<>();

    public Class<? extends BaseTransformer> getTransformerClass(String pluginName) {
        //必须在插件注册表,或者之前在注册表,后下线的
        PluginDefinition definition = pluginDefinitionMap.get(pluginName);
        if (definition == null) {
            logger.error("not found pluginName={} in db configs", pluginName);
            return null;
        }
        return pluginClassMap.get(pluginName);
    }

    public Map<String, Class<? extends BaseTransformer>> getTransformerClassMap() {
        return pluginClassMap;
    }

    protected Class<? extends BaseTransformer> createPlugin(String pluginClass) {
        try {
            Class<? extends BaseTransformer> clazz = loadPluginClass(pluginClass);
            return clazz;
        } catch (Exception e) {
            throw new PluginException("not found transformer class:" + pluginClass, e);
        }
    }

    protected abstract Class<? extends BaseTransformer> loadPluginClass(String clazz) throws ClassNotFoundException;

}

最后

以上就是风趣纸飞机为你收集整理的java自定义插件实现的全部内容,希望文章能够帮你解决java自定义插件实现所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(45)

评论列表共有 0 条评论

立即
投稿
返回
顶部