jetty容器部署 websocket 踩坑记

673 阅读3分钟

1.背景

在智能客服项目开发过程中,有一个需求,需要将vue和springboot 的SSE流式交互响应改造成 websocket 交互,以此来验证WAF设备会不会对websocket数据流进行截流处理,springboot项目打成war包部署在jetty容器中,JDK版本1.8, jetty版本9.4.44。

2.websocket功能示例

方式1:通过 @ServerEndpoint 注解来定义 WebSocket 端点

pom依赖:

 <dependencies>
        <!-- Spring Boot Starter Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
​
        <!-- Spring Boot Starter Test (for testing) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jetty</artifactId>
            <!--打成war包放到tomcat或者jetty容器里时,需要定义打包范围,打包时不包含jetty依赖-->
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

创建 WebSocket 服务端

创建一个使用 @ServerEndpoint 注解的 WebSocket 端点类。例如,一个简单的聊天服务:

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
​
@ServerEndpoint(value = "/chat")
@Component
public class ChatWebSocketServer {
​
    @OnOpen
    public void onOpen(Session session) {
        System.out.println("New session opened: " + session.getId());
    }
​
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("Message received: " + message);
        session.getOpenSessions().forEach(s -> {
            try {
                s.getBasicRemote().sendText("Server received: " + message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
​
    @OnClose
    public void onClose(Session session) {
        System.out.println("Session closed: " + session.getId());
    }
​
    @OnError
    public void onError(Session session, Throwable throwable) {
        System.err.println("Error in session " + session.getId() + ": " + throwable.getMessage());
    }
}
​

配置 WebSocket 支持

在 Spring Boot 的配置类中启用 WebSocket 支持:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
​
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
​
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 注册 WebSocket 端点,并允许跨域
        registry.addHandler(new ChatWebSocketHandler(), "/chat").setAllowedOrigins("*");
    }
}
​

前端vue示例代码

<template>
  <div>
    <input v-model="message" placeholder="Type a message" />
    <button @click="sendMessage">Send</button>
    <ul>
      <li v-for="msg in messages" :key="msg">{{ msg }}</li>
    </ul>
  </div>
</template><script>
export default {
  data() {
    return {
      message: "",
      messages: [],
      ws: null
    };
  },
  created() {
    this.ws = new WebSocket("ws://localhost:8080/chat");
​
    this.ws.onopen = () => {
      console.log("WebSocket connection opened");
    };
​
    this.ws.onmessage = (event) => {
      this.messages.push(event.data);
    };
​
    this.ws.onclose = () => {
      console.log("WebSocket connection closed");
    };
  },
  methods: {
    sendMessage() {
      if (this.message !== "") {
        this.ws.send(this.message);
        this.message = "";
      }
    }
  },
  beforeDestroy() {
    this.ws.close();
  }
};
</script>

方式2:基于 WebSocketHandler 的配置(适用于 Spring WebSocket)

如果您使用 WebSocketHandler(如当前配置中 MyWebSocketHandler),则无需 ServerEndpointExporter,只需保留 @EnableWebSocket 配置:

import org.springframework.beans.factory.annotation.Autowired;
import com.example.demo.websocket.handler.MyWebSocketHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.*;
​
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
​
    @Autowired
    private MyWebSocketHandler myWebSocketHandler;
​
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myWebSocketHandler, "/websocket").setAllowedOrigins("*");
    }
}
​

MyWebSocketHandler 类中实现 WebSocket 逻辑:

import com.example.demo.websocket.service.MyBusinessService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import org.springframework.stereotype.Component;
​
// 假设你有一个业务逻辑服务 MyBusinessService
@Component
public class MyWebSocketHandler extends TextWebSocketHandler {
​
    @Autowired
    private MyBusinessService myBusinessService;
​
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        System.out.println("连接已建立:" + session.getId());
    }
​
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        System.out.println("收到消息:" + message.getPayload());
​
        // 调用业务逻辑
        String responseMessage = myBusinessService.processMessage(message.getPayload());
​
        // 发送响应
        TextMessage response = new TextMessage("服务端响应:" + responseMessage);
        session.sendMessage(response);
    }
​
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        System.out.println("连接已关闭:" + session.getId());
    }
}
​
​

3.报错信息

使用以上两种方式,springboot集成websocket打成war包部署 jetty容器后,启动jetty报错:

java.lang.ClassNotFoundException:javax.websocket.Seeion

或者报错:

Unable to find ServletContexttHandler for provided ServletContext

查找了相关博客,搜索了GPT都是在修改pom依赖或者 springboot的启动类,尝试多次仍旧没有解决,后来求助 jetty容器运维的同学,

在jetty容器的目录下执行 java -jar start.jar --add-to-startd=websocket 命令 生成websocket的module,再次启动jetty容器,问题解决。

原因分析

  1. 模块化架构:Jetty 是一个模块化的应用服务器,它使用模块来增加功能和扩展。默认情况下,Jetty 的核心只提供基本的 HTTP 服务支持,不包括 WebSocket 支持。
  2. 缺少 WebSocket 支持:如果没有启用 WebSocket 模块,Jetty 容器在运行时不会加载相关的类库和依赖。这会导致 WebSocket 相关的类(如 javax.websocket.Session)无法被找到,从而抛出 ClassNotFoundException
  3. 启用 WebSocket 模块:执行 --add-to-startd=websocket 命令会修改 Jetty 配置并加载 websocket 模块。这会确保 Jetty 在启动时包含对 Java EE / Jakarta EE WebSocket API 的支持,从而解决 ClassNotFoundException 问题。

背后原理

  • Jetty 使用一个配置系统,其中 start.jar 可以解析 start.ini 文件和 start.d 目录中的配置文件。使用 --add-to-startd 命令会自动将指定模块的配置文件添加到 start.d 目录中。
  • 通过添加 websocket 模块,Jetty 会加载所需的库(如 WebSocket API 实现),从而支持 WebSocket 的功能。

4.总结

运行该命令后,Jetty 服务器会在启动时启用 WebSocket 支持,并确保能够使用 javax.websocket 类。这是因为默认情况下,这些类不会被 Jetty 加载,必须通过显式配置来启用。也有博客建议使用jakarta-websocket ,但是要求JDK11版本以上,各位可以自行尝试验证。