久久精品精选,精品九九视频,www久久只有这里有精品,亚洲熟女乱色综合一区
    分享

    Spring Boot 動態加載jar包,動態配置太強了!

     jacklopy 2024-03-12 發布于河北

    一、概述

    1、背景

    目前數據治理服務中有眾多治理任務,當其中任一治理任務有改動需要升級或新增一個治理任務時,都需要將數據治理服務重啟,會影響其他治理任務的正常運行。

    2、目標

    • 能夠動態啟動、停止任一治理任務
    • 能夠動態升級、添加治理任務
    • 啟動、停止治理任務或升級、添加治理任務不能影響其他任務

    3、方案

    • 為了支持業務代碼盡量的解耦,把部分業務功能通過動態加載的方式加載到主程序中,以滿足可插拔式的加載、組合式的部署。
    • 配合xxl-job任務調度框架,將數據治理任務做成xxl-job任務的方式注冊到xxl-job中,方便統一管理。

    二、動態加載

    1、自定義類加載器

    URLClassLoader 是一種特殊的類加載器,可以從指定的 URL 中加載類和資源。它的主要作用是動態加載外部的 JAR 包或者類文件,從而實現動態擴展應用程序的功。為了便于管理動態加載的jar包,自定義類加載器繼承URLClassloader。

    /**
     * 自定義類加載器
     *
     * @author lijianyu
     * @date 2023/04/03 17:54
     **/

    public class MyClassLoader extends URLClassLoader {

        private Map<String, Class<?>> loadedClasses = new ConcurrentHashMap<>();

        public Map<String, Class<?>> getLoadedClasses() {
            return loadedClasses;
        }

        public MyClassLoader(URL[] urls, ClassLoader parent) {
            super(urls, parent);
        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            // 從已加載的類集合中獲取指定名稱的類
            Class<?> clazz = loadedClasses.get(name);
            if (clazz != null) {
                return clazz;
            }
            try {
                // 調用父類的findClass方法加載指定名稱的類
                clazz = super.findClass(name);
                // 將加載的類添加到已加載的類集合中
                loadedClasses.put(name, clazz);
                return clazz;
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
                return null;
            }
        }

        public void unload() {
            try {
                for (Map.Entry<String, Class<?>> entry : loadedClasses.entrySet()) {
                    // 從已加載的類集合中移除該類
                    String className = entry.getKey();
                    loadedClasses.remove(className);
                    try{
                        // 調用該類的destory方法,回收資源
                        Class<?> clazz = entry.getValue();
                        Method destory = clazz.getDeclaredMethod("destory");
                        destory.invoke(clazz);
                    } catch (Exception e ) {
                        // 表明該類沒有destory方法
                    }
                }
                // 從其父類加載器的加載器層次結構中移除該類加載器
                close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    • 自定義類加載器中,為了方便類的卸載,定義一個map保存已加載的類信息。key為這個類的ClassName,value為這個類的類信息。
    • 同時定義了類加載器的卸載方法,卸載方法中,將已加載的類的集合中移除該類。由于此類可能使用系統資源或調用線程,為了避免資源未回收引起的內存溢出,通過反射調用這個類中的destroy方法,回收資源。
    • 最后調用close方法。

    2、動態加載

    由于此項目使用spring框架,以及xxl-job任務的機制調用動態加載的代碼,因此要完成以下內容

    • 將動態加載的jar包讀到內存中
    • 將有spring注解的類,通過注解掃描的方式,掃描并手動添加到spring容器中。
    • 將@XxlJob注解的方法,通過注解掃描的方式,手動添加到xxljob執行器中。
    /**
     * @author lijianyu
     * @date 2023/04/29 13:18
     **/

    @Component
    public class DynamicLoad {

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

        @Autowired
        private ApplicationContext applicationContext;

        private Map<String, MyClassLoader> myClassLoaderCenter = new ConcurrentHashMap<>();

        @Value("${dynamicLoad.path}")
        private String path;

        /**
         * 動態加載指定路徑下指定jar包
         * @param path
         * @param fileName
         * @param isRegistXxlJob  是否需要注冊xxljob執行器,項目首次啟動不需要注冊執行器
         * @return map<jobHander, Cron> 創建xxljob任務時需要的參數配置
         */

        public void loadJar(String path, String fileName, Boolean isRegistXxlJob) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
            File file = new File(path +"/" + fileName);
            Map<String, String> jobPar = new HashMap<>();
            // 獲取beanFactory
            DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
            // 獲取當前項目的執行器
            try {
                // URLClassloader加載jar包規范必須這么寫
                URL url = new URL("jar:file:" + file.getAbsolutePath() + "!/");
                URLConnection urlConnection = url.openConnection();
                JarURLConnection jarURLConnection = (JarURLConnection)urlConnection;
                // 獲取jar文件
                JarFile jarFile = jarURLConnection.getJarFile();
                Enumeration<JarEntry> entries = jarFile.entries();

                // 創建自定義類加載器,并加到map中方便管理
                MyClassLoader myClassloader = new MyClassLoader(new URL[] { url }, ClassLoader.getSystemClassLoader());
                myClassLoaderCenter.put(fileName, myClassloader);
                Set<Class> initBeanClass = new HashSet<>(jarFile.size());
                // 遍歷文件
                while (entries.hasMoreElements()) {
                    JarEntry jarEntry = entries.nextElement();
                    if (jarEntry.getName().endsWith(".class")) {
                        // 1. 加載類到jvm中
                        // 獲取類的全路徑名
                        String className = jarEntry.getName().replace('/''.').substring(0, jarEntry.getName().length() - 6);
                        // 1.1進行反射獲取
                        myClassloader.loadClass(className);
                    }
                }
                Map<String, Class<?>> loadedClasses = myClassloader.getLoadedClasses();
                XxlJobSpringExecutor xxlJobExecutor = new XxlJobSpringExecutor();
                for(Map.Entry<String, Class<?>> entry : loadedClasses.entrySet()){
                    String className = entry.getKey();
                    Class<?> clazz = entry.getValue();
                    // 2. 將有@spring注解的類交給spring管理
                    // 2.1 判斷是否注入spring
                    Boolean flag = SpringAnnotationUtils.hasSpringAnnotation(clazz);
                    if(flag){
                        // 2.2交給spring管理
                        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
                        AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
                        // 此處beanName使用全路徑名是為了防止beanName重復
                        String packageName = className.substring(0, className.lastIndexOf(".") + 1);
                        String beanName = className.substring(className.lastIndexOf(".") + 1);
                        beanName = packageName + beanName.substring(01).toLowerCase() + beanName.substring(1);
                        // 2.3注冊到spring的beanFactory中
                        beanFactory.registerBeanDefinition(beanName, beanDefinition);
                        // 2.4允許注入和反向注入
                        beanFactory.autowireBean(clazz);
                        beanFactory.initializeBean(clazz, beanName);
                        /*if(Arrays.stream(clazz.getInterfaces()).collect(Collectors.toSet()).contains(InitializingBean.class)){
                            initBeanClass.add(clazz);
                        }*/

                        initBeanClass.add(clazz);
                    }

                    // 3. 帶有XxlJob注解的方法注冊任務
                    // 3.1 過濾方法
                    Map<Method, XxlJob> annotatedMethods = null;
                    try {
                        annotatedMethods = MethodIntrospector.selectMethods(clazz,
                                new MethodIntrospector.MetadataLookup<XxlJob>() {
                                    @Override
                                    public XxlJob inspect(Method method) {
                                        return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class);
                                    }
                                });
                    } catch (Throwable ex) {
                    }
                    // 3.2 生成并注冊方法的JobHander
                    for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
                        Method executeMethod = methodXxlJobEntry.getKey();
                        // 獲取jobHander和Cron
                        XxlJobCron xxlJobCron = executeMethod.getAnnotation(XxlJobCron.class);
                        if(xxlJobCron == null){
                            throw new CustomException("500", executeMethod.getName() + "(),沒有添加@XxlJobCron注解配置定時策略");
                        }
                        if (!CronExpression.isValidExpression(xxlJobCron.value())) {
                            throw new CustomException("500", executeMethod.getName() + "(),@XxlJobCron參數內容錯誤");
                        }
                        XxlJob xxlJob = methodXxlJobEntry.getValue();
                        jobPar.put(xxlJob.value(), xxlJobCron.value());
                        if (isRegistXxlJob) {
                            executeMethod.setAccessible(true);
                            // regist
                            Method initMethod = null;
                            Method destroyMethod = null;
                            xxlJobExecutor.registJobHandler(xxlJob.value(), new CustomerMethodJobHandler(clazz, executeMethod, initMethod, destroyMethod));
                        }
                    }

                }
                // spring bean實際注冊
                initBeanClass.forEach(beanFactory::getBean);
            } catch (IOException e) {
                logger.error("讀取{} 文件異常", fileName);
                e.printStackTrace();
                throw new RuntimeException("讀取jar文件異常: " + fileName);
            }
        }
    }

    以下是判斷該類是否有spring注解的工具類

    apublic class SpringAnnotationUtils {

        private static Logger logger = LoggerFactory.getLogger(SpringAnnotationUtils.class);
        /**
         * 判斷一個類是否有 Spring 核心注解
         *
         * @param clazz 要檢查的類
         * @return true 如果該類上添加了相應的 Spring 注解;否則返回 false
         */

        public static boolean hasSpringAnnotation(Class<?> clazz) {
            if (clazz == null) {
                return false;
            }
            //是否是接口
            if (clazz.isInterface()) {
                return false;
            }
            //是否是抽象類
            if (Modifier.isAbstract(clazz.getModifiers())) {
                return false;
            }

            try {
                if (clazz.getAnnotation(Component.class) !null ||
                clazz.getAnnotation(Repository.class) !null ||
                clazz.getAnnotation(Service.class) !null ||
                clazz.getAnnotation(Controller.class) !null ||
                clazz.getAnnotation(Configuration.class) !null) {
                    return true;
                }
            }catch (Exception e){
                logger.error("出現異常:{}",e.getMessage());
            }
            return false;
        }
    }

    注冊xxljob執行器的操作是仿照的xxljob中的XxlJobSpringExecutor的注冊方法。

    3、動態卸載

    動態卸載的過程,就是將動態加載的代碼,從內存,spring以及xxljob中移除。

    代碼如下:

    /**
     * 動態卸載指定路徑下指定jar包
     * @param fileName
     * @return map<jobHander, Cron> 創建xxljob任務時需要的參數配置
     */

    public void unloadJar(String fileName) throws IllegalAccessException, NoSuchFieldException {
        // 獲取加載當前jar的類加載器
        MyClassLoader myClassLoader = myClassLoaderCenter.get(fileName);

        // 獲取jobHandlerRepository私有屬性,為了卸載xxljob任務
        Field privateField = XxlJobExecutor.class.getDeclaredField("jobHandlerRepository");
        // 設置私有屬性可訪問
        privateField.setAccessible(true);
        // 獲取私有屬性的值jobHandlerRepository
        XxlJobExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        Map<String, IJobHandler> jobHandlerRepository = (ConcurrentHashMap<String, IJobHandler>) privateField.get(xxlJobSpringExecutor);
        // 獲取beanFactory,準備從spring中卸載
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
        Map<String, Class<?>> loadedClasses = myClassLoader.getLoadedClasses();

        Set<String> beanNames = new HashSet<>();
        for (Map.Entry<String, Class<?>> entry: loadedClasses.entrySet()) {
            // 1. 將xxljob任務從xxljob執行器中移除
            // 1.1 截取beanName
            String key = entry.getKey();
            String packageName = key.substring(0, key.lastIndexOf(".") + 1);
            String beanName = key.substring(key.lastIndexOf(".") + 1);
            beanName = packageName + beanName.substring(01).toLowerCase() + beanName.substring(1);

            // 獲取bean,如果獲取失敗,表名這個類沒有加到spring容器中,則跳出本次循環
            Object bean = null;
            try{
                bean = applicationContext.getBean(beanName);
            }catch (Exception e){
                // 異常說明spring中沒有這個bean
                continue;
            }

            // 1.2 過濾方法
            Map<Method, XxlJob> annotatedMethods = null;
            try {
                annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(),
                        new MethodIntrospector.MetadataLookup<XxlJob>() {
                            @Override
                            public XxlJob inspect(Method method) {
                                return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class);
                            }
                        });
            } catch (Throwable ex) {
            }
            // 1.3 將job從執行器中移除
            for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
                XxlJob xxlJob = methodXxlJobEntry.getValue();
                jobHandlerRepository.remove(xxlJob.value());
            }
            // 2.0從spring中移除,這里的移除是僅僅移除的bean,并未移除bean定義
            beanNames.add(beanName);
            beanFactory.destroyBean(beanName, bean);
        }
        // 移除bean定義
        Field mergedBeanDefinitions = beanFactory.getClass()
                .getSuperclass()
                .getSuperclass().getDeclaredField("mergedBeanDefinitions");
        mergedBeanDefinitions.setAccessible(true);
        Map<String, RootBeanDefinition> rootBeanDefinitionMap = ((Map<String, RootBeanDefinition>) mergedBeanDefinitions.get(beanFactory));
        for (String beanName : beanNames) {
            beanFactory.removeBeanDefinition(beanName);
            // 父類bean定義去除
            rootBeanDefinitionMap.remove(beanName);
        }

        // 卸載父任務,子任務已經在循環中卸載
        jobHandlerRepository.remove(fileName);
        // 3.2 從類加載中移除
        try {
            // 從類加載器底層的classes中移除連接
            Field field = ClassLoader.class.getDeclaredField("classes");
            field.setAccessible(true);
            Vector<Class<?>> classes = (Vector<Class<?>>) field.get(myClassLoader);
            classes.removeAllElements();
            // 移除類加載器的引用
            myClassLoaderCenter.remove(fileName);
            // 卸載類加載器
            myClassLoader.unload();
        } catch (NoSuchFieldException e) {
            logger.error("動態卸載的類,從類加載器中卸載失敗");
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            logger.error("動態卸載的類,從類加載器中卸載失敗");
            e.printStackTrace();
        }
        logger.error("{} 動態卸載成功", fileName);

    }

    4、動態配置

    使用動態加載時,為了避免服務重新啟動后丟失已加載的任務包,使用動態配置的方式,加載后動態更新初始化加載配置。

    以下提供了兩種自己實際操作過的配置方式。

    4.1 動態修改本地yml

    動態修改本地yml配置文件,需要添加snakeyaml的依賴

    4.1.1 依賴引入

    <dependency>
     <groupId>org.yaml</groupId>
        <artifactId>snakeyaml</artifactId>
        <version>1.29</version>
    </dependency>

    4.1.2 工具類

    讀取指定路徑下的配置文件,并進行修改。

    /**
     * 用于動態修改bootstrap.yml配置文件
     * @author lijianyu
     * @date 2023/04/18 17:57
     **/

    @Component
    public class ConfigUpdater {

        public void updateLoadJars(List<String> jarNames) throws IOException {
            // 讀取bootstrap.yml
            Yaml yaml = new Yaml();
            InputStream inputStream = new FileInputStream(new File("src/main/resources/bootstrap.yml"));
            Map<String, Object> obj = yaml.load(inputStream);
            inputStream.close();

            obj.put("loadjars", jarNames);

            // 修改
            FileWriter writer = new FileWriter(new File("src/main/resources/bootstrap.yml"));
            DumperOptions options = new DumperOptions();
            options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
            options.setPrettyFlow(true);
            Yaml yamlWriter = new Yaml(options);
            yamlWriter.dump(obj, writer);
        }
    }

    4.2 動態修改nacos配置

    Spring Cloud Alibaba Nacos組件完全支持在運行時通過代碼動態修改配置,還提供了一些API供開發者在代碼里面實現動態修改配置。在每次動態加載或卸載數據治理任務jar包時,執行成功后都會進行動態更新nacos配置。

    @Configuration
    public class NacosConfig {
        @Value("${spring.cloud.nacos.server-addr}")
        private String serverAddr;

        @Value("${spring.cloud.nacos.config.namespace}")
        private String namespace;

        public ConfigService configService() throws NacosException {
            Properties properties = new Properties();
            properties.put("serverAddr", serverAddr);
            properties.put("namespace", namespace);
            return NacosFactory.createConfigService(properties);
        }
    }
    /**
     * nacos配置中,修改sjzl-loadjars.yml
     *
     * @author lijianyu
     * @date 2023/04/19 17:59
     **/

    @Component
    public class NacosConfigUtil {

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

        @Autowired
        private NacosConfig nacosConfig;

        private String dataId = "sjzl-loadjars.yml";

        @Value("${spring.cloud.nacos.config.group}")
        private String group;

        /**
         * 從nacos配置文件中,添加初始化jar包配置
         * @param jarName 要移除的jar包名
         * @throws Exception
         */

        public void addJarName(String jarName) throws Exception {
            ConfigService configService = nacosConfig.configService();
            String content = configService.getConfig(dataId, group, 5000);
            // 修改配置文件內容
            YAMLMapper yamlMapper = new YAMLMapper();
            ObjectMapper jsonMapper = new ObjectMapper();
            Object yamlObject = yamlMapper.readValue(content, Object.class);

            String jsonString = jsonMapper.writeValueAsString(yamlObject);
            JSONObject jsonObject = JSONObject.parseObject(jsonString);
            List<String> loadjars;
            if (jsonObject.containsKey("loadjars")) {
                loadjars = (List<String>) jsonObject.get("loadjars");
            }else{
                loadjars = new ArrayList<>();
            }
            if (!loadjars.contains(jarName)) {
                loadjars.add(jarName);
            }
            jsonObject.put("loadjars" , loadjars);

            Object yaml = yamlMapper.readValue(jsonMapper.writeValueAsString(jsonObject), Object.class);
            String newYamlString = yamlMapper.writeValueAsString(yaml);
            boolean b = configService.publishConfig(dataId, group, newYamlString);

            if(b){
                logger.info("nacos配置更新成功");
            }else{
                logger.info("nacos配置更新失敗");
            }
        }
    }

    三、分離打包

    分離打包時,根據實際情況在pom.xml中修改以下配置

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <filters>
                                <filter>
                                    <artifact>*:*</artifact>
                                    <includes>
                                        <include>com/jy/job/demo/**</include>
                                    </includes>
                                </filter>
                            </filters>
                            <finalName>demoJob</finalName>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    END

    【福利】2023 高薪課程,全面來襲(視頻+筆記+源碼)


    PS:防止找不到本篇文章,可以收藏點贊,方便翻閱查找哦。

      本站是提供個人知識管理的網絡存儲空間,所有內容均由用戶發布,不代表本站觀點。請注意甄別內容中的聯系方式、誘導購買等信息,謹防詐騙。如發現有害或侵權內容,請點擊一鍵舉報。
      轉藏 分享 獻花(0

      0條評論

      發表

      請遵守用戶 評論公約

      類似文章 更多

      主站蜘蛛池模板: 男女高潮喷水在线观看| 艳女性享受在线观看| 日韩在线成年视频人网站观看| 一本大道中文日本香蕉| 中出人妻中文字幕无码| 亚洲欧洲日韩精品在线| 麻花传媒剧国产MV免费播放| 亚洲精品无码国产片| 麻豆国产成人AV在线播放| 欧美一进一出抽搐大尺度视频| 亚洲日韩精品无码一区二区三区| 一本一本久久A久久精品综合不卡 一区二区国产高清视频在线 | 欧美老熟妇XB水多毛多| 无套内谢少妇一二三四| 久久经精品久久精品免费观看| 久久99国产精品尤物| 国产午夜成人无码免费看| 亚洲大尺度无码专区尤物| 亚洲乳大丰满中文字幕| 亚洲精品一区二区麻豆| 中文无码久久精品| 亚洲精品中文av在线| 久久久久99精品国产片| 中文字幕亚洲制服在线看| 波多野结衣AV一区二区全免费观看| 国产线播放免费人成视频播放| 日本不卡一区二区三区| 日本高清在线天码一区播放| 亚洲高清国产拍精品青青草原| 国产精品福利在线观看无码卡一| 精品一区二区中文字幕| 亚洲AV中文无码乱人伦| 国色天香成人一区二区| a级黑人大硬长爽猛出猛进| 亚洲综合小说另类图片五月天| 国产熟睡乱子伦视频在线播放 | 亚洲成年轻人电影网站WWW | 97人人超碰国产精品最新O| 国语自产拍精品香蕉在线播放| 中文字幕国产精品自拍| 狠狠色噜噜狠狠狠7777奇米|