I have a class which extends TextWebSocketHandler
and it's main purpose is to send messages to client after a certain event with WebSocketSession
. To implement this I've decided to use the method interception provided by Spring AOP.
The issue is when the interceptor is called the incapsulated instance of WebSocketSession
becomes null
even though before it was instantiated in method afterConnectionEstablished()
(which can be seen with debugger). Interesting thing is that in every other method of the class the session instance can be accessed.
Here is the code of the class:
@Aspect
public class SystemStateWS extends TextWebSocketHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(SystemStateWS.class);
private WebSocketSession session;
public SystemStateWS() {
}
@Override
public synchronized void afterConnectionEstablished(WebSocketSession session) throws Exception {
this.session = session;
}
@Override
protected synchronized void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
System.out.println(message.getPayload());
}
@Before("execution(* org.company.MyRestService.processTablesClearQuery(..))")
public void onConfigurationStart(JoinPoint joinPoint) {
if (session.isOpen()) { // session is null here
try {
session.sendMessage(new TextMessage("config started"));
LOGGER.debug("Configuration status message was sent to client");
} catch (IOException e) {
LOGGER.warn("Error during the passing of the message to client");
}
} else {
LOGGER.warn("Unable to send message to client: session is closed");
}
}
@Override
public synchronized void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
LOGGER.debug("Socket is closing");
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
super.handleTransportError(session, exception);
LOGGER.warn("Transport error", exception);
}
}
And the context configuration file:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">
<context:component-scan base-package="org.company" />
<aop:aspectj-autoproxy/>
...
<websocket:handlers>
<websocket:mapping path="/ws/sysState" handler="sysStateHandler" />
<websocket:handshake-handler ref="handshakeHandler"/>
</websocket:handlers>
<bean class="org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean">
<property name="maxTextMessageBufferSize" value="335544"/>
<property name="maxBinaryMessageBufferSize" value="335544"/>
</bean>
<bean id="handshakeHandler" class="org.springframework.web.socket.server.support.DefaultHandshakeHandler">
<constructor-arg ref="upgradeStrategy"/>
</bean>
<bean id="upgradeStrategy" class="org.springframework.web.socket.server.standard.TomcatRequestUpgradeStrategy">
</bean>
<bean id="sysStateHandler" class="org.springframework.web.socket.handler.PerConnectionWebSocketHandler">
<constructor-arg type="java.lang.Class" value="org.company.SystemStateWS"/>
</bean>
<bean id="systemStateWS" class="org.company.SystemStateWS"/>
</beans>
MyRestService.processTablesClearQuery
referenced in @Before
is actually the method in one of the application's REST controllers but I don't think that matters much here.
Unfortunately I'm not familiar with aspects that much. To begin with, I don't even know if it's possible to combine socket handler with @Aspect
. And if it's possible after all what is the reason why WebSocketSession
becomes null
an how can I possibly avoid it? Any help is appreciated.
So the actual reason of that behaviour was the existense of many instances of SystemStateWS
at the moment of triggering of the interceptor. And the interceptor just triggered exactly in the instance where method afterConnectionEstablished()
was never called and so the reference of WebSocketSession
continued to stay null
here.
So I've decided to register each instance of my web socket class whenever method afterConnectionEstablished()
is called in one special bean and then made standalone class for the interceptor. When it is called now, it just calls the necessary method in all registered web socket instances. This code did the trick for me:
Registration of the socket class in buffer:
@Autowired
private SystemStateWebSocketBuffer buffer;
//...
@Override
public synchronized void afterConnectionEstablished(WebSocketSession session)
throws Exception {
this.session = session;
buffer.add(this);
}
SystemStateWebSocketBuffer.java
@Component
public class SystemStateWebSocketBuffer {
private List<SystemStateWS> systemStateSockets = new ArrayList<>();
public void add(SystemStateWS systemStateWS) {
this.systemStateSockets.add(systemStateWS);
}
public List<SystemStateWS> getSystemStateSockets() {
return systemStateSockets;
}
public void setSystemStateSockets(List<SystemStateWS> systemStateSockets) {
this.systemStateSockets = systemStateSockets;
}
}
SystemStateRestInterceptor.java
@Aspect
public class SystemStateRestInterceptor {
@Autowired
private SystemStateWebSocketBuffer systemStateWebSocketBuffer;
@Before("execution(* org.company.MyRestService.processTablesClearQuery(..))")
public void interceptConfigStartEvent() {
systemStateWebSocketBuffer.getSystemStateSockets()
.forEach(socket -> socket.onConfigurationStart());
}
}