【SpringBoot 基础系列】接口上注解 AOP 拦截不到场景兼容实例演示 您所在的位置:网站首页 springboot切面不生效 【SpringBoot 基础系列】接口上注解 AOP 拦截不到场景兼容实例演示

【SpringBoot 基础系列】接口上注解 AOP 拦截不到场景兼容实例演示

2024-05-16 18:11| 来源: 网络整理| 查看: 265

开源软件供应链点亮计划,等你来!>>>

【SpringBoot 基础系列】接口上注解 AOP 拦截不到场景兼容

在 Java 的开发过程中,面向接口的编程可能是大家的常态,切面也是各位大佬使用 Spring 时,或多或少会使用的一项基本技能;结果这两个碰到一起,有意思的事情就发生了,接口方法上添加注解,面向注解的切面拦截,居然不生效

这就有点奇怪了啊,最开始遇到这个问题时,表示难以相信;事务注解也挺多是写在接口上的,好像也没有遇到这个问题(难道是也不生效,只是自己没有关注到?)

接下来我们好好瞅瞅,这到底是怎么个情况

I. 场景复现

这个场景复现相对而言比较简单了,一个接口,一个实现类;一个注解,一个切面完事

1. 项目环境

采用SpringBoot 2.2.1.RELEASE + IDEA + maven 进行开发

添加 aop 依赖

代码语言:javascript复制 org.springframework.boot spring-boot-starter-aop 2. 复现 case

声明一个注解

代码语言:javascript复制@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AnoDot { }

拦截切面,下面这段代码来自之前分享的博文 【基础系列】AOP 实现一个日志插件(应用篇)

代码语言:javascript复制@Aspect @Component public class LogAspect { private static final String SPLIT_SYMBOL = "|"; @Pointcut("execution(public * com.git.hui.boot.aop.demo.*.*(..)) || @annotation(AnoDot)") public void pointcut() { } @Around(value = "pointcut()") public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { Object res = null; String req = null; long start = System.currentTimeMillis(); try { req = buildReqLog(proceedingJoinPoint); res = proceedingJoinPoint.proceed(); return res; } catch (Throwable e) { res = "Un-Expect-Error"; throw e; } finally { long end = System.currentTimeMillis(); System.out.println(req + "" + JSON.toJSONString(res) + SPLIT_SYMBOL + (end - start)); } } private String buildReqLog(ProceedingJoinPoint joinPoint) { // 目标对象 Object target = joinPoint.getTarget(); // 执行的方法 Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); // 请求参数 Object[] args = joinPoint.getArgs(); 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; } }

然后定义一个接口与实现类,注意下面的两个方法,一个注解在接口上,一个注解在实现类上

代码语言:javascript复制public interface BaseApi { @AnoDot String print(String obj); String print2(String obj); } @Component public class BaseApiImpl implements BaseApi { @Override public String print(String obj) { System.out.println("ano in interface:" + obj); return "return:" + obj; } @AnoDot @Override public String print2(String obj) { System.out.println("ano in impl:" + obj); return "return:" + obj; } }

测试 case

代码语言:javascript复制@SpringBootApplication public class Application { public Application(BaseApi baseApi) { System.out.println(baseApi.print("hello world")); System.out.println("-----------"); System.out.println(baseApi.print2("hello world")); } public static void main(String[] args) { SpringApplication.run(Application.class); } }

执行后输出结果如下(有图有真相,别说我骗你 ?)

3. 事务注解测试

上面这个不生效,那我们通常写在接口上的事务注解,会生效么?

添加 mysql 操作的依赖

代码语言:javascript复制 mysql mysql-connector-java org.springframework.boot spring-boot-starter-jdbc

数据库配置 application.properties

代码语言:javascript复制## DataSource spring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2b8 spring.datasource.username=root spring.datasource.password=

接下来就是我们的接口定义与实现

代码语言:javascript复制public interface TransApi { @Transactional(rollbackFor = Exception.class) boolean update(int id); } @Service public class TransApiImpl implements TransApi { @Autowired private JdbcTemplate jdbcTemplate; @Override public boolean update(int id) { String sql = "replace into money (id, name, money) values (" + id + ", '事务测试', 200)"; jdbcTemplate.execute(sql); Object ans = jdbcTemplate.queryForMap("select * from money where id = 111"); System.out.println(ans); throw new RuntimeException("事务回滚"); } }

注意上面的 update 方法,事务注解在接口上,接下来我们需要确认调用之后,是否会回滚

代码语言:javascript复制@SpringBootApplication public class Application { public Application(TransApiImpl transApi, JdbcTemplate jdbcTemplate) { try { transApi.update(111); } catch (Exception e) { System.out.println(e.getMessage()); } System.out.println(jdbcTemplate.queryForList("select * from money where id=111")); } public static void main(String[] args) { SpringApplication.run(Application.class); } }

回滚了,有木有!!!

果然是没有问题的,吓得我一身冷汗,这要是有问题,那就...(不敢想不敢想)

所以问题来了,为啥第一种方式不生效呢???

II. 接口注解切面拦截实现

暂且按下探寻究竟的欲望,先看下如果想让我们可以拦截接口上的注解,可以怎么做呢?

既然拦截不上,多半是因为子类没有继承父类的注解,所以在进行切点匹配时,匹配不到;既然如此,那就让它在匹配时,找下父类看有没有对应的注解

1. 自定义 Pointcut

虽说是自定义,但也没有要求我们直接实现这个接口,我们选择StaticMethodMatcherPointcut来补全逻辑

代码语言:javascript复制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); } }

