Java以form-data(表单)的形式调用第三方接口
- 前言
- 本文目标
- 用到的类
- 工具类及测试信息
- 工具类代码
- 测试信息
- 测试代码
- 测试结果
- 遇到的问题
- getContentLength()的滥用
- 调用的错误
- 慎用请求输出流flush()方法
- 未写入标识
- 调用错误
- 总结
前言
之前写的调用第三方接口: Java使用原生API调用第三方接口
但是其中只包含了简单的接口(传递数据为JSON)调用。也就是Content-Type
的值是设置成:
httpCon.setRequestProperty("Content-Type", "application/json;charset=utf-8");
当第三方接口需要包含文件
类型的参数,我们要设置成以表单形式提交,就要那么该属性就应该设置成
httpCon.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
表示是以表单形式提交数据。请记住这里的boundary
,这是自己的设置的上传的信息的边界标识,稍后我会讲到。
本文目标
所以本文只针对以下类似
的情况,这里我以PostMan
的调用方式为示例。
如果在
PostMan
里面能调用成功,那么后续的内容是对你有用的。如果这里你的接口的调用
不通,则代表你可能需要使用其他的方式。不过阅读本文可能会对你有所帮助。
id
为String
、name
为String
。
uploadFiles
为文件集合,可以多选。
其中的uploadFiles
需要自己选择下,默认是Text
文本格式,不然无法选择文件。
多选文件的话是自己在选择文件的时候,按住ctrl
多选。。。真滴奇怪。。。
上述的参数,在请求中的存放形式为:
----------------------------boundaryContent-Disposition: form-data; name="uploadFiles"; filename="1.txt"<1.txt>----------------------------boundaryContent-Disposition: form-data; name="uploadFiles"; filename="2.txt"<2.txt>----------------------------boundaryContent-Disposition: form-data; name="id"1008611----------------------------boundaryContent-Disposition: form-data; name="name"张三----------------------------boundary--
记住以上的形式,这很重要,因为到时候我们需要按照上述形式拼接数据。
boundary
用于标识上传的数据,其中它的值,在设置里由自己设置,上面说过。
我这里是以
----------------------------boundary
开头
----------------------------boundary--
结尾
上述表示的是,我上传了两个文件
:1.txt、2.txt,它们的键是同一个uploadFiles
,两个字符参数
:id =1008611 ,name = 张三。
好了现在,我们就要用Java
来实现上述的PostMan
调用三方接口的方法。
用到的类
这里就不再赘述了,因为跟文头提到文章里用的是一样的。流程也差不多。
工具类及测试信息
工具类代码
package com.http;import java.io.*;import java.net.HttpURLConnection;import java.net.URL;import java.nio.charset.StandardCharsets;import java.util.Map;import java.util.UUID;/** * @author 三文鱼先生 * @title * @description * @date 2022/11/28 **/public class FormDataInterFaceUtils { //前缀 public static String PREFIX = "--"; //换行符 public static String ROW = "\r\n"; //产生一个边界 static String BOUNDARY = UUID.randomUUID().toString().replaceAll("-" , ""); /** * @description 将map里的表单信息 写入到所给的url请求中 并返回执行完请求的结果 * @author 三文鱼先生 * @date 9:38 2022/11/28 * @param url 所给的请求地址 * @param map 参数的键值对映射 使用泛型 文件和字符参数都以对象表示 * @return java.lang.String **/ public static String doPost(String url , Map<String , Object> map) { //构造连接 try { HttpURLConnection httpCon = getPostConnection(url); DataOutputStream outputStream = new DataOutputStream(httpCon.getOutputStream()); //部分的三方请求可能需要携带类似于token这样的信息到请求头里才可以正常访问 //可以使用setRequestProperty(键,值)来设置 for (Map.Entry<String, Object> entry : map.entrySet()) { Object o = entry.getValue(); if(o instanceof String) { //强转 String str = (String) o; //添加键值对 addKeyString(outputStream , entry.getKey() , str); int i = httpCon.getContentLength(); }else { //否则就是文件流 File file = (File) o; //添加文件 addFile(outputStream , entry.getKey() , file); } } //写入边界结束符 outputStream.write((PREFIX + BOUNDARY + PREFIX + ROW).getBytes(StandardCharsets.UTF_8)); outputStream.flush();//可以理解为发送请求 //获取返回结果 -- 默认为字符串 return getInvokeResult(httpCon); }catch (Exception e) { e.printStackTrace(); } return null; } /** * @description 写入键值对 示例为写入:name-张三 * @author 三文鱼先生 * @date 9:40 2022/11/28 * @param out 请求的输出流 * @param key 字符的键 * @param str 字符的值 * @return void **/ public static void addKeyString(DataOutputStream out, String key , String str) { try{ StringBuilder stringBuilder = new StringBuilder(); //先写入数据的边界标识 stringBuilder.append(PREFIX).append(BOUNDARY).append(ROW); stringBuilder.append("Content-Disposition: form-data; name=\"") .append(key).append("\"").append(ROW); //数据类型及编码 stringBuilder.append("Content-Type: text/plain; charset=UTF-8"); //Todo 连续两个换行符 表示文字的键信息部分结束 stringBuilder.append(ROW).append(ROW); //写入信息的值 stringBuilder.append(str); //表示数据的结尾 stringBuilder.append(ROW); //写入数据 键值对一起写入 out.write(stringBuilder.toString().getBytes(StandardCharsets.UTF_8)); } catch (IOException e) { e.printStackTrace(); } } /** * @description 向输出流中写入文件 示例为: a.txt - 对应的File对象 * @author 三文鱼先生 * @date 9:42 2022/11/28 * @param out 请求的输出流 * @param name 文件的键 * @param file 具体文件 * @return void **/ public static void addFile(DataOutputStream out , String name , File file) throws IOException { if(!file.exists()) System.out.println("文件不存在"); StringBuilder stringBuilder = new StringBuilder(); //标识这是一段边界内的数据 stringBuilder.append(PREFIX).append(BOUNDARY).append(ROW); //拼接文件名称 stringBuilder.append("Content-Disposition: form-data; name=\""); stringBuilder.append(name).append("\"; ")//文件的键 .append("filename=\"")//文件名称 .append(file.getName()) .append("\"") .append(ROW) //设置内容类型为流及编码为UTF-8 .append("Content-Type: application/octet-stream; charset=UTF-8"); //Todo 这两个换行很重要 标识文件信息的结束 后面的信息为文件流 stringBuilder.append(ROW).append(ROW); //写入文件的信息到输出流 out.write(stringBuilder.toString().getBytes(StandardCharsets.UTF_8)); //这里开始写入文件流 try( DataInputStream fileIn = new DataInputStream(new FileInputStream(file)) ) { //一次读取1M byte[] bytes = new byte[1024*1024]; int length = 0; while ((length = fileIn.read(bytes)) != -1) { out.write(bytes , 0 , length); } } catch (IOException e) { e.printStackTrace(); } //Todo 文件流写完之后 需要换行表示结束 out.write(ROW.getBytes(StandardCharsets.UTF_8)); } /** * @description 以所给的url获取一个Post类型的连接 * @author 三文鱼先生 * @date 9:43 2022/11/28 * @param url 请求的地址 * @return java.net.HttpURLConnection **/ public static HttpURLConnection getPostConnection(String url) { HttpURLConnection httpCon = null; try { URL urlCon = new URL(url); //在这里获取的就是一个已经打开的连接了 httpCon = (HttpURLConnection) urlCon.openConnection(); //请求方式为Post httpCon.setRequestMethod("POST"); //设置通用的请求属性 httpCon.setRequestProperty("accept", "*/*"); httpCon.setRequestProperty("connection", "Keep-Alive"); //设置浏览器代理 httpCon.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"); //这里要设置为表单类型 httpCon.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + BOUNDARY); //是否可读写 httpCon.setDoOutput(true); httpCon.setDoInput(true); //禁用缓存 httpCon.setUseCaches(false); //设置连接超时60s httpCon.setConnectTimeout(60000); //设置读取响应超时60s httpCon.setReadTimeout(60000); } catch (IOException e ) { e.printStackTrace(); } return httpCon; } /** * @description 从请求中获取请求的执行返回 * @author 三文鱼先生 * @date 9:44 2022/11/28 * @param httpCon 请求的连接 * @return java.lang.String **/ public static String getInvokeResult(HttpURLConnection httpCon) { try( BufferedReader reader = new BufferedReader(new InputStreamReader(httpCon.getInputStream())) ) { return reader.readLine(); } catch (IOException e) { e.printStackTrace(); } return null; }}
测试信息
下面是测试信息
测试代码
public class StrTest { public static void main(String[] args) { String url = "http://localhost:800/testInterface"; Map<String , Object> map = new HashMap<>(); map.put("id" ,"1008611"); map.put("name" ,"张三"); File file = new File("D:\\file\\1.txt"); map.put("uploadFiles" , file); File file1 = new File("D:\\file\\2.txt"); map.put("uploadFiles" , file1); System.out.println(FormDataInterFaceUtils.doPost(url, map)); }}
测试结果
{"msg":"以表单类型调用成功","code":"10086"}
遇到的问题
在撰写工具类的时候遇到一些问题,简单整理如下:
getContentLength()的滥用
方法描述是这样的:Returns the value of the content-length header field.
翻译过来就是:返回内容长度头字段的值 。
我以为是以下请求信息中的Content-Length
本来是想写入一个文件就调用该方法看看内容长度的,结果疯狂报错。。。后面经过一系列调试,才知道这方法那么麻烦。。。
当我们调用该方法的时候,它会以当前的数据发送请求,然后关闭输出流。
就是假如你要调用的接口有三个参数
,然后你每写入一个参数调用一次该方法
,那么就相当于你每次都以一个参数调用该接口
。但你并不能执行三次
,因为第一次
执行以后,输出流
就已经关闭
了,你也就无法再向其中写入信息了。
进入debug中可以看到当执行到HttpURLConnection.class
里的writeRequests()
方法中以下代码时候:
synchronized(this.poster) { this.poster.close();//这一行就是罪魁祸首 this.requests.set("Content-Length", String.valueOf(this.poster.size())); }
也就是这个方法会将当前请求输出流和输入流的信息获取
,然后关闭
输入输出流,以流里的数据执行请求,返回的是执行该请求后返回响应的长度。
简单来说,就是这个方法会以当前的数据帮你把这个请求执行,然后返回响应的结果
。执行成功就返回-1
,否则就是执行错误的长度
。跟你当前的没有内容长度没有半毛钱关系。。。
调用的错误
如果在数据未全部放进去的时候,执行该方法,则会导致调用失败,其返回为白页:
<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div>There was an unexpected error (type=null, status=null).</div></body></html>
慎用请求输出流flush()方法
在以往的使用习惯中,我们每次操作完某个流时,在最后都会加上这样的一段代码,用于将缓冲区
中的数据推送出去。
out.flush();
但是这一方法对于请求的输出流
来说,却有别的意思,在请求里的输出流
中这个方法表示执行请求
。
获取流、写入信息,并执行请求
,可以理解为以下图解的过程:
其中3中的发送请求,是以请求的输出流
的fulsh()
方法来实现的。
可以把请求
看作一个缓冲区
,我们写入的信息会先放到缓冲区中
,等执行flush()
方法后,才会将信息给到服务器
。
未写入标识
在键于值之间,必须写入两个换行符
来表示键值对的界限。也就是工具类中的:
stringBuilder.append(ROW).append(ROW);
调用错误
如果没有写入或者漏写,则会导致以下报错:
java.net.SocketException: Connection resetat java.net.SocketInputStream.read(SocketInputStream.java:209)at java.net.SocketInputStream.read(SocketInputStream.java:141)at java.io.BufferedInputStream.fill(BufferedInputStream.java:246)at java.io.BufferedInputStream.read1(BufferedInputStream.java:286)at java.io.BufferedInputStream.read(BufferedInputStream.java:345)at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:704)at sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:647)at sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:675)at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1536)at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1441)
总结
总体来说还是不难的,主要是格式
找了好久。还有就是键值的分隔符
那里浪费了比较久的时间。最重要的一点是,注释上写的东西与我理解的不一样
。。。。