zuul學習五:zuul 異常處理

過濾器的執行流程
  • 一般來講,正常的流程是pre-->route-->post
  • 在pre過濾器階段拋出異常,pre--> error -->post
  • 在route過濾器階段拋出異常,pre-->route-->error -->post
  • 在post過濾器階段拋出異常,pre-->route-->post--> error

通過上面請求生命周期和核心過濾器的介紹,我們發現在核心過濾器中并沒有實現error階段的過濾器,那么當過濾器出現異常的時候需要怎么處理呢?

自定義一個過濾器ThrowExceptionFilter在執行時期拋出異常

@Component
public class ThrowExceptionFilter extends ZuulFilter{

    private static Logger logger = LoggerFactory.getLogger(ThrowExceptionFilter.class);

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        logger.info("this is a pre filter,it will throw a RuntimeException");
        doSomething();
        return null;
    }

    private void doSomething(){
        throw new RuntimeException("exist some errors....");
    }
}

啟動服務,


訪問user服務,

http://192.168.5.5:6069/users/user/home

我們發現api網關服務的控制臺輸出ThrowExceptionFilter的過濾邏輯的日志信息,但是沒有輸出任何異常信息,同時發起的請求也沒有獲得任何響應結果。

為什么會出現這樣的情況?我們又該怎樣處理過濾器中的一場呢?

try-catch處理?

回想一下,我們在上一節中介紹的所有核心過濾器,有一個post過濾器SendErrorFilter用來處理一場信息的?根據正常的處理流程,該過濾器會處理異常信息,那么這里沒有出現任何異常信息說明很有可能就是這個過濾器沒有執行。所以看看SendErrorFiltershouldFilter函數:

@Override
public boolean shouldFilter() {
    RequestContext ctx = RequestContext.getCurrentContext();
    // only forward to errorPath if it hasn't been forwarded to already
    return ctx.containsKey("error.status_code")
            && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
}

可以看到,該方法的返回值中有一個重要的判斷依據ctx.containsKey("error.status_code"),也就是說請求上下文必須有error.status_code參數,我們實現的ThrowExceptionFilter中沒有設置這個參數,所以自然不會進入SendErrorFilter過濾器的處理邏輯。那么如何使用這個參數呢?可以看看route類型的幾個過濾器,由于這些過濾器會對外發起請求,所以肯定有異常需要處理,比如RibbonRoutingFilter的run方法實現如下:

@Override
public Object run() {
    RequestContext context = RequestContext.getCurrentContext();
    this.helper.addIgnoredHeaders();
    try {
        RibbonCommandContext commandContext = buildCommandContext(context);
        ClientHttpResponse response = forward(commandContext);
        setResponse(response);
        return response;
    }
    catch (ZuulException ex) {
            context.set(ERROR_STATUS_CODE, ex.nStatusCode);
            context.set("error.message", ex.errorCause);
            context.set("error.exception", ex);
    }
    catch (Exception ex) {
        context.set("error.status_code",
            HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        context.set("error.exception", ex);
    }
    return null;
}

可以看到,整個發起請求的邏輯都采用了try-catch塊處理。在catch異常的處理邏輯中并沒有任何輸出操作,而是向請求中添加了一些error相關的參數,主要有下面的三個參數。

error.status_code:錯誤代碼
error.exception:Exception異常信息
error.message:錯誤信息

error.status_code就是SendErrorFilter過濾器用來判斷是否需要執行的重要參數。可以改造一下我們的ThrowExceptionFilter的run方法,

改造ThrowExceptionFilter的run方法之后:

@Override
 public Object run() {
        logger.info("this is a pre filter,it will throw a RuntimeException");
        RequestContext context = RequestContext.getCurrentContext();
        try{
            doSomething();
        }catch (Exception e){
            context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            context.set("error.message",e.getMessage());
            context.set("error.exception", e);
      }

        return null;
}

書中有個錯context.set("error.message",e);這邊在SendErrorFilter的時候就會類型轉換報錯。

此時,異常信息已經被SendErrorFilter過濾器正常處理并返回給客戶端了,同時在網關的控制臺中也輸出了異常信息。從返回的響應信息中,可以看到幾個之前我們在請求上下文中設置的內容,

  • status:對應error.status_code參數值
  • exception:對應error.exception參數中Exception的類型
  • message:對應error.exception參數中Exception的message信息。

再去訪問http://192.168.1.57:6069/user-service/user/index,頁面顯示錯誤的頁面。

ErrorFilter處理

通過上面的分析與實驗,我們已經知道如何在過濾器中正確的處理異常,讓錯誤信息能夠順利地流轉到SendErrorFilter過濾器來組織和輸出。但是,我們可以在過濾器中使用try-catch來處理業務邏輯并向請求上下文中添加異常信息,但是不可控的人為因素,意外的程序因素等,依然會使得一些異常從過濾器中拋出,怎樣處理呢?

我們使用error類型的過濾器,在請求的生命周期的pre,route,post三個階段中有異常拋出的時候都會進入error階段的處理,所以可以通過創建一個error類型的過濾器來捕獲這些異常信息,并根據這些異常信息在請求上下文中注入需要返回給客戶端的錯誤描述。這里我們可以直接沿用try-catch處理異常信息時用的那些error參數,這樣就可以讓這些信息被SendErrorFilter捕獲并組織成響應消息返回給客戶端。

@Component
public class ErrorFilter extends ZuulFilter{

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public String filterType() {
        return "error";
    }

    @Override
    public int filterOrder() {
        return 10;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        Throwable throwable = context.getThrowable();
        logger.error("this is a ErrorFilter :{}",throwable.getCause().getMessage());
        context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        context.set("error.message",throwable.getCause().getMessage());
        return null;
    }
}

將上面的ThrowExceptionFilter過濾器不使用try...catch來處理,還是直接throw異常出去,這樣ErrorFilter過濾器就能接收到拋出的異常,并且能將其流轉到SendErrorFilter進行處理。(原因在于pre類型的過濾器流轉到error類型的過濾器最后還是要流轉到post類型的過濾器,之后會講到)

@Override
public Object run() {
        logger.info("this is a pre filter,it will throw a RuntimeException");
        doSomething();
        return null;
 }

訪問http://192.168.5.5:6069/users/user/index還是可以將異常和狀態碼打印在頁面上。

不足與優化

我們已經掌握了核心過濾器處理邏輯之下,對自定義過濾器中處理邏輯的兩種基本解決方法:一種是通過在各個階段的過濾器中增加try..catch塊,實現過濾器的內部處理;另外一種利用error類型過濾器的生命周期特性,集中處理pre,route,post階段拋出的異常信息。通常情況下,我們可以將這二種手段同時使用,其中第一種是對開發人員的基本要求,第二種是對第一種處理方式的補充,防止意外的異常拋出。

還是有一些不足,看看外部請求到達api網關服務之后,各個階段的過濾器是如何進行調度的,我們看com.netflix.zuul.http.ZuulServlet的service方法實現,定義了zuul處理外部請求過程,各個類型的過濾器的執行邏輯。代碼中可以看到3個try...catch塊,依次代表了preroutepost三個階段的過濾器調用。在catch的異常處理中我們可以看到它們都會被error過濾器進行處理(之前使用error過濾器來定義統一的異常處理也正是利用了這個特性);error類型的過濾器處理完畢后,處理來自post階段的異常外,都會在被post過濾器進行處理,

@Override
    public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        try {
            init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

            // Marks this request as having passed through the "Zuul engine", as opposed to servlets
            // explicitly bound in web.xml, for which requests will not have the same data attached
            RequestContext context = RequestContext.getCurrentContext();
            context.setZuulEngineRan();

            try {
                preRoute();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                route();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                postRoute();
            } catch (ZuulException e) {
                error(e);
                return;
            }

        } catch (Throwable e) {
            error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
        } finally {
            RequestContext.getCurrentContext().unset();
        }
    } 

