Java调用http接口的几种方式(HttpURLConnection、OKHttp、HttpClient、RestTemplate)

news2024/11/19 7:46:08

Java作为后端语言是开发接口实现功能供客户端调用接口,这些客户端中最主要是本项目的前端;但有时候也需要Java请求其他的接口,比如需要长连接转短链接(请求百度的一个接口可以实现)、获取三方OSS签名、微信小程序签名、甚至是本公司其他团队项目的接口等等都需要Java调用其他的接口,这时候就需要用Java发起相应的请求,这里以发起http请求为例介绍几种方式。

本文示例涉及的service(被调用方)和client(调用方)的接口代码如下(均以spring boot搭建的项目):

service

代码:

package com.http.test.service.controller;

import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

/**
 * 服务端controller即被调用方
 */
@RestController
@RequestMapping("/service")
public class ServiceController {

    /**
     * 用于模拟数据库,将数据保存在Map中
     */
    private Map<String,String> data = new HashMap<>();

    /**
     * get请求根据key获取数据
     * @param key
     * @return
     */
    @GetMapping("/get/{key}")
    public Map<String,String> get(@PathVariable("key") String key){
        Map<String,String> map = new HashMap<>();
        if (!data.containsKey(key)){
            throw new RuntimeException("数据不存在!");
        }
        map.put(key,data.get(key));
        return map;
    }

    /**
     * get请求获取所有数据
     * @return
     */
    @GetMapping("/list")
    public Map<String,String> list(){
        return data;
    }

    /**
     * post请求根据key和value保存数据
     * @param map
     * @return
     */
    @PostMapping("/set")
    public String set(@RequestBody Map<String,String> map){
        data.put(map.get("key"),map.get("value"));
        return "保存成功!";
    }

    /**
     * 文件上传
     * @param multipartFile 接收上传的文件
     * @param upLoadDTO 接收其他参数,注意:接收其他参数时这个变量不能用 @RequestBody 修饰,否则会改变接口支持的 Content-Type 会导致报错,并且这个参数只能是定义为类对象,在类中定义好每个参数
     *                  的名称,使用Map之类的不明确指定参数名称的对象会导致使用httpclient调用接口时无法接收到对应的参数
     * @return
     */
    @PostMapping("/upload")
    public String upload(@RequestParam("fileName") MultipartFile multipartFile, UpLoadDTO upLoadDTO){
        System.out.println("文件名称:"+multipartFile.getOriginalFilename());
        System.out.println(upLoadDTO);
        return "保存成功!";
    }


}

文件上传参数类:

package com.http.test.service.dto;

public class UpLoadDTO {

    private String desc;

    private String descCn;

    public String getDescCn() {
        return descCn;
    }

    public void setDescCn(String descCn) {
        this.descCn = descCn;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    @Override
    public String toString() {
        return "UpLoadDTO{" +
                "desc='" + desc + '\'' +
                ", descCn='" + descCn + '\'' +
                '}';
    }
}

配置信息:

server:
  port: 6666

client

代码:

package com.http.test.client.controller;

import org.springframework.web.bind.annotation.*;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Map;

/**
 * 客户端controller即调用方
 */
@RestController
@RequestMapping("/client")
public class ClientController {

    /**
     * 服务端接口地址前缀
     */
    private final String ADD_PREFIX = "http://127.0.0.1:6666/service/";

    /**
     * get请求根据key获取数据,调用service接口获取数据
     *
     * @param key
     * @return
     */
    @GetMapping("/get/{key}")
    public Object get(@PathVariable("key") String key) {
        String result = "默认数据,兜底!";
        return result;
    }

    /**
     * get请求获取所有数据,调用service接口获取数据
     *
     * @return
     */
    @GetMapping("/list")
    public Object list() {
        String result = "默认数据,兜底!";
        return result;
    }

    /**
     * post请求根据key和value保存数据,调用service接口保存数据
     *
     * @param map
     * @return
     */
    @PostMapping("/set")
    public Object set(@RequestBody Map<String, String> map) {
        String result = "默认数据,兜底!";
        return result;
    }

}

配置信息:

server:
  port: 7777

导入jar包:

        <!-- 做json字符串转换处理,比如调用post接口时入参必须是json字符串 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-json</artifactId>
            <version>5.7.12</version>
        </dependency>

关于keepAliveTimeout属性:首先从http response header中获取,如果没有取到,则默认为5秒;同时sun.net.www.http.KeepAliveCache.java中有一个线程,每5秒执行一次检查缓存的连接的空闲时间是否超过keepAliveTimeout,如果超过则关闭连接,从KeepAliveCache中获取缓存的连接时也会检查获取到的连接的空闲时间是否超过keepAliveTimeout,如果超过则关闭连接,并且获取下一个连接,再执行以上检查,直到获取到空闲时间在keepAliveTimeout以内的缓存连接为止。

MIME type 介绍:

MIME (Multipurpose Internet Mail Extensions) 原本指 多用途互联网邮件扩展类型,该参数是用来描述消息内容类型的因特网标准即通过设置此参数告诉对方发送的数据类型。
组成:

信息头含义例子
MIME-VersionMIME版本1.0
Content-Type内容类型application/x-www-form-urlencoded、application/json
Content-Transfer-Encoding编码格式8bit、binary
Content-Disposition内容排列方式上传文件时:Content-Disposition: form-data; name=“fileName”; filename=“C:Users\lenovo\Desktop\a.html”;下载文件时需要设置:Content-Disposition: attachment; filename=URLEncoder.encode(“xx.zip”,“UTF-8”)

可选值参考
比如:png格式的应答对于tomcat来说就是通过后缀在conf/web.xm文件中找到对应的mime-mapping的值设置响应头Content-Type的值为mime-type的值。

网页form表单nctype可用的MIME类型 (Content-Type类型):
1.application/x-www-form-urlencoded
2.multipart/form-data
3.text/plain

post请求常见的 Content-Type 值:
1.发送application/x-www-form-urlencoded类型的请求 form表单类型的post请求(接口不加 @RequestBody 注解或将 @RequestBody 注解换成 @RequestParam 注解)
2.发送application/json类型的请求 json类型的post请求(接口加 @RequestBody 注解)
3. 发送multipart/form-data类型上传文件的请求 上传文件类型的post请求

通过设置MIME type的值告诉对方发送的数据类型,主要是对于 Content-Type 的设置,常见form表单、视频、图片、json等对应 Content-Type 的设置。

一、HttpURLConnection

HttpURLConnection 是 Java 提供的发起 HTTP 请求的基础类库,提供了 HTTP 请求的基本功能,不过封装的比较少,在使用时很多内容都需要自己设置,也需要自己处理请求流和响应流;不需要导入jar包;所以简单场景可以使用HttpURLConnection处理http请求,复杂场景建议使用其他封装的三方库。

工具类:

package com.http.test.client.util;

import cn.hutool.json.JSONUtil;
import org.springframework.http.HttpMethod;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

/**
 * HttpURLConnection工具类
 */
public class HttpUrlConnectionUtil {

    /**
     * 将流数据转换为字符串工具方法
     * @param is
     * @return
     */
    public static String parseIstoString(InputStream is) {
        //用于组装字符串数据
        StringBuilder stringBuilder = new StringBuilder();
        //将流数据转为BufferedReader对象读取数据
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is));
        String line = null;
        try {
            while (null != (line = bufferedReader.readLine())) {
                stringBuilder.append(line);
            }
            //关闭流
            bufferedReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return stringBuilder.toString();
    }


