掘金 后端 ( ) • 2024-03-28 17:29

后端http请求

需求

最近一直在和其他系统进行对接,发现对接方直接给了一个http请求接口过来,但是咱们自己的业务和对方的http接口耦合太深的话,会有很多问题出现,比如请求超时或者连接超时导致业务接口报错,自己总结了一些封装方法

基本使用

jdk自己封装了http协议的一些基本功能,但是一般都会使用httpclient或者restTemplate

httpclient是Apache Jakarta Common 下的子项目

import org.apache.http.HttpEntity;  
import org.apache.http.HttpHost;  
import org.apache.http.HttpResponse;  
import org.apache.http.NameValuePair;  
import org.apache.http.client.HttpClient;  
import org.apache.http.client.config.RequestConfig;  
import org.apache.http.client.methods.HttpUriRequest;  
import org.apache.http.client.methods.RequestBuilder;  
import org.apache.http.conn.routing.HttpRoute;  
import org.apache.http.impl.client.CloseableHttpClient;  
import org.apache.http.impl.client.HttpClientBuilder;  
import org.apache.http.impl.client.HttpClients;  
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;  
import org.apache.http.message.BasicNameValuePair;  
import org.apache.http.util.EntityUtils;  
    
import java.io.IOException;  
import java.util.*;  
    
public class HttpClientUtils {  
    
    private static PoolingHttpClientConnectionManager connectionManager = null;  
    private static HttpClientBuilder httpBuilder = null;  
    private static RequestConfig requestConfig = null;  
    
    private static int MAXCONNECTION = 10;  
    
    private static int DEFAULTMAXCONNECTION = 5;  
    
    private static String IP = "cnivi.com.cn";  
    private static int PORT = 80;  
    
    static {  
        //设置http的状态参数  
        requestConfig = RequestConfig.custom()  
                .setSocketTimeout(5000)  
                .setConnectTimeout(5000)  
                .setConnectionRequestTimeout(5000)  
                .build();  
    
        HttpHost target = new HttpHost(IP, PORT);  
        connectionManager = new PoolingHttpClientConnectionManager();  
        connectionManager.setMaxTotal(MAXCONNECTION);//客户端总并行链接最大数  
        connectionManager.setDefaultMaxPerRoute(DEFAULTMAXCONNECTION);//每个主机的最大并行链接数  
        connectionManager.setMaxPerRoute(new HttpRoute(target), 20);  
        httpBuilder = HttpClients.custom();  
        httpBuilder.setConnectionManager(connectionManager);  
    }  
    
    public static CloseableHttpClient getConnection() {  
        CloseableHttpClient httpClient = httpBuilder.build();  
        return httpClient;  
    }  
    
    
    public static HttpUriRequest getRequestMethod(Map<String, String> map, String url, String method) {  
        List<NameValuePair> params = new ArrayList<NameValuePair>();  
        Set<Map.Entry<String, String>> entrySet = map.entrySet();  
        for (Map.Entry<String, String> e : entrySet) {  
            String name = e.getKey();  
            String value = e.getValue();  
            NameValuePair pair = new BasicNameValuePair(name, value);  
            params.add(pair);  
        }  
        HttpUriRequest reqMethod = null;  
        if ("post".equals(method)) {  
            reqMethod = RequestBuilder.post().setUri(url)  
                    .addParameters(params.toArray(new BasicNameValuePair[params.size()]))  
                    .setConfig(requestConfig).build();  
        } else if ("get".equals(method)) {  
            reqMethod = RequestBuilder.get().setUri(url)  
                    .addParameters(params.toArray(new BasicNameValuePair[params.size()]))  
                    .setConfig(requestConfig).build();  
        }  
        return reqMethod;  
    }  
    
    public static void main(String args[]) throws IOException {  
        Map<String, String> map = new HashMap<String, String>();  
        map.put("account", "");  
        map.put("password", "");  
    
        HttpClient client = getConnection();  
        HttpUriRequest post = getRequestMethod(map, "http://cnivi.com.cn/login", "post");  
        HttpResponse response = client.execute(post);  
    
        if (response.getStatusLine().getStatusCode() == 200) {  
            HttpEntity entity = response.getEntity();  
            String message = EntityUtils.toString(entity, "utf-8");  
            System.out.println(message);  
        } else {  
            System.out.println("请求失败");  
        }  
    }  
}  

restTemplate后面着重使用restTemplate实现功能

依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>5.2.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.7</version>
</dependency>

配置

import org.apache.http.client.HttpClient;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
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.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {
    
    /**
     * http连接管理器
     * @return
     */
    @Bean
    public HttpClientConnectionManager poolingHttpClientConnectionManager() {
        /*// 注册http和https请求
        Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", SSLConnectionSocketFactory.getSocketFactory())
                .build();
        PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(registry);*/
        
        PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
        // 最大连接数
        poolingHttpClientConnectionManager.setMaxTotal(500);
        // 同路由并发数(每个主机的并发)
        poolingHttpClientConnectionManager.setDefaultMaxPerRoute(100);
        return poolingHttpClientConnectionManager;
    }
    
    /**
     * HttpClient
     * @param poolingHttpClientConnectionManager
     * @return
     */
    @Bean
    public HttpClient httpClient(HttpClientConnectionManager poolingHttpClientConnectionManager) {
        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
        // 设置http连接管理器
        httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
        
        /*// 设置重试次数
        httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(3, true));*/
        
        // 设置默认请求头
        /*List<Header> headers = new ArrayList<>();
        headers.add(new BasicHeader("Connection", "Keep-Alive"));
        httpClientBuilder.setDefaultHeaders(headers);*/
        
        return httpClientBuilder.build();
    }
    
    /**
     * 请求连接池配置
     * @param httpClient
     * @return
     */
    @Bean
    public ClientHttpRequestFactory clientHttpRequestFactory(HttpClient httpClient) {
        HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
        // httpClient创建器
        clientHttpRequestFactory.setHttpClient(httpClient);
        // 连接超时时间/毫秒(连接上服务器(握手成功)的时间,超出抛出connect timeout)
        clientHttpRequestFactory.setConnectTimeout(5 * 1000);
        // 数据读取超时时间(socketTimeout)/毫秒(务器返回数据(response)的时间,超过抛出read timeout)
        clientHttpRequestFactory.setReadTimeout(10 * 1000);
        // 连接池获取请求连接的超时时间,不宜过长,必须设置/毫秒(超时间未拿到可用连接,会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool)
        clientHttpRequestFactory.setConnectionRequestTimeout(10 * 1000);
        return clientHttpRequestFactory;
    }
    
    /**
     * rest模板
     * @return
     */
    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
        // boot中可使用RestTemplateBuilder.build创建
        RestTemplate restTemplate = new RestTemplate();
        // 配置请求工厂
        restTemplate.setRequestFactory(clientHttpRequestFactory);
        return restTemplate;
    }
    
}

这里的restTemplate配置就配置好了,重要的参数就是从连接池获取连接的时间/连接超时时间/数据读取时间,最大连接数和默认请求头都可以使用时再添加

用一个获取token接口举例

public static TokenResponse token(TokenRequest tokenRequest) {
   TokenResponse data = new TokenResponse();
   RestTemplate restTemplate = getRestTemplate();
   //请求头
   HttpHeaders headers = new HttpHeaders();
   headers.set("Authorization", "Basic c2FiZXI6c2FiZXJfc2VjcmV0");
   headers.set("Tenant-Id", "000000");
   headers.set("Content-Type", "application/x-www-form-urlencoded");
   //map参数
   MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
   params.add("username", tokenRequest.getUsername());
   params.add("password", DigestUtils.md5Hex(tokenRequest.getPassword()));
   params.add("grant_type", tokenRequest.getGrant_type());
   params.add("scope", tokenRequest.getScope());
   params.add("tenantId", tokenRequest.getTenantId());
   HttpEntity entity = new HttpEntity(params, headers);
   data = restTemplate.postForObject(host + token_port + token_url, entity, TokenResponse.class);
   return data;
}

带附件的post请求

简单的使用已经是可以满足了,但是客户提了和对接系统传文件的需求,发现过程中出现了很多有意思的错误

对方的接口接收附件是这样的

image.png

我们知道后端使用http请求带附件的参数时,要模拟表单发送post请求,这里不清楚的可以看下多文件传参这篇--劝退记录:多文件feign接口传参 - 掘金 (juejin.cn)

