Spring Cloud discovery for multiple service versions - spring

I'm asking myself a question without finding responses for it. Maybe someone here would have ideas about that ;-)
Using a services registry (Eureka) in Spring Cloud with RestTemplate and Feign clients, I have different build versions of the same service. The build version being documented through Actuator's /info endpoint.
{
"build": {
"version": "0.0.1-SNAPSHOT",
"artifact": "service-a",
"name": "service-a",
"group": "com.mycompany",
"time": 1487253409000
}
}
...
{
"build": {
"version": "0.0.2-SNAPSHOT",
"artifact": "service-a",
"name": "service-a",
"group": "com.mycompany",
"time": 1487325340000
}
}
Is there any mean to ask for a particular build version at client's call?
Should I use gateway's routing filters in order to manage that? But the version detection would remain an issue I guess...
Well, any suggestion appreciated.

Ok. This is the code to inject the build version into the service ("service-a") instance metadata to be registered by Eureka:
#Configuration
#ConditionalOnClass({ EurekaInstanceConfigBean.class, EurekaClient.class })
public class EurekaClientInstanceBuildVersionAutoConfiguration {
#Autowired(required = false)
private EurekaInstanceConfig instanceConfig;
#Autowired(required = false)
private BuildProperties buildProperties;
#Value("${eureka.instance.metadata.keys.version:instanceBuildVersion}")
private String versionMetadataKey;
#PostConstruct
public void init() {
if (this.instanceConfig == null || buildProperties == null) {
return;
}
this.instanceConfig.getMetadataMap().put(versionMetadataKey, buildProperties.getVersion());
}
}
This is the code to validate metadata transmission within a "service-b":
#Component
public class DiscoveryClientRunner implements CommandLineRunner {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
#Autowired
private DiscoveryClient client;
#Override
public void run(String... args) throws Exception {
client.getInstances("service-a").forEach((ServiceInstance s) -> {
logger.debug(String.format("%s: %s", s.getServiceId(), s.getUri()));
for (Entry<String, String> md : s.getMetadata().entrySet()) {
logger.debug(String.format("%s: %s", md.getKey(), md.getValue()));
}
});
}
}
Notice that if "dashed composed" (i.e. "instance-build-version"), the metadata key is Camel Case forced.
And this is the solution I found to filter service instances according to their version:
#Configuration
#EnableConfigurationProperties(InstanceBuildVersionProperties.class)
public class EurekaInstanceBuildVersionFilterAutoConfig {
#Value("${eureka.instance.metadata.keys.version:instanceBuildVersion}")
private String versionMetadataKey;
#Bean
#ConditionalOnProperty(name = "eureka.client.filter.enabled", havingValue = "true")
public EurekaInstanceBuildVersionFilter eurekaInstanceBuildVersionFilter(InstanceBuildVersionProperties filters) {
return new EurekaInstanceBuildVersionFilter(versionMetadataKey, filters);
}
}
#Aspect
#RequiredArgsConstructor
public class EurekaInstanceBuildVersionFilter {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final String versionMetadataKey;
private final InstanceBuildVersionProperties filters;
#SuppressWarnings("unchecked")
#Around("execution(public * org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient.getInstances(..))")
public Object filterInstances(ProceedingJoinPoint jp) throws Throwable {
if (filters == null || !filters.isEnabled()) logger.error("Should not be filtering...");
List<ServiceInstance> instances = (List<ServiceInstance>) jp.proceed();
return instances.stream()
.filter(i -> filters.isKept((String) jp.getArgs()[0], i.getMetadata().get(versionMetadataKey))) //DEBUG MD key is Camel Cased!
.collect(Collectors.toList());
}
}
#ConfigurationProperties("eureka.client.filter")
public class InstanceBuildVersionProperties {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* Indicates whether or not service instances versions should be filtered
*/
#Getter #Setter
private boolean enabled = false;
/**
* Map of service instance version filters.
* The key is the service name and the value configures a filter set for services instances
*/
#Getter
private Map<String, InstanceBuildVersionFilter> services = new HashMap<>();
public boolean isKept(String serviceId, String instanceVersion) {
logger.debug("Considering service {} instance version {}", serviceId, instanceVersion);
if (services.containsKey(serviceId) && StringUtils.hasText(instanceVersion)) {
InstanceBuildVersionFilter filter = services.get(serviceId);
String[] filteredVersions = filter.getVersions().split("\\s*,\\s*"); // trimming
logger.debug((filter.isExcludeVersions() ? "Excluding" : "Including") + " instances: " + Arrays.toString(filteredVersions));
return contains(filteredVersions, instanceVersion) ? !filter.isExcludeVersions() : filter.isExcludeVersions();
}
return true;
}
#Getter #Setter
public static class InstanceBuildVersionFilter {
/**
* Comma separated list of service version labels to filter
*/
private String versions;
/**
* Indicates whether or not to keep the associated instance versions.
* When false, versions are kept, otherwise they will be filtered out
*/
private boolean excludeVersions = false;
}
}
You can specify for every consumed service a list of expected or avoided versions and the discovery will be filtered accordingly.
logging.level.com.mycompany.demo=DEBUG
eureka.client.filter.enabled=true
eureka.client.filter.services.service-a.versions=0.0.1-SNAPSHOT
Please submit as comments any suggestion. Thx

