Feign

Feign是声明式Web Service客户端,支持REST和SOAP Service,简化了Web Service客户端开发。

为使用Feign仅需创建一个接口并添加注解。

调用GitHub REST Service

public interface GitHub {
    @RequestLine("GET /repos/{owner}/{repo}/contributors")
    List contributors(@Param("owner") String owner, @Param("repo") String repo);

    @RequestLine("POST /repos/{owner}/{repo}/issues")
    void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo);
}

@Data
public class Contributor {
    private String login;
    private int contributions;
}

@Data
public class Issue {
    private String title;
    private String body;
    private List assignees;
    private int milestone;
    private List labels;
}

public class FeignExample {
    public static void main(String... args) {
        GitHub github = Feign.builder()
                .decoder(new GsonDecoder())
                .target(GitHub.class, "https://api.github.com");

        // Fetch and print a list of the contributors to this library.
        List contributors = github.contributors("OpenFeign", "feign");
        for (Contributor contributor : contributors) {
            System.out.println(contributor.getLogin() + " (" + contributor.getContributions() + ")");
        }
    }
}

调用SOAP Service

public interface MyApi {

    @RequestLine("POST /getObject")
    @Headers({
            "SOAPAction: getObject",
            "Content-Type: text/xml"
    })
    MyJaxbObjectResponse getObject(MyJaxbObjectRequest request);

}

public class SoapExample {
    public static void main(String[] args) {
        JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder()
                .withMarshallerJAXBEncoding("UTF-8")
                .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
                .build();

        MyApi api = Feign.builder()
                .encoder(new SOAPEncoder(jaxbFactory))
                .decoder(new SOAPDecoder(jaxbFactory))
                .errorDecoder(new SOAPErrorDecoder())
                .target(MyApi.class, "http://api");

        try {
            api.getObject(new MyJaxbObjectRequest());
        } catch (SOAPFaultException faultException) {
            log.info(faultException.getFault().getFaultString());
        }
    }
}

Feign支持Feign和JAX-RS注解。

Feign Annotations

Annotation Interface Target Usage
@RequestLine Method 定义HttpMethod和UriTemplate,用大括号 {expression} 引用@Param参数
@Param Parameter 定义template变量
@Headers Method, Type 定义HeaderTemplate
@QueryMap Parameter 定义名值对map或POJO, 用来动态构造查询字符串
@HeaderMap Parameter 定义名值对map,用来动态构造Http Headers
@Body Method 定义Template,相似于UriTemplate和HeaderTemplate,使用@Param注解解析相应的表达式

@QueryMap示例

public interface Api {
  @RequestLine("GET /find")
  V find(@QueryMap Map queryMap);
}

public interface Api {
  @RequestLine("GET /find")
  V find(@QueryMap CustomPojo customPojo);
}

@HeaderMap示例

public interface Api {
   @RequestLine("POST /")
   void post(@HeaderMap Map headerMap);
}

@Body示例

interface LoginClient {

  @RequestLine("POST /")
  @Headers("Content-Type: application/xml")
  @Body("")
  void xml(@Param("user_name") String user, @Param("password") String password);

  @RequestLine("POST /")
  @Headers("Content-Type: application/json")
  // json curly braces must be escaped!
  @Body("%7B\"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
  void json(@Param("user_name") String user, @Param("password") String password);
}

JAXRS Annotations

Annotation Interface Target Usage
@Path Type,Method Appends the value to Target.url(). Can have tokens corresponding to @PathParam annotations
@HttpMethod, @GET, @POST, @PUT, @DELETE ... Method Sets the request method
@Produces Method Adds all values into the Accept header
@Consumes Method Adds the first value as the Content-Type header
@PathParam Parameter Links the value of the corresponding parameter to a template variable declared in the path
@QueryParam Parameter Links the value of the corresponding parameter to a query parameter
@HeaderParam Parameter Links the value of the corresponding parameter to a header
@FormParam Parameter Links the value of the corresponding parameter to a key passed to Encoder.Text>.encode().

示例

public class JaxRsExample {
    interface GitHub {
        @GET
        @Path("/repos/{owner}/{repo}/contributors")
        List contributors(@PathParam("owner") String owner, @PathParam("repo") String repo);
    }

