AOP 实现积分服务
前言:
AOP(Aspect Oriented Programming)的是面向方面编程,如您不了解可搜索之。AOP目的是将系统按照功能进行横向切分,被切分下来的功能也就是面向的方面,例如系统 的日志处理、安全、事物等,ASP.NET MVC中的Filters就是AOP的思想实现。AOP带来的好处是什么呢?AOP是面向对象设计原则中的 单一职责(SAP)的体现,可以有效降低各个模块间的耦合度,使整个系统健康有效的抵御各种需求变化。
本文介绍的积分服务是在某团购网站中的一个模块,需求并不复杂,如下:
- 在一些功能点上对用户的积分进行变更。如:用户注册时给用户增加积分、用户交易成功时给用户增加积分、用户退团时给用户减少积分。
- 功能点与具体积分分值是可配置的。因为积分会根据产品的生命周期进行调整的,因此要能有效快速的控制积分策略。
Spring.net 是应用程序开发框架,提供了一整套在应用程序开发中的解决方案,如作为基础的:IOC 、AOP 以及在其上扩展的数据访问层支持、WEB支持、服务支持 等等,如果要用AOP就必须使用IOC,也就是说所有的对象都要从Spring.net的IOC容器中获取,Spring.net才能得到控制权拦截切入 点实现AOP。
实现方案:
首先介绍本项目的架构及背景:
- 标准的三层架构,并且每一层做了接口抽象,每一层只依赖于抽象也就是接口。
- WEB端使用了ASP.NET MVC,由于MVC的特性,基本上每个功能点对应一个方法,这样的粒度有利于功能点与积分项的绑定。
- 使用Spring.net 做IOC容器,对各个层的依赖关系做注入。
有了以上先决条件后使用Spring.net AOP实现 积分服务需要进行以下几个步骤:
- 创建积分服务类
积分服务类职责是封装与积分相关的处理逻辑,包括积分变更、查询积分历史等。我们这里对其做了抽象
public interface IScoreService { void Record(SiteUser user,int scoreValue, ScoreEvent scoreEvent,string data); }
实现类:
public class ScoreService:IScoreService { public ISiteUserDao SiteUserDao { get; private set; } public IScoreItemDao ScoreItemDao { get; set; } public ScoreService(ISiteUserDao siteUserDao, IScoreItemDao scoreItemDao) { SiteUserDao = siteUserDao; ScoreItemDao = scoreItemDao; } [Transaction] public void Record(SiteUser user, int scoreValue, ScoreEvent scoreEvent,string data) { if (user == null) throw new ArgumentNullException("user"); user.Score += scoreValue; if (user.Score < 0) throw new Exception("积分不足"); //todo: var si = new ScoreItem { ForUser = user, HappeningTime = DateTime.Now.ChinaTime(), Score = scoreValue, ScoreEvent = scoreEvent,Data=data}; ScoreItemDao.Insert(si);SiteUserDao.Update(user); }
其中SiteUserDao 与 ScoreItemDao 是数据访问层接口,具体实现类使用Spring.net在进行运行时注入进来。可能有人关注到了[Transaction] 这个Attribute,它是Spring.net对分布式事务的支持,详细请参考:http://www.springframework.net/doc-latest/reference/html/transaction.html。
- 创建积分策略实体类
积分策略类是一个实体类,它指明了功能点与积分的映射关系。在本项目中将积分策略直接配置到了IOC容器中,当然根据需求的变化也可以将其推入数据库存储。
/// <summary> /// 积分策略 /// </summary> public class ScorePolicy {/// <summary> /// 策略名称 /// </summary> public string PolicyName { get; set; }/// <summary> /// 积分事件 /// </summary> public ScoreEvent ScoreEvent{get;set;} /// <summary> /// 分值 /// </summary> public int ScoreValue { get; set; } //获取用户id表达式 note:spring.net expresstion public string GetUserIdExpr { get; set; } //产生附加数据表达式 note:spring.net expresstion public string GenDataExpr { get; set; } }
需要解释的是后两个成员 GetUserIdExpr 与GenDataExpr ,这2个成员使用了Spring.net的表达式,详细请参考:http://www.springframework.net/doc-latest/reference/html/expressions.html,这2个成员的用途与业务逻辑无关,是支撑性成员,稍后会在通知类中看到它们。
- 选择切入点
为Spring.net指定某种方式来拦截执行流。Spring.net支持静态切入点与动态切入点,在静态切入中包含正则表达式切入点与特性切入点,详细请参考:http://www.springframework.net/doc-latest/reference/html/aop.html#aop-pointcuts,本项目选择了使用特性切入点,也就是利用.net 的Attribute特性,显式的在被拦截的方法前指定,以便于Spring.net AOP对其敏感。
public class ScoreAttribute:Attribute { /// <summary> /// 策略名,默认值为当前方法名 /// </summary> public string PolicyName { get; set; } }
可以看到这是一个标准的Attribute,包含一个成员:策略名称,可以在显式插入Attribute时指定一个策略实体与之对应。
- 创建通知类。
当Spring.net拦截到指定的执行流中的消息后将按照指定的方式建立通知。Srping.net AOP支持4种方式通知,包括:
-
- 环绕通知
- 前置通知
- 异常通知
- 后置通知
详细请参考:http://www.springframework.net/doc-latest/reference/html/aop.html#aop-advice-types。本项目选择后置通知方式对积分处理,也就是在被拦截方法之后产生通知,如下:
public class ScoreAfterAdvice :Spring.Aop.IAfterReturningAdvice { public IScoreService ScoreService { get; set; } /// <summary> /// 积分策略表, /// </summary> private static IDictionary<string , ScorePolicy> _policies; public ScoreAfterAdvice(IDictionary<string, ScorePolicy> policies) { _policies = policies; } public void AfterReturning(object returnValue, MethodInfo method, object[] args, object target) { var attr = method.GetCustomAttributes(typeof(ScoreAttribute), false).FirstOrDefault() as ScoreAttribute; if (attr == null) throw new NullReferenceException(); var keyname=string.IsNullOrEmpty(attr.PolicyName) ? method.Name : attr.PolicyName; if(!_policies.ContainsKey(keyname)) return; //todo:record log //加载积分策略 var policy = _policies[keyname]; try { //获取目标用户ID var user = ExpressionEvaluator.GetValue(args, policy.GetUserIdExpr); if (user != null) { var data = string.IsNullOrEmpty(policy.GenDataExpr) ? string.Empty : ExpressionEvaluator.GetValue(args, policy.GenDataExpr); //处理积分 ScoreService.Record((SiteUser) user, policy.ScoreValue, policy.ScoreEvent,data.ToString() ); } } catch (Exception e) { //todo:log } } }
本类实现了String.Aop.IAfterReturningAdvice类,在产生通知时会调用 AfterReturning方法,其中有几个关键参数:
-
- returnValue, 被拦截方法的返回值,不能修改。
- method,被调用的方法。
- args ,被调用方法参数
代码并不复杂,可以看到如何利用这些参数进行协作的。
- 配置(集成)
万事俱备只欠东风,我们需要将之前的组成部分进行集成,让其运转起来。这个集成主要通过Spring.net配置来完成,其片段如下:
<aop:config > <aop:advisor advice-ref="ScoreAfterAdvice" pointcut-ref="ScoreAttributePointcut"/> </aop:config> <object id="ScoreAfterAdvice" type="GroupPurchase.Services.Score.Handler.ScoreAfterAdvice,GroupPurchase.Services"> <description>积分 afteradvice</description> <constructor-arg name="policies"> <dictionary key-type="string" value-type="GroupPurchase.Services.Score.Handler.ScorePolicy,GroupPurchase.Services"> <entry key="CreateSiteUser"><!--注册用户--> <object type="GroupPurchase.Services.Score.Handler.ScorePolicy,GroupPurchase.Services"> <property name="GetUserIdExpr" value="[0]"/> <property name="GenDataExpr" value=""/> <property name="ScoreValue" value="10"/> <property name="ScoreEvent" value="1"/> </object> </entry> </dictionary> </constructor-arg> <property name="ScoreService" ref="ScoreService"/> </object> <object id="ScoreAttributePointcut" type="Spring.Aop.Support.AttributeMatchMethodPointcut"> <property name="Attribute" value="GroupPurchase.Services.Score.Handler.ScoreAttribute,GroupPurchase.Services" /> </object>
可以看到我们定义了一个积分策略,在用户注册成功后将为用户增加10个积分,为ScoreAfterAdvice类注入一个积分策略字典与积分服务;另外配置了一个advisor 就是切入点和通知的映射关系。细节看代码,不做详细解释了。
- 使用
经过以上几步的折腾,我们算是搭建完毕了积分系统,如何使用呢? 分两步:
- 根据产品需求,对积分策略进行配置,参照上一步。
- 在对应的功能点插入Attribute,例如在服务层中的用户注册的代码片段:
[Notify(EventId = "AccountRegistered")] [Score] [Transaction] public void CreateSiteUser(SiteUser user) { SiteUserDao.Insert(user); }
结尾:
除了积分服务外,这个项目的通知服务也是用AOP方式实现的,总的来说AOP为我们带来了一定的好处,但目前注入对框架依赖性较强,如果我将Spring.net换成EntLib势必有部分代码要重写。欢迎各位朋友批评指正,共同进步:)