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