    public static void main(String[] args) {
        GitHub github = Feign.builder()
                .contract(new JAXRS2Contract())
                .decoder(new GsonDecoder())
                .target(GitHub.class, "https://api.github.com");

        // Fetch and print a list of the contributors to this library.
        List contributors = github.contributors("OpenFeign", "feign");
        for (Contributor contributor : contributors) {
            System.out.println(contributor.getLogin() + " (" + contributor.getContributions() + ")");
        }
    }
}

Encoder & Decoder

Feign支持Gson、Jackson JSON API编/解码,支持JAXB XML API编/解码,支持Sax XML API解码。

GSON

public class Example {
  public static void main(String[] args) {
    GitHub github = Feign.builder()
                         .encoder(new GsonEncoder())
                         .decoder(new GsonDecoder())
                         .target(GitHub.class, "https://api.github.com");
  }
}

Jackson

public class Example {
  public static void main(String[] args) {
      GitHub github = Feign.builder()
                     .encoder(new JacksonEncoder())
                     .decoder(new JacksonDecoder())
                     .target(GitHub.class, "https://api.github.com");
  }
}

JAXB

public class Example {
    public static void main(String[] args) {
        JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder()
                .withMarshallerJAXBEncoding("UTF-8")
                .withMarshallerSchemaLocation("http://apihost http://apihost/schema.xsd")
                .build();

        Response response = Feign.builder()
                .encoder(new JAXBEncoder(jaxbFactory))
                .decoder(new JAXBDecoder(jaxbFactory))
                .target(Response.class, "https://apihost");
    }
}

Sax

public class Example {
    public static void main(String[] args) {
        Api api = Feign.builder()
                .decoder(SAXDecoder.builder()
                        .registerContentHandler(UserIdHandler.class)
                        .build())
                .target(Api.class, "https://apihost");
    }
}

Spring OpenFeign

Spring Cloud OpenFeign是Spring Cloud的子项目,集成OpenFeign声明式REST Client,集成Ribbon、Eureka、Spring Cloud LoadBalancer实现负载均衡的HTTP客户端。

说明:Spring Cloud的子项目Spring Cloud Netflix集成了Ribbon(客户端负载均衡)、Eureka(服务发现),子项目Spring Cloud Commons提供Spring Cloud LoadBalancer。OpenFeign集成Eureka的例子请看spring-cloud-samples/feign-eureka 。

Spring Boot集成

  1. 添加spring-cloud-starter-openfeign依赖

    ...
    
        org.springframework.cloud
        spring-cloud-starter-openfeign
    
    ...



    
        
            org.springframework.cloud
            spring-cloud-dependencies
            ${spring-cloud.version}
            pom
            import
        
        ...
    
  1. 启用OpenFeign
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class OpenFeignApplication {

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

}
  1. 声明FeignClient
import org.itrunner.openfeign.model.Contributor;
import org.itrunner.openfeign.model.Issue;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.List;

@FeignClient(name = "github", url = "https://api.github.com")
public interface GitHubClient {
    @GetMapping("/repos/{owner}/{repo}/contributors")
    List contributors(@PathVariable("owner") String owner, @PathVariable("repo") String repo);

    @PostMapping("/repos/{owner}/{repo}/issues")
    void createIssue(@RequestBody Issue issue, @PathVariable("owner") String owner, @PathVariable("repo") String repo);
}
  1. 调用FeignClient
import org.itrunner.openfeign.feign.GitHubClient;
import org.itrunner.openfeign.model.Contributor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class GitHubController {
    private final GitHubClient gitHubClient;

    @Autowired
    public GitHubController(GitHubClient gitHubClient) {
        this.gitHubClient = gitHubClient;
    }

    @GetMapping("/contributors")
    public List contributors() {
        return gitHubClient.contributors("OpenFeign", "feign");
    }
}

OpenFeign配置

命名客户端

命名客户端(named client)是Spring Cloud OpenFeign的一个核心概念,使用@FeignClient注解指定。Spring Cloud为每个命名客户端创建一个ApplicationContext,包含feign.Decoder、 feign.Encoder、feign.Contract等。

默认配置

@FeignClient的configuration参数,默认是通过 FeignClientsConfiguration 类定义的,默认值如下(BeanType beanName: ClassName):

