Spring Cloud OpenFeign:基于Ribbon和Hystrix的声明式服务调用

 

 

 

1、简介

官网:https://spring.io/projects/spring-cloud-openfeign

feign是一个声明式WebService 客户端,使用Feign能让编写Web Service客户端更加简单,我们只需创建一个接口并用注解的方式来配置它,就可以实现对某个服务接口的调用,简化了直接使用RestTemplate来调用服务接口的开发量。Feign具备可插拔的注解支持,同时支持Feign注解、JAX-RS注解及SpringMvc注解。当使用Feign时,Spring Cloud集成了Ribbon和Eureka以提供负载均衡的服务调用及基于Hystrix的服务容错保护功能。

他的使用方法是定义一个服务接口,然后在上面添加注解。Feign也支持可插拔式的编码和解码器。Spring  Cloud 对Feign进行了封装,使其支持Spring MVC 标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡。

 

 2、Feign能干什么?

Feign使编写java Http客户端更加容易

前面使用Ribbon + RestTemplate时,利用RestTemplate对Http请求的封装处理,形成了一套模板化的调用方法,但是在实际的开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多次调用。所以Feign在此基础上做了进一步的封装,由他来帮助我们定义和实现依赖服务接口。在Feign的实现下,我们只需要创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring Cloud Ribbon,自动封装服务调用客户端的工作量。

Feign集成了Ribbon

  Ribbon维护了服务列表信息,并且通过轮询的方式实现客户端的负载均衡。而与Ribbon不同的是,通过Feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务的调用。

Feign 与 OpenFeign的区别?

Feign

OpenFeign

Feign是Spring Cloud组件中的一个轻量级RestFul的Http服务客户端Feign内置了Ribbon,用来做客户端的负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口就可以调用服务注册中心的服务。

OpenFeign是Spring Cloud在feign的基础上支持了SpringMvc的注解,如@RequestMapping等等

OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他的服务

<dependency>

    <groupId>org.springframework.cloud</ groupId >

    <artifactId>spring-cloud-starter-feign</ artifactId>

</ dependency >

 

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

 

3、OpenFeign 的使用

