Java以form-data(表单)的形式调用第三方接口
创始人
2024-02-22 18:40:08
0

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的调用方式为示例。

  1. 如果在PostMan里面能调用成功,那么后续的内容是对你有用的。

  2. 如果这里你的接口的调用不通,则代表你可能需要使用其他的方式。不过阅读本文可能会对你有所帮助。

在这里插入图片描述
idStringnameString
uploadFiles为文件集合,可以多选。
其中的uploadFiles需要自己选择下,默认是Text文本格式,不然无法选择文件。
在这里插入图片描述
多选文件的话是自己在选择文件的时候,按住ctrl多选。。。真滴奇怪。。。

上述的参数,在请求中的存放形式为:

----------------------------boundary
Content-Disposition: form-data; name="uploadFiles"; filename="1.txt"
<1.txt>
----------------------------boundary
Content-Disposition: form-data; name="uploadFiles"; filename="2.txt"
<2.txt>
----------------------------boundary
Content-Disposition: form-data; name="id"
1008611
----------------------------boundary
Content-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 map) {//构造连接try {HttpURLConnection httpCon = getPostConnection(url);DataOutputStream outputStream = new DataOutputStream(httpCon.getOutputStream());//部分的三方请求可能需要携带类似于token这样的信息到请求头里才可以正常访问//可以使用setRequestProperty(键,值)来设置for (Map.Entry 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))) {//一次读取1Mbyte[] 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();//请求方式为PosthttpCon.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);//设置连接超时60shttpCon.setConnectTimeout(60000);//设置读取响应超时60shttpCon.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 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,否则就是执行错误的长度。跟你当前的没有内容长度没有半毛钱关系。。。

调用的错误

如果在数据未全部放进去的时候,执行该方法,则会导致调用失败,其返回为白页:
在这里插入图片描述

Whitelabel Error Page

This application has no explicit mapping for /error, so you are seeing this as a fallback.

There was an unexpected error (type=null, status=null).

慎用请求输出流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)

总结

总体来说还是不难的,主要是格式找了好久。还有就是键值的分隔符那里浪费了比较久的时间。最重要的一点是,注释上写的东西与我理解的不一样。。。。

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...
有效的括号 一、题目 给定一个只包括 '(',')','{','}'...
【Ctfer训练计划】——(三... 作者名:Demo不是emo  主页面链接:主页传送门 创作初心ÿ...