Feign WHAT WHY HOW maven依赖 自动装配 编写接口 调用接口 注意事项 原理html
Feign的GitHub描述以下:java
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.git
简单的说,Feign是一套Http客户端"绑定器"。我的理解,这个"绑定"有点像ORM。ORM是把数据库字段和代码中的实体"绑定"起来;Feign提供的基本功能就是方便、简单地把Http的Request/Response和代码中的实体"绑定"起来。github
举个例子,在咱们系统调用时,咱们是这样写的:web
@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>了。spring
与其它的Http调用方式,例如URLConnection、HttpClient、RestTemplate相比,Feign有哪些优点呢?数据库
最核心的一点在于,Feign的抽象层次比其它几个工具、框架都更高。json
首先,通常来讲抽象层次越高,其中包含的功能也就越多。
api
此外,抽象层次越高,使用起来就越简便。例如,上面这个例子中,把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代码、查工单等重复劳动中解脱出来。怎样“以不变应万变”呢?提升系统设计的抽象层次就是一个不错的办法。
Feign有好几种用法:既能够在代码中直接使用FeignBuilder来构建客户端、也可使用Feign自带的注解、还可使用SpringMVC的注解。这里只介绍下使用SpringMVC注解的方式。
咱们系统引入的依赖是这样的:
<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地址与本系统中Controller定义的地址不能有冲突。例如:
@Controller
public class Con{
@PostMapping("/test")
public void test(){}
}
@FeignClient(name="testClient")
public interface Fei{
@PostMapping("/test")
public void test();
}
上面这种状况下,Feign解析会报错。
经过@FeignClient注解中configuration字段指定的自定义配置类,不能被SpringIoC扫描、装载进来,不然可能会有问题。
通常的文档都是这么写的,可是咱们系统在调用时的自定义配置是会被SpringIOC扫描装载的,并无遇到什么问题。
须要指定一个这样的bean,不然在装配Feign时会出现循环依赖的问题:
@Bean
public HttpMessageConverters HttpMessageConverters() {
return new HttpMessageConverters();
}
在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;
}
}
// 后续略