1 概述
前端Android
,上传与下载文件,使用OkHttp
处理请求,后端使用Spring Boot
,处理Android
发送来的上传与下载请求。这个其实不难,就是特别多奇奇怪怪的坑,因此,就一句话, 希望各位读者能少走弯路。
2 环境
Spring Boot 2.2.2
IDEA 2019.3.1
Android Studio 3.6
Tomcat 9.0.30
3 Android
端
3.1 准备工作
3.1.1 新建工程
这次用一个全新的例子写博客,因此从新建工程开始:
3.1.2 AndroidManifest.xml
加入
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application android:usesCleartextTraffic="true">
主要是各种权限申请:
- 网络权限
- 读写
SD
卡权限 HTTP
请求的权限
3.1.3 build.gradle
加入
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
这个是支持JDK8
的。
还有这两个OkHttp
与Conscrypt
依赖,最新版本戳这里和这里查看。
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'org.conscrypt:conscrypt-android:2.5.1'
3.1.4 上传文件
手动上传一些文件到AVD
中,为下一步选择与上传文件做准备,先把这个窗口工具栏打开:
打开后,点击在右侧栏中的Device File Explorer
:
然后选择sdcard
文件夹上传文件即可,其他文件夹一般没有权限:
3.1.5 布局
组件如下:
- 三个
button
:上传/下载/选择文件 - 一个
EditText
:上传文件名与下载文件名 - 一个
ImageView
:显示下载的图片
3.2 选择文件
3.2.1 申请权限
首先申请动态读写文件权限(其实选择文件只需要读权限,因为后面的下载需要写权限所以这里就一起申请了):
使用checkSelfPermission
检查权限,参数为一个Context
+String
,String
表示相应的权限:
- 如果有权限就会返回
PackageManager.PERMISSION_GRANTED
- 没有就返回
PackageManager.PERMISSION_DENIED
没有就利用requestPermissions()
申请,参数为Content
+String[]
+int
,String[]
表示要申请的所有权限,int
是一个requestCode
。
3.2.2 Intent
选择文件
新建一个Intent
后,设置选择类型,然后就重写onActivityResult
:
这是简化了的处理,因为选择的是图片,选择其他文件的话可以参照这里。
其中path
是选择的文件的路径,读者可能有疑问下面的路径是怎么拼接的:
String path = dir.toString().substring(0,dir.toString().indexOf("0")+2) +
DocumentsContract.getDocumentId(uri).split(":")[1];
其实是拼凑过来的,因为这是图片,是下面版本的简化版:
3.3 上传文件
参数为文件路径与文件名,然后使用OkHttpClient
,因为是文件,用的请求体是MultipartBody
,增加一个叫file
的FormDataPart
与一个叫filename
的FormDataPart
,然后使用execute()
发送请求,body()
获取响应内容。
这里假设了后端响应一个布尔,表示上传成功或失败,url
的话使用了本地的路径,注意不能是localhost
,使用内网ip
,然后还要与后端对应。
3.4 下载文件
参数为一个文件名,根据这个文件名返回对应的文件,返回一个File
。这里请求体可以选择FormBody
或MultipartBody
,因为这是一个文件名参数,这里笔者为了统一就选择了MultipartBody
,使用FormBody
的话,只需要将RequestBody
的那一行改为:
RequestBody body = new FormBody.Builder().add("filename",filename).build();
有了请求体后发送请求获取响应体,进而获取输入流,然后首先需要判断是否为空,但不能直接这样判断:
inputStream == null
因为后端是这样的:
从响应体获取的inputStream
肯定不为null
,需要先进行一次读取(也就是判断里面的文件是否为null
),若为null
的话删除这个文件,不为null
的话继续读取并写入文件。
4 Spring Boot
端
4.1 准备工作
4.1.1 新建工程
打包方式JAR
/WAR
均可:
两个,一个Spring Web
+一个模板引擎,用于显示视图,如果不需要显示可以不选。
4.1.2 application.properties
配置了三项:
- 上传文件总大小限制
- 单个文件大小限制
- 上传路径
4.1.3 pom.xml
这里其实不需要干什么,只是如果下载依赖慢的话,可以这样设置settings.xml
文件,在<mirrors>
中加上:
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
<mirror>
<id>uk</id>
<mirrorOf>central</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://uk.maven.org/maven2/</url>
</mirror>
<mirror>
<id>CN</id>
<name>OSChina Central</name>
<url>http://maven.oschina.net/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
<mirror>
<id>nexus</id>
<name>internal nexus repository</name>
<!-- <url>http://192.168.1.100:8081/nexus/content/groups/public/</url>-->
<url>http://repo.maven.apache.org/maven2</url>
<mirrorOf>central</mirrorOf>
</mirror>
Windows
用户的话这个文件在
C:\Users\{username}\.m2\settings.xml
Linux
的话在
~/.m2/settings.xml
4.2 处理上传文件
首先对应的POST
映射路径为/upload
,与Android
端的路径对应,然后需要一个表示文件的MultipartFile
与一个表示文件名的String
,判断这两个是否为空。
接着如果上传的文件夹不存在则先创建,存在的话直接进行复制,然后根据复制成功或失败返回布尔值。复制使用了Files.copy()
,第一个InputStream
为上传文件的输入流,第二个Path
为存储文件的路径,resolve(filename)
相当于在上传目录下的filename
文件。
4.3 处理下载文件
下载的话可以选择使用GET
或POST
请求,这里选择了POST
请求,因为Android
端是POST
请求,需要对应。
首先根据文件名获取对应文件,判断文件是否存在后返回一个ResponseEntity
,需要设定content-type
与body,content-type
,根据需要设置即可。这里是图片,默认.jpg
或.png
,body
的话使用FileSystemResource
,直接new
一个放进body
即可。
如果不存在相应的文件则返回null
,这里需要注意一下前端的判断,不能直接判断ResponseBody
是否为null
。
5 测试
5.1 Postman
测试
5.1.1 上传测试
在Headers
中设置了Content-Type
为multipart/form-data
后:
在body
添加一个叫file
的文件与一个叫filename
的字符串表示文件名:
发送,返回true
:
服务器端有输出提示:
查看文件夹:
5.1.2 下载测试
把file
参数关掉,保留filename
,修改路径:
然后发送,postman
可以直接显示图片:
5.2 Android
端测试
5.2.1 上传测试
后端提示:
查看文件夹:
5.2.2 下载测试
输入文件名后直接下载:
默认的话是放在这里,按需要更改位置即可,注意加上写权限:
若看不到文件选择synchronize
即可。
6 部署到服务器
服务器用的是Tomcat
,需要修改一些Spring Boot
的部分。
6.1 部署
6.1.1 改变打包方式
pom.xml
中jar
改成war
:
6.1.2 去除Tomcat
依赖
pom.xml
加入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
6.1.3 修改Main
修改Main
类,让其继承SpringBootServletInitializer
,重写configure()
,同时main()保持不变。
修改前:
修改后:
6.1.4 修改路径
这个按需要修改即可,在这里不需要,注意就是@PostMapping
、@GetMapping
等都是相对于
tomcat/webapps/项目/
目录下的。
6.1.5 设置打包名字
<build>
加上<finalName>
:
6.1.6 打包
6.1.7 上传到服务器
打包后的文件放在target
下,使用scp
上传即可。这里是本地的Tomcat
,就这接移动war
了。
6.1.8 运行
开启Tomcat
,双击startup.bat
即可:
Linux
的话:
cd xxxx/tomcat/bin
./startup.sh
6.2 测试
在测试前需要确保没有占用相应端口。默认8080
,也就是说,如果不改端口的话,需要关闭IDEA
运行中的SpringBoot
应用。
6.2.1 Postman
测试
上传测试,注意需要改路径,加上打包项目名,ip
的话可以使用localhost
或者内网ip
:
服务器这边收到了,因为上传路径只是直接写名字,因此会与startup.bat
同一路径:
下载测试:
服务器的输出:
6.2.2 Android
端测试
Android
端需要修改路径即可,加上war
打包的名字。
这里打包的名字是kr,直接加上即可:
上传那里也是要加上,然后:
服务器的输出:
查看文件:
7 一些坑
7.1 权限
Android
需要读权限才能读取文件并上传,需要写权限才能保存从服务器返回的文件,在AndroidManifest.xml
中加入:
<manifest>...
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application>...</application>
这是外部设备的读写权限。当然,加入这个还不能访问,因为,Android6.0
以后还需要动态申请权限,所以:
String [] permission = new String[]{
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE"
};
if(
ActivityCompat.checkSelfPermission(this,permission[0]) != PackageManager.PERMISSION_GRANTED
||
ActivityCompat.checkSelfPermission(this,permission[1]) != PackageManager.PERMISSION_DENIED
){
ActivityCompat.requestPermissions(this,permission,1);
}
7.2 路径
需要保证下面几个路径正确,还有可读,可写等:
URL
路径不能错- 前端上传文件的路径
- 后端接收前端上传文件的路径
- 后端发送前端需要下载的文件的路径
- 前端接收下载文件的路径
7.3 有关HTTP
的问题
7.3.1 OkHttp
的stream
关闭
若前端是这样写的,在工具类中返回了之后Response已经关闭,因此需要读取输入流之类的需要先读取再返回,而不是返回一个ResponseBody
或InputStream
进行读取,否则会提示"closed"
。
7.3.2 HTTP
Android P
开始默认禁用HTTP
,因此可以使用HTTPS
或者在AndroidManifest.xml
中允许HTTP
连接:
<application android:usesCleartextTraffic="true">
7.3.3 线程
网络请求不能在主线程中,新开一个线程即可。
7.3.4 AVD
若检查过了服务器与Android
端没问题,那么有可能是AVD
的问题,解决方法很简单,卸载,重启AVD
,注意一定要卸载再重启。
7.4 ip
在本地测试的话后端可以直接localhost
,在Android
端不能直接localhost
,可以使用ipconfig
或ifconfig
查看内网ip
,输入内网ip
即可。
若在服务器上测试直接使用服务器ip
。
7.5 判空处理
对于前端,应该判断存储路径是否为空,是否为null
等,再传给后端。对于后端,要判断文件是否存在等,不存在就返回null
,这时又需要前端进行判断返回的null
,在下载文件时,虽然对不存在的文件后端返回null
,但是,前端收到的是一个InputStream
,不能直接判断是否为null
,需要先读取一次,再进行剩下的读取:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。