jFinal路由解析源码分析 - Honwhy
jFinal
的路由解析是在JFinalFilter
中做的,这个Filter也需要在web.xml
中配置。JFinalFilter
实现了javax.servlet.Filter
接口,从这里也可以看出jFinal
是基于Servlet
的。JFinalFilter
在初始化时负责初始化jFinal
项目的配置(com.jfinal.core.Config)、路由表(Route)、映射表(ActionMapping)等;路由解析是在JFinalFilter
的dofilter
方法完成的。
关键词: Route
Handler
Action
ActionMapping
1. 项目配置
分析jFinal
的路由解析逻辑必须从jFinal
的一般项目配置入手,配置的作用是为路由解析提供支持的。和一般Java Web MVC框架不同的是jFinal
没有采用xml配置的形式,但不是不需要配置,还是需要提供一个JFinalConfig
的继承实现类,实现configXXX
方法来支持配置初始化,初始化的入口是JFinalFilter
的init
方法。
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
参数,它会在JFinalFilter
的init
方法中利用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
方法,用于增加路由
,map
和viewMap
的键都是controllerKey
。
关于Interceptor
、Handler
和Routes
下文会继续说明,我们先来看看自定义的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
添加了三个Controller
。jFinal
的路由是REST风格的,这里me.add("/api/user", UserController.class);
的意思大概是请求/api/user
时会交给UserController
来处理。具体地看下文JFinalFilter
的doFilter
方法小节。
这里抽象实现方法什么时候被调用具体看JFinalFilter
的init
方法小节。
2 路由和拦截器及Handler链
在进入JFinalFilter
的init
和doFilter
方法之前,我们将上面的提到的几个概念梳理一下。
2.1 Routes
Routes
是jFinal
的路由,有两个路由映射的容器,请求路径到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
,这非常容易理解。而通过controllerKey
在viewPathMap
中找到viewPath
,这个是用渲染页面是使用的路径,例如:
请求/api/user/edit
执行成功后渲染到/api/user/edit.jsp
页面。
一般我们定义controllery
为/api/user
,viewPath
为/api/user/
或者其他,而/edit
和edit.jsp
映射是约定好的。(但并不是直接映射的。)
最终的结果我们可以得到两个配置好的map
和viewPathMap
。
2.2 Interceptors
与Routes
同理,Interceptors
也作为com.jfinal.core.Config
的成员变量出现的,它本身是一个List
容器,记录的是项目的所有拦截器。在示例中com.app.common.Config
并没有设置拦截器,在实现的configInterceptor
方法中并没有做什么事情,如有需要我们可以调用Interceptors
的add
方法添加全局的拦截器。
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
链表的头节点最后保存在JFinal
的handler
变量,见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
链的使用是在JFinalFilter
的doFilter
方法中,下文会提及。
2.4 ActionMapping
ActionMapping
负责将Routes
和Interceptors
组织起来,整合后的结果存到在ActionMapping
的mapping
成员变量(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));
具体的可以参照ActionMapping
的buildActionMapping
方法。
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
的最终的拦截器数组actionInters
。jFinal
提供了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
可以提供实例化一个Controller
,methodName
可以确定调用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());}
createJFinalConfig
是JFinalFilter
内部方法,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);}
基本就是调用JFinalConfig
的configXXX
,具体如何做可以参考前面Routes
、Interceptors
和Handler
小节。
接着来关注initActionMapping
部分逻辑。
private void initActionMapping() { actionMapping = new ActionMapping(Config.getRoutes(), Config.getInterceptors()); actionMapping.buildActionMapping();}
基本就是调用ActionMapping
的buildActionMapping
方法了,buildActionMapping
可以参考前面ActionMapping
小节。
最后关注initHandler
部分逻辑。
private void initHandler() { Handler actionHandler = new ActionHandler(actionMapping, constants); handler = HandlerFactory.getHandler(Config.getHandlers().getHandlerList(), actionHandler);}
关于HandlerFactory
的使用可以参考Handler
小节。
执行完JFinalFilter
的init
就为整个项目的路由解析做好了准备了。
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);}
这里的handler
是JFinal.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
再执行匹配。para
用urlPara[0]
收集起来。
最后不管是否匹都配返回。
回到ActionHandler.handle()
方法,用获得的Action
进行调用处理请求。
new ActionInvocation(action, controller).invoke();
至此,jFinal
的路由解析模块就分析完了。
5 Thanks
以上用于分析的jFinal版本是jfinal-1.8-bin-with-src.jar。
感谢转神提供的案例
感谢豪的提点和帮助