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
分析jFinal的路由解析逻辑必须从jFinal的一般项目配置入手,配置的作用是为路由解析提供支持的。和一般Java Web MVC框架不同的是jFinal没有采用xml配置的形式,但不是不需要配置,还是需要提供一个JFinalConfig的继承实现类,实现configXXX方法来支持配置初始化,初始化的入口是JFinalFilter的init方法。
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();被实例化。
上面的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);在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的抽象方法实现。
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方法小节。
在进入JFinalFilter的init和doFilter方法之前,我们将上面的提到的几个概念梳理一下。
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。
与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;
}
//...
} 在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方法中,下文会提及。
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.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封装完成的,具体细节就不展开了。
最后来看看两个重要的流程。直接上代码
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;
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);
}基本就是调用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就为整个项目的路由解析做好了准备了。
还是直接上代码
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的路由解析模块就分析完了。