AOP场景
AOP(Aspect Oriented Programming):面向切面编程
OOP(Object Oriented Programming):面向对象编程
面向切面编程:基于OOP基础之上的新的编程思想
面向切面编程
指在程序运行期间,将某段代码动态的切入到指定方法的指定位置进行运行的这种编程方式,面向切面编程。辅助理解面向切面编程
场景:计算器运行计算方法的时候进行日志记录
日志记录:系统的辅助功能
业务逻辑:系统的核心功能
加日志记录:
1.直接编写在方法的内部,修改维护困难,造成日志记录与业务逻辑的耦合
2. 单独将日志抽取为一个类,一改全改,维护依旧困难,依旧耦合
我们希望的是:日志模块可以在核心功能运行期间,自己动态加上
使用动态代理来将代码动态的在目标执行前后先进行执行
- 难。
- jdk默认的动态代理,如果目标对象没有实现任何接口,则无法为目标添加动态代理
- 下面的代码是为Calculator 类实现的一个简单的动态代理
import inter.Calculator;
import demo.utils.LogUtils;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class CalculatorProxy {
// 传入了被代理对象:宝强
// 传出代理对象:宋喆
public static Calculator getProxy(final Calculator calculator) {
// 被代理对象的类加载器
ClassLoader loader = calculator.getClass().getClassLoader();
// 被代理对象实现的接口
Class<?>[] classes = calculator.getClass().getInterfaces();
// 方法执行器,她用来执行被代理对象的方法
InvocationHandler invocationHandler = new InvocationHandler() {
/*
* Object proxy:代理对象,给jdk使用,任何时候都不要使用这个对象
* Method method:当前要执行的目标对象的方法
* Object[] args:目标对象方法执行时,外界传给目标对象方法的参数值
* */
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 其返回值就是目标方法执行后的返回值,需要返回出去,外界才能拿到目标方法的执行后的返回值
Object result = null;
try {
// 前置通知,为了简化代码,将具体的通知操作(可以是一个输出语句或者可以是对数据库的操作)封装到了LogUtils类中
LogUtils.logStart(method, args);
// 利用反射执行目标方法
result = method.invoke(calculator, args);
// 返回通知
LogUtils.logReturn(method, result);
} catch (Exception e) {
// 异常通知
LogUtils.logException(method, e);
} finally {
// 后置通知
LogUtils.logEnd(method);
}
return result;
}
};
// 上面部分的代码是为了给Proxy.newProxyInstance准备相关参数
// 获取代理对象
Object proxy = Proxy.newProxyInstance(loader, classes, invocationHandler);
// 最后记得将结果作为返回值送出去
return (Calculator) proxy;
}
}
Spring动态代理
Spring实现了Aop功能;底层就是动态代理
- spring可以一句代码都不用创建动态代理
- 实现简单,没有强制要求目标对象必须实现接口
术语
横切关注点:想要在方法中进行日志记录的位置,
通知方法:被调用的日志记录方法
AOP使用步骤
-
导包:spring-aspects 基础包,
- 加强版:目标对象没有实现任何接口也能动态代理(下面3个都要)
- 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
- 上面3个依赖必须去全部导入,不然
-
写配置
-
将目标类和切面类(封装了通知方法(在目标方法执行前后执行的方法))加入到容器中,普通的@Component注解就可以
-
还应该告诉Spring到底是哪个类是切面类,使用@Aspect 注解
-
告诉Spring,切面类中的每一个方法都是何时何地执行。
- @Before:在目标方法之前运行,前置通知
- @After:在目标方法结束之后,抛出异常或者正常返回之后,后置通知
- @AfterReturning:在目标方法正常返回之后,返回通知
- @AfterThrowing:在目标方法抛出异常之后运行,异常通知
- @Around:环绕,后面再讲,环绕通知
-
还需要写入切入点表达式
// @Before("execution(访问权限符 返回值类型 方法签名)") // 方法签名就是方法的全类名,可以使用通配符 @Before("execution(public int com.hello.world.add(int,int))") @Before("execution(public int com.hello.world.*(int,int))")
-
如果使用xml配置(这里xml制作配置类的功能)的话,需要导入aop名称空间,再添加
或者在配置类中加入 @EnableAspectJAutoProxy 注解
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
-
-
测试:一定要从容器中取出组件,不要自己去new一个新的,
- 如果想要用类型获取,一定用他的接口类型,不要用本类
AOP细节
-
IOC容器中保存的是组件的代理对象,使用bean.getClass()可以获得代理对象的名字proxy
- AOP的底层就是动态代理,容器中保存的组件是他的代理对象
-
spring不会为接口类的组件创建对象。相当于只告诉ioc容器中有这种类型的组件
-
如果类没有实现接口,则ioc容器中保存的就是本类(实际上是CGLib帮我们创建好的代理对象)
-
通配符:
- *:匹配一个或多个字符、匹配任一个参数、匹配任意一层路径(*可以在任何位置,和正则表达式的用法一致,只有访问权限位置不能用)(public 可以省略)
- …:匹配任意数量、类型的参数、匹配多层路径
-
execute中可以使用逻辑运算符:&& 、!、||
-
通知的执行顺序:
- 正常执行:@Before——@After——@AfterReturning
- 异常执行:@Before——@After——@AfterThrowing
-
在通知方法运行时,拿到目标方法的详细信息
-
只需为通知方法的参数列表上写一个参数
JoinPoint joinPoint。里面有各种类型的方法来获取相关信息
-
想要获得返回结果或者错误信息怎么办呢?
-
我们的切入点表达式是execute的value属性,@AfterReturing 里面有result,@AfterThrowing 里面有throwing属性,来分别接受返回结果和异常信息。
-
我们需要获取相关信息时,只需在参数列表中添加相关参数(Object result,或者Exception e),并在对应的注解的参数列表中,给相关的属性传参数即可(通过给注解的属性传值,告诉spring,我们方法的参数都是干什么的)
-
(returning=“result”,result是之前在参数列表中添加的,专门接受目标方法返回结果的参数,的名字;或者throwing=“e”)。
@AfterReturning(value = "execution(public int demo.inter.MyMathCalculator.*(int,int))",returning = "result") public static void logReturn(Object result){ System.out.println("【】方法执行完毕,计算结果【"+result+"】");
-
-
-
spring通知方法的要求不严格,只要求方法的参数列表不能随便写,至于其他的都无所谓
- 原因:通知方法时spring通过反射调用的,每次方法调用得确定这个方法的参数表的值。参数表上的每一个参数,spring都得知道是什么。
-
这里有一个点需要说明:接受结果参数类型最好写较大的类,比如Object result可以接收各种类型的返回结果,而double result只能接收double类型的返回结果。接受异常的参数类型,尽可能写Exception,这样能接收到所有类型的异常,如果写NullPointerException,则只能接收到这个空指针这一特定类型的异常。
-
抽取可重用的切入点表达式:随便写个void方法,给其加@Pointcut 注解,并将切入点表达式传给他比如:@Pointcut(“execute(“com.hello.world.*”)”),之后其他方法的切入点表达式可以直接改为@Pointcut下方的方法名即可。
// 这样需要修改相关路径时就方便多了 @Pointcut("execution(public int demo.inter.MyMathCalculator.*(int,int))") public void hello(){ } @Before("hello()") public static void logStart(JoinPoint joinPoint){ // 获取目标方法参数列表 Object[] args = joinPoint.getArgs(); // 获取目标方法签名 Signature signature = joinPoint.getSignature(); String name = signature.getName(); System.out.println("【"+name+"】方法开始执行了,参数列表【"+ Arrays.asList(args) +"】");
-
AOP的异常通知只是提前捕获了异常信息,但并未对异常做特殊处理,此时程序依旧会报错(即使是想环绕通知中加了try-catch,程序依旧会因为错误而停止运行)。所以我们对可能会出错的程序段还需要额外进行处理
环绕通知
-
(@Around):spring中最强大的通知,是前置、后置、返回、异常通知四合一的通知(可以说,环绕通知就是一个动态代理)。
0里面有一个参数,里面有一个ProceedingJoinpoint,功能上类似于前面所说的 JoinPoint
-
环绕通知的执行顺序:
- 正常:前置通知——》返回通知——》后置通知
- 异常:前置通知——》异常通知——》后置通知
-
当一个切面类同时具有普通通知和环绕通知(二者在同一个切面类中)时,通知的执行顺序
- (环绕前置——》普通前置)——》目标方法——》普通后置——》环绕正常返回/出现异常——》 环绕后置——》普通正常返回/普通出现异常
- 括号中的两个通知的执行顺序可能会不一样
- (环绕前置——》普通前置)——》目标方法——》普通后置——》环绕正常返回/出现异常——》 环绕后置——》普通正常返回/普通出现异常
-
如果环绕通知中想要将异常抛出(比如和普通通知共存时,环绕通知必须把异常抛出,普通通知才能接受到异常信息)
@Around("hello()") public Object log(ProceedingJoinPoint pjp) throws Throwable { // 获得方法名 String name = pjp.getSignature().getName(); // proceed 后面用来接受目标方法执行后的返回结果 Object proceed=null; try { // 获得外界传给目标方法的参数列表 Object[] args = pjp.getArgs(); // 环绕前置通知 System.out.println("【环绕通知】:目标方法【"+name+"】开始执行,参数列表【"+ Arrays.asList(args) +"】"); // 就是利用反射调用目标方法,相当于method.invoke(obj,args), proceed = pjp.proceed(args); // 环绕返回通知 System.out.println("【环绕通知】,目标方法【"+name+"】返回,返回值【"+proceed+"】"); }catch (Exception e){ // 环绕异常通知 System.out.println("【环绕通知】,【"+name+"】出现异常,异常信息【"+e+"】"); // ************************这里需要将异常信息抛出********************************** throw new RuntimeException(e); }finally { // 环绕后置通知 System.out.println("【环绕通知】,【"+name+"】方法执行完毕"); } return proceed; }
-
普通通知只是在固定的位置进行通知,无法影响方法的运行,但是环绕通知却可以。如果需要在方法运行时,动态的影响方法,就使用环绕通知。
ObjectObject[] args = pjp.getArgs(); // 比如说,我们可以在这里对目标方法的参数进行改变 args[0]=200; Object proceed = pjp.proceed(args);
多个切面类,通知的执行顺序
-
当两个切面类都是普通通知时,“先进后出、后进先出”
-
假设有两个切面类 Logutils和VaAspect,
-
顺序:Logutils前置通知——》VaAspect前置通知——》VaAspect后置通知——》VaAspect正常返回通知/异常通知——》Logutils后置通知——》Logutils正常返回通知/异常通知
-
-
从图中可以更加清晰的理解,为什么是"先进后出、后进先出"这个顺序。先执行第一层LogUtils 在执行第二层VaAspect。
-
如果某个切面类中还有环绕通知,只会影响其所在切面类的层次内部的顺序,不会影响层次之间的执行顺序。
-
至于为什么是先执行Logutils的前置通知,而不是VaAspect的前置通知呢?spring会将切面类按照字母表进行排序,谁排在前面,第一个就先执行谁。
-
如果自己想要强制规定执行顺序的话,可以给切面类上面加@Order(int n)注解,传入的n的值越小,优先级越高,默认值是:2147483647
@Aspect @Component @Order(1) public class LogUtils { }
使用场景
- AOP加日志,保存到数据库
- AOP做权限验证
- AOP做安全检查
- AOP做事务控制
基于配置的AOP
注解相对来说更加快速方便,但是配置的功能更加完善,加入在使用第三方的jar包,此时注解无法使用,但是我们依旧可以使用xml 配置来实现我们想要的功能
重要的用配置,不重要的用注解;(通过这种方式来区分重要性,起码可以更方便的找到重要的部分在哪里)
- 其实基于配置和基于注解,只要会一个,另一个就没有什么难度了。二者都是同样的原理,只不过表现形式不同而已,只要知道了xml配置的通性,就完全可以从注解转化为xml配置
- 直接按照基于注解的步骤,一步步往下配置就可以
<?xml version="1.0" encoding="UTF-8"?>
<!--这里添加aop名称空间,其实不手动加也行,直接在下面输入<aop:config> 之后idea 会自动帮你导入对应名称空间-->
<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 id="myMathCalculator" class="demo.inter.MyMathCalculator"></bean>
<bean id="logUtils" class="demo.utils.LogUtils"></bean>
<bean id="aroundLog" class="demo.utils.AroundLog"></bean>
<!--需要aop名称空间-->
<aop:config>
<!--指定切面-->
<aop:aspect ref="logUtils">
<aop:pointcut id="haha" expression="execution(public int demo.inter.MyMathCalculator.*(int,int))"/>
<!--配置前置通知-->
<aop:before method="logStart" pointcut-ref="haha" />
<aop:after-returning method="logReturn" pointcut="execution(public int demo.inter.MyMathCalculator.*(int,int))" returning="result"/>
<aop:after method="logEnd" pointcut-ref="haha"/>
<aop:after-throwing method="logException" pointcut-ref="haha" throwing="e"/>
</aop:aspect>
<!--到这里一个切面类就配置好啦-->
<!--配置另一个切面类还是一样的步骤-->
</aop:config>
</beans>