使用Spring.net

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

  • 使用

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

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

[Score]

[Transaction]

public void CreateSiteUser(SiteUser user)

{

SiteUserDao.Insert(user);

}

结尾:


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

你可能感兴趣的:(spring)