spring揭秘11-aop05-aop应用经典场景及目标对象内部方法调用无法被拦截

【README】

本文总结自《spring揭秘》,作者王福强,非常棒的一本书,墙裂推荐;

本文总结了aop的3个经典应用场景(当然,aop的应用范围不止这3点):

  • 基于aop的全局异常处理;
  • 基于aop的权限检查;
  • 基于aop的缓存;


【1】基于aop实现全局异常处理

1)背景:业务逻辑在运行过程中会抛出运行时异常,如数据库连接不可用,http超时,空指针等;不同业务逻辑都需要定义异常处理逻辑,即便异常处理逻辑相同,即异常处理逻辑散落在各个业务逻辑中(散弹式代码);

  • 解决方法: 定义异常处理逻辑切面,在一个地方统一拦截系统异常,实现异常处理代码内聚。也可以拦截后在通知或横切逻辑内部做相关处理,如发送kafka日志,发送即时运维消息等;

【AopAppFaultBarrierMain】异常处理(故障屏障)切面测试main

public class AopAppFaultBarrierMain {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext container =
                new ClassPathXmlApplicationContext("chapter11/beans11aopappfaultbarrier.xml");

        // 获取目标对象的代理对象
        Object proxy = container.getBean("target");
        try {
            ((BusiFileReader) proxy).readFileName("abcd");
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

        // 获取目标对象的代理对象
        System.out.println("==== 我是分割线 ====");
        Object userDaoProxy = container.getBean("userDAO");
        try {
            ((UserDAO) userDaoProxy).qryUserNameById("");
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

【打印日志】

[@AfterThrowing]: faultBarrierHandleException
被切面拦截处理后的异常,原生异常消息=[文件不存在]
==== 我是分割线 ====
[@AfterThrowing]: faultBarrierHandleException
被切面拦截处理后的异常,原生异常消息=[用户id非法]

【beans11aopappfaultbarrier.xml】

<?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:context="http://www.springframework.org/schema/context"
       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/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">

    <!-- 使用 <aop:aspectj-autoproxy> 元素,用于替换 AnnotationAwareAspectJAutoProxyCreator 自动代理织入器 -->
    <aop:aspectj-autoproxy proxy-target-class="true" />

    <bean class="com.tom.springnote.chapter11.faultbarrier.FaultBarrierAspect" />
    <bean id="target" class="com.tom.springnote.chapter11.target.BusiFileReader" />
    <bean id="userDAO" class="com.tom.springnote.chapter11.target.UserDAO" />
</beans>

【FaultBarrierAspect】异常处理切面 (环绕通知)

@Aspect
public class FaultBarrierAspect {

    @AfterThrowing(pointcut = "execution(* *(..))", throwing = "e")
    public void faultBarrierHandleException(Exception e) {
        System.out.println("[@AfterThrowing]: faultBarrierHandleException");
        throw new RuntimeException("被切面拦截处理后的异常,原生异常消息=[" + e.getMessage() + "]", e);
    }
}

【BusiFileReader】目标类1

public class BusiFileReader {

    public String readFileName(String path) {
        File file = new File(path);
        if (file.exists()) {
            return file.getName();
        } else {
            throw new RuntimeException("文件不存在");
        }
    }
}

【UserDAO】目标类2

public class UserDAO {

    public String qryUserNameById(String userId) {
        if (!StringUtils.hasText(userId)) {
            throw new RuntimeException("用户id非法");
        }
        System.out.println("UserDAO#qryUserNameById() 方法被调用");
        return "张三" + userId;
    }
}

【补充:java异常】
补充1:java把Error子类或RuntimeException子类的所有异常称为非受查异常; 其他异常称为受查异常;( 也可以这样理解: 受查异常的根因通常是外部环境引起的,如文件不存在;而非受查异常根因是内部程序问题导致的,即程序一定有问题

补充2:受查指的是受编译器检查;即编译器在编译期间会检查所有受查异常是否都有异常处理器;如果没有,则程序编译不通过;

补充3:java异常层次结构:
在这里插入图片描述补充4:常见异常举例

  • 非受查异常举例
    • Error异常:
      • OutOfMemoryError
      • StackOverflowError
      • NoClassDefFoundError
    • RuntimeException异常:
      • NullPointerException
      • ArrayIndexOutOfBoundException
      • IllegalArgumentException
  • 受查异常举例
    • IOException
    • SQLException
    • ClassNotFoundException


【2】基于aop实现权限检查

1)背景:权限检查,包括但不限于安全检查,是一个web应用必须的功能; 如网关检查当前操作用户是否有操作权限,token解析是否成功等; 显然, 所有业务功能(对的就是所有业务功能)都需要权限检查,即权限检查是公共功能。 一般的,公共功能通过aop来实现,可以实现代码高内聚,且便于后续运维(如果某些方法不检查,仅需要修改pointcut表达式即可,而不用修改过多代码)

2)业务场景: 检查方法参数是否合法;不合法,抛出异常;

【AopAppSecurityCheckMain】

public class AopAppSecurityCheckMain {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext container =
                new ClassPathXmlApplicationContext("chapter11/beans11aopappchecksecurity.xml");

        // 获取目标对象的代理对象
        Object userDaoProxy = container.getBean("userDAO");
        try {
            ((UserDAO) userDaoProxy).qryUserNameById("");
        } catch (Exception e) {
            System.out.println("[main] " + e.getMessage());
        }
    }
}

【打印日志 】

[@Around] checkSecurity() 被调用
[main] 安全检查不通过: userId为空

【beans11aopappchecksecurity.xml】

<?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:context="http://www.springframework.org/schema/context"
       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/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">

