探究SLF4J日志框架的原理和使用

介紹

最近發(fā)現(xiàn)項(xiàng)目中使用log4j和SLF4J,打算研究下SLF4J。SLF4J用作各種日志框架的簡(jiǎn)單外觀或抽象弛矛,例如java.util.logging,logback和log4j比然。SLF4J允許最終用戶在部署時(shí)插入所需的日志記錄框架丈氓。
簡(jiǎn)單來說就是slf4j本身只是提供日志的外觀,而slf4j并不提供具體的日志實(shí)現(xiàn)
slf4j用戶手冊(cè):
https://www.slf4j.org/manual.html

SLF4J不提供具體實(shí)現(xiàn)

導(dǎo)入jar包: slf4j-api-1.7.7.jar

public class Slf4jTest {
    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(Slf4jTest.class);
        logger.info("HelloWorld");
    }
}
1.png

SLF本身不提供日志具體實(shí)現(xiàn)万俗,本身也是無法打印具體日志的湾笛,需要配合其他具體的日志框架實(shí)現(xiàn)。通過上面代碼可以看出如果我們不指定具體的日志框架實(shí)現(xiàn)闰歪,那么SLF4J就無法調(diào)用具體日志框架嚎研。

完整例子

導(dǎo)包: slf4j-log4j12-1.6.2.jar 、slf4j-api-1.7.7.jar 库倘、log4j-1.2.17.jar
log4j.properties

############# 日志輸出到控制臺(tái) #############
# 通過根元素指定日志輸出的級(jí)別临扮、目的地
# 日志輸出的優(yōu)先級(jí):  debug < info < warn < error
log4j.rootLogger=INFO,Console,File
# 日志輸出到控制臺(tái)使用的api類
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.Target=System.out
# 指定日志輸出的格式:靈活的格式
log4j.appender.Console.layout = org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%d %p %c.%M()-%m%n

############# 日志輸出到文件 #############
log4j.appender.dailyRollingFile = org.apache.log4j.DailyRollingFileAppender
# 當(dāng)前日志信息追加到文件的末尾
log4j.appender.File = org.apache.log4j.RollingFileAppender
# 文件參數(shù),指定日志文件的路徑(此處是輸出到E:/log/myLog.log)
log4j.appender.File.File = E:/log/myLog.log
# 文件參數(shù)教翩,指定日志文件的最大大小
log4j.appender.File.MaxFileSize = 10MB
log4j.appender.File.Threshold = ALL
log4j.appender.File.layout = org.apache.log4j.PatternLayout
log4j.appender.File.layout.ConversionPattern =[%p] [%d{yyyy-MM-dd HH\:mm\:ss}][%c]%m%n
public class LoggerTest {
    
    public static void main(String[] args) throws Exception {
        Logger logger = LoggerFactory.getLogger(Object.class);
        logger.info("sunpy");
    }
}
1.png

SLF4J源碼分析

public static Logger getLogger(Class<?> clazz) {
    // 獲取對(duì)應(yīng)的Logger日志器
    Logger logger = getLogger(clazz.getName());
    // 檢查日志器的名稱是否匹配
    if (DETECT_LOGGER_NAME_MISMATCH) {
        Class<?> autoComputedCallingClass = Util.getCallingClass();
        if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
            Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
                            autoComputedCallingClass.getName()));
            Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
        }
    }
    return logger;
}

public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
}

getILoggerFactory方法

public static ILoggerFactory getILoggerFactory() {
    // INITIALIZATION_STATE初始化值為UNINITIALIZED
    if (INITIALIZATION_STATE == UNINITIALIZED) {
        // 將LoggerFactory上鎖
        synchronized (LoggerFactory.class) {
            if (INITIALIZATION_STATE == UNINITIALIZED) {
                // 將初始化狀態(tài)設(shè)置為進(jìn)行中初始化狀態(tài)
                INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                // 執(zhí)行初始化
                performInitialization();
            }
        }
    }
    switch (INITIALIZATION_STATE) {
    case SUCCESSFUL_INITIALIZATION:
        return StaticLoggerBinder.getSingleton().getLoggerFactory();
    case NOP_FALLBACK_INITIALIZATION:
        return NOP_FALLBACK_FACTORY;
    case FAILED_INITIALIZATION:
        throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
    case ONGOING_INITIALIZATION:
        // support re-entrant behavior.
        // See also http://jira.qos.ch/browse/SLF4J-97
        return SUBST_FACTORY;
    }
    throw new IllegalStateException("Unreachable code");
}

說明:判斷當(dāng)前初始化狀態(tài)是否為未初始化狀態(tài)杆勇,如果未初始化就將狀態(tài)修改為當(dāng)前正在初始化中,執(zhí)行初始化方法performInitialization()迂曲。

