最近同事問(wèn)我有沒有有關(guān)于技術(shù)的電子書,我打開電腦上的小書庫(kù),但是郵件發(fā)給他太大了,公司又禁止用文件夾共享,于是花半天時(shí)間寫了個(gè)小的文件上傳程序,部署在自己的linux機(jī)器上。
提供功能: 1 .文件上傳 2.文件列表展示以及下載
原有的上傳那塊很丑,寫了點(diǎn)js代碼優(yōu)化了下,最后界面顯示如下圖:
先給出成果,下面就一步步演示怎么實(shí)現(xiàn)。
1.新建項(xiàng)目
首先當(dāng)然是新建一個(gè)spring-boot工程,你可以選擇在網(wǎng)站初始化一個(gè)項(xiàng)目或者使用ide的spring initialier功能,都可以新建一個(gè)項(xiàng)目。這里我從idea新建項(xiàng)目:
下一步,然后輸入group和artifact,繼續(xù)點(diǎn)擊next:
這時(shí)候出現(xiàn)這個(gè)模塊選擇界面,點(diǎn)擊web選項(xiàng),勾上web,證明這是一個(gè)webapp,再點(diǎn)擊template engines選擇前端的模板引擎,我們選擇thymleaf,spring-boot官方也推薦使用這個(gè)模板來(lái)替代jsp。

