Servlet由来与入门
编写HTTP服务器其实是非常简单的,只需要先编写基于多线程的TCP服务,然后在一个TCP连接中读取HTTP请求,发送HTTP响应即可。但是,要编写一个完善的HTTP服务器,以HTTP/1.1为例,需要考虑的包括:
- 识别正确和错误的HTTP请求;
- 识别正确和错误的HTTP头;
- 复用TCP连接;
- 复用线程;
- IO异常处理;
- …
这些基础工作需要耗费大量的时间,并且经过长期测试才能稳定运行。如果我们只需要输出一个简单的HTML页面,就不得不编写上千行底层代码,那就根本无法做到高效而可靠地开发。因此,在JavaEE平台上,==处理TCP连接==,==解析HTTP协议==这些底层工作统统扔给现成的Web服务器
去做,我们只需要把自己的应用程序跑在Web服务器上。为了实现这一目的,JavaEE提供了Servlet API,我们使用Servlet API编写
自己的Servlet来处理HTTP请求,Web服务器实现
Servlet API接口,实现底层功能:
1 | ┌───────────┐ |
我们来实现一个最简单的Servlet:
1 | // WebServlet注解表示这是一个Servlet,并映射到地址/: |
一个Servlet总是继承自HttpServlet
,然后覆写doGet()
或doPost()
方法。注意到doGet()
方法传入了HttpServletRequest
和HttpServletResponse
两个对象,分别代表HTTP请求和响应。我们使用Servlet API时,并不直接与底层TCP交互,也不需要解析HTTP协议,因为HttpServletRequest
和HttpServletResponse
就已经封装好了请求和响应。以发送响应为例,我们只需要设置正确的响应类型,然后获取PrintWriter
,写入响应即可。
现在问题来了:Servlet API是谁提供?
Servlet API是一个jar包,我们需要通过Maven来引入它,才能正常编译。编写pom.xml
文件如下:
1 |
|
注意到这个pom.xml
与前面我们讲到的普通Java程序有个区别,打包类型不是jar
,而是war
,表示Java Web Application Archive。注意到引入的Servlet API的<scope>
指定为provided
,表示编译时使用,但不会打包到.war
文件中,因为运行期Web服务器本身已经提供了Servlet API相关的jar包。
要务必注意servlet-api
的版本。4.0及之前的servlet-api
由Oracle官方维护,引入的依赖项是javax.servlet:javax.servlet-api
,编写代码时引入的包名为:
1 | import javax.servlet.*; |
而5.0及以后的servlet-api
由Eclipse开源社区维护,引入的依赖项是jakarta.servlet:jakarta.servlet-api
,编写代码时引入的包名为:
1 | import jakarta.servlet.*; |
引入不同的Servlet API版本,编写代码时导入的相关API的包名是不同的。
1 | web-servlet-hello |
目录webapp
目前为空,如果我们需要存放一些资源文件,则需要放入该目录。运行Maven命令mvn clean package
,在target
目录下得到一个hello.war
文件,这个文件就是我们编译打包后的Web应用程序。
现在问题又来了:我们应该如何运行这个war
文件?
普通的Java程序是通过启动JVM,然后执行main()
方法开始运行。但是Web应用程序有所不同,我们无法直接运行war
文件,必须先启动Web服务器,再由Web服务器加载我们编写的HelloServlet
,这样就可以让HelloServlet
处理浏览器发送的请求。
因此,我们首先要找一个支持Servlet API的Web服务器。无论使用哪个服务器,只要它支持Servlet API 5.0(因为我们引入的Servlet版本是5.0),我们的war包都可以在上面运行。这里我们选择使用最广泛的开源免费的Tomcat服务器。
要运行我们的hello.war
,首先要下载Tomcat服务器,解压后,把hello.war
复制到Tomcat的webapps
目录下,然后切换到bin
目录,执行startup.sh
或startup.bat
启动Tomcat服务器:
在浏览器输入http://localhost:8080/hello/
即可看到HelloServlet
的输出,需要注意:一个Web服务器允许同时运行多个Web App,而我们的Web App叫hello
,因此,第一级目录/hello
表示Web App的名字,后面的/
才是我们在HelloServlet
中映射的路径。
那能不能直接使用/
而不是/hello/
?毕竟/
比较简洁。
答案是肯定的。先关闭Tomcat(执行shutdown.sh
或shutdown.bat
),然后删除Tomcat的webapps目录下的所有文件夹和文件,最后把我们的hello.war
复制过来,改名为ROOT.war
,文件名为ROOT
的应用程序将作为默认应用,启动后直接访问http://localhost:8080/
即可。
实际上,类似Tomcat这样的服务器也是Java编写的,启动Tomcat服务器实际上是启动Java虚拟机,执行Tomcat的main()
方法,然后由Tomcat负责加载我们的.war
文件,并创建一个HelloServlet
实例,最后以多线程的模式来处理HTTP请求。如果Tomcat服务器收到的请求路径是/
(假定部署文件为ROOT.war),就转发到HelloServlet
并传入HttpServletRequest
和HttpServletResponse
两个对象。
因为我们编写的Servlet并不是直接运行,而是由Web服务器加载后创建实例运行,所以,类似Tomcat这样的Web服务器也称为Servlet容器。
在Servlet容器中运行的Servlet具有如下特点:
- 无法在代码中直接通过new创建Servlet实例,必须由Servlet容器自动创建Servlet实例;
- Servlet容器只会给每个Servlet类创建唯一实例;
- Servlet容器会使用多线程执行
doGet()
或doPost()
方法。
结合Java多线程的知识,我们可以总结一下:
- 在Servlet中定义的实例变量会被多个线程同时访问,要注意线程安全;
HttpServletRequest
和HttpServletResponse
实例是由Servlet容器传入的局部变量,它们只能被当前线程访问,不存在多个线程访问的问题;- 在
doGet()
或doPost()
方法中,如果使用了ThreadLocal
,但没有清理,那么它的状态很可能会影响到下次的某个请求,因为Servlet容器很可能用线程池实现线程复用。
因此,正确编写Servlet,要清晰理解Java的多线程模型,需要同步访问的必须同步。