【Spring】Spring核心概念AOP

本文详细介绍了Spring的AOP核心概念,包括连接点、切入点、通知等,并通过一个入门案例展示了如何使用注解实现方法执行前打印系统时间的功能。此外,还探讨了面向接口编程在Spring中的重要性,以及AOP的配置管理,如切入点表达式的使用。文章还阐述了AOP的五种通知类型,并展示了如何获取和处理通知中的参数、返回值及异常信息。

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

AOP介绍

Spring有两个核心概念,一个是IOC/DI,一个是AOP。AOP全称是Aspect Oriented Programming 即面向切面编程。它是一种编程范式,是一种编程思想。AOP的目的是在不惊动代码原始设计的基础上为其进行功能的增强。

AOP的核心概念有以下几点:

  1. 连接点:在Spring中实现类的所有方法被称为连接点。
  2. 切入点:实现类中需要被增强的方法称为切入点。
  3. 通知:通知类中的存放共性功能方法被称为通知。
  4. 通知类:通知方法所存在的类被称为通知类。
  5. 切面:通知和切入点之间的关系描述被称为通知。
  6. 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的。
  7. 代理(Proxy)目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现。

AOP的实现思路

  1. 导入坐标(pom.xml)
  2. 制作连接点(原始操作,Dao接口和实现类)
  3. 制作共性功能(通知类与通知)
  4. 定义切入点
  5. 绑定切入点与通知关系(切面)

AOP入门案例

需求分析:使用SpringAOP的注解方式完成在方法执行前打印出当前系统时间。

代码实现:

  1. 导入坐标,需要导入aspectjweaver以及spring-aop。其中spring-aop随着spring-context中,所以导入spring-context和aspectjweaver即可。
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>
  1. 制作连接点(原始操作,Dao接口和实现类)
package com.itheima.dao;

public interface BookDao {
    public void save();
    public void update();
}

package com.itheima.dao.impl;

import com.itheima.dao.BookDao;
import org.springframework.stereotype.Repository;

@Repository
public class BookDaoImpl implements BookDao {
    @Override
    public void save() {
        System.out.println(System.currentTimeMillis());
        System.out.println("save.....");
    }

    @Override
    public void update() {
        System.out.println("update....");
    }
}
  1. 创建aop包,在aop包下创建MyAdvice通知类,制作共性功能(通知类与通知)。
package com.itheima.aop;

public class MyAdvice {
    public void showTime(){
        System.out.println(System.currentTimeMillis());
    }
}
  1. 在通知类下定义切入点方法。权限必须为private 返回值为void 方法体为空。
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
  1. 绑定切入点与通知的关系,即获取切面
    @Before("pt()")
    public void showTime(){
        System.out.println(System.currentTimeMillis());
    }
  1. 使用注解将通知类加入到ioc容器,并申明为aspect。
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}

    @Before("pt()")
    public void showTime(){
        System.out.println(System.currentTimeMillis());
    }
}

  1. 在Spring配置类上添加注解,支持注解方式AOP
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}

至此便完成了SpringAOP入门,即使update方法体内没有申明打印系统时间的代码。但是我们通过AOP注解的方式,在不修改代码的基础上为update()方法做了功能增强。

学习总结
Spring中实现AOP的步骤如下:

  1. 导入依赖,spring-aop以及aspectjweaver的jar包
  2. 创建连接点,编写Dao层接口和实现类
  3. 创建通知类,在通知类上加@Component和@Aspect。表明此类是通知类并作为bean放入容器。
  4. 在通知类中使用@PointCut(“execution(void 包名.方法名(参数…))”)修饰一个private void 方法名(){}的方法,该方法即为切入点。
  5. 在通知类中使用@Before(“切入点方法名”)修饰通知方法。
  6. 最后在Spring配置类上添加支持使用注解实现AOP的注解@EnableAspectJAutoProxy

对Spring中面向接口编程的一些想法

在学习过Spring的IOC、ID以及AOP后,特别是使用注解完成Spring开发后。我对Spring中面向接口的印象越来越深。

面向接口编程的好处是程序结构清晰,并且降低了耦合。

第一次遇到面向接口在运行类中获取ApplicationContext对象,它的产生是可以通过new ClassPathXmlApplication(xxx.xml)也可以是new AnnotationConfigApplicationContext(SpringConfig.class)

第二次遇到是在使用注解开发IOC/DI的运行类中ctx.getBean(BookDao.class)的时候,在为BookDaoImpl上添加了@Repository注解后,我们既可以通过ctx.getBean(BookDao.class)获取BookDao对象,也可以通过ctx.getBean(BookDaoImpl.class)获取。但通常我们使用BookDao.class。有多个实现类的话

第三次是在使用注解完成AOP,在配置切入点的时候使用@Pointcut("execution(void com.itheima.dao.BookDao.update())")
并且在注解实现AOP时,运行类中不能使用ctx.getBean(BookDaoImpl.class)否则会报错。

总而言之,在Spring开发中,尽量使用面向接口编程。

AOP配置管理

AOP切入点表达式

  • 切入点:要进行增强的方法
  • 切入点表达式:要进行增强的方法的描述方式

