Administrator
发布于 2022-09-29 / 69 阅读
0
0

SpringAOP细节

AOP细节一:容器中保存的是组件的代理对象

如果被代理对象实现了接口,使用接口类型获取

//细节一:AOP底层就是动态代理,容器中保存的组件是他的代理对象:$Proxy23,
// 所有用MyMathCalculator.class获取不到,因为他本身不是这个类型,他是其的代理对象

//通过接口获取
Calculator bean = ioc.getBean(Calculator.class);
bean.add(1,2);
System.out.println(bean);//com.atguigu.inter.impl.MyMathCalculator@2e1d27ba
System.out.println(bean.getClass());//class com.sun.proxy.$Proxy23

//可通过id还获取(首字母小写)
Calculator bean1 = (Calculator) ioc.getBean("myMathCalculator");
System.out.println(bean.getClass());//class com.sun.proxy.$Proxy23

如果被代理的对象没有实现接口,则使用本类类型获取

  //没有接口就是本类类型
  //若如此,CGLIB会帮我们创建好代理对象
  
  //MyMathCalculator bean2 = ioc.getBean(MyMathCalculator.class);
  MyMathCalculator bean2 = (MyMathCalculator) ioc.getBean("myMathCalculator");
    //class com.atguigu.inter.impl.MyMathCalculator$$EnhancerBySpringCGLIB$$5d8c2ee8
  System.out.println(bean2.getClass());

总结:有接口就转成接口类型(Calculator.class),没有接口就转成本类类型(MyMathCalculator.class)


AOP细节二:切入表达式的写法(通配符)

语法格式:
execution([权限修饰符] [返回值类型] [简单类名/全类名] [方法名]([参数列表]))

exp:
execution(public int com.atguigu.spring.ArithmeticCalculator.add(int,int))


	*的用法
  1、匹配一个或者多个字符 (MyMath*r)-->开头为MyMath,结尾为r
      ("execution(public int com.atguigu.inter.impl.MyMath*r.*(int,int))")
  2、匹配任意一个参数      *(int,*) --->任意一个方法,第一个参数为int,第二个为任意类型,有且仅有两个参数
      ("execution(public int com.atguigu.inter.impl.MyMathCalculator.*(int,*))")
  3、只能匹配一层路径      
  com.atguigu.inter.*.MyMathCalculator --->inter路径下任意路径下的MyMathCalculator
      ("execution(public int com.atguigu.inter.*.MyMathCalculator.*(int,int))")
  4、权限位置*不能写:权限位置不写就行,默认就是public

	..的用法	
    1、匹配任意多个参数,任意类型参数   *(..) --->任意方法任意参数
        ("execution(public int com.atguigu.inter.impl.MyMathCalculator.*(..))")
    2、匹配任意多层路径      impl..MyMath --->impl路径下的任意路径MyMath
        ("execution(public int com.atguigu.inter.impl..MyMath.*(int,int))") 

在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。相当于判断语句的用法

exp:
execution (* *.add(int,..)) || execution(* *.sub(int,..))

AOP细节三:执行顺序

try{
    @Before
    method.invoke(obj,args);
    @AfterReturning
}catch(e){
    @AfterThrowing:在目标方法抛异常之后
}finally{
    @after:在目标方法运行结束之后
}

Spring4的时候,执行顺序
正常执行情况:@Before(前置通知)—>@After(后置通知)—>@AfterReturning(正常返回)

异常执行情况:@Before(前置通知)—>@After(后置通知)—>@AfterThrowing(方法异常)

Spring5执行顺序
正常执行情况:@Before(前置通知)—>@AfterReturning(正常返回)—>@After(后置通知)

异常执行情况:@Before(前置通知)—>@AfterThrowing(方法异常)—>@After(后置通知)

AOP细节四:JoinPoint获取目标方法的详细信息

只需在通知方法的参数列表写上一个参数:JoinPoint
joinPoint:封装了当前目标方法的详细信息

image-1664441999442

