webSocket初探

以前有遇到一些服務端客戶端交互問題,有時希望交互是異步的,服務器的響應是非即時的,但是http協(xié)議顯然不符合我的需求。所以最近專門找時間對websocket進行學習。
websocket和http是不同的網(wǎng)絡協(xié)議,有關網(wǎng)絡協(xié)議之間的詳細區(qū)別和特點就不追究了。這里區(qū)分一下websocket協(xié)議和http協(xié)議的一些重要的,顯而易見的區(qū)別:
1. websocket請求是以ws://開頭的,http請求是以http://開頭的。
2. http請求是客戶端請求,服務端響應的固定模式,服務端無法主動向客戶端發(fā)送消息。而websocket協(xié)議是長連接,客戶端向服務端發(fā)起請求建立連接以后,雙方可以互相發(fā)送消息,注意,這里的互相發(fā)送消息是沒有什么限制的,不一定要一來一回,服務端一次,客戶端一次這樣。
下面介紹一些webSocket的典型使用場景,比如聊天室。A向B發(fā)送一條消息,如果使用http協(xié)議,那B想要收到消息就要通過不斷的向服務端發(fā)請求,查詢有沒有發(fā)給自己的消息。而webSocket不同,在B登錄以后,就建立好長連接,當有發(fā)給B的消息的時候,服務端直接通過ws連接向B推送消息即可。
這次我就通過一個非常簡易的聊天功能,來完成對webSocket的學習。
先貼效果圖,第一個是連接界面(風格極簡未修飾)。


image

輸入用戶名test1,然后點擊連接,如下圖:


image

看一下瀏覽器network,能夠看到發(fā)出了ws請求:
image

然后新開一個窗口,重復上述操作,但是用戶名使用test2:
然后在test1的界面收信人填入test2,輸入信息123,點擊發(fā)送:
image

值得注意的是瀏覽器并沒有發(fā)送新的請求,還是之前的那個請求,但是在message這里有一條信息,左邊是向上的箭頭,代表是由客戶端發(fā)給服務端的。再切到test2的窗口,界面上展示了test1發(fā)來的信息,ws請求的message里多了一條信息,如圖:
image

左邊的箭頭向下,代表是由服務端推送的客戶端的消息。再使用test2的窗口給test1發(fā)消息,往返多試幾次,是沒有問題的。
image
image

下面說一下代碼實現(xiàn):

前端代碼:

<!DOCTYPE html>
<html lang="en">
    <meta charset="UTF-8">
    <title>Title</title>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js">