在之前的学习中,对切入点的描述有两种方式。
第一种,执行接口中的无参数的方法。比如@Pointcut("execution(void com.itheima.dao.BookDao.update())")
第二种,执行实现类中的无参数方法。不如@Pointcut("execution(void com.itheima.dao.impl.BookDaoImpl.update())")
这样的描述方式有一个坏处。如果有很多切入点。那么就要编写很多和上面一样的切入点代码。这样非常的繁琐。通配符的出现使得不再需要书写大量切入点表达式。

通配符介绍
使用通配符描述切入点,主要的目的就是简化之前的配置。通配符具体有以下三种:

  1. * :单个单独的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
execution(public * com.itheima.*.UserService.find*(*))
  1. .. :多个连续的任意符号,可以独立出现,常用于简化包名和参数的书写
execution(User com..UserService.findById(..))
  1. + :专用于匹配子类类型
execution(* *..*Service+.*(..))

AOP通知类型

AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码也要将去加入到合理的位置。可以通过在通知方法上加注解的方式来选择位置。一共有5种通知类型。

  • 前置通知
  • 后置通知
  • 环绕通知(重点)
  • 返回后通知(了解)
  • 抛出异常后通知(了解)

代码实现:
使用@Before(“pt()”)实现前置通知,具体操作在前面AOP入门的获取切面部分。

使用@After(“pt()”)实现后置通知,操作与前置通知类似

使用@Around(“pt()”)实现环绕通知。重点!!!

    @Around("pt()")
    public void showTime(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice");
        //表示对原始操作进行调用
        pjp.proceed();
        System.out.println("around after advice");
    }

以上是对于没有返回值的切入点实现的环绕通知,对于有返回值的切入点,如果直接用以上代码会报错!!!查看报错原因的大概意思是:空的返回值不匹配原始方法的返回。

对于有返回值的方法,不能直接用以上的代码。而是需要根据原始方法的返回值来设置环绕通知的返回值。
具体的解决方案为:

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.*())")
    private void pt(){}

    @Around("pt()")
    public Object showTime(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice");
        //表示对原始操作进行调用
        Object ret = pjp.proceed();
        System.out.println("around after advice");
        return ret;
    }
}

使用@AfterReturning(“pt()”)实现返回后通知。如果原始方法有抛出异常,则不会执行

使用@AfterThrowing(“pt()”)实现抛出异常后通知。

AOP通知获取数据

目前我们从AOP仅仅是在原始方法的前后追加了一些操作,还没有涉及到对数据相关内容的操作。接下来我们将从获取参数获取返回值获取异常3个方面来研究切入点的相关信息。

获取切入点方法的参数,所有的通知类型都可以获取参数

  • JoinPoint:适用于前置、后置、返回后、抛出异常后通知
  • ProceedingJoinPoint:适用于环绕通知

获取切入点方法的返回值,前置和抛出异常是没有返回值,后置通知可有可无,所以不做研究。

  • 返回后通知
  • 环绕通知

获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究。

  • 抛出异常后通知
  • 环绕通知

获取参数

非环绕通知 获取方式:在方法的形参中添加JoinPoint,通过JoinPoint的对象来获取参数。代码如下(代码中通知类型为@After):

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.*())")
    private void pt(){}

    @After("pt()")
    public void showTime(JoinPoint jp) throws Throwable {
        System.out.println("advice");
        Object[] args = jp.getArgs();
        System.out.println(Arrays.toString(args));
    }
}

环绕通知获取方式:在通知方法的形参中添加ProceedingJoinPoint,调用其对象来获取参数

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.*())")
    private void pt(){}

    @Around("pt()")
    public void showTime(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("advice");
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        pjp.proceed();
    }
}

获取返回值类型
对于获取返回值,分为环绕通知获取返回值和返回后通知获取返回值。

使用环绕通知获取返回值的代码如下:

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.*())")
    private void pt(){}

    @Around("pt()")
    public Object showTime(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("advice");
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        Object ret = pjp.proceed();
        return ret;
    }
}

上述代码中,ret就是方法的返回值,如果有需要还可以对其进行修改。

使用返回后通知获取返回值的代码如下:

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.*())")
    private void pt(){}

    @AfterReturning(value = "pt()",returning = "ret")
    public void showTime(Object ret) throws Throwable {
        System.out.println("AfterReturning advice..."+ret);
    }
}

其中 @AfterReturning(value = "pt()",returning = "ret")中returning为ret,则 public void showTime(Object ret)参数名也必须为ret

获取异常

对于获取抛出的异常,只有抛出异常后通知环绕通知可以获取。

环绕通知获取异常

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.*())")
    private void pt(){}

    @Around("pt()")
    public Object showTime(ProceedingJoinPoint pjp)  {
        System.out.println("advice");
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        Object ret = null;
        try {
            ret = pjp.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return ret;
    }
}

使用try-catch的方式即可轻松获取异常。

抛出异常后通知获取异常

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.*())")
    private void pt(){}

    @AfterThrowing(value = "pt()",throwing = "t")
    public void showTime(Throwable t)  {
        System.out.println("afterThrowing advice..."+t);
    }
}

以上代码中@AfterThrowing(value = "pt()",throwing = "t")中throwing的值必须和showTime(Throwable t)中参数名一样。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值