3.Spring AOP

Spring AOP

1.AOP的基本使用

(1)AOP的一些专业术语

try {
     方法开始
     Object result = method.invoke(target,args);
     方法正常返回
}catch{
     方法抛出异常
}finally {
     方法结束
}

切面类,横切关注点,通知方法,连接点,切入点,切入点表达式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sbmSxqSw-1686813171493)(C:\Users\10059\AppData\Roaming\Typora\typora-user-images\image-20220706160840737.png)]

(2)AOP的通知类型及对应注解

  1. 前置通知:@Before
  2. 后置通知:@AfterReturning
  3. 异常通知:@AfterException
  4. 结束通知:@After
  5. 环绕通知(⭐):@Around

(3)AOP的使用(注解方式)

注解名称解释
@Aspect来定义一个切面类。
@EnableAspectJAutoProxy导入AspectJAutoProxyRegistrar,把一些相关Bean注册到Spring中
@Pointcut用于定义切入点表达式。在使用时还需要定义一个包含名字和任意参数的方法签名来表示切入点名称,这个方法签名就是一个返回值为void,且方法体为空的普通方法。
@Before用于定义前置通知,相当于BeforeAdvice。在使用时,通常需要指定一个value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式)。
@AfterReturning用于定义后置通知,相当于AfterReturningAdvice。在使用时可以指定pointcut / value和returning属性,其中pointcut / value这两个属性的作用一样,都用于指定切入点表达式。
@After-Throwing用于定义异常通知来处理程序中未处理的异常,相当于ThrowAdvice。在使用时可指定pointcut / value和throwing属性。其中pointcut/value用于指定切入点表达式,而throwing属性值用于指定-一个形参名来表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法抛出的异常。
@After用于定义最终final 通知,不管是否异常,该通知都会执行。使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。
@Around用于定义环绕通知,相当于自定义类实现MethodInterceptor接口。在使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。

具体使用:

//真实对象接口
public interface IJdkProxyService {

    void doMethod1();

    String doMethod2();

    String doMethod3() throws Exception;
}
//真实对象
@Service
public class JdkProxyDemoServiceImpl implements IJdkProxyService {

    @Override
    public void doMethod1() {
        System.out.println("JdkProxyServiceImpl.doMethod1()");
    }

    @Override
    public String doMethod2() {
        System.out.println("JdkProxyServiceImpl.doMethod2()");
        return "hello world";
    }

    @Override
    public String doMethod3() throws Exception {
        System.out.println("JdkProxyServiceImpl.doMethod3()");
        throw new Exception("some exception");
    }
    
    @DataSource(value = "default")
    public String test(){
        
    }
}
//切面类
@Aspect//定义一个切面类
@Component//将切面类加入到ioc容器
@EnableAspectJAutoProxy//将相关bean注册到ioc容器
public class LogAspect {
    
    //JoinPoint joinPoint必须要有并且必须在参数列表第一个
    @Before("@annotation(ds)")
    public void beforeTest(JoinPoint joinPoint,DataSource ds){
        //这样可以获取到@DataSource的value值
    }

    /**
     * define point cut.
     */
    @Pointcut("execution(* tech.pdai.springframework.service.*.*(..))")
    private void pointCutMethod() {
    }


    /**
     * 环绕通知.
     *
     * @param pjp pjp
     * @return obj
     * @throws Throwable exception
     */
    @Around("pointCutMethod()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        String name = pjp.getSignature().getName();
        Object[] args = pjp.getArgs();
        Object proceed = null;
        try {
            System.out.println("环绕通知: 进入方法");;//相当于@Before
            if(args.length>0)
            	proceed = pjp.proceed(args);
            else
                proceed = pjp.proceed();
            System.out.println("【环绕返回】"+name+"方法返回值"+proceed);//相当于@AfterReturning
        } catch (Exception e) {
            System.out.println("【环绕异常】"+name+"方法异常信息"+e);//相当于@AfterThrowing
            throw new RuntimeException(e);//对于spring4.x来说。当切面类中的环绕通知和普通通知都切入时,由于在抛出异常时,【环绕异常】先捕获,而后【普通异常】就捕获不到了,所以把这个异常抛出去。如果是spring5.x就不用加这一句
        }finally {
            System.out.println("环绕通知: 退出方法");;//相当于@After
            return proceed;
        }
    }