最后一步,然后等待項(xiàng)目初始化成功。
2.pom設(shè)置
首先檢查項(xiàng)目需要添加哪些依賴,直接貼出我的pom文件:
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
58
59
60
61
62
|
<?xml version= "1.0" encoding= "utf-8" ?> <project xmlns= "http://maven.apache.org/pom/4.0.0" xmlns:xsi= "http://www.w3.org/2001/xmlschema-instance" xsi:schemalocation= "http://maven.apache.org/pom/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelversion> 4.0 . 0 </modelversion> <groupid>com.shuqing28</groupid> <artifactid>upload</artifactid> <version> 0.0 . 1 -snapshot</version> <packaging>jar</packaging> <name>upload</name> <description>demo project for spring boot</description> <parent> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-parent</artifactid> <version> 1.5 . 9 .release</version> <relativepath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceencoding>utf- 8 </project.build.sourceencoding> <project.reporting.outputencoding>utf- 8 </project.reporting.outputencoding> <java.version> 1.8 </java.version> </properties> <dependencies> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter</artifactid> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-thymeleaf</artifactid> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-configuration-processor</artifactid> <optional> true </optional> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-test</artifactid> <scope>test</scope> </dependency> <!-- https: //mvnrepository.com/artifact/org.webjars/bootstrap --> <dependency> <groupid>org.webjars</groupid> <artifactid>bootstrap</artifactid> <version> 3.3 . 5 </version> </dependency> <!-- https: //mvnrepository.com/artifact/org.webjars.bower/jquery --> <dependency> <groupid>org.webjars.bower</groupid> <artifactid>jquery</artifactid> <version> 2.2 . 4 </version> </dependency> </dependencies> <build> <plugins> <plugin> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-maven-plugin</artifactid> </plugin> </plugins> </build> </project> |
可以查看到 spring-boot-starter-thymeleaf 包含了webapp,最后兩個(gè)webjars整合了bootstrap和jquery,其它的等代碼里用到再說(shuō)。
最后一個(gè)spring boot maven plugin是系統(tǒng)創(chuàng)建時(shí)就添加的,它有以下好處:
1 . 它能夠打包c(diǎn)lasspath下的所有jar,構(gòu)建成一個(gè)可執(zhí)行的“über-jar”,方便用戶轉(zhuǎn)移服務(wù)
2 . 自動(dòng)搜索 public static void main() 方法并且標(biāo)記為可執(zhí)行類
3 . 根據(jù)spring-boot版本,提供內(nèi)建的依賴解釋。
3. 上傳文件控制器
如果你只是使用springmvc上傳文件,是需要配置一個(gè) multipartresolver 的bean的,或者在 web.xml 里配置一個(gè) <multipart-config> ,不過(guò)借助于spring-boot的自動(dòng)配置,你什么都不必做。直接寫控制器類,我們?cè)?src/main/java 下新建controller的package,并且新建fileuploadcontroller:
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
|
package com.shuqing28.upload.controller; import com.shuqing28.uploadfiles.pojo.linker; import com.shuqing28.uploadfiles.exceptions.storagefilenotfoundexception; import com.shuqing28.uploadfiles.service.storageservice; import org.springframework.beans.factory.annotation.autowired; import org.springframework.core.io.resource; import org.springframework.http.httpheaders; import org.springframework.http.responseentity; import org.springframework.stereotype.controller; import org.springframework.ui.model; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.multipartfile; import org.springframework.web.servlet.mvc.method.annotation.mvcuricomponentsbuilder; import org.springframework.web.servlet.mvc.support.redirectattributes; import java.io.ioexception; import java.util.list; import java.util.stream.collectors; @controller public class fileuploadcontroller { private final storageservice storageservice; @autowired public fileuploadcontroller(storageservice storageservice) { this .storageservice = storageservice; } @getmapping ( "/" ) public string listuploadedfiles(model model) throws ioexception { list<linker> linkers = storageservice.loadall().map( path -> new linker(mvcuricomponentsbuilder.frommethodname(fileuploadcontroller. class , "servefile" , path.getfilename().tostring()).build().tostring(), path.getfilename().tostring()) ).collect(collectors.tolist()); model.addattribute( "linkers" , linkers); return "uploadform" ; } @getmapping ( "/files/{filename:.+}" ) @responsebody public responseentity<resource> servefile( @pathvariable string filename) { resource file = storageservice.loadasresource(filename); return responseentity.ok().header(httpheaders.content_disposition, "attachment; filename=\"" + file.getfilename() + "\"" ).body(file); } @postmapping ( "/" ) public string handlefileupload( @requestparam ( "file" ) multipartfile file, redirectattributes redirectattributes) { storageservice.store(file); redirectattributes.addflashattribute( "message" , "you successfully uploaded " + file.getoriginalfilename() + "!" ); return "redirect:/" ; } @exceptionhandler (storagefilenotfoundexception. class ) public responseentity<?> handlestoragefilenotfound(storagefilenotfoundexception exc) { return responseentity.notfound().build(); } } |
類定義處添加了 @controller 注解,證明這是一個(gè)controller,每個(gè)方法前添加了 @getmapping 和 @postmapping 分別相應(yīng)get和post請(qǐng)求。
首先是 @getmapping("/") ,方法 listuploadedfiles ,顧名思義,顯示文件列表,這里我們借助于storageservice遍歷文件夾下的所有文件,并且用map方法提合成了鏈接和文件名列表,返回了一個(gè)linker對(duì)象的數(shù)組,linker對(duì)象是一個(gè)簡(jiǎn)單pojo,只包含下面兩部分:
1
2
|
private string fileurl; private string filename; |
這個(gè)方法包含了對(duì)java8中stream的使用,如果有不理解的可以看看這篇文章 java8 特性詳解(二) stream api .
接下來(lái)是 @getmapping("/files/{filename:.+}")
,方法是 servefile ,該方法提供文件下載功能,還是借助于storageservice,后面會(huì)貼出storageservice的代碼。最后使用responseentity,把文件作為body返回給請(qǐng)求方。
@postmapping("/")
的 handlefileupload 使用post請(qǐng)求來(lái)上傳文件,參數(shù) @requestparam("file") 提取網(wǎng)頁(yè)請(qǐng)求里的文件對(duì)象,還是使用storageservice來(lái)保存對(duì)象,最后使用重定向來(lái)刷新網(wǎng)頁(yè),并且給出成功上傳的message。
4. 文件處理
上面controller調(diào)用的很多方法由storageservice提供,我們定義一個(gè)接口,包含以下方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package com.shuqing28.uploadfiles.service; import org.springframework.core.io.resource; import org.springframework.web.multipart.multipartfile; import java.nio.file.path; import java.util.stream.stream; public interface storageservice { void init(); void store(multipartfile file); stream<path> loadall(); path load(string filename); resource loadasresource(string filename); void deleteall(); } |
因?yàn)槲疫@里只是借助于本地文件系統(tǒng)處理文件的長(zhǎng)傳下載,所以有了以下實(shí)現(xiàn)類:
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
|
package com.shuqing28.uploadfiles.service; import com.shuqing28.uploadfiles.exceptions.storageexception; import com.shuqing28.uploadfiles.exceptions.storagefilenotfoundexception; import com.shuqing28.uploadfiles.config.storageproperties; import org.springframework.beans.factory.annotation.autowired; import org.springframework.core.io.resource; import org.springframework.core.io.urlresource; import org.springframework.stereotype.service; import org.springframework.util.filesystemutils; import org.springframework.util.stringutils; import org.springframework.web.multipart.multipartfile; import java.io.ioexception; import java.net.malformedurlexception; import java.nio.file.files; import java.nio.file.path; import java.nio.file.paths; import java.nio.file.standardcopyoption; import java.util.stream.stream; @service public class filesystemstorageservice implements storageservice { private final path rootlocation; @autowired public filesystemstorageservice(storageproperties properties) { this .rootlocation = paths.get(properties.getlocation()); } @override public void init() { try { files.createdirectories(rootlocation); } catch (ioexception e) { throw new storageexception( "could not initialize storage" , e); } } @override public void store(multipartfile file) { string filename = stringutils.cleanpath(file.getoriginalfilename()); try { if (file.isempty()) { throw new storageexception( "failed to store empty file" + filename); } if (filename.contains( ".." )) { // this is a security check throw new storageexception( "cannot store file with relative path outside current directory " + filename); } files.copy(file.getinputstream(), this .rootlocation.resolve(filename), standardcopyoption.replace_existing); } catch (ioexception e) { throw new storageexception( "failed to store file" + filename, e); } } @override public stream<path> loadall() { try { return files.walk( this .rootlocation, 1 ) .filter(path -> !path.equals( this .rootlocation)) .map(path-> this .rootlocation.relativize(path)); } catch (ioexception e) { throw new storageexception( "failed to read stored files" , e); } } @override public path load(string filename) { return rootlocation.resolve(filename); } @override public resource loadasresource(string filename) { try { path file = load(filename); resource resource = new urlresource(file.touri()); if (resource.exists() || resource.isreadable()) { return resource; } else { throw new storagefilenotfoundexception( "could not read file: " + filename); } } catch (malformedurlexception e) { throw new storagefilenotfoundexception( "could not read file: " + filename, e); } } @override public void deleteall() { filesystemutils.deleterecursively(rootlocation.tofile()); } } |
這個(gè)類也基本運(yùn)用了java的nio,使用path對(duì)象定義了location用于文件的默認(rèn)保存路徑。
先看 store 方法,store接受一個(gè)multipartfile對(duì)象作為參數(shù),想比于傳統(tǒng)jsp中只是傳二進(jìn)制字節(jié)數(shù)組,multipartfile提供了很多方便調(diào)用的方法讓我們可以獲取到上傳文件的各項(xiàng)信息:
1
2
3
4
5
6
7
8
9
10
|
public interface multipartfile extends inputstreamsource { string getname(); string getoriginalfilename(); string getcontenttype(); boolean isempty(); long getsize(); byte [] getbytes() throws ioexception; inputstream getinputstream() throws ioexception; void transferto(file dest) throws ioexception, illegalstateexception; } |
代碼里使用了files的copy方法把文件流拷到location對(duì)應(yīng)的path里,當(dāng)然我們也可以使用transferto方法保存文件, file.transferto(this.rootlocation.resolve(filename).tofile());
loadall方法加載該路徑下的所有文件path信息, loadasresource 則是加載文件為一個(gè)resource對(duì)象,再看controller的代碼,最后是接受一個(gè)resource對(duì)象作為body返回給請(qǐng)求方。
5. 前端模板
最后定義了前端模板,這里依舊先看代碼:
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
58
59
60
61
62
63
64
65
66
67
68
69
|
<html xmlns:th= "http://www.thymeleaf.org" > <head> <title>share files</title> </head> <body> <div class = "col-md-8 col-md-offset-2" th: if = "${message}" > <h2 th:text= "${message}" /> </div> <div class = "col-md-8 col-md-offset-2" > <form method= "post" action= "/" enctype= "multipart/form-data" > <!-- component start --> <input type= "file" name= "file" class = "input-ghost" style= "visibility:hidden; height:0" /> <div class = "form-group" > <div class = "input-group input-file" name= "fichier1" > <input type= "text" class = "form-control" placeholder= 'choose a file...' /> <span class = "input-group-btn" > <button class = "btn btn-default btn-choose" type= "button" >choose</button> </span> </div> </div> <!-- component end --> <div class = "form-group" > <button type= "submit" class = "btn btn-primary pull-right" >submit</button> <button type= "reset" class = "btn btn-danger" >reset</button> </div> </form> </div> <div class = "col-md-8 col-md-offset-2" > <ul> <li th:each= "linker: ${linkers}" > <a th:href= "${linker.fileurl}" rel= "external nofollow" th:text= "${linker.filename}" /> </li> </ul> </div> <script src= "//ajax.aspnetcdn.com/ajax/jquery/jquery-1.9.1.min.js" ></script> <script src= "/webjars/bootstrap/3.3.5/js/bootstrap.min.js" ></script> <script type= "text/javascript" th:inline= "javascript" > function bs_input_file() { $( ".input-file" ).before( function() { if ( ! $( this ).prev().hasclass( 'input-ghost' ) ) { var element = $( ".input-ghost" ); element.change(function(){ element.next(element).find( 'input' ).val((element.val()).split( '\\' ).pop()); }); $( this ).find( "button.btn-choose" ).click(function(){ element.click(); }); $( this ).find( "button.btn-reset" ).click(function(){ element.val( null ); $( this ).parents( ".input-file" ).find( 'input' ).val( '' ); }); $( this ).find( 'input' ).css( "cursor" , "pointer" ); $( this ).find( 'input' ).mousedown(function() { $( this ).parents( '.input-file' ).prev().click(); return false ; }); return element; } } ); } $(function() { bs_input_file(); }); </script> <link rel= "stylesheet" href= "/webjars/bootstrap/3.3.5/css/bootstrap.min.css" rel= "external nofollow" /> </body> </html> |
這里重要的地方還是 <form> 標(biāo)簽內(nèi)的內(nèi)容, <form method="post" action="/" enctype="multipart/form-data"> enctype 一定要寫成 multipart/form-data ,使用post上傳文件,原有的上傳控件很丑,所以做了一個(gè)text+input放在表面,在下面放了一個(gè)隱形的上傳文件的input,可以自己看看代碼,本文就不啰嗦了。
下面還放了一個(gè)list用于展示文件列表,這里我們獲取到服務(wù)端提供的linkers對(duì)象,不斷foreach就可以獲得里面的兩個(gè)元素fileurl和filename。
這里jquery換成了微軟的cdn,webjars的總是引入不進(jìn)來(lái),不知道什么原因。
其它設(shè)置
在 src/main/resources/application.properties 里設(shè)置上傳文件大小限制
1
2
|
spring.http.multipart.max-file-size=128mb spring.http.multipart.max-request-size=128mb |
另外在``還設(shè)置了文件默認(rèn)保存路徑:
1
2
3
4
5
6
7
8
9
10
11
12
|
package com.shuqing28.uploadfiles.config; import org.springframework.boot.context.properties.configurationproperties; @configurationproperties ( "storage" ) public class storageproperties { private string location = "/home/jenkins/upload-files/" ; public string getlocation() { return location; } public void setlocation(string location) { this .location = location; } } |
這里注意,由于storageproperties的設(shè)置,在application的那個(gè)類中要添加上
1
2
3
4
5
6
7
8
|
@enableconfigurationproperties 注解 @springbootapplication @enableconfigurationproperties (storageproperties. class ) public class uploadapplication { public static void main(string[] args) { springapplication.run(uploadapplication. class , args); } } |
總結(jié)
以上所述是小編給大家介紹的spring boot + thymeleaf 實(shí)現(xiàn)文件上傳下載功能,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)服務(wù)器之家網(wǎng)站的支持!
原文鏈接:https://juejin.im/post/5a326dcaf265da431048685e