电竞比分网-中国电竞赛事及体育赛事平台

分享

關(guān)于OpenFeign那點(diǎn)事兒

 ygabg2pf4rzd4x 2022-11-16 發(fā)布于福建

引言

Hello 大家好,這里是Anyin。

在我們微服務(wù)開發(fā)過程中不可避免的會涉及到微服務(wù)之間的調(diào)用,例如:認(rèn)證Auth服務(wù)需要去用戶User服務(wù)獲取用戶信息。在Spring Cloud全家桶的背景下,我們一般都是使用Feign組件進(jìn)行服務(wù)之間的調(diào)用。

關(guān)于一般的Feign組件使用相信大家都很熟悉,但是在搭建整個微服務(wù)架構(gòu)的時(shí)候Feign組件遇到的問題也都熟悉嗎 ? 今天我們來聊一聊。

基礎(chǔ)使用

首先,我們先實(shí)現(xiàn)一個Feign組件的使用方法。

1.導(dǎo)入包

     <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-loadbalancer</artifactId>
     </dependency>


     <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-openfeign</artifactId>
     </dependency>

這里也導(dǎo)入了一個Loadbalancer組件,因?yàn)樵贔eign底層還會使用到負(fù)載均衡器進(jìn)行客戶端負(fù)載。

2.配置啟用FeignClient

@EnableFeignClients(basePackages = {
     "org.anyin.gitee.cloud.center.upms.api"
})

在我們的main入口的類上添加上@EnableFeignClients注解,并指定了包掃描的位置

3.編寫FeignClient接口

@FeignClient(name = "anyin-center-upms",
     contextId = "SysUserFeignApi",
     configuration = FeignConfig.class,
     path = "/api/sys-user")
public interface SysUserFeignApi {
 @GetMApping("/info/mobile")
 ApiResponse<SysUserResp> infoByMobile(@RequestParam("mobile") String mobile);
}

我們自定義了一個SysUserFeignApi接口,并且添加上了@FeignClient注解。相關(guān)屬性說明如下:

·name 應(yīng)用名,其實(shí)就是spring.application.name,用于標(biāo)識某個應(yīng)用,并且能從注冊中心拿到對應(yīng)的運(yùn)行實(shí)例信息

·contextId 當(dāng)你多個接口都使用了一樣的name值,則需要通過contextId來進(jìn)行區(qū)分

·configuration 指定了具體的配置類

·path 請求的前綴

1.編寫FeignClient接口實(shí)現(xiàn)

@RestController
@RequestMapping("/api/sys-user")
public class SysUserFeignController implements SysUserFeignApi {
 @Autowired
 private SysUserRepository sysUserRepository;
 @Autowired
 private SysUserConvert sysUserConvert;
 @Override
 public ApiResponse<SysUserResp> infoByMobile(@RequestParam("mobile") String mobile) {
     SysUser user = sysUserRepository.infoByMobile(mobile);
     SysUserResp resp = sysUserConvert.getSysUserResp(user);
     return ApiResponse.success(resp);
 }
}

這個就是一個簡單的Controller類和對應(yīng)的方法,用于根據(jù)手機(jī)號查詢用戶信息。

2.客戶端使用

@Component
@Slf4j
public class MobileInfoHandler{
 @Autowired
 private SysUserFeignApi sysUserFeignApi;
 @Override
 public SysUserResp info(String mobile) {
     SysUserResp sysUser = sysUserFeignApi.infoByMobile(mobile).getData();
     if(sysUser == null){
         throw AuthExCodeEnum.USER_NOT_REGISTER.getException();
     }
     return sysUser;
 }
}

這個是我們在客戶端服務(wù)使用Feign組件的代碼,它就像一個Service方法一樣,直接調(diào)用就行。無需處理請求和響應(yīng)過程中關(guān)于參數(shù)的轉(zhuǎn)換。

至此,我們的一個Feign組件基本使用的代碼就完成了。這個時(shí)候我們信心滿滿地趕緊運(yùn)行下我們代碼,測試下接口是否正常。

以上的代碼,是能夠正常運(yùn)行的。但是隨著我們遇到場景的增多,我們會發(fā)現(xiàn),理想很豐滿,顯示很骨感,以上的代碼并不能100%適應(yīng)我們遇到的場景。

接下來,我們來看看我們遇到哪些場景以及這些場景需要怎么解決。

場景一:日志

在以上的代碼中,因?yàn)槲覀兾醋鋈魏闻渲?,所以sysUserFeignApi.infoByMobile方法對于我們來講就像一個黑盒。

雖然我們傳遞了mobile值,但是不知道真實(shí)請求用戶服務(wù)的值是什么,是否有其他信息一起傳遞?雖然方法返回的參數(shù)是SysUserResp實(shí)體,但是我們不知道用戶服務(wù)返回的是什么,是否有其他信息一起返回?雖然我們知道Feign組件底層是http實(shí)現(xiàn),那么請求的過程是否有傳遞header信息?

