前幾天因爲公司項目架構調整,想將之前代碼開發爲主改爲配置文件配置爲主,即全部的外部服務調用由配置文件組織,由於必須高效,因此涉及包括調用順序,併發調用等,但配置文件的缺陷是隻能實現簡單的業務邏輯,因此咱們還用了jeval表達式Jar包。java
廢話很少說,因爲服務配置文件是放在Maven項目下的一個子模塊的classpath下,該子模塊在eclipse下運行是以用文件系統路徑來掃描到並解析的,但在線上環境,該子模塊是會被打成Jar包,就是說線上環境是須要解析該子模塊的Jar包才能取到配置文件的。spring
Jar包本質上是壓縮文件,之前也作個在壓縮文件中解析配置文件,但感受不太專業,因爲時間趕,不想在網上撈資料,並且靠不靠譜也不必定,因而想到了借鑑Spring中的掃描和解析配置文件的功能代碼。架構
咱們常常用以下Spring配置來解析資源文件和掃描class:併發
<context:component-scan base-package="com.manzhizhen.server.service,com.manzhizhen.server.aop" />app
<bean
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:conf/resource1.properties</value>
</list>
</property>
</bean>eclipse
我本地已經有Spring4的源碼,因而我直接在源碼中搜索base-package關鍵字,因而定位到ComponentScanBeanDefinitionParser類:ide
public class ComponentScanBeanDefinitionParser implements BeanDefinitionParser { this
private static final String BASE_PACKAGE_ATTRIBUTE = "base-package"; url
而後我搜索哪些類用到了BASE_PACKAGE_ATTRIBUTE,因而找到了ComponentScanBeanDefinitionParser類:
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:
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方法是怎麼實現的:
/**
* 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 <strong>super.getResources(locationPattern);</strong>
}
加黑部分,發現它是調了父類的方法AbstractApplicationContext#getResources:
public Resource[] getResources(String locationPattern) throws IOException {
return <strong>this.resourcePatternResolver.getResources(locationPattern);</strong>
}
this.resourcePatternResolver 是PathMatchingResourcePatternResolver類的對象,咱們看看它的getResources 方法:
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 中定義的常量:
/**
* 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 方法:
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(<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 方法源碼以下:
/**
* 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> <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);
}
使用舉例:
/**
* 將配置內容轉換成內置對象
*
* @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:
}
}
是否是至關簡單?