Why AOP?
Aspect Oriented Programming(AOP),面向切面编程,是一个比较热门的话题。AOP主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。比如我们最常见的就是日志记录了,举个例子,我们现在提供一个查询学生信息的服务,但是我们希望记录有谁进行了这个查询。如果按照传统的OOP的实现的话,那我们实现了一个查询学生信息的服务接口(StudentInfoService)和其实现 类 (StudentInfoServiceImpl.java),同时为了要进行记录的话,那我们在实现类(StudentInfoServiceImpl.java)中要添加其实现记录的过程。这样的话,假如我们要实现的服务有多个呢?那就要在每个实现的类都添加这些记录过程。这样做的话就会有点繁琐,而且每个实现类都与记录服务日志的行为紧耦合,违反了面向对象的规则。那么怎样才能把记录服务的行为与业务处理过程中分离出来呢?看起来好像就是查询学生的服务自己在进行,但却是背后日志记录对这些行为进行记录,并且查询学生的服务不知道存在这些记录过程,这就是我们要讨论AOP的目的所在。AOP的编程,好像就是把我们在某个方面的功能提出来与一批对象进行隔离,这样与一批对象之间降低了耦合性,可以就某个功能进行编程。
AOP的几个概念
-
切面(Aspect):一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是J2EE应用中一个关于横切关注点的很好的例子。在Spring AOP中,切面可以使用基于模式或者基于@Aspect注解的方式来实现。
-
连接点(Joinpoint):在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中,一个连接点总是表示一个方法的执行。
-
通知(Advice):在切面的某个特定的连接点上执行的动作。其中包括了“around”、“before”和“after”等不同类型的通知(通知的类型将在后面部分进行讨论)。许多AOP框架(包括Spring)都是以拦截器做通知模型,并维护一个以连接点为中心的拦截器链。
-
切入点(Pointcut):匹配连接点的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如,当执行某个特定名称的方法时)。切入点表达式如何和连接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法。
-
引入(Introduction):用来给一个类型声明额外的方法或属性(也被称为连接类型声明(inter-type declaration))。Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用引入来使一个bean实现IsModified接口,以便简化缓存机制。
-
目标对象(Target Object):被一个或者多个切面所通知的对象。也被称做被通知(advised)对象。既然Spring AOP是通过运行时代理实现的,这个对象永远是一个被代理(proxied)对象。
-
AOP代理(AOP Proxy):AOP框架创建的对象,用来实现切面契约(例如通知方法执行等等)。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
-
织入(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。
Maven引入AOP
再SpringBoot2中只需要引入aop-starter即可使用AOP切面编程
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
效果展示
开源项目
以下项目和代码可以在我的开源项目spring-cloud-study中的spring-boot-study-aop
中GET到。
https://github.com/moshowgame/spring-cloud-study
DEMO代码
AOP切面主方法切面如下:
package com.softdev.system.demo.config;
import com.alibaba.fastjson.JSON;
import lombok.extern.java.Log;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
@Aspect
@Component
@Log
public class AspectConfig {
@Pointcut("execution(public * com.softdev.system.demo.controller.DemoController.index(..))")
public void index_log(){}
/**
* 记录HTTP请求结束时的日志
*/
@Before("index_log()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 记录下请求内容
log.info(">>>>>>>>>>Before");
log.info("URL : " + request.getRequestURL().toString());
log.info("HTTP_METHOD : " + request.getMethod());
log.info("IP : " + request.getRemoteAddr());
log.info("PATH : " + request.getServletPath());
log.info("METHOD : " + request.getMethod());
log.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
log.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(returning = "obj",pointcut = "index_log()")
public void doAfterReturning(Object obj) throws Throwable {
//处理完请求,返回内容
log.info(">>>>>>>>>>AfterReturning");
log.info("RESPONSE : " + JSON.toJSONString(obj));
}
@AfterThrowing(value = "index_log()",throwing = "exception")
public void doAfterThrowing(JoinPoint joinPoint,Throwable exception){
//目标方法名:
log.info(">>>>>>>>>>AfterThrowing");
log.info(joinPoint.getSignature().getName());
if(exception instanceof NullPointerException){
log.info("发生了空指针异常!!!!!");
}else{
log.info("发生了未知异常!!!!!");
}
}
@Around(value = "index_log()")
public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
log.info(">>>>>>>>>>Around");
log.info("环绕通知的目标方法名:"+proceedingJoinPoint.getSignature().getName());
try {
Object obj = proceedingJoinPoint.proceed();
return obj;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return null;
}
}
Controller方法如下
@RestController
public class DemoController {
@GetMapping("/index")
public ApiReturnObject index(String data){
if(StringUtils.isEmpty(data)) {
data="hello spring-cloud-study";
}
return ApiReturnUtil.success(data);
}
}
@AfterReturning后置返回通知
在某连接点之后执行的通知,通常在一个匹配的方法返回的时候执行(可以在后置通知中绑定返回值)。
- 如果参数中的第一个参数为
JoinPoint
,则第二个参数为返回值
的信息。 - AfterReturning限定了只有目标方法返回值与通知方法
相同类型
的参数时才能执行后置返回通知,否则不执行。 - 对于returning对应的通知方法参数为
Object
类型将匹配任何目标返回值。
@AfterThrowing后置异常通知
在方法抛出异常退出时执行的通知
@After后置最终通知
当某连接点退出时执行的通知(不论是正常返回还是异常退出)。
@Around环绕通知
包围一个连接点的通知,如方法调用等。这是最强大
也最麻烦
的一种通知类型,可以在方法调用前后完成自定义
的行为,它也会选择是否继续执行连接点或者直接返回它自己的返回值或抛出异常来结束执行。
对方法的环绕,具体方法会通过代理传递到切面中去,切面中可选择执行方法与否,执行几次方法等。环绕通知使用一个代理ProceedingJoinPoint
类型的对象来管理目标对象,所以此通知的第一个参数必须是ProceedingJoinPoint类型
。在通知体内调用ProceedingJoinPoint的proceed()方法
会导致后台的连接点方法执行。proceed()
方法也可能会被调用并且传入一个Object[]
对象,该数组中的值将被作为方法执行时的入参。
切入点表达式
定义切入点的时候需要一个包含名字和任意参数的签名,还有一个切入点表达式,如@Pointcut("execution(public * com.softdev.system.demo.controller.DemoController.index(..))")
意思是,指定了DemoController的index方法。
切入点表达式的格式为
execution([可见性]返回类型[声明类型].方法名(参数)[异常])
其中[]内的是可选的,其它的还支持通配符的使用:
*
:匹配所有字符..
:一般用于匹配多个包,多个参数+
:表示类及其子类- 运算符有:
&&
,||
,!
切入点指示符
-
execution - 匹配方法执行的连接点,这是你将会用到的Spring的最主要的切入点指示符。
-
within - 限定匹配特定类型的连接点(在使用Spring AOP的时候,在匹配的类型中定义的方法的执行)。
-
this - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中bean reference(Spring AOP 代理)是指定类型的实例。
-
target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中目标对象(被代理的应用对象)是指定类型的实例。
-
args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中参数是指定类型的实例。
-
@target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中正执行对象的类持有指定类型的注解。
-
@args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中实际传入参数的运行时类型持有指定类型的注解。
-
@within - 限定匹配特定的连接点,其中连接点所在类型已指定注解(在使用Spring AOP的时候,所执行的方法所在类型已指定注解)。
-
@annotation - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中连接点的主题持有指定的注解。
当然,其中execution使用最频繁也是最方便,即某方法执行时进行切入。