国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

jFinal路由解析源碼分析

CatalpaFlat / 892人閱讀

摘要:只是暴露接口,配置信息最終保存在的靜態類中。整個項目只有一個,作為靜態類可以保證它是唯一的,而它的靜態成員也是整個項目中唯一的。至此的路由解析模塊就分析完了。

jFinal的路由解析是在JFinalFilter中做的,這個Filter也需要在web.xml中配置。JFinalFilter實現了javax.servlet.Filter接口,從這里也可以看出jFinal是基于Servlet的。JFinalFilter在初始化時負責初始化jFinal項目的配置(com.jfinal.core.Config)、路由表(Route)、映射表(ActionMapping)等;路由解析是在JFinalFilterdofilter方法完成的。

關鍵詞: Route Handler Action ActionMapping

1. 項目配置

分析jFinal的路由解析邏輯必須從jFinal的一般項目配置入手,配置的作用是為路由解析提供支持的。和一般Java Web MVC框架不同的是jFinal沒有采用xml配置的形式,但不是不需要配置,還是需要提供一個JFinalConfig的繼承實現類,實現configXXX方法來支持配置初始化,初始化的入口是JFinalFilterinit方法。

1.1 web.xml

jFinal工程同樣需要web.xml配置文件,但是較其他MVC框架的web.xml文件內容或許要簡單許多,除了配置welcome-file-list,只需要配置一個filter

  
      jfinal
      com.jfinal.core.JFinalFilter
      
          configClass
          com.app.common.Config
      
  
  
  
      jfinal
      /*
  

JFinalFilter是唯一需要配置的filter,只需要提供一個configClass參數,它會在JFinalFilterinit方法中利用Class.forName(configClass).newInstance();被實例化。

1.2 JFinalConfig

上面的configClass參數的值com.app.common.Config是項目定義的JFinalConfig的實現類,雖然整個項目沒有xml配置,但是這里就是,只不過是Java代碼的形式。
JFinalConfig只是暴露接口,配置信息最終保存在jFinal的靜態類com.jfinal.core.Config中。com.jfinal.core.Config類設計成不可以實例化,它定義的私有靜態成員變量可以保證唯一。JFinalConfig實現的接口就負責填充com.jfinal.core.Config成員變量。
在本文中會關注JFinalConfig的以下接口方法:

/**
 * Config route
 */
public abstract void configRoute(Routes me);

/**
 * Config interceptor applied to all actions.
 */
public abstract void configInterceptor(Interceptors me);

/**
 * Config handler
 */
public abstract void configHandler(Handlers me);
1.3 com.jfinal.core.Config

Config的成員變量中我們關注這幾個變量:

private static final Routes routes = new Routes(){public void config() {}};
private static final Interceptors interceptors = new Interceptors();
private static final Handlers handlers = new Handlers();

interceptors擁有所有的Interceptor,內部結構是List;
handlers擁有所有的handler,內部結構是List
Routes定義了兩個容器,

private final Map> map = new HashMap>();
private final Map viewPathMap = new HashMap();

對外提供了多個重載的add方法,用于增加路由mapviewMap的鍵都是controllerKey
關于InterceptorHandlerRoutes下文會繼續說明,我們先來看看自定義的JFinalConfig實現類com.app.common.Config做了什么事情。即是我們關注的JFinalConfig的抽象方法實現。

1.4 JFinalConfig抽象方法實現
package com.app.common;

public class Config extends JFinalConfig {

    @Override
    public void configConstant(Constants me) {
        //配置默認View類型
    }

    @Override
    public void configRoute(Routes me) {
        me.add("/api/user", UserController.class);
        me.add("/admin/user", ManagerController.class, "/admin");
        me.add("/admin/index", IndexController.class, "/admin");
        //...
    }

    @Override
    public void configPlugin(Plugins me) {
        //配置數據庫連接
        //配置數據表和pojo映射
    }

    @Override
    public void configInterceptor(Interceptors me) {
        //配置攔截器
    }

    @Override
    public void configHandler(Handlers me) {
        //配置Handler
        //這里沒有配置,JFinal.init()方法也會添加一個ActionHandler
    }

}

