項目中需要用到包掃描的情況是很多的,一般是在項目初始化的時候,根據一些條件來對某個package下的類進行特殊處理。現在想實現的功能是,在一個filter或interceptor初始化的時候,掃描指定的一些package路徑,遍歷下面的每個class,找出method上使用了一個特殊注解的所有方法,然后緩存起來,當方法攔截器工作的時候,就不用再挨個判斷方法是否需要攔截了
網上有很多自己編碼實現scan package功能的例子,但是如果有工具已經幫你實現了,並且經受了普遍的驗證,那么,自己造輪子的必要性就不大了
spring框架中有掃描包的類ClassPathBeanDefinitionScanner 里面的findCandidateComponents方法是我們進行改造的依據
/** * Scan the class path for candidate components. * @param basePackage the package to check for annotated classes * @return a corresponding Set of autodetected bean definitions */ public Set<BeanDefinition> findCandidateComponents(String basePackage) { Set<BeanDefinition> candidates = new LinkedHashSet<BeanDefinition>(); try { String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + resolveBasePackage(basePackage) + "/" + this.resourcePattern; Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath); boolean traceEnabled = logger.isTraceEnabled(); boolean debugEnabled = logger.isDebugEnabled(); for (Resource resource : resources) { if (traceEnabled) { logger.trace("Scanning " + resource); } if (resource.isReadable()) { try { MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource); if (isCandidateComponent(metadataReader)) { ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader); sbd.setResource(resource); sbd.setSource(resource); if (isCandidateComponent(sbd)) { if (debugEnabled) { logger.debug("Identified candidate component class: " + resource); } candidates.add(sbd); } else { if (debugEnabled) { logger.debug("Ignored because not a concrete top-level class: " + resource); } } } else { if (traceEnabled) { logger.trace("Ignored because not matching any filter: " + resource); } } } catch (Throwable ex) { throw new BeanDefinitionStoreException( "Failed to read candidate component class: " + resource, ex); } } else { if (traceEnabled) { logger.trace("Ignored because not readable: " + resource); } } } } catch (IOException ex) { throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex); } return candidates; }
改造如下:
方法loadCheckClassMethods的入參是逗號分隔的包路徑,如com.xx
利用Spring的
ResourcePatternResolver來尋找包下面的資源Resource,因為我們的掃描pattern是.class文件,所以這里的Resource就是class文件
protected static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
/** * 根據掃描包的配置 * 加載需要檢查的方法 */ private void loadCheckClassMethods(String scanPackages) { String[] scanPackageArr = scanPackages.split(","); ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourcePatternResolver); for (String basePackage : scanPackageArr) { if (StringUtils.isBlank(basePackage)) { continue; } String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(SystemPropertyUtils.resolvePlaceholders(basePackage)) + "/" + DEFAULT_RESOURCE_PATTERN; try { Resource[] resources = resourcePatternResolver.getResources(packageSearchPath); for (Resource resource : resources) { //檢查resource,這里的resource都是class loadClassMethod(metadataReaderFactory, resource); } } catch (Exception e) { log.error("初始化SensitiveWordInterceptor失敗", e); } } } /** * 加載資源,判斷里面的方法 * * @param metadataReaderFactory spring中用來讀取resource為class的工具 * @param resource 這里的資源就是一個Class * @throws IOException */ private void loadClassMethod(MetadataReaderFactory metadataReaderFactory, Resource resource) throws IOException { try { if (resource.isReadable()) { MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource); if (metadataReader != null) { String className = metadataReader.getClassMetadata().getClassName(); try { tryCacheMethod(className); } catch (ClassNotFoundException e) { log.error("檢查" + className + "是否含有需要信息失敗", e); } } } } catch (Exception e) { log.error("判斷類中的方法實現需要檢測xxx失敗", e); } } /** * 把action下面的所有method遍歷一次,標記他們是否需要進行xxx驗證 * 如果需要,放入cache中 * * @param fullClassName */ private void tryCacheMethod(String fullClassName) throws ClassNotFoundException { Class<?> clz = Class.forName(fullClassName); Method[] methods = clz.getDeclaredMethods(); for (Method method : methods) { if (method.getModifiers() != Modifier.PUBLIC) { continue; } if (CheckXXX.class.isAssignableFrom(CHECK_ANNOTATION)) { CheckXXX checkXXX = (CheckXXX) method.getAnnotation(CHECK_ANNOTATION); if (checkXXX != null && checkXXX.check()) { cache.put(fullClassName + "." + method.getName(), checkXXX); log.info("檢測到需要檢查xxx的方法:" + fullClassName + "." + method.getName()); } } } }
tryCacheMethod做的事就是緩存需要處理的public方法
經測試,這種方式可以取到web的class文件和jar包中的class文件
加強版,package父子集合判斷
package utils; import org.apache.commons.lang3.StringUtils; 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.SystemPropertyUtils; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.Set; /** * Created by cdliujian1 on 2016/1/13. * 包工具,根據package路徑,加載class */ public class PackageUtil { private final static Log log = LogFactory.getLog(PackageUtil.class); //掃描 scanPackages 下的文件的匹配符 protected static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; /** * 結合spring的類掃描方式 * 根據需要掃描的包路徑及相應的注解,獲取最終測method集合 * 僅返回public方法,如果方法是非public類型的,不會被返回 * 可以掃描工程下的class文件及jar中的class文件 * * @param scanPackages * @param annotation * @return */ public static Set<Method> findClassAnnotationMethods(String scanPackages, Class<? extends Annotation> annotation) { //獲取所有的類 Set<String> clazzSet = findPackageClass(scanPackages); Set<Method> methods = new HashSet<Method>(); //遍歷類,查詢相應的annotation方法 for (String clazz : clazzSet) { try { Set<Method> ms = findAnnotationMethods(clazz, annotation); if (ms != null) { methods.addAll(ms); } } catch (ClassNotFoundException ignore) { } } return methods; } /** * 根據掃描包的,查詢下面的所有類 * * @param scanPackages 掃描的package路徑 * @return */ public static Set<String> findPackageClass(String scanPackages) { if (StringUtils.isBlank(scanPackages)) { return Collections.EMPTY_SET; } //驗證及排重包路徑,避免父子路徑多次掃描 Set<String> packages = checkPackage(scanPackages); ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourcePatternResolver); Set<String> clazzSet = new HashSet<String>(); for (String basePackage : packages) { if (StringUtils.isBlank(basePackage)) { continue; } String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + org.springframework.util.ClassUtils.convertClassNameToResourcePath(SystemPropertyUtils.resolvePlaceholders(basePackage)) + "/" + DEFAULT_RESOURCE_PATTERN; try { Resource[] resources = resourcePatternResolver.getResources(packageSearchPath); for (Resource resource : resources) { //檢查resource,這里的resource都是class String clazz = loadClassName(metadataReaderFactory, resource); clazzSet.add(clazz); } } catch (Exception e) { log.error("獲取包下面的類信息失敗,package:" + basePackage, e); } } return clazzSet; } /** * 排重、檢測package父子關系,避免多次掃描 * * @param scanPackages * @return 返回檢查后有效的路徑集合 */ private static Set<String> checkPackage(String scanPackages) { if (StringUtils.isBlank(scanPackages)) { return Collections.EMPTY_SET; } Set<String> packages = new HashSet<String>(); //排重路徑 Collections.addAll(packages, scanPackages.split(",")); for (String pInArr : packages.toArray(new String[packages.size()])) { if (StringUtils.isBlank(pInArr) || pInArr.equals(".") || pInArr.startsWith(".")) { continue; } if (pInArr.endsWith(".")) { pInArr = pInArr.substring(0, pInArr.length() - 1); } Iterator<String> packageIte = packages.iterator(); boolean needAdd = true; while (packageIte.hasNext()) { String pack = packageIte.next(); if (pInArr.startsWith(pack + ".")) { //如果待加入的路徑是已經加入的pack的子集,不加入 needAdd = false; } else if (pack.startsWith(pInArr + ".")) { //如果待加入的路徑是已經加入的pack的父集,刪除已加入的pack packageIte.remove(); } } if (needAdd) { packages.add(pInArr); } } return packages; } /** * 加載資源,根據resource獲取className * * @param metadataReaderFactory spring中用來讀取resource為class的工具 * @param resource 這里的資源就是一個Class * @throws IOException */ 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("根據resource獲取類名稱失敗", e); } return null; } /** * 把action下面的所有method遍歷一次,標記他們是否需要進行敏感詞驗證 * 如果需要,放入cache中 * * @param fullClassName */ public static Set<Method> findAnnotationMethods(String fullClassName, Class<? extends Annotation> anno) throws ClassNotFoundException { Set<Method> methodSet = new HashSet<Method>(); Class<?> clz = Class.forName(fullClassName); Method[] methods = clz.getDeclaredMethods(); for (Method method : methods) { if (method.getModifiers() != Modifier.PUBLIC) { continue; } Annotation annotation = method.getAnnotation(anno); if (annotation != null) { methodSet.add(method); } } return methodSet; } public static void main(String[] args) { String packages = "com.a,com.ab,com.c,com.as.t,com.as,com.as.ta,com.at.ja,com.at.jc,com.at."; System.out.println("檢測前的package: " + packages); System.out.println("檢測后的package: " + StringUtils.join(checkPackage(packages), ",")); } }