四、SpringCloud alibaba 之 OpenFeign

4.1、调用过程的演化

JAVA 项目中如何实现接口调用?

远程调用工具 描述
HttpClient HttpClient 是 Apache Jakarta Common 下的子项目,用来提供高效的、最新的、功能丰富的支持 Http 协议的客户端编程工具包,并且它支持 HTTP 协议最新版本和建议。HttpClient 相比传统 JDK 自带的URLConnection,提升了易用性和灵活性,使客户端发送 HTTP 请求变得容易,提高了开发的效率。
Okhttp 一个处理网络请求的开源项目,是安卓端最火的轻量级框架,由 Square 公司贡献,用于替代HttpUrlConnection 和 Apache HttpClient。OkHttp 拥有简洁的 API、高效的性能,并支持多种协议(HTTP/2 和 SPDY)。
HttpURLConnection HttpURLConnection 是 Java 的标准类,它继承自 URLConnection,可用于向指定网站发送
RestTemplate RestTemplate 是 Spring 提供的用于访问 Rest 服务的客户端,RestTemplate 提供了多种便捷访问远程 HTTP 服务的方法,能够大大提高客户端的编写效率。

上面介绍的是最常见的几种调用接口的方法,我们下面要介绍的方法比上面的更简单、方便,它就是 Feign

4.1.1、点对点直接调用

在最开始,不同服务间调用采用的是http://ip:port/{请求路径}的点对点方式调用

4.1.2、本地负载均衡调用方式

如果被调用的服务可能存在多个实例,此时采用点对点直接调用就不合适,此时引入了nacos注册中心,每个服务实例都注册到nacos里面,此时采取客户端负载均衡的调用方式。

@RequestMapping("/test")
    public String order(){
        // 根据服务Id获取注册到nacos中的服务实例
        List<ServiceInstance> instances = discoveryClient.getInstances("stock-service");
        int index = new Random().nextInt(instances.size());
        ServiceInstance serviceInstance = instances.get(index);
        String host = serviceInstance.getHost();
        int port = serviceInstance.getPort();
        // 拼接调用地址
        String url="http://"+host+":"+port+"/stock/getStock";
        // 远程调用
        String result = restTemplate.getForObject(url, String.class);
        return result;

    }

自己写的调用负载均衡比较简单,此时可以引入Ribbon,可以采取http://{服务名称}/{请求路径}的调用方式。

注意:服务名调用方式需要RestTemplate被代理,也就是在注册RestTemplate时需要加上@LoadBalanced注解

4.1.3、OpenFeign介绍

Feign是Netflix开发的声明式、模板化的HTTP客户端,其灵感来自Retrofit、JAXRS-2.0以及WebSocket。Feign可帮助我们更加便捷、优雅地调用HTTP API。Feign支持多种注解,例如Feign自带的注解或者JAX-RS注解等。

Spring Cloud openfeign对Feign进行了增强,使其支持Spr ing MVC注解,另外还整合了Ribbon和Nacos,从而使得Feign的使用更加方便。

Feign可以做到使用 HTTP 请求远程服务时就像调用本地方法一样的体验,开发者完全感知不到这是远程方法,更感知不到这是个 HTTP 请求。它像 Dubbo 一样,consumer 直接调用接口方法调用 provider,而不需要通过常规的 Http Client 构造请求再解析返回数据。它解决了让开发者调用远程接口就跟调用本地方法一样,无需关注与远程的交互细节,更无需关注分布式环境开发。

4.2、OpenFeign快速使用

4.2.1、引入OpenFeign依赖

OpenFeign依赖于springcloud,在引入openfeign之前,检查一下是否已经引入了SpringCloud的依赖。

<!--openfeign的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

 

4.2.2、编写OpenFeign接口

在消费者端使用OpenFeign,需要一个@FeignClient注解及参数来定义一个接口。

FeignClient注解被@Target(ElementType.TYPE)修饰,表示FeignClient注解的作用目标在接口上。

@FeignClient标签的常用属性如下:

属性名 作用
name 指定FeignClient的名称。name有两个作用:1、作为Bean的名称注册到spring容器中。2、如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现。如果使用了url属性,此时name的第二个作用就失效了。
url url一般用于调试,可以手动指定@FeignClient调用的地址
decode404 当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException
configuration Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract
fallback 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口
fallbackFactory 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
path 定义当前FeignClient的统一前缀

 

一般 建议在消费者项目中创建一个feign的包,用于存放当前项目中调用别的服务的Feign接口

在Feign接口中,声明当前项目要调用的服务接口。接口的命名一般建议{要调用的服务名}FeignService

package com.java.coder.feign.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
/**
 * name: 指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现。如果使用了url属性,此时name属性就失效了。
 * path: 定义当前FeignClient的统一前缀, 一般是生产者类上@RequestMapping的value值
 */
@FeignClient(name = "stock-service",path = "/stock")
public interface StockFeignService {
     /**
     * openfeign中对注解的使用更加严格,PathVariable注解中的id必须写
     * @param id
     * @return
     */
   	@RequestMapping("/getStock/{id}")
    String addOrder(@PathVariable("id") Integer id);
    /**
     * 入参和出参都是json的格式
     * @param dto
     * @return
     */
    @RequestMapping("/qryStockById")
    @ResponseBody
    StockRespDto qryStockById(@RequestBody StockReqDto dto);
}

 

注意在Feign接口中声明的方法,必须与对应的服务提供方提供的方法声明一样。且在OpenFeign中注解的使用更加严格,比如:@PathVariable,在普通的springboot应用中,如果参数的名字与路径中的名字一样,那么@PathVariable中的name可以不用写,但是在OpenFeign中必须写

4.2.3、启动类添加注解

package com.java.coder.feign;

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

@SpringBootApplication
// 启动Feign接口客户端
@EnableFeignClients
public class FeignApplication {

    public static void main(String[] args) {
        SpringApplication.run(FeignApplication.class,args);
    }
}

4.2.4、Feign调用项目内服务

package com.java.coder.feign.controller;

import com.java.coder.feign.feign.StockFeignService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/feign")
@RestController
public class FeignController {

    // 引入之前声明的Feign接口
    @Autowired
    private StockFeignService stockFeignService;

    @RequestMapping("/add")
    public String addOrder(){
        // 直接通过Feign接口调用服务提供方
         String forObject = stockFeignService.addOrder(1100);
        return forObject;
    }
    
    @RequestMapping("/qryStockById")
    public String qryStockById(){
        StockReqDto stockReqDto=new StockReqDto();
        stockReqDto.setStockId(10000000);
        // 通过OpenFeign调用复杂参数接口
        StockRespDto respDto = stockFeignService.qryStockById(stockReqDto);
        log.info("respDto:{}",respDto);
        return "调用成功";
    }
}

4.2.5、Feign调用第三方服务

下面的代码用于调用github上的接口,此时调用的服务并非自己项目里面的服务,而是第三方的服务。

@FeignClient(name="github-client",url = "https://api.github.com")
public interface ThirdFeignService {
    @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
    String searchRepo(@RequestParam("q") String queryStr);
}

 

4.3、OpenFeign扩展配置

Feign 提供了很多的扩展机制,让用户可以更加灵活的使用。包括日志、超时时间、拦截器等。

4.3.1、日志配置

有时候我们遇到 Bug,比如接口调用失败、参数没收到等问题,或者想看看调用性能,就需要配置 Feign 的日志了,以此让 Feign 把请求信息输出来。

OpenFeign的日志等级有 4 种,分别是:

日志级别 备注
NONE 性能最佳,适用于生产,不记录任何日志(默认值)。
BASIC 适用于生产环境追踪问题,仅记录请求方法、URL、响应状态代码以及执行时间。
HEADERS 记录BASIC级别的基础上,记录请求和响应的header。
FULL 比较适用于开发及测试环境定位问题,记录请求和响应的header、body和元数据。

 

(1)全局日志配置

全局日志配置就是此配置对当前项目中所有的远程微服务调用都生效,只需要在@CompontScan注解可以扫描到的地方设置一个配置类,且配置类被@Configuration注解标注。

package com.java.coder.feign.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 全局配置: 当使用@Configuration 会将配置作用所有的服务提供方
* 局部配置: 如果只想针对某一个服务进行配置,不要加@Configuration
*/
@Configuration
public class OpenFeignConfig {

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

此处的Logger是feign.Logger

(2)局部配置

局部日志配置有两种形式:

1、配置类上不加@Configuration注解,在@FeignClient注解上指定配置类

@FeignClient(name = "stock-service",path = "/stock", configuration= OpenFeignConfig.class)
public interface StockFeignService {
    @RequestMapping("/getStock")
    public String addOrder();
}

 

此时OpenFeignConfig配置类中的相关配置,只有在调用StockFeignService中声明的服务时才生效。

@FeignClient(name = "stock-service",path = "/stock", configuration= OpenFeignConfig.class)

2、在配置文件中指定配置,且对应的属性配置类是FeignClientProperties.FeignClientConfiguration

org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration.FeignClientConfiguration

feign:
  client:
    config:
      stock-service: #服务的名称
        loggerLevel: FULL

在yml配置文件中执行 Client 的日志级别才能正常输出日志,格式是"logging.level.feign接口包路径=debug

logging:
  level:
	com.tuling.mall.feigndemo.feign: debug

如果采用properties文件的话,配置的内容是

logging.level.com.java.coder.order.feign=debug

 日志配置完后的运行结果如下

4.3.2、契约配置

Spring Cloud 在 Feign 的基础上做了扩展,使用 Spring MVC 的注解来完成Feign的功能。原生的 Feign 是不支持 Spring MVC 注解的,如果你想在 Spring Cloud 中使用原生的注解方式来定义客户端也是可以的,通过配置契约来改变这个配置,Spring Cloud 中默认的是 SpringMvcContract。

Spring Cloud 1 早期版本就是用的原生Fegin. 随着netflix的停更替换成了Open feign

1)修改契约配置,支持Feign原生的注解

 package com.java.coder.feign.config;
import feign.Contract;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OpenFeignConfig {

    /**
     * 配置openfeign的日志级别
     * @return
     */
    @Bean
    public Logger.Level feignLoggerLevel(){
        return Logger.Level.FULL;
    }

    /**
     * 修改契约配置,支持feign的原生注解
     * @return
     */
    @Bean
    public Contract feignContract(){
        return new Contract.Default();
    }
}

注意:修改契约配置后,OrderFeignService 不再支持springmvc的注解,需要使用Feign原生的注解也可以通过配置文件配置。

 

4.3.3、自定义拦截器

定义拦截器

首先在消费者端定义一个OpenFeign的拦截器,在拦截器中加入特定的请求头,可以加上微服务的名称。

package com.java.coder.feign.interceptor;

import feign.RequestInterceptor;
import feign.RequestTemplate;

public class FeignInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        requestTemplate.header("service-name", "order-service");
        // 可以设置一些token值
        requestTemplate.header("service-code", "001");
    }
}

让拦截器生效

1、通过配置类配置

package com.java.coder.feign.config;

import com.java.coder.feign.interceptor.FeignInterceptor;
import feign.Contract;
import feign.Logger;
import feign.Request;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OpenFeignConfig {

    /**
     * 配置openfeign的日志级别
     * @return
     */
    @Bean
    public Logger.Level feignLoggerLevel(){
        return Logger.Level.FULL;
    }

    /**
     * 修改契约配置,支持feign的原生注解
     * @return
     */
//    @Bean
//    public Contract feignContract(){
//        return new Contract.Default();
//    }

    /**
     * 超时时间配置
     * @return
     */
//    @Bean
//    public Request.Options options() {
//        return new Request.Options(5000,10000);
//    }

    /**
     * 拦截器配置
     * @return
     */
    @Bean
    public FeignInterceptor customFeignInterceptor(){
        return new FeignInterceptor();
    }


}