Service 1 registers v1 and v2 with Eureka
Service 2 discovers and sends requests to Service 1's v1 and v2 using different Ribbon clients
I got this demo to work and will blog about it in the next couple of days.
http://tech.asimio.net/2017/03/06/Multi-version-Service-Discovery-using-Spring-Cloud-Netflix-Eureka-and-Ribbon.html
The idea I followed was for RestTemplate to use a different Ribbon client for each version because each client has its own ServerListFilter.
Service 1
application.yml
...
eureka:
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8000/eureka/
instance:
hostname: ${hostName}
statusPageUrlPath: ${management.context-path}/info
healthCheckUrlPath: ${management.context-path}/health
preferIpAddress: true
metadataMap:
instanceId: ${spring.application.name}:${server.port}
---
spring:
profiles: v1
eureka:
instance:
metadataMap:
versions: v1
---
spring:
profiles: v1v2
eureka:
instance:
metadataMap:
versions: v1,v2
...
Service 2
application.yml
...
eureka:
client:
registerWithEureka: false
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8000/eureka/
demo-multiversion-registration-api-1-v1:
ribbon:
# Eureka vipAddress of the target service
DeploymentContextBasedVipAddresses: demo-multiversion-registration-api-1
NIWSServerListClassName: com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList
# Interval to refresh the server list from the source (ms)
ServerListRefreshInterval: 30000
demo-multiversion-registration-api-1-v2:
ribbon:
# Eureka vipAddress of the target service
DeploymentContextBasedVipAddresses: demo-multiversion-registration-api-1
NIWSServerListClassName: com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList
# Interval to refresh the server list from the source (ms)
ServerListRefreshInterval: 30000
...
Application.java
...
#SpringBootApplication(scanBasePackages = {
"com.asimio.api.multiversion.demo2.config",
"com.asimio.api.multiversion.demo2.rest"
})
#EnableDiscoveryClient
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
AppConfig.java (See how the Ribbon client name matches the Ribbon key found in application.yml
...
#Configuration
#RibbonClients(value = {
#RibbonClient(name = "demo-multiversion-registration-api-1-v1", configuration = RibbonConfigDemoApi1V1.class),
#RibbonClient(name = "demo-multiversion-registration-api-1-v2", configuration = RibbonConfigDemoApi1V2.class)
})
public class AppConfig {
#Bean(name = "loadBalancedRestTemplate")
#LoadBalanced
public RestTemplate loadBalancedRestTemplate() {
return new RestTemplate();
}
}
RibbonConfigDemoApi1V1.java
...
public class RibbonConfigDemoApi1V1 {
private DiscoveryClient discoveryClient;
#Bean
public ServerListFilter<Server> serverListFilter() {
return new VersionedNIWSServerListFilter<>(this.discoveryClient, RibbonClientApi.DEMO_REGISTRATION_API_1_V1);
}
#Autowired
public void setDiscoveryClient(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
}
RibbonConfigDemoApi1V2.java is similar but using RibbonClientApi.DEMO_REGISTRATION_API_1_V2
RibbonClientApi.java
...
public enum RibbonClientApi {
DEMO_REGISTRATION_API_1_V1("demo-multiversion-registration-api-1", "v1"),
DEMO_REGISTRATION_API_1_V2("demo-multiversion-registration-api-1", "v2");
public final String serviceId;
public final String version;
private RibbonClientApi(String serviceId, String version) {
this.serviceId = serviceId;
this.version = version;
}
}
VersionedNIWSServerListFilter.java
...
public class VersionedNIWSServerListFilter<T extends Server> extends DefaultNIWSServerListFilter<T> {
private static final String VERSION_KEY = "versions";
private final DiscoveryClient discoveryClient;
private final RibbonClientApi ribbonClientApi;
public VersionedNIWSServerListFilter(DiscoveryClient discoveryClient, RibbonClientApi ribbonClientApi) {
this.discoveryClient = discoveryClient;
this.ribbonClientApi = ribbonClientApi;
}
#Override
public List<T> getFilteredListOfServers(List<T> servers) {
List<T> result = new ArrayList<>();
List<ServiceInstance> serviceInstances = this.discoveryClient.getInstances(this.ribbonClientApi.serviceId);
for (ServiceInstance serviceInstance : serviceInstances) {
List<String> versions = this.getInstanceVersions(serviceInstance);
if (versions.isEmpty() || versions.contains(this.ribbonClientApi.version)) {
result.addAll(this.findServerForVersion(servers, serviceInstance));
}
}
return result;
}
private List<String> getInstanceVersions(ServiceInstance serviceInstance) {
List<String> result = new ArrayList<>();
String rawVersions = serviceInstance.getMetadata().get(VERSION_KEY);
if (StringUtils.isNotBlank(rawVersions)) {
result.addAll(Arrays.asList(rawVersions.split(",")));
}
return result;
}
...
AggregationResource.java
...
#RestController
#RequestMapping(value = "/aggregation", produces = "application/json")
public class AggregationResource {
private static final String ACTORS_SERVICE_ID_V1 = "demo-multiversion-registration-api-1-v1";
private static final String ACTORS_SERVICE_ID_V2 = "demo-multiversion-registration-api-1-v2";
private RestTemplate loadBalancedRestTemplate;
#RequestMapping(value = "/v1/actors/{id}", method = RequestMethod.GET)
public com.asimio.api.multiversion.demo2.model.v1.Actor findActorV1(#PathVariable(value = "id") String id) {
String url = String.format("http://%s/v1/actors/{id}", ACTORS_SERVICE_ID_V1);
return this.loadBalancedRestTemplate.getForObject(url, com.asimio.api.multiversion.demo2.model.v1.Actor.class, id);
}
#RequestMapping(value = "/v2/actors/{id}", method = RequestMethod.GET)
public com.asimio.api.multiversion.demo2.model.v2.Actor findActorV2(#PathVariable(value = "id") String id) {
String url = String.format("http://%s/v2/actors/{id}", ACTORS_SERVICE_ID_V2);
return this.loadBalancedRestTemplate.getForObject(url, com.asimio.api.multiversion.demo2.model.v2.Actor.class, id);
}
#Autowired
public void setLoadBalancedRestTemplate(RestTemplate loadBalancedRestTemplate) {
this.loadBalancedRestTemplate = loadBalancedRestTemplate;
}
}

This is the trick for hacking Eureka Dashboard.
Add this AspectJ aspect (because InstanceInfo used in EurekaController is not a Spring Bean) to the #EnableEurekaServer project:
#Configuration
#Aspect
public class EurekaDashboardVersionLabeler {
#Value("${eureka.instance.metadata.keys.version:instanceBuildVersion}")
private String versionMetadataKey;
#Around("execution(public * com.netflix.appinfo.InstanceInfo.getId())")
public String versionLabelAppInstances(ProceedingJoinPoint jp) throws Throwable {
String instanceId = (String) jp.proceed();
for (StackTraceElement ste : Thread.currentThread().getStackTrace()) {
// limit to EurekaController#populateApps in order to avoid side effects
if (ste.getClassName().contains("EurekaController")) {
InstanceInfo info = (InstanceInfo) jp.getThis();
String version = info.getMetadata().get(versionMetadataKey);
if (StringUtils.hasText(version)) {
return String.format("%s [%s]", instanceId, version);
}
break;
}
}
return instanceId;
}
#Bean("post-construct-labeler")
public EurekaDashboardVersionLabeler init() {
return EurekaDashboardVersionLabeler.aspectOf();
}
private static EurekaDashboardVersionLabeler instance = new EurekaDashboardVersionLabeler();
/** Singleton pattern used by LTW then Spring */
public static EurekaDashboardVersionLabeler aspectOf() {
return instance;
}
}
You also have to add a dependency not provided by starters:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
And activate the LTW a runtime with a VM arg, of course:
-javaagent:D:\.m2\repository\org\aspectj\aspectjweaver\1.8.9\aspectjweaver-1.8.9.jar

Related

Testcontainers mongodb - Cluster description not yet available. Waiting for 30000 ms before timing out

I have basic Spring repository:
#Repository
public interface LRepository extends MongoRepository<L, String> {}
And I am using it inside a service:
#Service
#RequiredArgsConstructor(onConstructor = #__(#Autowired))
public class SerImpl {
private final LRepository repository;
public String saveL(L l) {
l = repository.save(l);
return l.id;
}
}
On line with .save(l) my test timeouts.
#SpringBootTest
#ExtendWith(SpringExtension.class)
#Testcontainers
#ContextConfiguration(initializers = {MongoIntegrationTest.TestPropertiesInitializer.class})
class MongoIntegrationTest {
private SerImpl serImpl;
#Autowired
private LRepository lRepository;
public static class TestPropertiesInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
#Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
final TestPropertyValues values = TestPropertyValues.of(
"spring.data.mongodb.host=localhost",
"spring.data.mongodb.username=root",
"spring.data.mongodb.password=example",
"spring.data.mongodb.authentication-database=admin",
"spring.data.mongodb.database=l",
"spring.data.mongodb.uri=mongodb://root:example#localhost:27017"
);
values.applyTo(configurableApplicationContext);
}
}
#Container
public GenericContainer mongoDb = new GenericContainer("mongo:4.2.2")
.withExposedPorts(27017)
.withEnv("MONGO_INITDB_ROOT_USERNAME", "root")
.withEnv("MONGO_INITDB_ROOT_PASSWORD", "example");
#BeforeEach
public void setUp() {
mongoDb.start();
String address = mongoDb.getContainerIpAddress();
//List<Integer> exposedPorts = mongoDb.getExposedPorts();
Integer port = mongoDb.getFirstMappedPort();
Map<String,String> map = mongoDb.getEnvMap();
serImpl = new SerImpl(lRepository);
}
#Test
public void testSimplePutAndGet() {
String id = serImpl.saveL(new L("aaa"));
assertNotNull(id);
}
}
On line l = repository.save(l);
I get an error saying
org.mongodb.driver.cluster : Cluster description not yet available. Waiting for 30000 ms before timing out.
I tried to fix it by increasing timeout:
#Configuration
public class MongoConfiguration {
#Bean
public MongoClientOptions mongoOptions() {
return MongoClientOptions
.builder()
.serverSelectionTimeout(180000)
.build();
}
}
But more time did not help me here.
L is object with String field.
I simplified code above to make it more MVCE.
When the test runs i see the container running and exposing the port.
If i start mongo container from docker the test is successful.
I am running it on Windows.
I've added:
mongoDb.setPortBindings(List.of("27017:27017"));
This Fix this issue:
org.mongodb.driver.cluster : Cluster description not yet
available. Waiting for 30000 ms before timing out.

