ROO中的*.aj文件

问题提出

当我们用ROO命令生成Entity或者Controller的时候,系统会自动生成若干个*.aj文件,这些是什么文件呢?为什么ROO会采取这样的技术?如何使用?

AOP

ROO的文档Application Architecture中有对*.aj文件进行简单的介绍和使用说明:

AspectJ is a powerful and mature aspect oriented programming (AOP) framework that underpins many large-scale systems.

有关AOP的介绍可以看这里,注意核心关注点横切关注点的概念:

举个例子来说,一个信用卡处理系统的核心关注点是借贷/存入处理,而系统级的关注点则是日志、事务完整性、授权、安全及性能问题等,许多关注点——即横切关注点(crosscutting concerns)——会在多个模块中出现。 对于电子商务系统而言,每个需要权限验证的方法都是一个单独的join point。由于权限验证将在每个方法执行前执行,所以对于这一系列join point,只需要定义一个point cut。当系统执行到join point处时,将根据定义去查找对应的point cut,然后执行这个横切关注点需要实现的逻辑,即advice。而point cut和advice,就组合成了一个权限管理aspect。

实例图如下所示:

实例图 t

AspectJ

知道了AOP的概念后,再来认识一下AspectJ。首先,一个AOP的实现框架,需要满足如下几个技术特性:

  1. join point(连接点):一个抽象的概念,即可以让point cut切进来的地方,通常跟AOP框架的实现有关,例如某个类的某个方法。
  2. point cut(切入点):被横切的连接点,用来捕获方法调用等事件。
  3. advice(通知):执行的具体逻辑。
  4. aspect(方面):类似于一个操作的集合体,声明了多个point cut以及该point cut触发后调用的advice。
  5. introduce(引入):又称为mixin。为对象引入附加属性或方法的特性。

AspectJ作为AOP一个经典的实现,它是如何体现上述特性的呢?先来看一段示例代码

class Bank
{
public float deposit(AccountInfo account, float money)
{
  // 验证account是否为合法用户
  // Begin Transaction
  // 增加account账户的钱数,返回账户里当前的钱数
  // End Transaction
}
public float withdraw(AccountInfo account, float money)
{
  // 验证account是否为合法用户
  // Begin Transaction
  // 减少account账户的钱数,返回取出的钱数
  // End Transaction
}
};

注意到代码中的注释部分,可以看到在业务流程中夹杂着重复的验证、事务开启和关闭操作。随着业务的复杂度增加,该类型的“横切”代码将会遍布整个项目,使模块难以修改。 结合前面对AOP的特性的介绍,可以观察到示例代码中有两个joint point,分别是deposit()withdraw()两个方法。两个aspect,分别负责验证和事务管理。利用AspectJ,可以如下处理:

aspect AuthAspect
{
  pointcut bankMethods() : execution (* Bank.deposit(…)) || execution (* Bank. withdraw (…));
  Object around(): bankMethods()
  {
    // 验证account是否为合法用户
    return proceed();
  }
};

和:

aspect TransactionAspect
{
  pointcut bankMethods() : execution(* Bank.deposit(…)) || execution (* Bank. withdraw (…));
  Object around(): bankMethods()
  {
    // Begin Transaction
    Object result = proceed();
    // End Transaction
    return result;
  }
};

如上面的代码所示,定义了AuthAspectTransactionAspect两个切面。

