• Java以form-data(表单)的形式调用第三方接口


    前言

    之前写的调用第三方接口: Java使用原生API调用第三方接口

    但是其中只包含了简单的接口(传递数据为JSON)调用。也就是Content-Type的值是设置成:

     httpCon.setRequestProperty("Content-Type", 
     "application/json;charset=utf-8");
    
    • 1
    • 2

    当第三方接口需要包含文件类型的参数,我们要设置成以表单形式提交,就要那么该属性就应该设置成

    httpCon.setRequestProperty("Content-Type",
     "multipart/form-data; boundary=" + boundary);
    
    
    • 1
    • 2
    • 3

    表示是以表单形式提交数据。请记住这里的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--
     
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    记住以上的形式,这很重要,因为到时候我们需要按照上述形式拼接数据

    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200

    测试信息

    下面是测试信息

    测试代码

    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));
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    测试结果

    {"msg":"以表单类型调用成功","code":"10086"}
    
    • 1

    遇到的问题

    在撰写工具类的时候遇到一些问题,简单整理如下:

    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
    • 2
    • 3
    • 4

    也就是这个方法会将当前请求输出流和输入流的信息获取,然后关闭输入输出流,以流里的数据执行请求,返回的是执行该请求后返回响应的长度

    简单来说,就是这个方法会以当前的数据帮你把这个请求执行,然后返回响应的结果。执行成功就返回-1,否则就是执行错误的长度。跟你当前的没有内容长度没有半毛钱关系。。。

    调用的错误

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

    <html>
    	<body>
    		<h1>Whitelabel Error Pageh1>
    		<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>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    慎用请求输出流flush()方法

    在以往的使用习惯中,我们每次操作完某个流时,在最后都会加上这样的一段代码,用于将缓冲区中的数据推送出去。

    	out.flush();
    
    • 1

    但是这一方法对于请求的输出流来说,却有别的意思,在请求里的输出流中这个方法表示执行请求

    获取流、写入信息,并执行请求,可以理解为以下图解的过程:
    在这里插入图片描述
    其中3中的发送请求,是以请求的输出流fulsh()方法来实现的。

    可以把请求看作一个缓冲区,我们写入的信息会先放到缓冲区中,等执行flush()方法后,才会将信息给到服务器

    未写入标识

    在键于值之间,必须写入两个换行符来表示键值对的界限。也就是工具类中的:

    stringBuilder.append(ROW).append(ROW);
    
    • 1

    调用错误

    如果没有写入或者漏写,则会导致以下报错:

    java.net.SocketException: Connection reset
    	at 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)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    总结

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

  • 相关阅读:
    Opencv中使用Tracker实现物体跟踪
    SpringBoot整合RabbitMQ实现消息延迟队列
    博途PLC面向对象编程之批量调用实例化对象FB
    Qt第二十六章:Nuitka打包教程
    陪你去看 Lodash.js 起步
    【深入浅出玩转FPGA学习14----------测试用例设计2】
    瑞萨RA6系列bootloader分析
    零基础 Chrome 扩展开发指南
    2023.9.23(对这一年过去几个月的总结)
    Pr:导出设置之高级设置及 VR 视频
  • 原文地址:https://blog.csdn.net/qq_44717657/article/details/128019161