Error testing with Spring Cloud Stream Test

We are using spring-cloud-stream to manage messages between our applications.
We have custom bindings:
public interface InboundChannels {
String TASKS = "domainTasksInboundChannel";
String EVENTS = "eventsInboundChannel";
#Input(TASKS)
SubscribableChannel tasks();
#Input(EVENTS)
SubscribableChannel events();
}
public interface OutboundChannels {
String TASKS = "domainTasksOutboundChannel";
String EVENTS = "eventsOutboundChannel";
#Output(TASKS)
MessageChannel tasks();
#Output(EVENTS)
MessageChannel events();
}
There are processors that consumes tasks and generate events:
#EnableBinding({InboundChannels.class, OutboundChannels.class})
public class TasksProcessor {
public TasksProcessor(
UserService userService,
#Qualifier(OutboundChannels.EVENTS) MessageChannel eventsChannel
) {
this.userService = userService;
this.eventsChannel = eventsChannel;
}
#StreamListener(value = TASKS, condition = "headers['" + TYPE + "']=='" + CREATE_USER + "'")
public void createUser(Message<User> message) {
final User user = message.getPayload();
userService.save(user)
.subscribe(created -> {
Message<User> successMessage = fromMessage(message, Events.USER_CREATED, created).build();
eventsChannel.send(successMessage);
});
}
}
Now we wanted to test it using spring-cloud-stream-test-support and its amazing features:
#DirtiesContext
#SpringBootTest
#RunWith(SpringRunner.class)
public class TasksProcessorTest {
private User user;
#Autowired
private InboundChannels inboundChannels;
#Autowired
private OutboundChannels outboundChannels;
#Autowired
private MessageCollector collector;
#Before
public void setup() {
user = new User(BigInteger.ONE, "test#teste.com");
}
#Test
public void createUserTest() {
final Message<User> msg = create(CREATE_USER, user).build();
outboundChannels.tasks().send(msg);
final Message<?> incomingEvent = collector.forChannel(inboundChannels.events()).poll();
final String type = (String) incomingEvent.getHeaders().get(TYPE);
assertThat(type).isEqualToIgnoringCase(USER_CREATED);
}
}
application.properties
##
# Spring AMQP configuration
##
spring.rabbitmq.host=rabbitmq
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
# Events channels
spring.cloud.stream.bindings.eventsOutboundChannel.destination=events
spring.cloud.stream.bindings.eventsInboundChannel.destination=events
spring.cloud.stream.bindings.domainTasksOutboundChannel.destination=domainTasks
spring.cloud.stream.bindings.domainTasksInboundChannel.destination=domainTasks
spring.cloud.stream.bindings.userTasksInboundChannel.group=domainServiceInstances
spring.cloud.stream.bindings.eventsInboundChannel.group=domainServiceInstances
But then we get this error:
java.lang.IllegalArgumentException: Channel [eventsInboundChannel] was not bound by class org.springframework.cloud.stream.test.binder.TestSupportBinder
What are we doing wrong?
In the .subscribe() you do eventsChannel.send(successMessage);, where that eventsChannel is from the OutboundChannels.EVENTS, but what you try to do in the test-case is like inboundChannels.events(). And it doesn't look like you really bind this channel anywhere.
I'm sure if you would use outboundChannels.events() instead, that would work for you.

#RefreshScope annotated Bean registered through BeanDefinitionRegistryPostProcessor not getting refreshed on Cloud Config changes

I've a BeanDefinitionRegistryPostProcessor class that registers beans dynamically. Sometimes, the beans being registered have the Spring Cloud annotation #RefreshScope.
However, when the cloud configuration Environment is changed, such beans are not being refreshed. Upon debugging, the appropriate application events are triggered, however, the dynamic beans don't get reinstantiated. Need some help around this. Below is my code:
TestDynaProps:
public class TestDynaProps {
private String prop;
private String value;
public String getProp() {
return prop;
}
public void setProp(String prop) {
this.prop = prop;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
#Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("TestDynaProps [prop=").append(prop).append(", value=").append(value).append("]");
return builder.toString();
}
}
TestDynaPropConsumer:
#RefreshScope
public class TestDynaPropConsumer {
private TestDynaProps props;
public void setProps(TestDynaProps props) {
this.props = props;
}
#PostConstruct
public void init() {
System.out.println("Init props : " + props);
}
public String getVal() {
return props.getValue();
}
}
BeanDefinitionRegistryPostProcessor:
public class PropertyBasedDynamicBeanDefinitionRegistrar implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {
private ConfigurableEnvironment environment;
private final Class<?> propertyConfigurationClass;
private final String propertyBeanNamePrefix;
private final String propertyKeysPropertyName;
private Class<?> propertyConsumerBean;
private String consumerBeanNamePrefix;
private List<String> dynaBeans;
public PropertyBasedDynamicBeanDefinitionRegistrar(Class<?> propertyConfigurationClass,
String propertyBeanNamePrefix, String propertyKeysPropertyName) {
this.propertyConfigurationClass = propertyConfigurationClass;
this.propertyBeanNamePrefix = propertyBeanNamePrefix;
this.propertyKeysPropertyName = propertyKeysPropertyName;
dynaBeans = new ArrayList<>();
}
public void setPropertyConsumerBean(Class<?> propertyConsumerBean, String consumerBeanNamePrefix) {
this.propertyConsumerBean = propertyConsumerBean;
this.consumerBeanNamePrefix = consumerBeanNamePrefix;
}
#Override
public void setEnvironment(Environment environment) {
this.environment = (ConfigurableEnvironment) environment;
}
#Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory arg0) throws BeansException {
}
#Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefRegistry) throws BeansException {
if (environment == null) {
throw new BeanCreationException("Environment must be set to initialize dyna bean");
}
String[] keys = getPropertyKeys();
Map<String, String> propertyKeyBeanNameMapping = new HashMap<>();
for (String k : keys) {
String trimmedKey = k.trim();
String propBeanName = getPropertyBeanName(trimmedKey);
registerPropertyBean(beanDefRegistry, trimmedKey, propBeanName);
propertyKeyBeanNameMapping.put(trimmedKey, propBeanName);
}
if (propertyConsumerBean != null) {
String beanPropertyFieldName = getConsumerBeanPropertyVariable();
for (Map.Entry<String, String> prop : propertyKeyBeanNameMapping.entrySet()) {
registerConsumerBean(beanDefRegistry, prop.getKey(), prop.getValue(), beanPropertyFieldName);
}
}
}
private void registerConsumerBean(BeanDefinitionRegistry beanDefRegistry, String trimmedKey, String propBeanName, String beanPropertyFieldName) {
String consumerBeanName = getConsumerBeanName(trimmedKey);
AbstractBeanDefinition consumerDefinition = preparePropertyConsumerBeanDefinition(propBeanName, beanPropertyFieldName);
beanDefRegistry.registerBeanDefinition(consumerBeanName, consumerDefinition);
dynaBeans.add(consumerBeanName);
}
private void registerPropertyBean(BeanDefinitionRegistry beanDefRegistry, String trimmedKey, String propBeanName) {
AbstractBeanDefinition propertyBeanDefinition = preparePropertyBeanDefinition(trimmedKey);
beanDefRegistry.registerBeanDefinition(propBeanName, propertyBeanDefinition);
dynaBeans.add(propBeanName);
}
private String getConsumerBeanPropertyVariable() throws IllegalArgumentException {
Field[] beanFields = propertyConsumerBean.getDeclaredFields();
for (Field bField : beanFields) {
if (bField.getType().equals(propertyConfigurationClass)) {
return bField.getName();
}
}
throw new BeanCreationException(String.format("Could not find property of type %s in bean class %s",
propertyConfigurationClass.getName(), propertyConsumerBean.getName()));
}
private AbstractBeanDefinition preparePropertyBeanDefinition(String trimmedKey) {
BeanDefinitionBuilder bdb = BeanDefinitionBuilder.genericBeanDefinition(PropertiesConfigurationFactory.class);
bdb.addConstructorArgValue(propertyConfigurationClass);
bdb.addPropertyValue("propertySources", environment.getPropertySources());
bdb.addPropertyValue("conversionService", environment.getConversionService());
bdb.addPropertyValue("targetName", trimmedKey);
return bdb.getBeanDefinition();
}
private AbstractBeanDefinition preparePropertyConsumerBeanDefinition(String propBeanName, String beanPropertyFieldName) {
BeanDefinitionBuilder bdb = BeanDefinitionBuilder.genericBeanDefinition(propertyConsumerBean);
bdb.addPropertyReference(beanPropertyFieldName, propBeanName);
return bdb.getBeanDefinition();
}
private String getPropertyBeanName(String trimmedKey) {
return propertyBeanNamePrefix + trimmedKey.substring(0, 1).toUpperCase() + trimmedKey.substring(1);
}
private String getConsumerBeanName(String trimmedKey) {
return consumerBeanNamePrefix + trimmedKey.substring(0, 1).toUpperCase() + trimmedKey.substring(1);
}
private String[] getPropertyKeys() {
String keysProp = environment.getProperty(propertyKeysPropertyName);
return keysProp.split(",");
}
The Config class:
#Configuration
public class DynaPropsConfig {
#Bean
public PropertyBasedDynamicBeanDefinitionRegistrar dynaRegistrar() {
PropertyBasedDynamicBeanDefinitionRegistrar registrar = new PropertyBasedDynamicBeanDefinitionRegistrar(TestDynaProps.class, "testDynaProp", "dyna.props");
registrar.setPropertyConsumerBean(TestDynaPropConsumer.class, "testDynaPropsConsumer");
return registrar;
}
}
Application.java
#SpringBootApplication
#EnableDiscoveryClient
#EnableScheduling
public class Application extends SpringBootServletInitializer {
private static Class<Application> applicationClass = Application.class;
public static void main(String[] args) {
SpringApplication sa = new SpringApplication(applicationClass);
sa.run(args);
}
}
And, my bootstrap.properties:
spring.cloud.consul.enabled=true
spring.cloud.consul.config.enabled=true
spring.cloud.consul.config.format=PROPERTIES
spring.cloud.consul.config.watch.delay=15000
spring.cloud.discovery.client.health-indicator.enabled=false
spring.cloud.discovery.client.composite-indicator.enabled=false
application.properties
dyna.props=d1,d2
d1.prop=d1prop
d1.value=d1value
d2.prop=d2prop
d2.value=d2value
Here are some guesses:
1) Perhaps the #RefreshScope metadata is not being passed to your metadata for the bean definition. Call setScope()?
2) The RefreshScope is actually implemented by https://github.com/spring-cloud/spring-cloud-commons/blob/master/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/refresh/RefreshScope.java, which itself implements BeanDefinitionRegistryPostProcessor. Perhaps the ordering of these two post processors is issue.
Just guesses.
We finally resolved this by appending the #RefreshScope annotation on the proposed dynamic bean classes using ByteBuddy and then, adding them to Spring Context using Bean Definition Post Processor.
The Post Processor is added to spring.factories so that it loads before any other dynamic bean dependent beans.

