強悍的Spring之Spring Retry

強悍的Spring之Spring Retry


在日常開發(fā)中,我們經(jīng)常會遇到需要調(diào)用外部服務和接口的場景。外部服務對于調(diào)用者來說一般都是不可靠的喳魏,尤其是在網(wǎng)絡環(huán)境比較差的情況下撵枢,網(wǎng)絡抖動很容易導致請求超時等異常情況,這時候就需要使用失敗重試策略重新調(diào)用 API 接口來獲取凡涩。重試策略在服務治理方面也有很廣泛的使用,通過定時檢測疹蛉,來查看服務是否存活活箕。

Spring異常重試框架Spring Retry

Spring Retry支持集成到Spring或者Spring Boot項目中,而它支持AOP的切面注入寫法可款,所以在引入時必須引入aspectjweaver.jar包育韩。

1.引入maven依賴

 <dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.6</version>
</dependency>

2.添加@Retryable和@Recover注解

@Retryable注解,被注解的方法發(fā)生異常時會重試

  • value:指定發(fā)生的異常進行重試
  • include:和value一樣闺鲸,默認空筋讨,當exclude也為空時,所有異常都重試
  • exclude:指定異常不重試摸恍,默認空悉罕,當include也為空時,所有異常都重試
  • maxAttemps:重試次數(shù)立镶,默認3
  • backoff:重試補償機制蛮粮,默認沒有

@Backoff注解

  • delay:指定延遲后重試
  • multiplier:指定延遲的倍數(shù),比如delay=5000l,multiplier=2時谜慌,第一次重試為5秒后然想,第二次為10秒,第三次為20秒

@Recover注解:
當重試到達指定次數(shù)時欣范,被注解的方法將被回調(diào)变泄,可以在該方法中進行日志處理。需要注意的是發(fā)生的異常和入?yún)㈩愋鸵恢聲r才會回調(diào)恼琼。

@Service
public class RemoteService {

@Retryable(value = {Exception.class}, maxAttempts = 5, backoff = @Backoff(delay = 5000L, multiplier = 1))
public void call() {
    System.out.println(LocalDateTime.now() + ": do something...");
    throw new RuntimeException(LocalDateTime.now() + ": 運行調(diào)用異常");
}

@Recover
public void recover(Exception e) {
    System.out.println(e.getMessage());
}

3.啟用重試功能

啟動類上面添加@EnableRetry注解妨蛹,啟用重試功能,或者在使用retry的service上面添加也可以晴竞,或者Configuration配置類上面蛙卤。

建議所有的Enable配置加在啟動類上,可以清晰的統(tǒng)一管理使用的功能。

@SpringBootApplication
@EnableRetry
public class App {
    public static void main(String[] args) throws Exception {
        ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
        System.out.println("Start app success.");
        RemoteService bean = context.getBean(RemoteService.class);
        bean.call();
    }
}

4.啟動服務颤难,運行測試

通過在啟動類Context調(diào)用服務看到如下打印:

2019-03-09T15:22:12.781: do something...
2019-03-09T15:22:17.808: do something...
2019-03-09T15:22:22.835: do something...
2019-03-09T15:22:27.861: do something...
2019-03-09T15:22:32.887: do something...
2019-03-09T15:22:32.887: 運行調(diào)用異常

基于guava的重試組件Guava-Retryer

直接看組件作者對此組件的介紹:
This is a small extension to Google’s Guava library to allow for the creation of configurable retrying strategies for an arbitrary function call, such as something that talks to a remote service with flaky uptime.(這是對Google的guava庫的一個小擴展神年,允許為任意函數(shù)調(diào)用創(chuàng)建可配置的重試策略,例如與運行時間不穩(wěn)定的遠程服務對話的策略行嗤。)

第一步引入maven坐標:

 <dependency>
    <groupId>com.github.rholder</groupId>
    <artifactId>guava-retrying</artifactId>
    <version>2.0.0</version>
</dependency>

1.其主要接口及策略介紹

