在Java的开发过程中,面向接口的编程可能是大家的常态,切面也是各位大佬使用Spring时,或多或少会使用的一项基本技能;结果这两个碰到一起,有意思的事情就发生了,接口方法上添加注解,面向注解的切面拦截,居然不生效
这就有点奇怪了啊,最开始遇到这个问题时,表示难以相信;事务注解也挺多是写在接口上的,好像也没有遇到这个问题(难道是也不生效,只是自己没有关注到?)
接下来我们好好瞅瞅,这到底是怎么个情况
I. 场景复现
这个场景复现相对而言比较简单了,一个接口,一个实现类;一个注解,一个切面完事
1. 项目环境
采用SpringBoot 2.2.1.RELEASE + IDEA + maven 进行开发
添加aop依赖
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
|
2. 复现case
声明一个注解
1 2 3 4
| @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AnoDot { }
|
拦截切面,下面这段代码来自之前分享的博文 【基础系列】AOP实现一个日志插件(应用篇)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| @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; } }
|
然后定义一个接口与实现类,注意下面的两个方法,一个注解在接口上,一个注解在实现类上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13
| @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操作的依赖
1 2 3 4 5 6 7 8 9 10
| <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> </dependencies>
|
数据库配置 application.properties
1 2 3 4
| ## 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=
|
接下来就是我们的接口定义与实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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方法,事务注解在接口上,接下来我们需要确认调用之后,是否会回滚
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @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来补全逻辑
1 2 3 4 5 6 7 8 9 10 11
| import org.springframework.core.annotation.AnnotatedElementUtils;
public static class LogPointCut extends StaticMethodMatcherPointcut {
@SneakyThrows @Override public boolean matches(Method method, Class<?> aClass) { return AnnotatedElementUtils.hasAnnotation(method, AnoDot.class); } }
|
接下来我们采用声明式来实现切面逻辑
2. 自定义Advice
这个advice就是我们需要执行的切面逻辑,和上面的日志输出差不多,区别在于参数不同
自定义advice实现自接口MethodInterceptor,顶层接口是Advice
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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整合,实现我们的切面
1 2 3 4 5 6 7 8 9
| public static class LogAdvisor extends AbstractBeanFactoryPointcutAdvisor { @Setter private Pointcut logPointCut;
@Override public Pointcut getPointcut() { return logPointCut; } }
|
4. 最后注册切面
说是注册,实际上就是声明为bean,丢到spring容器中而已
1 2 3 4 5 6 7 8
| @Bean public LogAdvisor init() { LogAdvisor logAdvisor = new LogAdvisor(); logAdvisor.setLogPointCut(new LogPointCut()); logAdvisor.setAdvice(new LogAdvice()); return logAdvisor; }
|
然后再次执行上面的测试用例,输出如下

接口上的注解也被拦截了,但是最后一个耗时的输出,有点夸张了啊,采用上面这种方式,这个耗时有点夸张了啊,生产环境这么一搞,岂不是分分钟卷铺盖的节奏
- 可以借助 StopWatch 来查看到底是哪里的开销增加了这么多 (关于StopWatch的使用,下篇介绍)
- 单次执行的统计偏差问题,将上面的调用,执行一百遍之后,再看耗时,趋于平衡,如下图

5. 小结
到这里,我们实现了接口上注解的拦截,虽说解决了我们的需求,但是疑惑的地方依然没有答案
- 为啥接口上的注解拦截不到 ?
- 为啥事务注解,放在接口上可以生效,事务注解的实现机制是怎样的?
- 自定义的切点,可以配合我们的注解来玩么?
- 为什么首次执行时,耗时比较多;多次执行之后,则耗时趋于正常?
上面这几个问题,毫无意外,我也没有确切的答案,待我研究一番,后续再来分享
III. 其他
0. 项目
1. 一灰灰Blog
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

打赏
如果觉得我的文章对您有帮助,请随意打赏。
微信打赏
支付宝打赏