(五)springcloud从入门到放弃-Feign深入

Feign 的实战运用

Feign 默认Client 的替换

Feign 在默认情况下使用的是 JDK 原生的 URLConnection 发送 http 请求,没有连接池,但是每个地址会保持一个长连接,即利用 http 的 persistence Connection
我们可以使用 Apache 的 HttpClient 替换掉 Feign 原生的 HttpClient,通过设置连接池,超时时间等对服务之间的调用调优

使用 HTTPClient 替换 Feign 默认 Client
  • 创建一个提供服务的 feign-service, 为了演示方便 , 这里就不由提供方维护接口了
/**
 *@Description
 *@author apdoer
 *@CreateDate 2019/5/17-23:47
 *@Version 1.0
 *===============================
**/
@RestController
public class DemoController {
        @GetMapping("/hello")
        public String hello(){
            return "hello-service-feign";
        }

}
  • 创建调用服务的 http-client
    • 我们使用 apache 的 HTTPClient 来替换 feign 默认的 HTTPClient
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-openfeignartifactId>
        dependency>
        
        <dependency>
            <groupId>org.apache.httpcomponentsgroupId>
            <artifactId>httpclientartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
    
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
    dependencies>
    
    • 定义一个 client 来调用 feign-service 提供的接口
    @FeignClient(value = "feign-service") 
    public interface HelloClient {
        @GetMapping("/hello")
    	String hello();
    }
    

    通过@FeignClient指定需要调用的服务
    通过springmvc的注解绑定对应服务的对应映射

    • 创建一个controller来用于访问,方便测试
    /**
     *@Description
     *@author apdoer
     *@CreateDate 2019/5/17-23:37
     *@Version 1.0
     *===============================
    **/
    @RestController
    public class HelloController {
    
        @Autowired
        private HelloClient client;
    
        @GetMapping("/hello-client")
        public String getInfo(){
            return client.hello();
        }
    }
    
    • 在 application.yml 中配置让 feign 启动时加载 HTTP client 替换默认的 client
    server:
      port: 8010
    spring:
      application:
        name: http-client
    feign:
      httpclient:
        enabled: true
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:8761/eureka/
    
    • 然后我们访问 http-client 的接口 localhost:8010/hello-client 可以看到也是调用成功,替换成功!
      (五)springcloud从入门到放弃-Feign深入_第1张图片
使用 okhttp 替换 feign 默认的 client

http 是目前比较通用的网络请求方式
有效的使用 http 可以使应用访问速度变得更快,更节省带宽
okhttp 是一个很棒的 http 客户端,目前我自己所在的公司项目也在用,具有以下特性

支持 SPDY, 可以合并多个到同一个主机的请求
使用连接池技术减少请求的延迟【如果 SPDY 是可用的话】
使用 GZIP 压缩减少传输的数据量
缓存响应避免重复的网络请求

  • 创建一个名为 okhttp 的 springboot 工程,引入 okhttp 的依赖
 <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-openfeignartifactId>
        dependency>
        <dependency>
            <groupId>io.github.openfeigngroupId>
            <artifactId>feign-okhttpartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
    dependencies>
  • 在配置文件中开启okhttp 替换默认的client
server:
  port: 8011
spring:
  application:
    name: okhttp
feign:
  httpclient:
    enabled: false
  okhttp:
    enabled: true
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  • 这里说一下

okhttpClient 是 okhttp 的核心功能的执行者,可以通过 OKHttpClient client = new OKHTTPClient(); 来创建默认的 okHttpClient 对象
也可以使用如下的配置来自定义 okHttpClient 对象,托管给 spring 管理.这里只给了常用的配置项,其他请自行扩展

