实习期间接触到了早有耳闻的Webx,于是很自然的按照官方文档运行了一个 Demo ,粗略的阅读了一下代码,发现并不能很快的梳理清Web请求的处理逻辑,以及视图层和控制层之间的关联关系。execute()为什么会被调用?doChinese()为什么会被调用?这是我当时的两个疑问,为了解答这些疑问,需要理解Web请求在Webx中经历的处理流程,为此我阅读了Webx部分源码,并以此文作为小结。

在分析之前,我们需要强调Webx的一个重要设计理念——约定胜于配置。“约定”即规则,规则是预先定义的,工程师只需要按着规则来做事,就不需要额外的“配置”。对比其它一些框架,往往每增加一个页面,都需要在配置文件中增加若干行内容。

注意,本篇文章仅用于解答上文提出的两个疑问,更多Webx的设计理念及原理性知识请参阅官方文档

execute()为什么会被调用?

本文以解析请求 http://localhost:8080/webx/simple/say_hi.do为例来回答第一个问题,在SayHi类中execute()处添加断点,启动Tomcat,程序执行至断点处,查看此时当前线程的函数调用栈,如下图所示(截取了部分)。

想要更好的理解该函数调用栈的信息,需要先从理论上对Webx处理一个Web请求有一个认知。

  • 首先,增强requestresponsesession的功能,并把它们打包成更易使用的RequestContext对象。
  • 其次,它会调用相应子应用的pipeline,用它来做进一步的处理。

pipelineWebx的一种机制,这种机制给予开发者极大的自由来自定制处理请求的流程。pipeline由一系列的valve构成,一个请求在获取响应(Module)前需经历这一系列valveWebxURL映射成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()中,首先根据moduleTypemoduleName创建modulekeyWebx会对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方法中,一切开始变得清晰起来:首先尝试获取moduleObjectexecute()方法,若存在,那么当前target对应的Module的最关键部分也就构建完成了。至此,第一个疑问“为什么execute()会被调用”得到了解答

获取了Module之后,executeAndReturn()调用execute(),完成页面主体内容的处理。executeAndReturn()源码如下。

    public Object executeAndReturn() throws Exception {
        return this.executeMethod.invoke(this.moduleObject, this.log);
    }

doChinese()为什么会被调用?

此时Web请求URLhttp://localhost:8080/webx/multievent/say_hello_1/chinese.do ,因此相应的target即为/multievent/say_hello_1/chinese.do。与第一个问题的请求处理流程相同,Webx首先根据当前target去获取Module,若无法获取,那么认为当前target是由两部分组成:eventeventModuleName,解析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开头的方法,构建eventNameMethod的映射关系,存储在一个Map中,注意我们的target已被划分为eventeventModuleName,在executeAndReturn()中根据eventMap中获取相应的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