configRoute實現中我們使用了兩種Routes.add()方法,向Routes添加了三個ControllerjFinal的路由是REST風格的,這里
me.add("/api/user", UserController.class);的意思大概是請求/api/user時會交給UserController來處理。具體地看下文JFinalFilterdoFilter方法小節。
這里抽象實現方法什么時候被調用具體看JFinalFilterinit方法小節。

2 路由和攔截器及Handler鏈

在進入JFinalFilterinitdoFilter方法之前,我們將上面的提到的幾個概念梳理一下。

2.1 Routes

RoutesjFinal的路由,有兩個路由映射的容器,請求路徑到Controller的映射和請求路徑到渲染頁面的映射。
Routes在項目中是作為com.jfinal.core.Config的成員變量出現的,負責維護jFinal項目的路由映射。整個jFinal項目只有一個com.jfinal.core.Config,作為靜態類可以保證它是唯一的,而它的靜態成員也是整個項目中唯一的。routes就是其中之一。
Routes提供了多個重載的add方法,我們來看看我使用到的其中兩個。

/**
 * Add route
 * @param controllerKey A key can find controller
 * @param controllerClass Controller Class
 * @param viewPath View path for this Controller
 */
public Routes add(String controllerKey, Class controllerClass, String viewPath) {
    //很多很多的corner check
    //處理controllerKey的前綴,前綴加SLASH /
    //處理viewPath的前綴和后綴,都加上SLASH
    //如果viewPath的根路徑baseViewPath不為空則在viewPath前拼接
    map.put(controllerKey, controllerClass);
    viewPathMap.put(controllerKey, viewPath);
    return this;//為了鏈式寫法
}

另外一個

public Routes add(String controllerkey, Class controllerClass) {
    return add(controllerkey, controllerClass, controllerkey);
}

其實調用了上面的方法的。

public Routes add(String controllerKey, Class controllerClass, String viewPath) {
}

一般使用過程中通過controllerKey找到Controller,這非常容易理解。而通過controllerKeyviewPathMap中找到viewPath,這個是用渲染頁面是使用的路徑,例如:
請求/api/user/edit執行成功后渲染到/api/user/edit.jsp頁面。
一般我們定義controllery/api/userviewPath/api/user/或者其他,而/editedit.jsp映射是約定好的。(但并不是直接映射的。)
最終的結果我們可以得到兩個配置好的mapviewPathMap

2.2 Interceptors

Routes同理,Interceptors也作為com.jfinal.core.Config的成員變量出現的,它本身是一個List容器,記錄的是項目的所有攔截器。在示例中com.app.common.Config并沒有設置攔截器,在實現的configInterceptor方法中并沒有做什么事情,如有需要我們可以調用Interceptorsadd方法添加全局的攔截器。

final public class Interceptors {
    
    private final List interceptorList = new ArrayList();
    
    public Interceptors add(Interceptor globalInterceptor) {
        if (globalInterceptor != null)
            this.interceptorList.add(globalInterceptor);
        return this;
    }
    //...
}    
2.3 Handler

com.jfinal.core.Config有一個成員變量handlers,記錄的是項目所有的Handler,可以向它添加Handler。在示例中com.app.common.Config實現的configHandler方法中也沒有做具體的配置。
Handler有一個成員變量nextHandler指向下一個Handler,這樣可以用鏈表形式將所有的Handler連接起來。Handler鏈表的頭節點最后保存在JFinalhandler變量,見JFinalFilter的init方法小節。這里先提一下如何獲得鏈表的頭節點:在HandlerFacotry中提供的getHandler方法傳入原有的所有Handler和一個新的Handler,最終構造一條Handler鏈,新的Handler被添加到鏈表的尾部,最終返回頭節點。

/**
 * Build handler chain
 */
public static Handler getHandler(List handlerList, Handler actionHandler) {
    Handler result = actionHandler;
    
    for (int i=handlerList.size()-1; i>=0; i--) {
        Handler temp = handlerList.get(i);
        temp.nextHandler = result;
        result = temp;
    }
    
    return result;
}

Handler鏈的使用是在JFinalFilterdoFilter方法中,下文會提及。

2.4 ActionMapping

