书接上回,解决目前最后一个问题:根据模版mapping自动生成各个可能的XXX专用mapping。
上回简单的谈了一下思路
思路 把那些xml反序列化,然后Clone出来几份,replace一些东西之后再序列化 但是,在什么时机呢,由谁来做呢?可以有很多的选择。比如说可以在编译之前手动执行一个tool做这件事,然后把新得到的那些mapping都编译进去。但是这意味着每次客户追加一个XXX的可能取值,我们都需要重新编译我们的dll。这无疑是很麻烦的。
鉴于目前还不会实现完全动态的加载新的mapping,我给自己设定的目标是:手动改一处配置,然后重启一下进程。另外,为了获得更好的性能,希望已经创建好的mapping在下一次不需要重新copy-replace。
于是思路变成在BuildSessionFactory之前,读配置文件中的一个list(包含XXX的可能取值),看对应的assembly是否已经存在。若不存在emit一个出来并存在硬盘上,使下一次不用再次创建。
代码示例如下
private readonly XmlSerializer m_serializer = new XmlSerializer( typeof (HbmMapping));
private void EmbeddedHbmIntoMoudle(ModuleBuilder modBuilder, string replaceFrom, string replaceTo)
{
var templateAsm = Assembly.Load(TemplateAssemblyName);
foreach (var resourceName in templateAsm.GetManifestResourceNames())
{
if (resourceName.StartsWith(ReplacableResourceNamePrefix))
{
var mapping = m_serializer.Deserialize(templateAsm.GetManifestResourceStream(resourceName)) as HbmMapping;
foreach (var item in mapping.Items)
{
if (item is HbmClass)
{
ReplaceClassEntityName(item as HbmClass, replaceFrom, replaceTo);
}
else if (item is HbmJoinedSubclass)
{
ReplaceJoinSubClassEntityName(item as HbmJoinedSubclass, replaceFrom, replaceTo);
}
else
{
// ignore
}
}
var stream = new MemoryStream();
m_serializer.Serialize(stream, mapping);
modBuilder.DefineManifestResource(resourceName, stream, ResourceAttributes.Public);
}
else
{
// ignore
}
}
}
其中只处理了HbmClass和HbmJoinedSubclass(因为项目中只用到了这两种。。。),内容就是替换其entity-name,table,many-to-one的entity-name等一切包含了XXX的地方。啊,对了,不要忘记HbmJoinedSubclass的extends(我就给忘了,结果UT跑不过)。
关于调用的时机么,恩,这里项目出现了一些变化,客户终于允许我们使用Spring,算是帮了大忙。毕竟对于我本人来说,定制Spring的行为比定制Nhibernate的行为熟悉多了。具体到这个问题,就是继承一下Spring实现的LocalSessionFactoryObject,在某个时点插入一点自己的代码就好了。具体什么时点呢?我们来看一下AfterPropertiesSet的代码(这是Spring做完注入之后调用的一个方法)
{
// Create Configuration instance.
Configuration config = NewConfiguration();
if ( this .dbProvider != null )
{
config.SetProperty(Environment.ConnectionString, dbProvider.ConnectionString);
config.SetProperty(Environment.ConnectionProvider, typeof (DbProviderWrapper).AssemblyQualifiedName);
configTimeDbProvider = this .dbProvider;
}
if (ExposeTransactionAwareSessionFactory)
{
// Set ICurrentSessionContext implementation,
// providing the Spring-managed ISession s current Session.
// Can be overridden by a custom value for the corresponding Hibernate property
config.SetProperty(Environment.CurrentSessionContextClass, typeof (SpringSessionContext).AssemblyQualifiedName);
}
if ( this .entityInterceptor != null )
{
// Set given entity interceptor at SessionFactory level.
config.SetInterceptor( this .entityInterceptor);
}
if ( this .namingStrategy != null )
{
// Pass given naming strategy to Hibernate Configuration.
config.SetNamingStrategy( this .namingStrategy);
}
#if NH_2_1
if ( this .typeDefinitions != null )
{
// Register specified Hibernate type definitions.
IDictionary < string , string > typedProperties = new Dictionary < string , string > ();
foreach (DictionaryEntry entry in hibernateProperties)
{
typedProperties.Add(( string ) entry.Key, ( string ) entry.Value);
}
Dialect dialect = Dialect.GetDialect(typedProperties);
Mappings mappings = config.CreateMappings(dialect);
for ( int i = 0 ; i < this .typeDefinitions.Length; i ++ )
{
IObjectDefinition typeDef = this .typeDefinitions[i];
Dictionary < string , string > typedParamMap = new Dictionary < string , string > ();
foreach (DictionaryEntry entry in typeDef.PropertyValues)
{
typedParamMap.Add(( string ) entry.Key, ( string ) entry.Value);
}
mappings.AddTypeDef(typeDef.ObjectTypeName, typeDef.ObjectTypeName, typedParamMap);
}
}
#endif
if ( this .filterDefinitions != null )
{
// Register specified NHibernate FilterDefinitions.
for ( int i = 0 ; i < this .filterDefinitions.Length; i ++ )
{
config.AddFilterDefinition( this .filterDefinitions[i]);
}
}
#if NH_2_1
// check whether proxy factory has been initialized
if (config.GetProperty(Environment.ProxyFactoryFactoryClass) == null
&& (hibernateProperties == null || ! hibernateProperties.Contains(Environment.ProxyFactoryFactoryClass)))
{
// nothing set by user, lets use Spring.NET's proxy factory factory
#region Logging
if (log.IsInfoEnabled)
{
log.Info( " Setting proxy factory to Spring provided one as user did not specify any " );
}
#endregion
config.Properties.Add(
Environment.ProxyFactoryFactoryClass, typeof (Bytecode.ProxyFactoryFactory).AssemblyQualifiedName);
}
#endif
if ( this .hibernateProperties != null )
{
if (config.GetProperty(Environment.ConnectionProvider) != null &&
hibernateProperties.Contains(Environment.ConnectionProvider))
{
#region Logging
if (log.IsInfoEnabled)
{
log.Info( " Overriding use of Spring's Hibernate Connection Provider with [ " +
hibernateProperties[Environment.ConnectionProvider] + " ] " );
}
#endregion
config.Properties.Remove(Environment.ConnectionProvider);
}
Dictionary < string , string > genericHibernateProperties = new Dictionary < string , string > ();
foreach (DictionaryEntry entry in hibernateProperties)
{
genericHibernateProperties.Add(( string ) entry.Key, ( string ) entry.Value);
}
config.AddProperties(genericHibernateProperties);
}
if ( this .mappingAssemblies != null )
{
foreach ( string assemblyName in mappingAssemblies)
{
config.AddAssembly(assemblyName);
}
}
if ( this .mappingResources != null )
{
IResourceLoader loader = this .ResourceLoader;
if (loader == null )
{
loader = this .applicationContext;
}
foreach ( string resourceName in mappingResources)
{
config.AddInputStream(loader.GetResource(resourceName).InputStream);
}
}
if (configFilenames != null )
{
foreach ( string configFilename in configFilenames)
{
config.Configure(configFilename);
}
}
#if NH_2_1
// Tell Hibernate to eagerly compile the mappings that we registered,
// for availability of the mapping information in further processing.
PostProcessMappings(config);
config.BuildMappings();
if ( this .entityCacheStrategies != null )
{
// Register cache strategies for mapped entities.
foreach ( string className in this .entityCacheStrategies.Keys)
{
String[] strategyAndRegion = StringUtils.CommaDelimitedListToStringArray( this .entityCacheStrategies.GetProperty(className));
if (strategyAndRegion.Length > 1 )
{
config.SetCacheConcurrencyStrategy(className, strategyAndRegion[ 0 ], strategyAndRegion[ 1 ]);
}
else if (strategyAndRegion.Length > 0 )
{
config.SetCacheConcurrencyStrategy(className, strategyAndRegion[ 0 ]);
}
}
}
if ( this .collectionCacheStrategies != null )
{
// Register cache strategies for mapped collections.
foreach ( string collRole in collectionCacheStrategies.Keys)
{
string [] strategyAndRegion = StringUtils.CommaDelimitedListToStringArray( this .collectionCacheStrategies.GetProperty(collRole));
if (strategyAndRegion.Length > 1 )
{
throw new Exception( " Collection cache concurrency strategy region definition not supported yet " );
// config.SetCollectionCacheConcurrencyStrategy(collRole, strategyAndRegion[0], strategyAndRegion[1]);
}
else if (strategyAndRegion.Length > 0 )
{
config.SetCollectionCacheConcurrencyStrategy(collRole, strategyAndRegion[ 0 ]);
}
}
}
#endif
if ( this .eventListeners != null )
{
// Register specified NHibernate event listeners.
foreach (DictionaryEntry entry in eventListeners)
{
ListenerType listenerType;
try
{
listenerType = (ListenerType) Enum.Parse( typeof (ListenerType), ( string ) entry.Key);
}
catch
{
throw new ArgumentException( string .Format( " Unable to parse string '{0}' as valid {1} " , entry.Key, typeof (ListenerType)));
}
object listenerObject = entry.Value;
if (listenerObject is ICollection)
{
ICollection listeners = (ICollection) listenerObject;
EventListeners listenerRegistry = config.EventListeners;
// create the array and check that types are valid at the same time
ArrayList items = new ArrayList(listeners);
object [] listenerArray = ( object [])items.ToArray(listenerRegistry.GetListenerClassFor(listenerType));
config.SetListeners(listenerType, listenerArray);
}
else
{
config.SetListener(listenerType, listenerObject);
}
}
}
// Perform custom post-processing in subclasses.
PostProcessConfiguration(config);
#if NH_2_1
if (BytecodeProvider != null )
{
// set custom IBytecodeProvider
Environment.BytecodeProvider = BytecodeProvider;
}
else
{
// use Spring's as default
// Environment.BytecodeProvider = new Bytecode.BytecodeProvider(this.applicationContext);
}
#endif
// Build SessionFactory instance.
log.Info( " Building new Hibernate SessionFactory " );
this .configuration = config;
this .sessionFactory = NewSessionFactory(config);
AfterSessionFactoryCreation();
// set config time DB provider back to null
configTimeDbProvider = null ;
}
有点长,是不是?简单说来,这个方法做了如下的事情
{
// Create Configuration instance.
Configuration config = NewConfiguration();
// set一些基本的东西,跟Mapping息息相关
// ...
// Tell Hibernate to eagerly compile the mappings that we registered,
// for availability of the mapping information in further processing.
PostProcessMappings(config);
config.BuildMappings();
// set跟Mapping没啥关系的东西
PostProcessConfiguration(config);
// set另一些我们不关系的东西
// Build SessionFactory instance.
log.Info( " Building new Hibernate SessionFactory " );
this .configuration = config;
this .sessionFactory = NewSessionFactory(config);
AfterSessionFactoryCreation();
// set config time DB provider back to null
configTimeDbProvider = null ;
}
由此,我们就可以看出来,在config.BuildMappings()之前,我们必须完成我们的工作。我选择了override PostProcessMappings这个方法。
{
public IMappingAssemblyGenerator MappingAssemblyGenerator { get ; set ; }
protected override void PostProcessMappings(Configuration config)
{
base .PostProcessMappings(config);
foreach (var assembly in MappingAssemblyGenerator.Generate())
{
config.AddAssembly(assembly);
}
}
}
其中MappingAssemblyGenerator是一个用于copy-replace的类,核心的代码就是上面贴过的EmbeddedHbmIntoMoudle方法。
再贴一下调用EmbeddedHbmIntoMoudle并用emit创建dll并且保存至磁盘的方法
{
var rv = new List < Assembly > ();
var templateDllPath = new StringBuilder(DllPath).Append(TemplateAssemblyName).Append( " .dll " ).ToString();
var templateAssemblyFileLastWriteTime = GetTemplateAssemblyFileLastWriteTime(templateDllPath);
foreach (var replaceTo in ReplaceToList)
{
var sb = new StringBuilder(TargetAssemblyName).Append( " . " ).Append(replaceTo);
var asmName = sb.ToString();
var dllName = sb.Append( " .dll " ).ToString();
var dllPath = sb.Insert( 0 , DllPath).ToString();
if (NeedGenerate(dllPath, templateAssemblyFileLastWriteTime))
{
var asmBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( new AssemblyName(asmName), AssemblyBuilderAccess.RunAndSave, DllPath);
var modBuilder = asmBuilder.DefineDynamicModule(asmName, dllName);
EmbeddedHbmIntoMoudle(modBuilder, ReplaceFrom, replaceTo);
asmBuilder.Save(dllName);
}
rv.Add(Assembly.Load(File.ReadAllBytes(dllPath)));
}
return rv;
}
这个系列到这里就算完结了,虽然没有能够动态的实现创建mapping并加载,但还算是大体达到了客户的要求,也达到了我对自己的一个要求(手动改一处配置,然后重启一下进程就可以完成增加mapping),还算满意吧。
最后说一下,这个系列建立在NH2.1和Spring1.3的基础上。