共计 10839 个字符,预计需要花费 28 分钟才能阅读完成。
Spring AOP 简介
1. AOP 概述
1.1.1 What’s AOP?
AOP(Aspect Orient Programming): 面向切面编程
AOP 是一种设计思维,是软件设计畛域中的面向切面编程,它是面向对象编程(OOP) 的一种补充和欠缺。它以通过预编译形式和运行期动静代理形式,实现在不批改源代码的状况下给程序动静对立增加额定性能的一种技术。如图 - 1 所示:
AOP 与 OOP(Object Orient Programming)字面意思相近,但其实两者齐全是面向不同畛域的设计思维。理论我的项目中咱们通常将面向对象了解为一个动态过程 (例如一个零碎有多少个模块,一个模块有哪些对象,对象有哪些属性),面向切面的运行期代理形式,了解为一个动静过程,能够在对象运行时动静织入一些扩大性能或管制对象执行。
1.1.2 AOP 利用场景剖析?
理论我的项目中通常会将零碎分为两大部分,一部分是外围业务,一部分是非核业务。在编程实现时咱们首先要实现的是外围业务的实现,非核心业务个别是通过特定形式切入到零碎中,这种特定形式个别就是借助 AOP 进行实现。
AOP 就是要基于 OCP(开闭准则),在不扭转原有系统核心业务代码的根底上动静增加一些扩大性能并能够 ” 管制 ” 对象的执行。例如 AOP 利用于我的项目中的日志解决,事务处理,权限解决,缓存解决等等。如图 - 2 所示:
思考: 现有一业务, 在没有 AOP 编程时, 如何基于 OCP 准则实现性能扩大?
实现对象性能扩大如图所示:
1.1.3 AOP 利用原理剖析(先理解)?
Spring AOP 底层基于代理机制实现性能扩大:
- 如果指标对象 (被代理对象) 实现接口,则底层能够采纳 JDK 动静代理机制为指标对象创立代理对象(指标类和代理类会实现独特接口)。
- 如果指标对象 (被代理对象) 没有实现接口,则底层能够采纳 CGLIB 代理机制为指标对象创立代理对象(默认创立的代理类会继承指标对象类型)。
Spring AOP 原理剖析,如图 - 3 所示:
阐明:Spring boot2.x 版本中 AOP 当初默认应用的 CGLIB 代理, 如果须要应用 JDK 动静代理能够在配置文件 (applicatiion.properties) 中进行如下配置:
spring.aop.proxy-target-class=false
1.2 AOP 相干术语剖析
- 切面(aspect): 横切面对象, 个别为一个具体类对象(能够借助 @Aspect 申明)。
- 告诉(Advice): 在切面的某个特定连接点上执行的动作(扩大性能),例如 around,before,after 等。
- 连接点(joinpoint): 程序执行过程中某个特定的点,个别指被拦挡到的的办法。
- 切入点 (pointcut): 对多个连接点(Joinpoint) 一种定义, 个别能够了解为多个连接点的汇合。
连接点与切入点定义如图 - 4 所示:
阐明:咱们能够简略的将机场的一个安检口了解为连接点,多个安检口为切入点,安全检查过程看成是告诉。总之,概念很艰涩难懂,多做例子,做完就会清晰。先能够按文言去了解。
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 扩大业务剖析及实现
2.3.1 创立日志切面类对象
将此日志切面类作为外围业务加强(一个横切面对象)类,用于输入业务执行时长,其要害代码如下:
package com.cy.pj.common.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
/**
* @Aspect 注解,形容的类为 spring 容器中一个切面对象类型(此类型中封装切入点与告诉办法)
* 1)切入点:要执行扩大业务的办法的汇合
* 2)告诉办法:封装了在切入点办法上要执行的扩大业务办法
*/
@Order(1)//@Order 注解用于形容切面的优先级,数字越小优先级越高,默认优先级比拟低
@Aspect
@Slf4j
@Component
public class SysLogAspect {// private static final Logger log=LoggerFactory.getLogger(SysLogAspect.class); // 这句代码等效于 @Slf4j 注解
/**
* @Pointcut 注解用于形容切入点(在哪些点上执行扩大业务)
* bean(bean 对象名字 默认为类名首字母小写): 为一种切入点表达式(这个表达式中定义了哪个或哪些 bean 对象的办法要进行性能扩大).
* 例如,bean(sysUserServiceImpl)表达式示意名字为 sysUserServiceImpl 的 bean 对象中所有办法的汇合为切入点,
* 也就是说这个 sysUserServiceImpl 对象中的任意办法执行时都要进行性能扩大.
*/
@Pointcut("bean(sysUserServiceImpl)")
public void doLogPointCut() {}// 此办法外部不须要写具体实现,办法的办法名也是任意的
/**
* @Around 注解形容的办法为一个告诉办法(即是一个服务增益办法),此办法外部能够做服务增益(扩大业务),@Around 注解
* 外部要指定切入点表达式,在此切入点表达式对应的切入点办法上做性能扩大
* @param jp 示意连接点, 连接点是动静确定的, 用于封装正在执行的切入点办法 (指标办法) 信息.
* @return 指标办法的执行后果
* @throws Throwable 告诉办法中执行过程呈现的异样
*/
@Around("doLogPointCut()")
public Object around(ProceedingJoinPoint jp) throws Throwable {
try {
//1. 记录办法开始执行工夫
long start = System.currentTimeMillis();
log.info("start:{}",start);
//2. 执行指标办法
Object result=jp.proceed();// 最终(两头还能够调用本类其它告诉或其它切面的告诉)会调用指标办法
//3. 记录办法完结执行工夫
long after = System.currentTimeMillis();
log.info("after:{}",after);
String targetClassMethod=getTargetClassMethod(jp);
log.info("{}指标办法的执行耗时:{}",targetClassMethod,(after-start));
//4. 返回指标办法的执行后果
return result;// 指标办法的执行后果
}catch(Throwable e) {log.error("指标办法执行时呈现了异样:{}",e.getMessage());
throw e;
}
}
/** 获取指标办法的全限定名(指标类全名(包名 + 类名)+ 办法名)*/
private String getTargetClassMethod(ProceedingJoinPoint jp) {
//1. 获取指标对象的类型
Class<?> targetCls = jp.getTarget().getClass();
//2. 获取指标对象的类全名(包名 + 类名)
String targetClsName = targetCls.getName();
//3. 火球指标对象的办法名
//3.1 获取办法签名(办法签名对象中封装了办法相干信息)
MethodSignature ms = (MethodSignature)jp.getSignature();
//3.2 基于办法签名获取办法名
String methodName=ms.getName();
//4. 构建办法的全限定名并返回
return targetClsName+"."+methodName;
}
}
阐明:
- @Aspect 注解用于标识或者形容 AOP 中的切面类型,基于切面类型构建的对象用于为指标对象进行性能扩大或控制目标对象的执行。
- @Pointcut 注解用于形容切面中的办法,并定义切面中的切入点(基于特定表达式的形式进行形容),在本案例中切入点表达式用的是 bean 表达式,这个表达式以 bean 结尾,bean 括号中的内容为一个 spring 治理的某个 bean 对象的名字。
- @Around 注解用于形容切面中办法,这样的办法会被认为是一个盘绕告诉(外围业务办法执行之前和之后要执行的一个动作),@Aournd 注解外部 value 属性的值为一个切入点表达式或者是切入点表达式的一个援用(这个援用为一个 @PointCut 注解形容的办法的办法名)。
- ProceedingJoinPoint 类为一个连接点类型,此类型的对象用于封装要执行的指标办法相干的一些信息。只能用于 @Around 注解形容的办法参数。
2.3.1 业务切面测试实现
启动我的项目测试或者进行单元测试,其中 Spring Boot 我的项目中的单元测试代码如下:
@SpringBootTest
public class AopTests {
@Autowired
private SysUserService userService;
@Test
public void testSysUserService() {
PageObject<SysUserDeptVo> po\=
userService.findPageObjects("admin",1);
System.out.println("rowCount:"+po.getRowCount());
}
}
对于测试类中的 userService 对象而言, 它有可能指向 JDK 代理, 也有可能指向 CGLIB 代理, 具体是什么类型的代理对象, 要看 application.yml 配置文件中的配置。
aop:
proxy-target-class: false #false 示意零碎底层会基于 JDK 形式为指标对象创立代理对象。默认为 true,示意零碎底层会基于 CGLIB 形式为指标对象创立代理对象
2.3.2 利用总结剖析
在业务利用,AOP 相干对象剖析, 如图 - 5 所示:
2.4 扩大业务织入加强剖析
2.4.1 基于 JDK 代理形式实现
如果指标对象有实现接口, 则能够基于 JDK 为指标对象创立代理对象, 而后为指标对象进行性能扩大, 如图 - 6 所示:
2.4.2 基于 CGLIB 代理形式实现
如果指标对象没有实现接口 (当然实现了接口也是能够的),能够基于 CGLIB 代理形式为指标对象织入性能扩大,如图 - 7 所示:
阐明:指标对象实现了接口也能够基于 CGLIB 为指标对象创立代理对象。
3 Spring AOP 编程加强
3.1 切面告诉利用加强
3.1.1 告诉类型
在基于 Spring AOP 编程的过程中,基于 AspectJ 框架规范,spring 中定义了五种类型的告诉(告诉形容的是一种扩大业务),它们别离是:
- @Before。
- @AfterReturning。
- @AfterThrowing。
- @After。
- @Around. 重点把握(优先级最高)
阐明:在切面类中应用什么告诉,由业务决定,并不是说,在切面中要把所有告诉都写上。3.1.2 告诉执行程序
如果五种类型的告诉全副写到一个切面对象中,其执行程序及过程,如图 - 8 所示:阐明:理论我的项目中可能不会在切面中定义所有的告诉,具体定义哪些告诉要联合业务进行实现。
3.1.3 告诉实际过程剖析
代码实际剖析如下:
package com.cy.pj.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.stereotype.Component;
@Aspect
@Component
public class SysTimeAspect {@Pointcut("bean(sysUserServiceImpl)")
public void doTime() {// 不写具体内容}
@Before("doTime()")
public void doBefore(JoinPoint jp){System.out.println("@Before");
}
@After("doTime()")
public void doAfter(){System.out.println("@After");
}
/** 外围业务失常完结时执行 * 阐明:如果有 after,先执行 after, 再执行 returning*/
@AfterReturning("doTime()")
public void doAfterReturning(){System.out.println("@AfterReturning");
}
/** 外围业务出现异常时执行阐明:如果有 after,先执行 after, 再执行 Throwing*/
@AfterThrowing("doTime()")
public void doAfterThrowing(){System.out.println("@AfterThrowing");
}
@Around("doTime()")
public Object doAround(ProceedingJoinPoint jp)
throws Throwable{System.out.println("@Around.before");
try{Object obj=jp.proceed(); // 执行指标办法
System.out.println("@Around.after");
return obj;
}catch(Throwable e){//e.printStackTrace();
System.out.println(e.getMessage());
System.out.println("@Around.throwing");
throw e;
}
}
}
阐明:对于 @AfterThrowing 告诉只有在出现异常时才会执行,所以当做一些异样监控时可在此办法中进行代码实现。
课堂练习: 定义一个异样监控切面, 对指标页面办法进行异样监控, 并以日志信息的模式输入异样
package com.cy.pj.common.aspect;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Aspect
@Component
public class SysExceptionAspect {@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 属性的值,须要与它形容的办法的异样参数名雷同。
3.2 切入点表达式加强
Spring 中通过切入点表达式定义具体切入点,其罕用 AOP 切入点表达式定义及阐明:
表 -1 Spring AOP 中切入点表达式阐明
| 批示符 | 作用 |
|bean| 用于匹配指定 bean 对象的所有办法 |
| within | 用于匹配指定包下所有类内的所有办法 |
|execution | 用于按指定语法规定匹配到具体方法 |
| @annotation | 用于匹配指定注解润饰的办法 |
3.2.4 bean 表达式(重点)
bean 表达式个别利用于类级别,实现粗粒度的切入点定义,案例剖析:
- bean(“userServiceImpl”)指定一个 userServiceImpl 类中所有办法。
- bean(“*ServiceImpl”)指定所有后缀为 ServiceImpl 的类中所有办法。
阐明:bean 表达式外部的对象是由 spring 容器治理的一个 bean 对象, 表达式外部的名字应该是 spring 容器中某个 bean 的 name。
3.2.5 within 表达式(理解)
within 表达式利用于类级别,实现粗粒度的切入点表达式定义,案例剖析:
- within(“aop.service.UserServiceImpl”)指定以后包中这个类外部的所有办法。
- within(“aop.service.*”) 指定当前目录下的所有类的所有办法。
- within(“aop.service..*”) 指定当前目录以及子目录中类的所有办法。
within 表达式利用场景剖析:
1)对所有业务 bean 都要进行性能加强,然而 bean 名字又没有规定。
2)按业务模块 (不同包下的业务) 对 bean 对象进行业务性能加强。
3.2.6 execution 表达式(理解)
execution 表达式利用于办法级别,实现细粒度的切入点表达式定义,案例剖析:
语法:execution(返回值类型 包名. 类名. 办法名(参数列表))。
- execution(void aop.service.UserServiceImpl.addUser())匹配 addUser 办法。
- execution(void aop.service.PersonServiceImpl.addUser(String)) 办法参数必须为 String 的 addUser 办法。
- execution(* aop.service..*.*(..)) 万能配置。
3.2.7 @annotation 表达式(重点)
@annotaion 表达式利用于办法级别,实现细粒度的切入点表达式定义,案例剖析
- @annotation(anno.RequiredLog) 匹配有此注解形容的办法。
- @annotation(anno.RequiredCache) 匹配有此注解形容的办法。
其中:RequiredLog 为咱们本人定义的注解, 当咱们应用 @RequiredLog 注解润饰业务层办法时, 零碎底层会在执行此办法时进行日志扩大操作。
课堂练习: 定义一 Cache 相干切面, 应用注解表达式定义切入点, 并应用此注解对须要应用 cache 的业务办法进行形容, 代码剖析如下:
第一步: 定义注解 RequiredCache
package com.cy.pj.common.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解, 一个非凡的类, 所有注解都默认继承 Annotation 接口
* @Retention 注解用于定义注解何时失效
* @Target 注解用于定义注解能够形容对象
* @Documented 将注解中的文档正文在提取时也要生存 API 文档
* @author Administrator
*
*/
@Retention(RetentionPolicy.RUNTIME)//@Retention 注解用于定义注解何时失效
@Target(ElementType.METHOD) //@Target 注解用于定义注解能够形容对象
@Documented // 将注解中的文档正文在提取也要生存 API 文档
public @interface RequiredCache {String key() default "";
}
第二步: 定义 SysCacheAspect 切面对象
package com.cy.pj.common.aspect;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
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 SysCacheAspect {// 假如这个容器就是 cache(当然这个 cache 还须要进行更好的设计)
// private Map<Object,Object> cache=new HashMap<>();// 此 map 为线程不平安的 hashMap
private Map<Object,Object> cache=new ConcurrentHashMap<>(); // 此 map 为线程平安的 hashMap<>();// 此 map 为线程不平安的 hashMap
/**
* 基于 @annotation(注解类全名)表达式定义切入点(这种切入点通常了解为细粒度的切入点)
*/
@Pointcut("@annotation(com.cy.pj.common.annotation.RequiredCache)") // 自定义注解
public void doCache() {}
@Pointcut("@annotation(com.cy.pj.common.annotation.ClearCache)") // 自定义注解
public void doClearCache() {}
//FAQ 剖析 如果我当初须要一个分明 chache 的告诉办法,咱们该写在哪个告诉办法中?写在 @AfterReturning 告诉办法中
@AfterReturning("doClearCache()")
public void doAfterReturing() {cache.clear();
}
@Around("doCache()")
public Object around(ProceedingJoinPoint jp)throws Throwable{
//1. 从 cache 中获取数据,如果 cache 中有咱们须要的数据集则间接返回,不须要在查问数据,这样能够确保更好的性能
System.out.println("Get data from cahce");
Object result = cache.get("dept");// 这个 key 的名字是本人随便指定的(未来能够写得更加灵便)
//FAQ 剖析? 如何将 cache 中的 key 定义的更加灵便(在形容切入点的办法注解中直指定)
//FAQ 剖析? 如何获取切入点办法上注解中的 key?(向获取指标办法,而后基于指标办法获取办法上的注解,再通过注解提取 key 的值)
if(result!=null)return result;
//2. 如果 cache 中没有,则从数据库中去查问
result = jp.proceed();
System.out.println("Put data to cache");
//3. 将查问的后果存储到 cache 中,便于下次查问应用
cache.put("dept", result);
return result;
}
}
第三步: 应用 @RequiredCache 注解对特定业务指标对象中的查询方法进行形容。
@RequiredCache
@Override
public List<Map<String, Object\>> findObjects() {
….
return list;
}
3.3 切面优先级设置实现
切面的优先级须要借助 @Order 注解进行形容,数字越小优先级越高,默认优先级比拟低。例如:
定义日志切面并指定优先级。
@Order(1)
@Aspect
@Component
public class SysLogAspect {…}
定义缓存切面并指定优先级:
@Order(2)
@Aspect
@Component
public class SysCacheAspect {…}
阐明:当多个切面作用于同一个指标对象办法时,这些切面会构建成一个切面链,相似过滤器链、拦截器链,其执行剖析如图 - 9 所示:
3.4 要害对象与术语总结
Spring 基于 AspectJ 框架实现 AOP 设计的要害对象概览,如图 -10 所示: