SoapUI+Groovy做接口自動化測試

摘要: 從頭開始手把手教你新建一個SoapUI Project.

重點看是如何操作Excel中的Properties,以及錯誤Result Report。

關鍵詞:SoapUI接口測試,接口自動化測試,數(shù)據驅動測試,SoapUI進階使用, Groovy in SoapUI, SoapUI中Groovy的使用,數(shù)據分離。

閱讀這篇文章需要一定的SoapUI基礎,至少入過門,另外還需要一些Groovy的知識,當然如果你會java 也可以,這里用到的Groovy知識和Java很類似。

另外,本文的思路和我上一篇文章<零成本實現(xiàn)接口自動化測試 – Java+TestNG 測試Restful service>很相似,只不過把Java+TestNG的組合換成了SoapUI+Groovy, 另外測試對象也換成了基于Soap的web service, 依舊用Excel來管理數(shù)據,做到數(shù)據分離。

由于我用到的SoapUI是免費版本,相比Pro版,少很多的功能,像DataLoop之類的,所以只能通過Groovy寫一些腳本來做數(shù)據驅動的測試。

1. 首先打開SoapUI, 新建一個Workspace 名為Demo

2. 然后點擊File->New soapUI Project

3. 然后輸入你的Project Name,?WSDL 地址 點擊OK

4.輸入服務的用戶名密碼 點擊OK

5. 項目工程建好了

6. 右擊ServicePort 建立TestSuite

以其中一個接口為例 來生成用例

輸入TestSuite name 然后確認工程結構如下

7. 通過右擊TestCase -> Add Step ?增加 Groovy Script 和 Properties

增加四個Groovy Script, 并且命名成 Start, Process, Check Response, End

增加5個 Properties, 并且命名成Input, Baseline, Output, Result, fieldResult

調整它們的順序,最后形成下圖的工程目錄結構

8. Start腳本主要創(chuàng)建log文件

/*Check the log file, create folder and file if necessary*/

import java.io.*;

def cal = Calendar.instance;

def sysdate = cal.getTime();

def Y = cal.get(Calendar.YEAR);

def M = cal.get(Calendar.MONTH)+1;

def D = cal.get(Calendar.DATE);

if (D<10) D = "0"+D;

if (M<10) M = "0"+M;

date = Y+"-"+M+"-"+D;

time=sysdate.toString().replaceAll(':','-')

def testSuites=testRunner.testCase.getTestSuite();

def testcase = testRunner.testCase;

def logFolder = new File(context.expand('${#Project#LogFolder}'));

def responseLogName = (context.expand('${#Project#LogFolder}')+date+'\\'+testSuites.name+' - '+ testcase.name +' - ' + time+".log");

def responseDetailLogName = (context.expand('${#Project#LogFolder}')+date+'\\'+testSuites.name+' - ' + testcase.name +' - ' + time + " Response Detail.log");

def responseLogFile = new File(responseLogName);

def responseDetailLogFile = new File(responseDetailLogName);

def subFolder= new File (context.expand('${#Project#LogFolder}')+date+'\\')

/*Set date and Log Name to Project - Testcase Properties*/

testcase.setPropertyValue('date', date);

testcase.setPropertyValue('LogFile - Check Response', responseLogName);

testcase.setPropertyValue('LogFile - Response Detail', responseDetailLogName);

if(!logFolder.exists()){

? ? logFolder.mkdirs();

? ? }

if(!subFolder.exists()){

? ? subFolder.mkdirs();

? ? }

/*Check the file, create a new one if not found*/

if(!responseLogFile.exists()){

? ? responseLogFile.createNewFile();

? ? }

responseLogFile.append("---------------Test Start on "+sysdate+" ------------------------"+'\n');

if(!responseDetailLogFile.exists()){

? ? responseDetailLogFile.createNewFile();

? ? }

responseDetailLogFile.append("---------------Test Start on "+sysdate+" ------------------------"+'\n');

9. Process腳本是整個工程的核心,讀取Excel數(shù)據文件的Input, Baseline 放入二維數(shù)組,然后循環(huán)讀入Input和Baseline這兩個Properties, 調用request, 取到Ouput和Result?Properties的值放入Output, Result數(shù)組,最后更新Excel的Output, Result, Comparison sheet。

