Servlet
Servlet入门
Servlet概念
Java Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。
Servlet能做这些任务:
- 读取客户端(浏览器)发送的显式的数据。这包括网页上的 HTML 表单,或者也可以是来自 applet 或自定义的 HTTP 客户端程序的表单。
- 读取客户端(浏览器)发送的隐式的 HTTP 请求数据。这包括 cookies、媒体类型和浏览器能理解的压缩格式等等。
- 处理数据并生成结果。这个过程可能需要访问数据库,执行 RMI 或 CORBA 调用,调用 Web 服务,或者直接计算得出对应的响应。
- 发送显式的数据(即文档)到客户端(浏览器)。该文档的格式可以是多种多样的,包括文本文件(HTML 或 XML)、二进制文件(GIF 图像)、Excel 等。
- 发送隐式的 HTTP 响应到客户端(浏览器)。这包括告诉浏览器或其他客户端被返回的文档类型(例如 HTML),设置 cookies 和缓存参数,以及其他类似的任务。
HTML表单: 表单是一个包含表单元素的区域。表单元素是允许用户在表单中输入内容,比如:文本域(textarea)、下拉列表、单选框(radio-buttons)、复选框(checkboxes)等等。表单都以
<form>
和</form>
形式表现
1
2
3
4 <form>
First name: <input type="text" name="firstname"><br>
Last name: <iput type="text" name="lastname">
</form>
实现Servlet接口
继承Servlet
1 | public class ServletDemo1 implements Servlet { |
继承GenericServlet
1 | public class ServletDemo2 extends GenericServlet { |
实现了Servlet接口的除service方法,但很少使用
继承HttpServlet
1 | public class ServletDemo3 extends HttpServlet { |
HttpServlet
最为常用,需要实现doGet()和doPost()
方法。因为Httpservlet
本身覆写了Servlet
的service()
方法,其中通过 getMethod()
方法判断请求的类型,从而调用 doGet()
或者 doPost()
处理 get,post 请求,使用者只需要继承 HttpServlet,然后重写 doPost()
或者 doGet()
方法处理请求即可。
Servlet生命周期
Servlet 生命周期可被定义为从创建直到毁灭的整个过程。一次创建,到处服务.
- 1.实例化(使用构造方法创建对象)
- 2.初始化 执行init方法
- 3.服务 执行service方法
- 4.销毁 执行destroy方法
init()
init 方法被设计成只调用一次。它在第一次创建 Servlet 时被调用,在后续每次用户请求时不再调用。
1 | public void init() throws ServletException { |
service()
Servlet 容器(即 Web 服务器)调用 service()方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。
每次服务器接收到一个 Servlet 请求时,服务器会产生一个新的线程并调用服务。service() 方法检查 HTTP 请求类型(GET、POST、PUT、DELETE 等),并在适当的时候调用 doGet、doPost、doPut,doDelete 等方法。
1 | public void service(ServletRequest request, |
doGet() : GET 请求来自于一个 URL 的正常请求,或者来自于一个未指定 METHOD 的 HTML 表单,它由 doGet() 方法处理。
doPost(): POST 请求来自于一个特别指定了 METHOD 为 POST 的 HTML 表单,它由 doPost() 方法处理。
destroy()
destroy() 方法只会被调用一次,在 Servlet 生命周期结束时被调用。destroy() 方法可以让您的 Servlet 关闭数据库连接、停止后台线程、把 Cookie 列表或点击计数器写入到磁盘,并执行其他类似的清理活动。
Servlet实例
Tomcat搭建
目前我使用的是Tomcat8版本,不知道为什么最新的Tomcat10版本会出现问题。反正遇到问题版本回溯就对了!
安装好Tomcat并设置好环境变量后,我们就可以开始部署自己的Servlet了。
手动搭建Servlet
安装好Tomcat后能发现目录Webapps
,该目录下的文件夹都代表着已经部署到了Tomcat中,我们要部署新项目就直接在Webapps
下新建目录就行。我们以新项目WebProject
为例:
在Webapps
下新建文件夹WebProject
,并在新文件夹下新建目录WEB-INF
,其中又新建目录lib
和classes
以及web.xml
(可以将Webapps
的ROOT
的WEB-INF
中直接复制过来)
classes用来存放Servlet的java源码;lib用来存放
servlet-api.jar
包;web.xml
用来指示网络访问地址.
- 编写源码
在classes下新建myservlet.java
文件
1 | import javax.servlet.*; |
保存好后在命令行中通过javac myservlet.java
编译成myservlet.class
,同时删除myservlet.java
(可能可以不用删,最好删了)
通过在文件资源管理器的收缩栏输入cmd就可以直接进入到该目录的命令行中,很方便。当然也可以通过
Shift+右键
并点击PowerShell窗口
进入高级点的命令行,虽然我不知道两个有什么区别。
- 导入servlet-api库
将Tomcat下的lib目录中找到servlet-api.jar
,将其复制到项目文件的lib目录下
- 编写web.xml
将web.xml修改成如下:
1 |
|
<servlet>
:<servlet-name>
可以任取;<servlet-class>
指明了Servlet的源码路径,应为直接存放在了classes下,这里就只需要填写源码名.
<servlet-mapping>
:<servlet-name>
必须和上面一致;<url-pattern>
指明了在网络的访问路径.所以网络访问Servlet的逻辑是这样的:
https://localhost:8080/WebProject/A
找到<servlet-name>
my,通过my又找到了<servlet-class>
,进而运行Servlet.如果在src中新添了一个
Servlet.java
,相应就需要在web.xml
中新增一组<servlet></servlet>和<servlet-mapping></servlet-mapping>
- 访问Servlet
在Tomcat的bin
目录下启动startup.bat
脚本,然后在浏览器输入localhost:8080/WebProject/A
,会发现页面空白,打开startup.bat
界面会发现输出了My first Servlet
这是因为Servlet启动时调用的是service()
,而System.out.println()
是在控制台打印信息,想在网页上打印就需要用到doGet()和doPost()
.
- 项目目录结构
1 | E:\apache-tomcat-8.5.63\webapps\WebProject |
IDEA开发Servlet
先通过以下教程创建出Web工程如何使用IDEA2020.2新建servlet工程
再根据视频一步一步部署ServletIDEA实现Servlet部署
Servlet项目热部署
手动部署Servlet有一个很明显的缺点,当源码更新后需要重新build并重新将com文件复制到classes中,再手动启动Tomcat才能看到更新的效果。通过热部署我们可以将这一系列操作在IDEA中执行。
通过视频:IDEA热部署Web项目可以在IDEA中部署好Tomcat服务器,这样可以在IDEA中启动Tomcat服务器,在网页地址栏访问Servlet后就可以在IDEA控制台里看到输出消息了。
Servlet项目打包
我们发现,热部署可以使得代码的调试变得十分方便。如果Servlet开发完毕后。我们需要将整个WebProject
放到Tomcat的webapps
中,这样才能成为开发完整版的Servlet。
如果我们将项目打包成.war
包,只需要将.war
放到webapps
下,重新启动Tomcat后该.war
就会被解压成完整的项目。注意,如果需要更新.war
我们需要重新打包然后替换原先的.war
。
HTTP协议
HTTP协议特点
支持B/S模式和C/S模式
灵活:HTTp允许传输任意类型的数据,传输的类型又Content-Type标识
无连接:
- HTTP1.0采用的是短连接,即一次请求和响应完成后便断开连接
- HTTP1.1采用长连接,连接建立后如果在数秒内不再有新请求则断开连接
无状态:客户端和服务器之间的通信内容HTTP协议无法获知,就是说状态是保存在客户端和服务器上的
请求报文和响应报文
请求报文
请求报文由四个部分组成:
- 请求行:
请求方法/地址 URI协议/版本
- 请求头
- 空行
- 请求正文
1 | POST/http://localhost:8080/WebProject_war/A HTTP/1.1 |
响应报文
响应报文由四个部分组成
- 状态行:
URI协议/版本 状态码 状态描述
- 响应头
- 空行
- 响应正文:响应头中的
Conten-Type
内容和相应正文的内容对应的
1 | 200 OK |
状态码
状态码 | 状态描述 | 说明 |
---|---|---|
200 | OK | 客户端请求成功 |
302 | Found | 临时重定向 |
403 | Forbidden | 服务器收到请求,但是拒绝提供服务。响应正文中会提供原因 |
404 | Not Found | 请求资源不存在 |
500 | Internal Server Error | 服务器内部发生了不可预期的错误 |
HTTPServlet
GenericServlet
GenericServlet继承了Servlet,已经重写了四个基本方法,而是将service()
覆写为抽象方法,这样继承GenericServlet就必须实现service()
,也意味着只需要覆写一个方法就能实现Servlet
doGet()/doPost()
HTTPServlet继承了GenericServlet,但实现了service()
方法。
注意的是,HttpServlet的service()
没有直接提供服务,在提供服务之前先判定了请求类型Get
,再根据请求类型调用相应的方法doGet()
以提供服务。因此继承HTTPServlet需要实现的不是service()
,转而去实现也就是覆写doGet(),doPost()
等方法。
web.xml配置
- url-pattern
- 精确匹配:
/name
只有url路径完全正确才能触发对应Servlet - 后缀匹配:
.*name
只要结尾是name
就能触发Servlet - 通配符匹配:
/*
无论输入什么甚至不输入也能触发Servlet
- 精确匹配:
通配符匹配的Servlet不会覆盖精确匹配的Servlet,就是说输入
/name
不会优先触发通配符Servlet,即使符合匹配条件。
- load-on-startup
- 关系到Web应用程序启动时是否也同时启动该Servlet,在
<service>
中配置 - 值为负数或是没有设置时,容器会当Servlet被请求时才加载
- 值为非负数时,表示Web应用启动时就开始加载Servlet容器。值越小则越早启动该Servlet容器。值相同,容器就会自己选择顺序。
- 关系到Web应用程序启动时是否也同时启动该Servlet,在
@WebServlet
Servlet3.0版本后新增@WebServlet注解,通过注解后就无需在web.xml中配置<servlet>,<servlet-mapping>
。注解与web.xml不冲突。
name:Servlet名字(可忽略)
value/urlPatterns:配置url路径,作用和
<url-pattern>
一样,可配置多个路径loadOnStartup:作用和
<load-on-startup>
一样
1 |
|
这样可以通过/http、/HTTP、/httpservlet触发Servlet。
Requset接收
方法 | 说明 |
---|---|
String getParameter(String name) | 根据表单的键获取对应的值 |
void setCharacterEncoding(String charset) | 指定每个请求的编码 |
我们在项目的web
目录下建立register.html
用作用户的访问页面,用户在该网页的表单上提交信息,随后网页将信息以Get请求发送给registerServlet,随后Servlet调用doGet()方法完成操作。至此Request接收完毕。
1 | //register.html |
1 | //registerServlet.java |
Update Tomcat Application
后会发现out.artifacts.WebProject_war_exploded
会新增register.html
,说明已经全部部署完毕。网页地址输入http://localhost:8080/WebProject_war_exploded/register.html
即可访问。
输入完毕后,在地址栏可以看到发送的Request请求:http://localhost:8080/WebProject_war_exploded/rs?username=theo&password=123456789
,?
用作分隔URL和传输数据,数据以键值对形式表示,数据间用&
分隔。
相较于Get请求数据,有以下不同点需要注意
- register.xml中的method值更改为post
- registerServlet中需要实现doPost()方法
- Post方法提交Requset请求时如包含中文字符,在Servlet解析时
req.GetParameter()
会出现乱码,因而需要提前设置编码格式:req.setCharacterEncoding("utf-8")
- Post请求时网页地址栏不会出现传输数据,比起Get请求更为安全。因为数据藏在了传输的数据包中。
Response响应
方法名称 | 作用 |
---|---|
setHeader(name,value) | 设置响应信息头 |
setContentType(String) | 设置响应文件类型,响应式的编码格式 |
setCharacterEncoding(String) | 设置服务器响应内容编码格式 |
PrintWriter getWriter() | 获取字符输出流 |
1 | //registerServlet.java |
- 中文乱码问题
通过printwriter可以在网页界面上打印信息,而不是仅仅在终端打印。同样注意客户端出现中文乱码的问题。因为服务器默认采用ISO-8859-1编码响应报文,通过resp.setCharacterEncoding("utf-8")
让响应报文以utf-8编写,以便适配各种浏览器。同样客户端不一定默认以utf-8来解析响应报文,通过resp.setHeader("Content-Type","text/html;charset=utf-8")
设置响应头,告诉浏览器用utf-8来解析。服务器和客户端都用同一种编码格式就不会出现乱码现象。
使用
setContentType("text/html;charset=utf-8")
可以省略以上两行代码。更为推荐
Servlet四种响应
getWriter()
1 | PrintWriter printwriter = resp.getWriter(); |
getWriter 返回可将字符文本发送到客户端的 PrintWriter
对象。
getOutputStream()
1 | OutputStream os = response.getOutputStream(); |
getOutputStream 返回适用于在响应中编写二进制数据的ServletOutputStream
。OutputStream
通常与InputStream
一同使用,后者用于获取本地资源,前者用于将本地资源响应给网站。
1 | protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { |
转发和重定向
- 转发
1 | request.getRequestDispatcher(String URI).forward(request,response); |
URI
可以是Web中的静动态页面(“loginsuccess.jsp”)或另一个Servlet(“/loginServlet”)【@WebServlet(value = “loginServlet”)】。转发的执行优先度要高于doGet()的其他显示函数,即原先的页面不会显示。(原因会在后面解释)
1 |
|
1 |
|
转发的特点是request作用域
不会改变,网页地址栏的URL也不会改变,因为Servlet的转变是在服务器内部执行的,但客户端显示的还是在访问.../a
。对于上面两个Servlet,访问AServlet,我们向request作用域添加参数name
,再将request域和response作用域完整发送给BServlet,由BServlet做详细的处理。
不单单是Servlet中可以运用转发,jsp中也可以使用转发
1 | <jsp:forward page="b.jsp"/> <!--访问该jsp页面时将不会显示原页面内容,而是直接显示b.jsp内容--> |
转发,就是延长了request的作用域,将转向的页面覆盖到原先页面上。
- 重定向
1 | response.sendRedirect(String URI); |
URI
可以是Web中的静动态页面(“loginsuccess.jsp”)或另一个Servlet(“/TZQ_war_exploded/loginServlet”)【@WebServlet(value = “loginServlet”)】。转发的执行优先度要高于doGet()的其他显示函数,即原先的页面不会显示。
如果重定向的资源不是直接部署在Web目录下,如login.jsp。那么重定向是需要填写完整路径。
重定向的原理是访问该页面时,服务器会给你响应,让你去访问定向的资源。于是浏览器就会重新向新资源重新发送一次请求,这样我们也不难知道:重定向后网页的地址栏会改变成新资源的URL,同时request作用域和response作用域也会清空。如果想要携带信息进行重定向,URI可以传入参数:/TZQ_war_exploded/loginServlet?username=theo&password=123456
两个响应情况冲突
一个servlet请求会有一个request请求和一个response响应,那如果一个response想响应两次呢?一般都是会出错的。代码中避免使用多个响应的情况。
- getWriter()+getOutputStream() / 转发+重定向
这两种情况的冲突都无法正常显示任意一个响应情况的页面。
- getWriter() + 转发/重定向
只能正常显示转发/重定向的响应情况页面。后台会打印IllegalStateException
异常。
- 转发/重定向 + getOutputStream()
只能正常显示getOutputStream()响应内容。后台会打印IllegalStateException
异常。
1 | protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { |
Cookie
什么是Cookie?
- Cookie是在浏览器访问Web服务器的某个资源时,由Web服务器在HTTP响应消息头中附带传送给浏览器的一小段数据
- 一旦Web浏览器保存了某段Cookie,那么它在之后的每次访问服务器,都应在请求头中将该Cookie回传给Web服务器
- Cookie时将HTTP状态保存在客户端中的一种方式。
- Cookie缺点:
- 大多数浏览器会对Cookie大小做限制
- 有些用户可能会在浏览器中禁用Cookie
- Cookie可能会被劫持并被篡改,有一定风险。
添加Cookie
1 |
|
访问该Servlet后就能在开发者调试台中的网络项找到Cookie的设置。
1 | cookie.setPath("/TZQ_war_exploded/getCookie"); //指定路径上的Servlet才能获取浏览器请求头的Cookie |
1 | cookie.setMaxAge(int time); //time>0,设置cookie有效时间time秒;time<0或默认,设置cookie有效至浏览器关闭。 |
获取Cookie
1 |
|
修改Cookie
同一浏览器访问的Servlet中如果也设置了Cookie,而新Cookie的CookieName与CookiePath与浏览器中某个Cookie相同,那么新Cookie的CookieValue与CookieMaxAge会覆盖老Cookie,从而修改Cookie。
编码解码Cookie
Cookie默认不能储存中文,只能包含ASCII。因此服务器添加Cookie时需要对中文编码,获取Cookie时也需要对中文解码。
1 | new Cookie("姓名","琅然") -> new Cookie(URLEncoder.encode("姓名","UTF-8"),URLEncoder.encode("琅然","UTF-8")) |
1 | System.out.println(cookie.getName()+":"+cookie.getValue()) -> |
Session
Session概述
Seesion指的是在一段时间内,单个客户端与Web服务器之间的一连串相关交互信息。所以在一个Session中,客户可能会多次请求同一个资源,也可以是不同服务器的不同资源
Seesion由服务器创建,在与浏览器见创建会话时就会分配一个Session对象,之后该浏览器发起的多次请求都属于同一会话。
一次会话是指使用同一浏览器发送的多次请求,一旦浏览器关闭,会话结束。
Session分配
1 | HttpSession session = request.getSession(); //分配Session |
1 | String sessionid = session.getId(); //获取浏览器保存的Session |
浏览器每请求一次,服务器就能获取一次Session。只要是同一浏览器,服务器获取的SessionID就不会变。
同样,Session也能像Cookie一样,在服务器分配给浏览器Session时,也能在Session中存入信息,之后浏览器的请求都会带上该信息。
1 | session.setAttribute("username","theo"); //值可以是任何数据类型 |
1 | session.setMaxInactiveInterval(int Time); //设置Session的生命时长为Time秒,过期则Session自动销毁 |
Session获取与销毁
浏览器发送的请求中包含了Session,服务器可以获取该Session。如果该浏览器之前没有获取过服务器分配的的Session,那么服务器将无法获取Session。
1 | HttpSession session = request.getSession(); |
服务器也可以选择销毁浏览器的Seesion,而不用等到浏览器关闭。
1 | session.removeAttribute("username"); |
Session与重定向
之前我们知道重定向相当于浏览器对服务器发起一个新请求,原request作用域将会被刷新,如果不在重定向的URI中添加参数,那么新的request作用域将与原作用域没有任何关系。但我们注意到Session是一直保存在浏览器中的,即使是重定向Session内容也不会改变。因此前页面可以将部分信息保存在Session中,后页面就可以从Session中获取前页面保留的信息。
相比于重定向,Session将有以下明显的优势:
- 相比于在URI中传参,Seesion保存的信息不会明文显示在地址栏,更为隐私。
- Session可保存的信息更多,随着页面的多次跳转,Session不会使得URI越来越臃肿(重定向添加参数)。
禁用Cookie
默认情况下,服务器会通过Cookie方式把Session分配给浏览器,如果用户禁用了Cookie,浏览器将不会保存SessionID,那么浏览器再次请求服务器时不会携带Session信息,服务器就会认为这是一个新的浏览器,就会分配一个新的Session和SessionID。
这样我们就需要绕开Cookie,告诉浏览器保存Session。把Session封装到一个URL里供浏览器重定向是一个选择。
1 |
|
这样原本访问/TZQ_war_exploded/Session
直接分配Session的方式换成了:让浏览器去访问新的URL,该URL中包含了分配的Session,浏览器从URL中获取SessionID并保存,成功绕开了Cookie。
这样还有一个好处,如果浏览器没有禁用Cookie,封装URL和重定向的代码将会失效,也不影响Session的分配。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!