前几天由于公司项目架构调整,想将以前代码开发为主改成配置文件配置为主,即所有的外部服务调用由配置文件组织,因为必须高效,所以涉及包括调用顺序,并发调用等,但配置文件的缺陷是只能实现简单的业务逻辑,所以我们还用了jeval表达式Jar包。
废话不多说,由于服务配置文件是放在Maven项目下的一个子模块的classpath下,该子模块在eclipse下运行是以用文件系统路径来扫描到并解析的,但在线上环境,该子模块是会被打成Jar包,就是说线上环境是需要解析该子模块的Jar包才能取到配置文件的。
Jar包本质上是压缩文件,以前也做个在压缩文件中解析配置文件,但感觉不太专业,由于时间赶,不想在网上捞资料,而且靠不靠谱也不一定,于是想到了借鉴Spring中的扫描和解析配置文件的功能代码。
我们经常用如下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类:
Java代码
public class ComponentScanBeanDefinitionParser implements BeanDefinitionParser {
private static final String BASE_PACKAGE_ATTRIBUTE = "base-package";
然后我搜索哪些类用到了BASE_PACKAGE_ATTRIBUTE,于是找到了ComponentScanBeanDefinitionParser类:
Java代码
public class ComponentScanBeanDefinitionParser implements BeanDefinitionParser {
... ...
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
String[] basePackages = StringUtils.tokenizeToStringArray(element.getAttribute(<strong>BASE_PACKAGE_ATTRIBUTE</strong>),
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:
Java代码
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) {
<strong>Set<BeanDefinition> candidates = findCandidateComponents(basePackage);</strong>
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方法是怎么实现的:
Java代码
/**
* 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:
Java代码
/**
* 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 <strong>super.getResources(locationPattern);</strong>
}
加黑部分,发现它是调了父类的方法AbstractApplicationContext#getResources:
Java代码
public Resource[] getResources(String locationPattern) throws IOException {
return <strong>this.resourcePatternResolver.getResources(locationPattern);</strong>
}
this.resourcePatternResolver 是PathMatchingResourcePatternResolver类的对象,我们看看它的getResources 方法:
Java代码
public Resource[] getResources(String locationPattern) throws IOException {
Assert.notNull(locationPattern, "Location pattern must not be null");
if (<strong>locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)</strong>) {
// 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 中定义的常量:
Java代码
/**
* 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 <strong>CLASSPATH_ALL_URL_PREFIX</strong> = "classpath*:";
其值就是前面Spring给包路径加的前缀。
我们回到 PathMatchingResourcePatternResolver#getResources 的那段代码,继续往下看,getPathMatcher() 返回的是 AntPathMatcher 类的对象,咱们看看它的 isPattern 方法:
Java代码
public boolean isPattern(String path) {
return (path.indexOf('*') != -1 || path.indexOf('?') != -1);
}
由于前面Spring对包路径的加工,我们很幸运的就匹配上了,于是我们进入了下面的findPathMatchingResources(locationPattern); 方法,我们看看实现:
Java代码
/**
* 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(<strong>doFindPathMatchingJarResources</strong>(rootDirResource, subPattern));
}
else if (rootDirResource.getURL().getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirResource, subPattern, getPathMatcher()));
}
else {
result.addAll(<strong>doFindPathMatchingFileResources</strong>(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 方法源码如下:
Java代码
/**
* 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 类,其中做了简化处理:
Java代码
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> <strong>getJarUrl</strong>(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);
}
使用举例:
Java代码
/**
* 将配置内容转换成内置对象
*
* @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)) {
// <a class="header">org</a>.<a class="header">springframework</a>.<a class="header">util</a>.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:
}
}
是不是相当简单?