AOP面向切面编程也是Spring一个关键的组件,它的主要作用是将系统共用度高的系统逻辑与业务逻辑进行分离提高程序的重用,降低耦合度,使项目更加易于维护。比如,日志、事务及权限控制等。AOP中有比较的多的术语,先提前介绍一下。
1.AOP中主要术语介绍
- 切面(Aspect):系统中功能模块具有的一组横向需求的接口组合,我自己理解即是程序运行到某个地方,会”意外“的执行一些其他逻辑,执行完其他的逻辑后接着运行;“某个地方”这个层面,就是切面;
- 切点(Pointcut):官方的解释不太好理解,受数学上一句启发:点动成线,线动成面,面动成体;切面就是由切点组成的,但是这里的切点可以是一个也可以是多个;
- 通知(Advice):通知通俗的理解就是前面切面里会意外的执行一些其他逻辑,通知其实就是“意外执行的逻辑”,这个逻辑有:前置通知、后置通知、返回后通知、异常抛出后通知以及环绕通知,这些都什么意思,后面会在例子中说明;
- 连接点(JoinPoint):跟切点位置一样的,一对一,有一个起作用的切点就有一个连接点;
2.配置方式实现AOP
新的功能,新的命名空间是不能少的,先添加上AOP命名空间标签,如下:
xmlns:aop="http://www.springframework.org/schema/aop"
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
注:版本根据实际使用修改
第一步:首先创建一个切面:
ackage com.me.spring;
public class FirstAspect {
public void beforeAdvice(){
System.out.println("beforeAdvice(前置通知) doing......");
}
public void afterAdvice(){
System.out.println("afterAdvice(后置通知) doing......");
}
public void AfterThrowingAdvice(NullPointerException e){
System.out.println("AfterThrowingAdvice(异常抛出后通知) an exception: " + e.toString());
}
}
第二步:创建一个Person类,具有吃饭、睡觉以及一个模拟抛出异常的方法
package com.me.spring;
public class Person {
private String name;
public void eat(){
System.out.println(name+"在吃饭......");
}
public void sleep(){
System.out.println(name+"在睡觉......");
}
public void throwsExceptionTest() {
System.out.println("抛出空指针异常......");
throw new NullPointerException();
}
public String getName() {
return name;
}
}
第三步:增加bean配置以及AOP配置
<!--AOP配置-->
<aop:config>
<!--定义一个切面-->
<aop:aspect id="myAspect" ref="firstAspect">
<!--定义一个切点-->
<aop:pointcut id="myPointCut" expression="execution(* com.me.spring.Person.*(..))"></aop:pointcut>
<!--连接点引用切点,切面的通知起作用-->
<aop:before method="beforeAdvice" pointcut-ref="myPointCut"></aop:before>
<aop:after method="afterAdvice" pointcut-ref="myPointCut"></aop:after>
<aop:after-throwing method="AfterThrowingAdvice" pointcut-ref="myPointCut" throwing="e"></aop:after-throwing>
</aop:aspect>
</aop:config>
<!--bean配置-->
<bean id="person" class="com.me.spring.Person">
<property name="name" value="Snow"></property>
</bean>
<bean id="firstAspect" class="com.me.spring.FirstAspect"></bean>
其中,expression是指定切入点位置:expression(返回值 包路径.类.方法名(方法参数,可…代替)),里面可用通配符
第四步:获取bean,并调用人类的吃饭睡觉方法,抛异常后通知单独运行,看的清楚
Person person = (Person)context.getBean("person");
person.eat();
person.sleep();
运行测试结果:
beforeAdvice(前置通知) doing......
Snow在吃饭......
afterAdvice(后置通知) doing......
beforeAdvice(前置通知) doing......
Snow在睡觉......
afterAdvice(后置通知) doing......
可以看到,在吃饭睡觉方法执行前,执行了切面的前置通知;之后执行了后置通知。在实际当中,可以在前置通知中加入执行逻辑,判断当前会话者有没有执行睡觉吃饭的权限,即鉴权。后置通知亦如此,不再说明。
单独运行**throwsExceptionTest()**方法,结果如下:
Exception in thread "main" java.lang.NullPointerException
抛出空指针异常......
at com.me.spring.Person.throwsExceptionTest(Person.java:22)
AfterThrowingAdvice(异常抛出后通知) an exception: java.lang.NullPointerException
异常抛出后就执行切面的抛出异常后通知。
3.注解方式实现AOP
注解方式目的就是为了简化xml配置的,先目睹一下注解方式的配置内容:
<!--AOP注解配置声明-->
<aop:aspectj-autoproxy/>
<!--bean注解配置声明-->
<context:component-scan base-package="com.me.*"></context:component-scan>
这看上去比配置的清爽了许多,当然bean的注解不能少也要加上,接下来修改切面类,加注解:
package com.me.spring;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Repository;
@Aspect // 切面注解,声明该类为切面
@Repository
public class FirstAspect {
// 配置公共的切点:Person类里的所用方法,提高重用,下面通知中引用
@Pointcut("execution(* com.me.spring.Person.*(..))")
public void commonAspect(){}
@Before("commonAspect()") // 前置通知注解
public void beforeAdvice(){
System.out.println("beforeAdvice(前置通知) doing......");
}
@After("commonAspect()") // 后置通知注解
public void afterAdvice(){
System.out.println("afterAdvice(后置通知) doing......");
}
@AfterThrowing(value="commonAspect()", throwing = "e") // 抛出异常后通知注解
public void AfterThrowingAdvice(NullPointerException e){
System.out.println("AfterThrowingAdvice(异常抛出后通知) an exception: " + e.toString());
}
}
Person类没有特殊修改,只需要加上bean配置的注解
测试,因为使用注解,要为person实例设置名字属性,如下:
Person person = (Person)context.getBean("person");
person.setName("Snow");
person.eat();
person.sleep();
运行测试,结果:
beforeAdvice(前置通知) doing......
afterAdvice(后置通知) doing......
beforeAdvice(前置通知) doing......
Snow在吃饭......
afterAdvice(后置通知) doing......
beforeAdvice(前置通知) doing......
Snow在睡觉......
afterAdvice(后置通知) doing......
仔细观察,咦!奇怪怎么比用配置的方式多出了个前后置通知信息打印呢?原因就在于使用注解,测试的时候调用了person的setter方法设置姓名属性,而在公共切点配置的时候是:execution(* com.me.spring.Person.*(…))指的是Person类的所有方法,所以多出来的一个前后置通知就是setter方法上切面起作用的结果。异常抛出后通知结果与配置方式没区别,不在展示。
补充:JoinPoint连接点
前面提到连接点,在这直观的感受一下,以前置通知方法为例修改一下,加上JoinPoint:
@Before("commonAspect()") // 前置通知注解
public void beforeAdvice(JoinPoint jp){
System.out.println("Before当前连接点名称:"+jp.getSignature().getName());
System.out.println("beforeAdvice(前置通知) doing......");
}
运行测试结果:
当前连接点名称:setName
beforeAdvice(前置通知) doing......
afterAdvice(后置通知) doing......
当前连接点名称:eat
beforeAdvice(前置通知) doing......
Snow在吃饭......
afterAdvice(后置通知) doing......
当前连接点名称:sleep
beforeAdvice(前置通知) doing......
Snow在睡觉......
afterAdvice(后置通知) doing......
看结果第一个确实,连接点setName。至此,AOP面向前面编程终于理清楚,还需在更多的实践中加以检验,本文多以自我理解,如有不同见解或不当之处,望不吝赐教!