2021.02.03更新
1 概述
前端Android
,上傳與下載文件,使用OkHttp
處理請求,后端使用Spring Boot
,處理Android
發送來的上傳與下載請求。這個其實不難,就是特別多奇奇怪怪的坑,因此,就一句話, 希望各位讀者能少走彎路。
2 環境
Win10
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
,需要先讀取一次,再進行剩下的讀取: