【Spring基础系列5】Spring AOP基础(下)

简介: 看这篇文章前,请大家先看《【Spring基础系列5】Spring AOP基础(上)》,因为这两篇文章是自成体系,对于Spring AOP的基础知识,本来打算只写一篇,因为担心大家不愿意看长文,就拆成了上-下两篇文章。

KGFQCS8KLS02N2YVB]N~)KM.jpg

主要讲解AOP的实现姿势,包括AspectJ和Spring,以及自己对AOP内部实现的理解。


前言


看这篇文章前,请大家先看《【Spring基础系列5】Spring AOP基础(上)》,因为这两篇文章是自成体系,对于Spring AOP的基础知识,本来打算只写一篇,因为担心大家不愿意看长文,就拆成了上-下两篇文章。


Java中的AOP主要有4种实现方式,对于AspectJ基于XML的声明式,上一篇文章已经给出非常详细的示例,这篇文章主要针对AspectJ基于Annotation的声明式、基于Spring的JDK动态代理和CGLib动态代理这3种AOP实现方式进行讲解。

这篇文章也是Spring系列的最后一篇,前后花了近2周的时间总结的成果,如果想学习Spring的同学,建议从第一篇文章开始看:

  • 《【Spring基础系列1】基于注解装配Bean》
  • 《【Spring基础系列2】很全的Spring IOC基础知识》
  • 《【Spring基础系列3】Spring常用的注解》
  • 《【Spring基础系列4】注解@Transactional》
  • 《【Spring基础系列5】Spring AOP基础(上)》
  • 《【Spring基础系列5】Spring AOP基础(下)》


Spring AOP


Spring JDK动态代理

JDK 动态代理是通过 JDK 中的 java.lang.reflect.Proxy 类实现的。下面通过具体的案例演示 JDK 动态代理的使用,我们先新建一个接口,并给出具体实现类:

public interface CustomerDao {
    public void add(); // 添加
    public void update(); // 修改
    public void delete(); // 删除
    public void find(); // 查询
}
public class CustomerDaoImpl implements CustomerDao {
    @Override
    public void add() {
        System.out.println("添加客户...");
    }
    @Override
    public void update() {
        System.out.println("修改客户...");
    }
    @Override
    public void delete() {
        System.out.println("删除客户...");
    }
    @Override
    public void find() {
        System.out.println("修改客户...");
    }
}

新建一个切面类:

public class MyAspect {
    public void myBefore() {
        System.out.println("方法执行之前");
    }
    public void myAfter() {
        System.out.println("方法执行之后");
    }
}

再新建一个通过代理实现的工厂类:

public class MyBeanFactory {
    public static CustomerDao getBean() {
        // 准备目标类
        final CustomerDao customerDao = new CustomerDaoImpl();
        // 创建切面类实例
        final MyAspect myAspect = new MyAspect();
        // 使用代理类,进行增强
        return (CustomerDao) Proxy.newProxyInstance(
                // MyBeanFactory.class.getClassLoader(), // 这个也可以
                CustomerDao.class.getClassLoader(),
                new Class[] { CustomerDao.class }, new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        myAspect.myBefore(); // 前增强
                        Object obj = method.invoke(customerDao, args);
                        myAspect.myAfter(); // 后增强
                        return obj;
                    }
                });
    }
}

我使用MyBeanFactory.class.getClassLoader(),发现也不影响功能,后面有空再研究一下。

最后是测试示例:

public class JDKProxyTest {
    @Test
    public void test() {
        // 从工厂获得指定的内容(相当于spring获得,但此内容时代理对象)
        CustomerDao customerDao = MyBeanFactory.getBean();
        // 执行方法
        customerDao.add();
        customerDao.update();
        //customerDao.delete();
        //customerDao.find();
    }
}
// 输出:
// 方法执行之前
// 添加客户...
// 方法执行之后
// 方法执行之前
// 修改客户...
// 方法执行之后

从输出结果中可以看出,在调用目标类的方法前后,成功调用了增强的代码,由此说明,JDK 动态代理已经实现。


Spring CGLlB动态代理

JDK 动态代理使用起来非常简单,但是它也有一定的局限性,这是因为 JDK 动态代理必须要实现一个或多个接口,如果不希望实现接口,则可以使用 CGLIB 代理。

CGLIB(Code Generation Library)是一个高性能开源的代码生成包,它被许多 AOP 框架所使用,其底层是通过使用一个小而快的字节码处理框架 ASM(Java 字节码操控框架)转换字节码并生成新的类,因此 CGLIB 要依赖于 ASM 的包。

下面看一下CGLlB动态代理的实现姿势,先定义一个实现类:

public class GoodsDao {
    public void add() {
        System.out.println("添加商品...");
    }
    public void update() {
        System.out.println("修改商品...");
    }
    public void delete() {
        System.out.println("删除商品...");
    }
    public void find() {
        System.out.println("修改商品...");
    }
}

新建一个切面类:

public class MyAspect {
    public void myBefore() {
        System.out.println("方法执行之前");
    }
    public void myAfter() {
        System.out.println("方法执行之后");
    }
}

再新建一个通过代理实现工厂类:

public class MyBeanFactory {
    public static GoodsDao getBean() {
        // 准备目标类
        final GoodsDao goodsDao = new GoodsDao();
        // 创建切面类实例
        final MyAspect myAspect = new MyAspect();
        // 生成代理类,CGLIB在运行时,生成指定对象的子类,增强
        Enhancer enhancer = new Enhancer();
        // 确定需要增强的类
        enhancer.setSuperclass(goodsDao.getClass());
        // 添加回调函数
        enhancer.setCallback(new MethodInterceptor() {
            // intercept 相当于 jdk invoke,前三个参数与 jdk invoke—致
            @Override
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                myAspect.myBefore(); // 前增强
                Object obj = method.invoke(goodsDao, args); // 目标方法执行
                myAspect.myAfter(); // 后增强
                return obj;
            }
        });
        // 创建代理类
        GoodsDao goodsDaoProxy = (GoodsDao) enhancer.create();
        return goodsDaoProxy;
    }
}

最后是测试示例:

public class JDKProxyTest {
    @Test
    public void test() {
        // 从工厂获得指定的内容(相当于spring获得,但此内容时代理对象)
        GoodsDao goodsDao = MyBeanFactory.getBean();
        // 执行方法
        goodsDao.add();
        goodsDao.update();
        // goodsDao.delete();
        // goodsDao.find();
    }
}
// 输出:
// 方法执行之前
// 添加商品...
// 方法执行之后
// 方法执行之前
// 修改商品...
// 方法执行之后

从输出结果中可以看出,在调用目标类的方法前后,也成功调用了增强的代码,由此说明,使用 CGLIB 代理的方式同样实现了手动代理。


两者比较

可以直接参考文章《【Spring基础系列5】Spring AOP基础(上)》,再盗用上一篇文章的图,回顾一下:

image.gifJI1T[PI5U3YHRJ8OR$2FJ%0.png

重点:JDK动态代理是基于接口,CGLIB动态代理是基于类,上面的示例也能看出两者的区别,如果你在CGLIB代理的示例用接口替换,肯定会报错的。


使用ProxyFactoryBean创建AOP代理

这种方式我没有在项目中遇到过,仅作为扩展知识了解即可。

基础知识

上述已经讲解了 AOP 手动代理的两种方式,下面介绍 Spring 是如何创建 AOP 代理的。Spring 创建一个 AOP 代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean,这个类对应的切入点和通知提供了完整的控制能力,并可以生成指定的内容。

ProxyFactoryBean 类中的常用可配置属性如表所示:

image.gifF[ICNC66UVXVV[7PK`LGKIS.png


具体实例

我们还是先定义一个接口和一个实现类,CustomerDao、CustomerDaoImpl同“Spring JDK动态代理“示例内容一样,只是需要对CustomerDaoImpl加上注解@Component("customerDao"),主要是为了减少配置。

@Component("customerDao")
public class CustomerDaoImpl implements CustomerDao {
  // 方法同“Spring JDK动态代理“示例内容
}

然后定义一个切面类:

@Component
public class MyAspect implements MethodInterceptor {
    public Object invoke(MethodInvocation mi) throws Throwable {
        System.out.println("方法执行之前");
        Object obj = mi.proceed();
        System.out.println("方法执行之后");
        return obj;
    }
}

下面这个非常重要,就是在applicationContext.xml文件中增加相应配置:

<context:component-scan base-package="com.java.spring.aop.xml" />
<!--生成代理对象 -->
<bean id="customerDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
    <!--代理实现的接口 -->
    <property name="proxyInterfaces" value="com.java.spring.aop.xml.CustomerDao" />
    <!--代理的目标对象 -->
    <property name="target" ref="customerDao" />
    <!--用通知增强目标 -->
    <property name="interceptorNames" value="myAspect" />
    <!-- 如何生成代理,true:使用cglib; false :使用jdk动态代理 -->
    <property name="proxyTargetClass" value="true" />
</bean>

最后看一下测试用例:

public class FactoryBeanTest {
    @Test
    public void test() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        CustomerDao customerDao = (CustomerDao) applicationContext.getBean("customerDaoProxy");
        customerDao.add();
        customerDao.update();
        // customerDao.delete();
        // customerDao.find();
    }
}
// 输出:
// 方法执行之前
// 添加客户...
// 方法执行之后
// 方法执行之前
// 修改客户...
// 方法执行之后

这个和“Spring JDK动态代理”、“Spring CGLlB动态代理”的区别在于,前两者是手动方式,这个是自动方式,然后结合了“通知的分类”(可以参考《【Spring基础系列5】Spring AOP基础(上)》)。


AspectJ AOP


AspectJ 基于XML方式

直接参考文章《【Spring基础系列5】Spring AOP基础(上)》中的示例。


AspectJ 基于Annotation方式

基础知识

在 Spring 中,尽管使用 XML 配置文件可以实现 AOP 开发,但是如果所有的相关的配置都集中在配置文件中,势必会导致 XML 配置文件过于臃肿,从而给维护和升级带来一定的困难。

为此,AspectJ 框架为 AOP 开发提供了另一种开发方式——基于 Annotation 的声明式。AspectJ 允许使用注解定义切面、切入点和增强处理,而 Spring 框架则可以识别并根据这些注解生成 AOP 代理。关于 Annotation 注解的介绍如表所示:

image.gifUZ31(8%U~{DEJMY[0[U0{RG.png


具体实例

先定义一个具体的实现类:

@Component("customerDao")
public class CustomerDaoImpl {
    public void add() throws Exception {
        System.out.println("添加客户...");
        //throw new Exception("抛出异常测试");
    }
    public void update() {
        System.out.println("修改客户...");
    }
    public void delete() {
        System.out.println("删除客户...");
    }
    public void find() {
        System.out.println("修改客户...");
    }
}

然后定义一个切面类:

@Aspect
@Component
public class MyAspect {
    // 用于取代:<aop:pointcut expression="execution(* com.java.spring.aop.customer.*.*(..))" id="myPointCut"/>
    @Pointcut("execution(* com.java.spring.aop.customer.*.*(..))")
    private void myPointCut() {
    }
    // 前置通知
    @Before("myPointCut()")
    public void myBefore(JoinPoint joinPoint) {
        System.out.println("前置通知,方法名称:" + joinPoint.getSignature().getName());
    }
    // 后置通知
    @AfterReturning(value = "myPointCut()")
    public void myAfterReturning(JoinPoint joinPoint) {
        System.out.println("后置通知,方法名称:" + joinPoint.getSignature().getName());
    }
    // 环绕通知
    @Around("myPointCut()")
    public Object myAround(ProceedingJoinPoint proceedingJoinPoint)
            throws Throwable {
        System.out.println("环绕开始"); // 开始
        Object obj = proceedingJoinPoint.proceed(); // 执行当前目标方法
        System.out.println("环绕结束"); // 结束
        return obj;
    }
    // 异常通知
    @AfterThrowing(value = "myPointCut()", throwing = "e")
    public void myAfterThrowing(JoinPoint joinPoint, Throwable e) {
        System.out.println("异常通知,出错了");
    }
    // 最终通知
    @After("myPointCut()")
    public void myAfter() {
        System.out.println("最终通知");
    }
}

这里和“AspectJ 基于XML方式”一样,需要在applicationContext.xml文件中增加相应配置:

<context:component-scan base-package="com.java.spring.aop.customer" />
<context:component-scan base-package="com.java.spring.aop.annotation" />
<aop:aspectj-autoproxy proxy-target-class="true"/>

前面两行是自动注入注解的包,第三行是需要开启AOP,下面是测试用例:

public class AnnotationTest {
    @Test
    public void test() throws Exception {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        // 从spring容器获取实例
        CustomerDaoImpl customerDao = (CustomerDaoImpl) applicationContext.getBean("customerDao");
        // 执行方法
        customerDao.add();
    }
}
// 输出:
// 环绕开始
// 前置通知,方法名称:add
// 添加客户...
// 环绕结束
// 最终通知
// 后置通知,方法名称:add


AOP实现方式探讨


开始看“Spring JDK动态代理”的示例时,感觉这个示例非常熟悉,大家可以看看《【设计模式系列6】代理模式》这篇文章,其实就是通过代理模式进行前后增强,唯一的区别是“Spring JDK动态代理”中对代理的对象封装成一个工厂,所以是工厂模式 + 代理模式,感觉也没啥技术含量。

但是看到“AspectJ 基于XML方式”示例时,我们回顾一下它的测试用例:

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
CustomerDaoImpl customerDao = (CustomerDaoImpl) applicationContext.getBean("customerDao");
// 执行方法
customerDao.add();

大家有没有发现疑问,我们直接获取customerDao这个Bean,调用里面的add()方法,就能实现增强,这个是怎么实现的呢。我理解通过Spring拿到customerDao这个Bean,其实不是customerDao本身,而是customerDao的装饰类M,也就是这个装设类M对customerDao的方法进行了修饰,怎么修饰呢?就是通过切面方法进行修饰!

上面听得可能有点蒙,大家可以看文章《【设计模式系列7】装饰器模式》,我还是直接盗用里面的图:

}8UK~3`1UI`QXY`R}JJ[D%8.png

这里实体类是Chicken,装饰器是RoastFood,如果没有装饰器,调用cook()输出的是“鸡肉”,加上RoastFood装饰器修饰之后,调用cook()输出的是“烤鸡肉”,那么这个“烤”就是对“鸡肉”的装饰。

同理,CustomerDaoImpl对比Chicken,MyAspect对比“烤”,最后通过装饰器装饰后,将MyAspect中的方法装饰到CustomerDaoImpl中(《【设计模式系列7】装饰器模式》只在装饰类中定义了“烤”这个方法,你可以单独封装一个类N,里面封装一个“烤”方法,那么这个类N就可以对比为MyAspect)。

再回到我们上面的那个测试用例,应该就不难理解了,Spring先拿到CustomerDaoImpl类,这个类其实是通过MyAspect进行装饰,所以拿到的不是CustomerDaoImpl类本身,而是它的装饰器,最后将这个装饰器转成CustomerDaoImpl,至于为什么能转,是因为CustomerDaoImpl类和装饰器共同继承了CustomerDao接口。

总结:对于Spirng AOP,获取到的实体类其实不是实体类本身,而是这个实体类的装饰器,这个装饰器里应该是将实体类作为了装饰器的成员变量,然后通过切面MyAspect的方法进行增强,这里其实就是用到了装饰器模式。这个装饰器的获取,是通过工厂的封装,然后装饰器中实体类方法的调用,应该是采用的代理模式,所以Spirng AOP至少用到了装饰器、代理模式和工厂模式,然后完成整个功能的封装。

这个总结,仅是一家之言,可能有点天马行空,毕竟没有看AOP相关的源码和原理相关的文章,如果不是这样去设计AOP,那还有哪些其它的方法呢?目前我没有想到其它方式,如果文中有理解不对的地方,或者只是自己“一厢情愿”的话,欢迎大家给我指出!


后记


这篇文章就不写总结了,因为关于Spring AOP基础知识,我应该讲的还是比较详细,脉络也很清晰,就写点水文吧。


学习Spring相关的知识,总共学了10天,总结了6篇文章,虽然中间有几天状态不佳,但是整体学习的进度,我感觉还是不错的。下个系列我打算学习MyBatis,如果按照这个进度,MyBatis应该可以在6.20学习完毕。


学了Java也有一段时间,我也想说说我对Java的看法,我觉得Java真的就是一个工具,我学习Java的并发编程、Spring,感觉没有学到任何有关技术方面的东西,甚至可以说,学的东西和技术完全不靠边!学习Java语言生态的过程,其实就是学习Java的这一堆工具怎么使用,然后就没了,如果真说还学到其它什么知识,那可能就是编程的思想吧,因为Java的这些工具,用到了大量的设计模式,封装的功能确实非常通用,但是这个我其实只需要掌握到一定程度就够了,后面我还是需要在技术上深挖。


可能有同学会说,我学的还不够,是的,我也是刚开始学习Java,等我把MyBatis、SpringCloud、Dubbo等都学完了,那又能怎么样了,估计还是一堆工具。


不过话说话来,既然要去学习Java,这些工具我还是必须要掌握,不过等这堆工具我掌握到差不多的时候,我就不会再在上面花时间了,还是汲取些更优营养的知识,我才能更快成长。

相关文章
|
3月前
|
XML 安全 Java
使用 Spring 的 @Aspect 和 @Pointcut 注解简化面向方面的编程 (AOP)
面向方面编程(AOP)通过分离横切关注点,如日志、安全和事务,提升代码模块化与可维护性。Spring 提供了对 AOP 的强大支持,核心注解 `@Aspect` 和 `@Pointcut` 使得定义切面与切入点变得简洁直观。`@Aspect` 标记切面类,集中处理通用逻辑;`@Pointcut` 则通过表达式定义通知的应用位置,提高代码可读性与复用性。二者结合,使开发者能清晰划分业务逻辑与辅助功能,简化维护并提升系统灵活性。Spring AOP 借助代理机制实现运行时织入,与 Spring 容器无缝集成,支持依赖注入与声明式配置,是构建清晰、高内聚应用的理想选择。
436 0
|
2月前
|
XML Java 数据格式
《深入理解Spring》:AOP面向切面编程深度解析
Spring AOP通过代理模式实现面向切面编程,将日志、事务等横切关注点与业务逻辑分离。支持注解、XML和编程式配置,提供五种通知类型及丰富切点表达式,助力构建高内聚、低耦合的可维护系统。
|
7月前
|
监控 安全 Java
Spring AOP实现原理
本内容主要介绍了Spring AOP的核心概念、实现机制及代理生成流程。涵盖切面(Aspect)、连接点(Join Point)、通知(Advice)、切点(Pointcut)等关键概念,解析了JDK动态代理与CGLIB代理的原理及对比,并深入探讨了通知执行链路和责任链模式的应用。同时,详细分析了AspectJ注解驱动的AOP解析过程,包括切面识别、切点表达式匹配及通知适配为Advice的机制,帮助理解Spring AOP的工作原理与实现细节。
1126 13
|
4月前
|
人工智能 监控 安全
如何快速上手【Spring AOP】?核心应用实战(上篇)
哈喽大家好吖~欢迎来到Spring AOP系列教程的上篇 - 应用篇。在本篇,我们将专注于Spring AOP的实际应用,通过具体的代码示例和场景分析,帮助大家掌握AOP的使用方法和技巧。而在后续的下篇中,我们将深入探讨Spring AOP的实现原理和底层机制。 AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架中的核心特性之一,它能够帮助我们解决横切关注点(如日志记录、性能统计、安全控制、事务管理等)的问题,提高代码的模块化程度和复用性。
|
4月前
|
设计模式 Java 开发者
如何快速上手【Spring AOP】?从动态代理到源码剖析(下篇)
Spring AOP的实现本质上依赖于代理模式这一经典设计模式。代理模式通过引入代理对象作为目标对象的中间层,实现了对目标对象访问的控制与增强,其核心价值在于解耦核心业务逻辑与横切关注点。在框架设计中,这种模式广泛用于实现功能扩展(如远程调用、延迟加载)、行为拦截(如权限校验、异常处理)等场景,为系统提供了更高的灵活性和可维护性。
|
11月前
|
XML Java 开发者
Spring Boot中的AOP实现
Spring AOP(面向切面编程)允许开发者在不修改原有业务逻辑的情况下增强功能,基于代理模式拦截和增强方法调用。Spring Boot通过集成Spring AOP和AspectJ简化了AOP的使用,只需添加依赖并定义切面类。关键概念包括切面、通知和切点。切面类使用`@Aspect`和`@Component`注解标注,通知定义切面行为,切点定义应用位置。Spring Boot自动检测并创建代理对象,支持JDK动态代理和CGLIB代理。通过源码分析可深入了解其实现细节,优化应用功能。
530 6
|
10月前
|
XML Java 测试技术
Spring AOP—通知类型 和 切入点表达式 万字详解(通俗易懂)
Spring 第五节 AOP——切入点表达式 万字详解!
729 25
|
10月前
|
XML 安全 Java
Spring AOP—深入动态代理 万字详解(通俗易懂)
Spring 第四节 AOP——动态代理 万字详解!
448 24
|
9月前
|
Java API 微服务
微服务——SpringBoot使用归纳——Spring Boot中的切面AOP处理——Spring Boot 中的 AOP 处理
本文详细讲解了Spring Boot中的AOP(面向切面编程)处理方法。首先介绍如何引入AOP依赖,通过添加`spring-boot-starter-aop`实现。接着阐述了如何定义和实现AOP切面,包括常用注解如`@Aspect`、`@Pointcut`、`@Before`、`@After`、`@AfterReturning`和`@AfterThrowing`的使用场景与示例代码。通过这些注解,可以分别在方法执行前、后、返回时或抛出异常时插入自定义逻辑,从而实现功能增强或日志记录等操作。最后总结了AOP在实际项目中的重要作用,并提供了课程源码下载链接供进一步学习。
1095 0