Webx初学者教程

Webx Beginner Guide

Posted by Bo on April 26, 2016

写在两年之后

这份文档是2016年春天我在离开阿里之前写的,当时我每周要花6个小时坐大巴往来杭州和上海,在车上无聊之余写了本文。它只包含一些入门级的分析,在离开阿里之后的工作中再也没有接触过Webx,因此没有后续,也不再更新了。愿此文对你有帮助,以尽我绵薄之力。

0.为什么要写这份文档

那么我们就开始了。

和你们一样,我也是一个初学者。

我于2014年7月加入阿里,在加入阿里之前,我使用的编程语言一直是C和C++,以及少量的OC,从未写过Java代码。入职之后马上投入到Webx相关应用的开发中去,我感到举步维艰,每天想的就是两件事情:

  • 这特么是啥。
  • 那特么是啥。

熟悉上手Webx并能够自如的进行web应用开发花费了我几个月的时间,我将这一点很大程度上归咎于文档的匮乏,所以希望撰写一份面向初学者的文档。在此之前,我相信你们都读到了Michael Zhou——也就是Webx原作者——所撰写的《Webx框架指南》,但是,就我个人而言,我认为这是一份对初学者相当不友好的文档。在我看来,它更像是一本工具书而非初学者指南,容易让刚刚接触Webx的新人感到一头雾水:我的Http请求经过了哪些处理,最终调用了哪个方法?为什么Webx抛出了这些异常?我曾经在这些问题上浪费了大量的时间。对于像我这样资质鲁钝的人来说,更需要有一本由浅入深介绍这个框架及其实现细节的文档,因此,我决定在业余时间,将一些对我最初接触Webx时的迷惑的解答,以及对Webx的一些粗浅的理解写下来,以飨后人。

所以,我的目的就是,让每一个没有接触过Webx乃至Servlet开发的孩子都能少走弯路,用最快的速度,去未来。

本文分析的版本是Webx 3.0.9,即citrus-webx-all-3.0.9.jar,你可以在中央仓库中找到它的全部源代码。Webx的活跃开发者并不多,但是在本文写作的时候,Webx的最高版本已经达到了3.2.4,所以如果你手中的代码和文中分析的不一致,请勿惊慌。

下面是对阅读本文的一些建议:

  • 如果你从未接触过Webx框架,甚至从未接触过Java开发,建议你从第一章开始阅读。
  • 如果希望了解Spring或者Webx的启动过程,请阅读第二章。
  • 如果被“为啥我的断点死活进不来这个方法”之类的问题困扰,说明你和很多人一样踩到了Webx的“约定优于配置”的坑,请阅读第三章。

在阅读本文的过程中,如果发现疏漏和错误,请发邮件至zhangbodut@gmail.com

1.背景知识

在开始前,有一些基础知识是必须了解的,像我一样从来没有接触过企业级Java开发,甚至从来没有接触过Java开发的同学可能对这些知识一无所知。本章提到的这些知识可能有些你已经了解,有些你还不了解,有些你甚至达到了专家级水平,不论如何,我将会对这些知识进行简单的介绍,但是不会过于深入,若需要了解它们的细节,请参阅相关书籍。若你已经完全了解这些知识,请直接略过。

1.1 Java

这一节主要是面向像我一样的、工作语言从C/C++转到Java的同学。在刚入职的时候,被人丢过来一堆代码拍在脸上,那酸爽……(┬_┬)算了不说了说多了都是泪。

是时候放出这张图了,非原创,侵删:

1

1.1.1 注解与反射

在最开始接触业务代码的时候,最让我感到迷惑不解的就是Annotation,常见的译名是注解。这个神秘的小圆圈让我这条刚从校园里走出来的开发狗迷惑了很久。那个时候从来没有人告诉我这是个什么鬼,所以我想简单介绍一下。注解由jdk5引入,是附加于源代码上的一小段元信息。按照我个人的理解,注解本质上是一个class,在Java源代码中出现的每个注解相当于这个class的一个实例。例如,@Autowired是一个常见的注解:

public @interface Autowired {

	/**
	 * Declares whether the annotated dependency is required.
	 * <p>Defaults to <code>true</code>.
	 */
	boolean required() default true;

}

这是对AutoWired注解的声明,在org.springframework.beans.factory.annotation包中。 在这里,AutoWired实际上是一个以特殊方式声明的class:不使用通常的声明方式public class AutoWired而使用了public @interface AutoWired。同时,这个class有一个字段required。当它被添加于Java代码中时,每次出现都是AutoWired class的一个实例。例如:

public class Example{
    @AutoWired(required=true)
    private int member;
}

在这段代码中,int member这个字段带有一些额外的信息(元信息),即,一个AutoWired实例,且该AutoWired实例中,required字段为true。是的!你没有听错!这个源代码带了一个AutoWired类的实例!并且可以通过一些黑科技 (╭ ̄3 ̄)╭在运行时获取这个信息!是的!运行时!

什么是黑科技呢,就是说,当你需要一个Example实例的时候,你不需要用new,甚至连import都不需要,而是直接告诉虚拟机,给老子创建一个xxx包的Example类的实例!

虚拟机说:好的大王,没问题大王!

你说:这个Example里面有哪些字段?(注意,这个时候并没有import它)

虚拟机说,Example里面有个名字叫member的int类型成员。哦对了,这个成员脸上有个required=true的AutoWired注解实例。

你说:把这个member给老子赋值成42,以表达向宇宙的敬意!

虚拟机说:好的大王,没问题大王!

这个时候围观的你掀了桌子(╯°Д°)╯ ┴─┴ :等会,member可是private的,虚拟机你特么是知法犯法啊!

虚拟机把桌子摆摆好 ┬─┬ ノ( ‘ - ‘ノ):要么怎么说是黑科技呢。

这个黑科技,就是反射。

反射允许在运行时动态地操作类以及其成员(函数和属性),包括但不限于创建指定的类的实例,查找并调用指定的方法,为指定的属性赋值等。例如:

import java.lang.reflect.Method;
public class test{
    public static void main(String[] args) throws Exception {
		Class c = Class.forName("java.util.ArrayList");//寻找一个叫做java.util.ArrayList的类
		
		Object instance = c.newInstance();//直接创建这个类的实例
	
		Method isEmptyMethod = c.getDeclaredMethod("isEmpty");//去这个类中找一个叫isEmpty的方法

		boolean isEmpty = (Boolean) isEmptyMethod.invoke(instance);//调用该方法
	
		System.out.println(instance.toString());
		System.out.println(isEmpty);
    }
}

这些黑科技实际上是Spring容器实现控制反转(Inverse of Control,IoC)的方式,后面会讲到。

1.2 Maven

关于Maven,最好的书当然是《Maven实战》,读完这本书基本上就可以解决所有你遇到的有关Maven的问题了。如果你没有读过,也没关系,我想为初学者简单介绍一下Maven,确保在之后的阅读中不会因为缺少这些知识云里雾里。

首先我来问一个问题:源代码是如何变成服务器上可被运行的项目的?假如没有Maven,一切都手工来做的话,我们需要:

  • 从版本控制系统(SVN/Git)取出最新的代码
  • 编译,将java文件变成字节码文件。
  • 测试,确保所有的测试用例通过。
  • 打包成为容器期望的格式。
  • 部署到服务器上。

这就是一个项目构建的生命周期。以上这一系列事情的困难在于:

  • 这全都是编写源代码之外的事情,手工重复这些步骤意味着低效和容易出错。
  • 通常一个项目直接或间接地依赖了上百个jar包,必须确保在编译和运行时,所有依赖的jar包的版本正确,并且已经被放置在了正确的位置上。ClassLoader不会管这些事情,它们所做的一切就是:“喂,老王,我要的那个jar包呢?”

Maven出色的解决了这两个问题。

首先,它为构建引入了一个统一的接口,抽象了构建的生命周期,并为生命周期中的绝大部分任务提供了实现的插件。你不需要去关心这个生命周期里发生的事情,只需要把代码放在指定的位置,执行一条命令,整个构建过程就完成了。

其次,它为Java世界里的依赖引入了经纬度(groupId和artifactId),不再需要下载指定版本的jar包、拷贝到lib目录这些事情,只需要定义我的项目依赖了哪些构件(artifact),Maven会自动帮你管理依赖,并将jar包下载到正确的位置上。

1.3 HTTP与HttpServletRequest

你每打开一个网页,都要发生几十次上百次HTTP请求与响应。HTTP是我们这个五彩缤纷的互联网大厦的基石。HTTP协议定义于RFC2616,全称是Hyper Text Transport Protocol,超文本传输协议。为啥叫超文本呢,因为它除了能够传输文本,还能传输有格式的文本(HTML),以及二进制字节流。

这件事情的本质是:你丢过去一个HTTP请求,Web服务器丢回给你一个HTTP响应。

请求里面装的信息如下:

  • 请求方法(例如GET、POST等),请求的URL和参数
  • 一些附加信息(即Request Header Fields,例如Cookie、User-Agent等)

响应里装的信息如下:

  • 响应的状态状态码(1xx~5xx)
  • 响应的附加信息(即Response Header Fields,例如Set-Cookie等)
  • 响应的实体(例如HTML,下载的文件)

多数情况下,这些请求和响应都是以字节流形式存在的文本字符串。在这个过程中,框架做的事情实际上是屏蔽这些细节,让你站在更高的层次上面对和处理这些个玩意儿。这个更高层次的功能抽象,就是HttpServletRequest和HttpServletResponse。

Java Servlet对HTTP请求和HTTP响应的抽象表示为HttpServletRequest和HttpServletResponse。作为Java Web开发者,有必要对这两个类的所有方法烂熟于心。在它们之中,可以找到所有HTTP请求和响应的数据。我要从HTTP请求中拿Cookie怎么办?没问题,HttpServletRequest里有。我要知道HTTP请求的Referer怎么办?没问题,HttpServletRequest里面有。我想让这个HTTP响应被浏览器接收的时候自动转为下载文件怎么办?没问题,HttpServletResponse可以做到。我想要让这个页面跳转到另外一个页面怎么办?没问题,HttpServletResponse可以做到。总之,一切HTTP请求中包含的信息都可以从HttpServletRequest里面拿到,一切你希望做出的HTTP响应都可以通过HttpServletResponse实现。

1.4 Servlet与Servlet容器

既然是做Java Web开发的,那么对Servlet的了解必不可少。下面是Java Servlet Specification对其的定义。

A servlet is a Java. technology-based Web component, managed by a container, that generates dynamic content.

这段话恰到好处的指明了Servlet和Servlet容器的关系。你可以将Servlet容器想象成一个盒子,盒子上面有两条导线,分别是HttpServletRequest和HttpServletResponse。Servlet就是一个程序,可以看作盒子里的电源,它恰好有两个孔,可以将上述的两根导线接入。一旦接好之后,整个容器就工作起来了,像是下面这样。

servlet

实际上,和我们直观的感觉不一致的是,HttpServletResponse并不是Servlet接收HttpServletRequest之后产生的返回值,二者都是由容器产生并交给Servlet的。Servlet按照HttpServletRequest中的内容,产生相应的响应,放入HttpServletResponse中。本质上,Servlet只是一个接口,或者说API,真正工作的是实现该接口的一个Java类的实例,由容器负责实例化。当请求到来时,容器将调用该实例的service方法。

Servlet.java

public interface Servlet {

    public void init(ServletConfig config) throws ServletException;

    public ServletConfig getServletConfig();
    
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;

    public String getServletInfo();

    public void destroy();
}

实际上,一个容器可以包含不止一个Servlet。容器负责实例化每个Servlet,并在HTTP请求到来时决定交给哪一个Servlet。像是下面这样。

servlet

一个典型的事件序列如下:

  • 客户端(例如浏览器)发送一个HTTP请求到Web服务器(例如nginx)。
  • Web服务器收到请求,交给Servlet容器(例如Tomcat)处理。Web服务器和Servlet容器可以是一台主机上的同一个进程,也可以是不同的进程,或者位于不同主机中。
  • Servlet容器根据配置决定交给哪个Servlet进行处理,调用对应Servlet的service方法。
  • Servlet执行自身的逻辑,修改HttpServletResponse。
  • 完成处理后,Servlet容器保证HTTP响应全部离开缓冲区(ensures that the response is properly flushed),并将控制权交还给Web服务器。

这也是我们所有的基于Java的Web应用的底层细节。

1.5 xml和xsd

更多细节,请阅读Xml系列教程

1.6 Spring和SpringExt

