java代码动态自定义logback日志Appender

Java 程序中使用 Logback,需要依赖三个 jar 包,分别是 slf4j-api,logback-core,logback-classic,在 maven 项目中依赖如下:


<pre style="overflow: auto; margin: 0px; padding: 0px; list-style-type: none; list-style-image: none; font-family: &quot;Courier New&quot; !important; overflow-wrap: break-word; font-size: 12px !important;"><!-- springboot项目默认了logback的依赖,无需手动添加 -->
 <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.5</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.0.11</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.0.11</version>
</dependency></pre>

Logback 在启动时,根据以下步骤寻找配置文件:

1. 在 classpath 中寻找 logback-test.xml文件;

2. 如果找不到 logback-test.xml,则在 classpath 中寻找 logback.groovy 文件;

3. 如果找不到 logback.groovy,则在 classpath 中寻找 logback.xml文件;

4. 如果上述的文件都找不到,则 logback 会使用 JDK 的 SPI 机制查找 META-INF/services/ch.qos.logback.classic.spi.Configurator 中的 logback 配置实现类, 这个实现类必须实现 Configuration 接口,使用它的实现来进行配置;

5. 如果上述操作都不成功,logback 就会使用它自带的 BasicConfigurator 来配置,并将日志输出到 console;

logback的变量作用于有三种:local,context,system

1. local 作用域在配置文件内有效;

2. context 作用域的有效范围延伸至 logger context;

3. system 作用域的范围最广,整个 JVM 内都有效;

logback 在替换变量时,首先搜索 local 变量,然后搜索 context,然后搜索 system,在spring项目中,应将变量的作用域设置为context,并交给spring控制


<pre style="overflow: auto; margin: 0px; padding: 0px; list-style-type: none; list-style-image: none; font-family: &quot;Courier New&quot; !important; overflow-wrap: break-word; font-size: 12px !important;">## application.yml文件配置
spring:
  profiles:
    active: dev
  application:
    name: msg-consumer
logging:
  ## 自定义logback配置文件名,交给spring
  config: classpath:logback-custom.xml
logback:
  ## 在配置文件中指定日志路径
  logHome: logs</pre>


<pre style="overflow: auto; margin: 0px; padding: 0px; list-style-type: none; list-style-image: none; font-family: &quot;Courier New&quot; !important; overflow-wrap: break-word; font-size: 12px !important;"><!-- logback-custom.xml文件配置,放在resources目录下,与application.yml同级 -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- logback.xml和logback-test.xml会被logback组件直接读取 -->
    <!-- 如果要交给spring管理,需要修改配置文件名为logback-spring.xml -->
    <!-- springProfile标签可以为不同的环境使用不同的配置,设置scope="context",则在项目上下文中可以使用该变量 -->
    <springProperty scope="context" name="LOG_HOME" source="logback.logHome" defaultValue="log"/>
    <springProperty scope="context" name="LOG_NAME_PREFIX" source="spring.application.name" defaultValue=""/>
    <!-- %m输出的信息,%p日志级别,%t线程名,%d日期,%c类的全名,%i索引【从数字0开始递增】,,, -->
    <property scope="context" name="pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %level %logger{35}:%line - %msg%n"/>
    <timestamp scope="context" key="bySecond" datePattern="yyyyMMddHHmmss"/>
    <property scope="context" name="logPath" value="${LOG_HOME}/${LOG_NAME_PREFIX}"/>
    <!-- appender是configuration的子节点,是负责写日志的组件。 -->
    <!-- ch.qos.logback.core.ConsoleAppender:把日志输出到控制台 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!-- pattern节点,用来设置日志的输入格式 -->
            <pattern>${pattern}</pattern>
            <!-- 记录日志的编码 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>
    <!-- RollingFileAppender:滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->
    <appender name="ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>${logPath}-all.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 活动文件的名字会根据fileNamePattern的值,每隔一段时间改变一次,文件名:logger/sys.2020-03-28.0.logger -->
            <fileNamePattern>${logPath}/${LOG_NAME_PREFIX}-all.%d.%i.log</fileNamePattern>
            <maxFileSize>50MB</maxFileSize>
            <maxHistory>30</maxHistory>
            <totalSizeCap>2GB</totalSizeCap>
            <cleanHistoryOnStart>true</cleanHistoryOnStart>
        </rollingPolicy>
        <encoder>
            <pattern>${pattern}</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>${logPath}-info.log</File>
        <!--只输出INFO-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!--过滤 INFO-->
            <level>INFO</level>
            <!--匹配到就禁止-->
            <onMatch>ACCEPT</onMatch>
            <!--没有匹配到就允许-->
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${logPath}/${LOG_NAME_PREFIX}-info.%d.%i.log</fileNamePattern>
            <maxFileSize>50MB</maxFileSize>
            <maxHistory>30</maxHistory>
            <totalSizeCap>2GB</totalSizeCap>
            <cleanHistoryOnStart>true</cleanHistoryOnStart>
        </rollingPolicy>
        <encoder>
            <pattern>${pattern}</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>${logPath}-error.log</File>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <!--设置日志级别,过滤掉info日志,只输入error日志-->
            <level>ERROR</level>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${logPath}/${LOG_NAME_PREFIX}-error.%d.%i.log</fileNamePattern>
            <maxFileSize>50MB</maxFileSize>
            <maxHistory>30</maxHistory>
            <totalSizeCap>2GB</totalSizeCap>
            <cleanHistoryOnStart>true</cleanHistoryOnStart>
        </rollingPolicy>
        <encoder>
            <pattern>${pattern}</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 记录sql -->
    <appender name="SQL" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>${logPath}sql.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${logPath}/${LOG_NAME_PREFIX}-sql.%d.%i.log</fileNamePattern>
            <maxFileSize>50MB</maxFileSize>
            <maxHistory>30</maxHistory>
            <totalSizeCap>2GB</totalSizeCap>
            <cleanHistoryOnStart>true</cleanHistoryOnStart>
        </rollingPolicy>
        <encoder>
            <pattern>${pattern}</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 按发布环境,控制激活的日志级别 -->
    <springProfile name="dev,test">
        <!-- 控制台输出日志级别 -->
        <root level="INFO">
            <appender-ref ref="STDOUT"/>
            <appender-ref ref="ALL"/>
        </root>

        <!-- 定项目中某个包,eg:cn.henry.study为根包,也就是只要是发生在这个根包下面的所有日志操作行为的权限都是INFO -->
        <!-- 级别依次为【从高到低】:FATAL > ERROR > WARN > INFO > DEBUG > TRACE -->
        <logger name="cn.henry.study" level="INFO" additivity="false">
            <appender-ref ref="INFO"/>
            <appender-ref ref="ERROR"/>
            <appender-ref ref="STDOUT"/>
        </logger>

        <!-- mybatis loggers 可以按包的层级指定不同的日志级别 -->
        <logger name="cn.henry.study.web.mapper" level="DEBUG" additivity="false">
            <appender-ref ref="SQL"/>
            <appender-ref ref="STDOUT"/>
        </logger>
    </springProfile>

    <springProfile name="pro">
        <root level="INFO">
            <appender-ref ref="SERVICE_ALL"/>
            <appender-ref ref="STDOUT"/>
        </root>
    </springProfile>
</configuration></pre>

以上配置可满足日常开发的大部分需求,可以很方便的将info日志与error隔离开,并按照给定logger输出不同配置文件中,但存在以下问题:

1. 如果需要按照业务,将某些不同包下的日志,集中输出到指定的日志文件中,上述配置就难以实现;

2. 上述xml文件会产生大量重复配置,如appender和logger的配置,添加非常的繁琐,造成配置文件庞大;

解决方案:

1. 通过logback的SiftingAppender,通过ThreadLocal的方式动态切换,这个方案在我之前的博客中有详细实现,与业务耦合较高;

2. 在java代码中动态生成Appender,轻量,易拓展,实现代码如下;


import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.encoder.PatternLayoutEncoder; import ch.qos.logback.classic.filter.LevelFilter; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.Appender; import ch.qos.logback.core.ConsoleAppender; import ch.qos.logback.core.FileAppender; import ch.qos.logback.core.filter.Filter; import ch.qos.logback.core.rolling.RollingFileAppender; import ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP; import ch.qos.logback.core.rolling.TimeBasedRollingPolicy; import ch.qos.logback.core.spi.FilterReply; import ch.qos.logback.core.util.FileSize; import ch.qos.logback.core.util.OptionHelper; import cn.henry.study.common.enums.LogNameEnum; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Optional; /** * description: 自定义的日志工具类,
 * 需要在logback.xml,logback-spring.xml或自定义的logback-custom.xml中写入基础配置
 *
 * @citation https://blog.csdn.net/lw656697752/article/details/84904938 * @citation https://www.cnblogs.com/leohe/p/12117183.html * @author Hlingoes
 * @date 2020/6/10 19:38 */
public class LoggerUtils { private static String consoleAppenderName = "serve-console"; private static String maxFileSize = "50MB"; private static String totalSizeCap = "10GB"; private static int maxHistory = 30; private static ConsoleAppender defaultConsoleAppender = null; static {
        Map<String, Appender<ILoggingEvent>> appenderMap = allAppenders();
        appenderMap.forEach((key, appender) -> { // 如果logback配置文件中,已存在窗口输出的appender,则直接使用;不存在则重新生成
            if (appender instanceof ConsoleAppender) {
                defaultConsoleAppender = (ConsoleAppender) appender; return;
            }
        });
    } /** * description: 获取自定义的logger日志,在指定日志文件logNameEnum.getLogName()中输出日志
     * 日志中会包括所有线程及方法堆栈信息
     *
     * @param logNameEnum
     * @param clazz
     * @return org.slf4j.Logger
     * @author Hlingoes 2020/6/10 */
    public static Logger getLogger(LogNameEnum logNameEnum, Class clazz) {
        ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(clazz);
        LoggerContext loggerContext = logger.getLoggerContext();
        RollingFileAppender errorAppender = createAppender(logNameEnum.getLogName(), Level.ERROR, loggerContext);
        RollingFileAppender infoAppender = createAppender(logNameEnum.getLogName(), Level.INFO, loggerContext);
        Optional<ConsoleAppender> consoleAppender = Optional.ofNullable(defaultConsoleAppender);
        ConsoleAppender realConsoleAppender = consoleAppender.orElse(createConsoleAppender(loggerContext)); // 设置不向上级打印信息
        logger.setAdditive(false);
        logger.addAppender(errorAppender);
        logger.addAppender(infoAppender);
        logger.addAppender(realConsoleAppender); return logger;
    } /** * description: 创建日志文件的file appender
     *
     * @param name
     * @param level
     * @return ch.qos.logback.core.rolling.RollingFileAppender
     * @author Hlingoes 2020/6/10 */
    private static RollingFileAppender createAppender(String name, Level level, LoggerContext loggerContext) {
        RollingFileAppender appender = new RollingFileAppender(); // 这里设置级别过滤器
 appender.addFilter(createLevelFilter(level)); // 设置上下文,每个logger都关联到logger上下文,默认上下文名称为default。 // 但可以使用<scope="context">设置成其他名字,用于区分不同应用程序的记录。一旦设置,不能修改。
 appender.setContext(loggerContext); // appender的name属性
        appender.setName(name.toUpperCase() + "-" + level.levelStr.toUpperCase()); // 读取logback配置文件中的属性值,设置文件名
        String logPath = OptionHelper.substVars("${logPath}-" + name + "-" + level.levelStr.toLowerCase() + ".log", loggerContext);
        appender.setFile(logPath);
        appender.setAppend(true);
        appender.setPrudent(false); // 加入下面两个节点
 appender.setRollingPolicy(createRollingPolicy(name, level, loggerContext, appender));
        appender.setEncoder(createEncoder(loggerContext));
        appender.start(); return appender;
    } /** * description: 创建窗口输入的appender
     *
     * @param * @return ch.qos.logback.core.ConsoleAppender
     * @author Hlingoes 2020/6/10 */
    private static ConsoleAppender createConsoleAppender(LoggerContext loggerContext) {
        ConsoleAppender appender = new ConsoleAppender();
        appender.setContext(loggerContext);
        appender.setName(consoleAppenderName);
        appender.addFilter(createLevelFilter(Level.DEBUG));
        appender.setEncoder(createEncoder(loggerContext));
        appender.start(); return appender;
    } /** * description: 设置日志的滚动策略
     *
     * @param name
     * @param level
     * @param context
     * @param appender
     * @return ch.qos.logback.core.rolling.TimeBasedRollingPolicy
     * @author Hlingoes 2020/6/10 */
    private static TimeBasedRollingPolicy createRollingPolicy(String name, Level level, LoggerContext context, FileAppender appender) { // 读取logback配置文件中的属性值,设置文件名
        String fp = OptionHelper.substVars("${logPath}/${LOG_NAME_PREFIX}-" + name + "-" + level.levelStr.toLowerCase() + "_%d{yyyy-MM-dd}_%i.log", context);
        TimeBasedRollingPolicy rollingPolicyBase = new TimeBasedRollingPolicy<>(); // 设置上下文,每个logger都关联到logger上下文,默认上下文名称为default。 // 但可以使用<scope="context">设置成其他名字,用于区分不同应用程序的记录。一旦设置,不能修改。
 rollingPolicyBase.setContext(context); // 设置父节点是appender
 rollingPolicyBase.setParent(appender); // 设置文件名模式
 rollingPolicyBase.setFileNamePattern(fp);
        SizeAndTimeBasedFNATP sizeAndTimeBasedFNATP = new SizeAndTimeBasedFNATP(); // 最大日志文件大小
 sizeAndTimeBasedFNATP.setMaxFileSize(FileSize.valueOf(maxFileSize));
        rollingPolicyBase.setTimeBasedFileNamingAndTriggeringPolicy(sizeAndTimeBasedFNATP); // 设置最大历史记录为30条
 rollingPolicyBase.setMaxHistory(maxHistory); // 总大小限制
 rollingPolicyBase.setTotalSizeCap(FileSize.valueOf(totalSizeCap));
        rollingPolicyBase.start(); return rollingPolicyBase;
    } /** * description: 设置日志的输出格式
     *
     * @param context
     * @return ch.qos.logback.classic.encoder.PatternLayoutEncoder
     * @author Hlingoes 2020/6/10 */
    private static PatternLayoutEncoder createEncoder(LoggerContext context) {
        PatternLayoutEncoder encoder = new PatternLayoutEncoder(); // 设置上下文,每个logger都关联到logger上下文,默认上下文名称为default。 // 但可以使用<scope="context">设置成其他名字,用于区分不同应用程序的记录。一旦设置,不能修改。
 encoder.setContext(context); // 设置格式
        String pattern = OptionHelper.substVars("${pattern}", context);
        encoder.setPattern(pattern);
        encoder.setCharset(Charset.forName("utf-8"));
        encoder.start(); return encoder;
    } /** * description: 设置打印日志的级别
     *
     * @param level
     * @return ch.qos.logback.core.filter.Filter
     * @author Hlingoes 2020/6/10 */
    private static Filter createLevelFilter(Level level) {
        LevelFilter levelFilter = new LevelFilter();
        levelFilter.setLevel(level);
        levelFilter.setOnMatch(FilterReply.ACCEPT);
        levelFilter.setOnMismatch(FilterReply.DENY);
        levelFilter.start(); return levelFilter;
    } /** * description: 读取logback配置文件中的所有appender
     *
     * @param * @return java.util.Map<java.lang.String, ch.qos.logback.core.Appender < ch.qos.logback.classic.spi.ILoggingEvent>>
     * @author Hlingoes 2020/6/10 */
    private static Map<String, Appender<ILoggingEvent>> allAppenders() {
        Map<String, Appender<ILoggingEvent>> appenderMap = new HashMap<>();
        LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); for (ch.qos.logback.classic.Logger logger : context.getLoggerList()) { for (Iterator<Appender<ILoggingEvent>> index = logger.iteratorForAppenders(); index.hasNext(); ) {
                Appender<ILoggingEvent> appender = index.next();
                appenderMap.put(appender.getName(), appender);
            }
        } return appenderMap;
    }

}

import org.apache.commons.lang3.StringUtils; /** * description: 日志枚举类,防止随意生成日志文件
 *
 * @author Hlingoes 2020/6/10 */
public enum LogNameEnum {
    COMMON("common"),
    WEB_SERVER("webServer"),
    TEST("test"),
    ; private String logName;

    LogNameEnum(String fileName) { this.logName = fileName;
    } public String getLogName() { return logName;
    } public void setLogName(String logName) { this.logName = logName;
    } /** * description: 获取枚举类
     *
     * @param value
     * @return cn.henry.study.common.enums.LogNameEnum
     * @author Hlingoes 2020/6/10 */
    public static LogNameEnum getAwardTypeEnum(String value) {
        LogNameEnum[] arr = values(); for (LogNameEnum item : arr) { if (null != item && StringUtils.isNotBlank(item.logName)) { return item;
            }
        } return null;
    }
}
import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.core.util.OptionHelper; import cn.henry.study.common.enums.LogNameEnum; import cn.henry.study.common.utils.LoggerUtils; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * description:
 *
 * @author Hlingoes
 * @date 2020/5/22 23:45 */
public class PracticeTest { private static Logger logger = LoggerFactory.getLogger(PracticeTest.class); private static Logger testLogger = LoggerUtils.getLogger(LogNameEnum.TEST, PracticeTest.class);

    @Test public void loggerUtilsTest() {
        LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); /** *  <property scope="context" name="LOG_HOME" value="log"/>
         *  <property scope="context" name="LOG_NAME_PREFIX" value="common"/> */ String oph = OptionHelper.substVars("${LOG_HOME}/${LOG_NAME_PREFIX}/test-log.log", context);
     // 在日志文件common-info.log中
        logger.info("logger默认配置的日志输出");
     // 在日志文件common-test-info.log中
        testLogger.info("testLogger#####{}####", oph);
        testLogger.info("testLogger看到这条信息就是info");
     // 在日志文件common-test-error.log中
        testLogger.error("testLogger看到这条信息就是error");
    }

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