在Spring boot中类似于spring cloud 一样的使用OpenFeign(2)
一、前言
本篇接上一篇在Spring boot中类似于spring cloud 一样的使用OpenFeign(1)
二、实现思路
在Spring boot中类似spring cloud 一样的使用OpenFeign,完全是模范mybatis扫描Mapper接口来写的,确切的说是模仿了MapperScan注解的实现。
- 定义一个扫描包的注解
- 定义一个Registrar类实现ImportBeanDefinitionRegistrar接口
- 定义一个Scanner扫描器,扫描包
- 定义一个FactoryBean,用于创建接口的实例交给IOC容器
- 定义一个Configuration类,注入一些配置Bean
三、具体实现
1)定义一个扫描包的注解
这一步最简单,就是定义一个注解,用于了解需要扫描哪些包
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(OpenFeignClientScannerRegistrar.class)
public @interface OpenFeignClientScan {
/**
* Base packages to scan for OpenFeign interfaces.
*/
String[] basePackages() default {};
}
需要留意的点有两个:
1)、basePackages属性,用于声明要扫描的包,
2)、@Import注解
在OpenFeignClientScan注解上添加了一个@Import注解,Import注解中引入了一个OpenFeignClientScannerRegistrar类。如果没有添加@Import注解,那该注解没有任何的作用,那怕是我们在spring boot的启动类上加了@OpenFeignClientScan注解也不会有任何的作用。所以真正的核心是@Import注解中引入的OpenFeignClientScannerRegistrar类
2) 定义一个Registrar类实现ImportBeanDefinitionRegistrar接口
步骤1)中引入的OpenFeignClientScannerRegistrar类,实现了ImportBeanDefinitionRegistrar接口,ImportBeanDefinitionRegistrar接口中有个重要的方法registerBeanDefinitions方法,该方法有两个参数:
1、AnnotationMetadata importingClassMetadata
通过这个参数我们可以得到注解的元数据信息,比如这里我们可以得到@OpenFeignClientScan注解中配置的basePackages信息
2、BeanDefinitionRegistry registry
beanDefinition的注册器,我们扫描得到的所有接口要生成对应的Bean,都需要有对应的beanDefinition,spring容器内部怎么管理bean的,就是通过beanDefinition来管理的,它描述了一个bean是什么样的。。
public class OpenFeignClientScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
private ResourceLoader resourceLoader;
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//返回@OpenFeignClientScan注解的内容
AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(OpenFeignClientScan.class.getName()));
//构建扫描器
ClassPathOpenFeignClientScanner scanner = new ClassPathOpenFeignClientScanner(registry);
if (resourceLoader != null) {
scanner.setResourceLoader(resourceLoader);
}
//要扫描得包
String[] basePackages = annotationAttributes.getStringArray("basePackages");
//注册能被扫描
scanner.registerFilters();
//开始扫描
scanner.doScan(basePackages);
}
}
具体的步骤代码中已有描述,但需要注意的是ClassPathOpenFeignClientScanner类,这个类是我们自己创建的,我们需要告诉spring,包中的哪些类我们需要扫描,哪些类我们又不需要扫描,以及如何去创建一个OpenFeign接口Bean
3) 定义一个Scanner扫描器,扫描包
public class ClassPathOpenFeignClientScanner extends ClassPathBeanDefinitionScanner {
public ClassPathOpenFeignClientScanner(BeanDefinitionRegistry registry) {
super(registry);
}
@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
//父类扫描包中所有的OpenFeignClient
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if(beanDefinitions.isEmpty()) {
logger.warn("No OpenFeignClient was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
}else {
this.processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder definitionHolder:beanDefinitions) {
definition = (GenericBeanDefinition) definitionHolder.getBeanDefinition();
//创建FactoryBean的构造参数
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());
//创建FactoryBean需要注入的属性
definition.getPropertyValues().addPropertyValue("openFeignProperties",new RuntimeBeanReference("openFeignProperties"));
definition.getPropertyValues().addPropertyValue("openFeignHttpClient",new RuntimeBeanReference("openFeignHttpClient"));
definition.getPropertyValues().addPropertyValue("openFeignHttpEncoder",new RuntimeBeanReference("openFeignHttpEncoder"));
definition.getPropertyValues().addPropertyValue("openFeignHttpDecoder",new RuntimeBeanReference("openFeignHttpDecoder"));
//创建openFeign接口bean的工厂bean class
definition.setBeanClass(OpenFeignClientFactoryBean.class);
}
}
/**
* 配置哪些类能被扫描
*/
public void registerFilters() {
addIncludeFilter(new TypeFilter() {
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
throws IOException {
return true;
}
});
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
}
/**
* 检查factorybean是否已经被加载到容器中,如果被加载到容器中,则此次扫描过滤掉
*/
@Override
protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException {
if (super.checkCandidate(beanName, beanDefinition)) {
return true;
} else {
logger.warn("Skipping OpenFeignClientFactoryBean with name '" + beanName
+ "' and '" + beanDefinition.getBeanClassName() + "' mapperInterface"
+ ". Bean already defined with the same name!");
return false;
}
}
}
需要注意的是,在创建openFeign接口的bean之前,创建的是工厂bean,在由工厂bean生成接口的bean
4) 定义一个FactoryBean,用于创建接口的实例交给IOC容器
public class OpenFeignClientFactoryBean<T> implements FactoryBean<T> {
//需要交给spring容器管理的接口类
private Class<T> openFeignClientInterface;
//需要交给spring容器管理的接口类,当作构造函数传给factoryBean,factoryBean
//才能知道,他需要创建一个什么类型的bean
public OpenFeignClientFactoryBean(Class<T> openFeignClientInterface) {
super();
this.openFeignClientInterface = openFeignClientInterface;
}
// openfeign发起请求使用哪种httpClient
// okhttpClient,apacheHttpClient,ribbon
private Client openFeignHttpClient;
//对http请求参数的编码器
private Encoder openFeignHttpEncoder;
//对响应的解码器
private Decoder openFeignHttpDecoder;
// 使用openFeign在yml中的配置属性
private OpenFeignProperties openFeignProperties;
/**
* 返回创建的bean
*/
@Override
public T getObject() throws Exception {
//host注解是openFeign没有的注解,我自己添加的,标记在接口上,用于标识该接口类
//中,所有的请求使用的host
Host host = openFeignClientInterface.getAnnotation(Host.class);
String targetHost = "";
//接口类中如果没有标识host,则使用yml中配置的默认host
if(StringUtils.isNotEmpty(host.value())) {
targetHost = host.value();
}else {
targetHost = this.openFeignProperties.getDefaultHost();
}
//使用feign构建bean的实例
return Feign.builder()
.client(openFeignHttpClient)
.encoder(openFeignHttpEncoder)
.decoder(openFeignHttpDecoder)
.target(openFeignClientInterface, targetHost);
}
/**
* 返回要创建的bean的类型
*/
@Override
public Class<?> getObjectType() {
return openFeignClientInterface;
}
@Override
public boolean isSingleton() {
return true;
}
public Class<T> getOpenFeignClientInterface() {
return openFeignClientInterface;
}
public void setOpenFeignClientInterface(Class<T> openFeignClientInterface) {
this.openFeignClientInterface = openFeignClientInterface;
}
public Client getOpenFeignHttpClient() {
return openFeignHttpClient;
}
public void setOpenFeignHttpClient(Client openFeignHttpClient) {
this.openFeignHttpClient = openFeignHttpClient;
}
public Encoder getOpenFeignHttpEncoder() {
return openFeignHttpEncoder;
}
public void setOpenFeignHttpEncoder(Encoder openFeignHttpEncoder) {
this.openFeignHttpEncoder = openFeignHttpEncoder;
}
public Decoder getOpenFeignHttpDecoder() {
return openFeignHttpDecoder;
}
public void setOpenFeignHttpDecoder(Decoder openFeignHttpDecoder) {
this.openFeignHttpDecoder = openFeignHttpDecoder;
}
public OpenFeignProperties getOpenFeignProperties() {
return openFeignProperties;
}
public void setOpenFeignProperties(OpenFeignProperties openFeignProperties) {
this.openFeignProperties = openFeignProperties;
}
}
5) 定义一个Configuration类,注入一些配置Bean
@ConditionalOnClass(value= {Feign.class})
@Import(OpenFeignConfiguration.class)
public @interface EnableOpenFeignClient {
}
通过EnableXXXX的形式注入一些openFeign的配置类
@Configuration
@EnableConfigurationProperties(OpenFeignProperties.class)
public class OpenFeignConfiguration {
@Autowired
private OpenFeignProperties openFeignProperties;
@Bean
public Client openFeignHttpClient() {
return BeanUtils.instantiate(openFeignProperties.getClientTypeClass());
}
@Bean
public Encoder openFeignHttpEncoder() {
return BeanUtils.instantiate(openFeignProperties.getEncoderClass());
}
@Bean
public Decoder openFeignHttpDecoder() {
return BeanUtils.instantiate(openFeignProperties.getDecoderClass());
}
@Bean
public OpenFeignConfigs openFeignConfigs() {
OpenFeignConfigs configs = new OpenFeignConfigs();
configs.setOpenFeignProperties(openFeignProperties);
return configs;
}
}
配置属性类
@Component
@ConfigurationProperties(prefix="open.feign.client")
public class OpenFeignProperties {
private String defaultHost;
private Class<Client> clientTypeClass;
private Class<Decoder> decoderClass;
private Class<Encoder> encoderClass;
public String getDefaultHost() {
return defaultHost;
}
public void setDefaultHost(String defaultHost) {
this.defaultHost = defaultHost;
}
public Class<Client> getClientTypeClass() {
return clientTypeClass;
}
public void setClientTypeClass(Class<Client> clientTypeClass) {
this.clientTypeClass = clientTypeClass;
}
public Class<Decoder> getDecoderClass() {
return decoderClass;
}
public void setDecoderClass(Class<Decoder> decoderClass) {
this.decoderClass = decoderClass;
}
public Class<Encoder> getEncoderClass() {
return encoderClass;
}
public void setEncoderClass(Class<Encoder> encoderClass) {
this.encoderClass = encoderClass;
}
}
使用
使用就非常简单了,在启动类上添加两注解就可以了,扫描到的类可以像其它的bean一样的注入。
@SpringBootApplication
@EnableOpenFeignClient
@OpenFeignClientScan(basePackages= {"com.xxxx.xxx.wechat.api.*.feign"})
public class WechatApplication{
public static void main(String[] args) {
new SpringApplicationBuilder(WechatApplication.class).web(true).run(args);
}
}
open:
feign:
client:
default-host: http://114.114.114.114/XXXX
client-type-class: feign.httpclient.ApacheHttpClient
decoder-class: feign.gson.GsonDecoder
encoder-class: feign.gson.GsonEncoder