在Spring boot中类似于spring cloud 一样的使用OpenFeign(2)

一、前言

本篇接上一篇在Spring boot中类似于spring cloud 一样的使用OpenFeign(1)

二、实现思路

在Spring boot中类似spring cloud 一样的使用OpenFeign,完全是模范mybatis扫描Mapper接口来写的,确切的说是模仿了MapperScan注解的实现。

  1. 定义一个扫描包的注解
  2. 定义一个Registrar类实现ImportBeanDefinitionRegistrar接口
  3. 定义一个Scanner扫描器,扫描包
  4. 定义一个FactoryBean,用于创建接口的实例交给IOC容器
  5. 定义一个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
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值