【JavaWeb_Part09】面向接口编程?NO!我拒绝,我喜欢面向切面编程(AOP)。

本文深入讲解Spring框架中的AOP(面向切面编程)技术,包括AOP的基本概念、开发流程及通知类型的使用,并通过示例代码展示如何进行AOP配置。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

开篇

很久没有写文章了,记得上篇文章中说了一些 Spring 的入门以及 IOC 容器和 DI,今天难得有时间,就打算把剩下的一点 Spring 内容讲了。不能再拖了,虽然我是重度拖延症晚期患者。

Spring 测试

先说一下 Spring 中的测试功能吧,说起测试,肯定有很多朋友会想到单元测试(JUnit4),嗯。能够想到单元测试,这很棒。但是我们今天所讲的并不是单元测试,而是 Spring 与 单元测试集成后的测试功能,话不多说,直接干。

1. 导入 Spring 中的测试包

既然是 Spring 和单元测试集成,自然是会有一个集成包的,至于包嘛,在 Spring 的依赖文件件中。
这里写图片描述
就是这个目录下,将 jar 包直接拷贝到工程的lib 目录中。

2. 编写测试类 SpringTest.java

先看看之前我们进行测试的方式:

@Test
public void run(){
    //获取 applicationContext 对象
    ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
    //根据配置的 bean 标签中的 id 找到 UserService 的对象
    UserService userService = (UserService) applicationContext.getBean("userService");
    //调用 findAll 方法
    List<User> userList = userService.findAll();
    for (User user :
            userList) {
        System.out.println(user);
    }
}

以上就是我们以前的写法,如果我们要写 100 个测试方法,我们是不是就需要写下面的代码 100 次,显然不符合我们程序员“懒惰”的性格。

ClassPathXmlApplicationContext applicationContext = new             ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService) applicationContext.getBean("userService");

正是因为在 Spring 中单独利用 Junit4 做单元测试太复杂,所以 Spring 给我们提供了一个更加简便的测试方法。

如下

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpringTest {
    //Spring 和 JUnit4 整合后的测试
    @Autowired
    private UserService userService;
    @Test
    public void run2(){
        List<User> userList = userService.findAll();
        for (User user :
                userList) {
            System.out.println(user);
        }
    }
}

怎么样,是不是比上面的代码简单多了,配合上注解,几乎我们的测试方式已经简单到极致了。仅仅只需要两行注解,就可以搞定全部的单元测试,何乐而不为呢?

注意

这里我没有给出 UserService 以及 UserServiceImpl 中的代码,这两个 java 文件中的代码实在是太简单了,和上一篇文章中的代码一样,所以这里我就没有给出来,如果不明白的话,去 07 和 08 两篇文章中看一下。

电梯在这里:
【JavaWeb_Part07】功能堪比 502 的强大粘合剂?Spring(春天)框架表演秀
【JavaWeb_Part08】白娘子你就算淹了这深圳,我他妈也要去玩我的 Spring

Spring 中的 AOP

说到 AOP,可能有人一脸懵逼,就像下面这样。
这里写图片描述
what the fuck!AOP 又是个什么玩意?那就先来解释一下 AOP 是什么玩意吧!

1. 什么是AOP的技术?

  1. 在软件业,AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程。
  2. AOP 是一种编程范式,隶属于软工范畴,指导开发者如何组织程序结构。
  3. AOP 最早由 AOP 联盟的组织提出的,制定了一套规范。Spring 将 AOP 思想引入到框架中,必须遵守 AOP 联盟的规范。
  4. 通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
  5. AOP 是 OOP 的延续,是软件开发中的一个热点,也是 Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。
  6. 利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
  7. AOP 采取横向抽取机制,取代了传统纵向继承体系重复性代码(性能监视、事务管理、安全检查、缓存)。

上面都是一些概念性的东西,理解不了就算了。举个最通俗的例子吧,现在有两个类 UserDao 和 CustomerDao,如果我想要在执行 dao 层中的 save 方法之前进行一个打印日志的操作,聪明的你们可以动脑筋想一想,该怎么做?肯定有人想到了继承。嗯,继承确实是可以达到我们的目的,但是耦合度太高。况且这样也需要修改源代码。这种方法并不适合后期的维护,也不利于程序的健壮性。如果我们想要程序的耦合性不那么高,那么利用 aop 正好可以达到我们的要求。

