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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 在 Java 的开发过程中,面向接口的编程可能是大家的常态,切面也是各位大佬使用 Spring 时,或多或少会使用的一项基本技能;结果这两个碰到一起,有意思的事情就发生了,接口方法上添加注解,面向注解的切面拦截,居然不生效这就有点奇怪了啊,最开始遇到这个问题时,表示难以相信;事务注解也挺多是写在接口上的,好像也没有遇到这个问题(难道是也不生效,只是自己没有关注到?)接下来我们好好瞅瞅,这到底是怎么个情况

image.png


在 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.png


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.png


回滚了,有木有!!!


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

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


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.png


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


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


image.png


5. 小结


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


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

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



相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
2月前
|
Java API 数据安全/隐私保护
(工作经验)优雅实现接口权限校验控制:基于自定义注解、AOP与@ConditionalOnProperty配置开关的通用解决方案
(工作经验)优雅实现接口权限校验控制:基于自定义注解、AOP与@ConditionalOnProperty配置开关的通用解决方案
64 1
|
2月前
|
XML Java 数据格式
使用完全注解的方式进行AOP功能实现(@Aspect+@Configuration+@EnableAspectJAutoProxy+@ComponentScan)
本文介绍了如何使用Spring框架的注解方式实现AOP(面向切面编程)。当目标对象没有实现接口时,Spring会自动采用CGLIB库进行动态代理。文中详细解释了常用的AOP注解,如`@Aspect`、`@Pointcut`、`@Before`等,并提供了完整的示例代码,包括业务逻辑类`User`、配置类`SpringConfiguration`、切面类`LoggingAspect`以及测试类`TestAnnotationConfig`。通过这些示例,展示了如何在方法执行前后添加日志记录等切面逻辑。
159 2
使用完全注解的方式进行AOP功能实现(@Aspect+@Configuration+@EnableAspectJAutoProxy+@ComponentScan)
|
28天前
|
JSON Java 数据库
SpringBoot项目使用AOP及自定义注解保存操作日志
SpringBoot项目使用AOP及自定义注解保存操作日志
38 1
|
1月前
|
Java 开发者 Spring
精通SpringBoot:16个扩展接口精讲
【10月更文挑战第16天】 SpringBoot以其简化的配置和强大的扩展性,成为了Java开发者的首选框架之一。SpringBoot提供了一系列的扩展接口,使得开发者能够灵活地定制和扩展应用的行为。掌握这些扩展接口,能够帮助我们写出更加优雅和高效的代码。本文将详细介绍16个SpringBoot的扩展接口,并探讨它们在实际开发中的应用。
44 1
|
2月前
|
存储 安全 Java
|
2月前
|
存储 算法 安全
SpringBoot 接口加密解密实现
【10月更文挑战第18天】
|
2月前
|
Java API Spring
springboot学习七:Spring Boot2.x 拦截器基础入门&实战项目场景实现
这篇文章是关于Spring Boot 2.x中拦截器的入门教程和实战项目场景实现的详细指南。
28 0
springboot学习七:Spring Boot2.x 拦截器基础入门&实战项目场景实现
|
2月前
|
Java API Spring
springboot学习六:Spring Boot2.x 过滤器基础入门&实战项目场景实现
这篇文章是关于Spring Boot 2.x中过滤器的基础知识和实战项目应用的教程。
28 0
springboot学习六:Spring Boot2.x 过滤器基础入门&实战项目场景实现
|
2月前
|
监控 Java 开发者
掌握SpringBoot扩展接口:提升代码优雅度的16个技巧
【10月更文挑战第20天】 SpringBoot以其简化配置和快速开发而受到开发者的青睐。除了基本的CRUD操作外,SpringBoot还提供了丰富的扩展接口,让我们能够更灵活地定制和扩展应用。以下是16个常用的SpringBoot扩展接口,掌握它们将帮助你写出更加优雅的代码。
60 0
|
2月前
|
缓存 NoSQL Java
Springboot自定义注解+aop实现redis自动清除缓存功能
通过上述步骤,我们不仅实现了一个高度灵活的缓存管理机制,还保证了代码的整洁与可维护性。自定义注解与AOP的结合,让缓存清除逻辑与业务逻辑分离,便于未来的扩展和修改。这种设计模式非常适合需要频繁更新缓存的应用场景,大大提高了开发效率和系统的响应速度。
68 2