【SpringBoot 基礎(chǔ)系列】接口上注解 AOP 攔截不到場(chǎng)景兼容實(shí)例演示

image

【SpringBoot 基礎(chǔ)系列】接口上注解 AOP 攔截不到場(chǎng)景兼容

在 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é)果如下(有圖有真相,別說我騙你 ??)

image

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

回滾了钥飞,有木有]郝印!读宙!

果然是沒有問題的彻秆,嚇得我一身冷汗,這要是有問題,那就...(不敢想不敢想)

所以問題來了唇兑,為啥第一種方式不生效呢酒朵??扎附?

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è)試用例唇跨,輸出如下

image

接口上的注解也被攔截了稠通,但是最后一個(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í),趨于平衡虱而,如下圖
image

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)目

AOP 系列博文

1. 一灰灰 Blog

盡信書則不如,以上內(nèi)容著洼,純屬一家之言,因個(gè)人能力有限而叼,難免有疏漏和錯(cuò)誤之處身笤,如發(fā)現(xiàn) bug 或者有更好的建議,歡迎批評(píng)指正葵陵,不吝感激

下面一灰灰的個(gè)人博客液荸,記錄所有學(xué)習(xí)和工作中的博文,歡迎大家前去逛逛

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末脱篙,一起剝皮案震驚了整個(gè)濱河市娇钱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌绊困,老刑警劉巖文搂,帶你破解...
    沈念sama閱讀 222,627評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異秤朗,居然都是意外死亡煤蹭,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門取视,熙熙樓的掌柜王于貴愁眉苦臉地迎上來硝皂,“玉大人,你說我怎么就攤上這事作谭』铮” “怎么了?”我有些...
    開封第一講書人閱讀 169,346評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵折欠,是天一觀的道長(zhǎng)贝或。 經(jīng)常有香客問我吼过,道長(zhǎng),這世上最難降的妖魔是什么傀缩? 我笑而不...
    開封第一講書人閱讀 60,097評(píng)論 1 300
  • 正文 為了忘掉前任那先,我火速辦了婚禮,結(jié)果婚禮上赡艰,老公的妹妹穿的比我還像新娘售淡。我一直安慰自己,他們只是感情好慷垮,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,100評(píng)論 6 398
  • 文/花漫 我一把揭開白布揖闸。 她就那樣靜靜地躺著,像睡著了一般料身。 火紅的嫁衣襯著肌膚如雪汤纸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,696評(píng)論 1 312
  • 那天芹血,我揣著相機(jī)與錄音贮泞,去河邊找鬼。 笑死幔烛,一個(gè)胖子當(dāng)著我的面吹牛啃擦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播饿悬,決...
    沈念sama閱讀 41,165評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼令蛉,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了狡恬?” 一聲冷哼從身側(cè)響起珠叔,我...
    開封第一講書人閱讀 40,108評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎弟劲,沒想到半個(gè)月后祷安,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,646評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡兔乞,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,709評(píng)論 3 342
  • 正文 我和宋清朗相戀三年辆憔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片报嵌。...
    茶點(diǎn)故事閱讀 40,861評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡虱咧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出锚国,到底是詐尸還是另有隱情腕巡,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布血筑,位于F島的核電站绘沉,受9級(jí)特大地震影響煎楣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜车伞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,196評(píng)論 3 336
  • 文/蒙蒙 一择懂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧另玖,春花似錦困曙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鳄哭,卻和暖如春要糊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背妆丘。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工锄俄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人勺拣。 一個(gè)月前我還...
    沈念sama閱讀 49,287評(píng)論 3 379
  • 正文 我出身青樓珊膜,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親宣脉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,860評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容