讲一下AOP
AOP也叫做面向切面编程。最开始学Spring的时候就遇到了这个东西。但是那时候基本用不上所有看了一会儿就直接跳过了。
看一下它的优势:通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性。
记得之前看到一篇文章,很早的时候了。那个作者大概是说,因为他在写一个外接服务的应用。每次应用对接获取的时候需要校验码还有各种验证。他在那时候没有用AOP,直接写了上百个函数来实现。但是后来用了AOP预处理后,就变得很简单实现了。AOP还可以做这种事情。
至于为什么我会用到。因为我需要做一个操作日志管理,这样的操作。所以就开始认真研究住了。
到后面我也会开始看AOP的源码了。
项目情况
因为我在做一个商城项目,对各种精确操作,包括到函数还有函数参数以及执行时间时刻需要。
我以下会讲解两个实例
- 打印操作日志
- 保存自定义操作到数据库
AOP 切面统一打印出入参日志
这是一个简单操作
- 添加 AOP Maven 依赖
在项目 pom.xml 文件中添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 创建aop文件
@Component
@Aspect
public class WebLogAspect {
}
注意那个@Aspect注解。其为重中之重
这是结构目录
- 配置 AOP 切面
在配置 AOP 切面之前,我们需要了解下 aspectj 相关注解的作用:
- @Aspect:声明该类为一个注解类;
- @Pointcut:定义一个切点,后面跟随一个表达式,表达式可以定义为切某个注解,也可以切某个 package 下的方法;
切点定义好后,就是围绕这个切点做文章了: - @Before: 在切点之前,织入相关代码;
- @After: 在切点之后,织入相关代码;
- @AfterReturning: 在切点返回内容后,织入相关代码,一般用于对返回值做些加工处理的场景;
- @AfterThrowing: 用来处理当织入的代码抛出异常后的逻辑处理;
- @Around: 环绕,可以在切入点前后织入代码,并且可以自由的控制何时执行切点;
顺序:@Around->@Before->接口逻辑代码->@After->@AfterReturning
- 声明切点
/**匹配com.b2b.controller包及其子包下的所有类的所有方法*/
@Pointcut("execution(* com.b2b.mall.admin.controller..*.*(..))")
public void executeService(){
}
然后声明后就可以依据切点开始写了
- 完整代码
@Component
@Aspect
public class WebLogAspect {
private Map<Long, Map<String, List<Long>>> threadMap = new ConcurrentHashMap<>(200);
/**匹配com.b2b.controller包及其子包下的所有类的所有方法*/
@Pointcut("execution(* com.b2b.mall.admin.controller..*.*(..))")
public void executeService(){
}
/**
* 前置通知,方法调用前被调用
* @param joinPoint
*/
@Before("executeService()")
public void doBeforeAdvice(JoinPoint joinPoint){
System.out.println(joinPoint.toShortString() + " 开始");
Map<String, List<Long>> methodTimeMap = threadMap.get(Thread.currentThread().getId());
List<Long> list;
if (methodTimeMap == null) {
methodTimeMap = new HashMap<>();
list = new LinkedList<>();
list.add(System.currentTimeMillis());
methodTimeMap.put(joinPoint.toShortString(), list);
threadMap.put(Thread.currentThread().getId(), methodTimeMap);
} else {
list = methodTimeMap.get(joinPoint.toShortString());
if (list == null) {
list = new LinkedList<>();
}
list.add(System.currentTimeMillis());
methodTimeMap.put(joinPoint.toShortString(), list);
}
}
@After("executeService()")
public void doAfterAdvice(JoinPoint joinPoint){
//获取目标方法的参数信息
Object[] obj = joinPoint.getArgs();
//AOP代理类的信息
joinPoint.getThis();
//代理的目标对象
joinPoint.getTarget();
//用的最多 通知的签名
Signature signature = joinPoint.getSignature();
//代理的是哪一个方法
System.out.println("代理方法:" + signature.getName());
//AOP代理类的名字
System.out.println("AOP代理类的名字:" + signature.getDeclaringTypeName());
//AOP代理类的类(class)信息
signature.getDeclaringType();
//获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
//如果要获取Session信息的话,可以这样写:
Enumeration<String> enumeration = request.getParameterNames();
Map<String,String> parameterMap = new HashMap<>();
while (enumeration.hasMoreElements()){
String parameter = enumeration.nextElement();
parameterMap.put(parameter,request.getParameter(parameter));
}
String str = JSON.toJSONString(parameterMap);
if(obj.length > 0) {
System.out.println("请求的参数信息为:"+str);
}
System.out.println(joinPoint.toShortString() + " 结束");
Map<String, List<Long>> methodTimeMap = threadMap.get(Thread.currentThread().getId());
List<Long> list = methodTimeMap.get(joinPoint.toShortString());
System.out.println("代理方法:" + signature.getName() + ", 耗时:" +
(System.currentTimeMillis() - list.get(list.size() - 1)));
list.remove(list.size() - 1);
}
}
上面代码可以看到我先用了@Before 存储了起始时间和使用的函数名称还有线程id
接下来@After 打印出各种信息
注释也解释得差不多。下面会解释以一下joinpoint
然后我们运行在标注在切点下的Controller包里面的随意函数,运行
大致显示如此
- JoinPoint
这里说下JoinPoint
JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象.
我们进入这个对象,得到他的属性
Signature getSignature();
//获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息
Object[] getArgs();
//获取传入目标方法的参数对象
Object getTarget();
//获取被代理的对象
Object getThis();
//获取代理对象
也就这一个函数就完成了
接下来我说下基于注解的
使用自定义注解,AOP 切面统一打印出入参日志
- 自定义日志注解
注解是一个非常方便的东西,去认真看源码的话,可以看到它实际上就是接口。
先自定义一个注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value() default "";
}
上面的target和retention 是元注解
@Target注解,是专门用来限定某个自定义注解能够被应用在哪些Java元素上面的
@Retention注解,翻译为持久力、保持力。即用来修饰自定义注解的生命力。
注解的生命周期有三个阶段:1、Java源文件阶段;2、编译到class文件阶段;3、运行期阶段。
- 创建Aspect
@Slf4j
@Aspect
@Component
public class LogAspect {
@Pointcut("@annotation(com.b2b.mall.admin.annotation.Log)")
public void pointcut() {
// do nothing
}
看一下这次我们的切点和上次的区别。
之前是
"execution(* com.b2b.mall.admin.controller..*.*(..))"
这次是
@annotation(com.b2b.mall.admin.annotation.Log)
关于第一个execution有很多种用法,需要的时候再去查找
- 写Aspect
源码
@Slf4j
@Aspect
@Component
public class LogAspect {
@Autowired
private Properties properties;
@Autowired
private ILogService logService;
@Pointcut("@annotation(com.b2b.mall.admin.annotation.Log)")
public void pointcut() {
// do nothing
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
Object result;
long beginTime = System.currentTimeMillis();
// 执行方法
result = point.proceed();
HttpServletRequest request = HttpContextUtil.getHttpServletRequest();
// 设置 IP地址
String ip = IPUtil.getIpAddr(request);
// 执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
if (properties.isOpenAopLog()) {
// 保存日志
User user = (User) SecurityUtils.getSubject().getPrincipal();
LogWithBlobs log = new LogWithBlobs();
if (user != null) {
log.setUsername(user.getUserName());
}
log.setIp(ip);
log.setTime(time);
logService.saveLog(point, log);
}
return result;
}
}
这次用@Around,省事。
然后我们理一下,也就是说,当用到了@annotation,也就是我们的@Log(" ")注解时,就会执行AOP。
我里面的around怎么写不用在意,只是业务代码
这里用了ProceedingJoinPoint,其继承于JoinPoint,多了proceed方法。
环绕通知(Around) ProceedingJoinPoint 执行proceed方法的作用是让目标方法执行,这也是环绕通知和前置、后置通知方法的一个最大区别。
- saveLog(ProceedingJoinPoint point, LogWithBlobs log)
也只是业务代码,因为从point的获取的请求写在里头了。
public void saveLog(ProceedingJoinPoint point, LogWithBlobs log) throws JsonProcessingException {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
com.b2b.mall.admin.annotation.Log logAnnotation = method.getAnnotation(com.b2b.mall.admin.annotation.Log.class);
if (logAnnotation != null) {
// 注解上的描述
log.setOperation(logAnnotation.value());
}
// 请求的类名
String className = point.getTarget().getClass().getName();
// 请求的方法名
String methodName = signature.getName();
log.setMethod(className + "." + methodName + "()");
// 请求的方法参数值
Object[] args = point.getArgs();
// 请求的方法参数名称
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
String[] paramNames = u.getParameterNames(method);
if (args != null && paramNames != null) {
StringBuilder params = new StringBuilder();
params = handleParams(params, args, Arrays.asList(paramNames));
log.setParams(params.toString());
}
log.setCreateTime(new Date());
log.setLocation(AddressUtil.getCityInfo(log.getIp()));
// 保存系统日志
logMapper.insert(log);
}
- 使用注解
/**
* 退款管理
*
* @param order
* @param pageCurrent
* @param pageSize
* @param pageCount
* @param model
* @return
*/
@Log("打开退款管理")
@RequestMapping("/user/orderRefund_{pageCurrent}_{pageSize}_{pageCount}")
public String refundManage(Order order, @PathVariable Integer pageCurrent,
@PathVariable Integer pageSize,
@PathVariable Integer pageCount,
每次使用,都会通过logMapper.inset(log) 保存至数据库。
以上两个案例也就讲解完了。
AOP的使用方法大致就如此,配合注解使用非常便捷