【SpringBoot 根底系列】接口上注解 AOP 拦挡不到场景兼容
在 Java 的开发过程中,面向接口的编程可能是大家的常态,切面也是各位大佬应用 Spring 时,或多或少会应用的一项基本技能;后果这两个碰到一起,有意思的事件就产生了,接口办法上增加注解,面向注解的切面拦挡,竟然不失效
这就有点奇怪了啊,最开始遇到这个问题时,示意难以相信;事务注解也挺多是写在接口上的,如同也没有遇到这个问题(难道是也不失效,只是本人没有关注到?)
接下来咱们好好瞅瞅,这到底是怎么个状况
<!– more –>
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);
}
}
执行后输入后果如下(有图有假相,别说我骗你 🙃)
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);
}
}
回滚了,有木有!!!
果然是没有问题的,吓得我一身冷汗,这要是有问题,那就 …(不敢想不敢想)
所以问题来了,为啥第一种形式不失效呢???
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;
}
而后再次执行下面的测试用例,输入如下
接口上的注解也被拦挡了,然而最初一个耗时的输入,有点夸大了啊,采纳下面这种形式,这个耗时有点夸大了啊,生产环境这么一搞,岂不是分分钟卷铺盖的节奏
- 能够借助 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 或者有更好的倡议,欢送批评指正,不吝感谢
上面一灰灰的集体博客,记录所有学习和工作中的博文,欢送大家前去逛逛
- 一灰灰 Blog 集体博客 https://blog.hhui.top
- 一灰灰 Blog-Spring 专题博客 http://spring.hhui.top