SpringBoot2.0集成WebSocket實(shí)現(xiàn)后臺(tái)向前端推送信息
什么是WebSocket?
WebSocket協(xié)議是基于TCP的一種新的網(wǎng)絡(luò)協(xié)議。它實(shí)現(xiàn)了瀏覽器與服務(wù)器全雙工(full-duplex)通信——允許服務(wù)器主動(dòng)發(fā)送信息給客戶端。
為什么需要 WebSocket?
初次接觸 WebSocket 的人,都會(huì)問(wèn)同樣的問(wèn)題:我們已經(jīng)有了 HTTP 協(xié)議,為什么還需要另一個(gè)協(xié)議?它能帶來(lái)什么好處?
答案很簡(jiǎn)單,因?yàn)?HTTP 協(xié)議有一個(gè)缺陷:通信只能由客戶端發(fā)起,HTTP 協(xié)議做不到服務(wù)器主動(dòng)向客戶端推送信息。
舉例來(lái)說(shuō),我們想要查詢當(dāng)前的排隊(duì)情況,只能是頁(yè)面輪詢向服務(wù)器發(fā)出請(qǐng)求,服務(wù)器返回查詢結(jié)果。輪詢的效率低,非常浪費(fèi)資源(因?yàn)楸仨毑煌_B接,或者 HTTP 連接始終打開(kāi))。因此WebSocket 就是這樣發(fā)明的。 前言
2020-10-20
教程補(bǔ)充:
- 補(bǔ)充關(guān)于
@Component
和@ServerEndpoint
關(guān)于是否單例模式等的解答,感謝大家熱心提問(wèn)和研究。 Vue
版本的websocket連接方法
2020-01-05
教程補(bǔ)充:
- 整合了
IM相關(guān)的優(yōu)化
- 優(yōu)化
開(kāi)啟/關(guān)閉連接的處理
- 上傳到
開(kāi)源項(xiàng)目
spring-cloud-study-websocket,方便大家下載代碼。
感謝
大家的支持和留言,14W訪問(wèn)量是滿滿的動(dòng)力!接下來(lái)還會(huì)有websocket+redis集群優(yōu)化篇
針對(duì)多ws服務(wù)器做簡(jiǎn)單優(yōu)化處理,敬請(qǐng)期待!
話不多說(shuō),馬上進(jìn)入干貨時(shí)刻。
maven依賴
SpringBoot2.0對(duì)WebSocket的支持簡(jiǎn)直太棒了,直接就有包可以引入
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * 開(kāi)啟WebSocket支持 * @author zhengkai.blog.csdn.net */ @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
WebSocketConfig
啟用WebSocket的支持也是很簡(jiǎn)單,幾句代碼搞定
import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.socket.server.standard.ServerEndpointExporter;/** * 開(kāi)啟WebSocket支持 * @author zhengkai.blog.csdn.net */@Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
WebSocketServer
這就是重點(diǎn)了,核心都在這里。
- 因?yàn)閃ebSocket是類似客戶端服務(wù)端的形式(采用ws協(xié)議),那么這里的WebSocketServer其實(shí)就相當(dāng)于一個(gè)ws協(xié)議的Controller
- 直接
@ServerEndpoint("/imserver/{userId}")
、@Component
啟用即可,然后在里面實(shí)現(xiàn)@OnOpen
開(kāi)啟連接,@onClose
關(guān)閉連接,@onMessage
接收消息等方法。 - 新建一個(gè)
ConcurrentHashMap
webSocketMap 用于接收當(dāng)前userId的WebSocket,方便IM之間對(duì)userId進(jìn)行推送消息。單機(jī)版
實(shí)現(xiàn)到這里就可以。 集群版
(多個(gè)ws節(jié)點(diǎn))還需要借助mysql或者redis等進(jìn)行處理,改造對(duì)應(yīng)的sendMessage
方法即可。
package com.softdev.system.demo.config; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; import javax.websocket.OnClose; import javax.websocket.OnError; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.apache.commons.lang.StringUtils; import org.springframework.stereotype.Component; import cn.hutool.log.Log; import cn.hutool.log.LogFactory; /** * @author zhengkai.blog.csdn.net */ @ServerEndpoint("/imserver/{userId}") @Component public class WebSocketServer { static Log log=LogFactory.get(WebSocketServer.class); /**靜態(tài)變量,用來(lái)記錄當(dāng)前在線連接數(shù)。應(yīng)該把它設(shè)計(jì)成線程安全的。*/ private static int onlineCount = 0; /**concurrent包的線程安全Set,用來(lái)存放每個(gè)客戶端對(duì)應(yīng)的MyWebSocket對(duì)象。*/ private static ConcurrentHashMap<String,WebSocketServer> webSocketMap = new ConcurrentHashMap<>(); /**與某個(gè)客戶端的連接會(huì)話,需要通過(guò)它來(lái)給客戶端發(fā)送數(shù)據(jù)*/ private Session session; /**接收userId*/ private String userId=""; /** * 連接建立成功調(diào)用的方法*/ @OnOpen public void onOpen(Session session,@PathParam("userId") String userId) { this.session = session; this.userId=userId; if(webSocketMap.containsKey(userId)){ webSocketMap.remove(userId); webSocketMap.put(userId,this); //加入set中 }else{ webSocketMap.put(userId,this); //加入set中 addOnlineCount(); //在線數(shù)加1 } log.info("用戶連接:"+userId+",當(dāng)前在線人數(shù)為:" + getOnlineCount()); try { sendMessage("連接成功"); } catch (IOException e) { log.error("用戶:"+userId+",網(wǎng)絡(luò)異常!!!!!!"); } } /** * 連接關(guān)閉調(diào)用的方法 */ @OnClose public void onClose() { if(webSocketMap.containsKey(userId)){ webSocketMap.remove(userId); //從set中刪除 subOnlineCount(); } log.info("用戶退出:"+userId+",當(dāng)前在線人數(shù)為:" + getOnlineCount()); } /** * 收到客戶端消息后調(diào)用的方法 * * @param message 客戶端發(fā)送過(guò)來(lái)的消息*/ @OnMessage public void onMessage(String message, Session session) { log.info("用戶消息:"+userId+",報(bào)文:"+message); //可以群發(fā)消息 //消息保存到數(shù)據(jù)庫(kù)、redis if(StringUtils.isNotBlank(message)){ try { //解析發(fā)送的報(bào)文 JSONObject jsonObject = JSON.parseObject(message); //追加發(fā)送人(防止串改) jsonObject.put("fromUserId",this.userId); String toUserId=jsonObject.getString("toUserId"); //傳送給對(duì)應(yīng)toUserId用戶的websocket if(StringUtils.isNotBlank(toUserId)&&webSocketMap.containsKey(toUserId)){ webSocketMap.get(toUserId).sendMessage(jsonObject.toJSONString()); }else{ log.error("請(qǐng)求的userId:"+toUserId+"不在該服務(wù)器上"); //否則不在這個(gè)服務(wù)器上,發(fā)送到mysql或者redis } }catch (Exception e){ e.printStackTrace(); } } } /** * * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { log.error("用戶錯(cuò)誤:"+this.userId+",原因:"+error.getMessage()); error.printStackTrace(); } /** * 實(shí)現(xiàn)服務(wù)器主動(dòng)推送 */ public void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); } /** * 發(fā)送自定義消息 * */ public static void sendInfo(String message,@PathParam("userId") String userId) throws IOException { log.info("發(fā)送消息到:"+userId+",報(bào)文:"+message); if(StringUtils.isNotBlank(userId)&&webSocketMap.containsKey(userId)){ webSocketMap.get(userId).sendMessage(message); }else{ log.error("用戶"+userId+",不在線!"); } } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocketServer.onlineCount++; } public static synchronized void subOnlineCount() { WebSocketServer.onlineCount--; } }
消息推送
至于推送新信息,可以再自己的Controller寫(xiě)個(gè)方法調(diào)用WebSocketServer.sendInfo();即可
import com.softdev.system.demo.config.WebSocketServer; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.ModelAndView; import java.io.IOException; /** * WebSocketController * @author zhengkai.blog.csdn.net */ @RestController public class DemoController { @GetMapping("index") public ResponseEntity<String> index(){ return ResponseEntity.ok("請(qǐng)求成功"); } @GetMapping("page") public ModelAndView page(){ return new ModelAndView("websocket"); } @RequestMapping("/push/{toUserId}") public ResponseEntity<String> pushToWeb(String message, @PathVariable String toUserId) throws IOException { WebSocketServer.sendInfo(message,toUserId); return ResponseEntity.ok("MSG SEND SUCCESS"); } }
頁(yè)面發(fā)起
頁(yè)面用js代碼調(diào)用websocket
,當(dāng)然,太古老的瀏覽器是不行的,一般新的瀏覽器或者谷歌瀏覽器是沒(méi)問(wèn)題的。還有一點(diǎn),記得協(xié)議是ws
的,如果使用了一些路徑類,可以replace(“http”,“ws”)來(lái)替換協(xié)議。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>websocket通訊</title> </head> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script> <script> var socket; function openSocket() { if(typeof(WebSocket) == "undefined") { console.log("您的瀏覽器不支持WebSocket"); }else{ console.log("您的瀏覽器支持WebSocket"); //實(shí)現(xiàn)化WebSocket對(duì)象,指定要連接的服務(wù)器地址與端口 建立連接 //等同于socket = new WebSocket("ws://localhost:8888/xxxx/im/25"); //var socketUrl="${request.contextPath}/im/"+$("#userId").val(); var socketUrl="http://localhost:9999/demo/imserver/"+$("#userId").val(); socketUrl=socketUrl.replace("https","ws").replace("http","ws"); console.log(socketUrl); if(socket!=null){ socket.close(); socket=null; } socket = new WebSocket(socketUrl); //打開(kāi)事件 socket.onopen = function() { console.log("websocket已打開(kāi)"); //socket.send("這是來(lái)自客戶端的消息" + location.href + new Date()); }; //獲得消息事件 socket.onmessage = function(msg) { console.log(msg.data); //發(fā)現(xiàn)消息進(jìn)入 開(kāi)始處理前端觸發(fā)邏輯 }; //關(guān)閉事件 socket.onclose = function() { console.log("websocket已關(guān)閉"); }; //發(fā)生了錯(cuò)誤事件 socket.onerror = function() { console.log("websocket發(fā)生了錯(cuò)誤"); } } } function sendMessage() { if(typeof(WebSocket) == "undefined") { console.log("您的瀏覽器不支持WebSocket"); }else { console.log("您的瀏覽器支持WebSocket"); console.log('{"toUserId":"'+$("#toUserId").val()+'","contentText":"'+$("#contentText").val()+'"}'); socket.send('{"toUserId":"'+$("#toUserId").val()+'","contentText":"'+$("#contentText").val()+'"}'); } } </script> <body> <p>【userId】:<div><input id="userId" name="userId" type="text" value="10"></div> <p>【toUserId】:<div><input id="toUserId" name="toUserId" type="text" value="20"></div> <p>【toUserId】:<div><input id="contentText" name="contentText" type="text" value="hello websocket"></div> <p>【操作】:<div><a onclick="openSocket()">開(kāi)啟socket</a></div> <p>【操作】:<div><a onclick="sendMessage()">發(fā)送消息</a></div> </body> </html>
運(yùn)行效果
- v20200105,加入開(kāi)源項(xiàng)目spring-cloud-study-websocket,更新運(yùn)行效果,更方便理解。
- v1.1的效果,剛剛修復(fù)了日志,并且支持指定監(jiān)聽(tīng)某個(gè)端口,代碼已經(jīng)全部更新,現(xiàn)在是這樣的效果
打開(kāi)兩個(gè)頁(yè)面,按F12調(diào)出控控制臺(tái)查看測(cè)試效果:
頁(yè)面 | 參數(shù) |
---|---|
http://localhost:9999/demo/page | fromUserId=10,toUserId=20 |
http://localhost:9999/demo/page | fromUserId=20,toUserId=10 |
分別開(kāi)啟socket
,再發(fā)送消息
2. 向前端推送數(shù)據(jù):
http://localhost:9999/demo/push/10?message=123123
通過(guò)調(diào)用push api
,可以向指定的userId推送信息
,當(dāng)然報(bào)文這里亂寫(xiě),建議規(guī)定好格式。
后續(xù)
針對(duì)簡(jiǎn)單IM的業(yè)務(wù)場(chǎng)景,進(jìn)行了一些優(yōu)化,可以看后續(xù)的文章SpringBoot2+WebSocket之聊天應(yīng)用實(shí)戰(zhàn)(優(yōu)化版本)(v20201005已整合
)
主要變動(dòng)是CopyOnWriteArraySet改為ConcurrentHashMap
,保證多線程安全同時(shí)方便利用map.get(userId)
進(jìn)行推送到指定端口。
相比之前的Set,Set遍歷是費(fèi)事且麻煩的事情,而Map的get是簡(jiǎn)單便捷的,當(dāng)WebSocket數(shù)量大的時(shí)候,這個(gè)小小的消耗就會(huì)聚少成多,影響體驗(yàn),所以需要優(yōu)化。在IM的場(chǎng)景下,指定userId進(jìn)行推送消息更加方便。
Websocker注入Bean問(wèn)題
關(guān)于這個(gè)問(wèn)題,可以看最新發(fā)表的這篇文章,在參考和研究了網(wǎng)上一些攻略后,項(xiàng)目已經(jīng)通過(guò)該方法注入成功,大家可以參考。
Springboot 使用 JSR 303 對(duì) Controller 控制層校驗(yàn)及 Service 服務(wù)層 AOP 校驗(yàn) 使用消息資源文件對(duì)消息國(guó)際化
netty-websocket-spring-boot-starter
Springboot2構(gòu)建基于Netty的高性能Websocket服務(wù)器(netty-websocket-spring-boot-starter)
只需要換個(gè)starter即可實(shí)現(xiàn)高性能websocket,趕緊使用吧
Springboot2+Netty+Websocket
Springboot2+Netty實(shí)現(xiàn)Websocket,使用官方的netty-all的包,比原生的websocket更加穩(wěn)定更加高性能,同等配置情況下可以handle更多的連接。
代碼樣式全部已經(jīng)更正,也支持websocket連接url帶參數(shù)功能,另外也感謝大家的閱讀和評(píng)論,一起進(jìn)步,謝謝!~~
ServerEndpointExporter錯(cuò)誤
org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘
serverEndpointExporter
' defined in class path resource [com/xxx/WebSocketConfig.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: javax.websocket.server.ServerContainer not available
感謝@來(lái)了老弟兒 的反饋:
如果tomcat部署一直報(bào)這個(gè)錯(cuò),請(qǐng)移除 WebSocketConfig
中@Bean ServerEndpointExporter
的注入 。
ServerEndpointExporter
是由Spring官方提供的標(biāo)準(zhǔn)實(shí)現(xiàn),用于掃描ServerEndpointConfig
配置類和@ServerEndpoint
注解實(shí)例。使用規(guī)則也很簡(jiǎn)單:
如果使用默認(rèn)的嵌入式容器 比如Tomcat
則必須手工在上下文提供ServerEndpointExporter
。如果使用外部容器部署war
包,則不需要
提供提供ServerEndpointExporter
,因?yàn)榇藭r(shí)SpringBoot默認(rèn)將掃描服務(wù)端的行為交給外部容器處理,所以線上部署的時(shí)候要把WebSocketConfig中這段注入bean的代碼注掉。 正式項(xiàng)目的前端WebSocket框架 GoEasy
感謝kkatrina
的補(bǔ)充,正式的項(xiàng)目中,一般是用第三方websocket框架來(lái)做,穩(wěn)定性、實(shí)時(shí)性有保證的多,也會(huì)包括一些心跳、重連機(jī)制。
GoEasy專注于服務(wù)器與瀏覽器,瀏覽器與瀏覽器之間消息推送,完美兼容世界上的絕大多數(shù)瀏覽器,包括IE6, IE7之類的非常古老的瀏覽器。支持Uniapp,各種小程序,react,vue等所有主流Web前端技術(shù)。
GoEasy采用 發(fā)布/訂閱 的消息模式,幫助您非常輕松的實(shí)現(xiàn)一對(duì)一,一對(duì)多的通信。
https://www.goeasy.io/cn/doc/
@Component
和@ServerEndpoint
關(guān)于是否單例模式,能否使用static Map等一些問(wèn)題的解答
看到大家都在熱心的討論關(guān)于是否單例模式這個(gè)問(wèn)題,請(qǐng)大家相信自己的直接,如果websocket是單例模式,還怎么服務(wù)這么多session呢。
- websocket是
原型模式
,@ServerEndpoint
每次建立雙向通信的時(shí)候都會(huì)創(chuàng)建一個(gè)實(shí)例,區(qū)別于spring的單例模式。Spring的@Component
默認(rèn)是單例模式
,請(qǐng)注意,默認(rèn)
而已,是可以被改變的。 - 這里的
@Component
僅僅為了支持@Autowired依賴注入
使用,如果不加則不能注入任何東西,為了方便。 - 什么是
prototype 原型模式
? 基本就是你需要從A的實(shí)例得到一份與A內(nèi)容相同,但是又互不干擾的實(shí)例B的話,就需要使用原型模式。關(guān)于在原型模式下使用static 的webSocketMap
,請(qǐng)注意這是ConcurrentHashMap ,也就是線程安全/線程同步的,而且已經(jīng)是靜態(tài)變量
作為全局調(diào)用,這種情況下是ok的,或者大家如果有顧慮或者更好的想法的化,可以進(jìn)行改進(jìn)。 - 例如使用一個(gè)中間類來(lái)接收和存放session。為什么每次都@OnOpen都要檢查
webSocketMap.containsKey(userId)
,首先了為了代碼強(qiáng)壯性
考慮,假設(shè)代碼以及機(jī)制沒(méi)有問(wèn)題,那么肯定這個(gè)邏輯是廢的對(duì)吧。 - 但是實(shí)際使用的時(shí)候發(fā)現(xiàn)偶爾會(huì)出現(xiàn)重連失敗或者其他原因?qū)е轮暗膕ession還存在,這里就做了一個(gè)
清除舊session,迎接新session
的功能。
Vue版本的websocket連接
感謝**@GzrStudy**的貢獻(xiàn),供大家參考。
<script> export default { data() { return { socket:null, userId:localStorage.getItem("ms_uuid"), toUserId:'2', content:'3' } }, methods: { openSocket() { if (typeof WebSocket == "undefined") { console.log("您的瀏覽器不支持WebSocket"); } else { console.log("您的瀏覽器支持WebSocket"); //實(shí)現(xiàn)化WebSocket對(duì)象,指定要連接的服務(wù)器地址與端口 建立連接 //等同于socket = new WebSocket("ws://localhost:8888/xxxx/im/25"); //var socketUrl="${request.contextPath}/im/"+$("#userId").val(); var socketUrl = "http://localhost:8081/imserver/" + this.userId; socketUrl = socketUrl.replace("https", "ws").replace("http", "ws"); console.log(socketUrl); if (this.socket != null) { this.socket.close(); this.socket = null; } this.socket = new WebSocket(socketUrl); //打開(kāi)事件 this.socket = new WebSocket(socketUrl); //打開(kāi)事件 this.socket.onopen = function() { console.log("websocket已打開(kāi)"); //socket.send("這是來(lái)自客戶端的消息" + location.href + new Date()); }; //獲得消息事件 this.socket.onmessage = function(msg) { console.log(msg.data); //發(fā)現(xiàn)消息進(jìn)入 開(kāi)始處理前端觸發(fā)邏輯 }; //關(guān)閉事件 this.socket.onclose = function() { console.log("websocket已關(guān)閉"); }; //發(fā)生了錯(cuò)誤事件 this.socket.onerror = function() { console.log("websocket發(fā)生了錯(cuò)誤"); }; } }, sendMessage() { if (typeof WebSocket == "undefined") { console.log("您的瀏覽器不支持WebSocket"); } else { console.log("您的瀏覽器支持WebSocket"); console.log( '{"toUserId":"' + this.toUserId + '","contentText":"' + this.content + '"}' ); this.socket.send( '{"toUserId":"' + this.toUserId + '","contentText":"' + this.content + '"}' ); } }
到此這篇關(guān)于SpringBoot2.0集成WebSocket實(shí)現(xiàn)后臺(tái)向前端推送信息的文章就介紹到這了,更多相關(guān)SpringBoot2.0集成WebSocket內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot集成WebSocket實(shí)現(xiàn)后臺(tái)向前端推送信息的示例
- Spring?Boot?使用?SSE?方式向前端推送數(shù)據(jù)詳解
- SpringBoot整合WebSocket實(shí)現(xiàn)后端向前端主動(dòng)推送消息方式
- SpringBoot集成WebSocket實(shí)現(xiàn)后臺(tái)向前端推送信息
- SpringBoot使用WebSocket實(shí)現(xiàn)向前端推送消息功能
- SpringBoot+WebSocket向前端推送消息的實(shí)現(xiàn)示例
- 利用Netty+SpringBoot實(shí)現(xiàn)定時(shí)后端向前端推送數(shù)據(jù)
相關(guān)文章
Java中動(dòng)態(tài)地改變數(shù)組長(zhǎng)度及數(shù)組轉(zhuǎn)Map的代碼實(shí)例分享
這篇文章主要介紹了Java中動(dòng)態(tài)地改變數(shù)組長(zhǎng)度及數(shù)組轉(zhuǎn)map的代碼分享,其中轉(zhuǎn)Map利用到了java.util.Map接口,需要的朋友可以參考下2016-03-03詳解springMVC之與json數(shù)據(jù)交互方法
本篇文章主要介紹了詳解springMVC之與json數(shù)據(jù)交互方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-05-05Java使用ES?Client?調(diào)用滾動(dòng)查詢及Elasticsearch滾動(dòng)查詢Scrolling機(jī)制
Elasticsearch提供了一種稱為"滾動(dòng)查詢"(Scrolling)的機(jī)制,用于處理大型數(shù)據(jù)集的分頁(yè)查詢,這篇文章給大家介紹滾動(dòng)查詢的一般步驟及Java使用ESClient調(diào)用滾動(dòng)查詢的方法,感興趣的朋友一起看看吧2023-08-08Spring MVC url提交參數(shù)和獲取參數(shù)
本文重要講述通過(guò)url提交參數(shù)和獲取參數(shù)的具體操作與實(shí)現(xiàn)。具有很好的參考價(jià)值。下面跟著小編一起來(lái)看下吧2017-04-04詳解Java動(dòng)態(tài)字節(jié)碼技術(shù)
Java字節(jié)碼增強(qiáng)指的是在Java字節(jié)碼生成之后,對(duì)其進(jìn)行修改,增強(qiáng)其功能,可減少冗余代碼,提高性能等。本文將詳細(xì)介紹Java動(dòng)態(tài)字節(jié)碼技術(shù)。2021-05-05Spring使用Jackson實(shí)現(xiàn)轉(zhuǎn)換XML與Java對(duì)象
這篇文章主要為大家詳細(xì)介紹了Spring如何使用Jackson實(shí)現(xiàn)轉(zhuǎn)換XML與Java對(duì)象,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-02-02