1.上传下载是什么?
上传下载是我们日常生活中可以说经常遇见事情了,尤其是身为程序员的我们几乎天天都在和这个打交道。上传下载的意思实际上就是信息的一种传递,既然涉及到上传下载,必然是有服务器和客户端之分的。上传就是客户端将自己的本地文件信息传输给服务器端,服务器端接收并保存在自己想要的地方,下载则是相反,将服务器端的信息传递到客户端,客户端进行相应的接收。
2.在Spring Boot基础下的上传下载实质是什么?
上面我们说到上传下载的其实就是一种信息的传递,那么如何进行上传下载问题的关键也就是在于如何进行信息的传递。在Spring Boot的rest接口中,信息的传递都是通过网络请求和响应来完成的,那么上传下载自然也需要借助到请求和响应。请求和响应自然是无法直接携带文件的,携带的信息本质上其实都要转换成字符的形式,也就是说我们传递的其实不是文件的本身,而是文件里面所存在的内容,我们所做的就是将文件中的内容传递进请求,让另一方从请求中获取,或者说将文件的内容传递进响应,让另一方在响应中获得。那么说起传递文件信息的内容,自然想到的第一个就文件流,实际上,我们也正是借助流来将文件内容存进请求或者响应,然后让另一方获取。
3.如何在Spring Boot中实现简单的上传下载?
那搞清楚了上传下载的实质之后,接下来就让我们来了解一下在Spring Boot中如何进行上传下载的实现。之前也说到我们是借助流来将文件信息写入请求或者是响应,那么实现其实就是如何将文件读成流放进请求或者响应,或者想请求和响应中的流获取并使用。
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
|
package
com.bs.web_rest;
import
org.springframework.beans.factory.annotation.Value;
import
org.springframework.web.bind.annotation.GetMapping;
import
org.springframework.web.bind.annotation.RequestMapping;
import
org.springframework.web.bind.annotation.RequestParam;
import
org.springframework.web.bind.annotation.RestController;
import
org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import
javax.servlet.http.HttpServletResponse;
import
java.io.File;
import
java.io.FileInputStream;
import
java.io.FileNotFoundException;
import
java.io.InputStream;
/**
* 各类接口
*/
@RestController
@RequestMapping
(
"/api"
)
public
class
Resource {
private
String rootPath=
"F:/download/"
;
/**
* 下载文件
*
* @param filename
* @param response
* @return StreamingResponseBody
*/
@GetMapping
(value =
"/downloadFile"
)
public
StreamingResponseBody downloadFile(
@RequestParam
(value =
"filename"
) String filename, HttpServletResponse response) {
String filePath = rootPath + filename;
if
(
new
File(filePath).exists()) {
response.setContentType(
"application/text"
);
//设置响应头,保证数据能够以文件形式接收
response.setHeader(
"Content-Disposition"
,
"attachment;filename=\"download.txt\""
);
//设置下载文件名
try
{
final
InputStream is =
new
FileInputStream(filePath);
return
outputStream -> {
int
nRead;
byte
[] data =
new
byte
[
1024
];
while
((nRead = is.read(data,
0
, data.length)) != -
1
) {
outputStream.write(data,
0
, nRead);
}
outputStream.flush();
outputStream.close();
};
}
catch
(FileNotFoundException e) {
e.printStackTrace();
}
}
return
null
;
}
}
|
1-1就是一个典型的下载接口,参数接收一个文件名,拼接路径找到文件,设置好响应头部文件以后,将流写进响应,这样就完成了一个完整的下载流程。
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
|
package
com.bs.web_rest;
import
org.springframework.beans.factory.annotation.Value;
import
org.springframework.web.bind.annotation.PostMapping;
import
org.springframework.web.bind.annotation.RequestMapping;
import
org.springframework.web.bind.annotation.RestController;
import
javax.servlet.ServletException;
import
javax.servlet.http.HttpServletRequest;
import
javax.servlet.http.Part;
import
java.io.File;
import
java.io.IOException;
/**
* 各类接口
*/
@RestController
@RequestMapping
(
"/api"
)
public
class
Resource {
@Value
(
"${rootPath}"
)
private
String rootPath;
/**
* 接受上传文件,形成对应的文件
*
* @param request
*/
@PostMapping
(value =
"/upload"
)
public
void
upload(HttpServletRequest request) {
String realFile=rootPath +
"upload.txt"
;
File file =
new
File(realFile);
if
(!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
Part filePart =
null
;
try
{
filePart = request.getPart(
"UPLOAD"
);
filePart.write(realFile);
}
catch
(IOException e) {
e.printStackTrace();
}
catch
(ServletException e) {
e.printStackTrace();
}
}
}
|
1-2是一个上传接口例子,上传原理和之前说的一样,利用获取到的流写入自己的路径,这样就相当于把文件下载了下来。
4.java中该如何对应接口进行一个文件上传?
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
|
package
test;
import
java.io.File;
import
java.io.IOException;
import
java.util.List;
import
org.apache.http.HttpEntity;
import
org.apache.http.HttpResponse;
import
org.apache.http.client.ClientProtocolException;
import
org.apache.http.client.HttpClient;
import
org.apache.http.client.methods.HttpPost;
import
org.apache.http.entity.ContentType;
import
org.apache.http.entity.mime.MultipartEntityBuilder;
import
org.apache.http.entity.mime.content.FileBody;
import
org.apache.http.entity.mime.content.StringBody;
import
com.fasterxml.jackson.databind.ObjectMapper;
public
class
Test{
public
static
void
sendPost(){
HttpClient httpClient=
new
DefaultHttpClient();
HttpPost post=
new
HttpPost(
"<a href="http://localhost:8080/api/upload" "="" style="color: rgb(50, 108, 166); border-radius: 0px !important; background: none !important; border: 0px !important; bottom: auto !important; float: none !important; height: auto !important; left: auto !important; line-height: 20px !important; margin: 0px !important; outline: 0px !important; overflow: visible !important; padding: 0px !important; position: static !important; right: auto !important; top: auto !important; vertical-align: baseline !important; width: auto !important; box-sizing: content-box !important; min-height: auto !important;">http://localhost:8080/api/upload"
);//给指定接口发送Post请求
MultipartEntityBuilder builder=MultipartEntityBuilder.create();
ObjectMapper mapper =
new
ObjectMapper();
try
{
builder.addPart(
"UPLOAD"
,
new
FileBody(
new
File(
"F:/1.txt"
)));
//将文件传输进requstPart中
HttpEntity entity=builder.build();
post.setEntity(entity);
HttpResponse response=httpClient.execute(post);
//发送请求
}
catch
(ClientProtocolException e) {
e.printStackTrace();
}
catch
(IOException e) {
e.printStackTrace();
}
}
}
|
在这里使用的是HttpClient进行的请求发送,HttpClient相对于传统的Connection请求发送的确是简便了非常多,有关这两者的区别有机会再另开一篇叙述吧,这里就不多聊了。
以上接口样例经测试,都可以直接运行。
扩展–浅谈java中文件锁的机制以及简单的使用
之前的一个项目中做的一个上传下载功能中遇到了高并发访问情景,在一个文件被多个请求访问的时候上传下载将不再是安全的,无论是上传的时候别人进行下载还是下载的时候别人进行上传,都会造成文件污染。这就要求在一个请求进行操作的时候,需要保证这个锁是一个安全的。之前遇到的锁类型大多都是方法锁,锁住这个方法以保证方法被调用的时候另外一个调用者需要在锁池外等待,但是在接口中肯定是不能进行这种操作的,那么我们实际的安全问题出现在一个文件被多人访问的情况,那么我们应该对文件加锁而不是对这个方法进行加锁,这就引申出了文件锁的机制。
简单的介绍一下FileLock文件锁
1.FileLock是独占锁,控制不同程序(JVM)对同一文件的并发访问。
2.可以对写文件(w)加锁,而且必须是可写文件,不然回报:java.nio.channels.NonWritableChannelException异常,这样可以保证只有同一个进程才能拿到锁对文件访问。其他进程无法访问改文件,或者删除该文件的目录,由于是独占锁,所以可以保证进程间顺序访问该文件,避免数据错误。
3.FileLock的生命周期,在调用FileLock.release(),或者Channel.close(),或者JVM关闭,则生命周期结束。
有几个值得注意的地方
1.同一进程内,在文件锁没有被释放之前,不可以再次获取。即在release()方法调用前,只能lock()或者tryLock()一次。
2.文件锁的效果是与操作系统相关的。一些系统中文件锁是强制性的,就当Java的某进程获得文件锁后,操作系统将保证其它进程无法对文件做操作了。而另一些操作系统的文件锁是询问式的(advisory),意思是说要想拥有进程互斥的效果,其它的进程也必须也按照API所规定的那样来申请或者检测文件锁,不然将起不到进程互斥的功能。所以文档里建议将所有系统都当做是询问式系统来处理,这样程序更加安全也更容易移植。
3.如何避免死锁:在读写关键数据时加锁,操作完成后解锁;一次性申请所有需要的资源,并且在申请不成功的情况下放弃已申请到的资源。
敲了这么多,感觉还是例子直观一点。
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
|
@ApiOperation
(value =
"文件下载"
)
@GetMapping
(value =
"/files"
)
@ResponseBody
public
StreamingResponseBody getFile(
@RequestParam
(
"filePath"
) String filePath, HttpServletResponse response)
throws
Exception {
filePath = rootPath.endsWith(
"/"
) ? rootPath+dirName + filePath : rootPath +
"/"
+dirName + filePath;
Path path = Paths.get(filePath);
String mimeType = Files.probeContentType(path);
RandomAccessFile randomAccessFile =
new
RandomAccessFile(filePath,
"rw"
);
//给文件进行加锁,给予文件读写权限。
FileChannel fileChannel = randomAccessFile.getChannel();
//获取FileChannel对象以对锁文件进行相应的操作
FileLock fileLock = fileChannel.lock();
//文件加锁
response.setContentType(mimeType);
response.setHeader(
"Content-Disposition"
,
"attachment; filename=\""
+ fileName +
"\""
);
response.addHeader(
"Content-Length"
,
""
+ randomAccessFile.length());
response.setHeader(
"Cache-Control"
,
"no-cache"
);
response.setHeader(
"Pragma"
,
"no-cache"
);
response.setHeader(
"If-Modified-Since"
,
"0"
);
InputStream is = Channels.newInputStream(randomAccessFile.getChannel());
//从锁文件获取文件流
return
outputStream -> {
try
{
int
nRead;
byte
[] data =
new
byte
[
1024
];
while
((nRead = is.read(data,
0
,data.length)) != -
1
) {
outputStream.write(data,
0
, nRead);
}
outputStream.flush();
}
catch
(IOException e) {
e.printStackTrace();
}
finally
{
outputStream.close();
fileLock.release();
//释放锁
fileChannel.close();
//释放FileChannel对象
}
};
}
|
这样在文件被操作的时候就会获取一个锁,只有当自己的事情处理完成之后,这个锁才会释放,给锁池等待区的其他线程处理。