在上一篇博文《基于Feign的局部請求攔截》的最后,我提出了如何實現系統啟動將自定義注解的bean注入到Spring的ApplicationContext中,那么本博文我們就來探討下具體的代碼流程
小伙伴們在使用SpringCloud中集成的Feign功能時,只需要編寫一個接口,然后再給接口上添加注解@FeignClient
,然后配置上相關信息既可以調用其他系統的業務接口,非常的方便;
這里我們不講解如果在SpringCloud中集成Feign功能,這個網上有大把的博文來講解該如何使用,說的肯定比我的要詳細,精彩;在這里我就基于上一篇博文中,如果將添加自定義注解的組件掃描注入Spring的上下文中;
說明:本博文主要實現的功能是將指定路徑下的接口上添加指定注解的對象,通過代理工廠來生成對應的實例對象,然后將該對象注冊到Spring的上下文中
具體的業務流程邏輯是:
- 在SpringBoot的啟動類中添加自定義注解,在該注解中通過
@Import
導入自定義注冊器 - 自定義注冊器主要實現如下接口:
-
ImportBeanDefinitionRegistrar
: 該類只能通過其他類@Import的方式來加載,通常是啟動類或配置類,通過實現registerBeanDefinitions
方法來向Spring上下文中注冊自定義的bean組件 -
ResourceLoaderAware
: 獲取資源加載器,可以獲得外部資源文件 -
BeanClassLoaderAware
: 該接口有個setBeanClassLoader方法,與前兩個接口類似,實現了該接口后,可以向bean中注入加載該bean的ClassLoader -
EnvironmentAware
: 獲取項目的環境信息
-
- 在
ImportBeanDefinitionRegistrar
中的registerBeanDefinitions
來實現注冊的功能 - 通過注解中指定的掃描路徑,然后掃描添加指定注解的接口對象
- 然后通過代理工廠的方式來生成該接口的實例對象
- 將該實例對象注冊到Spring的上下文中
代碼
知道了大體的代碼流程邏輯,我們就廢話不多說了,直接上代碼:
- 啟動類上添加自定義注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.amos.baseframework.remote"})
@EnableRxFeignLocality(scanPackages = {"com.amos.baseframework.remote"})
public class BaseFrameworkApplication {
public static void main(String[] args) {
SpringApplication.run(BaseFrameworkApplication.class, args);
}
EnableRxFeignLocality
就是我們自定義的注解,具體的代碼如下:
/**
* Copyright ? 2018 五月工作室. All rights reserved.
*
* @Project: springcloudfunctionsample
* @ClassName: EnableRxFeignLocality
* @Package: com.amos.baseframework.anno
* @author: amos
* @Description: Fegin局部攔截注冊器
* <p>
* 主要項目啟動時掃描指定的包路徑下面含有指定注解的組件,
* 并且使用代理工廠生成對象,然后注冊到Spring的ApplicationContext中
* @date: 2020/2/21 0021 下午 16:09
* @Version: V1.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(RxFeignLocalityRegister.class)
public @interface EnableRxFeignLocality {
/**
* 掃描包路徑
*
* @return
*/
String[] scanPackages() default {};
}
這里我們就看到 @Import(RxFeignLocalityRegister.class)
這段代碼,這里就是我們自定義的注冊器
- 自定義注冊器
/**
* Copyright ? 2018 五月工作室. All rights reserved.
*
* @Project: springcloudfunctionsample
* @ClassName: RxFeignLocalityRegister
* @Package: com.amos.baseframework.register
* @author: amos
* @Description:
* @date: 2020/2/21 0021 下午 16:29
* @Version: V1.0
*/
public class RxFeignLocalityRegister implements ImportBeanDefinitionRegistrar,
ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {
public static final Logger logger = LoggerFactory.getLogger(RxFeignLocalityRegister.class);
private ClassLoader classLoader;
private ResourceLoader resourceLoader;
private Environment environment;
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
/**
* 存放 @EnableRxFeignLocality 注解的所有屬性
*/
private Map<String, Object> enableRxFeignLocalityAttributes = null;
/**
* 實現該方法,向Spring上下文中注冊指定路徑下,指定注解的Bean對象
*
* @param metadata 注解的元信息
* @param registry Spring內置的注冊器
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
enableRxFeignLocalityAttributes = metadata.getAnnotationAttributes(EnableRxFeignLocality.class.getName(), Boolean.TRUE);
logger.info("@EnableRxFeignLocality 注解中屬性:{}", enableRxFeignLocalityAttributes);
// 掃描自定義注解中指定路徑下的Bean組件,并且將其注冊到Spring的上下文中
this.registerRxFeignClient(metadata, registry);
}
......
}
這里我們主要來重寫 registerBeanDefinitions
方法來具體的功能
/**
* 掃描自定義注解中指定路徑下的Bean組件,并且將其注冊到Spring的上下文中
*
* @param metadata
* @param registry
*/
private void registerRxFeignClient(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 獲取Provider 其實就是一個是掃描器,提供掃描的功能
ClassPathScanningCandidateComponentProvider provider = this.getScanner();
// 給掃描器設置資源加載器
provider.setResourceLoader(resourceLoader);
// 添加掃描組件需要過濾的類型,這里我們需要掃描所有添加注解 @RxFeignClient 的class
AnnotationTypeFilter typeFilter = new AnnotationTypeFilter(RxFeignClient.class);
// 掃描器
provider.addIncludeFilter(typeFilter);
String[] scanPackageArr = (String[]) enableRxFeignLocalityAttributes.get("scanPackages");
// 如果沒有配置則直接就不掃描了 方法直接返回即可
if (null == scanPackageArr && scanPackageArr.length == 0) {
logger.info("@RxFeignLocality 中的scanPackages值為空");
return;
}
// 將需要掃描的路徑數組 轉化為 Set集合
Set<String> scanPackages = new HashSet<>(CollectionUtils.arrayToList(scanPackageArr));
Iterator<String> iterable = scanPackages.iterator();
while (iterable.hasNext()) {
String packages = iterable.next();
// 獲取指定包路徑下面所有添加注解的bean
Set<BeanDefinition> beanDefinitions = provider.findCandidateComponents(packages);
Iterator<BeanDefinition> bi = beanDefinitions.iterator();
while (bi.hasNext()) {
BeanDefinition beanDefinition = bi.next();
// 含有注解的bean
if (beanDefinition instanceof AnnotatedBeanDefinition) {
AnnotatedBeanDefinition annotatedBeanDefinition = (AnnotatedBeanDefinition) beanDefinition;
AnnotationMetadata annotationMetadata = annotatedBeanDefinition.getMetadata();
// 該注解只能添加在接口上
Assert.isTrue(annotationMetadata.isInterface(), "@" + RxFeignClient.class.getName() + " 只能標記在接口上");
// 將掃描到的接口 根據代理工廠生成實例對象 并且將該實例對象注冊到Spring的上下文中
this.registerRxFeignBean(registry, annotationMetadata, annotationMetadata.getAnnotationAttributes(RxFeignClient.class.getCanonicalName()));
}
}
}
}
/**
* 將接口根據代理工廠生成實例對象,并且將該實例對象注冊到Spring的上下文中
*
* @param registry
* @param annotationMetadata
* @param annotationAttributes
*/
private void registerRxFeignBean(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> annotationAttributes) {
// 獲取注解所在的類名
String className = annotationMetadata.getClassName();
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(RxFeignClientFactoryBean.class);
// 這里直接使用反射的方式 給通過代理工廠生成的實例對象進行賦值
// 注意 這里annotationMetadata.getClassName() 是字符串類型的,而在代理工廠類中 resourceClass 是Class類型的
// 按理說 賦值的話應該會報不合理參數的,但是這里運行沒有問題,可能是Spring內部做了處理
definition.addPropertyValue("resourceClass", annotationMetadata.getClassName());
definition.addPropertyValue("instanceId", annotationAttributes.get("instanceId"));
definition.addPropertyValue("url", annotationAttributes.get("directUrl"));
definition.addPropertyValue("requestProtocolEnum", annotationAttributes.get("RequestProtocol"));
definition.addPropertyValue("requestInterceptorClass", annotationAttributes.get("requestInterceptor"));
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
String clazzName = className.substring(className.lastIndexOf(".") + 1);
String alias = this.lowerFirstCapse(clazzName);
// 向Spring的上下文中注冊bean組件
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{alias});
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
/**
* 首字母變小寫
*
* @param str
* @return
*/
public String lowerFirstCapse(String str) {
char[] chars = str.toCharArray();
chars[0] += 32;
return String.valueOf(chars);
}
/**
* 項目路徑下的掃描器
* <p>
* ClassPathScanningCandidateComponentProvider 是Spring提供的工具,可以按照自定義的類型,查找classpath下符合要求的class文件
*
* @return
*/
protected ClassPathScanningCandidateComponentProvider getScanner() {
return new ClassPathScanningCandidateComponentProvider(false, this.environment) {
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
boolean isCandidate = false;
// 過濾掉不是注解的 bean
if (beanDefinition.getMetadata().isIndependent() && !beanDefinition.getMetadata().isAnnotation()) {
isCandidate = true;
}
return isCandidate;
}
};
}
在上面的代碼中我看到了代理工廠RxFeignClientFactoryBean
,通過這個代理工廠來生成接口對應的實例對象
- 代理工廠
/**
* Copyright ? 2018 五月工作室. All rights reserved.
*
* @Package com.amos.baseframework.beanfactory
* @ClassName RxFeignClientFactoryBean
* @Description TODO
* @Author Amos
* @Modifier
* @Date 2020/2/23 21:36
* @Version 1.0
**/
public class RxFeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
private ApplicationContext applicationContext;
private Class<?> resourceClass;
private String instanceId;
private String url;
private RequestProtocolEnum requestProtocolEnum;
private Class<? extends RequestInterceptor> requestInterceptorClass;
public static final String HTTPS = "https://";
public static final String HTTP = "http://";
@Override
public Object getObject() throws Exception {
return target();
}
/**
* 獲取目標對象
*
* @param <T>
* @return
*/
public <T> T target() {
Client client = (Client) getFeignContext().getInstances(instanceId, Client.class);
T t = (T) Feign.builder().decoder(new GsonDecoder())
.encoder(new GsonEncoder())
.client(client)
.requestInterceptor(requestInterceptorNewInstance())
.target(resourceClass, parseProtocol());
return t;
}
public RequestInterceptor requestInterceptorNewInstance() {
try {
return requestInterceptorClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private String parseProtocol() {
if (!StringUtils.isEmpty(url)) {
return url;
}
switch (requestProtocolEnum) {
case HTTP:
return HTTP + instanceId;
case HTTPS:
return HTTPS + instanceId;
default:
return null;
}
}
private FeignContext getFeignContext() {
return this.applicationContext.getBean(FeignContext.class);
}
@Override
public Class<?> getObjectType() {
return resourceClass;
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(resourceClass, "標記類不能為空");
if (StringUtils.isEmpty(instanceId) && StringUtils.isEmpty(url)) {
throw new IllegalArgumentException("實例名和url不能同時為空");
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public Class<?> getResourceClass() {
return resourceClass;
}
public void setResourceClass(Class<?> resourceClass) {
this.resourceClass = resourceClass;
}
public String getInstanceId() {
return instanceId;
}
public void setInstanceId(String instanceId) {
this.instanceId = instanceId;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public RequestProtocolEnum getRequestProtocolEnum() {
return requestProtocolEnum;
}
public void setRequestProtocolEnum(RequestProtocolEnum requestProtocolEnum) {
this.requestProtocolEnum = requestProtocolEnum;
}
public Class<? extends RequestInterceptor> getRequestInterceptorClass() {
return requestInterceptorClass;
}
public void setRequestInterceptorClass(Class<? extends RequestInterceptor> requestInterceptorClass) {
this.requestInterceptorClass = requestInterceptorClass;
}
}
至此,基本的功能就已經實現了,我們可以通過actuator
來監控bean組件是否注冊到Spring上下文中
- 引入 actuator
pom文件中引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
然后 application.yml文件中添加
management:
endpoint:
web:
base-path: /actuator
endpoints:
web:
exposure:
include: "*"
最后啟動項目即可
完整的代碼可以參考: spring-cloud-function-sample