代码改变世界

使用Spring.net AOP 实现积分服务

2011-06-21 17:01  ZQ  阅读(3977)  评论(14编辑  收藏  举报

前言:


 

        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 就是切入点和通知的映射关系。细节看代码,不做详细解释了。

  • 使用

经过以上几步的折腾,我们算是搭建完毕了积分系统,如何使用呢? 分两步:

  1. 根据产品需求,对积分策略进行配置,参照上一步。
  2. 在对应的功能点插入Attribute,例如在服务层中的用户注册的代码片段:
        [Notify(EventId = "AccountRegistered")]
[Score]
[Transaction]
public void CreateSiteUser(SiteUser user)
{
SiteUserDao.Insert(user);
}

结尾:


 

除了积分服务外,这个项目的通知服务也是用AOP方式实现的,总的来说AOP为我们带来了一定的好处,但目前注入对框架依赖性较强,如果我将Spring.net换成EntLib势必有部分代码要重写。欢迎各位朋友批评指正,共同进步:)