当前位置

网站首页> 程序设计 > 开源项目 > 程序开发 > 浏览文章

jFinal路由解析源码分析 - Honwhy

作者:小梦 来源: 网络 时间: 2024-07-06 阅读:

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

  <filter>      <filter-name>jfinal</filter-name>      <filter-class>com.jfinal.core.JFinalFilter</filter-class>      <init-param>          <param-name>configClass</param-name>          <param-value>com.app.common.Config</param-value>      </init-param>  </filter>    <filter-mapping>      <filter-name>jfinal</filter-name>      <url-pattern>/*</url-pattern>  </filter-mapping>

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<Interceptor>;
handlers拥有所有的handler,内部结构是List<Handler>
Routes定义了两个容器,

private final Map<String, Class<? extends Controller>> map = new HashMap<String, Class<? extends Controller>>();private final Map<String, String> viewPathMap = new HashMap<String, String>();

对外提供了多个重载的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<? extends Controller> 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<? extends Controller> controllerClass) {    return add(controllerkey, controllerClass, controllerkey);}

其实调用了上面的方法的。

public Routes add(String controllerKey, Class<? extends Controller> 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<Interceptor> interceptorList = new ArrayList<Interceptor>();        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<Handler> 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<String, Action> 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<String> excludedMethodName = buildExcludedMethodName();    InterceptorBuilder interceptorBuilder = new InterceptorBuilder();    Interceptor[] defaultInters = interceptors.getInterceptorArray();    interceptorBuilder.addToInterceptorsMap(defaultInters);    for (Entry<String, Class<? extends Controller>> entry : routes.getEntrySet()) {        Class<? extends Controller> 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;        elsethrow 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。
感谢转神提供的案例
感谢豪的提点和帮助

热点阅读

网友最爱