FeignFeign WHAT WHY HOW maven依赖 自动装配 编写接口 调用接口 注意事项 原理 WHATFeign的GitHub描述如下: Feign is a Java to Http client binder inspired by Retrofit, JAXRS-2.0, and WebSocket. Feign's first goal was reducing the complexity of binding Denominator uniformly to Http APIs regardless of ReSTfulness.
简单的说,Feign是一套Http客户端"绑定器"。个人理解,这个"绑定"有点像ORM。ORM是把数据库字段和代码中的实体"绑定"起来;Feign提供的基本功能就是方便、简单地把Http的Request/Response和代码中的实体"绑定"起来。 举个例子,在我们系统调用时,我们是这样写的: @FeignClient(url = "${feign.url.user}", name = "UserInfoService", configuration = FeignConfiguration.UserInfoFeignConfiguration.class) public interface UserInfoService { /** * 查询用户数据 * * @param userInfo 用户信息 * @return 用户信息 */ @PostMapping(path = "/user/getUserInfoRequest") BaseResult<UserInfoBean> queryUserInfo(Body4UserInfo userInfo); } // 使用时 BaseResult<UserInfoBean> response = UserInfoService.queryUserInfo(Body4UserInfo.of(userBean.getId())); 上面这段代码里,我们只需要创建一个Body4UserInfo,然后像调用本地方法那样,就可以拿到返回对象BaseResult<UserInfoBean>了。 WHY与其它的Http调用方式,例如URLConnection、HttpClient、RestTemplate相比,Feign有哪些优势呢? 最核心的一点在于,Feign的抽象层次比其它几个工具、框架都更高。 首先,一般来说抽象层次越高,其中包含的功能也就越多。
此外,抽象层次越高,使用起来就越简便。例如,上面这个例子中,把Body4UserInfo转换为HttpRequest、把HttpResponse转换为BaseResult<UserInfoBean>的操作,就不需要我们操心了。 当然,单纯从这一个例子中,看不出Feign提供了多大的帮助。但是可以想一下:如果我们调用的接口,有些参数要用RequestBody传、有些要用RequestParam传;有些要求加特殊的header;有些要求Content-Type是application/json、有些要求是application/x-www-form-urlencoded、还有些要求application/octet-stream呢?如果这些接口的返回值有些是applicaion/json、有些是text/html,有些是application/pdf呢?不同的请求和响应对应不同的处理逻辑。我们如果自己写,可能每次都要重新写一套代码。而使用Feign,则只需要在对应的接口上加几个配置就可以。写代码和加配置,显然后者更方便。 此外,抽象层次越高,代码可替代性就越好。如果尝试过Apache的HttpClient3.x升级到4.x,就知道这种接口不兼容的升级改造是多么痛苦。如果要从Apache的HttpClient转到OkHttp上,由于使用了不同的API,更要费一番周折。而使用Feign,我们只需要修改几行配置就可以了。即使要从Feign转向其它组件,我只需要给UserInfoService提供一个新的实现类即可,调用方代码甚至一行都不用改。如果我们升级一个框架、重构一个组件,需要改的代码成百上千行,那谁也不敢乱动代码。代码的可替代性越好,我们就越能放心、顺利的对系统做重构和优化。 而且,抽象层次越高,代码的可扩展性就越高。如果我们使用的还是URLConnection,那么连Http连接池都很难实现。如果我们使用的是HttpClient或者RESTTemplate,那么做异步请求、合并请求都需要我们自己写很多代码。但是,使用Feign时,我们可以轻松地扩展Feign的功能:异步请求、并发控制、合并请求、负载均衡、熔断降级、链路追踪、流式处理、Reactive……,还可以通过实现feign.Client接口或自定义Configuration来扩展其它自定义的功能。 放眼Java世界的各大框架、组件,无论是URLConnection、HttpClient、RESTTemplate和Feign,Servlet、Struts1.0/2.0和SpringMVC,还是JDBCConnection、myBatis/Hibernate和Spring-Data JPA,Redis、Jedis和Redisson,越新、越好用的框架,其抽象层级通常都更高。这对我们同样也是一个启示:我们需要去学习、了解和掌握技术的底层原理;但是在设计和使用时,我们应该从底层跳出来、站在更高的抽象层级上去设计和开发。尤其是对业务开发来说,频繁的需求变更是难以避免的,我们只有做出能够“以不变应万变”、“以系统的少量变更应对需求的大量变更”,才能从无谓的加班、copy代码、查工单等重复劳动中解脱出来。怎样“以不变应万变”呢?提高系统设计的抽象层次就是一个不错的办法。 HOWFeign有好几种用法:既可以在代码中直接使用FeignBuilder来构建客户端、也可以使用Feign自带的注解、还可以使用SpringMVC的注解。这里只介绍下使用SpringMVC注解的方式。 maven依赖我们系统引入的依赖是这样的: <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>2.1.1.RELEASE</version> <exclusions> <exclusion> <artifactId>spring-web</artifactId> <groupId>org.springframework</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okHttp</artifactId> <version>10.1.0</version> </dependency> 直接引入spring-cloud-starter-openfeign,是因为这个包内有feign的自动装配相关代码,不需要我们再自己手写。 另外,这里之所以是openfeign、而不是原生的feign,是因为原生的Feign只支持原生的注解,openfeign是SpringCloud项目加入了对SpringMVC注解的支持之后的版本。 引入feign-okHttp则是为了在底层使用okHttp客户端。默认情况下,feign会直接使用URLConnection;如果系统中引入了Apache的HttpClient包,则OpenFeign会自动把HttpClient装配进来。如果要使用OkHttpClient,首先需要引入对应的依赖,然后修改一点配置。 自动装配如果使用了SpringBoot,那么直接用@EnableFeignClient就可以自动装配了。如果没有使用SpringBoot,则需要自己导入一下其中的AutoConfiguration类: /** * 非SpringBoot的系统需要增加这个类,并保证Spring Context启动时加载到这个类 */ @Configuration @ImportAutoConfiguration({FeignAutoConfiguration.class}) @EnableFeignClients(basePackages = "com.test.test.feign") public class FeignConfiguration { } 上面这个类可以没有具体的实现,但是必须有几个注解。 @Configuration 使用这个注解是为了让Spring Conetxt启动时装载这个类。在xml文件里配<context:component-scan base-package="com.test.user">,或者使用@Component可以起到相同的作用。 @ImportAutoConfiguration({FeignAutoConfiguration.class}) 使用这个注解是为了导入FeignAutoConfiguration中自动装配的bean。这些bean是feign发挥作用所必须的一些基础类,例如feignContext、feignFeature、feignClient等等。 @EnableFeignClients(basePackages = "com.test.user.feign") 使用这个注解是为了扫描具体的feign接口上的@FeignClient注解。这个注解的用法到后面再说。
为了使用okHttp、而不是Apache的HttpClient,我们还需要在系统中增加两行配置: # 使用properties文件配置 feign.okHttp.enabled=true feign.Httpclient.enabled=false 这两行配置也可以用yml格式配置,只要能被SpringContext解析到配置就行。配置好以后,FeignAutoConfiguration就会按照OkHttpFeignConfiguration的代码来把okHttp3.OkHttpClient装配到FeignClient里去了。 @Configuration @ConditionalOnClass(Feign.class) @EnableConfigurationProperties({ FeignClientProperties.class, FeignHttpClientProperties.class }) public class FeignAutoConfiguration { // 其它略 @Configuration @ConditionalOnClass(ApacheHttpClient.class) @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer") @ConditionalOnMissingBean(CloseableHttpClient.class) @ConditionalOnProperty(value = "feign.Httpclient.enabled", matchIfMissing = true) protected static class HttpClientFeignConfiguration { // 其它略 } @Configuration @ConditionalOnClass(OkHttpClient.class) @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer") @ConditionalOnMissingBean(okHttp3.OkHttpClient.class) @ConditionalOnProperty("feign.okHttp.enabled") protected static class OkHttpFeignConfiguration { // 其它略 } 除了这两个配置之外,FeignClientProperties和FeignHttpClientProperties里面还有很多其它配置,大家可以关注下。 编写接口依赖和配置都弄好之后,就可以写一个Fiegn的客户端接口了: @FeignClient(url = "${feign.url.user}", name = "UserInfoService", configuration = FeignConfiguration.UserFeignConfiguration.class) public interface UserInfoService { @PostMapping(path = "/user/getUserInfoRequest") BaseResult<UserInfoBean> queryUserInfo(Body4UserInfo userInfo); 首先,我们只需要写一个接口,并在接口上加上@FeignClient注解、接口方法上加上@RequestMapping(或者@PostMapping、@GetMappping等对应注解)。Feign会根据@EnableFeignClients(basePackages = "com.test.user.feign")的配置,扫描到@FeignClient注解,并为注解类生成动态代理。因此,我们不需要写具体的实现类。 然后,配置好@FeignClient和@PostMapping中的各个字段。@PostMapping注解字段比较简单,和我们写@Controller时的配置方式基本一样。@FeignClient注解字段有下面这几个: @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface FeignClient { @AliasFor("name") String value() default ""; @Deprecated String serviceId() default ""; String contextId() default ""; @AliasFor("value") String name() default ""; String qualifier() default ""; String url() default ""; boolean decode404() default false; Class<?>[] configuration() default {}; Class<?> fallback() default void.class; Class<?> fallbackFactory() default void.class; String path() default ""; boolean primary() default true; } 每个字段的配置含义大家可以参考GitHub上的文档,或者看这个类的javadoc。常用的大概就是name、url、configuration这几个。 name字段有两种含义。如果是配合SpringCloud一起使用,并且没有配置url字段的情况下,那么name字段就是服务提供方在Eureka上注册的服务名。Feign会根据name字段到Eureka上找到服务提供方的url。如果没有与SpringCloud一起使用,name字段会用做url、contextId等字段的备选:如果没有配置后者,那么就拿name字段值当做后者来使用。 url字段用来指定服务方的地址。这个地址可以不带协议前缀(Http://,feign默认是Http,如果要用Https需要增加配置),例如我们配置了“ka.test.idc/”,实际调用时则是“Http://ka.test.idc/”。 configuration字段用来为当前接口指定自定义配置。有些接口调用需要在feign通用配置之外增加一些自定义配置,例如调用百度api需要走代理、调用接口需要传一些额外字段等。这些自定义配置就可以通过configuration字段来指定。不过configuration字段只能指定三类自定义配置:Encoder、Decoder和Contract。Encoder和Decoder分别负责处理对象到HttpRequest和HttpResponse到对象的转换;Contract则定义了如何解析这个接口和方法上的注解(SpringCloud就是通过Contract接口的一个子类SpringMvcContract来解析方法上的SpringMVC注解的)。 调用接口定义好了上面的接口后,我们使用起来就很简单了: @Service("UserInfoBusiness") public class UserInfoBusinessImpl implements UserInfoBusiness { @Resource private UserInfoService UserInfoService; @Override public UserInfoBean getUserInfo(String id) { //feign连接 BaseResult<UserInfoVo> response = UserInfoService.queryUserInfoRequest(UserInfoService.Body4UserInfo.of(id)); // 其它略 } 可以看到这里的代码,和我们使用其它的bean的方式是一样的。 注意事项使用Feign客户端需要注意几个事情。 Feign的RequestMapping不能与本系统中SpringMVC的配置冲突Feign接口上定义RequestMapping地址与本系统中Controller定义的地址不能有冲突。例如: @Controller public class Con{ @PostMapping("/test") public void test(){} } @FeignClient(name="testClient") public interface Fei{ @PostMapping("/test") public void test(); } 上面这种情况下,Feign解析会报错。 自定义configuration不能被装载到SpringContext中通过@FeignClient注解中configuration字段指定的自定义配置类,不能被SpringIoC扫描、装载进来,否则可能会有问题。 一般的文档都是这么写的,但是我们系统在调用时的自定义配置是会被SpringIOC扫描装载的,并没有遇到什么问题。 与SpringMVC配合使用时,需要单独声明HttpMessageConverters需要指定一个这样的bean,否则在装配Feign时会出现循环依赖的问题: @Bean public HttpMessageConverters HttpMessageConverters() { return new HttpMessageConverters(); } 使用@RequestParam注解时,必须指定name字段在SpringMVC中,@RequestParam注解如果不指定name字段,那么会以变量名作为queryString的参数名;但是在FeignClient中使用@RequestParam时,则必须指定name字段,否则会无法解析参数。 @Controller public class Con{ /**这里的@RequestParam不用指定name,调用时会根据变量名自动解析为 test=? */ @PostMapping("/test") public void test(@RequestParam String test){} } @FeignClient(name="test") public interface Fei{ /**这里的@RequestParam必须指定name,否则调用时会报错 */ @GetMapping("/test") public String test(@RequestParam(name="test") String test); } 原理说起来其实很简单,和其它使用注解的框架一样,Feign是通过动态代理来动态实现@FeignClient的接口的。 详细一点来说,Feign通过FeignClientBuilder来动态构建被代理对象。在构建动态代理时,通过FeignClientFactoryBean和Feign.Builder来把@FeignClient接口、Feign相关的Configuration组装在一起。 public class FeignClientBuilder{ public static final class Builder<T> { private FeignClientFactoryBean feignClientFactoryBean; /** * @param <T> the target type of the Feign client to be created * @return the created Feign client */ public <T> T build() { return this.feignClientFactoryBean.getTarget(); } } // 其它略 } class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware { <T> T getTarget() { FeignContext context = this.applicationContext.getBean(FeignContext.class); Feign.Builder builder = feign(context); if (!StringUtils.hasText(this.url)) { if (!this.name.startsWith("Http")) { this.url = "Http://" + this.name; } else { this.url = this.name; } this.url += cleanPath(); return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, this.url)); } if (StringUtils.hasText(this.url) && !this.url.startsWith("Http")) { this.url = "Http://" + this.url; } String url = this.url + cleanPath(); Client client = getOptional(context, Client.class); if (client != null) { if (client instanceof LoadBalancerFeignClient) { // not load balancing because we have a url, // but ribbon is on the classpath, so unwrap client = ((LoadBalancerFeignClient) client).getDelegate(); } builder.client(client); } Targeter targeter = get(context, Targeter.class); // 在这个里面生成一个代理 return (T) targeter.target(this, builder, context, new HardCodedTarget<>(this.type, this.name, url)); } // 其它略 } // 中间跳转略 public class ReflectiveFeign extends Feign { public <T> T newInstance(Target<T> target) { Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target); Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>(); List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>(); for (Method method : target.type().getMethods()) { if (method.getDeclaringClass() == Object.class) { continue; } else if (Util.isDefault(method)) { DefaultMethodHandler handler = new DefaultMethodHandler(method); defaultMethodHandlers.add(handler); methodToHandler.put(method, handler); } else { methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); } } InvocationHandler handler = factory.create(target, methodToHandler); // 在这里生成动态代理。 T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[] {target.type()}, handler); for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { defaultMethodHandler.bindTo(proxy); } return proxy; } } // 后续略
|