前几天由于公司项目架构调整,想将以前代码开发为主改成配置文件配置为主,即所有的外部服务调用由配置文件组织,因为必须高效,所以涉及包括调用顺序,并发调用等,但配置文件的缺陷是只能实现简单的业务逻辑,所以我们还用了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文件的。
我们发现,代码 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给包路径加的前缀。
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 之间会相互调用。请看上面代码我对两个方法进行了加黑: doFindPathMatchingJarResources 和 doFindPathMatchingFileResources,这两个方法分别完成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: } }