03 Spring Cloud Feign

Wu Jun 2019-12-25 15:59:04
10 微服务 > 1 服务调用

Spring Cloud Feign是一套基于Netflix Feign实现的声明式服务调用客户端,目的是让Web Service调用更加简单,使用HTTP请求远程服务时能与调用本地方法一样的编码体验。

工作机制

提供了HTTP请求模板,通过编写简单的接口和插入注解,就可完成对Web服务接口的绑定。扩展了对Spring MVC注解的支持,整合了Ribbon和Hystrix。内部机制是使用RestTemplate来实现。

服务调用

配置 Feign+Ribbon+Hystrix,消费服务。因为 Spring Cloud 的封装,我们几乎不需要关心 Ribbon 和 Hystrix 的细节,只需要进行 Feign 的配置即可。

  1. pom依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. 配置application.properties
spring.application.name=service-consumer
eureka.client.serviceUrl.defaultZone=http://localhost:8000/eureka
  1. 启动类添加 @EnableFeignClients 、@EnableDiscoveryClient注解,开启Eureka Client和Feign的相关功能

  2. 创建@FeignClient接口,统一声明依赖的服务

    • 有url参数也要设置name
    • configuration 参数可添加设定配置
    • fallback 参数开启Hystrix 并定义Hystrix fallback
    • 默认,@RequestParam注解的参数转换成字符串添加到URL中,将没有注解的参数通过Jackson转换成json放到请求体中。如果请求方式指定为POST,那么所有未标注解的参数将会被忽略。
    • 在Spring Cloud环境下,Feign的Encoder只会用来编码没有添加注解的参数

Client

//指定feign调用的服务和Hystrix Fallback
@FeignClient(name= "eureka-provider",fallback = ConsumerFallback.class,url = "http://localhost:8761/",configuration = EurekaConfiguration.class)
public interface ConsumerClient {
    //对应服务方提供的接口
    @RequestMapping(value = "/index", method = RequestMethod.GET)
    String consumer();
}

Fallback

//Hystrix Fallback
@Component
public class ConsumerFallbackimplements Consumer{
    @Override
    public String index()
    {
    return "Feign客户端访问失败";
    }
}

Configuration

@Configuration
public class EurekaConfiguration {

    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
        return new BasicAuthRequestInterceptor("zhihao.miao", "123456");
    }
    
    @Bean
    public Encoder feignEncoder() {
        HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter(new ObjectMapper());
        ObjectFactory<HttpMessageConverters> objectFactory = () -> new HttpMessageConverters(jacksonConverter);
        return new SpringEncoder(objectFactory);
    }
    
    @Bean
    public FeignErrorDecoder errorDecoder() {
        return new FeignErrorDecoder();
    }
}
public class FeignErrorDecoder implements ErrorDecoder {

    @Override
    public Exception decode(String methodKey, Response response) {
        HttpStatus status = HttpStatus.valueOf(response.status());

        byte[] responseBody;
        try {
            responseBody = IOUtils.toByteArray(response.body().asInputStream());
        } catch (IOException e) {
            throw new RuntimeException("Failed to process response body.", e);
        }

        if (status.is5xxServerError()) {
            return new HttpServerErrorException(status, response.reason(), responseBody, null);
        } else if (status.is4xxClientError()) {
            return new HttpClientErrorException(status, response.reason(), responseBody, null);
        }


        return FeignException.errorStatus(methodKey, response);
    }
}
  1. 通过接口调用服务
@RestController
public class DcController {
    @Autowired
    ConsumerClient consumerClient;
    @GetMapping("/consumer")
    public String index() {
        return consumerClient .consumer();
    }
}

第一次请求失败

Hystrix默认的超时时间是1秒,如果超过这个时间尚未响应,将会进入fallback代码。而首次请求往往会比较慢(Spring的懒加载机制)。解决方案有三种,以feign为例。

  1. 让Hystrix的超时时间改为5秒
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 5000
  1. 该配置,用于禁用Hystrix的超时时间
