Spring AOP 详解

个人博客地址:
http://xiaohe-blog.top

1. AOP概念

AOP :面向切面编程,全称 Aspect Oriented Programing,简而言之,在不惊动原始设计的基础上为其进行功能增强。

AOP本质就是Spring动态代理开发,通过代理类为原始类增加额外功能。我们知道,Spring动态代理就是将service层的附加业务抽取出来,让它专注核心业务的实现。

AOP中的名词 :切面、连接点、切入点、通知。

连接点 :JoinPoint,所有业务方法都是连接点。不管有没有添加额外功能。

切入点 :PointCut,真正添加了额外功能的方法。

通知 :Advice,额外功能(同样它也调用了核心业务)。

切面 :Aspect,将切入点和通知结合。

一定要分清连接点与切入点,业务方法都是连接点(可以连接的点)。但只有真正连接了额外方法才能叫作切入点。两者可以随时转变,我今天给一个业务增加额外功能,它叫切入点,明天我不给它加了,它就变回连接点了。

现在我们要实现一个项目 :对用户的增删改,增删的同时记录此次操作所用时间,改的时候不需要记录用时。

public interface UserService {
    public void add();
    public void delete();
    public void query();
}

public class UserServiceImpl implements UserService{
    @Autowired
    private UserMapper userMapper;
    public void add() {
        System.out.println("开始执行add方法");
        long start = System.currentTimeMillis();
        userMapper.addUser();
        long end = System.currentTimeMillis();
        long time = end - start;
        System.out.println("执行用时:" + time);
        System.out.println("add方法执行结束");
    }
    public void delete() {
        System.out.println("开始执行delete方法");
        userMapper.deleteUser();
        System.out.println("delete方法执行结束");
    }
    public void update() {
        System.out.println("开始执行update方法");
        userMapper.updateUser();
        System.out.println("update方法执行结束");
    }
}

现在add方法已经有记录时长的功能了,我们想要给delete加上这个功能,而update方法不需要这个功能。

在此处,连接点是 add、delete、update。而切入点是没有update的,因为你此时不需要为update添加额外功能。

image-20220826183111234

那么我们如何使用AOP给这些切入点绑定通知呢?

2. AOP开发

AOP编程的开发步骤 :

  1. 连接点 :编写核心业务。
  2. 通知 :编写附加业务
  3. 组装切面 :挑选切入点,使用切面将切入点与通知连接。

我们已经编写了连接点,只需要掌握通知与切面即可完成AOP。

2.1 切入点

我们已经确认了切入点是delete :

public void delete() {
    System.out.println("开始执行delete方法");
    userMapper.deleteUser();
    System.out.println("delete方法执行结束");
}

2.2 通知

AOP的通知有很多种 :

  • 前置通知 :通知先执行。
  • 后置通知 :通知后执行。
  • 环绕通知 :自己安排,想在哪就在哪。
  • 返回后通知
  • 异常后通知

这几种通知用的最多的是 环绕通知,环绕通知中的核心业务可以在任意时刻执行,完成诸如核心业务前开启事务,核心业务后关闭事务这样的操作。

为了防止战线拉的太长,笔者将通知与切入点表达式放到了文章末,尽量快点让大家接领会到什么是AOP

要实现环绕通知,要先了解一个接口 :MethodInterceptor。

image-20220825122806957

它只有一个方法 :invoke,届时我们的额外功能都会在此处编写。

MethodInvocation :核心业务,使用var1.proceed()可以调用核心业务。

返回值Object :invoke()返回值与核心业务的返回值相同。但是我们怎么知道核心业务返回什么呢?var1.proceed()执行的是核心业务,那么这个方法的返回值不就是核心业务的返回值吗 ?

所以我们可以将proceed的返回值返回 :

@Override
public Object invoke(MethodInvocation var1) throws Throwable {
    // 执行切入点的核心业务.
    Object proceed = var1.proceed();
    return procedd;
}

于是我们的通知就可以这样写 :

// 通知类
public class MyAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation var1) throws Throwable {
        System.out.println("通知开始执行");
        long start = System.currentTimeMillis();
        Object object = var1.proceed();
        long end = System.currentTimeMillis();
        long time = end - start;
        System.out.println("执行用时:" + time);
        System.out.println("通知执行结束");
        return object;
    }
}

2.3 切面

切面的目的是指定切入点并且绑定通知。这个过程可以使用注解形式完成,但是xml形式更加助于理解。

在Spring配置文件中,使用<aop: config>标签实现切面。它有两个子标签,<aop: pointcut> 和 <aop: advisor>。

<aop: pointcut> :用于指定切入点。

<aop: advisor> :用于绑定切入点与通知。

  1. 注入通知。
// 为什么将通知注入Spring?
// 我们想要spring替我们执行额外功能,肯定要配置啊。
<bean id="myAdvice" class="com.entity.MyAdvice"></bean>
  1. 实现切面,
<aop:config>
    <!-- 切入点pointcut-->
	<aop:pointcut id="pc" 
    	expression="execution(public void com.entity.UserServiceImpl.delete())">
    </aop:pointcut>	
    <!-- 通知,将通知与切入点连接-->
    <aop:advisor advice-ref="myAdvice" pointcut-ref="pc"></aop:advisor>
</aop:config>

