【问题标题】:Using map of maps as Maven plugin parameters使用地图的地图作为 Maven 插件参数
【发布时间】:2016-12-02 09:00:43
【问题描述】:

是否可以使用地图的地图作为 Maven 插件参数?例如

@Parameter
private Map<String, Map<String, String>> converters;

然后像使用它一样

<converters>
  <json>
     <indent>true</indent>
     <strict>true</strict>
  </json>
  <yaml>
      <stripComments>false</stripComments>
  </yaml>
<converters>

如果我这样使用它,converters 只包含键 jsonyaml,其值为 null。

我知道可以将复杂的对象作为值,但是否也可以像本例那样将映射用于可变元素值?

【问题讨论】:

    标签: maven maven-plugin


    【解决方案1】:

    这显然是 Mojo API 内部使用的 sisu.plexus 项目的限制。如果您查看MapConverter 源代码,您会发现它首先尝试通过尝试将配置解释为字符串(调用fromExpression)来获取映射的值,当失败时,looks up the expected type of the value .但是这个方法不检查parameterized types,这是我们这里的例子(因为映射值的类型是Map&lt;String, String&gt;)。我在这个项目的 Bugzilla 上提交了the bug 498757 来跟踪这个。

    使用自定义包装对象

    一种解决方法是不使用 Map&lt;String, String&gt; 作为值,而是使用自定义对象:

    @Parameter
    private Map<String, Converter> converters;
    

    有一个类 Converter,与 Mojo 位于同一包中,是:

    public class Converter {
    
        @Parameter
        private Map<String, String> properties;
    
        @Override
        public String toString() { return properties.toString(); } // to test
    
    }
    

    然后您可以使用以下命令配置您的 Mojo:

    <converters>
      <json>
        <properties>
          <indent>true</indent>
          <strict>true</strict>
        </properties>
      </json>
      <yaml>
        <properties>
          <stripComments>false</stripComments>
        </properties>
      </yaml>
    </converters>
    

    此配置将正确注入内部映射中的值。它还保留了可变方面:对象仅作为内部映射的包装器引入。我用一个简单的测试魔咒测试了这个

    public void execute() throws MojoExecutionException, MojoFailureException {
        getLog().info(converters.toString());
    }
    

    输出是预期的{json={indent=true, strict=true}, yaml={stripComments=false}}

    使用自定义配置器

    我还找到了一种使用自定义ComponentConfigurator 来保留Map&lt;String, Map&lt;String, String&gt;&gt; 的方法。

    所以我们想通过继承来修复MapConverter,问题是如何注册这个新的FixedMapConverter。默认情况下,Maven 使用 BasicComponentConfigurator 来配置 Mojo,它依赖于 DefaultConverterLookup 来查找用于特定类的转换器。在这种情况下,我们希望为Map 提供一个自定义转换,它将返回我们的固定版本。因此,我们需要扩展这个基本配置器并注册我们的新转换器。

    import org.codehaus.plexus.classworlds.realm.ClassRealm;
    import org.codehaus.plexus.component.configurator.BasicComponentConfigurator;
    import org.codehaus.plexus.component.configurator.ComponentConfigurationException;
    import org.codehaus.plexus.component.configurator.ConfigurationListener;
    import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
    import org.codehaus.plexus.configuration.PlexusConfiguration;
    
    public class CustomBasicComponentConfigurator extends BasicComponentConfigurator {
        @Override
        public void configureComponent(final Object component, final PlexusConfiguration configuration,
                final ExpressionEvaluator evaluator, final ClassRealm realm, final ConfigurationListener listener)
                throws ComponentConfigurationException {
            converterLookup.registerConverter(new FixedMapConverter());
            super.configureComponent(component, configuration, evaluator, realm, listener);
        }
    }
    

    然后我们需要告诉 Maven 使用这个新的配置器而不是基本的配置器。这是一个两步过程:

    1. 在您的 Maven 插件中,创建一个文件 src/main/resources/META-INF/plexus/components.xml 注册新组件:

      <?xml version="1.0" encoding="UTF-8"?>
      <component-set>
        <components>
          <component>
            <role>org.codehaus.plexus.component.configurator.ComponentConfigurator</role>
            <role-hint>custom-basic</role-hint>
            <implementation>package.to.CustomBasicComponentConfigurator</implementation>
          </component>
        </components>
      </component-set>
      

      注意几点:我们声明了一个带有提示 "custom-basic" 的新组件,这将用作引用它的 id,&lt;implementation&gt; 引用我们配置器的完全限定类名。

    2. 通过@Mojo 注释的configurator 属性告诉我们的Mojo 使用此配置器:

      @Mojo(name = "test", configurator = "custom-basic")
      

      这里传递的configurator对应上面components.xml中指定的role-hint。

    有了这样的设置,你终于可以申报了

    @Parameter
    private Map<String, Map<String, String>> converters;
    

    一切都将被正确注入:Maven 将使用我们的自定义配置器,它将注册我们固定版本的地图转换器并正确转换内部地图。


    FixedMapConverter 的完整代码(几乎复制粘贴了MapConverter,因为我们无法覆盖错误的方法):

    public class FixedMapConverter extends MapConverter {
    
        public Object fromConfiguration(final ConverterLookup lookup, final PlexusConfiguration configuration,
                final Class<?> type, final Type[] typeArguments, final Class<?> enclosingType, final ClassLoader loader,
                final ExpressionEvaluator evaluator, final ConfigurationListener listener)
                throws ComponentConfigurationException {
            final Object value = fromExpression(configuration, evaluator, type);
            if (null != value) {
                return value;
            }
            try {
                final Map<Object, Object> map = instantiateMap(configuration, type, loader);
                final Class<?> elementType = findElementType(typeArguments);
                if (Object.class == elementType || String.class == elementType) {
                    for (int i = 0, size = configuration.getChildCount(); i < size; i++) {
                        final PlexusConfiguration element = configuration.getChild(i);
                        map.put(element.getName(), fromExpression(element, evaluator));
                    }
                    return map;
                }
                // handle maps with complex element types...
                final ConfigurationConverter converter = lookup.lookupConverterForType(elementType);
                for (int i = 0, size = configuration.getChildCount(); i < size; i++) {
                    Object elementValue;
                    final PlexusConfiguration element = configuration.getChild(i);
                    try {
                        elementValue = converter.fromConfiguration(lookup, element, elementType, enclosingType, //
                                loader, evaluator, listener);
                    }
                    // TEMP: remove when http://jira.codehaus.org/browse/MSHADE-168
                    // is fixed
                    catch (final ComponentConfigurationException e) {
                        elementValue = fromExpression(element, evaluator);
    
                        Logs.warn("Map in " + enclosingType + " declares value type as: {} but saw: {} at runtime",
                                elementType, null != elementValue ? elementValue.getClass() : null);
                    }
                    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                    map.put(element.getName(), elementValue);
                }
                return map;
            } catch (final ComponentConfigurationException e) {
                if (null == e.getFailedConfiguration()) {
                    e.setFailedConfiguration(configuration);
                }
                throw e;
            }
        }
    
        @SuppressWarnings("unchecked")
        private Map<Object, Object> instantiateMap(final PlexusConfiguration configuration, final Class<?> type,
                final ClassLoader loader) throws ComponentConfigurationException {
            final Class<?> implType = getClassForImplementationHint(type, configuration, loader);
            if (null == implType || Modifier.isAbstract(implType.getModifiers())) {
                return new TreeMap<Object, Object>();
            }
    
            final Object impl = instantiateObject(implType);
            failIfNotTypeCompatible(impl, type, configuration);
            return (Map<Object, Object>) impl;
        }
    
        private static Class<?> findElementType( final Type[] typeArguments )
        {
            if ( null != typeArguments && typeArguments.length > 1 )
            {
                if ( typeArguments[1] instanceof Class<?> )
                {
                    return (Class<?>) typeArguments[1];
                }
                // begin fix here
                if ( typeArguments[1] instanceof ParameterizedType )
                {
                    return (Class<?>) ((ParameterizedType) typeArguments[1]).getRawType();
                }
                // end fix here
            }
            return Object.class;
        }
    
    }
    

    【讨论】:

    • 谢谢!因此,您似乎只能在每一秒级别使用映射或属性(这会在样板中增加一点,因为实际上不需要中间级别。但是它似乎 可以 查找类型参数(或者它如何将“Converter”检测为其他类型?),但这不适用于其他泛型类型,如 map。
    • @RolandHuß 是的,转换器将类识别为复杂类型,但它不处理参数化类型。我在这里bugs.eclipse.org/bugs/show_bug.cgi?id=498757 对项目的Bugzilla 提出了问题,正在考虑中。
    • 编写一个自定义的ConfigurationConverter 是否是一种解决方法,例如在MapHandler 类上,并从其fromConfiguration() 方法返回Map。然后插件会声明一个@Parameter private Map&lt;String,MapHandler&gt; myMap 参数。不过,我想知道如何注册自定义配置转换器。
    • @RolandHuß 我终于找到了一种不用新对象就能做到的方法!
    • 我也是 ;-) 见下文以及我对stackoverflow.com/questions/10188534/… 的回答非常感谢您的帮助!
    【解决方案2】:

    一种解决方案非常简单,适用于 1 级嵌套。在替代答案中可以找到更复杂的方法,它可能还允许更深地嵌套地图。

    不要使用接口作为类型参数,只需使用像TreeMap这样的具体类

     @Parameter
     private Map<String, TreeMap> converters.
    

    原因是MapConverter 中的此检查对接口失败但对具体类成功:

       private static Class<?> findElementType( final Type[] typeArguments )
       {
           if ( null != typeArguments && typeArguments.length > 1 
                && typeArguments[1] instanceof Class<?> )
           {
               return (Class<?>) typeArguments[1];
           }
           return Object.class;
       }
    

    作为旁注,由于它也与 Maven > 3.3.x 的 answer 相关,它还可以通过子类化 BasicComponentConfigurator 并将其用作 Plexus 组件来安装自定义转换器。 BasicComponentConfiguratorDefaultConverterLookup 作为受保护的成员变量,因此可以轻松访问以注册自定义转换器。

    【讨论】:

    • 理论上是否可以使用这种转换器技术使用 attributes 而不是元素名称来配置我的地图键?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-06
    • 1970-01-01
    • 1970-01-01
    • 2021-11-07
    • 2015-06-12
    相关资源
    最近更新 更多