Runtime.getRuntime().exec执行jar包修改配置文件并替换问题记录

微服务结构:子工程依赖父工程,父工程向子工程提供统一配置及公用类,多个子工程依赖同一个父工程。

角色定位:这个父工程,是整个应用系统的基础设施。这个基础设施,不是简单的抽取微服务的配置等,还包括了对第三方的组件集成与优化,减少子工程的配置文件,最好达到子工程只需要提供多种环境的yml文件即可而不需要额外的配置文件等。

结构依赖图示:

为了达到减少子工程的配置文件目的,把子工程所需要的scm.properties配置文件移到父工程中。

这带来了一个问题,每个子工程的scm.properties文件都是不同的,抽取到父工程时,必然涉及到子工程修改父工程的配置文件,但是,父工程现在已经作为一个jar包已经依赖到子工程中去。通过`Properties`修改的方式并不能达到如期目标,因为jar包中的文件已经是只读的,生成新的`scm.properties`文件是在子工程的目录下。

换另一种思路,就是在系统启动时,紧接着将子工程的`scm.properties`文件通过`jar uvf 带绝对路径的jar包 -C 带绝对路径的子包目录 scm.properties`命令替换父工程的scm.properties文件。

问题一:在本微服务应用中,通过`Runtime.getRuntime().exec`调用上述`jar`命令无效

原因:`Runtime.getRuntime().exec(jar)`的执行与本微服务应用是在通过一个jvm环境中。会看到虽然jar命令执行成功已经被输出到日志中,但实际应用还是启动不起来。原因就是该jar包已经被加载到jvm中。验证是这一原因的方式:把jar命令语句中的带绝对路径的jar包改成不带路径的jar包,系统仍然由读取到scm.properties中的值不对,而报错启动不起来,且输出日志中,也看到jar执行成功的v命令参数信息`正在添加: scm.properties(输入 = 157) (输出 = 145)(压缩了 7%)`。

解决方式:将`Runtime执行jar命令`的调用放到一个独立于本微服务应用中的Java类中,如:

/**
 * 以main方法独立执行系统调用命令
 *
 *
 * @author: 18109115
 * @since: 1.0
 * @see [相关类/方法](可选)
 */

public class ReplaceInstructionJar {

    public static void main(String[] args) {
        String jarName = args[0];
        String directory = args[1];
        System.out.println(" input param " + jarName + " , " + directory);
        String cmd = "jar uvf " + jarName + " -C " + directory + " scm.properties";
        String[] cmds = {"cmd", "/c", cmd};
        try {
            Process process = Runtime.getRuntime().exec(cmds);

            InputStream stream = process.getInputStream();
            InputStreamReader isr = new InputStreamReader(stream, "GBK");
            BufferedReader br = new BufferedReader(isr);
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
            process.waitFor();
            System.out.println(" end jar command. ");
        } catch (Exception e) {
            System.out.println(e);
        }

    }
}

然后在原来调用jar命令的地方改成`Runtime调用执行java命令,独立运行main方法的方式执行jar包修改并更新配置文件 `的方式(这个java文件必须是已经javac过的,我测试时,是用的断点设置,然后再另一个项目中main启动方式,道理是一样的,使用java命令的时候,别忘记`-classpath `指定该java文件所在的路径):

这种解决,会报错:

java.io.IOException: 写入现有的 jar 文件时出错
	at sun.tools.jar.Main.run(Main.java:286)
	at sun.tools.jar.Main.main(Main.java:1288)

因为,在jar包已经被当前应用加载了,不能再去修改。

第二种解决方式:

实现思路:

  1. 在父工程中,用Windows/Linux 文件复制命令,将父工程的jar包,复制到java.io.tmpdir目录下。
  2. 在父工程中,接着用jar包替换命令,将新生成的scm.properties用jar命令同第一种方式一样,实现jar包文件的替换。(这个不用担心,第一种方案的报错,因为,是新复制的jar包,不在当前jvm运行环境内)
  3. 在父工程中,用Windows/Linux文件替换命令,将java.io.tmpdir目录下的jar包替换掉应用下的jar包
  4. 用 Properties filePropertiesAfterUpdate = PropertiesLoaderUtils.loadAllProperties(SCM_CONFIG_PATH);去获取替换后的jar包的scm.properties文件打印,发现已替换