接下来我们采用声明式来实现切面逻辑

2. 自定义 Advice

这个 advice 就是我们需要执行的切面逻辑,和上面的日志输出差不多,区别在于参数不同

自定义 advice 实现自接口MethodInterceptor,顶层接口是Advice

代码语言:javascript复制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; } }3. 自定义 Advisor

将上面自定义的切点 pointcut 与通知 advice 整合,实现我们的切面

代码语言:javascript复制public static class LogAdvisor extends AbstractBeanFactoryPointcutAdvisor { @Setter private Pointcut logPointCut; @Override public Pointcut getPointcut() { return logPointCut; } }4. 最后注册切面

说是注册,实际上就是声明为 bean,丢到 spring 容器中而已

代码语言:javascript复制@Bean public LogAdvisor init() { LogAdvisor logAdvisor = new LogAdvisor(); // 自定义实现姿势 logAdvisor.setLogPointCut(new LogPointCut()); logAdvisor.setAdvice(new LogAdvice()); return logAdvisor; }

然后再次执行上面的测试用例,输出如下

接口上的注解也被拦截了,但是最后一个耗时的输出,有点夸张了啊,采用上面这种方式,这个耗时有点夸张了啊,生产环境这么一搞,岂不是分分钟卷铺盖的节奏

可以借助 StopWatch 来查看到底是哪里的开销增加了这么多 (关于 StopWatch 的使用,下篇介绍)单次执行的统计偏差问题,将上面的调用,执行一百遍之后,再看耗时,趋于平衡,如下图5. 小结

到这里,我们实现了接口上注解的拦截,虽说解决了我们的需求,但是疑惑的地方依然没有答案

为啥接口上的注解拦截不到 ?为啥事务注解,放在接口上可以生效,事务注解的实现机制是怎样的?自定义的切点,可以配合我们的注解来玩么?为什么首次执行时,耗时比较多;多次执行之后,则耗时趋于正常?

上面这几个问题,毫无意外,我也没有确切的答案,待我研究一番,后续再来分享

III. 不能错过的源码和相关知识点0. 项目工程:https://github.com/liuyueyi/spring-boot-demo接口切面拦截: https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/011-aop-logaspect事务: https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/101-jdbctemplate-transaction

AOP 系列博文

SpringBoot 基础系列 AOP 无法拦截接口上注解场景兼容SpringBoot 基础系列实现一个简单的分布式定时任务(应用篇)SpringBoot 基础篇 AOP 之拦截优先级详解SpringBoot 应用篇之 AOP 实现日志功能SpringBoot 基础篇 AOP 之高级使用技能SpringBoot 基础篇 AOP 之基本使用姿势小结1. 一灰灰 Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有