如何用Java來(lái)進(jìn)行文件切割和簡(jiǎn)單的內(nèi)容過(guò)濾的實(shí)現(xiàn)
一 由來(lái)
去年由于項(xiàng)目的需求,要將一個(gè)任意一個(gè)文件制作成一個(gè)xml文件,并且需要保持文件內(nèi)容本身不產(chǎn)生變化,還要能夠?qū)⑦@個(gè)xml重新還原為原文件。如果小型的文件還好處理,大型的xml,比如幾個(gè)G的文件,基本上就OOM了,很難直接從節(jié)點(diǎn)中提取數(shù)據(jù)。所以我采用了流的方式。于是有了這個(gè)文件的裁剪工具。
二 使用場(chǎng)景
本工具可能的使用場(chǎng)景:
1.對(duì)任一文件的切割/裁剪。通過(guò)字節(jié)流的方式,開(kāi)始節(jié)點(diǎn)和終止節(jié)點(diǎn),裁剪出兩個(gè)節(jié)點(diǎn)之間的部分。
2.往任一文件的頭/尾拼接指定字符串??梢院苋菀讓⒁粋€(gè)文件嵌入在某一個(gè)節(jié)點(diǎn)中。
3.簡(jiǎn)單的文本抽取。可以根據(jù)自己定義的規(guī)則,提取出來(lái)想要的文本內(nèi)容,并且允許對(duì)提取出來(lái)的文本進(jìn)行再處理(當(dāng)然,只是進(jìn)行簡(jiǎn)單地抽取文字,并不是什么智能的復(fù)雜過(guò)程的抽取T_T )。
4.文本過(guò)濾。根據(jù)自己制定的規(guī)則,過(guò)濾掉指定的文字。
整個(gè)工具僅是對(duì)Java文件操作api的簡(jiǎn)單加工,并且也沒(méi)有使用nio。在需要高效率的文件處理情景下,本工具的使用有待考量。文章目的是為了給出自己的一種解決方案,若有更好的方案,歡迎大家給出適當(dāng)?shù)慕ㄗh。
三 如何使用
別的先不說(shuō),來(lái)看看如何使用吧!
1.讀取文件指定片段
讀取第0~1048個(gè)字節(jié)之間的內(nèi)容。
public void readasbytes(){
FileExtractor cuter = new FileExtractor();
byte[] bytes = cuter.from("D:\\11.txt").start(0).end(1048).readAsBytes();
}
2.文件切割
將第0~1048個(gè)字節(jié)之間的部分切割為一個(gè)新文件。
public File splitAsFile(){
FileExtractor cuter = new FileExtractor();
return cuter.from("D:\\11.txt").to("D:\\22.txt").start(0).end(1048).extractAsFile();
}
3.將文件拼接到一個(gè)xml節(jié)點(diǎn)中
將整個(gè)文件的內(nèi)容作為Body節(jié)點(diǎn),寫(xiě)入到一個(gè)xml文件中。返回新生成的xml文件對(duì)象。
public File appendText(){
FileExtractor cuter = new FileExtractor();
return cuter.from("D:\\11.txt").to("D:\\44.xml").appendAsFile("<Document><Body>", "</Body></Document>");
}
4.讀取并處理文件中的指定內(nèi)容
假如有需求:讀取11.txt的前三行文字。其中,第一行和第二行不能出現(xiàn)”帥”字,并且在第三行文字后加上字符串“我好帥!”。
public String extractText(){
FileExtractor cuter = new FileExtractor();
return cuter.from("D:\\11.txt").extractAsString(new EasyProcesser() {
@Override
public String finalStep(String line, int lineNumber, Status status) {
if(lineNumber==3){
status.shouldContinue = false;//表示不再繼續(xù)讀取文件內(nèi)容
return line+"我好帥!";
}
return line.replaceAll("帥","");
}
});
}
4.簡(jiǎn)單的文本過(guò)濾
將一個(gè)文件中所有的“bug”去掉,且返回一個(gè)處理后的新文件。
public File killBugs(){
FileExtractor cuter = new FileExtractor();
return cuter.from("D:\\bugs.txt").to("D:\\nobug.txt").extractAsFile(new EasyProcesser() {
@Override
public String finalStep(String line, int lineNumber, Status status) {
return line.replaceAll("bug", "");
}
});
}
四 基本流程
通過(guò)接口回調(diào)的方式,將文件的讀取過(guò)程和處理過(guò)程分離開(kāi)來(lái);定義了IteratorFile類來(lái)負(fù)責(zé)遍歷一個(gè)文件,讀取文件的內(nèi)容;分字節(jié)、行兩種的方式來(lái)進(jìn)行文件內(nèi)容的遍歷。下面的介紹,也會(huì)分為讀取和處理兩個(gè)部分單獨(dú)介紹。
五 文件的讀取
定義回調(diào)接口
定義一個(gè)接口Process,對(duì)外暴露了兩個(gè)文件內(nèi)容處理方法,一個(gè)支持按字節(jié)進(jìn)行讀取,一個(gè)方法支持按行讀取。
public interface Process{
/**
* @param b 本次讀取的數(shù)據(jù)
* @param length 本次讀取的有效長(zhǎng)度
* @param currentIndex 當(dāng)前讀取到的位置
* @param available 讀取文件的總長(zhǎng)度
* @return true 表示繼續(xù)讀取文件,false表示終止讀取文件
* @time 2017年1月22日 下午4:56:41
*/
public boolean doWhat(byte[] b,int length,int currentIndex,int available);
/**
*
* @param line 本次讀取到的行
* @param currentIndex 行號(hào)
* @return true 表示繼續(xù)讀取文件,false表示終止讀取文件
* @time 2017年1月22日 下午4:59:03
*/
public boolean doWhat(String line,int currentIndex);
讓ItratorFile中本身實(shí)現(xiàn)這個(gè)接口,但是默認(rèn)都是返回true,不做任何的處理。如下所示:
public class IteratorFile implements Process
{
......
/**
* 按照字節(jié)來(lái)讀取遍歷文件內(nèi)容,根據(jù)自定義需要重寫(xiě)該方法
*/
@Override
public boolean doWhat(byte[] b, int length,int currentIndex,int available) {
return true;
}
/**
* 按照行來(lái)讀取遍歷文件內(nèi)容,根據(jù)自定義需要重寫(xiě)該方法
*/
@Override
public boolean doWhat(String line,int currentIndex) {
return true;
}
......
}
按字節(jié)遍歷文件內(nèi)容
實(shí)現(xiàn)按照字節(jié)的方式來(lái)進(jìn)行文件的遍歷(讀取)。在這里使用了skip()方法來(lái)控制從第幾個(gè)節(jié)點(diǎn)開(kāi)始讀取內(nèi)容;然后在使用文件流讀取的時(shí)候,將每次讀取到得數(shù)據(jù)傳遞給回調(diào)接口的方法;需要注意的是,每次讀取到得數(shù)據(jù)是存在一個(gè)字節(jié)數(shù)組bytes里面的,每次讀取的長(zhǎng)度也是需要傳遞給回調(diào)接口的。我們很容易看出,一旦dowhat()返回false,文件的讀取立即就退出了。
public void iterator2Bytes(){
init();
int length = -1;
FileInputStream fis = null;
try {
file = new File(in);
fis = new FileInputStream(file);
available = fis.available();
fis.skip(getStart());
readedIndex = getStart();
if (!beforeItrator()) return;
while ((length=fis.read(bytes))!=-1) {
readedIndex+=length;
if(!doWhat(bytes, length,readedIndex,available)){
break;
}
}
if(!afterItrator()) return;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
按行來(lái)遍歷文件內(nèi)容
常規(guī)的文件讀取方式,在while循環(huán)中,調(diào)用了回調(diào)接口的方法,并且傳遞相關(guān)的數(shù)據(jù)。
public void iterator2Line(){
init();
BufferedReader reader = null;
FileReader read = null;
String line = null;
try {
file = new File(in);
read = new FileReader(file);
reader = new BufferedReader(read);
if (!beforeItrator()) return;
while ( null != (line=reader.readLine())) {
readedIndex++;
if(!doWhat(line,readedIndex)){
break;
}
}
if(!afterItrator()) return ;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
read.close();
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
然后,還需要提供方法來(lái)設(shè)置要讀取的源文件路徑。
public IteratorFile from(String in){
this.in = in;
return this;
}
六 文件內(nèi)容處理
FileExtractor介紹
定義了FileExtractor類,來(lái)封裝對(duì)文件內(nèi)容的處理操作;該類會(huì)引用到遍歷文件所需要的類IteratorFile。
FileExtractor的基本方法
/**
* 往文件頭或者文件結(jié)尾插入字符串
* @tips 不能對(duì)同一個(gè)文件輸出路徑反復(fù)執(zhí)行該方法,否則會(huì)出現(xiàn)文本異常,因?yàn)橛玫搅薘andomAccessFile,如有需要,調(diào)用前需手動(dòng)刪除原有的同名文件
* @param startStr 文件開(kāi)頭要插入的字符串
* @param endStr 文件結(jié)尾要插入的字符串
* @return 生成的新文件
* @time 2017年1月22日 下午5:05:35
*/
public File appendAsFile(final String startStr,String endStr){}
/**
* 從指定位置截取文件
* @tips 適合所有的文件類型
* @return
* @time 2017年1月22日 下午5:06:36
*/
public File splitAsFile(){}
/**
* 文本文件的特殊處理(情景:文本抽取,文本替換等)
* @tips 只適合文本文件,對(duì)于二進(jìn)制文件,因?yàn)閾Q行符的原因?qū)е挛募霈F(xiàn)可能無(wú)法執(zhí)行等問(wèn)題。
* @time 2017年1月22日 下午5:09:14
*/
public File extractAsFile(FlowLineProcesser method) {
/**
* 文本文件的特殊處理(情景:文本抽取,文本替換等)
* @tips 只適合文本文件,對(duì)于二進(jìn)制文件,因?yàn)閾Q行符的原因?qū)е挛募霈F(xiàn)可能無(wú)法執(zhí)行等問(wèn)題。
* @time 2017年1月22日 下午5:09:14
*/
public String extractAsString(FlowLineProcesser method) {}
/**
* 讀取指定位置的文件內(nèi)容為字節(jié)數(shù)組
* @return
* @time 2017年1月23日 上午11:06:18
*/
public byte[] readAsBytes(){}
其中,返回值為File的方法在處理完成后,都出返回一個(gè)經(jīng)過(guò)內(nèi)容后的新文件。
其他方法
同樣,設(shè)置源文件位置的方法,以及截取位置的相關(guān)方法
/**
* 設(shè)置源文件
*/
public FileExtractor from(String in){
this.in = in;
return this;
}
/**
* 設(shè)置生成臨時(shí)文件的位置(返回值為File的方法均需要設(shè)置)
*/
public FileExtractor to(String out) {
this.out = out;
return this;
}
/**
* 文本開(kāi)始截取的位置(包含此位置),字節(jié)相關(guān)的方法均需要設(shè)置
*/
public FileExtractor start(int start){
this.startPos = start;
return this;
}
/**
* 文本截取的終止位置(包含此位置),字節(jié)相關(guān)方法均需要設(shè)置
*/
public FileExtractor end(int end) {
this.endPos = end;
return this;
}
按字節(jié)讀取文件時(shí)的文件內(nèi)容處理
有幾個(gè)重點(diǎn):
1.因?yàn)橐鶕?jù)字節(jié)的位置來(lái)進(jìn)行文件截取,所以需要根據(jù)字節(jié)來(lái)遍歷文件,所以要重寫(xiě)doWhat()字節(jié)遍歷的的方法。并在外部構(gòu)造一個(gè)OutPutStream來(lái)進(jìn)行新文件的寫(xiě)出工作。
2.每次遍歷讀取出的文件內(nèi)容,都存放在一個(gè)字節(jié)數(shù)組b里面,但并不是b中的數(shù)據(jù)都是有用的,所以需要傳遞b有效長(zhǎng)度length。
3.readedIndex記錄了到本次為止(包括本次)為止,已經(jīng)讀取了多少位數(shù)據(jù)。
4.按照自己來(lái)遍歷文件時(shí),如何判斷讀取到了的終止位置?
當(dāng)(已讀的數(shù)據(jù)總長(zhǎng)度)readedIndex>endPos(終止節(jié)點(diǎn))時(shí),說(shuō)明本次讀取的時(shí)候超過(guò)了應(yīng)該終止的位置,此時(shí)b數(shù)組中有一部分?jǐn)?shù)據(jù)就是多讀的了,這部分?jǐn)?shù)據(jù)是不應(yīng)該被保存的。我們可以通過(guò)計(jì)算得到讀超了多少位,即length-(readedIndex-endPos-1),那么只要保存這部分?jǐn)?shù)據(jù)就可以了。
讀取指定片段的文件內(nèi)容:
//本方法在需要讀取的數(shù)據(jù)多時(shí),不建議使用,因?yàn)閎yte[]是不可變的,多次讀取的時(shí)候,需要進(jìn)行多次的byete[] copy過(guò)程,效率“感人”。
public byte[] readAsBytes(){
try {
checkIn();
} catch (Exception e) {
e.printStackTrace();
return null;
}
//臨時(shí)保存字節(jié)的容器
final BytesBuffer buffer = new BytesBuffer();
IteratorFile c = new IteratorFile(){
@Override
public boolean doWhat(byte[] b, int length, int currentIndex,
int available) {
if(readedIndex>endPos){
//說(shuō)明已經(jīng)讀取到了endingPos位置并且讀超了
buffer.addBytes(b, 0, length-(readedIndex-endPos-1)-1);
return false;
}else{
buffer.addBytes(b, 0, length-1);
}
return true;
}
};
//按照字節(jié)進(jìn)行遍歷
c.from(in).start(startPos).iterator2Bytes();
return buffer.toBytes();
}
當(dāng)文件很大時(shí),生成一個(gè)新的文件的比較靠譜的方法,所以,類似直接返回byte[],在文件讀取之前,設(shè)置一個(gè)outputSteam,在內(nèi)容循環(huán)讀取的過(guò)程中,將讀取的內(nèi)容寫(xiě)入到一個(gè)新文件中去。
public File splitAsFile(){
......
final OutputStream os = FileUtils.openOut(file);
try {
IteratorFile itFile = new IteratorFile(){
@Override
public boolean doWhat(byte[] b, int length,int readedIndex,int available) {
try {
if(readedIndex>endPos){
//說(shuō)明已經(jīng)讀取到了endingPos位置,并且讀超了readedIndex-getEnd()-1位
os.write(b, 0, length-(readedIndex-endPos-1));
return false;//終止讀取
}else{
os.write(b, 0, length);
}
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
}.from(in).start(startPos);
itFile.iterator2Bytes();
} catch (Exception e) {
e.printStackTrace();
this.tempFile = null;
}finally{
try {
os.flush();
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return getTempFile();
}
按行來(lái)讀取時(shí)的文件內(nèi)容處理
首先,再次聲明,按行來(lái)遍歷文件的時(shí)候,只適合文本文件。除非你對(duì)每一行的換行符用\r還是\n沒(méi)有要求。像exe文件,如果用行來(lái)遍歷的話,你寫(xiě)出為一個(gè)新的文件的時(shí)候,任意一個(gè)的換行符的不對(duì)都可能導(dǎo)致一個(gè)exe文件變?yōu)椤眜nexe”文件!
過(guò)程中,我用到了:
一個(gè)輔助類Status,來(lái)輔助控制遍歷的流程。
一個(gè)接口FlowLineProcesser,類似于一個(gè)處理文本的流水線。
Status和FlowLineProcesser是相互輔助的,Status也能輔助FlowLineProcesse是流水線的具體過(guò)程,Status是控制處理過(guò)程中怎么處理d的。
我也想了許多次,到底要不要把這個(gè)過(guò)程搞的這么復(fù)雜。但是還是先留著吧…
先看輔助類Status:
public class Status{
/**
* 是否找到了開(kāi)頭,默認(rèn)false,若true則后續(xù)的遍歷不會(huì)執(zhí)行相應(yīng)的firstStep()方法
*/
public boolean overFirstStep = false;
/**
* 是否找到了結(jié)尾,默認(rèn)false,若true則后續(xù)的遍歷不會(huì)執(zhí)行相應(yīng)的finalStep()方法
*/
public boolean overFinalStep = false;
/**
* 是否繼續(xù)讀取源文件,默認(rèn)true表示繼續(xù)讀取,false則表示,執(zhí)行本次操作后,遍歷終止
*/
public boolean shouldContinue = true;
}
然后是FlowLineProcesser接口:
FlowLineProcesser是一個(gè)接口,類似于一個(gè)流水線。定義了兩步操作,分別對(duì)應(yīng)兩個(gè)方法fistStep()和finalStep()。其中兩個(gè)方法的返回值都是String,firstStep接受到得line是真正從文件中讀取到的行,它將line經(jīng)過(guò)自己的處理后,返回處理后的line給finalStep。所以,finalStep中得line其實(shí)是firstStep處理后的結(jié)果。但是最終真正返回給主處理流程的line,正是finalStep處理后的返回值。
public interface FlowLineProcesser{
/**
*
* @param line 讀取到的行
* @param lineNumber 行號(hào),從1開(kāi)始
* @param status 控制器
* @return
* @time 2017年1月22日 下午5:02:02
*/
String firstStep(String line,int lineNumber,Status status);
/**
* @tips
* @param line 讀取到的行(是firstStep()處理后的結(jié)果)
* @param lineNumber 行號(hào),從1開(kāi)始
* @param status 控制器
* @return
* @time 2017年1月22日 下午5:02:09
*/
String finalStep(String line,int lineNumber,Status status);
}
現(xiàn)在,可以來(lái)看一下如何去實(shí)現(xiàn)文本的抽取了:
所有讀取的行,都臨時(shí)存到一個(gè)stringbuilder中去。firstStep先進(jìn)行一次處理,得到返回值后傳遞給finalStep,再次處理后,將得到的結(jié)果保存下來(lái)。如果最后的結(jié)果是null,則不會(huì)保存。
public String extractAsString(FlowLineProcesser method) {
try {
checkIn();
} catch (Exception e) {
e.printStackTrace();
return null;
}
final StringBuilder builder = new StringBuilder();
this.mMethod = method;
new IteratorFile(){
Status status = new Status();
@Override
public boolean doWhat(String line, int currentIndex) {
String lineAfterProcess = "";
if(!status.overFirstStep){
lineAfterProcess = mMethod.firstStep(line, currentIndex,status);
}
if(!status.shouldContinue){
return false;
}
if(!status.overFinalStep){
lineAfterProcess = mMethod.finalStep(lineAfterProcess,currentIndex,status);
}
if(lineAfterProcess!=null){
builder.append(lineAfterProcess);
builder.append(getLineStr());//換行符被寫(xiě)死在這里了
}
if(!status.shouldContinue){
return false;
}
return true;
}
}.from(in).iterator2Line();
return builder.toString();
}
當(dāng)要抽取的文本太大的時(shí)候,可以采用生成新文件的方式。與返回string的流程基本一致。
public File extractAsFile(FlowLineProcesser method) {
try {
checkIn();
checkOut();
} catch (Exception e) {
e.printStackTrace();
return null;
}
this.mMethod = method;
File file = initOutFile();
if(file==null){
return null;
}
FileWriter fileWriter = null;
try {
fileWriter = new FileWriter(file);
} catch (Exception e) {
e.printStackTrace();
return null;
}
final BufferedWriter writer = new BufferedWriter(fileWriter);
IteratorFile itfile = new IteratorFile(){
Status status = new Status();
@Override
public boolean doWhat(String line, int currentIndex) {
String lineAfterProcess = "";
if(!status.overFirstStep){
lineAfterProcess = mMethod.firstStep(line, currentIndex,status);
}
if(!status.shouldContinue){
return false;
}
if(!status.overFinalStep){
lineAfterProcess = mMethod.finalStep(lineAfterProcess,currentIndex,status);
}
if(lineAfterProcess!=null){
try {
writer.write(lineAfterProcess);
writer.newLine();//TODO 換行符在此給寫(xiě)死了
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
if(!status.shouldContinue){
return false;
}
return true;
}
};
itfile.from(in).iterator2Line();
if(writer!=null){
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
try {
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
return getTempFile();
}
好啦,介紹到此就要結(jié)束啦,我們下次再聊~
代碼包供您下載哦!—>代碼包
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
在SpringBoot中,如何使用Netty實(shí)現(xiàn)遠(yuǎn)程調(diào)用方法總結(jié)
我們?cè)谶M(jìn)行網(wǎng)絡(luò)連接的時(shí)候,建立套接字連接是一個(gè)非常消耗性能的事情,特別是在分布式的情況下,用線程池去保持多個(gè)客戶端連接,是一種非常消耗線程的行為.那么我們?cè)撏ㄟ^(guò)什么技術(shù)去解決上述的問(wèn)題呢,那么就不得不提一個(gè)網(wǎng)絡(luò)連接的利器——Netty,需要的朋友可以參考下2021-06-06
Java8函數(shù)式編程應(yīng)用小結(jié)
Java8非常重要的就是引入了函數(shù)式編程的思想,使得這門(mén)經(jīng)典的面向?qū)ο笳Z(yǔ)言有了函數(shù)式的編程方式,彌補(bǔ)了很大程度上的不足,函數(shù)式思想在處理復(fù)雜問(wèn)題上有著更為令人稱贊的特性,本文給大家介紹Java8函數(shù)式編程應(yīng)用小結(jié),感興趣的朋友一起看看吧2023-12-12
Java使用OCR技術(shù)識(shí)別驗(yàn)證碼實(shí)現(xiàn)自動(dòng)化登陸方法
在本篇文章里小編給大家分享的是關(guān)于Java 如何使用 OCR 技術(shù)識(shí)別驗(yàn)證碼實(shí)現(xiàn)自動(dòng)化登陸的相關(guān)知識(shí)點(diǎn)內(nèi)容,需要的朋友們學(xué)習(xí)下。2019-08-08
Spring Boot 中的 CommandLineRunner 原理及使用示例
CommandLineRunner 是 Spring Boot 提供的一個(gè)非常有用的接口,可以幫助你在應(yīng)用程序啟動(dòng)后執(zhí)行初始化任務(wù),本文通過(guò)多個(gè)示例詳細(xì)介紹了如何在實(shí)際項(xiàng)目中使用 CommandLineRunner,感興趣的朋友一起看看吧2025-04-04
關(guān)于mybatis-plus-generator的簡(jiǎn)單使用示例詳解
在springboot項(xiàng)目中集成mybatis-plus是很方便開(kāi)發(fā)的,最近看了一下plus的文檔,簡(jiǎn)單用一下它的代碼生成器,接下來(lái)通過(guò)實(shí)例代碼講解關(guān)于mybatis-plus-generator的簡(jiǎn)單使用,感興趣的朋友跟隨小編一起看看吧2024-03-03
詳解SpringBoot2 使用Spring Session集群
這篇文章主要介紹了SpringBoot2 使用Spring Session集群,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2019-04-04
Java日期毫秒值和常見(jiàn)日期時(shí)間格式相互轉(zhuǎn)換方法
這篇文章主要給大家介紹了關(guān)于Java日期毫秒值和常見(jiàn)日期時(shí)間格式相互轉(zhuǎn)換的相關(guān)資料,在Java的日常開(kāi)發(fā)中,會(huì)隨時(shí)遇到需要對(duì)時(shí)間處理的情況,文中給出了詳細(xì)的示例代碼,需要的朋友可以參考下2023-07-07
java中l(wèi)ong(Long)與int(Integer)之間的轉(zhuǎn)換方式
這篇文章主要介紹了java中l(wèi)ong(Long)與int(Integer)之間的轉(zhuǎn)換方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-10-10
詳解SpringMVC實(shí)現(xiàn)圖片上傳以及該注意的小細(xì)節(jié)
本篇文章主要介紹了詳解SpringMVC實(shí)現(xiàn)圖片上傳以及該注意的小細(xì)節(jié),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-02-02