// 執(zhí)行初始化操作
private final static void performInitialization() {
    // 綁定
    bind();
    // 如果初始化狀態(tài)等于成功初始化狀態(tài)
    if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
        // 執(zhí)行版本安全檢查
        versionSanityCheck();
    }
}

versionSanityCheck方法

// 版本號(hào)檢查靶橱,如果不滿足1.6.x或者1.7.x的版本,將打印版本不匹配的錯(cuò)誤流信息
private final static void versionSanityCheck() {
    try {
        // public static String REQUESTED_API_VERSION = "1.6.99";
        // requested請(qǐng)求版本默認(rèn)為1.6.99
        String requested = StaticLoggerBinder.REQUESTED_API_VERSION;

        boolean match = false;
        
        // static private final String[] API_COMPATIBILITY_LIST = new String[] { "1.6", "1.7" };
        // 遍歷API_COMPATIBILITY_LIST
        for (String aAPI_COMPATIBILITY_LIST : API_COMPATIBILITY_LIST) {
            // 如果requested請(qǐng)求版本為1.6.x或者1.7.x路捧,那么就將是否匹配的標(biāo)識(shí)設(shè)置為true
            if (requested.startsWith(aAPI_COMPATIBILITY_LIST)) {
                match = true;
            }
        }
        
        // 如果標(biāo)識(shí)不匹配关霸,那么就打印SLF4J錯(cuò)誤:版本不匹配請(qǐng)滿足1.6.x或者1.7.x的版本
        if (!match) {
            Util.report("The requested version " + requested + " by your slf4j binding is not compatible with "
                            + Arrays.asList(API_COMPATIBILITY_LIST).toString());
            Util.report("See " + VERSION_MISMATCH + " for further details.");
        }
    } catch (java.lang.NoSuchFieldError nsfe) {
        // given our large user base and SLF4J's commitment to backward
        // compatibility, we cannot cry here. Only for implementations
        // which willingly declare a REQUESTED_API_VERSION field do we
        // emit compatibility warnings.
    } catch (Throwable e) {
        // we should never reach here
        Util.report("Unexpected problem occured during version sanity check", e);
    }
}

說明:主要就是版本號(hào)安全檢查,如果版本號(hào)不滿足1.6.x或者1.7.x版本杰扫,那么就打印版本不匹配錯(cuò)誤流信息队寇。

關(guān)鍵源碼設(shè)計(jì)

bind方法:綁定日志工廠

// 綁定
private final static void bind() {
    try {
      // 查找多個(gè)可能出現(xiàn)的StaticLoggerBinder類。
      Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
      // 校驗(yàn)設(shè)置的多個(gè)StaticLoggerBinder類
      reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
      // 創(chuàng)建一個(gè)默認(rèn)的Log4jLoggerFactory工廠
      StaticLoggerBinder.getSingleton();
      // 更新初始化狀態(tài)為成功初始化狀態(tài)
      INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
      // 報(bào)告實(shí)際上加入的多個(gè)StaticLoggerBinder的日志類型章姓。
      reportActualBinding(staticLoggerBinderPathSet);
      // 使用SLF4j提供的SubstituteLoggerFactory工廠
      fixSubstitutedLoggers();
    } catch (NoClassDefFoundError ncde) {
      String msg = ncde.getMessage();
      if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
        INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
        Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
        Util.report("Defaulting to no-operation (NOP) logger implementation");
        Util.report("See " + NO_STATICLOGGERBINDER_URL
                + " for further details.");
      } else {
        failedBinding(ncde);
        throw ncde;
      }
    } catch (java.lang.NoSuchMethodError nsme) {
      String msg = nsme.getMessage();
      if (msg != null && msg.indexOf("org.slf4j.impl.StaticLoggerBinder.getSingleton()") != -1) {
        INITIALIZATION_STATE = FAILED_INITIALIZATION;
        Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
        Util.report("Your binding is version 1.5.5 or earlier.");
        Util.report("Upgrade your binding to version 1.6.x.");
      }
      throw nsme;
    } catch (Exception e) {
      failedBinding(e);
      throw new IllegalStateException("Unexpected initialization failure", e);
    }
  }

findPossibleStaticLoggerBinderPathSet方法:查找多個(gè)可能出現(xiàn)的StaticLoggerBinder類佳遣。

