在现在的网络开发中,上传图片类的需求实在是太普通不过了,但是对于怎么样做到上传图片,对于刚开始建立项目的时候,还是有点不知所措的。也许有幸,我们做的项目是之前已经有人写过类似的用例了,那么我们只需要依葫芦画瓢就行了。
好好了解下图片上传(文件上传)的方式,对于认知的提升还是有好处的。而且说不定哪天你就有个这样的需求呢,这里是一条龙上传。
本文就一个从app到php层,再到java层的流程,演译下整个上传图片的流程吧。
一、app端获取用户选择的图片,转化为输入流,上传至php前端接口:
package com.dia.ration; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.UUID; /** * 上传文件到服务器类 */ public class UploadUtil { private static final String TAG = "uploadFile"; private static final int TIME_OUT = 10 * 1000; // 超时时间 private static final String CHARSET = "utf-8"; // 设置编码 /** * Android上传文件到服务端 * * @param file 需要上传的文件 * @param RequestURL 请求的rul * @return 返回响应的内容 */ public static String uploadFile(File file, String RequestURL) { String result = null; String BOUNDARY = UUID.randomUUID().toString(); // 边界标识 随机生成 String PREFIX = "--", LINE_END = " "; String CONTENT_TYPE = "multipart/form-data"; // 内容类型 try { URL url = new URL(RequestURL); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setReadTimeout(TIME_OUT); conn.setConnectTimeout(TIME_OUT); conn.setDoInput(true); // 允许输入流 conn.setDoOutput(true); // 允许输出流 conn.setUseCaches(false); // 不允许使用缓存 conn.setRequestMethod("POST"); // 请求方式 conn.setRequestProperty("Charset", CHARSET); // 设置编码 conn.setRequestProperty("connection", "keep-alive"); conn.setRequestProperty("Content-Type", CONTENT_TYPE + ";boundary=" + BOUNDARY); if (file != null) { DataOutputStream dos = new DataOutputStream(conn.getOutputStream()); StringBuffer sb = new StringBuffer(); sb.append(PREFIX); sb.append(BOUNDARY); sb.append(LINE_END); /** * 这里重点注意: name里面的值为服务端需要key 只有这个key 才可以得到对应的文件 * filename是文件的名字,包含后缀名的 比如:abc.png */ sb.append("Content-Disposition: form-data; name="uploadfile"; filename="" + file.getName() + """ + LINE_END); sb.append("Content-Type: application/octet-stream; charset=" + CHARSET + LINE_END); sb.append(LINE_END); dos.write(sb.toString().getBytes()); InputStream is = new FileInputStream(file); byte[] bytes = new byte[1024]; int len = 0; while ((len = is.read(bytes)) != -1) { dos.write(bytes, 0, len); } is.close(); dos.write(LINE_END.getBytes()); byte[] end_data = (PREFIX + BOUNDARY + PREFIX + LINE_END).getBytes(); dos.write(end_data); dos.flush(); InputStream input = conn.getInputStream(); StringBuffer sb1 = new StringBuffer(); int ss; while ((ss = input.read()) != -1) { sb1.append((char) ss); } result = sb1.toString(); } } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return result; } /** * 通过拼接的方式构造请求内容,实现参数传输以及文件传输 * * @param url Service net address * @param params text content * @param files pictures * @return String result of Service response * @throws IOException */ public static String post(String url, Map<String, String> params, Map<String, File> files) throws IOException { String BOUNDARY = UUID.randomUUID().toString(); String PREFIX = "--", LINEND = " "; String MULTIPART_FROM_DATA = "multipart/form-data"; String CHARSET = "UTF-8"; URL uri = new URL(url); HttpURLConnection conn = (HttpURLConnection) uri.openConnection(); conn.setReadTimeout(10 * 1000); // 缓存的最长时间 conn.setDoInput(true); // 允许输入 conn.setDoOutput(true); // 允许输出 conn.setUseCaches(false); // 不允许使用缓存 conn.setRequestMethod("POST"); conn.setRequestProperty("connection", "keep-alive"); conn.setRequestProperty("Charsert", "UTF-8"); conn.setRequestProperty("Content-Type", MULTIPART_FROM_DATA + ";boundary=" + BOUNDARY); // 首先组拼文本类型的参数 StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> entry : params.entrySet()) { sb.append(PREFIX); sb.append(BOUNDARY); sb.append(LINEND); sb.append("Content-Disposition: form-data; name="" + entry.getKey() + """ + LINEND); sb.append("Content-Type: text/plain; charset=" + CHARSET + LINEND); sb.append("Content-Transfer-Encoding: 8bit" + LINEND); sb.append(LINEND); sb.append(entry.getValue()); sb.append(LINEND); } DataOutputStream outStream = new DataOutputStream(conn.getOutputStream()); outStream.write(sb.toString().getBytes()); // 发送文件数据 if (files != null) for (Map.Entry<String, File> file : files.entrySet()) { StringBuilder sb1 = new StringBuilder(); sb1.append(PREFIX); sb1.append(BOUNDARY); sb1.append(LINEND); sb1.append("Content-Disposition: form-data; name="uploadfile"; filename="" + file.getValue().getName() + """ + LINEND); sb1.append("Content-Type: application/octet-stream; charset=" + CHARSET + LINEND); sb1.append(LINEND); outStream.write(sb1.toString().getBytes()); InputStream is = new FileInputStream(file.getValue()); byte[] buffer = new byte[1024]; int len = 0; while ((len = is.read(buffer)) != -1) { outStream.write(buffer, 0, len); } is.close(); outStream.write(LINEND.getBytes()); } byte[] end_data = (PREFIX + BOUNDARY + PREFIX + LINEND).getBytes(); outStream.write(end_data); outStream.flush(); int res = conn.getResponseCode(); InputStream in = conn.getInputStream(); StringBuilder sb2 = new StringBuilder(); if (res == 200) { int ch; while ((ch = in.read()) != -1) { sb2.append((char) ch); } } outStream.close(); conn.disconnect(); return sb2.toString(); } // 测试 public static void main(String[] args) throws IOException { String requestURL = "sss"; final Map<String, String> params = new HashMap<String, String>(); params.put("send_userId", String.valueOf(1)); params.put("send_email", "ss@ss.com"); final Map<String, File> files = new HashMap<String, File>(); files.put("uploadfile", new File("/var/data/de.jpg")); final String result = UploadUtil.post(requestURL, params, files); System.out.println("result is: " + result); } }
二、php服务端接收文件,临时保存并继续上传至java后端:
1. 接收文件类
<?php namespace AppController; use ActionRestAction; use ApiUploadApi; class UserController extends RestAction { /** * 用户头像上传 */ public function set_avatar_post($code) { $uploadApi = new UploadApi(); $res = $uploadApi->uploads('avatar'); $filename = $res['data']; $result = $uploadApi->uploadAvatar($code, $filename); @unlink($filename); //删除图片 if (!$result['status']) { $this->response($result); } $avatar = A("Personal", "Api")->getAvatar($code); $this->response($avatar); } }
2. 上传类
<?php namespace ApiAction; class UploadApi { public function __construct() { //... } public function curlGet($url, $param = array(), $timeout = 30, $ajaxResponseImmediately = true) { $opts = array( CURLOPT_TIMEOUT => $timeout, CURLOPT_RETURNTRANSFER => 1, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_HTTPHEADER => $header ); switch (strtoupper($method)) { // case 'POST': // $opts[CURLOPT_URL] = $url; // $opts[CURLOPT_POST] = 1; // $opts[CURLOPT_POSTFIELDS] = $param; // break; default: $opts[CURLOPT_URL] = $url . '?' . http_build_query($param); break; } $ch = curl_init(); curl_setopt_array($ch, $opts); $result = curl_exec($ch); //记录请求日志 curl_close($ch); return $result; } public function curlPost($url, $param = array(), $timeout = 30, $ajaxResponseImmediately = true) { $opts = array( CURLOPT_TIMEOUT => $timeout, CURLOPT_RETURNTRANSFER => 1, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_HTTPHEADER => $header ); switch (strtoupper($method)) { case 'POST': default: $opts[CURLOPT_URL] = $url; $opts[CURLOPT_POST] = 1; $opts[CURLOPT_POSTFIELDS] = $param; break; // $opts[CURLOPT_URL] = $url . '?' . http_build_query($param); // break; } $ch = curl_init(); curl_setopt_array($ch, $opts); $result = curl_exec($ch); $log_data['result'] = $result; if (!empty($param)) $log_data['param'] = $param; curl_close($ch); return $result; } public function uploads($param = '') { if ($param == '') { $param = 'imgFile'; } // 文件保存目录路径 $save_url = dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR . "Uploads" . DIRECTORY_SEPARATOR; // 定义允许上传的文件扩展名 $ext_arr = array( 'image' => array('gif', 'jpg', 'jpeg', 'png', 'bmp'), ); // 最大文件大小 $max_size = 20 * 1024; // PHP上传失败 if (!empty ($_FILES [$param] ['error'])) { switch ($_FILES [$param] ['error']) { case '1' : $error = '超过php.ini允许的大小。'; break; case '2' : $error = '超过表单允许的大小。'; break; case '3' : $error = '图片只有部分被上传。'; break; case '4' : $error = '请选择图片。'; break; case '6' : $error = '找不到临时目录。'; break; case '7' : $error = '写文件到硬盘出错。'; break; case '8' : $error = 'File upload stopped by extension。'; break; case '999' : default : $error = '未知错误。'; } $result = array('status' => '0', 'error' => '111111', 'msg' => $error); } // 有上传文件时 if (empty ($_FILES) === false) { $file_name = $_FILES [$param] ['name'];// 原文件名 $tmp_name = $_FILES [$param] ['tmp_name'];// 服务器上临时文件名 $file_size = $_FILES [$param] ['size'];// 文件大小 // 检查文件名 if (!$file_name) { $result = array('status' => '0', 'error' => '111111', 'msg' => '请选择文件'); } // 检查是否已上传 if (@is_uploaded_file($tmp_name) === false) { $result = array('status' => '0', 'error' => '111111', 'msg' => '上传失败'); } // 检查文件大小 if ($file_size > $max_size) { $result = array('status' => '0', 'error' => '111111', 'msg' => ''); } // 检查目录名 $dir_name = empty ($_GET ['dir']) ? 'image' : trim($_GET ['dir']); if (empty ($ext_arr [$dir_name])) { $result = array('status' => '0', 'error' => '111111', 'msg' => '目录名不正确'); } // 获得文件扩展名 $temp_arr = explode('.', $file_name); $file_ext = array_pop($temp_arr); $file_ext = trim($file_ext); $file_ext = strtolower($file_ext); // 检查扩展名 if (in_array($file_ext, $ext_arr [$dir_name]) === false) { $result = array('status' => '0', 'error' => '111111', 'msg' => '上传文件扩展名是不允许的扩展名'); } // 创建文件夹 if ($dir_name !== '') { if (!file_exists($save_url)) { mkdir($save_url); } } $new_file_name = date('YmdHis') . '_' . rand(10000, 99999) . '.' . $file_ext; $file_path = $save_url . $new_file_name; if (move_uploaded_file($tmp_name, $file_path) === false) { $result['msg'] = '上传文件失败'; $result = array('status' => '0', 'error' => '111111', 'msg' => '上传文件失败'); } else { $result = array('status' => '1', 'error' => '000000', 'data' => $file_path); } @chmod($file_path, 0644); return $result; } } public function uploadAvatar($code, $avatarImageName) { $url = $this->getApiUrl(__METHOD__); $data = array( "code" => $code, "ip" => $this->params['ip'], "avatar" => !empty($avatarImageName) ? '@' . $avatarImageName : '', ); $result = $this->curlPost($url, $data); return $result; } }
这样,php就已经接收到了来自客户端的 图片上传了,并且已经上传到java后端服务器。
注意:这里有个坑,即php版本大于5.6以后,直接使用 @ 符号无法上传文件了,需要 加上一个安全选项:CURLOPT_SAFE_UPLOAD => false 才可以,或者使用5.6以的高级上传类上传文件:
curl_setopt(ch, CURLOPT_POSTFIELDS, [ 'file' => new CURLFile(realpath('image.png')), ]);
三、java后端接收php上传的图片
package com.xx.c.action; import com.xx.core.pojo.Constants; import com.xx.core.pojo.MicroException; import com.xx.core.pojo.ResponseEntity; import com.xx.core.web.spring.bind.annotation.ClientIP; import com.xx.core.web.spring.bind.annotation.SessionUserId; import com.xx.c.pojo.user.UpFileUrlBean; import com.xx.c.service.user.UserService; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartHttpServletRequest; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.util.Iterator; @Controller @RequestMapping(value = "upload") public class UploadAction { @Resource(name = "userService") private UserService userService; @RequestMapping(value = "/uploadAvatar", method = RequestMethod.POST, produces = "application/json") @ResponseBody public Object uploadAvatar(@RequestParam String code, @ClientIP String addIp, @SessionUserId Long userId, @ModelAttribute UpFileUrlBean bean, HttpServletRequest request) throws MicroException { bean.setAddIp(addIp); bean.setUserId(userId); try { // 转型为MultipartHttpRequest: MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; // 从其中取出一个文件 后续可使用spring 上传文件方法:file.transferTo(destFile); MultipartFile file = null; for (Iterator<String> it = multipartRequest.getFileNames(); it.hasNext();) { file = multipartRequest.getFile((String) it.next()); } userService.uploadAvatar(file, bean); } catch (Exception e) { throw new MicroException(Constants.ErrCode.UPLOAD_AVATAR_TO_SERVER_FAILED, Constants.ErrMsg.UPLOAD_AVATAR_TO_SERVER_FAILED, e); } ResponseEntity ret = new ResponseEntity(Constants.System.OK); return ret; } }
至此,上传流程已经完成了。(当然,后续还可能使用其他上传,比如dubbo调用文件系统上传文件,调用第三方sdk上传到文件服务器。。。, 原理大抵一样,使用字节流进行传输,然后读取出来存储到文件)
一般为app写的接口中,都会涉及到加解密问题,此时,文件不应该算作加密的范畴,而应单独给一个字段。