在最初的 http 協(xié)議中,沒(méi)有上傳文件方面的功能。RFC1867("Form-based File Upload in HTML".)
為 http 協(xié)議添加了這個(gè)功能。客戶端的瀏覽器,如 Microsoft IE, Mozila, Opera 等,按照此規(guī)范將用
戶指定的文件發(fā)送到服務(wù)器。服務(wù)器端的網(wǎng)頁(yè)程序,如 php, asp, jsp 等,可以按照此規(guī)范,解析出用戶
發(fā)送來(lái)的文件。
2.1客戶端
簡(jiǎn)單來(lái)說(shuō),RFC1867規(guī)范要求http協(xié)議增加了file類型的input標(biāo)簽,用于瀏覽需要上傳的文件。同時(shí)
要求FORM表單的enctype屬性設(shè)置為“multipart/form-data”,method屬性設(shè)置為“post”即可,下面是我們文
件上傳頁(yè)面的表單代碼:
<form action="<%=request.getContextPath()%>/servlet/SimpleUpload" enctype="multipart/form-data"
method="post">
文本1:<input type="text" name="text1" value="文本1"><br>
文件2:<input type="text" name="text2" value="文本2"><br>
文件1:<input type="file" name="file1"><br>
文件2:<input type="file" name="file2"><br>
文件2:<input type="file" name="file3"><br>
<input type="submit" value="開始上傳">
</form>
2.2 服務(wù)器端
一個(gè)文件上傳請(qǐng)求的消息實(shí)體由一系列根據(jù) RFC1867("Form-based File Upload in HTML".)編碼的項(xiàng)目
(文本參數(shù)和文件參數(shù))組成。自己編程來(lái)解析獲取這些數(shù)據(jù)是非常麻煩的,還需要了解RFC1867規(guī)范對(duì)請(qǐng)
求數(shù)據(jù)編碼的相關(guān)知識(shí)。FileUpload 可以幫助我們解析這樣的請(qǐng)求,將每一個(gè)項(xiàng)目封裝成一個(gè)實(shí)現(xiàn)了FileItem
接口的對(duì)象,并以列表的形式返回。所以,我們只需要了解FileUpload的API如何使用即可,不用管它們的底
層實(shí)現(xiàn)。讓我們來(lái)看一個(gè)簡(jiǎn)單文件上傳處理代碼:
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload uploader = new ServletFileUpload(factory);
List<FileItem> list = uploader.parseRequest(request);
if (item.isFormField()){
// 處理普通表單域
String field = item.getFieldName();//表單域名
String value = item.getString("GBK");
} else {
//將臨時(shí)文件保存到指定目錄
String fileName = item.getName();//文件名稱
String filepath = "您希望保存的目錄/" + fileName;
item.write(new File(filepath));//執(zhí)行保存
}
怎么樣?簡(jiǎn)單吧!下面我們來(lái)繼續(xù)了解一些必須了解的API。
FileItem接口
org.apache.commons.fileupload.disk.DiskFileItem實(shí)現(xiàn)了FileItem接口,用來(lái)封裝單個(gè)表單字段元素的
數(shù)據(jù)。通過(guò)調(diào)用FileItem 定義的方法可以獲得相關(guān)表單字段元素的數(shù)據(jù)。我們不需要關(guān)心DiskFileItem的具
體實(shí)現(xiàn),在程序中可以采用FileItem接口類型來(lái)對(duì)DiskFileItem對(duì)象進(jìn)行引用和訪問(wèn)。FileItem類還實(shí)現(xiàn)了
Serializable接口,以支持序列化操作。
下圖是一個(gè)文件上傳表單:

