Servlet技术
文章目录
Servlet简介
• Java Servlet是和平台无关的服务器端组件,它运行在Servlet容器中。Servlet容器负责Servlet和客户的通信以及调用Servlet的方法,Servlet和客户的通信采用“请求/响应”的模式。
• Servlet可完成如下功能:
– 创建并返回基于客户请求的动态HTML页面。
– 创建可嵌入到现有 HTML 页面中的部分 HTML 页面(HTML 片段)。
– 与其它服务器资源(如数据库或基于Java的应用程序)进行通信。
Servlet API
• Servlet的框架是由两个Java包组成:
– javax.servlet包:定义了所有的Servlet类都必须实现或扩展的通用接口和类。
– javax.servlet.http 包 : 定 义 了 采 用 HTTP 协 议 通 信 的HttpServlet类。
package learn.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Servlet implementation class ServiceServlet
*/
public class ServiceServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
System.out.println(username + "=" + password);
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("doGet");
String username = request.getParameter("username");
String password = request.getParameter("password");
System.out.println(username + "=" + password);
}
}
结果是:
null=null
我们可以发现当我们重写service时程序就不默认启动doget方法了,而是启用service方法
每一个 Servlet 都必须要实现 Servlet 接口,GenericServlet 是个通用的、不特定于任何协议的 Servlet,它实现了 Servlet 接口,而HttpServlet继承于GenericServlet,因此HttpServlet也实现了Servlet接口,所以我们定义的 Servlet 只需要继承 HttpServlet 父类即可。
Servlet 接口中定义了一个 service 方法,HttpServlet 对该方法进行了实现,实现方式就是将 ServletRequest 与 ServletResponse 转换为HttpServletRequest 与 HttpServletResponse
源码:
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
} catch (ClassCastException e) {
throw new ServletException("non-HTTP request or response");
}
service(request, response);
}
转换完毕后,会调用 HttpServlet 类中自己定义的 service 方法,如下所示
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
在该 service 方法中,首先获得到请求的方法名,然后根据方法名调用对应的 doXXX 方法,比如说请求方法为 GET,那么就去调用 doGet方法;请求方法为 POST,那么就去调用 doPost 方法。
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
} catch (IllegalArgumentException iae) {
// Invalid date header - proceed as if none was set
ifModifiedSince = -1;
}
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} else {
//
// Note that this means NO servlet supports whatever
// method was requested, anywhere on this server.
//
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
在 HttpServlet 类中所提供的 doGet、doPost 等方法都是直接返回错误信息,所以我们需要在自己定义的 Servlet 类中 override 这些方法
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_get_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
} else {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
}
}
源码面前,了无秘密
• Servlet的框架的核心是javax.servlet.Servlet接口,所有的Servlet都必须实现这一接口。在Servlet接口 中 定 义 了 五 个 方 法 , 其 中 有 三 个 方 法 代 表 了Servlet的生命周期:
– init方法:负责初始化Servlet对象;
– service方法:负责响应客户的请求;
– destroy方法:当Servlet对象退出生命周期时,负责释放占用的资源。
如果你的Servlet类扩展了HttpServlet类,你通常不必实现service方法,因为HttpServlet类已经实现了service方法,
该方法的声明形式如下:
protected void service(HttpServletRequest request, HttpServletResponse response) throwsServletException, IOException;
• 在 HttpServlet 的 service 方 法 中 , 首先从HttpServletRequest对象中获取HTTP请求方式的信息,然后再根据请求方式调用相应的方法。例如:如果请求方式为GET,那么调用doGet方法;如果请求方式为POST,那么调用doPost方法。
ServletRequest接口
- ServletRequest接口中封装了客户请求信息,如客户请求方式、参数名和参数值、客户端正在使用的协议,以及发出客户请求的远程主机信息等 。ServletRequest接口还为Servlet提供了直接以二进制方式读取客户请求数据流的ServletInputStream。
- ServletRequest的子类可以为Servlet提供更多的和特定协议相关的数据. 例如: HttpServletRequest 提供了读取HTTP Head信息的方法。
ServletRequest接口中常用的方法
• getAttribute 根据参数给定的属性名返回属性值
• getContentType 返回客户请求数据MIME类型
• getInputStream 返回以二进制方式直接读取客户请求数据的输入流
• getParameter 根据给定的参数名返回参数值
• getRemoteAddr 返回远程客户主机的IP地址
• getRemoteHost 返回远程客户主机名
• getRemotePort 返回远程客户主机的端口
ServletResponse接口
• ServletResponse 接口为Servlet提供了返回响应结果的方法。它允许Servlet设置返回数据的长度和MIME类型, 并且提供输出流ServletOutputStream。
• ServletResponse子类可以提供更多和特定协议相关的方法。例如: HttpServletResponse 提供设定HTTP HEAD信息的方法。
ServletResponse接口的主要方法
• getOutputStream 返回可以向客户端发送二进制数据的输出流对象ServletOutputStream
• getWriter 返回可以向客户端发送字符数据的PrintWriter对象
• getCharacterEncoding 返回Servlet发送的响应数据的字符编码
• getContentType 返回Servlet发送的响应数据的MIME类型
• setContentType 设置Servlet发送的响应数据的MIME类型
Servlet的生命周期
• Servlet 的生命周期可以分为三个阶段:
– 初始化阶段
– 响应客户请求阶段
– 终止阶段
• 在javax.servlet.Servlet接口中定义了三个方法init(), service(), destroy(),它们将分别 在 Servlet 的 不 同 阶 段 被 调 用 。
Servlet的初始化阶段
• 在下列时刻Servlet容器装载Servlet:
– Servlet容器启动时自动装载某些Servlet
– 在Servlet容器启动后,客户首次向 Servlet 发出请求
– Servlet的类文件被更新后,重新装载Servlet
• Servlet被装载后,Servlet容器创建一个 Servlet 实例并且调用 Servlet 的 init()方法进行初始化。在Servlet的整个生命周期中,init方法只会被调用一次。
package learn.servlet;
import java.io.IOException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class InitServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println("init invoked!");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("doGet invoked!");
}
}
刷新几次结果是:
init invoked!
doGet invoked!
doGet invoked!
doGet invoked!
doGet invoked!
doGet invoked!
doGet invoked!
重启或修改后结果是:
init invoked!
doGet invoked!
init invoked!
在web.xml中加入代码里面的数字随意,系统会按数字顺序启动,如果23前面没有数字,那么23这个程序先启动
<servlet>
<description>
</description>
<display-name>InitServlet</display-name>
<servlet-name>InitServlet</servlet-name>
<servlet-class>learn.servlet.InitServlet</servlet-class>
<load-on-startup>23</load-on-startup>
</servlet>
一月 30, 2019 7:17:58 下午 org.apache.coyote.AbstractProtocol start
信息: Starting ProtocolHandler [“http-nio-8080”]
一月 30, 2019 7:17:58 下午 org.apache.coyote.AbstractProtocol start
信息: Starting ProtocolHandler [“ajp-nio-8009”]
一月 30, 2019 7:17:58 下午 org.apache.catalina.startup.Catalina start
信息: Server startup in 238 ms
Servlet的响应客户请求阶段
• 对于到达Servlet容器的客户请求,Servlet容器创建特定于这个请求的ServletRequest对象和ServletResponse对象,然后调用 Servlet 的service方法。service方法从ServletRequest对象获得客户请求信息、处理该请求,并通过ServletResponse对象向客户返回响应结果。
Servlet的终止阶段
• 当Web应用被终止,或Servlet容器终止运行,或Servlet容器重新装载Servlet的新实例时,Servlet容器会先调用 Servlet的destroy方法。在destroy方法中,可以释放Servlet所占用的资源。
Tomcat请求参数源代码深度解析
- 对于 Tomcat 来说,它会将客户传递过来的参数放在一个 Hashtable中,该 Hashtable 的定义
这是一个 String->String[]的键值映射。 - 某 些 Servlet 在 web.xml 文 件 中 只 有 < servlet> 元 素 而 没 有< servlet-mapping>元素,这样我们就无法通过 url 地址的方式访问这个 Servlet 了,这种 Servlet 通常会在< servlet>元素中配置一个< load-on-startup>子元素,让容器在启动的时候自动加载该 Servlet,并且调用其 init 方法完成一些全局性的初始化工作。
Web应用何时会被启动
- 当Servlet容器启动时,会启动所有的Web应用
- 通过控制台启动Web应用
- apache-tomcat-9.0.11\conf\tomcat-users.xml找到这个文件根据文档要求,创建管理员的用户名和密码,然后登录
登录后的界面
这就是tomcat的后台管理
接下来我们可以将eclipse里我们自己写的项目部署到这里面来,因为我是用eclipse复制tomcat到一个新目录下,所以这个manager看不到我之前写的程序,现在我给它导进来,首先生成war文件,右键项目,export,war file
然后点击
部署成功,我们看看我们的webapps文件夹
点击Undeploy是卸载
我们再看一下webapps
ServletContext和Web应用关系
当Servlet容器启动Web应用时,并为每个Web应用创建唯一的ServletContext对象。你可以把ServletContext看成是一个Web应用的服务器端组件的共享内存。在ServletContext中可以存放共享数据,它提供了读取或设置共享数据的方法:
– setAttribute(String name,Object object)把一个对象和一个属性名绑定,将这个对象存储在ServletContext中。
– getAttribute(String name)根据给定的属性名返回所绑定的对象
package learn.servlet;
import java.io.IOException;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class CounterServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
ServletContext context = request.getSession().getServletContext();
if(null == context.getAttribute("counter")){
context.setAttribute("counter", 1);
}
else {
int counter = (Integer)context.getAttribute("counter");
context.setAttribute("counter", counter + 1);
}
request.getRequestDispatcher("counter.jsp").forward(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
计数器:<%= application.getAttribute("counter") %>
</body>
</html>
Servlet的多线程同步问题(重要)
• Servlet/JSP技术和ASP、PHP等相比,由于其多线程运行而具有很高的执行效率。
• 由于Servlet/JSP默认是以多线程模式执行的,所以,在编写代码时需要非常细致地考虑多线程的同步问题。
• 如果在编写Servlet/JSP程序时不注意到多线程同步的问题,这往往造成编写的程序在少量用户访问时没有任何问题,而在并发用户上升到一定值时,就会经常出现一些莫明其妙的问题,对于这类随机性的问题调试难度也很大。
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<form action="HelloServlet">
username:<input type="text" name="username">
<br>
<input type="submit" value= "submit">
</form>
</body>
</html>
package learn.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class HelloServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private String username;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.username = request.getParameter("username");
//进行一些后端的业务处理
try {
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}
request.setAttribute("username", this.username);
request.getRequestDispatcher("hello.jsp").forward(request, response);
}
}
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
username:<%= request.getAttribute("username") %>
</body>
</html>
模拟两个用户访问:
先提交zhangsan然后提交lisi结果是:
出现这个结果的原因是因为,servlet是单实例的,所有的请求都是由一个servlet执行,所以成员变量username被后来请求的页面刷新了。
如果将hello.jsp改为
<%= request.getParameter("username") %>
结果正确,原因是,用户提交一次请求产生一个request对象,用户提交两次,产生了两个request对象,两个request对象互不干扰,虽然成员变量username的值都是lisi,但是getParameter是由请求中提取username,换句话说是直接从第一个页面提交的表单的username获取的值,所以不会出错。
将成员变量username改为局部变量结果也是正确的。
Servlet 的多线程同步问题:Servlet 本身是单实例的,这样当有多个用户同时访问某个 Servlet 时,会访问该唯一的 Servlet 实例中的成员变量,如果对成员变量进行写入操作,那就会导致 Servlet的多线程问题,即数据不一致。
加上这条语句说明servlet是支持多线程的
//得到当前线程的名字
System.out.println(Thread.currentThread().getName());
结果是:
http-nio-8080-exec-4
http-nio-8080-exec-2
解决同步问题的方案
Servlet实现javax.servlet.SingleThreadModel(Servlet2.4中已经废弃该接口),此时Servlet容器将保证Servlet实例以单线程方式运行,也就是说,同一时刻,只会有一个线程执行Servlet的service()方法。
• 去除实例变量,使用局部变量,参见HelloServlet
• 使用同步代码块: synchronized{…}
Cookie
• Cookie的英文原意是“点心”,它是用户访问Web服务器时,服务器在用户硬盘上存放的信息,好像是服务器送给客户的“点心”。
• 服务器可以根据Cookie来跟踪用户,这对于需要区别用户的场合(如电子商务)特别有用。
• 一个Cookie包含一对Key/Value。下面的代码生成一个Cookie并将它写到用户的硬盘上:
Cookie theCookie = new Cookie("cookieName","cookieValue");
response.addCookie(the Cookie);
package learn.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class CookieServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private int count1;
private int count2;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Cookie cookie = new Cookie("cookieName" + count1++, "cookieValue" + count2++);
//最大存活时间的单位是秒
cookie.setMaxAge(10);
response.addCookie(cookie);
Cookie[] cookies = request.getCookies();
if(null == cookies){
return;
}
for(Cookie cookie2 : cookies){
System.out.println("cookie name: " + cookie2.getName());
System.out.println("cookie value: " + cookie2.getName());
}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
doGet(request, response);
}
}
结果是:
cookie name: cookieName0
cookie value: cookieName0
刷新一次
cookie name: cookieName0
cookie value: cookieName0
cookie name: cookieName1
cookie value: cookieName1
刷新2次
cookie name: cookieName0
cookie value: cookieName0
cookie name: cookieName1
cookie value: cookieName1
cookie name: cookieName2
cookie value: cookieName2
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<%! int count1 = 0;
int count2 = 0;
%>
<% Cookie cookie = new Cookie("cookieName" + count1++,"cookieValue" + count2++);
cookie.setMaxAge(10);
response.addCookie(cookie);
Cookie[] cookies = request.getCookies();
if(null == cookies){
return;
}
for(Cookie c : cookies){
%>
<p>
<b>cookie name:</b><%=c.getName() %>
<br>
<b>cookie value:</b><%=c.getValue() %>
<% }%>
</body>
</html>
比较Servlet和JSP
• 有许多相似之处,都可以生成动态网页
• JSP的优点是擅长于网页制作,生成动态页面,比较直观。JSP的缺点是不容易跟踪与排错。
• Servlet是纯Java语言,擅长于处理流程和业务逻辑。Servlet的缺点是生成动态网页不直观。
练习
-
问题:HttpServletRequest对象是由谁创建的?
• 选项:
**(A)**由Servlet容器负责创建,对于每个HTTP请求, Servlet容器都会创建一个HttpServletRequest对象
(B)由JavaWeb应用的Servlet或JSP组件负责创建,当Servlet或JSP组件响应HTTP请求时,先创建HttpServletRequest对象 -
问题:从HTTP请求中,获得请求参数,应该调用哪个方法?
• 选项:
(A)调用HttpServletRequest对象的getAttribute()方法
(B)调用ServletContext对象的getAttribute()方法
**©**调用HttpServletRequest对象的getParameter()方法 -
问题:ServletContext对象是由谁创建的?
• 选项:
(A)由Servlet容器负责创建,对于每个HTTP请求, Servlet容器都会创建一个ServletContext对象
(B)由JavaWeb应用本身负责为自己创建一个ServletContext对象
**©**由Servlet容器负责创建,对于每个JavaWeb应用,在启动时,Servlet容器都会创建一个ServletContext对象 -
分析ServletRequest、ServletResponse、Servlet、ServletContext等对象的生命周期,何时被创建,何时被销毁。
ServletRequest、ServletResponse请求来了创建,响应过去销毁
Servlet是自己创建
ServletContext服务器启动、关闭或重启时被创建 -
问题:jspForward1.jsp要把请求转发给jspForward2.jsp,应该在jspForward1.jsp中如何实现?
• 选项:
(A) < a href=“jspForward2.jsp”>jspForward2.jsp
(B) < jsp:forward page=“jspForward2.jsp”> -
问题:jspForward1.jsp要把请求转发给jspForward2.jsp,在转发的时候,希望
把用户名 “小新”传给jspForward2.jsp,如何实现?
• 选项:
(A) request.setParameter(“小新”);
(B) request.setAttribute(“username”, “小新”);
© < a href=“jspForward2.jsp?username=小新”>jspForward2.jsp -
问题:当浏览器第二次访问该JSP网页时的输出结果是什么?
<!% int a=0; %>
<% int b=0;
a++;
b++;
%>
a:<%= a %> <br>
b:<%= b %>
a:2
b:1
- 问题:request.getAttribute()和request.getParameter()方法有什么异同?
• 选项:
**(A)**前者返回Object类型的对象,后者返回String类型的对象
(B) request.getAttribute()和request.setAttribute()对应
© request.getParameter()和request.setParameter()对应。
**(D)**当两个Web组件之间为链接关系时,被链接的组件通过getParameter()方法来获得请求参数
**(E)**当两个Web组件之间为转发关系时,转发目标组件通过getAttribute()方法来和转发源组件共享request范围内的数据。
(F) request.getParameter()方法传递的数据,会从Web客户端传到Web服务器端,代表HTTP请求数据。
(G) request.setAttribute()和getAttribute()方法传递的数据只会存在于Web容器内部,在具有转发关系的Web组件之间共享。