feign的应用总结

一、微服务调用第一种方式:Ribbon+RestTemplate

        1)首先RestTemplate

        restTemplate作用是发送http请求的客户端,功能类似httpclient,是spring原生的框架,用于2个服务之间的请求发送。与其功能相似的是Spring Cloud Netflix桶的Feign技术。

        常见的Http客户端请求工具:HttpURLConnection(自带的)、HttpClient、OkHttp
        restTemplate是一个同步的webHttp客户端请求模板工具,spring框架做的抽象。RestTemplate默认使用的HttpUrlConnection,可以通过构造方法替换底层的执行引擎。常见的执行引擎包括  HttpClient、OkHttp。
        也就是说底层使用的时候还是要选择 上面的某种方法去执行http请求,本身并不具备去请求http客户端的能力。

        2)常用方法:


  GET相关方法:

public <T> T getForobject(String url, Class<T> responseType,
    object... uriVariables);
    
public <T> T getForobject (String url, Class<T> responseType ,
    Map<String, ?> uriVariables) ;
    
public <T> T getForobject(URI url, Class<T> responseType) ;


 //简单示例 
 restTemplate.getForobject(
        "http://localhost:8081/house/data/ {name}",String.class,name);

 有三种重载方式: 

  • url:请求的 API 地址,有两种方式,其中一种是字符串,另一种是 URL 形式。
  • responseType :返回值的类型。
  • uriVariables : PathVariable 参数,有两种方式, 其中一种是可变参数,另一种是 Map 形式。

  除了 getForObject ,我们还可以使用 getForEntity 来获取数据:

    ResponseEntity<HouseInfo> responseEntity = restTemplate.getForEntity(
        "http:/ /localhost: 8081 /house/ data?name= "+name, HouseInfo.class) ;
    if (responseEntity.getStatusCodeValue() == 200) {
        return responseEntity.getBody();
    }

getForEntity 中可以获取返回的状态码、请求头等信息,通过 getBody 获取响应的内容。其余的和 getForObject 一样,也是有3个重载的实现。

POST相关方法:

public <T> T postForobject(String url, object request,
    Class<T> responseType, object... uriVariables);

public <T> T postForobject(String url, object request,
    Class<T> responseType, Map<String, ?> urivariables);

public <T> T postForobject(URI url, object request, Class<T> responseType);


//简单示例
    HouseInfo houseInfo = new HouseInfo();
    houseInfo.setCity("上海");
    houseInfo.setRegion("虹口");
    houseInfo.setName( "XXX");
    Long id = restTemplate.postFor0bject(
        "http: //1ocalhost:8081/ house/save",houseInfo,Long.class);

postForObject 同样有3个重载的实现。除了 postForObject 还可以使用 postForEntity 方法,用法都一样。

带HEAD访问接口:

// 请求头信息
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.valueOf("application/json;charset=UTF-8"));
//headers.add("headParam1", "headParamValue");

// 请求体内容
TempUser param = new TempUser();
param.setUserName("张三");
param.setAge(18);

// 组装请求信息
HttpEntity<TempUser> httpEntity=new HttpEntity<>(param,headers);

TempUser result = restTemplate.postForObject("http://localhost:8080/cs-admin/rest/getPostUser", httpEntity, TempUser.class);

    除了get和post对应的方法之外,RestTemplate 还提供了putdelete 等操作方法,还有一个比较实用的就是exchange方法。exchange 可以执行getpost、 put、 delete 这4种请求方式。
    示例略,需要自行搜寻。

Springboot之restTemplate配置及使用_L-960的博客-CSDN博客
【精选】Java中的HTTP客户端工具——RestTemplate_java resttemplate_意识流的博客-CSDN博客

负载均衡器 Ribbon

        所谓负载均衡,就是为资源分配负载,就是选择合适的服务处理请求。
        在微服务架构下,SpringCloud提供了统一的客户端负载均衡器的接口LoadBalancerClient,需要具体的技术来实现该接口,而Ribbon框架提供RibbonLoadBalancerClient作为实现类。

负载均衡器分为客户端负载均衡器和服务器端负载均衡器(nginx)

         我们在微服务架构中,往往通过RestTemplate发送RPC请求,然后通过Ribbon做客户端负载均衡。
        

Ribbon 与 Nginx 区别:

服务器端负载均衡 Nginx:

        nginx 是客户端所有请求统一交给 nginx,由 nginx 进行实现负载均衡请求转发,属于服务器端负载均衡。既请求由 nginx 服务器端进行转发。

客户端负载均衡 Ribbon:

Ribbon 是从 eureka 注册中心服务器端上获取服务注册信息列表,缓存到本地,然后在本地实现轮询负载均衡策略。既在客户端实现负载均衡。

应用场景的区别:

Nginx 适合于服务器端实现负载均衡 比如 Tomcat ,Ribbon 适合与在微服务中 RPC 远程调用实现本地服务负载均衡,比如 Dubbo、SpringCloud 中都是采用本地负载均衡。

补充:ribbon和feign的区别:

spring cloud的Netflix中提供了两个组件实现软负载均衡调用:ribbon和feign。

        Ribbon:
                是一个基于 HTTP 和 TCP 客户端的负载均衡器
                它可以在客户端配置 ribbonServerList(服务端列表),然后轮询请求以实现均衡负载。

        Feign:
                Spring Cloud Netflix 的微服务都是以 HTTP 接口的形式暴露的,所以可以用 Apache 的 HttpClient 或 Spring 的 RestTemplate 去调用,而 Feign 是一个使用起来更加方便的 HTTP 客戶端,使用起来就像是调用自身工程的方法,而感觉不到是调用远程方法。

RestTemplate与Ribbon整合:

通过 @LoadBalanced标记即可: 

    @Bean
    @LoadBalanced
    // 开启负载均衡 Ribbon, 发送的请求都会被Ribbon拦截。必须使用应用名代替ip,否则报错
    RestTemplate restTemplate() {
        return new RestTemplate();
    }

负载均衡示例:
1)首先部署多个服务:

2、然后可以使用服务名进行接口访问:

@RestController
@RequestMapping("/substitution")
public class Substitut ionController{
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping ("/cal1Hel1o")
    public String cal1Hello(){
        String result = restTemplate. getFor0bject(
            "http://fsh-house/house/he11o", String.class);
        System.out.print1n("调用结果: " + result);
        return result ;
    }
}

关于ribbon的原理:
今日教学:RestTemplate 结合 Ribbon 使用 - 知乎 (zhihu.com)

【精选】RestTemplate与Ribbon整合原理(基于spring-cloud-starter-alibaba-nacos-discovery、LoadBalancerClient负载均衡接口)_ribbon+resttemplate 原理-CSDN博客

二、Feign的调用

        feign其实本质上就是Ribbon+Hystrlx,提供了更加面向对象的服务调用方式。
        底层其实就是ribbon,但是负载均衡发送的请求可以是多样性的,比如默认使用原生的HttpUrlConnection,或者httpclient、Okhttp都可以。
        除了ribbon还封装了http发送方式,还有hystrlx等熔断限流的工具类。

1)feign的简单实现:

        首先引入依赖:

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

        然后启动类上加入注解:

@SpringBootApplication
@EnableDiscoveryClient//服务注册
@EnableFeignClients(basePackages = {"com.mother.fucker.api.**.feign"})//服务发现
public class AcceptApplication {

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

        然后在提供方编写feign接口:

@FeignClient(name = "accept-data",contextId = "file")
@Component
public interface IAcceptDataFeignClient {

    @GetMapping("/aaa/aaa")
    public CommonResp<String> aaa();


}

这个feign接口上的地址要跟controller中的接口地址相同,这样才能找到这个接口,名字可以不同,但是接口必须相同。

@RestController
@RequestMapping("/aaa")
public class HelloController {

    @Value("${server.port: 8088}")
    private String port;

    @GetMapping("/aaa")
    public CommonResp<String> convert() throws NacosException {
        return CommonResp.success(port);
    }
}

然后引入feign接口所在的包,使用依赖注入feign接口就可以直接调用feign中的方法了。

@RestController
@RequestMapping("/demo")
public class DemoController {

    @Autowired
    private IAcceptDataFeignClient acceptDataFeignClient;

    @GetMapping("/aaa")
    public CommonResp<String> convert() throws NacosException {
        CommonResp<String> convert = acceptDataFeignClient.aaa();
        return CommonResp.success(convert.getData());
    }

以上就是最简单的实例。
 

2)feign的封装与抽取

        正常情况下我们会将feign模块单独抽取出来做一个公共模块这样,其他服务调用时就可以直单独引入feign模块就可以了。
        然后最好用一个controller类来实现feign跟正常的feign区分开。

@RestController
public class acceptDataFeignController implements IAcceptDataFeignClient {

    @Autowired
    IFileService fileService;

