【问题标题】:Immutable @ConfigurationProperties不可变的@ConfigurationProperties
【发布时间】:2014-11-26 02:11:01
【问题描述】:

Spring Boot 的 @ConfigurationProperties 注释是否可以具有不可变(最终)字段?下面的例子

@ConfigurationProperties(prefix = "example")
public final class MyProps {

  private final String neededProperty;

  public MyProps(String neededProperty) {
    this.neededProperty = neededProperty;
  }

  public String getNeededProperty() { .. }
}

到目前为止我尝试过的方法:

  1. 使用两个构造函数创建MyProps 类的@Bean
    • 提供两个构造函数:空的和带有neededProperty 参数的构造函数
    • bean 是用new MyProps() 创建的
    • 字段中的结果为null
  2. 使用@ComponentScan@Component 提供MyProps bean。
    • 结果为@​​987654332@ -> NoSuchMethodException: MyProps.<init>()

我让它工作的唯一方法是为每个非最终字段提供 getter/setter。

【问题讨论】:

  • 据我所知,您尝试做的事情不会开箱即用。
  • 这很可悲。当然,我总是可以通过使用带有 @Value 注释的构造函数参数来使用普通 Spring 来实现。不过,如果 Spring Boot 也支持这个就好了。
  • 我在源代码上取得了一个小高峰,但支持您所要求的内容并非易事。当然,我不是 Spring 内部的专家,所以我可能会遗漏一些明显的东西
  • 这不是您正在寻找的,但可能会对这个现有的 Spring Boot 问题感兴趣:github.com/spring-projects/spring-boot/issues/1254
  • cmets 中提出的解决方案也可以解决我的问题。如果设置器不可见,则配置属性将无法修改而无需诉诸暴力:)

标签: java spring spring-boot properties-file


【解决方案1】:

从 Spring Boot 2.2 开始,终于可以定义一个用@ConfigurationProperties 装饰的不可变类。
The documentation 显示了一个示例。
您只需要声明一个带有要绑定的字段的构造函数(而不是 setter 方式),并在类级别添加 @ConstructorBinding 注释以指示应该使用构造函数绑定。
所以你没有任何设置器的实际代码现在很好:

@ConstructorBinding
@ConfigurationProperties(prefix = "example")
public final class MyProps {

  private final String neededProperty;

  public MyProps(String neededProperty) {
    this.neededProperty = neededProperty;
  }

  public String getNeededProperty() { .. }
}

【讨论】:

  • 请注意,您现在必须使用@ConstructorBinding 注释来完成这项工作。在此之前 (RC1),您必须改用 @ImmutableConfigurationProperties。关于为什么选择这个注解的更多信息,你可以参考issue 18563
  • @g00glen00b 感谢您的评论。我更新了当前的方法。
  • 这很有帮助,很好的答案。谢谢!
【解决方案2】:

我必须经常解决这个问题,并且我使用了一些不同的方法,它允许我在一个类中使用final 变量。

首先,我将所有配置保存在一个地方(类),例如,称为ApplicationProperties。该类具有带有特定前缀的 @ConfigurationProperties 注释。它也在@EnableConfigurationProperties注解中针对配置类(或主类)列出。

然后我提供我的ApplicationProperties 作为构造函数参数,并对构造函数内的final 字段执行赋值。

例子:

类:

@SpringBootApplication
@EnableConfigurationProperties(ApplicationProperties.class)
public class Application {
    public static void main(String... args) throws Exception {
        SpringApplication.run(Application.class, args);
    }
}

ApplicationProperties

@ConfigurationProperties(prefix = "myapp")
public class ApplicationProperties {

    private String someProperty;

    // ... other properties and getters

   public String getSomeProperty() {
       return someProperty;
   }
}

还有一个具有最终属性的类

@Service
public class SomeImplementation implements SomeInterface {
    private final String someProperty;

    @Autowired
    public SomeImplementation(ApplicationProperties properties) {
        this.someProperty = properties.getSomeProperty();
    }

    // ... other methods / properties 
}