AOP细节五:通过throwing、returning属性来接收结果返回值和异常信息


 //告诉Spring,用哪个参数接收返回值:returning = "result"
 @AfterReturning(value = "execution(public * com.lly.cal.MyMathCalculator.*(..))",returning = "result")
    public void logReturn(Object result) {
        System.out.println("返回通知执行----------result:" + result);
    }

 //告诉Spring,用哪个参数接收异常:throwing = "e" 
    @AfterThrowing(value = "execution(public * com.lly.cal.MyMathCalculator.*(..))",throwing = "e" )
    public void logexption(Exception e) {
        System.out.println("异常通知执行------"+e.getMessage());
    }

AOP细节六:Spring对通知方法的约束

Spring对通知方法的要求不严格:修改返回值;修改权限修饰符;去掉静态static都照样运行
但唯一有要求的是方法的参数列表一定不能乱写
原因:通知方法是Spring利用反射调用的,每次调用方法都需要确定这个方法的参数表的值;

参数表上的每一个参数,Spring都得知道是什么
即:参数列表上写的参数,一定要使用注解告诉Spring是什么?是returning;throwing等
ps:Object result参数的返回值类型会决定该方法能切的类型,如double result就
不切返回值是int等类型的方法,所以参数的返回值类型尽量往大的范围写。

AOP细节七:重用的切入点表达式使用@Pointcut注解

在AspectJ切面中,可以通过@Pointcut注解将一个切入点声明成简单的方法。切入点的方法体通常是空的,因为将切入点定义与应用程序逻辑混在一起是不合理的

在编写AspectJ切面时,可以直接在通知注解中书写切入点表达式。但同一个切点表达式可能会在多个通知中重复出现

步骤
1、随便声明一个没有实现的返回void的空方法

2、给方法上标注@Pointcut注解

3、在需要用到这个内容的注解execution属性内写入这个空方法的方法名
@Pointcut("execution(public * com.lly.cal.MyMathCalculator.*(..))")
   public void logPointcut(){}

AOP细节八:环绕通知

环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。

对于环绕通知来说,连接点的参数类型必须是ProceedingJoinPoint。它是 JoinPoint的子接口,
允许控制何时执行,是否执行连接点

在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法。
如果忘记这样做就会导致通知被执行了,但目标方法没有被执行。

注意:环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed();的
返回值,否则会出现空指针异常
    /***
     * @Around环绕:是Spring中强大的通知
     * try{
     *      //前置通知
     *      method.invoke(obj,args);
     *      //返回通知
     * }catch(e){
     *      //异常通知
     * }finally{
     *     //后置通知
     * }
     *
     * 四合一通知就是环绕通知
     *  环绕通知中有一个参数
     */
    @Around("logPointcut()")
    public Object Around(ProceedingJoinPoint pjp) throws Throwable {
        Object[] args = pjp.getArgs();
        String name = pjp.getSignature().getName();
        Object proceed = null;
        try {
            //@Before
            System.out.println("【环绕前置通知】【"+name+ "方法开始】");
            //利用反射调用目标方法,就是method.invoke(obj,args);
            proceed = pjp.proceed(args);
            //@AfterReturning
            System.out.println("【环绕返回通知】【"+name+ "方法返回,返回结果值为"+ proceed+"】");
        } catch (Exception e) {
            //@AfterThrowing
            System.out.println("【环绕异常通知】【"+name+ "方法异常,异常信息为"+ e+"】");
        } finally {
            //@After
            System.out.println("【环绕后置通知】【"+name+ "方法结束");
        }

        //反射调用后的返回值也一定返回出去,不返回会空指针
        return proceed;

    }

