后续我会陆陆续续更新Spring的一些介绍、应用、以及原理的文档。大家如果觉得对自己有用就点个关注吧。
文中描述如有问题,欢迎留言指证。
引言
本文中的例子我尽量写的简单,避免一些我平时查资料时一些例子中出现的大量无用代码,产生让人阅读不下去的感觉
什么是AOP
这个问题我就不复制粘贴了,一些很官方的回答你直接百度就行了。那些回答统一都是一个特点懂得人不看都懂,不懂人基本上看不懂。我就通俗的介绍一下。
其实AOP就是一个功能,这个功能可以让系统中的接口在不修改代码的情况下做更多的事,比如:我有一个方法
check()
,我通过AOP可以在不修改
check()
方法里面代码的情况下,让它在执行
check()
方法之前或之后打印出一条
log.info("我被增强了")
日志出来(当然打印日志只是举个栗子,你可以在调用方法之前或之后做更多的事情)。这个操作就叫增强,对这个接口的增强。
AOP:面向切面编程
什么是切面,通俗的说就是程序中可以被增强的接口(如果不理解什么是增强,你也暂时可以理解成,程序中可以特殊操作的接口)。这些接口,官方术语叫”连接点”,这里还有一个术语要清楚,他叫”切点”,切点是连接点的子集,怎么去理解他也很简单,你可以进行增强(操作)的接口叫”连接点”,当你真的选择一个或多个连接点进行增强(操作)的时候,那么这些接口就叫切点。
通知应用
定义切面类
好了,我们了解了什么是AOP现在让我们来使用他的功能吧,我们第一个用到的功能就是通知。这是AOP的核心功能!
通知分为5个类型
@Before ->前置通知
@AfterReturning ->后置通知
@Around ->环绕通知
@AfterThrowing ->异常通知
@After ->最终通知
好了现在我们来上代码,在代码里学习,其实很简单。
package com.wangx.sys.aspect;
import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* @author: wangxu
* @date: 2020/9/11 14:00
*/ @Aspect //声明这个是一个切面类 @Component //将当前类交给容器管理,就是把这个注入到IOC @Slf4j //LomBok插件(IDEA插件)可以自动获取log对象
public class testAspect {
//声明公共切入点
@Pointcut("execution(* com.wangx.sys.controller.*(..))")
public void pointcut() {}
//前置通知注解,只有一个参数时,value可以省略
@Before("execution(* com.wangx.sys.controller.SysUserController.test(..))")
public void printLogBefore(JoinPoint joinPoint){
log.info("前置通知->执行");
}
//后置通知注解,当参数不止一个是要写属性名
@AfterReturning(value="execution(* com.wangx.sys.controller.SysUserController.test(..))",returning = "result")
public void printLogAfterReturning(JoinPoint joinPoint,Object result){
log.info("后置通知->执行----返回结果: ->" + result);
}
//环绕通知,环绕通知可以修改方法的返回值功能异常强大
@Around(value = "pointcut()")
public Object printLogAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("环绕通知-前置通知->执行");
//执行方法
Object proceed = joinPoint.proceed();
log.info("方法执行后的结果->" + proceed);
log.info("环绕通知-后置通知->执行");
return proceed;
}
//最终通知
@After("execution(* com.wangx.sys.controller.SysUserController.test(..))")
public void printLogAfter(JoinPoint joinPoint){
log.info("最终通知->执行");
}
}
这些注解都会在你配置的路径中,搜索匹配条件的接口,然后在相应的位置执行这些你在切面类中自己定义的方法,这样就可以在不动原来的代码对其进行功能上的增强。
然后让我们来看看被增强的类
//省去导包
/**
* @author: wangxu
* @date: 2020/3/18 15:16
*/
@RestController @RequestMapping(value = "/v1/sys/sysuer") @Slf4j @WangxLog(value = "我是一个注解日志") public class SysUserController {
@ApiOperation(value = "测试接口")
@GetMapping(value = "/test")
public Object test() {
log.info("我是原方法->我已经执行了");
return "访问成功";
}
}
OK! 接下来看一下执行结果
2020-09-12 11:55:16.118 INFO 67052 --- [nio-8220-exec-7] com.wangx.sys.aspect.testAspect : 环绕通知-前置通知-执行
2020-09-12 11:55:16.119 INFO 67052 --- [nio-8220-exec-7]com.wangx.sys.aspect.testAspect : 前置通知->执行
2020-09-12 11:55:16.128 INFO 67052 --- [nio-8220-exec-7]c.w.sys.controller.SysUserController : 我是原方法->我已经执行了
2020-09-12 11:55:16.129 INFO 67052 --- [nio-8220-exec-7]com.wangx.sys.aspect.testAspect : 方法执行后的结果-访问成功
2020-09-12 11:55:16.129 INFO 67052 --- [nio-8220-exec-7]com.wangx.sys.aspect.testAspect : 环绕通知-后置通知->执行
2020-09-12 11:55:16.130 INFO 67052 --- [nio-8220-exec-7]com.wangx.sys.aspect.testAspect : 最终通知->执行
2020-09-12 11:55:16.130 INFO 67052 --- [nio-8220-exec-7]com.wangx.sys.aspect.testAspect : 后置通知->执行----返回结果: ->访问成功
这里有一点我们需要注意,来看一下他们的执行顺序
- 环绕前置通知
- 前置通知
- 原方法执行
- 环绕后置通知
- 最终通知
- 后置通知
好了,现在你已经完成了一个简单的通知功能,让我们来简单讲解一下上面得代码,这段代码你看懂以后其实自定义注解你也就会。首先让我们来看下类上面的三个注解
@Aspect //声明这个是一个切面类(这个是最用药的)
@Component //将当前类交给容器管理,就是把这个注入到IOC(什么?这个注解你是第一次听说?来人拖出去腿打断!)
@Slf4j //LomBok插件(IDEA插件)可以自动获取log对象(这就是一个IDEA的插件你也可以不用,那就手动获取logger对象)
然后是下面的几个方法注解,一些就基本的说明我在代码中已经注释了,这里面就是详细说明一下
//声明公共切入点
@Pointcut("execution(* com.wangx.sys.controller.*(..))")
//前置通知注解,只有一个参数时,value可以省略
@Before("execution(* com.wangx.sys.controller.SysUserController.test(..))")
//环绕通知,环绕通知可以修改方法的返回值功能异常强大
@Around(value = "execution(* com.wangx.sys.controller.SysUserController.test(..))")
public Object printLogAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("环绕通知-前置通知->执行");
//执行方法
//这个proceed拿到的是这个方法的返回值,你也看到了他拿到的是Object类型如果你想用它进行操作需要进行强制类型转换
Object proceed = joinPoint.proceed();
log.info("方法执行后的结果->" + proceed);
log.info("环绕通知-后置通知->执行");
//这个位置如果不把执行后回去的返回值在return回去,那么真正的方法就拿不到返回值了.
//换句话说,就是如果你这里return "返回值",那么在你调用被增强接口是,返回的就时"返回值"这个字符串
return proceed;
}
//最终通知
@After("execution(* com.wangx.sys.controller.SysUserController.test(..))")
你可看到上面的代码中我们用到
@Pointcut("execution(* com.wangx.sys.controller.*(..))")
和
@Before("execution(* com.wangx.sys.controller.SysUserController.test(..))")
这样的注解,他们的功能在之前我已经说过了。但是你可能对注解中的参数感到疑惑。那么下面我们就来说说SpringAOP表达式
SpringAOP表达式
类别 | 函数 | 入参 | 说明 |
---|---|---|---|
方法切点函数 | execution() | 方法匹配模式串 | 表示满足某一匹配模式的所有目标类方法连接点。如execution(* greetTo(…))表示所有目标类中的greetTo()方法。 |
@annotation() | 方法注解类名 | 表示标注了特定注解的目标方法连接点。如@annotation(com.baobaotao.anno.NeedTest)表示任何标注了@NeedTest注解的目标类方法。 | |
方法入参切点函数 | args() | 类名 | 通过判别目标类方法运行时入参对象的类型定义指定连接点。如args(com.baobaotao.Waiter)表示所有有且仅有一个按类型匹配于Waiter的入参的方法。 |
@args() | 类型注解类名 | 通过判别目标方法的运行时入参对象的类是否标注特定注解来指定连接点。如@args(com.baobaotao.Monitorable)表示任何这样的一个目标方法:它有一个入参且入参对象的类标注@Monitorable注解。 | |
target() | 类名 | 假如目标类按类型匹配于指定类,则目标类的所有连接点匹配这个切点。如通过target(com.baobaotao.Waiter)定义的切点,Waiter、以及Waiter实现类NaiveWaiter中所有连接点都匹配该切点。 | |
@within() | 类型注解类名 | 假如目标类按类型匹配于某个类A,且类A标注了特定注解,则目标类的所有连接点匹配这个切点。 如@within(com.baobaotao.Monitorable)定义的切点,假如Waiter类标注了@Monitorable注解,则Waiter以及Waiter实现类NaiveWaiter类的所有连接点都匹配。 | |
@target() | 类型注解类名 | 目标类标注了特定注解,则目标类所有连接点匹配该切点。如@target(com.baobaotao.Monitorable),假如NaiveWaiter标注了@Monitorable,则NaiveWaiter所有连接点匹配切点。 | |
代理类切点函数 | this() | 类名 | 代理类按类型匹配于指定类,则被代理的目标类所有连接点匹配切点。 |
配置这些的主要功能自动扫描我需要增强哪些类。所有配置上的接口都会被代理。对其实所谓增强其实就是代理,近期我会更新一篇介绍代理原理的文章。
在上面的列表中你也看到了一些是用来匹配方法名的,一些是用来匹配注解的,到这里我猜你应该想到了咱们Spirng全家桶中注解的工作原理了。对!用的就是代理。对一些标有注解接口的增强。
通过这个原理,我们开始展示如何自定义一个注解。
定义一个注解
package com.wangx.sys.annotation;
import java.lang.annotation.*;
/**
* @author: wangxu
* @date: 2020/9/11 11:29
*/
//@Documented:注解信息会被添加到Java文档中
//@Retention:注解的生命周期,表示注解会被保留到什么阶段,可以选择编译阶段、类加载阶段,或运行阶段
//@Target:注解作用的位置,ElementType.METHOD表示该注解仅能作用于方法上
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface WangxLog {
//标识这个注解有一个value值,default可以输入他的默认值,并且声明这个value是可填可不填的,如果没有这个关键字那这个value值就是必须的.
String value() default "";
//与上面同理
int num() default 1;
}
在注解上面的
@Target(ElementType.METHOD)
,
@Retention(RetentionPolicy.RUNTIME)
,
@Target(ElementType.METHOD)
被称为元注解,所谓元注解,可以简单理解为注解的注解。这一部分如果不清楚的话可以自行百度,后续我也会将这部分内容整理出来。接下来来看看我们是怎么配置他的切面类的。
注解的切面类
package com.wangx.sys.aspect;
import com.wangx.sys.annotation.KthLog;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* @author: wangxu
* @date: 2020/9/11 11:38
*/
@Aspect //声明这是一个切面类
@Component
public class WangxLogAspect {
@Pointcut("@annotation(com.wangx.sys.annotation.WangxLog)")
private void pointcut() {}
@Before("pointcut() && @annotation(log)")
public void advice(KthLog log) {
System.out.println("WangxLog日志的内容为[" + log.value() + "]");
}
}
其实呀!你仔细看你会发现他和上面的普通通知的使用是一样的唯一不一样的就是,切面类匹配的规则,这里面匹配的是带有@WangxLog 这个注解的接口进行增强。现在让我们来看看使用
注解使用
package com.wangx.sys.controller;
//省去导包
/**
* @author: wangxu
* @date: 2020/3/18 15:16
*/
@RestController
@RequestMapping(value = "/v1/sys/sysuer")
@Slf4j
@WangxLog(value = "我是一个注解日志")
public class SysUserController {
@ApiOperation(value = "测试接口")
@GetMapping(value = "/test")
public Object test() {
log.info("我是原方法->我已经执行了");
return "访问成功";
}
}
下面是执行结果
2020-09-12 11:55:11.367 INFO 67052 --- [nio-8220-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 8 ms --- Kth日志的内容为[我是一个注解日志] ---
2020-09-12 11:55:16.128 INFO 67052 --- [nio-8220-exec-7] c.w.sys.controller.SysUserController : 我是原方法->我已经执行了
如果喜欢请点击关注支持一下,后续还会更新更多我学以路上的一些知识总结。并且我尽量把这些知识进行分块出,以便朋友们更好的理解。