    /**
     * 前置通知.
     */
    @Before("pointCutMethod()")
    public void doBefore() {
        System.out.println("前置通知");
    }


    /**
     * 后置通知.
     *
     * @param result return val
     */
    @AfterReturning(pointcut = "pointCutMethod()", returning = "result")
    public void doAfterReturning(String result) {
        System.out.println("后置通知, 返回值: " + result);
    }

    /**
     * 异常通知.
     *
     * @param e exception
     */
    @AfterThrowing(pointcut = "pointCutMethod()", throwing = "e")
    public void doAfterThrowing(Exception e) throws Exception {
        System.out.println("异常通知, 异常: " + e.getMessage());
        throw new Exception(e);//对于spring5.x来说。当切面类中的环绕通知和普通通知都切入时,由于在抛出异常时,【普通异常】先捕获,而后【环绕异常】就捕获不到了,所以把这个异常抛出去。如果是spring4.x就不用加这一句
    }

    /**
     * 最终通知.
     */
    @After("pointCutMethod()")
    public void doAfter() {
        System.out.println("最终通知");
    }

}
//结果
-----------------------
环绕通知: 进入方法
前置通知
JdkProxyServiceImpl.doMethod1()
环绕返回    
最终通知
环绕通知: 退出方法
-----------------------
环绕通知: 进入方法
前置通知
JdkProxyServiceImpl.doMethod2()
环绕返回, 返回值: hello world
后置通知, 返回值: hello world
最终通知
环绕通知: 退出方法
-----------------------
环绕通知: 进入方法
前置通知
JdkProxyServiceImpl.doMethod3()
环绕异常, 异常: some exception
异常通知, 异常: some exception
最终通知
环绕通知: 退出方法

环绕通知的一种常见用法:

//这个MethodInterceptor和代理模式用的cglib包不一样,cglib代理用的是net.sf.cglib.proxy.MethodInterceptor;
//但是org.aopalliance.intercept.MethodInterceptor也算cglib代理
import org.aopalliance.intercept.MethodInterceptor;

public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {

    private static final Log LOG = LogFactory.getLogger(DynamicDataSourceAnnotationInterceptor.class);

    /**
     * 缓存方法注解值
     */
    private static final Map<Method, String> METHOD_CACHE = new ConcurrentHashMap<>();

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        try {
            String datasource = determineDatasource(invocation);
            if (!DynamicDataSourceContextHolder.containsDataSource(datasource)) {
                LOG.info(LogProperty.LOGTYPE_DETAIL, MessageFormat.format("数据源[{0}]不存在,使用默认数据源 >", datasource));
            }
            DynamicDataSourceContextHolder.setDataSourceRouterKey(datasource);
            return invocation.proceed();
        } finally {
            DynamicDataSourceContextHolder.removeDataSourceRouterKey();
        }
    }

    private String determineDatasource(MethodInvocation invocation) {
        Method method = invocation.getMethod();
        if (METHOD_CACHE.containsKey(method)) {
            return METHOD_CACHE.get(method);
        } else {
            DataSource ds = method.isAnnotationPresent(DataSource.class) ? method.getAnnotation(DataSource.class)
                : AnnotationUtils.findAnnotation(method.getDeclaringClass(), DataSource.class);
            METHOD_CACHE.put(method, ds.value());
            return ds.value();
        }
    }

}

------------------------------------------------------------------------
@Configuration
public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor {

    private final Advice advice;

    private final Pointcut pointcut;

    public DynamicDataSourceAnnotationAdvisor() {
        this.advice = new DynamicDataSourceAnnotationInterceptor();
        this.pointcut = this.buildPointcut();
    }

    protected Pointcut buildPointcut() {
        Pointcut classPointcut = new AnnotationMatchingPointcut(DataSource.class, true);
        Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataSource.class, true);
        return new ComposablePointcut(classPointcut).union(methodPointcut);
    }

}    

(4)AOP使用注意事项

①切入点表达式怎么写

aop支持的所有切入点函数:https://blog.csdn.net/xubo_ob/article/details/78182014?spm=1001.2101.3001.6650.16&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EOPENSEARCH%7ERate-16-78182014-blog-125322577.pc_relevant_multi_platform_whitelistv4&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EOPENSEARCH%7ERate-16-78182014-blog-125322577.pc_relevant_multi_platform_whitelistv4&utm_relevant_index=20