看下面一张图。
这里写图片描述

图画的有点丑,以前我们的做法是 service 层操作 dao 层,而 aop 的思想就是在所有的 aop 层添加了一个切面,这个切面不依赖任何类。并且可以在所有的 dao 中的 save 方法执行之前,搞一些事情,这也正好可以满足我们的条件,也是面向切面编程的由来,也就是所谓的对程序进行了增强。至于我们为什么要学习 aop,主要还是因为 aop 可以在不修改源代码的前提下,对程序进行增强!

2. AOP 开发

2.1 添加 jar 包

既然是要学习 aop 的思想,基本的搭建环境这部分我就不做过多说明了。主要是讲解 aop。学习 spring 的核心功能之一,首要的还是要导入 spring 的 aop 相关的 jar 包。那么 aop 需要哪些 jar 包呢?
我们需要导入的 jar 包主要有下面四个,这四个 jar 包都是和 aop 相关的。

spring-aspects-4.2.4.RELEASE.jar 
spring-aop-4.2.4.RELEASE.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
com.springsource.org.aopalliance-1.0.0.jar
2.2 编写切面类 MyAspectJ.java
public class MyAspectJ {
    public void log(){
        System.out.println("保存日志");
    }
}
2.3 配置切面类以及切面切入点
<!-- 配置切面类-->
<bean id="myAspectJ" class="com.student.aop.MyAspectJ"/>

<!-- 配置切入点-->
<aop:config>
     <aop:aspect ref="myAspectJ">
         <aop:before method="log" pointcut="execution(public void com.student.mapper.UserDaoImpl.save(..))"/>
     </aop:aspect>
 </aop:config>
2.4 运行结果演示

这里写图片描述

可能有人对第三步有点不理解,先别懵逼。我们看一下结果,很明显是在保存用户之前调用了切面类中的保存日志方法,而切面类不依赖任何一个类。所以这即达到了我们的要求,同时也降低了程序的耦合性。

2.5 配置详解

对初学者来说,2.3 中的配置可能有点难以理解,下面我们对 2.3 中的配置做一个说明。反正又是一大堆名词。前面的 bean 标签我就不说了,都知道。不理解的估计也就是这段。

<!-- 配置通知类型以及切入点-->
<aop:before method="log" pointcut="execution(public void com.student.mapper.UserDaoImpl.save(..))"/>

一个一个来,先说 pointcut,这个是表示切入点,那什么叫切入点呢?比如说我们想在 save 方法执行之前,执行保存日志的功能,那么 save 方法就是切入点。至于后面的一大坨的表达式,这个就厉害了。

  1. execution() 这个是必须的,必须这么写,别跟老子讲为什么,老子也不知道为什么,你记住必须要这么写就对了。
  2. public 表示方法的修饰符,可以省略。
  3. void 表示返回值。
  4. com.student.mapper.UserDaoImpl.save() 表示方法的全路径名称。
  5. save(..) .. 表示任意参数。
    上面的写法可能有点复杂,那我们就可以简写成下面的。
execution(* *..*.*DaoImpl.save*(..)) //这个写法就厉害了,肯定会有人一脸懵逼

先执行一把看看结果再来解释。
这里写图片描述

切入点的表达式如下:

execution([修饰符] 返回值类型 包名.类名.方法名(参数))

结果是一样的,

这里我们省略了 public。访问修饰符是可以不写的。
第一个 * 代表的是返回值。*表示任意返回值。返回值不能不写,必须存在。
*..* 是简写了前面的包名 com.student.mapper,简写了这段。
*DaoImpl 表示任何以 DaoImpl 结尾的,比如UserDaoImplCustomerDaoImpl等等。
save* 表示以 save 开头的方法。
(..) 表示任意参数。

诶,理解起来也不是那么困难嘛。上面的一段就这么容易就解决了。既然可以在 xml中配置,那么是不是同样也可以用注解来简化切面的配置呢?咦,你怎么这么聪明,肯定是可以的呀。

2.6 使用注解简化 AOP 的配置

首先在 applicationContext.xml 中定义切面类。

<bean id="myAspectJ" class="com.student.aop.MyAspectJ"/>

然后在切面类上加上注解 AspectJ,同时在增强方法上面加上 @Before 通知注解。

@Aspect
public class MyAspectJ {

    @Before(value = "execution(* *..*.*DaoImpl.save*(..))")
    public void log(){
        System.out.println("保存日志");
    }
}

通知注解中的 value = “内容”,这里“内容”部分就是切入点的表达式。
切记别忘记了在 applicationContext.xml 中开启自动代理。

<!-- 开启 aop 的自动代理-->
<aop:aspectj-autoproxy/>

到这里,AOP 的注解开发就完成了。最后还有一个需要讲。可能有人不明白为什么要用 Before 这个注解,这个注解是干嘛用的?这就要说到通知类型了。Before 是我们的一种通知类型。

2.7 通知类型

为了便于测试,我又另外新添加了一个方法 update()。
注意,所有的注解配置我已经都在截图中给出来了,不要再问我为什么不贴出来配置文件。
前置通知(Before)

在目标类的方法执行之前执行

运行截图

这里写图片描述
可以看到“保存日志” 这个打印结果在 save 方法执行之前输出的。

环绕通知(Around)

方法的执行前后执行。
要注意:目标的方法默认不执行,需要借助 ProceedingJoinPoint 对来让目标对象的方法执行。

不信我们就看看图吧。

这里写图片描述

save 方法中的保存用户并没有输出,所以我们需要手动执行我们的目标方法。自己手动添加一个参数,然后看执行结果。

这里写图片描述

ProceedingJoinPoint  //这个对象的实例就可以帮我们调用目标方法去执行。因为要演示环绕通知,所以我就加了一行打印语句。

最终通知(After)

在目标类的方法执行之后执行,如果程序出现了异常,最终通知也会执行。

//自己在 save 方法里面造一个异常
@Override
public void save() {
   int i = 1 / 0;
   System.out.println("保存用户...");
}

运行截图

这里写图片描述
异常信息出来了,但是我们的保存日志也打印出来了。如果你换成其他的通知类型,保存日志是不会被打印的,如果不信邪,自己去尝试,我是尝试过了才敢这么说的。

后置通知(AfterReturning)

方法正常执行后的通知。

运行截图

这里写图片描述

在 save 方法执行完成后调用了“保存日志”的方法。

异常抛出通知(AfterThrowing)

在抛出异常后通知
这种异常抛出通知用的不多,所以我就不做过多的解释了。如果你们有兴趣,可以自己去尝试一下,自己动手,丰衣足食。

2.8 自定义切入点

除了像上面那样配置切入点外,我们还可以自己去定义一个通用的切入点。比如像下面这样

@Aspect
public class MyAspectJ {

    @Before(value = "MyAspectJ.fn()")
    public void log(){
        System.out.println("保存日志");
        System.out.println("保存日志2");
    }

    @Pointcut(value = "execution(public void com.student.mapper.UserDaoImpl.save())")
    public void fn(){

    }
}

其输出结果是一样的。
这里写图片描述

注意:如果自定义的切入点和通知方法在同一个类中,Before 注解里面的值可以是

MyAspectJ.fn()
fn()

这两者均可。

结尾

唉,说了准备把 Spring 中的内容全部讲完的,但是一个 AOP 讲了太多了,只能留到下篇文章了。(不是我想拖,主要是在事务估计也有 AOP 这么多,再写下去就如滔滔江水,一发不可收拾了)。所以还是下篇文章再讲吧。反正这篇文章什么都没有讲,就是讲了 aop,你们看起来也不用那么累。
最后注意一点,本人能力有限,写出来的东西纯属是个人对于 Spring 这个框架的理解。所以在记录和理解的过程中难免会出现错误,如果出现了错误,望指正。同时也欢迎小伙伴们留言与我交流。
看到这里的朋友可以回顾一下什么是 Spring IOC 容器以及依赖注入。不记得的自己去面壁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值