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

2,312 阅读6分钟
原文链接: thomaslau.xyz

现象和问题:
写了个 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的几种方式:

<!--配置默认激活-->
<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>

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

<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 命令试试:

-> 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,这是一个技巧,代码:

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)

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是支持系统变量方式的:

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 实现原理。

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里已有的注释掉,现有注释部分打开,就解决该问题了。github.com/mozilla/web…
虽然写过一些js代码,但觉得js的这个功能很神奇。

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
}