Spring AOP技术的应用

Spring AOP 简介

1.1 AOP 概述

1.1.1 AOP 是什么?

AOP(Aspect Orient Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程(OOP)的一种补充和完善。它以通过预编译方式和运行期动态代理方式,实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。如图1 所示:
image.png

AOP 与 OOP 字面意思相近,但其实两者完全是面向不同领域的设计思想。实际项目中我们通常将面向对象理解为一个静态过程(例如一个系统有多少个模块,一个模块有哪些对象,对象有哪些属性),面向切面的运行期代理方式,理解为一个动态过程,可以在对象
运行时动态织入一些扩展功能或控制对象执行。

1.1.2 AOP 应用场景分析?

实际项目中通常会将系统分为两大部分,一部分是核心业务,一部分是非核业务。在编程实现时我们首先要完成的是核心业务的实现,非核心业务一般是通过特定方式切入到系统中,这种特定方式一般就是借助 AOP 进行实现。

AOP 就是要基于 OCP(开闭原则),在不改变原有系统核心业务代码的基础上动态添加
一些扩展功能并可以"控制"对象的执行。例如 AOP 应用于项目中的日志处理,事务处理,权限处理,缓存处理等等。如图-2 所示:

image.png

思考:现有一业务,在没有 AOP 编程时,如何基于 OCP 原则实现功能扩展?

方案1:基于继承方式实现其功能扩展,关键设计如下:

public class CglibLogNoticeService extends NoticeServiceImpl{
    public boolean send(String notice){
        System.out.println("Start:"+System.currentTimeMillis());
        super.send(notice);
        System.out.println("After:"+System.currentTimeMillis());
        return true;
    }
}

测试类如下:

public class NoticeServiceTests{
    public static void main(String[] args){
        NoticeService ns=new CglibLogNoticeService();
        ns.send("hello");
    }
}

基于继承方式实现功能扩展的优势,劣势分析:
1) 优势: 简单,容易理解.
2) 劣势:不够灵活(只能直接一个接口下的子类)和稳定(父类一旦修改了其方法,所有子类都要改.)

方案2:基于组合方式实现其功能扩展,关键代码设计如下:

public class JdkLogNoticeService implements NoticeService{
        private NoticeService noticeService;//has a
        public JdkLogNoticeService(NoticeService noticeService){
            this.noticeService=noticeService;
    }
    public boolean send(String notice){
        System.out.println("Start:"+System.currentTimeMillis());
        this.noticeService.send(notice);
        System.out.println("After:"+System.currentTimeMillis());
        return true;
    }
}

测试类

public class NoticeServiceTests{
    public static void main(String[] args){
        NoticeService ns=
                new JdkLogNoticeService(new NoticeServiceImpl());
        ns.send("hello");
    }
}

基于组合方式实现功能扩展的优势,劣势分析:
1) 优势: 灵活(可以为指定接口下的所有实现类做功能扩展),稳定(组合的具体对象发生变化,不会影响当前类)
2) 劣势:相对继承而言不容易理解.

总结:
无论是继承,还是组合都是基于OCP方式实现了对象功能扩展,都有相应的优缺点,并且我们都要自己去写这些子类或兄弟类,在这些类中调用目标对象(父类或兄弟类对象)的方法以及扩展业务逻辑.对于这样的模板代码我们能否进行简化呢?例如.由框架实现其共性(创建目录类型的子类类型或兄弟类型),特性交给用户自己实现.

1.1.3 Spring AOP 应用原理分析(先了解)?

Spring AOP 底层基于代理机制(动态方式)实现功能扩展:
1) 假如目标对象(被代理对象)实现接口,则底层可以采用 JDK 动态代理机制为目标对象创建代理对象(目标类和代理类会实现共同接口)。
2) 假如目标对象(被代理对象)没有实现接口,则底层可以采用 CGLIB 代理机制为目标对象创建代理对象(默认创建的代理类会继承目标对象类型)。

Spring AOP 原理分析,如图-3 所示:
image.png

说明:Spring boot2.x 中 AOP 现在默认使用的 CGLIB 代理,假如需要使用 JDK 动态
代理可以在配置文件(applicatiion.properties)中进行如下配置:

spring.aop.proxy-target-class=false

1.2 Spring 中 AOP 相关术语分析

切面(aspect): 横切面对象,一般为一个具体类对象(可以借助@Aspect 声明)。
通 知 (Advice): 在 切 面 的 某 个 特 定 连 接 点 上 执 行 的 动 作 ( 扩 展 功 能 ) , 例 如around,before,after 等。
连接点(joinpoint):程序执行过程中某个特定的点,一般指向被拦截到的目标方法。
切入点(pointcut):对多个连接点(Joinpoint)一种定义,一般可以理解为多个连接点的集合。

连接点与切入点定义如图-4 所示:
image.png

说明:我们可以简单的将机场的一个安检口理解为连接点,多个安检口为切入点,安全检查过程看成是通知。总之,概念很晦涩难懂,多做例子,做完就会清晰。先可以按白话去理解。

2 Spring AOP 快速实践

2.1 业务描述

基于项目中的核心业务,添加简单的日志操作,借助 SLF4J 日志 API 输出目标方法的
执行时长。(前提,不能修改目标方法代码-遵循 OCP 原则)

2.2 项目创建及配置

创建 maven 项目或在已有项目基础上添加 AOP 启动依赖:


    org.springframework.boot
    spring-boot-starter-aop

说明:基于此依赖spring可以整合AspectJ框架快速完成AOP的基本实现。AspectJ是一个面向切面的框架,他定义了 AOP 的一些语法,有一个专门的字节码生成器来生成遵守 java 规范的 class 文件。

2.3 扩展业务分析及实现

2.3.1 创建日志切面类对象

将此日志切面类作为核心业务增强(一个横切面对象)类,用于输出业务执行时长,其关键代码如下:

package com.cy.pj.common.aspect;
@Aspect
@Slf4j
@Component
public class SysLogAspect {

    @Pointcut("bean(sysUserServiceImpl)")
    public void doLogPointCut() {}
    
    @Around("doLogPointCut()")
    public Object around(ProceedingJoinPoint jp)
        throws Throwable{
        try {
             log.info("start:{}"+System.currentTimeMillis());
             Object result=jp.proceed();//最终会调用目标方法
             log.info("after:{}"+System.currentTimeMillis());
             return result;
        }catch(Throwable e) {
             log.error("after:{}",e.getMessage());
             throw e;
        }
    }
}

说明:

@Aspect 注解用于标识或者描述 AOP 中的切面类型,基于切面类型构建的对象用于为目标对象进行功能扩展或控制目标对象的执行。
@Pointcut 注解用于描述切面中的方法,并定义切面中的切入点(基于特定表达式的方式进行描述),在本案例中切入点表达式用的是 bean 表达式,这个表达式以 bean开头,bean 括号中的内容为一个 spring 管理的某个 bean 对象的名字。
@Around 注解用于描述切面中方法,这样的方法会被认为是一个环绕通知(核心业务方法执行之前和之后要执行的一个动作),@Aournd 注解内部 value 属性的值为一个切入点表达式或者是切入点表达式的一个引用(这个引用为一个@PointCut 注解描述的方法的方法名)。
ProceedingJoinPoint 类为一个连接点类型,此类型的对象用于封装要执行的目标方法相关的一些信息。只能用于@Around 注解描述的方法参数。

当我们切入点引入不正确时,会出现如图所示错误:

image.png

2.3.2 业务切面测试实现

启动项目测试或者进行单元测试,其中 Spring Boot 项目中的单元测试代码如下:

@SpringBootTest
public class AopTests {

    @Autowired
    private SysUserService userService;
    
    @Test
    public void testSysUserService() {
        PageObject po=
                userService.findPageObjects("admin",1);
        System.out.println("rowCount:"+po.getRowCount());
    }
}

对于测试类中的 userService 对象而言,它有可能指向 JDK 代理,也有可能指向 CGLIB代理,具体是什么类型的代理对象,要看 application.yml 配置文件中的配置.

2.3.3 应用总结分析

在业务应用,AOP 相关对象分析,如图-5 所示:
image.png

2.4 扩展业务织入增强分析

2.4.1 基于 JDK 代理方式实现

假如目标对象有实现接口,则可以基于 JDK 为目标对象创建代理对象,然后为目标对象
进行功能扩展,如图-6 所示:
image.png

说明:假如目标对象类型没有实现接口,则不允许使用 JDK 代理。

2.4.2 基于 CGLIB 代理方式实现

假如目标对象没有实现接口(当然实现了接口也是可以的),可以基于 CGLIB 代理方式为目标对象织入功能扩展,如图-7 所示:

image.png

说明:目标对象实现了接口也可以基于 CGLIB 为目标对象创建代理对象。但是目标对
象类型假如使用了 final 修饰,则不可以使用 CGBLIB。

3 Spring AOP 编程增强

3.1 切面通知应用增强

3.1.1 通知类型

在基于 Spring AOP 编程的过程中,基于 AspectJ 框架标准,spring 中定义了五种类型的通知(通知-Advice 描述的是一种扩展业务),它们分别是:

@Before (目标方法执行之前执行)
@AfterReturning (目标方法成功结束时执行)
@AfterThrowing (目标方法异常结束时执行)
@After (目标方法结束时执行)
@Around (重点掌握,目标方法执行前后都可以做业务拓展)(优先级最高)

说明:在切面类中使用什么通知,由业务决定,并不是说,在切面中要把所有通知都写上。代码实践分析如下:

package com.cy.pj.common.aspect;
@Component
@Aspect
public class SysTimeAspect {

    @Pointcut("bean(sysUserServiceImpl)")
    public void doTime(){}
    
    @Before("doTime()")
    public void doBefore(){
        System.out.println("time doBefore()");
    }
    
    @After("doTime()")
    public void doAfter(){
        System.out.println("time doAfter()");
    }
    
    /**核心业务正常结束时执行* 说明:假如有 after,先执行 after,再执行returning*/
    @AfterReturning("doTime()")
    public void doAfterReturning(){
        System.out.println("time doAfterReturning");
    }
    
    /**核心业务出现异常时执行说明:假如有 after,先执行 after,再执行Throwing*/
    @AfterThrowing("doTime()")
    public void doAfterThrowing(){
        System.out.println("time doAfterThrowing");
    }
    
    @Around("doTime()")
    public Object doAround(ProceedingJoinPoint jp)throws Throwable{
        System.out.println("doAround.before");
        try{
             Object obj=jp.proceed();
             System.out.println("doAround.after");
             return obj;
        }catch(Throwable e){
             System.out.println(e.getMessage());
             throw e;
         }
    }
}

3.2 切入点表达式增强

Spring 中通过切入点表达式定义具体切入点,其常用 AOP 切入点表达式定义及说明:
image.png

3.2.1 bean 表达式(重点)

bean 表达式一般应用于类级别,实现粗粒度的切入点定义,案例分析:

bean("userServiceImpl")指定一个 userServiceImpl 类中所有方法。
bean("*ServiceImpl")指定所有后缀为 ServiceImpl 的类中所有方法。

说明:bean 表达式内部的对象是由 spring 容器管理的一个 bean 对象,表达式内部的名字应该是 spring 容器中某个 bean 的 name。
缺陷:不能精确到具体方法,也不能针对于具体模块包中的方法做切入点设计

3.2.2 within 表达式(了解)

within 表达式应用于类级别,实现粗粒度的切入点表达式定义,案例分析:

within("aop.service.UserServiceImpl")指定当前包中这个类内部的所有方法。
within("aop.service.*") 指定当前目录下的所有类的所有方法。
within("aop.service..*") 指定当前目录以及子目录中类的所有方法。

within 表达式应用场景分析:
1)对所有业务 bean 都要进行功能增强,但是 bean 名字又没有规则。
2)按业务模块(不同包下的业务)对 bean 对象进行业务功能增强。

3.2.3 execution 表达式(了解)

execution 表达式应用于方法级别,实现细粒度的切入点表达式定义,案例分析:
语法:execution(返回值类型 包名.类名.方法名(参数列表))。

execution(void aop.service.UserServiceImpl.addUser())匹配 addUser 方法。
execution(void aop.service.PersonServiceImpl.addUser(String)) 方法参数必须为String 的 addUser 方法。
execution( aop.service...*(..)) 万能配置。

3.2.4 @annotation 表达式(重点)

@annotaion 表达式应用于方法级别,实现细粒度的切入点表达式定义,案例分析

@annotation(anno.RequiredLog) 匹配有此注解描述的方法。
@annotation(anno.RequiredCache) 匹配有此注解描述的方法。

其中:RequiredLog 为我们自己定义的注解,当我们使用@RequiredLog 注解修饰业务层方法时,系统底层会在执行此方法时进行日扩展操作。

练习:定义一 Cache 相关切面,使用注解表达式定义切入点,并使用此注解对需要使用 cache 的业务方法进行描述,代码分析如下:

第一步:定义注解 RequiredCache

