SpringBoot logging -2

SpringBoot源码中使用的日志API为JCL(Java common log),JCL实际上只是一个日志门面,没有具体的日志功能实现。如果将SpringBoot中的spring-boot-start-logging依赖排除,可以看到启动项目时打印的是红色字体的JUL日志。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>

image.png

如果不刻意的排除spring-boot-starter-logging,SpringBoot默认引入的logback日志,SpringBoot在一站式启动过程中,会发布各种通知事件,如项目正在启动事件,准备环境事件等,logback的加载就是通过事件监听者来响应上述事件从而触发的,

触发加载的原理与SpringBoot配置文件触发加载的过程是同理的,SpringBoot引入了一些新的监听器,从spring.factories文件中就能看到具体的监听器:

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener` </pre>

其中ConfigFileApplicationListener就是加载SpringBoot配置文件的监听器,LoggingApplicationListener就是加载日志的监听器。可以看到类中onApplicationEvent方法就是处理监听到的各种事件的:

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationStartingEvent) {
            onApplicationStartingEvent((ApplicationStartingEvent) event);
        }
        else if (event instanceof ApplicationEnvironmentPreparedEvent) {
            onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
        }
        else if (event instanceof ApplicationPreparedEvent) {
            onApplicationPreparedEvent((ApplicationPreparedEvent) event);
        }
        else if (event instanceof ContextClosedEvent) {
            onContextClosedEvent((ContextClosedEvent) event);
        }
        else if (event instanceof ApplicationFailedEvent) {
            onApplicationFailedEvent();
        }
    }

ApplicationStartingEvent

    private void onApplicationStartingEvent(ApplicationStartingEvent event) {
        this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
        this.loggingSystem.beforeInitialize();
    }

LoggingSystem.get方法就是记载具体日志的过程:

    public static LoggingSystem get(ClassLoader classLoader) {
        String loggingSystemClassName = System.getProperty(SYSTEM_PROPERTY);
        if (StringUtils.hasLength(loggingSystemClassName)) {
            if (NONE.equals(loggingSystemClassName)) {
                return new NoOpLoggingSystem();
            }
            return get(classLoader, loggingSystemClassName);
        }
        LoggingSystem loggingSystem = SYSTEM_FACTORY.getLoggingSystem(classLoader);
        Assert.state(loggingSystem != null, "No suitable logging system located");
        return loggingSystem;
    }

这里其实是遍历SYSTEMS里面的entry,判断key代表的class是否存在,如果存在就把value代表的那个LoggingSystem给加载了,看下SYSTEMS里面都有啥:

private static final LoggingSystemFactory SYSTEM_FACTORY = LoggingSystemFactory.fromSpringFactories();

在spring-boot-2.7.12.jar 里的spring.factories中可以看到

# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory

这里默认添加了3个日志框架,依次是logback、log4j2和jdk的log,因为spring-boot-starter-logging默认依赖了logback,因此,logback会被初始化使用,再这里产生了实例LogbackLoggingSystem。

SYSTEM_FACTORY.getLoggingSystem(classLoader) 会依次寻找定义的日志框架Factory的类,找到第一个返回构造LoggingSystem 返回。
接下来进入到 LogbackLoggingSystem#beforeInitialize 方法

@Override
public void beforeInitialize() {
   LoggerContext loggerContext = getLoggerContext();
   if (isAlreadyInitialized(loggerContext)) {
      return;
   }
   super.beforeInitialize();
   loggerContext.getTurboFilterList().add(FILTER);
}

通过 StaticLoggerBinder.getSingleton() 创建 LoggerContext

private LoggerContext getLoggerContext() {
   ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory();
   Assert.isInstanceOf(LoggerContext.class, factory,
         String.format(
               "LoggerFactory is not a Logback LoggerContext but Logback is on "
                     + "the classpath. Either remove Logback or the competing "
                     + "implementation (%s loaded from %s). If you are using "
                     + "WebLogic you will need to add 'org.slf4j' to "
                     + "prefer-application-packages in WEB-INF/weblogic.xml",
               factory.getClass(), getLocation(factory)));
   return (LoggerContext) factory;
}
image.png

进入到静态代码块的 init 方法

void init() {
    try {
        try {
            new ContextInitializer(defaultLoggerContext).autoConfig();
        } catch (JoranException je) {
            Util.report("Failed to auto configure default logger context", je);
        }
        // logback-292
        if (!StatusUtil.contextHasStatusListener(defaultLoggerContext)) {
            StatusPrinter.printInCaseOfErrorsOrWarnings(defaultLoggerContext);
        }
        contextSelectorBinder.init(defaultLoggerContext, KEY);
        initialized = true;
    } catch (Exception t) { // see LOGBACK-1159
        Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", t);
    }
}

最终会执行下面的代码、尝试获取默认配置文件、如果不存在则通过 SPI 获取 Configurator 实现类、如果还是没有、则使用默认的配置 BasicConfigurator

public void autoConfig() throws JoranException {
    StatusListenerConfigHelper.installIfAsked(loggerContext);
    URL url = findURLOfDefaultConfigurationFile(true);
    if (url != null) {
        configureByResource(url);
    } else {
        Configurator c = EnvUtil.loadFromServiceLoader(Configurator.class);
        if (c != null) {
            try {
                c.setContext(loggerContext);
                c.configure(loggerContext);
            } catch (Exception e) {
                throw new LogbackException(String.format("Failed to initialize Configurator: %s using ServiceLoader", c != null ? c.getClass()
                                .getCanonicalName() : "null"), e);
            }
        } else {
            BasicConfigurator basicConfigurator = new BasicConfigurator();
            basicConfigurator.setContext(loggerContext);
            basicConfigurator.configure(loggerContext);
        }
    }
}

ApplicationEnvironmentPreparedEvent

开始通过LogBackLoggingSystem配置LogBack,如果配置了自定义的日志配置文件就加载自己的文件,否则加载默认的配置文件。

Logback整合完成后,SpringBoot中的JCL API打印的日志是如何转变成logback格式的呢,这里看下spring-boot-starter-logging pom中引入的依赖:

<dependencies>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.2.3</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-to-slf4j</artifactId>
      <version>2.13.3</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jul-to-slf4j</artifactId>
      <version>1.7.30</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>


这里实际上涉及到纷繁复杂的日志体系中的日志转换,大致转换链路过程就是JCL->JUL->slf4j->logback classic->logback以及log4j->slf4j->logback classic->logback

    private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
        SpringApplication springApplication = event.getSpringApplication();
        if (this.loggingSystem == null) {
            this.loggingSystem = LoggingSystem.get(springApplication.getClassLoader());
        }
        initialize(event.getEnvironment(), springApplication.getClassLoader());
    }
    protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
        getLoggingSystemProperties(environment).apply();
        this.logFile = LogFile.get(environment);
        if (this.logFile != null) {
            this.logFile.applyToSystemProperties();
        }
        this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
        initializeEarlyLoggingLevel(environment);
        initializeSystem(environment, this.loggingSystem, this.logFile);
        initializeFinalLoggingLevels(environment, this.loggingSystem);
        registerShutdownHookIfNecessary(environment, this.loggingSystem);
    }

主要就是从 environment 对象中设置相关属性值到 System 中、然后判断是否设置了 log file

initializeSystem

    private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {
        String logConfig = StringUtils.trimWhitespace(environment.getProperty(CONFIG_PROPERTY));
        try {
            LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);
            if (ignoreLogConfig(logConfig)) {
                system.initialize(initializationContext, null, logFile);
            }
            else {
                system.initialize(initializationContext, logConfig, logFile);
            }
        }
        catch (Exception ex) {
            Throwable exceptionToReport = ex;
            while (exceptionToReport != null && !(exceptionToReport instanceof FileNotFoundException)) {
                exceptionToReport = exceptionToReport.getCause();
            }
            exceptionToReport = (exceptionToReport != null) ? exceptionToReport : ex;
            // NOTE: We can't use the logger here to report the problem
            System.err.println("Logging system failed to initialize using configuration from '" + logConfig + "'");
            exceptionToReport.printStackTrace(System.err);
            throw new IllegalStateException(ex);
        }
    }

其中,

public static final String CONFIG_PROPERTY = "logging.config";

也就是我们定义的log系统的配置文件:

logging: 
  #config: classpath:logback-spring.xml
  config: classpath:logback.xml
  file.path: ${LOGGING_PATH}
  register-shutdown-hook: false

我们仍以logback为例说说具体实现。
系统定义了logback的LoggingSystem实现类(这也是Springboot的默认实现)
LogbackLoggingSystem。
关于配置文件的加载核心代码块(AbstractLoggingSystem):

    @Override
    public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
        if (StringUtils.hasLength(configLocation)) {
            initializeWithSpecificConfig(initializationContext, configLocation, logFile);
            return;
        }
        initializeWithConventions(initializationContext, logFile);
    }

这里,configLocation就是logging.config定义的配置文件,如果该文件存在,那么就直接去初始化logback,这里的配置文件名没有特定要求,只要是logback的.groovy或者.xml配置即可。但是为了与系统内部日志配置保持一致,建议用logback.xml。
如果我们没有定义logging.config配置文件,那么就去找系统默认配置文件,查找的核心代码如下:

    private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
        String config = getSelfInitializationConfig();
        if (config != null && logFile == null) {
            // self initialization has occurred, reinitialize in case of property changes
            reinitialize(initializationContext);
            return;
        }
        if (config == null) {
            config = getSpringInitializationConfig();
        }
        if (config != null) {
            loadConfiguration(initializationContext, config, logFile);
            return;
        }
        loadDefaults(initializationContext, logFile);
    }

首先是通过String config = getSelfInitializationConfig()来加载:

    protected String getSelfInitializationConfig() {
        return findConfig(getStandardConfigLocations());
    }
    private String findConfig(String[] locations) {
        for (String location : locations) {
            ClassPathResource resource = new ClassPathResource(location, this.classLoader);
            if (resource.exists()) {
                return "classpath:" + location;
            }
        }
        return null;
    }
    protected abstract String[] getStandardConfigLocations();

在LogbackLoggingSystem的实现是

    @Override
    protected String[] getStandardConfigLocations() {
        return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml" };
    }

也就是说这些命名的配置文件都可以被加载。其中,logback.xml还可以与前面说的Springboot内部log共用。
如果这些文件都不存在,那么会执行config = getSpringInitializationConfig()去继续查找配置文件:

    protected String getSpringInitializationConfig() {
        return findConfig(getSpringConfigLocations());
    }
    protected String[] getSpringConfigLocations() {
        String[] locations = getStandardConfigLocations();
        for (int i = 0; i < locations.length; i++) {
            String extension = StringUtils.getFilenameExtension(locations[i]);
            locations[i] = locations[i].substring(0, locations[i].length() - extension.length() - 1) + "-spring."
                    + extension;
        }
        return locations;
    }

也就是说,对"logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml"这些文件名处理后再查找:"logback-test-spring.groovy", "logback-test-spring.xml", "logback-spring.groovy", "logback-spring.xml",这也是为什么网上很多教程要求大家配置logback-spring.xml文件名的原因。
如果这些处理后的文件还不存在,那就继续执行loadDefaults(initializationContext, logFile)去初始化logback配置:

protected abstract void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile);

在LogbackLoggingSystem中实现如下:

    @Override
    protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {
        LoggerContext context = getLoggerContext();
        stopAndReset(context);
        boolean debug = Boolean.getBoolean("logback.debug");
        if (debug) {
            StatusListenerConfigHelper.addOnConsoleListenerInstance(context, new OnConsoleStatusListener());
        }
        LogbackConfigurator configurator = debug ? new DebugLogbackConfigurator(context)
                : new LogbackConfigurator(context);
        Environment environment = initializationContext.getEnvironment();
        context.putProperty(LoggingSystemProperties.LOG_LEVEL_PATTERN,
                environment.resolvePlaceholders("${logging.pattern.level:${LOG_LEVEL_PATTERN:%5p}}"));
        context.putProperty(LoggingSystemProperties.LOG_DATEFORMAT_PATTERN, environment.resolvePlaceholders(
                "${logging.pattern.dateformat:${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}"));
        context.putProperty(LoggingSystemProperties.ROLLING_FILE_NAME_PATTERN, environment
                .resolvePlaceholders("${logging.pattern.rolling-file-name:${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}"));
        new DefaultLogbackConfiguration(initializationContext, logFile).apply(configurator);
        context.setPackagingDataEnabled(true);
    }

initializeFinalLoggingLevels

这一步是最后设置logginglevel, 在application.properties 里定义的logging.level将在这里覆盖上面初始化好的
see https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.logging.log-levels
The following example shows potential logging settings in application.properties:

logging.level.root=warn
logging.level.org.springframework.web=debug
logging.level.org.hibernate=error

Spring Boot allows you to define logging groups in your Spring Environment. For example, here is how you could define a “tomcat” group by adding it to your application.properties:
logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat
实现代码在下面

    protected void initializeSpringBootLogging(LoggingSystem system, LogLevel springBootLogging) {
        BiConsumer<String, LogLevel> configurer = getLogLevelConfigurer(system);
        SPRING_BOOT_LOGGING_LOGGERS.getOrDefault(springBootLogging, Collections.emptyList())
            .forEach((name) -> configureLogLevel(name, springBootLogging, configurer));
    }
     */
    protected void setLogLevels(LoggingSystem system, ConfigurableEnvironment environment) {
        BiConsumer<String, LogLevel> customizer = getLogLevelConfigurer(system);
        Binder binder = Binder.get(environment);
        Map<String, LogLevel> levels = binder.bind(LOGGING_LEVEL, STRING_LOGLEVEL_MAP).orElseGet(Collections::emptyMap);
        levels.forEach((name, level) -> configureLogLevel(name, level, customizer));
    }

LoggingApplicationListener 里有定义

    private static final ConfigurationPropertyName LOGGING_LEVEL = ConfigurationPropertyName.of("logging.level");

    private static final ConfigurationPropertyName LOGGING_GROUP = ConfigurationPropertyName.of("logging.group");

这些执行完毕之后,LoggingApplicationListener也起动起来了,同事还把前面定义的内部log系统更新为刚加载的log日志系统。
经过上面这些步骤,Springboot的日志系统算是正式启动起来了。


image.png

通过上面的分析,我们可以得出以下注意事项:

  • Springboot系统中不允许引入commons-logging.jar,但是我们在使用日志系统的时候,最好使用Log logger = LogFactory.getLog(SpringApplication.class)来定义,org.apache.commons.logging.Log和org.apache.commons.logging.LogFactory便于兼容多种日志系统;
  • log4j-api.jar、log4j-to-slf4j.jar、slf4j-api.jar可以共存,但是优先级是log4j > logback;**
  • log4j只能启用2.x;
  • Logback的配置文件命名最好使用logback.xml;
  • 最好明确定义出logging.config: classpath:logback.xml;
  • 可以通过继承LoggingSystem来定义自己的LoggingSystem,通过设置System.setProperty(全类名)进行加载;
  • LoggingSystem加载完毕后,系统注册了3个单例bean:springBootLoggingSystem=LoggingSystem实例,springBootLogFile=LogFile实例,对应于logging.file.path配置,springBootLoggerGroups=LoggerGroups日志分组实例。**
  • 由于我们修订的logback配置文件的名称为logback.xml,这样会导致系统未使用spring的方式加载logback,所以在logback.xml中的属性配置就不能再使用springProperty,而直接使用logback的标签property即可。

日志组

通常,对相关日志记录器进行分组非常有用,这样,就可以统一配置这些日志记录器。例如,需要修改Tomcat相关的所有日志记录器的日志级别,却又记不住顶层包名。

在Spring环境中,可以定义日志组。例如,在application.properties中,可以定义并添加tomcat日志组:

properties
logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat

yaml

logging:
  group:
    tomcat: "org.apache.catalina,org.apache.coyote,org.apache.tomcat"

定义日志组后,可以修改这个日志组中所有日志记录器的日志级别:

properties
logging.level.tomcat=trace
yaml

logging:
  level:
    tomcat: "trace"

Spring Boot包含以下预定义日志组:

名称 日志记录器
web org.springframework.core.codec
org.springframework.http
org.springframework.web
org.springframework.boot.actuate.endpoint.web
org.springframework.boot.web.servlet.ServletContextInitializerBeans
sql org.springframework.jdbc.core
org.hibernate.SQL
org.jooq.tools.LoggerListener

如何用log4j替换logback?

有时候,我们需要使用log4j而不是logback,那我们该如何做呢?

  • 根据上面的分析,我们清楚地知道,在classpath中存在log4j-api.jar并且不存在slf4j-api.jar时,第一阶段就会启用log4j日志系统,但是只能是启用2.x版;
  • 在第二阶段,加载LoggingSystem时有如下代码
    public static LoggingSystem get(ClassLoader classLoader) {
        String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
        if (StringUtils.hasLength(loggingSystem)) {
            if (NONE.equals(loggingSystem)) {
                return new NoOpLoggingSystem();
            }
            return get(classLoader, loggingSystem);
        }
        return SYSTEMS.entrySet().stream().filter((entry) -> ClassUtils.isPresent(entry.getKey(), classLoader))
                .map((entry) -> get(classLoader, entry.getValue())).findFirst()
                .orElseThrow(() -> new IllegalStateException("No suitable logging system located"));
    }

其中,SYSTEM_PROPERTY=LoggingSystem.class.getName(),只要loggingSystem存在,系统就会加载该LoggingSystem,而不会再去classpath去查找其他信息了。而log4j对应的LoggingSystem是org.springframework.boot.logging.log4j2.Log4J2LoggingSystem,因此,我们只需要在main启动的时候,添加对应的属性值即可:

    public static void main(String[] args) throws Exception {
        System.setProperty("org.springframework.boot.logging.LoggingSystem", 
                "org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");

        configureApplication(new SpringApplicationBuilder()).run(args);
    }

  • 注意配置log的配置文件:logging.config: classpath:log4j.properties,另外注意引入log4j的依赖jar。
    这样,Springboot的默认logback日志系统就会被log4j日志系统所替代。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,710评论 4 376
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,839评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,295评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,776评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,198评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,074评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,200评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,986评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,733评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,877评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,348评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,675评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,393评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,209评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,212评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,003评论 2 280

推荐阅读更多精彩内容