谈AOP要step by step
这篇文章的由来是我看了国外的一篇博客后觉得不错也就加了自己的理解后翻译了下来
我需要一个简单易用的拦截机制来应对一些要用AOP技术的需求 。当然,现在已经有一些拦截器了,不过大部分都是在运行时通过IL语言,来emit动态的子类,并且最终关于你可以拦截的类的限制几乎是一样的:非静态,必须是non-sealed,属性和方法必须是virtual 等等
其它的拦截机制需要改变生成过程,或者你得买一个license,我就提供不了。。。
目录
介绍
AOP,面向切面编程。我猜你们都很熟悉OP,那么我将要阐明的是“切面”的意思,不是“切糕”,呵呵。
我将保持这篇文章的难度在初学者水平。不过你需要有面向对象编程的知识,这也是唯一的要求。
高级开发人员看这!
如果你很熟悉AOP,也请不要离开哦!
我将会介绍一个拦截技术,它可以拦截到以下这些:
- 任何类(包括sealed ,static,value 类型)
- 构造函数
- 类型初始化器
- 实例方法,属性和事件(即使不是virtual类型的)
- 静态方法,属性和事件
不包括的:
- 对你的代码或程序集进行织入
- 反射发出IL代码
- 使用任何动态生成的东西
- 修改目标类
- 强制weavers实现功能(如MarshalByRef)
基本上,我们讨论的是一个纯托管代码技术,运行环境是.net 1.1(不过我这里用到了linq,你也可以改一改)你可以拦截几乎你能想到得任何点。
让我们更清楚点你要用到得技术:
你可以拦截如System.DateTime.Now 或者System.IO.File操作这些东西,也不会遇到那些流行的拦截库所会有的限制。
你是否有所怀疑了?那就继续看下去。
AOP的原则
背景
一些人也许会想他们还没有把面向对象编程的旅程走完,为什么要从OOP转到AOP并且放弃他们多年来所学到的概念。回答很简单:这里不存在转换,不存在OOP VS AOP!AOP是这些概念中的一个,在我看来,只是名字有些误导。拥抱AOP原则让你解决类,对象,接口,继承,多态,抽象等等的问题,所以即使你仍然完全专注于OOP,AOP也不会让你失去什么。
当你在你的代码中使用AOP,你试图分散一个特定的最少OOP原则封装的项目以横切关注
在过去的日子里,当internet还是黄页,公告板和新闻组的时候,如果你想学习什么你会更倾向于书籍(书就是充满各种条条框框的东西)。所有这些书大约都会提醒你下面的OOP要点
准则1:你必须封装所有的数据和代码
准则2:永远别违反准则1
封装在第三代语言(3GL)中已经成为了介绍OOP概念的最终目标
维基百科:
封装是指一种将抽象性函数接口的实现细节部份包装、隐藏起来的方法。同时,它也是一种防止外界呼叫端,去存取对象内部实现细节的手段,这个手段是由编程语言本身来提供的。
关于AOP的益处,一些场景…
场景A
你是银行里的一个软件开发人员,银行有一个很不错的工作操作系统。并且业务运行的也很不错,此时政府发布一个政策,强制银行要透明公开一些东西。比如钱的进出需要被记录。政府公开说这是首次针对透明化的措施,以后会有更多。
场景B
你的web应用程序已经提交了released版本给测试团队。所有的功能测试都通过了,不过挂在了负载测试上,这是一个非功能性的需求,任何页面在服务端的处理必须小于500ms。分析得到的结果是要通过缓存结果来避免多次的对数据库进行查询。
场景C
你花了2年多的时间建立了自己的领域模型,包含了一个拥有200+类的完美的库。最近你被告知有个家伙将会写新的前端应用程序并且那家伙需要绑定你的对象到UI上。为了简化这个任务,你所有的类都需要实现INotifyPropertyChanged
Cross-cutting concerns 横切关注点
“有时”指的是哪时呢?
一些(银行类,数据处理服务,领域模型类等…)以指定的功能设计的类,不得不被一些不是基于他们自己的业务的需求来要求修改的时候。
1.银行类的目的是钱的交易,日志是政府所关注的。
2.数据服务类的目的是检索数据,数据缓存是非功能性的需求。
3.领域模型类的目的是处理你公司的业务,属性改变的通知则是UI所关注的。
无论何时你得为了满足这些不同类的一个“额外”功能,不得不写一些外部代码。用AOP的话来说就是你需要一个横切关注点。
横切关注点是AOP的核心,No cross-cutting concern =no need for AOP
为什么我们需要横切关注点呢?
再回到场景C
假如你每个类平均有5个属性,那么200+的类你必须复制/粘贴1000+次,这是让人无法忍受的,就如下面的例子
public class Customer { public string Name { get; set; } }
改变成下面这样
public class Customer : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private string _Name; public string Name { get { return _Name; } set { if (value != _Name) { _Name = value; SignalPropertyChanged("Name"); } } } void SignalPropertyChanged(string propertyName) { var pcEvent = this.PropertyChanged; if (pcEvent != null) pcEvent(this, new PropertyChangedEventArgs(propertyName)); } }
额,这还只是一个属性!顺便说一声,知道那实习生为啥2天前走了么?哈哈。
更进一步说
将操作方法和封装的数据(方法,属性,事件等。。。)绑定(不是和实际的实现绑定)换句话说,对领域模型里的类(例如:Customer)实现横切INotifyPropertyChanged
关注,但是要对Customer类几乎无影响。
如果想要达到这些,就必须
- 对关注点有适当的分离
- 避免代码的重复并且要是代码易于维护
- 避免被大堆的样板式代码隐藏领域模型类的核心业务
不错,想的非常好,那么该怎么做呢?
理论答案
我们要有一个“横切关注点”(cross-cutting concern),而它也需要一些代码以便在一些类中执行。(这里就是入口点)
那些代码实现(例如实现日志功能,缓存等等)在AOP中叫做关注点(concern)
然后我们可以附加(注入,引入…你可以选择自己喜欢的叫法)关注点(concern)(这真的非常重要,concern 就是cross-cutting concern 的实现)到任何被选择的地方。
而下面这些地方就是我们可以实现注入concern的地方
- 静态初始化方法
- 构造方法
- 静态的属性的getters 和setters
- 实例属性的getters 和setters
- 静态方法
- 实例方法
- 析构方法
在一个完美的AOP世界中,我们应该能够将concern附加到目标的任何一行代码中。
愿望是美好的,但是如果我们想附加concern那我们就需要在目标处有一个钩子,你认为呢?
在AOP中钩子的概念叫做切入点(pointcut)。而实际中你的代码所执行附加操作的地方也有个名字,叫做连接点(joinpoint)
清楚了么?也许没有,这里有些伪代码希望你可以明白。
//目标类 class BankAccount { public string AccountNumber {get;} public int Balance {get; set;} void Withdraw(int AmountToWithdraw) { :public pointcut1; // a pointcut (想象一下,就好像是一个goto语句用的标签 ) Balance -= AmountToWithdraw; } } // 关注点 Concern concern LoggingConcern { void LogWithdraw(int AmountToWithdraw) { // 这里你必须想象一些奇迹发生了 // 'this'是一个 BankAccount类的实例. Console.WriteLine(this.AccountNumber + " withdrawal on-going..."); } } class Program { void Main() { // 通过反射我们获取pointcut的引用 pointcut = typeof(Bank).GetPointcut("pointcut1"); // 这就是连接点 joinpoint LoggingConcern.Join(cutpoint, LogWithdraw); // 在连接完后运行时将会有一个记录将会告诉BankAccount类在切入点 pointcut1执行我们的关注 //点(LoggingConcern) } }
一些概念
什么是切面Aspect?
它是concern、pointcut和joinpoint的结合。
比如:我有一个日志机制(concern),我将它的log方法在给定的地方执行了注册(joinpoint)到了我的应用程序的代码切入点(pointcut)处,这就叫做切面aspect
仔细思考下,这必须得十分清楚
Side effect
Side effect 是一种concern它不会改变切入点所在代码的行为,它只是引入额外的动作用来执行。比如日志功能(concern),回到上面的代码,当你执行Bank.Withdraw(int Amount)时候 LoggingConcern.LogWithdraw(int Amount) 将会被执行,而 Bank.Withdraw(int Amount) 也会继续执行。
Advice是一种可能改变方法的输入输出的concern。比如cache concern,当执行CustomerService.GetById(int Id)时候 CachingConcern.TryGetCustomerById(int Id)会被执行,并且如果在缓存中找到值则返回,否则才继续CustomerService.GetById(int Id) 方法的执行
Advices 可以做以下这些事:
- 检查目标切入点的输入参数,如果需要可以修改它们。
- 取消目标方法的执行,并且可以用不同的实现替代。
- 检查目标方法输出的结果并且修改或者替换它。
兄弟,你如果读到了这里,那么恭喜你,我们已经学习完了AOP的通用概念。现在让我们继续,看用c#怎样实现
我们如何实现的
展示代码
Concerns
Concern 应该有一个神奇的功能 this 属性指代我们的目标类型实例
public interface IConcern<T> { T This { get; } }
Pointcuts 引用
获取代码中单行的pointcut是很难的。但是我们可以很容易的从方法调用中获取,微软为我们提供了System.Reflection.MethodBase 类
Msdn: :Provides information about methods and constructors.
使用MethodBase获取pointcut的引用是非常强大的。
你可以获取构造函数、方法、属性和事件的pointcut引用,在.Net中几乎所有你在你的代码中定义的最终以花括号结尾的东西(除了字段)。
public class Customer { public event EventHandler<EventArgs> NameChanged; public string Name { get; private set; } public void ChangeName(string newName) { Name = newName; NameChanged(this, EventArgs.Empty); } } class Program { static void Main(string[] args) { var t = typeof(Customer); // 构造函数 var pointcut1 = t.GetConstructor(new Type[] { }); // ChangeName 方法 var pointcut2 = t.GetMethod("ChangeName"); // Name 属性 var nameProperty = t.GetProperty("Name"); var pointcut3 = nameProperty.GetGetMethod(); var pointcut4 = nameProperty.GetSetMethod(); // NameChanged 事件 var NameChangedEvent = t.GetEvent("NameChanged"); var pointcut5 = NameChangedEvent.GetRaiseMethod(); var pointcut6 = NameChangedEvent.GetAddMethod(); var pointcut7 = NameChangedEvent.GetRemoveMethod(); } }
Joinpoints
写连接方法是很容易的,看下面的函数申明
void Join(System.Reflection.MethodBase pointcutMethod, System.Reflection.MethodBase concernMethod);
我们可以为这个方法增加一种注册机制,我们可以想象一下
public class Customer { public string Name { get; set;} public void DoSomething () { System.Diagnostics.Trace.WriteLine(Name + " is doing is own business"); } } public class LoggingConcern : IConcern<Customer> { public Customer This { get; set; } public void DoSomething() { System.Diagnostics.Trace.WriteLine(This.Name + " is going to do is own business"); This.DoSomething (); System.Diagnostics.Trace.WriteLine(This.Name + " has finished doing its own business"); } } class Program { static void Main(string[] args)h { // 获取 Customer.DoSomething();的pointcut var pointcut1 = typeof(Customer).GetMethod("DoSomething"); var concernMethod = typeof(LoggingConcern).GetMethod("DoSomething"); // 连接它们 AOP.Registry.Join(pointcut1, concernMethod); } }
我们离我们要实现的还缺多少?告诉你已经不多了。。。
将所有的东西粘合到一起
这里的问题和乐趣将会开始变的多起来
先来一个简单的例子
registry注册机制
注册机制将会保存连接点的记录。它是一个单例集合(连接点项(joinpoints items))
Joinpoint 是一个简单的结构体。
public struct Joinpoint { internal MethodBase PointcutMethod; internal MethodBase ConcernMethod; private Joinpoint(MethodBase pointcutMethod, MethodBase concernMethod) { PointcutMethod = pointcutMethod; ConcernMethod = concernMethod; } // Utility method to create joinpoints public static Joinpoint Create(MethodBase pointcutMethod, MethodBase concernMethod) { return new Joinpoint (pointcutMethod, concernMethod); } }
没有什么太花哨的,它应该还要实现IEquatable<Joinpoint>,为了代码短点,我去掉了。
关于注册:我们这个类叫做AOP,以单例模式实现。它唯一暴露的是一个公共的静态属性(Registry)用来获取唯一实例。
public class AOP : List<Joinpoint> { static readonly AOP _registry; static AOP() { _registry = new AOP(); } private AOP() { } public static AOP Registry { get { return _registry; } } [MethodImpl(MethodImplOptions.Synchronized)] public void Join(MethodBase pointcutMethod, MethodBase concernMethod) { var joinPoint = Joinpoint.Create(pointcutMethod, concernMethod); if (!this.Contains(joinPoint)) this.Add(joinPoint); } }
通过AOP类我们可以像下面这样写:
AOP.Registry.Join(pointcut, concernMethod);
额,我们遇到问题了
我们遇到一个很明显的而且严重的问题,如果开发者按下面这样写
var customer = new Customer {Name="test"}; customer.DoYourOwnBusiness();
我们的注册机制没有被使用过,所以LoggingConcern.DoSomething() 是不会被执行的
我们的问题是.net没有提供给我们一个简单的方法去拦截这样的调用
这里也没有现成的方法,所以有一些工作我们是必须得做的。
我们需要驱动AOP这个类,当然这篇文章不讨论那些拦截技术,不过得注意到,在所有的AOP实现之间拦截模型是关键不同点。
在SharpCrafters 站上有2种主流技术的介绍
- Compile-time weaving
- Run-time weaving
我们的拦截机制: Proxying
如何拦截调用的方法是没有什么秘密的,你可以有如下3种选择
创造你自己的语言和编译器来生成.net程序集,那么在编译的时候你可以随意的注入你想要的。
实现一个解决方案修改程序集的运行时行为
给你的客户端一个代理并在当你marshaling一个目标对象时候用拦截类拦截方法调用
如果你需要在任何代码行设置切入点,那么前2个方案就有一点过分设计了。
我们将实现第三种方案,需要注意的是使用代理技术,这里将会带来一个好消息和一个坏消息:
先讲讲坏消息吧,你的目标对象在运行时必须被代理对象实例置换。意味着如果你想拦截一个构造函数,那么你就得将目标类实例的构造函数委托给一个工厂,如果你已经有了目标类的实例那么你得明确的请求置换。相对于控制反转和依赖注入创建委托对象不是一个问题。对于其他想完全的使用我们的拦截技术的人来说意味着他们必须得使用一个工厂。别急,下面我们会实现这个工厂。
好消息来了,那个代理已经有现成的了。
在我看来它的类名无法体现它的用途。我们需要的不是代理,而是一个拦截器。不过不管怎样它会提供给我们一个代理(通过调用GetTransparentProxy())这也是我们唯一需要的。
public class Interceptor : RealProxy, IRemotingTypeInfo { object theTarget { get; set; } public Interceptor(object target) : base(typeof(MarshalByRefObject)) { theTarget = target; } public override System.Runtime.Remoting.Messaging.IMessage Invoke(System.Runtime.Remoting.Messaging.IMessage msg) { IMethodCallMessage methodMessage = (IMethodCallMessage) msg; MethodBase method = methodMessage.MethodBase; object[] arguments = methodMessage.Args; object returnValue = null; // TODO: // here goes the implementation details for method swapping in case the AOP.Registry // has an existing joinpoint for the MethodBase which is hold in the "method" variable... // if the Registry has no joinpoint then simply search for the corresponding method // on the "theTarget" object and simply invoke it... return new ReturnMessage(returnValue, methodMessage.Args, methodMessage.ArgCount, methodMessage.LogicalCallContext, methodMessage); } #region IRemotingTypeInfo public string TypeName { get; set; } public bool CanCastTo(Type fromType, object o) { return true; } #endregion }
RealProxy类存在的目的是拦截远程对象的方法调用并封送目标对象。这里远程的意思是:其他应用程序(应用程序域,服务端…等等)里的对象,这里不深度讨论,不过在.net Remoting 中有2种方法封送对象:引用和值。基本上只要对象是继承了MarshalByRef 或者实现了ISerializable接口的你都可以封送。这里我们计划不使用远程的功能但是我们会让RealProxy类支持远程。所以我们将typeof(MarshalByRef) 传给了基类 RealProxy的构造函数。
RealProxy类通过System.Runtime.Remoting.Messaging.IMessage Invoke(System.Runtime.Remoting.Messaging.IMessage msg) 方法来接收所有在代理上产生的调用。那也是我们将要详细实现置换的地方。
关于IRemotingTypeInfo的实现:在一个真实的远程环境中,客户端向服务端请求一个对象。客户端应用程序运行时可能不知道封送的远程对象的类型。所以当客户端调用public object GetTransparentProxy()时候,运行时必须决定返回的对象是否有可装箱拆箱的能力相对于客户端的约束。通过实现 IRemotingTypeInfo 你就给了客户端运行时一个暗示,告诉客户端是否可以转换成指定的类型。
public bool CanCastTo(Type fromType, object o) { return true ; } |
我们所有AOP的实现只有在远程提供了“return true ”这2个字后才能成为可能。通过这一点,我们可以转换任何 GetTransparentProxy() 返回的对象成任何接口而不需运行时的检查。
运行时只会很纯粹的给我们一张“yes card”
在这一点上,我们为目标实例量身定制了一个比较好的拦截机制。我们仍然还没有构造函数的拦截方法,和代理的创建。当然那是工厂的工作
The Factory
废话不多说,下面是工厂的大体架构
public static class Factory { public static object Create<T>(params object[] constructorArgs) { T target; // TODO: // Base on typeof(T) and the list of constructorArgs (count and their Type) // we can ask our Registry if it has a constructor method joinpoint and invoke it // if the Registry has no constructor joinpoint then simply search for the corresponding one // on typeof(T) and invoke it... // Assign the result of construction to the "target" variable // and pass it to the GetProxy method. return GetProxyFor<T>(target); } public static object GetProxyFor<T>(object target = null) { // Here we are asked to intercept calls on an existing object instance (Maybe we constructed it but not necessarily) // Simply create the interceptor and return the transparent proxy return new Interceptor(target).GetTransparentProxy(); } }
注意工厂类总是返回一个Object类型的对象,我们不能返回一个T类型的对象,是因为代理不是T类型的,而是System.Runtime.Remoting.Proxies.__TransparentProxy不过别忘记“yes card”,我们可以转换任何返回的对象到任何接口而无需运行时的检查。我们将工厂类嵌入AOP 类以便使我们编程容易点。
使用
拦截方法和属性
首先你需要一个模型(用来被关注。。)
public interface IActor { string Name { get; set; } void Act(); } public class Actor : IActor { public string Name { get; set; } public void Act() { Console.WriteLine("My name is '{0}'. I am such a good actor!", Name); } }
然后我们需要一个concern
public class TheConcern : IConcern<Actor> { public Actor This { get; set; } public string Name { set { This.Name = value + ". Hi, " + value + " you've been hacked"; } } public void Act() { This.Act(); Console.WriteLine("You think so...!"); } }
在程序初始化的时候我们在joinpoints实现注册
// Weave the Name property setter AOP.Registry.Join ( typeof(Actor).GetProperty("Name").GetSetMethod(), typeof(TheConcern).GetProperty("Name").GetSetMethod() ); // Weave the Act method AOP.Registry.Join ( typeof(Actor).GetMethod("Act"), typeof(TheConcern).GetMethod("Act") );
最后我们可以通过工厂创建对象
var actor1 = (IActor) AOP.Factory.Create<Actor>(); actor1.Name = "the Dude"; actor1.Act();
结果
My name is 'the Dude. Hi, the Dude you've been hacked'. I am such a good actor! You think so...!
拦截 File.ReadAllText(string path)
这里有2个小问题
File类是静态的
没有实现任何接口
还记得“yes card”吗?这里没有类型检查
意味着,我们可以创建一个任意的接口,什么都不需要实现它,只是作为一个约束
下面我们就创建一个
public interface IFile { string[] ReadAllLines(string path); }
concern
public class TheConcern { public static string[] ReadAllLines(string path) { return File.ReadAllLines(path).Select(x => x + " hacked...").ToArray(); } }
注册
AOP.Registry.Join ( typeof(File).GetMethods().Where(x => x.Name == "ReadAllLines" && x.GetParameters().Count() == 1).First(), typeof(TheConcern).GetMethod("ReadAllLines") );
最终的执行
var path = Path.Combine(Environment.CurrentDirectory, "Examples", "data.txt"); var file = (IFile) AOP.Factory.Create(typeof(File)); foreach (string s in file.ReadAllLines(path)) Console.WriteLine(s);
在这里我们注意到,我们不能用Factory.Create<T>
源码下载 解压密码:www.inetfans.com