一、前言
AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态角度考虑程序运行过程。
AOP 底层,就是采用动态代理模式实现的。采用了两种代理:JDK 动态代理,与 CGLIB的动态代理。
二、两种动态代理的区别
主要来说是JDK动态代理需要借助接口才可实现,最终的代理类与原类实现同一个接口,属于兄弟关系。
CGLIB动态代理使用的是继承的方式,代理类继承于原类,属于父子关系。
在Spring实现AOP时会根据类有无接口自动选择代理方式,也可以手动指定使用CGLIB动态代理。实现方式是在Spring配置文件中加入如下。
下面展示一些 内联代码片
。
<!-- 强制使用cglib生动态代理 -->
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
在Spring Boot 2.0之后默认使用CGLIB动态代理。
三、相关注解介绍
在Spring Boot中通常使用AspectJ
实现AOP,AspectJ
中常用的通知有五种类型:
(1)前置通知
(2)后置通知
(3)环绕通知
(4)异常通知
(5)最终通知
下面介绍AspectJ
的常用注解:
- @Before:前置增强,目标方法执行之前执行。
- @AfterReturning:在目标方法执行之后执行可以获取到目标方法的返回值,该注解的
returning
属性就是用于指定接收方法返回值的变量名的。 - @Around:在目标方法执行之前之后执行。方法可以包含一个
ProceedingJoinPoint
类型的参数。接口ProceedingJoinPoint
其有一个proceed()
方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值。最后,环绕增强方法将其返回值返回。该增强方法实际是拦截了目标方法的执行。 - @AfterThrowing:异常通知,在目标方法抛出异常后执行。方法可以包含一个参数
Throwable
,参数名称为throwing
指定的名称,表示发生的异常对象。 - @After:无论目标方法是否抛出异常,该增强均会被执行。
- @Pointcut:定义切入点。
- @Aspect:把当前类标识为一个切面。
四、Spring Boot中实现 AOP主要步骤
在pom文件中加入如下依赖。
<!--aspect面向切面编程依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
下面为写项目时实现AOP的一个demo
@Aspect
@Component
public class LogAspect {
//定义日志对象
private final Logger logger = LoggerFactory.getLogger(this.getClass());
//设置切面,该方法没有实际意义
@Pointcut("execution(* com.yyy.blog.web.*.*(..))")
public void log(){}
//前置通知
@Before("log()")
public void doBefore(JoinPoint joinPoint){
//获取HttpServletRequest
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String url = request.getRequestURL().toString();
String ip = request.getRemoteAddr();
//获取请求方法名
String classMethod = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
//获取请求参数
Object[] args = joinPoint.getArgs();
RequestLog requestLog = new RequestLog(url,ip,classMethod,args);
logger.info("Request : {}",requestLog);
}
@After("log()")
public void doAfter(){
logger.info("doAfter");
}
@AfterReturning(returning = "result",pointcut = "log()")
public void doAfterReturn(Object result){
logger.info("Result : {}" + result);
}
private class RequestLog{
private String url;
private String ip;
private String classMethod;
private Object[] args;
public RequestLog(String url, String ip, String classMethod, Object[] args) {
this.url = url;
this.ip = ip;
this.classMethod = classMethod;
this.args = args;
}
@Override
public String toString() {
return "{" +
"url='" + url + '\'' +
", ip='" + ip + '\'' +
", classMethod='" + classMethod + '\'' +
", args=" + Arrays.toString(args) +
'}';
}
}
}
五、补充
在实际操作中,定义切点的方法有两种:
一是在某个通知注解上加入value属性来指定切点
@Before(value = "execution(* com.yyy.*.*(..))")
public void doBefore(JoinPoint joinPoint){
}
@AfterReturning(returning = "result",value = "execution(* com.yyy.*.*(..))")
public void doAfterReturn(Object result){
//result为切点方法的返回值
}
@Around(value = "execution(* com.yyy.*.*(..))")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable{
//用于接收切点方法返回值
Object obj;
//执行切点方法
obj = pjp.proceed();
}
第二种方法就是将@Pointcut
注解在一个方法之上,以后所有的 execution
的 value
属性值均可使用该方法名作为切入点。代表的就是@Pointcut
定义的入点。这个使用@Pointcut
注解的方法一般使用 private 的标识方法,即没有实际作用的方法。详情见“四、”。