上傳圖片(用戶頭像)至服務器
觀前提示:本系列文章有關服務器以及后端程序這些概念,我寫的全是自己的理解,並不一定正確,希望不要誤人子弟。歡迎各位大佬來評論區提出問題或者是指出錯誤,分享寶貴經驗。先謝謝了( ̄▽ ̄)"!
前兩期介紹了如何從服務器獲取數據和加載圖片,現在我們來看看如何把圖片上傳到服務器。這是一個很常見的需求,比如說上傳用戶頭像等,本期也將圍繞這個內容展開。首先服務器這邊我們寫一個上傳圖片的controller:
1 package dolphin.controller; 2 3 import org.springframework.stereotype.Controller; 4 import org.springframework.web.bind.annotation.RequestMapping; 5 import org.springframework.web.bind.annotation.ResponseBody; 6 import org.springframework.web.multipart.MultipartFile; 7 8 import javax.servlet.http.HttpServletRequest; 9 import java.io.File; 10 import java.io.IOException; 11 12 /** 13 * @description :數據更新控制層 14 * @author :郭小柒w 15 * @date :2020/5/16 16:03 16 * @version :1.0 17 */ 18 @Controller 19 public class UpdateController { 20 /** 21 * 22 * @param file 23 * @param request 24 * @return String 不同的返回值代表不同上傳結果 25 * @throws IllegalStateException 26 * @throws IOException 27 */ 28 @RequestMapping( "/Upload") 29 @ResponseBody 30 public String photoUpload(MultipartFile file, HttpServletRequest request) throws IllegalStateException, IOException { 31 if (file != null) {// 判斷上傳的文件是否為空 32 String path = null;// 文件路徑 33 String type = null;// 文件類型 34 String fileName = file.getOriginalFilename();// 文件原名稱 35 System.out.println("上傳的文件原名稱:"+fileName); 36 // 判斷文件類型 37 type = fileName.indexOf(".") != -1 ? fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length()) : null; 38 if (type != null) {// 判斷文件類型是否為空 39 if ("GIF".equals(type.toUpperCase()) || "PNG".equals(type.toUpperCase()) || "JPG".equals(type.toUpperCase())) { 40 // 項目在容器中實際發布運行的根路徑 41 String realPath = request.getSession().getServletContext().getRealPath("/"); 42 // 自定義的文件名稱 43 String trueFileName = fileName; 44 // 設置存放圖片文件的路徑 45 path = realPath + "WEB-INF\\images\\head\\" + trueFileName; 46 // 轉存文件到指定的路徑 47 file.transferTo(new File(path)); 48 System.out.println("文件成功上傳到指定目錄下"); 49 }else { 50 System.out.println("不是我們想要的文件類型,請按要求重新上傳"); 51 return "1"; 52 } 53 }else { 54 System.out.println("文件類型為空"); 55 return "2"; 56 } 57 }else { 58 System.out.println("沒有找到相對應的文件"); 59 return "3"; 60 } 61 return "0"; 62 } 63 }
單單有這個還不夠,我們還需要進行如下配置:
1.在pom.xml里加入上傳文件相關的依賴(版本可根據需要進行更改):
1 <!-- io包 --> 2 <dependency> 3 <groupId>org.apache.commons</groupId> 4 <artifactId>commons-io</artifactId> 5 <version>1.3.2</version> 6 </dependency> 7 <!-- 文件上傳組件 --> 8 <dependency> 9 <groupId>commons-fileupload</groupId> 10 <artifactId>commons-fileupload</artifactId> 11 <version>1.3.1</version> 12 </dependency>
2.在springmvc.xml里新增如下配置:
1 <!-- SpringMVC上傳文件時,需要配置MultipartResolver處理器 --> 2 <bean id="multipartResolver" 3 class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> 4 <property name="defaultEncoding" value="UTF-8" /> 5 <!-- 指定所上傳文件的總大小,單位字節。注意maxUploadSize屬性的限制不是針對單個文件,而是所有文件的容量之和 --> 6 <property name="maxUploadSize" value="10240000" /> 7 </bean>
然后是用於測試controller的頁面,index.jsp(enctype="multipart/form-data"的作用是將form表單的數據以二進制的方式傳輸)
1 <%@ page contentType="text/html;charset=UTF-8" language="java" %> 2 <html> 3 <head> 4 <title>Title</title> 5 </head> 6 <body> 7 <fieldset> 8 <legend>圖片上傳</legend> 9 <h2>只能上傳單張10M以下的 PNG、JPG、GIF 格式的圖片</h2> 10 <form action="./Upload" method="post" enctype="multipart/form-data"> 11 選擇文件:<input type="file" name="file"> 12 <input type="submit" value="上傳"> 13 </form> 14 </fieldset> 15 </body> 16 </html>
我們先看一下效果,從本地選擇一張符合要求圖片:

選擇完畢后是這樣:

然后點擊上傳,頁面跳轉。上傳成功的話頁面只有一個“0”,圖片不再貼出,我們先看一下控制台輸出:

從輸出的存放路徑找到我們上傳的圖片:

從圖中可以看出已經上傳成功,證明我們的controller是可行的,接下來就是編寫安卓端的代碼,嘗試從客戶端上傳圖片。
客戶端獲取圖片大致就兩種方式:1.拍照;2.本地圖庫。
由於博主也不是什么技術大佬,所以老老實實地使用大神們造的輪子,主要是以下兩個庫:
- TakePhoto:https://github.com/crazycodeboy/TakePhoto
- AndPermission:https://github.com/yanzhenjie/AndPermission
更詳細的介紹請到相應網址查看,我這只是簡單的使用,首先是頁面布局:
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 xmlns:tools="http://schemas.android.com/tools" 4 android:id="@+id/activity_main" 5 android:orientation="vertical" 6 android:layout_width="match_parent" 7 android:layout_height="match_parent" 8 tools:context=".UserHeadActivity"> 9 <LinearLayout 10 android:layout_width="match_parent" 11 android:layout_height="30dp"> 12 </LinearLayout> 13 <TextView 14 android:layout_width="wrap_content" 15 android:layout_height="wrap_content" 16 android:layout_gravity="center_horizontal" 17 android:text="UserHeadActivity" /> 18 <com.example.dolphin.utils.RoundImageView 19 android:id="@+id/image_view" 20 android:layout_marginTop="10dp" 21 android:layout_gravity="center_horizontal" 22 android:layout_width="150dp" 23 android:layout_height="150dp" 24 android:src="@drawable/ic_launcher" 25 /> 26 <Button 27 android:id="@+id/take_from_camera" 28 android:text="拍照" 29 android:layout_gravity="center_horizontal" 30 android:layout_width="wrap_content" 31 android:layout_height="wrap_content" /> 32 <Button 33 android:id="@+id/take_from_galley" 34 android:text="圖庫" 35 android:layout_gravity="center_horizontal" 36 android:layout_width="wrap_content" 37 android:layout_height="wrap_content" /> 38 </LinearLayout>
可能你會有疑問,“com.example.dolphin.utils.RoundImageView”是個什么玩意兒?由於要做用戶頭像,我就順便在網上找了一個自定義圓形ImageView控件的例子,非常簡單,代碼如下:
1 package com.example.dolphin.utils; 2 3 import android.annotation.SuppressLint; 4 import android.content.Context; 5 import android.graphics.Bitmap; 6 import android.graphics.BitmapShader; 7 import android.graphics.Canvas; 8 import android.graphics.Matrix; 9 import android.graphics.Paint; 10 import android.graphics.Shader; 11 import android.graphics.drawable.BitmapDrawable; 12 import android.graphics.drawable.Drawable; 13 import android.util.AttributeSet; 14 import android.widget.ImageView; 15 16 import androidx.annotation.Nullable; 17 18 /** 19 * @author :created by 郭小柒w 20 * 時間 2020/5/16 15 21 * 自定義的圓形ImageView,可以直接當組件在布局中使用。 22 */ 23 24 @SuppressLint("AppCompatCustomView") 25 public class RoundImageView extends ImageView { 26 27 //畫筆 28 private Paint mPaint; 29 //圓形圖片的半徑 30 private int mRadius; 31 //圖片的宿放比例 32 private float mScale; 33 34 public RoundImageView(Context context) { 35 super(context); 36 } 37 38 public RoundImageView(Context context, @Nullable AttributeSet attrs) { 39 super(context, attrs); 40 } 41 42 public RoundImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 43 super(context, attrs, defStyleAttr); 44 } 45 46 @Override 47 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 48 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 49 //由於是圓形,寬高應保持一致 50 int size = Math.min(getMeasuredWidth(), getMeasuredHeight()); 51 mRadius = size / 2; 52 setMeasuredDimension(size, size); 53 } 54 55 @SuppressLint("DrawAllocation") 56 @Override 57 protected void onDraw(Canvas canvas) { 58 59 mPaint = new Paint(); 60 61 Drawable drawable = getDrawable(); 62 63 if (null != drawable) { 64 Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); 65 66 //初始化BitmapShader,傳入bitmap對象 67 BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 68 //計算縮放比例 69 mScale = (mRadius * 2.0f) / Math.min(bitmap.getHeight(), bitmap.getWidth()); 70 71 Matrix matrix = new Matrix(); 72 matrix.setScale(mScale, mScale); 73 bitmapShader.setLocalMatrix(matrix); 74 mPaint.setShader(bitmapShader); 75 //畫圓形,指定好坐標,半徑,畫筆 76 canvas.drawCircle(mRadius, mRadius, mRadius, mPaint); 77 } else { 78 super.onDraw(canvas); 79 } 80 } 81 82 }
在你的項目里創建這個類,之后就可以像上邊那樣使用啦(使用時注意類的路徑,不要直接復制粘貼上面的代碼),最后是頁面對應的Activity:
1 package com.example.dolphin; 2 3 import android.Manifest; 4 import android.content.Intent; 5 import android.net.Uri; 6 import android.os.Environment; 7 import android.os.Bundle; 8 import android.util.Log; 9 import android.view.View; 10 import android.widget.Button; 11 import android.widget.ImageView; 12 import android.widget.Toast; 13 14 import com.bumptech.glide.Glide; 15 import com.example.dolphin.utils.Constants; 16 import com.jph.takephoto.app.TakePhoto; 17 import com.jph.takephoto.app.TakePhotoActivity; 18 import com.jph.takephoto.compress.CompressConfig; 19 import com.jph.takephoto.model.CropOptions; 20 import com.jph.takephoto.model.TResult; 21 import com.yanzhenjie.permission.AndPermission; 22 import com.yanzhenjie.permission.PermissionListener; 23 import com.zhy.http.okhttp.OkHttpUtils; 24 import com.zhy.http.okhttp.callback.StringCallback; 25 26 import java.io.File; 27 import java.util.List; 28 29 import okhttp3.Call; 30 31 public class UserHeadActivity extends TakePhotoActivity { 32 33 //UIs 34 private Button takeFromCameraBtn, takeFromGalleyBtn; //拍照以及從相冊中選取Button 35 private ImageView imageView; //圖片展示ImageView 36 37 //TakePhoto 38 private TakePhoto takePhoto; 39 private CropOptions cropOptions; //裁剪參數 40 private CompressConfig compressConfig; //壓縮參數 41 private Uri imageUri; //圖片保存路徑 42 43 @Override 44 protected void onCreate(Bundle savedInstanceState) { 45 super.onCreate(savedInstanceState); 46 setContentView(R.layout.activity_userhead); 47 //申請相關權限 48 initPermission(); 49 //設置壓縮、裁剪參數 50 initData(); 51 takeFromCameraBtn = (Button) findViewById(R.id.take_from_camera); 52 takeFromCameraBtn.setOnClickListener(new View.OnClickListener() { 53 @Override 54 public void onClick(View view) { 55 imageUri = getImageCropUri(); 56 //拍照並裁剪 57 takePhoto.onPickFromCaptureWithCrop(imageUri, cropOptions); 58 //僅僅拍照不裁剪 59 //takePhoto.onPickFromCapture(imageUri); 60 } 61 }); 62 63 takeFromGalleyBtn = (Button) findViewById(R.id.take_from_galley); 64 takeFromGalleyBtn.setOnClickListener(new View.OnClickListener() { 65 @Override 66 public void onClick(View view) { 67 imageUri = getImageCropUri(); 68 //從相冊中選取圖片並裁剪 69 takePhoto.onPickFromGalleryWithCrop(imageUri, cropOptions); 70 //從相冊中選取不裁剪 71 //takePhoto.onPickFromGallery(); 72 } 73 }); 74 75 imageView = (ImageView) findViewById(R.id.image_view); 76 } 77 78 @Override 79 public void takeSuccess(TResult result) { 80 super.takeSuccess(result); 81 String iconPath = result.getImage().getOriginalPath(); 82 //Toast顯示圖片路徑 83 Toast.makeText(this, "imagePath:" + iconPath, Toast.LENGTH_SHORT).show(); 84 //上傳圖片 85 submitHead(iconPath); 86 //Google Glide庫 用於加載圖片資源,這里是把圖片展示在頁面上 87 Glide.with(this).load(iconPath).asBitmap().into(imageView); 88 } 89 90 @Override 91 public void takeFail(TResult result, String msg) { 92 super.takeFail(result, msg); 93 Toast.makeText(UserHeadActivity.this, "Error:" + msg, Toast.LENGTH_SHORT).show(); 94 } 95 96 @Override 97 public void takeCancel() { 98 super.takeCancel(); 99 } 100 101 private void initPermission() { 102 // 申請權限。 103 AndPermission.with(this) 104 .requestCode(100) 105 .permission(Manifest.permission.CAMERA,Manifest.permission.WRITE_EXTERNAL_STORAGE) 106 .send(); 107 } 108 109 @Override 110 public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { 111 // 只需要調用這一句,其它的交給AndPermission吧,最后一個參數是PermissionListener。 112 AndPermission.onRequestPermissionsResult(requestCode, permissions, grantResults, listener); 113 } 114 115 //權限申請回調接口 116 private PermissionListener listener = new PermissionListener() { 117 @Override 118 public void onSucceed(int requestCode, List<String> grantedPermissions) { 119 // 權限申請成功回調。 120 if(requestCode == 100) { 121 // TODO 相應代碼。 122 //do nothing 123 } 124 } 125 @Override 126 public void onFailed(int requestCode, List<String> deniedPermissions) { 127 // 權限申請失敗回調。 128 129 // 用戶否勾選了不再提示並且拒絕了權限,那么提示用戶到設置中授權。 130 if (AndPermission.hasAlwaysDeniedPermission(UserHeadActivity.this, deniedPermissions)) { 131 132 // 用自定義的提示語 133 AndPermission.defaultSettingDialog(UserHeadActivity.this, 103) 134 .setTitle("權限申請失敗") 135 .setMessage("我們需要的一些權限被您拒絕或者系統發生錯誤申請失敗,請您到設置頁面手動授權,否則功能無法正常使用!") 136 .setPositiveButton("好,去設置") 137 .show(); 138 } 139 } 140 }; 141 142 private void initData() { 143 ////獲取TakePhoto實例 144 takePhoto = getTakePhoto(); 145 //設置裁剪參數 146 cropOptions = new CropOptions.Builder().setAspectX(1).setAspectY(1).setWithOwnCrop(false).create(); 147 //設置壓縮參數 148 compressConfig=new CompressConfig.Builder().setMaxSize(50*1024).setMaxPixel(800).create(); 149 takePhoto.onEnableCompress(compressConfig,true); //設置為需要壓縮 150 } 151 152 //獲得照片的輸出保存Uri 153 private Uri getImageCropUri() { 154 File file=new File(Environment.getExternalStorageDirectory(), "/temp/"+System.currentTimeMillis()+".jpg"); 155 if (!file.getParentFile().exists()) 156 file.getParentFile().mkdirs(); 157 return Uri.fromFile(file); 158 } 159 160 private void submitHead(String iconPath){ 161 //獲取對應圖片 162 File file = new File(iconPath); 163 //設置網絡請求路徑 164 String url = Constants.BASE_URL+"/Upload"; 165 OkHttpUtils.post().url(url) 166 .addFile("file",file.getName(),file) 167 .build() 168 .execute(new StringCallback() { 169 @Override 170 public void onError(Call call, Exception e, int id) { 171 Toast.makeText(UserHeadActivity.this,"網絡異常,請稍后再試",Toast.LENGTH_SHORT).show(); 172 System.out.println("頁面請求失敗=="+e.getMessage()); 173 } 174 175 @Override 176 public void onResponse(String response, int id) { 177 System.out.println("首頁請求成功=="+response); 178 ResultOfUpload(response); 179 } 180 }); 181 } 182 //對返回結果進行處理 183 private void ResultOfUpload(String code){ 184 if(code.equals("0")) 185 Toast.makeText(this,"上傳成功",Toast.LENGTH_SHORT).show(); 186 else if(code.equals("1")) 187 Toast.makeText(this,"文件格式不符,請重新上傳",Toast.LENGTH_SHORT).show(); 188 else if(code.equals("2")) 189 Toast.makeText(this,"文件類型為空",Toast.LENGTH_SHORT).show(); 190 else 191 Toast.makeText(this,"未找到對應文件",Toast.LENGTH_SHORT).show(); 192 } 193 }
所有工作都已完成,接下來看看實際效果如何。下面是調試時的錄屏,這里只給出從圖庫獲取並上傳的示例,拍照的模塊大家可以自行測試,我測試時沒問題(錄屏轉gif還挺麻煩,畫質有點糊,各位湊合着看吧🙃)。

IDEA控制台輸出信息如下:

可以看到圖片已經成功上傳:

最后非常感謝下面這幾篇博客,有了他們我才能東拼西湊,做出這期想做的東西:
—————————————我———是———分———割———線————————————
拖更快樂!(bushi)
托更也是無奈嘛╮(╯-╰)╭,誰讓上周五我又跟着同學們happy去了😜。畢竟是開學前的狂歡呀,雖然我的開學遙遙無期,甚至有的同學已經得到不開學的通知了(酸了🍋),不過還是有點盼望開學的(再不開代碼都敲不利索了!)最近的進度也是停滯不前,沒有干勁,真怕老師突然宣布要交出點成果了😱。仔細想想,可能還是因為目標不夠明確吧,都這個時候了還是不清楚往哪個方向用功,我已經能看到我慘淡的人生了😭。但是不管怎么樣,生活還是要繼續下去,總不能一直這么頹廢,這不一有時間就更新了么,嘿嘿(快誇我( ̄▽ ̄))。最近這個系列應該都不更了,因為也沒太多能分享的了,所以斷更一段時間,期間可能會更新計算機網絡或者算法相關的吧,有興趣的可以關注一下我的每周動態。那么我們有緣再見👋