  • Attempt:一次執(zhí)行任務已日;
  • AttemptTimeLimiter:單次任務執(zhí)行時間限制(如果單次任務執(zhí)行超時,則終止執(zhí)行當前任務)栅屏;
  • BlockStrategies:任務阻塞策略(通俗的講就是當前任務執(zhí)行完飘千,下次任務還沒開始這段時間做什么……),默認策略為:BlockStrategies.THREAD_SLEEP_STRATEGY 也就是調(diào)用 Thread.sleep(sleepTime);
  • RetryException:重試異常栈雳;
  • RetryListener:自定義重試監(jiān)聽器护奈,可以用于異步記錄錯誤日志;
  • StopStrategy:停止重試策略哥纫,提供三種:
  • StopAfterDelayStrategy :設定一個最長允許的執(zhí)行時間霉旗;比如設定最長執(zhí)行10s,無論任務執(zhí)行次數(shù)磺箕,只要重試的時候超出了最長時間,則任務終止抛虫,并返回重試異常RetryException松靡;
  • NeverStopStrategy :不停止,用于需要一直輪訓直到返回期望結(jié)果的情況建椰;
  • StopAfterAttemptStrategy :設定最大重試次數(shù)雕欺,如果超出最大重試次數(shù)則停止重試,并返回重試異常棉姐;
  • WaitStrategy:等待時長策略(控制時間間隔)屠列,返回結(jié)果為下次執(zhí)行時長:
  • FixedWaitStrategy:固定等待時長策略;
  • RandomWaitStrategy:隨機等待時長策略(可以提供一個最小和最大時長伞矩,等待時長為其區(qū)間隨機值)
  • IncrementingWaitStrategy:遞增等待時長策略(提供一個初始值和步長笛洛,等待時間隨重試次數(shù)增加而增加)
  • ExponentialWaitStrategy:指數(shù)等待時長策略;
  • FibonacciWaitStrategy :Fibonacci 等待時長策略乃坤;
  • ExceptionWaitStrategy :異常時長等待策略苛让;
  • CompositeWaitStrategy :復合時長等待策略;

2.根據(jù)結(jié)果判斷是否重試

使用場景:如果返回值決定是否要重試湿诊。

重試接口:

private static Callable<String> callableWithResult() {
    return new Callable<String>() {
        int counter = 0;

        public String call() throws Exception {
            counter++;
            System.out.println(LocalDateTime.now() + ": do something... " + counter);
            if (counter < 5) {
                return "james";
            }
            return "kobe";
        }
    };
}

測試:

public static void main(String[] args) {
    Retryer<String> retry = RetryerBuilder.<String>newBuilder()
            .retryIfResult(result -> !result.contains("kobe")).build();
    retry.call(callableWithResult());
}

輸出:

2019-03-09T15:40:23.706: do something... 1
2019-03-09T15:40:23.710: do something... 2
2019-03-09T15:40:23.711: do something... 3
2019-03-09T15:40:23.711: do something... 4
2019-03-09T15:40:23.711: do something... 5

3.根據(jù)異常判斷是否重試

使用場景:根據(jù)拋出異常類型判斷是否執(zhí)行重試狱杰。
重試接口:

private static Callable<String> callableWithResult() {
    return new Callable<String>() {
        int counter = 0;

        public String call() throws Exception {
            counter++;
            System.out.println(LocalDateTime.now() + ": do something... " + counter);
            if (counter < 5) {
                throw new RuntimeException("Run exception");
            }
            return "kobe";
        }
    };
}

測試:

public static void main(String[] args) throws ExecutionException, RetryException{
    Retryer<String> retry = RetryerBuilder.<String>newBuilder()
            .retryIfRuntimeException()
            .withStopStrategy(StopStrategies.neverStop())
            .build();
    retry.call(callableWithResult());
}

輸出:

2019-03-09T15:53:27.682: do something... 1
2019-03-09T15:53:27.686: do something... 2
2019-03-09T15:53:27.686: do something... 3
2019-03-09T15:53:27.687: do something... 4
2019-03-09T15:53:27.687: do something... 5

4.重試策略——設定無限重試

使用場景:在有異常情況下,無限重試(默認執(zhí)行策略)厅须,直到返回正常有效結(jié)果仿畸;

 Retryer<String> retry = RetryerBuilder.<String>newBuilder()
            .retryIfRuntimeException()
            .withStopStrategy(StopStrategies.neverStop())
            .build();
    retry.call(callableWithResult());

5.重試策略——設定最大的重試次數(shù)

使用場景:在有異常情況下,最多重試次數(shù),如果超過次數(shù)則會拋出異常错沽;

private static Callable<String> callableWithResult() {
    return new Callable<String>() {
        int counter = 0;

        public String call() throws Exception {
            counter++;
            System.out.println(LocalDateTime.now() + ": do something... " + counter);
            throw new RuntimeException("Run exception");
        }
    };
}

測試:

public static void main(String[] args) throws ExecutionException, RetryException{
    Retryer<String> retry = RetryerBuilder.<String>newBuilder()
            .retryIfRuntimeException()
            .withStopStrategy(StopStrategies.stopAfterAttempt(4))
            .build();
    retry.call(callableWithResult());
}

輸出:

2019-03-09T16:02:29.471: do something... 1
2019-03-09T16:02:29.477: do something... 2
2019-03-09T16:02:29.478: do something... 3
2019-03-09T16:02:29.478: do something... 4
Exception in thread "main" com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 4 attempts.

6.等待策略——設定重試等待固定時長策略

使用場景:設定每次重試等待間隔固定為10s簿晓;

 public static void main(String[] args) throws ExecutionException, RetryExceptio{
    Retryer<String> retry = RetryerBuilder.<String>newBuilder()
            .retryIfRuntimeException()
            .withStopStrategy(StopStrategies.stopAfterAttempt(4))
            .withWaitStrategy(WaitStrategies.fixedWait(10, TimeUnit.SECONDS))
            .build();
    retry.call(callableWithResult());
}

測試輸出,可以看出調(diào)用間隔是10S:

2019-03-09T16:06:34.457: do something... 1
2019-03-09T16:06:44.660: do something... 2
2019-03-09T16:06:54.923: do something... 3
2019-03-09T16:07:05.187: do something... 4
Exception in thread "main" com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 4 attempts.

7.等待策略——設定重試等待時長固定增長策略

場景:設定初始等待時長值甥捺,并設定固定增長步長抢蚀,但不設定最大等待時長;

public static void main(String[] args) throws ExecutionException, RetryException {
    Retryer<String> retry = RetryerBuilder.<String>newBuilder()
            .retryIfRuntimeException()
            .withStopStrategy(StopStrategies.stopAfterAttempt(4))
            .withWaitStrategy(WaitStrategies.incrementingWait(1, SECONDS, 1, SECONDS))
            .build();

    retry.call(callableWithResult());
}

測試輸出镰禾,可以看出調(diào)用間隔時間遞增1秒:

2019-03-09T18:46:30.256: do something... 1
2019-03-09T18:46:31.260: do something... 2
2019-03-09T18:46:33.260: do something... 3
2019-03-09T18:46:36.260: do something... 4
Exception in thread "main" com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 4 attempts.

8.等待策略——設定重試等待時長按指數(shù)增長策略

使用場景:根據(jù)multiplier值按照指數(shù)級增長等待時長皿曲,并設定最大等待時長;

 public static void main(String[] args) throws ExecutionException, RetryExceptio{
    Retryer<String> retry = RetryerBuilder.<String>newBuilder()
            .retryIfRuntimeException()
            .withStopStrategy(StopStrategies.stopAfterAttempt(4))
            .withWaitStrategy(WaitStrategies.exponentialWait(1000, 10,SECONDS))
            .build();
    retry.call(callableWithResult());
}

這個重試策略和入?yún)⒉皇呛芏庹欤冒墒讨ィ榭丛创a:

@Immutable
private static final class ExponentialWaitStrategy implements WaitStrategy {
    private final long multiplier;
    private final long maximumWait;

    public ExponentialWaitStrategy(long multiplier, long maximumWait) {
        Preconditions.checkArgument(multiplier > 0L, "multiplier must be > 0 but is %d", new Object[]{Long.valueOf(multiplier)});
        Preconditions.checkArgument(maximumWait >= 0L, "maximumWait must be >= 0 but is %d", new Object[]{Long.valueOf(maximumWait)});
        Preconditions.checkArgument(multiplier < maximumWait, "multiplier must be < maximumWait but is %d", new Object[]{Long.valueOf(multiplier)});
        this.multiplier = multiplier;
        this.maximumWait = maximumWait;
    }

    public long computeSleepTime(Attempt failedAttempt) {
        double exp = Math.pow(2.0D, (double)failedAttempt.getAttemptNumber());
        long result = Math.round((double)this.multiplier * exp);
        if(result > this.maximumWait) {
            result = this.maximumWait;
        }

        return result >= 0L?result:0L;
    }
}

