插件化框架

因为在写一个多租户可用的框架,希望在上线的时候各个租户能够尽量做到互不影响,所以写了一个插件化加载的方法。中间踩了不少坑,写篇文章记一下。

1 获取ClassLoader

1.1 上游获取ClassLoader

    /**
     * 反射设置addUrl为可达
     * @return
     */
    private static Method initAddMethod(){
        try{
            Method add = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
            add.setAccessible(true);
            return add;
        }catch (Exception e){
            logger.error("initAddMethod throw exception", e);
        }
        return null;
    }

    /**
     * 调用addURL这个函数
     * @param file 需要add的url
     * @param classLoader 当前classLoader
     * @throws MalformedURLException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    private static void addURL(File file, ClassLoader classLoader) throws MalformedURLException, InvocationTargetException, IllegalAccessException {
        addURL.invoke(classLoader, file.toURI().toURL());
    }
try {
    addURL(file, classLoader);
} catch (MalformedURLException | InvocationTargetException | IllegalAccessException e) {
    logger.error("addUrl throw exception", e);
    return Collections.emptyList();
}

1.2 新生成ClassLoader

在java中同一个ClassLoader不会加载重复的类,所以如果是需要重新加载Class,则需要使用心得ClassLoader。java中是用ClassLoader+类来唯一定义一个类的,所以一个应用里面是允许存在重复的类的。

new URLClassLoader(new URL[]{new URL("file:" + fileAbsPath)}, JarLoader.class.getClassLoader())

2 加载

上面获取ClassLoader之后,就可以加载了。
这里要注意一点,构造URLClassLoader的时候,如果不传入本类的ClassLoader来构造,在linux上会报错,但是我本地windows调试是没问题的。初步判断是classPath的问题。

    private static List<Class<?>> loadJar(File file, URLClassLoader classLoader){
        List<Class<?>> classList = new ArrayList<>();
        String className = "";
        String fileAbsPath = file.getAbsolutePath();
        //如果classLoader不为空则使用传入的classLoader,否则新建URLClassLoader
        try(JarFile jarFile = new JarFile(fileAbsPath);
            URLClassLoader loader = Objects.isNull(classLoader) ? new URLClassLoader(new URL[]{new URL(FILE_PREFIX + fileAbsPath)},
                    JarLoader.class.getClassLoader()) : classLoader){
            Enumeration<JarEntry> en = jarFile.entries();
            while (en.hasMoreElements()) {
                JarEntry je = en.nextElement();
                String name = je.getName();
                String s5 = name.replace('/', '.');
                if (s5.lastIndexOf(CLASS_POSTFIX) > 0) {
                    className = je
                            .getName()
                            .substring(0, je.getName().length() - CLASS_POSTFIX.length())
                            .replace('/', '.');
                    classList.add(loader.loadClass(className));
                }
            }
        } catch (IOException e) {
            logger.error("fail to load jar {}, throw exception", file.getAbsolutePath(), e);
        } catch (ClassNotFoundException | NoClassDefFoundError e) {
            logger.error("no class {} ", className, e);
        }
        return classList;
    }

3 注册到Spring中

3.1 获取BeanFactory

Spring中最常用的是DefaultListableBeanFactory,可以使用Aware接口来获取。

@Component
public class MySpringBeanUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if(MySpringBeanUtil.applicationContext == null){
            MySpringBeanUtil.applicationContext = applicationContext;
        }
    }

    //获取applicationContext
    public static ApplicationContext getApplicationContext(){
        return applicationContext;
    }

    //通过name获取Bean
    public static Object getBean(String name){
        return getApplicationContext().getBean(name);
    }

    //通过class获取Bean
    public static<T> T getBean(Class<T> clazz){
        return getApplicationContext().getBean(clazz);
    }

    //通过name,以及Clazz返回指定的Bean
    public static<T> T getBean(String name, Class<T> clazz){
        return getApplicationContext().getBean(name, clazz);
    }

    public static DefaultListableBeanFactory getBeanFactory(){
        ConfigurableApplicationContext context = (ConfigurableApplicationContext)applicationContext;
        return (DefaultListableBeanFactory)context.getBeanFactory();
    }

}

3.2 注册到Spring上下文中

调用beanFactory.registerBeanDefinition。

    private static List<String> registerClasses(List<Class<?>> clazzList, DefaultListableBeanFactory beanFactory ){
        List<String> registerClasses = new ArrayList<>();
        for(Class<?> clazz : clazzList){
            // 这里可以判断的更细致一点,比如头部有Component,头部有Service什么的
            if(!clazz.isInterface()){
                BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
                beanFactory.registerBeanDefinition(clazz.getSimpleName(), beanDefinitionBuilder.getRawBeanDefinition());
                registerProcessor(clazz, beanFactory);
                registerClasses.add(clazz.getSimpleName());
                logger.info("register {}", clazz.getSimpleName());
            }
        }
        return registerClasses;
    }

3.3 注册后放入业务map中

因为框架中是使用map来保存租户和类的关系的,所以通过beanFactory.getBean获取具体bean之后放到全局的<租户,Bean>的ConcurrentHashMap中。

    private static void registerProcessor(Class<?> clazz, DefaultListableBeanFactory beanFactory){
        Object bean = beanFactory.getBean(clazz.getSimpleName());
        if(clazz.getAnnotation(BeanCode.class) != null){
            String name = clazz.getAnnotation(BeanCode.class).value();
            beanMap.put(name, bean);
            logger.info("###注册单个意图识别前后置处理完成{} 类名{}", name, bean.getClass().getSimpleName());
        } else if(clazz.getAnnotation(PreprocessTypeAnnotation.class) != null){
            String name = clazz.getAnnotation(PreprocessTypeAnnotation.class).value();
            preprocessBeanMap.put(name, (PreprocessProcess) bean);
            logger.info("###注册预处理完成:{} 类名{}", name, clazz.getSimpleName());
        } else if(clazz.getAnnotation(IntentTypeAnnotation.class) != null){
            String name = clazz.getAnnotation(IntentTypeAnnotation.class).value();
            intentSerialBeanMap.put(name, (IntentProcess) bean);
            logger.info("###注册串行意图识别完成:{} 类名{}", name, clazz.getSimpleName());
        } else if(clazz.getAnnotation(RerankTypeAnnotation.class) != null){
            String name = clazz.getAnnotation(RerankTypeAnnotation.class).value();
            rerankBeanMap.put(name, (RerankProcess)bean);
            logger.info("###注册重排序完成:{} 类名{}", name, clazz.getSimpleName());
        } else if(clazz.getAnnotation(IntentParallelTypeAnnotation.class) != null){
            String name = clazz.getAnnotation(IntentParallelTypeAnnotation.class).value();
            intentParaBeanMap.put(name, bean.getClass().getName()); // 并行的需要在使用的时候构造实例
            logger.info("###注册并行意图识别完成:{} 类名{}", name, clazz.getSimpleName());
        } else if(clazz.getAnnotation(CommonProcessTypeAnnotation.class) != null){
            String name = clazz.getAnnotation(CommonProcessTypeAnnotation.class).value();
            commonBeanMap.put(name, (CommonProcess)bean);
            logger.info("###注册公共处理完成:{} 类名{}", name, clazz.getSimpleName());
        } else if(clazz.getAnnotation(SubRobotTypeAnnotation.class) != null){
            // 子机器人有分隔符
            String name = clazz.getAnnotation(SubRobotTypeAnnotation.class).value();
            String[] subRobots = name.split(SUBROBOT_SPLIT);
            for(String subRobot : subRobots) {
                subRobotBeanMap.put(subRobot, (SubRobotRecall)bean);
                logger.info("###注册机器人完成:{}", subRobot);
            }
        } else {
            logger.error("Clazz {} didnt match any process", clazz.getSimpleName());
        }
    }

ref:http://tutorials.jenkov.com/java-reflection/dynamic-class-loading-reloading.html
https://www.cnblogs.com/grey-wolf/p/11028613.html
https://www.cnblogs.com/grey-wolf/p/11028975.html
https://stackoverflow.com/questions/20913149/hot-swapping-the-jar-files-in-java
https://www.thinbug.com/q/30978695
https://codippa.com/how-to-load-same-class-more-than-once/
https://blog.csdn.net/dapanbest/article/details/73801815

comments powered by Disqus