Java使用HTTP编程模拟多参数多文件表单信息的请求与处理
本文目的是提供Java环境下模拟浏览器页面提交多参数多文件表单请求以及解析请求的Demo代码。这里用Java提供的HttpURLConnection类做HTTP请求,再原始点可以直接使用socket。使用socket的话,通用性会更好点。
首先要了解一个概念,文件和参数一起上传,HTTP请求头中包含了如下格式。
Content-Type: multipart/form-data; boundary=boundarystr
请求体的数据格式也是有一个标准的,具体如下。
HTTP请求头 --boundarystr Content-Disposition: form-data; name="param1" param1value --boundarystr Content-Disposition: form-data; name="param2" param2value --boundarystr Content-Disposition: form-data; name="param3"; filename="filename1" Content-Type: application/octet-stream 文件数据 --boundarystr Content-Disposition: form-data; name="param4"; filename="filename2" Content-Type: application/octet-stream 文件数据 --boundarystr--
boundary参数一般会取一些类似于UUID这样的值。请求头和请求体之间隔了两个回车,也就是一个"\r\n\r\n"。双横杠boundary和Content-Disposistion描述之间有一个回车("\r\n")。name和filename的参数值需要加双引号。Content描述和数据之间有两个回车("\r\n\r\n")。请求提最后的boundary后尾需要跟上双横杠。例子中,用application/octet-stream通用的描述文件数据内容格式,如果事先知道文件格式,可以根据MIME类型(http://www.w3school.com.cn/media/media_mimeref.asp)填写。好处是解析时候比较方便。
在编写代码时必须严格遵循这个消息格式,包括换行和横杠以及boundary。否则会导致数据无法解析。
下面是一个制作该HTTP消息并进行请求的代码。
/** * 制作多参数多文件消息并进行请求 * @param urlString 目标url * @param mutiFileList 含有文件信息的List<map>,最简单的情况是map中包含文件名称和路径,每个map表示一个文件。 * @param params 普通参数map,key作为参数名称,value作为参数值 * @return */ public String httpUploadMutiFile(String urlString,List<Map<String, Object>> mutiFileList, Map<String, String> params){ String repString = null; InputStream is = null; OutputStream out = null; //定义boundarystr final String BOUNDARYSTR = Long.toHexString(System.currentTimeMillis()); //定义回车换行 final String CRLF = "\r\n"; final String BOUNDARY = "--"+BOUNDARYSTR+CRLF; StringBuilder paramsText = new StringBuilder(); byte[] paraTextByte; try{ //首先制作普通参数信息 if(params != null && params.size() != 0){ for(String key:params.keySet()){ paramsText.append(BOUNDARY); paramsText.append("Content-Disposition: form-data;name=\""+key+"\""+CRLF); paramsText.append("Content-type: text/plain"+CRLF+CRLF); paramsText.append(params.get(key)); paramsText.append(CRLF); } paraTextByte = paramsText.toString().getBytes("UTF-8"); }else{ // paraTextByte = null; } }catch (Exception e){ e.printStackTrace(); paraTextByte = null; } //写入参数部分信息 try{ //先制作请求头 URL url = new URL(urlString); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setDoInput(true); con.setDoOutput(true); con.setRequestMethod("POST"); con.setRequestProperty("connection", "Keep-Alive"); con.setRequestProperty("Charset", "UTF-8"); con.setRequestProperty("Content-type", "multipart/form-data;boundary=" + BOUNDARYSTR); // 获得输出流 out = new DataOutputStream(con.getOutputStream()); // 写入普通参数部分 if(paraTextByte != null && paraTextByte.length > 0){ out.write(paraTextByte); } //写入文件信息 Map<String, Object> fileInfoMap; for(int i = 0; i < mutiFileList.size(); i++){ fileInfoMap = mutiFileList.get(i); //当前文件map存有文件名称(uuid型)和文件的中文名称,以及文件的字节数据 // 如果文件过大,不建议使用该方式,建议传递文件路径再读取写入。 String fileName = (String)fileInfoMap.get("fileName"); String suffix = fileName.substring(fileName.indexOf(".")); String chFileName = fileInfoMap.get("chName")+suffix; StringBuilder sb = new StringBuilder(); sb.append(BOUNDARY); sb.append("Content-Disposition: form-data;name=\""+chFileName+"\";filename=\"" + chFileName + "\""+CRLF); //sb.append("Content-Type:application/octet-stream"+CRLF+CRLF); //文件均是jpg图片类型 sb.append("Content-Type:image/jpg"+CRLF+CRLF); out.write(sb.toString().getBytes()); //写入输出流 byte[] fileData = (byte[])fileInfoMap.get("data"); /** * 如果map里存储的是文件路径,那么可以使用FileInputStream读取文件信息,并写入到OutputStream中。 * 具体就不写了。 */ out.write(fileData); out.write(CRLF.getBytes("UTF-8")); } byte[] foot = ("--" + BOUNDARYSTR + "--"+CRLF).getBytes("UTF-8");// 定义最后数据分隔线 out.write(foot); out.flush(); is = con.getInputStream(); repString = ioTool.getStringFromInputStream(is); }catch (Exception e){ e.printStackTrace(); logger.error("往业务系统写入文件失败:"+e.getMessage()); } return repString; }
下面是数据的解析。
以SpringMVC配置的HTTP接口为例。但具体的解析写法和Spring没什么关系。
在写这篇博客之前,我尝试使用了ServletFileUpload类和request.getParts()两种方法来解析。前一种的好处是有很多便利的Servlet API可以使用。但是,HTTP请求体内容只能解析一次。这意味着,在进行解析前我们不能使用以下方法操作request。https://stackoverflow.com/questions/13881272/servletfileuploadparserequestrequest-returns-an-empty-list
request.getParameter();
request.getParameterMap();
request.getParameterNames();
request.getParameterValues();
request.getReader();
request.getInputStream();
感觉不是很灵活。在servlet3.0后,request增加了getPart()和getParts()方法,利用这些方法处理文件数据表单可能要好一些。
@RequestMapping(value = "uploadFile",method = RequestMethod.POST) public void handleMutiPartForm(HttpServletRequest request, HttpServletResponse response){ //可以使用request.getParameter()获取普通参数数据 String param1 = request.getParameter("param1"); String param2 = request.getParameter("param2"); //获取parts Collection<Part> parts; try{ parts = request.getParts(); }catch (IOException ioe){ parts = null; }catch (ServletException se){ parts = null; } if(parts != null){ //遍历parts //因为所有的参数信息都会在Collection<Part>中体现 //这里只需要获取文件部分 String saveFilePath = "D://FileUpload/"; byte[] bytes = new byte[512]; for(Part part:parts){ //Content-type: image/* 图片类型数据 if(part.getContentType().startsWith("image")){ String fileName = part.getSubmittedFileName(); String paramName = part.getName(); try{ InputStream ins = part.getInputStream(); String filePath = saveFilePath+fileName; File file = new File(filePath); if(!file.exists()){ file.createNewFile(); } FileOutputStream fos = new FileOutputStream(file); while(ins.read(bytes, 0, 512) != -1){ fos.write(bytes); } ins.close(); fos.close(); }catch (IOException ioe){ } } } } try{ response.getWriter().write("返回的信息"); }catch (IOException ioe){ } }
RFC相关介绍:
http://www.faqs.org/rfcs/rfc2388.html
stackoverflow相关问题:
https://stackoverflow.com/questions/2793150/using-java-net-urlconnection-to-fire-and-handle-http-requests/