package com.cy.pj.common.annotation;
/**
* 自定义注解,一个特殊的类,所有注解都默认继承 Annotation 接口
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredCache {
    //...
}

第二步:定义 SysCacheAspect 切面对象。

package com.cy.pj.common.aspect;
@Aspect
@Component
public class SysCacheAspect {

    @Pointcut("@annotation(com.cy.pj.common.annotation.RequiredCache)")
     public void doCache() {}
     
     @Around("doCache()")
     public Object around(ProceedingJoinPoint jp)throws Throwable{
         System.out.println("Get data from cache");
         Object obj=jp.proceed();
         System.out.println("Put data to cache");
         return obj;
     }

}

第三步:使用@RequiredCache 注解对特定业务目标对象中的查询方法进行描述(这里
以部门模块的查询方法为例)。

@RequiredCache
@Override
public List> findObjects() {
    ….
    return list;
}

第四步:进行部门模块的访问测试,分析其结果.

3.3 切面优先级设置实现

切面的优先级需要借助@Order 注解进行描述,数字越小优先级越高,默认优先级比较
低。例如:
定义日志切面并指定优先级。

@Order(1)
@Aspect
@Component
public class SysLogAspect {
    …
}

定义缓存切面并指定优先级:

@Order(2)
@Aspect
@Component
public class SysCacheAspect {
    …
}

说明:当多个切面作用于同一个目标对象方法时,这些切面会构建成一个切面链,类似过
滤器链、拦截器链,其执行分析如图-9 所示:
image.png

3.4 关键对象与术语总结

Spring 基于 AspectJ 框架实现 AOP 设计的关键对象概览,如图-10 所示:
image.png

4 总结

4.1 重难点分析

AOP 是什么,解决了什么问题,应用场景?

AOP(面向切编程)是一种设计思想,它是 OOP(面向对象编程)的一种补充和完善。是在不修改源代码的情况下给程序动态扩展其额外功能或控制对象执行的一种技术。

在实际项目中通常会将系统分为两大部分,一部分是核心业务,一部分是非核心业务。在编程实现时我们首先要完成的是核心业务的实现,非核心业务一般是通过特定方式切入到系统中,这种特定方式一般就是借助 AOP 进行实现。

AOP 可以应用于项目中的日志记录,事务管理,权限控制,缓存处理等。

AOP 编程基本步骤及实现过程(以基于 AspectJ 框架实现为例)。

第一步:添加AOP依赖包


    org.springframework.boot
    spring-boot-starter-aop

第二步:定义一个切面类,使用@Aspect和@Compoent注解描述

/**
 * @Aspect 注解描述的类型为切面对象类型,此切面中可以定义多个切入点和通知方法.
 */
@Aspect
@Component
public class SysLogAspect {
    ...
}

第三步:在切面类中定义切入点方法,并使用@Pointcut注解描述其方法。

/**
     * @Pointcut注解用于定义切入点
     * bean("spring容器中bean的名字")这个表达式为切入点表达式定义的一种语法,
     * 它描述的是某个bean或多个bean中所有方法的集合为切入点,这个表达式是粗粒度的,
     * 不能精确到具体哪一个方法
     */
    @Pointcut("@annotation(com.cy.pj.sys.common.annotation.RequiredLog)")
    public void doLog(){} //此方法只负责承载切入点的定义
    

第四步:定义通知方法,并使用通知注解(@Around)进行描述

/**
     * @Around注解描述的方法,可以在切入点执行之前和之后执行,
     * 在当前业务中,此方法为日志通知方法
     * @param joinPoint 连接点对象,此对象封装了要执行的切入点方法信息.
     * 可以通过连接点对象调用目标方法.
     * @return 目标方法的执行结果
     * @throws Throwable
     */
    @Around("doLog()")
    public Object doAround(ProceedingJoinPoint joinPoint)throws Throwable{
        long t1=System.currentTimeMillis();
        log.info("Start:{}",t1);
        try {
            Object result = joinPoint.proceed(); //执行目标方法
            long t2=System.currentTimeMillis();
            log.info("After:{}",t2);
            saveUserLog(joinPoint,t2-t1);
            return result;
        }catch (Throwable e){
            e.printStackTrace();
            log.error("Exception:{}",System.currentTimeMillis());
            throw e;
        }
    }
    

第五步:自定义Annotation接口RequiredLog

package com.cy.pj.sys.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredLog {
    String value() default "operation";
}

第六步:在目标方法上添加@RequiredLog注解

AOP 编程中的核心对象及应用关系。

(代理对象,切面对象,通知,切入点)

AOP 思想在 Spring 中的实现原理分析。

(基于代理方式进行扩展业务的织入)

AOP 编程中基于注解方式的配置实现。

(@Aspect,@PointCut,@Around,...)

4.2 FAQ 分析

什么是 OCP 原则(开闭原则)?
什么是 DIP 原则 (依赖倒置)?
什么是单一职责原则(SRP)?
Spring 中 AOP 的有哪些配置方式?

(XML,注解)

Spring 中 AOP 的通知有哪些基本类型?

(5 种)

Spring 中 AOP 是如何为 Bean 对象创建代理对象的?

(JDK,CGLIB)

Spring 中 AOP 切面的执行顺序如何指定?

(@Order)

4.3 Bug 分析

image.png

切入点应用错误,如图-17 所示:

问题分析:检查切入点的引入是否丢掉了"()".

你可能感兴趣的:(java)