import java.io.*;

import jxl.*;

import java.text.DecimalFormat;

def readInput(workbook,inputSheetName)

{? ?

? ? Sheet sheet=workbook.getSheet(inputSheetName);

? ? rows = sheet.getRows();

? ? columns = sheet.getColumns();

/*Get content into array*/

? ? input = new Object [columns][rows]

? ? for (a=0;a

? ? ? ? {

? ? ? ? for (b=0;b

? ? ? ? ? ? {

? ? ? ? ? ? ? ? input[a][b]= sheet.getCell(a,b).getContents();

? ? ? ? ? ? }

? ? ? ? }

}

def readBaseline(workbook,sheetName)

{? ?

? ? Sheet sheet=workbook.getSheet(sheetName);

? ? Rows = sheet.getRows();

? ? Columns = sheet.getColumns();

/*Get content into array*/

? ? baseline = new Object [Columns][Rows]

? ? for (a=0;a

? ? ? ? {

? ? ? ? for (b=0;b

? ? ? ? ? ? {

? ? ? ? ? ? ? ? baseline[a][b]= sheet.getCell(a,b).getContents();

? ? ? ? ? ? }

? ? ? ? }

}

def updateOutput(writableWorkbook,sheetName,start_row,rows,columnNo,result,resultTag){

? ? ? ? Sheet[] sheet= writableWorkbook.getSheets();

? ? ? ? ws = writableWorkbook.getSheet(sheetName);

? ? ? ? if(ws==null){

? ? ? ? ws = writableWorkbook.createSheet(sheetName,sheet.length)

? ? ? ? }

? ? ? ? for (a=0;a

? ? ? ? {

? ? ? ? ? ? ws.addCell(new jxl.write.Label(a,0,result[a][0]));

? ? ? ? ? ? for (b=start_row;b

? ? ? ? ? ? {

? ? ? ? ? ? ? ? wc = new jxl.write.WritableCellFormat();

? ? ? ? ? ? ? ? if (resultTag[a][b]=='PASS')? ? ? ? ? ?

? ? ? ? ? ? ? ? {

? ? ? ? ? ? ? ? ? ? wc.setBackground(jxl.format.Colour.WHITE);

? ? ? ? ? ? ? ? ? ? ws.addCell(new jxl.write.Label(a,b,result[a][b],wc));

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? else if(resultTag[a][b]=='FAIL'){

? ? ? ? ? ? ? ? ? ? wc.setBackground(jxl.format.Colour.RED);

? ? ? ? ? ? ? ? ? ? ws.addCell(new jxl.write.Label(a,b,result[a][b],wc));

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

}

def updateResult(writableWorkbook,sheetName,start_row,rows,columnNo,result){

? ? ? ? Sheet[] sheet= writableWorkbook.getSheets();

? ? ? ? ws = writableWorkbook.getSheet(sheetName);

? ? ? ? if(ws==null){

? ? ? ? ws = writableWorkbook.createSheet(sheetName,sheet.length)

? ? ? ? }

? ? ? ? for (a=0;a

? ? ? ? {

? ? ? ? ? ? ws.addCell(new jxl.write.Label(a,0,result[a][0]));

? ? ? ? ? ? for (b=start_row;b

? ? ? ? ? ? {

? ? ? ? ? ? ? wcf = new jxl.write.WritableCellFormat();

? ? ? ? ? ? ? wcf.setBackground(jxl.format.Colour.RED);

? ? ? ? ? ? ? ? ? if(result[a][b] == 'FAIL'){

? ? ? ? ? ? ? ? ? ? ? ws.addCell(new jxl.write.Label(a,b,result[a][b],wcf));

? ? ? ? ? ? ? ? ? }? ? else? ? ? ? ? ?

? ? ? ? ? ? ? ? ? ? ws.addCell(new jxl.write.Label(a,b,result[a][b]));

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? wc = new jxl.write.WritableCellFormat();

? ? ? ? ? wcc = new jxl.write.WritableCellFormat();

? ? ? ? ? wc.setBackground(jxl.format.Colour.GRAY_25);

? ? ? ? ? wcc.setBackground(jxl.format.Colour.LIGHT_TURQUOISE);

? ? ? ? ? ws.addCell(new jxl.write.Label(0,rows-3,result[0][rows-3],wcc));?

? ? ? ? ? ws.addCell(new jxl.write.Label(0,rows-2,result[0][rows-2],wc));

? ? ? ? ws.addCell(new jxl.write.Label(0,rows-1,result[0][rows-1],wc));

}

def updateComparison(writableWorkbook,sheetName,start_row,rows,columnNo,output,outputTag,result,baseline){

? ? ? ? Sheet[] sheet= writableWorkbook.getSheets();

? ? ? ? ws = writableWorkbook.getSheet(sheetName);

? ? ? ? if(ws==null){

? ? ? ? ws = writableWorkbook.createSheet(sheetName,sheet.length)

? ? ? ? }

? ? ? ? for (a=0;a

? ? ? ? {

? ? ? ? ? ? ws.addCell(new jxl.write.Label(a,0,output[a][0]));

? ? ? ? ? ? x= 1;

? ? ? ? ? ? for (b=start_row;b

? ? ? ? ? ? {

? ? ? ? ? ? ? ? if(result[1][b] == 'FAIL'){

? ? ? ? ? ? ? ? ? wc = new jxl.write.WritableCellFormat();

? ? ? ? ? ? ? ? ? if (outputTag[a][b]=='PASS')? ? ? ? ? ?

? ? ? ? ? ? ? ? ? {

? ? ? ? ? ? ? ? ? ? ? ? wc.setBackground(jxl.format.Colour.WHITE);

? ? ? ? ? ? ? ? ? ? ws.addCell(new jxl.write.Label(a,x,output[a][b],wc));? ? ? ? ? ? ? ? ? ?

? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? ? else {

? ? ? ? ? ? ? ? ? ? wc.setBackground(jxl.format.Colour.RED);

? ? ? ? ? ? ? ? ? ? ws.addCell(new jxl.write.Label(a,x,output[a][b],wc));

? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? wbc = new jxl.write.WritableCellFormat();

? ? ? ? ? ? ? ? wbc.setBackground(jxl.format.Colour.ICE_BLUE);

? ? ? ? ? ? ? ? ws.addCell(new jxl.write.Label(a,x+1,baseline[a][b],wbc));

? ? ? ? ? ? ? ? x+=2;

? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

}

def removeSheetByName(writableWorkbook, name){

? ? ? ? Sheet[] sheet= writableWorkbook.getSheets();? ?

? ? ? ? ws = writableWorkbook.getSheet(name);

? ? ? ? if(ws != null){

? ? ? ? ? ? for (i = 0; i < sheet.length; i++) {

? ? ? ? ? ? ? ? sheetName = writableWorkbook.getSheet(i).getName();? ? ? ? ? ?

? ? ? ? ? ? ? ? if (sheetName.equalsIgnoreCase(name)) {

? ? ? ? ? ? ? ? ? ? writableWorkbook.removeSheet(i);

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

}

def setProperties(Name,Value,Place)

{? ?

? ? name = Name;

? ? target = testRunner.testCase.getTestStepByName(Place);

? ? target.setPropertyValue(name,Value);

? ? }

def cleanProperty(PropertyListName)

{

? ? ? ? PropertyList = testRunner.testCase.getTestStepByName(PropertyListName);

? ? ? ? size=PropertyList.getPropertyCount();

? ? ? ? if (size!=0)

? ? ? ? {

? ? ? ? ? ? ? ? ? for (i=0;i

? ? ? ? ? ? ? ? ? {

? ? ? ? ? ? ? ? ? ? ? ? ? ? PropertyList.removeProperty(PropertyList.getPropertyAt(0).name);

? ? ? ? ? ? ? ? ? }

? ? ? ? }

}

def logFile = new File(context.expand('${#TestCase#LogFile - Check Response}'));

def xlsName = context.expand('${#TestCase#Workbook}');

def project = testRunner.testCase.getTestSuite().getProject();

def testSuite = testRunner.testCase.getTestSuite();

def testcase = testRunner.testCase

def requests = testcase.getTestStepsOfType( com.eviware.soapui.impl.wsdl.teststeps.WsdlTestRequestStep.class )

def request = requests[0];

inputSheetName = "Input";

baselineSheet = "Baseline";

outputSheet = "Output";

resultSheet = "Result";

fieldResult = testcase.getTestStepByName('fieldResult');

ComparisonSheet = "Comparison";

Baseline = testcase.getTestStepByName(baselineSheet);

baselineSize = Baseline.getPropertyCount();

Output = testcase.getTestStepByName(outputSheet);

def cal = Calendar.instance;

def sysdate = cal.getTime();

sleepTime=context.expand('${#Project#sleepTime}').toInteger()

try{? ?

? ? ? Workbook workbook=Workbook.getWorkbook(new File(xlsName));

? ? ? cleanProperty(inputSheetName);

? ? ? readInput(workbook,inputSheetName);

? ? for (i=0;i

? ? {

? ? ? setProperties(input[i][0],'',inputSheetName)

? ? }

? ? ? cleanProperty(baselineSheet);

? ? ? readBaseline(workbook,baselineSheet);

? ? for (i=0;i

? ? {

? ? ? setProperties(baseline[i][0],'',baselineSheet)

? ? }

? ? ? workbook.close();


}catch(Exception e){

? ? ? e.printStackTrace();

}


/*-----------Set runSelect as 'true' in Project property to run selected test cases from startTag till endTag-------*/

def runSelected = context.expand('${#Project#runSelected}')

start_Test=1;

end_Test=rows-1;

def passNumbers = 0;

def decFormat = new DecimalFormat("##.00%");

if ('true'.equalsIgnoreCase(runSelected))

{

? ? startTag=context.expand('${#Project#startTag}').toInteger();

? ? endTag=context.expand('${#Project#endTag}').toInteger();


? ? if((0=startTag)&&(rows>endTag)){

? ? start_Test=startTag;

? ? end_Test=endTag;

? ? }

}


? ? result = new Object [2][rows+3]

? ? result[0][0]='Case Description';

? ? result[1][0]='Result'

? ? result[0][rows+1]='Start Time:';

? ? result[1][rows+1]=sysdate.toString();


/*--New object and put the output name and value into this list--*/

? ? output = new Object [baselineSize][rows]

? ? outputTag = new Object [baselineSize][rows]

? ? for (i=0;i

? ? {

? ? ? ? output[i][0]= Baseline.getPropertyAt(i).name;

? ? ? ? outputTag[i][0]= 'PASS';

? ? ? ? }


? ? for (m=start_Test;m<=end_Test;m++)

? ? {

? ? ? ? logFile.append('\n'+ testcase.name + ": "+m+" "+ ". "+sysdate+'\n');

? ? ? ? for (i=0;i

? ? ? ? {

? ? ? ? ? ? setProperties(input[i][0],input[i][m],inputSheetName)

? ? ? ? }

? ? ? ? for (j=0;j

? ? ? ? {? ?

? ? ? ? ? ? setProperties(baseline[j][0],baseline[j][m],baselineSheet)

? ? ? ? }


? ? ? ? testRunner.runTestStepByName(request.name);

? ? ? ? Thread.sleep(sleepTime);

? ? ? ? testRunner.runTestStepByName("Check Response");


? ? ? ? result[0][m]=context.expand('${'+inputSheetName+'#Case Description}')

? ? ? ? result[1][m]=context.expand('${'+resultSheet+'#result}')

? ? ? ? if(result[1][m] == 'PASS'){

? ? ? ? ? ? ? ? passNumbers++;

? ? ? ? }

? ? ? ? for (i=0;i

? ? ? ? {

? ? ? ? ? ? output[i][m]= Output.getPropertyAt(i).value;

? ? ? ? ? ? outputTag[i][m]= fieldResult.getPropertyAt(i).value;

? ? ? ? }

}

? ? ? ? ? result[0][rows+2]='End Time:';

? ? ? ? ? result[1][rows+2]=sysdate.toString();

? ? ? ? ? result[0][rows]='Pass Percentage:';

? ? ? ? ? passPercentage = decFormat.format(passNumbers/(end_Test-start_Test+1));

? ? ? ? ? result[1][rows] = passPercentage

/*--------------Update Output, Result, Comparison sheet---------*/

? ? try{

? ? ? ? workbook = Workbook.getWorkbook(new File(xlsName));

? ? ? ? writableWorkbook =? Workbook.createWorkbook(new File(xlsName), workbook);

? ? ? ? updateOutput(writableWorkbook,outputSheet,start_Test,end_Test+1,baselineSize,output,outputTag);

? ? ? ? updateResult(writableWorkbook,resultSheet,start_Test,rows+3,2,result);

? ? ? ? removeSheetByName(writableWorkbook,ComparisonSheet);

? ? ? ? if(passPercentage != '100.00%'){

? ? ? ? ? ? ? updateComparison(writableWorkbook,ComparisonSheet,start_Test,end_Test+1,baselineSize,output,outputTag,result,baseline);

? ? ? ? ? }

? ? ? ? ? writableWorkbook.write();

? ? ? ? writableWorkbook.close();?

? ? ? ? workbook.close();


? ? ? ? }catch(Exception e){

? ? ? ? ? ? e.printStackTrace();

? ? ? ? }

? ? ? ? ? setProperties('passPercentage', passPercentage ,'Result');

? ? ? ? testRunner.gotoStepByName('End');

10. Check Response 顧名思義,用來檢查返回結果, 通過XmlHolder getNodeValue 來取response各節(jié)點的值,并且填入Output Properties已作對比之用。

import java.lang.*;

import java.util.*;

import groovy.lang.*;

import groovy.util.*;

import com.eviware.soapui.support.XmlHolder

baselineSheet = "Baseline";

outputSheet = "Output";

resultSheet = "Result";

def testcase = testRunner.testCase;

Baseline = testcase.getTestStepByName(baselineSheet)

baselineSize = Baseline.getPropertyCount();

Output = testcase.getTestStepByName(outputSheet);

def requests = testcase.getTestStepsOfType( com.eviware.soapui.impl.wsdl.teststeps.WsdlTestRequestStep.class )

def request = requests[0];

def response = request.testRequest.response;

respXmlHolder = new XmlHolder(response.contentAsXml)

//declare namespace in xml response

respXmlHolder.declareNamespace("ns1", "http://schemas.xxx.com/v201203/yourservice")

def statusCode = request.testRequest.response.responseHeaders["#status#"].toString();

def setProperties(Name,Value,Place){? ? ? ? ? ? ?

? ? ? name = Name;

? ? ? target = testRunner.testCase.getTestStepByName(Place);

? ? ? target.setPropertyValue(name,Value);

}

def getspecifiedValue(field){

? ? ? prefix = "http://ns1:";

? ? ? nodePath = "${prefix}${field}"

? ? ? specifiedValue = respXmlHolder.getNodeValue("${nodePath}")

}

testRunner.testCase.getTestStepByName(outputSheet).clearPropertyValues();

//normal output, status = '200 OK'

if(statusCode.contains('200 OK')){

? ? ? for (i=1;i

? ? ? {

? ? ? ? ? ? ? specifiedName = Baseline.getPropertyAt(i).name;

? ? ? ? ? ? ? specifiedValue = getspecifiedValue(specifiedName);

? ? ? ? ? ? ? if(specifiedValue != null){

? ? ? ? ? ? ? setProperties(specifiedName,specifiedValue,outputSheet)

? ? ? ? ? ? ? } else? {

? ? ? ? ? ? ? ? ? setProperties(specifiedName,'',outputSheet)

? ? ? ? ? ? ? }? ? ? ? ? ?

? ? ? }


} else

? ? ? {

? ? ? ? ? setProperties(Baseline.getPropertyAt(0).name,Baseline.getPropertyAt(0).value,outputSheet);

? ? ? ? setProperties(Baseline.getPropertyAt(1).name,statusCode,outputSheet);


? ? ? ? ? for(t=2; t

? ? ? ? ? ? setProperties(Baseline.getPropertyAt(t).name, '' ,outputSheet);?

? ? ? }

}

setProperties(Baseline.getPropertyAt(0).name,Baseline.getPropertyAt(0).value,outputSheet);

setProperties('result','PASS', resultSheet);

logFile = new File(context.expand('${#TestCase#LogFile - Check Response}'));

responseDetailLogFile= new File(context.expand('${#TestCase#LogFile - Response Detail}'));

logFile.append(" ------? ? ? ? ? ? ? ? ? Start Response Check "+ " @ "+Calendar.instance.getTime()+"\n");

responseDetailLogFile.append("\n"+ testcase.name + " -- " + Baseline.getPropertyAt(0).value+ "\n" + response+"\n");

/*--------Compare the result with Baseline and update the result accordingly---------*/

for (i=0;i

{

? ? ? ? if (Baseline.getPropertyAt(i).value==Output.getPropertyAt(i).value)

? ? ? ? ? ? ? ? {

? ? ? ? ? ? ? ? ? ? ? setProperties(Baseline.getPropertyAt(i).name,'PASS','fieldResult');

? ? ? }

? ? ? ? ? ? ? else

? ? ? ? ? ? ? ? {

? ? ? ? ? ? ? ? ? ? ? setProperties(Baseline.getPropertyAt(i).name,'FAIL','fieldResult');

? ? ? ? ? ? ? ? ? ? ? setProperties('result','FAIL','Result');

? ? ? ? }

}

11. End腳本 在log上打時間戳

def cal = Calendar.instance;

def sysdate = cal.getTime();

responseLogFile = new File(context.expand('${#TestCase#LogFile - Check Response}'));

responseDetailLogFile= new File(context.expand('${#TestCase#LogFile - Response Detail}'));

responseLogFile.append('\n'+ "---------------Test End on " + sysdate.toString() + " ------------------------"+'\n');

responseDetailLogFile.append('\n'+ "---------------Test End on " + sysdate.toString() + " ------------------------"+'\n');

12.配置

雙擊DemoProject, 點擊下方的Properties tab

然后 Add property

Add 如下圖所示的5個Property

雙擊項目工程列表里的'Demo TestCase'

點擊Properties tab

Add Property 如圖

Value 填Excel 的路徑

Excel數(shù)據工作簿里的Input sheet 如圖

接下來需要把Input里的column name 與 Soap request里的input 字段映射起來

雙擊Test Steps 里的request

將xml文件里的?用參數(shù)來代替

左下角的TestRequest Properties ?要填上用戶名 密碼

Baseline sheet里要把輸出結果的字段名都定義好, 因為是根據字段名去response里取結果的

每條用例期望結果都寫好,用作和實際結果對比

另外Check Response里的腳本需要設置一下

假設你的response xml文件結構如下

如果你需要檢查Soap body標簽下的內容, 則你需要配置一下Check Response的腳本

將xml namespace的路徑配置一下

右擊Project ?Save一下Project?

最后可以運行了 雙擊Demo TestSuite ?點擊Run

注意:Groovy腳本也是用第三方的jxl.jar操作excel文件的,所以這個jar需要放到SoapUI安裝目錄的\lib下面,才能運行成功。

打開Workbook 數(shù)據工作簿查看結果

Output

Result

Comparison

備注:

這篇文章的部分內容轉自下面的博客:

http://www.cnblogs.com/wade-xu/p/4236295.html

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

相關閱讀更多精彩內容

  • 最近在網上爬取奧運項目資料,并寫入Excel中。在寫到Excel中是用到了OpenPyXL,翻譯了一部分自己用到的...
    LeeLom閱讀 201,374評論 7 78
  • using System; using System.Data;using System.Configuratio...
    北風知我意閱讀 6,274評論 2 2
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,734評論 18 399
  • >最近遇到一個情景,就是定期生成并發(fā)送服務器使用情況報表,按照不同維度統(tǒng)計,涉及python對excel的操作,上...
    有料美拓閱讀 7,141評論 0 0
  • Apache POI 是用Java編寫的免費開源的跨平臺的 Java API,Apache POI提供API給Ja...
    玩味Orz閱讀 2,757評論 0 0

友情鏈接更多精彩內容