這一切對我們來講就是一個黑盒,極大阻礙我們拔刀(排查問題)的速度。所以,我們需要配置日志,用于顯示請求過程中的所有信息傳遞。

在剛@FeignClient注解有個參數(shù),configuration 指定了具體的配置類,我們可以在這里指定日志的級別。如下:

public class FeignConfig {
    @Bean
    public Logger.Level loggerLevel(){
        return Logger.Level.FULL;
    }
}

接著還需要在配置文件指定具體FeignClient的日志級別為DEBUG。

logging:
  level:
    root: info
    org.anyin.gitee.cloud.center.upms.api.SysUserFeignApi: debug

這個時(shí)候,你在請求接口的時(shí)候,會發(fā)現(xiàn)多了好多日志。

關(guān)于OpenFeign那點(diǎn)事兒 - 使用篇

 

這里就可以詳細(xì)看到,在請求開始的時(shí)候攜帶的所有header信息以及請求參數(shù)信息,在響應(yīng)回來的時(shí)候通用打印了所有的響應(yīng)信息。

場景二:透傳header信息

在上一節(jié)中,我們在日志中看到了很多的請求頭header的信息,這些都是程序自己添加的嗎 ? 很明顯不是。例如x-application-name和x-request-id,這兩個參數(shù)就是我們自己添加的。

需要透傳header信息的場景,一般是出現(xiàn)在租戶ID或者請求ID的場景下。我們這里以請求ID為例,我們知道用戶的一個請求,可能會涉及多個服務(wù)實(shí)例,當(dāng)程序出現(xiàn)問題的時(shí)候?yàn)榱朔奖闩挪椋覀円话銜褂谜埱驣D來標(biāo)識一次用戶請求,并且這個請求ID貫穿所有經(jīng)過的服務(wù)實(shí)例,并且在日志中打印出來。這樣子,當(dāng)出現(xiàn)問題的時(shí)候,根據(jù)該請求ID就可以撈出本次用戶請求的所有日志信息。

關(guān)于請求ID打印到日志可以參考:
不會吧,你還不會用RequestId看日志 ?[1]

基于這種場景,我們需要手動設(shè)置透傳信息,F(xiàn)eign組件也給我們提供了對應(yīng)的方式。 只要實(shí)現(xiàn)了RequestInterceptor接口,即可透傳header信息。

public class FeignRequestInterceptor implements RequestInterceptor {
    @Value("${spring.application.name}")
    private String app;
    @Override
    public void apply(RequestTemplate template) {
        HttpServletRequest request = WebContext.getRequest();
        // job 類型的任務(wù),可能沒有Request
        if(request != null && request.getHeaderNames() != null){
            Enumeration<String> headerNames = request.getHeaderNames();
            while (headerNames.hasMoreElements()) {
                String name = headerNames.nextElement();
                // Accept值不傳遞,避免出現(xiàn)需要響應(yīng)xml的情況
                if ("Accept".equalsIgnoreCase(name)) {
                    continue;
                }
                String values = request.getHeader(name);
                template.header(name, values);
            }
        }
        template.header(CommonConstants.APPLICATION_NAME, app);
        template.header(CommonConstants.REQUEST_ID, RequestIdUtil.getRequestId().toString());
        template.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
    }
}

場景三:異常處理

在第一節(jié)我們客戶調(diào)用的時(shí)候,我們是沒有處理異常的,我們直接.getData()直接返回了,這其實(shí)是一個非常危險(xiǎn)的操作,.getData()的返回結(jié)果可能是null,很容易造成NPE的情況。

        SysUserResp sysUser = sysUserFeignApi.infoByMobile(mobile).getData();

回想下,當(dāng)我們調(diào)用其他當(dāng)前服務(wù)的Service方法的時(shí)候,如果遇到異常,是不是就是直接拋出異常,交由統(tǒng)一異常處理器進(jìn)行處理?所以,這里我們也是期望調(diào)用Feign和Service一樣,遇到異常,交由統(tǒng)一異常進(jìn)行處理。

如何處理這個需求呢? 我們可以在解碼的時(shí)候進(jìn)行處理。

@Slf4j
public class FeignDecoder implements Decoder {
    // 代理默認(rèn)的解碼器
    private Decoder decoder;
    public FeignDecoder(Decoder decoder) {
        this.decoder = decoder;
    }
    @Override
    public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {
        // 序列化為json
        String json = this.getResponseJson(response);
        this.processCommonException(json);
        return decoder.decode(response, type);
    }
    // 處理公共業(yè)務(wù)異常
    private void processCommonException(String json){
        if(!StringUtils.hasLength(json)){
            return;
        }
        ApiResponse resp = JSONUtil.toBean(json, ApiResponse.class);
        if(resp.getSuccess()){
            return;
        }
        log.info("feign response error: code={}, message={}", resp.getCode(), resp.getMessage());
        // 拋出我們期望的業(yè)務(wù)異常
        throw new CommonException(resp.getCode(), resp.getMessage());
    }
    // 響應(yīng)值轉(zhuǎn)json字符串
    private String getResponseJson(Response response) throws IOException {
        try (InputStream inputStream = response.body().asInputStream()) {
            return StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        }
    }
}

