简介
AOP (Aspect Oriented Programming) 即 面向切面编程,是一种编程典范,它通过分离横切关注点来增加程序的模块化。通俗说就是 AOP 可以在不修改现有代码的情况下对现有代码增加一些功能,那么这就是 AOP 最强大的功能。
目前最受欢迎的 AOP 库有两个,一个是 AspectJ, 另外一个是 Spring AOP。
核心概念
- Aspect:即切面,切面一般定义为一个 Java 类, 每个切面连接点所采用的处理逻辑,也就是向连接点注入的代码, AOP在特定的切入点上执行的增强处理。
- @Before: 标识一个前置增强方法,相当于BeforeAdvice的功能.
- @After: final增强,不管是抛出异常或者正常退出都会执行.
- @AfterReturning: 后置增强,似于AfterReturningAdvice, 方法正常退出时执行.
- @AfterThrowing: 异常抛出增强,相当于ThrowsAdvice.
- @Around: 环绕增强,相当于MethodInterceptor.
- Joinpoint:即连接点,程序执行的某个点,比如方法执行。构造函数调用或者字段赋值等。
- Pointcut:即切点,一个匹配连接点的正则表达式。指明Advice要在什么样的条件下才能被触发,当一个连接点匹配到切点时,一个关联到这个切点的特定的 (Advice) 会被执行。
- Advisor(增强): 是PointCut和Advice的综合体,完整描述了一个advice将会在Pointcut所定义的位置被触发。
- AOP Proxy:AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类。
使用实践
按照整体规划,这里需要给项目增加一个基于自定义注解的日志切面程序。实现目标:任何请求都要记录到日志,目前不引入数据库,先组织成对应的格式,在控制台输出。
目录组织形式:
1 引入Spring boot AOP的包
maven文件增加依赖
org.springframework.boot
spring-boot-starter-aop
2 定义切入点要干啥
定义记录那些信息Log类(Log.java)
package com.springboot.action.saas.common.logging.domain;
import lombok.Data;
import java.sql.Timestamp;
@Data
public class Log {
//描述
private String description;
//方法名
private String method;
//参数
private String params;
//日志类型
private String logType;
//请求ip
private String requestIp;
//请求耗时
private Long time;
//异常详细
private String exceptionDetail;
//请求实践
private Timestamp createTime;
//构造函数(代参)
public Log(String logType, Long time) {
this.logType = logType;
this.time = time;
}
}
日志记录业务接口(LogService.java)
package com.springboot.action.saas.common.logging.service;
import com.springboot.action.saas.common.logging.domain.Log;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.scheduling.annotation.Async;
/*
* 日志记录业务
*
*/
public interface LogService {
/*
* 记录日志,异步执行,不影响正常业务流程
* 参数 joinPoint 切面方法的信息,当前切入点各种信息
* log 要记录的日志信息有那些
* */
@Async
void save(ProceedingJoinPoint joinPoint, Log log);
}
日志记录业务接口实现(LogServiceImpl.java)
package com.springboot.action.saas.common.logging.service.impl;
import com.springboot.action.saas.common.logging.domain.Log;
import com.springboot.action.saas.common.logging.service.LogService;
import com.springboot.action.saas.common.utils.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
@Service
public class LogServiceImpl implements LogService {
/*
* 记录日志接口实现
**/
@Override
public void save(ProceedingJoinPoint joinPoint, Log log) {
//获取request 请求对象
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes())
.getRequest();
//getSignature获取切面相关信息,比如方法名、目标方法参数等信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取抽象类(代理对象)方法
Method method = signature.getMethod();
//返回该元素的指定类型的注释,这里是Log注解
com.springboot.action.saas.common.logging.annotation.Log aopLog = method.getAnnotation(com.springboot.action.saas.common.logging.annotation.Log.class);
//获取注解传递的参数
if (log != null) {
log.setDescription(aopLog.value());
}
//通过最笨的反射方法,获取方法路径
String methodName = joinPoint.getTarget().getClass().getName()+"."+signature.getName()+"()";
log.setMethod(methodName);
//参数处理
//获取参数值
Object[] argValues = joinPoint.getArgs();
//获取参数名
String[] argNames = ((MethodSignature)joinPoint.getSignature()).getParameterNames();
//组织参数列表
String params = "{";
if(argValues != null){
for (int i = 0; i < argValues.length; i++) {
params += " " + argNames[i] + ": " + argValues[i];
}
}
log.setParams(params + " }");
//获取IP地址
log.setRequestIp(StringUtils.getIP(request));
//输出下日志到控制台
System.out.println(log.toString());
}
}
3 定义切入点以及切入点具体动作
切点的表达式以指示器指定的类型,配合类似正则表达式的用来告诉 Spring AOP 如何匹配连接点,Spring AOP 提供了以下几种指示器:
- execution
- within
- this和target
- args
- @within
- @target
- @args
- @annotation
下面依次说明指示器的作用
execution
该指示器用来匹配方法执行连接点,即匹配哪个方法执行,如
@Pointcut("execution(public String aaric.springaopdemo.UserDao.findById(Long))")
上面这个切点会匹配在 UserDao 类中 findById 方法的调用,并且需要该方法是 public 的,返回值类型为 String,只有一个 Long 的参数。
切点的表达式同时还支持宽字符匹配,如
@Pointcut("execution(* aaric.springaopdemo.UserDao.*(..))")
上面的表达式中,第一个宽字符 * 匹配 任何返回类型,第二个宽字符 * 匹配 任何方法名,最后的参数 (..) 表达式匹配 任意数量任意类型 的参数,也就是说该切点会匹配类中所有方法的调用。
within
如果要匹配一个类中所有方法的调用,便可以使用 within 指示器
@Pointcut("within(aaric.springaopdemo.UserDao)")
这样便可以匹配该类中所有方法的调用了。同时,我们还可以匹配某个包下面的所有类的所有方法调用,如下面的例子
@Pointcut("within(aaric.springaopdemo..*)")
this 和 target
如果目标对象实现了任何接口,Spring AOP 会创建基于CGLIB 的动态代理,这时候需要使用 target 指示器
如果目标对象没有实现任何接口,Spring AOP 会创建基于JDK的动态代理,这时候需要使用 this 指示器
@Pointcut("target(aaric.springaopdemo.A)") A 实现了某个接口
@Pointcut("this(aaric.springaopdemo.B)") B 没有实现任何一个接口
args
该指示器用来匹配具体的方法参数
@Pointcut("execution(* *..find*(Long))")
这个切点会匹配任何以 find 开头并且只有一个 Long 类型的参数的方法。
如果我们想匹配一个以 Long 类型开始的参数,后面的参数类型不做限制,我们可以使用如下的表达式
@Pointcut("execution(* *..find*(Long,..))")
@target
该指示器不要和 target 指示器混淆,该指示器用于匹配连接点所在的类是否拥有指定类型的注解,如
@Pointcut("@target(org.springframework.stereotype.Repository)")
@annotation
该指示器用于匹配连接点的方法是否有某个注解
@Pointcut("@annotation(org.springframework.scheduling.annotation.Async)")
配置切点和切点对应的动作(好多文档都说是通知)
LogAspect.java
package com.springboot.action.saas.common.logging.aspect;
import com.springboot.action.saas.common.logging.domain.Log;
import com.springboot.action.saas.common.logging.service.LogService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class LogAspect {
//日志业务
@Autowired
private LogService logService;
//当前时间
private long currentTime = 0L;
/**
* 配置切入点, 匹配连接点的方法是否有Log注解,定义切点
*/
@Pointcut("@annotation(com.springboot.action.saas.common.logging.annotation.Log)")
public void logPointcut() {
// 该方法无方法体,主要为了让同类中其他方法使用此切入点
}
/**
* 配置环绕通知,使用在方法logPointcut()上注册的切入点,具体要通知在什么条件下执行和执行什么动作
*
* @param joinPoint join point for advice
*/
@Around("logPointcut()")
public Object logAround(ProceedingJoinPoint joinPoint){
//返回值
Object result = null;
//获取当前时间
currentTime = System.currentTimeMillis();
try {
//执行目标方法
result = joinPoint.proceed();
} catch (Throwable e) {
//抛异常
throw new RuntimeException(e.getMessage());
}
//创建日志对象
Log log = new Log("INFO",System.currentTimeMillis() - currentTime);
//记录日志
logService.save(joinPoint, log);
//返回目标方法的返回值
return result;
}
/**
* 配置异常通知,异常的日志也要记录
*
* @param joinPoint join point for advice
* @param e exception
*/
@AfterThrowing(pointcut = "logPointcut()", throwing = "e")
public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
//创建日志对象
Log log = new Log("ERROR",System.currentTimeMillis() - currentTime);
//异常设置
log.setExceptionDetail(e.getMessage());
//记录异常
logService.save((ProceedingJoinPoint)joinPoint, log);
}
}