ActionMapping負責將RoutesInterceptors組織起來,整合后的結果存到在ActionMappingmapping成員變量(Map mapping),Action是最終用于處理HTTP請求的Action(不知道怎么翻譯才恰當)。
具體過程則是,遍歷Routes所有Controller、遍歷Controller所有method,將類級別(Controller)和方法(method)級別對應的key或者名字連接起來作為鍵actionKey,將類級別(Controller)和方法(method)級別對應的Interceptor整合計算后得到Action的攔截器數組actionInters
最后用于實例化Action需要的變量如下所示:

new Action(controllerKey, actionKey, controllerClass, method, methodName, actionInters, routes.getViewPath(controllerKey));

具體的可以參照ActionMappingbuildActionMapping方法。

void buildActionMapping() {
    mapping.clear();
    Set excludedMethodName = buildExcludedMethodName();
    InterceptorBuilder interceptorBuilder = new InterceptorBuilder();
    Interceptor[] defaultInters = interceptors.getInterceptorArray();
    interceptorBuilder.addToInterceptorsMap(defaultInters);
    for (Entry> entry : routes.getEntrySet()) {
        Class controllerClass = entry.getValue();
        Interceptor[] controllerInters = interceptorBuilder.buildControllerInterceptors(controllerClass);
        Method[] methods = controllerClass.getMethods();
        for (Method method : methods) {
            String methodName = method.getName();
            if (!excludedMethodName.contains(methodName) && method.getParameterTypes().length == 0) {
                Interceptor[] methodInters = interceptorBuilder.buildMethodInterceptors(method);
                Interceptor[] actionInters = interceptorBuilder.buildActionInterceptors(defaultInters, controllerInters, controllerClass, methodInters, method);
                String controllerKey = entry.getKey();
                
                ActionKey ak = method.getAnnotation(ActionKey.class);
                if (ak != null) {
                    String actionKey = ak.value().trim();
                    if ("".equals(actionKey))
                        throw new IllegalArgumentException(controllerClass.getName() + "." + methodName + "(): The argument of ActionKey can not be blank.");
                    
                    if (!actionKey.startsWith(SLASH))
                        actionKey = SLASH + actionKey;
                    
                    if (mapping.containsKey(actionKey)) {
                        warnning(actionKey, controllerClass, method);
                        continue;
                    }
                    
                    Action action = new Action(controllerKey, actionKey, controllerClass, method, methodName, actionInters, routes.getViewPath(controllerKey));
                    mapping.put(actionKey, action);
                }
                else if (methodName.equals("index")) {
                    String actionKey = controllerKey;
                    
                    Action action = new Action(controllerKey, actionKey, controllerClass, method, methodName, actionInters, routes.getViewPath(controllerKey));
                    action = mapping.put(actionKey, action);
                    
                    if (action != null) {
                        warnning(action.getActionKey(), action.getControllerClass(), action.getMethod());
                    }
                }
                else {
                    String actionKey = controllerKey.equals(SLASH) ? SLASH + methodName : controllerKey + SLASH + methodName;
                    
                    if (mapping.containsKey(actionKey)) {
                        warnning(actionKey, controllerClass, method);
                        continue;
                    }
                    
                    Action action = new Action(controllerKey, actionKey, controllerClass, method, methodName, actionInters, routes.getViewPath(controllerKey));
                    mapping.put(actionKey, action);
                }
            }
        }
    }
    
    // support url = controllerKey + urlParas with "/" of controllerKey
    Action actoin = mapping.get("/");
    if (actoin != null)
        mapping.put("", actoin);
}

這個方法會是整篇文章提到的最復雜的方法,所以這里全部列出。
主要的邏輯是拼接${controlerKey}/methodName作為actionKey${controllerKey}類似/api/user是我們在JFinalConfig實現類中添加的。actionKey最后會和請求的URL比較,匹配時就返回其對應的Action。拼接actionKey的過程中有兩個需要注意的地方,一個是Controller的方法不能有參數,一個是如果方法名是index就將controllerKey作為actionKey,即是如果請求是/api/user最終調用的是UserController.index()。最后也做了請求是/的支持。
另外一個重要的是邏輯是整合計算Action的最終的攔截器數組actionIntersjFinal提供了Before注解的形式來在Controller類級別和method方法級別引入Interceptor,還有ClearInterceptor作為規則用于排除上層層次的Interceptor。這些細節就不展開了。

