Tomcat
什么是Tomcat
Tomcat,对外是Web服务器,对内是Servlet容器。
对外:Web服务器 \textbf{对外:Web服务器} 对外: Web 服务器
对内:Servlet容器 \textbf{对内:Servlet容器} 对内: Servlet 容器
基本使用
下载
下载地址如下:https://tomcat.apache.org/download-80.cgi
我们会看到如下的内容。
Windows用户选择32-bit Windows zip
或64-bit Windows zip
。
macOS用户选择zip
。
Linux用户选择tar.gz
。
安装
Tomcat是一个绿色软件,所以直接解压即可。
理论上解压到任意的目录都可以,但最好解压到一个没有中文,没有空格的目录,因为在部署项目的时候,如果路径有中文或者空格可能会导致程序部署失败。
在Linux上的解压方式如下:
1 tar -zxvf apache-tomcat-8.5.81.tar.gz
如果是用过MacOS的Safari浏览器下载的,因为Safari浏览器的缘故,会自动对.gz
的包进行解压。所以其解压命令如下:
1 tar -xvf apache-tomcat-8.5.81.tar
解压后,会得到如下内容:
bin
:可执行文件
该目录下主要有两类文件,一种是以.bat
结尾的,是Windows的可执行文件,一种是以.sh
结尾的,是macOS和Linux的可执行文件。
conf
:配置文件
lib
:Tomcat所依赖的JAR包
logs
:日志文件
temp
:临时文件
webapps
:应用发布目录
work
:工作目录
卸载
卸载,直接删除目录即可
启动
Windows
启动方式为执行bin
目录下的.\startup.bat
。
在Windows上,需要在环境变量中配置JAVA_HOME
或JRE_HOME
。不是说java.exe
已经被加入环境变量(在PATH
中有),而是需要在环境变量中配置JAVA_HOME
或JRE_HOME
。
例如,我们输入java -version
,返回了版本号,说明java.exe
被加入了环境变量。示例代码:
运行结果:
1 2 3 java version "1.8.0_333" Java(TM) SE Runtime Environment (build 1.8.0_333-b02) Java HotSpot(TM) 64-Bit Server VM (build 25.333-b02, mixed mode)
但是执行.\startup.bat
,依旧失败。示例代码:
运行结果:
1 2 Neither the JAVA_HOME nor the JRE_HOME environment variable is defined At least one of these environment variable is needed to run this program
配置环境变量JAVA_HOME
。
启动后,通过浏览器访问 http://localhost:8080
能看到Apache Tomcat的内容就说明Tomcat已经启动成功。
在启动过程中,可能会有乱码,如图:
修改conf\logging.properties
的java.util.logging.ConsoleHandler.encoding
的值,改为GBK
。
macOS
在macOS上执行sh startup.sh
。
如果出现如下的返回
1 2 3 Cannot find ./catalina.sh The file is absent or does not have execute permission This file is needed to run this program
是因为没有赋予文件catalina.sh
可执行的权限,执行chmod +x *.sh
,对bin
目录下的所有.sh
文件都赋执行权即可。
启动后,通过浏览器访问 http://localhost:8080
能看到Apache Tomcat的内容就说明Tomcat已经启动成功。
Linux
在Linux上执行sh startup.sh
。
因为Linux系统上一般没有浏览器,我们可以通过其他系统访问,以检查是否启动成功。
如果Linux机器是以虚拟机的方式安装在本地的话,需要注意虚拟机网络连接的方式,不同的连接方式,IP地址不同。一般有三种方式:
Use bridged networking
(使用桥接网络)
例如,VMware的VMnet0
,ParallelDesktop的Bridged Network
。
此时虚拟机相当于网络上的一台独立计算机,与主机一样,拥有一个独立的IP地址。
Use network address translation
(使用NAT网络)
例如,VMware的VMnet8
,ParallelDesktop的Shared Network
。
此时虚拟机可以通过主机单向访问网络上的其他工作站(包括Internet网络),其他工作站不能访问虚拟机。
Use Host-Only networking
(使用主机网络)
例如,VMware的VMnet1
,ParallelDesktop的Host-Only Network
。
此时虚拟机只能与虚拟机、主机互连,网络上的其他工作站不能访问。
在本文,采用Use network address translation
(使用NAT网络),IP地址如下:
如果无法访问,可能是因为Linux机器的8080端口未开放。
查询端口是否开放:
1 firewall-cmd --query-port=8080/tcp
永久开放8080端口:
1 firewall-cmd --permanent --add-port=8080/tcp
需要刷新,上述改动才会生效。刷新:
关闭
对于Windows,执行shutdown.bat
。
对于macOS和Linux,执行sh shutdown.sh
。
部署
将需要部署的内容,放置在webapps目录下,即部署完成。
如果放置的是.war
包,Tomcat会自动对其进行解压缩。
Web项目
结构
Web项目的结构分为两种。
开发中的项目,其目录结构如下:
开发完成可部署的项目,其目录结构如下:
对于开发中的项目:
通过执行Maven打包命令package
,可以得到"开发完成可部署的项目"。
编译后的,Java字节码文件和resources的资源文件,会被放到"开发完成可部署的项目"的WEB-INF
的classes
目录下。
pom.xml
中依赖坐标对应的Jar包,会被放打"开发完成可部署的项目"的WEB-INF
的lib
目录下。
创建
使用Maven创建Web项目,有两种方式:
使用骨架
不使用骨架
使用骨架
使用骨架的方式如下:
该方式需要联网,并且需要一定的时间以创建,创建完成后的目录结构如下:
修改pom.xml
,删除不必要的内容,只需要保留如下的内容即可。1 2 3 4 5 6 7 8 9 10 11 12 <?xml version="1.0" encoding="UTF-8"?> <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/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > com.kakawanyifan</groupId > <artifactId > mv1</artifactId > <version > 1.0-SNAPSHOT</version > <packaging > war</packaging > </project >
在main
目录下新建两个Directory:java
和resources
。
不使用骨架
不勾选Create from archetype
,即不使用骨架。
创建完成之后,修改pom.xml
,添加<packaging>war</packaging>
因为,默认的<packaging>
是jar
,我们需要指定为war
。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?xml version="1.0" encoding="UTF-8"?> <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/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > com.kakawanyifan</groupId > <artifactId > mv2</artifactId > <version > 1.0-SNAPSHOT</version > <packaging > war</packaging > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > </properties > </project >
补齐webapp
。步骤如下:
右键,选择Open Module Settings
:
依次点击,Facets
,+
:
选择Web
:
选择我们的项目:
修改箭头所指两处的路径,然后点击Apply
。
此处默认是web
,需要将web
修改为webapp
。
在IDEA使用Tomcat
在IDEA中使用Tomcat有两种方式:
集成本地Tomcat
tomcat7-maven-plugin
其中第二种方法,只支持7版本的Tomcat,而且相关的插件,最近一次更新还是在2013年11月。
我们主要讨论如何集成本地的Tomcat
点击Add Configuration
:
选择Tomcat Server
,Local
:
点击Configure
,配置Tomcat目录:
选择Tomcat的目录:
依次点击,Deployment
,+
:
选择我们的项目
war
模式是将WEB⼯程打成war包,把war包发布到Tomcat服务器上;部署成功后,Tomcat的webapps⽬录下会有部署的项⽬内容。
war exploded
模式是将WEB⼯程以当前⽂件夹的位置关系发布到Tomcat服务器上;部署成功后,Tomcat的webapps⽬录下没有,⽽使⽤的是项⽬的target⽬录下的内容进⾏部署。
添加完成后,我们还可以在下方,自定义项目的名称:
配置完成之后,即可以在IDEA的上方看到
特别的,我们还可以设置热部署,方法如下:
选择Deployment
的方式为war exploded
。
修改On Update action
和On frame deactivation
为Update classes and resources
。
有时候,如果我们通过IDEA启动Tomcat一直报错(例如,404),可以在第五步的时候,选择External Source
,看看我们已有的外部的应用(例如,Tomcat安装时自带的在webapps目录下的)是否正常。 这样有助于我们排查是Tomcat配置问题,还是IDEA中应用的问题。
Servlet
什么是Servlet?
Servlet = Server + applet \text{Servlet} = \text{Server} + \text{applet}
Servlet = Server + applet
其中,Server
指服务器,applet
指小程序,Servlet
含义是服务器端的小程序。
(当然,就规模而言,早已不是小程序了。)
例如,在我们查询员工列表的过程中,Servlet的角色如下
在整个Web应用中,Servlet主要负责处理请求、协调调度。
案例
我们创建一个Web项目web-demo
,并在pom.xml
中导入Servlet依赖坐标
1 2 3 4 5 6 <dependency > <groupId > javax.servlet</groupId > <artifactId > javax.servlet-api</artifactId > <version > 4.0.1</version > <scope > provided</scope > </dependency >
<scope>
是provided
,表示只在编译和测试过程中有效;这么设置的原因是,Tomcat的lib目录中也有servlet-api这个Jar包,如果在运行时生效,可能会和Tomcat中的Jar包冲突。
然后,创建一个类,该类实现Servlet接口,重写接口中所有方法。并为该类加上@WebServlet
注解,配置访问路径。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package com.kakawanyifan;import javax.servlet.*;import javax.servlet.annotation.WebServlet;import java.io.IOException;import java.io.PrintWriter;@WebServlet ("/demo" )public class ServletDemo implements Servlet { public void init (ServletConfig servletConfig) throws ServletException { } public ServletConfig getServletConfig () { return null ; } public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { System.out.println("Servlet Demo Hello" ); PrintWriter writer = servletResponse.getWriter(); writer.write("Hello,I am Servlet" ); } public String getServletInfo () { return null ; } public void destroy () { } }
然后,我们启动Tomcat,在浏览器中输入地址。
同时,我们会发现控制台也打印了Servlet Demo Hello
。
执行过程
但是,好像有问题?我们创建的是一个类ServletDemo
,没创建对象啊。
那么是谁执行了System.out.println("Servlet Demo Hello")
?还让我们看到了在网页看到了Hello,I am Servlet
?
这就涉及到Servlet的执行过程了。
浏览器发出请求:http://localhost:8080/web_demo_war/demo
根据localhost:8080
找到要访问的Tomcat Web服务器
根据web_demo_war
找到部署在Tomcat服务器上的web_demo_war
项目
根据/demo
找到被访问的Servlet类(通过@WebServlet注解进行匹配)
找到ServletDemo
这个类后,Tomcat会为ServletDemo
这个类创建一个对象,然后调用对象中的service方法。
那么,Tomcat是怎么知道调用service方法呢?
这是一种约定,我们自定义的Servlet类,必须实现Servlet接口并重写其方法,包括service方法。
生命周期
通过上文的讨论,我们知道了Servlet对象是由Tomcat创建的。
那么,Tomcat是什么时候创建的Servlet对象的?
这就涉及到Servlet的生命周期了,即一个对象从被创建到被销毁的整个过程。
Servlet对象是由Servlet容器(Tomcat)创建的,其生命周期也由Servlet容器(Tomcat)来管理,分为4个阶段:
加载和实例化
默认情况,Servlet对象会在第一次被访问的时候,由容器创建,但是如果创建Servlet对象比较耗时的话,那么第一个访问的人等待的时间就比较长,用户体验较差。也可以在服务启动的时候,就创建Servlet对象。1 @WebServlet (urlPatterns = "/demo1" ,loadOnStartup = 1 )
loadOnstartup
的取负整数表示第一次访问时创建Servlet对象,取0或正整数表示服务启动时创建Servlet对象(数字越小优先级越高)。
初始化
在Servlet实例化之后,容器将调用Servlet的init()
方法初始化这个对象,完成一些如加载配置文件、创建连接等初始化的工作。该方法只调用一次。
请求处理
每次请求时,Servlet容器都会调用Servlet对象的service()
方法对请求进行处理。
服务终止
当需要释放内存或者容器关闭时,容器就会调用Servlet对象的destroy()
方法完成资源的释放,随后会被Java的垃圾收集器回收。
我们可以验证一下,示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package com.kakawanyifan;import javax.servlet.*;import javax.servlet.annotation.WebServlet;import java.io.IOException;import java.io.PrintWriter;@WebServlet (urlPatterns = "/demo" ,loadOnStartup = 1 )public class ServletDemo implements Servlet { public ServletDemo () { System.out.println("构造..." ); } public void init (ServletConfig servletConfig) throws ServletException { System.out.println("初始化..." ); } public ServletConfig getServletConfig () { return null ; } public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { System.out.println("Servlet Demo Hello" ); PrintWriter writer = servletResponse.getWriter(); writer.write("Hello,I am Servlet" ); } public String getServletInfo () { return null ; } public void destroy () { System.out.println("销毁..." ); } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 【部分运行结果略】 构造... 初始化... 【部分运行结果略】 Servlet Demo Hello Servlet Demo Hello Servlet Demo Hello Servlet Demo Hello 【部分运行结果略】 销毁...
题外话: 除了Servlet有生命周期,一些基金公司的产品管理系统,也被命名为生命周期系统,指一个产品从创建到退出的整个过程。
HttpServlet
通过上文的讨论,我们知道要想编写一个Servlet就必须要实现Servlet接口,重写接口中的5个方法。但其实,我们更关注的其实只有service方法,有更简单方式来创建Servlet。直接继承HttpServlet。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.kakawanyifan;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@WebServlet (urlPatterns = "/demo2" )public class HttpServletDemo extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("get" ); resp.getWriter().write("get" ); } @Override protected void doPost (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("post" ); resp.getWriter().write("post" ); } }
对于GET
方法,通过浏览器访问即可。对于POST
,可以通过Postman等工具。
特别的,我们还可以看看HttpServlet的源码,示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package javax.servlet.http;【部分代码略】 public abstract class HttpServlet extends GenericServlet { 【部分代码略】 protected void service (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getMethod(); long lastModified; if (method.equals("GET" )) { lastModified = this .getLastModified(req); if (lastModified == -1L ) { this .doGet(req, resp); } else { long ifModifiedSince = req.getDateHeader("If-Modified-Since" ); if (ifModifiedSince < lastModified) { this .maybeSetLastModified(resp, lastModified); this .doGet(req, resp); } else { resp.setStatus(304 ); } } } else if (method.equals("HEAD" )) { lastModified = this .getLastModified(req); this .maybeSetLastModified(resp, lastModified); this .doHead(req, resp); } else if (method.equals("POST" )) { this .doPost(req, resp); } else if (method.equals("PUT" )) { this .doPut(req, resp); } else if (method.equals("DELETE" )) { this .doDelete(req, resp); } else if (method.equals("OPTIONS" )) { this .doOptions(req, resp); } else if (method.equals("TRACE" )) { this .doTrace(req, resp); } else { String errMsg = lStrings.getString("http.method_not_implemented" ); Object[] errArgs = new Object[]{method}; errMsg = MessageFormat.format(errMsg, errArgs); resp.sendError(501 , errMsg); } } 【部分代码略】 }
调用HttpServletRequest
对象的getMethod()
以获取方法,然后根据方法的不同,分别调用doGet
和doPost
。
访问路径
注解的方式
Servlet类编写好后,要想被访问到,就需要配置其访问路径(urlPattern)。我们可以为一个Servlet,配置多个urlPattern,示例代码:
1 2 @WebServlet (urlPatterns = {"/demo2" ,"/demo3" })public class HttpServletDemo extends HttpServlet {
如果我们只配置urlPatterns
,而且只配置一个urlPatterns
,也可以采用@WebServlet("/request")
这种的简化方式。
web.xml
还可以通过web.xml
进行配置。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.kakawanyifan;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;public class HttpServletDemoXML extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { System.out.println("XML" ); resp.getWriter().write("XML" ); } }
web.xml
的内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <!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 > <servlet > <servlet-name > what-ever</servlet-name > <servlet-class > com.kakawanyifan.HttpServletDemoXML</servlet-class > </servlet > <servlet-mapping > <servlet-name > what-ever</servlet-name > <url-pattern > /xml</url-pattern > </servlet-mapping > </web-app >
匹配方式
精确匹配
形如/user/select
,示例代码:
1 2 3 4 5 6 7 8 9 @WebServlet (urlPatterns = "/user/select" )public class HttpServletDemo2 extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { System.out.println("/user/select" ); resp.getWriter().write("/user/select" ); } }
访问/user/select
,运行结果:
目录匹配
形如/user/*
,示例代码:
1 2 3 4 5 6 7 8 9 @WebServlet (urlPatterns = "/user/*" )public class HttpServletDemo3 extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { System.out.println("/user/*" ); resp.getWriter().write("/user/*" ); } }
访问/user/【除select外的任意内容】
,运行结果:
注意,【除select外的任意内容】
,因为:精确匹配 的优先级高于目录匹配 。
后缀名匹配
形如*.do
,示例代码:
1 2 3 4 5 6 7 8 9 @WebServlet (urlPatterns = "*.do" )public class HttpServletDemo4 extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { System.out.println("*.do" ); resp.getWriter().write("*.do" ); } }
访问what-ever.do
,运行结果:
但是,如果我们访问user/what-ever.do
呢?运行结果:
因为目录匹配 的优先级高于后缀名匹配 。
任意匹配
任意匹配有两种。
形如/
,示例代码:
1 2 3 4 5 6 7 8 9 @WebServlet (urlPatterns = "/" )public class HttpServletDemo5 extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { System.out.println("/" ); resp.getWriter().write("/" ); } }
形如/*
,示例代码:
1 2 3 4 5 6 7 8 9 @WebServlet (urlPatterns = "/*" )public class HttpServletDemo6 extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { System.out.println("/*" ); resp.getWriter().write("/*" ); } }
/
和/*
的区别:
/*
的优先级会高于/
。
如果我们的项目中的Servlet配置了/
,会覆盖掉Tomcat中的DefaultServlet,DefaultServlet用来处理静态资源。
例如,如果我们配置了/
,就会导致项目中webapp
目录下的a.html
访问不到。
优先级
五种配置的优先级,从高到低为:
精确匹配
目录匹配
后缀匹配
/*
/
转发和重定向
转发
在请求的处理过程中,Servlet将请求转交给下一个资源处理,是在服务器端完成 的。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.kakawanyifan;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@WebServlet (urlPatterns = "/dispatcher" )public class HttpServletDemoDispatcher extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { req.getRequestDispatcher("/a.html" ).forward(req,resp); } }
此时,通过访问/dispatcher
会得到/a.html
的内容。
重定向
重定向,本质是一种特殊的响应,是Servlet以一个响应的方式告诉浏览器:需要你另外再访问下一个资源。在浏览器端完成 的。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.kakawanyifan;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@WebServlet (urlPatterns = "/redirect" )public class HttpServletDemoRedirect extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.sendRedirect("/web_demo/a.html" ); } }
我们通过浏览器访问/web_demo/a.html
,会发现浏览器实际上发了两个请求。
我们看到,第一个请求的状态码是302
,这里解释一下:
1XX:服务器就收客户端消息,但没有接受完成,等待一段时间后,发送1XX状态码
2XX:成功。代表:200。
3XX:重定向。代表:302(重定向),304(访问缓存)。
4XX:客户端错误。代表:404(请求路径没有对应的资源)。
5XX:服务器端错误。
比较
转发
重定向
一次请求
两次请求
浏览器地址栏显示的是第一个资源的地址
浏览器地址栏显示的是第二个资源的地址
全程使用的是同一个request对象
全程使用的是不同的request对象
在服务器端完成
在浏览器端完成
目标资源地址由服务器解析 所以在代码中不需要加虚拟目录)
目标资源地址由浏览器解析 所以在代码中需要加虚拟目录(项目访问路径)
目标资源可以在WEB-INF目录下
目标资源不能在WEB-INF目录下
目标资源仅限于本应用内部
目标资源可以是外部资源
在具体的代码实现中,转发是由Request 实例(请求对象)完成的,重定向是由Response 实例(响应对象)完成的。
而这两个,将是我们接下来讨论的重点。
Request
什么是Request
浏览器会发送HTTP请求到后台服务器(Tomcat),HTTP的请求中会包含很多请求数据(请求行+请求头+请求体),后台服务器(Tomcat)会对HTTP请求中的数据进行解析并把解析结果存入到一个对象中,所存入的对象就是Request对象。我们可以从Request对象中获取请求的相关参数,以完成后续操作。
我们再看看上文的代码,我们的Servlet类实现的是Servlet接口的时候,service方法中的参数是ServletRequest和ServletResponse,示例代码:
1 2 3 public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {}
当我们的Servlet类继承的是HttpServlet类的时候,doGet和doPost方法中的参数就变成HttpServletRequest和HttpServletReponse,示例代码:
1 2 3 4 @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException {}
如果我们点开看,会发现ServletRequest
是接口,HttpServletRequest
继承了ServletRequest
,也是接口。
在《2.面向对象》 ,我们讨论过,接口不能实例化,但可以通过实现类对象实例化,这叫接口多态。
那么,接口的实现类在哪里呢?
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.kakawanyifan;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@WebServlet (urlPatterns = "/find" )public class HttpServletDemoFind extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { System.out.println(req); } }
运行结果:
1 org.apache.catalina.connector.RequestFacade@6e160913
org.apache.catalina.connector.RequestFacade
,这就是其实现类。
即,其继承体系如下:
获取请求数据
HTTP请求数据总共分为三部分内容:请求行
、请求头
和请求体
。
获取请求行
请求行包含三块内容:请求方式
、请求资源路径
和HTTP协议及版本
方法如下:
获取请求方式:String getMethod()
获取虚拟目录(项目访问路径):String getContextPath()
获取URL(统一资源定位符):StringBuffer getRequestURL()
获取URI(统一资源标识符):String getRequestURI()
获取请求参数(GET方式):String getQueryString()
(POST方式的请求参数在请求体中)。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.kakawanyifan;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@WebServlet ("/request" )public class RequestDemo extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { String method = req.getMethod(); System.out.println(method); String contextPath = req.getContextPath(); System.out.println(contextPath); StringBuffer url = req.getRequestURL(); System.out.println(url.toString()); String uri = req.getRequestURI(); System.out.println(uri); String queryString = req.getQueryString(); System.out.println(queryString); } }
访问/request?u=123&p=abc
,运行结果:
1 2 3 4 5 GET /web_demo http://localhost:8080/web_demo/request /web_demo/request u=123&p=abc
获取请求头
请求头是key-value
的形式。
获取所有的key
:getHeaderNames()
。
获取具体的value
:getHeader
。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.kakawanyifan;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.util.Enumeration;@WebServlet ("/request" )public class RequestDemo extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { Enumeration<String> headerNames = req.getHeaderNames(); while (headerNames.hasMoreElements()) { String key = headerNames.nextElement(); System.out.println(key + " : " + req.getHeader(key)); } } }
访问/request
,运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 host : localhost:8080 connection : keep-alive cache-control : max-age=0 sec-ch-ua : " Not A;Brand";v="99", "Chromium";v="102", "Google Chrome";v="102" sec-ch-ua-mobile : ?0 sec-ch-ua-platform : "macOS" upgrade-insecure-requests : 1 user-agent : Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36 accept : text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 sec-fetch-site : none sec-fetch-mode : navigate sec-fetch-user : ?1 sec-fetch-dest : document accept-encoding : gzip, deflate, br accept-language : zh-CN,zh;q=0.9 cookie : JSESSIONID=4E40FFD71F9F301FDA16532C13397229
获取请求体
GET请求中是没有请求体的,我们需要把请求方式变更为POST。
有两种方式获取请求体中的数据:
获取字符输入流
BufferedReader getReader()
获取字节输入流
ServletInputStream getInputStream()
获取字符输入流
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.kakawanyifan;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.BufferedReader;import java.io.IOException;@WebServlet ("/request" )public class RequestDemo extends HttpServlet { @Override protected void doPost (HttpServletRequest req, HttpServletResponse resp) throws IOException { BufferedReader br = req.getReader(); String line = br.readLine(); System.out.println(line); } }
我们在Postman中,通过x-www-form-urlencoded
的格式调用。
运行结果:
获取字节输入流
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.kakawanyifan;import javax.servlet.ServletInputStream;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.FileOutputStream;import java.io.IOException;@WebServlet ("/request" )public class RequestDemo extends HttpServlet { @Override protected void doPost (HttpServletRequest req, HttpServletResponse resp) throws IOException { ServletInputStream servletInputStream = req.getInputStream(); FileOutputStream fileOutputStream = new FileOutputStream("程序都是一样的-副本.jpg" ); byte [] bys = new byte [1024 ]; int len; while ((len=servletInputStream.read(bys))!=-1 ) { fileOutputStream.write(bys,0 ,len); } fileOutputStream.close(); } }
我们在Postman中,通过binary
的格式调用。
会在Tomcat
的bin
目录下找到程序都是一样的-副本.jpg
。
统一的方式
通过上文的讨论,我们知道:
对于GET
请求,可以通过String getQueryString()
获取参数
对于POST
方式,可以通过BufferedReader getReader()
获取参数
那么,有没有统一的方式呢?
获取所有参数Map集合1 Map<String,String[]> getParameterMap()
因为参数的值可能是一个,也可能有多个,所以Map的值的类型为String数组。
根据名称获取参数值(数组)1 String[] getParameterValues(String name)
根据名称获取参数值(单个值)1 String getParameter(String name)
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.kakawanyifan;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.util.Enumeration;import java.util.Map;@WebServlet ("/request" )public class RequestDemo extends HttpServlet { @Override protected void doPost (HttpServletRequest req, HttpServletResponse resp) throws IOException { Map<String, String[]> parameterMap = req.getParameterMap(); Enumeration<String> parameterNames = req.getParameterNames(); while (parameterNames.hasMoreElements()) { String key = parameterNames.nextElement(); System.out.println(key + ":" ); System.out.println(req.getParameter(key)); String[] parameterValues = req.getParameterValues(key); for (String parameterValue: parameterValues) { System.out.println(parameterValue); } } System.out.println("------" ); for (String key: parameterMap.keySet()) { System.out.println(key + ":" ); String[] vArr = parameterMap.get(key); for (String v: vArr) { System.out.println(v); } } } @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { doPost(req, resp); } }
我们在Postman中,通过POST请求的x-www-form-urlencoded
格式调用。
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 u: 123 123 p: abc abc a: 1 1 2 ------ u: 123 p: abc a: 1 2
我们在Postman中,通过GET请求调用。
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 u: 123 123 p: abc abc a: 1 1 2 ------ u: 123 p: abc a: 1 2
application/json
还有一种请求方式是application/json
,以JSON的形式提交。
关于该方式,在《14.HttpClient》 有讨论。
其实也是从请求体中拿数据,只是关于该方式,不会将请求体的内容解析成"getParameterMap"的形式,而是解析成"JSON"的形式。
乱码
现象
我们传入中文。
运行结果:
1 2 3 4 5 6 ch: ä¸æ ä¸æ ------ ch: ä¸æ
为什么会乱码?
在《5.IO流》 ,我们讨论过乱码以及字符集编码。乱码是因为编码和解码所采用的字符集不对。
方法
因为客户端将数据发送给服务端后,并没有告诉服务端需要采取何种编码方式,这时候服务端会采取默认的ISO-8859-1
。
所以,我们可以像如下这样操作,先按照ISO-8859-1
解码,再按照utf-8
重新编码。
1 new String(【需要解码的内容】.getBytes("ISO-8859-1" ),"utf-8" )
第二种方法,我们直接为Request对象指定编码方式是utf-8
。
1 req.setCharacterEncoding("utf-8" );
第三种方法为采用过滤器Filter。
在7版本的Tomcat中,已经将这个Filter加入Tomcat内置了,具体位置:Tomcat目录下的conf/web.xml
,我们直接复制一下代码到项目的web.xml
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <filter > <filter-name > setCharacterEncodingFilter</filter-name > <filter-class > org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class > <init-param > <param-name > encoding</param-name > <param-value > UTF-8</param-value > </init-param > <async-supported > true</async-supported > </filter > <filter-mapping > <filter-name > setCharacterEncodingFilter</filter-name > <url-pattern > /*</url-pattern > </filter-mapping >
关于过滤器,我们会在下文做更多的讨论。
Get请求中的乱码
这个其实在8版本的Tomcat的默认配置中,已经不会复现了。
我们先讨论为什么会有乱码?为什么8版本的Tomcat没有复现,最后我们试图修改8版本的参数,让其复现。
对于Post请求,获取请求参数的方式是request.getReader()
,所以我们通过request.setCharacterEncoding("utf-8")
这种方法,指定编码方式可以解决。但是在Get请求中,获取参数的方式是request.getQueryString()
,根本就没有经过流。
在发送的过程中,客户端(浏览器)会将中文以UTF-8
的方式进行URL编码,然后这时候服务端(Tomcat)应该将收到的内容,以UTF-8
的形式进行解码。
在8版本的Tomcat中,的确是以UTF-8
的形式进行解码,但是在7版本的Tomcat中,是以ISO-8859-1
的方式进行解码的,所以就有乱码了。
我们修改Tomcat的配置文件conf/server.xml
,在原有的
1 2 3 <Connector port ="8080" protocol ="HTTP/1.1" connectionTimeout ="20000" redirectPort ="8443" />
加上URIEncoding="ISO-8859-1"
。
然后试一下
运行结果:
1 2 3 4 5 6 ch: ä¸æ ä¸æ ------ ch: ä¸æ
当然,如下的方式可以解决。
1 System.out.println(new String(req.getParameter(key).getBytes("ISO-8859-1" ),"utf-8" ));
更好的方法是修改Tomcat的配置文件conf/server.xml
,去除URIEncoding="ISO-8859-1"
,采取默认的UTF-8
。
特殊字符被拒
再来看一下现象,如果我们通过Get请求传递的参数中有特殊字符,请求就会报错
同时,控制台会打印
1 2 3 29-Jun-2022 22:17:29.453 INFO [http-nio-8080-exec-10] org.apache.coyote.http11.Http11Processor.service Error parsing HTTP request header Note: further occurrences of HTTP request parsing errors will be logged at DEBUG level. java.lang.IllegalArgumentException: Invalid character found in the request target [/web_demo_war/request?json={%22k%22:%22%E5%80%BC%22} ]. The valid characters are defined in RFC 7230 and RFC 3986
这是因为,Tomcat在7.0.73
、8.0.39
和8.5.7
版本后,在HTTP解析时做了严格限制。根据RFC3986文档规定,请求的URL中只允许包含英文字母(a-zA-Z)、数字(0-9)、-``_``.``~
4个特殊字符以及所有保留字符。
不允许出现:空格、反斜杠、#
、^
、|
、<
、>
、{
、}
等。
解决办法是在conf/catalina.properties
的最后如下的两行,并重启Tomcat以生效。
1 2 tomcat.util.http.parser.HttpParser.requestTargetAllow=|{} org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true
但requestTargetAllow
只能配置|
、{
、}
这三个字符,对于其他的(例如[
、]
),在请求时,仍然拦截。
如果使用了|
、{
、}
之外的其他字符,需要在conf/server.xml
中的<Connector>
节点中,添加2个属性:
1 2 relaxedPathChars="|{}[]," relaxedQueryChars="|{}[],"
Response
什么是Response
如何接受请求我们已经讨论过了,在接受到请求之后,就开始各种业务处理,在业务处理之后,就需要把结果返回给调用方。
怎么返回呢?return
?不是。service
方法都是void
的,不能返回值的,我们需要将将响应数据封装到Response对象中,后台服务器(Tomcat)会解析Response对象,按照HTTP的格式(响应行+响应头+响应体)拼接结果。
而Response对象,是调用service
时的一个入参。所以其返回方式是通过修改入参。
类似的还有《12.MyBatis》 获取自增主键,将自增的主键塞在我们入参中,也是修改入参。
Reponse的继承体系和Request的继承体系也非常相似:
响应
HTTP响应数据总共分为三部分内容:响应行
、响应头
和响应体
。
响应行
对于响应行,比较常用的就是设置响应状态码:
响应头
设置响应头键值对:
1 void setHeader (String name,String value) ;
响应体
对于响应体,是通过字符、字节输出流的方式往客户端写,
获取字符输出流:1 PrintWriter getWriter () ;
获取字节输出流:1 ServletOutputStream getOutputStream () ;
响应字符数据
步骤
通过Response对象获取字符输出流:1 PrintWriter writer = resp.getWriter();
通过字符输出流写数据:
响应简单字符串
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.kakawanyifan;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.PrintWriter;@WebServlet ("/response" )public class ResponseDemo extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { PrintWriter writer = resp.getWriter(); writer.write("Simple" ); } }
一次请求响应结束后,response对象就会被销毁掉,所以没有主动关闭流。
响应HTML字符串
示例代码:
1 2 3 4 5 6 protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setHeader("content-type" ,"text/html" ); PrintWriter writer = resp.getWriter(); writer.write("<h1>Title</h1>" ); }
如果没有设置"content-type","text/html"
,访问结果如图:
如果设置了"content-type","text/html"
,访问结果如图:
在一些浏览器,例如Chrome浏览器中,即使不设置"content-type","text/html"
,也会解析,这和浏览器自身的特性有关。
响应中文
返回一个中文的字符串,需要注意设置响应数据的编码为utf-8
。
示例代码:
1 2 3 4 5 6 7 protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setHeader("content-type" ,"text/html;charset=utf-8" ); PrintWriter writer = resp.getWriter(); writer.write("<h1>大标题</h1>" ); writer.close(); }
设置方法还有:
response.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=UTF-8");
注意,如果我先PrintWriter writer = resp.getWriter()
,再resp.setHeader("content-type","text/html;charset=utf-8")
,则不会生效。具体因为在resp.getWriter()
的时候,就会去设置字符集的编码。
响应字节数据
响应字节数据的方法为:
通过Response对象获取字节输出流:1 ServletOutputStream outputStream = resp.getOutputStream();
通过字节输出流写数据:1 outputStream.write(字节数据);
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.kakawanyifan;import javax.servlet.ServletOutputStream;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.FileInputStream;import java.io.IOException;@WebServlet ("/response" )public class ResponseDemo extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { FileInputStream fis = new FileInputStream("../../jpg.jpg" ); ServletOutputStream os = resp.getOutputStream(); byte [] buff = new byte [1024 ]; int len = 0 ; while ((len = fis.read(buff))!= -1 ){ os.write(buff,0 ,len); } fis.close(); } }
ServletContext
在上文,我们读取文件的代码是new FileInputStream("../../jpg.jpg");
,这个其实是相对于Tomcat的bin
的相对路径。我们也可以采取相对于项目的路径,这就是涉及到ServletContext。
ServletContext,代表整个web应用,可以和程序的容器(服务器)来通信。
获取方式:
通过request对象获取1 request.getServletContext();
通过HttpServlet获取1 this .getServletContext();
作用:
域对象:共享数据,所有用户所有请求的数据。
设置属性:setAttribute(String name,Object value)
获取属性:getAttribute(String name)
删除属性:removeAttribute(String name)
获取文件的真实(服务器)路径
方法:String getRealPath(String path)
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @WebServlet ("/response" )public class ResponseDemo extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { FileInputStream fis = new FileInputStream(this .getServletContext().getRealPath("/jpg.jpg" )); ServletOutputStream os = resp.getOutputStream(); byte [] buff = new byte [1024 ]; int len = 0 ; while ((len = fis.read(buff))!= -1 ){ os.write(buff,0 ,len); } fis.close(); } }
jpg.jpg
,在项目的根目录下。
Cookie
会话技术
我们知道,HTTP协议是无状态的,每次浏览器向服务器请求时,服务器都会将该请求视为新的请求。
那么,现在问题来了:我访问淘宝,登录,一个HTTP请求;登录成功后,我把商品加入购物车,一个HTTP请求;然后我支付,一个HTTP请求。这三个HTTP请求之间,是互相独立的,那么我加入购物车的时候,服务端是怎么知道是哪位用户要把商品加入购物车呢?支付的时候,服务端怎么知道是哪位用户要支付哪一笔订单呢?
客户端告诉服务端不就够了吗? 那么客户端怎么知道呢? 客户端先记下来! \begin{aligned}
& \text{客户端告诉服务端不就够了吗?} \\
& \text{那么客户端怎么知道呢?} \\
& \text{客户端先记下来!} \\
\end{aligned}
客户端告诉服务端不就够了吗? 那么客户端怎么知道呢? 客户端先记下来!
这就是基于Cookie的客户端会话跟踪技术。
在《基于JavaScript的前端开发入门:3.DOM和BOM》 的最后,讨论本地存储的时候,我们也讨论过Cookie,当时主要从客户端的角度讨论Cookie。
在《爬虫及其Python实现:1.发送请求获取响应》 ,也有讨论Cookie。当时Cookie在客户端(爬虫)的应用。
现在我们会从服务端以及服务端和客户端的交互的角度,讨论Cookie。
还有一种服务端会话跟踪技术,基于Session,而Session也是基于Cookie。
操作
一共有四个Cookie操作:
创建Cookie1 Cookie cookie = new Cookie("key" ,"value" );
发送Cookie1 response.addCookie(cookie);
接收Cookie1 Cookie[] cookies = request.getCookies();
解析Cookie1 2 3 4 cookie.getName(); cookie.getValue();
我们创建ServletA
,用于创建发送Cookie;再创建ServletB
,用于接收和解析Cookie。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.kakawanyifan;import javax.servlet.annotation.WebServlet;import javax.servlet.http.Cookie;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@WebServlet ("/a" )public class A extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { Cookie cookie1 = new Cookie("uid" ,"123" ); resp.addCookie(cookie1); Cookie cookie2 = new Cookie("pwd" ,"abc" ); cookie2.setValue("xyz" ); resp.addCookie(cookie2); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.kakawanyifan;import javax.servlet.annotation.WebServlet;import javax.servlet.http.Cookie;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@WebServlet ("/b" )public class B extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { Cookie[] cookies = req.getCookies(); for (Cookie cookie : cookies) { System.out.println(cookie.getName()); System.out.println(cookie.getValue()); } } }
我们访问/a
,会看到客户端(浏览器)中已经有Cookie了。
再访问http://localhost:8080/web_demo_war/b
,控制台会打印:
1 2 3 4 5 6 uid 123 pwd xyz JSESSIONID 180FB1E8DC7F1A789D2F1233E8E5DF1B
其中JSESSIONID
由Tomcat创建,Session就基于这个,在下文我们会继续讨论。
存活时间
默认情况下,Cookie存储在浏览器内存中,当浏览器关闭,内存释放,则Cookie被销毁。
如果需要将Cookie持久化的存储,可以设置Cookie的存活时间。
参数值为:
正数
:将Cookie进行持久化存储(客户端电脑的硬盘中),到时间自动删除。
负数
:Cookie在当前浏览器内存中,浏览器关闭,Cookie被销毁。
默认值
零
:对应Cookie立即失效。
存储中文
在8版本的Tomcat中,Cookie中不能直接存储中文数据(需要经过UrlEncode)。
在8版本的Tomcat之后,cookie支持中文数据,但特殊字符还是不支持。
但是,我们来看一个现象。
Chrome中可以存储中文,Safari中不能存储中文,但是Safari对于存储英文和数字却没有问题。
所以,建议,对于8版本的Tomcat,依然进行UrlEncode。
我们在ServletA
中进行编码,在ServletB
中进行解码。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.kakawanyifan;import javax.servlet.annotation.WebServlet;import javax.servlet.http.Cookie;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.net.URLEncoder;@WebServlet ("/a" )public class A extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { Cookie cookie1 = new Cookie("uid" ,"123" ); Cookie cookie2 = new Cookie("pwd" ,"abc" ); Cookie cookie3 = new Cookie("desc" ,"China" ); cookie1.setValue(URLEncoder.encode("中国" ,"utf-8" )); cookie2.setValue(URLEncoder.encode("中华" ,"utf-8" )); resp.addCookie(cookie2); resp.addCookie(cookie1); resp.addCookie(cookie3); System.out.println(cookie1.getMaxAge()); System.out.println(cookie2.getMaxAge()); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.kakawanyifan;import javax.servlet.annotation.WebServlet;import javax.servlet.http.Cookie;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.net.URLDecoder;@WebServlet ("/b" )public class B extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { Cookie[] cookies = req.getCookies(); for (Cookie cookie : cookies) { System.out.println(cookie.getName()); System.out.println(URLDecoder.decode(cookie.getValue(),"utf-8" )); } } }
通过Safari浏览器,先访问a
,再访问b
。
运行结果:
1 2 3 4 5 6 desc China pwd 中华 uid 中国
共享问题
假设在一个Tomcat服务器中,部署了多个web项目,那么在这些web项目中cookie能不能共享?
默认情况下cookie不能共享
但是可以通过setPath(String path)
,设置cookie的获取范围。默认情况下,设置当前的虚拟目录。如果要共享,则可以将path设置为/
。
不同的Tomcat服务器间cookie共享,当然也不能。
但可以通过setDomain(String path)
,设置cookie的获取域名。如果设置一级域名相同,那么多个服务器之间cookie可以共享。
例如:setDomain("baidu.com")
,那么tieba.baidu.com
和news.baidu.com
中cookie可以共享。
Session
概述
Session:服务端会话跟踪技术,将主要数据保存到服务端。
(注意,是主要数据,因为有些数据,还是需要保存在客户端。)
操作
获取Session对象,通过Request对象:1 HttpSession session = request.getSession();
存储数据到session域中:1 void setAttribute (String name, Object o)
根据key,获取值:1 Object getAttribute (String name)
根据key,删除该键值对1 void removeAttribute(String name)
我们在ServletA
中存储数据,在ServletB
中获取数据,在ServletC
中移除数据。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.kakawanyifan;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;@WebServlet ("/a" )public class A extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) { HttpSession httpSession = req.getSession(); httpSession.setAttribute("uid" ,"123" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.kakawanyifan;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;@WebServlet ("/b" )public class B extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) { HttpSession httpSession = req.getSession(); Object uid = httpSession.getAttribute("uid" ); System.out.println(uid); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.kakawanyifan;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;@WebServlet ("/c" )public class C extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) { HttpSession httpSession = req.getSession(); httpSession.removeAttribute("uid" ); } }
我们依次访问/a
、/b
、/c
、/d
,运行结果:
原理
Session是基于Cookie实现的。
Session要想实现一次会话多次请求之间的数据共享,就必须要保证多次请求获取Session的对象是同一个。
那么,是怎么做到是同一个的呢?
基于JSESSIONID
,而这个存储在Cookie中。
存活时间
默认情况下,无操作,30分钟自动销毁。
设置存活时间的方法有:
可以在Web容器中设置,通过Tomcat的\conf\web.xml
1 2 3 4 5 6 7 <session-config > <session-timeout > 30</session-timeout > </session-config >
默认值30分钟;可以根据需要修改,负数或0为不限制session失效时间。
可以在工程的web.xml中设置,通过工程的\WEB-INF\web.xml
1 2 3 4 <session-config > <session-timeout > 30</session-timeout > </session-config >
工程和Web容器一样,默认值30分钟;可以根据需要修改,负数或0为不限制session失效时间。
可以通过通过java代码设置1 session.setMaxInactiveInterval(30*60)
特别注意!这里的单位是秒 ,负数或0为不限制session失效时间。
上述三种方法,如果都设置,优先级最高的是在Java代码中设置,其次是工程,最后是在Tomcat中。
钝化和活化
钝化:在服务器正常关闭后,Tomcat会自动将Session数据写入硬盘的文件中。
活化:再次启动服务器后,从文件中加载数据到Session中
Filter
概述
Filter,过滤器,是JavaWeb三大组件(Servlet、Filter、Listener)之一。
Filter,可以把对资源的请求,进行拦截过滤,从而实现一些通用功能,例如:权限控制、统一编码处理等。
案例
定义类,实现 Filter接口,并重写其方法。
配置Filter拦截资源的路径:在类上定义 @WebFilter
注解。而注解的 value
属性值 /*
表示拦截所有的资源
在doFilter方法中打印内容,并放行。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.kakawanyifan;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@WebServlet ("/s" )public class HttpServletDemo extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.getWriter().write("123456" ); } }
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.kakawanyifan;import javax.servlet.*;import javax.servlet.annotation.WebFilter;import java.io.IOException;@WebFilter ("/*" )public class FilterDemo implements Filter { public void init (FilterConfig filterConfig) throws ServletException { } public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("Filter" ); filterChain.doFilter(servletRequest,servletResponse); } public void destroy () { } }
然后我们访问/s
,会发现注释filterChain.doFilter(servletRequest,servletResponse);
,不放行的时候,看不到内容。
放行后
如图是Filter的流程,我们需要关注的是,不但有放行前的流程,还是放行后的流程。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("Filter" ); filterChain.doFilter(servletRequest,servletResponse); servletResponse.getWriter().write("abcdef" ); }
路径配置
使用 @WebFilter
注解进行配置。如:@WebFilter("拦截路径")
和Servlet中的路径一样,Filter的拦截路径有如下四种配置方式,
精确匹配
形如/user/select
目录匹配
形如/user/*
后缀名匹配
形如*.do
任意匹配
/*
(与Servlet不同的是,在Filter中,任意匹配只有/*
,/
无效。)
过滤器链
过滤器链是指在一个Web应用,可以配置多个过滤器,这多个过滤器称为过滤器链。
通过注解无法排序
不少资料说,通过注解@WebFilter
配置的Filter过滤器,是按照过滤器类名(字符串)的自然排序。
实际上,绝非如此。
通过注解@WebFilter
配置的Filter过滤器,无法进行排序,若需要对Filter过滤器进行排序,建议使用web.xml进行配置。
例如,现在有FilterA
、FilterB
和FilterC
。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @WebFilter ("/*" )public class FilterA implements Filter { public void init (FilterConfig filterConfig) { } public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("FilterA 放行前" ); filterChain.doFilter(servletRequest,servletResponse); System.out.println("FilterA 放行后" ); } public void destroy () { } }
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @WebFilter ("/*" )public class FilterB implements Filter { public void init (FilterConfig filterConfig) { } public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("FilterB 放行前" ); filterChain.doFilter(servletRequest,servletResponse); System.out.println("FilterB 放行后" ); } public void destroy () { } }
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @WebFilter ("/*" )public class FilterC implements Filter { public void init (FilterConfig filterConfig) { } public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("FilterC 放行前" ); filterChain.doFilter(servletRequest,servletResponse); System.out.println("FilterC 放行后" ); } public void destroy () { } }
我们访问/s
,运行结果:
1 2 3 4 5 6 FilterB 放行前 FilterC 放行前 FilterA 放行前 FilterA 放行后 FilterC 放行后 FilterB 放行后
即!通过注解配置的过滤器,无法排序!
通过web.xml
设置
通过web.xml
设置,谁写在上面,谁先。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <!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 > <filter > <filter-name > FilterA</filter-name > <filter-class > com.kakawanyifan.FilterA</filter-class > </filter > <filter-mapping > <filter-name > FilterA</filter-name > <url-pattern > /*</url-pattern > </filter-mapping > <filter > <filter-name > FilterB</filter-name > <filter-class > com.kakawanyifan.FilterB</filter-class > </filter > <filter-mapping > <filter-name > FilterB</filter-name > <url-pattern > /*</url-pattern > </filter-mapping > <filter > <filter-name > FilterC</filter-name > <filter-class > com.kakawanyifan.FilterC</filter-class > </filter > <filter-mapping > <filter-name > FilterC</filter-name > <url-pattern > /*</url-pattern > </filter-mapping > </web-app >
运行结果:
1 2 3 4 5 6 FilterA 放行前 FilterB 放行前 FilterC 放行前 FilterC 放行后 FilterB 放行后 FilterA 放行后
Listener
Listener,监听器,是JavaWeb三大组件(Servlet、Filter、Listener)之一。
可以在监听到application
、session
、request
三个对象创建、销毁或者添加修改删除属性时;自动执行代码 。
JavaWeb中有八大监听器,三大域对象各有一个生命周期监听器和属性操作监听器,共计6个;还有2个与HttpSession相关的感知型监听器。
三大域:
ServletContext
生命周期监听器:ServletContextListener
,有两个方法,一个在出生时调用,一个在死亡时调用。
void contextInitialized(ServletContextEvent sce)
:创建ServletContext时调用
void contextDestroyed(ServletContextEvent sce)
:销毁ServletContext时调用
属性监听器:ServletContextAttributeListener
,它有三个方法:
void attributeAdded(ServletContextAttributeEvent event)
:添加属性时调用;
void attributeReplaced(ServletContextAttributeEvent event)
:替换属性时调用;
void attributeRemoved(ServletContextAttributeEvent event)
:移除属性时调用;
HttpSession
生命周期监听器:HttpSessionListener
,有两个方法,一个在出生时调用,一个在死亡时调用;
void sessionCreated(HttpSessionEvent se)
:创建session时调用
void sessionDestroyed(HttpSessionEvent se)
:销毁session时调用
属性监听器:HttpSessioniAttributeListener
,它有三个方法
void attributeAdded(HttpSessionBindingEvent event)
:添加属性时调用
void attributeReplaced(HttpSessionBindingEvent event)
:替换属性时调用
void attributeRemoved(HttpSessionBindingEvent event)
:移除属性时调用
ServletRequest
生命周期监听器:ServletRequestListener
,有两个方法,一个在出生时调用,一个在死亡时调用;
void requestInitialized(ServletRequestEvent sre)
:创建request时调用
void requestDestroyed(ServletRequestEvent sre)
:销毁request时调用
属性监听器:ServletRequestAttributeListener
,它有三个方法
void attributeAdded(ServletRequestAttributeEvent srae)
:添加属性时调用
void attributeReplaced(ServletRequestAttributeEvent srae)
:替换属性时调用
void attributeRemoved(ServletRequestAttributeEvent srae)
:移除属性时调用
还有2个感知型监听器,都与HttpSession相关:
HttpSessionBindingListener
:监听对象与session的绑定。
HttpSessionActivationListener
:监听session的钝化与活化。
我们定义一个类,实现ServletContextListener
接口,重写所有的抽象方法,使用@WebListener
进行配置,示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.kakawanyifan;import javax.servlet.ServletContextEvent;import javax.servlet.ServletContextListener;import javax.servlet.annotation.WebListener;@WebListener public class ContextLoaderListener implements ServletContextListener { @Override public void contextInitialized (ServletContextEvent sce) { System.out.println("ContextLoaderListener..." ); } @Override public void contextDestroyed (ServletContextEvent sce) { System.out.println("contextDestroyed" ); } }
运行结果:
1 2 ContextLoaderListener... contextDestroyed
也可以通过web.xml
进行配置,示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 <!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 > <listener > <listener-class > com.kakawanyifan.ContextLoaderListener</listener-class > </listener > </web-app >