    /**
     * get请求
     * @param urlParam  接口地址
     * @return
     */
    public static String get(String urlParam){
        String result = null;
        try {
            //创建url
            URL url = new URL(urlParam);
            //打开连接(返回值是URLConnection强转为子类HttpURLConnection方便操作)
            HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
            //设置请求方法类型,必须是大写,否则java.net.ProtocolException: Invalid HTTP method: get,也可以使用HttpMethod.GET.name()枚举类,如果有调用getOutputStream()方法那么会自动把GET请求改为POST请求
            httpURLConnection.setRequestMethod("GET");
            //设置建立连接超时时长,0表示不超时,单位毫秒
            httpURLConnection.setConnectTimeout(3000);
            //设置读取数据超时时长即获取接口返回数据超时时长(包含被调用接口的处理业务时长和数据传输回来的时长,网络没问题的情况下基本都是接口处理时长),0表示不超时,单位毫秒
            httpURLConnection.setReadTimeout(5000);
            //开启缓存,默认为 true
            httpURLConnection.setUseCaches(true);
            //设置是否可以向HttpURLConnection输出数据
            httpURLConnection.setDoOutput(false);
            //设置是否可以从HttpUrlConnection读取数据(比如读取应答码、消息、数据等)默认true,如果为false读取数据则会java.net.ProtocolException: Cannot read from URLConnection if doInput=false (call setDoInput(true))
            httpURLConnection.setDoInput(true);
            //设置此 HttpURLConnection 实例是否支持重定向,默认为 setFollowRedirects 方法设置的值,HttpURLConnection.setFollowRedirects(false),是静态方法是针对所有请求的设置而不是单个请求的设置
            httpURLConnection.setInstanceFollowRedirects(false);
            //建立连接,只是建立一个连接,并不会发送数据,显示建立连接,一般不会写这句代码
            httpURLConnection.connect();
            //读取应答状态码
            int responseCode = httpURLConnection.getResponseCode();
            System.out.println("get请求应答状态码:"+responseCode);
            //读取应答消息
            String responseMessage = httpURLConnection.getResponseMessage();
            System.out.println("get请求应答消息:"+responseMessage);
            //获取输入流,会隐式建立连接,因此就不需要connect()显示建立连接
            InputStream inputStream = httpURLConnection.getInputStream();
            //将返回的数据流转为字符串数据
            result = parseIstoString(inputStream);
            //关闭流
            inputStream.close();
            //断开连接释放资源
            httpURLConnection.disconnect();
            System.out.println("HttpURLConnection GET 接口请求结果:"+result);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * post请求
     * @param urlParam  接口地址
     * @param body  请求参数
     * @return
     */
    public static String post(String urlParam,Object body){
        String result = null;
        try {
            //创建url
            URL url = new URL(urlParam);
            //打开连接(返回值是URLConnection强转为子类HttpURLConnection方便操作)
            HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
            //设置请求方法类型,必须是大写,否则java.net.ProtocolException: Invalid HTTP method: get,也可以使用HttpMethod.POST.name()枚举类
            httpURLConnection.setRequestMethod(HttpMethod.POST.name());
            //设置建立连接超时时长,0表示不超时,单位毫秒
            httpURLConnection.setConnectTimeout(3000);
            //设置读取数据超时时长即获取接口返回数据超时时长(包含被调用接口的处理业务时长和数据传输回来的时长,网络没问题的情况下基本都是接口处理时长),0表示不超时,单位毫秒
            httpURLConnection.setReadTimeout(5000);
            //开启缓存,默认为 true
            httpURLConnection.setUseCaches(true);
            //设置是否可以向HttpURLConnection输出数据,对于post请求,参数要放在 http 正文body内,因此需要设为true,默认为false报错java.net.ProtocolException: cannot write to a URLConnection if doOutput=false - call setDoOutput(true)
            httpURLConnection.setDoOutput(true);
            //设置是否可以从HttpUrlConnection读取数据(比如读取应答码、消息、数据等)默认true,如果为false读取数据则会java.net.ProtocolException: Cannot read from URLConnection if doInput=false (call setDoInput(true))
            httpURLConnection.setDoInput(true);
            //设置此 HttpURLConnection 实例是否支持重定向,默认为 setFollowRedirects 方法设置的值,HttpURLConnection.setFollowRedirects(false),是静态方法是针对所有请求的设置而不是单个请求的设置
            httpURLConnection.setInstanceFollowRedirects(false);
            //设置header参数,请求数据类型为json
            httpURLConnection.setRequestProperty("Content-Type","application/json");
            //保持长连接,方便复用连接
            httpURLConnection.setRequestProperty("Connection","Keep-Alive");
            //获取输出流,会隐式建立连接,因此就不需要connect()显示建立连接,往body中写入数据
            OutputStream outputStream = httpURLConnection.getOutputStream();
            System.out.println("请求post接口参数:"+JSONUtil.toJsonStr(body));
            //写入参数时需要字符串的字节数据,由于service的接口要求json格式的数据,因此需要转为json字符串
            outputStream.write(JSONUtil.toJsonStr(body).getBytes());
            //刷掉缓存,防止数据未写入完毕
            outputStream.flush();
            //关闭流
            outputStream.close();
            //建立连接,只是建立一个连接,并不会发送数据,显示建立连接,一般不会写这句代码
            httpURLConnection.connect();
            //读取应答状态码
            int responseCode = httpURLConnection.getResponseCode();
            System.out.println("get请求应答状态码:"+responseCode);
            //读取应答消息
            String responseMessage = httpURLConnection.getResponseMessage();
            System.out.println("get请求应答消息:"+responseMessage);
            //获取输入流,会隐式建立连接,因此就不需要connect()显示建立连接
            InputStream inputStream = httpURLConnection.getInputStream();
            //将返回的数据流转为字符串数据
            result = parseIstoString(inputStream);
            //关闭流
            inputStream.close();
            //断开连接释放资源
            httpURLConnection.disconnect();
            System.out.println("HttpURLConnection GET 接口请求结果:"+result);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

}


client controller修改后:

package com.http.test.client.controller;

import com.http.test.client.util.HttpUrlConnectionUtil;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
 * 客户端controller即调用方
 */
@RestController
@RequestMapping("/client")
public class ClientController {

    /**
     * 服务端接口地址前缀
     */
    private final String ADD_PREFIX = "http://127.0.0.1:6666/service/";

    /**
     * get请求根据key获取数据,调用service接口获取数据
     *
     * @param key
     * @return
     */
    @GetMapping("/get/{key}")
    public Object get(@PathVariable("key") String key) {
        String result = "默认数据,兜底!";
        String temp = HttpUrlConnectionUtil.get(ADD_PREFIX + "get/" + key);
        result = temp == null ? result : temp;
        return result;
    }

    /**
     * get请求获取所有数据,调用service接口获取数据
     *
     * @return
     */
    @GetMapping("/list")
    public Object list() {
        String result = "默认数据,兜底!";
        String temp = HttpUrlConnectionUtil.get(ADD_PREFIX + "list");
        result = temp == null ? result : temp;
        return result;
    }

    /**
     * post请求根据key和value保存数据,调用service接口保存数据
     *
     * @param map
     * @return
     */
    @PostMapping("/set")
    public Object set(@RequestBody Map<String, String> map) {
        String result = "默认数据,兜底!";
        String temp = HttpUrlConnectionUtil.post(ADD_PREFIX + "set", map);
        result = temp == null ? result : temp;
        return result;
    }

}

注意:
a、对于post请求需要在body中写入数据因此需要设置setDoOutput(true)
b、对于任何请求一般都会调用 getInputStream() 方法读取应答数据因此需要setDoInput(true),默认为true可以不用设置
c、在调用connect()、getInputStream()、getOutputStream()这三个显示或隐式建立连接的方法之前需要设置好HttpURLConnection对象的参数
d、如果有调用 getOutputStream() 方法,那么不论是否设置为POST请求都会把请求方法改为 POST
e、connect() 方法显示建立连接、 getOutputStream() 方法隐式建立连接并写入数据关闭流后,也不会发送数据,只有调用 getInputStream()、getResponseCode()等方法才会真正的发送数据(因此 getInputStream() 需要在 getOutputStream() 写入完毕数据之后调用)
f、不设置超时可能会导致在请求接口的地方卡住(比如不设置setReadTimeout,那么调用的接口如果需要处理很长的时间或网络不佳,导致很长时间才会接收到应答数据的话,那么业务流程就会卡在这个地方等待调用的接口返回数据)
g、JDK8中的HttpURLConnection默认设置了Connection为Keep-Alive,底层socket在Keep-Alive超时之前不会关闭,使用后会放入缓存,后续同host:port的请求就可以复用这个socket;如果Connection为close则关闭getInputStream()之后就会断开连接
h、如果调用了disconnect()关闭连接,那么Connection为Keep-Alive保持socket不断开方便复用则不会生效

HttpURLConnection参数受system properties影响(默认值受以下参数影响,如果单独设置则按照单独设置为准):
http.keepAlive=boolean(默认值:true),是否启用keepAlive,如果设置为false,则HttpURLConnection不会缓存,使用完后会关闭socket连接。
http.maxConnections=int(默认值:5),表示每个目标host缓存socket连接的最大数。

被调用接口没有开启(比如被调用接口所在项目没有启动):
在这里插入图片描述
如果被调用方接口报错调用方日志如下:
在这里插入图片描述
被调用方报错日志:
在这里插入图片描述

接口测试:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二、OKHttp

OkHttp 是一套处理 HTTP 网络请求的依赖库,由 Square 公司设计研发并开源,目前可以在 Java 和 Kotlin 中使用;对于 Android App 来说,OkHttp 现在几乎已经占据了所有的网络请求操作,RetroFit + OkHttp 实现网络请求似乎成了一种标配。因此它也是每一个 Android 开发工程师的必备技能,由于Android也是Java编写,所以Java也可以使用OKHttp;支持连接池处理、支持SPDY和Http2.0、可以扩展拦截器处理请求和应答;但调用api及配置较为复杂、主要用于Android开发的http调用。
jar包:

        <!-- okhttp工具包 -->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>3.14.9</version>
        </dependency>

工具类:

package com.http.test.client.util;

import cn.hutool.json.JSONUtil;
import okhttp3.*;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * OKHttp工具类
 */
public class OKHttpUtil {

    /**
     * 连接池具体配置,最大连接数、连接存活时间及时间单位
     */
    private static ConnectionPool connectionPool = new ConnectionPool(20,5,TimeUnit.MINUTES);


    //创建一个OkHttpClient对象并设置一些参数
    private static OkHttpClient OKHTTPCLIENT = new OkHttpClient.Builder().
            //连接超时
            connectTimeout(3000, TimeUnit.MILLISECONDS).
            //读取超时
            readTimeout(3000,TimeUnit.MILLISECONDS).
            //写入超时
            writeTimeout(3000,TimeUnit.MILLISECONDS).
            //连接池
            connectionPool(connectionPool).
            build();


    /**
     * get请求
     * @param url  接口地址
     * @return
     */
    public static String get(String url) {
        //这里不能直接使用一个String类型的变量 result 来接收 enqueue 发送请求之后的结果信息,因为 onFailure 或 onResponse 是在 new Callback() 中重写的
        //如果在 onFailure 或 onResponse 中操作属于内部类操作外部的局部变量,那么可能存在外部的局部变量随着外部类的生命周期结束而销毁但内部类的生命周期还未结束,所以
        //如果需要在 onFailure 或 onResponse 中操作result变量则需要 final 修饰 result 变量,但final修饰后就是一个字符串常量了,没办法进行重新赋值,所以这里采用
        //List<String>类型接收返回值
        List<String> result = new ArrayList<>();
        //创建一个请求信息对象并加入请求参数
        Request request = new Request.Builder().get().url(url).build();
        //创建一个发送请求的对象
        Call call = OKHTTPCLIENT.newCall(request);
        //初始化计数器,计数为1
        CountDownLatch countDownLatch = new CountDownLatch(1);
        //发送请求及处理请求,enqueue异步执行,注意这种方式会采用异步线程处理请求,那么可能存在主线程结束但异步请求线程还未处理完请求,比如这里需要将请求结果写入result集合中,最终返回result.get(0)时
        //可能会因为异步线程还未处理完毕,那么result中还未添加请求结果,导致result.get(0)下标越界错误,因此这里需要result.get(0)时请求必须处理完毕,所以采用CountDownLatch进行阻塞
        //异步请求处理完成后扣减计数为0之后,才执行result.get(0)
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                System.out.println("发生错误:"+e.getMessage());
                result.add("发生错误:"+e.getMessage());
                //扣减计数1
                countDownLatch.countDown();
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                System.out.println("请求成功!"+response.isSuccessful()+" 状态码:"+response.code()+" 数据:"+response.body().toString());
                result.add(response.body().string());
                //扣减计数1
                countDownLatch.countDown();
            }
        });
        try {
            //阻塞,计数为0之后阻塞会被打断继续执行后续逻辑
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return result.get(0);
    }


    /**
     * post请求
     * @param url  接口地址
     * @param body  请求参数
     * @return
     */
    public static String post(String url,Object body){
        //构建请求体数据
        String jsonBody = JSONUtil.toJsonStr(body);
        RequestBody requestBody = RequestBody.create(MediaType.parse("application/json;charset=utf-8"), jsonBody);
        //接收处理请求结果
        String result = null;
        //创建一个请求信息对象并加入请求参数,设置为post请求
        Request request = new Request.Builder().post(requestBody).url(url).build();
        //创建一个发送请求的对象
        Call call = OKHTTPCLIENT.newCall(request);
        try {
            //发送请求及处理请求,execute同步执行
            Response execute = call.execute();
            int code = execute.code();
            result = execute.body().string();
            System.out.println("请求结果,状态码:"+ code +" 数据:"+result);
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("出现异常:"+e.getMessage());
        }
        return result;
    }

}

client controller修改后:

package com.http.test.client.controller;

import com.http.test.client.util.OKHttpUtil;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
 * 客户端controller即调用方
 */
@RestController
@RequestMapping("/client")
public class ClientController {

    /**
     * 服务端接口地址前缀
     */
    private final String ADD_PREFIX = "http://127.0.0.1:6666/service/";

    /**
     * get请求根据key获取数据,调用service接口获取数据
     *
     * @param key
     * @return
     */
    @GetMapping("/get/{key}")
    public Object get(@PathVariable("key") String key) {
        String result = OKHttpUtil.get(ADD_PREFIX + "get/" + key);
        return result;
    }

    /**
     * get请求获取所有数据,调用service接口获取数据
     *
     * @return
     */
    @GetMapping("/list")
    public Object list() {
        String result = OKHttpUtil.get(ADD_PREFIX + "list");
        return result;
    }

    /**
     * post请求根据key和value保存数据,调用service接口保存数据
     *
     * @param map
     * @return
     */
    @PostMapping("/set")
    public Object set(@RequestBody Map<String, String> map) {
        String result = OKHttpUtil.post(ADD_PREFIX + "set", map);
        return result;
    }

}

接口测试:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

三、HttpClient

由于HttpClient是进行封装的框架,使用起来更加便捷;所以在一些复杂请求处理时可使用HttpClient,在一些简单的场景下可使用HttpURLConnection,可以理解为HttpClient是HttpURLConnection的增强,在Java中HttpClient使用较多。

导入包:

        <!-- httpclient工具包,当前包已包含 httpmime 包,因此文件上传不需要导入 httpmime 包,有些老版本可能不包含这个包需要导入 -->
        <dependency>
            <groupId>org.apache.httpcomponents.client5</groupId>
            <artifactId>httpclient5</artifactId>
            <version>5.2.1</version>
        </dependency>

        <!-- httpclient上传文件工具包,如果要使用httpclient上传文件需要引入这个包 -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpmime</artifactId>
            <version>4.5.13</version>
        </dependency>

工具类:

package com.http.test.client.util;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONUtil;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
import org.apache.hc.client5.http.entity.mime.FileBody;
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
import org.apache.hc.client5.http.entity.mime.StringBody;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.*;
import org.apache.hc.core5.http.config.Registry;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.apache.hc.core5.pool.PoolStats;
import org.apache.hc.core5.ssl.SSLContextBuilder;
import org.apache.hc.core5.ssl.TrustStrategy;

import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * HttpClient工具类,这里基于httpclient5做的配置
 */
public class HttpClientUtil {

    private static CloseableHttpClient CLOSEABLEHTTPCLIENT;

    private static PoolingHttpClientConnectionManager POOLINGHTTPCLIENTCONNECTIONMANAGER;

    static {
        //设置http和https的支持,主要是绕过https不安全的验证
        Registry<ConnectionSocketFactory> BUILD = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.INSTANCE)
                //设置自定义的 ConnectionSocketFactory 绕过https的不安全验证,如果https的接口证书由三方机构颁发且未过期则认为是安全的,如果过期或自己生成的ssl证书则是不安全的
                //那么正常请求时会报错,所以这里设置跳过这个验证
                .register("https",getSSLConnectionSocketFactory())
                .build();
        POOLINGHTTPCLIENTCONNECTIONMANAGER = new PoolingHttpClientConnectionManager(BUILD);
        //设置最大连接数
        POOLINGHTTPCLIENTCONNECTIONMANAGER.setMaxTotal(100);
        //设置每个路由默认最大连接数(同一个域名认为是一个路由,即同一个域名可以保留的最大连接数,空闲连接和已使用连接一起计算数量)
        POOLINGHTTPCLIENTCONNECTIONMANAGER.setDefaultMaxPerRoute(10);
        //创建HttpClientBuilder对象
        HttpClientBuilder custom = HttpClients.custom();
        //设置自定义的 PoolingHttpClientConnectionManager,主要是连接池的信息和对https验证绕过的设置及http的设置
        HttpClientBuilder httpClientBuilder = custom.setConnectionManager(POOLINGHTTPCLIENTCONNECTIONMANAGER);
        //创建配置信息
        RequestConfig build = RequestConfig.custom()
                //设置连接超时,即和目标url建立连接的超时时间
                .setConnectTimeout(3000, TimeUnit.MILLISECONDS)
                //设置获取连接超时,即从连接池中获取一个连接的等待超时时间
                .setConnectionRequestTimeout(3000,TimeUnit.MILLISECONDS)
                //设置应答超时,即目标url处理请求并接收应答数据的超时时间,httpclient是socketTimeout,超时报错:java.net.SocketTimeoutException: Read timed out
               .setResponseTimeout(3000,TimeUnit.MILLISECONDS)
                .build();
        //设置默认配置信息
        httpClientBuilder.setDefaultRequestConfig(build);
        List<Header> headers = new ArrayList<>();
        //设置用户代理信息为浏览器发送的请求,防止请求的接口做了非浏览器请求拦截
        Header header = new BasicHeader("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36");
        headers.add(header);
        //设置默认header信息
        httpClientBuilder.setDefaultHeaders(headers);
        //CloseableHttpClient是线程安全的,创建之后可以复用
        CLOSEABLEHTTPCLIENT = httpClientBuilder.build();
    }


    /**
     * 访问图片地址,下载保存图片
     * @param url 图片地址
     */
    public static void getAndSaveImage(String url){
        //创建get请求
        HttpGet httpGet = new HttpGet(url);
        CloseableHttpResponse execute = null;
        try {
            //执行请求,null参数 代表的代理信息,可不设置
            execute = CLOSEABLEHTTPCLIENT.execute(null,httpGet);
            int code = execute.getCode();
            System.out.println("获取图片请求应答状态码:"+code);
            //获取应答对象
            HttpEntity entity = execute.getEntity();
            //将应答数据转为字节数组,比如媒体数据如图片等,就可以转为字节数组,然后通过输出流输出在目录中,达到将下载保存图片的目的
            byte[] bytes = EntityUtils.toByteArray(entity);
            String suffix = ".jpg";
            //contentType会包含本次应答的数据类型
            String contentType = entity.getContentType();
            if (contentType.contains("jpg") || contentType.contains("jpeg")){
                suffix = ".jpg";
            }else if (contentType.contains("bmp") || contentType.contains("bitmap")){
                suffix = ".bmp";
            }else if (contentType.contains("png")){
                suffix = ".png";
            }else if (contentType.contains("gif")){
                suffix = ".gif";
            }
            System.out.println("最终图片后缀:"+suffix);
            //保存图片
            FileOutputStream fileOutputStream = new FileOutputStream("d:\\b" + suffix);
            fileOutputStream.write(bytes);
            fileOutputStream.close();
            //确保流关闭
            EntityUtils.consume(entity);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            close(execute,null);
        }
    }



    /**
     * http get请求
     * @param url url地址
     * @param params 请求参数
     * @param  headers 请求头参数
     * @return
     * //添加header信息,User-Agent用户代理,用于传递客户端的一些信息,比如浏览器访问某个接口,那么就会在header中加入User-Agent及对应值,那么后端服务器就可以通过User-Agent标识的值判断是否为浏览器正常访问
     * //及获取客户端的信息,如操作系统,浏览器型号版本等;如果不是浏览器访问则不会有这些值,那么后端接口就可以因此判断是代码在请求接口可以做拦截(当然客户端代码如果设置了和浏览器一样的User-Agent值那么这个办法就失效了)
     * //httpGet.addHeader("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36");
     * //添加header信息,Referer标记访问接口的来源地址,可用作防盗链(比如在A网站对某个接口发起请求,那么这个值就是A网站的地址(一般是A网站的跟地址或当前页面的地址),此时如果这个接口允许A网站访问,那么就可以忽略
     * //如果不允许A网站进行访问,那么通过Referer字段检测出是A网站,就可以拒绝这个请求,防止自己的资源被其他网站使用),当然如果是这种方式防盗链拦截那么也可以伪造Referer的值跳过防盗链拦截
     * //httpGet.addHeader("Referer","http://xxx.com/");
     *
     * 获取应答header信息
     * Header[] headers = execute.getHeaders();
     * for (Header header : headers) {
     *     System.out.println("应答header信息:"+header.getName()+" 值:"+header.getValue());
     * }
     *
     * //设置访问代理信息,也可以在调用 execute 方法时传入代理信息 HttpHost,设置访问代理即不使用本机进行访问目标接口,而采用代理机器访问目标接口,这样可以避免频繁访问某个网站将本机地址纳入黑名单
     * //或者本机不能访问某个接口,通过代理进行访问这个接口
     * HttpHost httpHost = new HttpHost("127.0.0.1",8080);
     * RequestConfig build = RequestConfig.custom().setProxy(httpHost)
     *         //设置连接超时,即和目标url建立连接的超时时间
     *         .setConnectTimeout(3000, TimeUnit.MILLISECONDS)
     *         //设置获取连接超时,即从连接池中获取一个连接的等待超时时间
     *         .setConnectionRequestTimeout(3000,TimeUnit.MILLISECONDS)
     *         //设置应答超时,即目标url处理请求并接收应答数据的超时时间,httpclient是socketTimeout
     *         .setResponseTimeout(3000,TimeUnit.MILLISECONDS)
     *         .build();
     * httpGet.setConfig(build);
     */
    public static String get(String url, Map<String,Object> params,Map<String,String> headers){
        //get请求参数在url中,如果这个参数具有特殊字符比如 (空格)、|、+ 等等,那么在直接请求时会把url中的特殊字符进行转换最后就会导致这些特殊字符不能被接口正常接收,要么报错、要么接收的值不对
        //此时就需要对这种在url中的参数进行编码,防止被转换,以便让接口可以正确接收有特殊字符的参数,如果是浏览器访问的接口,那么这些特殊字符编码的处理浏览器会自动处理,特别是一些密码传输可能会加密
        //那么更容易产生特殊字符,需要进行 URLEncoder 处理
        if (params != null && params.size() > 0){
            for (String key : params.keySet()) {
                try {
                    //对参数进行编码
                    String encode = URLEncoder.encode(String.valueOf(params.get(key)), StandardCharsets.UTF_8.name());
                    if (url.indexOf("?") == -1){
                        //说明是第一个参数
                        url = url +"?"+key+"="+encode;
                    }else {
                        //说明不是第一个参数
                        url = url +"&"+key+"="+encode;
                    }
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                    System.out.println("参数编码报错:"+key+" 错误简略:"+e.getMessage());
                }
            }
        }
        //创建get请求
        HttpGet httpGet = new HttpGet(url);
        //设置header参数
        if (CollUtil.isNotEmpty(headers)){
            headers.forEach((k,v)->{
                httpGet.addHeader(new BasicHeader(k,v));
            });
        }
        CloseableHttpResponse execute = null;
        try {
            //执行请求
            execute = CLOSEABLEHTTPCLIENT.execute(httpGet);
            int code = execute.getCode();
            System.out.println("get请求应答状态码:"+code);
            //获取应答对象
            HttpEntity entity = execute.getEntity();
            //应答数据转为string
            String response = EntityUtils.toString(entity, StandardCharsets.UTF_8);
            //确保流关闭
            EntityUtils.consume(entity);
            System.out.println("get请求应答结果:"+response);
            //打印连接池信息,使用时注释掉
            //print();
            return response;
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ParseException e) {
            e.printStackTrace();
        }finally {
            close(execute,null);
        }
        return "未获取到正确数据!";
    }

    /**
     * post请求-form表单提交数据
     * @param url url地址
     * @param params 请求参数
     * @param headers header信息
     * @return
     * 请求的接口没有 @RequestBody 注解的 post 接口就是form表单类型的post接口,如果因为post接口去掉 @RequestBody 注解后,导致请求其他的get接口出现 Cannot call sendError() after the response has been committed 报错,那么就将post接口的 @RequestBody 注解换成 @RequestParam 注解即可
     */
    public static String postForm(String url, Map<String,Object> params,Map<String,String> headers){
        //创建post请求对象
        HttpPost httpPost = new HttpPost (url);
        //设置header信息
        if (CollUtil.isNotEmpty(headers)){
            headers.forEach((k,v)->{
                httpPost.addHeader(new BasicHeader(k,v));
            });
        }
        //设置请求参数
        //设置 application/x-www-form-urlencoded 类型的 post 请求参数
        List<NameValuePair> list = new ArrayList<>();
        params.forEach((k,v)->{
            NameValuePair nameValuePair = new BasicNameValuePair(k,String.valueOf(v));
            list.add(nameValuePair);
        });
        //UrlEncodedFormEntity对象用来设置 application/x-www-form-urlencoded 类型的请求参数即form表单的post请求
        UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(list);
        httpPost.setEntity(urlEncodedFormEntity);
        //HttpClient 发送 post 请求默认为 application/x-www-form-urlencoded 类型,因此如果发送form表单的post请求可以不设置 Content-Type,请求的接口没有 @RequestBody 注解的 post 接口或 @RequestParam 注解的 post 接口就是form表单类型的post接口
        //如果post接口有 @RequestBody 注解,那么就是 application/json 类型的post接口
        httpPost.addHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8");
        CloseableHttpResponse execute = null;
        try {
            execute = CLOSEABLEHTTPCLIENT.execute(httpPost);
            System.out.println("post请求应答码:"+execute.getCode());
            HttpEntity entity = execute.getEntity();
            String result = null;
            try {
                result = EntityUtils.toString(entity, StandardCharsets.UTF_8);
            } catch (ParseException e) {
                e.printStackTrace();
            }
            System.out.println("post请求结果:"+result);
            return result;
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            close(execute,null);
        }
        return "未获取到正确数据!";
    }

    /**
     * post请求-json格式提交数据
     * @param url url地址
     * @param params 请求参数
     * @param headers header信息
     * @return
     * 请求的接口有 @RequestBody 注解的 post 接口就是json类型的post接口
     */
    public static String postJSON(String url, Map<String,Object> params,Map<String,String> headers){
        //创建post请求对象
        HttpPost httpPost = new HttpPost (url);
        //设置header信息
        if (CollUtil.isNotEmpty(headers)){
            headers.forEach((k,v)->{
                httpPost.addHeader(new BasicHeader(k,v));
            });
        }
        //设置请求参数
        //设置 application/json 类型的 post 请求参数,因此需要参数是一个json字符串
        String jsonStr = JSONUtil.toJsonStr(params);
        StringEntity stringEntity = new StringEntity(jsonStr,StandardCharsets.UTF_8);
        httpPost.setEntity(stringEntity);
        httpPost.addHeader("Content-Type","application/json; charset=UTF-8");
        CloseableHttpResponse execute = null;
        try {
            execute = CLOSEABLEHTTPCLIENT.execute(httpPost);
            System.out.println("post请求应答码:"+execute.getCode());
            HttpEntity entity = execute.getEntity();
            String result = null;
            try {
                result = EntityUtils.toString(entity, StandardCharsets.UTF_8);
            } catch (ParseException e) {
                e.printStackTrace();
            }
            System.out.println("post请求结果:"+result);
            return result;
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            close(execute,null);
        }
        return "未获取到正确数据!";
    }

    /**
     * post请求-文件上传
     * @param url url地址
     * @param params 请求参数
     * @param headers header信息
     * @param file 需要上传的文件
     * @param fileName 接口文件参数名称
     * @return
     *
     * MultipartEntityBuilder一些api:
     * //以二进制的形式添加数据,同样是key/value格式,value可以通过File、byte[],InputStream等方式构建,key则是服务端接口定义的参数名
     * //.addBinaryBody(fileName, file)
     * //添加文本数据,key/value,注意:addTextBody 只能是字符类型的value,不能添加中文,中文会出现乱码,key则是服务端接口定义的参数名
     * //.addTextBody("desc", "remark")
     * //如果要添加中文值的参数,那么可以采用StringBody来构建对应的参数值,防止中文乱码
     * //.addPart("descCn",stringBody);
     */
    public static String postUpload(String url, Map<String,Object> params,Map<String,String> headers,File file,String fileName){
        //创建post请求对象
        HttpPost httpPost = new HttpPost (url);
        //设置header信息
        if (CollUtil.isNotEmpty(headers)){
            headers.forEach((k,v)->{
                httpPost.addHeader(new BasicHeader(k,v));
            });
        }
        //设置请求参数
        //设置上传文件参数
        MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
        multipartEntityBuilder.setCharset(StandardCharsets.UTF_8);
        //如果直接采用已有的定义 ContentType.MULTIPART_FORM_DATA 其具体定义是 create("multipart/form-data", StandardCharsets.ISO_8859_1) 可能会有中文乱码问题,所以这里设置 Content-Type 时,重新定义字符编码
        multipartEntityBuilder.setContentType(ContentType.create("multipart/form-data", StandardCharsets.UTF_8));
        //设置模式,默认 STRICT 模式
        //multipartEntityBuilder.setMode(HttpMultipartMode.STRICT);
        FileBody fileBody = new FileBody(file);
        //设置上传的具体文件可以通过 addPart 和 addBinaryBody 进行设置,key比如这里的 fileName 就是服务端接口定义的接收文件的参数的名称,addPart 和 addBinaryBody可以一起设置或设置一个即可,value则是这个文件的对应格式信息
        //addPart以Key/Value的形式添加ContentBody类型的数据,可以通过 File、byte[],InputStream等ContentBody实现类方式构建ContentBody,这里是通过 FileBody 构建,key则是服务端接口定义的参数名
        multipartEntityBuilder.addPart(fileName, fileBody);
        //设置参数
        if (CollUtil.isNotEmpty(params)){
            params.forEach((k,v)->{
                //中文参数值的设置方式,key是中文值,value是编码格式
                StringBody stringBody = new StringBody(String.valueOf(v),ContentType.create("text/plain", StandardCharsets.UTF_8));
                multipartEntityBuilder.addPart(k,stringBody);
            });
        }
        HttpEntity build = multipartEntityBuilder.build();
        httpPost.setEntity(build);
        //这里不能设置Content-Type,因为还需要设置 boundary 参数,这个参数值会根据文件进行变化,不设置 Content-Type 那么httpclient会自动设置 Content-Type 的值
        //httpPost.addHeader("Content-Type", ContentType.create("multipart/form-data", StandardCharsets.UTF_8));
        CloseableHttpResponse execute = null;
        try {
            execute = CLOSEABLEHTTPCLIENT.execute(httpPost);
            System.out.println("post请求应答码:"+execute.getCode());
            HttpEntity entity = execute.getEntity();
            String result = null;
            try {
                result = EntityUtils.toString(entity, StandardCharsets.UTF_8);
            } catch (ParseException e) {
                e.printStackTrace();
            }
            System.out.println("post请求结果:"+result);
            return result;
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            close(execute,null);
        }
        return "未获取到正确数据!";
    }


    /**
     * 构建定制化 ConnectionSocketFactory,主要对非安全的https协议支持,绕开验证(指的是对接口调用时HttpClient验证的绕开)
     * @return
     */
    private static ConnectionSocketFactory getSSLConnectionSocketFactory(){
        SSLContextBuilder sslContextBuilder = new SSLContextBuilder();
        try {
            sslContextBuilder.loadTrustMaterial(null, new TrustStrategy() {
                //判断是否信任url,可以根据规则定义,这里统一返回可信任
                @Override
                public boolean isTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
                    return true;
                }
            });
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        }
        SSLContext build = null;
        try {
            build = sslContextBuilder.build();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        }
        //定义支持的协议类型
        String[] strings = new String[]{"SSLv2Hello","SSLv3","TLSv1","TLSv1.1","TLSv1.2"};
        SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(build,strings,null, NoopHostnameVerifier.INSTANCE);
        return sslConnectionSocketFactory;
    }

    /**
     * 关闭流,CloseableHttpClient如果是每次调用接口时单独创建则需要关闭,如果是每种请求方式共用不能进行关闭,一旦关闭再次调用则会报错
     * @param execute
     * @param closeableHttpClient
     */
    private static void close(CloseableHttpResponse execute,CloseableHttpClient closeableHttpClient){
        if (execute != null){
            try {
                //关闭应答流
                execute.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (closeableHttpClient != null){
            try {
                //关闭httpClient对象
                closeableHttpClient.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /***********************以下方法用于验证,不需要调用,使用时可以删除以下代码*******************************/
    /**
     * 打印连接池信息,该方法不需要调用
     */
    private static void print(){
        PoolStats totalStats = POOLINGHTTPCLIENTCONNECTIONMANAGER.getTotalStats();
        System.out.println("已用连接数"+totalStats.getLeased());
        System.out.println("可用连接数:"+totalStats.getAvailable());
    }

    /**
     * 验证文件上传和线程池设置是否生效
     * @param args
     */
    public static void main(String[] args) {
        Map<String,Object> param = new HashMap<>();
        param.put("desc","英文说明");
        param.put("descCn","中文说明");
        postUpload("http://127.0.0.1:6666/service/upload",param,null,new File("D:\\b.jpg"),"fileName");
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0;i<100;i++){
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    get("http://127.0.0.1:6666/service/list",null,null);
                }
            });
        }
    }

}

client controller修改后:

package com.http.test.client.controller;

import com.http.test.client.util.HttpClientUtil;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
 * 客户端controller即调用方
 */
@RestController
@RequestMapping("/client")
public class ClientController {

