Micronaut 中的不可变配置和 YAML 中的列表

Immutable configuration in Micronaut with list in YAML

给出然后遵循 YAML 配置

a:
  b:
    instances:
      - name: T1
        endpoint: "http://t1"
        version: "1.5.3.24305,2021-08-09 18:01"
        read-timeout: 20
      - name: T2
        endpoint: "http://t1"
        version: "2.0.0.16555,2022-01-03 16:48"
        read-timeout: 20

我想使用 Micronaut 2.5.13 将它绑定到我的不可变配置界面。

@ConfigurationProperties("a.b")
public interface MyConfig {

  List<Instance> getInstances();

  interface Instance {
    String getName();
    URL getEndpoint();
    String getVersion();
    int getReadTimeout();
  }
}

然后我 运行 我的 Spock 集成测试

@MicronautTest
class MyConfigSpec extends Specification {
  @Inject
  MyConfig config

  void "Make sure the config has instances"() {

    expect:
    config.instances.isEmpty() == false
  }
}

断言失败,因为列表为空。我如何告诉 Micronaut 将 instances 列表映射到我的不可变 Instance 对象中?

问题是由于缺少知道如何从另一个类型创建 MyConfig.Instance 实例的 TypeConverter<S,T> 引起的。 属性 源解析器将 a.b.instances 配置视为 List<Map>,因此对于每个条目,它都会尝试找到满足 Map -> MyConfig.Instance 的转换器。因为没有明确满足这样的转换,所以退回到Map -> Object类型的转换器。这个转换器使用了 Jackson 的序列化和反序列化,它也不起作用,因为 Jackson 不知道如何反序列化一个 JSON 像这样:

{
  "name": "T1",
  "endpoint": "http://t1",
  "version": "1.5.3.24305,2021-08-09 18:01",
  "read-timeout": 20
}

MyConfig.Instance 的实例,因为没有可用于 Jackson 反序列化器的该接口的实现。

实现TypeConverter<Map, MyConfig.Instance>bean可以解决这个问题。它可以 return 一个匿名的 class 实现了 return 输入映射中所有值的接口。

import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.core.convert.ConversionContext;
import io.micronaut.core.convert.TypeConverter;

import javax.inject.Singleton;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@ConfigurationProperties("a.b")
public interface MyConfig {

    List<Instance> getInstances();

    interface Instance {
        String getName();
        URL getEndpoint();
        String getVersion();
        int getReadTimeout();
    }

    @Singleton
    class MapToInstanceConverter implements TypeConverter<Map, Instance> {

        @Override
        public Optional<Instance> convert(Map object, Class<Instance> targetType, ConversionContext context) {
            return Optional.of(new Instance() {
                @Override
                public String getName() {
                    return object.getOrDefault("name", "").toString();
                }

                @Override
                public URL getEndpoint() {
                    try {
                        return new URI(object.getOrDefault("endpoint", "").toString()).toURL();
                    } catch (MalformedURLException | URISyntaxException e) {
                        throw new RuntimeException(e);
                    }
                }

                @Override
                public String getVersion() {
                    return object.getOrDefault("version", "").toString();
                }

                @Override
                public int getReadTimeout() {
                    return Integer.parseInt(object.getOrDefault("read-timeout", 0).toString());
                }
            });
        }
    }
}

现在 运行 测试时,您将看到 return 由 MyConfig.getInstances() 方法编辑的两个条目。

“等等”,你可能会问,”为什么我定义了 Instance getInstance() 方法而不需要指定任何类型转换器?。这是正确的问题。正如您可能已经尝试过的那样,以下示例工作正常,无需提供任何额外的类型转换器:

@ConfigurationProperties("a.b")
public interface MyConfig {

    Instance getInstance();

    @ConfigurationProperties("instance")
    interface Instance {
        String getName();
        URL getEndpoint();
        String getVersion();
        int getReadTimeout();
    }
}

这两个示例之间的主要区别在于前一个示例使用了具有泛型类型的列表 - List<Instance>。不可变配置机制使用 ConfigurationIntroductionAdvice.intercept(ctx) 方法来调用您在接口中定义的每个 getter 方法。这里的问题是通用类型在运行时间被擦除,因此getInstances()方法的return类型被识别为List,而不是[=25] =].您可以在 ConfigurationIntroductionAdvice class 中的第 66 行添加一个断点并调试测试以查看会发生什么。当 return 类型是第二个示例中的 Instance 时,intercept(ctx) 方法通过 [=35= 识别 Instance 类型是 ConfigurationAdvice 的成员] 注释。在这种情况下,它使用拦截器从 Instance 接口调用每个方法——与它应用于 MyConfig 接口的机制相同。当对 List getInstances() 方法进行相同的检查时,它不会将 List 类型识别为 ConfigurationAdvice 的一部分,因此将其视为常规类型,即不是包裹在任何拦截器中,但只是像其他任何拦截器一样直接使用 class.