出于许多不同的原因,我更喜欢这种方法,例如如果我必须在构造函数中设置更多属性,我的构造函数参数列表不是“巨大的”,因为我总是有一个参数(在我的例子中是ApplicationProperties);如果需要添加更多 final 属性,我的构造函数保持不变(只有一个参数) - 这可能会减少其他地方的更改数量等。

希望对你有帮助

【讨论】:

  • 与仅使用 @Value 相比,这是很多样板
  • 这是java。更多样板意味着更好的代码
  • @Clijsters 老实说,我不知道你是不是在开玩笑,但我的意思是,它不太对,但也不远!
  • 是的!它本来是用来开玩笑的(但玩笑通常是真实的)。
【解决方案3】:

最后,如果你想要一个不可变的对象,你也可以“破解” setter

@ConfigurationProperties(prefix = "myapp")
public class ApplicationProperties {
    private String someProperty;

    // ... other properties and getters

    public String getSomeProperty() {
       return someProperty;
    }

    public String setSomeProperty(String someProperty) {
      if (someProperty == null) {
        this.someProperty = someProperty;
      }       
    }
}

很明显,如果属性不只是一个字符串,它是一个可变对象,事情会更复杂,但那是另一回事了。

你可以创建一个配置容器更好

@ConfigurationProperties(prefix = "myapp")
public class ApplicationProperties {
   private final List<MyConfiguration> configurations  = new ArrayList<>();

   public List<MyConfiguration> getConfigurations() {
      return configurations
   }
}

现在配置是一个没有

的类
public class MyConfiguration {
    private String someProperty;

    // ... other properties and getters

    public String getSomeProperty() {
       return someProperty;
    }

    public String setSomeProperty(String someProperty) {
      if (this.someProperty == null) {
        this.someProperty = someProperty;
      }       
    }
}

和 application.yml 一样

myapp:
  configurations:
    - someProperty: one
    - someProperty: two
    - someProperty: other

【讨论】:

  • 我想你的意思是if (this.someProperty == null) { this.someProperty = someProperty; }
  • 你的设计不是一成不变的,它只是防止设置两次即。在 A 点,属性的状态可能与 B 点不同。
  • patrickf 你是对的,事实上我不恰当地使用了“不可变”这个词。感谢您的评论。
【解决方案4】:

我的想法是通过内部类封装属性组,并仅使用 getter 公开接口。

属性文件:

myapp.security.token-duration=30m
myapp.security.expired-tokens-check-interval=5m

myapp.scheduler.pool-size=2

代码:

@Component
@ConfigurationProperties("myapp")
@Validated
public class ApplicationProperties
{
    private final Security security = new Security();
    private final Scheduler scheduler = new Scheduler();

    public interface SecurityProperties
    {
        Duration getTokenDuration();
        Duration getExpiredTokensCheckInterval();
    }

    public interface SchedulerProperties
    {
        int getPoolSize();
    }

    static private class Security implements SecurityProperties
    {
        @DurationUnit(ChronoUnit.MINUTES)
        private Duration tokenDuration = Duration.ofMinutes(30);

        @DurationUnit(ChronoUnit.MINUTES)
        private Duration expiredTokensCheckInterval = Duration.ofMinutes(10);

        @Override
        public Duration getTokenDuration()
        {
            return tokenDuration;
        }

        @Override
        public Duration getExpiredTokensCheckInterval()
        {
            return expiredTokensCheckInterval;
        }

        public void setTokenDuration(Duration duration)
        {
            this.tokenDuration = duration;
        }

        public void setExpiredTokensCheckInterval(Duration duration)
        {
            this.expiredTokensCheckInterval = duration;
        }

        @Override
        public String toString()
        {
            final StringBuffer sb = new StringBuffer("{ ");
            sb.append("tokenDuration=").append(tokenDuration);
            sb.append(", expiredTokensCheckInterval=").append(expiredTokensCheckInterval);
            sb.append(" }");
            return sb.toString();
        }
    }

    static private class Scheduler implements SchedulerProperties
    {
        @Min(1)
        @Max(5)
        private int poolSize = 1;

        @Override
        public int getPoolSize()
        {
            return poolSize;
        }

