转载

Spring如何扫描class和配置文件

前几天由于公司项目架构调整,想将以前代码开发为主改成配置文件配置为主,即所有的外部服务调用由配置文件组织,因为必须高效,所以涉及包括调用顺序,并发调用等,但配置文件的缺陷是只能实现简单的业务逻辑,所以我们还用了jeval表达式Jar包。

废话不多说,由于服务配置文件是放在Maven项目下的一个子模块的classpath下,该子模块在eclipse下运行是以用文件系统路径来扫描到并解析的,但在线上环境,该子模块是会被打成Jar包,就是说线上环境是需要解析该子模块的Jar包才能取到配置文件的。

Jar包本质上是压缩文件,以前也做个在压缩文件中解析配置文件,但感觉不太专业,由于时间赶,不想在网上捞资料,而且靠不靠谱也不一定,于是想到了 借鉴Spring中的扫描和解析配置文件的功能代码

(转载请注明出处: http://manzhizhen.iteye.com/blog/2244806 )

我们经常用如下Spring配置来解析资源文件和扫描class:

<context: component-scan base-package ="com.manzhizhen.server.service,com.manzhizhen.server.aop" />

<bean

class="org.springframework.beans.factory.config. PropertyPlaceholderConfigurer ">

<property name="locations">

<list>

<value>classpath:conf/resource1.properties</value>

</list>

</property>

</bean>

我本地已经有Spring4的源码,于是我直接在源码中搜索 base-package 关键字,于是定位到 ComponentScanBeanDefinitionParser 类:

public class ComponentScanBeanDefinitionParser implements BeanDefinitionParser {     private static final String BASE_PACKAGE_ATTRIBUTE = "base-package";
然后我搜索哪些类用到了BASE_PACKAGE_ATTRIBUTE,于是找到了 ComponentScanBeanDefinitionParser

类:

public class ComponentScanBeanDefinitionParser implements BeanDefinitionParser {  ... ...  @Override  public BeanDefinition parse(Element element, ParserContext parserContext) {   String[] basePackages = StringUtils.tokenizeToStringArray(element.getAttribute(   BASE_PACKAGE_ATTRIBUTE),     ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);   // Actually scan for bean definitions and register them.   ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);   Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);   registerComponents(parserContext.getReaderContext(), beanDefinitions, element);   return null;  } 

ComponentScanBeanDefinitionParser类的作用就是将解析来的xml元素转换成Bean定义,并将他们注册到上下文中,所以我可以从这里开始追踪Spring是如何根据我们定义的class路径去扫描class文件的。

其中,element.getAttribute(BASE_PACKAGE_ATTRIBUTE)的值就是我们配置的"com.manzhizhen.server.service,com.manzhizhen.server.aop",而ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS只是在Spring中预定义的配置路径分隔符而已,比如“.;/t/n”,最后经过分隔,得到的String[] basePackages就是com.manzhizhen.server.service和com.manzhizhen.server.aop组成的字符串列表了。

我们发现,代码 Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages); 就已经把我们配置的两个package下的所有class解析出来了,所以我决定看看scanner.doScan(basePackages)里面到底做了什么,于是我们来到了 ClassPathBeanDefinitionScanner#doScan

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {   Assert.notEmpty(basePackages, "At least one base package must be specified");   Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>();   for (String basePackage : basePackages) {       Set<BeanDefinition> candidates = findCandidateComponents(basePackage);    for (BeanDefinition candidate : candidates) {     ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);     candidate.setScope(scopeMetadata.getScopeName());     String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);     if (candidate instanceof AbstractBeanDefinition) {      postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);     }     if (candidate instanceof AnnotatedBeanDefinition) {      AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);     }     if (checkCandidate(beanName, candidate)) {      BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);      definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);      beanDefinitions.add(definitionHolder);      registerBeanDefinition(definitionHolder, this.registry);     }    }         }   return beanDefinitions;  }

由于上面加黑的代码就已经将class扫描出来了,于是去看看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;  }

代码 String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +

                    resolveBasePackage(basePackage) + "/" + this.resourcePattern; 将我们的包路径组装成Spring中能识别的格式,如把 “com.manzhizhen.server.service” 变成 " classpath*: com.manzhizhen.server.service /**/*.class ",对,就是对前后做了补充, 给后面的统一解析操作提供必要的指引 。我们发现代码 Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath); 就已经将所有的class扫描出来了,于是我们看看里面做了些什么,于是追踪到了 GenericApplicationContext # getResources

