Spring框架学习-深入理解AOP
—-AOP简介,AspectJ,AOP基于注解和XML配置(5种通知,切面优先级)
一、AOP简介
AOP(Aspect-Oriented Programming, 面向切面编程):是一种新的方法论,是对传统OOP(Object-OrientedProgramming,面向对象编程)的补充.
AOP 的主要编程对象是切面(aspect),而切面模块化横切关注点.
在应用 AOP 编程时,仍然需要定义公共功能,但可以明确的定义这个功能在哪里,以什么方式应用,并且不必修改受影响的类.这样一来横切关注点就被模块化到特殊的对象(切面)里.
1-1AOP 的好处:
- 每个事物逻辑位于一个位置,代码不分散,便于维护和升级
- 业务模块更简洁, 只包含核心业务代码.
1-2**AOP 术语**
- 切面(Aspect):横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象,把横切关注点的代码抽象到切面的类中
- 通知(Advice):切面必须要完成的工作,就是功能对应的方法
- 目标(Target):被通知的对象
- 代理(Proxy):向目标对象应用通知之后创建的对象
- 连接点(Joinpoint):程序执行的某个特定位置:如类某个方法调用前、调用后、方法抛出异常后等。连接点由两个信息确定:方法表示的程序执行点;相对点表示的方位。例如ArithmethicCalculator#add()方法执行前的连接点,执行点为ArithmethicCalculator#add();方位为该方法执行前的位置
- 切点(pointcut):每个类都拥有多个连接点:例如ArithmethicCalculator的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件。
二、AspectJ
AspectJ:Java社区里最完整最流行的AOP框架.
在 Spring2.0以上版本中,可以使用基于AspectJ注解或基于XML配置的AOP
1-1.在 Spring中启用AspectJ注解支持
要在 Spring应用中使用AspectJ注解,必须在classpath下包含AspectJ类库:aopalliance.jar、aspectj.weaver.jar和spring-aspects.jar
将 aop Schema添加到根元素中.
要在 SpringIOC容器中启用AspectJ注解支持,只要在Bean配置文件中定义一个空的XML 元素
当 SpringIOC 容器侦测到 Bean 配置文件中的元素时,会自动为与AspectJ切面匹配的Bean创建代理.
1-2.用 AspectJ注解声明切面
要在 Spring中声明AspectJ切面,只需要在IOC容器中将切面声明为Bean实例.当在SpringIOC容器中初始化AspectJ切面之后,Spring IOC容器就会为那些与AspectJ切面相匹配的Bean创建代理.
在 AspectJ注解中,切面只是一个带有@Aspect注解的 Java类.
通知是标注有某种注解的简单的 Java方法.
AspectJ支持5种类型的通知注解:
@Before: 前置通知,在方法执行之前执
@After: 后置通知,在方法执行之后执行
@AfterRunning:返回通知,在方法返回结果之后执行
@AfterThrowing:异常通知,在方法抛出异常之后
@Around: 环绕通知,围绕着方法执行
1-3.利用方法签名编写AspectJ切入点表达式
最典型的切入点表达式时根据方法的签名来匹配各种方法:
- execution com.atguigu.spring.ArithmeticCalculator.(..):匹配ArithmeticCalculator中声明的所有方法,第一个 * 代表任意修饰符及任意返回值.第二个 * 代表任意方法…匹配任意数量的参数.若目标类与接口与该切面在同一个包中,可以省略包名.
- execution public* ArithmeticCalculator.(..):匹配ArithmeticCalculator接口的所有公有方法.
- execution publicdouble ArithmeticCalculator.(..):匹配ArithmeticCalculator中返回double类型数值的方法
- execution publicdouble ArithmeticCalculator.(double,..):匹配第一个参数为double类型的方法,..匹配任意数量任意类型的参数
- execution publicdouble ArithmeticCalculator.*(double,double):匹配参数类型为double,double类型的方法.
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="Spring4_AOP.aopAnnotation"></context:component-scan>
<!--使AspectJ注解起作用:自动为匹配的类生成代理对象-->
</aop:aspectj-autoproxy>
</beans>
ArithmeticCalculatorImpl在参见一篇博客http://blog.csdn.net/ochangwen/article/details/52557459
所以的类都放个这个目录下Spring4_AOP.aopAnnotation
1-4 日志切面
编写切面类(把横切关注点的代码抽象到切面的类中):
- 4.1 一个一般的 Java 类
- 4.2 在其中添加要额外实现的功能.
1-5 配置切面
- 5.1 切面必须是 IOC 中的 bean: 实际添加了 @Component 注解
- 5.2 声明是一个切面: 添加 @Aspect
- 5.3 声明通知: 即额外加入功能对应的方法!!.
@Aspect
@Component
public class LoggingAspect {
/**
* 重用切入点定义
* 定义一个方法,用于声明切入点表达式,一般地,该方法中再不需要添入其它的代码
*/
@Pointcut("execution(* Spring4_AOP.aopAnnotation.*.*(..))")
public void declareJoinPointerExpression() {}
//1、前置通知: 在目标方法开始之前执行(就是要告诉该方法要在哪个类哪个方法前执行)
//@Before("execution(public int Spring4_AOP.aopAnnotation.*.*(int ,int))")
@Before("declareJoinPointerExpression()")
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object [] args = joinPoint.getArgs();
System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
}
//2、后置通知:在目标方法执行后(无论是否发生异常),执行的通知
//注意,在后置通知中还不能访问目标执行的结果!!!,执行结果需要到返回通知里访问
//@After("execution(* Spring4_AOP.aopAnnotation.*.*(..))")
@After("declareJoinPointerExpression()")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " ends");
}
//无论连接点是正常返回还是抛出异常, 后置通知都会执行. 如果只想在连接点返回的时候记录日志, 应使用返回通知代替后置通知.
//3、返回通知:在方法正常结束后执行的代码,返回通知是可以访问到方法的返回值的!!!
//@AfterReturning(pointcut = "execution(* Spring4_AOP.aopAnnotation.*.*(..))", returning = "result")
@AfterReturning(value = "declareJoinPointerExpression()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result){
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " AfterReturning ends with " + result);
}
//4、异常通知:在目标方法出现异常 时会执行的代码,可以访问到异常对象:且可以!!指定在出现特定异常时在执行通知!!,如果是修改为nullPointerException里,只有空指针异常才会执行
// @AfterThrowing(pointcut = "execution(* Spring4_AOP.aopAnnotation.*.*(..))", throwing = "except")
@AfterThrowing(value = "declareJoinPointerExpression())", throwing = "except")
public void afterThrowing(JoinPoint joinPoint, Exception except){
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " occurs exception " + except);
}
/**
* 5、环绕通知 需要携带 ProceedingJoinPoint 类型的参数.
* 环绕通知类似于动态代理的全过程: ProceedingJoinPoint 类型的参数可以决定是否执行目标方法.
* 且环绕通知必须有返回值, 返回值即为目标方法的返回值
*/
// @Around("execution(* Spring4_AOP.aopAnnotation.*.*(..))")
@Around("declareJoinPointerExpression()")
public Object aroundMethod(ProceedingJoinPoint pjd){
Object result = null;
String methodName = pjd.getSignature().getName();
try {
//前置通知
System.out.println("The method " + methodName + " begins with " + Arrays.asList(pjd.getArgs()));
//执行目标方法
result = pjd.proceed();
//返回通知
System.out.println("The method " + methodName + " ends with " + result);
} catch (Throwable e) {
//异常通知
System.out.println("The method " + methodName + " occurs exception:" + e);
throw new RuntimeException(e);
}
//后置通知
System.out.println("The method " + methodName + " ends");
return result;
}
}
1)表达式
在 AspectJ中,切入点表达式可以通过操作符&&,||, !结合起来.
@Pointcut("execution(* Spring4_AOP.aopAnnotation.*.add(..)) ||execution(* Spring4_AOP.aopAnnotation.*.sub(..))")
public void declareJoinPointerExpression() {}
在返回通知中访问连接点的返回值
在返回通知中, 只要将returning属性添加到@AfterReturning注解中,就可以访问连接点的返回值.该属性的值即为用来传入返 回值的参数名称.
必须在通知方法的签名中添加一个同名参数.在运行时,Spring AOP会通过这个参数传递返回值.
原始的切点表达式需要出现在pointcut属性中
2).异常通知
只在连接点抛出异常时才执行异常通知
将 throwing属性添加到@AfterThrowing注解中,也可以访问连接点抛出的异常.Throwable是所有错误和异常类的超类.所以在异常通知方法可以捕获到任何错误和异常.
3).环绕通知
环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点.甚至可以控制是否执行连接点.
对于环绕通知来说, 连接点的参数类型必须是ProceedingJoinPoint.它是JoinPoint的子接口,允许控制何时执行,是否执行连接点.
在环绕通知中需要明确调用 ProceedingJoinPoint的proceed()方法来执行被代理的方法.如果忘记这样做就会导致通知被执行了,但目标方法没有被执行.
注意: 环绕通知的方法需要返回目标方法执行之后的结果,即调用joinPoint.proceed();的返回值,否则会出现空指针异常
@Test
public void testAOPAnnotation() {
//1、创建Spring的IOC的容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("Spring4_AOP/applicationContext-aop.xml");
//2、从IOC容器中获取bean的实例
ArithmeticCalculator arithmeticCalculator = (ArithmeticCalculator) ctx.getBean("arithmeticCalculator");
//3、使用bean
int result = arithmeticCalculator.p(3, 1);
System.out.println("result:" + result);
}
2-1.指定切面的优先级
在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的.
切面的优先级可以通过实现 Ordered接口或利用@Order注解指定.
实现 Ordered接口,getOrder()方法的返回值越小,优先级越高,切面出的越在前面
若使用 @Order注解,序号出现在注解中
@Order(1)
@Aspect
@Component
public class ValidationAspect {
@Before("execution(public int Spring4_AOP.aopAnnotation.*.*(int ,int))")
public void validateArgs(JoinPoint joinPoint){
System.out.println("-->validate:" + Arrays.asList(joinPoint.getArgs()));
}
}
日志切面,使用上面的那个类,只增加Order
@Order(2)
@Aspect
@Component
public class LoggingAspect {
......
}
2-3.重用切入点定义
在编写 AspectJ切面时,可以直接在通知注解中书写切入点表达式.但同一个切点表达式可能会在多个通知中重复出现.
在 AspectJ切面中,可以通过@Pointcut注解将一个切入点声明成简单的方法.切入点的方法体通常是空的,因为将切入点定义与应用程序逻辑混在一起是不合理的.
切入点方法的访问控制符同时也控制着这个切入点的可见性.如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中.在这种情况下,它们必须被声明为public.在引入这个切入点时,必须将类名也包括在内.如果类没有与这个切面放在同一个包中,还必须包含包名.
其他通知可以通过方法名称引入该切入点.
三 、用基于 XML 的配置声明切面
除了使用 AspectJ注解声明切面,Spring 也支持在 Bean 配置文件中声明切面.这种声明是通过aopschema 中的 XML 元素完成的.
正常情况下, 基于注解的声明要优先于基于 XML 的声明.通过AspectJ注解,切面可以与AspectJ兼容,而基于XML的配置则是Spring专有的.由于AspectJ得到越来越多的AOP框架支持,所以以注解风格编写的切面将会有更多重用的机会.
1-1.基于 XML—- 声明切面
当使用 XML 声明切面时,需要在根元素中导入aopSchema
在 Bean 配置文件中,所有的SpringAOP 配置都必须定义在 元素内部.对于每个切面而言,都要创建一个元素来为具体的切面实现引用后端Bean实例.
切面 Bean 必须有一个标示符,供元素引用
1-2.基于 XML—- 声明切入点
切入点使用 元素声明切入点必须定义在 元素下,或者直接定义在元素下.–定义在 元素下:只对当前切面有效–定义在 元素下:对所有切面都有效基于XML的AOP配置不允许在切入点表达式中用名称引用其他切入点.
1-3.基于 XML—- 声明通知
在 aopSchema 中, 每种通知类型都对应一个特定的XML元素.通知元素需要使用 来引用切入点,或用直接嵌入切入点表达式. method 属性指定切面类中通知方法的名称.
1-4.声明引入
可以利用 元素在切面内部声明引入 ArithmeticCalulator2与上面ArithmeticCalulator接口内容 一样
public class ArithmeticCalculatorImplXML implements ArithmeticCalculator2 {
public int add(int i, int j) {
return i + j;
}
public int sub(int i, int j) {
return i - j;
}
public int mul(int i, int j) {
return i * j;
}
public int p(int i, int j) {
return i / j;
}
}
public class LoggingAspectXML {
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
Object [] args = joinPoint.getArgs();
System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
}
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " ends");
}
public void afterReturning(JoinPoint joinPoint, Object result){
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " ends with " + result);
}
public void afterThrowing(JoinPoint joinPoint, Exception e){
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " occurs excetion:" + e);
}
}
public class ValidationAspectXML {
public void validateArgs(JoinPoint joinPoint){
System.out.println("-->validate:" + Arrays.asList(joinPoint.getArgs()));
}
}<beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> </aop:config>
</beans>
@Test
public void testXML() {
//1、创建Spring的IOC的容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("Spring4_AOP/applicationContext-xml.xml");
//2、从IOC容器中获取bean的实例
ArithmeticCalculatorImplXML arithmeticCalculator = (ArithmeticCalculatorImplXML) ctx.getBean("arithmeticCalculatorXML");
//3、使用bean
int result = arithmeticCalculator.add(3, 3);
System.out.println("result:" + result);
arithmeticCalculator.p(10, 0);
}