    /**
     * 服务端接口地址前缀
     */
    private final String ADD_PREFIX = "http://127.0.0.1:6666/service/";

    /**
     * get请求根据key获取数据,调用service接口获取数据
     *
     * @param key
     * @return
     */
    @GetMapping("/get/{key}")
    public Object get(@PathVariable("key") String key) {
        String result = HttpClientUtil.get(ADD_PREFIX + "get/"+key,null,null);
        return result;
    }

    /**
     * get请求获取所有数据,调用service接口获取数据
     *
     * @return
     */
    @GetMapping("/list")
    public Object list() {
        String result = HttpClientUtil.get(ADD_PREFIX + "list",null,null);
        return result;
    }

    /**
     * post请求根据key和value保存数据,调用service接口保存数据
     *
     * @param map
     * @return
     */
    @PostMapping("/set")
    public Object set(@RequestBody Map<String, Object> map) {
        String result = HttpClientUtil.postJSON(ADD_PREFIX + "set", map,null);
        return result;
    }

}

文件上传注意事项:
服务端报错及解决方案:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
注意事项:

服务端接口:
a、参数定义时不能采用 @RequestBody 修饰,否则会改变接口支持的 Content-Type 会导致报错
b、接收文件外的其他参数只能是定义为类对象,在类中定义好每个参数的名称,使用Map之类的不明确指定参数名称的对象会导致使httpclient调用接口时无法接收到对应的参数

httpclient调用:
a、不能在header中设置 Content-Type 参数,即使设置为 multipart/form-data 类型也会由于没有设置 boundary 参数服务端接口报错,不设置httpclient会自动设置
b、MultipartEntityBuilder 对象设置 ContentType 参数时由于自带的 ContentType.MULTIPART_FORM_DATA 是 ISO_8859_1 编码,可能会中文乱码,需要重新设置为 ContentType.create(“multipart/form-data”, StandardCharsets.UTF_8) utf-8编码
c、如果传递除文件外的参数包含中文,那么需要借助于 StringBody 对象构建中文值,否则会出现乱码
d、addTextBody 只能添加字符串数据且不能有中文,否则会乱码
e、设置文件参数时,可以通过 addPart 和 addBinaryBody 进行设置,比较方便,两者设置其中一个即可也可以同时设置
f、addPart 和 addBinaryBody、addTextBody 其中的key都是指对应的参数名称,比如服务端接口定义的文件参数的名称、其他参数则是参数类中对应的字段名称,value则是这个文件或对应的参数值,根据要求构建这个文件对象或参数值对象

接口测试:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

四、RestTemplate

RestTemplate 位于 spring-web 包中,因此需要导入这个包,但如果是spring boot项目会导入 spring-boot-starter-web 包,这个包中包含了 spring-web 包,所以作为spring boot项目需要直接使用 RestTemplate 时,不需要额外导入包。
RestTemplate统一了http请求的相关api(类似的还有RedisTemplate等),同样的底层需要依赖于具体的http请求工具才能发起http请求,这样可以通过配置修改切换具体的http实现,但不需要改动具体的代码;默认使用jdk自带的HttpURLConnection作为http请求的具体实现,这种方式不能配置连接池等信息,如果需要切换具体的http实现以及配置连接池和证书等,可以设置为HttpClient或OKHttp来实现。
可以通过设置不同的ClientHttpRequestFactory来实现底层采用不同的HTTP连接方式。
a、SimpleClientHttpRequestFactory是jdk自带的HttpURLConnection作为实现
b、HttpComponentsClientHttpRequestFactory是HttpClient作为实现
c、OkHttp3ClientHttpRequestFactory是OKHttp作为实现

这里以使用HttpClient来配置:
导入的httpclient相关的包是httpclient,不能是httpclient5,因为配置RestTemplate时,HttpComponentsClientHttpRequestFactory可接收的参数是httpclient而非httpclient5,上面HttpClient的例子是单独使用HttpClient,不会受到RestTemplate的约束,因此使用了比较新且功能更强的httpclient5
导入包:

