Hytrix consumer throws timed-out and no fallback available exception - spring

I'm trying to test hytrix timeout fallback strategies for both consumer and provider services.
dependency versions:
spring-cloud-starter-openfeign = 2.2.1.RELEASE
spring-cloud-starter-netflix-hystrix = 2.2.1.RELEASE
I put a #HystrixCommand to a consumer's controller method whose timeout threshold is set to 3s, and bind the consumer's service class to the provider's app name.
In the provider's service class, I put a #HystrixCommand to one of its service method whose timeout threshold is 5s, which is longer than that of the consumer's.
I made use of the path variable to set the sleep time in the provider's service method, so I don't have to modify and restart everytime.
Here is the code:
consumer
application.yml
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
feign:
hystrix:
enabled: true
controller class:
public class OrderHystirxController {
#Resource
private PaymentHystrixService paymentHystrixService;
#GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(#PathVariable("id") Integer id) {
return paymentHystrixService.paymentInfo_OK(id);
}
#GetMapping("/consumer/payment/hystrix/timeout/{id}")
#HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
#HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
})
public String paymentInfo_TimeOut(#PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
// fallback method
public String paymentTimeOutFallbackMethod(#PathVariable("id") Integer id) {
return "Client Timeout";
}
service interface:
#Component
#FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT")
public interface PaymentHystrixService
{
#GetMapping("/payment/hystrix/ok/{id}")
String paymentInfo_OK(#PathVariable("id") Integer id);
#GetMapping("/payment/hystrix/timeout/{id}")
String paymentInfo_TimeOut(#PathVariable("id") Integer id);
}
provider
application.yml
server:
port: 8001
spring:
application:
name: cloud-provider-hystrix-payment
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka
controller
#RestController
public class PaymentController
{
#Autowired
private PaymentService paymentService;
#GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(#PathVariable("id") Integer id)
{
String result = paymentService.paymentInfo_OK(id);
System.out.println("****result: "+result);
return result;
}
#GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(#PathVariable("id") Integer id) throws InterruptedException
{
String result = paymentService.paymentInfo_TimeOut(id);
System.out.println("****result: "+result);
return result;
}
}
service
#Service
public class PaymentService {
public String paymentInfo_OK(Integer id) {
return "线程池:" + Thread.currentThread().getName() + " paymentInfo_OK,id: " + id;
}
#HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler", commandProperties = {
#HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")
})
public String paymentInfo_TimeOut(Integer id) {
long outTime = (long) id;
try {
TimeUnit.SECONDS.sleep(outTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池:" + Thread.currentThread().getName() + " paymentInfo_TimeOut,id: " + id + " 耗时: " + outTime;
}
// fallback method
public String paymentInfo_TimeOutHandler(Integer id) {
return "Server Timeout:" + "\t当前线程池名字" + Thread.currentThread().getName();
}
}
When accessed directly via provider's urls, everything went as expected.
But when accessed via consumer's url "/consumer/payment/hystrix/timeout/{id}", it didn't go as expected.
I thought it should be like:
id(timeout) set to below 3, no timeout occurs
id(timeout) set to over 3, consumer timeout occurs
But what happened is:
id(timeout) set to 0, no timeout occurs
id(timeout) set to 1 or above, consumer timeout occurs
If I try-catch the paymentInfo_TimeOut method of the consumer, it prints:
com.netflix.hystrix.exception.HystrixRuntimeException: PaymentHystrixService#paymentInfo_TimeOut(Integer) timed-out and no fallback available.
Did I configure hystrix wrong?
Help needed and thanks in advance.

Related

FeignException com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.springframework.http.ResponseEntity`

Any Help please !!
I receive this error when I'm calling my endpoint which call Feign in the background :
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of
`org.springframework.http.ResponseEntity` (no Creators, like default constructor, exist): cannot deserialize
from Object value (no delegate- or property-based Creator)
at [Source: (BufferedReader); line: 1, column: 2]
This is my endpoint inside Controller :
#RestController
#RequestMapping(Routes.URI_PREFIX)
public class CartoController {
#Autowired
private ReadCartographyApiDelegate readCartographyApiDelegate;
#GetMapping(value = "/cartographies/{uid}", produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseWrapper<ReadCartographyResponse> readCarto(HttpServletRequest request,
#PathVariable(name = "uid") String uid) {
ResponseEntity<ReadCartographyResponse> result ;
try {
result = readCartographyApiDelegate.readCartography(uid);
}catch (Exception e){
throw new TechnicalException("Error during read Carto");
}
return responseWrapperWithIdBuilder.of(result.getBody());
}
}
Interface ReadCartographyApiDelegate generated automatically by openApi from yaml file :
#javax.annotation.Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "...")
public interface ReadCartographyApiDelegate {
default Optional<NativeWebRequest> getRequest() {
return Optional.empty();
}
default ResponseEntity<ReadCartographyResponse> readCartography(String uid) {
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
String exampleString = "null";
ApiUtil.setExampleResponse(request, "application/json", exampleString);
break;
}
}
});
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
}
This my ReadCartoApiDelegateImpl which implements ReadCartographyApiDelegate interface :
#Service
public class ReadCartographyApiDelegateImpl implements ReadCartographyApiDelegate {
private EcomGtmClient ecomGtmClient;
public ReadCartographyApiDelegateImpl(EcomGtmClient ecomGtmClient) {
this.ecomGtmClient = ecomGtmClient;
}
#Override
public ResponseEntity<ReadCartographyResponse> readCartography(String uid) {
ResponseEntity<ReadCartographyResponse> response = ecomGtmClient.readCartography(uid);
return response;
}
}
This is the feign client :
#FeignClient(name = "ecomGtmSvc", url = "http://localhost/")
public interface EcomGtmClient {
#GetMapping(value = "/read-carto/{uid}")
ResponseEntity<ReadCartographyResponse> readCartography(#PathVariable("uid") String uid);
}
The problem is that ResponseEntity (spring class) class doesn't contain default constructor which is needed during creating of instance. is there Any config to resolve this issue ?
If you want access to the body or headers on feign responses, you should use the feign.Response class. ResponseEntity does not work with feign because it is not meant to. I think it is best if you just return Response from your feign client method. You should then be able to pass the body to the ResponseEntity instance in the Controller.
What is your reason to even use the response-wrapper, i can't really figure that out from your code?
Sadly I couldn't find any documentation on the Response class, but here's the link to the source on GitHub.
https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/Response.java
My Suggestion would be
#FeignClient(name = "ecomGtmSvc", url = "http://localhost/")
public interface EcomGtmClient {
#GetMapping(value = "/read-carto/{uid}")
ReadCartographyResponse readCartography(#PathVariable("uid") String uid);
}
#RestController
#RequestMapping(Routes.URI_PREFIX)
public class CartoController {
#Autowired
private ReadCartographyApiDelegate readCartographyApiDelegate;
#GetMapping(value = "/cartographies/{uid}", produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseWrapper<ReadCartographyResponse> readCarto(HttpServletRequest request,
#PathVariable(name = "uid") String uid) {
ReadCartographyResponse result ;
try {
result = readCartographyApiDelegate.readCartography(uid);
}catch (Exception e){
throw new TechnicalException("Error during read Carto");
}
// I don't know where you get the builder from, so I assume it does something import and is needed
return responseWrapperWithIdBuilder.of(result);
}
}
Of course you'd also have to change all intermediate classes.
The Response Output was the correct Object that I have to put, cause every time I need to check the status from my feign client endpoint to do différent logic
#FeignClient(name = "ecomGtmSvc", url = "http://localhost/")
public interface EcomGtmClient {
#GetMapping(value = "/read-carto/{uid}")
ReadCartographyResponse readCartography(#PathVariable("uid") String uid);
}

Spring Expression Language issue

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]

Netflix Feign custom error decoder doesn't work correctly

I have a problem with custom error decoder in netflix Feign.
I have a service A and a service B , service A is sending something to service B :
#FeignClient(value = "A", url = "${B.url}", configuration = MyConfig.class)
public interface NotificationClient {
#DeleteMapping(value = "/somepath", consumes = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<Void> sendSomething(MyObject myObject);
}
And MyConfig looks like:
#Configuration
public class MyConfig {
#Bean
ErrorDecoder errorDecoder() {
return new MyErrorDecoder();
}
}
And MyErrorDecoder :
public class MyErrorDecoder implements ErrorDecoder {
#Override
public Exception decode(String methodKey, Response response) {
if (response.status() >= 400 && response.status() <= 499) {
return new MyClientException(response.status(), response.reason(), response.request().url());
}
if (response.status() >= 500 && response.status() <= 599) {
return new MyServerException(response.status(), response.reason(), response.request().url());
}
return errorStatus(methodKey, response);
}
}
And for example MyClientException :
#Getter
public class MyClientException extends RuntimeException {
private static final long serialVersionUID = 8021218722733395779L;
private final int httpStatusCode;
public MyClientException(int status, String reason, String url){
super(String.format("Error when sending request to: ' %s ' : status: %d %s", url, status, reason != null ? reason : ""));
this.httpStatusCode = status;
}
}
My problem is that I have a validation in service 'B', and it returns http status code 400, but in my service 'A' i got status 500 and message like "Error when sending request to: ' url_from_properties/somepath ' : status: 400 ".. why? I want to have the same status code in service 'A' what i got in service 'B'.
I looking for answer but nothing works exactly like i expect. What is wrong here?

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