SpringBoot 配置 okhttp3的操作

时间:2021-10-03 16:53:54

1. Maven 添加依赖

?
1
2
3
4
5
<dependency>
 <groupId>com.squareup.okhttp3</groupId>
 <artifactId>okhttp</artifactId>
 <version>3.10.0</version>
</dependency>

2. application.properties 配置文件

?
1
2
3
4
5
6
7
ok.http.connect-timeout=30
ok.http.read-timeout=30
ok.http.write-timeout=30
# 连接池中整体的空闲连接的最大数量
ok.http.max-idle-connections=200
# 连接空闲时间最多为 300
ok.http.keep-alive-duration=300

3. OkHttpConfiguration 配置类

?
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
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.net.ssl.*;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;
/**
 * @author Answer.AI.L
 * @date 2019-04-09
 */
@Configuration
public class OkHttpConfiguration {
 @Value("${ok.http.connect-timeout}")
 private Integer connectTimeout;
 @Value("${ok.http.read-timeout}")
 private Integer readTimeout;
 @Value("${ok.http.write-timeout}")
 private Integer writeTimeout;
 @Value("${ok.http.max-idle-connections}")
 private Integer maxIdleConnections;
 @Value("${ok.http.keep-alive-duration}")
 private Long keepAliveDuration;
 @Bean
 public OkHttpClient okHttpClient() {
  return new OkHttpClient.Builder()
    .sslSocketFactory(sslSocketFactory(), x509TrustManager())
    // 是否开启缓存
    .retryOnConnectionFailure(false)
    .connectionPool(pool())
    .connectTimeout(connectTimeout, TimeUnit.SECONDS)
    .readTimeout(readTimeout, TimeUnit.SECONDS)
    .writeTimeout(writeTimeout,TimeUnit.SECONDS)
    .hostnameVerifier((hostname, session) -> true)
    // 设置代理
//      .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 8888)))
    // 拦截器
//    .addInterceptor()
    .build();
 }
 @Bean
 public X509TrustManager x509TrustManager() {
  return new X509TrustManager() {
   @Override
   public void checkClientTrusted(X509Certificate[] chain, String authType)
     throws CertificateException {
   }
   @Override
   public void checkServerTrusted(X509Certificate[] chain, String authType)
     throws CertificateException {
   }
   @Override
   public X509Certificate[] getAcceptedIssuers() {
    return new X509Certificate[0];
   }
  };
 }
 @Bean
 public SSLSocketFactory sslSocketFactory() {
  try {
   // 信任任何链接
   SSLContext sslContext = SSLContext.getInstance("TLS");
   sslContext.init(null, new TrustManager[]{x509TrustManager()}, new SecureRandom());
   return sslContext.getSocketFactory();
  } catch (NoSuchAlgorithmException | KeyManagementException e) {
   e.printStackTrace();
  }
  return null;
 }
 @Bean
 public ConnectionPool pool() {
  return new ConnectionPool(maxIdleConnections, keepAliveDuration, TimeUnit.SECONDS);
 }
}

4. OkHttp 类

?
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
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
 * @author Answer.AI.L
 * @date 2019-04-09
 */