通過源碼看出ExponentialWaitStrategy是一個不可變的內(nèi)部類哈恰,構(gòu)造器中校驗入?yún)ⅲ钪匾难舆t時間計算方法computeSleepTime(),可以看出延遲時間計算方式

  1. 計算以2為底失敗次數(shù)為指數(shù)的值
  2. 第一步的值構(gòu)造器第一個入?yún)⑾喑伺乇幔缓笏纳嵛迦氲玫窖舆t時間(毫秒)

通過以上分析可知入?yún)?000時間隔是應該為2,4瓦灶,8s

測試輸出烁登,可以看出調(diào)用間隔時間 2×1000,4×1000易阳,8×1000:

2019-03-09T19:11:23.905: do something... 1
2019-03-09T19:11:25.908: do something... 2
2019-03-09T19:11:29.908: do something... 3
2019-03-09T19:11:37.909: do something... 4
Exception in thread "main" com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 4 attempts.

9.等待策略——設定重試等待時長按斐波那契數(shù)列策略

使用場景:根據(jù)multiplier值按照斐波那契數(shù)列增長等待時長附较,并設定最大等待時長,斐波那契數(shù)列:1潦俺、1拒课、2、3事示、5早像、8、13肖爵、21卢鹦、34、……

public static void main(String[] args) throws ExecutionException, RetryException {
    Retryer<String> retry = RetryerBuilder.<String>newBuilder()
            .retryIfRuntimeException()
            .withStopStrategy(StopStrategies.stopAfterAttempt(4))
            .withWaitStrategy(WaitStrategies.fibonacciWait(1000, 10, SECONDS))
            .build();

    retry.call(callableWithResult());
}

同樣劝堪,看源碼可知計算可知延遲時間為斐波那契數(shù)列和第一入?yún)⒌某朔e(毫秒)

public long computeSleepTime(Attempt failedAttempt) {
    long fib = this.fib(failedAttempt.getAttemptNumber());
    long result = this.multiplier * fib;
    if(result > this.maximumWait || result < 0L) {
        result = this.maximumWait;
    }
    return result >= 0L?result:0L;
}

測試輸出法挨,可看出間隔調(diào)用為1×1000,1×1000幅聘,2×1000:

2019-03-09T19:28:43.903: do something... 1
2019-03-09T19:28:44.909: do something... 2
2019-03-09T19:28:45.928: do something... 3
2019-03-09T19:28:47.928: do something... 4
Exception in thread "main" com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 4 attempts.

10.等待策略——組合重試等待時長策略

使用場景:當現(xiàn)有策略不滿足使用場景時凡纳,可以對多個策略進行組合使用。

public static void main(String[] args) throws ExecutionException,     RetryException {
    Retryer<String> retry = RetryerBuilder.<String>newBuilder()
            .retryIfRuntimeException()
            .withStopStrategy(StopStrategies.stopAfterAttempt(10))
            .withWaitStrategy(WaitStrategies.join(WaitStrategies.exponentialWait(1000, 100, SECONDS)
                    , WaitStrategies.fixedWait(2, SECONDS)))
            .build();

    retry.call(callableWithResult());
}

同樣帝蒿,看源碼才能理解組合策略是什么意思:

public long computeSleepTime(Attempt failedAttempt) {
    long waitTime = 0L;

    WaitStrategy waitStrategy;
    for(Iterator i$ = this.waitStrategies.iterator(); i$.hasNext(); waitTime += waitStrategy.computeSleepTime(failedAttempt)) {
        waitStrategy = (WaitStrategy)i$.next();
    }
    return waitTime;
}

可看出組合策略其實按照多個策略的延遲時間相加得到組合策略的延遲時間荐糜。exponentialWait的延遲時間為2,4,8暴氏,16延塑,32...,fixedWait延遲為2答渔,2关带,2,2沼撕,2...,所以總的延遲時間為4宋雏,6,10务豺,18磨总,34...

測試輸出:

2019-03-09T19:46:45.854: do something... 1
2019-03-09T19:46:49.859: do something... 2
2019-03-09T19:46:55.859: do something... 3
2019-03-09T19:47:05.859: do something... 4
2019-03-09T19:47:23.859: do something... 5
2019-03-09T19:47:57.860: do something... 6
2019-03-09T19:49:03.861: do something... 7
2019-03-09T19:50:45.862: do something... 8

