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容器,问题解决。
原因分析
- 模块化架构:Jetty 是一个模块化的应用服务器,它使用模块来增加功能和扩展。默认情况下,Jetty 的核心只提供基本的 HTTP 服务支持,不包括 WebSocket 支持。
- 缺少 WebSocket 支持:如果没有启用 WebSocket 模块,Jetty 容器在运行时不会加载相关的类库和依赖。这会导致 WebSocket 相关的类(如
javax.websocket.Session)无法被找到,从而抛出ClassNotFoundException。 - 启用 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版本以上,各位可以自行尝试验证。