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这边插入预处理代码,就是提供一个思路,主要是网上居然没有这方面的解决方案,如果有大神有更好的思路,不妨来交流交流~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容