    <!-- 使用 <aop:aspectj-autoproxy> 元素,用于替换 AnnotationAwareAspectJAutoProxyCreator 自动代理织入器 -->
    <aop:aspectj-autoproxy proxy-target-class="true" />

    <bean class="com.tom.springnote.chapter11.security.SecurityCheckAspect" />
    <bean class="com.tom.springnote.chapter11.faultbarrier.FaultBarrierAspect" />

    <bean id="userDAO" class="com.tom.springnote.chapter11.target.UserDAO" />
</beans>

【SecurityCheckAspect】

@Aspect
public class SecurityCheckAspect {

    @Around("execution(* *(..)) && args(userId)")
    public Object checkSecurity(ProceedingJoinPoint joinPoint, String userId) throws Throwable {
        System.out.println("[@Around] checkSecurity() 被调用");
        if (!StringUtils.hasText(userId)) {
            throw new RuntimeException("安全检查不通过: userId为空"); // 方法调用阻断,校验不通过,不继续调用目标对象方法
        }
        return joinPoint.proceed(); // 调用目标对象方法 
    }
}

【注意】

  • 有个问题:代理类(即切面类SecurityCheckAspect)抛出的异常,不会被切面类 FaultBarrierAspect 异常处理类捕获; 即便 FaultBarrierAspect 是全局异常处理切面,因为其pointcut = "execution(* *(…)) ,匹配所有方法;
  • 但需要注意的是,这里的所有方法指的是目标对象方法,不包括代理对象方法; 所以当代理对象方法 checkSecurity (如环绕通知方法)抛出异常,是不会被异常处理切面 FaultBarrierAspect 匹配到的


【3】基于aop实现缓存

1)aop另一个主要应用场景: 为系统透明添加缓存,缓存在很大程度上可以提升系统性能,缓存是系统需求,而不是业务功能需求;

【AopCacheAspectMain】aop缓存main

public class AopCacheAspectMain {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext container =
                new ClassPathXmlApplicationContext("chapter11/beans11aopappcache.xml");

        // 获取代理对象
        Object proxy = container.getBean("userDAO");
        ((UserDAO) proxy).qryUserNameById("01");
        ((UserDAO) proxy).qryUserNameById("01");
        ((UserDAO) proxy).qryUserNameById("02");
        ((UserDAO) proxy).qryUserNameById("01");
    }
}

【打印日志】 ( 虽然qryUserNameById方法调用了4次,但通过日志可以看到,实际上目标对象只调用了2次,因为有缓存

[@Around] userInfoCache()被调用
UserDAO#qryUserNameById() 方法被调用  // 实际第1次调用 
[@Around] userInfoCache()被调用
[@Around] userInfoCache()被调用 
UserDAO#qryUserNameById() 方法被调用  // 实际第2次调用 
[@Around] userInfoCache()被调用 

【beans11aopappcache.xml】

<?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:context="http://www.springframework.org/schema/context"
       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/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">

    <!-- 使用 <aop:aspectj-autoproxy> 元素,用于替换 AnnotationAwareAspectJAutoProxyCreator 自动代理织入器 --> 
    <aop:aspectj-autoproxy proxy-target-class="true" />

    <bean class="com.tom.springnote.chapter11.cache.CacheAspect" />
    <bean id="userDAO" class="com.tom.springnote.chapter11.target.UserDAO" />
</beans>

【CacheAspect】缓存切面

@Aspect
public class CacheAspect {

    private static final ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();

    @Around("args(usreId)")
    public Object userInfoCache(ProceedingJoinPoint joinPoint, String userId) throws Throwable {
        System.out.println("[@Around] userInfoCache()被调用");
        Object value = cache.get(userId);
        if (Objects.isNull(value)) {
            cache.putIfAbsent(userId, joinPoint.proceed());
        }
        return value;
    }
}


【4】aop无法拦截目标对象内部方法调用的问题

【4.1】问题概述

1)代理对象与目标对象:

  • 表面看,织入器把横切逻辑或通知织入到与pointcut匹配的目标对象(或目标方法);
  • 但实际上,横切逻辑或通知是织入到代理对象;代理对象与目标对象有着相同的方法(JDK动态代理实现aop,则代理对象与目标对象实现相同接口;CGLIB动态代理,则代理对象继承目标类 ); 调用代理对象的方法,代理对象根据通知类型执行横切逻辑(前置或后置),接着执行目标对象方法;即通过代理对象调用方法才会被拦截,而通过目标对象本身调用方法不会被拦截

