【Spring】从面向对象再到面向切面

前言

Object object = new Object(); 世间万物的本质都可看作类的对象,面向对象(OOP)的模式让程序易维护、易复用、易扩展,而面向切面(AOP)则是面向对象的补充,让对象的功能更加强大 对比前面的日志框架技术二者非常相似,他的特点就是在不影响业务的前提下将程序的运行情况输出到控制台,总体来看是起一个辅助的作用,所谓的AOP亦是如此——是在不改原有代码的前提下对其进行增强

一.OOP&AOP

OOP将组件视为对象,AOP将对象的切面视为“对象” OOP&AOP让程序通过极其简单的方式变得更加全面、强大 AOP(Aspect Oriented Programming)面向切面编程、OOP(Object Oriented Programming)面向对象编程 OOP是一种编程思想,AOP也是一种编程思想,编程思想主要的内容就是指导程序员该如何编写程序,两者都是不同的编程范式各有特色

二.AOP核心

通过以下一个计算程序运行时间的功能,引出AOP相关概念

@Repository
public class AImpl implements A {
          
   
    public void save() {
          
   
        //记录程序当前执行执行(开始时间)
        Long startTime = System.currentTimeMillis();
        //业务执行万次
        for (int i = 0;i<10000;i++) {
          
   
            System.out.println("START ...");
        }
        //记录程序当前执行时间(结束时间)
        Long endTime = System.currentTimeMillis();
        //计算时间差
        Long totalTime = endTime-startTime;
        //输出信息
        System.out.println("执行万次程序消耗时间:" + totalTime + "ms");
    }
    public void m1(){
          
    System.out.println(" m1 ..."); }
    public void m2(){
          
    System.out.println(" m2 ..."); }
}

(1)save,m1和m2方法,这些方法我们给起了一个名字叫连接点 (2)对于需要增强的方法我们给起了一个名字叫切入点 (3)将功能抽取到一个方法中,换句话说就是存放共性功能的方法,我们给起了个名字叫通知 (4)通知是要增强的内容,会有多个,切入点是需要被增强的方法,也会有多个,那哪个切入点需要添加哪个通知,就需要提前将它们之间的关系描述清楚,那么对于通知和切入点之间的关系描述,我们给起了个名字叫切面 (5)通知是一个方法,方法不能独立存在需要被写在一个类中,这个类我们也给起了个名字叫通知类

三.第一个AOP案例

1.环境准备

    创建一个Maven项目 pom.xml添加Spring依赖spring-context 添加A和AImpl类 public interface A { public void save(); public void m1(); } @Repository public class AImpl implements A { public void save() { System.out.println(System.currentTimeMillis()); System.out.println("book dao save ..."); } public void m1(){ System.out.println("book dao m1 ..."); } } 创建Spring的配置类 @Configuration @ComponentScan("yu7daily") public class Config { } 编写Show运行类 public class Show { public static void main(String[] args) { ShowlicationContext ctx = new AnnotationConfigShowlicationContext(Config.class); A A = ctx.getBean(A.class); A.save(); } }

2.AOP实现步骤

1.@EnableAspectJAutoProxy 开启注解格式AOP功能 2.@Aspect设置当前类为AOP切面类 3.@Pointcut 设置切入点方法 4.@Before设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行 **1.添加依赖 pom.xml

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
</dependency>

因为spring-context中已经导入了spring-aop,所以不需要再单独导入spring-aop. 导入AspectJ的jar包,AspectJ是AOP思想的一个具体实现,Spring有自己的AOP实现,但是相比于AspectJ来说比较麻烦,所以我们直接采用Spring整合ApsectJ的方式进行AOP开发。 2.定义接口与实现类:环境准备的时候,AImpl已经准备好,不需要做任何修改 3.定义通知类和通知 通知就是将共性功能抽取出来后形成的方法,共性功能指的就是当前系统时间的打印

public class Test {
          
   
    public void method(){
          
   
        System.out.println(System.currentTimeMillis());
    }
}

类名和方法名没有要求,可以任意。 4.定义切入点 AImpl中有两个方法,分别是save和m1,我们要增强的是m1方法,该如何定义呢?

public class Test {
          
   
    @Pointcut("execution(void yu7daily.dao.A.m1())")
    private void po1(){
          
   }
    public void method(){
          
   
        System.out.println(System.currentTimeMillis());
    }
}

说明: 切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑。 execution及后面编写的内容 5.制作切面 切面是用来描述通知和切入点之间的关系,如何进行关系的绑定?

public class Test {
          
   
    @Pointcut("execution(void yu7daily.dao.A.m1())")
    private void po1(){
          
   }
    
    @Before("po1()")
    public void method(){
          
   
        System.out.println(System.currentTimeMillis());
    }
}

绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置 说明:@Before翻译过来是之前,也就是说通知会在切入点方法执行之前执行,除此之前还有其他四种类型 6.将通知类配给容器并标识其为切面类

@Component
@Aspect
public class Test {
          
   
    @Pointcut("execution(void yu7daily.dao.A.m1())")
    private void po1(){
          
   }
    
    @Before("po1()")
    public void method(){
          
   
        System.out.println(System.currentTimeMillis());
    }
}

7.开启注解格式AOP功能

@Configuration
@ComponentScan("yu7daily")
@EnableAspectJAutoProxy
public class Config {
          
   
}

8.运行程序

public class Show {
          
   
    public static void main(String[] args) {
          
   
        ShowlicationContext ctx = new AnnotationConfigShowlicationContext(Config.class);
        A A = ctx.getBean(A.class);
        A.m1();
    }
}

看到在执行m1方法之前打印了系统时间戳,说明对原始方法进行了增强,AOP编程成功!!!

四.切入点表达式

前面的案例中,有涉及到如下内容: 对于AOP中切入点表达式,我们总共会学习三个内容,分别是语法格式、通配符和书写技巧。

1.语法格式

首先我们先要明确两个概念: 切入点:要进行增强的方法 切入点表达式:要进行增强的方法的描述方式 描述方式一:执行yu7daily.dao包下的A接口中的无参数m1方法

execution(void yu7daily.dao.A.m1())

描述方式二:执行yu7daily.dao.impl包下的AImpl类中的无参数m1方法

execution(void yu7daily.dao.impl.AImpl.m1())

因为调用接口方法的时候最终运行的还是其实现类的方法,所以上面两种描述方式都是可以的。 对于切入点表达式的语法为:

    切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
execution(public User yu7daily.service.UserService.findById(int))

切入点表达式就是要找到需要增强的方法,所以它就是对一个具体方法的描述,但是方法的定义会有很多,所以如果每一个方法对应一个切入点表达式,极其复杂可以通过以下方式进行简化

2.通配符

使用通配符描述切入点,主要的目的就是简化之前的配置 *:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现

execution(public * yu7daily.*.UserService.find*(*))

匹配yu7daily包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法 ..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写

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

匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法 +:专用于匹配子类类型

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

使用切入点表达式来分析下:

execution(void yu7daily.dao.A.m1())
匹配接口,能匹配到
execution(void yu7daily.dao.impl.AImpl.m1())
匹配实现类,能匹配到
execution(* yu7daily.dao.impl.AImpl.m1())
返回值任意,能匹配到
execution(* yu7daily.dao.impl.AImpl.m1(*))
返回值任意,但是m1方法必须要有一个参数,无法匹配,要想匹配需要在m1接口和实现类添加参数
execution(void com.*.*.*.*.m1())
返回值为void,com包下的任意包三层包下的任意类的m1方法,匹配到的是实现类,能匹配
execution(void com.*.*.*.m1())
返回值为void,com包下的任意两层包下的任意类的m1方法,匹配到的是接口,能匹配
execution(void *..m1())
返回值为void,方法名是m1的任意包下的任意类,能匹配
execution(* *..*(..))
匹配项目中任意类的任意方法,能匹配,但是不建议使用这种方式,影响范围广
execution(* *..u*(..))
匹配项目中任意包任意类下只要以u开头的方法,m1方法能满足,能匹配
execution(* *..*e(..))
匹配项目中任意包任意类下只要以e结尾的方法,m1和save方法能满足,能匹配
execution(void com..*())
返回值为void,com包下的任意包任意类任意方法,能匹配,*代表的是方法
execution(* yu7daily.*.*Service.find*(..))
将项目中所有业务层方法的以find开头的方法匹配
execution(* yu7daily.*.*Service.save*(..))
将项目中所有业务层方法的以save开头的方法匹配

五.AOP通知类型

它所代表的含义是将通知添加到切入点方法执行的前面。 除了这个注解外,还有没有其他的注解,换个问题就是除了可以在前面加,能不能在其他的地方加? (1)前置通知,追加功能到方法执行前,类似于在代码1或者代码2添加内容 (2)后置通知,追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码5添加内容 (3)返回后通知,追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,如果方法执行抛出异常,返回后通知将不会被添加 (4)抛出异常后通知,追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码4添加内容,只有方法抛出异常后才会被添加 (5)环绕通知,环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式,它可以实现其他四种通知类型的功能

环境准备

1.pom.xml添加Spring依赖spring-context、aspectjweaver

2.添加A和AImpl类

public interface A {
          
   
    public void m1();
    public int m2();
}

@Repository
public class AImpl implements A {
          
   
    public void m1(){
          
   
        System.out.println(" m1 ...");
    }
    public int m2() {
          
   
        System.out.println(" m2 is running ...");
        return 1;
    }
}
    创建Spring的配置类 @Configuration @ComponentScan("yu7daily") @EnableAspectJAutoProxy public class Config { } 创建通知类 @Component @Aspect public class Test { @Pointcut("execution(void yu7daily.dao.A.m1())") private void po1(){ } public void around(){ System.out.println("around before advice ..."); System.out.println("around after advice ..."); } } 编写Show运行类 public class Show { public static void main(String[] args) { ShowlicationContext ctx = new AnnotationConfigShowlicationContext(Config.class); A A = ctx.getBean(A.class); A.m1(); } }

环绕通知

(1)原始方法有返回值的处理

    修改Test,对A中的m2方法添加环绕通知,
@Component
@Aspect
public class Test {
          
   
    @Pointcut("execution(void yu7daily.dao.A.m1())")
    private void po1(){
          
   }
    
    @Pointcut("execution(int yu7daily.dao.A.m2())")
    private void po2(){
          
   }
    
    @Around("po2()")
    public void aroundM2(ProceedingJoinPoint pjp) throws Throwable {
          
   
        System.out.println("around before advice ...");
        //表示对原始操作的调用
        pjp.proceed();
        System.out.println("around after advice ...");
    }
}
    修改Show类,调用m2方法
@Component
@Aspect
public class Test {
          
   
    @Pointcut("execution(void yu7daily.dao.A.m1())")
    private void po1(){
          
   }
    
    @Pointcut("execution(int yu7daily.dao.A.m2())")
    private void po2(){
          
   }
    
    @Around("po2()")
    public Object aroundM2(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类型更通用随时可以转型 在环绕通知中是可以对原始方法返回值就行修改的

1.返回后通知

@Component
@Aspect
public class Test {
          
   
    @Pointcut("execution(void yu7daily.dao.A.m1())")
    private void po1(){
          
   }
    
    @Pointcut("execution(int yu7daily.dao.A.m2())")
    private void po2(){
          
   }
    
    @AfterReturning("po2()")
    public void afterReturning() {
          
   
        System.out.println("afterReturning advice ...");
    }
}

注意:返回后通知是需要在原始方法m2正常执行后才会被执行,如果m2()方法执行的过程中出现了异常,那么返回后通知是不会被执行。后置通知则是不管原始方法有没有抛出异常都会被执行

2.异常后通知

@Component
@Aspect
public class Test {
          
   
    @Pointcut("execution(void yu7daily.dao.A.m1())")
    private void po1(){
          
   }
    
    @Pointcut("execution(int yu7daily.dao.A.m2())")
    private void po2(){
          
   }
    
    @AfterReturning("po2()")
    public void afterThrowing() {
          
   
        System.out.println("afterThrowing advice ...");
    }
}

环绕通知注意事项

1. 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知 2. 通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行 3. 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型 4. 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object 5. 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常
经验分享 程序员 微信小程序 职场和发展