什么是Spring的AOP?
AOP在spring中又叫“面向切面编程”,它可以说是对传统我们面向对象编程的一个补充,从字面上顾名思义就可以知道,它的主要操作对象就是“切面”,所以我们就可以简单的理解它是贯穿于方法之中,在方法执行前、执行时、执行后、返回值后、异常后要执行的操作。
相当于是将我们原本一条线执行的程序在中间切开加入了一些其他操作一样。
在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能应用在哪里,以什么方式应用,并且不必修改受核心业务类。这样一来横切关注点就被模块化到业务逻辑类里——这样的类我们通常就称之为“切面”。
例如下面这个图就是一个AOP切面的模型图,是在某一个方法执行前后执行的一些操作,并且这些操作不会影响程序本身的运行。
AOP切面编程的专业术语
术语 | 含义 |
---|---|
横切关注点 | 非核心业务,与业务逻辑无关的,需要关注的方法或功能,如日志,安全,缓存,事务 |
切面(Aspect) | 封装横切关注点的类 |
通知(Advice) | 切面必须要完成的各个具体工作(切面的方法) |
目标(Target) | 被通知的对象 |
代理(proxy) | 向目标对象应用通知之后创建的代理对象 |
连接点(Joinpoint) | 横切关注点在程序代码中的具体体现,对应程序执行的某个特定位置。 |
切入点(pointcut) | 执行或找到连接点的一些方式 |
现在大概的了解了AOP切面编程的基本概念,接下来就是实际操作了。
基于XML配置的AOP实现
方式一:基于spring的API实现(如MethodBeforeAdvice )
在bean配置文件中,所有的Spring AOP配置都必须定义在< aop:config>元素内部。对于每个切面而言,都要创建一个< aop:aspect>元素来为具体的切面实现引用后端bean实例。
切面bean必须有一个标识符,供< aop:aspect>元素引用。
1.导入aspectj依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.9.1</version>
</dependency>
2.定义切面实现advice
MethodBeforeAdvice ,AfterReturningAdvice分别是前置通知与返回后通知
public class BeforeLog implements MethodBeforeAdvice {
@Override
/*method要执行目标对象的方法
target目标对象
args参数
*/
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println(target.getClass().getName()+"的"+method.getName()+"被调用");
}
}
public class AfterLog implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("调用了"+method.getName()+" 返回值:"+returnValue);
}
}
定义了一个BeforeLog 和AfterLog类 ,它们是一个类,我们叫切面。
前置日志和后置日志这样的功能,他们不属于核心业务,属于横切关注点,
当这些切面下的方法(通知)切到核心业务逻辑方法的某个特定的位置如之前或者之后,就是aop面向切面的体现。
3.配置xml
- 注册bean
- 配置Aop
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--注册bean-->
<bean id="userManager" class="com.kxy.service.UserManagerImpl" name="manager"/>
<bean id="beforeLog" class="com.kxy.log.BeforeLog"/>
<bean id="afterLog" class="com.kxy.log.AfterLog"/>
<!--配置Aop-->
<aop:config>
<!--切入点-->
<aop:pointcut id="pointcut" expression="execution(* com.kxy.service.UserManagerImpl.*(..))"/>
<!--执行环绕通知-->
<aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
<aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
</aop:config>
</beans>
4.测试运行
以add方法和del方法为例
public class MyTest {
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("ApplicationBeans.xml");
UserManager userManagerImpl = (UserManager) context.getBean("manager");
userManagerImpl.addUser();
System.out.println("=============");
userManagerImpl.delUser();
}
}
其实质还是动态代理。所以AOP切面编程思想体现仍然是动态代理。
总结一下通过XML配置实现AOP切面编程的过程:
- 开启基于注解的AOP功能
- 将目标类和切面类加入到容器中 相当于注解@component
- 声明哪个类是切面类,相当于注解@Aspect
- 在配置文件中配置通知方法,告诉切面类中的方法都何时运行
方式二:基于自定义切面实现(aspect)
同样也是四步:
- 导入aspectj依赖
- 定义切面
- 配置xml
- 测试运行
2. 定义切面
public class DiyAspect {//切面
public void BeforeLog(){//通知
System.out.println("方法执行前打印日志......");
}
public void AfterLog(){
System.out.println("方法执行后打印日志......");
}
}
3.配置xml
<aop:config >Aop配置双标签下包裹<aop:pointcut >切入点,<aop:advisor >通知者 <aop:aspect >切面
<aop:aspect >切面标签,同样双标签,用来包裹通知如 <aop:before >
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--注册bean-->
<bean id="userManager" class="com.kxy.service.UserManagerImpl" name="manager"/>
<bean id="diyAspect" class="com.kxy.aspect.DiyAspect"/>
<aop:config>
<!--切入点-->
<aop:pointcut id="pointcut" expression="execution(* com.kxy.service.UserManagerImpl.*(..))"/>
<!--切面-->
<aop:aspect id="Aspect" ref="diyAspect">
<!--通知-->
<aop:before method="BeforeLog" pointcut-ref="pointcut"/>
<aop:after method="AfterLog" pointcut-ref="pointcut" />
</aop:aspect>
</aop:config>
</beans>
4.测试运行(代码在方式一.4)
运行结果:
很显然这种方式它不能够像基于spring的API例如MethodBeforeadvice通知的方式那样通过反射的确定方法名和具体类,所以它不够强大。
基于注解的AOP实现
1.开启aspectj的注解支持和component扫描
我们在基于xml的第二种方式自定义切面基础上,原来的接口和实现类不变,改变xml和切面类。开启了对component注解的扫描以及aspectj包下的注解支持。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--开启对扫描com.kxy下的所有component注解支持:关联xml-->
<context:component-scan base-package="com.kxy"/>
<!--开启对类似于before通知注解的支持:关联xml-->
<aop:aspectj-autoproxy/>
</beans>
把注解支持打开了,LogUtil下的@Component和@Aspect,@Before等才会生效
2.使用注解
@Component//LogUtil类注册Bean到Spring容器 相当于原xml里 <Bean id='logUtil' class=com.kxy.util.LogUtil/>
@Aspect//切面,相当于原来xml里的 <aop:aspect ref="logUtil"/>
public class LogUtil {//横切关注点:日志
@Pointcut("execution(* com.kxy.service.UserManagerImpl.*(..))")
public void pointcut() {
}
/*前置通知,相当于xml里的 <aop:before method="BeforeLog" pointcut-ref="pointcut"/>*/
@Before("pointcut()")
public void beforeLog(JoinPoint joinPoint) {
System.out.println("方法执行前打印日志...");
}
@After("pointcut()")
public void AfterLog(JoinPoint joinPoint) {
System.out.println("方法执行后打印日志...");
}
}
@Component:LogUtil类注册Bean到Spring容器 相当于原xml里
< Bean id='logUtil' class=com.kxy.util.LogUtil />
@Aspect:切面,相当于原来xml里的
<aop:aspect ref="logUtil"/>
@Before(“pointcut()”):前置通知,相当于xml里的
<aop:before method="BeforeLog" pointcut-ref="pointcut"/>*/
@Pointcut("execution(* com.kxy.service.UserManagerImpl.*(..))")
public void pointcut() {
}
等价于
<aop:pointcut id="pointcut" expression="execution(* com.kxy.service.UserManagerImpl.*(..))"/>
其实注解的方式,只是简化和优化了我们xml实现AOP的方式,本质上两者效果是一样的。
3.运行结果:
其他常用注解用法
@AfterReturning
@AfterReturning注解表示通知方法是在目标方法正常执行完之后执行的。
在返回通知中,只要将returning属性添加到@AfterReturning注解中,就可以访问连接点的返回值。returning属性的值即为用来传入返回值的参数名称,但是注意必须在通知方法的签名中添加一个同名参数。
在运行时Spring AOP会通过这个参数传递返回值,由于我们可能不知道返回值的类型,所以一般将返回值的类型设置为Object型。
切面下的返回后通知:
@AfterReturning(pointcut = "execution(* com.kxy.service.UserManagerImpl.*(..))",returning = "result")
public void LogReturn(JoinPoint joinPoint,Object result){
System.out.println("方法名:"+ joinPoint.getSignature().getName() + "返回值:" + result);
}
业务里:
public int checkLoginIn(){
System.out.println("UserManagerImpl.check");
return 1;
}
joinPoint.getSignature():获得方法签名
joinPoint.getSignature().getName:获得方法名
@AfterThrowing
我们需要将throwing属性添加到@AfterThrowing注解中,也可以访问连接点抛出的异常。Throwable是所有错误和异常类的顶级父类,所以在异常通知方法可以捕获到任何错误和异常。
如果只对某种特殊的异常类型感兴趣,可以将参数声明为其他异常的参数类型。然后通知就只在抛出这个类型及其子类的异常时才被执行。
@AfterThrowing(value = "pointcut()",throwing = "e")
public void LogError(JoinPoint joinPoint,Object e){
System.out.println("方法名为:"+ joinPoint.getSignature().getName() + "的异常为:" + e);
}
@Around
环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。
对于环绕通知来说,连接点的参数类型必须是ProceedingJoinPoint。它是 JoinPoint的子接口,允许控制何时执行,是否执行连接点。
在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法。如果忘记这样做就会导致通知被执行了,但目标方法没有被执行。这就意味着我们需要在方法中传入参数ProceedingJoinPoint来接收方法的各种信息。
注意:
环绕通知的方法需要返回目标方法执行之后的结果,即调用joinPoint.proceed();的返回值,否则会出现空指针异常。具体使用可以看下面这个实例:
/**
* 环绕通知方法
* 使用注解@Around()
* 需要在方法中传入参数proceedingJoinPoint 来接收方法的各种信息
* 使用环绕通知时需要使用proceed方法来执行方法
* 同时需要将值进行返回,环绕方法会将需要执行的方法进行放行
* *********************************************
* @throws Throwable
* */
@Around("public int com.spring.inpl.*.*(int, int)")
public Object MyAround(ProceedingJoinPoint pjp) throws Throwable {
// 获取到目标方法内部的参数
Object[] args = pjp.getArgs();
System.out.println("【方法执行前】");
// 获取到目标方法的签名
Signature signature = pjp.getSignature();
String name = signature.getName();
Object result= null;
try {
// 进行方法的执行
result= pjp.proceed();
System.out.println("方法返回时");
} catch (Exception e) {
System.out.println("方法异常时" + e);
}finally{
System.out.println("后置方法");
}
//将方法执行的返回值返回
return result;
}
4.通知注解的执行顺序
那么现在这五种通知注解的使用方法都已经介绍完了,
我们来总结一下这几个通知注解都在同一个目标方法中时的一个执行顺序。
在正常情况下执行:
@Before(前置通知)—>@After(后置通知)---->@AfterReturning(返回通知)
在异常情况下执行:
@Before(前置通知)—>@After(后置通知)---->@AfterThrowing(异常通知)
当普通通知和环绕通知同时执行时:
执行顺序是:
环绕前置----普通前置----环绕返回/异常----环绕后置----普通后置----普通返回/异常