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

文章目录
  1. I. 场景复现
    1. 1. 项目环境
    2. 2. 复现case
    3. 3. 事务注解测试
  2. II. 接口注解切面拦截实现
    1. 1. 自定义Pointcut
    2. 2. 自定义Advice
    3. 3. 自定义Advisor
    4. 4. 最后注册切面
    5. 5. 小结
  3. III. 其他
    1. 0. 项目
    2. 1. 一灰灰Blog

在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) {
// 直接使用spring工具包,来获取method上的注解(会找父类上的注解)
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或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog


打赏 如果觉得我的文章对您有帮助,请随意打赏。
分享到