    @Override
    public CommonResp<Boolean> save(File file) {
        return CommonResp.success(fileService.saveOrUpdate(file));
    }
}

3)  feign的超时与重试

        当A服务通过fegin去调用B服务时,feign其实集合了很多组件,最核心的组件就是Ribbon,Ribbon最核心的功能其实就是负载均衡器,最主要的作用就是去注册中心中发现服务,并寻找服务地址,再通过地址去发送请求。发送请求是由client,client是一个接口,这个接口会有各种的实现类 XxxxClient,由实现类来发送请求。发送请求的时候请求时间比较长的话,就会有超时时间、以及重试机制。
        feign的超时时间其实就是对ribbon的超时时间的封装而已,我们可以对ribbon设置超时时间,也可以对feign设置超时时间,但是对feign设置超时时间会被覆盖掉:超时设置要设置到调用方这边。
                        connectTimeout:连接超时,
           readTimeOut:读取超时,

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
      im-message-front:
        connectTimeout: 5000
        readTimeout: 5000   

           在default同级,加上服务名,可以配置服务单独的配置,连接超时时间跟读取超时时间。
       

        重试:
                ribbon是自带重试机制的,但是如果配置上图的配置文件,那么ribbon就会被覆盖掉,而feign的重试机制默认是关闭的,所以如果要开启重试,就需要去重写Retryer这个bean:


    @Bean
    public Retryer getRetryer(){
        //参数1:重试的时间间隔 默认100毫秒
        //参数2:重试的最大间隔时间默认1秒,重试间隔时间按照一定的规则逐渐增大,但不能超过最大间隔时间
        //参数3:最大重试次数 默认5次(包含第一次请求)
        return new Retryer.Default(100,500,2);
    }

        注意:
                1、实际开发过程中,可以根据业务适当的调整Read超时的大小
                2、时间开发过程中,根据需要可以开启过着关闭重试,但是要注意,如果开启了重试,就有重复请求的风险 ,尽可能的需要保证服务的被调用方的接口幂等。

        还需要特别注意的是,并非所有的异常都会触发重试策略,只有 RetryableException 异常才会触发异常策略。在默认Feign配置情况下,只有在网络调用时发生 IOException 异常时,才会抛出 RetryableException,因此常见的 SocketException、NoHttpResponseException、UnknownHostException、HttpRetryException、SocketConnectException、ConnectionClosedException 等异常都可触发Feign重试机制。

feign 接口重试配置 - 知乎 (zhihu.com)icon-default.png?t=N7T8https://zhuanlan.zhihu.com/p/585885159

 重试机制还可以结合自定义ErrorDecoder,进行自定义了异常重试处理,也就是说指定某种异常我们才进行重试,可以选择使用继承ErrorDecoder方式实现,

@Configuration
@Slf4j
public class FeignErrorDecoder implements ErrorDecoder {


    @Override
    public Exception decode(String methodKey, Response response) {
        if (is4xx(response.status())) {
            log.error("请求xxx服务-{},返回:{}", response.status(), response.body());
            throw new ClientException(response.status(), "xx");
        }
        FeignException exception = errorStatus(methodKey, response);
        if (response.request().httpMethod() == Request.HttpMethod.GET) { //只对 GET 重试
            return new RetryableException(
                    response.status(),
                    exception.getMessage(),
                    response.request().httpMethod(),
                    exception,
                    new Date(),
                    response.request());
        }
        return exception;
    }
}

在服务的提供端实现以上代码,里面的逻辑可以自定义实现,其实原理就是,当服务端在feign的调用时有异常抛出的话,捕获异常,并进行自定义处理,如果是需要重试的异常,只需要重新抛出一个RetryableException异常就好,这样被调用端捕获,就会自动进入重试。

4)feign的熔断限流hystrix

        feign中是集成了hystrix了的,所以我们只需要开启就好了,hystrix其实就是如果我们通过feign调用某个服务时,当服务挂掉或者遇到什么不稳定的时候,总有请求不通或者不稳定的时候,此时我们可以预先设定好一个返回值,当调用不通时,就返回默认值。

        看了很多资料都说首先开启配置:

feign:
  hystrix:
    enabled: true

      然后自定义实现 FeignClient 的接口返回Fallback类,就是接口调用失败的返回方法,定义格式如下:需要继承feign接口,并重写相关方法:

@Component
public class UserCenterFeignClientFallback implements UserCenterFeignClient {
    @Override
    public UserDTO findById(Integer id) {
        UserDTO userDTO = new UserDTO();
        userDTO.setWxNickname("流控/降级返回的用户");
        return userDTO;
    }
}

         然后在feign接口中指定 fallback 方法:

@FeignClient(name = "user-center",fallback = UserCenterFeignClientFallback.class)
public interface UserCenterFeignClient {
    /**
     * http://user-center/users/{id}
     *
     * @param id
     * @return
     */
    @GetMapping("/users/{id}")
    UserDTO findById(@PathVariable Integer id);
}

        很多资料上说到这就可以了,启动服务之后,让feign服务提供方down掉,调用接口发现依旧调用失败,没有返回提供的Fallback类中的方法,又查了很多资料说要单独引入sentinel的依赖

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

         引入之后,再次启动,服务启动报错,

        说是循环依赖,又查资料说 spring.cloud.sentinel.enabled默认是启用状态,为true,把它设为false之后就是不启用了。这样确实是不报循环依赖的错误了,但是项目中也没有使用sentinel了

spring:
  cloud:
    sentinel:
      enabled: false

        所以这种方式不推荐,第二种方式是配置允许循环依赖(推荐)修改Spring的配置,设置允许循环依赖,把spring.main.allow-circular-references设为true

spring:
  #允许循环依赖
  main:
    allow-circular-references: true

         注意如果你使用的是nacos,需要设置在bootstrap.yml文件中,不然服务根本读取不到nacos中的配置。

         再次配置好之后,再次启动,再次报错:
        java.lang.IllegalStateException: No fallback instance of type class
        又查资料,大致意思是,因为feign接口是在公共包中,fallback类也在公共包中,然后被引入到消费者服务中,所以不会直接注入到spring容器中,有两种解决方式
        第一种:主启动类配置好回调类的bean的扫描包路径。也就是加了@Component注解的回调类的路径。
        第二种:手动注入fallback类的bean。
        推荐第二种:

@Bean    
public UserCenterFeignClientFallback getUserCenterFeignClientFallback(){
    
        return new UserCenterFeignClientFallback();
    }

        再次启动,启动成功,并dowm掉feign调用的服务端,再次请求,返回fallBack类中的方法,实现成功。

        
        还有一种实现熔断降级的方式就是 实现FallbackFactory 工厂:

@Component
@Slf4j
public class UserCenterFeignClientFallbackFactory implements FallbackFactory<UserCenterFeignClient> {
    @Override
    public UserCenterFeignClient create(Throwable cause) {
        return new UserCenterFeignClient() {
         // 当 UserCenterFeignClient 有多个方法时,在这里逐个重写实现即可
            @Override
            public UserDTO findById(Integer id) {
                log.warn("远程调用被限流/降级了", cause);
                UserDTO userDTO = new UserDTO();
                userDTO.setWxNickname("流控/降级返回的用户");
                return userDTO;
            }
        };
    }
}

        调用方式:

@FeignClient(name = "user-center",fallbackFactory = UserCenterFeignClientFallbackFactory.class)

 通过 FallbackFactory 实现的降级可以捕获 熔断的异常信息,而通过方法实现的熔断,则不能获取熔断的异常信息。所以推荐使用 FallbackFactory 进行降级实现

        4)feign切换底层通讯框架

        ribbon获取地址,client执行请求,默认发送请求的方式是jdk自带的HttpURLConnection
        更换步骤:
        1)增加配置:

feign:   
  okhttp:
    enabled: true
  httpclient:
    enabled: false
  hystrix:
    enabled: false

        2)在项目当中添加依赖:

        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-okhttp</artifactId>
        </dependency>

         3)可以对okhttp进行配置覆盖原有配置,一般不设置,默认的就够用。

        关于底层框架切换时的坑:
        1、默认是使用HttpURLConnection没错,但是实际上只要在pom文件上加上HttpClient依赖,就会默认更换为HttpClient的方式调用,查了资料显示说并不需要在配置文件中配置feign.httpclient.enabled为true,因为从@ConditionalOnProperty注解可知,因为在默认情况下就为true:

        2、而且更更坑的就算是引入了OKhttp的依赖,并且配置文件中设置feign.okhttp.enabled: true ,但是如果依赖中包含HttpClient,那么OKhttp也不会生效,还需要将feign.httpclient.enabled为false才行。
        以上是我在项目实际中遇到,还没有深入的搞明白过,希望又大神可以帮忙解释一下。

5)feign拦截器自动化传递参数

        比如说我们A服务通过feign去调用B服务,B服务中又通过feign去调用C服务,有一些参数需要进行传递,就可以使用自动化参数传递。

        接口多版本的实现:(需要自定义实现注解)        
        传递参数需要用到feign包下的RequestInterceptor拦截器,重写apply方法。