3.1、在启动类上添加 @EnableFeignClients 注解来启用Feign的客户端功能。

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class FeignServiceApplication {

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

 

3.2、添加UserService接口完成对user-service服务的接口绑定

@FeignClient(value = "user-service")
public interface UserService {
    @PostMapping("/user/create")
    CommonResult create(@RequestBody User user);

    @GetMapping("/user/{id}")
    CommonResult<User> getUser(@PathVariable Long id);

    @GetMapping("/user/getByUsername")
    CommonResult<User> getByUsername(@RequestParam String username);

    @PostMapping("/user/update")
    CommonResult update(@RequestBody User user);

    @PostMapping("/user/delete/{id}")
    CommonResult delete(@PathVariable Long id);
}

 

3.3、修改yml,OpenFeign默认的等待时间1秒钟,超时后将报错:

server:
  port: 80

eureka:
  client:
    register-with-eureka: true
    service-url:
     defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka

spring:
  application:
    name: feign-customer-order

  # 设置feign客户端超时时间(openFeign默认支持Ribbon)
ribbon:
  # 建立连接后从服务器读取到可用资源的时间
  ReadTimeout: 5000
  # 建立连接所用的时间,适用于网络正常的情况下,两端连接所用的时间
  ConnectTimeout: 5000

 

4、Feign中的服务降级

Feign中的服务降级使用起来非常方便,只需要为Feign客户端定义的接口添加一个服务降级处理的实现类即可,下面我们为UserService接口添加一个服务降级实现类。

4.1、添加服务降级实现类UserFallbackService, 需要注意的是它实现了UserService接口,并且对接口中的每个实现方法进行了服务降级逻辑的实现。

public class UserFallbackService implements UserService {
    @Override
    public CommonResult create(User user) {
        User defaultUser = new User(-1L, "defaultUser", "123456");
        return new CommonResult<>(defaultUser);
    }

    @Override
    public CommonResult<User> getUser(Long id) {
        User defaultUser = new User(-1L, "defaultUser", "123456");
        return new CommonResult<>(defaultUser);
    }

    @Override
    public CommonResult<User> getByUsername(String username) {
        User defaultUser = new User(-1L, "defaultUser", "123456");
        return new CommonResult<>(defaultUser);
    }

    @Override
    public CommonResult update(User user) {
        return new CommonResult("调用失败,服务被降级",500);
    }

    @Override
    public CommonResult delete(Long id) {
        return new CommonResult("调用失败,服务被降级",500);
    }
}

4.2、修改UserService接口,设置服务降级处理类为UserFallbackService

修改@FeignClient注解中的参数,设置fallback为UserFallbackService.class即可。

@FeignClient(value = "user-service",  fallback = UserFallbackService.class)
public interface UserService {
.... }

4.3、修改application.yml,开启Hystrix功能

feign:
  hystrix:
    enabled: true #在Feign中开启Hystrix

 

5、日志打印功能

Feign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中Http请求的细节。

日志级别:

  • NONE:默认的,不显示任何日志;
  • BASIC:仅记录请求方法、URL、响应状态码及执行时间;
  • HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息;
  • FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。

我们通过java配置来使Feign打印最详细的Http请求日志信息。

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

在application.yml中配置需要开启日志的Feign客户端

logging:
  level:
    com.openapi.service: debug

 这里的com.openapi.service是openFeign接口所在的包名,当然你也可以配置一个特定的openFeign接口。

 

6、Feign的常用配置

feign:
  hystrix:
    enabled: true #在Feign中开启Hystrix
  compression:
    request:
      enabled: false #是否对请求进行GZIP压缩
      mime-types: text/xml,application/xml,application/json #指定压缩的请求数据类型
      min-request-size: 2048 #超过该大小的请求会被压缩
    response:
      enabled: false #是否对响应进行GZIP压缩
logging:
  level: #修改日志级别
    com.dw.cloud.service.UserService: debug

 如果openFeign没有设置对应得超时时间,那么将会采用Ribbon的默认超时时间。

理解了超时设置的原理,由之产生两种方案也是很明了了,如下:

  • 设置openFeign的超时时间
  • 设置Ribbon的超时时间(默认1s)

1、设置Ribbon的超时时间(不推荐)

设置很简单,在配置文件中添加如下设置:

ribbon:
  # 值的是建立链接所用的时间,适用于网络状况正常的情况下, 两端链接所用的时间
  ReadTimeout: 5000
  # 指的是建立链接后从服务器读取可用资源所用的时间
  ConectTimeout: 5000

2、设置openFeign的超时时间(推荐)

feign:
  client:
    config:
      ## default 设置的全局超时时间,指定服务名称可以设置单个服务的超时时间
      default:
        connectTimeout: 5000
        readTimeout: 5000

 

 

7、OpenFeign GET 请求使用 @SpringQueryMap 传递对象参数

4.1、SpringCloud在2.1.x版本中提供了 @SpringQueryMap 注解,可以传递对象参数。 但是传递的对象参数中只能包含基本类型,如果需要在传递的参数对象中, 包含其他的对象类型, 需要自定义

类实现 QueryMapEncode 接口,自定义参数解析。具体配置如下:

@Configuration
public class FeignConfiguration {
    /**
     * 使用@SpringQueryMap可以解决GET请求的时候,传递对象
     * 关于使用 @SpringQueryMap不能解析对象中的对象的问题
     * @return
     */
    @Bean
    public Feign.Builder feignBuilder() {
        return Feign.builder()
                .queryMapEncoder(new BeanQueryMapNestEncoder())
                .retryer(Retryer.NEVER_RETRY);
    }

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

 

4.2、自定义 BeanQueryMapNestEncoder 传递复杂参数解析配置:

import feign.Param;
import feign.QueryMapEncoder;
import feign.codec.EncodeException;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;

public class BeanQueryMapNestEncoder implements QueryMapEncoder {
    private final Map<Class<?>, BeanQueryMapNestEncoder.ObjectParamMetadata> classToMetadata = new HashMap();

    public BeanQueryMapNestEncoder() {
    }

    public Map<String, Object> encode(Object object) {
        try {
            Map<String, Object> propertyNameToValue = new HashMap();
            this.putPropertiesToMap((String)null, object, propertyNameToValue);
            return propertyNameToValue;
        } catch (IntrospectionException | InvocationTargetException | IllegalAccessException var3) {
            throw new EncodeException("Failure encoding object into query map", var3);
        }
    }

    private void putPropertiesToMap(String parentProperty, Object object, Map<String, Object> propertyNameToValue) 
throws IntrospectionException, InvocationTargetException, IllegalAccessException { BeanQueryMapNestEncoder.ObjectParamMetadata metadata = this.getMetadata(object.getClass()); Iterator var5 = metadata.objectProperties.iterator(); while(true) { while(true) { PropertyDescriptor pd; Method method; Object value; StringBuffer nameBuffer; do { do { if (!var5.hasNext()) { return; } pd = (PropertyDescriptor)var5.next(); method = pd.getReadMethod(); value = method.invoke(object); nameBuffer = new StringBuffer(); if (StringUtils.isNotEmpty(parentProperty)) { nameBuffer.append(parentProperty).append('.'); } } while(value == null); } while(value == object); Param alias = (Param)method.getAnnotation(Param.class); nameBuffer.append(alias != null ? alias.value() : pd.getName()); if (BeanUtils.isSimpleProperty(value.getClass())) { propertyNameToValue.put(nameBuffer.toString(), value); } else if (!(value instanceof Collection)) { this.putPropertiesToMap(nameBuffer.toString(), value, propertyNameToValue); } else { StringBuffer arrValueBuffer = new StringBuffer(); Object arrObj; for(Iterator var12 = ((Collection)value).iterator(); var12.hasNext(); arrValueBuffer.append(arrObj)) { arrObj = var12.next(); if (!BeanUtils.isSimpleProperty(arrObj.getClass())) { break; } if (arrValueBuffer.length() != 0) { arrValueBuffer.append(','); } } if (arrValueBuffer.length() > 0) { propertyNameToValue.put(nameBuffer.toString(), arrValueBuffer.toString()); } } } } } private BeanQueryMapNestEncoder.ObjectParamMetadata getMetadata(Class<?> objectType) throws IntrospectionException { BeanQueryMapNestEncoder.ObjectParamMetadata metadata = (BeanQueryMapNestEncoder.ObjectParamMetadata)this.classToMetadata.get(objectType); if (metadata == null) { metadata = BeanQueryMapNestEncoder.ObjectParamMetadata.parseObjectType(objectType); this.classToMetadata.put(objectType, metadata); } return metadata; } private static class ObjectParamMetadata { private final List<PropertyDescriptor> objectProperties; private ObjectParamMetadata(List<PropertyDescriptor> objectProperties) { this.objectProperties = Collections.unmodifiableList(objectProperties); } private static BeanQueryMapNestEncoder.ObjectParamMetadata parseObjectType(Class<?> type) throws IntrospectionException { List<PropertyDescriptor> properties = new ArrayList(); PropertyDescriptor[] var2 = Introspector.getBeanInfo(type).getPropertyDescriptors(); int var3 = var2.length; for(int var4 = 0; var4 < var3; ++var4) { PropertyDescriptor pd = var2[var4]; boolean isGetterMethod = pd.getReadMethod() != null && !"class".equals(pd.getName()); if (isGetterMethod) { properties.add(pd); } } return new BeanQueryMapNestEncoder.ObjectParamMetadata(properties); } } }

 

8、Feign 异常处理

5.1、自定义Feign解析器:

import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.crecgec.baseboot.jsoncore.exception.BaseException;
import feign.Response;
import feign.Util;
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;

@Configuration
public class FeignErrorDecoder implements ErrorDecoder {

    @Override
    public Exception decode(String methodKey, Response response) {
        try {
            // 这里直接拿到我们抛出的异常信息
            String message = Util.toString(response.body().asReader());
            try {
                JSONObject jsonObject = JSONObject.parseObject(message);
                return new BaseException(jsonObject.getString("resultMsg"), jsonObject.getInteger("resultCode"));
            } catch (JSONException e) {
                e.printStackTrace();
            }

        } catch (IOException ignored) {
        }
        return decode(methodKey, response);
    }
}

5.2、定义系统的异常类

public class BaseException extends RuntimeException {
    private int status ;
 
    public int getStatus() {
        return status;
    }
 
    public void setStatus(int status) {
        this.status = status;
    }
 
    public BaseException() {
    }
 
    public BaseException(String message, int status) {
        super(message);
        this.status = status;
    }
 
    public BaseException(String message) {
        super(message);
    }
 
    public BaseException(String message, Throwable cause) {
        super(message, cause);
    }
 
    public BaseException(Throwable cause) {
        super(cause);
    }
 
    public BaseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

5.3、统一异常拦截转换对应的异常信息返回前端

public class ResultSet {

    /**
     * 返回的状态码
     */
    private Integer resultCode;

    /**
     * 返回的消息
     */
    private String resultMsg;

    /**
     * 返回的数据
     */
    private Object data;

    public ResultSet() {
    }

    public ResultSet(Integer resultCode, String resultMsg) {
        this.resultCode = resultCode;
        this.resultMsg = resultMsg;
    }

    public ResultSet(Integer resultCode, String resultMsg, Object data) {
        this.resultCode = resultCode;
        this.resultMsg = resultMsg;
        this.data = data;
    }

    public Integer getResultCode() {
        return resultCode;
    }

    public void setResultCode(Integer resultCode) {
        this.resultCode = resultCode;
    }

    public String getResultMsg() {
        return resultMsg;
    }

    public void setResultMsg(String resultMsg) {
        this.resultMsg = resultMsg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

5.4、全局异常类处理配置:

@ExceptionHandler(value = BaseException.class)
public ResultSet defaultErrorHandler(HttpServletRequest req, HttpServletResponse resp, BaseException e) {
    ResultSet resultSet = new ResultSet();

    if (e.getStatus() == 400) {
        resultSet.setResultCode(-1);
        resultSet.setResultMsg(e.getMessage());
        resultSet.setData(null);
        resp.setStatus(400);
    } else {
        resp.setStatus(500);
        if(logger.isErrorEnabled()){
            logger.error("系统异常,请联系系统开发人员进行处理", e);
        }
        resultSet.setResultCode(-1);
        resultSet.setResultMsg(e.getMessage());
        resultSet.setData(null);
    }

    return resultSet;
}

 

9、使用OpenFeign 实现文件传输

参考 github上 的实现方式:https://github.com/OpenFeign/feign-form

6.1、引入依赖(如果引入的 OpenFeign 中没有以下依赖):

<dependencies>
  <dependency>
    <groupId>io.github.openfeign.form</groupId>
    <artifactId>feign-form</artifactId>
    <version>3.8.0</version>
  </dependency>
  <dependency>
    <groupId>io.github.openfeign.form</groupId>
    <artifactId>feign-form-spring</artifactId>
    <version>3.8.0</version>
  </dependency>
</dependencies>

 

6.2、Feign 调用端接口:

/**
 * @Author dw
 * @ClassName TestFileService
 * @Description
 * @Date 2021/9/20 10:11
 * @Version 1.0
 */
@FeignClient(value = "cloud-test-server")
public interface ITestFileService {

    @RequestMapping(value = "/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE )
    public Object fileUpload(@RequestPart("file")MultipartFile multipartFile, @SpringQueryMap MyUser myUser);
}

 

6.3、调用端的Controller

@RestController
@RequestMapping("file")
public class TestFileController {

    @Autowired
    private ITestFileService testFileService;

    @RequestMapping("upload")
    public Object fileUpload(@RequestPart("file") MultipartFile multipartFile){
        Role role = new Role("123", "管理員", 3);
        MyUser user = new MyUser("张三", 23, role);
        Object o = testFileService.fileUpload(multipartFile, user);
        return o;
    }
}

 

6.4、服务端Controller

@RestController
@RequestMapping("file")
public class TestController {
    
    @RequestMapping("upload")
    public Object fileUpload(@RequestPart("file") MultipartFile multipartFile, MyUser myUser){
        System.out.println(multipartFile);
        return myUser;
    }
}

OK...文件传输完成了

 

10、如何替换默认的httpclient?

Feign在默认情况下使用的是JDK原生的URLConnection发送HTTP请求,没有连接池,但是对每个地址会保持一个长连接,即利用HTTP的persistence connection。

在生产环境中,通常不使用默认的http client,通常有如下两种选择:

  • 使用ApacheHttpClient
  • 使用OkHttp

至于哪个更好,其实各有千秋,我比较倾向于ApacheHttpClient,毕竟老牌子了,稳定性不在话下。

那么如何替换掉呢?其实很简单,下面演示使用ApacheHttpClient替换。

1、添加ApacheHttpClient依赖

在openFeign接口服务的pom文件添加如下依赖:

<!--     使用Apache HttpClient替换Feign原生httpclient-->
    <dependency>
      <groupId>org.apache.httpcomponents</groupId>
      <artifactId>httpclient</artifactId>
    </dependency>
    
    <dependency>
      <groupId>io.github.openfeign</groupId>
      <artifactId>feign-httpclient</artifactId>
    </dependency>

为什么要添加上面的依赖呢?从源码中不难看出,请看org.springframework.cloud.openfeign.FeignAutoConfiguration.HttpClientFeignConfiguration这个类,代码如下:

Spring Cloud OpenFeign:基于Ribbon和Hystrix的声明式服务调用

 上述红色框中的生成条件,其中的@ConditionalOnClass(ApacheHttpClient.class),必须要有ApacheHttpClient这个类才会生效,并且feign.httpclient.enabled这个配置要设置为true

2、配置文件中开启

在配置文件中要配置开启,代码如下:

feign:
  client:
    httpclient:
      # 开启 Http Client
      enabled: true

3、如何验证已经替换成功?

其实很简单,在feign.SynchronousMethodHandler#executeAndDecode()这个方法中可以清楚的看出调用哪个client,如下图:

Spring Cloud OpenFeign:基于Ribbon和Hystrix的声明式服务调用

 上图中可以看到最终调用的是ApacheHttpClient

 

相关文章: