Path variables in Spring WebSockets #SendTo mapping - java

I have, what I think to be, a very simple Spring WebSocket application. However, I'm trying to use path variables for the subscription as well as the message mapping.
I've posted a paraphrased example below. I would expect the #SendTo annotation to return back to the subscribers based on their fleetId. ie, a POST to /fleet/MyFleet/driver/MyDriver should notify subscribers of /fleet/MyFleet, but I'm not seeing this behavior.
It's worth noting that subscribing to literal /fleet/{fleetId} works. Is this intended? Am I missing some piece of configuration? Or is this just not how it works?
I'm not very familiar with WebSockets or this Spring project yet, so thanks in advance.
Controller.java
...
#MessageMapping("/fleet/{fleetId}/driver/{driverId}")
#SendTo("/topic/fleet/{fleetId}")
public Simple simple(#DestinationVariable String fleetId, #DestinationVariable String driverId) {
return new Simple(fleetId, driverId);
}
...
WebSocketConfig.java
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/live");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/fleet").withSockJS();
}
}
index.html
var socket = new SockJS('/fleet');
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
// Doesn't Work
stompClient.subscribe('/topic/fleet/MyFleet', function(greeting) {
// Works
stompClient.subscribe('/topic/fleet/{fleetId}', function(greeting) {
// Do some stuff
});
});
Send Sample
stompClient.send("/live/fleet/MyFleet/driver/MyDriver", {}, JSON.stringify({
// Some simple content
}));

Even though #MessageMapping supports placeholders, they are not exposed / resolved in #SendTo destinations. Currently, there's no way to define dynamic destinations with the #SendTo annotation (see issue SPR-12170). You could use the SimpMessagingTemplate for the time being (that's how it works internally anyway). Here's how you would do it:
#MessageMapping("/fleet/{fleetId}/driver/{driverId}")
public void simple(#DestinationVariable String fleetId, #DestinationVariable String driverId) {
simpMessagingTemplate.convertAndSend("/topic/fleet/" + fleetId, new Simple(fleetId, driverId));
}
In your code, the destination '/topic/fleet/{fleetId}' is treated as a literal, that's the reason why subscribing to it works, just because you are sending to the exact same destination.
If you just want to send some initial user specific data, you could return it directly in the subscription:
#SubscribeMapping("/fleet/{fleetId}/driver/{driverId}")
public Simple simple(#DestinationVariable String fleetId, #DestinationVariable String driverId) {
return new Simple(fleetId, driverId);
}
Update:
In Spring 4.2, destination variable placeholders are supported it's now possible to do something like:
#MessageMapping("/fleet/{fleetId}/driver/{driverId}")
#SendTo("/topic/fleet/{fleetId}")
public Simple simple(#DestinationVariable String fleetId, #DestinationVariable String driverId) {
return new Simple(fleetId, driverId);
}

you can send a variable inside the path. for example i send "este/es/el/chat/java/" and obtaned in the server as "este:es:el:chat:java:"
client:
stompSession.send("/app/chat/este/es/el/chat/java/*", ...);
server:
#MessageMapping("/chat/**")
#SendToUser("/queue/reply")
public WebsocketData greeting(Message m,HelloMessage message,#Header("simpSessionId") String sessionId) throws Exception {
Map<String, LinkedList<String>> nativeHeaders = (Map<String, LinkedList<String>>) m.getHeaders().get("nativeHeaders");
String value= nativeHeaders.get("destination").getFirst().replaceAll("/app/chat/","").replaceAll("/",":");

Actually I think this is what you might be looking for:
#Autorwired
lateinit var template: SimpMessageTemplate;
#MessageMapping("/class/{id}")
#Throws(Exception::class)
fun onOffer(#DestinationVariable("id") id: String?, #Payload msg: Message) {
println("RECEIVED " + id)
template.convertAndSend("/topic/class/$id", Message("The response"))
}
Hope this helps someone! :)

Related

Sending triggers to client using websockets in spring boot and angular

I want to initiate a trigger(maybe a notification) from backend(based in spring boot) to a particular user whose userId is xyz.
the one way i have found is:
initially i connect to a websocket end point and subscribe to channel "/user/Notifications/xyz"
following is the relevant code in my angular typescript
connectToUserWebSocket(userId) {
let socket = new SockJS('http://localhost:5000/fellowGenius');
this.ws = Stomp.over(socket);
let that = this;
this.ws.connect(
{},
(frame) => {
that.ws.subscribe('/user/Notifications/' +userId, (message) => {
console.log("user subscribed");
});
},
(error) => {
alert('STOMP error ' + error);
}
);
}
Now once i have subscribed to my channel . I want to send a trigger to client which is initiated by backend itself so i run a code in my java service.
My relevant java code is:
#SendTo("/user/Notifications/{userId}")
public String sendMeetingNotificationWebSocket(#DestinationVariable String userId) {
return "hello";
}
my websocket configurations are:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer{
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/fellowGenius").setAllowedOrigins("*").addInterceptors(new HttpSessionHandshakeInterceptor()).withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/inbox/","/user/Notifications/");
}
}
But the problem is that even i can see one web socket connected in my spring boot console.
But i don't get a response from the function on the client side.
Please help me with this problem.

Feign Registration - Spring Cloud - Change Target without ribbon over-ride

Introduction
I would like to be able to have two different spring profiles, and depending on the profile to change to a hardcoded address for our feign builders.
Currently was have the following:
return builder.target(cls, "http://" + serviceName);
But I would actually like to do the following and over-ride the address:
return builder.target(cls, "http://our-server:8009/" + serviceName);
Why
Sometimes we don't want to run all the services within our development environment. Additionally, some of the services are only available through a zuul gateway sometimes.
So we run the same code in different situations and conditions.
Technical Details
We have the following code that we use for building our Feign Clients.
We had been using the #FeignClient annotation in the past, but lately we decided to start building our feignClients manually.
Example below:
#FeignClient(name = "ab-document-store", configuration = MultiPartSupportConfiguration.class, fallback = DocumentStoreFallback.class)
We call the feignRegistrar class with the following command:
return registerFeignClient(DocumentStoreClient.class, true);
#RequiredArgsConstructor
//#Component
#Slf4j
public class FeignRegistrar {
#Autowired
private Decoder decoder;
#Autowired
private Encoder encoder;
#Autowired
private Client client;
#Autowired
private Contract feignContract;
#Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
#Autowired
private List<RequestInterceptor> interceptors;
public <T> T register(Class<T> cls, String serviceName, boolean isDocumentStore) {
if(isDocumentStore){
encoder = new MultipartFormEncoder(new SpringEncoder(messageConverters));
}
//Client trustSSLSockets = new Client.Default(getSSLSocketFactory(), new NoopHostnameVerifier());
Feign.Builder builder = Feign.builder()
.client(client)
.encoder(encoder)
.decoder(decoder)
.contract(feignContract)
.logger(new Slf4Logger())
.logLevel(Logger.Level.HEADERS);
builder.requestInterceptor(new RequestInterceptor() {
#Override
public void apply(RequestTemplate template) {
template.header("X-Service-Name", serviceName);
}
});
for(RequestInterceptor interceptor : interceptors) {
builder.requestInterceptor(interceptor);
}
log.debug("Registering {} - as feign proxy ", serviceName);
return builder.target(cls, "http://" + serviceName);
}
public static class Slf4Logger extends Logger {
#Override
protected void log(String configKey, String format, Object... args) {
log.info("{} - {}", configKey, args);
}
}
}
Spring Cloud Property Over-ride
We have also been using property files such as application-ENV.property with entries such as the following:
ab-document-store.ribbon.NIWSServerListClassName:com.netflix.loadbalancer.ConfigurationBasedServerList
ab-document-store.ribbon.listOfServers: localhost:8025
Unfortunately, listOfServers is not enough for us. We would like to be able to assign a directory/path as well. Something like:
ab-document-store.ribbon.listOfServers: localhost:8025/ab-document-store
Otherworkaround
I have thought about sneaking in a header into all requests such as X-SERVICE-NAME using a feign interceptor. Then we could point all services to an address (e.g. localhost:9001) , and forward/proxy those requests to localhost:9001/X-SERVICE-NAME.
However, I would prefer a much easier solution such as:
ab-document-store.ribbon.listOfServers: localhost:8025/ab-document-store
But this doesn't work :(
Introduction
I found a solution for this using a proxy that detects a header.
So, I have a feign interceptor on the java-side that attaches a header x-service-name to every feign-request.
I also have a NodeJS proxy, that analyzes requests, finds x-service-name, and re-writes the requests to become: x-service-name/originalRequestPath.
This allows me to have all the microservices behind a zuul gateway but also access them using a eureka-over-ride.
Java-Feign-Interceptor
Feign.Builder builder = Feign.builder()
.client(client)
.encoder(usedEncoder)
.decoder(decoder)
.contract(feignContract)
.logger(new Slf4Logger())
.logLevel(Logger.Level.HEADERS);
builder.requestInterceptor(new RequestInterceptor() {
#Override
public void apply(RequestTemplate template) {
template.header("X-Service-Name", serviceName);
}
});
NodeJS proxy
In the example, my zuul gateway ( or another proxy ) is on localhost:9001.
I'm listening on localhost:1200 .
let enableProxyForJava = process.env.ENABLE_PROXY_FOR_JAVA;
if (enableProxyForJava != undefined && enableProxyForJava.toLowerCase() === 'true') {
var httpProxyJava = require('http-proxy');
var proxyJava = httpProxyJava.createProxy();
gutil.log( gutil.colors.green('Enabling Proxy for Java. Set your Eureka overrides to localhost:1200.') );
require('http').createServer(function(req, res) {
console.log("req.headers['x-service-name'] = " + req.headers['x-service-name']);
console.log("Before req.url:"+ req.url);
if( req.headers['x-service-name'] != undefined){
let change = req.headers['x-service-name'] +req.url;
console.log("After req.url:"+ change);
req.url = change;
}
proxyJava.web(req, res, {
target: 'http://localhost:9001/'
});
}).listen(1200);
}
Property file inside Java Application that has feign clients
mbak-microservice1.ribbon.NIWSServerListClassName:com.netflix.loadbalancer.ConfigurationBasedServerList
mbak-microservice1.ribbon.listOfServers: localhost:1200
mbak-microservice2.ribbon.NIWSServerListClassName:com.netflix.loadbalancer.ConfigurationBasedServerList
mbak-microservice2.ribbon.listOfServers: localhost:1200
mbak-document-store.ribbon.NIWSServerListClassName:com.netflix.loadbalancer.ConfigurationBasedServerList
mbak-document-store.ribbon.listOfServers: localhost:1200

Spring+WebSocket+STOMP. Message to specific session (NOT user)

I am trying to set up basic message broker on Spring framework, using a recipe I found here
Author claims it has worked well, but I am unable to receive messages on client, though no visible errors were found.
Goal:
What I am trying to do is basically the same - a client connects to server and requests some async operation. After operation completes the client should receive an event. Important note: client is not authenticated by Spring, but an event from async back-end part of the message broker contains his login, so I assumed it would be enough to store concurrent map of Login-SessionId pairs for sending messages directly to particular session.
Client code:
//app.js
var stompClient = null;
var subscription = '/user/queue/response';
//invoked after I hit "connect" button
function connect() {
//reading from input text form
var agentId = $("#agentId").val();
var socket = new SockJS('localhost:5555/cti');
stompClient = Stomp.over(socket);
stompClient.connect({'Login':agentId}, function (frame) {
setConnected(true);
console.log('Connected to subscription');
stompClient.subscribe(subscription, function (response) {
console.log(response);
});
});
}
//invoked after I hit "send" button
function send() {
var cmd_str = $("#cmd").val();
var cmd = {
'command':cmd_str
};
console.log("sending message...");
stompClient.send("/app/request", {}, JSON.stringify(cmd));
console.log("message sent");
}
Here is my configuration.
//message broker configuration
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
/** queue prefix for SUBSCRIPTION (FROM server to CLIENT) */
config.enableSimpleBroker("/topic");
/** queue prefix for SENDING messages (FROM client TO server) */
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/cti")
.setAllowedOrigins("*")
.withSockJS();
}
}
Now, after basic config I should implement an application event handler to provide session-related information on client connect.
//application listener
#Service
public class STOMPConnectEventListener implements ApplicationListener<SessionConnectEvent> {
#Autowired
//this is basically a concurrent map for storing pairs "sessionId - login"
WebAgentSessionRegistry webAgentSessionRegistry;
#Override
public void onApplicationEvent(SessionConnectEvent event) {
StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
String agentId = sha.getNativeHeader("Login").get(0);
String sessionId = sha.getSessionId();
/** add new session to registry */
webAgentSessionRegistry.addSession(agentId,sessionId);
//debug: show connected to stdout
webAgentSessionRegistry.show();
}
}
All good so far. After I run my spring webapp in IDE and connected my "clients" from two browser tabs I got this in IDE console:
session_id / agent_id
-----------------------------
|kecpp1vt|user1|
|10g5e10n|user2|
-----------------------------
Okay, now let's try to implement message mechanics.
//STOMPController
#Controller
public class STOMPController {
#Autowired
//our registry we have already set up earlier
WebAgentSessionRegistry webAgentSessionRegistry;
#Autowired
//a helper service which I will post below
MessageSender sender;
#MessageMapping("/request")
public void handleRequestMessage() throws InterruptedException {
Map<String,String> params = new HashMap(1);
params.put("test","test");
//a custom object for event, not really relevant
EventMessage msg = new EventMessage("TEST",params);
//send to user2 (just for the sake of it)
String s_id = webAgentSessionRegistry.getSessionId("user2");
System.out.println("Sending message to user2. Target session: "+s_id);
sender.sendEventToClient(msg,s_id);
System.out.println("Message sent");
}
}
A service to send messages from any part of the application:
//MessageSender
#Service
public class MessageSender implements IMessageSender{
#Autowired
WebAgentSessionRegistry webAgentSessionRegistry;
#Autowired
SimpMessageSendingOperations messageTemplate;
private String qName = "/queue/response";
private MessageHeaders createHeaders(String sessionId) {
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(sessionId);
headerAccessor.setLeaveMutable(true);
return headerAccessor.getMessageHeaders();
}
#Override
public void sendEventToClient(EventMessage event,String sessionId) {
messageTemplate.convertAndSendToUser(sessionId,qName,event,createHeaders(sessionId));
}
}
Now, let's try to test it. I run my IDE, opened Chrome and created 2 tabs form which I connected to server. User1 and User2. Result console:
session_id / agent_id
-----------------------------
|kecpp1vt|user1|
|10g5e10n|user2|
-----------------------------
Sending message to user2. Target session: 10g5e10n
Message sent
But, as I mentioned in the beginning - user2 got absolutely nothing, though he is connected and subscribed to "/user/queue/response". No errors either.
A question is, where exactly I am missing the point? I have read many articles on the subject, but to no avail.
SPR-11309 says it's possible and should work. Maybe, id-s aren't actual session id-s?
And well maybe someone knows how to monitor if the message actually has been sent, not dropped by internal Spring mechanics?
SOLUTION UPDATE:
A misconfigured bit:
//WebSocketConfig.java:
....
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
/** queue prefix for SUBSCRIPTION (FROM server to CLIENT) */
// + parameter "/queue"
config.enableSimpleBroker("/topic","/queue");
/** queue prefix for SENDING messages (FROM client TO server) */
config.setApplicationDestinationPrefixes("/app");
}
....
I've spent a day debugging internal spring mechanics to find out where exactly it goes wrong:
//AbstractBrokerMessageHandler.java:
....
protected boolean checkDestinationPrefix(String destination) {
if ((destination == null) || CollectionUtils.isEmpty(this.destinationPrefixes)) {
return true;
}
for (String prefix : this.destinationPrefixes) {
if (destination.startsWith(prefix)) {
//guess what? this.destinationPrefixes contains only "/topic". Surprise, surprise
return true;
}
}
return false;
}
....
Although I have to admit I still think the documentation mentioned that user personal queues aren't to be configured explicitly cause they "already there". Maybe I just got it wrong.
Overall it looks good, but could you change from
config.enableSimpleBroker("/topic");
to
config.enableSimpleBroker("/queue");
... and see if this works? Hope this help.

Angular get image from Spring RestController and cache it

I have a Client-Server application using SpringBoot and Angular2.
I would like to load a image from the server by filename. This works fine.
I store the attribute image:string at the client and I place it in the template again.
You might pay attention to return res.url;; I do not use the actual ressource, which might be wrong.
My objective is that image is cached. To my understanding the web-browser can automatically cache the images. Correct?
But the caching does not work yet and maybe somebody could give me a hint what needs to be adjusted?
Is a different header required?
Server (SpringBoot)
public class ImageRestController {
#RequestMapping(value = "/getImage/{filename:.+}", method = RequestMethod.GET)
public ResponseEntity<Resource> getImage(#PathVariable String filename) {
try {
String path = Paths.get(ROOT, filename).toString();
Resource loader = resourceLoader.getResource("file:" + path);
return new ResponseEntity<Resource>(loader, HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<Resource>(HttpStatus.NOT_FOUND);
}
}
}
Client (Angular2)
#Component({
selector: 'my-image',
template: `
<img src="{{image}}"/>
`
})
export class MyComponent {
image:string;
constructor(private service:MyService) {}
showImage(filename:string) {
this.service.getImage(filename)
.subscribe((file) => {
this.image = file;
});
}
}
export class MyService() {
getImage(filename:String):Observable<any> {
return this.http.get(imagesUrl + "getImage/" + filename)
.map(this.extractUrl)
.catch(this.handleError);
}
extractUrl(res:Response):string {
return res.url;
}
}
You could do something like this on the server side (and perhaps add an ETag or Last-Modified header if you can get that information):
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.body(loader);
See the HTTP caching part of the reference documentation in Spring.
If you're just serving resources and not applying any additional logic, then you'd better do the following:
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/getImage/**")
.addResourceLocations("classpath:/path/to/root/")
.setCacheControl(CacheControl.maxAge(1, TimeUnit.DAYS).cachePublic());
}
}
See the other relevant part of the reference documentation. You can also apply transformations and leverage cache busting (see this section as well).

Spring websocket end-poind and send message

I have an JMS listener, and I have to take the message, manipulate it and then redirect it to an page using websocket.
Well, I’m just confused about the configuration, I have configured the WebSocketConfig:
#Configuration
#EnableWebSocketMessageBroker
#EnableScheduling
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws/firstep/").withSockJS();
registry.addEndpoint("/ws/secondep/").withSockJS();
}
And this it should be correct, Then my webpage is:
var socket = new SockJS("/myapp-web/api/ws/secondep/",undefined,options);
var stompClient = Stomp.over(socket);
stompClient.connect({
company : "xxx"
}, function(frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/register', function(message){
console.log('message: ' + message);
});
stompClient.subscribe('/topic/update', function(message){
console.log('message: ' + message);
});
And the connection works.
Now On my jms listener I tried to send a message in this way:
public class ImporterListener implements MessageListener {
Logger logger = LoggerFactory.getLogger(ImporterListner.class);
#SendTo("/topic/register")
private String TestMessage() {
return "TestMessage";
}
#Override
public void onMessage(Message message) {
logger.info("Request on message");
if (message instanceof MapMessage) {
MapMessage t = (MapMessage) message;
TestMessage(); //<--- have to send the message here
But it doesn’t work.
The questions are:
How to send a message?
Where do I have to specify the end point (secondep) when I send a message?
Thank you! any help is appreciated!
TestMessage(); //<--- have to send the message here
No, it has't to send, because you use method from the same class, but #SendTo makes your ImporterListener as proxy and Advice will work on method only from another component.
You should inject this:
#Autowired
#Qualifier("brokerMessagingTemplate")
private MessageSendingOperations brokerMessagingTemplate;
And send a message using that:
brokerMessagingTemplate.convertAndSend("/topic/register", "TestMessage");
Where do I have to specify the end point (secondep) when I send a message?
It is for the #MessageMapping("/ws/secondep") on some POJO method to receive message from clients. It isn't for the sending part.

Categories

Resources