7.5 元数据引擎实现
前面我们设计了元数据引擎,这一节将看一下元数据引擎的具体实现。
7.5.1 根据元数据路径加载元数据
IMetaDataLoader接口中定义了根据元数据路径加载元数据运行时模型的方法public EntityModelInfo loadEntityByEntityPath(String path),实现此方法有两种思路:
l 编写一个专用的文件加载器,可以按照元数据路径从文件夹(开发环境部署模式)或者Jar文件(正式运行环境部署模式)中加载元数据文件,然后再对元数据文件进行解析。
l 将元数据所在的文件夹(开发环境部署模式)或者jar文件(正式运行环境部署模式)放入类路径(ClassPath)中,这样在程序中就可以通过getClass().getResource- AsStream("/com/cownew/PIS/demo/Person.emf")这样的方式来加载元数据文件了。
很显然,第二种方式实现起来最简单,所以在系统中将元数据所在的文件夹或者jar文件放入类路径(ClassPath)中。
7.5.2 元数据枚举器
IMetaDataLoader接口中定义了加载所有的实体元数据的path的方法public List loadAllEntityPath()。JDK没有提供枚举类路径中某一类文件的方法,我们必须开发这样的功能,我们称其为元数据枚举器。其接口定义如下:
public interface IMetaDataEnumerator
{
public List listAll();
}
接口只定义了一个listAll方法,它返回所有的元数据的路径列表。
元数据有文件夹和Jar文件两种部署模式,为了同时支持这两种方式的元数据枚举,可以编写一个元数据枚举器,在这个枚举器的listAll方法中判断是哪种部署模型,然后进行不同的处理。这样实现违背了面向对象开发的基本原则,应该将这两种不同的行为分别都定义在不同的枚举器中:DirectoryMetaDataEnumerator类负责在文件夹中进行元数据枚举,而JarFileMetaDataEnumerator类负责在Jar文件中进行元数据枚举。
【例7.4】元数据枚举器抽象类。
为了抽象出这两种枚举器的公共行为,首先编写一个实现了IMetaDataEnumerator接口的抽象类AbstractMetaDataEnumerator:
// 元数据枚举器抽象类
public abstract class AbstractMetaDataEnumerator implements
IMetaDataEnumerator
{
protected List list;
public List listAll()
{
// 进行惰性初始化处理,子类是要实现fillList方法,
// 在这个方法中向list中填充元数据路径即可,不用管惰性初始化问题
if (list == null)
{
fillList();
}
return list;
}
protected abstract void fillList();
}
抽象类中进行了惰性初始化处理,子类只要实现fillList方法即可。
【例7.5】Jar文件元数据枚举器。
接着编写从AbstractMetaDataEnumerator继承的JarFileMetaDataEnumerator类:
// Jar文件元数据枚举器
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.jar.JarFile;
public class JarFileMetaDataEnumerator extends AbstractMetaDataEnumerator
{
protected JarFile jarFile;
protected String suffix;
public JarFileMetaDataEnumerator(JarFile jarFile, String suffix)
{
super();
this.jarFile = jarFile;
this.suffix = suffix;
}
public void fillList()
{
list = new ArrayList();
Enumeration entries = jarFile.entries();
while (entries.hasMoreElements())
{
Object obj = entries.nextElement();
String e = obj.toString();
if (e.endsWith(suffix))
{
list.add("/" + e);
}
}
}
}
JarFileMetaDataEnumerator 的构造函数接受两个参数jarFile为元数据所在的Jar包,suffix为待匹配文件的扩展名,一般传递“.emf”。
通过JarFile 类的public Enumeration entries()方法即可得到遍历Jar文件中所有文件的枚举器,在遍历文件过程中判断文件扩展名,若等于suffix的话,就认为它是元数据文件。
【例7.6】目录元数据枚举器。
接着编写目录元数据枚举器,代码如下:
// 目录元数据枚举器
public class DirectoryMetaDataEnumerator extends AbstractMetaDataEnumerator
{
private File dir;
private String suffix;
public DirectoryMetaDataEnumerator(File dir, String suffix)
{
super();
this.dir = dir;
this.suffix = suffix;
}
public void fillList()
{
list = new ArrayList();
list.addAll(getChidren(dir, suffix));
}
private List getChidren(File f, String suffix)
{
List list = new ArrayList();
File[] listFiles = f.listFiles();
if (listFiles == null)
{
return list;
}
for (int i = 0, n = listFiles.length; i < n; i++)
{
File file = listFiles[i];
if (file.isFile())
{
if (file.getPath().endsWith(suffix))
{
String path = file.getPath().substring(
dir.toString().length());
path = path.replace(File.separatorChar, '/');
if (!path.startsWith("/"))
{
path = "/" + path;
}
list.add(path);
}
} else
{
list.addAll(getChidren(file, suffix));
}
}
return list;
}
}
此类的核心代码就是getChidren方法,此方法采用递归的方式遍历一个目录下的所有文件。当File的实例调用一个目录的时候,就调用其listFiles()方法得到其下的所有直接子文件(或者目录)。使用File类的isFile()方法判断子文件是文件还是文件夹,如果是文件夹则继续递归。文件的分隔符在不同的操作系统中有不同的形式,在UNIX系统中为“/”而在Windows中则为“\”,元数据路径遵守Java中的跨平台的要求,分隔符使用“/”,使用path = path.replace(File.separatorChar, '/')方法将路径中的平台相关的文件分隔符替换成“/”。
7.5.3 元数据缓存
元数据的解析是非常耗时且耗用内存的,如果每次加载元数据运行时模型都要去解析XML文件的话,会大大降低系统的运行效率,因此需要建立元数据的缓存机制。在loadEntityByEntityPath方法中首先判断要加载的元数据是否已经解析过并放在缓存中了,如果已经放在缓存中则只需到缓存中去取就可以;如果没有在缓存中,则解析元数据,然后将解析结果放入缓存。
由于缓存是建立在内存哈希表中的,当系统重启(包括客户端重启或者服务器端重启)以后,缓存就消失了,元数据必须被再次加载才能放到缓存中。既然上次运行的时候已经解析过元数据了,为什么不把上次的缓存保存下来呢?由于缓存是以哈希表的形式存在的,而哈希表是可以序列化的,所以在系统即将关闭的时候将缓存保存到硬盘中,下次系统重启的时候只要读取这个缓存文件并重建缓存即可。这项技术被称为“元数据延迟预编译”。更近一步,在系统正式安装运行的之前就将所有元数据解析一遍然后保存到缓存中,进而将缓存保存到文件中,系统运行的时候根本不用再去解析元数据,只要从缓存中读取就行了,这种技术被称为“元数据预编译”,和JSP页面预编译技术类似。这种技术在处理大数据量的不可变XML文件时很有用处。
元数据缓存在加快系统运行的同时也给开发人员带来了麻烦。举例来说:开发人员开发了实体元数据Person.emf,然后在系统中运行并加载了此元数据,这样元数据的运行时模型就保存到缓存中了。测试Person.emf的时候,开发人员发现要对Person.emf做一下修改,于是他修改了Person.emf,并保存了修改。当他再次调用元数据加载器加载此元数据的时候,由于Person.emf的元数据模型已经在缓存中存在了,所以他得到的是未修改之前的元数据模型。这种情况下,必须重启服务器和客户端,删除缓存文件。可以想象这是多么烦琐的过程,为了解决这个问题,我们在配置文件中增加一个缓存开关,在开发环境下关闭缓存开关,在正式运行时则打开缓存开关。
在ClientConfig.xml和ServerConfig.xml文件中同时增加下面针对元数据的配置项:
<MetaData>
<!--元数据缓存开关-->
<CacheEnabled>false</CacheEnabled>
<!--元数据路径-->
<MetaDataPath>F:\我的程序\java\CowNewPIS\metadata</MetaDataPath>
<!--元数据缓存文件的位置-->
<EntityCacheFile>C:\cownew\entity.cache</EntityCacheFile>
</MetaData>
并在ClientConfig.java和ServerConfig.java文件中增加读取这些配置项的代码。
7.5.4 元数据加载器
【例7.7】实现元数据加载器。
有了上面的这些类作为基础,下面就来看一下最核心的元数据加载器的实现代码:
// 元数据加载器
package com.cownew.PIS.framework.common.metaDataMgr;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarFile;
import org.apache.log4j.Logger;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.io.SAXReader;
import com.cownew.ctk.common.ExceptionUtils;
import com.cownew.ctk.io.ResourceUtils;
public class MetaDataLoader implements IMetaDataLoader
{
//实体路径到元数据模型的缓存
private static Map pathEntityMap;
//Vo类名到实体路径的缓存
private static Map voClassPathEntityMap;
//元数据的位置
private File metaDataPathFile;
//元数据缓存文件的位置
private File entityCacheFile;
//元数据枚举器
private IMetaDataEnumerator metaDataScanner;
//元数据缓存开关
private boolean cacheEnable;
/**
* @param metaDataPath 元数据所在的路径
* @param entityCacheFile 元数据缓存文件位置
*/
public MetaDataLoader(String metaDataPath, String entityCacheFile)
{
super();
metaDataPathFile = new File(metaDataPath);
if (!metaDataPathFile.exists())
{
throw new IllegalArgumentException("path:" + metaDataPath
+ " not found!");
}
//如果元数据所在的路径是目录,则使用目录元数据扫描器
//否则使用Jar文件元数据扫描器
if (metaDataPathFile.isDirectory())
{
metaDataScanner = new
DirectoryMetaDataEnumerator(metaDataPathFile,
"." + NameUtils.EMFEXT);
} else
{
try
{
metaDataScanner = new JarFileMetaDataEnumerator(new JarFile(
metaDataPathFile), "." + NameUtils.EMFEXT);
} catch (IOException e)
{
throw ExceptionUtils.toRuntimeException(e);
}
}
voClassPathEntityMap = new HashMap();
this.entityCacheFile = new File(entityCacheFile);
cacheEnable = false;
loadMetaCache();
}
public void setCacheEnable(boolean cacheEnable)
{
this.cacheEnable = cacheEnable;
}
//从元数据缓存文件中加载缓存
private void loadMetaCache()
{
if (!this.entityCacheFile.exists())
{
pathEntityMap = new HashMap();
}
ObjectInputStream ois = null;
try
{
ois = new ObjectInputStream(new FileInputStream(
this.entityCacheFile));
pathEntityMap = (Map) ois.readObject();
} catch (Exception e)
{
//只要发生异常就认为元数据缓存不可用,因此就不加载缓存
//发生异常一般是由于类版本不一致造成的
//这种异常一般只在开发环境或者正式运行环境的系统
//升级过程中出现
pathEntityMap = null;
Logger.getLogger(MetaDataLoader.class).error(e);
} finally
{
ResourceUtils.close(ois);
}
if (pathEntityMap == null)
{
pathEntityMap = new HashMap();
}
}
public EntityModelInfo loadEntityByEntityPath(String path)
throws MetaDataException
{
// 只有元数据缓存打开的时候才从缓存中读取
if (cacheEnable)
{
EntityModelInfo eInfo = (EntityModelInfo)
pathEntityMap.get(path);
if (eInfo != null)
{
return eInfo;
}
}
InputStream inStream = null;
try
{
inStream = getClass().getResourceAsStream(path);
Document doc = new SAXReader().read(inStream);
EntityModelInfo info = EntityMetaDataParser.xmlToBean(doc);
pathEntityMap.put(path, info);
return info;
} catch (DocumentException e)
{
throw new MetaDataException(
MetaDataException.LOADENTITYMETADATAERROR, e);
} finally
{
ResourceUtils.close(inStream);
}
}
public EntityModelInfo loadEntityByVOClass(Class voClass)
throws MetaDataException
{
return loadEntityByVOClass(voClass.getName());
}
public EntityModelInfo loadEntityByVOClass(String voClass)
throws MetaDataException
{
String entityPath = (String) voClassPathEntityMap.get(voClass);
if (entityPath == null)
{
entityPath = NameUtils.getEntityPath(voClass);
voClassPathEntityMap.put(voClass, entityPath);
}
EntityModelInfo info = loadEntityByEntityPath(entityPath);
return info;
}
public List loadAllEntityPath() throws MetaDataException
{
return metaDataScanner.listAll();
}
//保存缓存
public void saveCache()
{
ObjectOutputStream oos = null;
try
{
//无论缓存文件是否存在,重新创建文件
entityCacheFile.createNewFile();
oos = new ObjectOutputStream(new
FileOutputStream(entityCacheFile,false));
oos.writeObject(pathEntityMap);
} catch (Exception e)
{
Logger.getLogger(MetaDataLoader.class).error(e);
} finally
{
ResourceUtils.close(oos);
}
}
}
在初始化元数据枚举器的时候体现了基于接口编程的好处:
if (metaDataPathFile.isDirectory())
{
metaDataScanner = new DirectoryMetaDataEnumerator(metaDataPathFile,
"." + NameUtils.EMFEXT);
} else
{
metaDataScanner = new JarFileMetaDataEnumerator(
new JarFile(metaDataPathFile), "." + NameUtils.EMFEXT);
}
如果元数据文件是目录,则将metaDataScanner 变量初始化为DirectoryMetaDataEnumerator的实例;如果元数据文件是文件,则将metaDataScanner变量初始化为JarFileMetaDataEnumerator的实例。后面使用metaDataScanner的时候都是使用IMetaDataEnumerator接口声明的方法,而不管是哪个实现类的。
这里保存缓存的方式是直接将pathEntityMap对象序列化到文件中,这样做的优点是简单,缺点是当缓存中的对象对应类版本发生变化的时候(在开发环境中对类进行修改或者正式运行环境进行版本升级),反序列化就会失败。只要从缓存文件中反序列化pathEntityMap的时候发生任何异常,就重建缓存:
try
{
ois = new ObjectInputStream(new FileInputStream(
this.entityCacheFile));
pathEntityMap = (Map) ois.readObject();
} catch (Exception e)
{
pathEntityMap = null;
}
…
if (pathEntityMap == null)
{
pathEntityMap = new HashMap();
}
7.5.5 工具类
为了方便使用元数据引擎,系统中还内置了方便客户端和服务器端访问元数据的工具类。
【例7.8】内置方便客户端和服务器端访问元数据的工具类。
ClientMetaDataLoaderFactory是客户端元数据加载器工厂,它位于com.cownew. PIS.framework.client包中。
具体代码如下:
// 客户端元数据加载器工厂
public class ClientMetaDataLoaderFactory
{
private static MetaDataLoader loader;
public static IMetaDataLoader getLoader()
{
if (loader != null)
{
return loader;
}
ClientConfig config = ClientConfig.getInstance();
String entityCacheFile = config.getEntityCacheFile();
String metaDataPath = config.getMetaDataPath();
loader = new MetaDataLoader(metaDataPath, entityCacheFile); loader.setCacheEnable(
ClientConfig.getInstance().isMetaCacheEnabled());
return loader;
}
}
ServerMetaDataLoaderFactory是服务器端元数据加载器工厂,它位于com.cownew. PIS.framework.server.helper包中。
具体代码如下:
// 服务器端元数据加载器工厂
public class ServerMetaDataLoaderFactory
{
private static MetaDataLoader loader;
public static IMetaDataLoader getLoader()
{
if (loader != null)
{
return loader;
}
ServerConfig config = ServerConfig.getInstance();
String entityCacheFile = config.getEntityCacheFile();
String metaDataPath = config.getMetaDataPath();
loader = new MetaDataLoader(metaDataPath, entityCacheFile);
loader.setCacheEnable(
ServerConfig.getInstance().isMetaCacheEnabled());
return loader;
}
static
{
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run()
{
super.run();
ServerMetaDataLoaderFactory.getLoader().saveCache();
}
});
}
}
客户端的元数据缓存的保存动作是放到注销方法中的。与客户端不同的是,服务器关闭的时候没有一个准确的响应服务器关闭的入口,必须借助其他手段来实现服务器关闭时保存元数据缓存的功能。
java.lang.Runtime类有一个addShutdownHook方法,使用它我们可以把一个线程对象注册到虚拟机中。当虚拟机正常关闭时,虚拟机会调用注册的所有线程对象,并运行它们。这样把保存元数据缓存的方法写到一个线程对象中,然后调用addShutdownHook将其注册到JVM即可。
MetaDataHelper位于com.cownew.PIS.framework.common.metaDataMgr包中,这个助手类的构造函数要求一个IMetaDataLoader接口的实例。类中有一个方法:public String getPropertyAlias(Class voClass, String property),通过这个方法可以得知一个实体值对象的某个字段的别名。
例如调用metaDataHelper.getPropertyAlias(PersonInfo.class, "age")就会返回“年龄”。通过元数据引擎就能够得到数据的一些模型信息,这就是元数据的神奇之处。
MetaDataHelper类目前只有这一个方法,读者可以根据需要增加更多的方法,比如得到实体值对象的所有关联实体、得到实体值对象的某个字段的类型等。
7.5.6 待改进问题
到了这里,元数据引擎已经基本可用了。任何事情都不可能是完美的,这个元数据引擎还可以在如下几方面优化:
l 实现“元数据预编译”。目前实现的是“元数据延迟预编译”,在系统第一次运行的时候要进行元数据的解析,所以运行速度会比较慢,采用“元数据预编译”以后就可以避免此问题。
l 优化元数据缓存策略。现阶段系统中业务模块较少,元数据量较少,所以对所有访问过的元数据都进行了缓存。当系统的业务模块发展到一定规模以后,系统中会存在大量元数据,如果把这些元数据都加载到缓存中必将大量地占用内存。说到这里,第一个反应就是将目前的缓存改造成固定大小的采用LRU淘汰算法的缓存,但是这种淘汰算法只能解决客户端的问题。在真实的业务系统中,一个登录的客户端通常大部分时间只运行一部分业务功能,比如会计登录系统的时候只会登录财务模块、库管只会登录仓库管理模块,即使会访问其他模块也是暂时和短暂的;而应用服务器端则不同,应用服务器是为所有客户端提供服务的,它会几率均等地访问系统的各个模块。在客户端只有部分元数据会被频繁访问,而在应用服务器端大部分元数据都会被频繁地访问,所以说客户端的元数据访问具有局部性,而应用服务器端元数据访问的局部性则不明显。对访问局部性很强的客户端采用LRU淘汰算法能够起到非常良好的作用,而如果对应用服务器端元数据采用LRU淘汰算法则会导致缓存的抖动。基于此,我们建议对客户端元数据采用LRU淘汰算法,而对应用服务器端则采取增加内存容量的方式来解决问题。
l 采用非递归算法改造DirectoryMetaDataEnumerator类。当目录结构过深或者目录数量过多的话,此实现算法会导致系统性能急剧下降,甚至使机器发生故障。
<!-- page -->