Spring Expression Language issue - spring

I have the following class. I have verified in the console, the constructor of this class is called(during bean creation) before resolving the topic placeholder value in Kafka listener:
public class MsgReceiver<MSG> extends AbstractMsgReceiver<MSG> implements
MessageReceiver<MSG> {
#SuppressWarnings("unused")
private String topic;
public MsgReceiver(String topic, MessageHandler<MSG> handler) {
super(handler);
this.topic = topic;
}
#KafkaListener(topics = "${my.messenger.kafka.topics.#{${topic}}.value}", groupId = "${my.messenger.kafka.topics.#{${topic}}.groupId}")
public void receiveMessage(#Headers Map<String, Object> headers, #Payload MSG payload) {
System.out.println("Received "+payload);
super.receiveMessage(headers, payload);
}
}
I have my application.yml as follows:
my:
messenger:
kafka:
address: localhost:9092
topics:
topic_1:
value: my_topic
groupId: 1
During bean creation, I pass "topic_1" which I want should dynamically be used inside Kafka listener topic placeholder. I tried as shown in the code itself, but it does not work. Please suggest how to do that.

Placeholders are resolved before SpEL is evaluated; you can't dynamically build a placeholder name using SpEL. Also, you can't reference fields like that; you have to do it indirectly via the bean name (and a public getter).
So, to do what you want, you have to add a getter and get the property dynamically from the environment after building the property name with SpEL.
There is a special token __listener which allows you to reference the current bean.
Putting it all together...
#SpringBootApplication
public class So63056065Application {
public static void main(String[] args) {
SpringApplication.run(So63056065Application.class, args);
}
#Bean
public MyReceiver receiver() {
return new MyReceiver("topic_1");
}
#Bean
public NewTopic topic() {
return TopicBuilder.name("my_topic").partitions(1).replicas(1).build();
}
}
class MyReceiver {
private final String topic;
public MyReceiver(String topic) {
this.topic = topic;
}
public String getTopic() {
return this.topic;
}
#KafkaListener(topics = "#{environment.getProperty('my.messenger.kafka.topics.' + __listener.topic + '.value')}",
groupId = "#{environment.getProperty('my.messenger.kafka.topics.' + __listener.topic + '.groupId')}")
public void listen(String in) {
System.out.println(in);
}
}
Result...
2020-07-23 12:13:44.932 INFO 39561 --- [ main] o.a.k.clients.consumer.ConsumerConfig : ConsumerConfig values:
allow.auto.create.topics = true
auto.commit.interval.ms = 5000
auto.offset.reset = latest
bootstrap.servers = [localhost:9092]
check.crcs = true
client.dns.lookup = default
client.id =
client.rack =
connections.max.idle.ms = 540000
default.api.timeout.ms = 60000
enable.auto.commit = false
exclude.internal.topics = true
fetch.max.bytes = 52428800
fetch.max.wait.ms = 500
fetch.min.bytes = 1
group.id = 1
group.instance.id = null
...
and
1: partitions assigned: [my_topic-0]

Related

use #Bean inject a bean but could't pass check about #ConditionalOnBean