一开始直接给参数体对象中传MultipartFile[]发现接收参数为null,然后就去查调用方法,最后实现方法为:

    /**
    * 发邮件
    *
    * @param request
    * @return
    */
   public static R<LinkedHashMap<String, Object>> receiveEmail(ReceiveEmailRequest request) {
      R<LinkedHashMap<String, Object>> result = new R<LinkedHashMap<String, Object>>();
      RestTemplate restTemplate = getRestTemplate();
      //请求头
      HttpHeaders headers = new HttpHeaders();
      headers.set("Authorization", "Basic c2FiZXI6c2FiZXJfc2VjcmV0");
      headers.setContentType(MediaType.MULTIPART_FORM_DATA);
      MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
      //处理附件
      if(request.getFileSystemResources() != null && request.getFileSystemResources().size() > 0){
         for (int i = 0; i < request.getFileSystemResources().size(); i++) {
            params.add("filesArr",request.getFileSystemResources().get(i));
         }
         params.add("fileName",request.getFileName());
      }else {
         params.add("fileName","[]");
      }
      //map参数
      params.add("sendId",request.getSendId());
      params.add("sendName",request.getSendName());
      params.add("sendAccount",request.getSendAccount());
      params.add("srcName",request.getSrcName());
      params.add("receiverName",request.getReceiverName());
      params.add("isSeparate",request.getIsSeparate());
      params.add("title",request.getTitle());
      params.add("body",request.getBody());
      params.add("ifTimed",request.getIfTimed());
      params.add("ownType",request.getOwnType());
      params.add("isUrgent",request.getIsUrgent());
      params.add("needReceipt",request.getNeedReceipt());
      params.add("isEncrypted",request.getIsEncrypted());
//    params.add("filesArr",request.getFiles());
      HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(params, headers);
      ResponseEntity<R> r = restTemplate.postForEntity(host + receive_port + receive_url, entity, R.class);
      if (r.getStatusCode().is2xxSuccessful()) {
         result = r.getBody();
      }
      return result;
   }

注意事项: 1.MultiValueMap<String, Object> 对象同key put值时会加入到现有的对象成为一个集合,所以多文件直接传一个key值就可以 2.请求头里的值一定要改为表单请求 headers.setContentType(MediaType.MULTIPART_FORM_DATA); 3.传文件的对象是FileSystemResources,需要经过本地临时文件处理,方法在下面

public static File getFileByte(String fileURL) {
   File file = null;
   File tempFile = null;
   byte[] fileBytes = null;
   try {
      // 创建URL对象
      URL url = new URL(fileURL);
      // 打开连接
      URLConnection connection = url.openConnection();
      // 获取输入流
      InputStream inputStream = connection.getInputStream();
      // 使用缓冲流提高读取性能
      BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
      // 将文件流转换为二进制数据
      ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
      byte[] buffer = new byte[1024];
      int bytesRead;
      while ((bytesRead = bufferedInputStream.read(buffer)) != -1) {
         byteArrayOutputStream.write(buffer, 0, bytesRead);
      }
      fileBytes = byteArrayOutputStream.toByteArray();
      // 关闭流
      byteArrayOutputStream.close();
      bufferedInputStream.close();
      // 假设originalFile是已知的File对象,且其具有正确的文件扩展名
      String filePath = url.getPath();
      int lastSlashIndex = filePath.lastIndexOf('/');
      String originalFileName = filePath.substring(lastSlashIndex + 1);
      int extensionIndex = originalFileName.lastIndexOf(".");
      String originalExtension = (extensionIndex != -1) ? originalFileName.substring(extensionIndex + 1) : "";
      if (!originalExtension.isEmpty()) {
         tempFile = File.createTempFile("temp", "." + originalExtension);
      } else {
         // 如果原始文件没有扩展名,则仍然使用".docx"
         tempFile = File.createTempFile("temp", ".docx");
      }
      FileOutputStream outputStream = new FileOutputStream(tempFile);
      outputStream.write(fileBytes);
      outputStream.close();
      file = tempFile;
   } catch (IOException e) {
      e.printStackTrace();
   }finally {
      //删除本地临时文件
      tempFile.deleteOnExit();
   }
   return file;
}