Skip to content

itren/AspectJ

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 

Repository files navigation

AspectJ AOP

1. AOP概念

AOP(面向切面编程)作为OOP(面向对象编程)的一种补充,增强了OOP的封装性可重用性。OOP的可重用性通常依赖于类的继承扩展实现的,最终形成一个纵向的扩展层次树。然而OOP对于横向的可重用性缺乏很好的解决办法,正因为如此,才有了AOP编程。

当程序在多出都出现相同的功能代码时,而这样的代码和业务逻辑关系又不紧密或者毫无相干。譬如对业务逻辑的函数执行前后打Log日志的代码片段,会出现在每一个业务逻辑函数的执行开始和执行结束,这一方面造成了代码的冗余,也使得对这些非业务逻辑的代码的维护变得非常困难。任何微小修改都需要每一处重复做一次,这不仅费时费力,而且极容易出错。如果我们可以将这一部分与业务逻辑关系不大的代码分离出来,将大大增加代码的可重用性和封装性。而且由于这部分代码与业务逻辑不相关,这样做使得模块内聚程度更高,模块间耦合程度更松。而这一切正是AOP所要做的事情。

AOP从本质上来说就是将那些重复的代码从业务逻辑中剥离出来,独立封装成一个类和若干方法,这样剥离出来的类和方法,被称之为横切逻辑 / 增强(Advice)。剥离后的业务逻辑的那些类和方法称之为目标逻辑(Target)。剥离的位置称之为切点(PointCut)。另外增强有不同的增强类型。因此,AOP编程就是在正确的切点使用正确的增强类型,将横切逻辑植入目标逻辑中

2. 一个AOP的例子

业务逻辑(即目标逻辑)的抽象接口和接口实现类如下

public interface Waiter {
	void greetTo(String name);
	void serveTo(String name);
}
public class NaiveWaiter implements Waiter {
	public void greetTo(String name) {
		System.out.println("greet to " + name);
		System.out.println();
	}
	
	public void serveTo(String name) {
		System.out.println("serve to " + name);
		System.out.println();
	}
}

横切逻辑如下

@Aspect
public class PreGreetingAdvisor {
	
	@Before("execution(* greetTo(..))")
	public void beforeGreeting() {
		System.out.println("How are you!");
	}
}

该横切逻辑使用了AspectJ注解方式实现的,@Aspect注解表示PreGreetingAdvisor类是一个横切逻辑类。@Before()表明增强类型是在目标逻辑方法前植入横切逻辑。(* greetTo(..))")给出了匹配的切点的描述。

接下来在xml文件中利用aop:aspectj-autoproxy来将横切逻辑自动植入目标逻辑中。

	<bean id="waiter" class="aspectJDemo.NaiveWaiter"></bean>
	<bean class="aspectJDemo.PreGreetingAdvisor"></bean>
	<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

Tips: aop:aspectj-autoproxy有一个proxy-target-class属性,默认为false,表示使用JDK动态代理;当设置<aop:aspectj-autoproxy proxy-target-class='true'></aop:aspectj-autoproxy>时,将使用CGLIB代理。需要注意的是,如果目标类没有声明接口,则即使该属性设为false,Spring将仍然使用CGLIB代理。

最后我们在main函数中来测试下我们的AOP编程的效果

public class Main {
	public static void main(String[] args) {
		String configPathString = "aspectJDemo/beans.xml";
		ApplicationContext context = new ClassPathXmlApplicationContext(configPathString);
		Waiter waiter = (Waiter) context.getBean("waiter");
		waiter.greetTo("Jack");
		waiter.serveTo("Jack");
	}
}

运行结果如下

How are you!
greet to Jack

serve to Jack

这个例子已经把AOP的过程毫无保留的展示给读者了,通过AOP技术,横切逻辑被神奇地注入到了目标逻辑特定的切点处。下面详细介绍AOP技术的一些细节。增强类型将在第3节详细论述,利用AspectJ语法对切点的匹配描述将在第4节给出,访问连接点信息将在第5节叙述。

3. 增强类型

Spring支持前置增强@Before、后置增强@AfterReturning、环绕增强@Around、抛出增强@Throwing、Final增强@After以及引介增强@DeclareParent

前置增强是将横切逻辑植入切点方法的前面,执行完横切逻辑后接着执行目标逻辑。后置增强将横切逻辑植入目标方法的后面。环绕增强是将横切逻辑植入目标逻辑前后。抛出增强类似于捕获异常,并将横切逻辑作为捕获异常的处理。Final增强类似于Final,将横切逻辑作为Final必须执行的模块。引介增强是在不使用显式继承接口的方法为目标类引入新的接口。

