Spring-boot async controller via CompletableFuture works synchronicity - spring

I created 2 spring-boot services: slow-service and gateway
slow-service is service which reverses string but do it very slowly.
#Service
public class SlowReverseService implements ReverseService {
#Override
public String reverse(String message) {
StringBuilder builder = new StringBuilder(message);
try {
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
return builder.reverse().toString();
}
}
And Controller:
#RestController
public class SlowController {
private final ReverseService reverseService;
public SlowController(ReverseService reverseService) {
this.reverseService = reverseService;
}
#GetMapping(value = "/reverse/{message}")
public String reverseMessage(#PathVariable("message") String message){
return reverseService.reverse(message);
}
}
gateway uses slow-service and returns an async result.
#Service
public class AsyncReverseService implements ReverseService {
#Async
#Override
public CompletableFuture<String> reverse(String message) {
RestTemplate restTemplate = new RestTemplate();
return CompletableFuture.completedFuture(
restTemplate.getForObject("http://localhost:8081/reverse/" + message, String.class));
}
}
And Controller:
#RestController
public class GatewayController {
private final ReverseService reverseService;
public GatewayController(ReverseService reverseService) {
this.reverseService = reverseService;
}
#GetMapping(value = "/reverse/{message}")
public CompletableFuture<String> reverseMessage(#PathVariable("message") String message) {
String name = Thread.currentThread().getName();
System.out.println(new Date() + ":" + name);
return reverseService.reverse(message);
}
}
I set this param in the properties of gateway: server.tomcat.threads.max=2
I try to send 6 requests to the gateway and see this log:
Thu Aug 13 10:59:28 MSK 2020:http-nio-8080-exec-1
Thu Aug 13 10:59:29 MSK 2020:http-nio-8080-exec-2
Thu Aug 13 10:59:58 MSK 2020:http-nio-8080-exec-1
Thu Aug 13 10:59:59 MSK 2020:http-nio-8080-exec-2
Thu Aug 13 11:00:28 MSK 2020:http-nio-8080-exec-1
Thu Aug 13 11:00:29 MSK 2020:http-nio-8080-exec-2
My gateway controller receives 2 requests and sends it to slow-service. Slow-service works 30 sec and returns the result to the gateway. The gateway can process the next 2 requests. The Gateway can process 2 requests each 30 sec.
I Expected another behavior: Gateway receives 2 requests and delegates it to CompletableFuture. And frees threads. And Can receive the next 2 requests. Connections increases but my 2 threads are always available.

Related

Hytrix consumer throws timed-out and no fallback available exception

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.

Could not write JSON: JsonObject; nested exception is com.fasterxml.jackson.databind.JsonMappingException: JsonObject

Spring boot 2.5
#PostMapping("/cart/product")
public Response addProduct(#RequestBody Map<String, Object> payloadMap) {
logger.info("addProduct: payloadMap: " + payloadMap);
String userName = payloadMap.get("user_name").toString();
final Product product = new ObjectMapper().convertValue(payloadMap.get("product"), Product.class);
int quantity = (int) payloadMap.get("quantity");
Cart findCart = cartRepository.findByUsername(userName);
if (findCart == null) {
Cart cart = new Cart();
cart.setCreated(new Date());
cart.setUsername(userName);
cart.addProduct(product, quantity);
cartRepository.save(cart);
logger.info("addProduct: success_add_product_to_new_cart: " + cart);
return ResponseService.getSuccessResponse(GsonUtil.gson.toJsonTree(cart));
} else {
findCart.addProduct(product, quantity);
logger.info("addProduct: before_save_exist_cart: " + findCart);
cartRepository.save(findCart);
logger.info("addProduct: success_add_product_to_exist_cart: " + findCart);
return ResponseService.getSuccessResponse(GsonUtil.gson.toJsonTree(findCart));
}
}
public class ResponseService {
private static final int SUCCESS_CODE = 0;
private static final String SUCCESS_MESSAGE = "Success";
private static final int ERROR_CODE = -1;
private static Logger logger = LogManager.getLogger(ResponseService.class);
public static Response getSuccessResponse(JsonElement body) {
Response response = new Response(SUCCESS_CODE, SUCCESS_MESSAGE, body);
logger.info("getSuccessResponse: response = " + response);
return response;
}
import com.google.gson.JsonElement;
public class Response {
private int code;
private String message;
private JsonElement body;
public Response(int code, String message) {
this.code = code;
this.message = message;
}
public Response(int code, String message, JsonElement body) {
this.code = code;
this.message = message;
this.body = body;
}
But I get error when try to return response:
2020-04-12 12:02:18.825 INFO 9584 --- [nio-8092-exec-1] r.o.s.e.controllers.CartController : addProduct: success_add_product_to_new_cart: Cart{id=130, username='admin#admin.com', created=Sun Apr 12 12:02:18 EEST 2020, updated=null, productEntities=[
ProductEntity{id=131, created=Sun Apr 12 12:02:18 EEST 2020, updated=null, quantity=1, orders=null, product=
Product{id=132, name='product name', description='product description', created=Tue Mar 10 22:34:15 EET 2020, updated=null, price=11.15, currency='EUR', images=[http://www.gravatar.com/avatar/44444?s=200x200&d=identicon, http://www.gravatar.com/avatar/33333?s=200x200&d=identicon]}}], totalAmount=11.15, currency='EUR'}
2020-04-12 12:02:18.836 INFO 9584 --- [nio-8092-exec-1] r.o.s.e.service.ResponseService : getSuccessResponse: response = Response{code = 0, message = 'Success', body = '{"id":130,"username":"admin#admin.com","created":"Apr 12, 2020, 12:02:18 PM","productEntities":[{"id":131,"created":"Apr 12, 2020, 12:02:18 PM","quantity":1,"product":{"id":132,"name":"product name","description":"product description","created":"Mar 10, 2020, 10:34:15 PM","price":11.15,"currency":"EUR","images":["http://www.gravatar.com/avatar/44444?s=200x200&d=identicon","http://www.gravatar.com/avatar/33333?s=200x200&d=identicon"]}}],"totalAmount":11.15,"currency":"EUR"}'}
2020-04-12 12:02:18.861 WARN 9584 --- [nio-8092-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: JsonObject; nested exception is com.fasterxml.jackson.databind.JsonMappingException: JsonObject (through reference chain: ru.otus.software_architect.eshop_orders.api.Response["body"]->com.google.gson.JsonObject["asBoolean"])]
Spring Boot uses jackson by default to serialize json. In Response object you have field JsonElement body. It is object from gson package and jackson don't know how to serialize that.
Solution: add property (in the application.properties file) to use gson instead of jackson. Note that Spring Boot version is important.
Spring Boot >= 2.3.0.RELEASE:
spring.mvc.converters.preferred-json-mapper=gson
Spring Boot < 2.3.0.RELEASE:
spring.http.converters.preferred-json-mapper=gson
More informations:
Configuring Spring Boot to use Gson instead of Jackson
Spring Boot 2.3 Release Notes
Application.properties
spring.mvc.converters.preferred-json-mapper=gson
I've found a workaround by keeping Jackson but implementing my own Serializer for the classes that cause Jackson serialisation issue.
It's hackish solution but it's working now.
public class GSONObjectSerializer extends SimpleSerializers {
private static final long serialVersionUID = -8745250727467996655L;
private class EmptySerializer extends StdSerializer<Object> {
/**
*
*/
private static final long serialVersionUID = 5435498165882848947L;
protected EmptySerializer(Class t) {
super(t);
}
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
// --> Will write an empty JSON string
gen.writeStartObject();
gen.writeEndObject();
}
}
#Override
public JsonSerializer<?> findSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) {
// --> Here is to setup which class will be serialised by the custom serializer.
if (type.isTypeOrSubTypeOf(JsonObject.class) || type.isTypeOrSubTypeOf(StripeResponse.class)) {
return new EmptySerializer(type.getRawClass());
}
return super.findSerializer(config, type, beanDesc);
}
}
And you can register your serializer like this
#Configuration
public class SerialisationConfig {
#Bean
public ObjectMapper createMapper() {
SimpleModule simpleModule = new SimpleModule();
simpleModule.setSerializers(new GSONObjectSerializer());
return Jackson2ObjectMapperBuilder.json().modules(Arrays.asList(simpleModule)).build();
}
}

TcpOutboundGateway : Cannot correlate response - no pending reply for Cached

I use spring integration for connect to tcp/ip socket server, I created mock server based on the telnet-mock https://github.com/maltempi/telnet-mock . and I can send and received message, but when I shut down mock server, in main application a cyclic error occurs and takes up all the CPU time:
ERROR 13942 --- [pool-4-thread-1] o.s.i.ip.tcp.TcpOutboundGateway : Cannot correlate response - no pending reply for Cached:localhost:3002:46550:f6234e17-c486-4506-82c8-a757a08ba73d.
How can I resolve this problem? My config class:
#EnableIntegration
#RequiredArgsConstructor
#Configuration
public class StpClientConfiguration {
private static final String REQUEST_CHANNEL = "toStp";
private static final String OUTPUT_CHANNEL = "resultToMap";
private static final String CRLF = "\\0";
private final ApplicationProperties applicationProperties;
private final ApplicationContext context;
private static String readUntil(InputStream inputStream, String stopWord) throws IOException {
StringBuilder sb = new StringBuilder();
BufferedReader buffer = new BufferedReader(new InputStreamReader(inputStream));
int r;
while ((r = buffer.read()) != -1) {
char c = (char) r;
sb.append(c);
if (sb.toString().endsWith(stopWord)) {
break;
}
}
return sb.toString();
}
#Bean
public CachingClientConnectionFactory connectionFactory() {
TcpNetClientConnectionFactory factory = new TcpNetClientConnectionFactory(
applicationProperties.getHost(), applicationProperties.getPort());
factory.setApplicationEventPublisher(this.context);
factory.setTcpSocketSupport(new DefaultTcpSocketSupport());
factory.setDeserializer((InputStream inputStream) -> readUntil(inputStream, CRLF));
return new CachingClientConnectionFactory(factory, applicationProperties.getPoolSize());
}
/**
* Creates the tcp gateway for service activation.
*
* #return the message handler
*/
#Bean
#ServiceActivator(inputChannel = REQUEST_CHANNEL)
public MessageHandler outboundGateway() {
TcpOutboundGateway gateway = new TcpOutboundGateway();
gateway.setConnectionFactory(connectionFactory());
gateway.setOutputChannelName(OUTPUT_CHANNEL);
return gateway;
}
#MessagingGateway(defaultRequestChannel = REQUEST_CHANNEL)
public interface RequestGateway {
Map<String, String> send(String message);
}
#Bean
#Transformer(inputChannel = OUTPUT_CHANNEL)
public ObjectToMapTransformer objectToMapTransformer() {
return new ObjectToMapTransformer();
}
}
Your deserializer looks suspicious; telnet messages are terminated with \r\n, not \\0.
Use the default deserializer for telnet (the default is a ByteArrayCrLfSerializer).
When the deserializer detects a normal end of stream (-1), between messages, it must throw a SoftEndOfStreamException to tell the framework the socket is closed. Your code keeps returning a zero length String,
/**
* Used to communicate that a stream has closed, but between logical
* messages.
*/
public class SoftEndOfStreamException extends IOException {
Or, as I said, use the default deserializer.

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

Get principal inside scheduled method for Spring Websocket messaging

Want to send notifications to specific client with websockets. Have a scheduled task for sending notifications, but cannot get Principal inside that task. Found this post, but as I know Spring scheduled methods must be parameter-free.
#Scheduled(fixedDelay=5000)
public void sendMessages(Principal principal)
messagingTemplate
.convertAndSendToUser(principal.getName(), "/queue/horray", "Horray, " + principal.getName() + "!");
}
Is this possible? How can I get websocket principal within scheduled method?
You can not get principal in a scheduled method , because the method call is not initiated by user.
You can follow this approach:
1) Create a websocket end point "/app/events"
2) Let all users to subscribe to that end point
3) Get all the userids you want to send notifications
4) Send notification to single user
simpMessagingTemplate.convertAndSendToUser("userId", "/app/events", "messageEntity");
userId: can be actual user id if authenticated or it can be websocket session id.
I wrote a workaround for this situation. At first I create a listener for websockets events. In case of subscription request I keep the userId from the request and keep in a ConcurrentHashMap. On the other hand when client disconnects or send unsubscribe request I remove his userId from that Map.
My Listener class:
#Service
public class MyEventListener {
#Autowired
private NotificationPublisher notificationPublisher;
#EventListener({SessionSubscribeEvent.class})
public void onWebSocketSubscribeEvent(SessionSubscribeEvent event) {
notificationPublisher.subscribedUsers.put(event.getUser().getName(), Calendar.getInstance().getTimeInMillis());
}
#EventListener({SessionUnsubscribeEvent.class})
public void onWebSocketUnsubscribeEvent(SessionUnsubscribeEvent event) {
notificationPublisher.subscribedUsers.remove(event.getUser().getName());
}
#EventListener({SessionDisconnectEvent.class})
public void onWebSocketDisconnectEvent(SessionDisconnectEvent event) {
notificationPublisher.subscribedUsers.remove(event.getUser().getName());
}
}
Notification publisher class where actual job is running:
public class NotificationPublisher {
public final Map<String, Long> subscribedUsers = new ConcurrentHashMap<>();
#Autowired
private SimpMessagingTemplate messagingTemplate;
#Autowired
private MyService myService;
#Value("${task.notifications.publisher.websocket_timeout_seconds}")
private int websocketSessionTimeout;
public void sendDataUpdates() {
SocketResponseCount count = null;
for(String key: subscribedUsers.keySet()) {
long subscribeTime = subscribedUsers.get(key);
if(Calendar.getInstance().getTimeInMillis() - subscribeTime > websocketSessionTimeout*1000) {
subscribedUsers.remove(key);
continue;
}
count = myService.getNotificationsCount(key);
this.messagingTemplate.convertAndSendToUser(key, "/queue/publish",count);
}
}
}
Maybe it will help someone
my solution: I get all user sessions
#Autowired
private SimpMessagingTemplate template;
#Autowired
private MyRepository myRepository;
#Autowired
private SessionRegistry sessionRegistry;
#Scheduled(fixedRate = 5000)
public void greeting() {
List<SessionInformation> activeSessions = new ArrayList<>();
for (Object principal : sessionRegistry.getAllPrincipals() )
{
activeSessions.addAll( sessionRegistry.getAllSessions( principal, false ) );
}
for (SessionInformation session : activeSessions )
{
Object principalObj = session.getPrincipal();
if ( principalObj instanceof CurrentUser)
{
CurrentUser user = (CurrentUser) principalObj;
this.template.convertAndSendToUser(user.getUsername().toUpperCase(),"/queue/reply2",myRepository.findAll());
}
}
}

Resources