初探Webx之约定胜于配置
实习期间接触到了早有耳闻的Webx
,于是很自然的按照官方文档运行了一个 Demo ,粗略的阅读了一下代码,发现并不能很快的梳理清Web
请求的处理逻辑,以及视图层和控制层之间的关联关系。execute()
为什么会被调用?doChinese()
为什么会被调用?这是我当时的两个疑问,为了解答这些疑问,需要理解Web
请求在Webx
中经历的处理流程,为此我阅读了Webx
部分源码,并以此文作为小结。
在分析之前,我们需要强调Webx
的一个重要设计理念——约定胜于配置。“约定”即规则,规则是预先定义的,工程师只需要按着规则来做事,就不需要额外的“配置”。对比其它一些框架,往往每增加一个页面,都需要在配置文件中增加若干行内容。
注意,本篇文章仅用于解答上文提出的两个疑问,更多Webx
的设计理念及原理性知识请参阅官方文档。
execute()为什么会被调用?
本文以解析请求 http://localhost:8080/webx/simple/say_hi.do
为例来回答第一个问题,在SayHi
类中execute()
处添加断点,启动Tomcat
,程序执行至断点处,查看此时当前线程的函数调用栈,如下图所示(截取了部分)。
想要更好的理解该函数调用栈的信息,需要先从理论上对Webx
处理一个Web
请求有一个认知。
- 首先,增强
request
、response
、session
的功能,并把它们打包成更易使用的RequestContext
对象。 - 其次,它会调用相应子应用的
pipeline
,用它来做进一步的处理。
pipeline
是Webx
的一种机制,这种机制给予开发者极大的自由来自定制处理请求的流程。pipeline
由一系列的valve
构成,一个请求在获取响应(Module
)前需经历这一系列valve
。Webx
将URL
映射成target
,由开发者定制不同类型的target
所匹配的pipeline
。例如,当前请求的target
为/simple/say_hi.do
,与之对应的pipeline
如下所示。
<when>
<!-- 执行不带模板的screen,无layout。 -->
<pl-conditions:target-extension-condition extension="do" />
<pl-valves:performAction />
<pl-valves:performScreen />
</when>
结合pipeline
与函数调用栈,在执行execute()
之前,请求经历了一系列invoke()
与invokeNext()
。首先执行<pl-valves:performAction />
对应PerformActionValve
中的invoke()
与invokeNext()
,由于当前请求并不包含表单提交,所以PerformActionValve
不做任何操作。接着执行<pl-valves:performScreen />
对应的PerformScreenValve
中的invoke()
,这也是我们分析的重点,该invoke
方法对应的源码如下所示。
public void invoke(PipelineContext pipelineContext) throws Exception {
TurbineRunData rundata = TurbineUtil.getTurbineRunData(this.request);
if(!rundata.isRedirected()) {
this.setContentType(rundata);
Object result = null;
try {
result = this.performScreenModule(rundata);
} finally {
this.setOutputValue(pipelineContext, result);
}
}
pipelineContext.invokeNext();
}
进入performScreenModule
方法,其源码如下所示(省略部分无关代码)。
protected Object performScreenModule(TurbineRunData rundata) {
PerformScreenValve.ModuleFinder finder = new PerformScreenValve.ModuleFinder(rundata.getTarget());
rundata.setLayoutEnabled(true);
try {
Module e = finder.getScreenModule();
if(e != null) {
ScreenEventUtil.setEventName(rundata.getRequest(), finder.event);
Object var4;
try {
if(!(e instanceof ModuleReturningValue)) {
e.execute();
return null;
}
var4 = ((ModuleReturningValue)e).executeAndReturn();
} finally {
...
}
return var4;
}
...
}
...
}
performScreenModule
首先根据当前target
获取对应的Module
。在Webx
中,Module
承担了用户提交数据的接收与处理、请求的控制与转发、处理结果的展示等重要功能。Webx
缺省定义了三种类型的Module
。
action
:主要用于处理用户提交的数据,以及请求的控制与转发。screen
:主要用于处理页面的主体内容。control
:主要用于处理页面的部分内容,特别是可重用的内容。
我们接着进入finder.getScreenModule()
,源码如下。
public Module getScreenModule() throws ModuleLoaderException {
this.moduleName = PerformScreenValve.this.getModuleName(this.target);
Module module = PerformScreenValve.this.moduleLoaderService.getModuleQuiet("screen", this.moduleName);
if(module != null) {
return module;
} else {
if(this.parseEvent()) {
module = PerformScreenValve.this.moduleLoaderService.getModuleQuiet("screen", this.eventModuleName);
if(module instanceof ModuleEvent) {
return module;
}
}
return null;
}
}
打断点一步一步深入下去,进入getModuleQuiet()
,源码如下。
public Module getModuleQuiet(String moduleType, String moduleName) throws ModuleLoaderException {
ModuleKey moduleKey = new ModuleKey(moduleType, moduleName);
moduleType = moduleKey.getModuleType();
moduleName = moduleKey.getModuleName();
if(this.cacheEnabled.booleanValue()) {
Module moduleObject = (Module)this.moduleCache.get(moduleKey);
if(moduleObject != null) {
return moduleObject;
}
}
Object var10 = null;
Module module = null;
ModuleFactory[] arr$ = this.factories;
int len$ = arr$.length;
int i$;
for(i$ = 0; i$ < len$; ++i$) {
ModuleFactory adapter = arr$[i$];
var10 = adapter.getModule(moduleType, moduleName);
if(var10 != null) {
break;
}
}
if(var10 != null) {
if(var10 instanceof Module) {
module = (Module)var10;
} else {
ModuleAdapterFactory[] var11 = this.adapters;
len$ = var11.length;
for(i$ = 0; i$ < len$; ++i$) {
ModuleAdapterFactory var12 = var11[i$];
module = var12.adapt(moduleType, moduleName, var10);
if(module != null) {
break;
}
}
}
}
if(module == null && var10 != null) {
throw new UnadaptableModuleException("Could not adapt object to module: type=" + moduleType + ", name=" + moduleName + ", class=" + var10.getClass());
} else {
if(this.cacheEnabled.booleanValue() && module != null) {
this.moduleCache.put(moduleKey, module);
}
return module;
}
}
在getModuleQuiet()
中,首先根据moduleType
和moduleName
创建modulekey
,Webx
会对Module
进行缓存,因此首先根据moduleKey
从缓存中获取Module
。如果缓存中还不存在当前target
对应的Module
,继续深入,进入DataBindingAdapterFactory
类的adapt
方法,源码如下。
public Module adapt(String type, String name, Object moduleObject) {
ModuleInfo moduleInfo = new ModuleInfo(type, name);
Class moduleClass = moduleObject.getClass();
Method executeMethod = this.getMethod(moduleClass, "execute");
if(executeMethod != null) {
FastClass fc = FastClass.create(moduleClass);
FastMethod fm = fc.getMethod(executeMethod);
boolean skippable = "action".equalsIgnoreCase(type);
return new DataBindingAdapter(moduleObject, this.getMethodInvoker(fm, moduleInfo, skippable));
} else {
return null;
}
}
在adapt
方法中,一切开始变得清晰起来:首先尝试获取moduleObject
的execute()
方法,若存在,那么当前target
对应的Module的最关键部分也就构建完成了。至此,第一个疑问“为什么execute()
会被调用”得到了解答。
获取了Module
之后,executeAndReturn()
调用execute()
,完成页面主体内容的处理。executeAndReturn()
源码如下。
public Object executeAndReturn() throws Exception {
return this.executeMethod.invoke(this.moduleObject, this.log);
}
doChinese()为什么会被调用?
此时Web
请求URL
为 http://localhost:8080/webx/multievent/say_hello_1/chinese.do
,因此相应的target
即为/multievent/say_hello_1/chinese.do
。与第一个问题的请求处理流程相同,Webx
首先根据当前target
去获取Module
,若无法获取,那么认为当前target
是由两部分组成:event
和eventModuleName
,解析target
的源码如下。
private boolean parseEvent() {
int slashIndex = this.target.lastIndexOf("/");
int dotIndex = this.target.lastIndexOf(".");
if(slashIndex > 0) {
this.event = this.target.substring(slashIndex + 1, dotIndex > slashIndex?dotIndex:this.target.length());
this.eventModuleName = PerformScreenValve.this.getModuleName(this.target.substring(0, slashIndex));
return true;
} else {
return false;
}
}
继续打断点深入源码,Webx
会将eventModuleName
作为moduleName
获取Module
。同样的,首先尝试获取execute()
,如果获取不到便获取所有的EventHandler
,源码如下。
private Map<String, Method> getEventHandlers(Class<?> moduleClass) {
HashMap handlers = null;
Method[] arr$ = moduleClass.getMethods();
int len$ = arr$.length;
for(int i$ = 0; i$ < len$; ++i$) {
Method method = arr$[i$];
if(this.checkMethod(method)) {
String methodName = method.getName();
if(methodName.length() > 2 && methodName.startsWith("do") && Character.isUpperCase(methodName.charAt(2))) {
String eventName = StringUtil.toCamelCase(methodName.substring(2));
if("perform".equals(eventName)) {
eventName = null;
}
if(handlers == null) {
handlers = CollectionUtil.createHashMap();
}
handlers.put(eventName, method);
}
}
}
return handlers;
}
至此我们理解为什么doChinese()
为什么会被调用了:获取当前target
对应的screen
的类的所有以do
开头的方法,构建eventName
与Method
的映射关系,存储在一个Map
中,注意我们的target
已被划分为event
和eventModuleName
,在executeAndReturn()
中根据event
从Map
中获取相应的Method
并执行,源码如下(省略部分无关代码)。
public Object executeAndReturn() throws ModuleEventException, ModuleEventNotFoundException {
Object result = null;
String event = this.getEventName(this.request);
MethodInvoker handler = null;
if(event != null) {
handler = (MethodInvoker)this.handlers.get(event);
}
if(handler == null) {
handler = (MethodInvoker)this.handlers.get((Object)null);
}
...
result = handler.invoke(this.moduleObject, this.log);
...
}
所以第二个疑问也得到了解答 :P