3.1 引介增强

引介增强和其他增强区别略微有点大,这里单独给出一个例子帮助理解。
假如我们希望对NaiveWaiter添加一个Seller接口

public interface Seller {
	public void sell(String goods);
}

该接口的实现如下

public class SmartSeller implements Seller {

	@Override
	public void sell(String goods) {
		System.out.println("sell " + goods);
	}

}

现在我们来定义引介切面

@Aspect
public class SellerAspect {
	
	@DeclareParents(value="declareParents.NaiveWaiter", defaultImpl=SmartSeller.class)
	public Seller seller; //引入的接口Seller
}

@Aspect注解表明SellerAspect是一个切面;@DeclareParents()表明这是一个引介切面,value成员指明Target类,defaultImpl成员给出引入的接口的实现类class。 xml配置文件仍然借助aop:aspectj-autoproxy来自动装配SellerAspect引介切面到NaiveWaiter这个Target类。

	<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
	<bean id="waiter" class="declareParents.NaiveWaiter"></bean>
	<bean class="declareParents.SellerAspect"></bean>

测试程序如下

	public static void main(String[] args) {
		String configPathString = "declareParents/beans.xml";
		ApplicationContext context = new ClassPathXmlApplicationContext(configPathString);
		Waiter waiter = (Waiter) context.getBean("waiter");
		waiter.greetTo("Jack");
		waiter.serveTo("Jack");
		
		((Seller)waiter).sell("apple");
	}

运行结果如下

greet to Jack

serve to Jack

sell apple

waiter在引入Seller接口以后神奇地可以装扮成Seller来调用sell方法了。

4. 切点匹配语法

4.1 匹配注解

可以使用@annotation()切点函数来匹配注解。所有含有指定注解的方法将被匹配,进而被植入横切逻辑;所有不含有指定方法的方法将被忽略。

@AfterReturning("@annotation(aspect.FunAnnotation)")

所有被标注了FunAnnotation注解的方法将被匹配。

public class NaughtyWaiter implements Waiter {

	@FunAnnotation
	@Override
	public void greetTo(String name) {
		System.out.println("greet to " + name + "\n");
	}
	
	@FunAnnotation
	@Override
	public void serveTo(String name) {
		System.out.println("serve to " + name + "\n");
	}

}

4.2 匹配方法

execution()切点函数用来匹配方法最为全面,既可以匹配方法名,也可以匹配函数值类型、参数类型,还可以匹配包名、类名。

#####1. 匹配方法名为serveTo,返回值任意,参数任意的方法。

@Before("execution(* serveTo(..))")

#####2. 匹配方法名含有serve,返回值任意,参数任意的方法。

@Before("execution(* *serve*(..))")

#####3. 匹配方法名为serveTo,返回值任意,参数为(String, int)的方法。

@Before("execution(* serveTo(String, int))")

#####4. 匹配方法名为serveTo,返回值任意,第一个参数为String,第二个参数任意的方法。

@Before("execution(* serveTo(String, *))")

Tips: 对于不在java.lang包下的类,需要使用全限定类名,如java.util.List

#####5. 匹配方法名为serveTo,返回值任意,第一个参数为String,剩下参数个数和类型任意的方法。

@Before("execution(* serveTo(String, ..))")

#####6. 匹配方法名为serveTo,返回值任意,参数为Object或其子类类型的方法。

@Before("execution(* serveTo(Object+))")

Tips: 对于

@Before("execution(* serveTo(Object))")

其匹配的方法的参数将仅仅是Object类型,不包其子类类型。

#####7. 匹配类名为aspect.NaiveWaiter,返回值任意,参数任意的类的所有方法。

@Before("execution(* aspect.NaiveWaiter.*(..))")

#####8. 匹配aspect包及其子孙包下,方法名含有serve,返回值任意,参数任意的类的方法。

@Before("execution(* aspect..*.*serve*(..))")

4.3 仅匹配参数

如果我们仅仅需要匹配方法的参数,那么使用args()将更加简单。

@Before("args(String)")

将匹配所有参数类型为String的方法。

4.4 仅匹配类名

如果我们仅仅需要匹配类名,意图将横切逻辑注入到匹配的类的所有方法中,那么使用within()或者target()将更加简单。

#####1. 匹配类名为NaughtyWaiter的所有方法

@Before("within(aspect.NaughtyWaiter)")

#####2. 匹配类名为Waiter及Waiter所有子类的所有方法

@Before("within(aspect.Waiter+)")