11.監(jiān)聽器——RetryListener實現(xiàn)重試過程細節(jié)處理

使用場景:自定義監(jiān)聽器,分別打印重試過程中的細節(jié)笼沥,未來可更多的用于異步日志記錄蚪燕,亦或是特殊處理。

public class MyRetryListener implements RetryListener {
@Override
public <V> void onRetry(Attempt<V> attempt) {
    System.out.println(("retry times=" + attempt.getAttemptNumber()));
    // 距離第一次重試的延遲
    System.out.println("delay=" + attempt.getDelaySinceFirstAttempt());
    // 重試結(jié)果: 是異常終止, 還是正常返回
    System.out.println("hasException=" + attempt.hasException());
    System.out.println("hasResult=" + attempt.hasResult());
    // 是什么原因?qū)е庐惓?    if (attempt.hasException()) {
        System.out.println("causeBy=" + attempt.getExceptionCause());
    } else {
        // 正常返回時的結(jié)果
        System.out.println("result=" + attempt.getResult());
    }
    // 增加了額外的異常處理代碼
    try {
        Object result = attempt.get();
        System.out.println("rude get=" + result);
    } catch (ExecutionException e) {
        System.out.println("this attempt produce exception." + e.getCause());
    }
}

測試:

    public static void main(String[] args) throws ExecutionException, RetryException {
    Retryer<String> retry = RetryerBuilder.<String>newBuilder()
            .retryIfRuntimeException()
            .withStopStrategy(StopStrategies.stopAfterAttempt(2))
            .withRetryListener(new MyRetryListener())
            .build();
    retry.call(callableWithResult());
}

輸出:

2019-03-09T16:32:35.097: do something... 1
retry times=1
delay=128
hasException=true
hasResult=false
causeBy=java.lang.RuntimeException: Run exception
this attempt produce exception.java.lang.RuntimeException: Run exception
2019-03-09T16:32:35.102: do something... 2
retry times=2
delay=129
hasException=true
hasResult=false
causeBy=java.lang.RuntimeException: Run exception
this attempt produce exception.java.lang.RuntimeException: Run exception
Exception in thread "main" com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 2 attempts.

總結(jié)

兩種方式都是比較優(yōu)雅的重試策略奔浅,Spring-retry配置更簡單馆纳,實現(xiàn)的功能也相對簡單,Guava本身就是谷歌推出的精品java類庫汹桦,guava-retry也是功能非常強大鲁驶,相比較于Spring-Retry在是否重試的判斷條件上有更多的選擇性,可以作為Spring-retry的補充营勤。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末灵嫌,一起剝皮案震驚了整個濱河市壹罚,隨后出現(xiàn)的幾起案子葛作,更是在濱河造成了極大的恐慌,老刑警劉巖猖凛,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赂蠢,死亡現(xiàn)場離奇詭異,居然都是意外死亡辨泳,警方通過查閱死者的電腦和手機虱岂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來菠红,“玉大人第岖,你說我怎么就攤上這事∈运荩” “怎么了蔑滓?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我键袱,道長燎窘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任蹄咖,我火速辦了婚禮褐健,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘澜汤。我一直安慰自己蚜迅,他們只是感情好,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布银亲。 她就那樣靜靜地躺著慢叨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪务蝠。 梳的紋絲不亂的頭發(fā)上拍谐,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天,我揣著相機與錄音馏段,去河邊找鬼轩拨。 笑死,一個胖子當著我的面吹牛院喜,可吹牛的內(nèi)容都是我干的亡蓉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼喷舀,長吁一口氣:“原來是場噩夢啊……” “哼砍濒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起硫麻,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤爸邢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后拿愧,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體杠河,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年浇辜,在試婚紗的時候發(fā)現(xiàn)自己被綠了券敌。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡柳洋,死狀恐怖待诅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情熊镣,我是刑警寧澤卑雁,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布立由,位于F島的核電站,受9級特大地震影響序厉,放射性物質(zhì)發(fā)生泄漏锐膜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一弛房、第九天 我趴在偏房一處隱蔽的房頂上張望道盏。 院中可真熱鬧,春花似錦文捶、人聲如沸荷逞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽种远。三九已至,卻和暖如春顽耳,著一層夾襖步出監(jiān)牢的瞬間坠敷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工射富, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留膝迎,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓胰耗,卻偏偏與公主長得像限次,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子柴灯,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

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