情景
日常的企业级开发,开发一个项目往往需要配置多份的配置文件,比如开发需要一套、测试阶段需要一套、正式上线又有一套(比如JDBC连接的数据库,在不同环境肯定连接不同的库),但是maven给我们提供了filter功能可以很便捷地实现,关于maven的filter功能,不是重点,也实在太简单,需要的自行百度。。。
这里简单说明一下多filter的情况:
-
这里是多个filter配置文件,每个文件配置了对应环境下的jdbc连接配置:
-
这里是项目配置文件,用${}的方式引用filter文件中的相应配置
-
接下来是maven配置文件
- 这样,maven在打包时,只需要指定相应的
mvn clean install -Ptest
这样的参数,就能直接指定对应的环境配置参数替换项目配置文件中的${}参数,达到动态替换配置的目的。
局限性
-
-Ptest
参数是在maven的compile
生命周期时触发的,它会将对应环境的filter文件中参数替换掉项目resources目录下的.xml
或者.properties
配置文件中的${}包括的对应参数,只要你运行的代码触发可maven的compile周期,项目资源文件就能被正确替换,程序正确执行进行 -
但是!!!如果你只是简单地进行单元测试,如下所示:
当你在findById
上直接右键单元测试时,由于并没有触发maven的compile
周期,导致common.properties
文件中的${jdbc.url}
系列的参数没有被替换,可想而知,这样的单元测试注定无法正确执行的。
解决方案
- 临时把
common.properties
中的jdbc配置替换成当前所需环境的相应参数,等之后要部署上线记得回来重新把它改回去。 - 手动修改项目
target/classes
目录下的common.properties
,这样的好处是,项目的源文件不会被修改,但是项目每次重新打包都得手动替换一次。
- 以上两种方案都不够优雅,有没有一劳永逸的方案呢?网上找了很多,没发现这方面的相应对策,然后我采取的方案是:右键单元测试的时候,在单元测试启动前插入一段代码,对target/classes中相应的资源文件进行替换,这样,在单元测试执行时加载的资源文件就是替换完成的了,程序正确执行~
- 那如何在单元测试前插入代码呢?单元测试的启动是IDE有相应实现的(因为在idea中右键就直接启动了单元测试),但是天无绝人之路啊,我发现:
@RunWith(SpringJUnit4ClassRunner.class)
这个怎么看都像是单元测试的启动类,于是乎点开了它的源码,发现其中有两个带有before字样的method,如下:
显而易见,应该是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类进行单元测试即可,它就会在单元测试启动前期替换资源文件中的参数,再执行你的单元测试逻辑,如下:
结语
哈哈哈,代码本身很简单,这边有意思的是在@RunWith
这边插入预处理代码,就是提供一个思路,主要是网上居然没有这方面的解决方案,如果有大神有更好的思路,不妨来交流交流~