学习
对AOP的初印象
首先先给出一段比较专业的术语(来自百度):
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方
式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个
热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑
的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高
了开发的效率。
然后我们举一个比较容易理解的例子(来自:Spring 之 AOP)
要理解切面编程
,就需要先理解什么是切面
。
用刀把一个西瓜分成两瓣
,切开的切口就是切面
炒菜,锅与炉子共同来完成炒菜
,锅与炉子
就是切面
web层级设计中,web层->网关层->服务层->数据层
,每一层之间也是一个切面
。
编程中,对象与对象之间
,方法与方法之间
,模块与模块之间
都是一个个切面
。
我们一般做接口的时候,一般对每一个接口
都会做活动的有效性校验(是否开始、是否结束等等)
、以及这个接口是不是需要用户登录
。
按照正常的逻辑,我们可以这么做。
这有个问题就是,有多少接口
,就要多少次代码copy
。对于一个“懒人”,这是不可容忍的。好,提出一个公共方法
,每个接口都来调用这个接口
。这里有点切面的味道了。
同样有个问题,我虽然不用每次都copy代码了,但是,每个接口总得要调用这个方法
吧。于是就有了切面
的概念,我将方法注入到接口调用的某个地方(切点)
。
这样接口只需要关心具体的业务
,而不需要关注其他非该接口关注的逻辑或处理
。
红框处,就是面向切面编程
有A,B, C 三个方法
,但是在调用每一个方法之前,要求打印一个日志
:某一个方法被开始调用了!
在调用每个方法之后
,也要求打印日志
:某个方法被调用完了!
一般人会在每一个方法的开始和结尾部分都会添加一句日志打印
,这样做如果方法多了,就会有很多重复的代码
,显得很麻烦,这时候有人会想到,为什么不把打印日志这个功能封装一下,然后让它能在指定的地方(比如执行方法前,或者执行方法后)自动的去调用
呢?如果可以的话,业务功能代码中就不会掺杂这一下其他的代码
,所以AOP就是做了这一类的工作,比如,日志输出,事务控制,异常的处理等
。
OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP
利用一种称为“横切
”的技术,剖解开封装的对象内部
,并将那些影响了多了类的公共行为封装到一个可重用的模块
,并将其命名为“Aspect”
,即切面
。
AOP中的相关概念
看过了上面的例子,我想大家脑中对AOP已经有了一个大致的雏形,但是又对上面提到的切面之类的术语有一些模糊的地方,接下来就来讲解一下AOP中的相关概念,了解了AOP中的概念,才能真正的掌握AOP的精髓。
这里还是先给出一个比较专业的概念定义:
Aspect(切面)
:Aspect
声明类似于 Java 中的类声明
,在Aspect
中会包含着一些 Pointcut 以及相应的 Advice
。 SpringAOP 将切面定义的内容织入到我们的代码
中,从而实现前后的控制逻辑
。比如常写的拦截器Interceptor,这就是一个切面类
Joint point(连接点)
:表示在程序中明确定义的点
,典型的包括方法调用
,对类成员的访问
以及异常处理程序块的执行
等等,它自身还可以嵌套其它 joint point
。
如某个方法调用前,调用后,方法抛出异常后
,这些代码中的特定点称为连接点
。简单来说,就是在哪加入你的逻辑增强
连接点 表示 具体要拦截的方法 , 切点 是定义一个 范围 ,而 连接点 是具体到 某个方法
Pointcut(切点)
:表示一组 joint point
,这些 joint point 或是通过逻辑关系组合起来
,或是通过通配、正则表达式
等方式集中起来
,它定义了相应的 Advice 将要发生的地方
。
如何定位到某个感兴趣的连接点
,就需要通过切点来定位
。比如,连接点–数据库的记录
,切点–查询条件
切点用于来 限定Spring-AOP启动的 范围 ,通常我们采用表达式的方式来设置,所以关键词是 范围
Advice(增强)
:Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作
,就是拦截器定义的相关方法
,它通过before、after 和 around
来区别是在每个 joint point 之前、之后还是代替执行的代码
。Target(目标对象)
:织入 Advice 的目标对象
。需要被加强的业务对象
Weaving(织入)
:将Aspect 和其他对象连接起来
, 并创建 Adviced object 的过程
,将增强添加到对目标类具体连接点上
的过程
织入是一个形象的说法,具体来说,就是 生成代理对象 并将 切面内容 融入到 业务流程 的 过程
代理类(Proxy)
一个类被AOP织入增强后
,就产生了一个代理类
然后举一个容易理解的例子:
让我们来假设一下, 从前有一个叫爪哇的小县城
, 在一个月黑风高的晚上, 这个县城中发生了命案
. 作案的凶手十分狡猾, 现场没有留下什么有价值的线索. 不过万幸的是, 刚从隔壁回来的老王
恰好在这时候无意中发现了凶手行凶的过程
, 但是由于天色已晚, 加上凶手蒙着面, 老王并没有看清凶手的面目, 只知道凶手是个男性, 身高约七尺五寸
爪哇县的县令根据老王的描述, 对守门的士兵下命令说: 凡是发现有身高七尺五寸的男性, 都要抓过来审问. 士兵当然不敢违背县令的命令, 只好把进出城的所有符合条件的人都抓了起来
来让我们看一下上面的一个小故事和 AOP 到底有什么对应关系.
首先我们知道, 在Spring AOP
中Joint point
指代的是所有方法的执行点
, 而 point cut
是一个描述信息
, 它修饰的是 Joint point
, 通过 point cut
, 我们就可以确定哪些 Joint point 可以被织入 Advice
.
对应到我们在上面举的例子, 我们可以做一个简单的类比, Joint point
就相当于 爪哇的小县城里的百姓
, pointcut
就相当于 老王所做的指控
, 即凶手是个男性, 身高约七尺五寸
, 而 Advice
则是施加在符合老王所描述的嫌疑人的动作
: 抓过来审问
-
Joint point
:爪哇的小县城里的百姓
: 因为根据定义,Joint point
是所有可能被织入 Advice 的候选的点
, 在Spring AOP
中, 则可以认为所有方法执行点都是 Joint point
. 而在我们上面的例子中, 命案发生在小县城中, 按理说在此县城中的所有人都有可能是嫌疑人.
-
Pointcut
:男性, 身高约七尺五寸
: 我们知道, 所有的方法(joint point) 都可以织入 Advice, 但是我们并不希望在所有方法上都织入 Advice
, 而Pointcut 的作用就是提供一组规则来匹配joinpoint
,给满足规则的 joinpoint 添加 Advice.
同理, 对于县令来说, 他再昏庸, 也知道不能把县城中的所有百姓都抓起来审问, 而是根据凶手是个男性, 身高约七尺五寸, 把符合条件的人抓起来. 在这里 凶手是个男性, 身高约七尺五寸 就是一个修饰谓语
, 它限定了凶手的范围
,满足此修饰规则的百姓都是嫌疑人, 都需要抓起来审问.
-
Advice
:抓过来审问
, Advice 是一个动作
, 即一段 Java 代码
, 这段 Java 代码是作用于 point cut 所限定的那些 Joint point 上的
. 同理, 对比到我们的例子中,抓过来审问
这个动作
就是对作用于那些满足
男性, 身高约七尺五寸
的爪哇的小县城里的百姓.
-
Aspect
:Aspect 是point cut 与 Advice 的组合
, 因此在这里我们就可以类比: “根据老王的线索, 凡是发现有身高七尺五寸的男性, 都要抓过来审问
” 这一整个动作
可以被认为是一个 Aspect
advice 类型和表达式
AOP
中的Joinpoint
可以有多种类型
:构造方法调用
,字段的设置和获取
,方法的调用
,方法的执行
,异常的处理执行
,类的初始化
。也就是说在AOP的概念中我们可以在上面的这些Joinpoint上织入我们自定义的Advice
,但是在Spring中却没有实现上面所有的joinpoint,确切的说,Spring只支持方法执行类型的Joinpoint。
Advice 的类型
前置通知 before advice
, 在join point 前被执行的 advice
. 虽然before advice
是在 join point 前被执行
, 但是它并不能够阻止 join point 的执行
,除非发生了异常
(即我们在 before advice 代码中, 不能人为地决定是否继续执行 join point 中的代码)后置通知 after
,在join point后
做些操作,无论是否发生异常,它都会执行
,比如关闭连接对象返回通知 after return advice
, 在一个join point 正常返回后
执行的 advice异常通知 after throwing advice
, 当一个join point 抛出异常后
执行的 adviceafter(final) advice
, 无论一个 join point 是正常退出还是发生了异常,都会被执行的 advice
.环绕通知 around advice
, 在join point 前
和joint point 退出后
都执行的 advice. 这个是最常用的 advice
.introduction
,introduction可以为原有的对象增加新的属性和方法
。
try{
try{
@around
@before
method.invoke();
@around
}catch(){
throw new Exception();
}finally{
@after
}
@afterReturning
}catch(){
@afterThrowing
}
其中的around
是最为特殊的切入时机
, 它的切入点
也必须为ProceedingJoinPoint
, 其它均为JoinPoint
我们需要手动调用ProceedingJoinPoint的proceed方法
, 它会去执行目标方法的业务逻辑
around最麻烦, 却也是最强的
表达式
任意 公共方法 的执行: execution(public * *(..))
任何一个以 “set”开始 的方法的执行: execution(* set*(..))
AccountService 接口的 任意方法 的执行: execution(* com.xyz.service.AccountService.*(..))
定义在 service包 里的 任意方法 的执行: execution(* com.xyz.service.*.*(..))
定义在 service包 和 所有子包 里的 任意类的任意方法 的执行: execution(* com.xyz.service..*.*(..))
第一个 *
表示匹配任意的方法返回值
, .. (两个点)
表示零个或多个
,第一个 ..
表示service包及其子包
, 第二个 *
表示所有类
, 第三个 *
表示所有方法
,第二个 ..
表示方法的任意参数个数
定义在 pointcutexp包 和 所有子包 里的 JoinPointObjP2类 的 任意方法 的执行: execution(*com.test.spring.aop.pointcutexp..JoinPointObjP2.*(..))")
pointcutexp包里 的 任意类 :
within(com.test.spring.aop.pointcutexp.*)
pointcutexp包 和 所有子包 里的 任意类 :
within(com.test.spring.aop.pointcutexp..*)
实现了Intf接口的所有类,如果Intf不是接口,限定Intf单个类:this(com.test.spring.aop.pointcutexp.Intf)
当一个实现了接口的类被AOP的时候,用getBean方法必须cast为接口类型,不能为该类的类型
带有@Transactional标注 的 所有类 的 任意方法 :
@within(org.springframework.transaction.annotation.Transactional)
@target(org.springframework.transaction.annotation.Transactional)
带有@Transactional标注 的 任意方法:
@annotation(org.springframework.transaction.annotation.Transactional)
@within和@target 针对类 的注解,@annotation是 针对方法 的注解
参数 带有 @Transactional标注 的方法:
@args(org.springframework.transaction.annotation.Transactional)
参数为 String类型(运行是决定) 的方法: args(String)
AOP应用范围
Authentication 权限
Caching缓存
Context passing内容传递
Error handling 错误处理
Lazy loading 延时加载
Debugging 调试
logging, tracing, profiling and monitoring 记录跟踪 优化 校准
Performance optimization性能优化
Persistence 持久化
Resource pooling资源池
Synchronization 同步
Transactions事务
日志记录,跟踪,优化和监控
事务的处理
持久化
性能的优化
资源池,如数据库连接池的管理
系统统一的认证、权限管理等
应用系统的异常捕捉及处理
针对具体行业应用的横切行为
使用
自定义注解
自定义注解详细介绍
springboot项目中自定义注解的使用总结、java自定义注解实战(常用注解DEMO)
通过AOP+Java注解+EL表达式获取方法参数的值
支持 spel 表达式
使用Spring Expression Language (SpEL)解析表达式
硬核资源!清华博士的Spring Boot中AOP与SpEL笔记,码农:膜拜
AOP
: 面向切面编程?NO, 我们低端点, 它就是一个非常厉害的装饰器
, 可以和业务逻辑平行运行
, 适合处理一些日志记录/权限校验
等操作
SpEL:
全称SpEL表达式,
可以理解为JSP的超级加强版
, 使用得当可以为我们节省代码(此话为抄袭), 大家使用它最多的地方其实是引入配置
, 例如:
// 你看我熟悉不?
@Value("#{file.root}")
那么什么时候会一起使用它们呢?
其实很多经验丰富的大佬们下意识就能回答, 记录系统日志
没错, AOP是与业务逻辑平行
, SpEL是与业务数据平行
, 把它们结合起来, 就能让我们在传统的面向对象/过程编程的套路中更上一层楼
接下来我就用一个实际的记录业务日志
功能的实现来记录如何在Spring Boot中使用AOP与SpEL
要使用SpEL, 肯定难不住每一位小伙伴, 但它到底是如何从一个简单的文字表达式转换为运行中的数据内容呢?
其实Spring
和Java
已经提供了大部分功能, 我们只需要手动处理如下部分
:
TemplateParserContext: 表达式解析模板, 即如何提取SpEL表达式
Expression: 表达式对象
SpelExpressionParser: 表达式解析器
RootObject: 业务数据内容, SpEL表达式解析过程中需要的数据都从这其中获取
ParameterNameDiscoverer: 参数解析器, 在SpEL解析的过程中, 尝试从rootObject中直接获取数据
EvaluationContext: 解析上下文, 包含RootObject, ParameterNameDiscoverer等数据, 是整个SpEL解析的环境
MethodBasedEvaluationContext: 顶级父类是 EvaluationContext
package org.springframework.context.expression;
public class MethodBasedEvaluationContext extends StandardEvaluationContext {
private final Method method; //表达式作用的方法对象
private final Object[] arguments; //方法里的参数
private final ParameterNameDiscoverer parameterNameDiscoverer; //参数解析器
private boolean argumentsLoaded = false;
public MethodBasedEvaluationContext(Object rootObject, Method method, Object[] arguments, ParameterNameDiscoverer parameterNameDiscoverer) {
super(rootObject); //该方法的class
this.method = method;
this.arguments = arguments;
this.parameterNameDiscoverer = parameterNameDiscoverer;
}
@Nullable
public Object lookupVariable(String name) {
...
}
protected void lazyLoadArguments() {
...
}
}
MethodBasedEvaluationContext context = new MethodBasedEvaluationContext(targetClass, method, args, this.getParameterNameDiscoverer());
那么SpEL的过程我们可以粗略概括为:
设计RootObject
->设计SpEL表达式的运行上下文
->设计SpEL解析器(包括表达式解析模板和参数解析器)
日志记录
用户上下文,超级管理员,授权效验 等 百度网盘 AopDemo 必看
权限效验
缓存注解
spring里面有一个@Cacheable注解,使用这个注解的方法,如果key在缓存中已有,则不再进入方法,直接从缓存获取数据,缓存没有值则进入并且把值放到缓存里面,我们写一个类似的,简单的注解
备注:使用了这个注解,如果数据有修改,记得清除缓存
1)创建自定义注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomCache {
//缓存前缀
String prefix() default "";
//缓存key
String key() default "";
}
2)然后再创建一个AOP切面类来实现这个注解
@Aspect
@Component
public class CustomCacheAspect {
private static HashMap<String,Object> cacheMap = new HashMap<>();
@Pointcut("@annotation(CustomCache)")
public void cache() {
}
@Around("cache()")
public Object printLog(ProceedingJoinPoint joinPoint){
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取切入点所在的方法
Method method = signature.getMethod();
//获取操作
CustomCache customCache = method.getAnnotation(CustomCache.class);
String prefix = customCache.prefix();
if(prefix == null || prefix.equals("")){
//如果前缀为空,默认使用类型+方法名作为缓存的前缀
//获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
//获取请求的方法名
String methodName = method.getName();
prefix = className+"-"+methodName;
}
String key = customCache.key();
if(key == null || key.equals("")){
//获取接口的参数
Object[] o = joinPoint.getArgs();
//如果key为空,默认使用参数名称为id的值作为id
String[] parameterNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
for(int i=0;i<parameterNames.length;i++){
String paramName = parameterNames[i];
if(paramName.equals("id")){
key = o[i].toString();
}
}
}
String cacheKey = prefix+key;
Object result = cacheMap.get(cacheKey);
if(result != null){
//缓存不为空,直接返回缓存结果
return result;
}
try {
result = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
cacheMap.put(cacheKey,result);
return result;
}
}
3)把注解加在查询用户信息的Service上
@Override
@CustomCache()
public User findUser(Integer id) {
return baseMapper.selectById(id);
}
测试可以看到只有首次才会进入这个方法
查询用户信息,查询出用户信息后再调用这个方法只是从缓存中获取
防刷新注解
有一些场景,例如申请提交之后几秒内需要防止用户重复提交
,我们后端通过注解实现这一功能
1)创建自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AvoidRepeatSubmit {
/**
* 指定时间内不可重复提交,单位:s
*
* @return
*/
long timeout() default 3;
}
2)再创建一个AOP切面类来实现这个注解
@Aspect
@Component
@Slf4j
public class AvoidRepeatSubmitAspect {
@Autowired
private RedisRepository redisRepository;
@Before("@annotation(cn.com.bluemoon.admin.web.common.aspect.AvoidRepeatSubmit)")
public void repeatSumbitIntercept(JoinPoint joinPoint) {
// ip + 类名 + 方法 + timeout
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ip = IpAddressUtils.getIpAdrress(request);
String className = joinPoint.getTarget().getClass().getName();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String methodName = method.getName();
// 获取配置的过期时间
AvoidRepeatSubmit annotation = method.getAnnotation(AvoidRepeatSubmit.class);
long timeout = annotation.timeout();
StringBuilder builder = new StringBuilder();
builder.append(ip).append(",").append(className).append(",").append(methodName).append(",").append(timeout).append("s");
String key = builder.toString();
log.info(" --- >> 防重提交:key -- {}", key);
// 判断是否已经超过重复提交的限制时间
String value = redisRepository.get(key);
if (StringUtils.isNotBlank(value)) {
String messge = MessageFormat.format("请勿在{0}s内重复提交", timeout);
throw new WebException(messge);
}
this.redisRepository.setExpire(key, key, timeout);
}
}