Springboot @ServerEndPoint "Failed to find the root WebApplicationContext."

Y.M. picture Y.M. · May 27, 2015 · Viewed 9.7k times · Source

I'm having trouble using spring with @ServerEndPoint annotated class

i'm using Springboot 1.2.3 and i'm trying to figure it out how to have a single instance of the endpoint

@SpringBootApplication
@EnableJpaRepositories
@EnableWebSocket
public class ApplicationServer {
    public static void main(String[] args) {
        SpringApplication.run(ApplicationServer.class, args);
    }
}

Spring configuration:

@ConditionalOnWebApplication
@Configuration
public class WebSocketConfigurator {

    @Bean
    public ServerEndPoint serverEndpoint() {
        return new ServerEndPoint();
    }

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

WebSocket endpoint:

@ServerEndpoint(value = "/", decoders = MessageDecoder.class, 
encoders = MessageEncoder.class, configurator = SpringConfigurator.class)
public class ServerEndPoint {

    private static final Logger LOG = LoggerFactory.getLogger(ServerEndPoint.class);

    public static final Set<CommunicationObserver> OBSERVERS = Sets.newConcurrentHashSet();

    @OnMessage
    public void onMessage(Session session, Message msg) {
        LOG.debug("Received msg {} from {}", msg, session.getId());
        for (CommunicationObserver o : OBSERVERS) {
            o.packetReceived(session, msg);
        }
    }

This is based on the Spring WebSocket JSR-356 tutorial, but i've got the following error:

java.lang.IllegalStateException: Failed to find the root WebApplicationContext. Was ContextLoaderListener not used?
    at org.springframework.web.socket.server.standard.SpringConfigurator.getEndpointInstance(SpringConfigurator.java:68)
    at org.apache.tomcat.websocket.pojo.PojoEndpointServer.onOpen(PojoEndpointServer.java:50)
    at org.apache.tomcat.websocket.server.WsHttpUpgradeHandler.init(WsHttpUpgradeHandler.java:138)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:687)
    at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.process(Http11NioProtocol.java:223)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1558)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1515)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)

I have both tested in embedded mode and with external tomcat 8 and jetty 9 (in external mode, i remove de Spring config file) but the same error appeares.

the only workaround i've found is to create a custom configurator.

public class SpringEndpointConfigurator extends ServerEndpointConfig.Configurator {

    private static WebApplicationContext wac;

    public SpringEndpointConfigurator() {
    }

    public SpringEndpointConfigurator(WebApplicationContext wac) {
        SpringEndpointConfigurator.wac = wac;
    }

    @Override
    public <T> T getEndpointInstance(Class<T> endpointClass) throws InstantiationException {
        T endPoint = wac.getAutowireCapableBeanFactory().getBean(endpointClass);
        return (endPoint != null) ? endPoint : wac.getAutowireCapableBeanFactory().createBean(endpointClass);
    }

it is created as a @Bean with the parameterized constructor.

I must have missed something to get it done with the SpringConfigurator class, but i don't know what.

Answer

bhdrkn picture bhdrkn · Aug 13, 2016

SpringConfigurator uses ContextLoader to obtain spring context. Spring Boot does set up the ServletContext but it never uses ContextLoaderListener which initializes ContextLoader to hold static state of spring context. You can try to add ContextLoaderListener or as a workaround you can write your own context holder and configurator.

Here is an example:

First context holder and configurator:

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import javax.websocket.server.ServerEndpointConfig;

public class CustomSpringConfigurator extends ServerEndpointConfig.Configurator implements ApplicationContextAware {

    /**
     * Spring application context.
     */
    private static volatile BeanFactory context;

    @Override
    public <T> T getEndpointInstance(Class<T> clazz) throws InstantiationException {
        return context.getBean(clazz);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        CustomSpringConfigurator.context = applicationContext;
    }
}

To get context we need to configure this as Bean:

@ConditionalOnWebApplication
@Configuration
public class WebSocketConfigurator {

...

    @Bean
    public CustomSpringConfigurator customSpringConfigurator() {
        return new CustomSpringConfigurator(); // This is just to get context
    }
}

Then you need to set configurator correctly:

@ServerEndpoint(value = "/", decoders = MessageDecoder.class, 
encoders = MessageEncoder.class, configurator = CustomSpringConfigurator.class)
public class ServerEndPoint {
...
}

As a side note, yes if you delete SpringConfigurator your application will start and you can handle request. But you cannot autowire other beans.