Feign
OpenFeign是Netflix 開發的聲明式、模板化的HTTP請求客戶端。可以更加便捷、優雅地調用http api。
OpenFeign會根據帶有注解的函數信息構建出網絡請求的模板,在發送網絡請求之前,OpenFeign會將函數的參數值設置到這些請求模板中。
feign主要是構建微服務消費端。只要使用OpenFeign提供的注解修飾定義網絡請求的接口類,就可以使用該接口的實例發送RESTful的網絡請求。還可以集成Ribbon和Hystrix,提供負載均衡和斷路器。
英文表意為“假裝,偽裝,變形”, 是一個 Http 請求調用的輕量級框架,可以以 Java 接口注解的方式調用 Http 請求,而不用像 Java 中通過封裝 HTTP 請求報文的方式直接調用。通過處理注解,將請求模板化,當實際調用的時候,傳入參數,根據參數再應用到請求上,進而轉化成真正的請求,這種請求相對而言比較直觀。Feign 封裝 了HTTP 調用流程,面向接口編程,回想第一節課的SOP。
Feign和OpenFeign的關系
Feign本身不支持Spring MVC的注解,它有一套自己的注解
OpenFeign是Spring Cloud 在Feign的基礎上支持了Spring MVC的注解,如@RequesMapping等等。
OpenFeign的@FeignClient
可以解析SpringMVC的@RequestMapping注解下的接口,
並通過動態代理的方式產生實現類,實現類中做負載均衡並調用其他服務。
聲明式服務調用
provider方提供公用API包,Feign通過SpringMVC的注解來加載URI
1.創建項目User-Provider
選擇依賴
2.創建項目User-API
依賴 spring-boot-starter-web
創建一個接口 RegisterApi
package com.mashibing.UserAPI;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 用戶操作相關接口
* @author
*
*/
@RequestMapping("/User")
public interface RegisterApi {
@GetMapping("/isAlive")
public String isAlive();
}
3.User-Provider 實現API
配置文件
eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/
server.port=81
spring.application.name=user-provider
引入API
1.maven install User-Api項目
2.User-Provider的Pom.xml添加依賴
<dependency>
<groupId>com.mashibing.User-API</groupId>
<artifactId>User-API</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
創建UserController
實現Api的接口
package com.mashibing.UserProvider;
import com.mashibing.UserAPI.RegisterApi;
@RestController
public class UserController implements RegisterApi {
@Override
public String isAlive() {
// TODO Auto-generated method stub
return "ok";
}
}
4.Consumer調用
創建項目User-Consumer
依賴
引入API
Pom.xml添加依賴
<dependency>
<groupId>com.mashibing.User-API</groupId>
<artifactId>User-API</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
配置文件
eureka.client.service-url.defaultZone=http://euk1.com:7001/eureka/
server.port=90
spring.application.name=consumer
創建Service接口
package com.mashibing.UserConsumer;
import org.springframework.cloud.openfeign.FeignClient;
import com.mashibing.UserAPI.RegisterApi;
@FeignClient(name = "user-provider")
public interface UserConsumerService extends RegisterApi {
}
創建Controller
package com.mashibing.UserConsumer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ConsumerController {
@Autowired
UserConsumerService consumerSrv;
@GetMapping("/alive")
public String alive() {
return consumerSrv.isAlive();
}
}
修改啟動類
package com.mashibing.UserConsumer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
public class UserConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(UserConsumerApplication.class, args);
}
}
5.測試
訪問 http://localhost:90/alive 即可完成聲明式遠程服務調用
Get和Post
Feign默認所有帶參數的請求都是Post,想要使用指定的提交方式需引入依賴
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
並指明提交方式
@RequestMapping(value = "/alived", method = RequestMethod.POST)
@GetMapping("/findById")
帶參請求
@GetMapping("/findById")
public Map findById(@RequestParam("id") Integer id);
@PostMapping("/register")
public Map<String, String> reg(@RequestBody User user);
權限
feign的默認配置類是:org.springframework.cloud.openfeign.FeignClientsConfiguration。默認定義了feign使用的編碼器,解碼器等。
允許使用@FeignClient的configuration的屬性自定義Feign配置。自定義的配置優先級高於上面的FeignClientsConfiguration。
通過權限的例子,學習feign的自定義配置。
服務提供者。上述例子開放service-valuation的權限 后,訪問。
開放權限:
<!-- 安全認證 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 關閉csrf
http.csrf().disable();
// 表示所有的訪問都必須認證,認證處理后才可以正常進行
http.httpBasic().and().authorizeRequests().anyRequest().fullyAuthenticated();
// 所有的rest服務一定要設置為無狀態,以提升操作效率和性能
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
spring:
security:
user:
name: root
password: root
繼續feign原來訪問,報錯。401。
有如下兩種方式:
- 自定義配置類。
- 增加攔截器。
自定義配置
配置類:
public class FeignAuthConfiguration {
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("root", "root");
}
}
在feign上加配置
@FeignClient(name = "service-valuation",configuration = FeignAuthConfiguration.class)
OK,可以正常訪問了。
小結:如果在配置類上添加了@Configuration注解,並且該類在@ComponentScan所掃描的包中,那么該類中的配置信息就會被所有的@FeignClient共享。最佳實踐是:不指定@Configuration注解(或者指定configuration,用注解忽略),而是手動:
@FeignClient(name = "service-valuation",configuration = FeignAuthConfiguration.class)
攔截器
import feign.RequestInterceptor;
import feign.RequestTemplate;
public class MyBasicAuthRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// TODO Auto-generated method stub
template.header("Authorization", "Basic cm9vdDpyb290");
}
}
feign:
client:
config:
service-valuation:
request-interceptors:
- com.online.taxi.passenger.feign.interceptor.MyBasicAuthRequestInterceptor
代碼中取消上面的配置,訪問,報401.用下面的方式。
屬性定義
- 接上面例子,此例子和上面例子實現的功能一樣。記得兩者取一個即可。說明用屬性而不是用屬性中的configuration。
定義攔截器
public class MyBasicAuthRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// TODO Auto-generated method stub
template.header("Authorization", "Basic cm9vdDpyb290");
}
}
配置文件
feign:
client:
config:
service-valuation:
request-interceptors:
- com.online.taxi.passenger.feign.interceptor.MyBasicAuthRequestInterceptor
再次訪問,測試Ok。
- 擴展
指定服務名稱配置:
feign:
client:
config:
service-valuation:
connect-timeout: 5000
read-timeout: 5000
logger-level: full
通用配置
feign:
client:
config:
default:
connect-timeout: 5000
read-timeout: 5000
logger-level: full
屬性配置比Java代碼優先級高。也可通過配置設置java代碼優先級高。
feign:
client:
default-to-properties: false
feign在方法上可以設置:@RequestMapping,@ResponseBody。
方法中的參數可以設置:@RequestBody等等,Spring MVC中的注解。
推薦使用yml配置方式,在yml中按 代碼提示鍵,可以看到所有配置。
原理
- 主程序入口添加@EnableFeignClients注解開啟對Feign Client掃描加載處理。根據Feign Client的開發規范,定義接口並加@FeignClient注解。
- 當程序啟動時,會進行包掃描,掃描所有@FeignClient注解的類,並將這些信息注入Spring IoC容器中。當定義的Feign接口中的方法被調用時,通過JDK的代理方式,來生成具體的RequestTemplate。當生成代理時,Feign會為每個接口方法創建一個RequestTemplate對象,該對象封裝了HTTP請求需要的全部信息,如請求參數名、請求方法等信息都在這個過程中確定。
- 然后由RequestTemplate生成Request,然后把這個Request交給client處理,這里指的Client可以是JDK原生的URLConnection、Apache的Http Client,也可以是Okhttp。最后Client被封裝到LoadBalanceClient類,這個類結合Ribbon負載均衡發起服務之間的調用。
壓縮
服務端provider配置
#服務端開啟壓縮
server.compression.enabled=true
調用方consumer配置
#配置請求GZIP壓縮
feign.compression.request.enabled=true
#配置響應GZIP壓縮
feign.compression.response.enabled=true
#單位是B
feign.compression.request.min-request-size=100
請求
API
@FeignClient(name = "user-provider")
public interface ConsumerApi extends UserApi {
@GetMapping("/getMap")
Map<Integer, String> getMap(@RequestParam("id") Integer id);
@GetMapping("/getMap2")
Map<Integer, String> getMap2(@RequestParam("id") Integer id,@RequestParam("name") String name);
@GetMapping("/getMap3")
Map<Integer, String> getMap3(@RequestParam Map<String, Object> map);
@PostMapping("/postMap")
Map<Integer, String> postMap(Map<String, Object> map);
}
Controller
package com.mashibing.UserConsumer;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.mashibing.UserAPI.UserApi;
@RestController
public class MainController {
@Autowired
ConsumerApi api;
@Autowired
MashibingApi mapi;
@GetMapping("/alive")
public String alive() {
/**
* URL 不能變
* jar文檔
*/
return api.alive();
}
@GetMapping("/vip")
public String vip() {
return mapi.getVip();
}
@GetMapping("/map")
public Map<Integer, String> map(Integer id) {
System.out.println(id);
return api.getMap(id);
}
@GetMapping("/map2")
public Map<Integer, String> map2(Integer id,String name) {
System.out.println(id);
return api.getMap2(id,name);
}
@GetMapping("/map3")
public Map<Integer, String> map3(@RequestParam Map<String, Object> map) {
// System.out.println(id);
// HashMap<String, Object> map = new HashMap<>(2);
//
// map.put("id", id);
// map.put("name", name);
// syso
System.out.println(map);
return api.getMap3(map);
}
@GetMapping("/map4")
public Map<Integer, String> map4(@RequestParam Map<String, Object> map) {
// System.out.println(id);
// HashMap<String, Object> map = new HashMap<>(2);
//
// map.put("id", id);
// map.put("name", name);
// syso
System.out.println(map);
return api.postMap(map);
}
}
Provider
package com.mashibing.UserProvider;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.mashibing.UserAPI.UserApi;
@RestController
public class UserController implements UserApi {
@Value("${server.port}")
String port;
private AtomicInteger count = new AtomicInteger();
@Override
public String alive() {
try {
System.out.println("准備睡");
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
int i = count.getAndIncrement();
System.out.println("====好的第:" + i + "次調用");
return "port:" + port;
}
@Override
public String getById(Integer id) {
// TODO Auto-generated method stub
return null;
}
@GetMapping("/getMap")
public Map<Integer, String> getMap(@RequestParam("id") Integer id) {
// TODO Auto-generated method stub
System.out.println(id);
return Collections.singletonMap(id, "mmeme");
}
@GetMapping("/getMap2")
public Map<Integer, String> getMap2(Integer id,String name) {
// TODO Auto-generated method stub
System.out.println(id);
return Collections.singletonMap(id, name);
}
@GetMapping("/getMap3")
public Map<Integer, String> getMap3(@RequestParam Map<String, Object> map) {
// TODO Auto-generated method stub
System.out.println(map);
return Collections.singletonMap(Integer.parseInt(map.get("id").toString()), map.get("name").toString());
}
@PostMapping("/postMap")
public Map<Integer, String> postMap(@RequestBody Map<String, Object> map) {
// TODO Auto-generated method stub
System.out.println(map);
return Collections.singletonMap(Integer.parseInt(map.get("id").toString()), map.get("name").toString());
}
}
開啟日志
配置文件
logging.level.com.mashibing.UserConsumer:debug
重寫日志等級
package com.mashibing.UserConsumer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import feign.Logger;
@Configuration
public class FeiginConfig {
@Bean
Logger.Level logLevel(){
return Logger.Level.BASIC;
}
}
超時
Feign默認支持Ribbon;Ribbon的重試機制和Feign的重試機制有沖突,所以源碼中默認關閉Feign的重試機制,使用Ribbon的重試機制
#連接超時時間(ms)
ribbon.ConnectTimeout=1000
#業務邏輯超時時間(ms)
ribbon.ReadTimeout=6000
重試
#同一台實例最大重試次數,不包括首次調用
ribbon.MaxAutoRetries=1
#重試負載均衡其他的實例最大重試次數,不包括首次調用
ribbon.MaxAutoRetriesNextServer=1
#是否所有操作都重試
ribbon.OkToRetryOnAllOperations=false
使用ribbon重試機制,請求失敗后,每個6秒會重新嘗試
Hystrix
spring cloud 用的是 hystrix,是一個容錯組件。
Hystrix實現了 超時機制和斷路器模式。
Hystrix是Netflix開源的一個類庫,用於隔離遠程系統、服務或者第三方庫,防止級聯失敗,從而提升系統的可用性與容錯性。主要有以下幾點功能:
- 為系統提供保護機制。在依賴的服務出現高延遲或失敗時,為系統提供保護和控制。
- 防止雪崩。
- 包裹請求:使用HystrixCommand(或HystrixObservableCommand)包裹對依賴的調用邏輯,每個命令在獨立線程中運行。
- 跳閘機制:當某服務失敗率達到一定的閾值時,Hystrix可以自動跳閘,停止請求該服務一段時間。
- 資源隔離:Hystrix為每個請求都的依賴都維護了一個小型線程池,如果該線程池已滿,發往該依賴的請求就被立即拒絕,而不是排隊等候,從而加速失敗判定。防止級聯失敗。
- 快速失敗:Fail Fast。同時能快速恢復。側重點是:(不去真正的請求服務,發生異常再返回),而是直接失敗。
- 監控:Hystrix可以實時監控運行指標和配置的變化,提供近實時的監控、報警、運維控制。
- 回退機制:fallback,當請求失敗、超時、被拒絕,或當斷路器被打開時,執行回退邏輯。回退邏輯我們自定義,提供優雅的服務降級。
- 自我修復:斷路器打開一段時間后,會自動進入“半開”狀態,可以進行打開,關閉,半開狀態的轉換。前面有介紹。
hystrix獨立使用脫離spring cloud
package com.mashibing.UserConsumer;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
public class HystrixTest extends HystrixCommand {
protected HystrixTest(HystrixCommandGroupKey group) {
super(group);
// TODO Auto-generated constructor stub
}
public static void main(String[] args) {
// HystrixTest hystrixTest = new HystrixTest(HystrixCommandGroupKey.Factory.asKey("ext"));
/**
* execute():以同步阻塞方式執行run()。以demo為例,調用execute()后,
* hystrix先創建一個新線程運行run(),
* 接着調用程序要在execute()調用處一直阻塞着,直到run()運行完成
*/
// System.out.println("result:" + hystrixTest.execute());
/**
* queue():以異步非阻塞方式執行run()。以demo為例,
* 一調用queue()就直接返回一個Future對象,
* 同時hystrix創建一個新線程運行run(),
* 調用程序通過Future.get()拿到run()的返回結果,
* 而Future.get()是阻塞執行的
*/
Future<String> futureResult = new HystrixTest(HystrixCommandGroupKey.Factory.asKey("ext")).queue();
String result = "";
try {
result = futureResult.get();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("程序結果:"+result);
}
@Override
protected Object run() throws Exception {
// TODO Auto-generated method stub
System.out.println("執行邏輯");
int i = 1/0;
return "ok";
}
@Override
protected Object getFallback() {
// TODO Auto-generated method stub
return "getFallbackgetFallback";
}
}
整合Resttemplate
Service
@HystrixCommand(fallbackMethod = "back")
public String alive() {
// 自動處理URL
RestTemplate restTemplate = new RestTemplate();
String url ="http://user-provider/User/alive";
String object = restTemplate.getForObject(url, String.class);
return object;
}
public String back() {
return "請求失敗~bbb...";
}
啟動類
@EnableCircuitBreaker
整合Feign
配置
feign.hystrix.enabled=true
接口
@FeignClient(name = "user-provider",fallback = AliveBack.class)
public interface ConsumerApi {
@RequestMapping(value = "/User/alive",method = RequestMethod.GET)
public String alive();
@RequestMapping(value = "/User/getById",method = RequestMethod.GET)
public String getById(Integer id);
}
實現
package com.mashibing.UserConsumer;
import java.util.Map;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
@Component
public class AliveBack implements ConsumerApi{
@Override
public String alive() {
// TODO Auto-generated method stub
return "aaa";
}
@Override
public String getById(Integer id) {
// TODO Auto-generated method stub
return null;
}
}
使用fallbackFactory檢查具體錯誤
實現類
package com.mashibing.UserConsumer;
import java.util.Map;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.springframework.stereotype.Component;
import com.mashibing.UserAPI.Person;
import feign.hystrix.FallbackFactory;
@Component
public class WebError implements FallbackFactory<ConsumerApi> {
@Override
public ConsumerApi create(Throwable cause) {
// TODO Auto-generated method stub
return new ConsumerApi() {
@Override
public Person postPserson(Person person) {
// TODO Auto-generated method stub
return null;
}
@Override
public String getById(Integer id) {
// TODO Auto-generated method stub
return null;
}
@Override
public String alive() {
// TODO Auto-generated method stub
System.out.println(cause.getLocalizedMessage());
cause.printStackTrace();
return ToStringBuilder.reflectionToString(cause);
}
@Override
public Map<Integer, String> postMap(Map<String, Object> map) {
// TODO Auto-generated method stub
return null;
}
@Override
public Map<Integer, String> getMap3(Map<String, Object> map) {
// TODO Auto-generated method stub
return null;
}
@Override
public Map<Integer, String> getMap2(Integer id, String name) {
// TODO Auto-generated method stub
return null;
}
@Override
public Map<Integer, String> getMap(Integer id) {
// TODO Auto-generated method stub
return null;
}
};
}
}
針對不同異常返回響應
@Override
public String alive() {
// TODO Auto-generated method stub
System.out.println(cause);
if(cause instanceof InternalServerError) {
System.out.println("InternalServerError");
return "遠程服務報錯";
}else if(cause instanceof RuntimeException) {
return "請求時異常:" + cause;
}else {
return "都算不上";
}
}
信號量隔離與線程隔離
默認情況下hystrix使用線程池控制請求隔離
線程池隔離技術,是用 Hystrix 自己的線程去執行調用;而信號量隔離技術,是直接讓 tomcat 線程去調用依賴服務。信號量隔離,只是一道關卡,信號量有多少,就允許多少個 tomcat 線程通過它,然后去執行。
信號量隔離主要維護的是Tomcat的線程,不需要內部線程池,更加輕量級。
配置
hystrix.command.default.execution.isolation.strategy 隔離策略,默認是Thread, 可選Thread|Semaphore
thread 通過線程數量來限制並發請求數,可以提供額外的保護,但有一定的延遲。一般用於網絡調用
semaphore 通過semaphore count來限制並發請求數,適用於無網絡的高並發請求
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds 命令執行超時時間,默認1000ms
hystrix.command.default.execution.timeout.enabled 執行是否啟用超時,默認啟用true
hystrix.command.default.execution.isolation.thread.interruptOnTimeout 發生超時是是否中斷,默認true
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests 最大並發請求數,默認10,該參數當使用ExecutionIsolationStrategy.SEMAPHORE策略時才有效。如果達到最大並發請求數,請求會被拒絕。理論上選擇semaphore size的原則和選擇thread size一致,但選用semaphore時每次執行的單元要比較小且執行速度快(ms級別),否則的話應該用thread。
semaphore應該占整個容器(tomcat)的線程池的一小部分。
Feign下配置
hystrix.command.default.execution.isolation.strategy=SEMAPHORE
開啟dashboard
啟動類
@EnableHystrixDashboard
引入依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>