        <!-- 导入的是httpclient,不能是httpclient5,因为配置RestTemplate时,HttpComponentsClientHttpRequestFactory可接收的参数是httpclient而非httpclient5 -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.13</version>
        </dependency>

配置类:

package com.http.test.client.config;


import org.apache.http.Header;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * RestTemplate可以直接根据ip端口、域名等地址信息发起请求
 * 也可以使用服务名然后根据服务名在注册中心上查找对应服务名的地址进行发起请求
 * 在配置RestTemplate时,可以构建一个RestTemplate对象交由ioc管理,这样可以实现需要使用时直接注入一个RestTemplate对象即可,一般都是这样做,也就是不需要写一个工具类
 * 当然也可以封装一个工具类,使用时调用工具类的方法即可,这里写的 RestTemplateUtil 就是封装的工具类
 */
@Configuration
public class RestTemplateConfig {


    /**
     * 根据具体地址发起http请求
     * @return
     */
    @Bean(name = "restTemplate")
    public RestTemplate createRestTemplate(ClientHttpRequestFactory clientHttpRequestFactoryC) {
        RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactoryC);
        List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
        for (HttpMessageConverter<?> messageConverter : messageConverters) {
            if (messageConverter instanceof StringHttpMessageConverter){
                StringHttpMessageConverter messageConverter1 = (StringHttpMessageConverter) messageConverter;
                //System.out.println("原本字符集编码:"+messageConverter1.getDefaultCharset());
                messageConverter1.setDefaultCharset(StandardCharsets.UTF_8);
                //System.out.println("修改后字符集编码:"+messageConverter1.getDefaultCharset());
            }
        }
        //设置form表单请求的消息转换器,如果发送form表单的请求出现这个错误 No HttpMessageConverter for java.util.LinkedHashMap and content type "application/x-ww-form-urlencoded" 可以设置上这个转换器
        //messageConverters.add(new FormHttpMessageConverter());
        return restTemplate;
    }

    /**
     * 根据注册中心上的服务名称发起http请求,区别在于多了 @LoadBalanced 注解,其余一样
     * @return
     */
    @Bean(name = "restTemplateLb")
    @LoadBalanced
    public RestTemplate createRestTemplateLb(ClientHttpRequestFactory clientHttpRequestFactoryC) {
        RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactoryC);
        List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
        for (HttpMessageConverter<?> messageConverter : messageConverters) {
            if (messageConverter instanceof StringHttpMessageConverter){
                StringHttpMessageConverter messageConverter1 = (StringHttpMessageConverter) messageConverter;
                messageConverter1.setDefaultCharset(StandardCharsets.UTF_8);
            }
        }
        return restTemplate;
    }

    /**
     * 配置 ClientHttpRequestFactory,主要可以用于配置连接池、证书、默认请求信息、默认请求头等信息
     * 通过设置 ClientHttpRequestFactory,配置具体的http请求实现
     * @return
     */
    @Bean(name = "clientHttpRequestFactoryC")
    public ClientHttpRequestFactory buildFactory(PoolingHttpClientConnectionManager poolingHttpClientConnectionManager) {
        RequestConfig requestConfig = RequestConfig.custom()
                //设置连接超时,即和目标url建立连接的超时时间
                .setConnectTimeout(3000)
                //设置获取连接超时,即从连接池中获取一个连接的等待超时时间
                .setConnectionRequestTimeout(3000)
                //设置应答超时,即目标url处理请求并接收应答数据的超时时间,低版本是socketTimeout,超时报错:java.net.SocketTimeoutException: Read timed out
                .setSocketTimeout(3000)
                .build();
        //设置默认header信息
        List<Header> headers = new ArrayList<>();
        //设置用户代理信息为浏览器发送的请求,防止请求的接口做了非浏览器请求拦截
        Header header = new BasicHeader("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36");
        headers.add(header);
        CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).setConnectionManager(poolingHttpClientConnectionManager).setDefaultHeaders(headers).build();
        HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
        return clientHttpRequestFactory;
    }

    /**
     * 构建PoolingHttpClientConnectionManager
     * 由于上面创建 restTemplateLb 和 restTemplate 对象时,使用的都是当前的 PoolingHttpClientConnectionManager 对象,所以在使用这两个RestTemplate时,他们的连接池是同一个
     * 如果构建多个RestTemplate对象,需要单独使用连接池的话,那就需要在创建每个RestTemplate对象时,单独创建 PoolingHttpClientConnectionManager 和 ClientHttpRequestFactory 对象,好处在于连接池不会共用
     * 不会由于某个场景大量占用连接导致其他场景使用出现问题,坏处时定义连接池数量时不合理会导致资源的浪费
     * @return
     */
    @Bean
    public PoolingHttpClientConnectionManager buildPoolingHttpClientConnectionManager(){
        //设置http和https的支持,如果需要绕过https的不安全验证可以参考HttpClientUtil的方式自定义一个factory
        Registry<ConnectionSocketFactory> build = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", SSLConnectionSocketFactory.getSocketFactory())
                .build();
        PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(build);
        //设置最大连接数
        poolingHttpClientConnectionManager.setMaxTotal(100);
        //设置每个路由默认最大连接数(同一个域名认为是一个路由,即同一个域名可以保留的最大连接数,空闲连接和已使用连接一起计算数量)
        poolingHttpClientConnectionManager.setDefaultMaxPerRoute(10);
        return poolingHttpClientConnectionManager;
    }

}

