AOP基础了解与应用

AOP 概述

AOP 是什么?

AOP(Aspect Orient Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程(OOP)的一种补充和完善。它以通过预编译方式和运行期动态代理方式,实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。
在这里插入图片描述
AOP与OOP字面意思相近,但其实两者完全是面向不同领域的设计思想。实际项目中我们通常将面向对象理解为一个静态过程(例如一个系统有多少个模块,一个模块有哪些对象,对象有哪些属性),面向切面的运行期代理方式,理解为一个动态过程,可以在对象运行时动态织入一些扩展功能或控制对象执行。

AOP 应用场景分析

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

AOP就是要基于OCP(开闭原则),在不改变原有系统核心业务代码的基础上动态添加一些扩展功能并可以"控制"对象的执行。例如AOP应用于项目中的日志处理,事务处理,权限处理,缓存处理等等
(PS:现有一业务,在没有AOP编程时,如何基于OCP原则实现功能扩展?)
在这里插入图片描述

AOP 应用原理分析

Spring AOP底层基于代理机制实现功能扩展:

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

Spring AOP 原理分析
在这里插入图片描述
在这里插入图片描述
说明:Spring boot2.0 中AOP现在默认使用的CGLIB代理,即非接口类代理,假如需要使用JDK动态代理可以在配置文件(applicatiion.properties)中进行如下配置:

spring.aop.proxy-target-class=false

AOP 相关术语分析

  • 切面(aspect): 横切面对象,一般为一个具体类对象(可以借助@Aspect声明)。
  • 通知(Advice):在切面的某个特定连接点上执行的动作(扩展功能),例如around,before,after等。
  • 连接点(joinpoint):程序执行过程中某个特定的点,一般指被拦截到的的方法。
  • 切入点(pointcut):对多个连接点(Joinpoint)一种定义,一般可以理解为多个连接点的集合。
    在这里插入图片描述
    说明:我们可以简单的将机场的一个安检口理解为连接点,多个安检口为切入点,安全检查过程看成是通知。

Spring AOP快速实践

业务描述

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

项目创建及配置

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

            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>2.3.1.RELEASE</version>

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

扩展业务分析及实现

创建日志切面类对象

将此日志切面类作为核心业务增强(一个横切面对象)类,用于输出业务执行时长

package com.cy.common.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

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

业务切面测试实现

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

package com.cy.aop;

import com.cy.common.bo.PageObject;
import com.cy.sys.pojo.UserDept;
import com.cy.sys.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class AopTests {
	 @Autowired
	 private UserService userService;
	 @Test
	 public void testUserService() {
		 PageObject<UserDept> po=
				 userService.findPageObjects("admin",1);
		 System.out.println("rowCount:"+po.getRowCount());
	 }
}

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

应用总结分析

在业务应用,AOP相关对象分析。
在这里插入图片描述

扩展业务织入增强分析

基于JDK代理方式实现

假如目标对象有实现接口,则可以基于JDK为目标对象创建代理代理对象,然后为目标对象进行功能扩展
在这里插入图片描述

基于CGLIB代理方式实现

假如目标对象没有实现接口,可以基于CGLIB代理方式为目标织入功能扩展
在这里插入图片描述
说明:目标对象实现了接口也可以基于CGLIB为目标对象创建代理对象。

Spring AOP编程增强

切面通知应用增强

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

//前置通知
- @Before
//后置运行通知
- @AfterReturning
后置异常通知
- @AfterThrowing
//后置通知
- @After
//环绕通知
- @Around 			重点掌握(优先级最高)

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

通知执行顺序

在这里插入图片描述
说明:实际项目中可能不会在切面中定义所有的通知,具体定义哪些通知要结合业务进行实现。通知实践过程分析

package com.cy.common.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

//@Order(1)
@Component
@Aspect
public class TimeAspect {
	@Pointcut("bean(userServiceImpl)")
	public void doTime(){} //不写具体内容
 
	@Around("doTime()")
	public Object doAround(ProceedingJoinPoint jp) throws Throwable{
		Object obj;
		System.out.println("doAround.before");
		try{
			obj=jp.proceed();
			System.out.println("doAround.after");
		}catch(Throwable e){
			System.out.println(e.getMessage());
			throw e;
		}
		return obj;
	}

	/**
	 * 前置通知
	 * @param jp
	 */
	@Before("doTime()")
	public void doBefore(JoinPoint jp){
		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");
	}
}

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

定义一个异常监控切面,对目标页面方法进行异常监控,并以日志信息形式输出异常

package com.cy.common.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
 
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Aspect
@Component
public class ExceptionAspect {
	
	//@Pointcut("bean(*ServiceImpl)")
	//public void  doExceptionPointCut() {}
	//通过如下方法记录异常日志,但是异常还要抛出,所以在afterThrowing注解中要添加throwing属性,他的值为方法参数 e的名字
	@AfterThrowing(pointcut = "bean(*ServiceImpl)", throwing = "e")
	public void doHandleException(JoinPoint jp, Throwable e) {
		MethodSignature ms = (MethodSignature) jp.getSignature();
		log.error("{}exception msg is {}", ms.getName(), e.getMessage());
	}
}

说明:AfterThrowing中throwing属性的值,需要与它描述的方法的异常参数名相同。()ps:就是那个e)

切入点表达式增强

Spring AOP 中切入点表达式说明

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

bean表达式(重点)

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

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

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

within表达式(了解)

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

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

execution表达式(了解)

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

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

@annotation表达式(重点)

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

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

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

案例

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

package com.cy.common.annotation;

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

/**
 * 自定义注解,一个特殊的类,所有注解都默认继承Annotation接口
 */
@Retention(RetentionPolicy.RUNTIME)		//@Retention用于定义注解何时有效
@Target(ElementType.METHOD)				//@Target用于定义注解可以描述的对象
@Documented 							//将注解中的文档注释在提取也要生成API文档
public @interface RequiredCache {

}

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

package com.cy.common.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class CacheAspect {
	    @Pointcut("@annotation(com.cy.common.annotation.RequiredCache)")
	  public void doCache() {}

	  @Around("doCache()")
	  public Object around(ProceedingJoinPoint jp)throws Throwable{
		  System.out.println("Get data from cache");
		  Object object=jp.proceed();
		  System.out.println("Put data to cache");
		  return object;
	  }
    
}

第三步:使用@RequiredCache注解对特定业务目标对象中的查询方法进行描述。
在这里插入图片描述
删除的方法

	@Pointcut("@annotation(com.cy.common.annotation.ClearCache)")
	public void doClearCache() {}

	@AfterReturning("doClearCache()")
	public void doAfterReturning(){
		cache.clear();
	}

删除的注解

package com.cy.common.annotation;

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

/**
 * @author SXJ
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ClearCache {
    /**
     * 将来可能不同模块会有不同cache,基于key先获取cache对象
     * @return
     */
    String key() default "";
}//Map<Key,Cache>

切面优先级设置实现

切面的优先级需要借助@Order注解进行描述,数字越小优先级越高,默认优先级比较低
在这里插入图片描述
在这里插入图片描述
数字越小优先级越高,没写默认最低

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

关键对象与术语总结

Spring 基于AspectJ框架实现AOP设计的关键对象概览
在这里插入图片描述
@Aspect

@pointcut      切入点
JoinPoint    连接点
@Order       优先级
Advice			通知

Advice

@Before
@AfterReturning
@AfterThrowing
@After
@Around 		

@pointcut

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

猜你喜欢

转载自blog.csdn.net/SkyCloud_/article/details/108094775
今日推荐