coding……
但行好事 莫问前程

透过现象看本质——什么是servlet

我们通过几篇文章介绍了Spring的IOC和AOP两大属性,本来打算开始介绍Spring MVC的。但是想了一下,在介绍MVC框架之前,还是应该来缕清MVC底层的本质——Servlet,这个看着“非常久远”的概念,以便我们能更好地了解学习框架。

提到Servlet,有很多人可能是有些陌生的,因为在工作中一般很少直接用到Servlet,都是直接上手框架,以至于很容易出现如下两种看法:

  • 框架和Servlet割离,认为是两个概念,比如“Servlet还在使用吗”
  • 知道框架和Servlet有关联,但把Servlet想的太过复杂,比如如何实现网络通讯的

上述两种都是对于Servlet比较常见的错误看法,本篇文章,我们就来看一下Servlet的本质,之后会介绍Servlet容器Tomcat,在学习MVC框架之前,从本质上看web开发。

1. Web起源

首先我们来看一下Web的起源,当然这里介绍的不一定那么精准,引用《码农翻身》这本书中的内容,但对于我们理解Web起源有很大的帮助。

很久以前,互联网还没有出现,大家的电脑之间虽然可以通信,但也就可以收发一下邮件、用FTP传输一下文件这样简单地功能。

你是个球迷+程序员,电脑里有很多的记录足球的文件,例如足球.txt, 巴塞罗那.txt , 曼联.txt……等,这些文件很好地按照文件的树形结构组织着。这样的文件有成百上千个,你每天晚上都要打开这些文件,欣赏一些这些球队的风采。有一天你在打开“足球.txt”,里面在介绍西班牙足球的时候,出现了一个词“巴塞罗那”。想去看还要新打开一个文件,很不方便。于是你灵机一动,如果“巴塞罗那”这四个字上面有一个链接该多好,只需要轻轻一点,就能连接到这个文件,省的自己在硬盘上费尽的去找这个文件了。

凭着自己过硬的技术,你定义了一个协议,让这些文件之间可以通过一些词链接起来,比如原来的文本是这么显式的:西甲联赛中最著名的球队是巴塞罗那和皇家马德里。现在给这个纯文本加一些标记:

西甲联赛中最著名的球队是<a href = "西甲/巴塞罗那.txt">巴塞罗那</a>和<a href = "西甲/皇马.txt">皇家马德里</a>

这些标记人能读懂,但是没什么用处。于是你开发了一个软件,把带标记的文本显示成这个样子:

西甲联赛中最著名的球队是巴塞罗那皇家马德里

只要在软件中点击带下划线的文字,就会把对应的文件打开。加了链接的文本就不是普通文本了,而升级为超文本(HyperText)了。你决定给这个牛逼的软件起个名字叫浏览器,因为可以浏览这些文件,并且可以在这些文件之间用链接跳来跳去。

时间久了,你觉得只看文字太乏味了,能不能加一点表格、列表、图片呢?你想到<a href = “”>标签,既然它可以定义链接,自然也可以定义别的东西。比如:

  • <table></table>表示表格
  • <li></li>表示列表
  • <img></img>
  • ……

这些都被称为标记(Markup)。原来的浏览器只能显示文本和处理链接,现在还需要处理这些标签,遇到不同的标签就显示相应的内容。这样一来,超文本就变得丰富多彩了。你意识到,自己其实定义了一套描述界面的标记语言:HyperText Markup Language,简称HTML

于是你用HTML,把自己收藏的文本统统改写了以便,这下引起了哥们的注意。他认为你这个软件不错,于是把你的软件拷贝回去使用了起来。不过很快他发现了一个问题,就是他电脑里的文件“足球.txt”里面有“曼联”两个字,但是曼联的介绍在你的电脑上,加入在“曼联”这两个字上加上链接,怎么才能显示你电脑上的文件呢?

这一下子把你点醒了,各自电脑上的文件价值是有限的,不同电脑中的文件互联起来才有价值。一个单机运行的浏览器肯定是不行的,必须有网络。单独有网络还不行,还要解决通信问题

于是你对浏览器进行了扩展,不仅把本电脑里的HTML文件管理起来,还允许通过网络访问别的电脑的HTML文件。比如你的某个HTML中有这样一段话:

<a href = "http://192.168.0.10/football/广州恒大.html">广州恒大</a>连续7年获得中超冠军,这是一项了不起的成就。

当点击“广州恒大”这个链接的时候,浏览器需要向192.168.0.10这台电脑(哥们的电脑)发一次请求:

GET http://192.168.0.10/football/广州恒大.html

这时候哥们的电脑需要能识别其他电脑浏览器发过来的请求,并且响应才行。于是你又开发了一个软件,这个软件运行在哥们的电脑上,它收到请求以后,可以找到“football”目录,读取“广州恒大.html”这个文件,然后通过网络把文件发送回去,并且告诉对方:

  • 200,成功了
  • 404,找不到“广州恒大.html”这个文件
  • 500,内部出错了

这个软件很像一个贴心的服务员,专门在网络上为别人服务,那就叫网络服务器吧,再给它起个名字叫Apache

通过上述一些列的骚操作,你已经把文本变成了超文本,还定义了一套规范用语浏览器和服务器之间传输超文本,那通信方法就叫做超文本传输协议(HyperText Transfer Protocol,HTTP)吧。

有了HTML、HTTP、网络服务器、浏览器等软件和协议的支持,不仅可以使大家方便快捷地发布图文并茂的信息,而且可以轻松地从一个网站调到另一个网站,极大地促进了内容和信息的共享。网站之间的互联很快成燎原之势,最后形成了一张全世界互联的大王,称为World Wide Web(www)。

2. servlet

2.1 什么是servlet

上面我们介绍到,通过HTML、HTTP、网络服务器、浏览器等软件和协议的支持,可以轻松实现HTML文件的传输。但是我们可以发现,就是通过上述这种方式,只能传输静态的HTML文件,对于门户网站这种类型的网站来说是适用的,但是一旦要做到动态页面,上述方式就无法工作了。那有什么方式解决这个问题吗,其实也比较简单,那就是动态生成HTML文件。服务器端是用Java开发的,它用什么生成HTML文件中一大堆HTML语句?答案就是Servlet。

想到Servlet可以动态生成HTML,我们往往会把Servlet想成比较复杂的东西,比如如何实现网络通讯,端口监听等。但令人失望的是,Servlet跟网络通讯、端口监听这些”高大上“的概念完全没关系。简单来讲Servlet就是一个接口,定义了Servlet一套处理网络请求的规范。Servlet一套处理网络请求的规范。

所有实现servlet的类,都需要实现接口中定义的五个方法,其中最主要的是两个生命周期方法init()和destroy(),还有一个处理请求的service(),也就是说,所有实现servlet接口的类,或者说,所有想要处理网络请求的类,都需要回答这三个问题:

  • 初始化时要做什么
  • 销毁时要做什么
  • 接受到请求时要做什么

servlet是一个规范,那实现了servlet的类,就能处理网络请求了吗?答案是不能的。你可以随便谷歌一个Servlet的HelloWorld教程,里面都会让你写一个servlet,但你从来不会在Servlet中写什么监听8080端口的代码,servlet不会直接和客户端打交道!那请求怎么到达Servlet的呢?答案是servlet容器,比如我们最常用的Tomcat、Jetty。你随便谷歌一个Servlet的HelloWorld教程,里面肯定会让你把servlet部署到一个容器中,不然你的servlet压根不会起作用。Tomcat才是与客户端直接打交道的,它监听了端口,请求过来后,根据url等信息,确定要将请求交给哪个servlet去处理,然后调用那个servlet的service方法,service方法的入参都是Tomcat构造的,service方法调用结束后,response参数被赋值,通过Tomcat返回给客户端。

2.2 web服务器

当你在公司,是否可以访问自己家里电脑的一张图片?理论上是不行的。但是我们却可以通过URL访问图片,文章、电影等。一个资源如果无法映射到URL,那就无法被外界访问。而Web服务器说白了就是用来将主机上的资源映射为外部可访问的URL。

而Web服务器中,我们接触比较多的就是Apache。Apache,指的应该是Apache软件基金会下的一个项目——Apache HTTP Server Project

HTTP服务器本质上也是一种应用程序——它通常运行在服务器之上,绑定服务器的IP地址并监听某一个tcp端口来接收并处理HTTP请求,这样客户端(如IE、Firefox、Chrome等)就能够通过HTTP协议来获取服务器上的网页(HTML格式)、文档(PDF格式)、音频(MP4格式)、视频(MOV格式)等资源。

除了Apache,Nginx也是一款常用的Web服务器(日常一般用来做代理、负载均衡,但不影响它是一个常用的Web服务器)。甚至绝大多数编程语言所包含的类库中也都实现了简单的HTTP服务器方便开发者使用:

使用这些类库能够非常容易的运行一个HTTP服务器,它们都能够通过绑定IP地址并监听tcp端口来提供HTTP服务。

2.3 servlet容器

上面介绍到,单独使用web服务器,只能对外暴露静态资源,如果要生成动态页面的话,就需要servlet的支持。但是servlet是不能单独工作的,必须为servlet提供运行环境,才能使servlet发挥作用。这里说的运行环境就是servlet容器。

servlet容器,顾名思义就是存放servlet对象的容器。我们为什么能通过servlet容器访问特定URL的资源,那么servlet容器肯定也会提供如下几个基础功能:

  • 接收请求
  • 处理请求
  • 响应请求

其中接受请求和响应请求是共性功能,没有差异性。比如我们访问淘宝和京东,淘宝和京东的业务处理逻辑肯定是不同的,但是接受请求(监听指定端口的socket请求、根据路由分发)和响应结果(将结果通过socket写回到客户端浏览器)的过程是一致的。于是就把接受请求和响应请求两个过程抽象为servlet容器的基础功能,而把处理请求抽象为Servlet,留给程序员自己去实现

servlet容器中,比较常见也是我们日常使用最多的就是Tomcat。可以独立于Apache运行。关于Tomcat,有太多需要介绍的东西,我们再后面的文章再单独介绍。这里我们只需要理解它是用来配合servlet实现Java动态web服务的容器(运行环境)。

2.4 如何编写一个servlet

我们尝试学习servlet时,可以树立一个信念——框架不会让我们写比较复杂的代码。servlet作为一个接口让我们自己实现,肯定是非常简单的(其实通过前面的介绍,答案我们都已经知道了,我们不需要关注网络请求和响应,只需要关注我们的业务逻辑就行了)。

  • init:Tomcat通过反射创建servlet对象后,会调用init方法,通过参数传入ServletConfig对象
  • getServletConfig:返回servlet初始化启动参数配置信息,这些信息的抽象为ServletConfig对象
  • service:实现具体的业务逻辑,最难得部分,接收Http请求&响应Http请求,Tomcat已经帮我们做了,通过方法参数传给我们
  • getServletInfo:获取Servlet信息,比如作者、版本等,这个方法没有参数,我们可以自由实现
  • destroy:该方法由servlet容器调用,用来停止servlet对外提供服务。该方法只会在service方法中的线程退出或超过超时时间后调用一次。

2.4.1 ServletConfig

Servlet接口中定义的init方法的参数类型为ServletConfig,该对象是Tomcat生成的,我们自己实现Servlet接口时,可以直接使用该ServletConfig对象。顾名思义,该对象其实就是Servlet配置。我们是在web.xml文件中配置的servlet,也就是说Tomcat帮我们把xml文件解析为对象,供我们直接使用,免去我们使用dom4j解析xml文件的麻烦。

2.4.2 Request/Response

关于Request和Response,看名字我们就知道,这是网络请求的请求内容和响应内容,是网络请求的核心。跟ServletConfig一样,Request和Response也是Tomcat帮我们生成了的。Http请求到达Tomcat后,Tomcat通过解析,把请求头(Header)、请求地址(URL)、请求参数(QueryString)都封装到了Request对象中,在我们实现的Servlet中,我们通过调用:

request.getHeader();
request.getUrl();
request.getQueryString();

等方法,都可以得到浏览器发送的请求信息。

至于Response,我们可能会有些疑问,为什么请求还没被Servlet处理呢,就有Response生成了。其实Tomcat传给Servlet时,它还是空的对象。Servlet逻辑处理后得到结果,最终通过response.write()方法,将结果写入response内部的缓冲区。Tomcat会在servlet处理结束后,拿到response,遍历里面的信息,组装成HTTP响应发给客户端。

这里为了方便理解,先提前把Tomcat的系统结构列出来,关于Tomcat的具体细节在后面的文章再详细介绍。

2.4.3 编写Servlet

接下来我们来编写一个Servlet,通过上面的介绍我们知道Servlet是一个接口,那么我们编写Servlet是不是直接实现Servlet接口就可以了。其中init、service、destroy是生命周期方法,init和destroy各自只执行一次,即servlet创建和销毁时。而service会在每次有新请求到来时被调用,也就是说,我们主要的业务代码需要写在service中。


我们自己来实现Servlet,可以发现是比较烦的,每次实现都需要写一堆模板似的的代码,处理GET请求,处理POST请求。有没有更简单地办法。通过查找,我们发现了一个抽象类,GenericServlet。

我们可以发现GenericServlet做了以下改良:

  • 添加了ServletConfig成员变量,通过init方法对成员变量进行赋值,扩大了init方法形参的作用域,方便其它方法使用
  • init方法中,还调用了另一个init空参方法,如果我们希望在servlet创建时做一些什么初始化操作,可以继承GenericServlet后,覆盖init空参方法
  • 提供getServletConfig、getServletContext等方法,可以在其它方法中方便使用ServletConfig、ServletContext对象

但是service方法是个抽象方法,还是要像上面实现Servlet接口一样,写一堆模块代码,处理GET请求,处理POST请求。可以说,GenericServlet抽象类,只是方便我们使用一些配置信息,Context信息(可以直接通过方法调用获得)。

我们继续寻找,又发现了一个继承了GenericServlet的抽象类——HttpServlet。

GenericServlet本身是一个抽象类,有一个抽象方法service。HttpServlet已经实现了service方法:

也就是说HttpServlet的service方法已经替我们完成了繁琐的请求方法判断。

但是,我翻遍整个HttpServlet源码,都没有找出一个抽象方法。所以为什么HttpServlet还要声明成抽象类呢?看一下HttpServlet的文档注释:

一个类声明成抽象方法,一般有两个原因:

  • 有抽象方法
  • 没有抽象方法,但是不希望被实例化

这里HttpServlet声明为抽象类,仅仅是为了不让new。

它为什么不希望被实例化,且要求子类重写doGet、doPost等方法呢?我们来看一下源码:

如果我们没重写会怎样?那么就会调用HttpServlet自己提供的doXXX方法,进而返回Http错误的状态码。

也就是说,HttpServlet虽然在service中帮我们写了请求方式的判断。但是针对每一种请求,业务逻辑代码是不同的,HttpServlet无法知晓子类想干嘛,所以就抽出七个方法,并且提供了默认实现:报405、400错误,提示请求不支持。

最后我们可以总结一下,如何编写一个Servlet?

我们不用自己实现Servlet接口,也不用自己继承GenericServlet抽象类。我们只需要继承HttpServlet抽象类,并重写doGet、doPost等方法即可,在重写的方法中输出Html内容。可以发现,其实这里使用了设计模式中的模板方法模式。

3. JSP

通过上面的介绍,我们知道,Servlet为了输出动态html,需要在Servlet中通过Response获取PrintWriter对象,然后调用println方法,将html输出。

在使用Servlet进行web开发的年代,通常是美工写好静态html页面后,丢给程序员,然后在拿到数据后,逐句复制html静态页面上的html语句到Servlet的中,同时把需要动态数据的部分拼接上去,比如:

out.println("<span>用户名是:"+user.name+<"/span>");

将html和其所需要的数据后台输出。可以看到这种方式效率是非常低的,想拼接数据并完整输出一个html页面,一个doXXX方法没有上百行是不可能的。

而同时期的php就优秀的多,它使用在“html页面”中嵌入相应的语言来引入动态数据,避免了后台方法中输出一大堆前段html的尴尬。一部分Java程序员一看,发现“PHP是世界上最好的语言啊”,web开发可以如此顺滑,于是转向了PHP或者其他语言的开发。这样,Java流失了一部分程序员。SUN公司一看,这不行啊,Java也要搞一个。

仔细想来,我们的主要目的就是希望在最终输出的html的代码中嵌入后台数据罢了。除了把html语句拿出来放在Servlet里拼接好再输出这种方式外,有没有可能直接在html语句中写入动态数据呢?这也就是当时其他友商的做法。这几乎是完全相反的两种设计思路。于是,JSP应运而生。

3.1 什么是JSP

JSP全称Java Server Page,直译就是“运行在服务器端的页面”。上面已经介绍过,我们可以直接在JSP文件里写HTML代码,使用上把它当做HTML文件。而且JSP中HTML/CSS/JS等的写法和HTML文件中的写法是一模一样的。

但它毕竟不是HTML,而且本质差了十万八千里。因为我们还可以把Java代码内嵌在JSP页面中,很方便地把动态数据渲染成静态页面。这一点,HTML打死都做不到。

当有人请求JSP时,服务器内部会经历一次动态资源(JSP)到静态资源(HTML)的转化,服务器会自动帮我们把JSP中的HTML片段和数据拼接成静态资源响应给浏览器。也就是说JSP运行在服务器端,但最终发给客户端的都已经是转换好的HTML静态页面(在响应体里)。服务器并没有把JSP文件发给浏览器(假装重要的事情已经说3遍)。

即:JSP = HTML + Java片段(各种标签本质上还是Java片段)

所谓的“JSP和HTML相似”只是JSP给我们的表面印象。我们还要继续往下挖一挖。实际上,JSP和HTML差远了。JSP本质是一个Java类(Servlet),是在服务器混的,只不过它输出结果是HTML。蜜蜂产出蜂蜜,但是蜜蜂跟蜂蜜肯定是完全不同的!

通过上面2.4.2介绍的Web容器的工作原理,我们已经清除知道Servlet的工作原理。即通过Servlet容器监听Socket网络请求,然后容器会将请求转发到对应的Servlet类,调用service方法,输出html。接下来看一下JSP的本质,其实JSP本质就是一个Servlet。具体的源码我就不带大家看了,大致流程是这样的:

原本,我们需要把美工的HTML代码一行行复制到Servlet中,然后拼接数据,最后向客户端响应拼接好的HTML页面。

JSP出现后,就可以不用一行行复制HTML代码了,而是在JSP中直接写HTML代码和Java代码,后期JSP编译成一个Servlet,通过Java代码获取后台数据后拼接到HTML片段中,最终通过out.println()输出。

现在可以回答上面的问题:为什么完全相反的两种设计理念却完成了同样的需求呢?答案可以有多种,但是其中一种就是:这两种殊途同归,最终实现是一样的,都是在一个Servlet中输出的。

我们不妨看一下my.jsp编译后生成的java类,这里我们把my.jsp放在tomcat安装目录下……/webapps/examples/目录下,编译后会在tomcat的……/work/Catalina/localhost/examples/org/apache/jsp下生成my.jsp编译后生成的字节码class文件和java文件,名称生成规则是jsp的名称拼上”_jsp”,比如test.jsp就会生成test_jsp.class、test_jsp.java。

也就是说,虽然我们不用像上古时期一样手动复制html语句到Servlet了,但是JSP编译后的Java类其实还是在out.println()输出。和我们手动复制是一样的结果。