AOP细节九:环绕通知顺序&抛出异常让其他通知感受到

     *  Spring4的顺序:
     *      环绕前置
     *      普通前置
     *      目标方法执行
     *      环绕返回/异常
     *      环绕后置
     *      普通返回/异常
     *      普通后置
     *
     	Spring5:
     *      环绕前置
     *      普通前置
     *      目标方法执行
     *      普通返回/异常
     *      普通后置
     *      环绕返回/异常
     *      环绕后置

     */
    @Around("OniMyPoint()")
    public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
        Object[] args = pjp.getArgs();
        String name = pjp.getSignature().getName();
        //利用反射调用目标方法,就是method.invoke(obj,args);
        Object proceed = null;
        try {
            //@Before
            System.out.println("【环绕前置通知】【"+name+ "方法开始】");
            proceed = pjp.proceed(args);
            //@AfterReturning
            System.out.println("【环绕返回通知】【"+name+ "方法返回,返回结果值为"+ proceed+"】");
        } catch (Exception e) {
            //@AfterThrowing
            System.out.println("【环绕异常通知】【"+name+ "方法异常,异常信息为"+ e+"】");
            //为了让外界能知道这个异常,这个异常一定要抛出去
            throw new RuntimeException(e);
        } finally {
            //@After
            System.out.println("【环绕后置通知】【"+name+ "方法结束");
        }

        //反射调用后的返回值也一定返回出去
        return proceed;

    }

ps:如果只有一个切面类,里面写有环绕通知和普通通知,那么异常不用抛也是可以感知的
如果有多个切面类,如果环绕通知里不抛出异常,那么别的切面就无法感知

AOP细节十:多切面运行顺序

写在前面
@Order注解可以设置切面的优先级,数字越小优先级越高

image-1664452964002

	知道顺序和栈一样就行,先进后出,如图,清晰明了
奇怪的理解:
根据优先级执行切面(先选出log)---->log环绕前置执行-->  proceed = pjp.proceed(args);
---->log的普通前置---->log的前置执行完,到va的环绕前置---->proceed = pjp.proceed(args);
             ----> va普通前置---->目标方法……

AOP应用

1、AOP加日志保存到数据库中

2、AOP做权限验证

3、AOP做安全检查

4、AOP做事务控制

基于xml的AOP

如果是别的包有的切面类想引用,注解是无法引用得,那么就可以使用配置方法配置切面

web.xml

<!--基于注解的AOP步骤:
    1、将目标类、切面类加入到IOC容器中===>@Component
    2、告诉Spring哪个是切面类===>@Aspect
    3、在切面类中使用5个通知注解,来配置切面中的这些通知方法都何时何地运行
    4、在配置文件中开启基于注解的AOP功能 <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
-->


    <!--基于配置的AOP @Component-->
    <!--切面加入到容器中-->
    <bean id="myMathCalculator" class="com.atguigu.inter.impl.MyMathCalculator"></bean>
    <bean id="logUtils" class="com.atguigu.utlis.LogUtils"></bean>
    <bean id="validateApsect" class="com.atguigu.utlis.ValidateApsect"></bean>
    <!--使用AOP名称空间-->
    <aop:config>
        
        <!--指定切面 @Aspect-->
        <aop:aspect ref="logUtils">
            <aop:pointcut id="myPoint" expression="execution(* com.atguigu.inter.impl.*.*(..))"/>
            <!--配置哪个方法是前置通知 method属性:指定方法名
            logStart @Before("切入点表达式")
            -->
            <aop:before method="logStart" pointcut="execution(* com.atguigu.inter.impl.*.*(..))"></aop:before>
            <aop:after-returning method="logReturn" pointcut-ref="myPoint" returning="result"></aop:after-returning>
            <aop:after-throwing method="logException" pointcut-ref="myPoint" throwing="e"></aop:after-throwing>
            <aop:after method="logEnd" pointcut-ref="myPoint"></aop:after>
            <aop:around method="myAround" pointcut-ref="myPoint"></aop:around>
        </aop:aspect>
        
        <aop:aspect ref="validateApsect">
            <aop:pointcut id="myPoint" expression="execution(* com.atguigu.inter.impl.*.*(..))"/>
            <aop:before method="logStart" pointcut-ref="myPoint"></aop:before>
            <aop:after-returning method="logReturn" pointcut-ref="myPoint" returning="result"></aop:after-returning>
            <aop:after-throwing method="logException" pointcut-ref="myPoint" throwing="e"></aop:after-throwing>
            <aop:after method="logEnd" pointcut-ref="myPoint"></aop:after>
        </aop:aspect>      
    </aop:config>


评论