        public void setPoolSize(int poolSize)
        {
            this.poolSize = poolSize;
        }

        @Override
        public String toString()
        {
            final StringBuilder sb = new StringBuilder("{ ");
            sb.append("poolSize=").append(poolSize);
            sb.append(" }");
            return sb.toString();
        }
    }

    public SecurityProperties getSecurity()     { return security; }
    public SchedulerProperties getScheduler()   { return scheduler; }

    @Override
    public String toString()
    {
        final StringBuilder sb = new StringBuilder("{ ");
        sb.append("security=").append(security);
        sb.append(", scheduler=").append(scheduler);
        sb.append(" }");
        return sb.toString();
    }
}

【讨论】:

    【解决方案5】:

    使用与https://stackoverflow.com/a/60442151/11770752类似的方法

    但您可以使用RequiredArgsConstructor 代替AllArgsConstructor

    考虑关注applications.properties

    myprops.example.firstName=Peter
    myprops.example.last-name=Pan
    myprops.example.age=28
    

    注意:与您的属性保持一致,我只是想证明两者都是正确的(fistNamelast-name)。


    Java 类获取属性

    @Getter
    @ConstructorBinding
    @RequiredArgsConstructor
    @ConfigurationProperties(prefix = "myprops.example")
    public class StageConfig
    {
        private final String firstName;
        private final Integer lastName;
        private final Integer age;
    
        // ...
    }
    
    

    此外,您必须在构建工具中添加一个依赖项。

    build.gradle

        annotationProcessor('org.springframework.boot:spring-boot-configuration-processor')
    

    pom.xml

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <version>${spring.boot.version}</version>
    </dependency>
    

    如果您更进一步为您的配置提供准确而准确的描述,请考虑在目录src/main/resources/META-INF 中创建一个文件additional-spring-configuration-metadata.json

    {
      "properties": [
        {
          "name": "myprops.example.firstName",
          "type": "java.lang.String",
          "description": "First name of the product owner from this web-service."
        },
        {
          "name": "myprops.example.lastName",
          "type": "java.lang.String",
          "description": "Last name of the product owner from this web-service."
        },
        {
          "name": "myprops.example.age",
          "type": "java.lang.Integer",
          "description": "Current age of this web-service, since development started."
        }
    }
    

    (清理&编译生效)


    至少在 IntelliJ 中,当您将鼠标悬停在 application.propoerties 内的属性上时,您会清楚地了解您的自定义属性。对其他开发者非常有用。

    这为我的属性提供了一个简洁明了的结构,我在 spring 服务中使用它。

    【讨论】:

      【解决方案6】:

      使用 Lombok 注释,代码将如下所示:

      @ConfigurationProperties(prefix = "example")
      @AllArgsConstructor
      @Getter
      @ConstructorBinding
      public final class MyProps {
      
        private final String neededProperty;
      
      }
      

      此外,如果您想直接自动装配此属性类而不使用@Configuration 类和@EnableConfigurationProperties,则需要将@ConfigurationPropertiesScan 添加到使用@SpringBootApplication 注释的主应用程序类中。

      在此处查看相关文档:https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config-constructor-binding

      【讨论】:

        【解决方案7】:

        您可以通过@Value 注解设置字段值。这些可以直接放在字段上,不需要任何设置器:

        @Component
        public final class MyProps {
        
          @Value("${example.neededProperty}")
          private final String neededProperty;
        
          public String getNeededProperty() { .. }
        }
        

        这种方法的缺点是:

        • 您需要在每个字段中指定完全限定的属性名称。
        • 验证不起作用(参见this question

        【讨论】:

        • 这行不通。您将收到 needProperty might have not been initialized 错误。一种解决方案是使用带有 @Value("${example.neededProperty}") String neededProperty 作为参数的构造函数,然后初始化 requiredProperty
        猜你喜欢
        • 1970-01-01
        • 2018-03-12
        • 1970-01-01
        • 2021-06-03
        • 2017-06-02
        • 2021-09-22
        • 2017-02-01
        • 1970-01-01
        • 2019-03-28
        相关资源
        最近更新 更多