什么时候要用到面向切面AOP呢?
举个例子,你想给你的网站加上鉴权,
对某些url,你认为不需要鉴权就可以访问,
对于某些url,你认为需要有特定权限的用户才能访问
如果你依然使用OOP,面向对象,
那你只能在那些url对应的Controller代码里面,一个一个写上鉴权的代码
一、AOP即面向切向编程
AOP 是 Aspect Oriented Programming 的缩写,译为面向切向编程。用我们最常用的 OOP 来对比理解
纵向关系 OOP,横向角度 AOP
- 对象设计的时候一般都是纵向思维,如果这个时候考虑这些不同类对象的共性,不仅会增加设计的难度和复杂性,还会造成类的接口过多而难以维护
- 需要对现有的对象 动态增加 某种行为或责任时非常困难
二、应用场景
只要系统的业务模块都需要引用通用模块,就可以使用AOP。
- 日志记录
- 事物处理
- 性能统计
- 安全控制
以日志记录为例,举个小小的例子(期间用到的注解下文会讲)
/**
* 日志切面类
*/
@Aspect
@Component
@Slf4j
public class LogAspect {
/**
* ..表示包及子包 该方法代表controller层的所有方法 TODO 路径需要根据自己项目定义
*/
@Pointcut("execution(public * com.demo.controller..*.*(..))")
public void controllerMethod() {
}
/**
* 方法执行前
*
* @param joinPoint
* @throws Exception
*/
@Before("controllerMethod()")
public void LogRequestInfo(JoinPoint joinPoint) throws Exception {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
StringBuilder requestLog = new StringBuilder();
Signature signature = joinPoint.getSignature();
requestLog.append(((MethodSignature) signature).getMethod().getAnnotation(ApiOperation.class).value()).append("\t")
.append("请求信息:").append("URL = {").append(request.getRequestURI()).append("},\t")
.append("请求方式 = {").append(request.getMethod()).append("},\t")
.append("请求IP = {").append(request.getRemoteAddr()).append("},\t")
.append("类方法 = {").append(signature.getDeclaringTypeName()).append(".")
.append(signature.getName()).append("},\t");
// 处理请求参数
String[] paramNames = ((MethodSignature) signature).getParameterNames();
Object[] paramValues = joinPoint.getArgs();
int paramLength = null == paramNames ? 0 : paramNames.length;
if (paramLength == 0) {
requestLog.append("请求参数 = {} ");
} else {
requestLog.append("请求参数 = [");
for (int i = 0; i < paramLength - 1; i++) {
requestLog.append(paramNames[i]).append("=").append(JSONObject.toJSONString(paramValues[i])).append(",");
}
requestLog.append(paramNames[paramLength - 1]).append("=").append(JSONObject.toJSONString(paramValues[paramLength - 1])).append("]");
}
log.info(requestLog.toString());
}
/**
* 方法执行后
*
* @param resultVO
* @throws Exception
*/
@AfterReturning(returning = "resultVO", pointcut = "controllerMethod()")
public void logResultVOInfo(ResultVO resultVO) throws Exception {
log.info("请求结果:" + resultVO.getCode() + "\t" + resultVO.getMsg());
}
}
三、Spring中的AOP
AOP 领域中的特性术语:
- 通知(Advice): AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理。
- 连接点(join point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用。
- 切点(PointCut): 可以插入增强处理的连接点。
- 切面(Aspect): 切面是通知和切点的结合。
- 引入(Introduction):引入允许我们向现有的类添加新的方法或者属性。
- 织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象,这个过程就是织入。
首先,在pom中加入SpringAOP的相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
用到的注解:
@Aspect和@Component
首先,这个@Aspect注释告诉Spring这是个切面类,然后@Compoment将转换成Spring容器中的bean或者是代理bean。 总之要写切面这两个注解一起用就是了。
@PointCut
这个注解包含两部分,PointCut表达式和PointCut签名。表达式是拿来确定切入点的位置的,说白了就是通过一些规则来确定,哪些方法是要增强的,也就是要拦截哪些方法。
@PointCut(...........)括号里面那些就是表达式。这里的execution是其中的一种匹配方式
execution: 匹配连接点
within: 某个类里面
this: 指定AOP代理类的类型
target:指定目标对象的类型
args: 指定参数的类型
bean:指定特定的bean名称,可以使用通配符(Spring自带的)
@target: 带有指定注解的类型
@args: 指定运行时传的参数带有指定的注解
@within: 匹配使用指定注解的类
@annotation:指定方法所应用的注解
@Before
这个是决定advice在切入点方法的什么地方执行的标签,这个注解的意思是在切入点方法执行之前执行我们定义的advice。
/**
* 方法执行前
*
* @param joinPoint
* @throws Exception
*/
@Before("controllerMethod()")
public void LogRequestInfo(JoinPoint joinPoint) throws Exception {
@After
这个注解就是在切入的方法运行完之后把我们的advice增强加进去。一样方法中可以添加JoinPoint。
@Around
这个注解可以简单地看作@Before和@After的结合。这个注解和其他的比比较特别,它的方法的参数一定要是ProceedingJoinPoint,这个对象是JoinPoint的子类。我们可以把这个看作是切入点的那个方法的替身,这个proceedingJoinPoint有个proceed()方法,相当于就是那切入点的那个方法执行,简单地说就是让目标方法执行,然后这个方法会返回一个对象,这个对象就是那个切入点所在位置的方法所返回的对象。
@AfterReturning
顾名思义,这个注解是在目标方法正常完成后把增强处理织入。这个注解可以指定两个属性(之前的三个注解后面的括号只写一个@PointCut表达式,也就是只有一个属性),一个是和其他注解一样的PointCut表达式,也就是描述该advice在哪个接入点被织入;然后还可以有个returning属性,表明可以在Advice的方法中有目标方法返回值的形参。
@AfterThrowing
异常抛出增强,在异常抛出后织入的增强。有点像上面的@AfterReturning,这个注解也是有两个属性,pointcut和throwing。
注解也就说完了,具体代码实现呢,可以参考上面的小例子