<span>如何搭建一个WEB服务器项目(六)—— 上传图片至服务器</span>
上传图片(用户头像)至服务器
观前提示:本系列文章有关服务器以及后端程序这些概念,我写的全是自己的理解,并不一定正确,希望不要误人子弟。欢迎各位大佬来评论区提出问题或者是指出错误,分享宝贵经验。先谢谢了( ̄▽ ̄)"!
前两期介绍了如何从服务器获取数据和加载图片,现在我们来看看如何把图片上传到服务器。这是一个很常见的需求,比如说上传用户头像等,本期也将围绕这个内容展开。首先服务器这边我们写一个上传图片的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去了😜。毕竟是开学前的狂欢呀,虽然我的开学遥遥无期,甚至有的同学已经得到不开学的通知了(酸了🍋),不过还是有点盼望开学的(再不开代码都敲不利索了!)最近的进度也是停滞不前,没有干劲,真怕老师突然宣布要交出点成果了😱。仔细想想,可能还是因为目标不够明确吧,都这个时候了还是不清楚往哪个方向用功,我已经能看到我惨淡的人生了😭。但是不管怎么样,生活还是要继续下去,总不能一直这么颓废,这不一有时间就更新了么,嘿嘿(快夸我( ̄▽ ̄))。最近这个系列应该都不更了,因为也没太多能分享的了,所以断更一段时间,期间可能会更新计算机网络或者算法相关的吧,有兴趣的可以关注一下我的每周动态。那么我们有缘再见👋