一、前言
Servlet是Java这种编程语言为如何生成动态网页提供的解决办法。我在之前的一篇文章中分析过,所谓网页其实就是一堆HTML标记,由浏览器负责解析这些标记并展现成我们所看到的样子。这些HTML标记浏览器本身是没有的,它要跟服务器去要,因为只有服务器才有。
那么服务器上的HTML是怎么来的呢?两种方式,一种是静态的,也就是我们提前把HTML标记保存到一个文本文件中,然后以.html为后缀名。每次访问这个文件,得到的都是相同的HTML标记,除非修改了这个文件,这也就是称之为“静态”的原因。另外一种是所谓的动态,HTML标记并不是人工手写的,而由程序根据输入条件自动生成的,浏览器每次访问这个程序可能会获取到不同的HTML标记,这就是所谓的“动态”。
至于这个生成HTML标记的程序长啥样,浏览器其实并不关心,你只要给我一段完整的HTML标记就好了。目前有很多语言可以干这件事,除了Java还有PHP、Python、Ruby等等甚至C语言。因为说真的,不就是生成一对HTML格式的字符串吗,太简单了。
恩,是简单,所以我选择了java。其实Java很强大,可以做很多事,一个Servlet技术只是其中之一而已。Servlet技术由容器加Servlet接口两部分组成,我们平时开发用的其实只是一个接口,但是光有接口是无法完成生成HTML标签的伟业的,还需要容器的支持。我想,在Servlet技术刚刚诞生的时候,应该还没有这么细分。只是随着需求的变更,生成HTML标签这件事越来越复杂,需要分工和专业化,所以将原来的整体拆分成容器和接口两部分,容器提供底层支持,而接口则专注于具体业务。可以简单的理解为,容器是接口的实现,而接口则是Servlet暴露给我们的API,我们只需要关注如何利用这些API实现自己的业务逻辑就好了。不过既然说到了接口的实现,那必然不只是一种实现,我们熟悉的Tomcat只是其中之一,其他的还有Jetty、GlassFish、JBoss等等。
我写这一系列的文章,第一步是分析这些接口,然后再分析其中一种实现,也就是Tomcat。
二、Servlet API
说我们平时用的Servlet只是一个API是有证据的,首先就是我们用maven做一个工程时所引入的jar名称,如下图:
然后看一下jar包的内容架构:
东西还不少,看一下Servlet这个最重要的“类”:
哇,就是一个interface嘛!这个包里的大部分类其实都是接口。
Servlet给我们的API主要分为三大块:Servlet、Filter和Listener,这个可以从包内类名上得到印证。然后,主要到有一个叫http的子包,这个包的东西专门用来处理和http协议有关的事情。这意味着,Servlet一开始设计的时候,其实是更加可扩展的,不仅仅是为了http,还可以处理其他类型的协议。然而由于http协议在web中的统治地位,所以Servlet还是专门为http增加了一个子包。
三、Servlet
先看下这个接口中的方法:
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(); }
实现这个接口是业务开发要做的事情,他们要把所有的业务逻辑实现在这几个方法中。观察可以发现,接口中并没有main方法,那么实现类要怎么去执行呢?这就是容器的职责了。容器负责管理Servlet接口的实现类,在不同的阶段调用不同的方法。main方法在容器中。
讲Servlet的书或文章都会讲到生命周期,这个事情其实也是由容器控制的,容器只会调用一次init和destroy方法,但是每次请求到来都会反复调用service方法。所以在实现这个接口时,初始化工作放在init中,清理工作放在destroy中,而主要的业务逻辑放在service中。
另外两个方法是辅助用的,其中getServletInfo很少用到,它的任务是返回关于实现类的描述信息;另一个方法getServletConfig比较重要,它的任务是从配置文件中获取到servlet实现类的初始化参数。这意味着servlet的初始化参数并没有放在实现类本身内部,而是放到配置文件中。有没有觉得不对劲?难道我就是要在实现类中放几个private变量不行吗?当然可以,但是从getServletConfig这个方法的存在上就可以看出,其实servlet的发明者不建议这么做,尤其是这个参数跟业务紧密关联而时常需要改变时。实际上,容器只会为每个servlet的实现类创建一个对象,只用这一个对象来应付所有访问它的请求。当不止一个请求时,为了效率必然要开启多线程。也就是说,到时会有多个线程在访问这个对象的service方法,如果在service方法中修改了private变量,那必然会造成数据不一致的问题;而如果把private变量设成同步的,又会造成效率的下降。所以就把初始化参数放到配置文件中,但是servlet接口中并没有修改他们的方法;这就是说在程序运行过程中初始化参数是只读的,要改变参数值,只能在程序运行前修改配置文件。
然而,要做到这一点也可以通过把private变量设置成final的做到啊?但是要改变参数值时,是修改类代码方便还是修改配置文件方便呢?
然后来看一下第一个方法init,容器调用它时需要传入一个ServletConfig参数,这说明容器已经根据配置文件生成了一个ServletConfig对象。那么在init方法里面要做些什么?对应第二个方法getServletConfig,它要返回一个ServletConfig对象,这个对象从哪儿来呢?这个对象必然是容器调用init时传入的那个ServletConfig对象,所以代码应该是这样的:
public class MyServlet implements Servlet { private ServletConfig servletConfig; public init(ServletConfig servletConfig) throw ServletException { this.servletConfig = servletConfig; } public ServletConfig getServletConfig() { return this.servletConfig; } ... }
也就是说,实现类中还是要有一个private的ServletConfig变量,而且是必须的。幸好的是,这个变量跟业务无关,并且通常你只是用它来获取信息。而init的第一件任务必然初始化这个ServletConfig变量。
接着分析一下service方法,他有两个参数ServletRequest和ServletResponse,这两个参数显然也是容器传入的。当请求到来时,容器会将请求封装成一个ServletRequest对象,同时生成一个ServletResponse对象,都传递给servlet。后者从ServletRequest对象中获取必要信息,然后把处理结果写入到ServletResponse对象。需要注意的是,ServletRequest和ServletResponse都只是接口,它们的实现由容器完成。
四、ServletConfig接口
这个接口也是由容提负责具体实现,可以看出,凡是涉及底层支持的接口,都是容器负责实现;而涉及业务的就是自己实现,我觉得这个设计很棒。先看接口的架构:
public interface ServletConfig { public String getServletName(); public ServletContext getServletContext(); public String getInitParameter(String name); public Enumeration<String> getInitParameterNames(); }
只有四个方法,最重要的当然是后两个,用于获取配置文件中的参数。但是分析它们之前,先要知道配置文件到底是啥,在哪儿?这就要从servlet规范规定的Web应用目录结构说起。一个动态的Web应用包含servlet编译后的class文件,html静态文件,jsp文件,js脚本,各种配置文件和图像文件等其他多媒体资源文件。这些文件显然不能混杂的扔到一个目录中。不管使用Tomcat还是Jetty做容器,首先要有自己的工程目录存放上述文件。比如工程目录起名叫MyWeb,那么servlet规范规定了MyWeb下必须要有一个WEB-INF目录。WEB-INF目录下面要有两个子目录classes和lib,前者存放编译后的class文件,后者存放用到的jar包。WEB-INF下还要有一个最重要的配置文件,这就是我们熟悉的web.xml:
<web-app> <servlet> <servlet-name>CometServlet</servlet-name> <servlet-class>comet.CometServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>CometServlet</servlet-name> <url-pattern>/test/comet</url-pattern> </servlet-mapping> </web-app>
最顶层的标签是web-app,它下面有很多子标签,但是现在我们先关注<servlet>和<servlet-mapping>,其他标签用到的时候再慢慢加进来。
我们看到<servlet>标签中有两个元素:<servlet-name>和<servlet-class>,后者是servlet实现类的全限定类名,容器会自动从WEB-INF/classes目录下去查找;而前者是我们给它起的名字——可以任意起名。getServletName方法一般也就是返回的这个名字。这个名字是为<servlet-mapping>服务的,这个标签的的主要作用将一个url映射到一个servlet上,当浏览器访问这个url时,容器就会调用这个servlet的service方法,将其产生的html标签返回给浏览器。
在此有必要提一下url:配置文件中的url显然不是一个完整的url,它是有前缀的,前缀包括三部分,IP,端口号,web应用名。其中IP和端口号之间用冒号分割,但是web应用名是可以为空的。当在浏览器中直接访问IP:端口号,比如localhost:8080时,浏览器实际访问的路径是localhost:8080/,也就是会默认加上一个斜线。这个斜线代表根目录,至于根目录到底指向哪个web应用,不同的容器有不同的方案,比如Tomcat会访问ROOT目录。为了区别根目录的默认值,我们最好还是给自己的web应用起一个有意义的名字。对于Tomcat而言,应用的名字是在server.xml文件中配置的,具体细节先参考这里,等我解读Tomcat源码时会仔细分析。
应用名后面一般不再有斜线,所以<url-pattern>标签的值往往以斜线开头,斜线后面的部分可以是一个正则表达式,比如/*代表任何路径,也就是所有访问这个web应用的请求都会由对应的servlet处理。当然也可以把值写的非常具体,那么对应的servlet就只负责这一个url的请求。但是这里有一点需要非常注意:具体url的优先级要高于泛用的url。比如servlet1对应的url是/*,而servlet2对应的url是/test,那么浏览器访问/test时,容器将优先调用servlet2。
好了,扯了半天都没有说到ServletConfig。servlet初始化参数是<servlet>标签内定义的,如下:
<servlet> <servlet-name>CometServlet</servlet-name> <servlet-class>comet.CometServlet</servlet-class> <init-param> <param-name>color</param-name> <param-value>red</param-value> </init-param> </servlet>
<param-name>标签定义参数名,<param-value>给出了参数值,它们两个要用<init-param>包围起来。当定义多个初始化参数时,就需要多个<init-param>标签。配置文件中定义的参数取出来都是String类型的,唯一的类型。而getInitParameterNames方法返回的Enumeration中,包含了所有的<init-param>。
容器是在什么时候读取配置文件中这些初始化参数呢?首先要明白的一点是,容器启动时不会马上把所有的Servlet都加载到内存中,而是等到有浏览器访问对应的url时才会去加载这个servlet,也就是new这个实现类的对象。读取初始化参数应该是在new完对象、调用init方法之前。
ServletConfig还有一个方法是getServletContext,它返回一个ServletContext对象,我们稍后会分析它。
本篇先写这么多吧。