需求描述:
在RocketMQ客戶端開發過程中,發現對於消費端的消費程序客戶端在開發過程中是無法提前預知的,即消費端的消費程序需要具體到業務中去實現。
因此,發現具體的消費方法是RocketMQ客戶端設計過程中一個不得不考慮的問題。爲了降低客戶端對上層業務系統的侵入性,計劃採用業務消費類
添加特定“註解”+客戶端“掃包”的方式來發現業務消費程序。由於Spring的掃包方法是經受過普遍考驗的,因此決定在Spring源碼的基礎上進行
修改實現。
測試代碼:
package com.abc.lottery.easymq;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.Iterator;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.SystemPropertyUtils;
import com.google.common.collect.Sets;
import com.abc.lottery.easymq.handler.MQConsumerAnnotation;
public class ScanPackageTest
{
private final static Log log = LogFactory.getLog(ScanPackageTest.class);
//掃描 scanPackages 下的文件匹配符
public static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
/**
* 找到scanPackages下帶註解annotation的全部類信息
* @param scanPackages 掃包路徑,多個路徑用","分割
* @param annotation 註解類
*/
public static Set<String> findPackageAnnotationClass(String scanPackages, Class<? extends Annotation> annotation)
{
if (StringUtils.isEmpty(scanPackages))
{
return Sets.newHashSet();
}
// 排重包路徑,避免父子路徑重複掃描
Set<String> packages = checkPackages(scanPackages);
//獲取Spring資源解析器
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
//創建Spring中用來讀取resource爲class的工具類
MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
Set<String> fullClazzSet = Sets.newHashSet();
for (String basePackage : packages)
{
if (StringUtils.isEmpty(basePackage))
{
continue;
}
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+ ClassUtils.convertClassNameToResourcePath(SystemPropertyUtils.resolvePlaceholders(basePackage))
+ "/" + DEFAULT_RESOURCE_PATTERN;
try
{
//獲取packageSearchPath下的Resource,這裏得到的Resource是Class信息
Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
for (Resource resource : resources)
{
//檢查resource,這裏的resource都是class
String fullClassName = loadClassName(metadataReaderFactory, resource);
if (isAnnotationClass(fullClassName, annotation))
{
fullClazzSet.add(fullClassName);
}
}
}
catch (Exception e)
{
log.error("獲取包下面的類信息失敗,package:" + basePackage, e);
}
}
return fullClazzSet;
}
/**
* 排重、檢測package父子關係,避免多次掃描
* @param scanPackages 掃包路徑
* @return 返回全部有效的包路徑信息
*/
private static Set<String> checkPackages(String scanPackages)
{
if (StringUtils.isEmpty(scanPackages))
{
return Sets.newHashSet();
}
Set<String> packages = Sets.newHashSet();
//排重路徑
Collections.addAll(packages, scanPackages.split(","));
for (String packageStr : packages.toArray(new String[packages.size()]))
{
if (StringUtils.isEmpty(packageStr) || packageStr.equals(".") || packageStr.startsWith("."))
{
continue;
}
if (packageStr.endsWith("."))
{
packageStr = packageStr.substring(0, packageStr.length() - 1);
}
Iterator<String> packageIte = packages.iterator();
boolean needAdd = true;
while (packageIte.hasNext())
{
String pack = packageIte.next();
if (packageStr.startsWith(pack + "."))
{
//如果待加入的路徑是已經加入的pack的子集,不加入
needAdd = false;
}
else if (pack.startsWith(packageStr + "."))
{
//如果待加入的路徑是已經加入的pack的父集,刪除已加入的pack
packageIte.remove();
}
}
if (needAdd)
{
packages.add(packageStr);
}
}
return packages;
}
/**
* 加載資源,根據resource獲取className
* @param metadataReaderFactory spring中用來讀取resource爲class的工具
* @param resource 這裏的資源就是一個Class
*/
private static String loadClassName(MetadataReaderFactory metadataReaderFactory, Resource resource)
throws IOException
{
try
{
if (resource.isReadable())
{
MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource);
if (metadataReader != null)
{
return metadataReader.getClassMetadata().getClassName();
}
}
}
catch (Exception e)
{
log.error("根據Spring resource獲取類名稱失敗", e);
}
return null;
}
/**
* 判斷類是否包含特定的註解
* @param fullClassName
* @param annotationClass
* @return
* @throws ClassNotFoundException
*/
private static boolean isAnnotationClass(String fullClassName, Class<? extends Annotation> annotationClass)
throws ClassNotFoundException
{
//利用反射,根據類名獲取類的全部信息
Class<?> clazz = Class.forName(fullClassName);
//獲取該類的註解信息
Annotation annotation = clazz.getAnnotation(annotationClass);
if (annotation != null)
{//包含annotationClass註解
return true;
}
return false;
}
}
輸出結果
public static void main(String[] args)
{
String packages = "com.abc.lottery.easymq";
System.out.println("annotation class:" + findPackageAnnotationClass(packages, MQConsumerAnnotation.class));
}
輸出結果:annotation class:[com.abc.lottery.easymq.handler.PaySuccessMQHandler]
結果說明
自己定義了一個註解MQConsumerAnnotation
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MQConsumerAnnotation
{
String topic();
String tags();
}
PaySuccessMQHandler類是com.abc.lottery.easymq包路徑下唯一一個添加MQConsumerAnnotation註解的類
@MQConsumerAnnotation(topic = "paySuccessTopic", tags = "tagA")
public class PaySuccessMQHandler implements MQRecMsgHandler
{
public void handle(List<String> msg) throws Exception
{
System.out.println(msg);
}
}
因此,掃描com.abc.lottery.easymq包路徑得到唯一的一個類PaySuccessMQHandler的全路徑信息。後面,可以根據PaySuccessMQHandler的類信息,通過反射獲取類實例,完成掃包獲取消費方法。