Loading...
墨滴

楼仔

2021/06/04  阅读:39  主题:橙心

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

主要讲解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基础(上)》,再盗用上一篇文章的图,回顾一下:

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

使用ProxyFactoryBean创建AOP代理

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

基础知识

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

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

具体实例

我们还是先定义一个接口和一个实现类,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 注解的介绍如表所示:

具体实例

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

@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】装饰器模式》,我还是直接盗用里面的图:

这里实体类是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,这些工具我还是必须要掌握,不过等这堆工具我掌握了,我就不会再在上花时间了,还是汲取些更优营养的知识,我才能更快成长。

楼仔

2021/06/04  阅读:39  主题:橙心

作者介绍

楼仔