工具类:

package com.http.test.client.util;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONUtil;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 一般使用RestTemplate时可以配置RestTemplate对象之后直接使用,不写工具类也可以
 * 如果封装工具类有两种方式可选
 * 第一:将工具类交给ioc管理,这样可以直接使用配置类配置的RestTemplate对象,并且配置的RestTemplate对象如果采用了 @LoadBalanced 注解,开启根据服务名进行发起调用,那么就可以直接支持服务名的url
 * 第二:工具类不交给ioc管理,那么就需要在工具类中构建一个RestTemplate对象,如果需要RestTemplate可以根据服务名进行发起请求,这需要去注册中心中查找对应的服务名进行替换为具体的ip端口信息,这部分功能就需要手动实现
 * 比较方便的方式是可以注入 LoadBalancerClient  对象进行获取服务名对应的ip端口信息,但这样做也需要将工具类交给ioc管理,因此如果封装工具类,那么将工具类交给ioc管理可以方便使用
 *
 * restTemplate发起http请求有几种方式(如:getForObject、postForObject、getForEntity、postForEntity)
 * ForEntity方式发起请求,请求结果会包装为ResponseEntity类型,可以通过ResponseEntity类的getStatusCode()方法获取应答状态码,getBody()获取返回结果,返回结果的类型可以通过Class<T> responseType参数指定,请求参数用HttpEntity包装,可以传递header等信息
 * ForObject方式发起请求,请求结果直接是对应的body,类型可以通过Class<T> responseType参数指定,请求参数采用Object类型
 * exchange方式发起请求,请求结果同样会包装为ResponseEntity类型,只是需要指定的参数更多更原始一些,比如请求方式等;同时该方法接收的请求参数为 HttpEntity 类型,那么可以在构建 HttpEntity 对象时传入相应的header信息
 * 使用 HttpEntity 构建参数时指定的是 header和body信息,所以设置的参数传递的位置是在body中而非拼接在url的后面
 * 例如:String forObject = restTemplate.getForObject(url, String.class, urlParam);
 *
 * List<NameValuePair> param = new ArrayList<>();
 * URI uri = new URIBuilder(url).setCharset(StandardCharsets.UTF_8).addParameters(param).build(); restTemplate可以使用字符串的url也可以接收URI作为请求地址
 * param 是需要拼接在url后的参数,通过?和&连接的那部分参数
 * 区别在于:
 * 当传入URI作为请求地址时由于URI本来的特性会对地址中的特殊字符进行编码,同时restTemplate提供的方法中没有对URI类型的参数进行替换地址中的参数为参数值的方法,因此这部分需要手动进行替换
 * 原因可能是由于URI中的参数名称如果有特殊字符会进行编码,那么就可能导致和传入的需要替换的参数名称不能对应
 *
 * 当传入字符串的url时,如果参数值有特殊字符需要使用 URLEncoder.encode() 进行处理,当然restTemplate提供的方法中有对url中的参数进行替换参数值的方法,这点不需要手动操作
 */
@Configuration
public class RestTemplateUtil {


    /**
     * 根据ip地址请求接口
     */
    @Resource(name = "restTemplate")
    private RestTemplate restTemplate;

    /**
     * 根据服务名请求接口
     */
    @Resource(name = "restTemplateLb")
    private RestTemplate restTemplateLb;

    /**
     * 注入PoolingHttpClientConnectionManager方便获取连接池的信息
     */
    @Autowired
    private PoolingHttpClientConnectionManager poolingHttpClientConnectionManager;



    /**
     * get请求封装
     * @param url 请求地址
     * @param urlParam 用于替换url中的参数为具体的参数值
     * @param responseType 请求的接口应答的数据需要转换的类型
     * @param headers header信息
     * @return
     *
     */
    public <T> T get(String url, Map<String,String> urlParam,Class<T> responseType,MultiValueMap<String, String> headers,Map<String,String> formParam){
        if (CollUtil.isEmpty(headers)){
            headers = new HttpHeaders();
        }
        HttpEntity httpEntity = new HttpEntity(null,headers);
        ResponseEntity<T> forEntity = null;
        //构建url中拼接的参数对象,采用URI作为接口地址时,可调用URI的addParameters方法添加地址中的参数
        List<NameValuePair> param = new ArrayList<>();
        if (CollUtil.isNotEmpty(formParam)){
            for (String key : formParam.keySet()) {
                NameValuePair nameValuePair = new BasicNameValuePair(key,formParam.get(key));
                param.add(nameValuePair);
            }
        }
        //替换url中的参数为参数值
        if (CollUtil.isNotEmpty(urlParam)){
            for (String key : urlParam.keySet()) {
                url = url.replaceAll("\\{"+key+"}",urlParam.get(key));
            }
        }
        System.out.println("替换后的url:"+url);
        //使用URI作为接口调用地址,可以对参数中的特殊字符进行编码,防止接口接收参数时出现错误或接收的参数值不对
        //也可以采用URLEncoder.encode对参数值进行编码,但这种方式还需要服务端使用URLDecoder.decode进行解码,URI则不需要进行编解码
        URI uri = null;
        try {
            uri = new URIBuilder(url).setCharset(StandardCharsets.UTF_8).addParameters(param).build();
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
        forEntity = restTemplate.exchange(uri, HttpMethod.GET,httpEntity,responseType);
        //打印连接池信息
        outPool();
        return forEntity.getBody();
    }

    /**
     * post请求json格式
     * @param url 请求地址
     * @param param 请求参数
     * @param headers header信息
     * @param responseType 请求的接口应答的数据需要转换的类型
     * @param <T>
     * @return
     */
    public <T> T postJSON(String url, Object param, MultiValueMap<String,String> headers, Class<T> responseType){
        //将参数转为json字符串
        String jsonStr = JSONUtil.toJsonStr(param);
        if (CollUtil.isEmpty(headers)){
            headers = new HttpHeaders();
        }
        //验证header中传递中文使用,可直接删除这部分代码
        /*String token = "用户信息";
        String encode = null;
        try {
            //客户端调用时header中的token的值如果是中文或特殊字符需要经过URLEncoder.encode编码,服务端接收参数后通过URLDecoder.decode进行解码才能正确的传递和接收中文或特殊字符
            //如果是在url中拼接的中文或特殊字符也需要进行编码然后服务端进行解码才能正确传输
            encode = URLEncoder.encode(token, StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        headers.add("token",encode);*/
        headers.add("Content-Type", MediaType.APPLICATION_JSON_VALUE);
        HttpEntity<String> body = new HttpEntity<>(jsonStr,headers);
        ResponseEntity<T> responseEntity = restTemplate.postForEntity(url, body, responseType);
        System.out.println("post JSON请求应答状态码:"+responseEntity.getStatusCode());
        return responseEntity.getBody();
    }

    /**
     * post请求form表单格式
     * @param url 请求地址
     * @param param 请求参数
     * @param headers header信息
     * @param responseType 请求的接口应答的数据需要转换的类型
     * @param <T>
     * @return
     */
    public <T> T postForm(String url, Object param, MultiValueMap<String,String> headers, Class<T> responseType){
        if (CollUtil.isEmpty(headers)){
            headers = new HttpHeaders();
        }
        //使用MultiValueMap构建参数后,即使没有设置 Content-Type 为 application/x-www-form-urlencoded 类型,restTemplate在发送请求时也会设置该值
        //所以采用 MultiValueMap 构建参数后意味着已经是发送 application/x-www-form-urlencoded 类型的请求了
        //headers.add("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE);
        //发送form表单类型的post请求一定要使用 MultiValueMap 构建参数
        MultiValueMap<String,Object> linkedMultiValueMap = new LinkedMultiValueMap<>();
        Map<String,Object> hashMap = JSONUtil.toBean(JSONUtil.toJsonStr(param), HashMap.class);
        for (String key : hashMap.keySet()) {
            linkedMultiValueMap.add(key,hashMap.get(key));
        }
        HttpEntity<MultiValueMap<String,Object>> body = new HttpEntity<>(linkedMultiValueMap,headers);
        ResponseEntity<T> responseEntity = restTemplateLb.postForEntity(url, body, responseType);
        System.out.println("post FORM表单请求应答状态码:"+responseEntity.getStatusCode());
        return responseEntity.getBody();
    }


    /**
     * 文件上传
     * @param url 请求地址
     * @param param 请求参数
     * @param headers header信息(HttpHeaders实现了MultiValueMap接口,构建HttpEntity时header参数需要MultiValueMap类型,因此使用MultiValueMap接口的实现类即可,HttpHeaders封装了方法对于设置header时更方便)
     * @param responseType 请求的接口应答的数据需要转换的类型
     * @param file
     * @param <T>
     * @return
     */
    public <T> T uploadFile(String url, Object param, HttpHeaders headers, Class<T> responseType, File file){
        if (null == headers){
            headers = new HttpHeaders();
        }
        // 设置 headers 中 content-type 类型为 multipart/form-data,该参数可以不用设置,restTemplate会自动设置 boundary 的值
        //headers.setContentType(MediaType.MULTIPART_FORM_DATA);
        // MultiValueMap 模拟表单提交,用于设置文件参数及其他参数
        MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
        //传入文件,key和接口接收文件的参数名称一致 @RequestParam("fileName") MultipartFile multipartFile
        form.add("fileName", new FileSystemResource(file));
        //设置其他参数
        Map<String,Object> hashMap = JSONUtil.toBean(JSONUtil.toJsonStr(param), HashMap.class);
        for (String key : hashMap.keySet()) {
            form.add(key,hashMap.get(key));
        }
        HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(form, headers);
        T result = restTemplate.postForObject(url, httpEntity, responseType);
        System.out.println("文件上传接口应答:"+result);
        return result;
    }








    /**以下方式是通过静态方法封装请求方法以及手动实现 @LoadBalanced 相同功能,不建议这样使用,要么选择上面的方式封装方法要么就需要使用RestTemplate时直接使用即可**/
    @Autowired
    private LoadBalancerClient loadBalancerClient;

    /**
     * 静态方法通过RestTemplate发起get请求
     * @param url 请求地址
     * @param urlParam 用于替换url中的参数为具体的参数值,key是参数名(key需要和url中的参数名称对应)、value是参数值
     *                 例如:http://127.0.0.1:6666/service/get/{aa}/{bb},HashMap<String, Object> objectObjectHashMap = new HashMap<>(); objectObjectHashMap.put("aa","xx"); objectObjectHashMap.put("bb","yy");
     *                 那么最终请求的url就会变为http://127.0.0.1:6666/service/get/xx/yy
     * @return
     */
    public static String getStatic(String url, Map<String,Object> urlParam){
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> forEntity = null;
        if (CollUtil.isNotEmpty(urlParam)){
            forEntity = restTemplate.getForEntity(url, String.class, urlParam);
        }else {
            forEntity = restTemplate.getForEntity(url, String.class);
        }
        return forEntity.getBody();
    }

    /**
     * 自己实现根据服务名获取具体服务的地址进行发起接口调用,相当于简易实现 @LoadBalanced 的功能
     * @param url 请求地址,服务名形式
     * @param urlParam 用于替换url中的参数为具体的参数值
     * @return
     */
    public String getLbChoose(String url,Map<String,Object> urlParam){
        String[] protocol = url.split("//");
        String[] path = protocol[1].split("/");
        //通过服务名称获取该服务的地址信息
        ServiceInstance choose = loadBalancerClient.choose(path[0]);
        String host = choose.getHost();
        int port = choose.getPort();
        String changeUrl = protocol[0]+"//"+host+":"+port;
        if (path.length > 1){
            String residuePath = "";
            for (int i = 1; i < path.length; i++) {
                residuePath = residuePath +"/"+ path[i];
            }
            changeUrl = changeUrl + residuePath;
        }
        System.out.println("替换后的url为:"+changeUrl);
        RestTemplate restTemplate = new RestTemplate();
        String result = null;
        if (CollUtil.isNotEmpty(urlParam)){
            result = restTemplate.getForObject(changeUrl, String.class, urlParam);
        }else {
            result = restTemplate.getForObject(changeUrl, String.class);
        }
        return result;
    }


    /**
     * 输出restTemplate连接池信息,采用不同的底层连接输出相关信息的api不同,由于这里使用的HttpClient(HttpComponentsClientHttpRequestFactory),所以以下方式输出
     * 需要获取到RestTemplate对象使用的 poolingHttpClientConnectionManager 对象才可以获取连接池的信息,poolingHttpClientConnectionManager 是被ioc管理的,所以可以直接注入
     */
    private void outPool(){
        System.out.println("最大连接数:"+Thread.currentThread().getId()+":"+poolingHttpClientConnectionManager.getMaxTotal());
        System.out.println("默认同一个路由连接数:"+Thread.currentThread().getId()+":"+poolingHttpClientConnectionManager.getDefaultMaxPerRoute());
        System.out.println("可用连接数:"+Thread.currentThread().getId()+":"+poolingHttpClientConnectionManager.getTotalStats().getAvailable());
        System.out.println("已用连接数:"+Thread.currentThread().getId()+":"+poolingHttpClientConnectionManager.getTotalStats().getLeased());
        System.out.println("待分配连接数:"+Thread.currentThread().getId()+":"+poolingHttpClientConnectionManager.getTotalStats().getPending());
        System.out.println("----------------"+Thread.currentThread().getId()+"");
    }

}

client controller修改后:

package com.http.test.client.controller;

import com.alibaba.fastjson.JSONObject;
import com.http.test.client.util.RestTemplateUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

/**
 * 客户端controller即调用方
 */
@RestController
@RequestMapping("/client")
public class ClientController {