上述123步骤中,都是通过Runtime.getRuntime().exec(cmd)来调用系统环境的命令。

题外话:

我这样实现后,应用第一次启动,报错:Caused by: java.util.zip.ZipException: invalid distance too far back

原因:我司的统一配置中间件,底层实现时,用的是java的Properties的load(stream)方法。关键是,在获取输入流的时候,是通过:

           InputStream inputStream = SCMConfiguration.class.getResourceAsStream(configFile);

这种方式来实现的,相当于还是依赖了jvm的类加载系统,已经加载过的。

第二次启动应用时,正常启动。这里读取的流就是替换之后的,因为这个第二次应用启动,是重新加载到jvm中。

如果这里的实现,换成 PropertiesLoaderUtils.loadAllProperties(SCM_CONFIG_PATH);来获得Properties,就不会报错。(我这么推断的原因是,我在自动配置类中,做完jar包替换的工作以后,接着用该方法来获取配置文件并打印,从日志中看到是修改后的值。)

为什么两者方式会不同?

因为Spring提供的获取属性值的方法,当不传指定类加载器时,采用当前线程的类加载器【Thread.currentThread().getContextClassLoader();】。在SpringBoot应用中,此时的类加载器时RestartClassLoader,如果是没应用spring-boot-devtools这种方式,也有作用。此时当前的类加载器AppClassLoader,通过其getResources,按照双亲委托模型,去父类加载器ExtClassLoader去加载修改后的scm.properties文件。

而我司那种使用方式,此时的类加载器是jdk提供的AppClassLoader应用类加载器,直接通过其SCMConfiguration.class.getResourceAsStream()方式即加载当前classpath下的jar包。

核心代码如下:

/**
     * 通过 ScmProperties 去操作修改父工程的scm.properties 文件(已经打包后的)
     *
     */
    @Override
    public void afterPropertiesSet() throws Exception {

        // 这种是dev环境的初始化,用自带web服务器 内容为*的原始文件
        Properties fileProperties = PropertiesLoaderUtils.loadAllProperties(SCM_CONFIG_PATH);
        log.info(" from yml, scm.properties: appCode = {}", fileProperties.getProperty("appCode"));

        fileProperties.setProperty("appCode", scmProperties.getAppCode());
        fileProperties.setProperty("zkServer", scmProperties.getZkServer());
        fileProperties.setProperty("scmServer", scmProperties.getScmServer());
        fileProperties.setProperty("secretKey", scmProperties.getSecretKey());

        // 根据properties,生成新的 scm.properties文件
        File file = new File("" + SCM_CONFIG_PATH);

        try (FileWriter fis = new FileWriter(file)) {
            fileProperties.store(fis,null);
        } catch (Exception e) {
            log.error(" update scm.properties, occur ex ", e);
        }

        log.info("newFile's path is {}", file.getAbsolutePath());
        String newFilePath = file.getAbsolutePath();
        String directory = newFilePath.substring(0, newFilePath.lastIndexOf(File.separator));
        log.info("directory {}", directory);

        // 获取jvm当前jar的加载路径
        String path = System.getProperty("java.class.path");

        log.info(" jar/war path is {}", path);
        String[] jarPaths = path.split(";");

        String jarName = Arrays.stream(jarPaths).filter(
                jarPath -> jarPath.contains("spring-boot-starter-jwms-instruction")
        ).findAny().get();

        log.info(" jwms instruction jar/war is {}", jarName);

        // 将基础设施中的jar包复制到临时目录 java.io.tmpdir
        String tmpdir  = System.getProperty("java.io.tmpdir");

        String copyJarPackage = " copy " + jarName + " " + tmpdir;

        String[] copyCmd = new String[] {"cmd", "/c", copyJarPackage};

        Process copyProcess = Runtime.getRuntime().exec(copyCmd);

        InputStream errStream = copyProcess.getErrorStream();
        InputStreamReader errReader = new InputStreamReader(errStream, "GBK");
        BufferedReader errBuff = new BufferedReader(errReader);
        String errLine;
        while ((errLine = errBuff.readLine()) != null) {
            log.error(errLine);
        }

        InputStream inStream = copyProcess.getInputStream();
        InputStreamReader inReader = new InputStreamReader(inStream, "GBK");
        BufferedReader inBuff = new BufferedReader(inReader);
        String inLine;
        while ((inLine = inBuff.readLine()) != null) {
            log.info(inLine);
        }

        copyProcess.waitFor();

        log.info(" copy, file is {}", "");
        
        // 用 java的 jar uvf命令将新生成的scm.properties文件打包到tmpdir目录下的jar包中
        log.info(" start replace jar by using jar command. ");

        int start = jarName.lastIndexOf(File.separator) + 1;
        String tmpJarName = jarName.substring(start);
        String jarAbsoluteDirectory = jarName.substring(0, start - 1);

        // 改成动态jar包名
        String replaceCmd = "jar uvf " + tmpdir + File.separator +

                tmpJarName + " -C " + directory + " scm.properties";

        String[] replaceJarCmd = new String[] {"cmd", "/c", replaceCmd};

        Process replacePross = Runtime.getRuntime().exec(replaceJarCmd);

        InputStream errStreamForRepl = replacePross.getErrorStream();
        InputStreamReader errReaderForRepl = new InputStreamReader(errStreamForRepl, "GBK");
        BufferedReader errBuffForRepl = new BufferedReader(errReaderForRepl);
        String errLineForRepl;
        while ((errLineForRepl = errBuffForRepl.readLine()) != null) {
            log.error(errLineForRepl);
        }

        InputStream inStreamForRepl = replacePross.getInputStream();
        InputStreamReader inReaderForRepl = new InputStreamReader(inStreamForRepl, "GBK");
        BufferedReader inBuffForRepl = new BufferedReader(inReaderForRepl);
        String inLineForRepl;
        while ((inLineForRepl = inBuffForRepl.readLine()) != null) {
            log.info(inLineForRepl);
        }

        // 用cmd命令,删除原来的jar包,替换成 tmpdir 目录下的jar包
        log.info(" start to replace jar by cmd command. ");
        String replaceFileUseCmd = " replace " + tmpdir + File.separator +

                tmpJarName + " " + jarAbsoluteDirectory;
        String[] exeCmds = new String[] {"cmd", "/c", replaceFileUseCmd};

        Process replFilePross = Runtime.getRuntime().exec(exeCmds);
        InputStream errStreamForReplFile = replFilePross.getErrorStream();
        InputStreamReader errReaderForReplFile = new InputStreamReader(errStreamForReplFile, "GBK");
        BufferedReader errBuffForReplFile = new BufferedReader(errReaderForReplFile);
        String errLineForReplFile;
        while ((errLineForReplFile = errBuffForReplFile.readLine()) != null) {
            log.error(errLineForReplFile);
        }

        InputStream inStreamForReplFile = replFilePross.getInputStream();
        InputStreamReader inReaderForReplFile = new InputStreamReader(inStreamForReplFile, "GBK");
        BufferedReader inBuffForReplFile = new BufferedReader(inReaderForReplFile);
        String inLineForReplFile;
        while ((inLineForReplFile = inBuffForReplFile.readLine()) != null) {
            log.info(inLineForReplFile);
        }
        
        // 获取基础设施,修改后的scm.properties文件
        Properties filePropertiesAfterUpdate = PropertiesLoaderUtils.loadAllProperties(SCM_CONFIG_PATH);
        String zkServerNew = filePropertiesAfterUpdate.getProperty("zkServer");
        log.info(" after update scm.properties by yml, zkServerNew={}", zkServerNew);

        // 向jvm中重新加载jar包【想解决第一次启动报错的问题,上述修改方式导致的加载问题】

    }

总结:

其实,最简单的方式就是在子工程添加scm.properties文件,这样我司中间件ShardRedis就不会报错,我司中间件ShardRedis使用时是强依赖scm.properties的。

最后解决方式是,在子工程中新增scm.properties配置文件。

想减少子工程的配置文件数目,用第二种方式比较好。但是第一次启动会报错,因为我司中间件获取Properties的输入流Stream时,是通过类加载的getResourceAsStream方法而不是getResources方法,这两个方法的区别就是,一个就是通过本类加载器直接获取资源,另一个就是按照双亲委派模型来获取资源。除非重新加载jar包。(后续持续优化这个思路。)

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值