maven多套环境配置文件和junit冲突问题解决方案

情景

日常的企业级开发,开发一个项目往往需要配置多份的配置文件,比如开发需要一套、测试阶段需要一套、正式上线又有一套(比如JDBC连接的数据库,在不同环境肯定连接不同的库),但是maven给我们提供了filter功能可以很便捷地实现,关于maven的filter功能,不是重点,也实在太简单,需要的自行百度。。。

这里简单说明一下多filter的情况:

  1. 这里是多个filter配置文件,每个文件配置了对应环境下的jdbc连接配置:


    maven-filters.png
  2. 这里是项目配置文件,用${}的方式引用filter文件中的相应配置


    common.png
  3. 接下来是maven配置文件


    pom.png

    profiles.png
  • 这样,maven在打包时,只需要指定相应的mvn clean install -Ptest这样的参数,就能直接指定对应的环境配置参数替换项目配置文件中的${}参数,达到动态替换配置的目的。

局限性

  • -Ptest参数是在maven的compile生命周期时触发的,它会将对应环境的filter文件中参数替换掉项目resources目录下的.xml或者.properties配置文件中的${}包括的对应参数,只要你运行的代码触发可maven的compile周期,项目资源文件就能被正确替换,程序正确执行进行
  • 但是!!!如果你只是简单地进行单元测试,如下所示:


    spring-junit.png

当你在findById上直接右键单元测试时,由于并没有触发maven的compile周期,导致common.properties文件中的${jdbc.url}系列的参数没有被替换,可想而知,这样的单元测试注定无法正确执行的。

解决方案

  1. 临时把common.properties中的jdbc配置替换成当前所需环境的相应参数,等之后要部署上线记得回来重新把它改回去。
  2. 手动修改项目target/classes目录下的common.properties,这样的好处是,项目的源文件不会被修改,但是项目每次重新打包都得手动替换一次。
  • 以上两种方案都不够优雅,有没有一劳永逸的方案呢?网上找了很多,没发现这方面的相应对策,然后我采取的方案是:右键单元测试的时候,在单元测试启动前插入一段代码,对target/classes中相应的资源文件进行替换,这样,在单元测试执行时加载的资源文件就是替换完成的了,程序正确执行~
  • 那如何在单元测试前插入代码呢?单元测试的启动是IDE有相应实现的(因为在idea中右键就直接启动了单元测试),但是天无绝人之路啊,我发现:@RunWith(SpringJUnit4ClassRunner.class)这个怎么看都像是单元测试的启动类,于是乎点开了它的源码,发现其中有两个带有before字样的method,如下:
    before-test-method.png

    before-test-class.png

显而易见,应该是class之前执行参数替换喽~ 所以我就继承SpringJUnit4ClassRunner这个类,复写其withBeforeClasses方法,如下:

/**
 * 自定义SpringJUnit4ClassRunner,在Spring单元测试之前将maven的filter环境配置替换工程的配置文件,实现maven一键式单元测试的目的
 *
 * @author linyuqiang
 * @version 1.0.0 2017/3/14
 */
public class WDSpringJUnit4ClassRunner extends SpringJUnit4ClassRunner {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    public WDSpringJUnit4ClassRunner(Class<?> clazz) throws InitializationError {
        super(clazz);
    }

    @Override
    protected Statement withBeforeClasses(Statement statement) {
        ConfigFilesCatcher configFilesCatcher = new ConfigFilesCatcher(new WDTestConfiguration());
        //获取filter文件
        File filterFile = configFilesCatcher.getFilterFile();
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(filterFile);
            Properties properties = new Properties();
            //filter中的键值对
            properties.load(fis);

            //遍历类路径下的所有配置文件
            List<File> classpathFiles = configFilesCatcher.getClasspathFiles();
            for (File file : classpathFiles) {
                FileInputStream is = null;
                FileOutputStream fos = null;
                try {
                    is = new FileInputStream(file);
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    IOUtils.copy(is, baos);
                    //将该配置文件中含有filter中的元素子串替换
                    String content = new String(baos.toByteArray());
                    for (Map.Entry<Object, Object> entry : properties.entrySet()) {
                        String key = "${" + entry.getKey() + "}";
                        if (content.contains(key)) {
                            content = content.replace(key, (CharSequence) entry.getValue());
                        }
                    }
                    //配置文件替换完毕回写
                    fos = new FileOutputStream(file);
                    fos.write(content.getBytes());
                    logger.info("{}文件配置替换成功", file.getName());
                } catch (IOException e) {
                    logger.warn("{} 文件的filter替换失败", file.getName());
                    //不影响下一个文件
                    continue;
                } finally {
                    closeIs(is);
                    closeOs(fos);
                }
            }
        } catch (FileNotFoundException e) {
            logger.warn("filter文件不存在", e);
            throw new RuntimeException("filter文件不存在",e);
        } catch (IOException e) {
            logger.warn("filter文件加载失败", e);
            throw new RuntimeException("filter文件加载失败",e);
        } finally {
            closeIs(fis);
        }
        logger.info("filter文件配置替换完毕");
        //执行SpringJUnit4ClassRunner原本逻辑
        return super.withBeforeClasses(statement);
    }

    private void closeIs(InputStream is) {
        if (is != null) {
            try {
                is.close();
                is = null;
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private void closeOs(OutputStream os) {
        if (os != null) {
            try {
                os.close();
                os = null;
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

然后这是参数替换的逻辑:

/**
 * 单元测试替换文件列表获取器
 *
 * @author linyuqiang
 * @version 1.0.0 2017/3/14
 */
public class ConfigFilesCatcher {

    private Logger logger = LoggerFactory.getLogger(this.getClass());
    private WDTestConfiguration configuration;
    private String projectPath;//项目绝对路径

    public ConfigFilesCatcher(WDTestConfiguration configuration) {
        this.configuration = configuration;
        this.projectPath = getProjectPath();
        logger.info("文件获取器初始化成功,项目绝对路径为:{}", this.projectPath);
    }

    /**
     * 获取filter文件
     *
     * @return
     */
    public File getFilterFile() {
        File file = null;
        if (configuration.isAbsolute()) {
            file = new File(configuration.getFilterPath());
        } else {
            file = new File(projectPath + "/src/main/filters/" + configuration.getFilterFile());
        }
        logger.info("filter文件获取成功:{}", file.getAbsolutePath());
        return file;
    }

    private String getProjectPath() {
        File file = new File(this.getClass().getClassLoader().getResource("").getFile());
        while (file != null && file.getParentFile() != null && !file.getName().equals("target")) {
            file = file.getParentFile();
            if (file.getName() != null && file.getName().equals("target")) {
                file = file.getParentFile();
                break;
            }
        }
        logger.info("项目绝对路径获取成功");
        return FilePathDecodeUtil.pathDecode(file.getAbsolutePath());
    }

    /**
     * 获取类路径下的所有配置文件列表
     *
     * @return
     */
    public List<File> getClasspathFiles() {
        String classes = projectPath + "/target/classes";
        String testClasses = projectPath + "/target/test-classes";
        List<File> classesFiles = new ArrayList<>();
        findFiles(new File(classes), classesFiles);
        findFiles(new File(testClasses), classesFiles);
        logger.info("类路径下配置文件列表获取成功:{}", classesFiles);
        return classesFiles;
    }

    //递归遍历类路径下的所有class外的配置文件
    private void findFiles(File file, List<File> files) {
        if (file.isDirectory()) {
            File[] fs = file.listFiles();
            for (File f : fs) {
                findFiles(f, files);
            }
        } else {
            if (file.getName().endsWith(".xml") || file.getName().endsWith(".properties")) {
                files.add(file);
            }
        }
    }
}

代码本身很简单,读取filters文件夹下的对应环境filter文件,对target目录下的资源文件(.xml.properties)进行单纯地字符串正则替换的逻辑。
剩下的就是直接使用该Runner类进行单元测试即可,它就会在单元测试启动前期替换资源文件中的参数,再执行你的单元测试逻辑,如下:

junit-wdspring.png

结语

哈哈哈,代码本身很简单,这边有意思的是在@RunWith这边插入预处理代码,就是提供一个思路,主要是网上居然没有这方面的解决方案,如果有大神有更好的思路,不妨来交流交流~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容