Spring Filter深度解析

Filter的用法

public interface Filter {
    //初始化方法紫岩,整個生命周期中只執(zhí)行一次火邓。
    //在init方法成功(失敗如拋異常等)執(zhí)行完前袖订,不能提供過濾服務慰毅。
    //參數(shù)FilterConfig用于獲取初始化參數(shù)
    public void init(FilterConfig filterConfig) throws ServletException;

    //執(zhí)行過濾任務的方法隘截,參數(shù)FilterChain表示過濾器鏈,doFilter方法中只有執(zhí)行chain.doFilter()后才能調用下一個過濾器的doFilter方法
    //才能將請求交經(jīng)下一個Filter或Servlet執(zhí)行
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;

    //銷毀方法汹胃,當移出服務時由web容器調用婶芭。整個生命周期中destroy方法只會執(zhí)行一次
    //destroy方法可用于釋放持有的資源,如內存着饥、文件句柄等
    public void destroy();
}
  • Filter的接口定義包含init犀农、doFilter、destroy等接口宰掉。
@Component
public class TimeFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("time filter init");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("time filter start");
        long startTime = System.currentTimeMillis();

        filterChain.doFilter(servletRequest, servletResponse);

        long endTime = System.currentTimeMillis();
        System.out.println("time filter consume " + (endTime - startTime) + " ms");
        System.out.println("time filter end");
    }

    @Override
    public void destroy() {
        System.out.println("time filter init");
    }
}
  • 自定義 Filter對象需要實現(xiàn)Filter的接口并實現(xiàn)其中的方法井赌。


Filter的初始化

public class StandardContext extends ContainerBase
        implements Context, NotificationEmitter {

    private HashMap<String, FilterDef> filterDefs = new HashMap<>();
    private HashMap<String, ApplicationFilterConfig> filterConfigs = new HashMap<>();

    @Override
    protected synchronized void startInternal() throws LifecycleException {
            // 省略其他代碼
            if (ok) {
                if (!filterStart()) {
                    log.error(sm.getString("standardContext.filterFail"));
                    ok = false;
                }
            }
    }

    public boolean filterStart() {
        boolean ok = true;
        synchronized (filterConfigs) {
            filterConfigs.clear();
            // 遍歷filterDefs的map初始化Filter對象
            for (Entry<String,FilterDef> entry : filterDefs.entrySet()) {
                String name = entry.getKey();
                try {
                    ApplicationFilterConfig filterConfig =
                            new ApplicationFilterConfig(this, entry.getValue());
                    filterConfigs.put(name, filterConfig);
                } catch (Throwable t) {
                    ok = false;
                }
            }
        }

        return ok;
    }
}
  • StandardContext#filterStart負責遍歷filterDefs并創(chuàng)建ApplicationFilterConfig對象惋耙。
  • ApplicationFilterConfig是包含 Filter 實例的對象网棍,F(xiàn)ilterDef是包含的過濾器的定義腌闯。
  • StandardContext的filterDefs保存 Filter 的定義芭概,filterConfigs保存 Filter 的實例包裝對象ApplicationFilterConfig硅堆。


Filter核心類定義

public class FilterDef implements Serializable {

    private static final long serialVersionUID = 1L;
    private static final StringManager sm = StringManager.getManager(Constants.PACKAGE_NAME);

    private String description = null;
    private String displayName = null;
    private transient Filter filter = null;
    private String filterClass = null;
    private String filterName = null;
    private String largeIcon = null;
    private final Map<String, String> parameters = new HashMap<>();
    private String smallIcon = null;
    private String asyncSupported = null;
}
  • FilterDef是Filter的定義類筹燕,filterClass表示過濾器的定義類记某。
public final class ApplicationFilterConfig implements FilterConfig, Serializable {

    private static final long serialVersionUID = 1L;
    static final StringManager sm = StringManager.getManager(Constants.Package);
    private static final List<String> emptyString = Collections.emptyList();
    private final transient Context context;
    private transient Filter filter = null;
    private final FilterDef filterDef;
    private transient InstanceManager instanceManager;
    private ObjectName oname;

    ApplicationFilterConfig(Context context, FilterDef filterDef)
            throws ClassCastException, ClassNotFoundException, IllegalAccessException,
            InstantiationException, ServletException, InvocationTargetException, NamingException,
            IllegalArgumentException, NoSuchMethodException, SecurityException {

        super();

        this.context = context;
        this.filterDef = filterDef;
        if (filterDef.getFilter() == null) {
            getFilter();
        } else {
            this.filter = filterDef.getFilter();
            getInstanceManager().newInstance(filter);
            initFilter();
        }
    }

