美文网首页
【SpringBoot 基础系列】接口上注解 AOP 拦截不到场

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

作者: 一灰灰blog | 来源:发表于2021-06-08 19:36 被阅读0次
    image

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

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

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

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

    I. 场景复现

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

    1. 项目环境

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

    添加 aop 依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    

    2. 复现 case

    声明一个注解

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AnoDot {
    }
    

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

    @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;
        }
    }
    

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

    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

    @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);
        }
    }
    

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

    image

    3. 事务注解测试

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

    添加 mysql 操作的依赖

    <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

    ## 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=
    

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

    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 方法,事务注解在接口上,接下来我们需要确认调用之后,是否会回滚

    @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);
        }
    }
    
    image

    回滚了,有木有!!!

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

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

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

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

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

    1. 自定义 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);
        }
    }
    

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

    2. 自定义 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;
        }
    }
    

    3. 自定义 Advisor

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

    public static class LogAdvisor extends AbstractBeanFactoryPointcutAdvisor {
        @Setter
        private Pointcut logPointCut;
    
        @Override
        public Pointcut getPointcut() {
            return logPointCut;
        }
    }
    

    4. 最后注册切面

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

    @Bean
    public LogAdvisor init() {
        LogAdvisor logAdvisor = new LogAdvisor();
        // 自定义实现姿势
        logAdvisor.setLogPointCut(new LogPointCut());
        logAdvisor.setAdvice(new LogAdvice());
        return logAdvisor;
    }
    

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

    image

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

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

    5. 小结

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

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

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

    III. 不能错过的源码和相关知识点

    0. 项目

    AOP 系列博文

    1. 一灰灰 Blog

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

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

    相关文章

      网友评论

          本文标题:【SpringBoot 基础系列】接口上注解 AOP 拦截不到场

          本文链接:https://www.haomeiwen.com/subject/aamjeltx.html