OpenFeign服務接口調用

1.OpenFeign簡介

1.什麼是OpenFeign

  是一個聲明式的webService客戶端。使用OpenFeign能讓Web Service客戶端更加簡單。java

  它的使用方法是定義一個服務接口而後在上面添加註解。Feign也支持可拔插式的編碼器和解碼器。SpringCloud對Feign進行了封裝,使其支持了SpringMVC標準註解和HeetMessageConverters。Feign能夠與Eureka和Ribbon組合使用以支持負載均衡。web

  官網地址:https://spring.io/projects/spring-cloud-openfeignspring

2.Feign能幹什麼

  Feign旨在使編寫Java客戶端變得更容易。apache

  以前使用是用Ribbon+RestTemplate來進行服務間調用。在實際開發中,因爲對服務的調用不止一處,每每一個接口會被屢次調用,因此一般會針對每一個微服務自行封裝一些客戶端來包裝這些依賴服務的調用。因此,Feign在此基礎上作了進一步封裝,由他來幫助咱們定義和實現依賴服務接口的定義。json

  Feign集成了Ribbon,利用Ribbon維護服務的信息,而且經過輪詢實現客戶端的負載均衡。而與Ribbon不一樣的是,Feign只須要定義服務綁定接口且以聲明式的方法,優雅而簡單的實現了服務調用。api

3.OpenFeign和Feign的區別

4.OpenFeign默認集成了Ribbon

好比咱們查看依賴圖以下:服務器

2 OpenFeign使用

1.新建項目cloud-consumer-feign-order80cookie

 2.修改pom網絡

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud</artifactId>
        <groupId>cn.qz.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-consumer-feign-order80</artifactId>

    <dependencies>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--eureka-client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--引入本身抽取的工具包-->
        <dependency>
            <groupId>cn.qz.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

3.修改ymlsession

server:
  port: 80

spring:
    application:
        name: cloud-order-service

eureka:
  client:
    register-with-eureka: true
    service-url:
      #單機版
      defaultZone: http://localhost:7001/eureka/

4.主啓動類:

package cn.qz.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * @Author: qlq
 * @Description
 * @Date: 14:07 2020/10/17
 */
@SpringBootApplication
@EnableFeignClients
public class OrderFeignMain80
{
    public static void main(String[] args) {
        SpringApplication.run(OrderFeignMain80.class, args);
    }
}

5.業務類:

(1)Service接口:FeignClient註解聲明用Feign獲取服務,調用CLOUD-PAYMENT-SERVICE中相關接口

package cn.qz.cloud.service;

import cn.qz.cloud.utils.JSONResultUtil;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;
import java.util.Map;

/**
 * @Author: qlq
 * @Description
 * @Date: 14:08 2020/10/17
 */
@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService {
    @GetMapping(value = "/pay/listAll")
    JSONResultUtil<List<Map<String, Object>>> listAll();

    @GetMapping("/pay/getServerPort")
    JSONResultUtil<String> getServerPort();
}

(2)Controller

package cn.qz.cloud.controller;

import cn.qz.cloud.service.PaymentFeignService;
import cn.qz.cloud.utils.JSONResultUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;

/**
 * @Author: qlq
 * @Description
 * @Date: 14:14 2020/10/17
 */
@RestController
@RequestMapping("/consumer")
@Slf4j
public class OrderFeignController {
    @Resource
    private PaymentFeignService paymentFeignService;

    @GetMapping(value = "/pay/listAll}")
    public JSONResultUtil<List<Map<String, Object>>> listAll() {
        return paymentFeignService.listAll();
    }

    @GetMapping("/pay/getServerPort")
    JSONResultUtil<String> getServerPort() {
        return paymentFeignService.getServerPort();
    }
}

3.OpenFeign超時控制

  消費者調用提供者,存在超時現場。好比服務提供者Payment服務中使用線程休眠模擬處理請求耗時3秒鐘,

    @GetMapping("/getServerPort")
    public JSONResultUtil<String> getServerPort() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return JSONResultUtil.successWithData(serverPort);
    }

咱們在上面用Feign調用時,報錯以下:

2020-10-17 15:12:32.519 ERROR 8356 --- [p-nio-80-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is feign.RetryableException: connect timed out executing GET http://CLOUD-PAYMENT-SERVICE/pay/getServerPort] with root cause

java.net.SocketTimeoutException: connect timed out

 解決辦法:增長客戶端超時時間(修改yml,因爲feign是封裝了ribbon,因此是設置ribbon相關設置)

server:
  port: 80

spring:
    application:
        name: cloud-order-service

eureka:
  client:
    register-with-eureka: true
    service-url:
      #單機版
      defaultZone: http://localhost:7001/eureka/
#設置feign客戶端超時時間(OpenFeign默認支持ribbon)
ribbon:
#指的是創建鏈接所用的時間,適用於網絡情況正常的狀況下,兩端鏈接所用的時間
  ReadTimeout: 5000
#指的是創建鏈接後從服務器讀取到可用資源所用的時間
  ConnectTimeout: 5000

4.OpenFeign日誌加強

  openFeign增長日誌打印功能。打印請求以及相應的相關信息,方便聯調。能夠經過設置日誌級別,來了解Feign Http中的請求細節。也就是對Feign接口的調用狀況進行監控和輸出。

相關日誌級別有:

NONE:默認的,不顯示任何日誌
BASIC:僅記錄請求方法、URL、 響應狀態碼及執行時間
HEADERS:除了BASIC中定義的信息以外,還有請求和響應的頭信息
FULL:除了HEADERS中定義的信息以外,還有請求和響應的正文及元數據

配置方式:

(1)增長配置類

package cn.qz.cloud.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Author: qlq
 * @Description
 * @Date: 15:45 2020/10/17
 */
@Configuration
public class FeignConfig {

    @Bean
    Logger.Level feignLoggerLevel(){
        return Logger.Level.FULL;
    }
}

(2)yml配置日誌級別:

logging:
  level:
    # feign日誌以什麼級別監控哪一個接口
    cn.qz: debug

(3)訪問查看日誌:

2020-10-17 15:48:41.903 DEBUG 10024 --- [p-nio-80-exec-2] cn.qz.cloud.service.PaymentFeignService  : [PaymentFeignService#getServerPort] ---> GET http://CLOUD-PAYMENT-SERVICE/pay/getServerPort HTTP/1.1
2020-10-17 15:48:41.904 DEBUG 10024 --- [p-nio-80-exec-2] cn.qz.cloud.service.PaymentFeignService  : [PaymentFeignService#getServerPort] ---> END HTTP (0-byte body)
2020-10-17 15:48:41.943 DEBUG 10024 --- [p-nio-80-exec-2] cn.qz.cloud.service.PaymentFeignService  : [PaymentFeignService#getServerPort] <--- HTTP/1.1 200 (37ms)
2020-10-17 15:48:41.944 DEBUG 10024 --- [p-nio-80-exec-2] cn.qz.cloud.service.PaymentFeignService  : [PaymentFeignService#getServerPort] connection: keep-alive
2020-10-17 15:48:41.945 DEBUG 10024 --- [p-nio-80-exec-2] cn.qz.cloud.service.PaymentFeignService  : [PaymentFeignService#getServerPort] content-type: application/json
2020-10-17 15:48:41.945 DEBUG 10024 --- [p-nio-80-exec-2] cn.qz.cloud.service.PaymentFeignService  : [PaymentFeignService#getServerPort] date: Sat, 17 Oct 2020 07:48:41 GMT
2020-10-17 15:48:41.946 DEBUG 10024 --- [p-nio-80-exec-2] cn.qz.cloud.service.PaymentFeignService  : [PaymentFeignService#getServerPort] keep-alive: timeout=60
2020-10-17 15:48:41.946 DEBUG 10024 --- [p-nio-80-exec-2] cn.qz.cloud.service.PaymentFeignService  : [PaymentFeignService#getServerPort] transfer-encoding: chunked
2020-10-17 15:48:41.947 DEBUG 10024 --- [p-nio-80-exec-2] cn.qz.cloud.service.PaymentFeignService  : [PaymentFeignService#getServerPort] 
2020-10-17 15:48:41.948 DEBUG 10024 --- [p-nio-80-exec-2] cn.qz.cloud.service.PaymentFeignService  : [PaymentFeignService#getServerPort] {"success":true,"code":"200","msg":"","data":"8081"}
2020-10-17 15:48:41.948 DEBUG 10024 --- [p-nio-80-exec-2] cn.qz.cloud.service.PaymentFeignService  : [PaymentFeignService#getServerPort] <--- END HTTP (52-byte body)

補充:關於feign接口調用傳遞參數使用以下 

(1)例如商品服務的接口:

package cn.xm.controller;

import cn.xm.bean.Product;
import cn.xm.service.BaseSequenceService;
import cn.xm.service.ProductService;
import cn.xm.utils.JSONResultUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Controller
@RequestMapping("product")
public class ProductController extends AbstractController<Product> {

    private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class);

    @Autowired
    private ProductService productService;

    @PutMapping("/{id}")
    @ResponseBody
    public JSONResultUtil<Void> updateAmount(@PathVariable("id") Integer id, @RequestParam("amount") long amount) {
        Product byId = productService.findById(id);
        if (byId != null) {
            byId.setAmount(amount);
            productService.update(byId);
        }

        return JSONResultUtil.ok();
    }
}

(2)訂單服務feign調用

package cn.xm.service;

import cn.xm.bean.Product;
import cn.xm.utils.JSONResultUtil;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Component
@FeignClient(value = "CLOUD-PRODUCT")
public interface ProductService {

    @GetMapping(value = "/product/detail/{id}")
    JSONResultUtil<Product> detail(@PathVariable(value = "id") Integer id);

    @PutMapping(value = "/product/{id}")
    JSONResultUtil<Void> updateAmount(@PathVariable(value = "id") Integer id, @RequestParam("amount") long amount);
} 

補充:feign 能夠攜帶請求頭、feign能夠基於URL實現遠程調用。URL配置不須要註冊中心,也不須要服務名稱,能夠簡單的理解爲HttpClient + 轉換回傳數據

例如:

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

@FeignClient(name = "account-service", url = "127.0.0.1:8083")
public interface AccountFeignClient {

    @GetMapping("/debit")
    Boolean debit(@RequestParam("userId") String userId, @RequestParam("money") BigDecimal money);
}

FeignClient源碼以下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.cloud.netflix.feign;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
    @AliasFor("name")
    String value() default "";

    /** @deprecated */
    @Deprecated
    String serviceId() default "";

    @AliasFor("value")
    String name() default "";

    String qualifier() default "";

    String url() default "";

    boolean decode404() default false;

    Class<?>[] configuration() default {};

    Class<?> fallback() default void.class;

    Class<?> fallbackFactory() default void.class;

    String path() default "";
}

解釋:

name/value屬性: 這兩個的做用是同樣的,指定的是調用服務的微服務名稱。

url : 指定調用服務的全路徑,常常用於本地測試。不使用註冊中心的狀況下可使用url來進行訪問。

若是同時指定name和url屬性: 則以url屬性爲準,name屬性指定的值便當作客戶端的名稱

補充:自定義Feign的Logger,場景有密碼相關或者有文件調用相關的接口日誌作特殊處理

1. 添加本身的日誌記錄器

package cn.qz.cloud.config;

import feign.Logger;
import feign.Request;
import feign.Response;

import java.io.IOException;

/**
 * @author: 喬利強
 * @date: 2020/12/30 20:18
 * @description:
 */
public class FeignLogger extends Logger {

    final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(FeignLogger.class.getName());

    @Override
    protected void log(String configKey, String format, Object... args) {
        logger.info(String.format(methodTag(configKey) + format, args));
    }

    @Override
    protected void logRequest(String configKey, Level logLevel, Request request) {
        log(configKey, "%s %s HTTP/1.1, log request begin. <---", request.httpMethod().name(), request.url());
        super.logRequest(configKey, logLevel, request);
        log(configKey, "%s %s HTTP/1.1, log request end. <---", request.httpMethod().name(), request.url());
    }

    @Override
    protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException {
        String reason = response.reason() != null && logLevel.compareTo(Logger.Level.NONE) > 0 ? " " + response.reason() : "";
        int status = response.status();
        log(configKey, "%s%s HTTP/1.1 (%sms) log logAndRebufferResponse begin. <---", status, reason, elapsedTime);
        Response response1 = super.logAndRebufferResponse(configKey, logLevel, response, elapsedTime);
        log(configKey, "%s%s HTTP/1.1 (%sms) log logAndRebufferResponse end. <---", status, reason, elapsedTime);
        return response1;
    }
}

  這徹底是參考的feign.Logger進行編寫,這裏面的默認實現能夠用做本身改造時候作參考。

2. 注入到spring以及修改日誌級別

package cn.qz.cloud.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author: 喬利強
 * @date: 2020/12/30 20:35
 * @description:
 */
@Configuration
public class FeignLoggerConfiguration {

    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }

    /**
     * 自定義feignlogger
     *
     * @return
     */
    @Bean
    public feign.Logger feignLogger() {
        return new FeignLogger();
    }
}

3.查看默認的實現注入

(1) feign 默認是經過工廠方式注入的bean

org.springframework.cloud.netflix.feign.FeignClientsConfiguration#feignLoggerFactory  源碼以下:

    @Bean
    @ConditionalOnMissingBean({FeignLoggerFactory.class})
    public FeignLoggerFactory feignLoggerFactory() {
        return new DefaultFeignLoggerFactory(this.logger);
    }

logger是一個自動注入的實現:

    @Autowired(
        required = false
    )
    private Logger logger;

@ConditionalOnMissingBean({FeignLoggerFactory.class})   也是spring便於擴展的設計方式。  也能夠經過本身的LoggerFactory來注入Logger。

(2)查看工廠源碼以下: 若是logger是空的話會默認採用Slf4jLogger。若是本身注入了logger會用本身的logger。

package org.springframework.cloud.openfeign;

import feign.Logger;
import feign.slf4j.Slf4jLogger;

public class DefaultFeignLoggerFactory implements FeignLoggerFactory {
    private Logger logger;

    public DefaultFeignLoggerFactory(Logger logger) {
        this.logger = logger;
    }

    public Logger create(Class<?> type) {
        return (Logger)(this.logger != null ? this.logger : new Slf4jLogger(type));
    }
}

補充:OpenFeign攜帶自定的Header 

1. 方式一:對請求方法單獨設置

第一種: 參數寫死

    @GetMapping(value = "/hystrix/payment/getSessionId", headers = {"token=111222"})
    JSONResultUtil<Map<String, Object>> getSessionId();

第二種: 使用變量,以下

    @GetMapping(value = "/hystrix/payment/getSessionId", headers = {"token=111222"})
    JSONResultUtil<Map<String, Object>> getSessionId(@RequestHeader("token2") String token4);

調用方:

Map<String, Object> data = paymentHystrixService.getSessionId("testHeader").getData();

結果:

2021-03-06 11:51:02.185  INFO 21416 --- [io-8011-exec-10] cn.qz.cloud.config.FeignLogger           : [PaymentHystrixService#getSessionId] token: 111222
2021-03-06 11:51:02.185  INFO 21416 --- [io-8011-exec-10] cn.qz.cloud.config.FeignLogger           : [PaymentHystrixService#getSessionId] token2: testHeader

2. 方式二: 全局配置增長RequestInterceptor  來給全部 的Feign請求增長Header

package cn.qz.cloud.config;

import com.google.common.collect.Lists;
import feign.Logger;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.*;

@Configuration
@Slf4j
public class FeignConfiguration {

    public final static String HEADER_CALLER = "cloud_caller";

    public final static String COOKIE_COOKIE_NAME = "cookie";
    public final static String SESSIONID_COOKIE_NAME = "SESSION";

    @Value("${spring.application.name}")
    String application;

    // 對系統中容許後向傳遞的頭進行控制
    private static List<String> disallowhHeaderNames = Lists.newArrayList(
            "cache-control"
            , "origin"
            , "upgrade-insecure-requests"
            , "user-agent"
            , "referer"
            , "accept"
            , "accept-language"
            , "connection"
    );

    @Bean
    public RequestInterceptor requestHeaderInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                //記錄調用方信息
                requestTemplate.header(HEADER_CALLER, application);
                if (RequestContextHolder.getRequestAttributes() == null) {
                    return;
                }

                // 注意子線程問題(RequestContextHolder 內部使用的是ThreadLocal)
                HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                        .getRequest();
                // 當前請求攜帶的header
                Enumeration<String> headerNames = request.getHeaderNames();
                // 已經存在feign的requestTemplate的header
                Map<String, Collection<String>> headers = requestTemplate.headers();
                if (headerNames != null) {
                    while (headerNames.hasMoreElements()) {
                        String name = headerNames.nextElement();
                        boolean exists = false; // 避免重複header日後傳播,如content-type傳播會影響後續body解析
                        for (String th : headers.keySet()) {
                            if (name.equalsIgnoreCase(th)) {// 忽略大小寫
                                exists = true;
                                break;
                            }
                        }
                        if (!exists && !disallowhHeaderNames.contains(name.toLowerCase())) {
                            requestTemplate.header(name, Collections.list(request.getHeaders(name)));
                        }
                    }
                }
            }
        };
    }

    private String sessionId() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            return null;
        }
        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
        return request.getSession().getId();
    }

    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }

    /**
     * 自定義feignlogger
     *
     * @return
     */
    @Bean
    public feign.Logger feignLogger() {
        return new FeignLogger();
    }
}

 

【當你用心寫完每一篇博客以後,你會發現它比你用代碼實現功能更有成就感!】
相關文章
相關標籤/搜索