    Filter getFilter() throws ClassCastException, ClassNotFoundException, IllegalAccessException,
            InstantiationException, ServletException, InvocationTargetException, NamingException,
            IllegalArgumentException, NoSuchMethodException, SecurityException {

        if (this.filter != null)
            return (this.filter);

        // 創(chuàng)建 并初始化 Filter 對象
        String filterClass = filterDef.getFilterClass();
        this.filter = (Filter) getInstanceManager().newInstance(filterClass);
        initFilter();

        return (this.filter);
    }


    private void initFilter() throws ServletException {
        if (context instanceof StandardContext &&
                context.getSwallowOutput()) {
            try {
                SystemLogHandler.startCapture();
                filter.init(this);
            } finally {
                String capturedlog = SystemLogHandler.stopCapture();
                if (capturedlog != null && capturedlog.length() > 0) {
                    getServletContext().log(capturedlog);
                }
            }
        } else {
            filter.init(this);
        }

        registerJMX();
    }
}
  • ApplicationFilterConfig的創(chuàng)建過程就是通過實例化FilterDef的 filterClass的類并調用 Filter 的 init 方法初始化 Filter 對象痊夭。
  • initFilter方法負責初始化 Filter 對象舞丛,也就是調用 filter 的 init 方法耘子。
  • ApplicationFilterConfig的filter字段保存實例話后的 Filter 實例。


FilterDef的加載

FilterDef 的來源需要如果是 web.xml 定義那么就從 webxml 中獲取球切,如果是springboot 工程谷誓,就通過ApplicationContextFacade類型進行獲取。

public class ContextConfig implements LifecycleListener {

    private void configureContext(WebXml webxml) {
        // 省略相關代碼
        for (FilterDef filter : webxml.getFilters().values()) {
            if (filter.getAsyncSupported() == null) {
                filter.setAsyncSupported("false");
            }
            context.addFilterDef(filter);
        }
    }
}
  • 通過webxml.getFilters()獲取過濾器的FilterDef并添加到StandardContext對象當中吨凑。
  • 上述方式一般在 spring MVC 項目在 web.xml 配置過濾器的時候使用捍歪。
public class ApplicationContextFacade implements org.apache.catalina.servlet4preview.ServletContext {

    @Override
    public FilterRegistration.Dynamic addFilter(String filterName,
            Filter filter) {
        if (SecurityUtil.isPackageProtectionEnabled()) {
            return (FilterRegistration.Dynamic) doPrivileged("addFilter",
                    new Class[]{String.class, Filter.class},
                    new Object[]{filterName, filter});
        } else {
            return context.addFilter(filterName, filter);
        }
    }
}


public class ApplicationContext implements org.apache.catalina.servlet4preview.ServletContext {

    private FilterRegistration.Dynamic addFilter(String filterName,
            String filterClass, Filter filter) throws IllegalStateException {

        FilterDef filterDef = context.findFilterDef(filterName);
        // context是StandardContext對象
        if (filterDef == null) {
            filterDef = new FilterDef();
            filterDef.setFilterName(filterName);
            context.addFilterDef(filterDef);
        } else {
            if (filterDef.getFilterName() != null &&
                    filterDef.getFilterClass() != null) {
                return null;
            }
        }

        if (filter == null) {
            filterDef.setFilterClass(filterClass);
        } else {
            filterDef.setFilterClass(filter.getClass().getName());
            filterDef.setFilter(filter);
        }

        return new ApplicationFilterRegistration(filterDef, context);
    }
}
  • 通過ApplicationContextFacade的addFilter方法并最終調用ApplicationContext的addFilter方法將過濾器的FilterDef并添加到StandardContext對象當中。
  • 是上述方式一般在springboot 工程中的加載過程鸵钝。


Filter的加載流程

  • Filter的定義的加載順序如上圖所示糙臼,包括解析 web.xml 文件生成 FilterDef并保存到 StandardContext 當中,遍歷 StandardContext 的 FilterDef 生成ApplicationFilterConfig并保存到StandardContext當中恩商。
  • StandardContext負責保存核心的FilterDef和ApplicationFilterConfig变逃。


Filter執(zhí)行

Filter整體流程

