Aop面向切面编程
文章目录
- Aop面向切面编程
- 什么是AOP
- AOP术语
- Spring AOP 的使用
- 导入依赖
- 编写切面类
- 切面定义语法
- 小细节
- 输出日志成功
什么是AOP
AOP
:(Aspect Oriented Programming
)面向切面编程,和OOP(Object Oriented Programming
)面向对象编程一样,也是计算机开发的一种程序设计思想,与OOP将程序中的每个环节对象化,如实体类等相比,面向切面就是在不改变程序现有代码的前提下,可以设置某方法运行前或运行后新增额外代码的操作,减少对代码的入侵。包括过滤器、拦截器都是一种AOP的思想,只不过Spring AOP是Spring给的,(注:AOP并不是Spring框架独有的,而是从AspectJ框架中借鉴而来)过滤器是Java给的,拦截器是SpringMVC给的。
对AOP官方文档翻译:
AOP
目标是将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。通过在现有代码基础上增加额外的通知(Advice
)机制,能够对被声明为“切点“(Pointcut
)的代码块进行统一管理与扩展
AOP术语
-
切面(
Aspect
):是一个可以加入额外代码运行的特定位置,一般指方法之间的调用,可以在不修改原代码的情况下,添加新的代码,对现有代码进行升级维护和管理 -
织入(
Weaving
):选定一个切面,利用动态代理技术,为原有的方法的持有者生成动态对象,然后将它和切面关联,在运行原有方法时,就会按织入之后的流程运行了-
动态代理:
-
-
目标对象(
Target
):需要被加强的业务对象 -
代理类(
Proxy
) 一个类被AOP织入通知后,就产生了一个代理类。 -
切点(
PointCut
):每个程序的连接点有多个,如何定位到某个感兴趣的连接点,就需要通过切点来定位。 -
连接点(
Joinpoint
):程序执行的某个特定位置,如某个方法调用前,调用后,方法抛出异常后,这些代码中的特定点称为连接点。简单来说,就是在哪加入你的通知- 连接点表示具体要拦截的方法,切点是定义一个范围,而连接点是具体到某个方法
-
通知(
Advice
):原意为增强,是织入到目标类连接点上的一段程序代码。分以下几种:
- 前置通知(
before
):执行业务代码前做些操作,比如获取连接对象 - 后置通知(
after
):在执行业务代码后做些操作,无论是否发生异常,它都会执行,比如关闭连接对象 - 异常通知(
afterThrowing
):在执行业务代码后出现异常,需要做的操作,比如回滚事务 - 返回通知(
afterReturning
):在执行业务代码后无异常,会执行的操作 - 环绕通知(
around
):
- 前置通知(
Spring AOP 的使用
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
编写切面类
@Slf4j
@Aspect //切面
@Component
public class LogAspect {
private HashMap<String, Object> map = new HashMap<>();
private final String filePath = "logFile.txt";
//1.定义切面
/**
* 此步骤单纯定义切面,就是指定一个方法
* 可以在这个方法运行前或运行后等位置织入额外代码
*
* 匹配com.liner.controller包及其子包下的所有类的所有方法
*/
@Pointcut("execution(public * com.liner.controller.*.*(..))")
// 切入点 执行 公有方法 任意返回值 包路径 任何类 任何方法 任何参数
public void pointCut() {}
//2.织入内容
/**
* 前置通知,目标方法调用前被调用
* 向切面方法前添加代码,在方法运行前输出指定内容
*/
@Before("pointCut()")
public void beforeAdvice(JoinPoint joinPoint) { //JoinPoint 会包含当前切面的各种信息,连接点
log.info("----------- 前置通知 -----------");
Signature signature = joinPoint.getSignature(); //获得当前切面对应方法的信息
log.info("返回目标方法的签名:{}", signature);
log.info("代理的是哪一个方法:{}", signature.getName());
Object[] args = joinPoint.getArgs();
log.info("获取目标方法的参数信息:{}", Arrays.asList(args));
map.put("方法签名", signature);
map.put("代理方法", signature.getName());
map.put("参数信息", Arrays.asList(args));
}
/**
* 后置通知,也叫最终通知,目标方法执行完之后执行
*/
@After("pointCut()")
public void afterAdvice() {
log.info("----------- 后置通知 -----------");
}
/**
* 后置返回通知
* 如果参数中的第一个参数为JoinPoint,则第二个参数为返回值的信息
* 如果参数中的第一个参数不为JoinPoint,则第一个参数为returning中对应的参数
* returning 只有目标方法返回值与通知方法相应参数类型时才能执行后置返回通知,否则不执行
*
* @param joinPoint
* @param keys
*/
@AfterReturning(pointcut = "pointCut()", returning = "keys")
public void afterReturningAdvice(JoinPoint joinPoint, String keys) {
log.info("~~~~~~~~~~ 后置返回通知 ~~~~~~~~~~");
log.info("后置返回通知的返回值:{}", keys);
}
/**
* 后置异常通知
* 定义一个名字,该名字用于匹配通知实现方法的一个参数名,当目标方法抛出异常返回后,将把目标方法抛出的异常传给通知方法;
* throwing 只有目标方法抛出的异常与通知方法相应参数异常类型时才能执行后置异常通知,否则不执行,
*
* @param e
*/
@AfterThrowing(value = "pointCut()", throwing = "e")
public void afterThrowingAdvice(Exception e) {
log.info("~~~~~~~~~~ 后置异常通知 ~~~~~~~~~~");
log.info(e.toString());
}
/**
* 环绕通知
* 环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。
* 环绕通知第一个参数必须写,且是org.aspectj.lang.ProceedingJoinPoint类型 JoinPoint的子接口,拥有更多方法
* 这个方法还要有返回值,因为调用的切面的方法可能有返回值,环绕advice如果不返回这个值,调用者就接收不到这个值了
* @param proceedingJoinPoint
*/
@Around(value = "pointCut()")
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
log.info("----------- 环绕通知 -----------");
log.info("环绕通知的目标方法名:{}", proceedingJoinPoint.getSignature().getName());
map.put("目标方法", proceedingJoinPoint.getSignature().getName());
try {
return proceedingJoinPoint.proceed(); //ProceedingJoinPoint类型的参数具有调用切面方法的功能
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
log.info("---------- 环绕通知结束 -------------");
writeLog(map);
}
return null;
}
/**
* 打印日志
* @param map
*/
void writeLog(HashMap<String, Object> map) {
try {
long currentTimeMillis = System.currentTimeMillis();
SimpleDateFormat format = new SimpleDateFormat("yyyy年MM月dd日-hh时mm分ss秒");
Date date = new Date(currentTimeMillis);
FileWriter fw = new FileWriter(filePath,true);
fw.write("----------- 环绕通知 -----------"+ format.format(date)+ "\r\n");
fw.write("环绕通知的目标方法名:" + (String) map.get("目标方法") + "\r\n");
fw.write("----------- 前置通知 -----------"+ "\r\n");
fw.write("返回目标方法的签名:" + (String) map.get("方法签名").toString() + "\r\n");
fw.write("代理的是哪一个方法:" + (String) map.get("代理方法") + "\r\n");
fw.write("获取目标方法的参数信息:" + (String) map.get("参数信息").toString() + "\r\n");
fw.write("----------- 最终通知 -----------"+ "\r\n");
fw.write("---------- 环绕通知结束 -------------"+ "\r\n");
fw.flush();
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
切面定义语法
@Pointcut("execution(public * com.liner.controller.*.*(..))")
下面介绍切面定义表达式的详细语法规则
语法模板:
execution(
modifier-pattern?
ret-type-pattern
declaring-type-pattern?
name-pattern(param-pattern)
throws-pattern?
)
带?
的是可选属性,不带?
是必须写的
modifier-pattern
:访问修饰符(可选)ret-type-pattern
:返回值类型(必写),一般为*
declaring-type-pattern
:全路径类名(可选)name-pattern(param-pattern)
:方法名(必写)param-pattern
:参数列表(必写),一般为..
throws-pattern
:抛出的异常类型(可选)
小细节
几种通知图标不同:
-
前置通知(
before
): -
后置通知(
after
): -
异常通知(
afterThrowing
): -
返回通知(
afterReturning
): -
环绕通知(
around
):