/**
 *@Description 
 *@author apdoer
 *@CreateDate 2019/5/18-16:19
 *@Version 1.0
 *===============================
**/
@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignOkhttpConfig {

    @Bean
    public okhttp3.OkHttpClient okHttpClient(){
        return new okhttp3.OkHttpClient.Builder()
                //设置连接超时时间
                .connectTimeout(60,TimeUnit.SECONDS)
                //设置读超时
                .readTimeout(60,TimeUnit.SECONDS)
                //设置写超时
                .writeTimeout(60,TimeUnit.SECONDS)
                //是否自动重连
                .retryOnConnectionFailure(true)
                .connectionPool(new ConnectionPool())
                .build();
    }
}
  • 启动项目访问localhost:8011/hello-okhttp 可以看到使用成功
    (五)springcloud从入门到放弃-Feign深入_第2张图片
Feign 的 post 和 get 的多参数传递

在实际开发中,使用 feign 进行服务间调用,多参数传递是无法避免的

web 开发中,springmvc 是支持 GET 方法直接绑定 pojo 的,但是 Feign 的实现并未覆盖所有的springmvc功能,目前常见的解决方式有下面这些

  • 把 pojo 拆散成一个个单独的属性作为方法参数
  • 把方法参数变成 Map 传递
  • 使用 GET 请求传递 @RequestBody , 但此方法违反 Restful 规范

这里我们介绍一种更加优雅的方式 , 即通过 Feign 拦截器的方式处理

通过实现 Feign 的RequestInterceptor 中的 apply 方法来进行统一拦截转换处理 Feign 中的 GET 方法多参数传递问题
Feign 进行 POST 多参数相比 GET 来说就简单的多

拦截器代码如下

@Component
public class FeignRequestInterceptor implements RequestInterceptor {

    @Autowired
    private ObjectMapper mapper;

    @Override
    public void apply(RequestTemplate requestTemplate) {
        //Feign 不支持GET 请求传 pojo ,json body 转 query
        if (requestTemplate.method().equals("GET") && requestTemplate.body() != null){
            try {
                JsonNode jsonNode = mapper.readTree(requestTemplate.body());
                requestTemplate.body((Request.Body) null);

                Map<String, Collection<String>> queries = new HashMap<>();
                buildQuery(jsonNode,"",queries);
                requestTemplate.queries(queries);
            } catch (IOException e) {
                //根据实际业务处理异常
                e.printStackTrace();
            }
        }
    }
     private void buildQuery(JsonNode jsonNode, String path, Map<String, Collection<String>> queries) {
        if (!jsonNode.isContainerNode()) {   // 叶子节点
            if (jsonNode.isNull()) {
                return;
            }
            Collection<String> values = queries.get(path);
            if (null == values) {
                values = new ArrayList<>();
                queries.put(path, values);
            }
            values.add(jsonNode.asText());
            return;
        }
        if (jsonNode.isArray()) {   // 数组节点
            Iterator<JsonNode> it = jsonNode.elements();
            while (it.hasNext()) {
                buildQuery(it.next(), path, queries);
            }
        } else {
            Iterator<Map.Entry<String, JsonNode>> it = jsonNode.fields();
            while (it.hasNext()) {
                Map.Entry<String, JsonNode> entry = it.next();
                if (StringUtils.hasText(path)) {
                    buildQuery(entry.getValue(), path + "." + entry.getKey(), queries);
                } else {  // 根节点
                    buildQuery(entry.getValue(), entry.getKey(), queries);
                }
            }
        }
    }
  }
  • 然后在 feign-sevice 另外再提供两个接口
    @GetMapping("/testGET")
    public String hello(User user) {
        return user.toString();
    }

    @PostMapping("/testPOST")
    public String testPost(User user) {
        return user.toString();
    }
  • 在 服务消费方client端维护这两个接口,在controller中调用
    @GetMapping("/okhttp_get")
    public String get(User user){
        System.out.println(user);
        return client.get(user);
    }

    @PostMapping("okhttp_post")
    public String post(User user){

        return client.post(user);
    }
  • 访问localhost:8011/okhttp_get 和用postman访问localhost:8011/okhttp_post都是可以访问到的
    (五)springcloud从入门到放弃-Feign深入_第3张图片
Feign 的文件上传