/**   * This implementation delegates to this context's ResourceLoader if it   * implements the ResourcePatternResolver interface, falling back to the   * default superclass behavior else.   * @see #setResourceLoader   */  @Override  public Resource[] getResources(String locationPattern) throws IOException {   if (this.resourceLoader instanceof ResourcePatternResolver) {    return ((ResourcePatternResolver) this.resourceLoader).getResources(locationPattern);   }   return    super.getResources(locationPattern);  }

加黑部分,发现它是调了父类的方法 AbstractApplicationContext # getResources

public Resource[] getResources(String locationPattern) throws IOException {   return    this.resourcePatternResolver.getResources(locationPattern);  }

 this.resourcePatternResolverPathMatchingResourcePatternResolver 类的对象,我们看看它的 getResources 方法:

public Resource[] getResources(String locationPattern) throws IOException {   Assert.notNull(locationPattern, "Location pattern must not be null");   if (   locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {    // a class path resource (multiple resources for same name possible)    if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {     // a class path resource pattern     return findPathMatchingResources(locationPattern);    }    else {     // all class path resources with the given name     return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));    }   }   else {    // Only look for a pattern after a prefix here    // (to not get fooled by a pattern symbol in a strange prefix).    int prefixEnd = locationPattern.indexOf(":") + 1;    if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {     // a file pattern     return findPathMatchingResources(locationPattern);    }    else {     // a single resource with the given name     return new Resource[] {getResourceLoader().getResource(locationPattern)};    }   }  }
 CLASSPATH_ALL_URL_PREFIX 是 PathMatchingResourcePatternResolver 的实现接口 ResourcePatternResolver 中定义的常量:
/**   * Pseudo URL prefix for all matching resources from the class path: "classpath*:"   * This differs from ResourceLoader's classpath URL prefix in that it   * retrieves all matching resources for a given name (e.g. "/beans.xml"),   * for example in the root of all deployed JAR files.   * @see org.springframework.core.io.ResourceLoader#CLASSPATH_URL_PREFIX   */  String    CLASSPATH_ALL_URL_PREFIX = "classpath*:";
其值就是前面Spring给包路径加的前缀。

我们回到 PathMatchingResourcePatternResolver#getResources 的那段代码,继续往下看, getPathMatcher() 返回的是 AntPathMatcher 类的对象,咱们看看它的 isPattern 方法:

public boolean isPattern(String path) {   return (path.indexOf('*') != -1 || path.indexOf('?') != -1);  }
由于前面Spring对包路径的加工,我们很幸运的就匹配上了,于是我们进入了下面的 findPathMatchingResources (locationPattern); 方法,我们看看实现:
/**   * Find all resources that match the given location pattern via the   * Ant-style PathMatcher. Supports resources in jar files and zip files   * and in the file system.   * @param locationPattern the location pattern to match   * @return the result as Resource array   * @throws IOException in case of I/O errors   * @see #doFindPathMatchingJarResources   * @see #doFindPathMatchingFileResources   * @see org.springframework.util.PathMatcher   */  protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {   String rootDirPath = determineRootDir(locationPattern);   String subPattern = locationPattern.substring(rootDirPath.length());   Resource[] rootDirResources = getResources(rootDirPath);   Set<Resource> result = new LinkedHashSet<Resource>(16);   for (Resource rootDirResource : rootDirResources) {    rootDirResource = resolveRootDirResource(rootDirResource);    if (isJarResource(rootDirResource)) {     result.addAll(   doFindPathMatchingJarResources(rootDirResource, subPattern));    }    else if (rootDirResource.getURL().getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {     result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirResource, subPattern, getPathMatcher()));    }    else {     result.addAll(   doFindPathMatchingFileResources(rootDirResource, subPattern));    }   }   if (logger.isDebugEnabled()) {    logger.debug("Resolved location pattern [" + locationPattern + "] to resources " + result);   }   return result.toArray(new Resource[result.size()]);  }
第一行 String rootDirPath = determineRootDir(locationPattern);   得到的 rootDirPath 值为“classpath*:com/kuaidadi/liangjian/allconfig/server/service/”, 即资源文件根目录。第二行 String subPattern = locationPattern.substring(rootDirPath.length()); 得到的 subPattern“**/*.class”, 即需要扫描的资源文件类型。接下来的 Resource[] rootDirResources = getResources(rootDirPath); 将该资源根路径解析成Spring中的资源对象。 其实 getResourcesfindPathMatchingResources 之间会相互调用。请看上面代码我对两个方法进行了加黑: doFindPathMatchingJarResourcesdoFindPathMatchingFileResources ,这两个方法分别完成Jar包和文件系统资源的扫描工作,doFindPathMatchingFileResources方法实现比较简单,文件系统的读取大家都会,咱们看看Spring是如何解析Jar包中的资源的, doFindPathMatchingJarResources 方法源码如下:
/**   * Find all resources in jar files that match the given location pattern   * via the Ant-style PathMatcher.   * @param rootDirResource the root directory as Resource   * @param subPattern the sub pattern to match (below the root directory)   * @return the Set of matching Resource instances   * @throws IOException in case of I/O errors   * @see java.net.JarURLConnection   * @see org.springframework.util.PathMatcher   */  protected Set<Resource> doFindPathMatchingJarResources(Resource rootDirResource, String subPattern)    throws IOException {    URLConnection con = rootDirResource.getURL().openConnection();   JarFile jarFile;   String jarFileUrl;   String rootEntryPath;   boolean newJarFile = false;    if (con instanceof JarURLConnection) {    // Should usually be the case for traditional JAR files.    JarURLConnection jarCon = (JarURLConnection) con;    jarCon.setUseCaches(false);    jarFile = jarCon.getJarFile();    jarFileUrl = jarCon.getJarFileURL().toExternalForm();    JarEntry jarEntry = jarCon.getJarEntry();    rootEntryPath = (jarEntry != null ? jarEntry.getName() : "");   }   else {    // No JarURLConnection -> need to resort to URL file parsing.    // We'll assume URLs of the format "jar:path!/entry", with the protocol    // being arbitrary as long as following the entry format.    // We'll also handle paths with and without leading "file:" prefix.    String urlFile = rootDirResource.getURL().getFile();    int separatorIndex = urlFile.indexOf(ResourceUtils.JAR_URL_SEPARATOR);    if (separatorIndex != -1) {     jarFileUrl = urlFile.substring(0, separatorIndex);     rootEntryPath = urlFile.substring(separatorIndex + ResourceUtils.JAR_URL_SEPARATOR.length());     jarFile = getJarFile(jarFileUrl);    }    else {     jarFile = new JarFile(urlFile);     jarFileUrl = urlFile;     rootEntryPath = "";    }    newJarFile = true;   }    try {    if (logger.isDebugEnabled()) {     logger.debug("Looking for matching resources in jar file [" + jarFileUrl + "]");    }    if (!"".equals(rootEntryPath) && !rootEntryPath.endsWith("/")) {     // Root entry path must end with slash to allow for proper matching.     // The Sun JRE does not return a slash here, but BEA JRockit does.     rootEntryPath = rootEntryPath + "/";    }    Set<Resource> result = new LinkedHashSet<Resource>(8);    for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {     JarEntry entry = entries.nextElement();     String entryPath = entry.getName();     if (entryPath.startsWith(rootEntryPath)) {      String relativePath = entryPath.substring(rootEntryPath.length());      if (getPathMatcher().match(subPattern, relativePath)) {       result.add(rootDirResource.createRelative(relativePath));      }     }    }    return result;   }   finally {    // Close jar file, but only if freshly obtained -    // not from JarURLConnection, which might cache the file reference.    if (newJarFile) {     jarFile.close();    }   }  }
这就拿到了我想要的代码的,我 定义了一个 ResourceTool 类,其中做了简化处理:
public class ResourceTool {  /**   * 获取默认的类加载器   *    * @return   */  public static ClassLoader getDefaultClassLoader() {   ClassLoader cl = null;   try {    cl = Thread.currentThread().getContextClassLoader();   } catch (Throwable ex) {   }   if (cl == null) {    // No thread context class loader -> use class loader of this class.    cl = ClassUtils.class.getClassLoader();   }   return cl;  }  /**   * 获取配置文件资源对象   *    * @param location   * @return   * @throws IOException   */  public static List<URL> findAllClassPathResources(String location) throws IOException {   String path = location;   if (path.startsWith("/")) {    path = path.substring(1);   }   Enumeration<URL> resourceUrls = getDefaultClassLoader().getResources(location);   List<URL> result = Lists.newArrayList();   while (resourceUrls.hasMoreElements()) {    result.add(resourceUrls.nextElement());   }   return result;  }  /**   * 获取指定路径下的指定文件列表   *    * @param rootFile     文件路径   * @param extensionName 文件扩展名   * @return   */  public static List<File> getFiles(File rootFile, String extensionName) {   List<File> fileList = Lists.newArrayList();   String tail = null;   if (extensionName == null) {    tail = "";   } else {    tail = "." + extensionName;   }   if (rootFile == null) {    return fileList;   } else if (rootFile.isFile() && rootFile.getName().endsWith(tail)) {    fileList.add(rootFile);    return fileList;   } else if (rootFile.isDirectory()) {    File[] files = rootFile.listFiles();    for (File file : files) {     if (file.isFile() && file.getName().endsWith(tail)) {      fileList.add(file);     } else if (file.isDirectory()) {      fileList.addAll(getFiles(file, extensionName));     }    }   }   return fileList;  }  public static List<URL> getJarUrl(URL rootUrl, String extensionName) throws IOException {   List<URL> result = Lists.newArrayList();   if (rootUrl == null || !"jar".equals(rootUrl.getProtocol())) {    return result;   }   if (StringUtils.isNotBlank(extensionName)) {    extensionName = "." + extensionName;   }   if (extensionName == null) {    extensionName = "";   }   URLConnection con = rootUrl.openConnection();   JarURLConnection jarCon = (JarURLConnection) con;   jarCon.setUseCaches(false);   JarFile jarFile = jarCon.getJarFile();   JarEntry jarEntry = jarCon.getJarEntry();   String rootEntryPath = (jarEntry != null ? jarEntry.getName() : "");   if (!"".equals(rootEntryPath) && !rootEntryPath.endsWith("/")) {    rootEntryPath = rootEntryPath + "/";   }   for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {    JarEntry entry = entries.nextElement();    String entryPath = entry.getName();    if (entryPath.startsWith(rootEntryPath)) {     String relativePath = entryPath.substring(rootEntryPath.length());     if (relativePath.endsWith(".service")) {      result.add(createRelative(rootUrl, relativePath));     }    }   }   return result;  }  private static URL createRelative(URL url, String relativePath) throws MalformedURLException {   if (relativePath.startsWith("/")) {    relativePath = relativePath.substring(1);   }   return new URL(url, relativePath);  } } 
使用举例:
/**  * 将配置内容转换成内置对象  *   * @return  */    private Map<String, ServiceSetting> getServiceSettingList(String path) {     Map<String, ServiceSetting> map = Maps.newHashMap();     try {      List<URL> urlList = ResourceTool.findAllClassPathResources(path);      for (URL url : urlList) {       String protocol = url.getProtocol();       // org.springframework.util.ResourceUtils       if (ResourceUtils.URL_PROTOCOL_JAR.equals(protocol)) {        // 资源文件扩展名为"service"        List<URL> result = ResourceTool.getJarUrl(url, "service");        for (URL jarUrl : result) {         URLConnection connection = jarUrl.openConnection();         try {          /**        * 得到InputStream,即可解析配置文件        */          ServiceSetting serviceSetting = reloadServiceSetting(connection.getInputStream());          /**        * 检查服务配置正确性        */          boolean check = checkServiceSetting(serviceSetting);          if (check) {           map.put(serviceSetting.getName(), serviceSetting);           logger.info("成功加载文件:" + jarUrl.getFile() + ", serviceSetting:"              + JsonUtil.toJson(serviceSetting));          }         } catch (Exception e) {         // TODO:         }        }       } else if (ResourceUtils.URL_PROTOCOL_FILE.endsWith(protocol)) {        // org.   springframework.   util.StringUtils        File file = new File(         new URI(StringUtils.replace(url.toString(), " ", "%20")).getSchemeSpecificPart());  ////  资源文件扩展名为"service"        List<File> fileList = ResourceTool.getFiles(file, "service");        for (File serviceFile : fileList) {         ServiceSetting serviceSetting = reloadServiceSetting(new FileInputStream(serviceFile));         /**       * 检查服务配置正确性       */         boolean check = checkServiceSetting(serviceSetting);         if (check) {          map.put(serviceSetting.getName(), serviceSetting);          logger.info("成功加载文件:" + serviceFile.getPath() + ", serviceSetting:"             + JsonUtil.toJson(serviceSetting));         }        }       }      }      return map;     } catch (Exception e) {     // TODO:     }    } 

是不是相当简单?

已有 0 人发表留言,猛击->> 这里 <<-参与讨论

ITeye推荐

  • —软件人才免语言低担保 赴美带薪读研!—
正文到此结束
Loading...