开发者社区> pgnozxzkp4mgq> 正文

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

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
23552 0
阿里云服务器ECS远程登录用户名密码查询方法
阿里云服务器ECS远程连接登录输入用户名和密码,阿里云没有默认密码,如果购买时没设置需要先重置实例密码,Windows用户名是administrator,Linux账号是root,阿小云来详细说下阿里云服务器远程登录连接用户名和密码查询方法
22293 0
阿里云服务器安全组设置内网互通的方法
虽然0.0.0.0/0使用非常方便,但是发现很多同学使用它来做内网互通,这是有安全风险的,实例有可能会在经典网络被内网IP访问到。下面介绍一下四种安全的内网互联设置方法。 购买前请先:领取阿里云幸运券,有很多优惠,可到下文中领取。
22315 0
如何设置阿里云服务器安全组?阿里云安全组规则详细解说
阿里云安全组设置详细图文教程(收藏起来) 阿里云服务器安全组设置规则分享,阿里云服务器安全组如何放行端口设置教程。阿里云会要求客户设置安全组,如果不设置,阿里云会指定默认的安全组。那么,这个安全组是什么呢?顾名思义,就是为了服务器安全设置的。安全组其实就是一个虚拟的防火墙,可以让用户从端口、IP的维度来筛选对应服务器的访问者,从而形成一个云上的安全域。
19332 0
windows server 2008阿里云ECS服务器安全设置
最近我们Sinesafe安全公司在为客户使用阿里云ecs服务器做安全的过程中,发现服务器基础安全性都没有做。为了为站长们提供更加有效的安全基础解决方案,我们Sinesafe将对阿里云服务器win2008 系统进行基础安全部署实战过程! 比较重要的几部分 1.
11992 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,云吞铺子总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系统盘、创建快照、配置安全组等操作如何登录ECS云服务器控制台? 1、先登录到阿里云ECS服务器控制台 2、点击顶部的“控制台” 3、通过左侧栏,切换到“云服务器ECS”即可,如下图所示 通过ECS控制台的远程连接来登录到云服务器 阿里云ECS云服务器自带远程连接功能,使用该功能可以登录到云服务器,简单且方便,如下图:点击“远程连接”,第一次连接会自动生成6位数字密码,输入密码即可登录到云服务器上。
36385 0
126
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
JS零基础入门教程(上册)
立即下载
性能优化方法论
立即下载
手把手学习日志服务SLS,云启实验室实战指南
立即下载