神奇的BeanDefinitionRegistrar
使用场景介绍
由于工作上是负责一个数智中台,一个配置智能客服与机器人的中台,比如一个我们在打开一个app后,点反馈功能的一个im对话功能。在该对话中,有一个很重要的接口,是query接口,当用户发送消息时,其实就是调用这个query,从而去判断回复什么,是否需要转人工等数据。
而在query接口中,需要去判断用户发送的文本会命中后台配置的哪一个标准问,这个判断是别的小组底层unit做的,我们需要做的是调用对方的接口。而且这种调用底层unit的场景还蛮多的
项目一开始是对HttpClient进行了再封装成一个HttpUnit的,调用方法时是调用这样的doPost方法
public static <T> T doPost(String target,
String uri,
QueryString qs,
Headers headers,
Object body,
boolean isJsonBody,
Map<String, MultiContent> multis,
ConfigOption configOption,
List<CustomizePostProcessor> customizePostProcessors,
Type typeOfT) {
return doPost(target, uri, qs, headers, body, isJsonBody, multis,
configOption, customizePostProcessors, typeOfT, null);
}
但随着调用的底层unit接口越来越多,调用底层接口本身需要携带某些特定的参数,又与中台本身逻辑耦合在一起,可维护性变得很差,于是后面就有了一种基于注解的方案,有点类似于OpenFeign,接口声明像下面这样,但同时可以添加一些自己的逻辑进去,包括可以修改重传次数什么的。
@HttpUtil.AHttpClient(target = "https://aip.baidubce.com",
connectTimeout = "3500",
readTimeout = "3500",
retryTimes = "3",
requestInterceptors = "unitAccessTokenInterceptor") // 在request前的一个拦截器
public interface UnitModelApi {
@PostMapping("/rpc/2.0/unit/v3/faqskill/model/train")
UnitResult<Model> train(@RequestBody TrainModelDTO trainModelDTO);
}
然后再调用时我们只需要这样调用
UnitAccessTokenInterceptor.setRobotId(robotId); // 添加调用所需的AccessToken
result = unitQueryOfflineApi.query(queryDTO);
这样就变得简单很多了。
使用介绍
我们点进去看拦截器,很简单的就把token注入进去
@Slf4j
@Component
public class UnitAccessTokenInterceptor implements RequestInterceptor {
private static final ThreadLocal<Long> ROBOT_IDS = new ThreadLocal<>();
public static void setRobotId(long robotId) {
ROBOT_IDS.set(robotId);
}
@Override
public void intercept(String uri, QueryString qs, Headers headers, JsonElement body) {
//省略若干代码...
Long theRobotId = ROBOT_IDS.get();
UnitAccountService unitAccountService = ApplicationContextUtil.getBean("unitAccountService");
String accessToken = unitAccountService.getAccessTokenToRedis(theRobotId);
qs.addQsKv("access_token", accessToken); // qs是url请求参数
ROBOT_IDS.remove();
}
}
可以看到,是把线程需要的token放进qs中,qs其实是HttpUtil中的k/v参数,简化了调用远程接口所需参数的注入。那为什么写上@AHttpClient注解,就可以办到呢?在HttpUtil中有两个注解,
public class HttpUtil {
// 省略若干行代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AHttpClient {
String name() default "";
String value() default "";
String target() default "";
String connectTimeout() default "";
String readTimeout() default "";
String retryTimes() default "";
String retryInterval() default "";
String[] requestInterceptors() default {};
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(AHttpClientScannerRegistrar.class) // 注意这里,引入我们自己的类注册器
@Documented
public @interface EnableAHttpClients {
/*
* 需要扫描的包路径, value与basePackages同意
*/
String[] value() default {};
String[] basePackages() default {};
}
}
我们在启动类上注上@EnableAHttpClients,即表示开启我们自己的AHttpClient注解功能,在这个
@EnableAHttpClients中,可以看到,它上面又@Import(AHttpClientScannerRegistrar.class),表示引入了我们自己的BeanDefinitionRegistar注册类,这个类有什么用呢?
ImportBeanDefinitionRegistrar接口其实是Spring的扩展点之一,它可以支持我们自己的代码封装成BeanDefinition对象;实现此接口的类会回调postProcessBeanDefinitionRegistry方法,注册到Spring容器中。
AHttpClientScannerRegistrar注册类
private static class AHttpClientScannerRegistrar implements ImportBeanDefinitionRegistrar,
ResourceLoaderAware, EnvironmentAware {
private ResourceLoader resourceLoader;
private Environment environment;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// 创建扫描带有@AHttpClient注解类的scanner
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(
false, this.environment) {
@Override
protected boolean isCandidateComponent(
AnnotatedBeanDefinition beanDefinition) {
return true;
}
};
scanner.setResourceLoader(this.resourceLoader);
scanner.addIncludeFilter(new AnnotationTypeFilter(AHttpClient.class));
// 获取带有@EnableClients注解的实际属性值: value/basePackages, 取到需要扫描的包路径
Set<String> basePackages = new HashSet<>();
Map<String, Object> enableAHttpClientsAttributes = metadata
.getAnnotationAttributes(EnableAHttpClients.class.getName());
if (enableAHttpClientsAttributes != null) {
for (String pkg : (String[]) enableAHttpClientsAttributes.get("value")) {
if (StringUtils.isNotBlank(pkg)) {
basePackages.add(pkg);
}
}
for (String pkg : (String[]) enableAHttpClientsAttributes.get("basePackages")) {
if (StringUtils.isNotBlank(pkg)) {
basePackages.add(pkg);
}
}
}
if (basePackages.isEmpty()) {
basePackages.add(ClassUtils.getPackageName(metadata.getClassName()));
}
log.info("[AHttpClientScannerRegistrar] basePackages = {}", JsonUtil.toJson(basePackages));
// 逐个扫描出带有@AHttpClient注解的类
for (String basePackage : basePackages) {
for (BeanDefinition candidateComponent : scanner.findCandidateComponents(basePackage)) {
// 如果容器类不是注解,跳过
if (!(candidateComponent instanceof AnnotatedBeanDefinition)) {
continue;
}
// 获取metaData
AnnotationMetadata aHttpClientMetadata =
((AnnotatedBeanDefinition) candidateComponent).getMetadata();
String className = aHttpClientMetadata.getClassName();
log.info("[AHttpClientScannerRegistrar] |-- [{}] candidate class found", className);
Class<?> clazz = null;
try {
clazz = Class.forName(className);
} catch (ClassNotFoundException e) {
// throw new RuntimeException("[AHttpClientScannerRegistrar] invalid class " + className, e);
log.warn("[AHttpClientScannerRegistrar] |--- [{}] invalid class: ", className, e);
continue;
}
// 并且需要确保,该注解是声明在接口上的,而不是类上的
if (!aHttpClientMetadata.isInterface()) {
log.warn("[AHttpClientScannerRegistrar] |--- [{}] @AHttpClient can only be used on interface! "
+ "this one will be ignored", className);
continue;
}
// 获取到带有@AHttpClient注解的实际属性值
Map<String, Object> aHttpClientAttributes = aHttpClientMetadata
.getAnnotationAttributes(AHttpClient.class.getName());
// beanName 默认为clazz simpleName 首字母小写
String beanName = StringUtils.uncapitalize(clazz.getSimpleName());
// 从attributes中获取指定的name, 替换默认beanName, 如果有的话
if (aHttpClientAttributes != null) {
String specifiedBeanName = (String) aHttpClientAttributes.get("name");
if (StringUtils.isNotBlank(specifiedBeanName)) {
beanName = specifiedBeanName;
}
}
/**
* 下面这句话是最重要的
* 使用AHttpClientFactoryBean来创建BeanDefinitionBuilder
* BeanDefination不是真正的bean, 真正的bean实例化由AHttpClientFactoryBean来完成
* 用户逻辑上是感知不到所用类是一个普通类还是一个工厂类的,但工厂类可以返回这个工厂的getObjet()返回的类
*/
BeanDefinitionBuilder aHttpClientBeanDefinitionBuilder = BeanDefinitionBuilder
.genericBeanDefinition(AHttpClientFactoryBean.class);
aHttpClientBeanDefinitionBuilder.addPropertyValue("type", clazz);
aHttpClientBeanDefinitionBuilder.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
// 将@AHttpClient中的各个属性, 添加到beanDefinination中
// 注意: 这里需要确保属性在@AHttpClient, AHttpClientFactoryBean中保持名称一致
if (aHttpClientAttributes != null) {
// 属性: value & target (优先value)
for (String attrName : new String[] {"value", "target"}) {
String value = (String) aHttpClientAttributes.get(attrName);
if (StringUtils.isNotBlank(value)) {
value = environment.resolveRequiredPlaceholders(value);
}
if (StringUtils.isNotBlank(value)) {
log.info("[AHttpClientScannerRegistrar] |--- [{}] addProperty: target = {}",
className, value);
// aHttpClientBeanDefinition.getPropertyValues().add("target", value);
aHttpClientBeanDefinitionBuilder.addPropertyValue("target", value);
// 取到即退出循环
break;
}
}
// 属性: connectTimeout, readTimeout, retryTimes, retryInterval
for (String attrName : new String[] {
"connectTimeout", "readTimeout", "retryTimes", "retryInterval"}) {
String value = (String) aHttpClientAttributes.get(attrName);
if (StringUtils.isNotBlank(value)) {
value = environment.resolveRequiredPlaceholders(value);
}
// 这组属性, 本质上是int, 这里开始转换
if (StringUtils.isNotBlank(value)) {
int valueInt = Integer.parseInt(value);
log.info("[AHttpClientScannerRegistrar] |--- [{}] addProperty: {} = {}",
className, attrName, valueInt);
// aHttpClientBeanDefinition.getPropertyValues().add(attrName, valueInt);
aHttpClientBeanDefinitionBuilder.addPropertyValue(attrName, valueInt);
}
}
// requestInterceptors
String[] requestInterceptors = (String[]) aHttpClientAttributes.get("requestInterceptors");
log.info("[AHttpClientScannerRegistrar] |--- [{}] addProperty: requestInterceptors = {}",
className, JsonUtil.toJson(requestInterceptors));
// aHttpClientBeanDefinition.getPropertyValues().add("requestInterceptors", requestInterceptors);
aHttpClientBeanDefinitionBuilder.addPropertyValue("requestInterceptors", requestInterceptors);
}
// 注册
BeanDefinitionHolder holder = new BeanDefinitionHolder(
aHttpClientBeanDefinitionBuilder.getBeanDefinition(), beanName);
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
log.info("[AHttpClientScannerRegistrar] |-- [{}] beanDefination created", className);
}
}
}
}
AHttpClientFactoryBean工厂类
大概意思就是说,把@AHttpClient类加入到容器中,并且会把使用AHttpClientFactoryBean来构造一个BeanDefinitionBuilder,把属性注入进去,之后用户在Autowired引入接口类时,实际上就是返回AHttpClientFactoryBean的getObject()所返回的类,那我们来看看这个方法内有什么
public static class AHttpClientFactoryBean
implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
// 一系列属性注入
private boolean propsAllSet = false;
private ApplicationContext applicationContext;
private Class<?> type;
private String target;
private int connectTimeout = CONFIG_NOT_SET;
private int readTimeout = CONFIG_NOT_SET;
private int retryTimes = CONFIG_NOT_SET;
private int retryInterval = CONFIG_NOT_SET;
private String[] requestInterceptors;
public Class<?> getType() {
return type;
}
public void setType(Class<?> type) {
log.info("[AHttpClientFactoryBean.setType] type = {}", type);
this.type = type;
}
public void setTarget(String target) {
log.info("[AHttpClientFactoryBean.setTarget] target = {}", target);
this.target = target;
}
public void setConnectTimeout(int connectTimeout) {
log.info("[AHttpClientFactoryBean.setConnectTimeout] connectTimeout = {}", connectTimeout);
this.connectTimeout = connectTimeout;
}
public void setReadTimeout(int readTimeout) {
log.info("[AHttpClientFactoryBean.setReadTimeout] readTimeout = {}", readTimeout);
this.readTimeout = readTimeout;
}
public void setRetryTimes(int retryTimes) {
log.info("[AHttpClientFactoryBean.setRetryTimes] retryTimes = {}", retryTimes);
this.retryTimes = retryTimes;
}
public void setRetryInterval(int retryInterval) {
log.info("[AHttpClientFactoryBean.setRetryInterval] retryInterval = {}", retryInterval);
this.retryInterval = retryInterval;
}
public void setRequestInterceptors(String[] requestInterceptors) {
log.info("[AHttpClientFactoryBean.setRequestInterceptors] requestInterceptors = {}",
JsonUtil.toJson(requestInterceptors));
this.requestInterceptors = requestInterceptors;
}
@Override
public Object getObject() throws Exception {
// 通过HttpCallerBuilder创建接口代理, 并注册成bean放入spring容器中
if (!propsAllSet) {
throw new RuntimeException("[AHttpClientFactoryBean.getObject] props not set yet, can't build");
}
HttpCallerBuilder builder = HttpCallerBuilder.build();
builder.setConnectTimeout(connectTimeout);
builder.setReadTimeout(readTimeout);
builder.setRetryTimes(retryTimes);
builder.setRetryInterval(retryInterval);
if (requestInterceptors != null) {
for (String requestInterceptorBeanName : requestInterceptors) {
if (StringUtils.isNotBlank(requestInterceptorBeanName)) {
builder.addRequestInterceptor(applicationContext.getBean(
requestInterceptorBeanName, RequestInterceptor.class));
}
}
}
return builder.target(type, target); // 返回代理对象
}
@Override
public Class<?> getObjectType() {
return type;
}
@Override
public void afterPropertiesSet() throws Exception {
log.info("[AHttpClientFactoryBean.afterPropertiesSet] all set");
propsAllSet = true;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
HTTP代理
可以看到最后会去执行builder.target(type, target);返回代理对象,执行动态代理的invoke方法,我们知道这个方法的参数是有Method的,我们可以获取这个上的@RequestMapping等的注解进行获取封装,最后会去调用到doGet或doPost方法进行方法的真正执行,这部分的代码比较长就不贴了。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 省略
}
原文内容来自
https://www.yuque.com/yinhe-dglmu/zye9qy/aofso8
有很多项目干货
总结
- 通过@EnableAHttpClient导入了AHttpClientScannerRegistrar,触发Spring应用程序对classpath中@AHttpClient修饰类的扫描。
- 解析到@AHttpClient修饰类后,通过扩展Spring Bean Definition的注册逻辑,最终注册一个AHttpClientFeactoryBean进入Spring容器。
- Spring容器在初始化其他用到@AHttpClient接口的类时,其实获得的是AHttpClientFactoryBean产生的一个代理对象Proxy。
- 基于Java原生的动态代理机制,针对Proxy的调用,都会统一转发给一个Invocationhandler,又该Handler完成后续的HTTP转换,发送,接收,HTTP响应的工作。