  • Decoder feignDecoder: ResponseEntityDecoder
  • Encoder feignEncoder: SpringEncoder
  • Logger feignLogger: Slf4jLogger
  • Contract feignContract: SpringMvcContract
  • Feign.Builder feignBuilder: HystrixFeign.Builder
  • Client feignClient: 如classpath中存在Ribbon且被启用则为LoadBalancerFeignClient,如classpath中存在Spring Cloud LoadBalancer则为FeignBlockingLoadBalancerClient,如果前两者都不存在,则为默认的feign client HttpURLConnection。

说明:spring-cloud-starter-openfeign包含optional spring-cloud-starter-netflix-ribbon和spring-cloud-starter-loadbalancer。

自定义配置

  • 使用@FeignClient声明额外的configuration
@FeignClient(name = "github", configuration = GitHubConfiguration.class)
public interface GitHubClient {
    //..
}
public class GitHubConfiguration {
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }

    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
        return new BasicAuthRequestInterceptor("user", "password");
    }
}

上例,client将组合FeignClientsConfiguration和GitHubConfiguration的配置,后者会覆盖前者。

注意:Configuration不能使用注解@Configuration,否则应将其放在@ComponentScan扫描范围外的地方。不然,该configuration将作为默认Configuration。

如果我们创建了多个同名的FeignClient,以便指向相同的server,但他们的自定义配置不同,可以使用contextId属性避免冲突:

@FeignClient(contextId = "common", name = "github")
public interface GitHubClient {
    //..
}

@FeignClient(contextId = "auth", name = "github", configuration = GitHubConfiguration.class)
public interface AuthGitHubClient {
    @GetMapping("/repos/{owner}/{repo}/traffic/views")
    Traffic traffics(@PathVariable("owner") String owner, @PathVariable("repo") String repo, @SpringQueryMap Params params);
}

另外,可以使用@EnableFeignClients的defaultConfiguration属性定义全局默认配置。

  • 自定义配置属性

application.yml

feign:
  client:
    config:
      feignName:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: full
        errorDecoder: com.example.SimpleErrorDecoder
        retryer: com.example.SimpleRetryer
        requestInterceptors:
          - com.example.FooRequestInterceptor
          - com.example.BarRequestInterceptor
        decode404: false
        encoder: com.example.SimpleEncoder
        decoder: com.example.SimpleDecoder
        contract: com.example.SimpleContract

定义默认配置:

feign:
  httpclient:
    enabled: true
  client:
    default-config: default
    default-to-properties: true
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic

说明:

  1. 配置属性的优先级高于@Configuration bean
  2. 默认启用Apache HTTP Client,feign.httpclient.enabled=true
  3. 若要启用OkHttpClient,将feign.okhttp.enabled设为true并添加okhttp依赖:

  com.squareup.okhttp3
  okhttp
  4.5.0-RC1

集成Ribbon

  1. 添加依赖

  org.springframework.cloud
  spring-cloud-starter-netflix-ribbon
  1. 启用Ribbon
spring:
  cloud:
    loadbalancer:
      ribbon:
        enabled: true
  1. 配置Server
github:
  ribbon:
    listOfServers: https://api.github.com
  1. 修改FeignClient

@FeignClient注解不必定义url,ribbon会自动查找server地址。

@FeignClient(name = "github")
public interface GitHubClient {
  \\..
}

集成Spring Cloud LoadBalancer

Spring Cloud Netflix Ribbon正在维护中,推荐使用Spring Cloud LoadBalancer。

  1. 添加依赖

  org.springframework.cloud
  spring-cloud-starter-loadbalancer
  1. 禁用Ribbon

为保持向后兼容,默认load-balancer是ribbon,为使用Spring Cloud LoadBalancer,要禁用ribbon。

spring:
  cloud:
    loadbalancer:
      ribbon:
        enabled: false
  1. 配置discovery client
spring:
  cloud:
    discovery:
      client:
        simple:
          instances:
            github[0]:
              uri: https://api.github.com
  1. 修改FeignClient

@FeignClient注解不必定义url,LoadBalancer会自动查找Service Instance。

@FeignClient(name = "github")
public interface GitHubClient {
  \\..
}

GitHub源码

参考文档

Spring Cloud OpenFeign