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
。
jfinal
com.jfinal.core.JFinalFilter
configClass
com.app.common.Config
jfinal
/*
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
;handlers
拥有所有的handler
,内部结构是List
。Routes
定义了两个容器,
private final Map> map = new HashMap>();
private final Map viewPathMap = new HashMap();
对外提供了多个重载的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 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
链表的头节点最后保存在JFinal
的handler
变量,见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
链的使用是在JFinalFilter
的doFilter
方法中,下文会提及。
2.4 ActionMapping
ActionMapping
负责将Routes
和Interceptors
组织起来,整合后的结果存到在ActionMapping
的mapping
成员变量(MapAction
是最终用于处理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 excludedMethodName = buildExcludedMethodName();
InterceptorBuilder interceptorBuilder = new InterceptorBuilder();
Interceptor[] defaultInters = interceptors.getInterceptorArray();
interceptorBuilder.addToInterceptorsMap(defaultInters);
for (Entry> 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;
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
就为整个项目的路由解析做好了准备了。
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。
感谢转神提供的案例
感谢豪的提点和帮助
支持一下文章作者吧!