Spring Framework 4.0 M1: WebSocket 支持

正如你可能已经看到的,Spring Framework 4.0 第一个里程碑版本已经宣布,且我们已经发布了早期的WebSocket支持。为什么WebSocket重要呢?在web上,需要在客户端(典型如浏览器)和服务器间进行高频率低延迟的消息交换是在应用中必不可少的,它使有效的,双向的通信成为可能。常见的例子包括交易,游戏,协作,数据可视化,其他的一系列场景和用例将随时间而增加。

WebSocket是非常宽泛的话题!你可以在InfoQ上观看我们SpringOne 2012的“WebSocket介绍”获取更全面的介绍。能够简单的使用WebSocket仅仅是一个开始。你将需要一个备用策略用于那些不支持它的浏览器(如 IE < 10)和用于阻止其使用的网络代理。此外,套接字编程,是非常,非常低级的编程。大多数的应用将受益于一个更高级的编程模型。这也是公认的,在WebSocket协议中通过一种机制 来允许使用一个“子协议”(例如一个更高级别的协议),就像今天我们都使用的HTTP,不是一个原始的TCP套接字。例如,子协议包括STOMPWAMP,和更多其它的。

请记住,这是一个早期版本。它关注的基本面包括JSR-356支持和浏览器内部使用的备选项(fallback options)。也没有子协议支持,那是下一个里程碑版本的目标。

WebSocket的Java API  (JSR-356)

WebSocket的Java API是最近完成的并是Java EE 7的一部分。它定义了两种类型的端点—Endpoint子类及注解的端点,如@ClientEndpoint和@ServerEndpoint。完整的介绍超出了本文的范围。我将只会提到在Spring应用中理解如何配置和使用端点的最低要求。

在JSR-356中有两种方法来部署服务器 — 通过Servlet容器扫描(Servlet 3.0特性)和在编程式启动。对于Servlet容器扫描,规范要求注解的端点有一个默认构造器,但Endpoint子类不能被自动部署。相反,Servlet容器扫描检测ServerApplicationConfig类型,其按照预期的循环为每一个Endpoint应用Server/ClientEndpointConfig。

在试图搞清楚这一切之前,你可能想知道的是它如何与你的Spring应用关联。M1版本通过Spring提供了用于初始化两种类型端点的全部支持,包括适当的构造器依赖注入以及每个连接和单例端点生命周期。此外,你应该关闭Servlet容器扫描,其是相当重量级的并扫描所有类包括第三方依赖。

给我代码!

用Spring初始化一个注解的端点,只需简单地用类级别注解配置一个SpringConfigurator:

import javax.websocket.server.ServerEndpoint;
import org.springframework.web.socket.server.endpoint.SpringConfigurator;
 
@ServerEndpoint(value = "/echo", configurator = SpringConfigurator.class)
public class EchoEndpoint {
 
  private final EchoService echoService;
 
  @Autowired
  public EchoEndpoint(EchoService echoService) {
    this.echoService = echoService;
  }
 
  @OnMessage
  public void handleMessage(Session session, String message) {
    // ...
  }
 
}

上面的代码假设SpringContextLoaderListener用于装载Spring配置, 但通常是在web应用的情况。除此之外没有别的要求。Servlet容器扫描发现注解的端点和SpringConfigurator为每个WebSocket会话初始化一个新的实例,这也是规范中定义的默认的生命周期。

如果你想使用一个单例或想关闭Servlet容器扫描,声明EchoEndpoint作为一个Spring bean,且也加添加一个forServerEndpointExporter bean声明(只需一次!)。如下示例使用Spring的Java配置,但你也可以添加基于XML配置等价的声明。

import org.springframework.web.socket.server.endpoint.ServerEndpointExporter;
 
@Configuration
public class EndpointConfig {
 
  @Bean
  public EchoEndpoint echoEndpoint() {
    return new EchoEndpoint(echoService());
  }
 
  @Bean
  public EchoService echoService() {
    // ...
  }
 
  @Bean
  public ServerEndpointExporter endpointExporter() {
    return new ServerEndpointExporter();
  }
 
}

 Endpoint子类能连同一个ServerEndpointExporter声明(只需一次)一起通过EndpointRegistration被部署。

import org.springframework.web.socket.server.endpoint.ServerEndpointExporter;
import org.springframework.web.socket.server.endpoint.ServerEndpointRegistration;
 
@Configuration
public class EndpointConfig {
 
  @Bean
  public EndpointRegistration echoEndpoint() {
    return new EndpointRegistration("/echo", EchoEndpoint.class);
  }
 
  @Bean
  public ServerEndpointExporter endpointExporter() {
    return new ServerEndpointExporter();
  }
 
  // ..
 
}

EndpointRegistration也有一个接受一个端点实例参数的构造器。这允许你在有每个WebSocket会话有一个新实例或单个实例服务所有会话间进行选择。

客户端呢?

JSR-356提供如下API用于连接服务器:

WebSocketContainer container = ContainerProvider.getWebSocketContainer();
container.connectToServer(EchoEndpoint.class, new URI("ws:localhost:8080/webapp/echo"));

这是很简单的,但如果也能声明它也将是不错的。常见的情况是 — 当web应用启动时,它应该自动的连接到远程端点,开始处理消息并当应用关闭时停止。

你可以通过一个如下所示的连接管理器来做到,即当Spring ApplicationContext刷新或关闭时分别进行WebSocket连接建立和关闭。

import org.springframework.web.socket.client.endpoint.AnnotatedEndpointConnectionManager;
 
@Configuration
public class EndpointConfig {
 
  // For Endpoint sub-classes use EndpointConnectionManager instead
 