<input id="username" type="text" placeholder="用戶名" />
<input id="to" type="text" placeholder="收信人" hidden/>
<input id="msg" type="text" placeholder="消息內容" hidden/>
<button id="connectBtn" type="button" onclick="connect()">連接</button>
<button id="sendBtn" type="button" onclick="send()" hidden>發(fā)送</button>
<div id="record">
    var websocket =null;
    function connect() {
        var host =document.location.host;
        var username = $("#username").val();
        //判斷當前瀏覽器是否支持WebSocket
        if ('WebSocket' in window) {
            websocket =new WebSocket('ws://' + host +'/websocketDemo/webSocket/' + username);
            $("#username").attr("hidden", "hidden");
            $("#connectBtn").attr("hidden", "hidden");
            $("#msg").attr("hidden", false);
            $("#to").attr("hidden", false);
            $("#sendBtn").attr("hidden", false);
            //連接發(fā)生錯誤的回調方法
            websocket.onerror =function () {
                alert("WebSocket連接發(fā)生錯誤")
                setMessageInnerHTML("WebSocket連接發(fā)生錯誤");
            };
            //連接成功建立的回調方法
            websocket.onopen =function () {
                alert("連接成功")
                setMessageInnerHTML("連接成功");
            }
            //接收到消息的回調方法
            websocket.onmessage =function (event) {
                setMessageInnerHTML(event.data);
            }
            //連接關閉的回調方法
            websocket.onclose =function () {
                setMessageInnerHTML("WebSocket連接關閉");
            }
          }else {
              alert('當前瀏覽器 Not support websocket'
          }
}
function send() {
var data = {
message: $("#msg").val(),
            To: $("#to").val()
};
        websocket.send(JSON.stringify(data));
    }

//監(jiān)聽窗口關閉事件,當窗口關閉時,主動去關閉websocket連接,防止連接還沒斷開就關閉窗口,server端會拋異常。
    window.onbeforeunload =function () {
closeWebSocket();
    }

//關閉WebSocket連接
    function closeWebSocket() {
websocket.close();
    }

//將消息顯示在網(wǎng)頁上
    function setMessageInnerHTML(innerHTML) {
document.getElementById('record').innerHTML += innerHTML +'<br/>';
    }
</html>

其中主要的邏輯點擊連接按鈕時建立websocket連接,這里有一個需要注意的點是并不是所有瀏覽器都支持websocket協(xié)議的,所以需要先進行判斷,當然,對于一些低版本的瀏覽器,也是有一些JS插件可以提供支持的,這里暫時不討論這個細節(jié),然后在點擊發(fā)送按鈕的時候通過ws協(xié)議向服務端發(fā)送消息,目的是給另外一個人發(fā)送一條信息,另外還定義了一個回調函數(shù),負責服務器推送來的消息,注意這里是服務器推送來的消息,所以發(fā)送消息的請求發(fā)出后就結束了,而不是像http請求那樣等待服務器的響應,也不是類似異步ajax請求那樣的回調方法。

后端實現(xiàn)代碼如下:

package websocket;
import net.sf.json.JSONObject;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
@ServerEndpoint("/webSocket/{username}")
public class WebSocketTest {

private static int onlineCount =0;

   private static Map<String, WebSocketTest>  clients =new ConcurrentHashMap<String, WebSocket>();

    private Session session;

    private String username;

    @OnOpen
public void onOpen(@PathParam("username") String username, Session session)throws IOException {
        this.username = username;
        this.session = session;
        addOnlineCount();
        clients.put(username, this);
        System.out.println("已連接");
    }

@OnClose
public void onClose(){
clients.remove(username);
        subOnlineCount();
    }

@OnMessage
public void onMessage(String message){
JSONObject jsonTo = JSONObject.fromObject(message);
        String mes = (String) jsonTo.get("message");
        if (!jsonTo.get("To").equals("All")) {
sendMessageTo(mes, jsonTo.get("To").toString());
        }else {
sendMessageAll(mes);
        }
}

@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
    }

public void sendMessageTo(String message, String To) {
for (WebSocketTest item : clients.values()) {
if (item.username.equals(To)) {
item.session.getAsyncRemote().sendText(message);
            }
}
}

public void sendMessageAll(String message){
for (WebSocketTest item : clients.values()) {
item.session.getAsyncRemote().sendText(message);
        }
}

public static synchronized int getOnlineCount() {
return onlineCount;
    }

public static synchronized void addOnlineCount() {
WebSocketTest.onlineCount++;
    }

public static synchronized void subOnlineCount() {
WebSocketTest.onlineCount--;
    }

public static synchronized Map getClients() {
return clients;
    }

}

主要是通過ServerEndpoint注解聲明該類處理webSocket請求,注解的值是請求路徑,然后用Component注解將類注冊到上下文中,@OnOpen的方法處理建立連接的請求,并使用用戶名創(chuàng)建一個WebSocketTest對象,然后通過鍵值對的形式存儲在client屬性中,此時相當于將這個用戶注冊到服務端中,當需要給該用戶發(fā)送消息時,通過用戶名取出他的WebSocketTest對象,獲取該用戶的session,向該用戶發(fā)送消息。