@Slf4j
@Component
public class OkHttpCli {
 private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
 private static final MediaType XML = MediaType.parse("application/xml; charset=utf-8");
 @Autowired
 private OkHttpClient okHttpClient;
 /**
  * get 请求
  * @param url  请求url地址
  * @return string
  * */
 public String doGet(String url) {
  return doGet(url, null, null);
 }
 /**
  * get 请求
  * @param url  请求url地址
  * @param params 请求参数 map
  * @return string
  * */
 public String doGet(String url, Map<String, String> params) {
  return doGet(url, params, null);
 }
 /**
  * get 请求
  * @param url  请求url地址
  * @param headers 请求头字段 {k1, v1 k2, v2, ...}
  * @return string
  * */
 public String doGet(String url, String[] headers) {
  return doGet(url, null, headers);
 }
 /**
  * get 请求
  * @param url  请求url地址
  * @param params 请求参数 map
  * @param headers 请求头字段 {k1, v1 k2, v2, ...}
  * @return string
  * */
 public String doGet(String url, Map<String, String> params, String[] headers) {
  StringBuilder sb = new StringBuilder(url);
  if (params != null && params.keySet().size() > 0) {
   boolean firstFlag = true;
   for (String key : params.keySet()) {
    if (firstFlag) {
     sb.append("?").append(key).append("=").append(params.get(key));
     firstFlag = false;
    } else {
     sb.append("&").append(key).append("=").append(params.get(key));
    }
   }
  }
  Request.Builder builder = new Request.Builder();
  if (headers != null && headers.length > 0) {
   if (headers.length % 2 == 0) {
    for (int i = 0; i < headers.length; i = i + 2) {
     builder.addHeader(headers[i], headers[i + 1]);
    }
   } else {
    log.warn("headers's length[{}] is error.", headers.length);
   }
  }
  Request request = builder.url(sb.toString()).build();
  log.info("do get request and url[{}]", sb.toString());
  return execute(request);
 }
 /**
  * post 请求
  * @param url  请求url地址
  * @param params 请求参数 map
  * @return string
  */
 public String doPost(String url, Map<String, String> params) {
  FormBody.Builder builder = new FormBody.Builder();
  if (params != null && params.keySet().size() > 0) {
   for (String key : params.keySet()) {
    builder.add(key, params.get(key));
   }
  }
  Request request = new Request.Builder().url(url).post(builder.build()).build();
  log.info("do post request and url[{}]", url);
  return execute(request);
 }
 /**
  * post 请求, 请求数据为 json 的字符串
  * @param url  请求url地址
  * @param json  请求数据, json 字符串
  * @return string
  */
 public String doPostJson(String url, String json) {
  log.info("do post request and url[{}]", url);
  return exectePost(url, json, JSON);
 }
 /**
  * post 请求, 请求数据为 xml 的字符串
  * @param url  请求url地址
  * @param xml  请求数据, xml 字符串
  * @return string
  */
 public String doPostXml(String url, String xml) {
  log.info("do post request and url[{}]", url);
  return exectePost(url, xml, XML);
 }
 private String exectePost(String url, String data, MediaType contentType) {
  RequestBody requestBody = RequestBody.create(contentType, data);
  Request request = new Request.Builder().url(url).post(requestBody).build();
  return execute(request);
 }
 private String execute(Request request) {
  Response response = null;
  try {
   response = okHttpClient.newCall(request).execute();
   if (response.isSuccessful()) {
    return response.body().string();
   }
  } catch (Exception e) {
   log.error(ExceptionUtils.getStackTrace(e));
  } finally {
   if (response != null) {
    response.close();
   }
  }
  return "";
 }
}

5. 使用验证

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class AnswerController {
 @Autowired
 private OkHttpCli okHttpCli;
 
 
 @RequestMapping(value = "show", method = RequestMethod.GET)
 public String show() {
  String url = "https://www.baidu.com/";
 String message = okHttpCli.doGet(url);
  return message;
 }
 
}

6. 双向认证(待证)

?
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
@Bean
public SSLSocketFactory sslSocketFactory() {
 String certPath = "";
 String caPath = "";
 String certPwd = "";
 String caPwd = "";
 try {
  ClassPathResource selfcertPath = new ClassPathResource(certPath);
  ClassPathResource trustcaPath = new ClassPathResource(caPath);
  KeyStore selfCert = KeyStore.getInstance("pkcs12");
  selfCert.load(selfcertPath.getInputStream(), certPwd.toCharArray());
  KeyManagerFactory kmf = KeyManagerFactory.getInstance("sunx509");
  kmf.init(selfCert, certPwd.toCharArray());
  KeyStore caCert = KeyStore.getInstance("jks");
  caCert.load(trustcaPath.getInputStream(), caPwd.toCharArray());
  TrustManagerFactory tmf = TrustManagerFactory.getInstance("sunx509");
  tmf.init(caCert);
  SSLContext sslContext = SSLContext.getInstance("TLS");
  sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
  return sslContext.getSocketFactory();
 } catch (Exception e) {
  e.printStackTrace();
 }
 return null;
}

补充:Spring Cloud Feign 总结问题,注意点,性能调优,切换okhttp3

Feign常见问题总结

FeignClient接口如使用@PathVariable ,必须指定value属性

