我前两天看见同事用写了用AOP技术实现缓存的方案,于是好奇看了一下这是怎么实现的。原来是用了.NET中的一个类ContextBoundObject和Attribute相关技术。其实个类在.NET Framework很早就有,至今才认识它,是有点相见恨晚的感觉。网上一搜,已经有了很多使用ContextBoundObject类实现AOP的例子,其中我就看到一篇利用ContextBoundObject和Attribute实现AOP事务实现例子,我想应该和实现AOP缓存是一个道理。下面我就把这篇文章分享出来:
前言
使用Attribute来实现方法级别事务一直是我的梦想,浅谈Attribute [C# | Attribute | DefaultValueAttribute]有体现我的无奈,Attribute确实是真真切切的非侵入式的东西(其实我是想侵入的: ) ),前有DUDU的Attribute在.net编程中的应用系列文章,但是总是离想象和需求有那么点出入,通过三天的努力,Google的陪伴,下面和大家一起分享我这三天的成果 用Attribute实现AOP事务 吧!
致谢文章
1. Aspect-Oriented Programming Enables Better Code Encapsulation and Reuse 关键性的CallContext是在这里发现的。
2. C# Attribute在.net编程中的应用 (转) 这篇文章原文地址找不到了,DUDU的Attribute在.net编程中的应用系列文章就是这篇文章的分解,他写到了五,后面的大家可以从这篇文章里面提前看到了。
阅前注意
1. 整篇文章的核心和突破点在于上下文Context的使用,务必注意CallContext在整个程序中起到的作用
2. 本文中看到的SqlHelper使用的是微软SqlHelper.cs。
3. 本文重点在于如何实现,并且已经测试通过,只贴关键性代码,所以请认真阅读,部分代码直接拷贝下来运行是会出错的!
正文
首先我们来看一段未加事务的代码:
SqlDAL.cs
public abstract class SqlDAL { #region ConnectionString private SqlConnectionStringBuilder _ConnectionString = null; /// <summary> /// 字符串连接 /// </summary> public virtual SqlConnectionStringBuilder ConnectionString { get { if (_ConnectionString == null || string.IsNullOrEmpty(_ConnectionString.ConnectionString)) { _ConnectionString = new SqlConnectionStringBuilder(Configurations.SQLSERVER_CONNECTION_STRING); } return _ConnectionString; } set { _ConnectionString = value; } } #endregion #region ExecuteNonQuery public int ExecuteNonQuery(string cmdText) { return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, CommandType.Text, cmdText); } public int ExecuteNonQuery(string cmdText, CommandType type) { return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, type, cmdText); } public int ExecuteNonQuery(string cmdText, CommandType type, params SqlParameter[] cmdParameters) { return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, type, cmdText, cmdParameters); } #endregion }
代码说明:
1. 本类对SqlHelper.cs 进一步封装。
2. Configurations.SQLSERVER_CONNECTION_STRING 替换成自己的连接字符串就行了。
UserInfoAction.cs
public class UserInfoAction : SqlDAL { /// <summary> /// 添加用户 /// </summary> public void Add(UserInfo user) { StringBuilder sb = new StringBuilder(); sb.Append("UPDATE [UserInfo] SET Password='"); sb.Append(user.Password); sb.Append("' WHERE UID="); sb.Append(user.UID); ExecuteNonQuery(sql); } }如果我们要加入事务,通常的办法就是在方法内try、catch然后Commit、Rollback,缺点就不说了,下面我会边贴代码边讲解,力图大家也能掌握这种方法: )
先贴前面两个被我修改的类
SqlDAL.cs
public abstract class SqlDAL : ContextBoundObject { private SqlTransaction _SqlTrans; /// <summary> /// 仅支持有事务时操作 /// </summary> public SqlTransaction SqlTrans { get { if (_SqlTrans == null) { //从上下文中试图取得事务 object obj = CallContext.GetData(TransactionAop.ContextName); if (obj != null && obj is SqlTransaction) _SqlTrans = obj as SqlTransaction; } return _SqlTrans; } set { _SqlTrans = value; } } #region ConnectionString private SqlConnectionStringBuilder _ConnectionString = null; /// <summary> /// 字符串连接 /// </summary> public virtual SqlConnectionStringBuilder ConnectionString { get { if (_ConnectionString == null || string.IsNullOrEmpty(_ConnectionString.ConnectionString)) { _ConnectionString = new SqlConnectionStringBuilder(Configurations.SQLSERVER_CONNECTION_STRING); } return _ConnectionString; } set { _ConnectionString = value; } } #endregion #region ExecuteNonQuery public int ExecuteNonQuery(string cmdText) { if (SqlTrans == null) return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, CommandType.Text, cmdText); else return SqlHelper.ExecuteNonQuery(SqlTrans, CommandType.Text, cmdText); } public int ExecuteNonQuery(string cmdText, CommandType type) { if (SqlTrans == null) return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, type, cmdText); else return SqlHelper.ExecuteNonQuery(SqlTrans, type, cmdText); } public int ExecuteNonQuery(string cmdText, CommandType type, params SqlParameter[] cmdParameters) { if (SqlTrans == null) return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, type, cmdText, cmdParameters); else return SqlHelper.ExecuteNonQuery(SqlTrans, type, cmdText, cmdParameters); } #endregion }代码说明:
1. 加了一个属性(Property)SqlTrans,并且每个ExecuteNonQuery执行前都加了判断是否以事务方式执行。这样做是为后面从上下文中取事务做准备。
2. 类继承了ContextBoundObject,注意,是必须的,MSDN是这样描述的:定义所有上下文绑定类的基类。
3. TransactionAop将在后面给出。
UserInfoAction.cs
[Transaction] public class UserInfoAction : SqlDAL { [TransactionMethod] public void Add(UserInfo user) { StringBuilder sb = new StringBuilder(); sb.Append("UPDATE [UserInfo] SET Password='"); sb.Append(user.Password); sb.Append("' WHERE UID="); sb.Append(user.UID); ExecuteNonQuery(sql); } }代码说明:
1. 很简洁、非侵入式、很少改动、非常方便(想要事务就加2个标记,不想要就去掉)。
2. 两个Attribute后面将给出。
/// <summary> /// 标注类某方法内所有数据库操作加入事务控制 /// </summary> [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public sealed class TransactionAttribute : ContextAttribute, IContributeObjectSink { /// <summary> /// 标注类某方法内所有数据库操作加入事务控制,请使用TransactionMethodAttribute同时标注 /// </summary> public TransactionAttribute() : base("Transaction") { } public IMessageSink GetObjectSink(MarshalByRefObject obj, IMessageSink next) { return new TransactionAop(next); } } /// <summary> /// 标示方法内所有数据库操作加入事务控制 /// </summary> [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public sealed class TransactionMethodAttribute : Attribute { /// <summary> /// 标示方法内所有数据库操作加入事务控制 /// </summary> public TransactionMethodAttribute() { } }代码说明:
1. 在上面两篇文章中都是把IContextProperty, IContributeObjectSink单独继承并实现的,其实我们发现ContextAttribute已经继承了IContextProperty,所有这里我仅仅只需要再继承一下IContributeObjectSink就行了。关于这两个接口的说明,上面文章中都有详细的说明。
2. TransactionAop将在后面给出。
3. 需要注意的是两个Attribute需要一起用,并且我发现Attribute如果标记在类上他会被显示的实例化,但是放在方法上就不会,打断点可以跟踪到这一过程,要不然我也不会费力气弄两个来标注了。
TransactionAop.cs
public sealed class TransactionAop : IMessageSink { private IMessageSink nextSink; //保存下一个接收器 /// <summary> /// 构造函数 /// </summary> /// <param name="next">接收器</param> public TransactionAop(IMessageSink nextSink) { this.nextSink = nextSink; } /// <summary> /// IMessageSink接口方法,用于异步处理,我们不实现异步处理,所以简单返回null, /// 不管是同步还是异步,这个方法都需要定义 /// </summary> /// <param name="msg"></param> /// <param name="replySink"></param> /// <returns></returns> public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink) { return null; } /// <summary> /// 下一个接收器 /// </summary> public IMessageSink NextSink { get { return nextSink; } } /// <summary> /// /// </summary> /// <param name="msg"></param> /// <returns></returns> public IMessage SyncProcessMessage(IMessage msg) { IMessage retMsg = null; IMethodCallMessage call = msg as IMethodCallMessage; if (call == null || (Attribute.GetCustomAttribute(call.MethodBase, typeof(TransactionMethodAttribute))) == null) retMsg = nextSink.SyncProcessMessage(msg); else { //此处换成自己的数据库连接 using (SqlConnection Connect = new SqlConnection(Configurations.SQLSERVER_CONNECTION_STRING)) { Connect.Open(); SqlTransaction SqlTrans = Connect.BeginTransaction(); //讲存储存储在上下文 CallContext.SetData(TransactionAop.ContextName, SqlTrans); //传递消息给下一个接收器 - > 就是指执行你自己的方法 retMsg = nextSink.SyncProcessMessage(msg); if (SqlTrans != null) { IMethodReturnMessage methodReturn = retMsg as IMethodReturnMessage; Exception except = methodReturn.Exception; if (except != null) { SqlTrans.Rollback(); //可以做日志及其他处理 } else { SqlTrans.Commit(); } SqlTrans.Dispose(); SqlTrans = null; } } } return retMsg; } /// <summary> /// 用于提取、存储SqlTransaction /// </summary> public static string ContextName { get { return "TransactionAop"; } } }代码说明:
1. IMessageSink MSDN:定义消息接收器的接口。
2. 主要关注SyncProcessMessage方法内的代码,在这里创建事务,并存储在上下文中间,还记得上面SqlDAL的SqlTrans属性么,里面就是从上下文中取得的。
3. 请注意了,这里能捕捉到错误,但是没有办法处理错误,所以错误会继续往外抛,但是事务的完整性我们实现了。你可以在Global.asax可以做全局处理,也可以手动的try一下,但是我们不需要管理事务了,仅仅当普通的错误来处理了。
结束
大家可以看到,在被标注的方法里面所有的数据库操作都会被事务管理起来,也算是了了我心愿,貌似我的Attribute做权限又看到了一丝希望了,欢迎大家多提意见:)
补充(2009-1-8)
关于在评论中提到的性能的问题,如果要使用AOP的方式来实现事务肯定比直接try catch 然后Commit 和 Rollback效率要低的,但是很明显可维护性、使用方便性要高得多的,所以看个人需求了。这里补充的是关于SqlDAL继承ContextBoundObject的问题,以下是想到的解决办法:
1. 最简单、修改UserInfoAction最少的办法:把SqlDAL复制一份改下类名,继承一下ContextBoundObject,然后把继承类改一下。很不推荐: (
2. 从一开始就不使用继承方法来访问数据层的方法,而是将SqlDAL改成一个普通类,通过声明一个SqlDAL方式来访问数据层:
private SqlDAL _sqlDao; public SqlDAL SqlDao { get { if (_sqlDao == null) { _sqlDao = new SqlDAL(); object obj = CallContext.GetData(TransactionAop.ContextName); if (obj != null && obj is SqlTransaction) _sqlDao.SqlTrans = obj as SqlTransaction; } return _sqlDao; } }
这样相对于没有加事务类仅仅多一个取值过程和判断过程,效率应该还是比继承SqlDAL直接继承ContextBoundObject好很多。 个人感觉还是不是很好,继续探索,已经想到了减少一个Attribute的办法了,感谢欢迎大家提建议 :) 原文:http://www.cnblogs.com/over140/archive/2009/01/07/1371307.html
使用ContextBoundObject实现AOP总结:
1、要定义两个特性,一个是标记是用来标记要AOP的类(TransactionAttribute ),一个特性是用来标记要AOP的方法(TransactionMethodAttribute )。定义标记要AOP类要继承接口:ContextAttribute, IContributeObjectSink,定义用来标记要AOP的方法要继承接口Attribute,这里只是一个标示作用。在定义标记要AOP类(TransactionAttribute )要定义一个方法GetObjectSink,返回一个继承接口IMessageSink类(TransactionAop)的实例。
2、要定义一个继承接口IMessageSink类(TransactionAop),这个类就是要实现AOP是核心代码,你可以在执行真正代码之前或之后加入一些自己的代码,比如日志记录,缓存,事务等等。
3、在要使用的类必须满足以下条件:
1、继承类ContextBoundObject
2、类和方法都要分别加上我们之前定义的相应的特性(TransactionAttribute ,TransactionMethodAttribute)。