1. AOP 概述

1.1 AOP 是什么?

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

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

AOP 应用场景分析?

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

Spring AOP 应用原理分析

Spring AOP底层基于代理机制(动态方式)实现功能扩展:

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

说明: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)一种定义,一般可以理解为多个连接点的集合。

连接点与切入点定义如图所示:

2 Spring AOP快速实践

2.1 业务描述

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

2.2项目创建及配置

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

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

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

2.3扩展业务分析及实现

1)创建日志切面类对象

/**
 * @Aspect 注解描述的类为spring aop中的一个切面类型,此类型可以定义:
 * 1)切入点(PointCut)方法(可以是多个):要进行功能扩展的一些点
 * 2)通知(Advice)方法(可以是多个):封装了扩展功能的一些方法(在切入点方法之前或之后要执行的方法)
 */
//@Order(1)
@Slf4j
@Aspect
@Component
public class SysLogAspect {
     /**
      * @Pointcut 注解描述的方法为切入点方法,注解中定义的内容为切入点表达式(可以有多种形式)
      * 1)bean(bean名称) 切入点表达式,这个表达式中的名字为spring容器中管理的一个bean的名字
      * 2)bean表达式是一种粗粒度的切入点表达式,这种表达式定义的切入点表示bean中的所有方法
             *  都是将来要切入扩展功能的一些方法(目标方法)
             *  在当前应用中,sysUserServiceImpl这个名字对应的bean中所有的方法的集合为切入点
      */
    @Pontcut("bean(sysUserServiceImpl)")
     public void doLogPointCut() {}//方法中不写任何内容,只是切入点表达式的载体
    

     /**
      *     @Around 注解描述的方法为一个通知方法,这个通知我们称之为环绕通知,可以在
      *     目标方法执行之前或之后做服务增益。在环绕通知方法我们可以自己控制目标方法的
      *     调用。
      * @param jp 连接点对象,此对象封装了要执行的目标方法信息
      * @return 目标的执行结果
      * @throws 执行目标方法过程中出现的异常。
      */
     @Around("doLogPointCut()")
     public Object doLogAround(ProceedingJoinPoint jp)throws Throwable {
         long t1=System.currentTimeMillis();
         try {
         Object result=jp.proceed();//假如本类有@Before先执行@Before,没有看是否有一个切面,最后执行你的目标方法
         long t2=System.currentTimeMillis();
         log.info("目标方法执行时长:{}",t2-t1);
         //将用户行为信息写入到数据库
         saveUserLog(jp,(t2-t1));
         return result;//目标方法的执行结果
         }catch(Throwable e) {
         log.error("目标方法执行过程中出现了问题,具体为{}",e.getMessage());
         throw e;
         }
     }
说明:ProceedingJoinPoint类为一个连接点类型,此类型的对象用于封装要执行的目标方法相关的一些信息。只能用于@Around注解描述的方法参数

2)应用总结分析
对于目标对象(被代理对象)而言,它有可能指向JDK代理,也有可能指向CGLIB代理,具体是什么类型的代理对象,要看application.yml配置文件中的配置.

2.4扩展业务织入增强分析

1)基于JDK代理方式实现

假如目标对象有实现接口,则可以基于JDK为目标对象创建代理对象,然后为目标对象进行功能扩展

2)基于CGLIB代理方式实现

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

3 Spring AOP编程增强

3.1切面通知应用增强

1)通知类型

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

  • @Before。
  • @AfterReturning。
  • @AfterThrowing。
  • @After。
  • @Around.重点掌握(优先级最高)

    说明:在切面类中使用什么通知,由业务决定,并不是说,在切面中要把所有通知都写上。

2)通知执行顺序

假如这些通知全部写到一个切面对象中,其执行顺序及过程

3)通知实践过程分析

@Aspect
@Component
public class SysTimeAspect {

    @Pointcut("bean(sysUserServiceImpl)")
    public void doTime() {}
    
    /**before通知在目标方法执行之前执行*/
    @Before("doTime()")
    public void doBefore(JoinPoint jp) {
        System.out.println("@Before");
    }
    /**after通知在目标方法结束之前(return或throw之前)执行*/
    @After("doTime()")
    public void doAfter() {
        System.out.println("@After");
    }
    /**after之后程序没有出现异常则执行此通知*/
    @AfterReturning("doTime()")
    public void doAfterTurning() {
        System.out.println("@AfterReturning");
    }
    /**after之后程序出现异常则执行此通知*/
    @AfterThrowing("doTime()")
    public void AfterThrowing() {
        System.out.println("@AfterThrowing");
    }
    //ProceedingJoinPoint这个类型只能作为环绕通知的方法参数
    @Around("doTime()")
    public Object doAround(ProceedingJoinPoint jp) throws Throwable{
        System.out.println("@Around.Before");
        try {
        Object result=jp.proceed();
        System.out.println("@Around.after");
        return result;
        }catch(Throwable e) {
        System.out.println("@Around.error");
        throw e;
        }
    }
}

