Spring AOP简介
AOP 概述
1 AOP 是什么?
AOP(Aspect Orient Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程(OOP)的一种补充和完善。它以通过预编译方式和运行期动态代理方式,实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。
AOP与OOP字面意思相近,但其实两者完全是面向不同领域的设计思想。实际项目中我们通常将面向对象理解为一个静态过程(例如一个系统有多少个模块,一个模块有哪些对象,对象有哪些属性),面向切面的运行期代理方式,理解为一个动态过程,可以在对象运行时动态织入一些扩展功能或控制对象执行。
AOP 应用场景分析?
实际项目中通常会将系统分为两大部分,一部分是核心业务,一部分是非核业务。在编程实现时我们首先要完成的是核心业务的实现,非核心业务一般是通过特定方式切入到系统中,这种特定方式一般就是借助AOP进行实现。AOP就是要基于OCP(开闭原则),在不改变原有系统核心业务代码的基础上动态添加一些扩展功能并可以"控制"对象的执行。例如AOP应用于项目中的日志处理,事务处理,权限处理,缓存处理等等。
AOP 应用原理分析
Spring AOP底层基于代理机制实现功能扩展:
-
假如目标对象(被代理对象)实现接口,则底层可以采用JDK动态代理机制为目标对象创建代理对象(目标类和代理类会实现共同接口)。
-
假如目标对象(被代理对象)没有实现接口,则底层可以采用CGLIB代理机制为目标对象创建代理对象(默认创建的代理类会继承目标对象类型)。
Spring AOP 原理分析
说明:Spring boot2.x 中AOP现在默认使用的CGLIB代理,假如需要使用JDK动态代理可以在配置文件(applicatiion.properties或者application.yml)中进行如下配置:
spring.aop.proxy-target-class=false
AOP 相关术语
横切关注点:跨越应用程序多个模块的方法或功能。即是,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志 , 安全 , 缓存 , 事务等等 ....
Joinpoint(连接点): 所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的 连接点。 (目标对象所有方法叫连接点)
Pointcut(切入点): 所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。 (确定被增强的方法叫切入点)
Advice(通知/增强): 所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。 通知的类型:before,after,afterReturning,afterThrowing,around。
Introduction(引介): 引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方 法或 Field。
Target(目标对象): 代理的目标对象。 (目标对象,真实对象,被代理对象)
Weaving(织入): 是指把增强应用到目标对象来创建新的代理对象的过程。 spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。(可以理解成增强目标对象后返回新的代理对象,这整个过程叫做织入)
Proxy(代理): 一个类被 AOP 织入增强后,就产生一个结果代理类。
Aspect(切面): 是切入点和通知(引介)的结合。一般为一个具体类对象(可以借助@Aspect声明)。
Spring 基于AspectJ框架实现AOP设计的关键对象概览
Spring AOP快速入门(boot项目)
在spring项目中需要首先导入依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.5</version>
</dependency>
<!--spring等依赖略-->
若是spring boot项目,则需导入这个依赖,springboot 会自动添加相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
模拟一个业务层类
@Service
public class UserServiceImpl implements UserService {
@Override
public void add() {
System.out.println("新增用户");
}
}
在spring项目中需要通过配置文件来开启注解,如果是springboot项目则补需要,
配置文件
<context:component-scan base-package="com.gavin"/>
<context:annotation-config/>
<!-- 此功能为开启aop的注解功能, 否则aop注解无效 -->
<aop:aspectj-autoproxy/>
Spring中基于XML的AOP配置步骤
1、把通知Bean也交给spring来管理
2、使用aop: config标签表明开始AOP的配置
3、使用aop: aspect标签表明配置切面
id属性:是给切面提供一个唯一标识
ref属性:是指定通知类bean的Id。
4、在aop:aspect标签的内部使用对应标签来配置通知的类型
假如现在示例是让printLog方法在切入点方法执行之前之前:所以是前置通知
aop:before:表示配置前置通知
method属性:用于指定Logger类中哪个方法是前置通知
pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
<aop: config>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!--配置前置通知:在切入点方法执行之前执行-->
<aop:before method= "beforePrintlog" pointcut-ref="pt1" ></ aop :before>
<!-- 配置后置通知:在切入点方法正常执行之后值。它和异常通知永远只能执行一个-->
<aop:after-returning method=”afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>
<!-- 配置异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个-->
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after- throwing>
<!--配置最终通知:无论切入点方法是否正常执行它都会在其后面执行-->
<aop:after method ="afterPrintLog" pointcut-ref="pt1"></aop:after>
<!--配置切入点表达式id属性用于指定表达式的唯一 标识。expression属性用于指定表达式内容-->
<aop:pointcut id="pt1" expression= ”execution(* com.gavin .service.impl.*.*(..)) "></aop: pointcut>
</ aop:aspect>
</aop:config>
<aop:pointcut id="pt1" expression=" execution(* *.*.*(.. ))"></aop: pointcut>
<!-- 配置切入点表达式id属性用于指定表达式的唯一标识。 expression属性用于指定表达式内容
此标签写在aop:aspect标签内部只能当前切面使用。
它还可以写在aop:aspect外面,此时就变成了所有切面可用
-->
定义一个切面
注意, @Aspect 和 @Component 这两个注解必须有
@Aspect
@Component//讲这个类交给spring容器去管理
public class UserServiceAspect {
/**
* @PointCut 注解用户定义注解, 具体方式可以基于特定表达式进行实现
* 1. bean为一种切入点表达式类型
* 2. sysUserServiceImpl 为Spring容器中的一个bean的名字
* 这里的含义是当sysUserServiceImpl对象中的任意方法执行时, 都有本切面
* 对象的通知方法做功能增强
*/
@Pointcut("bean(userServiceImpl)")
public void doUserService() {}
/**
* 由@Around 注解描述的方法为一个环绕通知方法, 我们可以在此方法内部
* 手动调用目标方法(通过连接点对象ProceedingJoinPoint的proceed方法进行调用)
* 环绕通知 此环绕通知使用的切入点为"bean(sysUserServiceImpl)"
* 环绕通知的特点:
* 1. 编写(格式):
* a. 方法的返回值为Object,
* b. 方法参数为ProceedingJoinPoint类型,
* c. 方法抛出异常为Throwable
* 2. 应用:
* a. 目标方法执行之前和之后都可以进行功能拓展
* b. 相对于其他通知优先级最高
* @param jp 为一个连接对象(封装了正在要执行的目标方法信息)
* @return 目标方法的执行结果
*/
@Around("doUserService()") // 环绕消息
public Object AroundUserService(ProceedingJoinPoint pj) throws Throwable {
System.out.println("执行前");
Object result = pj.proceed(); // 执行业务中的方法,目标方法
System.out.println("执行后");
return result;
}
}
说明:
@Aspect 注解用于标识或者描述AOP中的切面类型,基于切面类型构建的对象用于为目标对象进行功能扩展或控制目标对象的执行。
@Pointcut注解用于描述切面中的方法,并定义切面中的切入点(基于特定表达式的方式进行描述),在本案例中切入点表达式用的是bean表达式,这个表达式以bean开头,bean括号中的内容为一个spring管理的某个bean对象的名字。
@Around注解用于描述切面中方法,这样的方法会被认为是一个环绕通知(核心业务方法执行之前和之后要执行的一个动作),@Aournd注解内部value属性的值为一个切入点表达式或者是切入点表达式的一个引用(这个引用为一个@PointCut注解描述的方法的方法名)。
ProceedingJoinPoint类为一个连接点类型,此类型的对象用于封装要执行的目标方法相关的一些信息。一般用于@Around注解描述的方法参数。
业务切面测试:
package com.gavin.test;
@SpringBootTest
public class Test1 {
@Autowired
private UserService userService;
@Test
public void testAop() {
userService.add();
}
}
结果:
执行前
新增用户
执行后
如果是spring项目中测试
public class AopTest {
AbstractApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
@Test
public void test1() { // 测试环境没有问题
UserService userService = (UserService) ac.getBean("userServiceImpl");
userService.add();
}
}
扩展业务织入增强分析
基于JDK代理方式实现
假如目标对象有实现接口,则可以基于JDK为目标对象创建代理对象,然后为目标对象进行功能扩展
执行流程内部基于反射调用方法
基于CGLIB代理方式实现
假如目标对象没有实现接口,可以基于CGLIB代理方式为目标织入功能扩展
以日志模块为例分析
切面通知应用增强
通知类型
在基于Spring AOP编程的过程中,基于AspectJ框架标准,spring中定义了五种类型的通知(通知描述的是一种扩展业务),它们分别是:
- 前置通知 (
@Before
) 。 - 返回通知 (
@AfterReturning
) 。 - 异常通知 (
@AfterThrowing
) 。 - 后置通知 (
@After
)。 - 环绕通知 (
@Around
) :重点掌握(优先级最高)
通知执行顺序
假如这些通知全部写到一个切面对象中,其执行顺序及过程,如图所示:
执行顺序测试
@Aspect
@Component
public class UserServiceAspect {
@Pointcut("bean(userServiceImpl)")
public void doUserService() {}
@Before("doUserService()")
public void doBefore() {
System.out.println("@Before");
}
@Around("doUserService()") // 环绕消息
public Object AroundUserService(ProceedingJoinPoint pj) throws Throwable {
System.out.println("@Around.Before");
Object result = pj.proceed();
System.out.println("@Around.After");
return null;
}
@After("doUserService()")
public void doAfter() {
System.out.println("@After");
}
@AfterReturning("doUserService()")
public void doAfterReturnin() {
System.out.println("@AfterReturning");
}
@AfterThrowing("doUserService()")
public void doAfterThrowing() {
System.out.println("@AfterThrowing");
}
}
测试结果验证
@Around.Before
@Before
新增用户
@Around.After
@After
@AfterReturning
切入点表达式
表达式用于@Pointcut()
注解中的参数
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(void aop.service.UserServiceImpl.addUser())
匹配addUser方法。execution(void aop.service.PersonServiceImpl.addUser(String))
方法参数必须为String的addUser方法。execution(* aop.service..*.*(..))
万能配置。
@annotation表达式(重点)
@annotaion表达式应用于方法级别,实现细粒度的切入点表达式定义,案例分析
一般我们可以使用自定义注解放在指定的方法上
@annotation(@Pointcut("@annotation(com.aaron.annotation.RequiredCache)"))
匹配有此@RequiredCache
注解描述的方法。
其中:RequiredCache为我们自己定义的注解,当我们使用@RequiredCache注解修饰业务层方法时,系统底层会在执行此方法时进行缓存扩展操作。
自定义注解:
/**
* 自定义注解: (使用@interface定义注解, 默认所有注解都继承Annotation)
* @Target 注解用于告诉JDK我们自己写的注解可以描述的对象
* @Rectention 注解用于告诉JDK我们自己写的注解何时有效
* 说明: 所有的注解都是一种元数据(Meta Data) 一种描述数据的数据
* (例如使用的注解描述类, 描述方法, 描述属性, 木奥数方法参数等等)
*/
@Target(ElementType.METHOD) // 只能用于方法上
@Retention(RetentionPolicy.RUNTIME) // 在运行时有效
public @interface RequiredCache {
}
注解案例
第一步:自定义一个注解
package com.gavin.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 UserAnnotation {
}
第二步:在我们的userService接口上添加一个删除用户的操作,并且实现类重写该方法
第三步:在删除方法上使用自定义注解
package com.gavin.test4;
import org.springframework.stereotype.Service;
import com.gavin.annotation.UserAnnotation;
@Service
public class UserServiceImpl implements UserService {
@Override
public void add() {
System.out.println("新增用户");
}
//在删除方法上使用自定义注解
@UserAnnotation
@Override
public void deleteById(Integer id) {
System.out.println("删除了用户id为"+id+"的用户");
}
}
第四步:创建一个切面类,对使用注解的方法进行业务扩展
package com.gavin.test4;
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 UserServiceAspect {
//使用自定义注解作为切入点
@Pointcut("@annotation(com.gavin.annotation.UserAnnotation)")
public void doUserService() {}
@Around("doUserService()")
public Object doAround(ProceedingJoinPoint jp) throws Throwable {
System.out.println("删除方法之前执行的操作");
Object result = jp.proceed();
System.out.println("删除方法之后执行的操作");
return result;
}
}
第五步:测试
package com.gavin.test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.gavin.test4.UserService;
@SpringBootTest
public class Test1 {
@Autowired
private UserService userService;
@Test
public void testAop() {
userService.add();
System.out.println("-------");
userService.deleteById(22);
}
}
第六步:结果展示
新增用户
-------
删除方法之前执行的操作
删除了用户id为22的用户
删除方法之后执行的操作
从执行的结果分析,使用自定义注解可以更加灵活的进行业务的横切.
切面优先级设置实现
切面的优先级需要借助@Order注解进行描述,数字越小优先级越高,默认优先级比较低。
案例
定义日志切面并指定优先级。
@Order(1)
@Aspect
@Component
public class SysLogAspect {
…
}
定义缓存切面并指定优先级:
@Order(2)
@Aspect
@Component
public class SysCacheAspect {
…
}
说明:当多个切面作用于同一个目标对象方法时,这些切面会构建成一个切面链,类似过滤器链、拦截器链
注解表达式参数获取
第一步:给自定义的注解中设置属性
package com.gavin.annotation;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface UserAnnotation {
String value() default "";
}
第二步:在实现类中给自定义注解的方法的注解上设置参数
package com.gavin.test4;
@Service
public class UserServiceImpl implements UserService {
@Override
public void add() {
System.out.println("新增用户");
}
//在注解上给注解的属性赋值
@UserAnnotation("用户执行删除操作")
@Override
public void deleteById(Integer id) {
System.out.println("删除了用户id为"+id+"的用户");
}
}
第三步:在切面中进行参数获取
package com.gavin.test4;
import java.lang.reflect.Method;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import com.gavin.annotation.UserAnnotation;
@Aspect
@Component
public class UserServiceAspect {
@Pointcut("@annotation(com.gavin.annotation.UserAnnotation)")
public void doUserService() {}
@Around("doUserService()")
public Object doAround(ProceedingJoinPoint jp) throws Throwable {
System.out.println("删除方法之前执行的操作");
Object result = jp.proceed();
System.out.println("删除方法之后执行的操作");
//获取方法签名对象,此对象中封装了要执行的目标方法信息
MethodSignature methodSignature=(MethodSignature) jp.getSignature();
//获取此注解所在的类的字节码对象
Class<?> class1 =jp.getTarget().getClass();
//获取正在执行的方法的参数字节码对象的数组
Class[] parameterTypes = methodSignature.getParameterTypes();
System.out.println("methodSignature paramerterTypes:"+parameterTypes);
//methodSignature paramerterTypes:[Ljava.lang.Class;@6b8280e6
//获取正在执行的方法的名字
String name = methodSignature.getName();
System.out.println("methodSignature.getName name:"+name);
//deleteById
//获取包名+类名
String name2 = class1.getName();
System.out.println("class1.getName name2"+name2);
//com.gavin.test4.UserServiceImpl
//获取类中对应的方法,包括了修饰符 返回值类型以及包名+类名+方法名+方法参数
Method method = class1.getMethod(name, parameterTypes);
System.out.println("class1.getMethod method:"+method);
//public void com.gavin.test4.UserServiceImpl.deleteById(java.lang.Integer)
//假如是jdk代理的话默认取到的是接口中的方法对象,如果是cglib代理的话则是实现类
Method method2 = methodSignature.getMethod();
System.out.println("methodSignature method2"+method2);
//public void com.gavin.test4.UserServiceImpl.deleteById(java.lang.Integer)
//获取注解所在的包和注解名以及参数
UserAnnotation annotation = method2.getAnnotation(UserAnnotation.class);
System.out.println("annotation :"+annotation);
//@com.gavin.annotation.UserAnnotation(value=用户执行删除操作)
//获取注解的参数
String value = annotation.value();
System.out.println("annotation value:"+value);
//用户执行删除操作
return result;
}
}
第四步:测试
package com.gavin.test;
@SpringBootTest
public class Test1 {
@Autowired
private UserService userService;
@Test
public void testAop() {
userService.deleteById(22);
}
}
第五步:测试结果
见上方的代码区域
扩展: 自定义缓存
自定义一个简单的缓存
简单Cache对象的实现, SimpleCache
产品级别Cache要考虑:
- 存储结构, 存储内容(存储对象字节还是存储对象引用)
- 缓存淘汰策略(缓存满的时候是否要淘汰数据)
- GC策略(JVM内存不足时, 是否允许清除Cache中数据)
- 任务调度策略(是否需要每隔一段时间刷新一下缓存)
- 日志记录方式(记录命中次数)
- 线程安全
- ......
package com.cy.pj.common.cache;
import java.util.Map;
@Component
public class SimpleCache {
private Map<Object,Object> cache=new ConcurrentHashMap<>();
public boolean putObject(Object key,Object value) {
cache.put(key, value);
return true;
}
public Object getObject(Object key) {
return cache.get(key);
}
//一般清除缓存的时候,是在方法正常结束后返回进行清除(当业务发生修改新增以及删除操作时)
public void clearObject() {
cache.clear();
}
//.......
}