在 Java 的開發(fā)過程中嗽仪,面向接口的編程可能是大家的常態(tài)挺据,切面也是各位大佬使用 Spring 時(shí)缤沦,或多或少會(huì)使用的一項(xiàng)基本技能;結(jié)果這兩個(gè)碰到一起闪水,有意思的事情就發(fā)生了,接口方法上添加注解纵刘,面向注解的切面攔截弃酌,居然不生效
這就有點(diǎn)奇怪了啊,最開始遇到這個(gè)問題時(shí)诀姚,表示難以相信响牛;事務(wù)注解也挺多是寫在接口上的,好像也沒有遇到這個(gè)問題(難道是也不生效赫段,只是自己沒有關(guān)注到呀打?)
接下來我們好好瞅瞅,這到底是怎么個(gè)情況
I. 場(chǎng)景復(fù)現(xiàn)
這個(gè)場(chǎng)景復(fù)現(xiàn)相對(duì)而言比較簡(jiǎn)單了糯笙,一個(gè)接口贬丛,一個(gè)實(shí)現(xiàn)類;一個(gè)注解给涕,一個(gè)切面完事
1. 項(xiàng)目環(huán)境
采用SpringBoot 2.2.1.RELEASE
+ IDEA
+ maven
進(jìn)行開發(fā)
添加 aop 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. 復(fù)現(xiàn) case
聲明一個(gè)注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnoDot {
}
攔截切面豺憔,下面這段代碼來自之前分享的博文 【基礎(chǔ)系列】AOP 實(shí)現(xiàn)一個(gè)日志插件(應(yīng)用篇)
@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) {
// 目標(biāo)對(duì)象
Object target = joinPoint.getTarget();
// 執(zhí)行的方法
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
// 請(qǐng)求參數(shù)
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;
}
}
然后定義一個(gè)接口與實(shí)現(xiàn)類,注意下面的兩個(gè)方法够庙,一個(gè)注解在接口上恭应,一個(gè)注解在實(shí)現(xiàn)類上
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;
}
}
測(cè)試 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);
}
}
執(zhí)行后輸出結(jié)果如下(有圖有真相,別說我騙你 ??)
3. 事務(wù)注解測(cè)試
上面這個(gè)不生效耘眨,那我們通常寫在接口上的事務(wù)注解昼榛,會(huì)生效么?
添加 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>
數(shù)據(jù)庫(kù)配置 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=
接下來就是我們的接口定義與實(shí)現(xiàn)
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 + ", '事務(wù)測(cè)試', 200)";
jdbcTemplate.execute(sql);
Object ans = jdbcTemplate.queryForMap("select * from money where id = 111");
System.out.println(ans);
throw new RuntimeException("事務(wù)回滾");
}
}
注意上面的 update 方法剔难,事務(wù)注解在接口上胆屿,接下來我們需要確認(rèn)調(diào)用之后,是否會(huì)回滾
@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. 接口注解切面攔截實(shí)現(xiàn)
暫且按下探尋究竟的欲望蔫耽,先看下如果想讓我們可以攔截接口上的注解,可以怎么做呢?
既然攔截不上留夜,多半是因?yàn)樽宇悰]有繼承父類的注解匙铡,所以在進(jìn)行切點(diǎn)匹配時(shí),匹配不到碍粥;既然如此鳖眼,那就讓它在匹配時(shí),找下父類看有沒有對(duì)應(yīng)的注解
1. 自定義 Pointcut
雖說是自定義嚼摩,但也沒有要求我們直接實(shí)現(xiàn)這個(gè)接口钦讳,我們選擇StaticMethodMatcherPointcut
來補(bǔ)全邏輯
import org.springframework.core.annotation.AnnotatedElementUtils;
public static class LogPointCut extends StaticMethodMatcherPointcut {
@SneakyThrows
@Override
public boolean matches(Method method, Class<?> aClass) {
// 直接使用spring工具包,來獲取method上的注解(會(huì)找父類上的注解)
return AnnotatedElementUtils.hasAnnotation(method, AnoDot.class);
}
}
接下來我們采用聲明式來實(shí)現(xiàn)切面邏輯
2. 自定義 Advice
這個(gè) advice 就是我們需要執(zhí)行的切面邏輯枕面,和上面的日志輸出差不多愿卒,區(qū)別在于參數(shù)不同
自定義 advice 實(shí)現(xiàn)自接口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) {
// 目標(biāo)對(duì)象
Object target = joinPoint.getThis();
// 執(zhí)行的方法
Method method = joinPoint.getMethod();
// 請(qǐng)求參數(shù)
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
將上面自定義的切點(diǎn) pointcut 與通知 advice 整合潮秘,實(shí)現(xiàn)我們的切面
public static class LogAdvisor extends AbstractBeanFactoryPointcutAdvisor {
@Setter
private Pointcut logPointCut;
@Override
public Pointcut getPointcut() {
return logPointCut;
}
}
4. 最后注冊(cè)切面
說是注冊(cè)琼开,實(shí)際上就是聲明為 bean,丟到 spring 容器中而已
@Bean
public LogAdvisor init() {
LogAdvisor logAdvisor = new LogAdvisor();
// 自定義實(shí)現(xiàn)姿勢(shì)
logAdvisor.setLogPointCut(new LogPointCut());
logAdvisor.setAdvice(new LogAdvice());
return logAdvisor;
}
然后再次執(zhí)行上面的測(cè)試用例唇跨,輸出如下
接口上的注解也被攔截了稠通,但是最后一個(gè)耗時(shí)的輸出,有點(diǎn)夸張了啊买猖,采用上面這種方式改橘,這個(gè)耗時(shí)有點(diǎn)夸張了啊,生產(chǎn)環(huán)境這么一搞玉控,豈不是分分鐘卷鋪蓋的節(jié)奏
- 可以借助 StopWatch 來查看到底是哪里的開銷增加了這么多 (關(guān)于 StopWatch 的使用飞主,下篇介紹)
- 單次執(zhí)行的統(tǒng)計(jì)偏差問題,將上面的調(diào)用高诺,執(zhí)行一百遍之后碌识,再看耗時(shí),趨于平衡虱而,如下圖
5. 小結(jié)
到這里筏餐,我們實(shí)現(xiàn)了接口上注解的攔截,雖說解決了我們的需求牡拇,但是疑惑的地方依然沒有答案
- 為啥接口上的注解攔截不到 魁瞪?
- 為啥事務(wù)注解穆律,放在接口上可以生效,事務(wù)注解的實(shí)現(xiàn)機(jī)制是怎樣的导俘?
- 自定義的切點(diǎn)峦耘,可以配合我們的注解來玩么?
- 為什么首次執(zhí)行時(shí)旅薄,耗時(shí)比較多辅髓;多次執(zhí)行之后,則耗時(shí)趨于正常少梁?
上面這幾個(gè)問題洛口,毫無意外,我也沒有確切的答案凯沪,待我研究一番绍弟,后續(xù)再來分享
III. 不能錯(cuò)過的源碼和相關(guān)知識(shí)點(diǎn)
0. 項(xiàng)目
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 接口切面攔截: https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/011-aop-logaspect
- 事務(wù): https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/101-jdbctemplate-transaction
AOP 系列博文
- SpringBoot 基礎(chǔ)系列 AOP 無法攔截接口上注解場(chǎng)景兼容
- SpringBoot 基礎(chǔ)系列實(shí)現(xiàn)一個(gè)簡(jiǎn)單的分布式定時(shí)任務(wù)(應(yīng)用篇)
- SpringBoot 基礎(chǔ)篇 AOP 之?dāng)r截優(yōu)先級(jí)詳解
- SpringBoot 應(yīng)用篇之 AOP 實(shí)現(xiàn)日志功能
- SpringBoot 基礎(chǔ)篇 AOP 之高級(jí)使用技能
- SpringBoot 基礎(chǔ)篇 AOP 之基本使用姿勢(shì)小結(jié)
1. 一灰灰 Blog
盡信書則不如,以上內(nèi)容著洼,純屬一家之言,因個(gè)人能力有限而叼,難免有疏漏和錯(cuò)誤之處身笤,如發(fā)現(xiàn) bug 或者有更好的建議,歡迎批評(píng)指正葵陵,不吝感激
下面一灰灰的個(gè)人博客液荸,記錄所有學(xué)習(xí)和工作中的博文,歡迎大家前去逛逛
- 一灰灰 Blog 個(gè)人博客 https://blog.hhui.top
- 一灰灰 Blog-Spring 專題博客 http://spring.hhui.top