一、概念
【关于面向切面】
Aspect-Oriented Programming,面向切面编程,是一种在编程过程中理解编程任务的思路(编程范式)
贴图如下:
拿建筑构图设计来说:
把整个设计按照功能模块拆分,一一完成后调用,就是面向过程;
接到设计之后,先抽象出一堆公用模块,再根据需求个性化调用,就是面向对象;
把设计过程中的事务类操作抽出来,形成一个横向模块,交由Spring控制织入,就是面向切面;
所以AOP被认为是OOP(Object-Oriented Programming)面向对象编程的补充和扩展。
更多关于编程范式的理解,详见 https://www.cnblogs.com/qfmy07/p/11023220.html
【个人理解】
把原本business类内日志事务等与具体业务无关的、重复性高的操作拿出来封装成一个Aspect类,按照business方法执行前后等维度分成before()、after()、around()、afterReturn()等方法,把Aspect类的作用域(如business类)标明后,交给Spring控制。Spring会在作用域内(如business类)的方法执行时按照Aspect中规定好的方法执行前后织入顺序织入business类。这样一来,与具体业务无关的操作(如方法执行前后打印日志等操作)就可以在Aspect类内单独维护,解耦。、
【笔记】
“Aop 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来”
“AOP将应用程序中的商业逻辑同对其提供支持的通用服务进行分离”
“这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程”
二、应用场景
如:在实际开发中,需要添加log/开始安全事务(以备作为原子统一回滚,保证一致性)
public String doPay(){
//记录方法开始时间
Long start=System.currentTimeMillis();
//记录方法开始日志
log.debug("dopay begin");
//安全事务开启
//具体业务开始
...
...
...
...
...
//具体业务结束
//安全事务关闭
//记录方法结束日志
log.debug("dopay begin");
//记录方法结束时间
Long end =System.currentTimeMillis();
log.info("程序运行花费时间:"+(end-start))
}
常用于:log日志、异常处理、事务、权限认证、性能监控,装饰器模式、代理模式、javaweb的拦截器中也运用了AOP的思想
三、主要术语
1、join point(连接点):通用叫法,所有可以看为切入点的点
2、point cut(切入点):什么时候做
3、advice(通知):做什么
4、aspect(方面):什么时候做什么事,即切点+通知=切面
四、代码
【pom.xml文件】
<!-- AOP配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
【Aspect类】
package com.example.common.util;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Maps;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.*;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.Map;
/**
* 切面类
*/
//把切面类加入到IOC容器中
@Component
//定义为一个切面类
@Aspect
public class logUtil {
private Logger log = LoggerFactory.getLogger(logUtil.class);
/**
* 定义切入点,切入点为 com.example.controller下的所有函数
*/
@Pointcut(value = "execution(* com.example.controller.*.*(..))")
public void pointOne(){}
/**
* 通知,(value=切点)
* 注意:任何通知方法都可以将第一个参数定义为org.aspectj.lang.JoinPoint类型
* 环绕通知需要定义第一个参数为ProceedingJoinPoint类型,
* 它是 JoinPoint 的一个子类)。JoinPoint接口提供了一系列有用的方法,比
* 如 getArgs()(返回方法参数)、getThis()(返回代理对象)、getTarget()
* (返回目标)、getSignature()(返回正在被通知的方法相关信息)和 toString()(打印出正在被通知的方法的有用信息)。
*/
//前置
@Before(value = "pointOne()")
public void doBefore(JoinPoint joinPoint){
log.info("@Before-前置通知");
//获取目标方法的参数信息
Object[] obj = joinPoint.getArgs();
//AOP代理类的信息
joinPoint.getThis();
//代理的目标对象
joinPoint.getTarget();
//用的最多 通知的签名
Signature signature = joinPoint.getSignature();
//代理的是哪一个方法
log.info("代理的方法是:"+signature.getName());
//AOP代理类的名字
log.info("AOP代理类的名字是:"+signature.getDeclaringTypeName());
//AOP代理类的类(class)信息
signature.getDeclaringType();
//获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
//如果要获取Session信息的话,可以这样写:
//HttpSession session = (HttpSession) requestAttributes.resolveReference(RequestAttributes.REFERENCE_SESSION);
//获取请求参数
Enumeration<String> enumeration = request.getParameterNames();
Map<String,String> parameterMap = Maps.newHashMap();
while (enumeration.hasMoreElements()){
String parameter = enumeration.nextElement();
parameterMap.put(parameter,request.getParameter(parameter));
}
String str = JSON.toJSONString(parameterMap);
}
//后置(不论是正常返回还是异常退出)
@After(value = "pointOne()")
public void doAfter(JoinPoint joinPoint){
log.info("@After-方法"+joinPoint.getSignature().getName()+"最终执行完了");
}
//异常
@AfterThrowing(value = "pointOne()",throwing = "exception")
public void doThrows(JoinPoint joinPoint, Throwable exception){
log.info("@AfterThrowing-异常通知");
log.info("目标方法名称:"+ joinPoint.getSignature().getName());
log.info("exception:"+exception);
}
//结束后通知
@AfterReturning(value = "pointOne()")
public void doAfterReturn(){
log.info("@AfterReturning-结束后通知");
}
/**
* 环绕
* 包围一个连接点的通知,如方法调用等。
* 环绕通知可以在方法调用前后完成自定义的行为,它也会选择是否继续执行连接点或者直接返回它自己的返回值或抛出异常来结束执行。
* 环绕通知最强大,也最麻烦,是一个对方法的环绕,具体方法会通过代理传递到切面中去,切面中可选择执行方法与否,执行几次方法等。
* 环绕通知使用一个代理ProceedingJoinPoint类型的对象来管理目标对象,所以此通知的第一个参数必须是ProceedingJoinPoint类型。
* 在通知体内调用ProceedingJoinPoint的proceed()方法会导致后台的连接点方法执行。
* proceed()方法也可能会被调用并且传入一个Object[]对象,该数组中的值将被作为方法执行时的入参。
* @param proceedingJoinPoint
*/
@Around(value = "pointOne()")
public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
Long start=System.currentTimeMillis();
try {
//方法运行
Object obj = proceedingJoinPoint.proceed();
Long end=System.currentTimeMillis();
log.info("方法"+proceedingJoinPoint.getSignature().getName()+"的具体运行时间是"+(end-start));
return obj;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return null;
}
// 6、有时候我们定义切面的时候,切面中需要使用到目标对象的某个参数,如何使切面能得到目标对象的参数呢?
// 可以使用args来绑定。如果在一个args表达式中应该使用类型名字的地方使用一个参数名字,
// 那么当通知执行的时候对象的参数值将会被传递进来。
@Before("execution(* findById*(..)) &&" + "args(id,..)")
public void twiceAsOld1(Long id){
System.err.println ("切面before执行了。。。。id==" + id);
}
【扩展】
切点表达式相关知识点较复杂,引用大神文章如下
切点表达式总结
五、内部实现机制
运行期,生成字节码,再加载到虚拟机中,JDK是利用反射原理,CGLIB使用了ASM原理。
初始化的时候,已经将目标对象进行代理,放入到spring 容器中
如果实现了接口的类,是使用jdk。如果没实现接口,就使用cglib。
从技术上来说,AOP基本上是通过代理机制实现的。
六、参考文档
https://www.cnblogs.com/zhugenqiang/archive/2008/07/27/1252761.html
https://www.cnblogs.com/zhangxufeng/p/9160869.html