Building custom API over Spring Websockets - spring

I'm have to implement custom API over Websockets that requires:
Custom WAMP-like subprotocol
Path parameters in socket URI
So I've following questions:
Is there any documentation or guides on implementing custom subprotocols in Spring? Protocol requires that exact version must be specified in the Sec-Websocket-Protocol field. Where this field could be read on server side?
What is a proper way to pass path parameters into a message handler? I could use ant patterns in handler registration
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(customHandler(), "/api/custom/{clientId}");
}
but those seems not available at TextWebSocketHandler. I'm solved this for now by extending default HttpSessionHandshakeInterceptor in a following way:
public class CustomHandshakeInterceptor extends HttpSessionHandshakeInterceptor {
private static final UriTemplate URI_TEMPLATE = new UriTemplate("/api/custom/{clientId}");
#Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
Map<String, String> segments = URI_TEMPLATE.match(request.getURI().getPath());
attributes.put("CLIENTID", segments.get("clientId"));
return super.beforeHandshake(request, response, wsHandler, attributes);
}
}
and then accessing it in TextWebSocketHandler:
public class CustomHandler extends TextWebSocketHandler {
#Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
super.handleTextMessage(session, message);
String clientId = session.getAttributes().get("CLIENTID");
...
session.sendMessage(response);
}
}
but this method, in my opinion, is a bit clunky. Is there more proper way to solve this?
Thanks.

The best advice I could give is to follow the example of the sub-protocol support that's built in -- starting with SubProtocolWebSocketHandler and the SubProtocolHandler's it delegates to including the StompSubProtocolHandler implementation. The SubProtocolWebSocketHandler is further connected to "clientInbound" and "clientOutbound" channels which are then used to form a processing flow as well as to provide thread boundaries.
There is a description for the processing flow for STOMP http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-message-flow which includes delegating messages to annotated controllers and/or to a message broker which can also send messages back downstream to clients.
Essentially the StompSubProtocolHandler translates to and from a WebSocketMessage and a Spring Message with protocol-specific content. So that controllers, message brokers, or any other consumer of the messages from the client inbound channel are decoupled and unaware from the WebSocket transport layer. Many of the facilities built around the building, sending, and processing of such sub-protocol messages are meant to be potentially usable for support of other STOMP-like protocols. That includes all the classes in the org.springframework.messaging.simp package.
As for URL path parameters, Spring doesn't provide anything at the WebSocket level which is mostly a transport layer. Most of the interesting stuff happens at the sub-protocol level. For example for STOMP a MessageMapping is supported based on the destination header along with a #DestinationVariable which is comparable to using #PathVariable in Spring MVC but based on the destination header, not the URL.

Related

How to intercept a RequestBody before RestController and do some business rules handling from another microservice?

Basically, we have a big monolithic application built on Spring Boot v1.2 and we would like to handle some business rules processing from a MicroService (let's call it BR engine) built on Spring Boot v2.1.6.
How can I intercept the requestBody and send it first to BR engine and then once done, it will either proceed to the actual handler (Monolithic Controller) or not based from BR engine results - for simplicity let's say BR engine returns either true or false. If true, proceed to actual handler if false, return an exception.
I wanted to use HandlerInterceptorAdapter however, not sure how I can intercept the requestBody and pass it to a microservice - then from the results it will either proceed or not to the actual handler.
As an example, let's say I have a POST mapping to the Monolithic controller:
#PostMapping("/save")
public ResponseEntity<Client> save(#RequestBody ClientDTO dto) {
log.debug("Saving...");
Client newClient = Client.builder().build();
BeanUtils.copyProperties(dto, newClient);
return new ResponseEntity<>(clientService.save(newClient), HttpStatus.OK);
}
Now I wanted to intercept the ClientDTO requestBody and send it first to the BR engine and do some stuff from there. I have thought of using an interceptor and add it to my config which implements WebMvcConfigurer. However, I am not sure how can perform a restTemplate here and get a response from BR engine of pass or fail - if fail the actual handler will be skipped and just throw an exception
#Component
public class RuleEngineInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// intercept requestBody then send it to BR engine
// but how? and how can I get the response from BR engine
// to decide whether it will proceed to actual handler
// or not - probably return an exception.
return true;
}