@Configuration
public class FeignRequestConfig implements RequestInterceptor {

    /**
     * 请求参数集合
     */
    private List<String> paramNames;


    @Override
    public void apply(RequestTemplate template) {

        //获取当前的请求对象
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (Objects.isNull(attributes)){
            return;
        }
        HttpServletRequest request = attributes.getRequest();
        //获取请求参数,请求参数可能在请求头、也可能在请求体中
        //根据参数名获取请求头
//        request.getHeader(headerName);
        //根据参数名获取请求体
//        request.getParameter(paramName);
        for (String paramName : paramNames) {
            String headerValue=request.getHeader(paramName);
            if (StrUtil.isNotBlank(headerValue)){
                //获取到参数值,将参数值放入到feign请求的请求头中
                template.header(paramName,headerValue);
            }

            String paramValue = request.getParameter(paramName);
            if (StrUtil.isNotBlank(paramValue)){
                //获取到参数值,将参数值放入到feign请求的请求参数中
                template.header(paramName,paramValue);
            }
        }
    }
}

        RequestTemplate请求模板,feign发请求是通过这个RequestTemplate,底层其实还是封装的Client
        上面的拦截器,并不是加个@Configuration注解就会生效的,需要在feign的自动装配的注解上@EnableFeignClients加这个配置,如下所示:

        (事实上是加上@Configuration注解就是会生效,我的理解是加上@Configuration注解这个配置可能会全局生效,但是将配置加入以下注解只会局部生效而已)

@EnableFeignClients(basePackages = {"com.mother.fucker.api.**.feign"},defaultConfiguration = FeignRequestConfig.class)

         这样feign就会对这个拦截器生效了。
         需要拦截传递的参数列表,最好做成可配置的,可以写一个配置类,专门接受参数:

@ConfigurationProperties(prefix = "feign")
@Data
public class FeignParamter {

    /**
     * 需要传递的参数名列表
     */
    private List<String> paramNames;

}

这样设置配置类需要在启动类上加上注解,才能生效:@EnableConfigurationProperties(FeignParamter.class)
或者
@ConfigurationPropertiesScan(basePackages = {"com.mother.fucker.data.config"})
然后在nacos中配置即可,因为是列表所以可以直接用数组形式配置:

feign:
  #设置需要feign继续传递的参数名称  
  paramNames:
    - api-version
    

然后将参数列表直接注入就可以获取了。

@Autowired
private FeignParamter feignParamter;

List<String> paramNames = feignParamter.getParamNames();

拦截器也可以直接使用配置文件的方式进行配置:

  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        requestInterceptors[0]:
          com.mother.fucker.data.config.FeignRequestConfig

6)  feign的日志打印

        日志的等级有4种,分别是:
         NONE : 性能最佳,适用于生产:不记录任何日志(默认值)。
         BASIC:  适用于生产环境追踪问题:仅记录请求方法,URL、响应状态代码以及执行时间。
         HEADERS: 记录BASIC级别的基础上,记录请求和响应的header.
         FULL:比较适用于开发及测试环境定位问题:记录请求个响应的header、body和元数据
        
        1)第一种方式定义一个配置类:

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

        注意这个配置类,如果加上@Configuration就是这个服务中的所有的feign都生效,如果只想对某一个接口生效,需要将这个配置类单独引入这个接口:

@FeignClient(name = "accept-data",contextId = "file",configuration = FeignConfig.class)
@Component
public interface IAcceptDataFeignClient {


    @PostMapping("/save")
    CommonResp<Boolean> save(@RequestBody File file);

    @GetMapping("/aaa/aaa")
    public CommonResp<String> aaa();


}

        还需要设置日志级别,springBoot默认的日志级别是info,feign的debug日志级别就不会输出

logging:
  level: debug #对所有的日志级别进行配置
    com.mother.fucker.api.data.feign: debug #单独对feign包的接口进行配置 

 
              2)第二种直接在配置文件中配置

feign:
  client:
    config:
      accept-data: #服务名
        loggerLevel: BASIC #日志级别

7)feign中url参数的应用

        feign中有url参数,配了这个参数之后就不会再去服务注册中心根据服务名拉取服务地址了,而是默认访问配置的地址。
        测试了一下,发现直接配置另外一个系统的地址,也可以访问,我得理解是其实就类似于便捷发送http请求类似。

  • 17
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值