您的位置:首页 > 其它

jFinal路由解析源码分析

2018-01-29 22:00 429 查看
转自https://segmentfault.com/a/1190000004141126

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;
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。

感谢转神提供的案例

感谢豪的提点和帮助

支持一下文章作者吧!



2015年12月13日发布
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