其中pointcut bankMethods() : execution (* Bank.deposit(…)) || execution (* Bank. withdraw (…));一句声明了point cut,即符合* Bank.deposit(…)) || execution (* Bank. withdraw (…)条件的方法在执行时,触发advice。Object around():一句定义了point cut的类型为around类型。在advice的结束时检查账户的合法性,然后返回proceed()对象,返回核心关注点继续执行。

再看一个例子,来自这里

aspect FaultHandler {

   private boolean Server.disabled = false;

   private void reportFault() {
     System.out.println("Failure! Please fix it.");
   }

   public static void fixServer(Server s) {
     s.disabled = false;
   }

   pointcut services(Server s): target(s) && call(public * *(...));

   before(Server s): services(s) {
     if (s.disabled) throw new DisabledException();
   }

   after(Server s) throwing (FaultException e): services(s) {
     s.disabled = true;
     reportFault();
   }
 }

上述例子中,有:

  1. 一个 inter-type field:private boolean Server.disabled = false;
  2. 两个methods:reportFault()fixServer(Server s)
  3. 一个point cut定义:pointcut services(Server s): target(s) && call(public * *(..));
  4. 两个advice:before(Server s): services(s)after(Server s) throwing (FaultException e): services(s)

下面就每个关键词进行解析:

inter-type field

例子中,private boolean Server.disabled = false;为Server类添加了一个私有的属性disable,并且初始值为false。与之类似,如果你想为Server添加doSomething()方法,可以这样写:

public void Server.doSomething() {
    ....
}

那么编译器就会在编译时为Server添加doSomething()方法,静态织入完成。

reportFault()和fixServer(Server s)

此Aspect中定义了两个方法,与普通Java方法无异。

pointcut

pointcut时刻监测着某些(符合表达式的)的joint point,例如方法,构造函数,异常处理,属性的访问和赋值等等。

如例子中的:

pointcut services(Server s): target(s) && call(public * *(...))

声明了一个名为services的pointcut,它会根据冒号(:)后面的表达式是否为真,来判断该pointcut是否触发。这个pointcut负责监测任意函数值任意函数名的Server类的public方法,其中target(s)的意思是被织入的对象为Server类(在冒号左边声明了)。

advice

advice的调用有一个很抽象的描述:

A piece of advice brings together a pointcut and a body of code to define aspect implementation that runs at join points picked out by the pointcut.

大意是advice是由pointcut和一段代码共同定义的在pointcut被join points触发时执行的逻辑

例子中,before(Server s): services(s)的意思是在join point触发前执行这一段advice,并且Server类作为参数s传入advice之中。after(Server s) throwing (FaultException e): services(s)的意思是在join point触发后执行这一段advice,并且可能抛出FaultException。

这里有一个Pointcuts表达式中函数的列表,其中有:

target(Type or Id)
every join point when the target executing object is an instance of Type or Id’s type

除了before()after()之外,还有around()around()的意思如下所示

around advice traps the execution of the join point; it runs instead of the join point. The original action associated with the join point can be invoked through the special proceed call

也就是说,around类型的advice中的代码将会整体替换掉原逻辑代码,但可以通过调用proceed()方法来执行原来的逻辑代码。因为around advice的特性可以使业务动态地执行或者不执行,所以它适合用在需要判断条件的横切关注点上,例如用户权限验证功能。

ROO中的AspectJ

资料来自Spring Roo in Action。当我们在ROO shell中输入

entity jpa --class ~.model.Course
field string --fieldName name

时,ROO为我们新建了一系列的文件:

  1. Course.java
  2. Course_Roo_Configurable.aj
  3. Course_Roo_ToString.aj
  4. Course_Roo_Jpa_Entity.aj
  5. Course_Roo_JavaBean.aj
  6. Course_Roo_Jpa_ActiveRecord.aj

打开Course.java文件,可以看到代码如下:

@RooJavaBean
@RooToString
@RooJpaActiveRecord
public class Course {
}

注意到Course上方的三个注解。其中@RooJavaBean对应Course_Roo_JavaBean.aj切面文件,@RooToString对应Course_Roo_ToString.aj@RooJpaActiveRecord对应Course_Roo_Configurable.ajCourse_Roo_Jpa_Entity.ajCourse_Roo_Jpa_ActiveRecord.aj。我们可以不通过Roo Shell,手动地添加entity,然后按需要添加上述注解,Roo会自动地根据注解内容为我们添加或删除对应的aj文件。

总体架构如下所示:

架构

可以看出:

  1. Course_Roo_ToString.aj定义了entity的toString()方法。
  2. Course_Roo_Configurable.aj为entity添加了@Configurable标签(请看这里)。
  3. Course_Roo_Jpa_Entity.aj为entity提供了JPA支持。
  4. Course_Roo_JavaBean.aj定义了一系列的getter和setter方法。
  5. Course_Roo_Jpa_ActiveRecord.aj包装了一系列的CRUD方法。

ROO通过将与Entity相关的不同职能的方法封装在不同的aj文件中,在编码时帮助程序员实时管理aj文件,最后通过AspectJ在编译时织入,组成目标class文件。这样子有利于程序员吧注意力集中在核心业务上,不会消耗大多精力在getter、setter和CRUD上。当你想自定义aj文件中某个方法时,不需要修改aj文件,只需要在entity.java中以相同的方法签名进行添加即可,Roo会自动帮你在aj文件中删除掉对应的自动生成的方法。