Maven Profile的两个技巧和一个注意事项

现象和问题:
写了个 xx-service 的基于maven项目,借助spring配置文件可执行,现在有需求是希望将该项目以SDk方式打包到maven仓库以便使用其中某些service类(暂不论合理与否),如何通过maven profile实现在编辑器里不同开发人员默认用不同的 active profile?
实现不同的 active profile是希望双方在开发的时候,一个可以直接package出来 不含配置文件的纯 SDK 包,而我这边使用eclipse时可以默认生成需要的配置文件(local profile)到classpath下,免于命令行切换,提高开发/测试效率。

源项目通过Main函数启动,所以一种方案是定义spring.active.profile的方式,在eclipse配置参数方式启动,但是我不希望引入这种不纯粹的方式,过多依赖在一些零散的地方设置,就像我觉得使用lomb组件的不配谈代码优雅。
大部分了解maven的会想到active profile实现,但我也不想通过maven安装包下的 settings.xml中使用activeProfiles指定的方式,更不想通过两人部分代码不同的方式实现,即希望两人看到的代码一样。
最好能都在项目代码里如pom.xml/xx.properties(配置即是代码!)里设置搞定。
那么有什么好办法呢?
因为classpath生成是在编译之后,即maven编译之后的,所以不要妄想通过eclipse的配置 jvm run 参数,必定是在maven编译前或时(eclipse通过m2e和maven交互即时编译),而maven pom.xml没有支持 if-else这类表达式语言的,实际上maevn基于配置的理念并不能支持其如DSL一般灵活,gradle可以。m2eclipse只是扩展了maven集成到eclipse的能力,并未扩展功能。
如果pom指定了A profile为active的,没有理由在其他人机器上A profile不是active的,除非maven支持基于机器等设置profile。好在maven 对profile本身支持力度是勉强可以达到上述功能。

让我们先看看官方支持激活profile的几种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!--配置默认激活-->
<activeByDefault>true</activeByDefault>
<!--通过jdk版本-->
<jdk>1.5</jdk>
<jdk>[1.5,)</jdk>
<!--根据当前操作系统-->
<os>
<name>Windows XP</name>
<family>Windows</family>
<arch>x86</arch>
<version>5.1.2600</version>
</os>
<!--通过系统环境变量-->
<property>
<name>env</name>
<value>test</value>
</property>
<!--通过文件的存在或缺失-->
<file>
<missing>target/generated-sources/axistools/wsdl2java/wdl</missing>
<exists/>
</file>

所以解决办法就是环境变量或者文件是否存在的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<profile>
<id>local</id>
<activation>
<property>
<name>user.name</name>
<value>thomas</value>
</property>
</activation>
<properties>
<log.path>./logs</log.path>
</properties>
<build>
<resources>
<resource>
<directory>${basedir}/src/main/resources</directory>
<filtering>true</filtering>
</resource>
<resource>
<directory>${basedir}/profiles/common</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
</profile>
<profile>
<id>sdk</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>*</exclude>
</excludes>
</resource>
</resources>
</build>
</profile>

该方式虽然也依赖一些配置,但好处是一目了然,看到代码即可明白。
通过上述配置,在我的eclipse上是classpath下包含配置的local模式可运行项目,而在对方机器就是sdk可打包项目,非常轻便了。
对方可通过 mvn deploy 正常的发布包,但是,当我使用 “mvn package deploy -Psdk” 时,却发现打出来的包中包含了配置文件!

为什么,指定了 sdk 的profile却发现配置文件被包含进来

怎么定位问题呢?使用 help:active-profiles 命令试试:

1
2
3
4
5
6
7
8
9
10
11
12
-> mvn -P sdk help:active-profiles
...
Active Profiles for Project 'org.thomas:justitia-service:jar:0.2.8-SNAPSHOT':
The following profiles are active:
- sonar (source: external)
- gdev (source: external)
- local (source: org.thomas:justitia-service:0.2.8-SNAPSHOT)
- sdk (source: org.thomas:justitia-service:0.2.8-SNAPSHOT)
- dev (source: org.thomas:justitia-parent:0.2.8-SNAPSHOT)
...

即,”mvn -P sdk”并不表示 仅使用 sdk 或parent/setting.xml里指定的active的profile打包,还包括任何能够匹配的profile!
即上面输出里看到的,默认生效的profile有 settings.xml定义的 sonar/gdev,还有本pom.xml里的 local/sdk 的profile,以及父类的默认 dev ,这是一个注意事项

怎么解决或者原因在哪呢?

其实 -P 参数支持简单的包含/排除运算,即 mvn package deploy -Plocal 就表示排除 local 这个active profile, !local 也表示排除, +local表示添加 local,这是一个技巧,代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
List<String> activeProfiles = new ArrayList<>();
List<String> inactiveProfiles = new ArrayList<>();
if ( commandLine.hasOption( CLIManager.ACTIVATE_PROFILES ) )
{
String[] profileOptionValues = commandLine.getOptionValues( CLIManager.ACTIVATE_PROFILES );
if ( profileOptionValues != null )
{
for ( String profileOptionValue : profileOptionValues )
{
StringTokenizer profileTokens = new StringTokenizer( profileOptionValue, "," );
while ( profileTokens.hasMoreTokens() )
{
String profileAction = profileTokens.nextToken().trim();
if ( profileAction.startsWith( "-" ) || profileAction.startsWith( "!" ) )
{
inactiveProfiles.add( profileAction.substring( 1 ) );
}
else if ( profileAction.startsWith( "+" ) )
{
activeProfiles.add( profileAction.substring( 1 ) );
}
else
{
activeProfiles.add( profileAction );
}
}
}
}
}