2)通过代理对象调用方法才会被拦截,而通过目标对象本身调用方法不会被拦截;

  • 如 通过ProxyObject 调用方法(要被pointcut表达式匹配)会被拦截;
  • 而通过 targetObject.method()1 调用 target自身方法method2(),不会被拦截;(即便method2被pointcut表达式匹配)

在这里插入图片描述

3)aop不拦截目标对象内部方法之间调用

【NestedCallMain】目标对象内部方法调用测试main

public class NestedCallMain {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext container = new ClassPathXmlApplicationContext("chapter12/beans12nestedcall.xml");

        // 获取代理对象
        Object proxy = container.getBean("nestedCallTarget");
        ((NestedCallTarget) proxy).method1();
    }
}

【打印日志】

stopWatch.start()
NestedCallTarget#method1() 方法被调用
NestedCallTarget#method2() 方法被调用 // 显然,目标对象nestedCallTarget内部方法method1() 调用内部方法method2(),没有触发aop拦截
stopWatch.stop() 
方法执行耗时1.028E-4

显然,目标对象nestedCallTarget内部方法method1() 调用内部方法method2(),没有触发aop拦截

【beans12nestedcall.xml】

<?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:context="http://www.springframework.org/schema/context"
       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/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">

    <!-- 使用 <aop:aspectj-autoproxy> 元素,用于替换 AnnotationAwareAspectJAutoProxyCreator 自动代理织入器 --> 
    <aop:aspectj-autoproxy proxy-target-class="true" />

    <bean class="com.tom.springnote.chapter12.target.NestedCallAspect" />
    <bean id="nestedCallTarget" class="com.tom.springnote.chapter12.target.NestedCallTarget" />
</beans>

【NestedCallAspect】切面

@Aspect
public class NestedCallAspect {

    @Around("execution(* method1(..)) || execution(* method2(..))")
    public Object timeCost(ProceedingJoinPoint joinPoint) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        try {
            System.out.println("stopWatch.start()");
            stopWatch.start();
            return joinPoint.proceed();
        } finally {
            System.out.println("stopWatch.stop()");
            stopWatch.stop();
            System.out.printf("方法执行耗时%s\n", stopWatch.getTotalTime(TimeUnit.SECONDS));
        }
    }
}

【NestedCallTarget】目标对象 ( 内部方法method1() 调用内部方法method2() )

public class NestedCallTarget {

    public void method1() {
        System.out.println("NestedCallTarget#method1() 方法被调用");
        method2();
    }

    public void method2() {
        System.out.println("NestedCallTarget#method2() 方法被调用");
    }
}

【4.2】解决方法

1)目标对象内部方法method1() 调用 method2()时, 不调用目标对象的method2()方法,而调用代理对象的 method2()方法

class Target {
  method1() {
      proxy.method2();
  }    
} 

2)目标对象如何获取代理对象proxy ; 通过调用 AopContext.currentProxy() 来实现, 且同时设置织入器的 exposeProxy属性为true ;

【NestedCallTargetWithHoldProxyMain】持有代理对象的目标对象内部方法调用main

public class NestedCallTargetWithHoldProxyMain {
    public static void main(String[] args) {
        NestedCallTargetWithHoldProxy target = new NestedCallTargetWithHoldProxy();
        AspectJProxyFactory weaver = new AspectJProxyFactory(target);
        weaver.setExposeProxy(true); // 设置为true ,AopContext.currentProxy() 才生效 
        weaver.addAspect(NestedCallAspect.class); 

        // 获取代理对象
        Object proxy = weaver.getProxy();
        ((NestedCallTargetWithHoldProxy) proxy).method1();
    }
}

【打印日志】

stopWatch.start()
NestedCallTarget#method1() 方法被调用
stopWatch.start()
NestedCallTarget#method2() 方法被调用
stopWatch.stop()
方法执行耗时6.5E-5
stopWatch.stop()
方法执行耗时0.0029408

【NestedCallTargetWithHoldProxy】持有代理对象的内嵌调用目标类

public class NestedCallTargetWithHoldProxy {

    public void method1() {
        System.out.println("NestedCallTarget#method1() 方法被调用");
        NestedCallTargetWithHoldProxy proxy = (NestedCallTargetWithHoldProxy) AopContext.currentProxy();
        proxy.method2();
    }

    public void method2() {
        System.out.println("NestedCallTarget#method2() 方法被调用");
    }
}

【补充】当然,我们可以注入 NestedCallTargetWithHoldProxy代理对象(依赖)到 NestedCallTargetWithHoldProxy目标对象中;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值