Servlet简介
- 网络请求即是进行
Http
连接,http
本质上也是通过tcp
进行数据传输。使用原生的http``tcp
接口十分复杂,所以我们可以借助servlet等web容器,把连接等底层步骤交给servlet和web服务器去做 - 我们使用Servlet API编写自己的Servlet来处理HTTP请求,Web服务器实现Servlet API接口。Servlet帮助我们完成大量的底层工作
- 每一次请求,
servlet容器
都会对请求生成HttpServletResponse
和HttpServletRequest
2个数据,这两个数据包含了一些请求的信息,如请求头,请求方式,cookie,请求参数和返回参数等等Response
还可以设置返回状态,默认成功状态是200
- 请求体和响应体无关请求方式,post和get都能用
每次请求servlet都会根据请求路径发送到对应的servlet
HttpServlet…
HttpServletRequest
HttpServletRequest封装了一个HTTP请求,它实际上是从ServletRequest继承而来。最早设计Servlet时,设计者希望Servlet不仅能处理HTTP,也能处理类似SMTP等其他协议,因此,单独抽出了ServletRequest接口,但实际上除了HTTP外,并没有其他协议会用Servlet处理,所以这是一个过度设计。
我们通过HttpServletRequest提供的接口方法可以拿到HTTP请求的几乎全部信息,常用的方法有:getMethod():返回请求方法,例如,”GET”,”POST”;
- getRequestURI():返回请求路径,但不包括请求参数,例如,”/hello”;
- getQueryString():返回请求参数,例如,”name=Bob&a=1&b=2”;
- getParameter(name):返回请求参数,GET请求从URL读取参数,POST请求从Body中读取参数;
- getContentType():获取请求Body的类型,例如,”application/x-www-form-urlencoded”;
- getContextPath():获取当前Webapp挂载的路径,对于ROOT来说,总是返回空字符串””;
- getCookies():返回请求携带的所有Cookie;
- getHeader(name):获取指定的Header,对Header名称不区分大小写;
- getHeaderNames():返回所有Header名称;
- getInputStream():如果该请求带有HTTP Body,该方法将打开一个输入流用于读取Body;
- getReader():和getInputStream()类似,但打开的是Reader;
- getRemoteAddr():返回客户端的IP地址;
- getScheme():返回协议类型,例如,”http”,”https”;
此外,HttpServletRequest还有两个方法:setAttribute()和getAttribute(),可以给当前HttpServletRequest对象附加多个Key-Value,相当于给HttpServletRequest添加了一个临时的数据存放map
调用HttpServletRequest的方法时,注意务必阅读接口方法的文档说明,因为有的方法会返回null,例如getQueryString()的文档就写了:
… This method returns null if the URL does not have a query string…
HttpServletResponse
HttpServletResponse封装了一个HTTP响应。由于HTTP响应必须先发送Header,再发送Body,所以,操作HttpServletResponse对象时,必须先调用设置Header的方法,最后调用发送Body的方法。
常用的设置Header的方法有:
- setStatus(sc):设置响应代码,默认是200;
- setContentType(type):设置Body的类型,例如,”text/html”;
- setCharacterEncoding(charset):设置字符编码,例如,”UTF-8”;
- setHeader(name, value):设置一个Header的值;
- addCookie(cookie):给响应添加一个Cookie;
- addHeader(name, value):给响应添加一个Header,因为HTTP协议允许有多个相同的Header;
写入响应时,需要通过getOutputStream()获取写入流,或者通过getWriter()获取字符流,二者只能获取其中一个。
写入响应前,无需设置setContentLength(),因为底层服务器会根据写入的字节数自动设置,如果写入的数据量很小,实际上会先写入缓冲区,如果写入的数据量很大,服务器会自动采用Chunked编码让浏览器能识别数据结束符而不需要设置Content-Length头。
但是,写入完毕后调用flush()却是必须的,因为大部分Web服务器都基于HTTP/1.1协议,会复用TCP连接。如果没有调用flush(),将导致缓冲区的内容无法及时发送到客户端。此外,写入完毕后千万不要调用close(),原因同样是因为会复用TCP连接,如果关闭写入流,将关闭TCP连接,使得Web服务器无法复用此TCP连接。
写入完毕后对输出流调用flush()而不是close()方法!
有了HttpServletRequest和HttpServletResponse这两个高级接口,我们就不需要直接处理HTTP协议。注意到具体的实现类是由各服务器提供的,而我们编写的Web应用程序只关心接口方法,并不需要关心具体实现的子类。
准备:
新建javaweb项目
在maven项目的基础上增加这些文件:webapp,WEB-INF,web.xml这些是必须的web应用文件及文件夹
web-servlet-hello
├── pom.xml
└── src
└── main
├── java
│
├── resources
└── webapp
└── WEB-INF
└── web.xml
导入Servlet依赖
Servlet API是一个jar包,我们需要通过Maven来引入它,才能正常编译。
<scope>
指定为provided
,表示编译时使用,但不会打包到.war
文件中,因为运行期Web服务器本身已经提供了Servlet API相关的jar包。
- 注意到这个
pom.xml
与前面我们讲到的普通Java程序有个区别,打包类型不是jar
,而是war
,表示Java Web Application Archive(java网络应用包):<packaging>war</packaging>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itranswarp.learnjava</groupId>
<artifactId>web-servlet-hello</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>hello</finalName>
</build>
</project>
配置web.xml
我们还需要在工程目录下创建一个
web.xml
描述文件,放到src/main/webapp/WEB-INF
目录下(固定目录结构,不要修改路径,注意大小写)。文件内容可以固定如下:<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app> <display-name>Archetype Created Web Application</display-name> </web-app>
整个工程结构如下:
web-servlet-hello ├── pom.xml └── src └── main ├── java │ └── com │ └── itranswarp │ └── learnjava │ └── servlet │ └── HelloServlet.java ├── resources └── webapp └── WEB-INF └── web.xml
第一个Servlet应用
一个Servlet应用类总是继承自
**HttpServlet**
,然后覆写**doGet()**
或**doPost()**
方法。一个项目可以有很多个ServletdoGet()
方法可以传入HttpServletRequest
和HttpServletResponse
两个对象,分别代表HTTP请求和响应。- 我们使用Servlet API时,并不直接与底层TCP交互,也不需要解析HTTP协议,因为HttpServletRequest和HttpServletResponse就已经封装好了请求和响应
- 输出中文需要修改编码
resp.setCharacterEncoding("UTF-8");
// WebServlet注解表示这是一个Servlet,并映射到地址/: @WebServlet(urlPatterns = "/") public class HelloServlet extends HttpServlet { //获取HTTP请求与响应 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 设置响应类型: resp.setContentType("text/html"); // 获取输出流: PrintWriter pw = resp.getWriter(); // 写入响应: pw.write("<h1>Hello, world!</h1>"); // 最后不要忘记flush强制输出: pw.flush(); } }
打包部署web程序
打包:运行Maven命令mvn clean package
,在target
目录下得到一个hello.war
文件,这个文件就是我们编译打包后的Web应用程序。(可以在idea结构图的war文件那,右键 copy->copy Path知道war文件路径)
- 需要配置maven环境变量才能使用maven的命令
- 如果环境变量配置成功,idea里还是不能使用mvn命令,就管理员身份运行idea)
- 运行:先启动Web服务器,再由Web服务器加载我们编写的
HelloServlet
,这样就可以让HelloServlet
处理浏览器发送的请求。- 把hello.war复制到Tomcat的webapps目录下,然后切换到bin目录,执行
**.\startup.sh**
或**.\startup.bat**
启动Tomcat服务器:(win10里面./和.\一样的)
- 把hello.war复制到Tomcat的webapps目录下,然后切换到bin目录,执行
错误之一
执行startup.bat后报文件提前结束
。是因为web.xml文件是空文件。 写入<xml version="1.0” encoding=”utf-8" />
即可。正确启动服务器并加载项目文件,startup.bat命令末尾会提示?.war项目的部署已在?ms内完成
在浏览器输入http://localhost:8080/hello/即可看到HelloServlet的输出
浏览器运行
输入
[http://localhost:8080/?/](http://localhost:8080/hello/)
?为war文件名- 可以把webapp下所有文件删除。然后将war文件名改为
ROOT.war
就可以将项目页面改为localhost:8080
的默认页面 - tomcat启动时命令行的
**http-nio-xxxx**
即可快速看到端口号Servlet和Tomcat原理及注意
实际上,类似Tomcat这样的服务器也是Java编写的,启动Tomcat服务器实际上是启动Java虚拟机,执行Tomcat的main()
方法,然后由Tomcat负责加载我们的.war
文件,并创建一个HelloServlet
实例,最后以多线程的模式来处理HTTP请求。如果Tomcat服务器收到的请求路径是/
(假定部署文件为ROOT.war),就转发到HelloServlet
并传入HttpServletRequest
和HttpServletResponse
两个对象。
因为我们编写的Servlet并不是直接运行,而是由Web服务器加载后创建实例运行,所以,类似Tomcat这样的Web服务器也称为Servlet容器。
- 可以把webapp下所有文件删除。然后将war文件名改为
在Servlet容器中运行的Servlet具有如下特点:
- 无法在代码中直接通过new创建Servlet实例,必须由Servlet容器自动创建Servlet实例;
- Servlet容器只会给每个Servlet类创建唯一实例;
- Servlet容器会使用多线程执行
doGet()
或doPost()
方法。
Servlet多线程模型
一个Servlet类在服务器中只有一个实例,但对于每个HTTP请求,Web服务器会使用多线程执行请求。因此,一个Servlet的doGet()、doPost()等处理请求的方法是多线程并发执行的。如果Servlet中定义了字段,要注意多线程并发访问的问题:
public class HelloServlet extends HttpServlet { private Map
对于每个请求,Web服务器会创建唯一的HttpServletRequest和HttpServletResponse实例,因此,HttpServletRequest和HttpServletResponse实例只有在当前处理线程中有效,它们总是局部变量,不存在多线程共享的问题。
- 在Servlet中定义的实例变量会被多个线程同时访问,要注意线程安全;
**HttpServletRequest**
和**HttpServletResponse**
实例是由Servlet容器传入的局部变量,它们只能被当前线程访问,不存在多个线程访问的问题;- 在
doGet()
或doPost()
方法中,如果使用了ThreadLocal
,但没有清理,那么它的状态很可能会影响到下次的某个请求,因为Servlet容器很可能用线程池实现线程复用。
因此,正确编写Servlet,要清晰理解Java的多线程模型,需要同步访问的必须同步。
内部启动Tomcat
tomcat可以实现在idea里启动项目和tomcat,无需再打包然后复制到webapp中。
导入扩展依赖
在常规web项目的基础上增加如下依赖,这2个依赖都是用于内嵌tomcat的
- 这两个依赖也相当于引入了一个tomcat服务器,通过api来设置与启动服务器
这两个依赖的版本信息必须设置为tomcat服务器的版本
tomcat-embed-jasper
tomcat-embed-core
<dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> <version>8.5.59</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <version>8.5.59</version> <scope>provided</scope> </dependency>
在main()中启动tomcat
tomcat启动代码貌似必须写在
**main**
方法中- 启动代码如下,照搬即可,无需修改
下面即成功运行的截图:public class Main { public static void main(String[] args) throws Exception { // 启动Tomcat: Tomcat tomcat = new Tomcat(); //设置服务器端口 tomcat.setPort(Integer.getInteger("port", 8888)); //建立连接 tomcat.getConnector(); // 创建webapp: Context ctx = tomcat.addWebapp("", new File("src/main/webapp").getAbsolutePath()); WebResourceRoot resources = new StandardRoot(ctx); resources.addPreResources( new DirResourceSet(resources, "/WEB-INF/classes", new File("target/classes").getAbsolutePath(), "/")); ctx.setResources(resources); tomcat.start(); tomcat.getServer().await(); } }
- 启动代码如下,照搬即可,无需修改