上圖表單提交的http數(shù)據(jù)包的內(nèi)容:
POST /demo/servlet/SimpleUpload HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/msword,
application/vnd.ms-excel, application/vnd.ms-powerpoint, */*
Referer: http://127.0.0.1:8080/demo/simpleUpload.jsp
Accept-Language: zh-cn
Content-Type: multipart/form-data; boundary=---------------------------7da1772c5504c6
UA-CPU: x86
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 2.0.50727)
Host: 127.0.0.1:8080
Content-Length: 184423
Connection: Keep-Alive
Cache-Control: no-cache
Cookie: JSESSIONID=BD8E58E5BAD9B559C0262077FB5E0B4E
-----------------------------7da1772c5504c6
Content-Disposition: form-data; name="text1"
鄭州蜂鳥科技有限公司
-----------------------------7da1772c5504c6
Content-Disposition: form-data; name="text2"
申林
-----------------------------7da1772c5504c6
Content-Disposition: form-data; name="file1"; filename="C:\Documents and Settings\All Users\
Documents\My Pictures\示例圖片\Blue hills.jpg"
Content-Type: image/pjpeg
大量二進(jìn)制數(shù)據(jù)內(nèi)容,無(wú)法復(fù)制 …….
-----------------------------7da1772c5504c6
Content-Disposition: form-data; name="file2"; filename="C:\Documents and Settings\All Users\
Documents\My Pictures\示例圖片\Sunset.jpg"
Content-Type: image/pjpeg
大量二進(jìn)制數(shù)據(jù)內(nèi)容,無(wú)法復(fù)制 …….
-----------------------------7da1772c5504c6
Content-Disposition: form-data; name="file3"; filename="C:\Documents and Settings\All Users\
Documents\My Pictures\示例圖片\Water lilies.jpg"
Content-Type: image/pjpeg
大量二進(jìn)制數(shù)據(jù)內(nèi)容,無(wú)法復(fù)制 …….
從第一行,也就是請(qǐng)求行,我們可以看出這是一個(gè)post請(qǐng)求。在請(qǐng)求頭部部分,我們可以看到這樣
一個(gè)頭部信息:
Content-Type: multipart/form-data; boundary=---------------------------7da1772c5504c6
其中紅色部分說(shuō)明該請(qǐng)求是一個(gè)multipart/form-data類型即多媒體類型的請(qǐng)求。藍(lán)色部分boundary的值
定義了一個(gè)字段分隔界線。在消息體部分可以看出每個(gè)表單字段元素?cái)?shù)據(jù)之間采用字段分隔界線進(jìn)行分
割,兩個(gè)分隔界線間的內(nèi)容稱為一個(gè)分區(qū),每個(gè)分區(qū)中的內(nèi)容包括兩部分,一部分是對(duì)表單字段元素進(jìn)
行描述的描述頭,另外一部分是表單字段元素的主體內(nèi)容。
通過(guò)對(duì)比描述頭,我們可以很容易區(qū)分文本字段和文件字段。不管是文件字段還是文本字段,都有
name屬性,即該字段作為一個(gè)表單域的名字。而文件字段還有filename,即上傳文件本身的名字。另外,
還有conten-type屬性用于指明文件的類型。
每一個(gè)表單字段,不管它是文本還是文件,都被封裝成 FileItem 對(duì)象,我們稱之為文件項(xiàng),當(dāng)文件
項(xiàng)數(shù)據(jù)內(nèi)容尺寸小于DiskFileItemFactory 的sizeThreshold 屬性設(shè)置的臨界值時(shí),直接保存在內(nèi)存中;否則,
將數(shù)據(jù)流以臨時(shí)文件的形式,保存在 DiskFileItemFactory 的 repository 屬性指定的臨時(shí)目錄中。臨時(shí)文件
名形如“upload_00000005(八位或八位以上的數(shù)字).tmp”。FileItem類內(nèi)部提供了維護(hù)臨時(shí)文件名中的
數(shù)值不重復(fù)的機(jī)制,以保證了臨時(shí)文件名的唯一性。另外,如何保證臨時(shí)文件能被及時(shí)清除,釋放寶貴
的系統(tǒng)資源,是非常重要的,我們將在后面講解。
FileItem類常用的方法:
1. boolean isFormField()方法
isFormField方法用于判斷FileItem類對(duì)象封裝的數(shù)據(jù)是一個(gè)普通文本表單字段,還是一個(gè)文件表單字
段,如果是普通表單字段則返回true,否則返回false。
2. String getName()方法
getName方法用于獲得文件上傳字段中的文件名,即表單字段元素描述頭中的filename屬性值,如“C:\Documents and Settings\All Users\Documents\My Pictures\示例圖片\Sunset.jpg”。如果FileItem類對(duì)象對(duì)
應(yīng)的是普通表單字段,getName方法將返回null。即使用戶沒(méi)有通過(guò)網(wǎng)頁(yè)表單中的文件字段傳遞任何
文件,但只要設(shè)置了文件表單字段的name屬性,瀏覽器也會(huì)將文件字段的信息傳遞給服務(wù)器,
只是文件名和文件內(nèi)容部分都為空,但這個(gè)表單字段仍然對(duì)應(yīng)一個(gè)FileItem對(duì)象,此時(shí),
getName方法返回結(jié)果為空字符串"",讀者在調(diào)用Apache文件上傳組件時(shí)要注意考慮這個(gè)情況。
注意:上面的數(shù)據(jù)包是通過(guò)IE提交,所以是完整的路徑和名稱。如
C:\Documents and Settings\All Users\Documents\My Pictures\示例圖片\Sunset.jpg。如果是其它瀏覽
器,如火狐和Chromium,則僅僅是名字,沒(méi)有路徑,如Sunset.jpg。
3. String getFieldName()方法
getFieldName方法用于返回表單字段元素描述頭的name屬性值,也是表單標(biāo)簽name屬性的值。例
如“name=file1”中的“file1”。
4. void write(File file)方法
write方法用于將FileItem對(duì)象中保存的主體內(nèi)容保存到某個(gè)指定的文件中。如果FileItem對(duì)象中的主
體內(nèi)容是保存在某個(gè)臨時(shí)文件中,該方法順利完成后,臨時(shí)文件有可能會(huì)被清除。該方法也可將普通
表單字段內(nèi)容寫入到一個(gè)文件中,但它主要用途是將上傳的文件內(nèi)容保存在本地文件系統(tǒng)中。
5. String getString()方法
getString方法用于將FileItem對(duì)象中保存的數(shù)據(jù)流內(nèi)容以一個(gè)字符串返回,它有兩個(gè)重載的定義形式:
public java.lang.String getString()
public java.lang.String getString(java.lang.String encoding)
throws java.io.UnsupportedEncodingException
前者使用缺省的字符集編碼將主體內(nèi)容轉(zhuǎn)換成字符串,后者使用參數(shù)指定的字符集編碼將主體內(nèi)容
轉(zhuǎn)換成字符串。如果在讀取普通表單字段元素的內(nèi)容時(shí)出現(xiàn)了中文亂碼現(xiàn)象,請(qǐng)調(diào)用第二個(gè)
getString方法,并為之傳遞正確的字符集編碼名稱。
6. String getContentType()方法
getContentType 方法用于獲得上傳文件的類型,即表單字段元素描述頭屬性“Content-Type”的值,
如“image/jpeg”。如果FileItem類對(duì)象對(duì)應(yīng)的是普通表單字段,該方法將返回null。
7. boolean isInMemory()方法
isInMemory方法用來(lái)判斷FileItem對(duì)象封裝的數(shù)據(jù)內(nèi)容是存儲(chǔ)在內(nèi)存中,還是存儲(chǔ)在臨時(shí)文件中,
如果存儲(chǔ)在內(nèi)存中則返回true,否則返回false。
8. void delete()方法
delete方法用來(lái)清空FileItem類對(duì)象中存放的主體內(nèi)容,如果主體內(nèi)容被保存在臨時(shí)文件中,
delete方法將刪除該臨時(shí)文件。
盡管當(dāng)FileItem對(duì)象被垃圾收集器收集時(shí)會(huì)自動(dòng)清除臨時(shí)文件,但及時(shí)調(diào)用delete方法可以更早的
清除臨時(shí)文件,釋放系統(tǒng)存儲(chǔ)資源。另外,當(dāng)系統(tǒng)出現(xiàn)異常時(shí),仍有可能造成有的臨時(shí)文件被永久
保存在了硬盤中。
9. InputStream getInputStream()方法
以流的形式返回上傳文件的數(shù)據(jù)內(nèi)容。
10. long getSize()方法
返回該上傳文件的大小(以字節(jié)為單位)。
DiskFileItemFactory類
將請(qǐng)求消息實(shí)體中的每一個(gè)項(xiàng)目封裝成單獨(dú)的DiskFileItem (FileItem接口的實(shí)現(xiàn)) 對(duì)象的任務(wù)
由 org.apache.commons.fileupload.FileItemFactory 接口的默認(rèn)實(shí)現(xiàn)
org.apache.commons.fileupload.disk.DiskFileItemFactory 來(lái)完成。當(dāng)上傳的文件項(xiàng)目比較小時(shí),直接保
存在內(nèi)存中(速度比較快),比較大時(shí),以臨時(shí)文件的形式,保存在磁盤臨時(shí)文件夾(雖然速度
慢些,但是內(nèi)存資源是有限的)。
屬性
1) public static final int DEFAULT_SIZE_THRESHOLD :將文件保存在內(nèi)存還是
磁盤臨時(shí)文件夾的默認(rèn)臨界值,值為10240,即10kb。
2) private File repository:用于配置在創(chuàng)建文件項(xiàng)目時(shí),當(dāng)文件項(xiàng)目大于臨界值時(shí)使
用的臨時(shí)文件夾,默認(rèn)采用系統(tǒng)默認(rèn)的臨時(shí)文件路徑,可以通過(guò)系統(tǒng)屬性 java.io.tmpdir
獲取。如下代碼:
System.getProperty("java.io.tmpdir");
3) private int sizeThreshold:用于保存將文件保存在內(nèi)存還是磁盤臨時(shí)文件夾的臨界值
構(gòu)造方法
1) public DiskFileItemFactory():采用默認(rèn)臨界值和系統(tǒng)臨時(shí)文件夾構(gòu)造文件項(xiàng)工廠對(duì)象。
2) public DiskFileItemFactory(int sizeThreshold,File repository):采用參數(shù)指定臨界值和系統(tǒng)臨時(shí)
文件夾構(gòu)造文件項(xiàng)工廠對(duì)象。
FileItem createItem() 方法
根據(jù)DiskFileItemFactory相關(guān)配置將每一個(gè)請(qǐng)求消息實(shí)體項(xiàng)目創(chuàng)建 成DiskFileItem 實(shí)例,并返回。
該方法從來(lái)不需要我們親自調(diào)用,FileUpload組件在解析請(qǐng)求時(shí)內(nèi)部使用。
void setSizeThreshold(int sizeThreshold)
Apache文件上傳組件在解析上傳數(shù)據(jù)中的每個(gè)字段內(nèi)容時(shí),需要臨時(shí)保存解析出的數(shù)據(jù),以便
在后面進(jìn)行數(shù)據(jù)的進(jìn)一步處理(保存在磁盤特定位置或插入數(shù)據(jù)庫(kù))。因?yàn)?/font>Java虛擬機(jī)默認(rèn)可以使
用的內(nèi)存空間是有限的,超出限制時(shí)將會(huì)拋出“java.lang.OutOfMemoryError”錯(cuò)誤。如果上傳的文件
很大,例如800M的文件,在內(nèi)存中將無(wú)法臨時(shí)保存該文件內(nèi)容,Apache文件上傳組件轉(zhuǎn)而采用臨時(shí)
文件來(lái)保存這些數(shù)據(jù);但如果上傳的文件很小,例如600個(gè)字節(jié)的文件,顯然將其直接保存在內(nèi)存中
性能會(huì)更加好些。
setSizeThreshold方法用于設(shè)置是否將上傳文件已臨時(shí)文件的形式保存在磁盤的臨界值(以字節(jié)
為單位的int值),如果從沒(méi)有調(diào)用該方法設(shè)置此臨界值,將會(huì)采用系統(tǒng)默認(rèn)值10KB。對(duì)應(yīng)的
getSizeThreshold() 方法用來(lái)獲取此臨界值。
void setRepository(File repository)
setRepositoryPath方法用于設(shè)置當(dāng)上傳文件尺寸大于setSizeThreshold方法設(shè)置的臨界值時(shí),將文件以
臨時(shí)文件形式保存在磁盤上的存放目錄。有一個(gè)對(duì)應(yīng)的獲得臨時(shí)文件夾的 File getRespository() 方法。
注意:當(dāng)從沒(méi)有調(diào)用此方法設(shè)置臨時(shí)文件存儲(chǔ)目錄時(shí),默認(rèn)采用系統(tǒng)默認(rèn)的臨時(shí)文件路徑,可以
通過(guò)系統(tǒng)屬性 java.io.tmpdir 獲取。如下代碼:
System.getProperty("java.io.tmpdir");
Tomcat系統(tǒng)默認(rèn)臨時(shí)目錄為“<tomcat安裝目錄>/temp/”。
ServletFileUpload 類
org.apache.commons.fileupload.servlet.ServletFileUpload類是Apache文件上傳組件處理文件上傳的
核心高級(jí)類(所謂高級(jí)就是不需要管底層實(shí)現(xiàn),暴露給用戶的簡(jiǎn)單易用的接口)。
使用其 parseRequest(HttpServletRequest) 方法可以將通過(guò)表單中每一個(gè)HTML標(biāo)簽提交的數(shù)據(jù)封裝
成一個(gè)FileItem對(duì)象,然后以List列表的形式返回。使用該方法處理上傳文件簡(jiǎn)單易用。
如果你希望進(jìn)一步提高新能,你可以采用 getItemIterator 方法,直接獲得每一個(gè)文件項(xiàng)的數(shù)據(jù)輸
入流,對(duì)數(shù)據(jù)做直接處理。
在使用ServletFileUpload對(duì)象解析請(qǐng)求時(shí)需要根據(jù)DiskFileItemFactory對(duì)象的屬性 sizeThreshold(臨
界值)和repository(臨時(shí)目錄) 來(lái)決定將解析得到的數(shù)據(jù)保存在內(nèi)存還是臨時(shí)文件中,如果是臨時(shí)
文件,保存在哪個(gè)臨時(shí)目錄中?。所以,我們需要在進(jìn)行解析工作前構(gòu)造好DiskFileItemFactory對(duì)象,
通過(guò)ServletFileUpload對(duì)象的構(gòu)造方法或setFileItemFactory()方法設(shè)置 ServletFileUpload對(duì)象的
fileItemFactory屬性。
ServletFileUpload繼承結(jié)構(gòu):
java.lang.Object
|—org.apache.commons.fileupload.FileUploadBase
|—org.apache.commons.fileupload.FileUpload
|—org.apache.commons.fileupload.servlet.ServletFileUpload
構(gòu)造方法:
1) public ServletFileUpload():構(gòu)造一個(gè)未初始化的實(shí)例,需要在解析請(qǐng)求之前先調(diào)用
setFileItemFactory()方法設(shè)置 fileItemFactory屬性。
2) public ServletFileUpload(FileItemFactory fileItemFactory):構(gòu)造一個(gè)實(shí)例,并根據(jù)參數(shù)
指定的FileItemFactory 對(duì)象,設(shè)置 fileItemFactory屬性。
ServletFileUpload類常用方法:
1. public void setSizeMax(long sizeMax)方法
setSizeMax方法繼承自FileUploadBase類,用于設(shè)置請(qǐng)求消息實(shí)體內(nèi)容(即所有上傳數(shù)據(jù))的最大
尺寸限制,以防止客戶端惡意上傳超大文件來(lái)浪費(fèi)服務(wù)器端的存儲(chǔ)空間。其參數(shù)是以字節(jié)為單位的
long型數(shù)字。
在請(qǐng)求解析的過(guò)程中,如果請(qǐng)求消息體內(nèi)容的大小超過(guò)了setSizeMax方法的設(shè)置值,將會(huì)拋出
FileUploadBase內(nèi)部定義的SizeLimitExceededException異常(FileUploadException的子類)。如:
org.apache.commons.fileupload.FileUploadBase$SizeLimitExceededException:
the request was rejected because its size (1649104) exceeds the configured
maximum (153600)
該方法有一個(gè)對(duì)應(yīng)的讀方法:public long getSizeMax()方法。
2. public void setFileSizeMax(long fileSizeMax)方法
setFileSizeMax方法繼承自FileUploadBase類,用于設(shè)置單個(gè)上傳文件的最大尺寸限制,以防止客戶
端惡意上傳超大文件來(lái)浪費(fèi)服務(wù)器端的存儲(chǔ)空間。其參數(shù)是以字節(jié)為單位的long型數(shù)字。該方法有一個(gè)
對(duì)應(yīng)的讀方法:public long geFileSizeMax()方法。
在請(qǐng)求解析的過(guò)程中,如果單個(gè)上傳文件的大小超過(guò)了setFileSizeMax方法的設(shè)置值,將會(huì)拋出
FileUploadBase內(nèi)部定義的FileSizeLimitExceededException異常(FileUploadException的子類)。如:
org.apache.commons.fileupload.FileUploadBase$FileSizeLimitExceededException: The field file1 exceeds its
maximum permitted size of 51200 characters.
3. public List parseRequest(javax.servlet.http.HttpServletRequest req)
parseRequest 方法是ServletFileUpload類的重要方法,它是對(duì)HTTP請(qǐng)求消息體內(nèi)容進(jìn)行解析的入口
方法。它解析出FORM表單中的每個(gè)字段的數(shù)據(jù),并將它們分別包裝成獨(dú)立的FileItem對(duì)象,然后將這
些FileItem對(duì)象加入進(jìn)一個(gè)List類型的集合對(duì)象中返回。
該方法拋出FileUploadException異常來(lái)處理諸如文件尺寸過(guò)大、請(qǐng)求消息中的實(shí)體內(nèi)容的類型不
是“multipart/form-data”、IO異常、請(qǐng)求消息體長(zhǎng)度信息丟失等各種異常。每一種異常都是
FileUploadException的一個(gè)子類型。
4. public FileItemIterator getItemIterator(HttpServletRequest request)
getItemIterator方法和parseRequest 方法基本相同。但是getItemIterator方法返回的是一個(gè)迭代
器,該迭代器中保存的不是FileItem對(duì)象,而是FileItemStream 對(duì)象,如果你希望進(jìn)一步提高新能,
你可以采用 getItemIterator 方法,直接獲得每一個(gè)文件項(xiàng)的數(shù)據(jù)輸入流,做底層處理;如果性能不
是問(wèn)題,你希望代碼簡(jiǎn)單,則采用parseRequest方法即可。
5. public stiatc boolean isMultipartContent(HttpServletRequest req)
isMultipartContent方法方法用于判斷請(qǐng)求消息中的內(nèi)容是否是“multipart/form-data”類型,是則返
回true,否則返回false。isMultipartContent方法是一個(gè)靜態(tài)方法,不用創(chuàng)建ServletFileUpload類的實(shí)例對(duì)
象即可被調(diào)用。
6. getFileItemFactory()和setFileItemFactory(FileItemFactory)方法
方法繼承自FileUpload類,用于設(shè)置和讀取fileItemFactory屬性。
7. public void setProgressListener(ProgressListener pListener)
設(shè)置文件上傳進(jìn)度監(jiān)聽器。關(guān)于監(jiān)聽器的具體內(nèi)容,將在后面學(xué)習(xí)。該方法有一個(gè)對(duì)應(yīng)的讀取
方法:ProgressListener getProgressListener()。
8.public void setHeaderEncoding()方法
在文件上傳請(qǐng)求的消息體中,除了普通表單域的值是文本內(nèi)容以外,文件上傳字段中的文件路
徑名也是文本,在內(nèi)存中保存的是它們的某種字符集編碼的字節(jié)數(shù)組,Apache文件上傳組件在讀取
這些內(nèi)容時(shí),必須知道它們所采用的字符集編碼,才能將它們轉(zhuǎn)換成正確的字符文本返回。
setHeaderEncoding方法繼承自FileUploadBase類,用于設(shè)置上面提到的字符編碼。如果沒(méi)有設(shè)置,
則對(duì)應(yīng)的讀方法getHeaderEncoding()方法返回null,將采用HttpServletRequest設(shè)置的字符編碼,如果
HttpServletRequest的字符編碼也為null,則采用系統(tǒng)默認(rèn)字符編碼。可以通過(guò)一下語(yǔ)句獲得系統(tǒng)默認(rèn)
字符編碼:
System.getProperty("file.encoding"));
好,到這里我們學(xué)習(xí)了主要的一些API,足夠我們來(lái)完成一個(gè)簡(jiǎn)單文件上傳的功能了,下一章,
我們將一起來(lái)編寫一個(gè)文件上傳應(yīng)用程序。
鄭州蜂鳥科技有限公司 申林 QQ:38185398 鄭州軟件開發(fā)興趣小組群:38236716
學(xué)軟件開發(fā),到蜂鳥科技!超強(qiáng)的師資力量 、完善的課程體系 、超低的培訓(xùn)價(jià)格 、真實(shí)的企業(yè)項(xiàng)目。
地址:鄭州市文化路豐產(chǎn)路口東50米豐產(chǎn)路21號(hào)SOHO世紀(jì)城西塔20樓F
電話:0371-63839606 手機(jī):13838505572(申老師) 13673990036 (許老師)
QQ: 1073422643 1群:47614738 2群:108157678
網(wǎng)址:www.ntcsoft.com