execution(modifiers-pattern? ret-type-pattern name-pattern(param-pattern) throws-pattern?)

  • modifiers-pattern:修饰符类型,这个要么写,要么不写,不写代表不限制修饰符。这里不能使用占位符
  • ret-type-pattern:返回值类型【必填】
  • name-pattern:全类名+方法名【必填】
  • param-pattern:方法参数【必填】
  • throws-pattern:异常列表

占位符和逻辑运算符

  • 匹配一个或多个字符
  • 匹配一层路径
  • 匹配一个方法参数
  • 无法匹配修饰符类型
  • 匹配多层路径
  • 匹配多个方法参数
  • 无法匹配修饰符类型
  1. &&,||,!
②通知方法执行顺序(⭐注意spring5和spring4的顺序不同)

spring4:

image-20220707173005046

spring5:

  1. 单切面
image-20220707161645977
  1. 多切面(按切面类类名的字母大小写顺序排序,小的在前)

同一个方法被多个 Aspect 类拦截:https://my.oschina.net/u/3434392/blog/1625493

image-20220707161829034
③使用细节
  1. 默认为jdk代理,除非引cglib包。所以真实对象要实现一个接口
  2. 最终容器中不存在目标对象,存在的是代理对象
  3. 后置通知和异常通知的切面方法参数列表的类型=真实方法的返回类型/真实方法的异常类型

2.实现原理详解之AOP切面的实现

  1. IOC容器在初始化的时候,会解析出AOP的BeanDefinition,具体解析的实现是交给AnnotationAwareAspectJAutoProxyCreator,解析过程中直接开始bean的实例化**【注解方式都是这样子的,比如@Component】**
  2. 核心类:AbstractAutoProxyCreator(注解方式:子类AnnotationAwareAspectJAutoProxyCreator)
  • 实现了Bean级生命周期接口:BeanFactoryAware。实现了容器级生命周期接口:BeanPostProcessor
    • postProcessorBeforeInitialization方法
      • 总结:在ioc容器内判断各个类是否属于aop切面类(根据是否有@Aspect注解),将切面类中所有的通知方法根据注解转换成Advice对象,然后将Advice对象,切入点匹配器,切面类信息一起封装成Advisor对象(最终的切面类对象)然后放入缓存中
    • postProcessorAfterInitialization方法
      • 总结:将Advisor对象注入到合适位置(根据Advisor中的切入点匹配器),创建代理(jdk代理,cglib代理)

3.实现原理详解之AOP代理的实现

https://www.pdai.tech/md/spring/spring-x-framework-aop-source-2.html

SpringBoot1.x的AOP:默认jdk(和Spring AOP一样)

SpringBoot2.x的AOP:默认cglib,用户想要默认用jdk则要去配置proxyTargetClass

总结:Spring AOP默认在目标类实现接口时是通过JDK代理实现的,只有非接口的是通过Cglib代理实现的。还可以通过proxyTargetClass 配置(true or false)来决定用哪种方式。

  • 默认情况下 proxyTargetClass=false:
    • beanClass 是接口类型,且 beanClass 实现了一个合理(即不是空方法的接口)的接口,则使用 jdk proxy 来产生代理
    • beanClass 是接口类型,但是 beanClass 实现了一个不合理(空方法的接口)的接口,spring 会将 proxyTargetClass 校正为 true,最后使用 cglib 来产生代理
    • beanClass 不是接口类型,则使用cglib proxy 来产生代理
  • 如果用户指定了 proxyTargetClass=true
    • beanClass 是接口类型,则使用 jdk proxy
    • beanClass 不是接口类型,则使用 cglib
@RestController
@SpringBootApplication
public class AopApplication {
    @Resource
    ApplicationContext applicationContext;

    public static void main(String[] args) {
        // 将 proxy-target-class 设置为 true,默认走cglib代理
        System.setProperty("spring.aop.proxy-target-class", "true");
        SpringApplication app = new SpringApplication(AopApplication.class);
        app.run(args);
    }
}

pApplication {
@Resource
ApplicationContext applicationContext;

public static void main(String[] args) {
    // 将 proxy-target-class 设置为 true,默认走cglib代理
    System.setProperty("spring.aop.proxy-target-class", "true");
    SpringApplication app = new SpringApplication(AopApplication.class);
    app.run(args);
}

}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值