文章目录
AOP概述
1、AOP
- AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统OOP(Object-Oriented Programming,面向对象编程)的补充。
- AOP编程操作的主要对象是切面(aspect),而切面模块化横切关注点。
- 在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的类里——这样的类我们通常称之为“切面”。
- AOP的好处:
<1>每个事务逻辑位于一个位置,代码不分散,便于后期维护和升级。
<2>业务模块更简洁,只包含核心业务代码。
<3>AOP图解:
2、环境搭建
通过网站https://mvnrepository.com/下载对应自己的Spring版本的jar包
com.springsource.net.sf.cglib-2.2.0.jar
com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
spring-aop-4.0.0.RELEASE.jar
spring-aspects-4.0.0.RELEASE.jar
将它们放在lib文件夹下,然后点击add as library即可。
3、AOP术语
- [1] 横切关注点
从每个方法中抽取出来的同一类非核心业务。 - [2]切面(Aspect)
封装横切关注点信息的类,每个关注点体现为一个通知方法。 - [3]通知(Advice)
切面必须要完成的各个具体工作。
<1>@Before:前置通知,在方法执行之前执行
<2>@After:后置通知,在方法执行之后执行
<3>@AfterReturning:返回通知,在方法返回结果之后执行
<4>@AfterThrowing:异常通知,在方法抛出异常执行
<5>@Around:环绕通知,环绕着方法执行 - [4]目标(Target)
被通知的对象 - [5]代理(Proxy)
向目标对象应用通知之后创建的代理对象。 - [6]连接点(Joinpoint)
横切关注点在程序代码中的具体体现,对应程序执行的某个特定位置。例如:类某个方法调用前、调用后、方法捕获到异常后等。
在应用程序中可以使用横纵两个坐标来定位一个具体的连接点。 - [7]切入点(pointcut)
定位连接点的方式。每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物。如果把连接点看作数据库中的记录,那么切入点就是查询条件——AOP可以通过切入点定位到特定的连接点。切点通过org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
图解:
4、AspectJ五种通知详解
<1>、前置通知
1、创建一个实现简单运算的接口类和一个实现类,并且在xml文件中进行组件扫面和切面标识。
public interface ArithmeticCalculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
@Component
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
@Override
public int add(int i, int j) {
int result = i + j;
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
return result;
}
}
<!--组件扫描 -->
<context:component-scan base-package="spring.aspectJ"></context:component-scan>
<!--基于注解使用AspectJ:主要作用是为切面中通知能作用到的目标类生成代理-->
<aop:aspectj-autoproxy/>
2、创建一个LoggingAspect作为日志切面,并且使用@Component标识为一个组件放入IOC容器中
3、LoggingAspect作为一个日志切面包含许多通知方法,Before中的参数是为了确定在哪个方法前添加前置通知。
@Before("execution(public int spring.aspectJ.annotation.ArithmeticCalculatorImpl.add(int, int ))")
public void beforeMethod(JoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
String name = joinPoint.getSignature().getName();
System.out.println("LoggingAspect======>The method" + name + ",,begins with" + Arrays.asList(args));
}
4、测试方法如下:
ApplicationContext context = new ClassPathXmlApplicationContext("spring.aspect_annotation.xml");
ArithmeticCalculator ac = context.getBean("arithmeticCalculatorImpl", ArithmeticCalculator.class);
int result = ac.add(1, 10);
System.out.println(result);
//输出结果:LoggingAspect======>The methodadd,,begins with[1, 10]
//11
此时ac是接口实例实现方法的代理对象,虽然我们对接口实例化,但是我们却得到接口实现类的实例,是因为:
<!--基于注解使用AspectJ:主要作用是为切面中通知能作用到的目标类生成代理-->
<aop:aspectj-autoproxy/>
@Before里面标记的类,会为其生成代理。通过getBean获取代理对象时里面的类是被aop切面通知且作用过的,因此返回了代理对象。当代理对象调用代理方法时会先执行@Before所产生的前置通知,ac是代理对象,无法由接口实现类实例化生成。
<2>、后置通知
/**
* 后置通知:在目标方法(连接点)之后执行,不管目标方法是否抛出异常,不会得到最后的结果。
*
* * spring.aspectJ.annotation.*.*(..)
* *:任意修饰符,任意返回值
* *:任意类
* *:任意方法
* (..):任意参数列表
*
* JoinPoint:连接点对象
*/
//@After("execution(* spring.aspectJ.annotation.*.*(..))")
@After("declarePointcut()") //这里是调用了重用的切入点表达式
public void afterMethod(JoinPoint joinPoint){
//获取方法名字
String name = joinPoint.getSignature().getName();
System.out.println("LoggingAspect=======>The method " + name + ",,ends");
}
测试方法:
public class test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring.aspect_annotation.xml");
ArithmeticCalculator ac = context.getBean("arithmeticCalculatorImpl", ArithmeticCalculator.class);
int result = ac.mul(2, 10);
System.out.println(result);
}
}
//LoggingAspect=======>The method add,,ends
//11
<3>、返回通知
/**
* 返回通知:在目标方法正常执行之后执行,可以获取到方法的返回值
*
* 获取返回值的方法:使用returning来指定一个名字,必须与方法中形参的其中一个名字一致
*/
@AfterReturning(value = "execution(* spring.aspectJ.annotation.*.*(..))", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result){
String name = joinPoint.getSignature().getName();
System.out.println("LoggingAspect========>The method" + name + ",,end with:" + result);
}
测试方法:
public class test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring.aspect_annotation.xml");
ArithmeticCalculator ac = context.getBean("arithmeticCalculatorImpl", ArithmeticCalculator.class);
int result = ac.add(2, 10);
System.out.println(result);
}
}
//LoggingAspect========>The methodadd,,end with:12
//12
<4>、异常通知
/**
* 异常通知:在目标方法抛出异常时执行
* 获取异常值的方法:使用throwing 来指定一个名字,必须与方法中形参的其中一个名字一致
* 可以通过设置形参中异常的类型来指定只有满足此异常才会执行异常通知
*/
@AfterThrowing(value = "execution(* spring.aspectJ.annotation.*.*(..))", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, ArithmeticException ex){
String name = joinPoint.getSignature().getName();
System.out.println("LoggingAspect=====>The method " + name + "occur exception" + ex);
}
测试方法:
public class test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring.aspect_annotation.xml");
ArithmeticCalculator ac = context.getBean("arithmeticCalculatorImpl", ArithmeticCalculator.class);
int div = ac.div(2, 0);
System.out.println(div);
}
}
//LoggingAspect=====>The method divoccur exceptionjava.lang.ArithmeticException: / by zero
<5>、环绕通知
/**
* 环绕通知:环绕着目标方法执行,可以理解为前置,后置,返回,异常的组合体
*/
@Around(value = "execution(* spring.aspectJ.annotation.*.*(..))")
public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint){
String name = null;
Object[] args;
try {
//前置通知
args = proceedingJoinPoint.getArgs();
name = proceedingJoinPoint.getSignature().getName();
System.out.println("LoggingAspect======>The method" + name + ",,begins with" + Arrays.asList(args));
Object result = proceedingJoinPoint.proceed(); //执行目标方法
//返回通知
System.out.println("LoggingAspect========>The method" + name + ",,end with:" + result);
return result;
} catch (Throwable ex) {
//异常通知
System.out.println("LoggingAspect=====>The method " + name + "occur exception" + ex);
}finally {
//后置通知
System.out.println("LoggingAspect=======>The method " + name + ",,ends");
}
return null;
}
测试方法:
public class test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring.aspect_annotation.xml");
ArithmeticCalculator ac = context.getBean("arithmeticCalculatorImpl", ArithmeticCalculator.class);
int result = ac.add(2, 10);
System.out.println(result);
}
}
//LoggingAspect======>The methodadd,,begins with[2, 10]
//LoggingAspect========>The methodadd,,end with:12
//LoggingAspect=======>The method add,,ends
//12
5、重用切入点表达式
在开发中我们要使用到相同到切入点表达式,所以我们可以使用@Pointcut定义一个可以重用的切入点表达式。
/**
* 重用切入点表达式
*/
@Pointcut("execution(* spring.aspectJ.annotation.*.*(..))")
public void declarePointcut(){
}
@After("declarePointcut()")
public void afterMethod(JoinPoint joinPoint){
//获取方法名字
String name = joinPoint.getSignature().getName();
System.out.println("LoggingAspect=======>The method " + name + ",,ends");
}
通过@Pointcut定义好的切入点表达式,给其他通知使用时可以直接将方法名当做参数传入即可,这样通知就可以定义好切入点表达式了。
6、切面的优先级
<1>在同一个连接点上应用不止一个切面时,不明确指定,它们的优先级不确定的。
<2>切面的优先级可以通过实现Ordered接口或者利用@Order注解指定。
<3>实现Ordered接口,getOrder()返回值越小,优先级越高。
<4>若是使用@Order注解,序号写在注解中。
@Component
@Aspect
@Order(1) //设置切面的优先级 默认值值是2147483647 值越小优先级越高
public class Validation {
//@Before("execution(* spring.aspectJ.annotation.*.*(..))")
@Before("LoggingAspect.declarePointcut()")
public void validationMethod(JoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
String name = joinPoint.getSignature().getName();
System.out.println("Validation=======>The method " + name + " begins with " + Arrays.asList(args));
}
}
@Component
@Aspect
@Order(2)
public class LoggingAspect {
/**
* 前置通知:在目标方法(连接点)之前执行
*/
@Before("execution(public int spring.aspectJ.annotation.ArithmeticCalculatorImpl.add(int, int ))") //切入点表达式
public void beforeMethod(JoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
String name = joinPoint.getSignature().getName();
System.out.println("LoggingAspect======>The method" + name + ",,begins with" + Arrays.asList(args));
}
测试方法:
public class test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring.aspect_annotation.xml");
ArithmeticCalculator ac = context.getBean("arithmeticCalculatorImpl", ArithmeticCalculator.class);
int result = ac.mul(2, 10);
System.out.println(result);
}
}
//Validation=======>The method mul begins with [2, 10]
//LoggingAspect======>The methodmul,,begins with[2, 10]
//20
7、以XML方式配置切面
除了使用AspectJ注解声明切面以外,Spring也支持在bean配置文件中声明切面,通过aop名称空间中的XML元素完成的。
正常情况下,基于注解的声明要优先于基于XML的声明。通过AspectJ注解,切面可以与AspectJ兼容,而基于XML的配置则是Spring专有的。由于AspectJ得到越来越多的 AOP框架支持,所以以注解风格编写的切面将会有更多重用的机会。
上面所有基于注解的配置下面都基于XML配置了一遍供大家学习参考。
<!--目标对象-->
<bean id="arithmeticCalculatorImpl" class="spring.aspectJ.annotation.ArithmeticCalculatorImpl"></bean>
<!--切面-->
<bean id="loggingAspect" class="spring.aspectJ.annotation.LoggingAspect"></bean>
<bean id="validationAspect" class="spring.aspectJ.annotation.Validation"></bean>
<!--AOP 切入点表达式,通知,切面-->
<aop:config>
<!--切面-->
<aop:aspect id="loggingAspect" order="2">
<!--切入点表达式-->
<aop:pointcut id="myPointcut" expression="execution(* spring.aspectJ.annotation.*.*(..))"></aop:pointcut>
<aop:before method="beforeMethod" pointcut-ref="myPointcut"/>
<aop:after method="afterMethod" pointcut-ref="myPointcut"/>
<aop:after-returning method="afterReturningMethod" pointcut-ref="myPointcut"/>
<aop:after-throwing method="afterThrowingMethod" pointcut-ref="myPointcut"/>
<aop:around method="aroundMethod" pointcut-ref="myPointcut"/>
</aop:aspect>
<aop:aspect id="validationAspect" order="1">
<aop:before method="beforeMethod" pointcut-ref="myPointcut"/>
</aop:aspect>
</aop:config>