對于從post過濾器中拋出的異常的情況,在經過error過濾器之后,就沒有其他類型的過濾器來接手了,回想之前實現的二種異常處理方法,其中非常核心的一點是,這兩種處理方法都在異常處理時向請求上下文添加了一系列的error.*參數,而這些參數真正起作用的地方是在post階段的SendErrorFilter,在該過濾器中會使用這些參數來組織內容返回給客戶端。而對于post階段拋出的異常的情況,由error過濾器處理之后并不會再調用post階段的請求,自然這些error.*參數也就不會被SendErrorFilter消費輸出。我們在自定義post過濾器的時候,沒有正確處理異常,就依然有可能出現日志中沒有異常但請求響應內容為空的問題。可以將之前的ThrowExceptionFilter的filterType改為post來驗證這個問題的存在,

@Component
public class ThrowExceptionFilter extends ZuulFilter{

    private static Logger logger = LoggerFactory.getLogger(ThrowExceptionFilter.class);

    @Override
    public String filterType() {
        return "post";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        logger.info("this is a pre filter,it will throw a RuntimeException");
        doSomething();
        return null;
    }

    private void doSomething(){
        throw new RuntimeException("exist some errors....");
    }
}

訪問:http://192.168.5.5:6069/users/user/index再去訪問就會出現白板不拋異常的情況。

解決這個問題的方法有很多種,最直接的我們可以在實現error過濾器的時候,直接組織結果返回就能實現效果。缺點很明顯,對于錯誤信息組織和返回代碼實現會存在多份,不利于維護,我們希望將post過濾器拋出的異常交給SendErrorFilter來處理。

我們在之前實現了一個ErrorFilter來捕獲pre,route,post過濾器拋出的異常,并組織error.*參數保存到請求的上下文。由于我們的目標是沿用SendErrorFilter,這些error.*參數依然對我們有用,所以可以繼續沿用該過濾器,讓它在post過濾器拋出異常的時候,繼續組織error.*參數,只是這里我們已經無法將這些error.*參數傳遞給SendErrorFilter過濾器來處理了。所以,我們需要在ErrorFilter過濾器之后再定義一個error類型的過濾器,讓它來實現SendErrorFilter的功能,但是這個error過濾器并不需要處理所有出現異常的情況,它僅僅處理post過濾器拋出的異常,復用它的run方法,然后重寫它的類型,順序及執行條件,實現對原有邏輯的復用

public class ErrorExtFilter extends SendErrorFilter{

    @Override
    public String filterType() {
        return "error";
    }

    @Override
    public int filterOrder() {
        return 30; //大于ErrorFilter的值
    }


    //只處理post過濾器拋出異常的過濾器
    @Override
    public boolean shouldFilter() {
        return true;
    }
}

如何實現shouldFilter的邏輯呢?當有異常拋出的時候,記錄下拋出的過濾器,這樣我們就可以在ErrorExtFilter過濾器的shouldFilter方法中獲取并以此判斷異常是否來自于post階段的過濾器了。

為了擴展過濾器的處理邏輯,為請求上下文增加一些自定義屬性,深入了解zuul過濾器的核心處理器:com.netflix.zuul.FilterProcessor,定義了過濾器調用和處理相關的核心方法:

  • getInstance:該方法用來獲取當前處理器的實例
  • setProcessor(FilterProcessor processor):該方法用來設置處理器實例,可以使用此方法來設置自定義的處理器。
  • processZuulFilter(ZuulFilter filter):該方法定義了用來執行filter的具體邏輯,包括對請求上下文的設置,判斷是否應該執行,執行時一些異常處理等。
  • runFilters(String sType):該方法會根據傳入的filterType來調用getFiltersByType(String filterType)獲取排序后的過濾器列表,然后輪詢這些過濾器,并調用processZuulFilter(ZuulFilter filter)來依次執行它們。
  • preRoute():調用runFilters("pre")來執行所有pre類型的過濾器。
  • route():調用runFilters("route")來執行所有route類型的過濾器。
  • postRoute():調用runFilters("post")來執行所有post類型的過濾器。
  • error():調用runFilters("error")來執行所有error類型的過濾器。

直接擴展processZuulFilter(ZuulFilter filter),當過濾器執行拋出異常的時候,我們來捕獲它,并向請求上下文中記錄一些信息,

public class DidiFilterProcessor extends FilterProcessor{

    @Override
    public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
        try{
            return super.processZuulFilter(filter);
        }catch (ZuulException e){
            RequestContext requestContext = RequestContext.getCurrentContext();
            requestContext.set("failed.filter",filter);
            throw e;
        }
    }
}

在上面的代碼實現中,創建了一個FilterProcessor的子類,并重寫了processZuulFilter(ZuulFilter filter),雖然主邏輯依然使用了父類的實現,但是在最外層,我們為其增加了異常捕獲,并在異常處理中為請求上下文添加failed.filter屬性,以存儲拋出異常的過濾器實例。在實現了這個擴展之后,我們可以完善之前的ErrorExtFiltershouldFilter()方法了,通過從請求上下文中獲取信息作出正確的判斷:

@Component
public class ErrorExtFilter extends SendErrorFilter{

    @Override
    public String filterType() {
        return "error";
    }

    @Override
    public int filterOrder() {
        return 30; //大于ErrorFilter的值
    }

    //只處理post過濾器拋出異常的過濾器
    @Override
    public boolean shouldFilter() {
        //判斷,僅處理來自post過濾器引起的異常
        RequestContext context = RequestContext.getCurrentContext();
        ZuulFilter failedFilter =(ZuulFilter)context.get("failed.filter");
        if(failedFilter != null && failedFilter.filterType().equals("post")){
            return true;
        }

        return false;

    }
}

最后,我們還要調用FilterProcessor.setProcessor(new DidiFilterProcessor());方法來啟動自定義的核心處理器。

@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
    public static void main(String[] args) {
        FilterProcessor.setProcessor(new DidiFilterProcessor());
        SpringApplication.run(ZuulApplication.class,args);
}

自定義異常信息

實際應用到業務系統中,默認的錯誤信息并不符合系統設計的響應格式,那么我們就需要對返回的異常信息進行定制。對于如何定制這個錯誤信息有很多種方法可以實現。
最直接的是,可以編寫一個自定義的post過濾器來組織錯誤結果,該方法實現起來簡單粗暴,完全可以參考SendErrorFilter的實現,然后直接組織請求響應而不是forward到/error端點,只是使用該方法時需要注意:為了替代SendErrorFilter,還需要禁用SendErrorFilter過濾器(下面提到怎么禁用zuul的filter)。

demo
寫的很隨意的一個過濾器,參考SendErrorFilter和SendResponseFilter過濾器:

@Component
public class SendNewErrorFilter extends ZuulFilter{

    private Logger log = LoggerFactory.getLogger(getClass());

    protected static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran";

    @Override
    public String filterType() {
        return "post";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        // only forward to errorPath if it hasn't been forwarded to already
        return ctx.containsKey("error.status_code")
                && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
    }

    @Override
    public Object run() {
        try {
            RequestContext ctx = RequestContext.getCurrentContext();
            HttpServletRequest request = ctx.getRequest();

            HttpServletResponse servletResponse = ctx.getResponse();
            servletResponse.setCharacterEncoding("UTF-8");
            OutputStream outStream = servletResponse.getOutputStream();
            String errormessage = "error,try again later!!";
            InputStream is = new ByteArrayInputStream(errormessage.getBytes(servletResponse.getCharacterEncoding()));
            writeResponse(is,outStream);
        }
        catch (Exception ex) {
            ReflectionUtils.rethrowRuntimeException(ex);
        }
        return null;
    }


