徹底征服 Spring AOP 之 實戰(zhàn)篇

接上一小節(jié)徹底征服 Spring AOP 之 理論篇

Spring AOP 實戰(zhàn)

看了上面這么多的理論知識, 不知道大家有沒有覺得枯燥哈. 不過不要急, 俗話說理論是實踐的基礎(chǔ), 對 Spring AOP 有了基本的理論認(rèn)識后, 我們來看一下下面幾個具體的例子吧.
下面的幾個例子是我在工作中所遇見的比較常用的 Spring AOP 的使用場景, 我精簡了很多有干擾我們學(xué)習(xí)的注意力的細(xì)枝末節(jié), 以力求整個例子的簡潔性.

下面幾個 Demo 的源碼都可以在我的 Github 上下載到.

HTTP 接口鑒權(quán)

首先讓我們來想象一下如下場景: 我們需要提供的 HTTP RESTful 服務(wù), 這個服務(wù)會提供一些比較敏感的信息, 因此對于某些接口的調(diào)用會進(jìn)行調(diào)用方權(quán)限的校驗, 而某些不太敏感的接口則不設(shè)置權(quán)限, 或所需要的權(quán)限比較低(例如某些監(jiān)控接口, 服務(wù)狀態(tài)接口等).
實現(xiàn)這樣的需求的方法有很多, 例如我們可以在每個 HTTP 接口方法中對服務(wù)請求的調(diào)用方進(jìn)行權(quán)限的檢查, 當(dāng)調(diào)用方權(quán)限不符時, 方法返回錯誤. 當(dāng)然這樣做并無不可, 不過如果我們的 api 接口很多, 每個接口都進(jìn)行這樣的判斷, 無疑有很多冗余的代碼, 并且很有可能有某個粗心的家伙忘記了對調(diào)用者的權(quán)限進(jìn)行驗證, 這樣就會造成潛在的 bug.
那么除了上面的所說的方法外, 還有沒有別的比較優(yōu)雅的方式來實現(xiàn)呢? 當(dāng)然有啦, 不然我在這啰嗦半天干嘛呢, 它就是我們今天的主角: AOP.

讓我們來提煉一下我們的需求:

  1. 可以定制地為某些指定的 HTTP RESTful api 提供權(quán)限驗證功能.
  2. 當(dāng)調(diào)用方的權(quán)限不符時, 返回錯誤.

根據(jù)上面所提出的需求, 我們可以進(jìn)行如下設(shè)計:

  1. 提供一個特殊的注解 AuthChecker, 這個是一個方法注解, 有此注解所標(biāo)注的 Controller 需要進(jìn)行調(diào)用方權(quán)限的認(rèn)證.
  2. 利用 Spring AOP, 以 @annotation 切點標(biāo)志符來匹配有注解 AuthChecker 所標(biāo)注的 joinpoint.
  3. 在 advice 中, 簡單地檢查調(diào)用者請求中的 Cookie 中是否有我們指定的 token, 如果有, 則認(rèn)為此調(diào)用者權(quán)限合法, 允許調(diào)用, 反之權(quán)限不合法, 范圍錯誤.

根據(jù)上面的設(shè)計, 我們來看一下具體的源碼吧.
首先是 AuthChecker 注解的定義:
AuthChecker.java:

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

AuthChecker 注解是一個方法注解, 它用于注解 RequestMapping 方法.

有了注解的定義, 那我們再來看一下 aspect 的實現(xiàn)吧:
HttpAopAdviseDefine.java:

@Component
@Aspect
public class HttpAopAdviseDefine {

    // 定義一個 Pointcut, 使用 切點表達(dá)式函數(shù) 來描述對哪些 Join point 使用 advise.
    @Pointcut("@annotation(com.xys.demo1.AuthChecker)")
    public void pointcut() {
    }

    // 定義 advise
    @Around("pointcut()")
    public Object checkAuth(ProceedingJoinPoint joinPoint) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                .getRequest();

        // 檢查用戶所傳遞的 token 是否合法
        String token = getUserToken(request);
        if (!token.equalsIgnoreCase("123456")) {
            return "錯誤, 權(quán)限不合法!";
        }

        return joinPoint.proceed();
    }

    private String getUserToken(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if (cookies == null) {
            return "";
        }
        for (Cookie cookie : cookies) {
            if (cookie.getName().equalsIgnoreCase("user_token")) {
                return cookie.getValue();
            }
        }
        return "";
    }
}

在這個 aspect 中, 我們首先定義了一個 pointcut, 以 @annotation 切點標(biāo)志符來匹配有注解 AuthChecker 所標(biāo)注的 joinpoint, 即:

// 定義一個 Pointcut, 使用 切點表達(dá)式函數(shù) 來描述對哪些 Join point 使用 advise.
@Pointcut("@annotation(com.xys.demo1.AuthChecker)")
public void pointcut() {
}

然后再定義一個 advice:

// 定義 advise
@Around("pointcut()")
public Object checkAuth(ProceedingJoinPoint joinPoint) throws Throwable {
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
            .getRequest();

    // 檢查用戶所傳遞的 token 是否合法
    String token = getUserToken(request);
    if (!token.equalsIgnoreCase("123456")) {
        return "錯誤, 權(quán)限不合法!";
    }

    return joinPoint.proceed();
}

當(dāng)被 AuthChecker 注解所標(biāo)注的方法調(diào)用前, 會執(zhí)行我們的這個 advice, 而這個 advice 的處理邏輯很簡單, 即從 HTTP 請求中獲取名為 user_token 的 cookie 的值, 如果它的值是 123456, 則我們認(rèn)為此 HTTP 請求合法, 進(jìn)而調(diào)用 joinPoint.proceed() 將 HTTP 請求轉(zhuǎn)交給相應(yīng)的控制器處理; 而如果user_token cookie 的值不是 123456, 或為空, 則認(rèn)為此 HTTP 請求非法, 返回錯誤.

接下來我們來寫一個模擬的 HTTP 接口:
DemoController.java:

@RestController
public class DemoController {
    @RequestMapping("/aop/http/alive")
    public String alive() {
        return "服務(wù)一切正常";
    }

    @AuthChecker
    @RequestMapping("/aop/http/user_info")
    public String callSomeInterface() {
        return "調(diào)用了 user_info 接口.";
    }
}

注意到上面我們提供了兩個 HTTP 接口, 其中 接口 /aop/http/alive 是沒有 AuthChecker 標(biāo)注的, 而 /aop/http/user_info 接口則用到了 @AuthChecker 標(biāo)注. 那么自然地, 當(dāng)請求了 /aop/http/user_info 接口時, 就會觸發(fā)我們所設(shè)置的權(quán)限校驗邏輯.

接下來我們來驗證一下, 我們所實現(xiàn)的功能是否有效吧.
首先在 Postman 中, 調(diào)用 /aop/http/alive 接口, 請求頭中不加任何參數(shù):

1478954534732.png

可以看到, 我們的 HTTP 請求完全沒問題.

那么再來看一下請求 /aop/http/user_info 接口會怎樣呢:

1478957022891.png

當(dāng)我們請求 /aop/http/user_info 接口時, 服務(wù)返回一個權(quán)限異常的錯誤, 為什么會這樣呢? 自然就是我們的權(quán)限認(rèn)證系統(tǒng)起了作為: 當(dāng)一個方法被調(diào)用并且這個方法有 AuthChecker 標(biāo)注時, 那么首先會執(zhí)行到我們的 around advice, 在這個 advice 中, 我們會校驗 HTTP 請求的 cookie 字段中是否有攜帶 user_token 字段時, 如果沒有, 則返回權(quán)限錯誤.
那么為了能夠正常地調(diào)用 /aop/http/user_info 接口, 我們可以在 Cookie 中添加 user_token=123456, 這樣我們可以愉快的玩耍了:

1478948169975.png

注意, Postman 默認(rèn)是不支持 Cookie 的, 所以為了實現(xiàn)添加 Cookie 的功能, 我們需要安裝 Postman 的 interceptor 插件. 安裝方法可以看官網(wǎng)的文章

完整源碼

HTTP 接口鑒權(quán)

方法調(diào)用日志

第二個 AOP 實例是記錄一個方法調(diào)用的log. 這應(yīng)該是一個很常見的功能了.
首先假設(shè)我們有如下需求:

  1. 某個服務(wù)下的方法的調(diào)用需要有 log: 記錄調(diào)用的參數(shù)以及返回結(jié)果.
  2. 當(dāng)方法調(diào)用出異常時, 有特殊處理, 例如打印異常 log, 報警等.