(是不是觉得很麻烦 ?这已经很简单了,看到那个expression属性了没?它叫做“切入点表达式”,下面要花很大功夫讲它。)

这就是完整的aop开发步骤 :完成连接点、完成通知、编写切面。

3. AOP原理分析

上一节中使用getBean(“userService”)获得UserService的实现类还是UserServiceImpl吗?

很明显不是了,我们可以获取它,执行delete,可以看到执行的已经不是原本的delete方法,因为在我们看不见的地方,Spring给我们组装好了动态代理对象。

# 如果没有用到AOP,那么代码应该是这样的:
UserService userService = new UserServiceImpl();
# 用到了AOP,代码是这样的:
UserService userService = new UserServiceProxy();
public static void main(String[] args) {
    ApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserService userService = app.getBean("userService");
    userService.delete();
}
image-20220826185604652

image-20220827173316038

那么Spring创建的动态代理类在哪里呢 ?

Spring框架在运行时,通过动态字节码技术创建在JVM中,运行在JVM内部,等程序结束后,会和JVM一起消失。

JVM运行一个类其实就是运行这个.java文件加载后的.class文件(字节码文件)。

动态字节码技术不需要我们编写.java文件,它通过第三方框架直接动态生成代理对象的字节码文件。

AOP的底层实现就是Spring动态代理的实现。通过动态代理创建出一个同时具有 额外功能 + 核心业务 的代理类

如果你对动态代理技术不太了解,可以阅读这位大佬的博客 :http://t.csdn.cn/Z6x35

那么问题来了,为什么我们通过getBean(“userService”)获得的Bean对象是动态代理对象而不是原对象呢?

不仅是 getBean(“userService”),就连 getBean(UserServiceImpl.class) 都无法获取UserServiceImpl。

肯定是 Spring 做的,它是如何实现的?

在之前的学习中,我们接触到了 BeanPostProcessor 这个类,这个类在 Spring 创建 bean 对象之后执行。

它有两个方法,一个before、一个after,before方法在 bean 调用构造函数之后&初始化之前执行,after在bean初始化之后执行。

image-20220828193807403

Spring在 BeanPostProcessor 中的 After 方法将UserServiceImpl类改为UserServiceProxy 并返回给我们,所以我们获取的不是实现类而代理类。

image-20220828194659062

4. 通知

spring通知共5种 :

  1. 前置通知 Before :MethodBeforeAdvice
  2. 后置通知 After :AfterAdvice
  3. 环绕通知 Around :MethodInterceptor
  4. 返回后通知 After-returning :AfterReturningAdvice
  5. 异常后通知 After-throwing :ThrowsAdvice

共有5中,常用的其实就一个环绕通知。但是前置通知和异常后通知需要讲一下。

4.1 前置通知

接口为 MethodBeforeAdvice

image-20220826212845999

Method :切入点,你想给delete增加额外方法,Method就是delete。

Object[] :切入点的所有参数。delete的参数。

Object :切入点所在类的实例。delete在UserService中,那Object就是UserService。

下面动手完成AOP的前置通知 :

AOP编程的步骤是一成不变的 :完成连接点、完成通知、编写切面。

  • 连接点:
public void delete() {
    System.out.println("开始执行delete方法");
    userMapper.deleteUser();
    System.out.println("delete方法执行结束");
}
  • 前置通知
public class MyBeforeAdvice implements MethodBeforeAdvice {
    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("前置通知执行...");
    }
}
  • 编写切面
<bean id="myBeforeAdvice" class="com.entity.MyBeforeAdvice"></bean>

<aop:config>
	<aop:pointcut id="pc" 
    	expression="execution(public void com.entity.UserServiceImpl.delete())">
    </aop:pointcut>	
    <aop:advisor advice-ref="myBeforeAdvice" pointcut-ref="pc"></aop:advisor>
</aop:config>

以后的拦截器跟这个原理很像。

4.2 异常后通知

接口为 ThrowsAdvice。其中没有方法,需要我们自定义

public class MyExceptionAdvice implements ThrowsAdvice {
	public void afterThrowing(Exception ex) {
		System.out.println("异常信息 :"+ex.getMessage());
    }
}

以后的全局异常处理器跟这个原理很像。

5. 切入点表达式

刚才咱们在切面中写的代码是 :

expression="execution(public void com.entity.UserServiceImpl.delete())"

这个就是切入点表达式,试想一下,如果我们还有一个save、一个add需要添加这个通知,我们该怎么写?总不能再配置一个吧。这时就要学习切面表达式了。

5.1 方法切入点

方法切入点表达式由两部分组成 :修饰符+返回值、方法名(参数)。

如果我们想要给所有方法加上通知,该怎么做?使用通配符 *

因为分为两个部分,那么就是用两个 *,参数用两个 …代替,即为任意参数

image-20220826224857756

定义所有login作为切入点

* login(..)

指定有两个String形参的所有login作为切入点

* login(String, String)
// 注意: 非java.lang包中的类型必须写全限定类名* login(com.xiaohe.User)

指定第一个参数为String,其他参数随意的所有login作为切入点

* login(String, ..)

4.2 类切入点

指定整个类为切入点,它的全部方法都会成为切入点。

* com.service.UserServiceImpl.*(..)

4.3 包切入点

指定整个包为切入点,这个包中的所有类的所有方法都成为切入点。

* com.service.*.*()

太累了,写不下去了,种地去了。

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值