  @Bean
  public AnnotatedEndpointConnectionManager connectionManager() {
    return new AnnotatedEndpointConnectionManager(echoEndpoint(), "ws://localhost:8080/webapp/echo");
  }
 
  @Bean
  public EchoEndpoint echoEndpoint() {
    // ...
  }
 
}

你也可以使用autoStartup属性来开启/禁用自动连接。如果禁用,你可以手工调用start()和stop()。

到此总结了JSR-356支持的概述。

Spring WebSocket API

除了JSR-356的支持,这个版本提供了导致一些明显问题的Spring WebSocket API的新的开始。

为什么是我们自己的API?我们在内部它作为更高级服务的基础如SockJS。它允许我们有可能插入额外的Java WebSocket实现并添加额外的特性。例如JSR-356没有提供从一个存在的Servlet初始化WebSocket握手的方式,当添加SockJS支持时这让我们发现非常有用。此外,尽管Jetty也没有提供JSR-356支持,我们也是能够在这个版本插入(所有新的)Jetty 9 WebSocket API和包括Jetty 9支持。我们可以坚持使用Jetty API直接前进,因为它提供了一组更丰富的WebSocket配置和处理选项及比Java WebSocket API更频繁的更新。

为什么仅基于类型(例如,不是注解)?Spring WebSocket API以框架使用为主为目标。应用当然可以使用它,但我们相信,到套接字的编程对于大多数应用来组织他们的业务逻辑和提供健壮的消息处理太低级了。为了更好地体会到这一点,可以考虑如果一个应用暴露单个WebSocket连接(在大多数情况,它应该),它将不得不从单个类中处理所有应用消息类型。即使是注解,不可能自适应实际应用的复杂性。想像一下REST没有名词(URLs)和动词(HTTP methods),仅有原始的socket。这就是为什么子协议支持和更高级别编程模型是非常必要的,这也是我们更像是要有一些注解。

希望注意到“为什么”问题。现在,让我们来看一些代码。

Spring WebSocket API核心接口是WebSocketHandler。下面是对一个它的实现用于处理文本消息。基类有个空方法,只是按照协议中定义的以状态1003(not acceptable)关闭会话来拒绝二进制消息。

import org.springframework.web.socket.adapter.TextWebSocketHandlerAdapter;
 
public class EchoHandler extends TextWebSocketHandlerAdapter {
 
  @Override
  public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    session.sendMessage(message);
  }
 
}

 注意,handleTextMessage允许异常传播。这不同于JSR-356,它不允许。如果一个Exception(或Throwable)逸出方法,会话会自动以状态1011关闭(server error)。这意味着你可以选择处理异常,如果有任何有意义的事情去做;或者否则让它以默认方式处理。默认异常处理通过WebSocketHandlerDecorator机制提供。它能被扩展和/或替换。这些仅是几个我们自己的API能让我们去做什么的例子。

WebSocketHandler处理器能通过WebSocketHttpRequestHandler插入到Spring MVC。

import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler;
 
@Configuration
public class WebConfig {
 
  @Bean
  public SimpleUrlHandlerMapping handlerMapping() {
 
    Map<String, Object> urlMap = new HashMap<String, Object>();
    urlMap.put("/echo", new WebSocketHttpRequestHandler(new EchoHandler()));
 
    SimpleUrlHandlerMapping hm = new SimpleUrlHandlerMapping();
    hm.setUrlMap(urlMap);
    return hm;
  }
 
}

SockJS备选项

SockJS是一个浏览器JavaScript类库,其提供了WebSocket类似的编程模型和一系列特定浏览器的传输(transport),如果WebSocket在浏览器不支持或网络问题阻止它时使用。我们很高兴的宣布在这个版本中支持SockJS。SockJS的更多细节和各种传输选项,请访问sockjs-client项目页面。

开启SockJS支持,简单地声明一个SockJsService,映射它到一些URL,并提供一个用于出入传入消息(incoming message)的WebSocketHandler。注意,WebSocketHandler是之上讨论的相同的处理器。换句话说,当使用SockJS,编程模型仍然相同,但底层传输可能改变为HTTP流,长轮询,或其他的东西是必要的。

import org.springframework.web.socket.sockjs.SockJsService;
// ...
 
@Configuration
public class WebConfig {
 
  @Bean
  public SimpleUrlHandlerMapping handlerMapping() {
 
    SockJsService sockJsService = new DefaultSockJsService(taskScheduler());
 
    Map<String, Object> urlMap = new HashMap<String, Object>();
    urlMap.put("/echo/**", new SockJsHttpRequestHandler(sockJsService, new EchoHandler()));
 
    SimpleUrlHandlerMapping hm = new SimpleUrlHandlerMapping();
    hm.setUrlMap(urlMap);
    return hm;
  }
 
  @Bean
  public ThreadPoolTaskScheduler taskScheduler() {
    ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
    taskScheduler.setThreadNamePrefix("SockJS-");
    return taskScheduler;
  }
 
}

如果你想知道之上 的任务调度用于各种SockJS相关的任务,例如在HTTP流请求上定期发送心跳信息(以防止代理认为连接是不新鲜的),移除过期的SockJS会话等。

最后

可以在Github上找到一个示例和介绍的项目。它包括配置JSR-356端点的示例,Spring WebSocketHandler,以及SockJS服务。对于所有示例,建议使用Google Chrome网络选项卡开发工具,为了观察WebSocket和HTTP流量,观察错误等。

如果您有反馈意见和建议,我们很乐意听到它!

原文:http://blog.springsource.org/2013/05/22/spring-framework-4-0-m1-websocket-support/

相关阅读:

Spring Framework 4.0M1 & 3.2.3 发布了

JSR-356 WebSocket API规范中文版

Servlet 3.1规范

相关推荐