final class StandardWrapperValve
    extends ValveBase {

    @Override
    public final void invoke(Request request, Response response)
        throws IOException, ServletException {

        // 省略相關代碼
        ApplicationFilterChain filterChain =
                ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

        try {
            if ((servlet != null) && (filterChain != null)) {
                if (context.getSwallowOutput()) {
                    try {
                        SystemLogHandler.startCapture();
                        if (request.isAsyncDispatching()) {
                            request.getAsyncContextInternal().doInternalDispatch();
                        } else {
                            filterChain.doFilter(request.getRequest(),
                                    response.getResponse());
                        }
                    } finally {
                    }
                } else {
                    if (request.isAsyncDispatching()) {
                        request.getAsyncContextInternal().doInternalDispatch();
                    } else {
                        filterChain.doFilter
                            (request.getRequest(), response.getResponse());
                    }
                }

            }
        } catch (Throwable e) {

        }

        // Release the filter chain (if any) for this request
        if (filterChain != null) {
            filterChain.release();
        }
    }
}
  • Filter 執(zhí)行流程在StandardWrapperValve#invoke 方法當中,核心流程包括創(chuàng)建 Filter 調用鏈和執(zhí)行 Filter 調用鏈怠堪。
  • ApplicationFilterFactory.createFilterChain負責創(chuàng)建調用鏈對象ApplicationFilterChain揽乱。
  • filterChain.doFilter負責執(zhí)行 Filter 調用鏈。


filterChain的構建

public final class ApplicationFilterChain implements FilterChain {
    public static final int INCREMENT = 10;
    private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
    private int pos = 0;
    private int n = 0;
    private Servlet servlet = null;
    private boolean servletSupportsAsync = false;
    private static final StringManager sm = StringManager.getManager(Constants.Package);

    void addFilter(ApplicationFilterConfig filterConfig) {
        for(ApplicationFilterConfig filter:filters)
            if(filter==filterConfig)
                return;

        if (n == filters.length) {
            ApplicationFilterConfig[] newFilters =
                new ApplicationFilterConfig[n + INCREMENT];
            System.arraycopy(filters, 0, newFilters, 0, n);
            filters = newFilters;
        }
        filters[n++] = filterConfig;
    }
}
  • ApplicationFilterChain的內部維護ApplicationFilterConfig[] filters來保存 Filter 對象粟矿。
public final class ApplicationFilterFactory {

    public static ApplicationFilterChain createFilterChain(ServletRequest request,
            Wrapper wrapper, Servlet servlet) {

        ApplicationFilterChain filterChain = null;
        if (request instanceof Request) {
            Request req = (Request) request;
            if (Globals.IS_SECURITY_ENABLED) {
                filterChain = new ApplicationFilterChain();
            } else {
                filterChain = (ApplicationFilterChain) req.getFilterChain();
                if (filterChain == null) {
                    filterChain = new ApplicationFilterChain();
                    req.setFilterChain(filterChain);
                }
            }
        } else {
            filterChain = new ApplicationFilterChain();
        }

        filterChain.setServlet(servlet);
        filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());

        // 通過StandardContext#findFilterMaps獲取所有的Filter對象
        StandardContext context = (StandardContext) wrapper.getParent();
        FilterMap filterMaps[] = context.findFilterMaps();

        if ((filterMaps == null) || (filterMaps.length == 0))
            return (filterChain);

        DispatcherType dispatcher =
                (DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR);

        String requestPath = null;
        Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR);
        if (attribute != null){
            requestPath = attribute.toString();
        }

        String servletName = wrapper.getName();

        // 匹配路徑  Add the relevant path-mapped filters to this filter chain
        for (int i = 0; i < filterMaps.length; i++) {
            if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
                continue;
            }
            if (!matchFiltersURL(filterMaps[i], requestPath))
                continue;

            // 通過StandardContext#findFilterConfig獲取Filter對象
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
                context.findFilterConfig(filterMaps[i].getFilterName());
            if (filterConfig == null) {
                continue;
            }
            filterChain.addFilter(filterConfig);
        }

        // 匹配servlet的名字 Add filters that match on servlet name second
        for (int i = 0; i < filterMaps.length; i++) {
            if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
                continue;
            }
            if (!matchFiltersServlet(filterMaps[i], servletName))
                continue;
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
                context.findFilterConfig(filterMaps[i].getFilterName());
            if (filterConfig == null) {
                // FIXME - log configuration problem
                continue;
            }
            filterChain.addFilter(filterConfig);
        }

        // Return the completed filter chain
        return filterChain;
    }
}
  • 獲取StandardContext的FilterMap[] 對象凰棉,遍歷FilterMap[]后進行規(guī)則匹配,匹配后通過 StandardContext 獲取ApplicationFilterConfig對象添加到ApplicationFilterChain當中嚷炉。
  • StandardContext本身維護的ApplicationFilterConfig的加載流程已經(jīng)分析渊啰,需要了解FilterMap的加載過程


Filter 執(zhí)行流程

public final class ApplicationFilterChain implements FilterChain {

    public static final int INCREMENT = 10;
    private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
    private int pos = 0;
    private int n = 0;
    private Servlet servlet = null;
    private boolean servletSupportsAsync = false;
    private static final StringManager sm = StringManager.getManager(Constants.Package);


    @Override
    public void doFilter(ServletRequest request, ServletResponse response)
        throws IOException, ServletException {

        if( Globals.IS_SECURITY_ENABLED ) {
            // 省略相關代碼
        } else {
            internalDoFilter(request,response);
        }
    }

    private void internalDoFilter(ServletRequest request,
                                  ServletResponse response)
        throws IOException, ServletException {

        if (pos < n) {
            ApplicationFilterConfig filterConfig = filters[pos++];
            try {
                Filter filter = filterConfig.getFilter();

                if( Globals.IS_SECURITY_ENABLED ) {
                    // 省略相關代碼
                } else {
                    filter.doFilter(request, response, this);
                }
            } catch (IOException | ServletException | RuntimeException e) {

            } catch (Throwable e) {

            }
            return;
        }

        try {
            if ((request instanceof HttpServletRequest) &&
                    (response instanceof HttpServletResponse) &&
                    Globals.IS_SECURITY_ENABLED ) {
                // 省略相關代碼
            } else {
                servlet.service(request, response);
            }
        } catch (IOException | ServletException | RuntimeException e) {

        } catch (Throwable e) {

        } finally {

        }
    }
}
  • ApplicationFilterChain#internalDoFilter負責 Filter 調用鏈的執(zhí)行申屹,內部通過維護 Filter 的對象數(shù)組filters和下標pos依次執(zhí)行 Filter绘证。


FilterMap介紹

/**
 * 來看下這個類的官方解釋:
 * Web應用程序的過濾器映射的表示形式,如部署描述符中<filter-mapping>元素中的所示
 * 每個過濾器映射都必須包含過濾器名稱以及URL模式或servlet名稱
 * 例如以下配置:
 * <filter-mapping>  
 *    <filter-name>MyFilter</filter-name>  
 *    <url-pattern>/my</url-pattern> 
 * </filter-mapping> 
 * 
 * 說白了哗讥,這個FilterMap就是封裝了配置信息中<filter-mapping>標簽中的元素
 * 其中還包含了兩個重點屬性:過濾器名嚷那、過濾器對應過濾的url
 */
public class FilterMap extends XmlEncodingBase implements Serializable {
    private boolean matchAllUrlPatterns = false;
    private boolean matchAllServletNames = false;

    // serverlet的名字,對應多個
    private String[] servletNames = new String[0];
    // 過濾器名杆煞,對應的是<filter-name>中的內容
    private String filterName = null; 
    // 過濾url魏宽,對應的是<url-pattern>中的內容(可配置多個<filter-mapping>匹配不同的url腐泻,因此是數(shù)組形式)
    private String[] urlPatterns = new String[0]; 
}
  • FilterMap的核心字段包括匹配的 Url格式,對應的 Filter 對象的filterName等队询。


FilterMap的加載

public class ContextConfig implements LifecycleListener {

    private void configureContext(WebXml webxml) {
        // 省略相關代碼

        for (FilterMap filterMap : webxml.getFilterMappings()) {
            context.addFilterMap(filterMap);
        }
    }
}
  • 通過webxml.getFilterMappings()獲取過濾器的filterMap并添加到StandardContext中派桩。
  • 上述方式一般在 spring MVC 項目在 web.xml 配置過濾器的時候使用。
public class ApplicationFilterRegistration
        implements FilterRegistration.Dynamic {

    private static final StringManager sm =
      StringManager.getManager(Constants.Package);

    private final FilterDef filterDef;
    private final Context context;

    public ApplicationFilterRegistration(FilterDef filterDef,
            Context context) {
        this.filterDef = filterDef;
        this.context = context;
    }

    @Override
    public void addMappingForServletNames(
            EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter,
            String... servletNames) {

        FilterMap filterMap = new FilterMap();
        filterMap.setFilterName(filterDef.getFilterName());

        if (dispatcherTypes != null) {
            for (DispatcherType dispatcherType : dispatcherTypes) {
                filterMap.setDispatcher(dispatcherType.name());
            }
        }

        if (servletNames != null) {
            for (String servletName : servletNames) {
                filterMap.addServletName(servletName);
            }

            if (isMatchAfter) {
                context.addFilterMap(filterMap);
            } else {
                context.addFilterMapBefore(filterMap);
            }
        }
    }

    @Override
    public void addMappingForUrlPatterns(
            EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter,
            String... urlPatterns) {

        FilterMap filterMap = new FilterMap();
        filterMap.setFilterName(filterDef.getFilterName());

        if (dispatcherTypes != null) {
            for (DispatcherType dispatcherType : dispatcherTypes) {
                filterMap.setDispatcher(dispatcherType.name());
            }
        }

        if (urlPatterns != null) {
            for (String urlPattern : urlPatterns) {
                filterMap.addURLPattern(urlPattern);
            }

            if (isMatchAfter) {
                context.addFilterMap(filterMap);
            } else {
                context.addFilterMapBefore(filterMap);
            }
        }
    }
}
  • addMappingForServletNames和addMappingForUrlPatterns負責獲取過濾器的filterMap并添加到StandardContext中蚌斩。
  • 上述方式一般在 spring boot 工程中加載FilterMap使用铆惑。


Filter執(zhí)行流程圖

  • 通過解析 web.xml 文件生成 FilterMap并保存到 StandardContext 當中。
  • ApplicationFilterChaiFactory 負責創(chuàng)建 Filter 對象 ApplicationFilterChain送膳,然后遍歷FilterMap s并添加符合的 Filter 包裝對象 ApplicationFilterConfig员魏。
  • 執(zhí)行ApplicationFilterChain的doFilter方法調用過濾器。
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末叠聋,一起剝皮案震驚了整個濱河市撕阎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌碌补,老刑警劉巖虏束,帶你破解...
    沈念sama閱讀 206,013評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異脑慧,居然都是意外死亡魄眉,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評論 2 382
  • 文/潘曉璐 我一進店門闷袒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來坑律,“玉大人,你說我怎么就攤上這事囊骤』卧瘢” “怎么了?”我有些...
    開封第一講書人閱讀 152,370評論 0 342
  • 文/不壞的土叔 我叫張陵也物,是天一觀的道長宫屠。 經(jīng)常有香客問我,道長滑蚯,這世上最難降的妖魔是什么浪蹂? 我笑而不...
    開封第一講書人閱讀 55,168評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮告材,結果婚禮上坤次,老公的妹妹穿的比我還像新娘。我一直安慰自己斥赋,他們只是感情好缰猴,可當我...
    茶點故事閱讀 64,153評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著疤剑,像睡著了一般滑绒。 火紅的嫁衣襯著肌膚如雪闷堡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,954評論 1 283
  • 那天疑故,我揣著相機與錄音杠览,去河邊找鬼。 笑死焰扳,一個胖子當著我的面吹牛倦零,可吹牛的內容都是我干的。 我是一名探鬼主播吨悍,決...
    沈念sama閱讀 38,271評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蹋嵌!你這毒婦竟也來了育瓜?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,916評論 0 259
  • 序言:老撾萬榮一對情侶失蹤栽烂,失蹤者是張志新(化名)和其女友劉穎躏仇,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體腺办,經(jīng)...
    沈念sama閱讀 43,382評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡焰手,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,877評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了怀喉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片书妻。...
    茶點故事閱讀 37,989評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖躬拢,靈堂內的尸體忽然破棺而出躲履,到底是詐尸還是另有隱情,我是刑警寧澤聊闯,帶...
    沈念sama閱讀 33,624評論 4 322
  • 正文 年R本政府宣布工猜,位于F島的核電站,受9級特大地震影響菱蔬,放射性物質發(fā)生泄漏篷帅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,209評論 3 307
  • 文/蒙蒙 一拴泌、第九天 我趴在偏房一處隱蔽的房頂上張望魏身。 院中可真熱鬧,春花似錦弛针、人聲如沸叠骑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宙枷。三九已至掉房,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間慰丛,已是汗流浹背卓囚。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留诅病,地道東北人哪亿。 一個月前我還...
    沈念sama閱讀 45,401評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像贤笆,于是被迫代替她去往敵國和親蝇棉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,700評論 2 345

推薦閱讀更多精彩內容