2.5 Action

2.4 ActionMapping已經提到了Action,這里提一下Action是怎么調用的。我們注意到實例化Action時傳入了很多參數。

new Action(controllerKey, actionKey, controllerClass, method, methodName, actionInters, routes.getViewPath(controllerKey));

其中controllerClass可以提供實例化一個ControllermethodName可以確定調用Controller的哪個方法,actionInters可以在調用Controller方法前執行攔截過濾等,攔截過濾后再回到Action去調用真正的methodName方法。整個調用過程是ActionInvocation封裝完成的,具體細節就不展開了。

3 JFinalFilter init

最后來看看兩個重要的流程。直接上代碼

public void init(FilterConfig filterConfig) throws ServletException {
    //實例化JFinalConfig實現類
    createJFinalConfig(filterConfig.getInitParameter("configClass"));
    //配置初始化
    //初始化Handler ActionMapping
    if (jfinal.init(jfinalConfig, filterConfig.getServletContext()) == false)
        throw new RuntimeException("JFinal init error!");
    //Handler鏈頭節點
    handler = jfinal.getHandler();
    constants = Config.getConstants();
    encoding = constants.getEncoding();
    jfinalConfig.afterJFinalStart();
    
    String contextPath = filterConfig.getServletContext().getContextPath();
    contextPathLength = (contextPath == null || "/".equals(contextPath) ? 0 : contextPath.length());
}

createJFinalConfigJFinalFilter內部方法,filterConfig.getInitParameter("configClass")是從web.xml獲得配置的JFinalConfig實現類,目的是實例化JFinalConfig

private void createJFinalConfig(String configClass) {
    if (configClass == null)
        throw new RuntimeException("Please set configClass parameter of JFinalFilter in web.xml");
    
    try {
        Object temp = Class.forName(configClass).newInstance();
        if (temp instanceof JFinalConfig)
            jfinalConfig = (JFinalConfig)temp;
        else
            throw new RuntimeException("Can not create instance of class: " + configClass + ". Please check the config in web.xml");
    } catch (InstantiationException e) {
        throw new RuntimeException("Can not create instance of class: " + configClass, e);
    } catch (IllegalAccessException e) {
        throw new RuntimeException("Can not create instance of class: " + configClass, e);
    } catch (ClassNotFoundException e) {
        throw new RuntimeException("Class not found: " + configClass + ". Please config it in web.xml", e);
    }
}

接下來是調用

jfinal.init(jfinalConfig, filterConfig.getServletContext())

這部分是在JFinal類中完成的。

boolean init(JFinalConfig jfinalConfig, ServletContext servletContext) {
    this.servletContext = servletContext;
    this.contextPath = servletContext.getContextPath();
    
    initPathUtil();
    //調用JFinalConfig實現類的configXXX方法
    Config.configJFinal(jfinalConfig);    // start plugin and init logger factory in this method
    constants = Config.getConstants();
    
    //初始化actionMapping
    initActionMapping();
    //新建一個ActionHandler并且構造一條Handler鏈并保存頭節點
    initHandler();
    initRender();
    initOreillyCos();
    initI18n();
    initTokenManager();
    
    return true;
}

這個方法中開始做整個項目的配置初始化,具體可以看Config.configJFinal(jfinalConfig)的實現。

/*
 * Config order: constant, route, plugin, interceptor, handler
 */
static void configJFinal(JFinalConfig jfinalConfig) {
    jfinalConfig.configConstant(constants);                initLoggerFactory();
    jfinalConfig.configRoute(routes);
    jfinalConfig.configPlugin(plugins);                    startPlugins();    // very important!!!
    jfinalConfig.configInterceptor(interceptors);
    jfinalConfig.configHandler(handlers);
}

基本就是調用JFinalConfigconfigXXX,具體如何做可以參考前面RoutesInterceptorsHandler小節。
接著來關注initActionMapping部分邏輯。

private void initActionMapping() {
    actionMapping = new ActionMapping(Config.getRoutes(), Config.getInterceptors());
    actionMapping.buildActionMapping();
}

