Spring根应用程序上下文和servlet上下文的混淆

17

我知道我需要在servlet上下文中注册标注有@Controller的类才能让我的Web应用程序可以访问。通常,我会按照以下方式进行操作:

@Configuration
@EnableWebMvc
@ComponentScan({"foo.bar.controller"})
public class WebConfig extends WebMvcConfigurerAdapter {
    //other stuff like ViewResolvers, MessageResolvers, MessageConverters, etc.
}

我将所有其他的配置类都加入了我的根应用程序上下文中。以下是我的调度程序初始化程序通常的外观:

public class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] { RootConfig.class, ServiceConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { WebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }
}

但是当我开始使用WebSockets时,事情变得更有趣了。要使WebSockets工作,您必须将WebSocketConfig.class放入servlet上下文中。以下是我的WebSocketConfig示例:

@Configuration
@EnableScheduling
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/chat").withSockJS();
    }

    @Override
    public void configureClientInboundChannel(ChannelRegistration channelRegistration) {
        channelRegistration.taskExecutor().corePoolSize(4).maxPoolSize(8);
    }

    @Override
    public void configureClientOutboundChannel(ChannelRegistration channelRegistration) {
        channelRegistration.taskExecutor().corePoolSize(4).maxPoolSize(8);
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/queue", "/topic");
        registry.setApplicationDestinationPrefixes("/app");
    }

}

同时,我创建了一个发送消息到主题的服务:

@Service
public class TimeServiceWsImpl implements TimeServiceWs {

    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    @Override
    public void sentCurrentTime() {
        long currentTime = System.currentTimeMillis();
        String destination = "/topic/chatty";
        logger.info("sending current time to websocket /topic/time : " + currentTime);
        this.messagingTemplate.convertAndSend(destination, currentTime);
    }
}

我需要在其他服务中使用此服务(自动装配它)。但现在我陷入了僵局:
  1. 如果我尝试在根应用程序上下文内创建 TimeServiceWs bean,那么如预期所料,它将无法看到 SimpMessagingTemplate bean,并抛出 NoSuchBeanDefinitionException
  2. 如果我尝试在 servlet 上下文中创建 TimeServiceWs bean,则无法将其自动装配到任何其他服务中,因为根上下文无法看到 servlet 上下文中的 bean(据我所知)
  3. 如果我将所有配置移动到 servlet 上下文中,则所有 bean 都将成功创建,但我会得到以下异常:java.lang.IllegalStateException: No WebApplicationContext found,并且无法访问我的 web 应用程序
我应该怎么做?根上下文中应该包含什么?servlet 上下文中应该包含什么?请再次澄清这些上下文之间的区别。
如果您需要任何其他信息,请告诉我。
2个回答

26

大多数Spring MVC应用程序具有一个根上下文,其中包含所有服务层/DAO层bean,并且每个应用程序的spring分发器servlet都有一个servlet上下文,其中至少包含每个servlet的控制器。

想法是一个应用程序可能有多个servlet调度程序,例如URL /shopping/* 和另一个URL /reporting/*,每个调度程序都有自己的一组控制器。

一个servlet调度程序的控制器彼此隔离,这意味着尽管它们也是Spring bean,但它们不能相互注入。

在根上下文中的服务层和DAO bean可在所有servlet上下文中可见,因此服务层bean可以注入任何控制器,但反过来则不行。

根上下文被称为控制器servlet上下文/上下文的父级。

这一切都旨在通过隔离bean组来确保没有意外依赖关系。

考虑到这一点,经过问题:

  • 如果我试图在根应用程序上下文中创建TimeServiceWs bean,则无法看到SimpMessagingTemplate bean并引发NoSuchBeanDefinitionException:将SimpMessagingTemplate移动到根上下文中,它像DAO一样是一个可以在应用程序的任何位置有用的bean,因此应位于共享的根上下文中。

  • 如果我尝试在servlet上下文中创建TimeServiceWs bean,则无法自动装配到任何其他服务:如果它是要自动连接到其他服务,请将其保留在根上下文中。

    - 如果我将所有配置移动到servlet上下文,则所有bean都会成功创建,但我会收到java.lang.IllegalStateException: No WebApplicationContext found:反之亦然,基本上将所有bean移动到根上下文中,并仅在servlet上下文中保留特定于该部分应用程序的bean,很多时候只有控制器。


1
当你说“将SimpleMessagingTemplate移动到根上下文”时,我真的不明白如何移动...很抱歉...对Spring非常陌生... - Saurabh Kumar
问题在于我无法轻松地移动SimpMessagingTemplate,因为Spring创建了这个bean,并且它依赖于其他由Spring创建的bean。我找到的解决方案是转移到Spring Boot - 可能会在根应用程序配置中注册WebSocketConfig,因此问题不会发生。因此,问题的根源在于我在错误的上下文中注册了配置。 - Oleksii Duzhyi

8

WebSocket 相关配置归属于 DispatcherServlet 配置的某种方式。毕竟,HTTP 握手是通过其处理程序映射由 DispatcherServlet 处理的。

在只有一个 DispatcherServlet 的部署场景中,您应该能够使用单个 Spring 上下文。如果使用 Spring Security,将配置合并到根上下文中更加合理,尽管 AbstractAnnotationConfigDispatcherServletInitializer 存在 bug(请参见 SPR-11357)。将其合并到 DispatcherServlet 上下文中也是可能的,但您写道您遇到了异常。您能提供异常详细信息吗?

还可以同时拥有根和 DispatcherServlet 上下文。在这种情况下,WebSocket 配置将位于 DispatcherServlet 上下文中,并且无法将 SimpMessagingTemplate 注入到根上下文中的 bean 中。实际上,每个 DispatcherServlet(或其他一些 Servlet)需要一个 SimpMessagingTemplate。所需的是一个 Web 层组件,可能是服务层 bean (如上面的 TimeServiceWs)周围的轻量级包装器,也可以注入 SimpMessagingTemplate。该 Web 层组件实质上充当桥梁。


谢谢您的回答,我认为使用SimpMessagingTemplate并借助薄包装器是关键所在。 - Oleksii Duzhyi
你能进一步解释一下如何注入这个Web层组件吗?例如,我有一个根上下文、我的Spring MVC Servlet及其上下文以及另一个Servlet。如果可能的话,我希望消息代理可以被所有人共享。如果SimpMessagingTemplate不在根上下文中,我不明白如何做到这一点。 - Jason
Jason,你所问的有点不同。一种选择是在根上下文中配置你的WebSocket消息 -- Spring MVC 也可以在根上下文中找到它所需的内容,如果另一个Servlet不是Spring MVC,它也不会关心。但是,如果你有两个DispatcherServlets,那么你可以扩展AbstractMessageBrokerConfiguration(仅限消息),并在根上下文中声明它。Spring MVC映射(即WebSocketMessageBrokerConfigurationSupport中的bean)将进入Spring MVC配置。不是很简单,但也不太难。 - Rossen Stoyanchev
将扩展AbstractWebSocketMessageBrokerConfigurer的配置文件移动到根上下文对我很有帮助,因为我需要从根上下文的服务层向客户端发送消息。 - Bal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接