对于@AfterThrowing通知只有在出现异常时才会执行,所以当做一些异常监控时可在此方法中进行代码实现。

@Slf4j
@Aspect
@Component
public class SysExceptionAspect {
    /**此方法可以作为一个异常监控方法*/
    @AfterThrowing(pointcut = "bean(*ServiceImpl)",throwing = "ex")
    public void handleException(JoinPoint jp,Throwable ex) {
        //通过连接点获取目标对象类型
        Class<?> targetClass=jp.getTarget().getClass();
        String className=targetClass.getName();
        //通过连接点获取方法签名对象
        MethodSignature s=(MethodSignature)jp.getSignature();
        String methodName=s.getName();//获取目标方法名
        String targetClassMethod=className+"."+methodName;
        log.error("{}'exception msg is  {}",targetClassMethod,ex.getMessage());
        
        //拓展?
        //1)将日志写到日志文件
        //2)将出现异常的这个信息发送到某个人的邮箱(email),例如QQ邮箱
        //3)将出现异常的情况发短信给某人(运维人员)
        //4)报警(播放一段难听的音乐)
    }
}

3.2切入点表达式增强

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

指示符作用
bean用于匹配指定bean对象的所有方法
within用于匹配指定包下所有类内的所有方法
execution用于按指定语法规则匹配到具体方法
@annotation用于匹配指定注解修饰的方法

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

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

说明:bean表达式内部的对象是由spring容器管理的一个bean对象,表达式内部的名字应该是spring容器中某个bean的name。

2)within表达式(了解)

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

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

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

3)execution表达式(了解)

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

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

    4)@annotation表达式(重点)

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

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

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

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

第一步:定义注解RequiredCache

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

}

//再定义一个清缓存的接口
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ClearCache {

}

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

@Aspect
@Component
public class SysCacheAspect {
     //假设此对象为存储数据的一个缓存对象
     private Map<String,Object> cache=new ConcurrentHashMap<>();//线程安全的hashmap
    
     //注解方式的切入点表达式的定义(细粒度的表达式)
     @Pointcut("@annotation(com.cy.pj.common.annotation.RequiredCache)")
     public void doCache() {}
     @Pointcut("@annotation(com.cy.pj.common.annotation.ClearCache)")
     public void doClear(){}
     
     //????用什么通知方法去清除cache数据
     @AfterReturning("doClear()")
     public void doClearCache() {
         cache.clear();
         //cache.remove("deptKey");
     }
     @Around("doCache()")
     public Object doAround(ProceedingJoinPoint jp)throws Throwable{
         System.out.println("Get Data from cache");
         Object result=cache.get("deptKey");//这里的deptKey目前为一个固定值
         if(result!=null)return result;
         result=jp.proceed();
         System.out.println("Put data to cache");
         cache.put("deptKey", result);
         return result;
     }

第三步:使用@RequiredCache注解对特定业务目标对象中的查询方法进行描述。使用@ClearCache对增删改业务进行描述清理缓存。

@Service
public class SysDeptServiceImpl implements SysDeptService {
    @Autowired
    private SysDeptDao sysDeptDao;
    /**@RequiredCache 注解描述的方法为一个切入点目标方法,此方法执行时我们要对其进行cache操作*/
    @RequiredCache
    @Override
    public List<Map<String, Object>> findObjects() {
        List<Map<String, Object>> list=
        sysDeptDao.findObjects();
        if(list==null||list.size()==0)
        throw new ServiceException("没有部门信息");
        return list;
    }
    @ClearCache
    @Override
    public int updateObject(SysDept entity) {
    。。。。。
    }
    
    @ClearCache
    @Override
    public int deleteObject(Integer id) {
    。。。。。
    }
    @ClearCache
    @Override
    public int saveObject(SysDept entity) {
        。。。。
    }

3.3切面优先级设置实现

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

定义日志切面并指定优先级。

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

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

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

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

3.4关键对象与术语总结


萧渊之
41 声望20 粉丝