WHAT
Feign的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代码、查工单等重复劳动中解脱出来。怎样“以不变应万变”呢?提高系统设计的抽象层次就是一个不错的办法。
HOW
Feign有好几种用法:既可以在代码中直接使用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注解。这个注解的用法到后面再说。
feign.okhttp.enabled
为了使用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;
}
}
// 后续略
©著作权归作者所有:来自51CTO博客作者winters1224的原创作品,请联系作者获取转载授权,否则将追究法律责任 Feign简介 https://blog.51cto.com/winters1224/2445560