本文最后更新于 947 天前,其中的信息可能已经有所发展或是发生改变。
AOP的概念
AOP(Aspect Oriented Programming)面向切面编程,一种使用静态或动态代理的方式在不修改源代码的情况下插入某种通用功能的思想。AOP是OOP(面向对象编程)的补充,利用AOP可以对业务逻辑的各个部分进行隔离,从而减少重复代码,降低模块耦合。
- 静态代理,AOP框架在编译阶段对源代码进行修改,生成的class文件已经被修改,需要使用特定的编译器如AspectJ。
- 动态代理,AOP框架在运行阶段动态生成字节码文件,需要使用特定工具如JDK动态代理、CGLib。
Spring AOP
AOP框架有很多种,每种框架的实现方式也不尽相同,例如AspectJ和JBoss除了支持方法连接点,还支持字段和构造器的连接点,而Spring AOP只支持方法连接点。虽然Spring AOP提供了@AspectJ注解,可以声明一个普通类为切面,但Spring AOP的实现并不依赖AspectJ编译器,而是使用JDK动态代理和CGLib。
切面 (AspectJ)
- 定义切点,此处使用注解的方式
import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Cache { String key(); } - 定义切面,注意要将切面Bean交由Spring容器管理
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Aspect @Component public class CacheAspectJ { private static final Mapmap = new ConcurrentHashMap<>(); @AfterReturning(value = "@annotation(cache)", returning = "returnValue") public void tag(JoinPoint joinPoint, Object returnValue, Cache cache) { map.put(cache.key(), returnValue); } public static Object getValue(String key) { return map.get(key); } } - 定义连接点
package com.tenbeggar.service; import org.springframework.stereotype.Service; import java.util.UUID; @Service public class TokenService { @Cache(key = "token") public String getToken(String salt) { System.out.println("getToken run..."); return UUID.randomUUID().toString(); } }
通知 (Advice)
| 注解 | 说明 |
|---|---|
| @Before | 在目标方法前执行 |
| @After | 在目标方法返回或抛出异常后执行 |
| @AfterReturning | 在目标方法返回后执行 |
| @AfterThrowing | 在目标方法抛出异常后执行 |
| @Around | 环绕通知,自定义执行顺序 |
切点 (PointCut)
| AspectJ 表达式 | 说明 |
|---|---|
| execution | 匹配指定方法 |
| @annotation | 匹配被指定注解修饰的方法 |
| within | 匹配指定类的所有方法,不包含继承关系,示例:within(com.tenbeggar.service.TokenService) |
| @within | 匹配被指定注解修饰的类的所有方法,不包含继承关系 |
| target | 匹配指定类或接口的所有方法,包含继承关系,示例:target(com.tenbeggar.service.Token) |
| @target | 匹配被指定注解修饰的类或接口的所有方法,包含继承关系 |
| args | 匹配有指定入参的方法,包含继承关系,需配合其他表达式使用:args(java.lang.String) && execution(* com.tenbeggar.service.Token.*(..)) |
| @args | 匹配被指定注解修饰的入参的方法,包含继承关系 |
| bean | 匹配Spring IOC容器管理的Bean的所有方法 |
| this | 匹配被代理后的对象,配合@DeclareParents使用更佳 |
execution 使用样例
execution(public * *(..)),所有public方法,第一个*代表返回值,第二个*代表方法名execution(* set*(..)),set开头的方法,第一个*代表返回值execution(* com.tenbeggar.service.Token.*(..)),Token类或接口及其子类的所有方法,一个*代表返回值,第二个*代表方法名execution(* com.tenbeggar.*.*(..)),com.tenbeggar包下的所有方法execution(* com.tenbeggar..*.*(..)),com.tenbeggar包及其子包下的所有方法execution(* *()),所有无参方法,第一个*代表返回值,第二个*代表方法名execution(* *(*, String)),所有第一个是任何类型第二个是String类型的方法execution(public int com.tenbeggar.*.*(String, ..))
支持的通配符:
*匹配所有字符..匹配多个包或者多个参数+表示类及其子类&&||!逻辑运算符
详情参见AspectJ编程指南Language Semantics。
连接点 (Joinpoint)
任何切面都可将org.aspectj.lang.JoinPoint作为第一个参数引入方法,JoinPoint接口提供了一系列用于获取连接点信息的方法。
@Around环绕通知则可将org.aspectj.lang.ProceedingJoinPoint作为第一个参数引入进来,它继承自JoinPoint接口。
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class LogAspectJ {
//@Pointcut声明通用切点,方便各个切面统一使用
@Pointcut("execution(* com.tenbeggar.service.*.*(..))")
public void defaultPoint() {
}
@Around("defaultPoint()")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("prefix");
Object proceed = joinPoint.proceed();//执行目标方法,类似于tokenService.orderToken("admin")
log.info("suffix");
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
System.out.println(arg);//admin
}
Object target = joinPoint.getTarget();//com.tenbeggar.service.TokenService@53ba7997
Signature signature = joinPoint.getSignature();//String com.tenbeggar.service.TokenService.orderToken(String)
String name = signature.getName();//orderToken
String simpleName = signature.getDeclaringType().getSimpleName();//TokenService
String declaringTypeName = signature.getDeclaringTypeName();//com.tenbeggar.service.TokenService
int modifiers = signature.getModifiers();//1
String s = Modifier.toString(modifiers);//public
return proceed;
}
}
引入 (Introduction)
引入可以代替目标对象实现指定接口,使用@DeclareParents注解来定义引入。
- 定义目标接口
public interface Console { void error(Throwable ex); } - 默认接口实现
import lombok.extern.slf4j.Slf4j; @Slf4j public class DefaultConsole implements Console { @Override public void error(Throwable ex) { log.error(ex.getMessage()); } } - 定义切面
import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Aspect @Component public class LogAspectJ { //@DeclareParents注解的value属性支持AspectJ表达式,任何符合匹配的Bean都会实现Console接口 @DeclareParents(value = "com.tenbeggar.service.TokenService", defaultImpl = DefaultConsole.class) public static Console console; @AfterThrowing(value = "this(console)", throwing = "ex", argNames = "ex,console") public void recordUsage(Throwable ex, Console console) { console.error(ex); } }
织入 (Weaving)
把切面连接到目标对象中,并创建一个被通知的对象。 这些可以在编译时、类加载时和运行时完成。Spring就是在运行时完成织入的。
