Feign简介与Spring代理自动注入
Feign是一款Java的基于注解的HTTP客户端,主要在服务端使用较广,属于Netflix系列。本文从动态代理开始介绍,然后推广到如何分离硬编码,到最后基于Spring的扫描装配实现。
面向读者
- 有一定的Feign, Retrofit, Mybatis等基础
- 了解Spring,反射,动态代理等技术
读者将学到
- Feign的关键代码位置
- 如何持续改进项目,降低硬编码
- 如何自己实现Spring的自动注入
Feign的简单介绍
注解与动态代理
Feign本身只是一个注解的Parser,并没有负载均衡的功能。它与Retrofit类似,通过注解这种外部DSL拼装出容易理解的HTTP请求,并通过JDK动态代理实现
feign.ReflectiveFeign#newInstance
默认的Handler实现
feign.InvocationHandlerFactory.Default#create
默认动态代理调用实现是
feign.SynchronousMethodHandler#invoke
网络请求客户端
网络传输Client是通过对OkHttp/自带/RxJava等客户端的包装实现,只用实现Client接口,就可以对业务进行定制。比如自带的如下
// 自带网络请求实现
feign.Client.Default#execute
再比如SpringCloud中的负载均衡客户端如下
// Ribbon负载均衡
org.springframework.cloud.netflix.feign.ribbon.LoadBalancerFeignClient#execute
总的来说,有读过Retrofit的人再看这个难度不大,相反由于使用了Java8,代码量进一步减小
上述过程可以参考Retrofit的相关资料,比如参考这里
使用Feign一步步简化业务硬编码
在IT业务开发中,一般会涉及到与其它小组进行对接,其它组可能并没有用Eureka,而是采用了传统的HTTP接口。这时问题就来了,如果有多个业务需要拉通,就需要维护多个URL,传统的手段是采用多个HTTPClient类进行拼装与解析JSON,但是效率非常低(比如某些外包,拿着上万的工资,宁可复制20份url,写20个HTTPClient,也不想办法去思考改进,害人害己,30岁就废了),这个代码例子就不举例了。
对此,我们可以参考MybatisSpring的Mapper搜索,SpringCloud等源码中的实现方法,并进行学习吸收,实现简化与第三方的拉通。
使用Feign干掉模版代码
假如说,我们现有业务系统需要集成对接一个HTTP DNS的服务
# 下为DNSPod的例子
$ curl http://119.29.29.29/d?dn=gitbook.com
104.25.212.20;104.25.213.20
现在我们可以仿照Feign官网的例子,写一个Demo
DNSPodService service = Feign.builder().decoder(new Decoder() {
@Override
public List<String> decode(Response response, Type type) throws IOException, DecodeException, FeignException {
// 下面没有校验,为了节约版面
String s = Util.toString(response.body().asReader());
if (s.contains(";")) {
return Arrays.asList(s.split(";"));
}
return Collections.singletonList(s);
}
}).target(DNSPodService.class, "http://119.29.29.29");
List<String> ipInfo = service.getIpInfo("gitbook.io");
接口如下
// 下面的dn是硬编码,不要在意这些细节,因为我没有引入复杂的Encoder
public interface DNSPodService {
@RequestLine("GET /d?dn={domain}")
List<String> getIpInfo(@Param("domain") String domain);
}
这样,我们的一个服务就完成了,当接口中的方法比较多时,相对于纯HTTPClient的写法,这种收益就比较明显了。
通过注解替换Java中的地址硬编码
上面的代码中的Url是在Java中硬编码写死的,这样肯定不好维护。我们考虑加入如下注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HTTPUrl {
String value() default "";
}
然后加到接口中
@HTTPUrl("http://119.29.29.29")
public interface DNSPodService {
...
}
接着,使用反射获取值
HTTPUrl anno = DNSPodService.class.getAnnotation(HTTPUrl.class);
// 此处可以用CucurrentHashMap<Class,String>来做缓存
String url = anno.value();
// 下文省略了Decoder
Feign.builder().target(DNSPodService.class, url)
通过注解标记,看起来更加连贯,读者心理上阅读代码会更加顺畅,看到这个接口就能想到有哪些服务需要维护。
通过Properties/配置中心/数据字典干掉注解硬编码
虽然这样修改有了一定改进,但是这样做仍然没有解决硬编码的问题,我们首先修改接口为如下
+ @HTTPUrl("dnspod.addr")
- @HTTPUrl("http://119.29.29.29")
public interface DNSPodService {
}
接着新增一个命名服务的接口
// 此接口将读取"dnspod.addr",并返回真正的url
public interface Resolvable {
String resolve(String key);
}
然后我们先实现一个基于properties的实现类
public class PropertiesResolvable implements Resolvable {
Environment env;
public PropertiesResolvable(Environment env) {
this.env = env;
}
@Override
public String resolve(String key) {
return env.getProperty(key);
}
}
然后在application.properties
中配置好这个kv,接着跑起来
// 你的SpringBoot项目实现CommandLineRunner接口
@Autowired
Environment env;
@Override
public void run(String... args) throws Exception {
HTTPUrl anno = DNSPodService.class.getAnnotation(HTTPUrl.class);
String value = anno.value();
Resolvable resolvable = new PropertiesResolvable(env);
String realAddr = resolvable.resolve(value);
// 下文省略了Decoder
DNSPodService service = Feign.builder().target(DNSPodService.class, realAddr);
List<String> ipInfo = service.getIpInfo("gitbook.io");
System.out.println("ipInfo = " + ipInfo);
}
可以发现,目前我们实现了将硬编码管理转移到了Properties上,同时没有丢失Java代码的可读性
使用配置中心/数据字典的例子同上,只要实现了Resolvable
接口即可热部署。
使用Spring与扫描器实现自动注册
制定目标----通过@Autowired
自动生成Feign实例
我们希望通过自动/主动装配实现Feign自动生成接口
@Autowired
private DNSPodService service;
//然后直接使用
在本文开始时,曾经讲过可以参考Mybatis扫描Mapper的形式,或者SpringCloud的形式实现Feign的自动注入。
比如Mybatis通过如下注解实现注入
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar
再比如SpringCloud中通过如下注解实现注入
org.springframework.cloud.netflix.feign.FeignClientsRegistrar#registerFeignClients
准备工作
我们首先仿照Mybatis等框架,写一个@Enable
的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(ClientRegistrar.class)
public @interface EnableScanFeign {
String path() default "";
}
并注解到Application中
@SpringBootApplication
@EnableScanFeign(path = "com.example.demo.feign")
public class DemoApplication{}
@import
这个注解除了直接注入各种Config外,也可以使用自定义的Selector
接着实现ClientRegistrar
,它将在启动时读取注解的上下文
public class ClientRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
String path = ((String) metadata.getAnnotationAttributes(EnableScanFeign.class.getName()).get("path"));
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.findAnnotated(HTTPUrl.class, path);
Set<Class<? extends Class<?>>> classes = resolverUtil.getClasses();
System.out.println("classes = " + classes);
}
}
这样如果顺利的话,你就可以看到被扫描的接口了,不过目前这些只是接口,而没有实现类
为了降低文章难度,我们这里借用了Mybatis的VFS工具类,因此暂时需要导入Mybatis的包,你可以学习扫描jar包是如何实现的。此外,还有更简单的ClassPathMapperScanner或者更简单的ClassPathScanningCandidateComponentProvider也可以学习一个
Bean的注册流程
在Spring第三方库的开发中,我们可以从中学到常见的注册方法如下
// 接着上文的System.out来写
classes.stream()
.map(this::generateHolder)// todo 生成BeanDefinitionHolder
.forEach(holder ->
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry)
);
其中generateHolder就是我们抽出来的需要完善的方法,本文没有直接用Spring自带的来生成
配置FactoryBean
FactoryBean也是一种Bean,在真实项目中一般定制的就是这里
public static class ClientFactoryBean implements FactoryBean {
// 这里通过properties写入
private Class type;
private String path;
public Class getType() {
return type;
}
public void setType(Class type) {
this.type = type;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
@Override
public Object getObject() throws Exception {
return Feign.builder()
.decoder(/*同上*/)
.target(DNSPodService.class, path);
}
@Override
public Class<?> getObjectType() {
return type;
}
@Override
public boolean isSingleton() {
return false;
}
}
BeanHolder的实现
这里其实很简单,就是首先构造一个beanDefinition,接着通过property传递参数给Factory即可
// 带 Aware 的继承,都可以在生命周期中获得某个上下文对象
public class ClientRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
Environment environment;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
// 除了样板代码外,主要就是传递property
private BeanDefinitionHolder generateHolder(Class<? extends Class<?>> aClass) {
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(ClientFactoryBean.class);
beanDefinitionBuilder.addPropertyValue("type", aClass);
String value = aClass.getAnnotation(HTTPUrl.class).value();
beanDefinitionBuilder.addPropertyValue("path", environment.getProperty(value));
BeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();
return new BeanDefinitionHolder(beanDefinition, aClass.getName());
}
}
测试用例
测试如下,发现只要是Autowired即可自动注解
@SpringBootApplication
@EnableScanFeign(path = "com.example.demo.feign")
public class DemoApplication implements ApplicationRunner {
@Autowired
DNSPodService service;
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Override
public void run(ApplicationArguments args) throws Exception {
List<String> ipInfo = service.getIpInfo("www.qq.com");
System.out.println("ipInfo = " + ipInfo);
}
}