说明:AOP(Aspect Oriented Programming,面相切面编程),是Spring的特性之一,可以在不改变代码的情况下,对方法进行增强,其底层实现是动态代理。
一、代码实现
第一步:使用AOP,先添加依赖
<!--AOP依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
第二步:创建AOP类
创建一个AOP类,类名上加@Component(将该类加入到IOC容器中)、@Aspect注解(表明该类是一个AOP类)
再写一个方法,对代理对象进行增强逻辑,该方法上加@Around()注解,括号内填切入点表达式,表达式里面定义的是目标对象(即被增强对象)
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;/*** AOP类(通知类)*/
@Component
@Aspect
public class MyAop {/*** 通知* @param pjp* @throws Throwable*/@Around("execution(* com.essay.service.impl.AopServiceImpl.*(..))")public Object aopFunction(ProceedingJoinPoint pjp) throws Throwable {// 记录程序开始时的时间戳System.out.println("<<<<<<<<<<<程序开始,获取开始时间戳>>>>>>>>");long begin = System.currentTimeMillis();// 获取代理方法的返回值对象Object obj = pjp.proceed();// 记录程序结束后的时间戳long end = System.currentTimeMillis();System.out.println("<<<<<<<<<<<程序结束,执行时间=" + (end - begin) + "ms>>>>>>>>");return obj;}
}
AopService、AopServiceImpl类
public interface AopService {void test1();void test2();String test3();
}
import com.essay.service.AopService;
import org.springframework.stereotype.Service;@Service
public class AopServiceImpl implements AopService {@Overridepublic void test1() {int sum = 0;for (int i = 0; i < 1000; i++) {sum += i;}System.out.println(sum);}@Overridepublic void test2() {int sum = 0;for (int i = 0; i < 10000; i++) {sum += i;}System.out.println(sum);}@Overridepublic String test3(){System.out.println("test3()...");return "这是一个带返回值的方法...";}
}
第三步:创建测试类
创建测试类,调用AopServiceImpl内的三个方法
import com.essay.service.AopService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest
public class MyTest {@Autowiredprivate AopService aopService;@Testpublic void aopTest1(){aopService.test1();}@Testpublic void aopTest2(){aopService.test2();}@Testpublic void aopTest3(){System.out.println(aopService.test3());}
}
第四步:启动
可以看出,AOP对目标对象的所有方法都做了增强。
aopTest1()方法
aopTest2()方法
aopTest3()方法
二、执行流程
在动态代理中(参考:http://t.csdn.cn/no8zu),使用Proxy.newProxyInstance()根据需要代理的类,会生成一个Proxy对象,该对象的方法包含了被代理类的所有方法,使用Proxy对象的方法,就是使用被代理类的方法+逻辑增强的方法。
AOP技术底层实现原理是动态代理,所以AOP的执行流程中也做了类似这样的事情。在项目启动时,程序实际上已经根据AOP类中对目标对象的增强方法,生成了代理对象,后续项目执行具体方法时,实际上就是代理对象在执行对应的方法,此时的执行结果就是目标对象的方法+AOP中的增强方法。
可以使用Debug,查看使用AOP时,aopService对象的内容
(未使用AOP)
(使用AOP,实际执行方法的是代理对象,执行的结果就是在AOP类中写的通知方法)
三、术语
AOP中的术语及指代如下,熟悉后方便沟通交流:
被增强对象(被代理对象/目标对象): 要被增强的对象【aopServiceImpl类】;
增强对象(代理对象): 增强后的对象 【Debug中看到的aopService的内容】;
连接点: 目标对象所有可以资格被增强的方法都被称为连接点 【aopServiceImpl类中的所有方法】;
切入点表达式: 把连接点转为切入点的表达式,【aopFunction()方法上面,@Around()注解里括号的内容】;
切入点: 确定了要增强的方法,由切入点表达式决定,即切入点表达式指定的方法;
通知: 最终增强的业务逻辑(方法)【aopFunction()方法】;
织入: 通知+切入点合并到一起的过程称为织入(动态代理) 【aopFunction()方法执行的过程】;
切面: 织入的结果(面向切面编程),通知+切入点=切面 【aopFunction()方法+切入点表达式指定的方法】
四、通知类型
AOP的通知类型,即增强方法的类型,除了Around(前置通知)外,还有另外四种,分别是:Before(前置通知)、AfterReturning(返回后通知)、AfterThrowing(异常后通知)和After(后置通知),使用时在方法上添加对应的注解。
为了方便记忆,可以将类型分为两类,一类是Around,另一类是非Around。因为这四种类型做的增强方法,Around也都可以做,可以理解为这四种类型是对Around的具体表现。
(1)非Around
非Around对目标对象的增强,执行结构如下:
try {// 前置通知aopBefor();// 目标对象的方法System.out.println("这是目标对象方法");// 返回后通知aopAfterReturning();}catch (Throwable throwable){// 异常后通知aopAfterThrowing();}finally {// 后置通知aopAfter();}
我这里将非Around的四个方法都写出来,看下这四个方法对目标对象方法的增强效果
(MyAop类)
@Before("execution(* com.essay.service.impl.AopServiceImpl.*(..))")public void aopBefor(){System.out.println("这是前置通知...");}@After("execution(* com.essay.service.impl.AopServiceImpl.*(..))")public void aopAfter(){System.out.println("这是后置通知...");}@AfterReturning("execution(* com.essay.service.impl.AopServiceImpl.*(..))")public void aopAfterReturning(){System.out.println("这是返回后通知...");}@AfterThrowing("execution(* com.essay.service.impl.AopServiceImpl.*(..))")public void aopAfterThrowing(){System.out.println("这是异常后通知...");}
添加测试方法
(AopService类)
void test4();
(AopServiceImpl类)
@Overridepublic void test4(){System.out.println("这是目标对象方法");}
(MyTest类)
@Testpublic void aopTest4(){aopService.test4();}
执行结果
在目标对象方法内,设置一个异常,查看执行结果
(AopServiceImpl类)
@Overridepublic void test4(){// 手动设置一个算数异常int i = 1/0;System.out.println("这是目标对象方法");}
根据上面的结果可分析出,这四种通知类型的执行情况:
@Before(前置通知):此注解标注的通知方法在目标方法前被执行;
@AfterReturning(返回后通知):此注解标注的通知方法在目标方法后被执行,有异常不会执行;
@AfterThrowing(异常后通知):此注解标注的通知方法发生异常后执行;
@After(后置通知):此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行;
(2)Around
@Around环绕通知,完全可以做到以上四种类型一模一样的执行步骤,代码如下
/*** 环绕通知* @param pjp* @throws Throwable*/@Around("execution(* com.essay.service.impl.AopServiceImpl.*(..))")public void aopAround(ProceedingJoinPoint pjp) {try {System.out.println("前置通知");pjp.proceed();System.out.println("返回后通知");}catch (Throwable throwable){System.out.println("异常后通知");}finally {System.out.println("后置通知");}}
执行test4()方法
手动添加一个异常,执行
可以看到执行结果一模一样,因为环绕通知捕捉了异常,所以程序不会因为报错终止。
小结
@Around环绕通知,可以达到与其他四种通知相同的效果,但使用环绕通知需要使用ProceedingJoinPoint对象,决定切入点对象放置的位置(位置放错了,执行结果会不一样)、接收该proceed()方法的结果并返回,其他四种通知不需要。具体使用时,需根据实际需求选择。
另外,需要注意的是,如果切入点方法有返回值,但在环绕通知中未返回Object,方法执行并不会报错,而是返回null,如使用以下的通知,执行test3()方法
/*** 通知* @param pjp* @throws Throwable*/@Around("execution(* com.essay.service.impl.AopServiceImpl.test3(..))")public void aopFunction(ProceedingJoinPoint pjp) throws Throwable {// 记录程序开始时的时间戳System.out.println("<<<<<<<<<<<程序开始,获取开始时间戳>>>>>>>>");long begin = System.currentTimeMillis();// 获取代理方法的返回值对象pjp.proceed();// 记录程序结束后的时间戳long end = System.currentTimeMillis();System.out.println("<<<<<<<<<<<程序结束,执行时间=" + (end - begin) + "ms>>>>>>>>");}
五、切面表达式
切面表达式为,通知注解括号里面的内容,指定了目标对象的范围。
切面表达式由以下六个部分组成,除了修饰符外均不可省略,书写规格如下:
(1)修饰符:public/private,可以省略不写;
(2)返回值:可指定返回值,或者(/*)表示所有返回值;
(3)包:全包名,(/*)代替一个包名,(/…)代替任意子包;
(4)类名:可以指定类名,或者(/*)代替,表示包下的所有类;
(5)方法名:可指定方法名,或者(/*)代替,表示类里面的所有方法;
(6)方法参数:(…/)表示任意类型的参数;
可以看到,此处切面表达式对目标对象的选择,基于包、类和方法的命名,如果包、类、方法的命名越规范,(如查询类方法以find开头,更新方法以update开头),则表达式越容易选择出目标对象。
另外,建议在满足业务需要的前提下,尽量缩小切入点的匹配范围。如,包名匹配尽量不使用…(任意子包),使用*匹配单个包,可以提高效率。
(1)@Pointcut
可使用@Pointcut注解,将切入点表达式的内容抽取出来成一个无参、无返回值、无内容、方法名随意的方法,方便后面通知直接复用切入点表达式,如前面写的四种非Around类型的通知,可以下面这样:
@Pointcut("execution(* com.essay.service.impl.AopServiceImpl.*(..))")public void pt(){}@Before("pt()")public void aopBefor(){System.out.println("这是前置通知...");}@After("pt()")public void aopAfter(){System.out.println("这是后置通知...");}@AfterReturning("pt()")public void aopAfterReturning(){System.out.println("这是返回后通知...");}@AfterThrowing("pt()")public void aopAfterThrowing(){System.out.println("这是异常后通知...");}
值得一提的是,@Pointcut方法的权限修饰符可以设置为private/public,可决定仅被本类复用还是被其他类使用。但建议一个AOP类定义一个@Pointcut方法,仅供本类服务,避免AOP类之间相互调用@Pointcut方法,造成结构混乱。
(2)匹配类中的多个方法
如果需要匹配某个类中的多个方法,不想全选类,也不能单独选一个方法,可以使用两种方法实现
第一种:使用或(||)连接两个目标对象;
限定test1()方法、test2()方法为切入点
@Around("execution(* com.essay.service.impl.AopServiceImpl.test1(..))"+"execution(* com.essay.service.impl.AopServiceImpl.test2(..))")
注:根据业务需要,也可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式
第二种:使用自定义注解;
自定义一个注解,设置该注解仅用于方法上【@Target(ElementType.METHOD)】,且保留至程序运行时【@Retention(RetentionPolicy.RUNTIME)】
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 MyAnnotation {
}
在需要使用AOP的方法上,添加该注解,选择test3()、test4()方法
@Override@MyAnnotationpublic String test3(){System.out.println("test3()...");return "这是一个带返回值的方法...";}@Override@MyAnnotationpublic void test4(){// 手动设置一个算数异常int i = 1/0;System.out.println("这是目标对象方法");}
在AOP类中,切入表达式写成以下这样,表示目标对象为加了@MyAnnotation注解的方法
@Pointcut("@annotation(com.itheima.aop.MyAnnotation)")
注:这有点像接口的作用,将具有某一类功能/特性的类,实现同一个接口。只不过这里作用于不是类上,而是方法上。
六、AOP执行顺序
当多个AOP对同一目标对象的方法增强时,AOP的执行顺序是按照AOP类的排序顺序,当有前置通知、后置通知时,执行结果是嵌套的。
我这里使用@Around,设置前置通知、后置通知对test1()方法的增强。
@Pointcut("execution(* com.essay.service.impl.AopServiceImpl.*(..))")public void pt(){}/*** 环绕通知01* @param pjp* @throws Throwable*/@Around("pt()")public void aopAround01(ProceedingJoinPoint pjp) throws Throwable {System.out.println("01——前置通知");pjp.proceed();System.out.println("01——后置通知");}/*** 环绕通知02* @param pjp* @throws Throwable*/@Around("pt()")public void aopAround02(ProceedingJoinPoint pjp) throws Throwable {System.out.println("02——前置通知");pjp.proceed();System.out.println("02——后置通知");}
七、连接点参数对象(ProceedingJoinPoint)
前面介绍过,ProceedingJoinPoint对象的proceed(),表示执行目标对象的方法,当使用@Around环绕通知时,必须接收该方法执行的结果并返回。
该对象还有以下方法可供使用:
// 1.获取切入点的类名String className = pjp.getTarget().getClass().getName();// 2.获取切入点的方法签名Signature signature = joinPoint.getSignature(); // 3. 获取切入点的方法名String methodName = pjp.getSignature().getName();// 4.获取切入点的参数String methodArgs = Arrays.toString(pjp.getArgs());
以上方法,配合Token令牌,配合AOP对一些操作方法(增加、修改、删除)进行增强,可以实现对用户操作(增加、修改、删除)的日志记录,将用户的信息、操作封装成一个日志对象,写入到数据库中。
日志类(OperateLog )
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.time.LocalDateTime;@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {/*** 日志ID*/private Integer id;/*** 操作人用户名*/private String operateUser;/*** 操作时间*/private LocalDateTime operateTime;/*** 操作的类名*/private String className;/*** 操作的方法名*/private String methodName;/*** 操作的方法参数*/private String methodParams;/*** 操作的方法返回值*/private String returnValue;/*** 操作的时长*/private Long costTime;
}
AOP类中通知方法
......@Pointcut("@annotation(com.itheima.aop.MyAnnotation)")public void pt(){};@Around("pt()")public Object around(ProceedingJoinPoint pjp) throws Throwable {// 记录程序执行开始时间戳long begin = System.currentTimeMillis();// 执行切入点的方法Object obj = pjp.proceed();// 记录程序执行结束时间戳long end = System.currentTimeMillis();// 1.获取切入点的类名String className = pjp.getTarget().getClass().getName();// 2. 获取切入点的方法名String methodName = pjp.getSignature().getName();// 3.获取切入点的参数String methodArgs = Arrays.toString(pjp.getArgs());// 4.获取方法的执行时长long time = end - begin;// 5.获取当时操作人的操作时间LocalDateTime dateTime = LocalDateTime.now();// 获取Token令牌String token = request.getHeader("token");// 解析Token令牌Claims claims = JwtUtils.parseJWT(token);// 6.获取Token令牌中的用户名信息String username = ((String) claims.get("username"));// 7.将收集到的信息封装成一个日志对象OperateLog log = new OperateLog(null, username, dateTime, className, methodName, methodArgs, obj.toString(), time);// 8.写入到数据库中logMapper.insert(log);return obj;}
然后在Controller层中需要记录日志的业务方法上加上@MyAnnotation注解,表示这些方法用户进行操作时需要记录下日志信息
数据库记录的日志如下