java 不可变@ConfigurationProperties

5anewei6  于 2023-06-04  发布在  Java
关注(0)|答案(9)|浏览(465)

是否可以使用Sping Boot 的@ConfigurationProperties注解来拥有不可变(final)字段?下面的例子

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

  private final String neededProperty;

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

  public String getNeededProperty() { .. }
}

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

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

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

nzk0hqpo

nzk0hqpo1#

从Sping Boot 2.2开始,终于可以定义一个用@ConfigurationProperties修饰的不可变类。
文档显示了一个示例。
您只需要声明一个带有要绑定的字段的构造函数(而不是setter方法),并在类级别添加@ConstructorBinding注解以指示应使用构造函数绑定。
因此,没有任何setter的实际代码现在很好:

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

  private final String neededProperty;

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

  public String getNeededProperty() { .. }
}
jmp7cifd

jmp7cifd2#

我经常需要解决这个问题,所以我使用了一种不同的方法,它允许我在类中使用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;
   }
}

和具有final属性的类

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

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

    // ... other methods / properties 
}

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

camsedfj

camsedfj3#

使用与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."
    }
}

(clean & compile to take effect)
至少在IntelliJ中,当您将鼠标悬停在application.propoerties中的属性上时,您可以清楚地看到自定义属性的描述。对其他开发人员非常有用。
这给了我一个很好的和简洁的属性结构,我正在使用我的服务与Spring。

xxls0lw8

xxls0lw84#

最后,如果你想要一个不可变的对象,你也可以“破解”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
kq0g1dla

kq0g1dla5#

我的想法是通过内部类封装属性组,并仅使用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();
    }
}
lstz6jyr

lstz6jyr6#

只是对最新Spring-Boot版本的最新支持的更新:
如果你使用的是>= 14的jdk版本,你可以使用record类型,它或多或少与Lombok版本相同,但没有Lombok。

@ConfigurationProperties(prefix = "example")
public record MyProps(String neededProperty) {
}

您还可以在MyProps记录中使用record来管理嵌套属性。你可以看到一个例子here
另一个有趣的帖子here表明,如果只声明一个构造函数,@ConstructorBinding注解甚至不再必要。

muk1a3rh

muk1a3rh7#

使用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

xe55xuns

xe55xuns8#

如果你想在不加载整个spring Boot 上下文的情况下在你的应用程序中切片测试你的属性,在你的测试中使用@EnableConfigurationProperties
示例:
src/main/resources/application.yml

myApp:
  enabled: true
  name: "test"
@Getter
@AllArgsConstructor
@Configuration
@ConfigurationProperties(prefix = "myApp")
public class MyApplicationProperties {

    boolean enabled;
    String name;

}
// this will only load MyApplicationProperties.class in spring boot context making it fast
@SpringBootTest( classes = MyApplicationProperties.class})
@EnableConfigurationProperties
class MyApplicationPropertiesTest {

    @Autowired
    MyApplicationProperties myApplicationProperties ;

    @Test
    void test_myApplicationProperties () {
        assertThat(myApplicationProperties.getEnabled()).isTrue();
        assertThat(myApplicationProperties.getName()).isEqualTo("test");

}
f2uvfpb9

f2uvfpb99#

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

@Component
public final class MyProps {

  @Value("${example.neededProperty}")
  private final String neededProperty;

  public String getNeededProperty() { .. }
}

这种方法的缺点是:

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

相关问题