[Java]Spring-Day 3

[Java]Spring-Day3

学习使用工具

黑马2023新版Java视频教程 https://www.bilibili.com/video/BV1Fv4y1q7ZH?p=8&vd_source=03da0cdb826d78c565cd22a83928f4c2

Java程序员进阶之路 https://tobebetterjavaer.com/overview/java-can-do-what.html

黑马程序员SSM框架教程 https://www.bilibili.com/video/BV1Fi4y1S7ix?p=2&vd_source=03da0cdb826d78c565cd22a83928f4c2

一、AOP简介

Spring有两个核心的概念,一个是IOC/DI,一个是AOP。前面已经对IOC/DI进行了系统的学习,接下来要学习它的另一个核心内容,就是AOP。

  • 定义:面向切面编程,一种编程范式,指导开发者如何组织程序结构。
  • 特点:在不改原有代码的前提下对其进行增强。
  • 核心概念
    • 连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
      • 在SpringAOP中,理解为方法的执行
    • 切入点(Pointcut):匹配连接点的式子
      • 在SpringAOP中,一个切入点可以描述一个具体方法,也可也匹配多个方法
        • 一个具体的方法:如com.itheima.dao包下的BookDao接口中的无形参无返回值的save方法
        • 匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法
      • 连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一定要被增强,所以可能不是切入点。
    • 通知(Advice):在切入点处执行的操作,也就是共性功能
      • 在SpringAOP中,功能最终以方法的形式呈现
    • 通知类:定义通知的类
    • 切面(Aspect):描述通知与切入点的对应关系。

二、AOP入门案例

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

  2. 思路分析

    • 导入坐标(pom.xml)
    • 制作连接点(原始操作,Dao接口与实现类)
    • 制作共性功能(通知类与通知)
    • 定义切入点
    • 绑定切入点与通知关系(切面)
  3. 环境准备

    1. BookDao和BookDaoImpl类。save方法中已有输出当前系统时间的代码,update方法则没有。

      public interface BookDao {
          public void save();
          public void update();
      }
      
      @Repository
      public class BookDaoImpl implements BookDao {
      
          public void save() {
              System.out.println(System.currentTimeMillis());
              System.out.println("book dao save ...");
          }
      
          public void update(){
              System.out.println("book dao update ...");
          }
      }
      
    2. Spring配置类

      @Configuration
      @ComponentScan("com.itheima")
      public class SpringConfig {
      }
      
    3. App运行类

      public class App {
          public static void main(String[] args) {
              ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
              BookDao bookDao = ctx.getBean(BookDao.class);
              bookDao.save();
          }
      }
      
  4. AOP实现步骤

    1. 在pom.xml中添加依赖项,导入AspectJ。AspectJ是AOP思想的一个具体实现,Spring有自己的AOP实现,但是相比于AspectJ来说比较麻烦,所以我们直接采用Spring整合ApsectJ的方式进行AOP开发。

    2. 定义通知类和通知

      通知就是将共性功能抽取出来后形成的方法,共性功能指的就是当前系统时间的打印。

      public class MyAdvice {
          public void method(){
              System.out.println(System.currentTimeMillis());
          }
      }
      
    3. 定义切入点

      public class MyAdvice {
          @Pointcut("execution(void com.itheima.dao.BookDao.update())")
          private void pt(){}
          public void method(){
              System.out.println(System.currentTimeMillis());
          }
      }
      

      切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑。

    4. 制作切面

      切面用来描述通知和切入点之间的关系。@Before表示通知会在切入点方法执行之前执行。

      public class MyAdvice {
          @Pointcut("execution(void com.itheima.dao.BookDao.update())")
          private void pt(){}
          
          @Before("pt()")
          public void method(){
              System.out.println(System.currentTimeMillis());
          }
      }
      
    5. 将通知类配给容器并标识为切面类

      @Component
      @Aspect
      public class MyAdvice {
          @Pointcut("execution(void com.itheima.dao.BookDao.update())")
          private void pt(){}
          
          @Before("pt()")
          public void method(){
              System.out.println(System.currentTimeMillis());
          }
      }
      
    6. 开启注解格式AOP功能

      @Configuration
      @ComponentScan("com.itheima")
      @EnableAspectJAutoProxy
      public class SpringConfig {
      }
      

三、AOP工作流程与核心概念

  • 工作流程
    1. 容器启动
    2. 读取所有切面配置中的切入点
    3. 初始化bean,判定bean对应的类中的方法是否匹配到任意切入点。匹配失败,创建原始对象。匹配成功,创建原始对象(目标对象)的代理对象。因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象,最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强。
    4. 获取bean执行方法
  • 核心概念
    • 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的。也叫原始对象,它在运行的过程中对于要增强的内容是缺失的。
    • 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现。SpringAOP是在不改变原有设计(代码)的前提下对其进行增强的,它的底层采用的是代理模式实现的,所以要对原始对象进行增强,就需要对原始对象创建代理对象。

四、AOP配置管理

  1. AOP切入点表达式

    @Pointcut("execution(void com.itheima.dao.BookDao.save())")
    
    execution(public User com.itheima.service.UserService.findById(int))
    
    • execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
    • public:访问修饰符,还可以是public,private等,可以省略
    • User:返回值,写返回值类型
    • com.itheima.service:包名,多级包使用点连接
    • UserService:类/接口名称
    • findById:方法名
    • int:参数,直接写参数的类型,多个类型用逗号隔开
    • 异常名:方法定义中抛出指定异常,可以省略
  2. 通配符

    如果每一个方法对应一个切入点表达式,编写起来会比较麻烦。使用通配符可以简化配置描述。

    • *:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现

      execution(public * com.itheima.*.UserService.find*(*))
      

      匹配com.itheima包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法

    • ..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写

      execution(public User com..UserService.findById(..))
      

      匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法

    • +:专用于匹配子类类型

      execution(* *..*Service+.*(..))
      

      使用率较低,描述子类的。*Service+,表示所有以Service结尾的接口的子类。

    execution(* com.itheima.dao.impl.BookDaoImpl.update(*))
    返回值任意,但是update方法必须要有一个参数
    execution(void com.*.*.*.*.update())
    返回值为void,com包下的任意包三层包下的任意类的update方法,匹配到的是实现类
    execution(void com.*.*.*.update())
    返回值为void,com包下的任意两层包下的任意类的update方法,匹配到的是接口
    execution(void *..update())
    返回值为void,方法名是update的任意包下的任意类,能匹配
    execution(* *..*(..))
    匹配项目中任意类的任意方法,能匹配,但是不建议使用这种方式,影响范围广
    execution(* *..u*(..))
    匹配项目中任意包任意类下只要以u开头的方法
    execution(* *..*e(..))
    匹配项目中任意包任意类下只要以e结尾的方法
    execution(void com..*())
    返回值为void,com包下的任意包任意类任意方法,能匹配,*代表的是方法
    execution(* com.itheima.*.*Service.find*(..))
    将项目中所有业务层方法的以find开头的方法匹配
    execution(* com.itheima.*.*Service.save*(..))
    将项目中所有业务层方法的以save开头的方法匹配
    
    • 描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了
    • 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述
    • 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
    • 包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配
    • 接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名
    • 方法名书写以动词进行精准匹配,名词采用匹配,例如getById书写成getBy,selectAll书写成selectAll
    • 参数规则较为复杂,根据业务方法灵活调整
    • 通常不使用异常作为匹配规则
  3. 类型介绍

    5种通知类型:

    • 前置通知@Before("pt()"):追加功能到方法执行前,类似于在代码1或者代码2添加内容
    • 后置通知 @After("pt()"):追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码5添加内容
    • 环绕通知 @Around("pt()"):环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式,它可以实现其他四种通知类型的功能。
    • 返回后通知@AfterReturning("pt2()"):追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,如果方法执行抛出异常,返回后通知将不会被添加
    • 抛出异常后通知@AfterThrowing("pt2()"):追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码4添加内容,只有方法抛出异常后才会被添加

    1630166147697

  4. 环绕通知

    @Component
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(void com.itheima.dao.BookDao.update())")
        private void pt(){}
        
        @Around("pt()")
        public void around(){
            System.out.println("around before advice ...");
            System.out.println("around after advice ...");
        }
    }
    
    public interface BookDao {
        public void update();
    }
    
    @Repository
    public class BookDaoImpl implements BookDao {
        public void update(){
            System.out.println("book dao update ...");
        }
    }
    

    此时运行,会发现通知的内容打印出来,但是原始方法的内容却没有被执行。

    因为环绕通知需要在原始方法的前后进行增强,所以环绕通知就必须要能对原始操作进行调用。因此添加对原始操作的调用。

    @Component
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(void com.itheima.dao.BookDao.update())")
        private void pt(){}
        
        @Around("pt()")
        public void around(ProceedingJoinPoint pjp) throws Throwable{
            System.out.println("around before advice ...");
            //表示对原始操作的调用
            pjp.proceed();
            System.out.println("around after advice ...");
        }
    }
    
    • 原始方法有返回值时,要根据原始方法的返回值来设置环绕通知的返回值。

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

      为什么返回的是Object而不是int的主要原因是Object类型更通用。在环绕通知中是可以对原始方法返回值就行修改的。

    • 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知。通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行

    • 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型,原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object

    • 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常

五、案例:记录万次执行的时间

@Component
@Aspect
public class ProjectAdvice {
    //配置业务层的所有方法
    @Pointcut("execution(* com.itheima.service.*Service.*(..))")
    private void servicePt(){}
    //@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式
    @Around("servicePt()")
    public void runSpeed(ProceedingJoinPoint pjp){
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
           pjp.proceed();
        }
        long end = System.currentTimeMillis();
        System.out.println("业务层接口万次执行时间: "+(end-start)+"ms");
    } 
}

程序优化:多个方法一起执行测试的时候,控制台都打印的是业务层接口万次执行时间:xxxms,没有办法区分到底是哪个接口的哪个方法执行的具体时间。

@Component
@Aspect
public class ProjectAdvice {
    //配置业务层的所有方法
    @Pointcut("execution(* com.itheima.service.*Service.*(..))")
    private void servicePt(){}
    //@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式
    @Around("servicePt()")
    public void runSpeed(ProceedingJoinPoint pjp){
        //获取执行签名信息
        Signature signature = pjp.getSignature();
        //通过签名获取执行操作名称(接口名)
        String className = signature.getDeclaringTypeName();
        //通过签名获取执行操作名称(方法名)
        String methodName = signature.getName();
        
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
           pjp.proceed();
        }
        long end = System.currentTimeMillis();
        System.out.println("万次执行:"+ className+"."+methodName+"---->" +(end-start) + "ms");
    } 
}

六、AOP通知获取数据

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

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

      @Component
      @Aspect
      public class MyAdvice {
          @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
          private void pt(){}
      
          @Before("pt()")
          public void before(JoinPoint jp) 
              Object[] args = jp.getArgs();
              System.out.println(Arrays.toString(args));
              System.out.println("before advice ..." );
          }
      }
      
    • ProceedingJoinPoint:适用于环绕通知。

      @Component
      @Aspect
      public class MyAdvice {
          @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
          private void pt(){}
      
          @Around("pt()")
          public Object around(ProceedingJoinPoint pjp)throws Throwable {
              Object[] args = pjp.getArgs();
              System.out.println(Arrays.toString(args));
              Object ret = pjp.proceed();
              return ret;
          }
      	//其他的略
      }
      

      pjp.proceed()方法有两个构造方法

      • 调用无参数的proceed,当原始方法有参数,会在调用的过程中自动传入参数

      • 所以调用这两个方法的任意一个都可以完成功能

      • 但是当需要修改原始方法的参数时,就只能采用带有参数的方法

        @Around("pt()")
            public Object around(ProceedingJoinPoint pjp) throws Throwable{
                Object[] args = pjp.getArgs();
                System.out.println(Arrays.toString(args));
                args[0] = 666;
                Object ret = pjp.proceed(args);
                return ret;
            }
        
  • 获取切入点方法返回值,只有返回后AfterReturing和环绕Around这两个通知类型可以获取。

    • 返回后通知

      @Component
      @Aspect
      public class MyAdvice {
          @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
          private void pt(){}
      
          @AfterReturning(value = "pt()",returning = "ret")
          public void afterReturning(Object ret) {
              System.out.println("afterReturning advice ..."+ret);
          }
      }
      

      注意returning = "ret" 与 public void afterReturning(Object ret)的Object ret中的ret位置名称必须一致。

      为了能匹配更多的参数类型,参数一般写成Object类型。

      如果有JoinPoint参数,参数必须要放在第一位。

    • 环绕通知,ret就是方法的返回值

      @Component
      @Aspect
      public class MyAdvice {
          @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
          private void pt(){}
      
          @Around("pt()")
          public Object around(ProceedingJoinPoint pjp) throws Throwable{
              Object[] args = pjp.getArgs();
              System.out.println(Arrays.toString(args));
              args[0] = 666;
              Object ret = pjp.proceed(args);
              return ret;
          }
      }
      
  • 获取切入点方法运行异常信息,只有抛出异常后AfterThrowing和环绕Around这两个通知类型可以获取。

    • 抛出异常后通知

      @Component
      @Aspect
      public class MyAdvice {
          @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
          private void pt(){}
      
          @AfterThrowing(value = "pt()",throwing = "t")
          public void afterThrowing(Throwable t) {
              System.out.println("afterThrowing advice ..."+t);
          }
      }
      

      注意throwing = "t"与参数部分Throwable t的"t"位置名称必须一致。

    • 环绕通知

      @Component
      @Aspect
      public class MyAdvice {
          @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
          private void pt(){}
      
          @Around("pt()")
          public Object around(ProceedingJoinPoint pjp){
              Object[] args = pjp.getArgs();
              System.out.println(Arrays.toString(args));
              args[0] = 666;
              Object ret = null;
              try{
                  ret = pjp.proceed(args);
              }catch(Throwable throwable){
                  t.printStackTrace();
              }
              return ret;
          }
      }
      
posted @ 2023-03-29 19:38  无机呱子  阅读(18)  评论(0编辑  收藏  举报