@onMessage注解方法用于接收客戶端發(fā)送到服務端的消息,可以看到在方法中獲取了消息和接收人,然后從clients中取出接收人的WebSocketTest對象,然后獲取他的ws連接并向他發(fā)送消息。@onError注解方法顧名思義,在出現(xiàn)錯誤的時候執(zhí)行,我沒觸發(fā)過這個方法,所以暫時不太清楚能夠觸發(fā)它的是哪些錯誤,又不包含哪些錯誤,值得注意的是,@onClose方法是由客戶端觸發(fā)的,當客戶端需要JS的websocket對象調用ws連接時,會觸發(fā)該方法,另外,回顧前端代碼,可以看到監(jiān)聽了窗口關閉事件,當窗口被關閉時,會調用ws對象的關閉連接方法。這是因為如果ws連接沒有被關閉,連接就突然斷開,服務端是會報錯的,所以在使用websocket的時候,務必確保連接不再使用時,正確的將它關閉。
最后附上pom依賴的內容。


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.test</groupId>
  <artifactId>websocket</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>
  <name>websocket Maven Webapp</name>
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring.version>4.1.4.RELEASE</spring.version>
    <jackson.version>2.5.0</jackson.version>
    <lucene.version>6.0.1</lucene.version>
  </properties>

  <dependencies>
    <!-- webSocket 開始-->
    <dependency>
      <groupId>javax.websocket</groupId>
      <artifactId>javax.websocket-api</artifactId>
      <version>1.1</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>javax</groupId>
      <artifactId>javaee-api</artifactId>
      <version>7.0</version>
      <scope>provided</scope>
    </dependency>
    <!-- webSocket 結束-->
    <dependency>
      <groupId>net.sf.json-lib</groupId>
      <artifactId>json-lib</artifactId>
      <version>2.4</version>
      <classifier>jdk15</classifier>
    </dependency>
    <!-- spring -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>${spring.version}</version>
      <scope>test</scope>
    </dependency>
    <!-- log4j -->
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.17</version>
    </dependency>
    <!-- servlet -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>3.0-alpha-1</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>
  <build>
    <finalName>websocket</finalName>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.2.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.12.4</version>
          <configuration>
            <forkMode>once</forkMode>
            <argLine>-Dfile.encoding=UTF-8</argLine>
          </configuration>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

然后再看一版以sockJS與springBoot集成的websocket的例子(源碼來源于慕課網(wǎng)示例demo, 對代代碼進行了部分調整),效果就不演示了,功能是類似的,但是相比于第一種實現(xiàn)應該使用的更廣泛,只是前端使用了sockJS,后端使用springBoot中集成的webSocket.簡單說一下核心區(qū)別,sockJS是是一個瀏覽器的JavaScript庫,它提供了webSocket的類似實現(xiàn),但是解決了低版本瀏覽器不支持webSocket的問題,并且它擁有spring的后端實現(xiàn)支持。spring的重要性不用多說,這版的demo就是使用spring集成的webSocket,實踐一下用法。
先看后端代碼,主要分析一些不同點:
首先需要一個配置類,來配置注冊URL,前后端交互的URL前綴:

package fxz.test.websocketdemo.Config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    /**
     * 注冊端點,發(fā)布或者訂閱消息的時候需要連接此端點
     * setAllowedOrigins 非必須,*表示允許其他域進行連接
     * withSockJS  表示開啟sockejs支持
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/endpoint-websocket").setAllowedOrigins("*").withSockJS();
    }

    /**
     * 配置消息代理(中介)
     * enableSimpleBroker 服務端推送給客戶端的路徑前綴
     * setApplicationDestinationPrefixes  客戶端發(fā)送數(shù)據(jù)給服務器端的一個前綴
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/getMessage");
        registry.setApplicationDestinationPrefixes("/sendMessage");
    }
}

這里可以看到和第一版的一個重要區(qū)別是,將服務端推送給客戶端的路徑前綴與客戶端向服務端發(fā)送數(shù)據(jù)的路徑前綴區(qū)分開來了,當然你設置成一樣的URL前綴其實也是可以的,但是第一版則不行。不過暫時沒有想到這種設置的重要應用有什么,不過相信可區(qū)分肯定是比不可區(qū)分有優(yōu)勢的。
然后需要一個消息實體類,很簡單:

package fxz.test.websocketdemo.model;

public class Message {
    private String fromUser;
    private String toUser;
    private String message;

    public String getFromUser() {
        return fromUser;
    }

    public void setFromUser(String fromUser) {
        this.fromUser = fromUser;
    }

    public String getToUser() {
        return toUser;
    }

    public void setToUser(String toUser) {
        this.toUser = toUser;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    @Override
    public String toString() {
        return "Message{" +
                "fromUser='" + fromUser + '\'' +
                ", toUser='" + toUser + '\'' +
                ", message='" + message + '\'' +
                '}';
    }
}

再需要一個Controller來接收消息,這里需要注意的點是URL注解使用@MessageMapping,另外我們的入?yún)⑹荕essage示例類,而不再是String了,這個和Stomp有關系,后續(xù)會具體提到:

package fxz.test.websocketdemo.controller;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
import fxz.test.websocketdemo.Service.WebSocketService;
import fxz.test.websocketdemo.model.Message;

@Controller
public class WebSocketController {

    private WebSocketService webSocketService;

    public WebSocketController(WebSocketService webSocketService) {
        this.webSocketService = webSocketService;
    }

    @MessageMapping(value = "/single/chat")
    public void sendMessage(Message message){
        webSocketService.sendMessageTo(message.getFromUser(), message.getToUser(), message.getMessage());
    }
}

補充WebSocketService的代碼內容,其中的SimpMessagingTemplate對象是直接從應用上下文中注入進來的,通過它可以向客戶端推送消息,URL需要符合我們注冊過的推送URL:

package fxz.test.websocketdemo.Service;

import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;

@Service
public class WebSocketService {
    private final SimpMessagingTemplate simpMessagingTemplate;

    public WebSocketService(SimpMessagingTemplate simpMessagingTemplate) {
        this.simpMessagingTemplate = simpMessagingTemplate;
    }

    public void sendMessageTo(String fromUser, String toUser, String message){
        simpMessagingTemplate.convertAndSend("/getMessage/single/" + toUser + fromUser, message);
    }
}

后端代碼主要就是這些內容,主要流程是,應用啟動時注冊相關的URL,然后前端通過注冊端點建立和服務端的WS連接,發(fā)送的請求會被WebSocketController處理,處理完消息數(shù)據(jù)后,通過SimpMessagingTemplate對象向客戶端推送消息。
下面看一下前端代碼的核心內容,前面說過這一版的代碼前端不再使用原生的webSocket對象,因為在低版本的瀏覽器中是不支持的,SockJS就是來解決這個問題的,除了SockJS之外,我們還需要再引入StompJS,[STOMP]是一種簡單的面向文本的消息傳遞協(xié)議,概念就只說這么一句,談談我對他的理解:

  1. 消息傳遞協(xié)議不是網(wǎng)絡協(xié)議
    不能把它和HTTP,webSocket混淆了,完全不是一碼事,它是用來規(guī)定服務端和客戶端交互的時候數(shù)據(jù)傳輸數(shù)據(jù)的規(guī)范的。
  2. 什么是面向文本的
    數(shù)據(jù)傳輸通常分為文本和二進制,文本的像json等,二進制通常就是傳輸文件了,那么面向文本就含義就顯而易見了,它是不能傳輸二進制的,也就是這個協(xié)議不能傳文件。
  3. 它和webSocket的關系,這個引用我看到的一段解釋,還算通俗易懂
  1. HTTP協(xié)議解決了 web 瀏覽器發(fā)起請求以及 web 服務器響應請求的細節(jié),假設 HTTP 協(xié)議 并不存在,只能使用 TCP 套接字來 編寫 web 應用,你可能認為這是一件瘋狂的事情;
  2. 直接使用 WebSocket(SockJS) 就很類似于 使用 TCP 套接字來編寫 web 應用,因為沒有高層協(xié)議,就需要我們定義應用間所發(fā)送消息的語義,還需要確保連接的兩端都能遵循這些語義;
  3. 同 HTTP 在 TCP 套接字上添加請求-響應模型層一樣,STOMP 在 WebSocket 之上提供了一個基于幀的線路格式層,用來定義消息語義;
    概括一下就是,webSocket請求的消息體這一塊的約定或者協(xié)議一直沒人管,后來它負責管這些東西。那它到底有什么用呢,怎么管的,我這邊肉眼可見的區(qū)別是,如前面提到的,處理ws請求的方法入?yún)⒖梢允褂媒Y構化的實體Message類了,這里的反例是第一版代碼,我們在前端使用json封裝數(shù)據(jù),但是后臺使用String接收,這里我產(chǎn)生了一個疑問,那我能不能直接使用實體類接收呢,答案是不能,這里放出截圖看一下效果:


    image.png

    可以看到,是無法通過編譯的,但是第二版代碼是可以的,我的理解就是stomp協(xié)議幫我們處理了這個數(shù)據(jù)轉換的過程。
    然后具體看一下核心代碼內容(DOM與前端交互邏輯的代碼就不再貼出了):

function connect() {
    var from = $("#from").val();
    var to = $("#to").val();
    //新建SockJS實例
    var socket = new SockJS('/endpoint-websocket');
    //使用SockJS對象初始化Stomp對象,Stomp.over方法接收一個符合WebSocket定義的對象
    stompClient = Stomp.over(socket);
/*    發(fā)起ws建立連接請求,第一個參數(shù)是header對象,形如 var headers = {
           login: 'mylogin',
           passcode: 'mypasscode',
           // additional header
           'client-id': 'my-client-id'
         };
         這里傳空,暫時不知道什么時候需要傳值
    第二個參數(shù)是連接成功的回調,也可以傳入第三個函數(shù)作為失敗回調,這里省略了
    回調函數(shù)里調用了訂閱方法,用于接收服務端推送來的消息,showContent函數(shù)將推送消息展示
    */
    stompClient.connect({}, function (frame) {
        setConnected(true);
        console.log('Connected: ' + frame);
        stompClient.subscribe('/getMessage/single/'+ from + to, function (result) {
            showContent(result.body);
        });
    });
}