更多细节,请阅读The Spring Framework - Reference DocumentationExtensible XML authoring(可能需要翻墙 FuckGFW

2 启动

Web容器的启动一直是我很困惑的一个地方,一直到很久以后自己慢慢单步撸才了解了整个过程。

2.1 从web.xml开始

既然是一个初学者,那么我们就从web.xml开始。这是一切Java Servlet Web应用的起点。这货是个啥呢?简单来说,就是你写给Web容器的启动说明书。容器就按照你写的这份说明书来启动。web.xml定义于Java Servlet Specification,正式的名称是部署描述符(Deployment Descriptor)。这个文件给出了Servlet容器的基本参数,容器就依靠这个文件进行启动。我们来看web.xml的其中一段(要是你的web.xml里面没有这一段,证明你的web.xml是盗版,请将相关人员扭送派出所):

代码片段2-1

<listener>
	<listener-class>com.alibaba.citrus.webx.context.WebxContextLoaderListener</listener-class>
</listener>

这里是整个Webx应用的起点,声明了一个listener。顾名思义,listener就是监听者,它竖起耳朵监听了容器的事件,当容器发生一些事情时,它将会做出预先定义的动作。看WebxContextLoaderListener这个类名:Webx环境加载器监听者。我们可以知道,这个监听器是负责在启动的时候加载Webx环境的(Context比较通用的译名是上下文,但是我个人觉得这个译名过于晦涩难懂,所以翻译成环境)。我们打开它:

代码片段2-2

public class WebxContextLoaderListener extends ContextLoaderListener {
	...
}

public class ContextLoaderListener implements ServletContextListener {
	...
}
public interface ServletContextListener extends EventListener {
	
	public void contextInitialized ( ServletContextEvent sce );

	public void contextDestroyed ( ServletContextEvent sce );
}

上面为了方便直接把三个相关类放到一起了。可见,WebxContextLoaderListener继承了ContextLoaderListener,而ContextLoaderListener实现了EventListener接口。

这个接口是干嘛用的呢?在这里我想多废话一句,有关接口以及实现。引用知乎上的一个比喻:一个接口好比是发射导弹这个功能,一个实现了这个接口的类好比是战斗机,它的子类好比是F-16战斗机,而它的的实例就是某架特定的战斗机。很多时候,飞行员(用户)需要的只是发射导弹这个功能,只要你给他的一架飞机(类的实例)实现了发射导弹这个功能(接口)即可。

容器为了能够在自身发生变化后(启动、销毁)通知自己的小弟去做相应的事情,只需要这些小弟能够完成以下的特定功能:

  • 容器初始化完成后做的事情
  • 容器销毁后做的事情

这就是EventListener接口。只要你的listener实现了这个接口,容器才不顾管你是阿猫阿狗孙悟空,爱谁谁,反正我初始化和销毁的时候就要调用这两个接口方法。就是这么任性!哼!(。・`ω´・)

所以,这个过程总结起来就是,容器根据说明书里的这段描述实例化WebxContextLoaderListener,然后在初始化完成的时候调用该实例的contextInitialized方法,从而实现了一种通知机制。下面是WebxContextLoaderListener的全部源代码。全。部。

代码片段2-3

public class WebxContextLoaderListener extends ContextLoaderListener {
    @Override
    protected final ContextLoader createContextLoader() {
        return new WebxComponentsLoader() {

            @Override
            protected Class<? extends WebxComponentsContext> getDefaultContextClass() {
                Class<? extends WebxComponentsContext> defaultContextClass = WebxContextLoaderListener.this
                        .getDefaultContextClass();

                if (defaultContextClass == null) {
                    defaultContextClass = super.getDefaultContextClass();
                }

                return defaultContextClass;
            }
        };
    }

    protected Class<? extends WebxComponentsContext> getDefaultContextClass() {
        return null;
    }
}

WebxContextLoaderListener这个类简单到你都会觉得,(⊙0⊙) 卧槽,难道我屌屌的大阿里使用的框架就是这么个玩意儿实现的?

废话,当然不是。

其中createContextLoader方法简单粗暴,大多数初学者可能不太熟悉这种匿名类的写法,我来详细解释一下:

  • createContextLoader返回的是一个ContextLoader类的实例。
  • 方法里new出来一个WebxComponentsLoader的实例作为返回值,因此,这个返回值实际上是WebxComponentsLoader类的实例。看这个名字,Webx组件加载器,就知道,这货只是个加载器,类似于PC的引导程序。
  • 方法把这个实例new出来的时候,顺便覆盖了一下WebxComponentsLoader类的getDefaultContextClass方法,因此,返回的实例实际上是一个WebxComponentsLoader类的匿名子类的实例,且这个子类覆盖了getDefaultContextClass方法。

ContextLoader是Spring的一个类,大家可以打开一下,看看那四十来行的英文注释,OK,可以关掉了。我来给大家翻译一下这段英文注释的意思:别人都是扯淡的,只有我是干活的(Performs the actual initialization work for the root application context)。那么他究竟在弄啥咧?我们暂且不管,继续刚刚分析的启动流程。

上文说到,只要实现了EventListener接口,就可以在容器初始化完成的时候得到通知,因此,我们看看WebxContextLoaderListener这个类对contextInitialized方法的实现。容易发现,它通过继承ContextLoaderListener方法实现了这个方法,因此这个方法会在容器初始化的时候被容器调用:

代码片段2-4

public class ContextLoaderListener implements ServletContextListener {

	private ContextLoader contextLoader;

	/**
	 * Initialize the root web application context.
	 */
	public void contextInitialized(ServletContextEvent event) {
		this.contextLoader = createContextLoader();
		this.contextLoader.initWebApplicationContext(event.getServletContext());
	}
	...
}

这个方法做了两件事情:用createContextLoader方法新建一个contextLoader成员并且调用其initWebApplicationContext方法。显然,ContextLoader就是环境加载器,主要作用就是加载并启动下文会讲到的WebApplicationContext。

除此之外,在这里,还有一些微妙的事情发生了:由于WebxContextLoaderListener覆盖了createContextLoader方法,因此在我们的启动过程中,实际上调用的是代码片段2-3中的createContextLoader方法。所以,这个新建的过程返回的是上文分析过的WebxComponentsLoader类的匿名子类的实例,从而,调用的initWebApplicationContext方法也是WebxComponentsLoader类的initWebApplicationContext方法。

2.2 BeanFactory与ApplicationContext

上文提到,Webx框架启动时,被调用的实际上是WebxComponentsLoader的initWebApplicationContext方法,所以我们在这里首先简单介绍一下WebApplicationContext。

在此之前,我想首先简单介绍一下BeanFactory和ApplicationContext。

为什么说是简单呢,因为:

  • 懂这个的人太多,不好浑水摸鱼。
  • 这个话题要是展开讲,一不小心三百页就没了。
  • Spring的文档极其丰富,去书店,十本里面有十一本半都在讲Spring(所以Webx你说你文档这么匮乏你对得起谁)。

现在请你牢牢记住三个类:BeanFactory、ApplicationContext和WebApplicationContext。

现在请你牢牢记住三个类:BeanFactory、ApplicationContext和WebApplicationContext。

现在请你牢牢记住三个类:BeanFactory、ApplicationContext和WebApplicationContext。

大家为什么爱用Spring?因为Spring的依赖注入可以帮你管理你的Bean实例的生命周期,在需要的时候,你可以直接向系统索取。

这就好比是,你在宇宙大爆炸的虚无中,想吃一个煎饼果子。那么你要做什么呢?首先,你需要一个世界。于是你创造了世界和世间万物,这还不够,你又创造了各种物理定律让世间万物能够相互影响,一切搭建好以后,这个世界才能运转。可惜,这个世界还没有初始化,满世界跑的都是单细胞生物。

后来,出现了一个上帝,他帮你创造好了世间万物和物理定律,你来了,说,我要吃一个煎饼果子。

上帝说,给你。

你说,不要葱。

上帝说,给你。

你说,加一百个鸡蛋。

上帝说,给你。

你说,这个卖煎饼果子的小妹好像长得不错……

上帝说,喂,110吗?

这里的上帝就是BeanFactory。屁话少说,放码上来。

public interface BeanFactory {
	...
	Object getBean(String name) throws BeansException;

	Object getBean(String name, Class requiredType) throws BeansException;

	Object getBean(String name, Object[] args) throws BeansException;

	boolean containsBean(String name);

	boolean isSingleton(String name) throws NoSuchBeanDefinitionException;

	boolean isPrototype(String name) throws NoSuchBeanDefinitionException;

	boolean isTypeMatch(String name, Class targetType) throws NoSuchBeanDefinitionException;

	Class getType(String name) throws NoSuchBeanDefinitionException;

	String[] getAliases(String name);
}

可见,BeanFactory极其简洁的定义了与Bean有关的一些功能,在需要的时候,你可以直接向其索取。

ApplicationContext是BeanFactory的子接口,提供了一些扩展的功能。

WebApplicationContext是ApplicationContext的子接口,提供了一些与Web容器相关的功能。

由于大家都想吃煎饼果子,都不愿意创造世界,所以Spring越来越流行。baobao也是,只不过,他不满足于Spring提供的煎饼果子,他想自己造煎饼果子。于是他重写了initWebApplicationContext方法,改变了煎饼果子,哦不,是框架的初始化过程。

2.3 WebxComponentsLoader

来看被调用的initWebApplicationContext方法:

@Override
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) throws IllegalStateException,BeansException {
    this.servletContext = servletContext;
    init();

    return super.initWebApplicationContext(servletContext);
}

卧槽,原来作者也没有从头开始造煎饼果子,而是装模做样的做了一些事情之后(init方法)又去买了上帝家的煎饼果子(调用了父类的initWebApplicationContext方法)。这个init方法仅仅是从ServletContext中读取了一个配置项而已。这个配置项以后会有用。这就是面向对象的精髓:复用,绝不重复发明轮子。

接下来,程序流程似乎又回到了ContextLoader——也就是我们上文提到的、唯一在干活的类里。它的造煎饼果子方法(initWebApplicationContext)太长,并且做的事情也和方法名所指出的一样:初始化WebApplicationContext——就是我们上文提到的、具有网络相关功能的BeanFactory。这里我只贴一行:

this.context = createWebApplicationContext(servletContext, parent);

这行代码的功能正如方法名,新建一个WebApplicationContext。这里我想额外讲一句,我们迄今为止看到的代码都极其清晰,原因是因为,作者使用方法名来注释代码,阅读代码就像阅读文档一样清晰,这是非常值得学习的。写出让机器理解的代码并不难,难的是写出让人理解的代码。引用《重构——改善现有代码的设计》一文中的一句话:

当你感觉想要撰写注释时,请先尝试重构,试着让所有的注释都显得多余。

打开createWebApplicationContext方法,其中有一行:

Class contextClass = determineContextClass(servletContext);

这行代码的功能也正如方法名指出的那样:决定Context究竟使用哪个类的实例。这是因为,WebApplicationContext本质上是个接口,是不能实例化的,必须决定实例化时所使用的类是什么。Spring对此的默认实现是XmlWebApplicationContext,也就是最常用的读取xml配置文件来建立Spring容器环境的类。但是,请注意,此时正在运行的ContextLoader实例是WebxComponentsLoader的实例,它覆盖了这个方法:

@Override
protected final Class<?> determineContextClass(ServletContext servletContext) throws ApplicationContextException {
    String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);

    if (contextClassName != null) {
        try {
            return ClassUtils.forName(contextClassName);
        } catch (ClassNotFoundException ex) {
            throw new ApplicationContextException("Failed to load custom context class [" + contextClassName + "]",
                                                  ex);
        }
    } else {
        return getDefaultContextClass();
    }
} 我知道大家看代码都很头疼,所以我尽力解释一下每段代码的含义,让我头疼一次,以后就可以不再头疼。这段代码是说,如果ServletContext的初始化参数指定了类名,那么就使用该类,否则就使用WebxComponentsContext类作为实际上启动的Spring容器的ApplicationContext。到这里,其实ContextLoader的事情都已经结束了,Webx通过自定义的WebxComponentsContext代替了Spring默认的XmlWebApplicationContext。

2.4 Spring容器的启动

众所周知,Webx是基于Spring的,因此Spring容器的启动是必须的。这个启动过程我仍然不会详细讲述,原因还是因为上面那三个。我只请大家关注一下AbstractApplicationContext的refresh方法,这个方法是对Bean定义资源的载入过程。注意这个类的名字,AbstractApplicationContext。这个类名暗示了另外一种思想:用接口提供功能描述,用抽象类复用代码。与之类似的还有java.util.List和java.util.AbstractList。

public void refresh() throws BeansException, IllegalStateException {
	synchronized (this.startupShutdownMonitor) {
		// Prepare this context for refreshing.
		prepareRefresh();

		// Tell the subclass to refresh the internal bean factory.
		ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

		// Prepare the bean factory for use in this context.
		prepareBeanFactory(beanFactory);
		try {
			// Allows post-processing of the bean factory in context subclasses.
			//被WebxComponentsContext所覆盖
			postProcessBeanFactory(beanFactory);

			// Invoke factory processors registered as beans in the context.
			invokeBeanFactoryPostProcessors(beanFactory);

			// Register bean processors that intercept bean creation.
			registerBeanPostProcessors(beanFactory);

			// Initialize message source for this context.
			initMessageSource();

			// Initialize event multicaster for this context.
			initApplicationEventMulticaster();

			// Initialize other special beans in specific context subclasses.
			onRefresh();

			// Check for listener beans and register them.
			registerListeners();

			// Instantiate all remaining (non-lazy-init) singletons.
			finishBeanFactoryInitialization(beanFactory);

			// Last step: publish corresponding event.
			//被WebxComponentsContext所覆盖
			finishRefresh();
		}

		catch (BeansException ex) {
			...
		}
	}
}

我很少贴这么长的代码,但是这个方法实在是太重要了,所以不得不破例。又是像文档一样清晰的代码,其中的英文注释我没有翻译出来,是因为我自认为不能准确的翻译出来它们表达的意思。有两个方法标注了WebxComponentsContext所覆盖,就是这两个 方法实现了Webx组件的加载。先来看第一个:

@Override
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
    super.postProcessBeanFactory(beanFactory);
    getLoader().postProcessBeanFactory(beanFactory);
}

作者先从上帝家买了煎饼果子,然后才进行自己的加工。这个加工过程是什么呢?来看被调用的WebxComponentsLoader.postProcessBeanFactory方法,这个方法里面废话太多,就不贴了,总结一下,它完成的工作是实例化了一个Bean,其类型是WebxComponentsLoader$WebxComponentsCreator内部类,用于加载Webx的组件定义。这个方法通过调用WebxComponentsLoader.createComponents方法来完成这个过程。看到这里你肯定跟我有一样的疑问,为啥要这么绕一下呢,直接让postProcessBeanFactory方法调用不就得了。

这个问题的答案在WebxComponentsLoader.postProcessBeanFactory方法的注释里,大家自己去看吧,中文写的。

此外,WebxComponentsContext的父类WebxApplicationContext还覆盖了一个方法:

public class WebxApplicationContext extends ResourceLoadingXmlWebApplicationContext {
    @Override
    protected String[] getDefaultConfigLocations() {
        if (getNamespace() != null) {
            return new String[] { WEBX_COMPONENT_CONFIGURATION_LOCATION_PATTERN.replace("*", getNamespace()) };
        } else {
            return new String[] { WEBX_CONFIGURATION_LOCATION };
        }
    }
}

Spring容器在启动的时候会默认从WEB-INF/applicationContext.xml中读取配置,WebxComponentsContext通过覆盖这个方法将默认的配置文件名改为WEB-INF/webx.xml。

自此,我们才真正进入Webx的世界。

2.5 Webx的世界

上文提到,我们终于通过WEB-INF/webx.xml和WebxComponentsLoader.createComponents方法进入了Webx的世界。

WebxComponentsLoader.createComponents方法代码很长,但是做的事情只有一件:创建Webx组件(这不是废话么,名字是干什么的)。Webx中的组件是一组相关功能的集合,本质上是一个WebApplicationContext。组件的来源有两个:

  • 自动扫描WEB-INF目录下的配置文件生成
  • webx.xml中指定

其中自动扫描是这样工作的:扫描WEB-INF目录下所有匹配webx-*.xml的文件,其中*所代替的字符串为组件名。例如,WEB-INF下有一个名为webx-home.xml的文件,那么它代表一个名为home的组件。

而webx.xml中指定的方式是这样的:

<services:webx-configuration>
	...省略其他配置...
	<services:components defaultComponent="home" >
		<services:component name="test" path="/test">
		</services:component>
	</services:components>		
</services:webx-configuration>

指定了一个名为test的组件,它的path属性为字符串”/test”,这个属性是干嘛的第三章会讲到(那个看上去有点奇怪的defaultComponent属性也会在那里讲到)。

将两种方式获取到的组件名集合取并集,就是最终将要处理的组件名集合。注意,在webx.xml中指定的组件名,其配置文件的名字也是webx-*.xml的形式,并不能随意指定。你可能要问了,既然自动扫描能够获得所有的Webx组件,为什么还要在webx.xml中指定?

原因有两个:

  • 自动扫描是可以关掉的,这个时候,只能通过配置的方式来获取组件
  • 组件的componentPath属性只能通过webx.xml设定,细节详见下一章

现在我们不妨打开WEB-INF下面的webx.xml和webx-*.xml。一般来说,它们做的是导入bean、加载服务之类的事情。其中的services命名空间的xml元素,例如是通过webx自定义的xml解析器来解析的,这个对应关系可以在citrus-webx-all-3.0.9.jar中的META-INF文件夹找到。对于这些服务的具体配置方法,请查阅《Webx框架指南》。

这些配置被解析完之后,将会生成指定的模块的实例,用于在运行时提供对应的服务。

例如,在我的webx-home.xml中,有一句是酱紫的:

<!-- 执行管道。 -->
<beans:import resource="common/pipeline.xml" />

这将会导入pipeline.xml。你肯定已经阅读过《Webx框架指南》,知道pipeline用于控制请求处理的过程,那导入这个文件会生成怎样的内部实例呢?我们以《Webx框架指南》中的pipeline.xml为例来详细分析一下。

<services:pipeline xmlns="http://www.alibaba.com/schema/services/pipeline/valves">

	<!-- 初始化turbine rundata,并在pipelineContext中设置可能会用到的对象(如rundata、utils),以便valve取得。-->
	<prepareForTurbine />

	<!-- 设置日志系统的上下文,支持把当前请求的详情打印在日志中。 -->
	<setLoggingContext />

	<!-- 分析URL,取得target。 -->
	<analyzeURL homepage="homepage" />

	<!-- 检查csrf token,防止csrf攻击和重复提交。 -->
	<checkCsrfToken />

	<loop>
		<choose>

			<when>
			<!-- 执行带模板的screen,默认有layout。 -->
				<pl-conditions:target-extension-condition extension="null, vm, jsp" />
				<performAction />
				<performTemplateScreen />
				<renderTemplate />
			</when>

			<when>
				<!-- 执行不带模板的screen,默认无layout。 -->
				<pl-conditions:target-extension-condition extension="do" />
				<performAction />
				<performScreen />
			</when>

			<otherwise>
				<!-- 将控制交还给servlet engine。 -->
				<exit />
			</otherwise>
		
		</choose>
		
		<!-- 假如rundata.setRedirectTarget()被设置,则循环,否则退出循环。 -->
		<breakUnlessTargetRedirected />
	</loop>
</services:pipeline>

如前所述,我们想要知道应该生成什么样的Bean,于是我们打开citrus-webx-all-3.0.9.jar中的/META-INF/services.bean-definition-parsers文件:

pipeline=com.alibaba.citrus.service.pipeline.impl.PipelineDefinitionParser

所以这个标签由PipelineDefinitionParser负责解析。打开它,可以知道,在解析标签的时候,Webx会生成一个```com.alibaba.citrus.service.pipeline.impl.PipelineImpl```实例。其中的每一个Valve标签,即<prepareForTurbine\>、<setLoggingContext\>等,被按照类似的方式解析成一个个的```com.alibaba.citrus.service.pipeline.Valve```实例,所有的Valve实例组成一个数组,赋值给PipelineImpl实例的valves成员。这样,就完成了从pipeline.xml得到初始化完毕的pipeline实例的过程。

换句话说,在这个过程中,一个完整的、包含Valve的Pipeline的实例被建立了。管道和阀门已经建立完毕,接下来只需要通水了。pipeline的具体流程分析将留在第三章进行。

加载完组件之后,我们刚刚提到的WebxComponentsContext覆盖的两个方法中的第一个方法就结束了,我们来看它覆盖的第二个方法finishRefresh:

@Override
protected void finishRefresh() {
    super.finishRefresh();
    getLoader().finishRefresh();
}

和之前类似,都是先去买上帝家的煎饼果子然后自己加工。这两个被覆盖的方法暗示了一个信息:Spring容器所做的事情Webx一件不落的全部做了。这也是Webx和Spring保持完全兼容的原因。来看它自己对煎饼果子的加工方法:

/** 初始化所有components。 */
public void finishRefresh() {
    components.getWebxRootController().onFinishedProcessContext();

    for (WebxComponent component : components) {
		...
        WebxComponentContext wcc = (WebxComponentContext) component.getApplicationContext();
        WebxController controller = component.getWebxController();

        wcc.refresh();
        controller.onFinishedProcessContext();
    }
	...
}

这段代码说的是,对Webx的每个组件调用refresh方法。我相信看到这里,大家内心心里都是崩溃的,这特喵的在搞神马!下面我试着来详细解释一下。

按照Spring的设计,在创建的时候,ApplicationContext可以通过指定parent的方式来实现父子关系,然后可以通过getParent接口获得父ApplicationContext。如果没有特殊的设置,Spring容器只会实例化一个ApplicationContext,即根容器。而之前我们实例化并refresh的是WebxComponentsContext,它是ApplicationContext的实现类,所以它就是根容器。该容器是通过读取webx.xml并使用Webx自定义的解析器解析建立的。

前已述及,每个webx-*.xml对应一个组件,Webx将每个组件实例化为一个WebxComponentContext,它也是ApplicationContext的实现类。注意这个名字,WebxComponentContext和WebxComponentsContext,只差 了一个s。在每个WebxComponentContext建立时,Webx将其父ApplicationContext指定为刚刚建立的WebxComponentsContext实例。这样,就建立了一个级联关系:

  • 父容器(WebxComponentsContext,通过webx.xml建立)
  • 若干子容器(即组件,WebxComponentContext,通过webx-*.xml建立)

父容器的refresh方法之前已经被调用过了,子容器(各组件)的还没有,因此这里对每个组件调用refresh方法。

这就是《Webx框架指南》25页的内容:初始化级联的Spring容器。

2.6 将HTTP请求路由给Webx

上面的所有步骤都是在启动Webx框架,在启动完成后,另一个问题来了:HTTP请求是如何被路由给Webx框架来处理的?

事实上,Webx框架对这件事情是无能为力的。HTTP请求的分发并不由Webx框架控制,而是由Servlet容器控制。因此,web.xml还需要做另外一件事情。

<filter>
	<filter-name>webx</filter-name>
	<filter-class>com.alibaba.citrus.webx.servlet.WebxFrameworkFilter</filter-class>
</filter>
...
<filter-mapping>
	<filter-name>webx</filter-name>
	<url-pattern>*.htm</url-pattern>
</filter-mapping>

那什么是Filter呢?Java Servlet Specification对它的定义如下:

Filters are Java components that allow on the fly transformations of payload and header information in both the request into a resource and the response from a resource

我斗胆翻译一下:

Filter是一种Java组件,允许在运行中改变对资源的请求和返回的响应中的payload和header信息。

也就是说,它用来接收处理HttpRequest并给出相应的HttpResponse。

OK,就是它了。Java Servlet Specification指出,一个有效的Filter必须实现javax.servlet.Filter接口。Servlet容器负责实例化web.xml中声明的Filter并按照其被声明的顺序组成Filter链。额外的,只有设置了,对应的请求才会路由给指定的Filter。

因此,所有匹配*.htm的HTTP请求现在都会交给com.alibaba.citrus.webx.servlet.WebxFrameworkFilter来处理了。至于具体的处理流程,已经属于运行过程了,因此我们留到第三章分析。

2.7 总结

上面这一大坨一大坨啰里吧嗦的代码摆在这里,假如你不是真心想要深入了解原理的,肯定看不下去。所以我简单的总结一下:

  • 用WebxComponentsContextLoader取代SpringMVC的ContextLoader进行环境加载
  • 上面这个货新建一个WebxComponentsContext作为根容器,这是SpringMVC默认的XmlWebApplicationContext的派生类;另外,这个根容器默认读取webx.xml作为配置项
  • 除了webx.xml,每个匹配webx-*.xml的配置文件,都会对应上述WebxComponentsContext根容器的一个子容器
  • 在webx.xml和webx-*.xml中定义的beans将按照Spring容器的方式管理,而Webx自定义的服务,例如pipeline、form等,将使用Webx自定义的解析器解析为对应的bean实例,安装到Webx框架中,以备未来使用
  • Enjoy!

3 运行

Webx是个大坑,很大一部分原因就是Webx所推崇的约定优于配置造成的。这是一柄双刃剑。相对而言,SpringMVC要简单的多,在它的世界里,绝大部分情况下,请求是是通过明确的URL映射来映射给处理方法的。要想知道这个HTTP请求被哪个方法处理了,只要在代码里搜索一下URL,就可以顺藤摸瓜,轻松获得程序的控制流程。

但是Webx不。他搞了一套约定。大多数情况下,这套约定工作的很好,你只要简单的学习一下这些约定,一样可以完成工作。少数情况下,它会出现各种各样稀奇古怪的问题,排查它们就像是陷到烂泥潭里,让人无从下手。

因此,这一章就从代码的角度,详细分析Webx的坑人约定,以及整个请求的处理流程。

3.1 入口

第二章的结尾,我们提到,Webx是通过com.alibaba.citrus.webx.servlet.WebxFrameworkFilter来接收和处理请求的。很容易的,我们找到了所有请求的入口:

代码片段3-1

WebxFrameworkFilter.java

@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws IOException, ServletException {
	...省略废话代码
	getWebxComponents().getWebxRootController().service(request, response, chain);
	...省略废话catch语句
}

聪明的你肯定一眼就看出来关键在service方法,我们打开它:

代码片段3-2

AbstractWebxRootController.java

public final void service(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws Exception {
    RequestContext requestContext = null;
    ...
    requestContext = assertNotNull(getRequestContext(request, response), "could not get requestContext");

    if (isRequestPassedThru(request) || !handleRequest(requestContext)) {
        giveUpControl(requestContext, chain);
    }
    ...
}

这里有两个地方值得关注:首先,Webx将HttpServletRequest和HttpServletResponse封装成了com.alibaba.citrus.service.requestcontext.RequestContext对象;其次,真正执行对请求的处理的方法是handleRequest。熟悉SpringMVC的同学可以知道,这个方法类似于SpringMVC的HandlerAdapter.handle方法,通过调用指定的处理器来处理业务逻辑。 RequestContext的注释说:

包含了request、response和servletContext几个对象的集合体,用来表示当前HTTP请求的状态。

我们把它简单的理解成将HttpServletRequest和HttpServletResponse相关的东西打包在一起,暂且不去管它。来看handleRequest方法的实现:

WebxRootControllerImpl.java

@Override
protected boolean handleRequest(RequestContext requestContext) throws Exception {
    HttpServletRequest request = requestContext.getRequest();
    ...省略废话注释...
    String path = ServletUtil.getResourcePath(request);

    // 再根据path查找component
    WebxComponent component = getComponents().findMatchedComponent(path);
    boolean served = false;

    if (component != null) {
        try {
            WebxUtil.setCurrentComponent(request, component);
            served = component.getWebxController().service(requestContext);
        } finally {
            WebxUtil.setCurrentComponent(request, null);
        }
    }

    return served;
} 我们终于进入了调用的细节部分。

3.2 开始执行请求

相信你肯定已经知道,对于/xxx/yyy形式的请求,Webx会去寻找/xxx/yyy.java,用它的运行结果渲染/xxx/yyy.vm。然而,很多情况下,仅仅知道这一点并没有什么卵用。想象一下下面的场景吧,你按照约定放好了所有的东西,运行,咦,怎么没有生效?那把它换个位置试一下,哦,这样还是不行,奇怪,别的项目里面这么写是可以的啊,那我再改个名字试试。啊哈哈,看上去它正常工作了,我可以宣布问题搞定了。

靠随机修改代码的方式来获取正确的输出,这不是编程,这是跳大神。这种行为有一个学名,叫做voodoo programming(巫毒编程)。

我们想要完完全全的知道,请求是怎样被解析到执行的代码上的。这也是本节所要达到的目标。

3.2.1根据URL查找组件

还记得么,我们上一章提到过,每个webx-*.xml对应一个ApplicationContext,在Webx的世界中被称为组件(Component),这个组件的名字就是webx-*.xml里面对应的那个*。例如,有两个配置文件webx-aaa.xml和webx-bbb.xml,那么Webx将会为根容器创建两个子容器(组件),这两个组件的名字分别为aaa和bbb。

匹配的第一步,是首先决定这个URL交给哪个组件来处理。

你肯定知道,HTTP是通过URL来定位资源的,如下:

“http:” “//” host [ “:” port ] [ abs_path [ “?” query ]]

也就是说,当你访问http://www.google.com.hk:80/search?q=FuckGFW这个URL的时候,www.google.com.hk是host,80是HTTP的默认端口,可以省略,/search是绝对路径,?之后的内容是查询字符串。

String path = ServletUtil.getResourcePath(request);

Webx首先通过这行代码获取绝对路径,也就是port之后到?之前的这一段字符串。在上面的例子中,这个path就是/search。如果是http://aaa.bbb.com/xxx/yyy/zzz.jsonp?key=value,那么这个path就是/xxx/yyy/zzz.jsonp。得到绝对路径之后,就按照该路径去寻找匹配的组件,前方代码预警!

com.alibaba.citrus.webx.context.WebxComponentsLoader

public WebxComponent findMatchedComponent(String path) {
        if (!path.startsWith("/")) {
            path = "/" + path;
        }

        WebxComponent defaultComponent = getDefaultComponent();
        WebxComponent matched = null;

        // 前缀匹配componentPath。
        for (WebxComponent component : this) {
            if (component == defaultComponent) {
                continue;
            }

            String componentPath = component.getComponentPath();

            if (!path.startsWith(componentPath)) {
                continue;
            }

            // path刚好等于componentPath,或者path以componentPath/为前缀
            if (path.length() == componentPath.length() || path.charAt(componentPath.length()) == '/') {
                matched = component;
                break;
            }
        }

        // fallback to default component
        if (matched == null) {
            matched = defaultComponent;
        }

        return matched;
    }

每个组件有一个名为componentPath的属性,上述代码的意思是,在所有的组件中寻找一个匹配的组件,使得该组件的componentPath是刚刚获取的绝对路径的前缀。若未找到匹配组件,则使用默认组件。还记得么,第二章我们提到过组件的生成过程,除了自动扫描组件之外,webx.xml中可以指定组件名以及属性。下面的配置中,名为test的组件的componentPath属性就是”/test”,而名为home的组件(webx-home.xml)是默认组件。

webx.xml

<services:webx-configuration>
	...省略其他配置...
	<services:components defaultComponent="home" >
		<services:component name="test" path="/test">
		</services:component>
	</services:components>		
</services:webx-configuration> 找到匹配的组件之后,请求就被交给该组件去处理。这种匹配类似于web.xml中的filter-mapping元素:

<filter-mapping>
	<filter-name>ssoFilter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

你可能会问了,既然Servlet已经能够完成类似的功能了,为什么Webx还要自己实现一套呢?我想,这是另外一层划分,是为了让请求处理策略更加灵活。例如,pipeline中,是按照URI的后缀名来决定控制流经过的Valve的,可是如果想要让两个相同后缀名的URI经过不同的Valve应该怎么办?这个时候上述基于组件的策略就可以派上用场了,每个组件可以拥有自己的一套pipeline,实现不同的处理流程。

3.2.2 Pipeline

组件——即WebxComponent——有一个方法是获取用来处理请求的WebxController:

com.alibaba.citrus.webx.WebxComponent

public interface WebxComponent {	
    ...
    /** 取得用来处理当前component请求的controller。 */
    WebxController getWebxController();
    ...
}

每个组件可以指定自定义的WebxController,方法是在webx.xml中指定:

webx.xml

<services:webx-configuration>
	...省略其他配置...
	<services:components defaultComponent="home" >
		<services:component name="test" path="/test">
			<services:controller class="com.myframework.MyController" />
		</services:component>
	</services:components>		
</services:webx-configuration>

如果你不指定controller,Webx将使用com.alibaba.citrus.webx.impl.WebxControllerImpl来代替。这件事情发生在com.alibaba.citrus.webx.context.WebxComponentsLoader.createComponents方法中。

WebxController是个啥玩意呢?你可以理解成SpringMVC中的Controller,是用来处理请求的。

com.alibaba.citrus.webx.WebxController

public interface WebxController {
    void init(WebxComponent component);

    void onRefreshContext() throws BeansException;

    void onFinishedProcessContext();

    boolean service(RequestContext requestContext) throws Exception;
}

其中的service方法执行对请求的处理。还记得它的参数类型RequestContext么?这货是对HttpServletRequest和HttpServletResponse的封装。这个方法的默认实现如下(也就是Webx框架的默认实现):

com.alibaba.citrus.webx.impl.WebxControllerImpl

public boolean service(RequestContext requestContext) throws Exception {
    PipelineInvocationHandle handle = pipeline.newInvocation();

    handle.invoke();

    // 假如pipeline被中断,则视作请求未被处理。filter将转入chain中继续处理请求。
    return !handle.isBroken();
}

没错,就是Pipeline!对请求的实际处理转化为了Pipeline的开始,其中的pipeline就是上一章中根据pipeline.xml建造的Pipeline的实例。这个方法形象一点的比喻就是,创建一套新的管道系统,拧开第一个阀门,通水。

而水将会流向哪里,将由Valve负责。

3.3 Module

在继续我们接下来对Valve的分析之前,必须先对Webx框架内部的一些概念做出说明。只有在了解了这些概念之后,我才能愉快的带你装逼带你飞。

这一节非常重要,几乎所有的Webx的坑爹约定都可以在这里得到体现。

Here we go!

3.3.1 Module是个什么鬼

打开com.alibaba.citrus.service.moduleloader.Module接口,代码如下:

/**
 * 代表一个模块。
 *
 * @author Michael Zhou
 */
public interface Module {
    /** 执行模块。 */
    void execute() throws Exception;
}

必须指出,这个注释等于什么都没说。

其实,它是一个抽象的“可以被执行的东西”。至于这个可以被执行的东西到底是个什么鬼,我们会在后面慢慢讲述。现在你只需要知道,在Webx默认的处理流程中,不论是screen,还是action,最终都是包装成了一个Module,一个“可以被执行的东西”,对它的execute方法的调用就对应了相应的处理方法的调用。

3.3.2 Module的获取

如果你读过源代码,那么你可能会在执行Action处理的PerformActionValve类中的这一行处感到不解:

moduleLoaderService.getModule(ACTION_MODULE, action).execute();

9E665159_527D_4862_8836_B269282B8AED

啥?这是干什么玩意儿呢?咋一行代码就完了?我裤子都脱了你就给我看这个?

别急别急,我们来捋一捋。

这里有一个moduleLoaderService的getModule方法,就是根据Module的类型和名字获取Module这个“可以被执行的东西”。默认情况下,moduleLoaderService是com.alibaba.citrus.service.moduleloader.impl.ModuleLoaderServiceImpl的实例,我们打开来看,找到获取Module的方法是:

public Module getModuleQuiet(String moduleType, String moduleName) throws ModuleLoaderException {
	//...省略废话
    // 从factory中装载
    Object moduleObject = null;
    Module module = null;

    for (ModuleFactory factory : factories) {
        moduleObject = factory.getModule(moduleType, moduleName);

        if (moduleObject != null) {
            break;
        }
    }

    // 通过适配器转换接口
    if (moduleObject != null) {
        if (moduleObject instanceof Module) {
            module = (Module) moduleObject; // 假如moduleObject直接实现了接口
        } else {
            for (ModuleAdapterFactory adapter : adapters) {
                module = adapter.adapt(moduleType, moduleName, moduleObject);

                if (module != null) {
                    break;
                }
            }
        }
    }
	//...省略废话
    return module;
}

解释一下,这个方法执行了以下两个步骤:

  • 使用配置的一些ModuleFactory去尝试获取指定的moduleName对应的moduleObject,注意,得到的是一个Object,并不是Module。
  • 如果获取的Object已经实现了Module,直接使用之,否则使用配置的一些ModuleAdapterFactory将moduleObject适配为Module。
  • 返回上一步得到的Module。

下面来详细分析这些过程。

(1)ModuleFactory是一个抽象的“生产moduleObject的工厂”。Webx官方对它的实现在com.alibaba.citrus.service.moduleloader.impl.factory包下,只有一个,就是AbstractBeanFactoryBasedModuleFactory。在Webx容器启动的时候,它会扫描所有的action/layout/screen类,并将其注册为bean,其名称为类名的相对路径。例如,类按照如下路径进行组织:

 +- xxx
    \- yyy
       +- screen
	   |  \- home
	   |     +- Index
	   |
       |- action
	   |  +- PlayAction
	   |  \- go
	   |     \-GoAction
	   |
       \- layout

那么PlayAction对应的bean名字就是“PlayAction”,而GoAction对应的名字是go.GoAction。因此,上述工厂生产moduleObject的过程实际上就是在BeanFactory——Spring的核心——中查找指定名称的bean。如果找到,则返回之。

(2)拿到上一步获取的moduleObject之后,我们可以看到,上述代码使用了一系列ModuleAdapterFactory来对moduleObject进行适配。这是一种设计模式——适配器模式的典型应用。

Adapter:Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.

适配器模式:将一个类的接口转换为客户希望的另外一个接口,使得那些原本由于接口不兼容而不能一起工作的类可以一起工作。

注意,这里的“接口”的语义不是Java语言中的“接口”的语义。这里的“接口”指的是,在面向对象系统中,一个类对外暴露出来的、可以被外界操作的行为的集合。

在我们的这个例子里,ModuleLoaderService期望获得的是一个实现了Module接口的实例,从而能在接下来的过程中调用其execute方法。但是很不幸,我们给它的是由用户编写的、五花八门的类实例。这个时候,ModuleAdapterFactory就粉墨登场了。

ModuleAdapterFactory的接口定义如下:

/**
 * 将任意类型的module对象转换成<code>Module</code>接口的适配器工厂。
 *
 * @author Michael Zhou
 */
public interface ModuleAdapterFactory {
    /** 将对象转换成module接口。 */
    Module adapt(String type, String name, Object moduleObject) throws ModuleLoaderException;
}

看它的adapt方法,可以把ModuleAdapterFactory理解成一个傲娇的工厂:客户要求他生产指定类型和名称的产品(Module),并且供给它原料(moduleObject),但是生产过程全凭它自己的喜好,如果原料没达到它的要求,它可能根本不生产任何成品(返回null)。

它的官方实现都在com.alibaba.citrus.service.moduleloader.impl.adapter包下。来看一下他的继承体系:

 +- ModuleAdapterFactory
    \- AbstractDataBindingAdapterFactory
       +- AbstractModuleEventAdapterFactory
	   |  +- ActionEventAdapterFactory
	   |  \- ScreenEventAdapterFactory
	   |
	   \- DataBindingAdapterFactory

默认配置下,ModuleLoaderServiceImpl会实例化三个ModuleAdapterFactory:

  • DataBindingAdapterFactory
  • ActionEventAdapterFactory
  • ScreenEventAdapterFactory

Webx框架就像是一个想把moduleObject加工成Module的客户,它会拿着moduleObject依次询问上面这三家工厂:你生产不?你不生产我就找下家了啊。只要有一家工厂答应生产(返回值不为null),他就开心地拿着产品走了,不再询问其他的工厂。

下面详细介绍这些官方实现的ModuleAdapterFactory。

3.3.2.1 DataBindingAdapterFactory

这个工厂对原料比较挑剔,只有满足以下条件的moduleObject才可被这个工厂适配:

  • moduleObject有一个名为execute的方法
  • 该方法为public或protected,且非static

它的adapt方法的返回值类型是DataBindingAdapter,它实现了Module接口。返回的这个DataBindingAdapter实例中,除了包含moduleObject,还包含通过反射获取的moduleObject的execute方法的handler,调用返回的Module的execute方法就等于调用moduleObject的execute方法。

现在,你知道为什么Webx会默认调用screen类的execute方法了吧!

3.3.2.2 ScreenEventAdapterFactory与ActionEventAdapterFactory

通常来说,DataBindingAdapterFactory位于第一位,但是它对于原料非常挑剔:欲进行适配的moduleObject中必须包含名为execute的方法,如果类中没有这样一个方法,它就直接返回null。

最要命的是,它只调用execute方法,难道我给每个业务逻辑都写一个只有一个execute方法的类?

这种情况下,AbstractModuleEventAdapterFactory都就闪亮登场了。

AbstractModuleEventAdapterFactory是ScreenEventAdapterFactory和ActionEventAdapterFactory的共同父类,是一个抽象的骨架。它认为一个类中每个符合条件的方法都是一个“event”。它的适配过程是这样的:

(1)使用反射,查找moduleObject中所有符合以下条件的方法:

  • 方法名以字符串“do”开头,且第三个字符是大写;
  • public或者protected,且非static;

(2)获得所有满足上一条的方法的集合,转换为eventName到方法handler的映射。什么是eventName呢?就是将方法名的“do”前缀去掉,第三个字母小写得到的字符串。 额外的,对名为“doPerform”的方法进行特殊处理,作为默认方法,即未来查找方法失败时调用的方法。

例如,moduleObject包含三个方法:doPerform,doPlayFootball,doPlayTennis,那么得到的映射就是:

  • 默认→doPerform方法
  • playFootball→doPlayFootball方法
  • playTennis→doPlayTennis方法

(3)若该类中包含名为beforeExecution和afterExecution的方法,获取其handler备用。注意,同样要求这两个方法是public或者protected,且非static。

在完成上面的处理后,adapt方法返回的Module产品中含有moduleObject和上面找到的方法handler。在它的execute过程中,它会动态决定“event”,从而调用相应的方法handler。

那ScreenEventAdapterFactory与ActionEventAdapterFactory有什么区别呢?区别就在于:

  • ScreenEventAdapterFactory生产的产品类型是ScreenEventAdapter,ActionEventAdapterFactory生产的产品是ActionEventAdapter。这两种产品获得“event”的方式不一样,细节详见下文分析。
  • 我们提到过,客户要求ModuleAdapterFactory生产产品(Module)时会指定类型(即adapt方法的moduleType参数)。ScreenEventAdapterFactory只生产类型为screen的产品,而ActionEventAdapterFactory只生产类型为action的产品。

3.3.3 Module的执行

上面讲到,在Webx框架的内部,不论是screen还是action,最终都是将实际的业务处理对象转变成一个Module并调用其execute方法。那么问题就来了,这个过程具体是怎样的呢?为什么调用它的execute方法就能完成实际的处理方法的调用呢?本节将会给出解答。

(1)对于DataBindingAdapterFactory,它的返回值类型是实现了Module接口的DataBindingAdapter,来看它的execute方法:

public void execute() throws Exception {
    executeMethod.invoke(moduleObject, log);
}

其中的executeMethod就是在适配过程中,通过反射获得的moduleObject的execute方法handler,至于它为什么不用官方的反射,而是cglib,可能是因为性能问题。

(2)对于另外两个工厂,上文提到,它们的返回值类型分别是它的返回值的类型是ScreenEventAdapter和ActionEventAdapter。它们的共同父类是AbstractModuleEventAdapter,同样实现了Module接口,来看它的execute方法(有省略):

/** 执行一个module,并返回值。 */
public Object executeAndReturn() throws ModuleEventException, ModuleEventNotFoundException {
    Object result = null;
    String event = getEventName(request);
    MethodInvoker handler = null;

    // 查找精确匹配的方法
    if (event != null) {
        handler = handlers.get(event);
    }

    // 查找fallback method
    if (handler == null) {
        handler = handlers.get(null);
    }

    // 未找到合适的handler method,报错
    if (handler == null) {
        throw new ModuleEventNotFoundException("Could not find handler method for event: " + event);
    }

    // 执行preHandler
    if (preHandler != null) {
        preHandler.invoke(moduleObject, log);
    }

    try {
        // 执行event handler
        result = handler.invoke(moduleObject, log);
    } finally {
        // 执行postHandler
        if (postHandler != null) {
            postHandler.invoke(moduleObject, log);
        }
    }

    return result;
}

可以看到,这是一个模板方法。它的调用过程是这样的:

  • 获取eventName
  • 查找eventName对应的方法
  • 如果未找到,查找默认方法(doPerform方法)
  • 如果仍未找到,抛出异常,中止执行流程
  • 如果Module中包含名为“beforeExecution”的方法,则调用之。如果该方法抛出异常,中止执行流程
  • 执行刚刚找到的方法
  • 如果Module中包含名为“afterExecution”的方法,调用之。

那么,什么是eventName呢?screen和action对此的定义是不同的,获取eventName的方法我们将在相关Valve中详细分析。

合上代码,我不禁陷入了深深的沉思。坑爹呢这是!

_

beforeExecution、afterExecution、doPerform这种约定都是硬编码在Java代码里的呀!!!这特喵的不看源代码谁知道你们这种鬼约定啊!!!

3.3.4 参数的注入

至此,几乎所有有关Module调用的问题都找到了答案。

为什么说是“几乎”,是因为还有一个问题。

我们都知道,通过@Param注解,可以将HTTP请求中的参数自动绑定到处理方法上。那么,这个过程是怎么实现的呢?

3.4 Valve

一说到Valve,我的脑海里首先浮现出的是这幅图片(听说能看懂这幅图片的人都已经结婚了):

valve

Valve接口的实现非常简单,只有一个方法,就是invoke,这个方法用于告诉一个阀门,到你了,你决定水的流向:

com.alibaba.citrus.service.pipeline.Valve

/**
 * 代表pipeline中的一个“阀门”。
 * <p>
 * 如同真实世界里的水管中的阀门,它可以控制和改变液体的流向,<code>Valve</code> 也可以控制pipeline中后续valves的执行。
 * <code>Valve</code>可以决定是否继续执行后续的valves,或是中断整个pipeline的执行。
 * </p>
 *
 * @author Michael Zhou
 */
public interface Valve {
    void invoke(PipelineContext pipelineContext) throws Exception;
}

不过通常我们都继承com.alibaba.citrus.service.pipeline.support.AbstractValve来创造自己的Valve,它是一个空实现:

/**
 * <code>Valve</code>的通用基类,提供了基于spring的初始化功能。
 *
 * @author Michael Zhou
 */
public abstract class AbstractValve extends BeanSupport implements Valve {
}

而这也是Webx自带的若干Valve的实现方法。下面将会详细分析常见Valve的具体实现。

3.4.1 PrepareForTurbineValve

在我们通常的pipeline中,它通常都是第一个。那么他是做什么的呢?

我必须万分悲痛的告诉大家,我也不知道。

        pipelineContext.setAttribute("rundata", rundata);

        for (Map.Entry<String, Object> entry : Utils.getUtils().entrySet()) {
            pipelineContext.setAttribute(entry.getKey(), entry.getValue());
        }

        pipelineContext.invokeNext();

上面就是这个Valve的代码。全部。看下来之后,给人的感觉就是:

_

总结一下,这个Valve把一个TurbineRunData和一堆工具类放进了pipeline的上下文里,方便之后的Valve调用。这是标准的责任链模式。这样做的好处就是,极大的降低了耦合,每个Valve只和PipelineContext打交道,而不知道其他Valve的存在。

那TurbineRunData是个啥呢?打开看一下注释:

可被应用程序使用的request scope数据接口。

看来我们并不需要关心这个东西是什么,我们只需要知道,这是一个运行上下文,需要的时候,可以从这货里面取出来一些只在当次请求内有效的数据。OK,让我们继续。

3.4.2 AnalyzeURLValve

看名字就知道,这个Valve用于解析URL。这个类太长,我就不贴完整的代码了。

  • 从URL获取资源路径。例如,对于请求http://aaa.bbb.com/xxx/yyy/zzz.jsonp?key1=value1&key2=value2,资源路径指的就是/xxx/yyy/zzz.jsonp。额外的,这个资源路径是截断组件路径前缀之后的结果。例如,某个组件配置了组件路径为/test,那么对于请求http://aaa.bbb.com/test/yyy/zzz.jsonp?key1=value1&key2=value2,得到的资源路径就不是/test/yyy/zzz.jsonp,而是/yyy/zzz.jsonp。关于组件的分析,详见3.2.1节。

  • 上一步如果获得的资源路径是/,那么将资源路径设置为AnalyzeURLValve的homepage属性。该属性可在pipeline.xml中设置。
  • 将获得的资源名转换为驼峰格式。有两点需要注意:第一,只转换资源名,而不转换路径;第二,转换为驼峰形式的时候将替换掉所有的空格和下划线。例如,资源路径/x_x_x/yYy/aBc_def.jsonp转换后的结果是/x_x_x/yYy/aBcDef.jsonp。
  • 根据刚刚转换后的资源路径,获得target,并放入TurbineRunData中。
  • 从请求中,获得action,并放入TurbineRunData中。
  • 从请求中,获得actionEvent,并放入TurbineRunData中。

下面仔细分析一下最后三个步骤。

(1)根据资源名获取target。target是个什么鬼,baobao在文档中提到了,这是一个抽象的东西,可能是模板名,也可能是其他东西,我们暂且不管,来看代码:

target = mappingRuleService.getMappedName(EXTENSION_INPUT, pathInfo);

mappingRuleService是由Webx框架注入的,它的这个方法用来执行一个指定的映射。打开它的默认实现,是通过一系列的映射规则来执行从一个字符串到另外一个字符串的映射。这个映射规则的抽象,叫做MappingRule。默认的MappingRule的实现都在com.alibaba.citrus.service.mappingrule.impl.rule包下。

现在,我们要求获得一个类型为EXTENSION_INPUT、名为当前的pathInfo的映射结果。从这个类型就可以猜到,它试图根据输入的扩展名类型来决定target。

对于这个类型,默认的实现是ExtensionMappingRule,简单来说,它的映射规则是:资源名的扩展名(extension)按照如下方法映射:htm→vm, vhtml→vhtml, vm→vm。如果扩展名不是上述三者之一,则保持不变。

例如,如果请求是http://aaa.bbb.com/xxx/yyy/zzz.htm?key1=value1&key2=value2,那么得到的target就是/xxx/yyy/zzz.vm

(2)从请求的参数中,获得action。具体方法是,如果请求参数中包含一个名为“action”的参数,就将它的值取出来,并使用action类型对应的映射规则得到映射结果,放入上下文中。action类型默认的映射规则实现是DirectModuleMappingRule,它执行的规则简单来说就是将所有的“/”替换为“.”,然后将最后一个单词首字母大写。例如,如果请求中带有一个参数action=/test/testAction,那么最终得到的action的值就是test.TestAction

(3)从请求的参数中,获得actionEvent。

这个处理过程具体如下:

  • 首先在请求中寻找一个符合这两个条件的参数:1.key转换为驼峰格式后符合“eventSubmitDo”格式;2.value不为空。
  • 然后将此参数的key截去前缀的eventSubmitDo,得到的字符串首字母小写。

得到的结果称为actionEvent。

例如,若请求为http://abc.com/index.htm?action=/test/testAction&event_submit_doGetSomethingSpecial=1,则actionEvent为getSomethingSpecial。

那什么是actionEvent呢,这要从Webx的约定开始说起了。在Webx中,有一种东西叫做action,action的本意是用来处理用户提交的表单的,但是实际上,近年来,大量强交互的Web应用的发展使得action被大量用于处理ajax异步请求。通常情况下,action指明了本次请求的处理类,而actionEvent指明了本次请求的处理类中的处理方法。

至此,整个Valve的工作就结束了。请注意,这个Valve仅仅是分析URL,从URL中获取相应的信息,并不做任何实际的操作。

3.4.3 PerformActionValve

打开这个类,我们看到注释是

执行action module,通常用来处理用户提交的表单。

这个Valve中真正执行处理的的代码只有一行:

moduleLoaderService.getModule(ACTION_MODULE, action).execute();

有关Module的讲解请参考3.3节,一个Module代表一个“可以执行的东西”。在这里,这个Valve执行的操作就是获取一个类型为action、名为AnalyzeURLValve中获得的那个action的“可以执行的东西”,然后执行它。

我们来举个栗子,注意,以下分析均建立在默认配置的基础上。

screenshot

一个HTTP请求是/xxx/yyy.zzz?action=/test/testAction&event_submit_doSomethingSpecial=1&key=value。

  • 从上下文中取得在AnalyzeURLValve中解析出的action,即test.TestAction。
  • 使用AbstractBeanFactoryBasedModuleFactory获得moduleObject,即${basePackage}.action.test.TestAction的实例bean。其中${basePackage}由配置指定。
  • 如果TestAction中含有execute方法,返回DataBindingAdapter的一个实例,调用它的execute方法等于调用TestAction的execute方法。
  • 否则,返回ActionEventAdapter的一个实例。调用它的execute方法时:(1)首先它从上下文中获得eventName。这个eventName就是AnalyzeURLValve中解析出的actionEvent。在本例中,这个actionEvent就是somethingSecial。(2)依次调用:beforeExecution方法、[eventName对应的方法](doSomethingSpecial方法或doPerform方法)、afterExecution方法,详见3.3节。

3.4.4 PerformScreenValve与PerformTemplateScreenValve

与PerformActionValve类似,这两个Valve的职责是,执行screen对应的“可以执行的东西”。上一节中,Webx框架查找action模块的依据是AnalyzeURLValve中获得的action,而这一节中,Webx框架查找screen模块的依据是AnalyzeURLValve中获得的target。忘了它们是神马的小伙伴请复习3.4.2节。

一个target应该具有/aaa/bbb/ccc.xxx格式。这两个Valve将会按照如下规则查找对应的模块:

  • 首先分别使用对应的MappingRule将target映射成Module名,默认的映射规则是DirectModuleMappingRule,它执行这样的映射:(1)用“/”分割整个target字符串;(2)去掉最后一个单词的扩展名,然后将其首字母大写;(3)用“.”进行连接。例如,若target为/aaa/bbb/ccc.xxx,那么映射后的Module名为aaa.bbb.Ccc。
  • 使用moduleLoaderService,根据上一步得到的Module名获取moduleObject。
  • 使用3.3节中的过程对moduleObject进行适配并执行。
  • 执行的返回值放入上下文中,以备后面的Valve使用。

这两个Valve的不同之处在于:

  • PerformScreenValve使用名为“screen.notemplate”的映射规则,而PerformTemplateScreenValve使用名为“screen”的映射规则。不过,在默认情况下,这两种映射规则实际上是同一个实现,即DirectModuleMappingRule
  • 如果上一步中根据Module名对应的Module失败,PerformScreenValve还会多做一步,尝试将target解释为moduleName/eventName并查找Module。即,查找最后一个“/”,将“/”之前的字符串认为是Module名,“/”之后的认为是eventName。

继续举栗说明。若target为/aaa/bbb/ccc.xxx:

如果${basePackage}.screen.aaa.bbb.Ccc类存在,那么获取其实例bean作为moduleObject,按照3.3节中的过程对其适配并执行,执行的方法是execute或者doPerform(因为eventName为空,所以使用的是默认方法doPerform)。

如果${basePackage}.screen.aaa.bbb.Ccc类不存在,那么PerformTemplateScreenValve就认为screen类不存在,直接执行接下来的pipeline流程(模板驱动的页面可以没有screen类);而PerformScreenValve会尝试查找${basePackage}.screen.aaa.Bbb类,如果它存在,就获取其实例bean作为moduleObject,按照3.3节中的过程对其适配并执行,此时的eventName就是ccc,执行的方法是execute或者doCcc。

其中,${basePackage}由配置指定。

可见,勤劳的PerformScreenValve做了一件挺让人摸不着头脑的事情,这是为什么呢?

有理由认为,在Webx发展过程的前期,Web应用基本上都是同步的,这也解释了为什么Webx进行Screen/Action的划分。这留下了一个比较尴尬的问题:异步请求怎么办?我们知道,在现代的Web应用中,大量的异步请求使用json和jsonp来通信,但是Webx设计的初衷是“提供同步的、基于模板渲染的动态Web服务”,对这类问题并没有给出原生的解决方案。在Webx 3.0.9之前,大家不得不使用一种不太优雅的折中方案:

/xxx/yyy.htm?action=AjaxAction&event_submit_doSomethingSpecial&param=value

然后在AjaxAction的doSomethingSpecail方法中,将需要返回的结果序列化成字符串,然后渲染到yyy.vm上去。显然,这样显得过于啰嗦。

当然,你也可以引入webx-rpc框架,它是一个轻量级的异步请求扩展。

Webx 3.0.9之后,官方终于给出了一种更为优雅的解决方案:

/xxx/yyy/somethingSpecial.json?param=value

对于该请求,按照上面的分析,PerformScreenValve将会执行xxx.Yyy类的doSomethingSpecial方法(记得在pipeline.xml中把json扩展名指派给它)并将返回值放入上下文中。这样,只要在PerformScreenValve之后串联一个自定义的Valve,把上一步得到的返回值序列化成JSON字符串,写入HttpServletResponse即可完成异步请求的处理。

显然,这种方法比之前的高到不知道哪里去了。

jzm