而my_jsp.java这个类继承了HttpJspBase,而HttpJspBase间接实现了Servlet接口。所以可以说,my.jsp被翻译后的Java类也是一个Servlet,所以JSP本质也是一个Servlet。

绕了这么一大圈,我们终于明白:

为了不让Java程序员一行行复制HTML代码到Servlet里,SUN公司干脆让Java程序员直接把HTML写在了Servlet里!但是毕竟SUN还没有那么明目张胆,让这个Servlet伪装了一把,打扮成JSP,然后跟程序员说:看,我搞了个JSP,能在上面同时写HTML和Java代码。 等你写完JSP,回头访问时,Tomcat就把这个JSP翻译成Servlet,原先写在JSP里的HTML代码就自动放在了out.println()里啦!相当于程序帮我做了“逐行复制HTML代码到Servlet”这一步。

至此,我们已经知道,JSP就是一个Servlet。那么丝毫不用怀疑,今后无论你在JSP看到什么奇奇怪怪的东西,只要不报错,说明JSP就有足够自信把它变成Java代码的一部分:

  • 要么被当成字符串输出(HTML片段)
  • 要么本身就是Java片段
  • 要么会转成Java片段(EL表达式)

所以,大家千万别把EL表达式想太难,记个语法,知道怎么用就行了。至于它怎么变成Java代码的,我们就不需要担心了。

最后还要提醒一下EL表达式这些标签是在何时何地起作用的。很多人误以为EL表达式可以在浏览器起作用。根本原因还是对JSP不了解。JSP是服务器端的,所有操作必须在响应给浏览器之前做完。这些标签,会在JSP文件编译成Servlet时,自动转化为Java代码,然后对数据做处理。所以本质上和你在JSP页面写的<%%>之类的Java片段一样。它负责从变量(不确定的数)中取出数据,变成静态数据后(确定的数)拼接在HTML静态页面上。

3.2 JSP与AJAX + HTML

其实请求、响应这么一来一回,无非要的就两样东西:数据+HTML骨架。如果把服务器端比作淘宝卖家,客户端(浏览器)比作买家,而数据和HTML则是一件商品的两个重要组成部件。那么我们很自然地能够想到,其实运输方式至少可以有两种:

  1. 卖家组装好商品后再发货(JSP)
  2. 卖家把部件拆开,运到之后买家自己组装(AJAX+HTML)

JSP是服务器端的,它的局限性在于数据必须在返回给客户端之前就“装载”完毕。不然HTML都已经发出去了,你就没办法跑到浏览器里把数据给它安上。而有了AJAX,就可以实现零件发送、目的地组装了。

再强调一点,虽然我们在浏览器地址栏输入localhost:8080/xxx/xxx.jsp,就显示出了当前页面,但那不是JSP页面,而是HTML页面。服务器并没有直接把JSP文件从服务端扔到客户端!JSP是Java Server Page,是服务器端的东西。服务器的东西永远不可能直接在浏览器运行。浏览器只能接受静态页面。

3.3 MVC模式与JavaEE三层架构

我们最开始接触web开发时,肯定会接触到MVC的概念。同时也会了解到JavaEE三层架构。很多时候我们会把这两个概念混淆在一起,认为MVC和JavaEE三层架构是一个东西。其实这两者之间还是有很多区别的。

MVC是web开发都有的一种模式,比如PHP开发web时也有MVC模式。而三层架构则是JavaEE的:Controller/Service/Dao。分层开发是为了使代码逻辑更加清晰,也起到了一定的解耦合作用。

我们说的MVC只存在于Web层。而JavaEE的三层架构中,Web层只是三层中的一部分。当然,如果站在更高的角度,可以看成这样:

参考链接:

1. 《码农翻身》

2. servlet的本质是什么,它是如何工作的?

3. 怎样学习JSP?

4. 初学 Java Web 开发,请远离各种框架,从 Servlet 开发

赞(2) 打赏
Zhuoli's Blog » 透过现象看本质——什么是servlet
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址