Create TCP client to send and receive in spring boot application

I am supposed to interact with a legacy system where I have to setup a TCP client using spring-integration in java/kotlin to send a message to a TCP system and receive its response, parse this response and send it to other client via REST. I went through many documentations and blogs which tells how to do via xml. Not able to find corresponding annotations for everything. Any code snippet will be very helpful.
#Service
class MyService{
#Autowired
MyGateway gateway;
public String callTCPClient(String msg){
return gateway.exchange(msg);
}
}
interface MyGateway{
String exchange (String msg)
}
As is shown in that answer, you can do whatever you want after the response is received...
#Bean
public IntegrationFlow client() {
return IntegrationFlows.from(MyGateway.class)
.handle(Tcp.outboundGateway(
Tcp.netClient("localhost", 1234)
.serializer(codec()) // default is CRLF
.deserializer(codec()))) // default is CRLF
.transform(Transformers.objectToString()) // byte[] -> String
.get();
}
In this case, we simply transform the byte array to a String, but you can perform whatever operations you want on it, e.g. JSON to Object. You can add as many steps as you want - add .handle(...) elements to call arbitrary methods. Read the Spring Integration Reference Manual.

issue with Spring and asynchronous controller + HandlerInterceptor + IE/Edge

I am working on a Spring application that serves up REST endpoints. One of the endpoints essentially acts as a proxy between the HTML client and a third party cloud storage provider. This endpoint retrieves files from the storage provider and proxies them back to the client. Something like the following (note there is a synchronous and asynchronous version of the same endpoint):
#Controller
public class CloudStorageController {
...
#RequestMapping(value = "/fetch-image/{id}", method = RequestMethod.GET, produces = MediaType.IMAGE_JPEG_VALUE)
public ResponseEntity<byte[]> fetchImageSynchronous(#PathVariable final Long id) {
final byte[] imageFileContents = this.fetchImage(id);
return ResponseEntity.ok().body(imageFileContents);
}
#RequestMapping(value = "/fetch-image-async/{id}", method = RequestMethod.GET, produces = MediaType.IMAGE_JPEG_VALUE)
public Callable<ResponseEntity<byte[]>> fetchImageAsynchronous(#PathVariable final Long id) {
return () -> {
final byte[] imageFileContents = this.fetchImage(id);
return ResponseEntity.ok().body(imageFileContents);
};
}
private byte[] fetchImage(final long id) {
// fetch the file from cloud storage and return as byte array
...
}
...
}
Due to the nature of the client app (HTML5 + ajax) and how this endpoint is used, user authentication is supplied to this endpoint differently that the other endpoints. To handle this, a HandlerInterceptor was developed to deal with authentication for this endpoint:
#Component("cloudStorageAuthenticationInterceptor")
public class CloudStorageAuthenticationInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) {
// examine the request for the authentication information and verify it
final Authentication authenticated = ...
if (authenticated == null) {
try {
pResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
} catch (IOException e) {
throw new RuntimeException(e);
}
return false;
}
else {
try {
request.login(authenticated.getName(), (String) authenticated.getCredentials());
} catch (final ServletException e) {
throw new BadCredentialsException("Bad credentials");
}
}
return true;
}
}
The interceptor is registered like this:
#Configuration
#EnableWebMvc
public class ApiConfig extends WebMvcConfigurerAdapter {
#Autowired
#Qualifier("cloudStorageAuthenticationInterceptor")
private HandlerInterceptor cloudStorageAuthenticationInterceptor;
#Override
public void addInterceptors(final InterceptorRegistry registry) {
registry.addInterceptor(this.cloudStorageAuthenticationInterceptor)
.addPathPatterns(
"/fetch-image/**",
"/fetch-image-async/**"
);
}
#Override
public void configureAsyncSupport(final AsyncSupportConfigurer configurer) {
final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(this.asyncThreadPoolCoreSize);
executor.setMaxPoolSize(this.asyncThreadPoolMaxSize);
executor.setQueueCapacity(this.asyncThreadPoolQueueCapacity);
executor.setThreadNamePrefix(this.asyncThreadPoolPrefix);
executor.initialize();
configurer.setTaskExecutor(executor);
super.configureAsyncSupport(configurer);
}
}
Ideally, the image fetching would be done asynchronously (using the /fetch-image-asyc/{id} endpoint) because it has to call a third party web service which could have some latency.
The synchronous endpoint (/fetch-image/{id}) works correctly for all browsers. However, if using the asynchronous endpoint (/fetch-image-async/{id}), Chrome and Firefox work as expect.
However, if the client is Microsoft IE or Microsoft Edge, we seem some strange behavior. The endpoint is called correctly and the response sent successfully (at least from the server's viewpoint). However, it seems that the browser is waiting for something additional. In the IE/Edge DevTools window, the network request for the image shows as pending for 30 seconds, then seems to timeout, updates to successful and the image is successfully display. It also seems the connection to the server is still open, as the server side resources like database connections are not released. In the other browsers, the async response is received and processed in a second or less.
If I remove the HandlerInterceptor and just hard-wire some credentials for debugging, the behavior goes away. So this seems to have something to with the interaction between the HandlerInterceptor and the asynchronous controller method, and is only exhibited for some clients.
Anyone have a suggestion on why the semantics of IE/Edge are causing this behavior?
Based on your description, there are some different behaviors when using IE or Edge
it seems that the browser is waiting for something additional
the connection seems still open
it works fine if remove HandlerInterceptor and use hard code in auth logic
For the first behavior, I would suggest you use fiddler to trace all http requests. It is better if you could compare two different actions via fiddler (1) run on chrome, 2) run on edge ). Check all http headers in requests and responses carefully to see whether there is some different part. For the other behaviors, I would suggest you write logs to find which part spend the most time. It will provide you useful information to troubleshot.
After much tracing on the server and reading through the JavaDocs comments for AsyncHandlerInterceptor, I was able to resolve the issue. For requests to asynchronous controller methods, the preHandle method of any interceptor is called twice. It is called before the request is handed off to the servlet handling the request and again after the servlet has handled the request. In my case, the interceptor was attempting to authenticate the request for both scenarios (pre and post request handling). The application's authentication provider checks credentials in a database. For some reason if the client is IE or Edge, the authentication provider was unable to get a database connection when called from preHandle in the interceptor after the servlet handled the request. The following exception would be thrown:
ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DataAccessResourceFailureException: Could not open connection; nested exception is org.hibernate.exception.JDBCConnectionException: Could not open connection] with root cause
java.sql.SQLTransientConnectionException: HikariPool-0 - Connection is not available, request timed out after 30001ms.
So the servlet would successfully handle the request and send a response, but the filter would get hung up for 30 seconds waiting for the database connection to timeout on the post processing called to preHandle.
So for me, the simple solution was to add a check in preHandle if it is being called after the servlet has already handled the request. I updated the preHandle method as follows:
#Override
public boolean preHandle(final HttpServletRequest pRequest, final HttpServletResponse pResponse, final Object pHandler) {
if (pRequest.getDispatcherType().equals(DispatcherType.REQUEST)) {
... perform authentication ...
}
return true;
}
That solved the issue for me. It doesn't explain everything (i.e., why only IE/Edge would cause the issue), but it seems that preHandle should only do work before the servlet handles the request anyways.

