1 理解AOP
1.1 什么是AOP
AOP(Aspect Oriented Programming),面向切面思想,是Spring的三大核心思想之一(两外两个:IOC-控制反转、DI-依赖注入)。
那么AOP为何那么重要呢?在我们的程序中,经常存在一些系统性的需求,比如权限校验、日志记录、统计等,这些代码会散落穿插在各个业务逻辑中,非常冗余且不利于维护。 例如日志模块 我们想把核心业务代码跟日志实现解耦
让日志代码修改 和核心业务模块代码修改互不影响 实现有序升级 。 这时候就得用到我们aop 而且aop的主要用途之一 也是日志模块的实现
Spring AOP里的名词概念
翻阅Spring AOP相关的文档,发现里边有好多概念性的东西,有很多名词,有很多概念都写的很玄乎,读好几遍都读不懂,我个人认为下边的几个名词比较关键,是必须知道和掌握的
切面类:就是要执行的增强的方法所在的类, 而切点往往是在你的切面类下
大白话的话: 你的切点在哪(需要增强的**
方法
**) 你包含这个方法的类就叫切面类
通知:切面类里的增强方法, (在你切点上要需要加入功能 实现的方法)
目标方法:要执行的目标方法,
织入:把通知和目标方法进行结合,形成代理对象的过程就叫织入
代理对象的方法=通知(增强方法)+目标方法
在我的理解: 切面 = 切点(pointcut) + 通知(advice)
切点 决定了你要额外附加的功能实现在哪个目标上 通知是在这个目标如何执行(执行目标前 通知先执行 - before 通知后执行-- after 还是around 环绕式增强 你自己根据业务需求决定)
aop的本质 是动态代理 在设计模式种 动态代理和静态代理都是对我们理解架构设计有着巨大的帮助 但在此篇不展开 有兴趣可以自行观看
本文注重的是实践 并非原理:
此例子为自定义注解 aop实现日志功能
不多bb 上代码
自定义注解 包含两个属性 value spelValue
package com.example.RasTest.bussinessLog.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
//普通的操作说明
String value() default "";
//spel表达式的操作说明 这是我们获取动态参数的入口
String spelValue() default "";
}
aop 日志功能
package com.example.RasTest.bussinessLog.bussinessAspect;
import com.alibaba.druid.support.logging.Log;
import com.alibaba.druid.util.StringUtils;
import com.alibaba.fastjson.JSONObject;
import com.example.RasTest.bussinessLog.annotation.SysLog;
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.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
@Component //注入到ioc容器中
@Aspect //标注这是一个aop
public class DynamicLogAspect {
@Autowired
private HttpServletRequest request;
/**
* 用于SpEL表达式解析.
*/
private static SpelExpressionParser parser = new SpelExpressionParser();
/**
* 用于获取方法参数定义名字. 能根据你传入的方法 获取该方法的参数名
*/
private static DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
//切点 表明我们要对哪个目标进行增加 附加功能 本例对注解附加
@Pointcut("@annotation(com.example.RasTest.bussinessLog.annotation.SysLog)")
public void logPointCut() {
}
@Around("logPointCut()")
public void around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
//执行方法
Object result = point.proceed();
//执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
System.out.println(result);
//保存日志
saveSysLog(point, time);
}
private void saveSysLog(ProceedingJoinPoint joinPoint, long time) {
//根据切点获取我们的切面类的方法 目标方法 target 被增强的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 在此可以定义我们的日志实体类 用于插入数据库 如果只是单纯写入文件或者输出到控制台 不必定义
/*Log sysLog = new Log();
sysLog.setTime(time);*/
SysLog syslog = method.getAnnotation(SysLog.class);
//判断是否获取得到我们在目标方法上使用的
if (syslog != null) {
//注解上的描述
if (!StringUtils.isEmpty(syslog.value())) {
System.out.println(syslog.value());
}
if (!StringUtils.isEmpty(syslog.spelValue())) {
String spelValue = generateKeyBySpEL(syslog.spelValue(), joinPoint);
System.out.println(spelValue);
}
}
//请求的方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
//sysLog.setMethod(className + "." + methodName + "()");
System.out.println(className + "." + methodName + "()");
//请求的参数 从切点获取目标方法的参数
Object[] args = joinPoint.getArgs();
try {
System.out.println(JSONObject.toJSONString(args));
} catch (Exception e) {
}
//设置IP地址
/*sysLog.setIp(ServletUtil.getClientIP(request));
UserAgent ua = UserAgentUtil.parse(request.getHeader("User-Agent"));
sysLog.setBrowser(ua.getBrowser().toString());*/
System.out.println("successful");
//保存系统日志
// logService.create(sysLog);
}
public static String generateKeyBySpEL(String spELString, ProceedingJoinPoint joinPoint) {
// 通过joinPoint获取被注解方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 使用spring的DefaultParameterNameDiscoverer获取方法形参名数组
String[] paramNames = nameDiscoverer.getParameterNames(method);
// 解析过后的Spring表达式对象
Expression expression = parser.parseExpression(spELString);
// spring的表达式上下文对象
EvaluationContext context = new StandardEvaluationContext();
// 通过joinPoint获取被注解方法的形参
Object[] args = joinPoint.getArgs();
// 给上下文赋值
for (int i = 0; i < args.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
// 表达式从上下文中计算出实际参数值
/*如:
@annotation(key="#student.name")
method(Student student)
那么就可以解析出方法形参的某属性值,return “xiaoming”;
*/
return expression.getValue(context).toString();
}
}
入参
@Data
public class SearchVo {
String keyWord;
String name;
}
controller
@PostMapping(value = "/test/aopLog")
@SysLog(value = "用户登录",spelValue = "'高级搜索' + #searchVo.keyWord + #id")
public String test(@RequestBody SearchVo searchVo){
String id = UUID.randomUUID().toString();
return id;
}
如果想实现日志数据入库 就实现日志数据的service 和dao层 在增强方法@Around中调用