?
1
2
3
4
5
6
7
//在一些早期版本中, @PathVariable("id") 中的 "id" ,也就是value属性,必须指定,不能省略。
@FeignClient("microservice-provider-user")
public interface UserFeignClient {
 @RequestMapping(value = "/simple/{id}", method = RequestMethod.GET)
 public User findById(@PathVariable("id") Long id);
 ...
}

Java代码自定义Feign Client的注意点与坑

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@FeignClient(name = "microservice-provider-user", configuration = UserFeignConfig.class)
public interface UserFeignClient {
 @GetMapping("/users/{id}")
 User findById(@PathVariable("id") Long id);
}
/**
 * 该Feign Client的配置类,注意:
 * 1. 该类可以独立出去;
 * 2. 该类上也可添加@Configuration声明是一个配置类;
 * 配置类上也可添加@Configuration注解,声明这是一个配置类;
 * 但此时千万别将该放置在主应用程序上下文@ComponentScan所扫描的包中,
 * 否则,该配置将会被所有Feign Client共享,无法实现细粒度配置!
 * 个人建议:像我一样,不加@Configuration注解
 *
 * @author zhouli
 */
class UserFeignConfig {
 @Bean
 public Logger.Level logger() {
 return Logger.Level.FULL;
 }
}

配置类上也可添加@Configuraiton 注解,声明这是一个配置类;但此时千万别将该放置在主应用程序上下文@ComponentScan 所扫描的包中,否则,该配置将会被所有Feign Client共享(相当于变成了通用配置,其实本质还是Spring父子上下文扫描包重叠导致的问题),无法实现细粒度配置!

个人建议:像我一样,不加@Configuration注解,省得进坑。

最佳实践:尽量用配置属性自定义Feign的配置!!!

@FeignClient 注解属性

?
1
2
3
4
//@FeignClient(name = "microservice-provider-user")
//在早期的Spring Cloud版本中,无需提供name属性,从Brixton版开始,@FeignClient必须提供name属性,否则应用将无法正常启动!
//另外,name、url等属性支持占位符。例如:
@FeignClient(name = "${feign.name}", url = "${feign.url}")

类级别的@RequestMapping会被Spring MVC加载

?
1
2
3
4
5
@RequestMapping("/users")
@FeignClient(name = "microservice-user")
public class TestFeignClient {
 // ...
}

类上的@RequestMapping 注解也会被Spring MVC加载。该问题现已经被解决,早期的版本有两种解决方案:方案1:不在类上加@RequestMapping 注解;方案2:添加如下代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
@ConditionalOnClass({ Feign.class })
public class FeignMappingDefaultConfiguration {
 @Bean
 public WebMvcRegistrations feignWebRegistrations() {
  return new WebMvcRegistrationsAdapter() {
   @Override
   public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
    return new FeignFilterRequestMappingHandlerMapping();
   }
  };
 }
 private static class FeignFilterRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
  @Override
  protected boolean isHandler(Class<?> beanType) {
   return super.isHandler(beanType) && !beanType.isInterface();
  }
 }
}

首次请求失败Ribbon的饥饿加载(eager-load)模式

如需产生Hystrix Stream监控信息,需要做一些额外操作Feign本身已经整合了Hystrix,可直接使用@FeignClient(value = "microservice-provider-user", fallback = XXX.class) 来指定fallback类,fallback类继承@FeignClient所标注的接口即可。

但是假设如需使用Hystrix Stream进行监控,默认情况下,访问http://IP:PORT/actuator/hystrix.stream 是会返回404,这是因为Feign虽然整合了Hystrix,但并没有整合Hystrix的监控。如何添加监控支持呢?需要以下几步:

第一步:添加依赖,示例:

?
1
2
3
4
5
<!-- 整合hystrix,其实feign中自带了hystrix,引入该依赖主要是为了使用其中的hystrix-metrics-event-stream,用于dashboard -->
<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>

第二步:在启动类上添加@EnableCircuitBreaker 注解,示例:

?
1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
@EnableCircuitBreaker
public class MovieFeignHystrixApplication {
 public static void main(String[] args) {
 SpringApplication.run(MovieFeignHystrixApplication.class, args);
 }
}

第三步:在application.yml中添加如下内容,暴露hystrix.stream端点:

?
1
2
3
4
5
management:
 endpoints:
 web:
  exposure:
  include: 'hystrix.stream'

这样,访问任意Feign Client接口的API后,再访问http://IP:PORT/actuator/hystrix.stream ,就会展示一大堆Hystrix监控数据了。

