SpringBoot中集成串口通信的項(xiàng)目實(shí)踐
串口通信介紹
- 串口通信是一種按位發(fā)送和接收字節(jié)的簡(jiǎn)單概念,盡管比并行通信慢,但串口可以同時(shí)使用一根線發(fā)送數(shù)據(jù)和接收數(shù)據(jù)。
- 串口通信簡(jiǎn)單且能夠?qū)崿F(xiàn)遠(yuǎn)距離通信,例如,串口的長(zhǎng)度可達(dá)1200米,而并行通信的長(zhǎng)度限制為20米.
- 串口通常用于ASCII碼字符的傳輸,通信使用地線、發(fā)送線和接收線三根線完成。
- 重要的參數(shù)有波特率、數(shù)據(jù)位、停止位和奇偶校驗(yàn)。
波特率
這是一個(gè)衡量符號(hào)傳輸速率的參數(shù)。指的是信號(hào)被調(diào)制以后在單位時(shí)間內(nèi)的變化,即單位時(shí)間內(nèi)載波參數(shù)變化的次數(shù),如每秒鐘傳送240個(gè)字符,而每個(gè)字符格式包含10位(1個(gè)起始位,1個(gè)停止位,8個(gè)數(shù)據(jù)位),這時(shí)的波特率為240Bd,比特率為10位*240個(gè)/秒=2400bps。一般調(diào)制速率大于波特率,比如通常電話線的波特率為14400,28800和36600。波特率可以遠(yuǎn)遠(yuǎn)大于這些值,但是波特率和距離成反比。高波特率常常用于放置的很近的儀器間的通信,典型的例子就是GPIB設(shè)備的通信
數(shù)據(jù)位
這是衡量通信中實(shí)際數(shù)據(jù)位的參數(shù)。當(dāng)計(jì)算機(jī)發(fā)送一個(gè)信息包,實(shí)際的數(shù)據(jù)往往不會(huì)是8位的,標(biāo)準(zhǔn)的值是6、7和8位。如何設(shè)置取決于你想傳送的信息。比如,標(biāo)準(zhǔn)的ASCII碼是0~127(7位)。擴(kuò)展的ASCII碼是0~255(8位)。如果數(shù)據(jù)使用簡(jiǎn)單的文本(標(biāo)準(zhǔn) ASCII碼),那么每個(gè)數(shù)據(jù)包使用7位數(shù)據(jù)。每個(gè)包是指一個(gè)字節(jié),包括開始/停止位,數(shù)據(jù)位和奇偶校驗(yàn)位。由于實(shí)際數(shù)據(jù)位取決于通信協(xié)議的選取,術(shù)語“包”指任何通信的情況。
停止位
用于表示單個(gè)包的最后一位。典型的值為1,1.5和2位。由于數(shù)據(jù)是在傳輸線上定時(shí)的,并且每一個(gè)設(shè)備有其自己的時(shí)鐘,很可能在通信中兩臺(tái)設(shè)備間出現(xiàn)了小小的不同步。因此停止位不僅僅是表示傳輸?shù)慕Y(jié)束,并且提供計(jì)算機(jī)校正時(shí)鐘同步的機(jī)會(huì)。適用于停止位的位數(shù)越多,不同時(shí)鐘同步的容忍程度越大,但是數(shù)據(jù)傳輸率同時(shí)也越慢。
奇偶校驗(yàn)位
在串口通信中一種簡(jiǎn)單的檢錯(cuò)方式。有四種檢錯(cuò)方式:偶、奇、高和低。當(dāng)然沒有校驗(yàn)位也是可以的。對(duì)于偶和奇校驗(yàn)的情況,串口會(huì)設(shè)置校驗(yàn)位(數(shù)據(jù)位后面的一位),用一個(gè)值確保傳輸?shù)臄?shù)據(jù)有偶個(gè)或者奇?zhèn)€邏輯高位。例如,如果數(shù)據(jù)是011,那么對(duì)于偶校驗(yàn),校驗(yàn)位為0,保證邏輯高的位數(shù)是偶數(shù)個(gè)。如果是奇校驗(yàn),校驗(yàn)位為1,這樣就有3個(gè)邏輯高位。高位和低位不真正的檢查數(shù)據(jù),簡(jiǎn)單置位邏輯高或者邏輯低校驗(yàn)。這樣使得接收設(shè)備能夠知道一個(gè)位的狀態(tài),有機(jī)會(huì)判斷是否有噪聲干擾了通信或者是否傳輸和接收數(shù)據(jù)是否不同步。
開始集成
組件介紹
對(duì)于Java集成串口通信,常見的選擇有 原生Java串口通信API、RXTX庫(kù)、jSerialComm庫(kù),
- 原生Java串口通信API只支持到Java6版本,后續(xù)便不再維護(hù),所以不推薦使用
- RXTX庫(kù)是過去主流開發(fā)串口通信使用的依賴組件,但是由于需要在jvm包中添加指定的依賴組件,其次,RXTX的穩(wěn)定性和兼容性可能存在一些問題,且僅維護(hù)至Jdk8版本,后續(xù)不再持續(xù)維護(hù)了,所以本次也不考慮使用它
- 所以本次采用的是jSerialComm庫(kù),以下是jSerialComm庫(kù)的一些主要特點(diǎn)和功能:
- 跨平臺(tái)支持:jSerialComm可以在多個(gè)操作系統(tǒng)上使用,包括Windows、Linux和MacOS等。
- 多串口支持:它可以同時(shí)管理多個(gè)串口,通過獲取和管理已連接的串口列表,方便選擇和使用特定的串口。
- 簡(jiǎn)單的API:jSerialComm提供了簡(jiǎn)潔易用的API,使串口的打開、讀取、寫入和關(guān)閉等操作變得簡(jiǎn)單和直觀。
- 支持異步讀?。嚎梢允褂没卣{(diào)函數(shù)或監(jiān)聽器來異步讀取串口數(shù)據(jù),實(shí)現(xiàn)非阻塞的讀取操作。
- 高性能:jSerialComm使用了底層的串口通信庫(kù),具有高效的讀寫性能,適用于處理大量的串口數(shù)據(jù)。
- 可靠性和穩(wěn)定性:它經(jīng)過了充分測(cè)試和優(yōu)化,具有良好的穩(wěn)定性和可靠性,能夠處理各種串口通信場(chǎng)景。
- 開源免費(fèi):jSerialComm是一個(gè)開源庫(kù),使用MIT許可證,可以免費(fèi)使用和修改。
Maven依賴導(dǎo)入
<!-- COM串口通信 -->
<dependency>
<groupId>com.fazecast</groupId>
<artifactId>jSerialComm</artifactId>
<version>2.6.2</version>
</dependency>
<!-- hutool工具 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.5</version>
</dependency>配置類
創(chuàng)建一個(gè) SerialConfig 用于定義串口通用信息配置
import com.fazecast.jSerialComm.SerialPort;
import lombok.Data;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
?* ?用于定義串口通用信息配置
?* */
@Configuration
public class SerialConfig {
? ? /**
? ? ?* ?波特率
? ? ?* */
? ? public static int baudRate = 19200;
? ? /**
? ? ?* 數(shù)據(jù)位
? ? ?*/
? ? public static int dataBits = 8;
? ? /**
? ? ?* 停止位 ( 1停止位 = 1 ?、 1.5停止位 = 2 、2停止位 = 3)
? ? ?* */
? ? public static int stopBits = 1;
? ? /**
? ? ?* 校驗(yàn)?zāi)J?( 無校驗(yàn) = 0 ?、奇校驗(yàn) = 1 、偶校驗(yàn) = 2、 標(biāo)記校驗(yàn) = 3、 空格校驗(yàn) = 4 ?)
? ? ?* */
? ? public static int parity = 1;
? ? /**
? ? ?* ?是否為 Rs485通信
? ? ?* */
? ? public static boolean rs485Mode = true;
? ? /**
? ? ?* ?串口讀寫超時(shí)時(shí)間(毫秒)
? ? ?* */
? ? public static int timeOut = 300;
? ? /**
? ? ?* 消息模式
? ? ?* 非阻塞模式: #TIMEOUT_NONBLOCKING ? ? ? ? ? 【在該模式下,readBytes(byte[], long)和writeBytes(byte[], long)調(diào)用將立即返回任何可用數(shù)據(jù)?!?
? ? ?* 寫阻塞模式: #TIMEOUT_WRITE_BLOCKING ? ? ? ?【在該模式下,writeBytes(byte[], long)調(diào)用將阻塞,直到所有數(shù)據(jù)字節(jié)都成功寫入輸出串口設(shè)備。】
? ? ?* 半阻塞讀取模式: #TIMEOUT_READ_SEMI_BLOCKING 【在該模式下,readBytes(byte[], long)調(diào)用將阻塞,直到達(dá)到指定的超時(shí)時(shí)間或者至少可讀取1個(gè)字節(jié)的數(shù)據(jù)。】
? ? ?* 全阻塞讀取模式:#TIMEOUT_READ_BLOCKING ? ? ? 【在該模式下,readBytes(byte[], long)調(diào)用將阻塞,直到達(dá)到指定的超時(shí)時(shí)間或者可以返回請(qǐng)求的字節(jié)數(shù)?!?
? ? ?* 掃描器模式:#TIMEOUT_SCANNER ? ? ? ? ? ? ? ?【該模式適用于使用Java的java.util.Scanner類從串口進(jìn)行讀取,會(huì)忽略手動(dòng)指定的超時(shí)值以確保與Java規(guī)范的兼容性】
? ? ?* */
? ? public static int messageModel = SerialPort.TIMEOUT_READ_BLOCKING;
? ? /**
? ? ?* ?已打開的COM串口 (重復(fù)打開串口會(huì)導(dǎo)致后面打開的無法使用,所以打開一次就要記錄到公共變量存儲(chǔ))
? ? ?* */
? ? public final static Map<String, SerialPort> portMap = new HashMap<>();
}串口工具類
準(zhǔn)備一個(gè)SerialService 用于創(chuàng)建串口,關(guān)閉串口,收發(fā)消息
import cn.hutool.core.codec.BCD;
import com.fazecast.jSerialComm.SerialPort;
import com.tce.station.common.config.SerialConfig;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 串口服務(wù)類
* */
@AllArgsConstructor
@Slf4j
@Service
public class SerialService {
/**
* 獲取串口及狀態(tài)
* */
public Map<String, Boolean> getPortStatus(){
Map<String, Boolean> comStatusMap = new HashMap<>();
List<SerialPort> commPorts = Arrays.asList(SerialPort.getCommPorts());
commPorts.forEach(port->{
comStatusMap.put(port.getSystemPortName(), port.isOpen());
});
return comStatusMap;
}
/**
* 添加串口連接
* */
public void connectSerialPort(String portName){
SerialPort commPort = SerialPort.getCommPort(portName);
if (commPort.isOpen()){
throw new RuntimeException("該串口已被占用");
}
if (SerialConfig.portMap.containsKey(portName)){
throw new RuntimeException("該串口已被占用");
}
// 打開端口
commPort.openPort();
if (!commPort.isOpen()){
throw new RuntimeException("打開串口失敗");
}
// 設(shè)置串口參數(shù) (波特率、數(shù)據(jù)位、停止位、校驗(yàn)?zāi)J?、是否為Rs485)
commPort.setComPortParameters(SerialConfig.baudRate, SerialConfig.dataBits,SerialConfig.stopBits, SerialConfig.stopBits, SerialConfig.rs485Mode);
// 設(shè)置串口超時(shí)和模式
commPort.setComPortTimeouts(SerialConfig.messageModel ,SerialConfig.timeOut, SerialConfig.timeOut);
// 添加至串口記錄Map
SerialConfig.portMap.put(portName, commPort);
}
/**
* 關(guān)閉串口連接
* */
public boolean closeSerialPort(String portName){
if (!SerialConfig.portMap.containsKey(portName)){
throw new RuntimeException("該串口未啟用");
}
// 獲取串口
SerialPort port = SerialConfig.portMap.get(portName);
// 關(guān)閉串口
port.closePort();
// 需要等待一些時(shí)間,否則串口關(guān)閉不完全,會(huì)導(dǎo)致無法打開
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (port.isOpen()){
return false;
}else {
// 關(guān)閉成功返回
return true;
}
}
/**
* 串口發(fā)送數(shù)據(jù)
* */
public void sendComData(String portName, byte[]sendBytes){
if (!SerialConfig.portMap.containsKey(portName)){
throw new RuntimeException("該串口未啟用");
}
// 獲取串口
SerialPort port = SerialConfig.portMap.get(portName);
// 發(fā)送串口數(shù)據(jù)
int i = port.writeBytes(sendBytes, sendBytes.length);
if (i == -1){
log.error("發(fā)送串口數(shù)據(jù)失敗{}, 數(shù)據(jù)內(nèi)容{}",portName, BCD.bcdToStr(sendBytes));
throw new RuntimeException("發(fā)送串口數(shù)據(jù)失敗");
}
}
/**
* 串口讀取數(shù)據(jù)
* */
public byte[] readComData(String portName){
if (!SerialConfig.portMap.containsKey(portName)){
throw new RuntimeException("該串口未啟用");
}
// 獲取串口
SerialPort port = SerialConfig.portMap.get(portName);
// 讀取串口流
InputStream inputStream = port.getInputStream();
// 獲取串口返回的流大小
int availableBytes = 0;
try {
availableBytes = inputStream.available();
} catch (Exception e) {
e.printStackTrace();
}
// 讀取指定的范圍的數(shù)據(jù)流
byte[] readByte = new byte[availableBytes];
int bytesRead = 0;
try {
bytesRead = inputStream.read(readByte);
} catch (Exception e) {
e.printStackTrace();
}
return readByte;
}
}串口業(yè)務(wù)類使用
基于以上的工具類就已經(jīng)可以對(duì)串口通信進(jìn)行開發(fā)了,以下是使用案例
1.創(chuàng)建串口連接
可以使用監(jiān)聽器方式接收數(shù)據(jù),但是需要進(jìn)行綁定,后續(xù)會(huì)介紹
// 從數(shù)據(jù)庫(kù)或者配置表中讀取設(shè)定要打開的串口
List<String> comList = comService.list();
// 關(guān)閉之前的監(jiān)聽連接(提取所有串口避免重復(fù)關(guān)閉) ?
SerialConfig.portMap.forEach((com,serialPort) ->{ ?
?? ?serialService.closeSerialPort(com); ?
});
// 等待之前的串口發(fā)送和 2倍監(jiān)聽超時(shí),避免還有串口通信線程未關(guān)閉 ?
try { ?
?? ?Thread.sleep((SerialConfig.timeOut + 1) * 2); ?
} catch (InterruptedException e) { ?
?? ?throw new RuntimeException(e); ?
}
// 清空COM口記錄 ?
SerialConfig.portMap.clear();
// 重新連接串口 ?
gunList.forEach(gun->{ ?
?? ?// 如果COM口沒有就打開 ?
?? ?if (!SerialConfig.portMap.containsKey(gun.getCom())){ ?
?? ??? ?// 創(chuàng)建連接 ?
?? ??? ?SerialPort serialPort = serialService.connectSerialPort(gun.getCom()); ?
?? ??? ?// 綁定監(jiān)聽器 ?
?? ??? ?// serialPort.addDataListener(new MessageListener()); ?
?? ?} ??
});2.關(guān)閉串口連接
String com = "COM1"; serialService.closeSerialPort(com);
3.定時(shí)發(fā)送串口數(shù)據(jù)
/**
* 周期性向串口發(fā)送數(shù)據(jù)
* */
@Scheduled(fixedRate = 1500L)
public void send{
// 因?yàn)槭亲枞潜O(jiān)聽線程,所以使用線程處理
Thread thread = new Thread(() -> {
try {
SerialConfig.portMap.forEach((com,serialPort)->{
// 等待0.1秒
try {
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
// 調(diào)用業(yè)務(wù)邏輯獲取需要推送的數(shù)據(jù)
byte[] sendBytes = getPushData(com);
// 發(fā)送串口數(shù)據(jù)
serialService.sendComData(com, sendBytes);
log.info("向串口發(fā)送 {}",gun.getGunNum(), com, BCD.bcdToStr(sendBytes));
});
}catch (ConcurrentModificationException e){
log.info("COM口配置發(fā)生變化,等待配置生效");
}
});
// 開啟發(fā)送線程
thread.start();
}4.周期性讀取串口數(shù)據(jù)
/**
* 周期性讀取串口數(shù)據(jù)
* */
@Scheduled(fixedRate = 1000L)
public void readComData() {
// 遍歷監(jiān)聽
SerialConfig.portMap.forEach((com,serialPort)->{
// 因?yàn)槭亲枞潜O(jiān)聽線程,所以使用線程處理,否則某個(gè)讀取失敗,會(huì)阻塞整個(gè)程序
Thread thread = new Thread(() -> {
byte[] readByte = serialService.readComData(com);
// 有數(shù)據(jù)才執(zhí)行
if (readByte.length > 1) {
try {
log.info("收到串口數(shù)據(jù): {}", BCD.bcdToStr(readByte));
// 調(diào)用串口響應(yīng)業(yè)務(wù)操作
comOperationByData(comResult,BCD.strToBcd(res), com);
}catch (Exception e){
e.printStackTrace();
}
});
// 開啟線程
thread.start();
}5.監(jiān)聽式讀取串口數(shù)據(jù)
監(jiān)聽式讀取數(shù)據(jù)使用的是非阻塞行讀取數(shù)據(jù),有數(shù)據(jù)就會(huì)觸發(fā)
創(chuàng)建一個(gè)監(jiān)聽器
@Slf4j ?
public class MessageListener implements SerialPortDataListener { ?
@Autowired ?
ICommandService commandService; ?
/** ?
* 監(jiān)聽事件設(shè)置 ?
* */ ?
@Override ?
public int getListeningEvents() { ?
?? ?// 持續(xù)返回?cái)?shù)據(jù)流模式 ?
?? ?return SerialPort.LISTENING_EVENT_DATA_AVAILABLE; ?
?? ?// 收到數(shù)據(jù)立即返回 ?
?? ?// return SerialPort.LISTENING_EVENT_DATA_RECEIVED; ?
} ?
/** ?
* 收到數(shù)據(jù)監(jiān)聽回調(diào) ?
* */ ?
@Override ?
public void serialEvent(SerialPortEvent event) { ?
?? ?// 因?yàn)槭亲枞潜O(jiān)聽線程,所以使用線程處理 ?
?? ?Thread thread = new Thread(() -> { ?
?? ??? ?// 讀取串口流 ?
?? ??? ?InputStream inputStream = event.getSerialPort().getInputStream(); ?
?? ??? ?// 獲取串口返回的流大小 ?
?? ??? ?int availableBytes = 0; ?
?? ??? ?try { ?
?? ??? ??? ?availableBytes = inputStream.available(); ?
?? ??? ?} catch (Exception e) { ?
?? ??? ??? ?e.printStackTrace(); ?
?? ??? ?} ?
?? ??? ?// 讀取指定的范圍的數(shù)據(jù)流 ?
?? ??? ?byte[] readByte = new byte[availableBytes]; ?
?? ??? ?int bytesRead = 0; ?
?? ??? ?try { ?
?? ??? ??? ?bytesRead = inputStream.read(readByte); ?
?? ??? ?} catch (Exception e) { ?
?? ??? ??? ?e.printStackTrace(); ?
?? ??? ?} ?
?? ??? ?try { ?
?? ??? ??? ?inputStream.close(); ?
?? ??? ?} catch (IOException e) { ?
?? ??? ??? ?throw new RuntimeException("關(guān)閉串口流失敗"+e.getMessage()); ?
?? ??? ?} ?
?? ??? ?// 有數(shù)據(jù)才執(zhí)行
?? ??? ?if (readByte.length > 1) { ?
?? ??? ??? ?try { ?
?? ??? ??? ??? ?log.info("收到串口數(shù)據(jù): {}", BCD.bcdToStr(readByte)); ?
?? ??? ??? ??? ?// 調(diào)用串口響應(yīng)業(yè)務(wù)操作 ?
?? ??? ??? ??? ?comOperationByData(comResult,BCD.strToBcd(res), com); ?
?? ??? ??? ?}catch (Exception e){ ?
?? ??? ??? ??? ?e.printStackTrace(); ?
?? ??? ??? ?}?
?? ??? ?}?
?? ?}); ?
// 開啟線程
thread.start(); ?
} ?
}給串口連接進(jìn)行綁定監(jiān)聽器
// 創(chuàng)建連接 SerialPort serialPort = serialService.connectSerialPort(gun.getCom()); // 綁定監(jiān)聽器 serialPort.addDataListener(new MessageListener());
需要注意的是監(jiān)聽器接收數(shù)據(jù)和定時(shí)接收數(shù)據(jù)選取其中一個(gè)就好了
到此這篇關(guān)于SpringBoot中集成串口通信的項(xiàng)目實(shí)踐的文章就介紹到這了,更多相關(guān)SpringBoot 串口通信內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot+Kotlin中使用GRPC實(shí)現(xiàn)服務(wù)通信的示例代碼
- SpringBoot實(shí)現(xiàn)WebSocket全雙工通信的項(xiàng)目實(shí)踐
- Springboot集成SSE實(shí)現(xiàn)單工通信消息推送流程詳解
- SpringBoot整合websocket實(shí)現(xiàn)即時(shí)通信聊天
- SpringBoot實(shí)現(xiàn)jsonp跨域通信的方法示例
- SpringMvc/SpringBoot HTTP通信加解密的實(shí)現(xiàn)
- 使用 Spring Boot 實(shí)現(xiàn) WebSocket實(shí)時(shí)通信
- Springboot實(shí)現(xiàn)阿里云通信短信服務(wù)有關(guān)短信驗(yàn)證碼的發(fā)送功能
- Spring Boot 開發(fā)私有即時(shí)通信系統(tǒng)(WebSocket)
相關(guān)文章
SpringCloud URL重定向及轉(zhuǎn)發(fā)代碼實(shí)例
這篇文章主要介紹了SpringCloud URL重定向及轉(zhuǎn)發(fā)代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03
Spring + Spring Boot + MyBatis + MongoDB的整合教程
這篇文章主要給大家介紹了關(guān)于Spring + Spring Boot + MyBatis + MongoDB的整合教程,文中通過圖文以及示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧。2017-12-12
MyBatis使用resultMap如何解決列名和屬性名不一致
這篇文章主要介紹了MyBatis使用resultMap如何解決列名和屬性名不一致的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01
Java中的Callable實(shí)現(xiàn)多線程詳解
這篇文章主要介紹了Java中的Callable實(shí)現(xiàn)多線程詳解,接口Callable中有一個(gè)call方法,其返回值類型為V,這是一個(gè)泛型,值得關(guān)注的是這個(gè)call方法有返回值,這意味著線程執(zhí)行完畢后可以將處理結(jié)果返回,需要的朋友可以參考下2023-08-08
關(guān)于javax.validation.constraints的超詳細(xì)說明
這篇文章主要給大家介紹了關(guān)于javax.validation.constraints的超詳細(xì)說明,文中通過代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2025-07-07
Java事務(wù)管理學(xué)習(xí)之Hibernate詳解
hibernate是jdbc輕量級(jí)的封裝,本身不具備事務(wù)管理的能力,在事物管理層面,一般是委托于底層的jdbc和jta來完成調(diào)度的。下面這篇文章主要給大家介紹了Java事務(wù)管理學(xué)習(xí)之Hibernate的相關(guān)資料,需要的朋友可以參考下。2017-03-03