    /**
     * 注入工具类使用 RestTemplate
     */
    @Autowired
    private RestTemplateUtil restTemplateUtil;

    /**
     * 也可以注入 RestTemplate 对象直接使用
     */
    @Resource(name = "restTemplate")
    private RestTemplate restTemplate;

    /**
     * 服务端接口地址前缀
     */
    private final String ADD_PREFIX = "http://127.0.0.1:6666/service/";

    /**
     * get请求根据key获取数据,调用service接口获取数据
     *
     * @param key
     * @return
     */
    @GetMapping("/get/{key}")
    public Object get(@PathVariable("key") String key) {
        System.out.println("参数:"+key);
        HashMap<String, String> objectObjectHashMap = new HashMap<>();
        objectObjectHashMap.put("aa",key);
        objectObjectHashMap.put("bb",key);
        Map<String,String> map = new HashMap<>();
        map.put("name","rest");
        JSONObject result = restTemplateUtil.get(ADD_PREFIX + "get/{aa}/{bb}", objectObjectHashMap, JSONObject.class, null,map);
        return result.toJSONString();
    }

    /**
     * get请求获取所有数据,调用service接口获取数据
     *
     * @return
     */
    @GetMapping("/list")
    public Object list() {
        Map<String,String> map = new HashMap<>();
        map.put("size","10");
        map.put("page","#1;:,");
        for (int i = 0;i<100;i++){
            new Thread(()->{
                restTemplateUtil.get(ADD_PREFIX+"/list",null,String.class,null,map);
            }).start();
        }
        return "xwecewc";
    }

    /**
     * post请求根据key和value保存数据,调用service接口保存数据
     *
     * @param map
     * @return
     */
    @PostMapping("/set")
    public Object set(@RequestBody Map<String, Object> map) {
        String result = restTemplateUtil.postJSON(ADD_PREFIX+"/set",map,null,String.class);
        return result;
    }


}

service controller修改后(主要配合测试验证做的一些修改):

package com.http.test.service.controller;

import com.http.test.service.dto.UpLoadDTO;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * 服务端controller即被调用方
 */
@RestController
@RequestMapping("/service")
public class ServiceController {

    /**
     * 用于模拟数据库,将数据保存在Map中
     */
    private Map<String,String> data = new HashMap<>();

    /**
     * get请求根据key获取数据
     * @param key 路径中的参数值
     * @param key2 路径中的参数值
     * @param param ?拼接在路径后的参数
     * @return
     */
    @GetMapping("/get/{key}/{key2}")
    public Map<String,String> get(@PathVariable("key") String key, @PathVariable("key2") String key2, @RequestParam Map<String,String> param, HttpServletRequest httpServletRequest){
        outHeader(httpServletRequest);
        for (String paramKey : param.keySet()) {
            System.out.println("url中参数:"+paramKey + " : " + param.get(paramKey));
        }
        Map<String,String> map = new HashMap<>();
        if (!data.containsKey(key)){
            throw new RuntimeException("数据不存在key!");
        }
        if (!data.containsKey(key2)){
            throw new RuntimeException("数据不存在key2!");
        }
        map.put(key,data.get(key));
        return map;
    }

    /**
     * get请求获取所有数据
     * @return
     */
    @GetMapping("/list")
    public Map<String,String> list(@RequestParam Map<String,String> param,HttpServletRequest httpServletRequest){
        outHeader(httpServletRequest);
        for (String paramKey : param.keySet()) {
            System.out.println("url中参数:"+paramKey + " : " + param.get(paramKey));
        }
        return data;
    }

    /**
     * post请求根据key和value保存数据
     * @param map
     * @return
     */
    @PostMapping("/set")
    public String set(@RequestBody Map<String,String> map,HttpServletRequest httpServletRequest){
        outHeader(httpServletRequest);
        data.put(map.get("key"),map.get("value"));
        return "保存成功!";
    }

    /**
     * 文件上传
     * @param multipartFile 接收上传的文件
     * @param upLoadDTO 接收其他参数,注意:接收其他参数时这个变量不能用 @RequestBody 修饰,否则会改变接口支持的 Content-Type 会导致报错,并且这个参数只能是定义为类对象,在类中定义好每个参数
     *                  的名称,使用Map之类的不明确指定参数名称的对象会导致使用httpclient调用接口时无法接收到对应的参数
     * @return
     */
    @PostMapping("/upload")
    public String upload(@RequestParam("fileName") MultipartFile multipartFile, UpLoadDTO upLoadDTO,HttpServletRequest httpServletRequest){
        outHeader(httpServletRequest);
        System.out.println("文件名称:"+multipartFile.getOriginalFilename());
        System.out.println(upLoadDTO);
        return "保存成功!";
    }



    /**
     * 打印header信息
     * @param httpServletRequest
     */
    private void outHeader(HttpServletRequest httpServletRequest){
        Enumeration<String> headerNames = httpServletRequest.getHeaderNames();
        while (headerNames.hasMoreElements()){
            String header = headerNames.nextElement();
            System.out.println("header信息: "+header+" : "+httpServletRequest.getHeader(header));
            if ("token".equals(header)){
                try {
                    //解码
                    String decode = URLDecoder.decode(httpServletRequest.getHeader(header), StandardCharsets.UTF_8.name());
                    System.out.println("header信息-解码后: "+header+" : "+decode);
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
            }
        }
    }


}

注意:使用 RestTemplate 必须要对 RestTemplate 进行配置,并将配置的 RestTemplate 对象交由 ioc 管理,否则项目会启动失败(前提是:没有配置RestTemplate对象,并且使用了 @Autowired、@Resource等方式注入了 RestTemplate 对象才会报错):
在这里插入图片描述
如果配置多个RestTemplate对象交给ioc管理(比如一个根据ip端口发起请求、一个根据服务名发起请求),需要为每个RestTemplate对象指定名称,否则项目启动时报错如下(也可以采用@Primary注解标记一个RestTemplate对象,但是这样会导致每次注入的RestTemplate对象都是被@Primary标记的这个,无法注入另外一个RestTemplate对象,无法使用另外一个RestTemplate对象,因此需要配置多个时,为每个RestTemplate对象指定名称,使用时采用 @Resource 注解注入对应RestTemplate对象的名称即可):
在这里插入图片描述
使用了 @LoadBalanced 注解设置RestTemplate对象开启服务名请求接口时,如果请求的url中不是服务名而是ip、端口则会报错如下(使用服务名进行请求的前提:需要当前服务和被请求的服务注册在同一个注册中心上,这样才可以在当前服务配置的注册中心上找到被请求服务的地址信息):
在这里插入图片描述
没有使用 @LoadBalanced 注解设置的RestTemplate对象则是根据具体的IP、端口地址信息进行请求接口,如果url中是服务名则会报错如下:
在这里插入图片描述
也就是说没有注解 @LoadBalanced 的实例是根据ip、端口等具体的地址信息进行请求,注解了 @LoadBalanced 的实例则根据服务名进行请求,两者不能混用。

如果因为post接口去掉 @RequestBody 注解导致请求其他的get接口出现以下报错时(这两个报错是一起出现的),那么就将 post 接口的 @RequestBody 注解换成 @RequestParam 注解即可(去掉 @RequestBody 注解或添加 @RequestParam 注解的post接口那么接收的参数类型是form表单(application/x-www-form-urlencoded)而非json格式的参数(application/json))。
在这里插入图片描述
在这里插入图片描述

如果发送form表单的请求出现这个错误 No HttpMessageConverter for java.util.LinkedHashMap and content type “application/x-ww-form-urlencoded” 可以设置上这个消息转换器 FormHttpMessageConverter
在这里插入图片描述
在这里插入图片描述

调用接口时如果header中的值存在中文或特殊字符需要经过URLEncoder.encode编码,服务端接收参数后通过URLDecoder.decode进行解码才能正确的传递和接收中文或特殊字符,如果是在url中拼接的中文或特殊字符也需要进行编码然后服务端进行解码才能正确传输,当然url中的参数还可以使用URI进行构建会自动进行编码同时服务端也不需要进行解码操作。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

目前使用RestTemplate较多,但在spring 5.0 之后官方推荐发起http请求时使用WebClient,WebClient是一个强大的、非阻塞的HTTP客户端,用于构建响应式RESTful服务调用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1594290.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构(六)----串

目录 1.串的定义 2.串的基本操作 3.串的存储结构 (1)串的定义 •顺序存储 •链式存储 (2)求串长 (3)求子串 (4)比较串的大小 (5)定位操作 4.字符串的模式匹配 (1)朴素模式匹配算法 (2)KMP算法 •求模式串中的next数组&#xff08;重点&#xff09; •练习&#…

第四百六十回

文章目录 1. 概念介绍2. 方法与细节2.1 获取方法2.2 使用细节 3. 示例代码4. 内容总结 我们在上一章回中介绍了"如何获取当前系统语言"相关的内容&#xff0c;本章回中将介绍如何获取时间戳.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在本章…

适配器模式:连接不兼容接口的桥梁

在软件开发中&#xff0c;适配器模式是一种结构型设计模式&#xff0c;它允许不兼容的接口之间进行交互&#xff0c;从而使它们能够一起工作。这个模式经常用于系统升级或集成第三方库的时候&#xff0c;当现有的代码无法直接使用新系统或库提供的接口时&#xff0c;适配器可以…

基于Java+Vue的中国咖啡文化宣传网站(源码+文档+包运行)

一.系统概述 本课题是根据咖啡文化宣传需要以及网络的优势建立的一个中国咖啡文化宣传网站&#xff0c;来实现中国咖啡文化宣传以及咖啡商品售卖的功能。 本中国咖啡文化宣传网站应用Java技术&#xff0c;MYSQL数据库存储数据&#xff0c;基于SSMVue框架开发。在网站的整个开发…

【QT入门】Qt自定义控件与样式设计之自定义QLineEdit实现搜索编辑框

往期回顾 【QT入门】Qt自定义控件与样式设计之qss的加载方式-CSDN博客 【QT入门】Qt自定义控件与样式设计之控件提升与自定义控件-CSDN博客 【QT入门】Qt自定义控件与样式设计之鼠标相对、绝对位置、窗口位置、控件位置-CSDN博客 【QT入门】Qt自定义控件与样式设计之自定义QLin…

找不到mfc110u.dll怎么办,总结5种有效的解决方法

在日常操作计算机的过程中&#xff0c;我们时常会遭遇各类突发状况&#xff0c;其中一种颇为常见的问题便是当试图运行某个特定软件时&#xff0c;系统突然弹出一则令人困扰的错误提示&#xff1a;“由于找不到mfc110u.dll&#xff0c;无法继续执行代码”。这个问题通常是由于缺…

Vol.44 一个分享网站的网站,每个月8.7万访问量

哈咯&#xff0c;各位朋友好啊&#xff0c;我是欧维&#xff0c;今天要给大家分享的网址是Fuun.fun&#xff0c;奇趣网站收藏家&#xff1b; 它的网址是&#xff1a;FUUN.FUN 这是一个我经常逛的网站&#xff0c;为什么我经常逛呢&#xff1f;因为可以从中发现一些有意思的网站…

Vol.46 一个在线小游戏网站,每个月50万访问量

大家好&#xff0c;我是欧维Ove&#xff0c;今天要给大家分享的网站是&#xff1a;小霸王&#xff0c;这是一个可以在线玩小霸王游戏的网站&#xff0c;网址是&#xff1a;小霸王&#xff0c;其樂無窮。紅白機&#xff0c;FC線上遊戲&#xff0c;街機遊戲&#xff0c;街機線上&…

一种驱动器的功能安全架构介绍

下图提供了驱动器实现安全功能的架构 具有如下特点&#xff1a; 1.通用基于总线或者非总线的架构。可以实现ethercat的FSOE&#xff0c;profinet的profisafe&#xff0c;或者伺服本体安全DIO现实安全功能。 2.基于1oo2D架构&#xff0c;安全等级可以达到sil3。 3.高可用性。单…

第17天:信息打点-语言框架开发组件FastJsonShiroLog4jSpringBoot等

第十七天 本课意义 1.CMS识别到后期漏洞利用和代码审计 2.开发框架识别到后期漏洞利用和代码审计 3.开发组件识别到后期漏洞利用和代码审计 一、CMS指纹识别-不出网程序识别 1.概念 CMS指纹识别一般能识别到的都是以PHP语言开发的网页为主&#xff0c;其他语言开发的网页识…

攻防世界---Web_php_include

1.题目链接 2.补充知识&#xff1a; 3.构造&#xff1a;执行成功 /?pagedata://text/plain,<?php phpinfo()?> 4.构造下面url&#xff0c;得到目录路径 /?pagedata://text/plain,<?php echo $_SERVER[DOCUMENT_ROOT]?> 5构造下面url&#xff0c;读取该路径的…

Alibaba --- 如何写好 Prompt ?

如何写好 Prompt 提示工程&#xff08;Prompt Engineering&#xff09;是一项通过优化提示词&#xff08;Prompt&#xff09;和生成策略&#xff0c;从而获得更好的模型返回结果的工程技术。总体而言&#xff0c;其实现逻辑如下&#xff1a; &#xff08;注&#xff1a;示例图…

【C++杂货铺】模板进阶

目录 &#x1f308;前言&#x1f308; &#x1f4c1; 泛型编程 &#x1f4c1; 函数模板 &#x1f4c2; 概念 &#x1f4c2; 格式 &#x1f4c2; class 和 typename &#x1f4c2; 原理 &#x1f4c2; 函数模板实例化 &#x1f4c2; 匹配原则 &#x1f4c1; 类模板 &#x1…

全球历年GDP增长率_探数API数据统计

以下是数据的详细说明&#xff1a; 全球GDP增长最快的年份是1964年&#xff0c;全球GDP增速达到6.65%。2021年的GDP增长率也相当高&#xff0c;主要受2020年衰退后的恢复性增长推动。 全球GDP增长最慢的年份包括&#xff1a;1974年、1975年&#xff08;第一次石油危机引发&…

clion最新安装教程

还在用Dev-C吗&#xff1f;也尝试了很多C编辑器&#xff0c;不是太老&#xff0c;就是太复杂。对于c开发者来说clion真的好用&#xff0c;CLion是一款专为开发C及C所设计的跨平台IDE。难受的是cion并不免费&#xff0c;仿佛是在证明好货不贵的道理&#xff0c;只能免费用30天。…

2024年阿里云优惠券领取攻略

阿里云作为国内领先的云计算服务提供商&#xff0c;以其稳定、高效、安全的服务赢得了众多用户的青睐。为了吸引用户上云&#xff0c;阿里云经常推出各种优惠活动&#xff0c;其中就包括阿里云优惠券。本文将为大家详细解读2024年阿里云优惠券的领券攻略&#xff0c;帮助大家轻…

【WinForm】如何在自己的程序窗口中显示并调用外部桌面程序

当你爱上一个程序的功能&#xff0c;并且希望扩展它以满足自己的需求时&#xff0c;你可能会觉得困惑。毕竟&#xff0c;你已经为此付出了很多努力&#xff0c;并希望能够有效地整合这些功能。那么&#xff0c;是否可以将这些功能嵌套到自己的程序中呢&#xff1f; 首先&#…

【操作系统专题】详解操作系统 | 操作系统的目标和功能 | 操作系统如何工作

&#x1f341;你好&#xff0c;我是 RO-BERRY &#x1f4d7; 致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f384;感谢你的陪伴与支持 &#xff0c;故事既有了开头&#xff0c;就要画上一个完美的句号&#xff0c;让我们一起加油 目录 1.操作系统的目标和功能2…

计算机炸了,电子信息也是劝退专业?

还不是因为这个版本&#xff0c;计算机专业受到了制裁&#xff0c;导致这些偏计算机类的专业也受到了牵连 我本科的时候是一所双一流院校的计科专业&#xff0c;我们学校的电子信息专业堪称苦逼&#xff0c;我们计科学的东西&#xff0c;他们都要学&#xff0c;他们学的一些东…

如何使用 LangChain 构建基于LLMs的应用——入门指南

大型语言模型(LLMs)是非常强大的通用推理工具&#xff0c;在各种情况下都非常有用。 但是&#xff0c;与构建传统软件不同&#xff0c;使用LLMs存在一些挑战&#xff1a; 调用往往是长时间运行的&#xff0c;并且随着可用输出而逐步生成输出。与固定参数的结构化输入&#xf…