    private void writeResponse(InputStream zin, OutputStream out) throws Exception {
        byte[] bytes = new byte[1024];
        int bytesRead = -1;
        while ((bytesRead = zin.read(bytes)) != -1) {
            out.write(bytes, 0, bytesRead);
        }
    }
}

然后禁用調默認的SendErrorFilter過濾器

zuul:
  SendErrorFilter:
    post:
      disable: true
  SendResponseFilter:
    post:
      disable: true

再去訪問http://192.168.1.57:6069/user-service/user/index頁面展示自定義的異常。

方法二
如果不采用重寫過濾器的方式,依然想要使用SendErrorFilter來處理異常返回的話,我們需要如何去定制返回的結果呢?這個時候,我們的關注點就不能放在zuul的過濾器上了,因為錯誤信息的生成實際上并不是由spring cloud zuul完成的。我們在介紹SendErrorFilter的時候提到過,它會根據請求上下文保存的錯誤信息來組織一個forward到/error端點的請求來獲取錯誤響應,所以我們的擴展目標轉移到/error端點的實現。

/error端點的實現來源于Springboot的org.springframework.boot.autoconfigure.web.BasicErrorController

@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
    Map<String, Object> body = getErrorAttributes(request,
            isIncludeStackTrace(request, MediaType.ALL));
    HttpStatus status = getStatus(request);
    return new ResponseEntity<Map<String, Object>>(body, status);
}
protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
        boolean includeStackTrace) {
    RequestAttributes requestAttributes = new ServletRequestAttributes(request);
    return this.errorAttributes.getErrorAttributes(requestAttributes,
            includeStackTrace);
}

getErrorAttributes的實現默認的是DefaultErrorAttributes的實現。

從源碼中可以看到,實現非常簡單,通過getErrorAttributes方法根據請求參數組織錯誤信息的返回結果,而這里的getErrorAttributes方法會將具體組織邏輯委托給org.springframework.boot.autoconfigure.web.ErrorAttributes接口提供的
getErrorAttributes來實現。在spring boot的自動化配置機制中,默認會采用org.springframework.boot.autoconfigure.web.DefaultErrorAttributes作為該接口的實現。

再定義Error處理的自動化配置中,該接口的默認實現采用@ConditionalOnMissingBean修飾,說明DefaultErrorAttributes實例僅在沒有ErrorAttributes接口的實例時才會被創建出來使用,所以我們只需要自己編寫一個自定義的ErrorAttributes接口實現類,并創建它的實例替代這個默認實現,達到自定義錯誤信息的效果。

@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties(ResourceProperties.class)
public class ErrorMvcAutoConfiguration {

    private final ApplicationContext applicationContext;

    private final ServerProperties serverProperties;

    private final ResourceProperties resourceProperties;

    @Autowired(required = false)
    private List<ErrorViewResolver> errorViewResolvers;

    public ErrorMvcAutoConfiguration(ApplicationContext applicationContext,
            ServerProperties serverProperties, ResourceProperties resourceProperties) {
        this.applicationContext = applicationContext;
        this.serverProperties = serverProperties;
        this.resourceProperties = resourceProperties;
    }

    @Bean
    @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
    public DefaultErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes();
}

舉個例子,我們不希望將exception屬性返回給客戶端,那么就可以編寫一個自定義的實現,它可以基于DefaultErrorAttribute,然后重寫getErrorAttributes方法,從原來的結果中將exception移除即可,具體實現如下:

public class DidiErrorAttributes extends DefaultErrorAttributes{


    @Override
    public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
        Map<String,Object> result = super.getErrorAttributes(requestAttributes,includeStackTrace);
        result.put("error","missing error");
        return result;
    }
}

發現去掉四個屬性都不行,具體的細節源碼沒去研究,

最后,為了讓自定義的錯誤信息生成邏輯生效,需要在應用主類中加入如下代碼,為其創建實例代替默認的實現:

@Bean
public DefaultErrorAttributes errorAttributes(){
       return new DidiErrorAttributes();
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內容