2、通过配置文件配置

feign:
  client:
    config:
      stock-service: #服务的名称
        loggerLevel: FULL
        requestInterceptors[0]: com.java.coder.feign.interceptor.FeignInterceptor

在服务提供方验证

1、定义一个普通的拦截器

package com.java.coder.stock.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
public class LogInterceptor implements HandlerInterceptor {
    private String TRACE_ID = "TRACE_ID";

    private String SERVICE_NAME = "service-name";
    private String SERVICE_CODE = "service-code";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

        log.info("service name:{}", request.getHeader(SERVICE_NAME));
        log.info("service code:{}", request.getHeader(SERVICE_CODE));
        return true;

    }

}

 

2、把拦截器加入到拦截器链

package com.java.coder.stock.config;

import com.java.coder.stock.interceptor.LogInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    /**
     * 配置拦截器
     *
     * @param registry 相当于拦截器的注册中心
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       //下面这句代码相当于添加一个拦截器   添加的拦截器就是我们刚刚创建的
        registry.addInterceptor(new LogInterceptor())
        //配置我们要拦截哪些路径 addPathPatterns("/**")表示拦截所有请求,包括我们的静态资源
                .addPathPatterns("/**")
//       excludePathPatterns()表示我们要放行哪些(表示不用经过拦截器)
//       excludePathPatterns("/","/login")表示放行“/”与“/login”请求
//       如果有静态资源的时候可以在这个地方放行
                .excludePathPatterns("/", "/login");
    }

}

容错配置

1、开启容错配置

feign:
	hystrix:
		enabled: true

 

2、定义容错类

@Component
public class StockFeignServiceImpl implements StockFeignService {

    @Override
    public String getStock() {
        System.out.println("调用失败");
        return "调用失败";
    }
}

 

3、在Feign接口上配置容错类

/**
 * name: 指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现。如果使用了url属性,此时name属性就失效了。
 * path: 定义当前FeignClient的统一前缀, 一般是生产者类上@RequestMapping的value值
 */
@FeignClient(name = "stock-service",path = "/stock",fallback = StockFeignServiceImpl.class)
public interface StockFeignService {
    /**
     * openfeign中对注解的使用更加严格,PathVariable注解中的id必须写
     * @return
     */
    @RequestMapping("/getStock")
    String getStock();


}

或者采取fallbackFactory配置也可以

 

带请求头

1、在@RequestMapping中的headers属性中添加

@FeignClient(name="custorm",fallback=Hysitx.class)
public interface IRemoteCallService {
	@RequestMapping(value="/custorm/getTest",
                    method = RequestMethod.POST,
                    headers = {"Content-Type=application/json;charset=UTF-8"})
    List<String> test(@RequestParam("names") String[] names);
}

 

2、在方法参数中添加


@FeignClient(name="custorm",fallback=Hysitx.class)
public interface IRemoteCallService {
    
	@RequestMapping(value="/custorm/getTest",method = RequestMethod.POST,
		headers = {"Content-Type=application/json;charset=UTF-8"})
    List<String> test(@RequestParam("names") String[] names, 
                      @RequestHeader MultiValueMap<String, String> headers);

3、使用@Header注解

@FeignClient(name="custorm",fallback=Hysitx.class)
public interface IRemoteCallService {
    
	@RequestMapping(value="/custorm/getTest",method = RequestMethod.POST)
	@Headers({"Content-Type: application/json;charset=UTF-8"})
    List<String> test(@RequestParam("names") String[] names);
}

 

4、实现RequestInterceptor接口

@Configuration
public class FeignRequestInterceptor implements RequestInterceptor {
 
    @Override
    public void apply(RequestTemplate temp) {
        temp.header(HttpHeaders.AUTHORIZATION, "XXXXX");
    }
 
}

4.4、OpenFeign调用原理

在之前的学习,我们通过定义一个符合OpenFeign的接口,不用去实现这个接口就可以完成对远程方法的调用。实现这个功能,肯定用了动态代理。

我们可以看到,实现动态代理的类就是ReflectiveFeign的静态内部类FeignInvocationHandler实现的。

最后调用接口的时候采用的是feign.Client接口的默认实现类lClient.Default里面的convertAndSend方法。

HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
      final URL url = new URL(request.url());
      final HttpURLConnection connection = this.getConnection(url);
      if (connection instanceof HttpsURLConnection) {
        HttpsURLConnection sslCon = (HttpsURLConnection) connection;
        if (sslContextFactory != null) {
          sslCon.setSSLSocketFactory(sslContextFactory);
        }
        if (hostnameVerifier != null) {
          sslCon.setHostnameVerifier(hostnameVerifier);
        }
      }
      connection.setConnectTimeout(options.connectTimeoutMillis());
      connection.setReadTimeout(options.readTimeoutMillis());
      connection.setAllowUserInteraction(false);
      connection.setInstanceFollowRedirects(options.isFollowRedirects());
      connection.setRequestMethod(request.httpMethod().name());

      Collection<String> contentEncodingValues = request.headers().get(CONTENT_ENCODING);
      boolean gzipEncodedRequest =
          contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP);
      boolean deflateEncodedRequest =
          contentEncodingValues != null && contentEncodingValues.contains(ENCODING_DEFLATE);

      boolean hasAcceptHeader = false;
      Integer contentLength = null;
      for (String field : request.headers().keySet()) {
        if (field.equalsIgnoreCase("Accept")) {
          hasAcceptHeader = true;
        }
        for (String value : request.headers().get(field)) {
          if (field.equals(CONTENT_LENGTH)) {
            if (!gzipEncodedRequest && !deflateEncodedRequest) {
              contentLength = Integer.valueOf(value);
              connection.addRequestProperty(field, value);
            }
          } else {
            connection.addRequestProperty(field, value);
          }
        }
      }
      // Some servers choke on the default accept string.
      if (!hasAcceptHeader) {
        connection.addRequestProperty("Accept", "*/*");
      }

      if (request.body() != null) {
        if (disableRequestBuffering) {
          if (contentLength != null) {
            connection.setFixedLengthStreamingMode(contentLength);
          } else {
            connection.setChunkedStreamingMode(8196);
          }
        }
        connection.setDoOutput(true);
        OutputStream out = connection.getOutputStream();
        if (gzipEncodedRequest) {
          out = new GZIPOutputStream(out);
        } else if (deflateEncodedRequest) {
          out = new DeflaterOutputStream(out);
        }
        try {
          out.write(request.body());
        } finally {
          try {
            out.close();
          } catch (IOException suppressed) { // NOPMD
          }
        }
      }
      return connection;
    }
  }

 

默认情况下是没有采取http连接池进行远程调用,为了性能考虑,建议采取连接池

4.5、OpenFeign性能优化

Feign底层发起http请求,依赖于其它的框架。其底层支持的http客户端实现包括:

  • HttpURLConnection:默认实现,不支持连接池

  • Apache HttpClient :支持连接池

  • OKHttp:支持连接池

因此我们通常会使用带有连接池的客户端来代替默认的HttpURLConnection。比如,我们使用OK Http.

在项目中引入OkHttp依赖

<!--OK http 的依赖 -->
 		<dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-okhttp</artifactId>
            <version>11.8</version>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-core</artifactId>
            <version>11.8</version>
        </dependency>

 

在配置文件中开启OKHTTP

feign:
  okhttp:
    enabled: true # 开启OKHttp功能

重启服务,连接池就生效了。

因为我的负载均衡采用的是ribbon,因此真正执行远程调用的方法是FeignLoadBalancer的execute里面。

org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer#execute

如果没有配置OKHttprequest.client()方法返回的是Client.Default

如果配置了OKHttprequest.client()方法返回的是OkHttpClient

 

 

posted @ 2024-02-04 20:09  阿瞒123  阅读(64)  评论(0编辑  收藏  举报