Tips: 上面的写法与@Before("target(aspect.Waiter)")完全等价。也就是说within()是精确类名匹配,而target()是匹配类名及其派生类。

#####3. 匹配aspect包下的所有类的所有方法

@Before("within(aspect.*)")

#####4. 匹配aspect包及其子孙包下的所有类的所有方法

@Before("within(aspect..*)")

4.5 复合匹配

可以使用&&、||、和!分别表示多个匹配条件的且、或和非的逻辑关系。 以下匹配条件将匹配Waiter及其派生类中的serveTo函数。

@Before("execution(* serveTo(..)) && target(aspect.Waiter)")

5. 访问连接点信息

5.1 利用JointPoint和ProceedingJointPoint访问连接点信息

JointPoint可以用于横切逻辑函数访问连接点信息之用,其包括以下常用方法

Object[] getArgs()	//获取参数列表
Signature getSignature()	//获取方法签名
Object getTarget()	//获取Target对象
Object getThis()	//获取代理对象本身

ProceedingJointPoint继承自JointPoint,通常用于环绕增强(@Arround)。其主要新增以下方法

Object proceed() throws Throwable	//通过反射执行Target对象在连接点处的方法

以下是使用ProceedingJointPoint访问连接点信息的例子,总可以通过将横切逻辑函数的第一个参数声明为JointPoint来获取连接点信息。

	@Around("execution(* serveTo(..)) && target(aspect.NaiveWaiter)")
	public void greet(ProceedingJoinPoint joinPoint) throws Throwable {
		System.out.println("---joinPoint---");
		
		System.out.println("arg[0] = " + joinPoint.getArgs()[0]);
		System.out.println("target = " + joinPoint.getTarget().getClass());
		
		joinPoint.proceed();
		
		System.out.println("---joinPoint---");
	}

运行结果如下:

---joinPoint---
arg[0] = Jack
target = class aspect.NaiveWaiter
serve to Jack with price 1000
---joinPoint---

5.2 绑定连接点方法传入参数

你可能会疑惑既然execution那么强大,为什么还要用args呢?仅仅是为了一种简化?事实上args可以用来绑定连接点方法传入参数,从而使得横切逻辑函数可以很容易访问连接点方法的传入参数。以下给出一个例子。

	@Before("execution(* serveTo(..)) && args(name, price)")
	public void greet(String name, int price) throws Throwable {
		
		System.out.println("name = " + name);
		System.out.println("price = " + price);
		
	}

@Before("execution(* serveTo(..)) && args(name, price)")通过void greet(String name, int price)获知nameprice的类型,从而将匹配方法名为serveTo,参数为(String, int),返回值任意的方法。并试图植入横切逻辑。args的强大之处在于它绑定到了连接点方法的传入参数,这使得我们直接可以在greet(String name, int price)横切逻辑函数中方便地使用连接点方法的传入参数。 因此以上程序的运行结果如下

name = Jack
price = 1000
serve to Jack with price 1000

5.3 绑定连接点方法返回值

通常用在AfterReturning横切类型中,通过AfterReturningreturning指定返回值对象。

	@AfterReturning(value="target(aspect.Seller)", returning="retValue")
	public void bindReturnValue(int retValue) {
		System.out.println("return value = " + retValue);
	}

5.4 绑定抛出的异常

必须使用在AfterThrowing横切类型中,通过AfterThrowingthrowing指定抛出异常对象。

	@AfterThrowing(value="target(aspect.Seller)", throwing="exception")
	public void bindException(IllegalArgumentException exception) {
		System.out.println("exception:" + exception.getMessage());
	}

以上切面将匹配aspect.Seller类运行时抛出异常类型为IllegalArgumentException的方法。以下为运行结果

sellBeer()
checkBill
exception:illegal argument

5.5 绑定proxy对象

可以使用this()或者target()来绑定proxy对象,此时切点函数仍然具有匹配功能。以下为一个例子

	@Before("target(waiter)")
	public void bindProxyObj(Waiter waiter) {
		System.out.println(waiter.getClass().getName());
	}

以上切面将匹配所有waiter类或其子类的方法。并且这样的proxy对象将通过waiter对象很容易的在横切逻辑函数中被访问。

aspect.NaiveWaiter
greet to Jack

aspect.NaiveWaiter
serve to Jack

aspect.NaiveWaiter
serve to Jack with price 1000

aspect.NaughtyWaiter
greet to Tom

aspect.NaughtyWaiter
serve to Tom

sellBeer()
checkBill

About

Aspect简易语法

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published