这是第二个技巧

但为什么 sonar/gdev/local/sdk/dev 这些profile都生效了?


org.apache.maven.cli.configuration.SettingsXmlConfigurationProcessor.populateFromSettings(MavenExecutionRequest, Settings)
这个方法有涉及,不过这里我们倒序追溯一下:

mvn 命令的入口都在 org.apache.maven.cli.MavenCli 类里面,MavenCli 封装配置build好一个MavenExecutionRequest 之后,最终调用DefaultMaven.doExecute(MavenExecutionRequest request)实现,也是所有maven项目的MavenExecutionRequest,Project/Module/Artifact等都是在此实现的,该方法会调用这个方法进行 Project 的初始化:
org.apache.maven.project.DefaultProjectBuilder.initProject(MavenProject, ModelBuildingResult, Map)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private void initProject( MavenProject project, ModelBuildingResult result, Map<File, Boolean> profilesXmls )
{
Model model = result.getEffectiveModel();
project.setModel( model );
project.setOriginalModel( result.getRawModel() );
project.setFile( model.getPomFile() );
File parentPomFile = result.getRawModel( result.getModelIds().get( 1 ) ).getPomFile();
project.setParentFile( parentPomFile );
Artifact projectArtifact =
repositorySystem.createArtifact( project.getGroupId(), project.getArtifactId(), project.getVersion(), null,
project.getPackaging() );
project.setArtifact( projectArtifact );
if ( project.getFile() != null )
{
Build build = project.getBuild();
project.addScriptSourceRoot( build.getScriptSourceDirectory() );
project.addCompileSourceRoot( build.getSourceDirectory() );
project.addTestCompileSourceRoot( build.getTestSourceDirectory() );
}
List<Profile> activeProfiles = new ArrayList<Profile>();
activeProfiles.addAll( result.getActivePomProfiles( result.getModelIds().get( 0 ) ) );
activeProfiles.addAll( result.getActiveExternalProfiles() );
project.setActiveProfiles( activeProfiles );
project.setInjectedProfileIds( "external", getProfileIds( result.getActiveExternalProfiles() ) );
for ( String modelId : result.getModelIds() )
{
project.setInjectedProfileIds( modelId, getProfileIds( result.getActivePomProfiles( modelId ) ) );
}
String modelId = findProfilesXml( result, profilesXmls );
if ( modelId != null )
{
ModelProblem problem =
new DefaultModelProblem( "Detected profiles.xml alongside " + modelId
+ ", this file is no longer supported and was ignored" + ", please use the settings.xml instead",
ModelProblem.Severity.WARNING, model, -1, -1, null );
result.getProblems().add( problem );
}
}

这几个方法就是分别从 pom/父项目/maven settings中获取active profile的,而profile 判定isActive是支持系统变量方式的:

1
2
3
4
activeProfiles.addAll( result.getActivePomProfiles( result.getModelIds().get( 0 ) ) );
activeProfiles.addAll( result.getActiveExternalProfiles() );
project.setActiveProfiles( activeProfiles );
project.setInjectedProfileIds( "external", getProfileIds( result.getActiveExternalProfiles() ) );

三类profile都会作为maven project的Active Profiles,这也是 help:active-profiles 实现原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private void getActiveProfileStatement( MavenProject project, StringBuilder message )
{
Map<String, List<String>> activeProfileIds = project.getInjectedProfileIds();
message.append( LS );
message.append( "Active Profiles for Project \'" ).append( project.getId() ).append( "\':" );
message.append( LS ).append( LS );
if ( activeProfileIds.isEmpty() )
{
message.append( "There are no active profiles." );
}
else
{
message.append( "The following profiles are active:" ).append( LS );
for ( Map.Entry<String, List<String>> entry : activeProfileIds.entrySet() )
{
for ( String profileId : entry.getValue() )
{
message.append( LS ).append( " - " ).append( profileId );
message.append( " (source: " ).append( entry.getKey() ).append( ")" );
}
}
}
message.append( LS );
}

其他

说两个不相关的问题:
1.
Mac 的sed命令不支持一些linux sed写法 众所周知,有时甚至导致一些莫名其妙运行结果,故建议避免使用,改为linux sed 即安装gsed:
brew install gnu-sed
不过不建议把gsed替换或直接别名为sed, 有些外部进程会调用sed。
2.
最近在写一个Chrome extension,发现下面写法返回空,要是把 sendResponse 放到chrome.storage.local.get的回调函数里就会报一个 类似:
“unchecked runtime lasterror the message port closed before a response”的错误,原因就是因为异步且没有作为回调函数的方式,但即便用Promise也是该情况,
解决办法是给函数加一个简单的 return true;即把下面else里已有的注释掉,现有注释部分打开,就解决该问题了。https://github.com/mozilla/webextension-polyfill/issues/130
虽然写过一些js代码,但觉得js的这个功能很神奇。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
chrome.runtime.onMessage.addListener(main.onMessageListener);
...
onMessageListener: function(message, sender, sendResponse){
if(message == 'tabs'){
sendResponse(main.queue.slice());
}else{
var msg;
chrome.storage.local.get(message, function(result) {
msg=result[message];
alert(result)
});
sendResponse(msg);
/*new Promise(function(resolve, reject){
chrome.storage.local.get(message, function(result) {
msg=result[message];
resolve(msg)
});
}).then((data)=> sendResponse(data));*/
// return true
}
// return true
}