發(fā)送消息的方法如下:

//獲取消息內容,轉JSON發(fā)送
function sendMsg() {
    var toUser = document.getElementById("to").value;
    var fromUser = document.getElementById("from").value;
    var message = document.getElementById("content").value;
    stompClient.send("/sendMessage/single/chat", {}, JSON.stringify({
        'message': $("#content").val(),
        'toUser': $("#to").val(),
        'fromUser': $("#from").val()
    }));
}

斷開連接使用:

//可以傳入回調函數(shù)
stompClient.disconnect();

最后附上pom依賴內容不迷路:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>xyz.suiwo</groupId>
    <artifactId>websocket-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>websocket-demo</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>webjars-locator</artifactId>
            <version>0.31</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>sockjs-client</artifactId>
            <version>1.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>stomp-websocket</artifactId>
            <version>2.3.3</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>bootstrap</artifactId>
            <version>3.3.7</version>
        </dependency>
        
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.1.0</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 2019-10-21 1.一天ASPICE封閉培訓,主要內容項目管理,進行了三次模擬訓練,原以為自己做事很有條理,...
    傲雪楓閱讀 289評論 0 0
  • 焦點解決網(wǎng)絡初級第14期 信陽堅持分享第240天 受到他人無理的指責或誹謗時,不爭辯或反駁對方,這是一種胸懷。...
    山花爛漫2閱讀 173評論 0 4
  • 為了紀念初中生活,給孩子們留下美好回憶,家委會一開始籌備畢業(yè)活動時就計劃拍畢業(yè)創(chuàng)意照、制作相冊。 我先和冷...
    夏蓮沐閱讀 354評論 0 0
  • 我很想念我的小哥哥,即使他并不想我。昨天在夢里看到他了,他說,我們,就這樣吧。我很平靜的回答,好呀,就這樣吧。
    木木的心心閱讀 125評論 0 0

友情鏈接更多精彩內容