根據(jù)上面的需求, 我們可以使用 before advice 來在調(diào)用方法前打印調(diào)用的參數(shù), 使用 after returning advice 在方法返回打印返回的結(jié)果. 而當(dāng)方法調(diào)用失敗后, 可以使用 after throwing advice 來做相應(yīng)的處理.
那么我們來看一下 aspect 的實現(xiàn):

@Component
@Aspect
public class LogAopAdviseDefine {
    private Logger logger = LoggerFactory.getLogger(getClass());

    // 定義一個 Pointcut, 使用 切點表達(dá)式函數(shù) 來描述對哪些 Join point 使用 advise.
    @Pointcut("within(NeedLogService)")
    public void pointcut() {
    }

    // 定義 advise
    @Before("pointcut()")
    public void logMethodInvokeParam(JoinPoint joinPoint) {
        logger.info("---Before method {} invoke, param: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs());
    }

    @AfterReturning(pointcut = "pointcut()", returning = "retVal")
    public void logMethodInvokeResult(JoinPoint joinPoint, Object retVal) {
        logger.info("---After method {} invoke, result: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs());
    }

    @AfterThrowing(pointcut = "pointcut()", throwing = "exception")
    public void logMethodInvokeException(JoinPoint joinPoint, Exception exception) {
        logger.info("---method {} invoke exception: {}---", joinPoint.getSignature().toShortString(), exception.getMessage());
    }
}

第一步, 自然是定義一個 pointcut, 以 within 切點標(biāo)志符來匹配類 NeedLogService 下的所有 joinpoint, 即:

@Pointcut("within(NeedLogService)")
public void pointcut() {
}

接下來根據(jù)我們前面的設(shè)計, 我們分別定義了三個 advice, 第一個是一個 before advice:

@Before("pointcut()")
public void logMethodInvokeParam(JoinPoint joinPoint) {
    logger.info("---Before method {} invoke, param: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs());
}

它在一個符合要求的 joinpoint 方法調(diào)用前執(zhí)行, 打印調(diào)用的方法名和調(diào)用的參數(shù).

第二個是 after return advice:

@AfterReturning(pointcut = "pointcut()", returning = "retVal")
public void logMethodInvokeResult(JoinPoint joinPoint, Object retVal) {
    logger.info("---After method {} invoke, result: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs());
}

這個 advice 會在方法調(diào)用成功后打印出方法名還反的參數(shù).

最后一個是 after throw advice:

@AfterThrowing(pointcut = "pointcut()", throwing = "exception")
public void logMethodInvokeException(JoinPoint joinPoint, Exception exception) {
    logger.info("---method {} invoke exception: {}---", joinPoint.getSignature().toShortString(), exception.getMessage());
}

這個 advice 會在指定的 joinpoint 拋出異常時執(zhí)行, 打印異常的信息.

接下來我們再寫兩個 Service 類:
NeedLogService.java:

@Service
public class NeedLogService {
    private Logger logger = LoggerFactory.getLogger(getClass());
    private Random random = new Random(System.currentTimeMillis());

    public int logMethod(String someParam) {
        logger.info("---NeedLogService: logMethod invoked, param: {}---", someParam);
        return random.nextInt();
    }

    public void exceptionMethod() throws Exception {
        logger.info("---NeedLogService: exceptionMethod invoked---");
        throw new Exception("Something bad happened!");
    }
}

NormalService.java:

@Service
public class NormalService {
    private Logger logger = LoggerFactory.getLogger(getClass());

    public void someMethod() {
        logger.info("---NormalService: someMethod invoked---");
    }
}

根據(jù)我們 pointcut 的規(guī)則, 類 NeedLogService 下的所有方法都會被織入 advice, 而類 NormalService 則不會.

最后我們分別調(diào)用這幾個方法:

@PostConstruct
public void test() {
    needLogService.logMethod("xys");
    try {
        needLogService.exceptionMethod();
    } catch (Exception e) {
        // Ignore
    }
    normalService.someMethod();
}

我們可以看到有如下輸出:

---Before method NeedLogService.logMethod(..) invoke, param: [xys]---
---NeedLogService: logMethod invoked, param: xys---
---After method NeedLogService.logMethod(..) invoke, result: [xys]---

---Before method NeedLogService.exceptionMethod() invoke, param: []---
---NeedLogService: exceptionMethod invoked---
---method NeedLogService.exceptionMethod() invoke exception: Something bad happened!---

---NormalService: someMethod invoked---

根據(jù) log, 我們知道, NeedLogService.logMethod 執(zhí)行的前后確實有 advice 執(zhí)行了, 并且在 NeedLogService.exceptionMethod 拋出異常后, logMethodInvokeException 這個 advice 也被執(zhí)行了. 而由于 pointcut 的匹配規(guī)則, 在 NormalService 類中的方法則不會織入 advice.

完整源碼

方法調(diào)用日志

方法耗時統(tǒng)計

作為程序員, 我們都知道服務(wù)監(jiān)控對于一個服務(wù)能夠長期穩(wěn)定運行的重要性, 因此很多公司都有自己內(nèi)部的監(jiān)控報警系統(tǒng), 或者是使用一些開源的系統(tǒng), 例如小米的 Falcon 監(jiān)控系統(tǒng).

那么在程序監(jiān)控中, AOP 有哪些用武之地呢? 我們來假想一下如下場景:

有一天, leader 對小王說, "小王啊, 你負(fù)責(zé)的那個服務(wù)不太穩(wěn)定啊, 經(jīng)常有超時發(fā)生! 你有對這些服務(wù)接口進(jìn)行過耗時統(tǒng)計嗎?"
耗時統(tǒng)計? 小王嘀咕了, 小聲的回答到: "還沒有加呢."
leader: "你看著辦吧, 我明天要看到各個時段的服務(wù)接口調(diào)用的耗時分布!"
小王這就犯難了, 雖然說計算一個方法的調(diào)用耗時并不是一個很難的事情, 但是整個服務(wù)有二十來個接口呢, 一個一個地添加統(tǒng)計代碼, 那還不是要累死人了.
看著同事一個一個都下班回家了, 小王眉頭更加緊了. 不過此時小王靈機一動: "噫, 有了!".
小王想到了一個好方法, 立即動手, 吭哧吭哧地幾分鐘就搞定了.

那么小王的解決方法是什么呢? 自然是我們的主角 AOP 啦.

首先讓我們來提煉一下需求:

  1. 為服務(wù)中的每個方法調(diào)用進(jìn)行調(diào)用耗時記錄.
  2. 將方法調(diào)用的時間戳, 方法名, 調(diào)用耗時上報到監(jiān)控平臺

有了需求, 自然設(shè)計實現(xiàn)就很簡單了. 首先我們可以使用 around advice, 然后在方法調(diào)用前, 記錄一下開始時間, 然后在方法調(diào)用結(jié)束后, 記錄結(jié)束時間, 它們的時間差就是方法的調(diào)用耗時.

我們來看一下具體的 aspect 實現(xiàn):

ExpiredAopAdviseDefine.java:

@Component
@Aspect
public class ExpiredAopAdviseDefine {
    private Logger logger = LoggerFactory.getLogger(getClass());

    // 定義一個 Pointcut, 使用 切點表達(dá)式函數(shù) 來描述對哪些 Join point 使用 advise.
    @Pointcut("within(SomeService)")
    public void pointcut() {
    }

    // 定義 advise
    // 定義 advise
    @Around("pointcut()")
    public Object methodInvokeExpiredTime(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        // 開始
        Object retVal = pjp.proceed();
        stopWatch.stop();
        // 結(jié)束

        // 上報到公司監(jiān)控平臺
        reportToMonitorSystem(pjp.getSignature().toShortString(), stopWatch.getTotalTimeMillis());

        return retVal;
    }


    public void reportToMonitorSystem(String methodName, long expiredTime) {
        logger.info("---method {} invoked, expired time: {} ms---", methodName, expiredTime);
        //
    }
}

aspect 一開始定義了一個 pointcut, 匹配 SomeService 類下的所有的方法.
接著呢, 定義了一個 around advice:

@Around("pointcut()")
public Object methodInvokeExpiredTime(ProceedingJoinPoint pjp) throws Throwable {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    // 開始
    Object retVal = pjp.proceed();
    stopWatch.stop();
    // 結(jié)束

    // 上報到公司監(jiān)控平臺
    reportToMonitorSystem(pjp.getSignature().toShortString(), stopWatch.getTotalTimeMillis());

    return retVal;
}

advice 中的代碼也很簡單, 它使用了 Spring 提供的 StopWatch 來統(tǒng)計一段代碼的執(zhí)行時間. 首先我們先調(diào)用 stopWatch.start() 開始計時, 然后通過 pjp.proceed() 來調(diào)用我們實際的服務(wù)方法, 當(dāng)調(diào)用結(jié)束后, 通過 stopWatch.stop() 來結(jié)束計時.

接著我們來寫一個簡單的服務(wù), 這個服務(wù)提供一個 someMethod 方法用于模擬一個耗時的方法調(diào)用:
SomeService.java:

@Service
public class SomeService {
    private Logger logger = LoggerFactory.getLogger(getClass());
    private Random random = new Random(System.currentTimeMillis());

    public void someMethod() {
        logger.info("---SomeService: someMethod invoked---");
        try {
            // 模擬耗時任務(wù)
            Thread.sleep(random.nextInt(500));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

這樣當(dāng) SomeService 類下的方法調(diào)用時, 我們所提供的 advice 就會被執(zhí)行, 因此就可以自動地為我們統(tǒng)計此方法的調(diào)用耗時, 并自動上報到監(jiān)控系統(tǒng)中了.
看到 AOP 的威力了吧, 我們這里僅僅使用了寥寥數(shù)語就把一個需求完美地解決了, 并且還與原來的業(yè)務(wù)邏輯完全解耦, 擴展及其方便.

完整源碼

方法耗時統(tǒng)計

總結(jié)

通過上面的幾個簡單例子, 我們對 Spring AOP 的使用應(yīng)該有了一個更為深入的了解了. 其實 Spring AOP 的使用的地方不止這些, 例如 Spring 的 聲明式事務(wù) 就是在 AOP 之上構(gòu)建的. 讀者朋友也可以根據(jù)自己的實際業(yè)務(wù)場景, 合理使用 Spring AOP, 發(fā)揮它的強大功能!

End.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌杂穷,老刑警劉巖士修,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窃诉,死亡現(xiàn)場離奇詭異鹉梨,居然都是意外死亡,警方通過查閱死者的電腦和手機廓奕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來档叔,“玉大人懂从,你說我怎么就攤上這事《灼眩” “怎么了番甩?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長届搁。 經(jīng)常有香客問我缘薛,道長窍育,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任宴胧,我火速辦了婚禮漱抓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘恕齐。我一直安慰自己乞娄,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布显歧。 她就那樣靜靜地躺著仪或,像睡著了一般。 火紅的嫁衣襯著肌膚如雪士骤。 梳的紋絲不亂的頭發(fā)上范删,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音拷肌,去河邊找鬼到旦。 笑死,一個胖子當(dāng)著我的面吹牛巨缘,可吹牛的內(nèi)容都是我干的添忘。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼若锁,長吁一口氣:“原來是場噩夢啊……” “哼昔汉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起拴清,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤靶病,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后口予,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體娄周,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年沪停,在試婚紗的時候發(fā)現(xiàn)自己被綠了煤辨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡木张,死狀恐怖众辨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情舷礼,我是刑警寧澤鹃彻,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站妻献,受9級特大地震影響蛛株,放射性物質(zhì)發(fā)生泄漏团赁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一谨履、第九天 我趴在偏房一處隱蔽的房頂上張望欢摄。 院中可真熱鬧,春花似錦笋粟、人聲如沸怀挠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽绿淋。三九已至,卻和暖如春吨艇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背腾啥。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工东涡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人倘待。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓疮跑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親凸舵。 傳聞我的和親對象是個殘疾皇子祖娘,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

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

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)啊奄,斷路器渐苏,智...
    卡卡羅2017閱讀 134,659評論 18 139
  • 基本知識 其實, 接觸了這么久的 AOP, 我感覺, AOP 給人難以理解的一個關(guān)鍵點是它的概念比較多, 而且坑爹...
    永順閱讀 8,209評論 5 114
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,822評論 6 342
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,163評論 25 707
  • 窗戶邊上, 長亭走廊菇夸, 馬路左邊琼富, 九號公交。 點點記憶碎片庄新, 都在一滴一滴敲打在我的心房鞠眉。 我在迷惘彷徨, 只因...
    阿俊xi閱讀 285評論 0 0