有關微服務中,服務與服務如何通信,我已經給大家介紹了Ribbon遠程調用的相關知識,不知道大家有沒有發現Ribbon的問題呢?
Ribbon的問題
在Ribbon中,如果我們想要發起一個調用,是這樣的:
@Resource
private RestTemplate restTemplate
String result = restTemplate.getForObject("http://my-goods/goods/get", String.class);
Goods goods = JSONObject.parseObject(result, Goods.class);
這就像一個普通的http請求一樣,需要對入參和出參進行手動處理。
打一眼看上去好像沒什么問題,但仔細一想就不對勁了:這個被調用的接口都是我們自己寫的,入參和出參都是確定的,甚至寫被調用的接口的人都是同一個...有沒有一種更好的方式,比如像調用本地方法一樣直接調用它呢?
比如這樣:
Goods goods = goodsServer.get();
這個術語叫:遠程方法調用
今天的主角
Feign
就為我們實現了這樣的功能,下面有請~
什么是Feign
官方是這樣介紹的:Feign 是受Retrofit、JAXRS-2.0和WebSocket啟發的 Java 到 HTTP 客戶端binder(我也不知道這里怎么翻譯)。
為什么說是binder?在看Feign所實現的功能及出發點來說,Feign本身並未實現HTPP客戶端,而且在其他組件的客戶端的上層進行了增強,我們可以先來感受一下Feign所帶給我們的功能
客戶端
- java.net.URL
- Apache HTTP
- OK Http
- Ribbon
- Java 11 http2
- ....
規范
- Feign
- JAX-RS
- JAX-RS 2
- SOAP
- Spring 4
- ....
編解碼
- GSON
- JAXB
- Jackson
- ....
其他
- Hystrix
- SLF4J
- Mock
更多內容查看github: https://github.com/OpenFeign/feign
基本使用
1. 在商品服務中編寫crud
小伙伴可以自己隨便找個項目寫,主要就是搞幾個接口來調用
@RestController
@RequestMapping("/goods")
public class GoodsController {
@GetMapping("/get-goods")
public Goods getGoods(){
return new Goods().setName("蘋果")
.setPrice(1.1)
.setNumber(2);
}
@GetMapping("/list")
public List<Goods> list(){
ArrayList<Goods> goodsList = new ArrayList<>();
Goods apple = new Goods().setName("蘋果")
.setPrice(1.1)
.setNumber(2);
goodsList.add(apple);
Goods lemon = new Goods().setName("檸檬")
.setPrice(5.1)
.setNumber(3);
goodsList.add(lemon);
return goodsList;
}
@PostMapping("save")
public void save(@RequestBody Goods goods){
System.out.println(goods);
}
@DeleteMapping
public void delete(String id){
System.out.println(id);
}
}
2. 建立一個demo,引入依賴
<dependencies>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
</dependency>
<!-- 用於編解碼 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jackson</artifactId>
</dependency>
</dependencies>
3.編寫feign規范的接口
public interface GoodsApi {
@RequestLine("GET /goods/get-goods")
Goods getGoods();
@RequestLine("GET /goods/list")
List<Goods> list();
@RequestLine("POST /goods/save")
@Headers("Content-Type: application/json")
void save(Goods goods);
@RequestLine("DELETE /goods?id={id}")
@Headers("Content-Type: application/json")
void delete(@Param("id") String id);
}
Feign規范是什么相信小伙伴一看例子就懂了
4.測試
public class FeignDemo {
public static void main(String[] args) {
// 構建feign接口
GoodsApi goodsApi = Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(GoodsApi.class, "http://localhost:8082");
// 調用測試
System.out.println(goodsApi.getGoods());
System.out.println(goodsApi.list());
goodsApi.save(new Goods().setName("banana"));
goodsApi.delete("1");
}
}
靈光一閃
看到這里,不知道大家有沒有靈光一閃呢?
我要是把構建的代碼寫這樣
@Bean
public GoodsApi goodsApi(){
return Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(GoodsApi.class, "http://localhost:8082");
}
然后使用時直接注入,woc,這不是爽翻天?
比較常用的使用方式
回顧什么是Feign章節,除了上面基本使用之外,feign還支持Spring 4的規范,以及各種http客戶端(如okHttp),重試超時,日志等,我給大家介紹一個比較常用的方式
增加依賴
<!-- spring4規范 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-spring4</artifactId>
<version>10.10.1</version>
</dependency>
<!-- ribbon客戶端 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-ribbon</artifactId>
</dependency>
<!-- okhttp客戶端 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-slf4j</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
編寫接口
public interface Spring4GoodsApi {
@GetMapping("/goods/get-goods")
Goods getGoods();
@GetMapping("/goods/list")
List<Goods> list();
@PostMapping(value = "/goods/save", consumes = MediaType.APPLICATION_JSON_VALUE)
void save(Goods goods);
@DeleteMapping("/goods")
void delete(@RequestParam(value = "id") String id);
}
測試
public class Spring4FeignDemo {
public static void main(String[] args) {
Spring4GoodsApi goodsApi = Feign.builder()
// 使用spring4規范
.contract(new SpringContract())
// 使用jackson編解碼
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
// okhttp客戶端
.client(new OkHttpClient())
// 請求失敗重試,默認最大5次
.retryer(new Retryer.Default())
// 請求超時配置
.options(new Request.Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true))
// 日志配置,將在請求前后打印日志
.logger(new Slf4jLogger())
// 日志等級配置,BASIC:只打印請求路徑和響應狀態碼基本信息
.logLevel(Logger.Level.BASIC)
.target(Spring4GoodsApi.class, "http://localhost:8082");
System.out.println(goodsApi.getGoods());
System.out.println(goodsApi.list());
goodsApi.save(new Goods().setName("banana"));
goodsApi.delete("1");
}
}
攔截器
是個http客戶端就會有攔截器機制,用於在請求前統一做一些操作:比如添加請求頭
feign的攔截器使用方式如下:
public class AuthFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
System.out.println("進入攔截器");
HttpServletRequest request =
((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
template.header("token", request.getHeader("token"));
}
}
實現RequestInterceptor即可
在build時加入
Feign.builder()
.requestInterceptor(new AuthFeignInterceptor())
.target(Spring4GoodsApi.class, "http://localhost:8082");
以上就是Feign的常用方式了,學會之后,整合到Spring Cloud也是手到擒來。
整合Spring Cloud
官網文檔:https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/index.html
該整合樣例為訂單服務調用商品服務
直接三板斧走起
1. 加入依賴
在order-server中引入依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2. 增加注解
在Application類中加上注解EnableFeignClients
@EnableFeignClients
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(GoodsApplication.class, args);
}
}
任何一個配置類都可以,但是推薦在啟動類上加,因為注解默認掃描該注解類路徑下的所有包,然后啟動類又是在最頂端的,所以這樣就可以掃描到所有的包了。
當然,你也可以直接掃描某個包,畢竟一般feign接口都放在一起
3. 編寫配置
見配置章節
三板斧結束,開始編寫樣例
4. 編寫樣例
@FeignClient(name = "my-goods", path = "/goods", contextId = "goods")
public interface GoodsApi {
@GetMapping("/get-goods")
Goods getGoods();
/**
* get 方式傳參加上需@SpringQueryMap注解
*/
@GetMapping("/goods")
Goods getGoods(@SpringQueryMap Goods goods);
@GetMapping("/list")
List<Goods> list();
@PostMapping(value = "/save")
void save(Goods goods);
@DeleteMapping
void delete(String id);
}
FeignClient:
name: 調用的服務名稱
path: 路徑前綴,該類下的所有接口都會繼承該路徑
contextId: 用於區分不同的feign接口,因為一般來說一個服務不止一個feign接口,比如還有個GoodsDetailApi(商品詳情), 但是他們的name屬性是相同的,都是商品服務,所有需要一個contextId來區分不同的業務場景
其他的與常用方式相同
5. 測試
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource
private GoodsApi goodsApi;
@GetMapping("/get-goods")
public Goods getGoods(){
return goodsApi.getGoods();
}
}
配置
在常用方式中,我們構建一個feign接口的各種屬性,是通過硬編碼完成的,整合到spring之后,可以通過配置的方式完成了,更加的靈活。
日志
feign:
client:
config:
# 全局配置, 配置類里面的屬性名叫defaultConfig, 值卻是default, 注意不要搞錯
default:
loggerLevel: FULL
# 單獨服務配置 對應的是contextId 優先級更高
goods:
loggerLevel: BASIC
全局配置這里特別坑,不看源碼根本不知道怎么配,小伙伴一定要注意
客戶端
feign默認使用的HttpURLConnection
作為客戶端,小伙伴也可以替換成其他的客戶端
前言:所有的客戶端都為Client
接口的實現類,想要知道是否替換成功只需在對應的實現類中打個斷點
使用httpclient
引入依賴
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>10.10.1</version>
</dependency>
這樣就可以了,不用修改任何配置,這是因為當服務中包含ApacheHttpClient
的class時,httpClient的feign自動配置類就會生效,並且比默認的HttpURLConnection
自動配置類優先級更高。此時服務就將注入HttpClient作為客戶端。
源碼如下:
@Import 導入的順序是HttpClient, OkHttp, Default(HttpURLConnection)
該配置類的生效條件就是存在ApacheHttpClient類,而
feign.httpclient.enabled
配置默認不配也是生效。
使用OkHttp
引入依賴
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
由於我們在上一步引入了httpclient依賴,而httpclient的優先級比okhttp高,並且是默認生效,所以想要okhttp生效有兩種方式:
- 刪除掉httpclient的依賴
- 顯示關閉httpclient
這里我使用第二種方式
feign:
# 將httpclient關閉
httpclient:
enabled: false
# 將okhttp開啟
okhttp:
enabled: true
GZIP壓縮
有時當請求數據過大時,進行壓縮數據可以有效的提高請求性能,feign也提供這樣的配置方式
注意:壓縮只在非okhttp客戶端時生效
feign:
httpclient:
enabled: true
# 配置壓縮
compression:
request:
enabled: true
# 壓縮的類型,默認就是這些
mime-types: text/xml, application/xml, application/json
# 壓縮的字節最小閾值, 超過該大小才進行壓縮,默認1024
min-request-size: 10
這里我使用httpclient,不寫也可以,我只是為了讓大家知道我沒用okhttp
容錯重試
互聯網應用無法解決的問題之一:網絡分區。當被服務提供者出現這種情況,一直無法響應情況,我們也不可能讓服務消費者一直傻傻的等着,所以我們可以給服務配置一個超時時間,超過一定的時間被調用的服務未響應,就把請求掐斷。
feign的配置:
feign:
client:
config:
# 全局配置
default:
# 連接超時時間 單位毫秒 默認10秒
connectTimeout: 1000
# 請求超時時間 單位毫秒 默認60秒
readTimeout: 5000
互聯網應用無法解決的問題之二:網絡抖動。當經歷這種情況時,我們只能讓服務進行重試。
feign配置:
feign:
client:
config:
# 全局配置
default:
# 重試 默認重試5次
retryer: feign.Retryer.Default
攔截器
攔截器與常用方式相同,實現RequestInterceptor
接口即可
@Component
public class AuthFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
System.out.println("進入攔截器");
HttpServletRequest request =
((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
template.header("token", request.getHeader("token"));
}
}
加上@Component注解放到spring容器中,服務啟動時會自動加入到feign的攔截器鏈
也可以使用配置的方式
feign:
client:
config:
# 全局配置
default:
requestInterceptors:
- com.my.micro.service.order.interceptor.AuthFeignInterceptor
小結
本篇詳細介紹了一種遠程方法調用方式:Feign,現在,帶大家來簡單的回顧一下。
什么是Feign?
一種遠程方法調用客戶端,整合了ribbon,httpclient, okhttp, 支持各種各樣的規范,如Spring4
Feign的基本使用方式?
編輯接口,加上Feign支持的規范注解,使用Feign.Builder構建出代理類,發起調用。
如何整合SpringCloud?
引入依賴,加上@EnableFeignClients注解,根據需求增加配置
本篇內容基本涵蓋了Feign的常用方式,希望大家有所收獲,我們下期再見~
gittee: https://gitee.com/lzj960515/my-micro-service-demo
個人博客空間:https://zijiancode.cn/archives/feign
看完之后想必有所收獲吧~ 想要了解更多精彩內容,歡迎關注公眾號:程序員阿鑒,阿鑒在公眾號歡迎你的到來~