hystrix.command.default.execution.timeout.enabled: false
  1. 索性禁用feign的hystrix
feign.hystrix.enabled: false

处理异常

如果在FeignClient调用接口,接口服务出现异常的情况下需要提取异常信息,可以使用fallbackFactory。

@FeignClient(value = "bClient", url = "${interfase.user.management}", fallbackFactory = IBClientServiceFallBackFactory.class)
public interface IBClientService {
    @RequestMapping(value = "/qual/company", method = RequestMethod.GET)
    Result<BCompanyAuthInfo> getCompanyAuthInfo(@RequestHeader("cookie") String cookie);
}

@Slf4j
@Component
class IBClientServiceFallBackFactory implements FallbackFactory<IBClientService> {

    @Override
    public IBClientService create(Throwable cause) {
        return new IBClientService() {
            @Override
            public Result<BCompanyAuthInfo> getCompanyAuthInfo(String cookie) {
                log.error(cookie + cause.getMessage());
                Result<BCompanyAuthInfo> res = new Result<>();
                res.setCode(ResultCode.FAIL.getCode());
                res.setMessage(cause.getMessage());
                return res;
            }
        };
    }
}

保留原始异常信息

当调用服务时,如果服务返回的状态码不是200,就会进入到Feign的ErrorDecoder中,因此如果我们要解析异常信息,就要重写ErrorDecoder

public class KeepErrMsgConfiguration {
    @Bean
    public ErrorDecoder errorDecoder() {
        return new UserErrorDecoder();
    }
    /**
     * 自定义错误解码器
     */
    public class UserErrorDecoder implements ErrorDecoder {
        private Logger logger = LoggerFactory.getLogger(getClass());
        @Override
        public Exception decode(String methodKey, Response response) {
            Exception exception = null;
            try {
                // 获取原始的返回内容
                String json = Util.toString(response.body().asReader());
                exception = new RuntimeException(json);
                // 将返回内容反序列化为Result,这里应根据自身项目作修改
                Result result = JsonMapper.nonEmptyMapper().fromJson(json, Result.class);
                // 业务异常抛出简单的 RuntimeException,保留原来错误信息
                if (!result.isSuccess()) {
                    exception = new RuntimeException(result.getMessage());
                }
            } catch (IOException ex) {
                logger.error(ex.getMessage(), ex);
            }
            return exception;
        }
    }
}

在@FeignClient注解中指定configuration

@FeignClient(name = "serviceId", fallbackFactory = TestServiceFallback.class, configuration = {KeepErrMsgConfiguration.class})
public interface TestService {
    @RequestMapping(value = "/get/{id}", method = RequestMethod.GET)
    String get(@PathVariable("id") Integer id);
}

超时设置

线程池设置

hystrix.threadpool.default.coreSize=100
hystrix.threadpool.default.maxQueueSize=1000

断路器超时设置和请求的超时

hystrix.command.default.execution.timeout.enabled=true
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=300000
ribbon.ConnectTimeout=300000
ribbon.ReadTimeout=300000

设置回退的最大线程数

hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests=50

HTTP Client

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

Apache的HTTP Client替换Feign原始的http client, 从而获取连接池、超时时间等与性能息息相关的控制能力。Spring Cloud从Brixtion.SR5版本开始支持这种替换,首先在项目中声明Apache HTTP Client和feign-httpclient依赖:

<!-- 使用Apache HttpClient替换Feign原生httpclient -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>
        <dependency>
            <groupId>com.netflix.feign</groupId>
            <artifactId>feign-httpclient</artifactId>
            <version>${feign-httpclient}</version>
        </dependency>

然后在application.properties中添加:

feign.httpclient.enabled=true

还可以替换OkHttpClient

feign.okhttp.enabled=true

日志

Feign的日志可以通过修改配置文件来实现

logging:
    level:
        project.user.UserClient: DEBUG

Configuration里

    @Bean
    public Logger.Level feignLoggerLevel() {
        //设置日志
        return Logger.Level.FULL;
    }