When I comment out the line #ConditionalOnBean(name = "customRedisConnectionFactory"),the function is execute and the customRedisConnectionFactory is the object that I injected. But when I enabled this line, the method doesn't execute. I want to know the reason. Can someone help me answer it.
RedisConfig
#Configuration
#ConditionalOnClass(RedisOperations.class)
#AutoConfigureBefore({CacheAutoConfiguration.class})
public class RedisConfig {
#Bean(name = "customRedisConnectionFactory")
public RedisConnectionFactory customRedisConnectionFactory(){
RedisStandaloneConfiguration redisStandaloneConfiguration=new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setDatabase(12);
redisStandaloneConfiguration.setHostName("127.0.0.1");
redisStandaloneConfiguration.setPassword("yichen");
redisStandaloneConfiguration.setPort(6379);
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(redisStandaloneConfiguration);
return jedisConnectionFactory;
}
}
CustomRedisCacheManagerConfiguration
#Configuration
#AutoConfigureAfter({CacheAutoConfiguration.class})
#ConditionalOnClass(RedisOperations.class)
#EnableConfigurationProperties({RedisProperties.class, CacheProperties.class, RedisCacheExpiresProperties.class})
public class CustomRedisCacheManagerConfiguration {
private final CacheProperties cacheProperties;
public CustomRedisCacheManagerConfiguration(CacheProperties cacheProperties) {
this.cacheProperties = cacheProperties;
}
#Bean(name = "serviceRedisCacheManager")
#ConditionalOnBean(name = "customRedisConnectionFactory")
public RedisCacheManager serviceRedisCacheManager(
#Qualifier("customRedisConnectionFactory") RedisConnectionFactory customRedisConnectionFactory,
RedisCacheExpiresProperties redisCacheExpiresProperties) {
RedisCacheManager.RedisCacheManagerBuilder builder =
RedisCacheManager.builder(customRedisConnectionFactory).cacheDefaults(determineConfigurationDefault());
Map<String, Long> cacheConfigurations = redisCacheExpiresProperties.getCacheExpires();
if (cacheConfigurations != null && cacheConfigurations.size() > 0) {
Map<String, RedisCacheConfiguration> redisCacheConfigurations = new HashMap<>();
for (String cacheName : cacheConfigurations.keySet()) {
Assert.notNull(cacheName, "CacheName must not be null!");
long ttl = cacheConfigurations.get(cacheName);
Assert.isTrue(ttl > 0, "Expire must not be null!");
RedisCacheConfiguration redisCacheConfiguration = determineConfiguration(cacheName, ttl);
redisCacheConfigurations.put(cacheName, redisCacheConfiguration);
}
builder.withInitialCacheConfigurations(redisCacheConfigurations);
}
return builder.build();
}
private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfigurationDefault() {
CacheProperties.Redis redisProperties = this.cacheProperties.getRedis();
org.springframework.data.redis.cache.RedisCacheConfiguration config =
org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(String cacheName,long ttl) {
org.springframework.data.redis.cache.RedisCacheConfiguration config =
org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.entryTtl(Duration.ofSeconds(ttl));
config = config.disableCachingNullValues();
return config;
}
}
application.properties
spring.redis.password=yichen
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=10
spring.redis.max.idle=10
spring.redis.max.total=30
spring.redis.max.wait.mills=-1
spring.cache.redis.time-to-live=60000
spring.cache.redis.key-prefix=test
spring.cache.redis.cache-expires.c3m=180
spring.cache.redis.cache-expires.c5m=300
spring.cache.redis.cache-expires.c10m=600
spring.cache.redis.cache-expires.c30m=1800
spring.cache.redis.cache-expires.c24h=86400
spring.cache.redis.cache-expires.c7d=604800
spring.cache.redis.cache-expires.c30d=2592000
RedisCacheExpiresProperties
#ConfigurationProperties(prefix = "spring.cache.redis")
public class RedisCacheExpiresProperties {
private Map<String, Long> cacheExpires;
public Map<String, Long> getCacheExpires() {
return cacheExpires;
}
public void setCacheExpires(Map<String, Long> cacheExpires) {
this.cacheExpires = cacheExpires;
}
}
I think it's maybe a problem with the order the two configuration class (RedisConfig and CustomRedisCacheManagerConfiguration) were auto configured. Make sure you registered the class as auto configuration class and RedisConfig is auto configured before CustomRedisCacheManagerConfiguration.
When I try to replace #DependsOn("customRedisConnectionFactory") with #ConditionalOnBean(type = "RedisConnectionFactory"), it successfully executed.
Their differences and implementation details are to be studied.

Why are the application config values parsed by my custom Spring PropertySourceLoader not being used?

I am attempting to write a TOML PropertySourceLoader implementation. I took a look at some other examples on GitHub and stackoverflow, all of which seem to eventually parse the result out to a map and then return an OriginTrackedMapPropertySource, which is what I tried below:
public final class TomlPropertySourceFactory implements PropertySourceFactory {
#Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
if (!ClassUtils.isPresent("com.fasterxml.jackson.dataformat.toml.TomlFactory", null)) {
throw new IllegalStateException(
"Attempted to load " + name + " but jackson-dataformat-toml was not found on the classpath");
}
if (!ClassUtils.isPresent("com.fasterxml.jackson.core.io.ContentReference", null)) {
throw new IllegalStateException(
"Attempted to load " + name + " but jackson-core was either not found on the classpath or below version 2.13.0");
}
final ObjectMapper mapper = new ObjectMapper(new TomlFactory());
final Map<String, Object> resultMap = mapper.convertValue(mapper.readTree(resource.getInputStream()), new TypeReference<Map<String, Object>>(){});
return new OriginTrackedMapPropertySource(Optional.ofNullable(name).orElseGet(resource.getResource()::getFilename), resultMap);
}
}
public final class TomlPropertySourceLoader implements PropertySourceLoader {
#Override
public String[] getFileExtensions() {
return new String[]{"tml", "toml"};
}
#Override
public List<PropertySource<?>> load(final String name, final Resource resource) throws IOException {
final EncodedResource encodedResource = new EncodedResource(resource);
return Collections.singletonList(new TomlPropertySourceFactory().createPropertySource(name, encodedResource));
}
}
This code does seem to more or less do what is expected; it is executed when application.toml is present, it loads and parses the file out to a <String, Object> map, but from there, none of the actual properties seem to be present in the application — be it when using #Value, #ConfigurationProperties or even when attempting to set stuff like the port Tomcat runs on.
There's not a ton of information available on the internet, without digging into the depths of Spring, about what exactly it is expecting. I'm not sure if the problem is due to how my map is structured or perhaps due to something with the name.
Below you can find my application.toml file:
[spring.datasource]
url = "jdbc:hsqldb:file:testdb"
username = "sa"
[spring.thymeleaf]
cache = false
[server]
port = 8085
[myapp]
foo = "Hello"
bar = 42
aUri = "https://example.org/hello"
targetLocale = "en-US"
[myapp.configuration]
endpoints = ["one", "two", "three"]
[myapp.configuration.connectionSettings]
one = "hello"
[myapp.configuration.connectionSettings.two]
two_sub = "world!"
And my configuration classes:
#Data
#NoArgsConstructor
#Component
#ConfigurationProperties("myapp")
public class AppConfig {
private String foo;
private int bar;
private URI aUri;
private Locale targetLocale;
private SubConfiguration configuration;
}
#Data
#NoArgsConstructor
public class SubConfiguration {
private List<String> endpoints;
private Map<String, Object> connectionSettings;
}
As well as my testing controller:
#RestController
#RequiredArgsConstructor
public final class TomlDemoController {
private final AppConfig appConfig;
#GetMapping("/api/config")
AppConfig getConfig() {
return appConfig;
}
}
The issue was the structure of the property map. The keys have to be flattened in order to work. As an example, for a given table:
[server]
port = 8085
Rather than producing a nested map:
Properties = {
"server" = {
"port" = 8085
}
}
Spring is expecting something more like:
Properties = {
"server.port" = 8085
}
A quick solution using the ObjectToMapTransformer found in Spring Integration:
#Override
#SuppressWarnings("unchecked")
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
if (!ClassUtils.isPresent("com.fasterxml.jackson.dataformat.toml.TomlFactory", null)) {
throw new IllegalStateException(
"Attempted to load " + name + " but jackson-dataformat-toml was not found on the classpath");
}
if (!ClassUtils.isPresent("com.fasterxml.jackson.core.io.ContentReference", null)) {
throw new IllegalStateException(
"Attempted to load " + name + " but jackson-core was either not found on the classpath or below version 2.13.0");
}
final ObjectMapper mapper = new ObjectMapper(new TomlFactory());
final Message<JsonNode> message = new GenericMessage<>(mapper.readTree(resource.getInputStream()));
final ObjectToMapTransformer transformer = new ObjectToMapTransformer();
transformer.setShouldFlattenKeys(true);
Map<String,Object> resultMap = (Map<String, Object>) transformer.transform(message).getPayload();
return new OriginTrackedMapPropertySource(Optional.ofNullable(name).orElseGet(resource.getResource()::getFilename), resultMap);
}

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.

Spring Cloud discovery for multiple service versions

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

Spring Data Redis SET command supports EX and NX

Do Spring Data Redis support SET command with Options
My use case:
127.0.0.1:6379> set lock.foo RUNNING NX EX 20
Then check if Redis return value OK or (nil)
Use RedisTemplate#execute(RedisCallback<T> method, demo:
#Autowired
private RedisTemplate redisTemplate;
public void test() {
String redisKey = "lock.foo";
String value = "RUNNING";
long expire = 20L;
Boolean result = (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
byte[] redisKeyBytes = redisTemplate.getKeySerializer().serialize(redisKey);
byte[] valueBytes = redisTemplate.getValueSerializer().serialize(value);
Expiration expiration = Expiration.from(expire, TimeUnit.SECONDS);
return connection.set(redisKeyBytes, valueBytes, expiration, RedisStringCommands.SetOption.SET_IF_ABSENT);
});
System.out.println("result = " + result);
}
RedisTemplate config:
#Configuration
public class RedisConfig {
#Bean
public RedisSerializer<String> keySerializer() {
return new StringRedisSerializer();
}
#Bean
public RedisSerializer<Object> valueSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
#Bean
public RedisTemplate redisTemplate(RedisTemplate redisTemplate, RedisSerializer keySerializer, RedisSerializer valueSerializer) {
//set key serializer
redisTemplate.setKeySerializer(keySerializer);
redisTemplate.setHashKeySerializer(keySerializer);
//set value serializer
redisTemplate.setValueSerializer(valueSerializer);
redisTemplate.setHashValueSerializer(valueSerializer);
return redisTemplate;
}
}
Cannot see any Spring template value operations solutions, so I did a 'native' execute on the connection org.springframework.data.redis.connection.StringRedisConnection#execute(java.lang.String, java.lang.String...)
Then it is up to me to take care of processing of arguments and the result.

Resources