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.resourcePatternResolver PathMatchingResourcePatternResolver类的对象,我们看看它的 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中的资源对象。 其实 getResources findPathMatchingResources 之间会相互调用。请看上面代码我对两个方法进行了加黑: 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:
        }
    }
  是不是相当简单?      
   

猜你喜欢

转载自manzhizhen.iteye.com/blog/2244806