一、jxl 的用法:
缺点:仅限 excel2003
特点:代码中,没有用到 FileInputStream
<dependency>
<groupId>net.sourceforge.jexcelapi</groupId>
<artifactId>jxl</artifactId>
<version>2.6.3</version>
</dependency>
jxl - EXCEL2003 |
步骤 |
a. 创建输出流
1 2 3 | OutputStream os = response.getOutputStream();
response.setHeader( "Content-Disposition" ,
"attachment; filename=" + new String(fileName.getBytes( "gb2312" ), "iso-8859-1" ));
|
|
b. 创建 WritableWorkbook (也可选择加载模板)
1 2 3 4 5 | WritableWorkbook wwb = Workbook.createWorkbook(os);
/** 加载模板
Workbook wb = Workbook.getWorkbook(new File(path));
WritableWorkbook wwb = Workbook.createWorkbook(os, wb)
*/
|
|
c. 创建 WritableSheet (或者选取指定已有sheet)
1 2 3 4 | WritableSheet sheet = wwb.createSheet(fileName, 0 );
/** 指定 sheet
WritableSheet sheet = wwb.getSheet("Sheet1");
*/
|
|
d. 创建 WritableFont
1 | WritableFont wf = new WritableFont(WritableFont.createFont( "Arial Unicode MS" ), 9 );
|
|
e. 创建 WritableCellFormat 用于 excel 单元格的格式
1 | WritableCellFormat wcf = new WritableCellFormat(wf);
|
|
f. 创建 Label(列 行 从 0 开始)
1 | Label label = new Label(column_num, row_num, value, wcf);
|
|
g. 添加 cell
|
h. 输出,并关闭各文件,各流
1 2 3 4 | wwb.write();
wwb.close();
os.flush();
os.close();
|
|
二、poi 处理(雷很多多多多多多。。。)
1. 生成 excel2003 & excel2007
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>3.9</version>
</dependency>
poi - EXCEL |
步骤 |
a. 创建输出流
1 2 3 | OutputStream os = response.getOutputStream();
response.setHeader( "Content-Disposition" ,
"attachment; filename=" + new String(fileName.getBytes( "gb2312" ), "iso-8859-1" ));
|
|
b. 创建 HSSFWorkbook \ XSSFWorkbook(也可加载模板)
1 2 3 4 5 6 7 | Workbook workbook = new HSSFWorkbook();
/** 加载模板(需要创建输入流)
FileInputStream fis = new FileInputStream(new File(path));
Workbook workbook = new HSSFWorkbook(fis);
// Workbook workbook = new XSSFWorkbook(fis);// 用于2007
*/
|
|
c. 创建 Sheet(或者选取指定已有 sheet)
1 2 3 4 | Sheet sheet = workbook.createSheet( "testdata" );
/** 指定 sheet
Sheet sheet = workbook.getSheetAt(0);
*/
|
|
d. 创建 Row(或者指定已有 row)
创建 Cell(或者指定已有 Cell)
(**** 注:1. 赋值之前一定要判断空值,实际中,无缘无故说 cell 为空,非常无语。O__O "…)
1 2 3 4 5 6 7 8 9 | Row row = sheet.getRow(row_num);
if (row == null ) {
row = sheet.createRow(row_num);
}
Cell cell = row.getCell(col_num);
if (cell == null ) {
cell = row.createCell(col_num);
}
cell.setCellValue(value);
|
|
e. 合并单元格


(注:报“修复”的错误,是如下的原因。网上说的什么方法过时,什么导错包,都是扯淡!!!
①:excel 模板中的坐标已经合并了单元格,程序中,在相同坐标又重复合并了单元格
②:在循环中用到了合并单元格,那么可能重复合并了单元格,用 debug 查下
③:代码:sheet.addMergedRegion(new CellRangeAddress(12, 12,0, 0))的意思是 从 13 行 1 列到 13 行 1 列合并,也就是说值合并了一个单元格,也是错!!
根据上面三个原因,终归是因为一个原因:重复合并单元格!!!)
1 2 | /** 这个方法有点怪:先 行 后 列 */
sheet.addMergedRegion( new CellRangeAddress(rowStart, rowEnd, columnStart, columnEnd))
|
|
f. 设定单元格值
1 | cell.setCellValue(value);
|
|
g. 输出,并关闭各流
1 2 3 4 | workbook.write(outputStream);
outputStream.flush();
outputStream.close();
fileInputStream.close();
|
|
2. 生成 word2003(弊端:图片只能跟文字在一个层级上,而且不能旋转。最好用 jacob)
poi - WORD2003(以使用模板形式为例) |
步骤 |
a. 创建输出流
1 2 3 | OutputStream os = response.getOutputStream();
response.setHeader( "Content-Disposition" ,
"attachment; filename=" + new String(fileName.getBytes( "gb2312" ), "iso-8859-1" ));
|
|
b. 导入模板(需要使用 FileInputStream)
1 2 | FileInputStream fis = new FileInputStream( new File(path));
HWPFDocument doc = new HWPFDocument(fis);
|
|
c. 创建 Rang:
1 | Range range = hwpfDocument.getRange();
|
|
d. 替换 指定文字:
(注:网上有很多的 通过 paragraph 和 table 来替换的,但是我试了下,如果你有指定的key(比如自定义 @key@),那么可以直接替换)
1 | range.replaceText(key, value);
|
|
e. 输出,并关闭各流:
1 2 3 4 | hwpfDocument.write(outputStream);doc.write(os);
outputStream.flush();
outputStream.close();
fileInputStream.close();
|
|
3. 生成 word2007
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.9</version>
</dependency>
poi - WORD2007(以使用模板形式为例) |
步骤 |
a. 创建输出流
1 2 3 | OutputStream os = response.getOutputStream();
response.setHeader( "Content-Disposition" ,
"attachment; filename=" + new String(fileName.getBytes( "gb2312" ), "iso-8859-1" ));
|
|
b. 加载模板:
(注:网上打开模板的方式如下,但是这个方式会直接修改模板文件。也就是说,当你运行完一次以后,模板也会被修改。当第二次运行时,模板已经变成了第一次运行之后的文件了。因此要慎用)
1 2 3 4 5 | XWPFDocument xwpfDocument = new XWPFDocument( new File(path));
/** 网上打开模板的方式如下
OPCPackage opcPackage = POIXMLDocument.openPackage(path);
xwpfDocument = new XWPFDocument(opcPackage);
*/
|
|
c. 获得 paragraph list,并替换 pargraph 中的关键字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | for (XWPFParagraph xwpfParagraph : paragraphList) {
xwpfParagraph = replaceKeyInParagraph(xwpfParagraph, paraList);
}
public XWPFParagraph replaceKeyInParagraph(XWPFParagraph xwpfParagraph,
List<RPTBean> paraList) {
List<XWPFRun> xwpfRunsList = xwpfParagraph.getRuns();
if (xwpfRunsList != null && !xwpfRunsList.isEmpty()) {
for (XWPFRun xwpfRun : xwpfRunsList) {
int testPosition = xwpfRun.getTextPosition();
String text = xwpfRun.getText(testPosition);
if (text != null && ! "" .equals(text)){
for (RPTBean rptBean : paraList) {
if (text.contains(rptBean.getFieldName())){
int startIndex = text.indexOf(rptBean.getFieldName());
xwpfRun.setText(rptBean.getValue(), startIndex);
}
}
}
}
}
return xwpfParagraph;
}
|
|
d. 获得 table list,并替换 table 中的关键字 (注:1. 修改 cell 时,要先本来是通过 cell.removeParagraph(0) 清空cell。 2. 开始本人用循环数据组,多次修改cell值,但是试验证明,cell 被替换过一次以后,再次 cell.getText();就会获得到空值。因此就创建了临时字符串,先获得 cell 内容,再修改 临时字符串,最后 setText()。)
for (XWPFTable xwpfTable : xwpfTables) {
xwpfTable = replaceKeyInTable(xwpfTable, paraList);
}
public XWPFTable replaceKeyInTable(XWPFTable xwpfTable, List<RPTBean> paraList) {
for (int i = 0; i<xwpfTable.getNumberOfRows(); i++) {
XWPFTableRow row = xwpfTable.getRow(i);
List<XWPFTableCell> cellList = row.getTableCells();
if (cellList != null && !cellList.isEmpty()) {
for (XWPFTableCell cell : cellList) {
if (cell != null) {
// 下面有另一种方法 String text = cell.getText();
for (RPTBean rptBean : paraList) {
String key = rptBean.getFieldName();
String value = rptBean.getValue();
if (text.contains(key)) {
StringBuffer newText = new StringBuffer();
int startIndex = text.indexOf(key);
text = newText.append(text.substring(0, startIndex))
.append(value)
.append(text.substring(startIndex+key.length(), text.length()))
.toString();
cell.removeParagraph(0); cell.setText(text); }
}
cell.getText();
}
}
}
}
}
|
上述方法生成的 表中没有格式。要生成格式 两种方法:
1. 先获取格式,然后修改内容,最后添加格式
2. 用 paragraph 来做
1 2 3 4 | List<XWPFParagraph> paragraphList = cell.getParagraphs();
for (XWPFParagraph xwpfParagraph : paragraphList) {
replaceKeyInParagraph(xwpfParagraph, paraList);
}
|
|
e. 输出,并关闭各流
1 2 3 4 5 6 7 8 9 | xwpfDocument.write(outputStream);
outputStream.flush();
outputStream.close();
fileInputStream.close();
/** 如果用网上办法,连模板都修改的话,那么最好再关闭下面两个
opcPackage.flush();
opcPackage.close();
*/
|
|
ps1. 获取文本框(没测试过)(参考:http://www.cnblogs.com/liaokunhong/p/5403279.html)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | List<XWPFParagraph> paragraphs = document.getParagraphs();
for ( XWPFParagraph paragraph : paragraphs) {
String text = paragraph.getText();
if (StringUtils.isBlank(text)) {
continue ;
}
List<CTR> rList = paragraph.getCTP().getRList();
for (CTR ctr : rList) {
XmlObject xmlObject = ctr.copy();
Node domNode = xmlObject.getDomNode();
NodeList nodeList = domNode.getChildNodes();
for ( int idx = 0 ; idx < nodeList.getLength(); idx++) {
Node item = nodeList.item(idx);
if ( "w:pict" .equals(item.getNodeName())) {
NodeList pictChildNodes = item.getChildNodes();
for ( int i = 0 ; i < pictChildNodes.getLength(); i++) {
if (! "v:shape" .equals(vShapeNode.getNodeName())) {
continue ;
}
NodeList vShapeChildNodes = vShapeNode.getChildNodes();
Node textboxNode = vShapeChildNodes.item( 0 );
NodeList textboxChildNodes = textboxNode.getChildNodes();
Node textContentNode = textboxChildNodes.item( 0 );
NodeList txbxContentChildNodes = textContentNode.getChildNodes();
Node wpNode = txbxContentChildNodes.item( 0 );
NodeList wpNodeChildNodes = wpNode.getChildNodes();
for ( int j = 0 ; j < wpNodeChildNodes.getLength(); j++) {
Node wrNode = wpNodeChildNodes.item(j);
if (! "w:r" .startsWith(wrNode.getNodeName())) {
continue ;
}
NodeList wrNodeChildNodes = wrNode.getChildNodes();
Node wtNode = wrNodeChildNodes.item( 1 );
Node targetNode = wtNode.getChildNodes().item( 0 );
if (targetNode == null ) {
continue ;
}
String targetNodeValue = targetNode.getNodeValue();
Object value = tags.get(targetNodeValue);
if (value != null ) {
targetNode.setNodeValue(value.toString());
}
ctr.set(xmlObject); } } } }}
|
|
ps2. 简化 ps1,用递归,获得<w:t>(没测过)(参考:http://blog.csdn.net/calance_h/article/details/52808778)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public void replaceParagraphText( final int paragraphPos, final Entry<String,String> text) throws IndexOutOfBoundsException {
if ( this .ctTc.sizeOfPArray() < paragraphPos){
throw new IndexOutOfBoundsException();
}
final CTP ctP = this .ctTc.getPArray(paragraphPos);
final XWPFParagraph par = new XWPFParagraph(ctP, this );
List<XWPFRun> runs = par.getRuns();
for (XWPFRun run : runs){
for ( int i = 0 ; i < run.getCTR().sizeOfTArray(); i++){
if (run.getText(i).equals(text.getKey())){
String replaceBy = run.getText(i).replaceAll(text.getKey(), text.getValue());
run.setText(replaceBy, i);
}
}
}
} }
|
|
ps3. 处理书签:
https://wenku.baidu.com/view/2206d072f342336c1eb91a37f111f18583d00c7e.html
http://elim.iteye.com/blog/2031335
|
ps4:通过xml来处理word
http://www.infoq.com/cn/articles/cracking-office-2007-with-java
|
三、jacob(在服务器端要安装 office )
参考:
1. http://men4661273.iteye.com/blog/2097871
2. https://wenku.baidu.com/view/ba3cb447fe4733687e21aa2e.html
3. http://blog.csdn.net/songbaojie/article/details/1842756
Java COM Bridge:即 java 和 com 组件间的桥梁,com 一般表现为 dll 或者 exe 等二进制文件。
这些文件合称为接口 api。
1. 需要2个文件,而且分 32 & 64 版本
下载地址:http://downloads.sourceforge.net/jacob-project/jacob_1.9.zip?modtime=1109437002&big_mirror=0
文件名称 |
存放位置 |
jacob.jar |
项目lib中 |
jacob-1.18-x64.dll |
jdl的lib中 |
jacob-1.18-x86.dll |
2. 代码
jacob 以模板形式为例 |
步
骤
|
Dispatch:调度处理类,封装了一些操作来操作 office ,里面所有的可操作对象基本都是这种类型,所以 jacob 是一种链式操作模式,就像 StringBuilder 对象,调用 append() 方法之后返回的还是 StringBuilder 对象。
Dispatch的几种静态方法:这些方法就是要用来操作office的。这些方法中有的有很多重载方法,调用不同的方法时需要放置不同的参数,至于哪些参数代表什么意思,具体放什么值,就需要参考vba代码了,仅靠jacob是无法进行变成的。
•call( ) / callN( ): 调用 com 对象的方法,返回 Variant 类型值。 •invoke( ): 和 call( ) 作用相同,但是不返回值。 •get( ): 获取 com 对象属性,返回 variant 类型值。 •put( ): 设置 com 对象属性。
call() / callN():
1 2 | this .document = Dispatch.call( this .documents, "Open" , templateFile).toDispatch();
this .document = Dispatch.callN( this .documents, "Open" , new Object[]{templateFile}).toDispatch();
|
Variant:封装参数数据类型,因为操作 office 是的一些方法参数(可能是字符串类型,可能是数字类型)。可以通过 Variant 来进行转换通用的参数类型,new Variant(1),new Variant("1")。
Variant 对象中的 toDispatch():将以上方法返回的 Variant 类型转换为 Dispatch,进行下一次链式操作。
|
1. 初始化com线程:大概意思是打开冰箱门,准备放大象。。。
|
2. 创建office的一个应用,比如你操作的是 word 还是 excel
1 | ActiveXComponent word = new ActiveXComponent( "Word.Application" );
|
|
3. 设置编辑器是否可见:
1 | word.setProperty( "Visible" , new Variant( false ));
|
|
4. 获取文档属性
1 | Dispatch documents = word.getProperty( "Documents" ).toDispatch();
|
|
5. 打开激活文档 |
1 2 | Dispatch doc = Dispatch.invoke(documents, "Open" , Dispatch.Method, new Object[]{inputPath, new Variant( false )}, new int [ 1 ]).toDispatch();
|
|
Selection:代表当前光标位置或者所选范围。该对象代表窗口或窗格中的当前所选内容。若文档中没有所选内容,则代表插入点。每个文档窗格只能有一个活动的 Selection对象,并且整个应用程序中只能有一个活动的 Selection对象。 |
6. 选定的范围或插入点
1 | Dispatch selection = Dispatch.get(word, "Selection" ).toDispatch();
|
|
7. 从选定内容或插入点开始查找文本
1 | Dispatch find = Dispatch.call( this .selection, "Find" ).toDispatch();
|
|
8. 查找字符串(替换)
1 2 3 | Dispatch.put(find, "Text" , "name" );
Dispatch.call(find, "Execute" );
Dispatch.put(selection, "Text" , "111" );
|
或者
1 2 3 4 5 6 | Boolean f = new Boolean( false );
Boolean t = new Boolean( true );
int wdReplaceTime = 2 ;
int wdFindContinue = 1 ;
Object[] args={ "被替换的值" ,t,f,f,f,f,t, new Integer(wdFindContinue),f, "替换为" , new Integer(wdReplaceTime),f,f,f,f};
Dispatch.callN( this .find, "Execute" ,args);
|
|
9. 文本框,只能用书签形式(替换)
1 2 3 4 5 6 7 8 9 10 11 12 | Dispatch bookMarks = Dispatch.get(doc, "Bookmarks" ).toDispatch();
int bCount = Dispatch.get(bookMarks, "Count" ).getInt();
for ( int i = 1 ; i <= bCount; i++) {
Dispatch items = Dispatch.call(bookMarks, "Item" , i).toDispatch();
String bookMarkKey = String.valueOf(Dispatch.get(items, "Name" ).getString()).replaceAll( "null" , "" );
Dispatch range = Dispatch.get(items, "Range" ).toDispatch();
String bookMarkValue = String.valueOf(Dispatch.get(range, "Text" ).getString()).replaceAll( "null" , "" );
if (bookMarkKey.equal( "标签值" )) {
Dispatch.put(range, "Text" , new Variant( "被替换值" )); }
}}
|
|
10. 文件另存为
1 | Dispatch.invoke(doc, "SaveAs" , Dispatch.Method, new Object[] { "目标文件路径" , new Variant(fileType)} , new int [ 1 ]);
|
或者以流方式输出:我能想到的就是,保存在本地的文件,用POI重新打开,然后以流方式输出。再将本地保存的文件删掉
删除文件:
1 2 3 4 5 | File file= new File( "目标文件路径" );
if (file.exists()) {
printWord();
file.delete();
}
|
|
11. 关闭模板文件(电脑任务管理器 - 前台应用)
val 的可选值:0/false 不保存修改 -1 保存修改 -2 提示是否保存修改
1 | Dispatch.call(doc, "Close" , new Variant(val));
|
|
12. 关闭 office 应用(电脑任务管理器 - 后台进程)
1 | word.invoke( "Quit" , new Variant[] {});
|
|
13. 关闭线程
|
3. 类似上面,第 9 步中,jacob 还能 获得的 com
Dispatch content = Dispatch.call(document, "content").getDispatch();
Open |
打开文档 |
ActiveXComponent.Visible |
设置编辑器是否可见 |
Tables |
获得所有的表格 |
Bookmarks |
所有标签 |
Selection |
光标所在处或选中的区域 |
select |
选中 |
typeParagraph |
设置为一个段落 |
ParagraphFormat |
段落格式,用alignment设置 |
alignment |
1、居中,2、靠右,3、靠左 |
Add |
新建一个word文档 |
Close |
关闭文档:
0/false 不保存,-1保存,-2弹出框确认
|
SaveAS |
另存为 |
save |
保存 |
printOut |
打印 |
Application |
得到ActiveXComponent的实例 |
WindowState |
Application的属性,表示窗口的大小,
0、default,1、maximize,2、minimize
|
top、left、height、width |
application的属性,表示窗口的位置 |
ActiveXComponent.Quit |
关闭所有word文档,但是不退出整个word程序 |
Range |
表示文档中的一个连续范围。
由一个起始字符位置和一个终止字符位置定义,进而可以得到格式的信息
|
Item |
得到指定的表格 |
Rows |
得到表格的所有行 |
Cell |
表格的一个单元格 |
Text |
word的文本内容 |
InsertFile |
插入文件 |
InsertRowsBelow |
在指定的行下面插入一行 |
InsertAfter |
在指定对象后插入 |
Delete |
删除,可以是表格的行 |
Count |
返回数目,比如Rows、Tables的数目 |
Height |
返回高度,比如行高、表格行的高 |
Split |
拆分单元格,要指定行数和列数 |
Merge |
合并单元格 |
Exists |
指定的对象是否存在,返回bool值 |
Copy |
复制 |
Paste |
粘贴 |
Font |
字体 |
Name |
字体的名字 |
Bold |
字体是否为粗体 |
Italic |
字体是否为斜体 |
Underline |
字体是否有下划线 |
Color |
颜色 |
Size |
大小 |
Borders |
指定边框:
-1为上边框,-2左边框,-3为下边框,-4有右边框,-5为横向边框,
-6为纵向边框,-7从左上角开始的斜线,-8从左下角开始的斜线
|
AutoFitBehavior |
自动调整大小:
1为内容自动调整大小,2为窗口自动调整大小
|
Content |
去的内容 |
InLineShapes |
|
AddPicture |
增加一张图片,需要制定路径 |
homeKey |
光标移到开头 |
moveDown |
光标往下一行 |
moveUp |
光标往上一行 |
moveRight |
光标往左一列 |
moveLeft |
光标往右一列 |
find |
要查找的文本 |
Forward |
向前查找 |
Format |
查找的文本格式 |
MatchCase |
大小写匹配 |
MatchWholeWord |
全字匹配 |
Execute |
开始执行查找 |
LineSpacingRule |
行间距 |
4. 遇到的错误
1.
com.jacob.com.ComFailException:
Can't map name to dispid: Open
或者,一直都卡在那里。
|
因为电脑的任务管理器中积压了很多 office 进程/应用程序,没有内存打开新 office 文件了。
方案:除了关闭 任务管理器中的 进程/应用程序外,程序:
1 2 3 4 5 | finally {
Dispatch.call(doc, "Close" , new Variant( false ));
this .word.invoke( "Quit" , new Variant[] {});
ComThread.Release();
}
|
|
2.
在开着 tomcat 时,调试程序,会报如下错:
java.lang.NoClassDefFoundError:
com.jacob.com.JacobObject
|
方案:重启 tomcat |
3.
java.lang.IllegalStateException:
Dispatch not hooked to windows memory
|
我没遇到,网上办法:http://blog.sina.com.cn/s/blog_49cc672f0100pp0p.html
然后每次操作完成后都会调用ComThread.Release()去释放,但释放后word和documents并不为null,所以每次使用jacob都只有第一次是正常的,后面就要报错,然后必须重启tomcat才行。
1 2 3 4 5 6 7 8 | if ( this .word == null || this .word.m_pDispatch== 0 ) {
this .word = new ActiveXComponent( "Word.Application" );
this .word.setProperty( "Visible" , new Variant( false ));
this .word.setProperty( "DisplayAlerts" , new Variant( false ));
}
if (documents == null ||documents.m_pDispatch== 0 ) {
this .documents = this .word.getProperty( "Documents" ).toDispatch();
}
|
|
4.
com.jacob.com.ComFailException:
Invoke of: SaveAs
|
因为保存的类型出错了
Dispatch.invoke(doc, "SaveAs", Dispatch.Method, new Object[] {output, new Variant(fileType)} , new int[1]);
fileType:数字表示不同类型

|
5.
com.jacob.com.ComFailException: Invoke of: Item Source: Microsoft Word Description: 集合所要求的成员不存在。
|
原因:将选中一段内容来作为标签,类似将“word”作为标签
方案:以光标形式作为标签
|
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 软件产品开发中常见的10个问题及处理方法
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
· 一次Java后端服务间歇性响应慢的问题排查记录
· dotnet 源代码生成器分析器入门
· ThreeJs-16智慧城市项目(重磅以及未来发展ai)
· 软件产品开发中常见的10个问题及处理方法
· Vite CVE-2025-30208 安全漏洞
· 互联网不景气了那就玩玩嵌入式吧,用纯.NET开发并制作一个智能桌面机器人(四):结合BotSharp
· MQ 如何保证数据一致性?