背景:
根据业务需求需要,需要实现一个rocketmq客户端扩展,可以连接多个rocketmq服务器以及注解化+配置化配置订阅关系
1.配置注解化:
封装了一个注解,包含了常用的mq监听器的配置
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface RocketMqMessageListener {
/**
* rocketmq实例
*/
String rocketMqName();
/**
* 订阅的topic
*/
String topic();
/**
* rocketmq的tag
*/
String tag();
/**
* 是否开启消费者
*/
String enable() default "true";
}
包扫描注解,借助spring读取加了RocketMqMessageListener注解的类
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RocketMqListenerImporter.class)
public @interface RocketMqListenerScanner {
String[] basePackages();
}
2.扫描监听器列表并且自动装配订阅关系:
2.1 注册包扫描的bean
public class RocketMqListenerImporter implements ImportBeanDefinitionRegistrar {
...
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//获取启动类上的RocketMqListenerScanner注解,并且获取basePackages配置
Map<String, Object> maps = importingClassMetadata.getAnnotationAttributes(RocketMqListenerScanner.class.getName());
AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap(maps);
String[] basePackages = annotationAttributes.getStringArray("basePackages");
this.runnerRegistrarProcess(basePackages, registry);
}
private void runnerRegistrarProcess(String[] basePackages, BeanDefinitionRegistry registry){
//这边操作相当于@Bean
//手动注册bean之前先把也basePackages的值放进去
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(RocketMqInitRunner.class);
beanDefinition.setDependsOn("springUtil");
MutablePropertyValues mpv = beanDefinition.getPropertyValues();
mpv.addPropertyValue("basePackages", basePackages);
registry.registerBeanDefinition("rocketMqInitRunner", beanDefinition);
}
...
}
2.2 扫描包含@RocketMqMessageListener的类
先扫描包下的所有类(不包含接口和抽象类):
...
public Set<Class<?>> doScanPackage(String[] basePackages){
Set<Class<?>> classes = new HashSet<>();
CachingMetadataReaderFactory cachingMetadataReaderFactory = new CachingMetadataReaderFactory();
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
for(String basePackage : basePackages){
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
.concat(ClassUtils.convertClassNameToResourcePath(SystemPropertyUtils.resolvePlaceholders(basePackage))
.concat("/**/*.class"));
Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
MetadataReader metadataReader;
for (Resource resource : resources) {
if (resource.isReadable()) {
metadataReader = cachingMetadataReaderFactory.getMetadataReader(resource);
try {
if (metadataReader.getClassMetadata().isConcrete()) {
//当类型不是抽象类或接口在添加到集合
classes.add(Class.forName(metadataReader.getClassMetadata().getClassName()));
}
} catch (Exception e) {
}
}
}
}
return classes;
}
...
在扫描后的结果集里进行筛选,这里的逻辑比较简单:
...
Set<Class<?>> listenerClasses = this.doScanPackage(basePackages);
//按名称归类各mq实例的订阅关系
Map<String, Map> subRelationMap = new LinkedHashMap<>();
if (CollectionUtil.isNotEmpty(listenerClasses)) {
for (Class<?> listenerClass : listenerClasses) {
RocketMqMessageListener listener = AnnotationUtils.findAnnotation(listenerClass, RocketMqMessageListener.class);
if (listener == null) {
continue;
}
...
//实现订阅关系自动装配功能,同时实现了一个监听器订阅多个topic
if(topics.contains("|")){
String[] topicStrs = topics.split("\|");
Arrays.stream(topicStrs).distinct().forEach(topic -> createSubscriptionMap(subRelationMap, name, listenerClass, tag, topic));
}else{
createSubscriptionMap(subRelationMap, name, listenerClass, tag, topics);
}
...
}
}
...
2.3 连接多mq服务端支持
原理是使用了springboot的map配置,且name属性和注解上的rocketMqName属性对应,然后启动相对应数量的rocketmq生产者和消费者客户端对象:
/**
* 一组mq生产者&消费者配置
* rocket.message.mq-config-map.rocket1.address
* ......
* rocket.message.mq-config-map.rocket2.address
*/
private Map<String, RocketMqConfig> mqConfigMap;
3.解析配置:
3.1 简单的配置解析实现
这里的代码目的就是将配置填入注解中的占位符${}中,比如
@RocketMqMessageListener(rocketMqName = "rocket1", topic="${listener1.topic}", tag="*", enable = "${listener1.enable}")
示例代码中的listener1.enable和 listener1.topic 需要从配置中获取后填入:
...
private String valueAnalysis(String str){
//类型1,类型2:需要解析
if(str.startsWith("${") && str.endsWith("}")){
str = str.substring(2, str.length()-1);
Assert.notEmpty(str, "you cannot pass on ${} on a value");
return handleExpressionValue(str);
} else {
//类型3 原样返回
return str;
}
}
private String handleExpressionValue(String str){
//类型1 需要继续解析
if(str.contains(":")){
String[] strArray = str.split(":");
str = propertyValueSet(strArray[0]);
//spring 中找不到
if(StringUtil.isNullOrEmpty(str)){
//找冒号后面的值,找不到就抛错
if(strArray.length == 1){
Assert.notEmpty(str, "you cannot pass on ${"+ str +":} on a value");
}
//返回默认值
return strArray[1];
} else {
//spring 中找到
return propertyValueSet(str);
}
} else {
//类型2 直接返回
return propertyValueSet(str);
}
}
3.2 获取属性值
获取值其实很简单,直接从spring环境中获取就好了:
@Override
protected String propertyValueSet(String str) {
return environment.getProperty(str);
}
自此 一个简单的自动装配订阅关系的功能实现完成。
4.使用方式:
@RocketMqMessageListener(rocketMqName = "rocket1", topic="${listener1.topic}", tag="*", enable = "${listener1.enable}")
@SpringBootApplication
@RocketMqListenerScanner(basePackages = {"xxx.xxx.xxx.xxx"})
@EnableScheduling
public class AppStarter {
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication(AppStarter.class);
springApplication.setAllowBeanDefinitionOverriding(true);
springApplication.run(args);
}
}
总结:
本文使用spring的元数据扫描自动装配了监听器订阅关系,并且简化了配置,将一些固定值的配置移到注解上,同时支持了一个监听器监听多个topic,使用了springboot的map配置支持了mq客户端连接多个mq服务端。