spring-aop
术语说明
- 目标target,被增强的对象
- 连接点Joinpoint,指的是可以被拦截到的方法
- 切入点Pointcut,指的是真正被拦截到的方法
- 通知Advice,指的是拦截到切入点后要做的事情
- 织入Weaving,指的是将通知Advice应用到目标target的这个过程
- 代理Proxy,目标Target被增强后,产生了一个代理对象
- 切面Aspect,切入点Pointcut和通知Advice的组合
实现原理
- JDK动态代理(目标类实现了接口)
- Cglib动态代理(目标类无接口实现)
切入点表达式
参数包括:execution("修饰符 返回值类型 包.类.方法名(参数..) throws异常")
- 修饰符(举例):一般省略
* 任意
public 公共访问
- 返回值(举例):
void 无返回值
String 返回值是字符串类型
* 返回值任意
- 包(举例):
com.xx.user.dao 固定包
com.xx.*.dao com.xx下的任意包中的dao包
com.xx.user.dao.. 包括dao下所有子包中
- 类(举例):
UserDaoImpl 具体类
User* 以User开头类
*User 以User结尾类
* 任意类
- 方法(举例):
addUser 具体方法
* 任意方法
*User 以add结尾方法
add* 以add开头方法
- 参数(无参):
() 无参
(..) 任意参数
(String, int) 1个String和1个int类型的参数
(int) 1个int类型参数
- throws,可省略一般不写
通知类型
@Aspect
@Component
public class MyAspect {
/**
* 定义一个切入点表达式,表示需要应用的切入点
*/
@Pointcut("execution(* org.example.aop.AopTarget.*(..))")
public void firstJoinPoint() {
}
/**
* 前置通知
* @param joinPoint
*/
@Before("firstJoinPoint()")
public void before(JoinPoint joinPoint) {
System.out.println("@Before前置通知");
}
/**
* 后置通知
* @param joinPoint
* @param result
*/
@AfterReturning(value = "firstJoinPoint()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
System.out.println("@AfterReturning后置通知结果为:" + result);
}
/**
* 环绕通知 如果方法出异常,环绕后通知将不会执行
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("firstJoinPoint()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("@Around环绕通知=前");
Object obj = joinPoint.proceed();
System.out.println("@Around环绕通知=后");
return obj;
}
/**
* 异常抛出通知
* @param e
*/
@AfterThrowing(value = "firstJoinPoint()", throwing = "e")
public void afterThrowing(Throwable e) {
System.out.println("@AfterThrowing异常抛出通知" + e.getMessage());
}
/**
* 最终通知
*/
@After("firstJoinPoint()")
public void after() {
System.out.println("@After最终通知");
}
/**
* 打印切点属性
* @param joinPoint
*/
private void printJoinPoint(JoinPoint joinPoint) {
System.out.println("目标方法名为:" + joinPoint.getSignature().getName());
System.out.println("目标方法所属类的简单类名:" + joinPoint.getSignature().getDeclaringType().getSimpleName());
System.out.println("目标方法所属类的类名:" + joinPoint.getSignature().getDeclaringTypeName());
System.out.println("目标方法声明类型:" + Modifier.toString(joinPoint.getSignature().getModifiers()));
// 获取传入目标方法的参数
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
System.out.println("第" + (i + 1) + "个参数为:" + args[i]);
}
System.out.println("被代理的对象:" + joinPoint.getTarget());
System.out.println("代理对象自己:" + joinPoint.getThis());
}
}
通知执行顺序
多个切面执行顺序
- A order 小于 B
- 使用
@Order()
定义顺序,越小方法前通知先执行;方法后通知后执行
其他切入点
- within表达式的粒度为类,其参数为全路径的类名(可使用通配符),表示匹配当前表达式的所有类
within(declaring-type-pattern)
@Pointcut("within(com.example.demo.Target)")
public void joinPoint() {}
- args表达式的作用是匹配指定参数类型和指定参数数量的方法,无论其类路径或者是方法名是什么。这里需要注意的是,args指定的参数必须是全路径的。可以使用通配符,但这里通配符只能使用 ..
@Pointcut("args(java.lang.Integer,java.lang.Integer)")
- this和target
this将匹配代理对象为指定类型的类
@Pointcut("this(com.example.demo.Target)")
target匹配业务对象为指定类型的类
@Pointcut("target(com.example.demo.Target)")
- @within匹配带有指定注解的类
@Pointcut("@within(annotation-type)")
- @annotation匹配指定注解标注的方法
@Pointcut("@annotation(annotation-type)")
- @args则表示使用指定注解标注的类作为某个方法的参数时该方法将会被匹配。注意可能导致spring 启动报错使用
@args(*.*.*.*)
的时候,会匹配到spring中某些被final修饰过的类(具体为什么会匹配上,不详)。因此需要在后面加个限定条件:&&within(com.example.demo..*)
@Pointcut("@args(annotation-type)")
- @DeclareParents也称为Introduction(引入),表示为指定的目标类引入新的属性和方法。perthis和pertarget 多例环境下切面
perthis(pointcut-expression)
pertarget(pointcut-expression)
@Aspect("perthis(this(com.spring.service.Apple))")
@Aspect("pertarget(target(com.spring.service.Apple))")
参考: https://www.cnblogs.com/zhangxufeng/p/9160869.html
AOP不生效几种情况
- private方法
- this调用
可以用SpringUtil.getBean方法调用
AOP 无法拦截接口上注解场景兼容
原因:实现类没有继承接口的注解,所以在进行切点匹配时,匹配不到
解决方案:
- 自定义 Pointcut虽说是自定义,但也没有要求我们直接实现这个接口,我们选择StaticMethodMatcherPointcut来补全逻辑
import org.springframework.core.annotation.AnnotatedElementUtils;
public static class LogPointCut extends StaticMethodMatcherPointcut {
@SneakyThrows
@Override
public boolean matches(Method method, Class<?> aClass) {
// 直接使用spring工具包,来获取method上的注解(会找父类上的注解)
return AnnotatedElementUtils.hasAnnotation(method, AnoDot.class);
}
}
- 接下来我们采用声明式来实现切面逻辑,自定义 Advice,这个 advice 就是我们需要执行的切面逻辑,和上面的日志输出差不多,区别在于参数不同,自定义 advice 实现自接口MethodInterceptor,顶层接口是Advice
public static class LogAdvice implements MethodInterceptor {
private static final String SPLIT_SYMBOL = "|";
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
Object res = null;
String req = null;
long start = System.currentTimeMillis();
try {
req = buildReqLog(methodInvocation);
res = methodInvocation.proceed();
return res;
} catch (Throwable e) {
res = "Un-Expect-Error";
throw e;
} finally {
long end = System.currentTimeMillis();
System.out.println("ExtendLogAspect:" + req + "" + JSON.toJSONString(res) + SPLIT_SYMBOL + (end - start));
}
}
private String buildReqLog(MethodInvocation joinPoint) {
// 目标对象
Object target = joinPoint.getThis();
// 执行的方法
Method method = joinPoint.getMethod();
// 请求参数
Object[] args = joinPoint.getArguments();
StringBuilder builder = new StringBuilder(target.getClass().getName());
builder.append(SPLIT_SYMBOL).append(method.getName()).append(SPLIT_SYMBOL);
for (Object arg : args) {
builder.append(JSON.toJSONString(arg)).append(",");
}
return builder.substring(0, builder.length() - 1) + SPLIT_SYMBOL;
}
}
- 自定义 Advisor,将上面自定义的切点 pointcut 与通知 advice 整合,实现我们的切面
public static class LogAdvisor extends AbstractBeanFactoryPointcutAdvisor {
@Setter
private Pointcut logPointCut;
@Override
public Pointcut getPointcut() {
return logPointCut;
}
}
- 最后注册切面,实际上就是声明为 bean,丢到 spring 容器中而已
@Bean
public LogAdvisor init() {
LogAdvisor logAdvisor = new LogAdvisor();
// 自定义实现姿势
logAdvisor.setLogPointCut(new LogPointCut());
logAdvisor.setAdvice(new LogAdvice());
return logAdvisor;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用