I have to add support for a custom WebSocket subprotocol (so not STOMP) in a Spring Boot application, but I'm having a very hard time understanding what I need to provide and what Spring already has.
This is how far I got:
#Configuration
#EnableWebSocket
public class WebSocketAutoConfiguration implements WebSocketConfigurer {
public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
webSocketHandlerRegistry.addHandler(this.webSocketHandler(), new String[]{endpointUrl});
}
#Bean
public WebSocketHandler webSocketHandler() {
ExecutorSubscribableChannel clientInboundChannel = new ExecutorSubscribableChannel();
ExecutorSubscribableChannel clientOutboundChannel = new ExecutorSubscribableChannel();
SubProtocolWebSocketHandler subProtocolWebSocketHandler = new SubProtocolWebSocketHandler(clientInboundChannel, clientOutboundChannel);
subProtocolWebSocketHandler.addProtocolHandler(new SubProtocolHandler() {
public List<String> getSupportedProtocols() {
return Collections.singletonList("custom-protocol");
}
public void handleMessageFromClient(WebSocketSession session, WebSocketMessage<?> message, MessageChannel outputChannel) throws Exception {
session.sendMessage(new TextMessage("some message"));
}
public void handleMessageToClient(WebSocketSession session, Message<?> message) throws Exception {
}
public String resolveSessionId(Message<?> message) {
return UUID.randomUUID().toString();
}
public void afterSessionStarted(WebSocketSession session, MessageChannel outputChannel) throws Exception {
System.out.println("SESSION STARTED");
}
public void afterSessionEnded(WebSocketSession session, CloseStatus closeStatus, MessageChannel outputChannel) throws Exception {
session.close();
System.out.println("SESSION ENDED");
}
});
return subProtocolWebSocketHandler;
}
}
This works, in the sense that handleMessageFromClient does get triggered on a web socket message, but I fail to understand the purpose of MessageChannel outputChannel and handleMessageToClient.
Is it possible to get the PerConnectionWebSocketHandler semantics with SubProtocolWebSocketHandler?
The documentation around this is basically non-existent e.g. the docs for handleMessageToClient say:
Handle the given {#link Message} to the client associated with the given WebSocket session.
Well, fantastic. And the STOMP implementations are mind-boggling, so they're not very usable as a guideline.
Any example, broad steps or anything, really, would be much appreciated.
Turns out it is exceptionally easy. No need to mess with SubProtocolWebSocketHandler at all. The only requirement is that the provided WebSocketHandler implements SubProtocolCapable.
public class CustomHandler implements WebSocketHandler, SubProtocolCapable {
...
}
That's all. To make a PerConnectionWebSocketHandler, it's enough to simply extend it and implement SubProtocolCapable:
public class CustomHandler extends PerConnectionWebSocketHandler implements SubProtocolCapable {
...
}
Related
Probably something simple, but I can't figure it out on my own. I have some sample of Spring Boot WebSockets implementation and wanted to display total active sessions. So I created #Scheduled activeSessions task, which should display actual count, but it's always 0. When afterConnectionEstablished is called I get expected sessions size. Whats the catch?
#Configuration
public class Monitoring extends TextWebSocketHandler {
private List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();
#Override
protected void handleTextMessage(WebSocketSession session, TextMessage message)
throws Exception {
String clientMessage = message.getPayload();
System.out.println(clientMessage);
sessions.forEach(s -> {
try {
s.sendMessage(new TextMessage("Hello! You session id is: " + s.getId()));
activeSessions();
} catch (IOException e) {
e.printStackTrace();
}
});
}
#Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
//the messages will be broadcasted to all users.
System.out.println("Adding new session.");
sessions.add(session);
System.out.println("Current session count: " + sessions.size());
}
#Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
sessions.remove(session);
}
#Scheduled(fixedRate = 2000)
public void activeSessions() {
System.out.println("Total sessions: " + sessions.size());
}
}
Configuration part:
#Configuration
#EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
#Override
public void registerWebSocketHandlers(#NotNull WebSocketHandlerRegistry registry) {
registry.addHandler(new Monitoring(), "/socket");
}
}
Due conflict described, custom scheduler:
#Configuration
#EnableScheduling
public class SchedulingConfig {
// https://stackoverflow.com/questions/49343692/websocketconfigurer-and-scheduled-are-not-work-well-in-an-application
#Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(10);
taskScheduler.initialize();
return taskScheduler;
}
}
You currently have 2 separate instances of the Monitoring class. One created by yourself, doing the request handling (which isn't a Spring managed bean!) and another one detected by Spring due to the #Configuration (shouldn't that be an #Component?).
Remove the #Configuration and replace it with an #Bean method, such that your WebSocketConfig looks like the following
#Configuration
#EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
#Override
public void registerWebSocketHandlers(#NotNull WebSocketHandlerRegistry registry) {
registry.addHandler(monitoring(), "/socket");
}
#Bean
public Monitoring monitoring() {
return new Monitoring();
}
}
You now have a single instance of the bean, managed by Spring.
I'm in the process of learning how to use the Java Spring Framework and started experimenting with Spring Integration. I'm trying to use Spring Integration to connect my application to an MQTT broker both to publish and subscribe to messages but I'm having trouble finding a way to manually publish messages to an outbound channel. If possible I want to build it using notations in the java code exclusively rather than xml files defining beans and other related configuration.
In every example I've seen the solution to manually publishing a message seems to be to use a MessagingGateway Interface and then use the SpringApplicationBuilder to get the ConfigurableApplicationContext to get a reference to the gateway interface in the main method. The reference is then used to publish a message. Would it be possible to use AutoWired for the interface instead? In my attempts I just get a NullPointer.
My aim is to build a game where I subscribe to a topic to get game messages and then whenever the user is ready to make the next move, publish a new message to the topic.
Update:
This is one of the examples I've been looking at of how to setup an outbound channel: https://docs.spring.io/spring-integration/reference/html/mqtt.html
Update 2 after answer from Gary Russel:
This is some example code I wrote after looking at examples which gets me a NullPointer when using #AutoWired for the Gateway when running gateway.sendToMqtt in Controller.java. What I want to achieve here is to send an mqtt message manually when a GET request is handled by the controller.
Application.java
#SpringBootApplication
public class Application {
public static void main(String[] args){
SpringApplication.run(Application.class, args);
}
}
Controller.java
#RestController
#RequestMapping("/publishMessage")
public class Controller {
#Autowired
static Gateway gateway;
#RequestMapping(method = RequestMethod.GET)
public int request(){
gateway.sendToMqtt("Test Message!");
return 0;
}
}
MqttPublisher.java
#EnableIntegration
#Configuration
public class MqttPublisher {
#Bean
public MqttPahoClientFactory mqttClientFactory(){
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
factory.setServerURIs("tcp://localhost:1883");
return factory;
}
#Bean
#ServiceActivator(inputChannel = "mqttOutboundChannel")
public MessageHandler mqttOutbound(){
MqttPahoMessageHandler messageHandler =
new MqttPahoMessageHandler("clientPublisher", mqttClientFactory());
messageHandler.setAsync(true);
messageHandler.setDefaultTopic("topic");
return messageHandler;
}
#Bean
public MessageChannel mqttOutboundChannel(){
return new DirectChannel();
}
#MessagingGateway(defaultRequestChannel = "mqttOutboundChannel")
public interface Gateway {
void sendToMqtt(String data);
}
}
Update:
Not sure if this is the proper logging but it is what I get from adding:
logging.level.org.springframework.web=Debug
logging.level.org.hibernate=Error
to application.properties.
https://hastebin.com/cuvonufeco.hs
Use a Messaging Gateway or simply send a message to the channel.
EDIT
#SpringBootApplication
public class So47846492Application {
public static void main(String[] args) {
SpringApplication.run(So47846492Application.class, args).close();
}
#Bean
public ApplicationRunner runner(MyGate gate) {
return args -> {
gate.send("someTopic", "foo");
Thread.sleep(5_000);
};
}
#Bean
#ServiceActivator(inputChannel = "toMqtt")
public MqttPahoMessageHandler mqtt() {
MqttPahoMessageHandler handler = new MqttPahoMessageHandler("tcp://localhost:1883", "foo",
clientFactory());
handler.setDefaultTopic("myTopic");
handler.setQosExpressionString("1");
return handler;
}
#Bean
public MqttPahoClientFactory clientFactory() {
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
factory.setUserName("guest");
factory.setPassword("guest");
return factory;
}
#Bean
public MqttPahoMessageDrivenChannelAdapter mqttIn() {
MqttPahoMessageDrivenChannelAdapter adapter =
new MqttPahoMessageDrivenChannelAdapter("tcp://localhost:1883", "bar", "someTopic");
adapter.setOutputChannelName("fromMqtt");
return adapter;
}
#ServiceActivator(inputChannel = "fromMqtt")
public void in(String in) {
System.out.println(in);
}
#MessagingGateway(defaultRequestChannel = "toMqtt")
public interface MyGate {
void send(#Header(MqttHeaders.TOPIC) String topic, String out);
}
}
After some time I got my WebSocket running with this config:
#Configuration
//#EnableWebSocket
public class WebSocketServerConfig implements WebSocketConfigurer {
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/var").setAllowedOrigins("*");
}
#Bean
public WebSocketHandler myHandler() {
return new WebsocketServer();
}
}
But I'm unable to find a way to launch this WebSocket from my code.
Is there a way to launch this WebSocket later in my program?
I found this document but it does not provide a way to implement a startWebSocket() function or something similar.
The document you linked shows an example, where it returns an instance of EchoWebSocketHandler.
You could accept connections and implement your own logic for afterConnectionEstablished or handleMessage using some internal status to determine whether you should accept requests, i.e.
#Configuration
#EnableWebSocket
public class MyConfiguration implements WebSocketConfigurer {
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoWebSocketHandler(), "/echo").withSockJS();
}
#Bean
public WebSocketHandler echoWebSocketHandler() {
//FooWebSocketHandler implements your specific logic
return new FooWebSocketHandler();
}
}
public class FooWebSocketHandler extends AbstractWebSocketHandler {
private boolean enabled;
...
public void handleMessage(WebSocketSession session,
WebSocketMessage<?> message)
throws Exception {
if (enabled) {//work
} else {
//disabled, i.e. throw exception or send data according to your api
}
}
}
Spring allows interception of messages for many of their products, like RestTemplate and SpringMVC. Is it possible to intercept Spring Cloud Stream messages? For both incoming and outgoing messages.
Was able to intercept inbound and outbound Spring Cloud Stream messages using the GlobalChannelInterceptor annotation and ChannelInterceptor interface. See sample below.
import org.springframework.integration.config.GlobalChannelInterceptor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.stereotype.Component;
#Component
#GlobalChannelInterceptor
public class Interceptor implements ChannelInterceptor {
private final Logger log = LoggerFactory.getLogger(Interceptor.class);
#Override
public Message<?> preSend(Message<?> msg, MessageChannel mc) {
log.info("In preSend");
return msg;
}
#Override
public void postSend(Message<?> msg, MessageChannel mc, boolean bln) {
log.info("In postSend");
}
#Override
public void afterSendCompletion(Message<?> msg, MessageChannel mc, boolean bln, Exception excptn) {
log.info("In afterSendCompletion");
}
#Override
public boolean preReceive(MessageChannel mc) {
log.info("In preReceive");
return true;
}
#Override
public Message<?> postReceive(Message<?> msg, MessageChannel mc) {
log.info("In postReceive");
return msg;
}
#Override
public void afterReceiveCompletion(Message<?> msg, MessageChannel mc, Exception excptn) {
log.info("In afterReceiveCompletion");
}
}
Not sure what you mean by interception here - both examples you give are not message-based :).
But you want to get access to the full message, you can use that as argument to a #StreamListener or #ServiceActivator-annotated method. Also, Spring Cloud Stream allows you to set up a full Spring Integration pipeline, so you can add advices and everything you need - see here: https://github.com/spring-projects/spring-integration-java-dsl/wiki/Spring-Integration-Java-DSL-Reference.
I would encourage you to take a look at the Spring Integration reference as well http://docs.spring.io/autorepo/docs/spring-integration/4.2.6.RELEASE/reference/html/. Spring Cloud Stream injects the channels automatically, and from there you have full freedom on how you construct your pipeline.
Hope this helps,
Marius
The FailedMessageAspect.afterMethod() below gets called successfully during RabbitConsumerMain.main() below. However, it doesn't get called when it's used in the context of listening for a RabbitMQ message - when MessageHandlerImpl.handleMesasge() receives a message from a RabbitMQ queue. Any idea why?
FailedMessageAspect.java
#Aspect
#Component
public class FailedMessageAspect {
#AfterReturning("execution(* com..MessageHandlerImpl.testAspect(..))")
private void afterMethod() {
System.out.println("aspect foo");
}
}
MessageHandlerImpl.java
#Component
public class MessageHandlerImpl implements MessageHandler {
#Override
public void testAspect() {
System.out.println("handler foo");
}
#Override
public void handleMessage(String message) {
// handleMessage is called successfully when message is received
testAspect();
// FailedMessageAspect.afterMethod() does not get called
}
}
RabbitConsumerMain.java
#Controller
#SpringBootApplication
public class RabbitConsumerMain implements CommandLineRunner {
#Autowired
private MessageHandler messageHandler;
public static void main(String[] args) throws Exception {
SpringApplication.run(RabbitConsumerMain.class, args);
}
#Override
public void run(String... args) {
messageHandler.testAspect();
//FailedMessageSpect.afterMethod() gets called right here
}
}
ConsumerConfiguration.java
#Configuration
public class ConsumerConfiguration {
#Autowired #Lazy
private MessageHandler messageHandler;
//other standard AMQP configs
#Bean
public MessageListenerContainer messageListenerContainer() {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory());
container.setQueues(workQueue());
MessageListenerAdapter adapter = new MessageListenerAdapter(messageHandler, new Jackson2JsonMessageConverter());
container.setMessageListener(adapter);
return container;
}
}
You don't show all your configuration but, just to be clear, Spring AOP does not advise internal method calls such as handleMessage calling testAspect() within the same class instance.
You need to use AspectJ for that; otherwise, all methods you advise must be public methods invoked via bean definitions, so Spring can invoke the method via a proxy. Internal calls within a bean are never advised.
See the reference manual for a complete explanation.