feign 早先不支持文件上传,后来虽然支持但是需要一次性完整读取到内存再编码发送
现在 feign 官方提供了子项目 feign-form , 其中实现了上传所需的 Encoder
下面会使用 feign-form 结合feign实现文件的上传

  • 创建 feign-file-server 作为文件服务器,服务提供者
    • 该工程用于接受 feign 客户端发过来的文件,这里模拟获取文件名,controller 代码如下
    @PostMapping(value = "/upload",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public String fileUploadServer(MultipartFile file){
        return file.getOriginalFilename();
    }
    
    • 在调用上传服务的客户端 client 我们创建 feign client
     @PostMapping(value = "/upload",
            consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
            produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})
    String fileUpload(@RequestPart(value = "file")MultipartFile file);
    
    • controller 代码如下
     @PostMapping(value = "/upload_client",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public String post(@RequestPart("file") MultipartFile file){
        return client.fileUpload(file);
    }
    
  • 可以看到,我们用 postman 测试是可以拿到 文件的 originName 的.由此可以证明 service 端获取到了对应的文件

需要注意的几点

  • 文件上传的客户端 produces 和 consumes 必填
  • 注意区分 @RequestParam 和 @RequestPart 的区别,不要写错

解决 Feign 首次请求失败的问题

当 Feign 和 Ribbon 整合了 Hystrix 之后,可能会出现首次调用失败的问题,原因如下

  • Hystrix 的默认超时时间是 1 秒, 如果超过 1秒未响应 , 将会进入 fallback 代码, 由于 bean 的装配以及懒加载机制等 , Feign首次请求都会比较慢,大于1秒, 就会出现请求失败的情况

解决方式

  • 1 将Hystrix 的超时时间改为5秒
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds:5000
  • 2 禁用 hystrix 的超时时间
hystrix.command.default.execution.timeout.enabled:false
  • 3 使用 Feign 的时候直接关闭 Hystrix , 一般不这么做
feign.hystrix.enabled:false

Feign 返回图片流处理方式

在使用 Feign 的过程中 可以将流转换成 字节数组传递 , 但是因为Controller 层的返回不能直接返回 byte ,因此可以将 Feign 的返回值修改为 response或者ResponseEntity

@GetMapping("/createImage")
public byte[] createImage(@RequestParam("imageKey") String imageKey);
@GetMapping("/createImage")
public Response createImage(@RequestParam ("imageKey") String imageKey);
@GetMapping("/createImage")
public ResponseEntity<byte> createImage(@RequestParam ("imageKey") String imageKey);

Feign 调用传递 Token

在进行认证鉴权的时候, 不管是 jwt 还是security ,当使用 Feign 时就会发现外部请求到 A服务的时候,A服务是可以拿到 token 的, 然而当服务使用 Feign 调用 服务B服务时 , Token 就会丢失,从而认证失败,解决比较简单, 就是在Feign调用时,向请求头里添加需要传递的 Token

实现 Feign 提供的 一个接口 RequestInterceptor, 假设我们在验证权限时放在请求头里的key 为 oauthToken ,先获取当前请求的key为 oauthToken ,然后放到 Feign 的请求 Header上【像这种通用的代码确定传递的key后建议统一到通用的二方库使用】

/**
 *@Description token 过滤器
 *@author apdoer
 *@CreateDate 2019/5/19-0:07
 *@Version 1.0
 *===============================
**/
public class FeignTokenInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        if (null == getHttpServletRequest()){
            return;
        }

        //将获取的Token对应值向下传递
        requestTemplate.header("oauthToken",getHttpServletRequest().getHeader("oauthToken"));
    }
    


    private HttpServletRequest getHttpServletRequest() {
        try {
            return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        }catch (Exception e){
            return null;
        }
    }
}

总结

  • 使用 apache 的HTTPClient 替换 Feign 自带的 HTTPClient
  • 使用 okhttp 替换 Feign 自带的 HttpClient
  • Feign 的多参数传递问题
  • Feign 服务间文件上传问题
  • Feign 的图片返回和 Token 传递问题
  • 以上

你可能感兴趣的:(springcloud)