Spring integration headers on message events

I have a simple TCP connection factory implemented in Spring Integration:
#Bean
#ServiceActivator(inputChannel = "toTcpChannel")
public TcpSendingMessageHandler tcpOutClient() throws Exception {
TcpSendingMessageHandler sender = new TcpSendingMessageHandler();
sender.setConnectionFactory(clientFactory());
sender.setClientMode(false);
sender.afterPropertiesSet();
return sender;
}
#Bean
public AbstractClientConnectionFactory clientFactory() {
final TcpNioClientConnectionFactory factory = new TcpNioClientConnectionFactory(tcpHost, tcpPort);
factory.setSingleUse(true);
return factory;
}
#EventListener
public void handleTcpConnectionOpenEvent(TcpConnectionOpenEvent event) throws Exception {
LOGGER.info("TCP connection OPEN event: {}", event.getConnectionId());
// HERE I would like to have "myCustomID" header here.
}
I am looking for getting the custom ID that I am providing via Gateway in the produced TcpConnectionOpenEvent (or similar via interceptors)
#Gateway(requestChannel="toTcpChannel")
public void sendToTcp(#Payload String message, #Header("myCustomID") Long myCustomID);
I know this is an event not a message but I do know how to get the Connection ID that I will receive in the input channel in any other way.
I am creating a type of hash map of my custom id – connection id.
I cannot use a custom correlation via aggregator because the response message will not contain any information about the previously sent message. Any suggestions will be welcome.
Oh! I see. Not sure what you are going to do from your custom TcpSendingMessageHandler, but as far as ApplicationEventPublisher is single-threaded, you can store the connectionId in the ThreadLocal variable and obtain it from there after send operation.

Spring send message to Websocket Message Broker

I want to send a message to websocket subscribers of a specific record - when an action takes place in one of my service class.
I'm trying to read the Spring Websocket documentation but it's kind of ambiguous to the point of how to get all these things working together.
Here are my setup files (this is extending jHipster btw):
WebsocketConfiguration.java
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableStompBrokerRelay("/queue/", "/topic/", "/exchange/");
config.setApplicationDestinationPrefixes("/app");
config.setPathMatcher(new AntPathMatcher("."));
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS();
}
WebsocketSecurity.java
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
// message types other than MESSAGE and SUBSCRIBE
.nullDestMatcher().authenticated()
// matches any destination that starts with /rooms/
.simpDestMatchers("/topic/tracker").hasAuthority(AuthoritiesConstants.ADMIN)
.simpDestMatchers("/topic/**").authenticated()
// (i.e. cannot send messages directly to /topic/, /queue/)
// (i.e. cannot subscribe to /topic/messages/* to get messages sent to
// /topic/messages-user<id>)
.simpTypeMatchers(SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE).denyAll()
// catch all
.anyMessage().denyAll();
}
Controller class (attempt at implementing a simple broker I can test subscribing to from sockjs and recieving messages generated elsewhere in the application:
#MessageMapping("/ws")
#SendTo("/topic/sendactivity.{id}")
public void activity(#DestinationVariable string id, #Payload String message){
log.debug("Sending command center: "+message);
}
#RequestMapping(value = "/updateactivity", method = RequestMethod.PUT)
public ResponseEntity<Membership> updateMembership(
#RequestBody Membership membership) throws URISyntaxException {
// ...
String testString = "test";
messagingTemplate.convertAndSend("/topic/commandcenter"+membership.getId().toString(), testString);
// ...
}
When I put a breakpoint on the public void activity method, I don't get anything?
Sending a message to "/topic/commandcenterID" using the messaging template will send that message to the message broker, which will dispatch that message to clients subscribed to that topic. So it won't flow through your activity method.
When using #MessageMapping annotated methods, you're declaring those as application destinations. So sending a message to "/app/ws" should map to that method. Note that in that case I doubt it'll work since the destination variable you're expecting as a method argument is missing from the path definition in the #MessageMapping annotation.
Also, the #SendTo annotation in fact tells Spring that the value returned by the method should be converted to a message and sent to the given destination.
It seems you're mixing things up here, and I think you should:
read carefully the flow of messages in Spring STOMP support
look at a few example apps like the websocket portfolio and websocket chat

Resources