Asp.Net大型项目实践(7)-用Unity实现AOP之事务处理+为啥要用AOP(附源码)
在目录中我计划对权限管理,异常管理,事务管理,日志管理,异常管理等项目中AOP典型应用场景进行详细的描述,本篇我们用Unity的Interception来实现项目中的事务处理。
- 为啥要用AOP
由于这是第一篇写关于AOP的场景,所以我觉得有必要通俗的说明一下在项目中使用AOP的好处。按照弦哥的惯例,关于AOP大套的理论大家自己去google吧,我下面举一个通俗的例子来说明。比如在项目中我们有一个业务逻辑的方法:
public void 我是一个干净的业务逻辑方法()
{
我的N行业务逻辑代码....
我的N行业务逻辑代码....
我的N行业务逻辑代码....
我的N行业务逻辑代码....
}我们可以看到他相对来说“很干净”,只有他本身应该内聚的业务逻辑。
接下来项目要求这个干净的业务逻辑方法 需要支持:拦截异常并向上层重新抛出异常;记录异常日志;记录操作日志(如操作人,操作时间,执行时间等..);因为里面多次更新数据库要绑上事务回滚;要进行权限控制。于是乎他变成了下面这样一个“不干净”的业务逻辑方法:代码public void 我是一个不干净的业务逻辑方法()
{
using(我要记录操作日志)
{
我要判断权限
try(我要异常处理)
{
using (我要事务处理)
{
我的N行业务逻辑代码....
我的N行业务逻辑代码....
我的N行业务逻辑代码....
我的N行业务逻辑代码....
我的N行业务逻辑代码....
}
}
catch(Exception e)
{
throw e;
我要这里记录异常日志..
}
}
}我们看到,由于这些非功能性需求所带来的代码耦合在了这个方法里,使得他不干净了,我上面的伪代码还是默认你有比较好的这些非功能性需求的公共类,如果没做好估计更乱,相信这种场景大家在开发中都会经常遇到。业务代码和非功能性需求代码混杂在一起了 影响了可读性是其次的,更主要的是当业务改变时你需要去掉一些非功能性需求的时候,那将是你噩梦的开始....如:在系统刚上线实施的时候 我们可能需要对每个方法进行详细的日志记录,异常记录,以便保证上线实施工作的顺利进行。但当系统相对稳定的时候,可能对部分不是很重要的方法 我们就想取消掉日志记录以提高系统性能,要是全部取消还好说,改公共类就OK了,要是要求部分取消.....改到你眼花手抽筋!
下面我们来看这个业务逻辑方法被AOP之后是啥效果:代码[我要判断权限]
[我要记录操作日志]
[我要事务处理]
[我要异常处理]
public void 我是一个被AOP的业务逻辑方法()
{
我的N行业务逻辑代码....
我的N行业务逻辑代码....
我的N行业务逻辑代码....
我的N行业务逻辑代码....
}我们用Attribute给这个进行了标记,很明显这个业务逻辑方法又“干净”了。
有同学会撅着小嘴说:按你上面那个日志记录的例子,取消掉部分方法的日志记录还不是要修改代码。。。其实我们除了可以通过Attribute方式对方法进行标记,还可以通过XML文件配置进行实现。
看到这里相信你已经了解为什么我们要在项目恰当的场景使用AOP了,下面我们开始实践如何使用Unity实现事务处理的AOP 。 -
首先我们创建一个类TransactionInterceptor,我们叫他事务拦截器
代码public class TransactionInterceptor : ICallHandler
{
public IsolationLevel Level { get; private set; }
protected virtual ISession Session
{
get { return SessionBuilder.CreateSession(); }
}
public TransactionInterceptor(IsolationLevel level,int order)
{
this.Level = level;
this.Order = order;
}
#region ICallHandler 成员
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
using (ITransaction ts = Session.BeginTransaction(Level))
{
var result = getNext()(input, getNext);
if (result.Exception == null)
{
ts.Commit();
}
else
{
ts.Rollback();
}
return result;
}
}
public int Order { get; set; }
#endregion
}a.注意必须继承ICallHandler;
b.Level 允许设置事务隔离级别
c.Session,因为我们用的是NHibernate,所以这里需要把Session带过来,你看上去是SessionBuilder.CreateSession()去创建了一个新的Session,其实不然,别忘记了我们的Session的生存周期是绑定在一次Asp.net MVC的Action上的;
d.Order表示执行顺序,因为上面我们知道一个方法可能存在多个拦截器,那么你有可能会需要显示的控制多个拦截器的执行顺序;
e. using (ITransaction ts = Session.BeginTransaction(Level))这里我们使用NHibernate的事务处理,我看了下NHibernate的源码,其实还是用的System.Data.IDbTransaction。
f.注意啦!!var result = getNext()(input, getNext);这句话就表示被拦截的方法里的代码在这句话出现的时候执行
g.注意异常的处理,不能用try catch,而是用if (result.Exception == null) 去判断是否产生异常,这里多谢 Artech同学的提醒;
h.其实大家可以看到这里事务的处理还是比较基础的,没有考虑分布式事务,Ado.net与NHibernate混用等情况。没关系现在先这样,因为我们可以预见就算将来需要的时候再加也不会对系统带来任何震荡:)。 -
对于事务的处理我希望显示的用Attribute对方法进行标记,而不是用XML文件配置。所以我们要定义一个TransactionAttribute,这样带这个标记的方法就表示需要拦截进行事务处理:
代码/// <summary>
/// 事务处理标记
/// </summary>
public class TransactionAttribute : HandlerAttribute
{
public override ICallHandler CreateHandler(Microsoft.Practices.Unity.IUnityContainer container)
{
return new TransactionInterceptor(Level,Order);
}
/// <summary>
/// 事务隔离级别
/// </summary>
public IsolationLevel Level { get; private set; }
/// <summary>
/// 自定义事务隔离级别
/// </summary>
/// <param name="level"></param>
public TransactionAttribute(IsolationLevel level)
{
this.Level = level;
}
/// <summary>
/// 使用数据库默认隔离级别
/// </summary>
public TransactionAttribute() : this(IsolationLevel.Unspecified) { }
}a.注意需要继承HandlerAttribute;
b.重写了基类的CreateHandler方法 返回我们刚才建立的事务拦截器TransactionInterceptor -
特别注意!!对于事务处理的边界,正确的设计是在项目中的Services层(为什么事务的边界是在Services层可以自己好好想想,,也可以google下事务边界的相关知识。有不少人把事务边界放在Repositorie层那是完全错误的!)
-
接前面几篇里讲的字典功能那个例子,比如我们需要在一个方法里同时插入2个字典项,并且要绑定事务(当然这不可能是实际业务,只是为了方便代码举例),上面说了要放Services层,代码如下:
Service接口(里面有个方法叫插2次....):public interface IDictionaryService
{
//插2次-_-b
void InsertTwice();
}Service实现:
代码public class DictionaryServiceImpl : IDictionaryService
{
private IDictionaryRepository DictionaryRepository;
public DictionaryServiceImpl(IDictionaryRepository DictionaryRepository)
{
this.DictionaryRepository = DictionaryRepository;
}
//上面的代码是上篇讲的依赖注入,别忘记了:)
#region IDictionaryService 成员
public void InsertTwice()
{
var dic1 = new Dictionary();
var dic2 = new Dictionary();
DictionaryRepository.SaveOrUpdate(dic1);
DictionaryRepository.SaveOrUpdate(dic2);
}
#endregion
}通过依赖注入IDictionaryRepository的SaveOrUpdate方法 插了2个数据字典项,但是并没有绑定事务。
-
Unity的AOP可以从3种标记的情况拦截:
TransparentProxyInterceptor:直接在类的方法上进行标记,但是这个类必须继承MarshalByRefObject...这玩意儿我觉得不大好,不建议用
VirtualMethod:直接在类的方法上进行标记,但这个方法必须是虚方法(就是方法要带virtual关键字)
InterfaceInterceptor:在接口的方法上进行标记,这样继承这个接口的类里实现这个接口方法的方法就能被拦截(有点拗口...)
在这里我们使用第三种方法InterfaceInterceptor,所以我们把接口的方法打上标记TransactionAttribute:public interface IDictionaryService
{
//插2次-_-b
[Transaction]
void InsertTwice();
} -
Unity对AOP的实现还是要依赖于UnityContainer,也有两种实现方式:代码实现和XML配置实现,这里我们使用XML配置实现。
新建一个XML文件unity.aop.infrastructure.config(注意看里面的注释):代码<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="unity"
type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"/>
</configSections>
<unity>
<typeAliases>
<typeAlias alias="InterfaceInterceptor" type="Microsoft.Practices.Unity.InterceptionExtension.InterfaceInterceptor, Microsoft.Practices.Unity.Interception" />
</typeAliases>
<containers>
<container>
<extensions>
<add type="Microsoft.Practices.Unity.InterceptionExtension.Interception, Microsoft.Practices.Unity.Interception"/>
</extensions>
<extensionConfig>
<add name="interception"
type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationElement, Microsoft.Practices.Unity.Interception.Configuration">
<interceptors>
<interceptor type="InterfaceInterceptor">
<!--这里表示需要对IDictionaryService里的方法进行拦截处理,至于拦截里面哪个方法,是根据继承了HandlerAttribute的标记决定-->
<key type="Demo.HIS.Infrastructure.Core.Services.IDictionaryService, Infrastructure.Core"/>
</interceptor>
</interceptors>
</add>
</extensionConfig>
</container>
</containers>
</unity>
</configuration>然后别忘记把IDictionaryService加到上篇中的unity.di.infrastructure.config文件里:
代码<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="unity"
type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration, Version=1.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</configSections>
<unity>
<typeAliases>
<!--BeginRepository-->
<typeAlias alias="IDictionaryRepository" type="Demo.HIS.Infrastructure.Core.Repositories.IDictionaryRepository, Infrastructure.Core" />
<!--EndRepository-->
<!--BeginService-->
<typeAlias alias="IDictionaryService" type="Demo.HIS.Infrastructure.Core.Services.IDictionaryService, Infrastructure.Core" />
<!--EndService-->
<!--BeginFacade-->
<typeAlias alias="IDictionaryFacade" type="Demo.HIS.Infrastructure.Facade.IDictionaryFacade, Infrastructure.Facade" />
<!--EndFacade-->
</typeAliases>
<containers>
<container>
<types>
<!--BeginRepository-->
<type type="IDictionaryRepository" mapTo="Demo.HIS.Infrastructure.Repositories.DictionaryRepositoryImpl, Infrastructure.Repositories" />
<!--EndRepository-->
<!--BeginService-->
<type type="IDictionaryService" mapTo="Demo.HIS.Infrastructure.Core.Services.Impl.DictionaryServiceImpl, Infrastructure.Core" />
<!--EndService-->
<!--BeginFacade-->
<type type="IDictionaryFacade" mapTo="Demo.HIS.Infrastructure.Facade.DictionaryFacadeImpl, Infrastructure.Facade" />
<!--EndFacade-->
</types>
</container>
</containers>
</unity>
</configuration> -
还记得上篇我们的ContainerFactory类吗,XML文件unity.aop.infrastructure.config的读取还是在这里:
代码public static class ContainerFactory
{
public static IUnityContainer GetContainer()
{
IUnityContainer Container = new UnityContainer();
ExeConfigurationFileMap infraFileMap = new ExeConfigurationFileMap();
infraFileMap.ExeConfigFilename = HttpContext.Current.Server.MapPath("~/unity.di.infrastructure.config");
UnityConfigurationSection infraConfig = (UnityConfigurationSection)ConfigurationManager
.OpenMappedExeConfiguration(infraFileMap, ConfigurationUserLevel.None)
.GetSection("unity");
if (infraConfig.Containers.Default != null)
{
infraConfig.Containers.Default.Configure(Container);
}
//上面是上篇讲的DI依赖注入XML文件的读取,下面是本篇讲的AOP的XML文件读取
ExeConfigurationFileMap infraAopFileMap = new ExeConfigurationFileMap();
infraAopFileMap.ExeConfigFilename = HttpContext.Current.Server.MapPath("~/unity.aop.infrastructure.config");
UnityConfigurationSection infraAopConfig = (UnityConfigurationSection)ConfigurationManager
.OpenMappedExeConfiguration(infraAopFileMap, ConfigurationUserLevel.None)
.GetSection("unity");
if (infraAopConfig.Containers.Default != null)
{
infraAopConfig.Containers.Default.Configure(Container);
}
return Container;
}
} -
在Facade层把Services层接口简单“包装”一下,然后就可以在Presentation层(Asp.Net MVC的Controller)使用了,代码我就不写了,上篇和前几篇都有讲过了
源码:所有实现代码都贴出来了,且也偷了下懒没按照惯例写我们“土土的测试”。写下篇的时候再上源码吧:)