How to map configuration objects to java object

I have a spring boot application which is using a spring cloud config.
How can i map a configuration element with some java object.
My config is something like this:
clients:
- id : 1
name: client 1
groups : [a,b]
- id : 2
name: client 2
groups : [a]
And my java object is:
public class ClientInfo {
private String clientId;
private List<String> profiles;
public ClientInfo(String clientId, List<String> pips) {
this.clientId = clientId;
this.profiles = pips;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public List<String> getProfiles() {
return profiles;
}
public void setProfiles(List<String> profiles) {
this.profiles = profiles;
}
}
I want to map my configuration with List
Use below code to configure configuration properties in to java Object,
#Component
#EnableConfigurationProperties
#ConfigurationProperties(prefix = "clients")
public class ClientInfo {
private String id;
private String name;
private List<String> groups;
public String getId(){ return id;}
public String getName(){ return name;}
public List<String> getGroups(){ return groups;}
}
Check following for example http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html
Inject this class in another class :
#Autowired
private ClientInfo clientInfo;
The above auto wiring will not work if the class is instantiated using "new operator".
Actually I found the reason why it was not working.
All that was needed is to have another class which contains a list of ClientInfo and have #EnableConfigurationProperties and #ConfigurationProperties annotations on it. This is because "clients" in my configuration is a list. After this change we can use #Autowired annotation to inject the configuration.

Use #Autowired in #Webservice (other solutions found did not work)

We are trying to use autowiring in our webservice, but this doens't seem to work (generates nullPointer). We have been searching for a solution for quite a long time, but did not succeed.
Our webservice:
#WebService(wsdlLocation = "/WEB-INF/wsdl/contract.wsdl", serviceName = "BookingService", targetNamespace = "http://realdolmen.com/", portName = "BookingServicePortType")
public class BookingService extends SpringBeanAutowiringSupport implements BookingServicePortType {
#Autowired
BookingServiceBean bookingServiceBean;
#Autowired
TariffService tariffService;
#Override
public BookingResponse createBooking(#WebParam(name = "bookingInput", targetNamespace = "http://realdolmen.com/", partName = "tariffId") BookingInput input) {
Tariff tariff = tariffService.getTariffById(input.getTariffId());
Booking booking = new Booking.BookingBuilder().withBaggageAllowance(tariff.getFlight().getBaggageAllowance())
.withDayOfDeparture(input.getDayOfDeparture()).withHourOfDeparture(input.getHourOfDeparture()).withTariff(tariff).withDuration(input.getDuration()).createBooking();
bookingServiceBean.createBooking(booking);
BookingResponse bookingResponse = new BookingResponse();
bookingResponse.setBookingId(booking.getId());
bookingResponse.setBaggageAllowance(booking.getBaggageAllowance());
bookingResponse.setDayOfDeparture(createWeirdDateClass(booking.getDayOfDeparture()));
bookingResponse.setDuration(booking.getDuration());
bookingResponse.setHourOfDeparture(booking.getHourOfDeparture());
return bookingResponse;
}
private XMLGregorianCalendar createWeirdDateClass(String lexicalRepresentation) {
try {
return DatatypeFactory.newInstance().newXMLGregorianCalendar(lexicalRepresentation);
} catch (DatatypeConfigurationException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
return null;
}
}
}
our spring service:
#Service
#Transactional
public class BookingServiceBeanImpl implements BookingServiceBean {
#Autowired
BookingDAO bookingDAO;
#Override public void createBooking(Booking booking) {
bookingDAO.createBooking(booking);
}
}
The spring bean can be used in the spring controllers so I don't think there's a problem there..

Resources