這里我們的處理方式是在解碼的時(shí)候,先從響應(yīng)結(jié)果中拿到是否有業(yè)務(wù)異常的判斷,如果有,則構(gòu)造業(yè)務(wù)異常實(shí)例,然后拋出信息。

運(yùn)行下代碼,我們會發(fā)現(xiàn)統(tǒng)一異常那邊還是無法處理由下游服務(wù)返回的異常,原因是雖然我們拋出了一個CommonException,但是其實(shí)最后還是會被Feign捕獲,然后重新封裝為DecodeException異常,再進(jìn)行拋出

Object decode(Response response, Type type) throws IOException {
    try {
      return decoder.decode(response, type);
    } catch (final FeignException e) {
      throw e;
    } catch (final RuntimeException e) {
      // 重新封裝異常
      throw new DecodeException(response.status(), e.getMessage(), response.request(), e);
    }
  }

所以,我們還需要在統(tǒng)一異常那邊再做下處理,代碼如下:

    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler(DecodeException.class)
    public ApiResponse decodeException(DecodeException ex){
        log.error("解碼失敗: {}", ex.getMessage());
        String id = RequestIdUtil.getRequestId().toString();
        if(ex.getCause() instanceof CommonException){
            CommonException ce = (CommonException)ex.getCause();
            return ApiResponse.error(id, ce.getErrorCode(), ce.getErrorMessage());
        }
        return ApiResponse.error(id, CommonExCodeEnum.DATA_PARSE_ERROR.getCode(), ex.getMessage());
    }

在運(yùn)行下代碼,我們就可以看到異常從用戶服務(wù)->認(rèn)證服務(wù)->網(wǎng)關(guān)->前端這么一個流程。

·用戶服務(wù)拋出的異常

關(guān)于OpenFeign那點(diǎn)事兒 - 使用篇

 

·認(rèn)證服務(wù)拋出的異常

關(guān)于OpenFeign那點(diǎn)事兒 - 使用篇

 

·前端顯示的異常

關(guān)于OpenFeign那點(diǎn)事兒 - 使用篇

 

場景四:時(shí)區(qū)問題

隨著業(yè)務(wù)的變化,我們可能會在請求參數(shù)或者響應(yīng)參數(shù)中增加關(guān)于Date類型的參數(shù),這個時(shí)候你會發(fā)現(xiàn),它的時(shí)區(qū)不對,少了8個小時(shí)。

這個問題其實(shí)是Jackson組件帶來的,該問題其實(shí)也有不同的解法。

1.在每個Date屬性添加上@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")2.傳參統(tǒng)一轉(zhuǎn)為yyyy-MM-dd HH:mm:ss格式的字符3.統(tǒng)一配置spring.jackson

很明顯,第三種解法最合適,我們在配置文件做如下的配置即可。

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

這里需要注意下,我們@FeignClient的配置是自定義配置的FeignConfig類,在自定義配置類中加載了解碼器,而解碼器依賴的是全局的HttpMessageConverters實(shí)例,和SpringMVC依賴的是同一個實(shí)例,所以該配置生效。有些場景下會自定義HttpMessageConverters,那么該配置則不生效。

public class FeignConfig {
    @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverters;
    // 自定義解碼器
    @Bean
    public Decoder decoder(ObjectProvider<HttpMessageConverterCustomizer> customizers){
        return new FeignDecoder(
                new OptionalDecoder(
                        new ResponseEntityDecoder(
                                new SpringDecoder(messageConverters, customizers))));
    }
}

其他問題

不知道細(xì)心的朋友是否有看到在第一節(jié)定義SysUserFeignApi接口的時(shí)候,我在@FeignClient注解上使用了一個屬性:path,并且接口上沒有使用@RequestMapping注解。

回想下,之前我們在使用Feign的時(shí)候,是不是這么使用的:

@FeignClient(name = "anyin-center-upms",
        contextId = "SysUserFeignApi",
        configuration = FeignConfig.class)
@RequestMapping("/api/sys-user")        
public interface SysUserFeignApi {}

這里不使用這個方式的原因是我當(dāng)前版本的Spring Cloud OpenFeign已經(jīng)不支持識別@RequestMapping注解了,它不會在請求的時(shí)候加入到請求的前綴,所以即使解決了@RequestMapping注解被SpringMVC識別為Controller類也無法正常運(yùn)行。

所以,這里使用了path屬性。

最后

以上,就是我們在使用OpenFeign組件的時(shí)候會遇到的大部分場景,你了解嗎 ?

相關(guān)源碼地址:Anyin Cloud[2]

References

[1] 不會吧,你還不會用RequestId看日志 ?: https:///post/7029880952666980388
[2] Anyin Cloud: https:///anyin/anyin-cloud

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多