Я использую Spring Boot Binder API в EnvironmentPostProcessor
(т. Е. До обновления (обновления) фактического контекста приложения) для привязки пользовательского ConfigurationProperty
объекта.
Я хочу, чтобы пользователям приходилось указывать ровно одно обязательное свойство в application.yml
: com.acme.kafka.service-instance-name: <user-provided value>
.
Учитывая это, я смогу получить другое (требуется, но не обязательно, чтобы оно было введено user) properties:
com:
acme:
kafka:
username: <can be fetched from VCAP_SERVICES, or specified explicitly>
password: <can be fetched from VCAP_SERVICES, or specified explicitly>
brokers: <can be fetched from VCAP_SERVICES, or specified explicitly>
token-endpoint: <can be fetched from VCAP_SERVICES, or specified explicitly>
token-validity: <can be fetched from VCAP_SERVICES, or specified explicitly>
Таким образом, в простейшем случае для пользователя application.yml
должен содержать только:
com:
acme:
kafka:
service-instance-name: myKafkaInstance
Я создал для этого пользовательский ConfigurationProperty
, который выглядит следующим образом:
package com.acme.kafka;
import java.net.URL;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.List;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.validation.annotation.Validated;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@Validated
@ConfigurationProperties(prefix = AcmeKafkaConfigurationProperties.PREFIX)
public class AcmeKafkaConfigurationProperties {
public static final String PREFIX = "com.acme.kafka";
static final String USERNAME_FROM_VCAP_SERVICES = "${vcap.services.${com.acme.kafka.service-instance-name}.credentials.username:}";
static final String PASSWORD_FROM_VCAP_SERVICES = "${vcap.services.${com.acme.kafka.service-instance-name}.credentials.password:}";
static final String BROKERS_FROM_VCAP_SERVICES = "${vcap.services.${com.acme.kafka.service-instance-name}.credentials.cluster.brokers:}";
static final String TOKEN_ENDPOINT_FROM_VCAP_SERVICES = "${vcap.services.${com.acme.kafka.service-instance-name}.credentials.urls.token:}";
@NotBlank(message = "Acme Kafka service instance name must not be blank.")
private String serviceInstanceName;
@NotBlank(message = "Username must not be blank. Make sure it is either specified explicitly or available from VCAP_SERVICES environment.")
@Value(USERNAME_FROM_VCAP_SERVICES)
private String username;
@NotBlank(message = "Password must not be blank. Make sure it is either specified explicitly or available from VCAP_SERVICES environment.")
@Value(PASSWORD_FROM_VCAP_SERVICES)
private String password;
@NotEmpty(message = "Brokers must not be empty. Make sure it is either specified explicitly or available from VCAP_SERVICES environment.")
@Value(BROKERS_FROM_VCAP_SERVICES)
private List<@NotBlank String> brokers;
@NotNull(message = "Token endpoint URL must not be null and must be a valid URL. Make sure it is either specified explicitly or available from VCAP_SERVICES environment.")
@Value(TOKEN_ENDPOINT_FROM_VCAP_SERVICES)
private URL tokenEndpoint;
@NotNull(message = "Token validity must not be null and a value given in seconds (1s), minutes (2m), hours (3h), or days (365d).")
@DurationUnit(ChronoUnit.DAYS)
private Duration tokenValidity = Duration.ofDays(3650);
private String springCloudStreamBinderName;
private boolean autoCreateTruststore;
private String sslTruststoreLocation;
private String sslTruststorePassword;
}
Обратите внимание, что AcmeKafkaConfigurationProperties
использует аннотации @Value
для некоторых свойств, которые (если они явно не настроены в application.yml) должны быть заполнены значениями из CF VCAP_SERVICES
окружающая обстановка. Эти свойства (поскольку они необходимы) также снабжены аннотациями для проверки правильности заполнения.
После контекст приложения Spring Boot обновлен, приведенный выше код работает как Очарование: 1. Создается экземпляр AcmeKafkaConfigurationProperties
2. Сначала привязываются значения VCAP_SERVICES
, затем (если явно указано) они переопределяются тем, что в application.yml
3. Затем начинается проверка и evth. просто работает.
Однако, так как мне нужно AcmeKafkaConfigurationProperties
уже в EnvironmentPostProcessor (где контекст еще не обновлен), я делаю это:
@Component
public class AcmeKafkaEnvironmentPostprocessor implements EnvironmentPostProcessor, ApplicationListener<ApplicationPreparedEvent>, Ordered {
private AcmeKafkaConfigurationProperties acmeKafkaProps;
private PropertySourcesPlaceholdersResolver resolver;
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
// Put defaults that should be read from VCAPS if not specified on Environment as last property source.
// This is a workaround for the fact that Spring Boot's Binder does not allow to resolve
// @Value annotations against vcap.services environment.
HashMap<String, Object> map = new HashMap<>();
map.put("com.acme.kafka.username", AcmeKafkaConfigurationProperties.USERNAME_FROM_VCAP_SERVICES);
map.put("com.acme.kafka.password", AcmeKafkaConfigurationProperties.PASSWORD_FROM_VCAP_SERVICES);
map.put("com.acme.kafka.brokers", AcmeKafkaConfigurationProperties.BROKERS_FROM_VCAP_SERVICES);
map.put("com.acme.kafka.token-endpoint", AcmeKafkaConfigurationProperties.TOKEN_ENDPOINT_FROM_VCAP_SERVICES);
environment.getPropertySources().addLast(new MapPropertySource("acmeKafkaDefaults", map));
// For Details see this excellent blog post:
// https://spring.io/blog/2018/03/28/property-binding-in-spring-boot-2-0
Iterable<ConfigurationPropertySource> sources = ConfigurationPropertySources.get(environment);
resolver = new PropertySourcesPlaceholdersResolver(environment);
// Just to check that values are resolved properly. Ok!
String result = (String) resolver.resolvePlaceholders("${vcap.services.${com.acme.kafka.service-instance-name}.credentials.urls.token:}");
Binder binder = new Binder(sources, resolver);
Bindable<AcmeKafkaConfigurationProperties> bindable = Bindable.of(AcmeKafkaConfigurationProperties.class);
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
SpringValidatorAdapter springValidator = new SpringValidatorAdapter(validator);
BindResult<AcmeKafkaConfigurationProperties> bindResult = binder.bind(AcmeKafkaConfigurationProperties.PREFIX,
bindable,
new ValidationBindHandler(springValidator));
acmeKafkaProps = bindResult.get();
System.out.println("ServiceInstanceName: " + acmeKafkaProps.getServiceInstanceName());
System.out.println("UserName: " + acmeKafkaProps.getUsername());
System.out.println("Password: " + acmeKafkaProps.getPassword());
System.out.println("TokenValidity: " + acmeKafkaProps.getTokenValidity());
System.out.println("TokenEndpoint: " + acmeKafkaProps.getTokenEndpoint());
System.out.println("Brokers: " + acmeKafkaProps.getBrokers());
}
Обратите внимание на часть где я помещаю карту со значениями по умолчанию от VCAP_SERVICES
в среде:
HashMap<String, Object> map = new HashMap<>();
map.put("com.acme.kafka.username", AcmeKafkaConfigurationProperties.USERNAME_FROM_VCAP_SERVICES);
map.put("com.acme.kafka.password", AcmeKafkaConfigurationProperties.PASSWORD_FROM_VCAP_SERVICES);
map.put("com.acme.kafka.brokers", AcmeKafkaConfigurationProperties.BROKERS_FROM_VCAP_SERVICES);
map.put("com.acme.kafka.token-endpoint", AcmeKafkaConfigurationProperties.TOKEN_ENDPOINT_FROM_VCAP_SERVICES);
environment.getPropertySources().addLast(new MapPropertySource("acmeKafkaDefaults", map));
Это, по сути, помещает пары значений следующей формы в среду:
com.acme.kafka.<propertyname> : "${vcap.services.<instancename>.credentials.<path-to-property>}"
В Мне кажется, что это обходной путь, поскольку он необходим, поскольку API-интерфейс Binder Spring Boot не позволяет разрешать аннотацию @Value
. Вместо этого он всегда ищет имя свойства для привязки (в данном случае com.acme.kafka
-префиксированных значений) в среде, и если он не находит их там, он приходит к выводу, что его значение не установлено. Он также никогда не проверяет, существует ли аннотация @Value
, которая может привести к необходимости поиска привязываемого значения по совершенно другому префиксу, например, vcap.services...
- по сути, это заполнитель, указанный в аннотации @Value
.
Итак, я попытался создать свой собственный BindHandler
, который, как я понял, может повлиять на процесс привязки, например, с учетом аннотаций. Это, например, то, как Spring Boot Binder API поддерживает обработку аннотаций проверки - предоставляя ValidationBindHandler
, используемый в приведенном выше коде.
Итак, вот код BindHandler
, который я пытался использовать:
private class MyBindHandler implements BindHandler {
@Override
public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target, BindContext context) {
Value valueAnnotation = target.getAnnotation(Value.class);
if(valueAnnotation == null) { // property has no @Value annotation
return target;
}
String vcapServicesReference = valueAnnotation.value();
// PropertySourcesPlaceholdersResolver resolver = new PropertySourcesPlaceholdersResolver(environment);
// ... defined in EnvironmentPostProcessor.
Object resolvedValue = resolver.resolvePlaceholders(vcapServicesReference);
return target.withExistingValue((T) resolvedValue);
//also tried this:
//return target.withSuppliedValue(() -> {
// return (T) resolver.resolvePlaceholders(vcapServicesReference);
//});
}
@Override
public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) {
return result;
}
@Override
public Object onCreate(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) {
return result;
}
@Override
public Object onFailure(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Exception error)
throws Exception {
throw error;
}
@Override
public void onFinish(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result)
throws Exception {
}
}
К сожалению, это не работает. onStart()
и onFinish
- единственные обратные вызовы, которые в данный момент вызываются в моей установке, и (вручную) разрешенное значение из аннотации @Value
, которую я вставил в Bindable target
, никогда не рассматривается.
Отладка всего стека, я думаю, что проблема заключается в этом методе в Binder.class
(см. Комментарии к проблеме):
private <T> Object bindObject(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler,
Context context, boolean allowRecursiveBinding) {
// This call does not find any property `com.acme.kafka.username`...
ConfigurationProperty property = findProperty(name, context);
// ...and this statement evaluates to 'true', leading to an immediate 'return null'
// which is equivalent to 'I give up, there is no value for that property I can bind':
if (property == null && containsNoDescendantOf(context.getSources(), name) && context.depth != 0) {
//Here could / should (?) be a check, if the target was modified, i.e. has a
//value supplier or an existing value set (e.g. by my BindHandler above)
// and if that is the case, the bound property should be returned.
return null;
}
AggregateBinder<?> aggregateBinder = getAggregateBinder(target, context);
if (aggregateBinder != null) {
return bindAggregate(name, target, handler, context, aggregateBinder);
}
if (property != null) {
try {
return bindProperty(target, context, property);
}
catch (ConverterNotFoundException ex) {
Object instance = bindDataObject(name, target, handler, context, allowRecursiveBinding);
if (instance != null) {
return instance;
}
throw ex;
}
}
return bindDataObject(name, target, handler, context, allowRecursiveBinding);
}
Поэтому у меня два вопроса:
Является ли мой обходной путь, описанный выше (куда я сбрасываю значения по умолчанию в окружающую среду) намеченным способом, или действительно обходной путь?
Как можно разрешить и связать аннотации @Value
с API Spring Boot Binder, и будут ли предложенные изменения возможными?
Спасибо !