问题提出
当我们用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的实现框架,需要满足如下几个技术特性:
- join point(连接点):一个抽象的概念,即可以让point cut切进来的地方,通常跟AOP框架的实现有关,例如某个类的某个方法。
- point cut(切入点):被横切的连接点,用来捕获方法调用等事件。
- advice(通知):执行的具体逻辑。
- aspect(方面):类似于一个操作的集合体,声明了多个point cut以及该point cut触发后调用的advice。
- 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;
}
};
如上面的代码所示,定义了AuthAspect
和TransactionAspect
两个切面。
其中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();
}
}
上述例子中,有:
- 一个 inter-type field:
private boolean Server.disabled = false;
- 两个methods:
reportFault()
和fixServer(Server s)
- 一个point cut定义:
pointcut services(Server s): target(s) && call(public * *(..));
- 两个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为我们新建了一系列的文件:
- Course.java
- Course_Roo_Configurable.aj
- Course_Roo_ToString.aj
- Course_Roo_Jpa_Entity.aj
- Course_Roo_JavaBean.aj
- 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.aj
、Course_Roo_Jpa_Entity.aj
和Course_Roo_Jpa_ActiveRecord.aj
。我们可以不通过Roo Shell,手动地添加entity,然后按需要添加上述注解,Roo会自动地根据注解内容为我们添加或删除对应的aj文件。
总体架构如下所示:
可以看出:
Course_Roo_ToString.aj
定义了entity的toString()方法。Course_Roo_Configurable.aj
为entity添加了@Configurable标签(请看这里)。Course_Roo_Jpa_Entity.aj
为entity提供了JPA支持。Course_Roo_JavaBean.aj
定义了一系列的getter和setter方法。Course_Roo_Jpa_ActiveRecord.aj
包装了一系列的CRUD方法。
ROO通过将与Entity相关的不同职能的方法封装在不同的aj文件中,在编码时帮助程序员实时管理aj文件,最后通过AspectJ在编译时织入,组成目标class文件。这样子有利于程序员吧注意力集中在核心业务上,不会消耗大多精力在getter、setter和CRUD上。当你想自定义aj文件中某个方法时,不需要修改aj文件,只需要在entity.java中以相同的方法签名进行添加即可,Roo会自动帮你在aj文件中删除掉对应的自动生成的方法。