基本就是調用ActionMappingbuildActionMapping方法了,buildActionMapping可以參考前面ActionMapping小節。
最后關注initHandler部分邏輯。

private void initHandler() {
    Handler actionHandler = new ActionHandler(actionMapping, constants);
    handler = HandlerFactory.getHandler(Config.getHandlers().getHandlerList(), actionHandler);
}

關于HandlerFactory的使用可以參考Handler小節。
執行完JFinalFilterinit就為整個項目的路由解析做好了準備了。

4 JFinalFilter doFilter

還是直接上代碼

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest)req;
    HttpServletResponse response = (HttpServletResponse)res;
    request.setCharacterEncoding(encoding);
    //獲得請求URL
    String target = request.getRequestURI();
    if (contextPathLength != 0)
        //切掉上下文路徑,contextPathLength是上下文路徑的長度
        target = target.substring(contextPathLength);
    
    boolean[] isHandled = {false};
    try {
        //Handler鏈調用
        handler.handle(target, request, response, isHandled);
    }
    catch (Exception e) {
        if (log.isErrorEnabled()) {
            String qs = request.getQueryString();
            log.error(qs == null ? target : target + "?" + qs, e);
        }
    }
    
    if (isHandled[0] == false)
        chain.doFilter(request, response);
}

這里的handlerJFinal.initHanlder()方法獲得Handler鏈的頭節點,如果整個項目沒有其他Handler,頭節點應該是一個ActionHandler類型實例。
接下來看ActionHandler.handle方法

/**
 * handle
 * 1: Action action = actionMapping.getAction(target)
 * 2: new ActionInvocation(...).invoke()
 * 3: render(...)
 */
public final void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) {
    if (target.indexOf(".") != -1) {
        return ;
    }
    
    isHandled[0] = true;
    String[] urlPara = {null};
    Action action = actionMapping.getAction(target, urlPara);
    
    if (action == null) {
        if (log.isWarnEnabled()) {
            String qs = request.getQueryString();
            log.warn("404 Action Not Found: " + (qs == null ? target : target + "?" + qs));
        }
        renderFactory.getErrorRender(404).setContext(request, response).render();
        return ;
    }
    
    try {
        Controller controller = action.getControllerClass().newInstance();
        controller.init(request, response, urlPara[0]);
        
        if (devMode) {
            boolean isMultipartRequest = ActionReporter.reportCommonRequest(controller, action);
            new ActionInvocation(action, controller).invoke();
            if (isMultipartRequest) ActionReporter.reportMultipartRequest(controller, action);
        }
        else {
            new ActionInvocation(action, controller).invoke();
        }
        
        Render render = controller.getRender();
        if (render instanceof ActionRender) {
            String actionUrl = ((ActionRender)render).getActionUrl();
            if (target.equals(actionUrl))
                throw new RuntimeException("The forward action url is the same as before.");
            else
                handle(actionUrl, request, response, isHandled);
            return ;
        }
        
        if (render == null)
            render = renderFactory.getDefaultRender(action.getViewPath() + action.getMethodName());
        render.setContext(request, response, action.getViewPath()).render();
    }
    catch (RenderException e) {
        if (log.isErrorEnabled()) {
            String qs = request.getQueryString();
            log.error(qs == null ? target : target + "?" + qs, e);
        }
    }
    catch (ActionException e) {
        int errorCode = e.getErrorCode();
        if (errorCode == 404 && log.isWarnEnabled()) {
            String qs = request.getQueryString();
            log.warn("404 Not Found: " + (qs == null ? target : target + "?" + qs));
        }
        else if (errorCode == 401 && log.isWarnEnabled()) {
            String qs = request.getQueryString();
            log.warn("401 Unauthorized: " + (qs == null ? target : target + "?" + qs));
        }
        else if (errorCode == 403 && log.isWarnEnabled()) {
            String qs = request.getQueryString();
            log.warn("403 Forbidden: " + (qs == null ? target : target + "?" + qs));
        }
        else if (log.isErrorEnabled()) {
            String qs = request.getQueryString();
            log.error(qs == null ? target : target + "?" + qs, e);
        }
        e.getErrorRender().setContext(request, response).render();
    }
    catch (Exception e) {
        if (log.isErrorEnabled()) {
            String qs = request.getQueryString();
            log.error(qs == null ? target : target + "?" + qs, e);
        }
        renderFactory.getErrorRender(500).setContext(request, response).render();
    }
}

render部分暫且不看。
從作者的代碼注釋中可以看出這個handle方法的主要邏輯。
我們就看其中兩個。

* 1: Action action = actionMapping.getAction(target)
* 2: new ActionInvocation(...).invoke()

target是減去了contextPath部分的請求路徑,在ActionMapping.getAction(target)方法中將與ActionMapping維護的mapping表中的所有actionKey作比較,如果匹配就獲得一個Action
看下實現代碼

/**
 * Support four types of url
 * 1: http://abc.com/controllerKey                 ---> 00
 * 2: http://abc.com/controllerKey/para            ---> 01
 * 3: http://abc.com/controllerKey/method          ---> 10
 * 4: http://abc.com/controllerKey/method/para     ---> 11
 */
Action getAction(String url, String[] urlPara) {
    Action action = mapping.get(url);
    if (action != null) {
        return action;
    }
    
    // --------
    int i = url.lastIndexOf(SLASH);
    if (i != -1) {
        action = mapping.get(url.substring(0, i));
        urlPara[0] = url.substring(i + 1);
    }
    
    return action;
}

簡單解釋下,這個方法支持四種形式的請求,見注釋。
首先嘗試mapping.get(url)
如果結果不為空,結合前面ActionMapping.buildActionMapping(),
我們知道這時/controllerKey或者/controllery/method匹配到了。
進一步截取并嘗試mapping.get(url.substring(0,i))
即將/controllerKey/para/controllerKey/method/para減去/para再執行匹配。
paraurlPara[0]收集起來。
最后不管是否匹都配返回。

回到ActionHandler.handle()方法,用獲得的Action進行調用處理請求。

new ActionInvocation(action, controller).invoke();

至此,jFinal的路由解析模塊就分析完了。

5 Thanks

以上用于分析的jFinal版本是jfinal-1.8-bin-with-src.jar。
感謝轉神提供的案例
感謝豪的提點和幫助
支持一下文章作者吧!

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/65316.html

相關文章

  • jfinal路由簡單解析

    摘要:在中,通過對所有的類進行過濾。在這個類中,均以成員變量的形式存在。中放置的是和的鍵值對。在中主要是調用了方法。 在jfinal中,通過JFinalFilter對所有的類進行過濾。 以下是路由的調用關系(我在調用關系旁邊做了標記,會貼出具體的代碼和解釋): -1- Config: Routes -2- Interceptors Handlers -3- publi...

    cncoder 評論0 收藏0
  • 快速上手JFinal

    摘要:是國產的框架,由五大部分組成。本文通過一個例子上手,旨在熟悉中各組件的用法。指的是表名,指的是主鍵數據庫連接池使用的是,還支持。默認訪問方法,這點類似于如果之前有基礎,上手會非常快。映射在上使用了校驗攔截器,使用了權限攔截器。 JFinal是國產的MVC框架,由 Handler、Interceptor、Controller、Render、Plugin 五大部分組成。本文通過一個例子上手...

    susheng 評論0 收藏0
  • jfinal 日志log4j使用

    摘要:本文使用環境如何使用導入包將官網提供的包導入項目配置文件在項目配置文件中配置如下內容即可生效生成日志文件運行項目后將在根目錄下生成的文件。 本文使用環境 win7 Idea 14.1.4 jfinal 2.0 1.jfinal如何使用log4j a.導入jar包 將官網提供的log4j.jar包導入項目 b.配置文件 在項目配置文件(project/src/log4j.prope...

    Little_XM 評論0 收藏0
  • jfinal 使用redis

    摘要:使用可以極度方便的使用,該插件不僅提供了豐富的,而且還同時支持多服務端。擁有超高的性能,豐富的數據結構,天然支持數據持久化,是目前應用非常廣泛的數據庫。 預設 Ubuntu 上 安裝 redis 參見http://segmentfault.com/a/1190000004109484 概述 jfinal 2.0 中已集成RedisPlugin插件。 RedisPlugin 是支持 ...

    Harriet666 評論0 收藏0

發表評論

0條評論

CatalpaFlat

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<