static Set<URL> findPossibleStaticLoggerBinderPathSet() {
    Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
    try {
        // 獲取類加載器
        ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
        Enumeration<URL> paths;
        // 當(dāng)前類加載器為啟動(dòng)類加載器
        if (loggerFactoryClassLoader == null) {
            // 查找路徑"org/slf4j/impl/StaticLoggerBinder.class"上的資源URL
            paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
        } else { // 當(dāng)前類加載器為非啟動(dòng)類加載器
            // 查找路徑"org/slf4j/impl/StaticLoggerBinder.class"上的資源URL
            paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
        }
        // 遍歷枚舉集合
        while (paths.hasMoreElements()) {
            // 獲取對(duì)應(yīng)的URL資源
            URL path = paths.nextElement();
            // 將URL放入集合LinkedHashSet中
            staticLoggerBinderPathSet.add(path);
        }
    } catch (IOException ioe) {
        Util.report("Error getting resources from path", ioe);
    }
    return staticLoggerBinderPathSet;
}

說明:使用ClassLoader類加載器獲取類org.slf4j.impl.StaticLoggerBinder.class,將返回可能多個(gè)對(duì)應(yīng)的URL類型的Set集合凡伊。


reportMultipleBindingAmbiguity方法:校驗(yàn)設(shè)置的多個(gè)StaticLoggerBinder類

private static void reportMultipleBindingAmbiguity(Set<URL> staticLoggerBinderPathSet) {
    // Set集合staticLoggerBinderPathSet中找到大于1個(gè)StaticLoggerBinder對(duì)應(yīng)的URL類
    if (isAmbiguousStaticLoggerBinderPathSet(staticLoggerBinderPathSet)) {
      // 打印錯(cuò)誤流信息:類路徑中包含多個(gè)SLF4J框架綁定
      Util.report("Class path contains multiple SLF4J bindings.");
      Iterator<URL> iterator = staticLoggerBinderPathSet.iterator();
      // 遍歷Set集合
      while (iterator.hasNext()) {
        // 獲取遍歷到的資源URL
        URL path = (URL) iterator.next();
        // 打印錯(cuò)誤流信息:發(fā)現(xiàn)綁定路徑有零渐。。系忙。
        Util.report("Found binding in [" + path + "]");
      }
      Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
    }
}

說明:校驗(yàn)Set集合中出現(xiàn)多個(gè)StaticLoggerBinder類對(duì)應(yīng)的訪問資源URL類時(shí)诵盼,打印錯(cuò)誤信息流。


StaticLoggerBinder.getSingleton():?jiǎn)卫J皆O(shè)計(jì)
針對(duì)上面的StaticLoggerBinder出現(xiàn)多個(gè)的情況银还,那么SLF4J中是不希望出現(xiàn)多個(gè)StaticLoggerBinder风宁,使用單例設(shè)計(jì)模式來防止產(chǎn)生多個(gè)對(duì)象的問題。

public class StaticLoggerBinder implements LoggerFactoryBinder {

  // 創(chuàng)建唯一StaticLoggerBinder實(shí)例SINGLETON
  private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();

  // 對(duì)外提供獲取單個(gè)實(shí)例的靜態(tài)方法
  public static final StaticLoggerBinder getSingleton() {
    return SINGLETON;
  }

  // 私有構(gòu)造器蛹疯,防止外部new創(chuàng)建StaticLoggerBinder實(shí)例
  private StaticLoggerBinder() {
    // 創(chuàng)建一個(gè)Log4jLoggerFactory工廠類
    loggerFactory = new Log4jLoggerFactory();
    try {
      Level level = Level.TRACE;
    } catch (NoSuchFieldError nsfe) {
      Util
          .report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version");
    }
  }
  
  戒财。。捺弦。
}

說明:對(duì)于SLF4J日志門面對(duì)應(yīng)多個(gè)日志實(shí)現(xiàn)類中的StaticLoggerBinder 饮寞,SLF4J采用單例設(shè)計(jì)模式來實(shí)現(xiàn)的孝扛,采用餓漢式實(shí)現(xiàn)的。 --> 我早期博客:
創(chuàng)建型模式之單例設(shè)計(jì)模式(java版)


reportActualBinding方法:報(bào)告實(shí)際上加入的多個(gè)StaticLoggerBinder的日志類型骂际。

// 提示實(shí)際綁定的StaticLoggerBinder
private static void reportActualBinding(Set<URL> staticLoggerBinderPathSet) {
    // staticLoggerBinderPathSet集合中元素個(gè)數(shù)大于1
    if (isAmbiguousStaticLoggerBinderPathSet(staticLoggerBinderPathSet)) {
      // 打印錯(cuò)誤信息流: 實(shí)際綁定日志類型為L(zhǎng)og4j
      Util.report("Actual binding is of type ["+StaticLoggerBinder.getSingleton().getLoggerFactoryClassStr()+"]");
    }
}

SubstituteLoggerFactory工廠方法模式設(shè)計(jì)

public class SubstituteLoggerFactory implements ILoggerFactory {

  final ConcurrentMap<String, SubstituteLogger> loggers = new ConcurrentHashMap<String, SubstituteLogger>();

  public Logger getLogger(String name) {
    // 獲取代理Logger
    SubstituteLogger logger = loggers.get(name);
    // 如果logger為null
    if (logger == null) {
      // 直接創(chuàng)建SubstituteLogger
      logger = new SubstituteLogger(name);
      // 如果集合loggers包含name鍵疗琉,那么直接獲取對(duì)應(yīng)的logger冈欢,返回對(duì)應(yīng)的logger
      // 如果集合loggers不包含name鍵歉铝,那么設(shè)置到loggers中,返回oldLogger為null
      SubstituteLogger oldLogger = loggers.putIfAbsent(name, logger);
      // 如果返回oldLogger不為null
      if (oldLogger != null)
        logger = oldLogger;
    }
    return logger;
  }

  public List<String> getLoggerNames() {
    return new ArrayList<String>(loggers.keySet());
  }

  public List<SubstituteLogger> getLoggers() {
    return new ArrayList<SubstituteLogger>(loggers.values());
  }

  public void clear() {
    loggers.clear();
  }
}

說明:實(shí)際上SubstituteLoggerFactory工廠實(shí)現(xiàn)了日志抽象工廠ILoggerFactory凑耻,實(shí)現(xiàn)了getLogger方法來獲取創(chuàng)建的SubstituteLogger太示。思考:如果我們想創(chuàng)建一個(gè)自定義的日志類XXXLogger,那么我們直接就可以創(chuàng)建一個(gè)XXXLoggerFactory工廠實(shí)現(xiàn)ILoggerFactory接口香浩,加入我們需要的邏輯到實(shí)現(xiàn)的getLogger方法中类缤。
getLogger方法:如果我們沒有找到默認(rèn)的Log4j,那么就使用SubstituteLogger來代替邻吭。


fixSubstitutedLoggers方法:使用SLF4J提供的日志SubstitutedLogger(代理Logger)實(shí)現(xiàn)

private final static void fixSubstitutedLoggers() {
    // 獲取所有的SubstituteLogger
    List<SubstituteLogger> loggers = TEMP_FACTORY.getLoggers();
    // 如果ArrayList集合loggers為空餐弱,直接退出方法
    if(loggers.isEmpty()){
      return;
    }

    Util.report("The following set of substitute loggers may have been accessed");
    Util.report("during the initialization phase. Logging calls during this");
    Util.report("phase were not honored. However, subsequent logging calls to these");
    Util.report("loggers will work as normally expected.");
    Util.report("See also " + SUBSTITUTE_LOGGER_URL);
    // 遍歷loggers集合
    for(SubstituteLogger subLogger : loggers){
      // 通過名稱獲取對(duì)應(yīng)的logger俘枫,然后設(shè)置委托類
      subLogger.setDelegate(getLogger(subLogger.getName()));
      Util.report(subLogger.getName());
    }

    TEMP_FACTORY.clear();
  }

Log4jLoggerFactory工廠解析

前面大概說了下SLF4J中獲取Logger的流程瞄摊,但是其中的一些細(xì)節(jié)需要再深入思考下,譬如我們前面提到了loggerFactory首先是使用Log4j的工廠Log4jLoggerFactory來進(jìn)行初始化的兼呵。下面主要講下Log4j的工廠Log4jLoggerFactory畸写。

總結(jié)

前面大概分析了下SLF4J中獲取Logger的流程驮瞧;
1. 如果SLF4J為未初始化將進(jìn)行初始化,其初始化默認(rèn)使用Log4j的工廠Log4jLoggerFactory枯芬。如果找不到將使用SLF4J的SubstituteLoggerFactory工廠论笔。
2. SLF4J使用ClassLoader類加載器來加載指定路徑資源。
3. 對(duì)于多個(gè)StaticLoggerBinder千所,其獲取采用餓漢式-單例設(shè)計(jì)模式來設(shè)計(jì)的狂魔。
4. SubstituteLoggerFactory工廠類設(shè)計(jì),采用工廠方法模式來設(shè)計(jì)的淫痰。

思考

我們前面提到了loggerFactory首先是使用Log4j的工廠Log4jLoggerFactory來進(jìn)行初始化的最楷。那么Log4j工廠是如何進(jìn)行初始化使用的?下篇文章再說黑界。管嬉。。Log4j的一些實(shí)現(xiàn)朗鸠。

最后編輯于
?著作權(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
  • 我被黑心中介騙來泰國打工迁筛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留煤蚌,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,287評(píng)論 3 379
  • 正文 我出身青樓细卧,卻偏偏與公主長(zhǎng)得像尉桩,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子贪庙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,860評(píng)論 2 361