Feign 上传文件

加依赖

?
1
2
3
4
5
6
7
8
9
10
<dependency>
 <groupId>io.github.openfeign.form</groupId>
 <artifactId>feign-form</artifactId>
 <version>3.0.3</version>
</dependency>
<dependency>
 <groupId>io.github.openfeign.form</groupId>
 <artifactId>feign-form-spring</artifactId>
 <version>3.0.3</version>
</dependency>

编写Feign Client

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@FeignClient(name = "ms-content-sample", configuration = UploadFeignClient.MultipartSupportConfig.class)
public interface UploadFeignClient {
 @RequestMapping(value = "/upload", method = RequestMethod.POST,
   produces = {MediaType.APPLICATION_JSON_UTF8_VALUE},
   consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
 @ResponseBody
 String handleFileUpload(@RequestPart(value = "file") MultipartFile file);
 class MultipartSupportConfig {
  @Bean
  public Encoder feignFormEncoder() {
   return new SpringFormEncoder();
  }
 }
}

如代码所示,在这个Feign Client中,我们引用了配置类MultipartSupportConfig ,在MultipartSupportConfig 中,我们实例化了SpringFormEncoder 。这样这个Feign Client就能够上传啦。

注意点

?
1
2
3
4
//RequestMapping注解中的produeces 、consumes 不能少;
@RequestMapping(value = "/upload", method = RequestMethod.POST,
   produces = {MediaType.APPLICATION_JSON_UTF8_VALUE},
   consumes = MediaType.MULTIPART_FORM_DATA_VALUE)

接口定义中的注解@RequestPart(value = "file") 不能写成@RequestParam(value = "file") 。

最好将Hystrix的超时时间设长一点,例如5秒,否则可能文件还没上传完,Hystrix就超时了,从而导致客户端侧的报错。

Feign实现Form表单提交

添加依赖:

?
1
2
3
4
5
6
7
8
9
10
<dependency>
 <groupId>io.github.openfeign.form</groupId>
 <artifactId>feign-form</artifactId>
 <version>3.2.2</version>
</dependency>
<dependency>
 <groupId>io.github.openfeign.form</groupId>
 <artifactId>feign-form-spring</artifactId>
 <version>3.2.2</version>
</dependency>

Feign Client示例:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@FeignClient(name = "xxx", url = "http://www.itmuch.com/", configuration = TestFeignClient.FormSupportConfig.class)
public interface TestFeignClient {
 @PostMapping(value = "/test",
   consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE},
   produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}
   )
 void post(Map<String, ?> queryParam);
 class FormSupportConfig {
  @Autowired
  private ObjectFactory<HttpMessageConverters> messageConverters;
  // new一个form编码器,实现支持form表单提交
  @Bean
  public Encoder feignFormEncoder() {
   return new SpringFormEncoder(new SpringEncoder(messageConverters));
  }
  // 开启Feign的日志
  @Bean
  public Logger.Level logger() {
   return Logger.Level.FULL;
  }
 }
}

调用示例:

?
1
2
3
4
5
6
7
8
@GetMapping("/user/{id}")
public User findById(@PathVariable Long id) {
 HashMap<String, String> param = Maps.newHashMap();
 param.put("username","zhangsan");
 param.put("password","pwd");
 this.testFeignClient.post(param);
 return new User();
}

日志:

?
1
2
3
4
5
6
7
...[TestFeignClient#post] ---> POST http://www.baidu.com/test HTTP/1.1
...[TestFeignClient#post] Accept: application/json;charset=UTF-8
...[TestFeignClient#post] Content-Type: application/x-www-form-urlencoded; charset=UTF-8
...[TestFeignClient#post] Content-Length: 30
...[TestFeignClient#post]
...[TestFeignClient#post] password=pwd&username=zhangsan
...[TestFeignClient#post] ---> END HTTP (30-byte body)

由日志可知,此时Feign已能使用Form表单方式提交数据。

Feign GET请求如何构造多参数

假设需请求的URL包含多个参数,例如http://microservice-provider-user/get?id=1&username=张三 ,该如何使用Feign构造呢?我们知道,Spring Cloud为Feign添加了Spring MVC的注解支持,那么我们不妨按照Spring MVC的写法尝试一下:

?
1
2
3
4
5
@FeignClient("microservice-provider-user")
public interface UserFeignClient {
 @RequestMapping(value = "/get", method = RequestMethod.GET)
 public User get0(User user);
}

然而,这种写法并不正确,控制台会输出类似如下的异常。

?
1
2
feign.FeignException: status 405 reading UserFeignClient#get0(User); content:
{"timestamp":1482676142940,"status":405,"error":"Method Not Allowed","exception":"org.springframework.web.HttpRequestMethodNotSupportedException","message":"Request method 'POST' not supported","path":"/get"}

由异常可知,尽管我们指定了GET方法,Feign依然会使用POST方法发送请求。于是导致了异常。正确写法如下

方法一[推荐]注意:使用该方法无法使用Fegin的继承模式

?
1
2
3
4
5
@FeignClient("microservice-provider-user")
public interface UserFeignClient {
 @GetMapping("/get")
 public User get0(@SpringQueryMap User user);
}

方法二[推荐]

?
1
2
3
4
5
@FeignClient(name = "microservice-provider-user")
public interface UserFeignClient {
 @RequestMapping(value = "/get", method = RequestMethod.GET)
 public User get1(@RequestParam("id") Long id, @RequestParam("username") String username);
}

这是最为直观的方式,URL有几个参数,Feign接口中的方法就有几个参数。使用@RequestParam注解指定请求的参数是什么。

方法三[不推荐]多参数的URL也可使用Map来构建。当目标URL参数非常多的时候,可使用这种方式简化Feign接口的编写。

?
1
2
3
4
5
@FeignClient(name = "microservice-provider-user")
public interface UserFeignClient {
 @RequestMapping(value = "/get", method = RequestMethod.GET)
 public User get2(@RequestParam Map<String, Object> map);
}

在调用时,可使用类似以下的代码。

?
1
2
3
4
5
6
public User get(String username, String password) {
 HashMap<String, Object> map = Maps.newHashMap();
 map.put("id", "1");
 map.put("username", "张三");
 return this.userFeignClient.get2(map);
}

注意:这种方式不建议使用。主要是因为可读性不好,而且如果参数为空的时候会有一些问题,例如map.put("username", null); 会导致服务调用方(消费者服务)接收到的username是"" ,而不是null。

切换为 Okhttp3 提升 QPS 性能优化

加依赖引入okhttp3

?
1
2
3
4
5
<dependency>
 <groupId>io.github.openfeign</groupId>
 <artifactId>feign-okhttp</artifactId>
 <version>${version}</version>
</dependency>

写配置

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
feign:
 # feign启用hystrix,才能熔断、降级
 # hystrix:
 # enabled: true
 # 启用 okhttp 关闭默认 httpclient
 httpclient:
 enabled: false #关闭httpclient
 # 配置连接池
 max-connections: 200 #feign的最大连接数
 max-connections-per-route: 50 #fegin单个路径的最大连接数
 okhttp:
 enabled: true
 # 请求与响应的压缩以提高通信效率
 compression:
 request:
  enabled: true
  min-request-size: 2048
  mime-types: text/xml,application/xml,application/json
 response:
  enabled: true

参数配置

?
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
/**
 * 配置 okhttp 与连接池
 * ConnectionPool 默认创建5个线程,保持5分钟长连接
 */
@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class) //SpringBoot自动配置
public class OkHttpConfig {
 // 默认老外留给你彩蛋中文乱码,加上它就 OK
 @Bean
 public Encoder encoder() {
  return new FormEncoder();
 }
 @Bean
 public okhttp3.OkHttpClient okHttpClient() {
  return new okhttp3.OkHttpClient.Builder()
    //设置连接超时
    .connectTimeout(10, TimeUnit.SECONDS)
    //设置读超时
    .readTimeout(10, TimeUnit.SECONDS)
    //设置写超时
    .writeTimeout(10, TimeUnit.SECONDS)
    //是否自动重连
    .retryOnConnectionFailure(true)
    .connectionPool(new ConnectionPool(10, 5L, TimeUnit.MINUTES))
    .build();
 }
}

以上为个人经验,希望能给大家一个参考,也希望大家多多支持服务器之家。如有错误或未考虑完全的地方,望不吝赐教。

原文链接:https://jaemon.blog.csdn.net/article/details/89103162