前言
前面为大家介绍了如何使用 servlet 写一个简单的网站,前面只是大概了解了如何使用简单的 servlet,而平时网站的逻辑肯定不会简单,所以就需要详细的了解 servlet,servlet 作为一个 API,里面含有很多的类,不同的类中又含有不同的方法,这篇文章将为大家详细的介绍 servlet API 当中常用的类和方法
什么是Servlet
Servlet是一种使用Java编写的服务器端程序,也称为小服务程序或服务连接器。它具有独立于平台和协议的特性,主要功能在于交互式地浏览和生成数据,生成动态Web内容。它担当客户请求(Web浏览器或其他HTTP客户程序)与服务器响应(HTTP服务器上的数据库或应用程序)的中间层。
从原理上讲,Servlet可以响应任何类型的请求,但绝大多数情况下Servlet只用来扩展基于HTTP协议的Web服务器。最早支持Servlet标准的是JavaSoft的Java Web Server,此后,一些其它的基于Java的Web服务器开始支持标准的Servlet。
Servlet运行于支持Java的应用服务器中,如Tomcat、Jetty、Server等,被称为Servlet容器。Servlet容器负责调用和执行Servlet。在开发Servlet时,需要遵循Servlet规范,通过继承HttpServlet类来实现。
HttpServlet
我们在写 servlet 的代码的时候,类需要继承 HttpServlet 类,HttpServlet 中的主要方法有以下几种。
方法名称 | 调用时机 |
init | 在 HttpServlet 实例化之后被调用一次 |
destory | 在 HttpServlet 实例不再使用的时候调用一次 |
service | 收到 HTTP 请求的时候调用 |
doGet | 收到 GET 请求的时候调用(由 service 方法调用) |
doPost | 收到 POST 请求的时候调用(由 service 方法调用) |
doPut/doDelete/doOptions/… | 收到其他请求的时候调用(由 service 方法调用) |
上面的几种方法都是 HTTP 服务器自动调用的,不需要我们手动调用,我们需要做的就是重写doGet、doPost、doPut……等这些方法,将我们的代码逻辑添加进去。
既然这样的话,就会有很多人觉得这样的设定被限制了,确实这样的设定,降低了上限,但是却提升了下线,并且在实际的开发中,不要求有多么高的上限,而是需要保证下线不能很低,所以这种模式在实际开发中更常用。
当 HttpServlet 实例被创建的时候,就会自动调用 init() 方法,对实例对象进行一系列的初始化;然后当 HTTP 服务器接收到客户端的请求了之后,就会调用 service() 方法,并且在这个 service() 方法中,会调用对应的 doGet()、doPost()、doPut()……等方法;当服务器处理完请求数据并且返回响应的时候,如果没有收到请求了之后,就会调用 destory() 方法进行释放资源的操作,但是很可惜,destory() 并不是每次都能被调用到。关闭 tomcat 的方法有两种,一种是直接在资源管理器中关闭这个进程,那么这时 HttpServlet 中就来不及调用 destory() 方法,而如果是通过 8005 端口访问 tomcat 的管理端对 tomcat 下达“关闭”命令的时候,HttpServlet 中才会调用到 destory() 方法。
上面的描述也可以被称为 HttpServlet 的生命周期
HttpServletRequest
HttpServletRequest 表示 HTTP 请求,当 HTTP 服务器接收到客户端发送来的请求之后,会创建出 HttpServletRequest 对象,并且通过请求的数据去构造这个 HttpServletRequet 对象,在 service() 方法中调用的 doGet()、doPost()、doPut()等方法中传递的 HttpServletRequest 对象都是被处理之后的对象。
以下是 HttpServletRequest 类中的几种常用方法。
方法 | 描述 |
String getProtocol() | 返回请求协议的名称和版本 |
String getMethod() | 返回请求的 HTTP 方法的名称,例如,GET、POST 或 PUT。 |
String getRequestURI() | 从协议名称直到 HTTP 请求的第一行的查询字符串中,返回该请求的 URL 的一部分 |
String getContextPath() | 返回指示请求上下文的请求 URI 部分 |
String getQueryString() | 返回包含在路径后的请求 URL 中的查询字符串 |
Enumeration getParameterNames() | 返回一个 String 对象的枚举,包含在该请求中包含的参数的名称 |
String getParameter(String name) | 以字符串形式返回请求参数的值,或者如果参数不存在则返回null |
String[] getParameterValues(String name) | 返回一个字符串对象的数组,包含所有给定的请求参数的值,如果参数不存在则返回 null |
Enumeration getHeaderNames() | 返回一个枚举,包含在该请求中包含的所有的头名 |
String getHeader(String name) | 以字符串形式返回指定的请求头的值 |
String getCharacterEncoding() | 返回请求主体中使用的字符编码的名称 |
String getContentType() | 返回请求主体的 MIME 类型,如果不知道类型则返回 null |
int getContentLength() | 以字节为单位返回请求主体的长度,并提供输入流,或者如果长度未知则返回 -1 |
nputStream getInputStream() | 用于读取请求的 body 内容. 返回一个 InputStream 对象 |
下面我将用一些代码来展示这些方法的使用。
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; import java.util.Enumeration; @WebServlet("/httpservletreq") public class HttpServletreq extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { StringBuilder sb = new StringBuilder(); sb.append(req.getProtocol()); //返回协议名称和版本 sb.append("<br>"); sb.append(req.getMethod()); //返回HTTP请求的方法 sb.append("<br>"); sb.append(req.getRequestURI()); //返回从协议名开始直到HTTP请求中的query string sb.append("<br>"); sb.append(req.getContextPath()); //返回HTTP请求的URI部分 sb.append("<br>"); sb.append(req.getQueryString()); //返回请求URL中的query string部分 sb.append("<br>"); Enumeration<String> parameter = req.getParameterNames(); //返回URL中query string的所有key while (parameter.hasMoreElements()) { String key = (String)parameter.nextElement(); //获取query string的指定key的value String value = req.getParameter(key); sb.append(key + ": " + value + "<br>"); } Enumeration<String> enumeration = req.getHeaderNames(); //返回请求Header中的所有key while (enumeration.hasMoreElements()) { String key = enumeration.nextElement(); //获取Header中指定key的value String value = req.getHeader(key); sb.append(key + ": " + value + "<br>"); } sb.append(req.getCharacterEncoding()); //返回请求主体使用的字符编码 sb.append("<br>"); sb.append(req.getContentType()); //返回请求主体的MIME类型 sb.append("<br>"); sb.append(req.getContentLength()); //返回请求主体body部分的长度 resp.setContentType("test/html; charset=utf8"); //告诉浏览器我们返回的数据的数据类型 resp.getWriter().write(sb.toString()); //将StringBuider对象中的内容返回给浏览器 } }
当写出上面的代码之后,启动这个程序,并且我们使用 postman 来构造一个 HTTP 请求。
String[] getParameterValues(String name)往往使用在一个key有多个value的时候,比如a=1&a=2。
URL和URI的区别
含义不同:URI是uniform resource identifier,统一资源标识符,用来唯一的标识一个资源。URL是uniform resource locator,统一资源定位器,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。
格式不同:URL的格式一般由下列三部分组成:第一部分是协议(或称为服务方式);第二部分是存有该资源的主机IP地址(有时也包括端口号);第三部分是主机资源的具体地址。URI一般由三部分组成:访问资源的命名机制;存放资源的主机名;资源自身的名称,由路径表示。
位置不同:绝对URL(absolute URL)显示文件的完整路径,这意味着绝对URL本身所在的位置与被引用的实际文件的位置无关。绝对的URI指以scheme(后面跟着冒号)开头的URI,你可以把绝对的URI看作是以某种方式引用某种资源,而这种方式对标识符出现的环境没有依赖。
简单来说,URL 是包含于 URI 的。
这里我们构造的 HTTP 请求中没有 query string 部分,所以getQueryString() getParameterNames()返回的都是null,这里我们可以添加 query string。
除了 query string,我们还可以通过 POST 方法来通过 HTTP 请求的 body 来传递参数。
1. form表单构造POST请求
使用 POST 方法就需要在我们的程序中重写 doPost 方法。
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { StringBuilder sb = new StringBuilder(); sb.append(req.getProtocol()); //返回协议名称和版本 sb.append("<br>"); sb.append(req.getMethod()); //返回HTTP请求的方法 sb.append("<br>"); sb.append(req.getRequestURI()); //返回从协议名开始直到HTTP请求中的query string sb.append("<br>"); sb.append(req.getContextPath()); //返回HTTP请求的URI部分 sb.append("<br>"); sb.append(req.getQueryString()); //返回请求URL中的query string部分 sb.append("<br>"); Enumeration<String> parameter = req.getParameterNames(); //返回URL中query string的所有key while (parameter.hasMoreElements()) { String key = (String)parameter.nextElement(); //获取query string的指定key的value String value = req.getParameter(key); sb.append(key + ": " + value + "<br>"); } Enumeration<String> enumeration = req.getHeaderNames(); //返回请求Header中的所有key while (enumeration.hasMoreElements()) { String key = enumeration.nextElement(); //获取Header中指定key的value String value = req.getHeader(key); sb.append(key + ": " + value + "<br>"); } sb.append(req.getCharacterEncoding()); //返回请求主体使用的字符编码 sb.append("<br>"); sb.append(req.getContentType()); //返回请求主体的MIME类型 sb.append("<br>"); sb.append(req.getContentLength()); //返回请求主体body部分的长度 resp.setContentType("test/html; charset=utf8"); //告诉浏览器我们返回的数据的数据类型 resp.getWriter().write(sb.toString()); //将StringBuider对象中的内容返回给浏览器 }
postman form 表单构造 POST 请求。
Fiddler 抓取 HTTP 请求包。
form 表单实际上就是将query string部分给放入到 body 部分,我们还可以使用 JSON 的方式来表示 body 部分。
2. JSON形式表示body部分
form 表单表示 body 的方法是 servlet 本身就支持的,而使用 JSON 格式则需要引入第三方库 Jackson。
然后选择一个版本,将坐标给导入到 maven 中。
jackson 中主要使用的就是一个类 ObjectMapper 和两个方法:read 方法——将 json 字符串映射为一个Java对象,write 方法——把一个Java对象映射为一个json字符串。
import com.fasterxml.jackson.databind.ObjectMapper; 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; class Request { public String username; public String password; } class Response { public boolean ok; } @WebServlet("/jsonpost") public class JsonPost extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ObjectMapper objectMapper = new ObjectMapper(); Request request = objectMapper.readValue(req.getInputStream(), Request.class); System.out.println("username: " + request.username); System.out.println("password: " + request.password); Response response = new Response(); response.ok = true; String respJson = objectMapper.writeValueAsString(response); resp.setContentType("application/json; charset=utf8"); resp.getWriter().write(respJson); } }
Request request = objectMapper.readValue(req.getInputStream(), Request.class); 这个方法首先会执行 req.getInputStream() 方法,获取到 InputStream 流,然后 readValue() 方法会读取这个 InputStream 中的所有数据,并且将这里面的数据按照 JSON 的格式进行解析,解析成 Map(键值对)的形式;然后 readValue() 方法就会调用反射的 API,创建出 Request.class 实例,获取到 Request 类中的所有属性——username和password,然后在前面解析成的 Map 中查找 Request 中的属性,并且将查找的结果复制给对应属性。
objectMapper.writeValueAsString(response) 这个方法则是通过传入的参数获取到类对象,获取到类中的属性,然后在这个类中查找这个属性的值,并且按照 JSON 的格式,将属性和属性值构造成字符串。形如:“{ok:true}”
服务器部分代码编写完成之后,我们就来构造一个 JSON 表示body的 HTTP 请求。
Fiddler 抓取 HTTP 请求。
HttpServletResponse
HttpServletResponse 是服务端根据客户端发来的请求做出响应的类,HttpServletResponse中也包含了很多的方法。
方法 | 描述 |
void setStatus(int sc) | 为该响应设置状态码 |
void setHeader(String name,String value) | 设置一个带有给定的名称和值的 header. 如果 name 已经存在,则覆盖旧的值 |
void addHeader(String name, String value) | 添加一个带有给定的名称和值的 header. 如果 name 已经存在,不覆盖旧的值, 并列添加新的键值对 |
void setContentType(String type) | 设置被发送到客户端的响应的内容类型 |
void setCharacterEncoding(String charset) | 设置被发送到客户端的响应的字符编码(MIME 字符集)例如,UTF-8 |
void sendRedirect(String location) | 使用指定的重定向位置 URL 发送临时重定向响应到客户端 |
PrintWriter getWriter() | 用于往 body 中写入文本格式数据 |
OutputStream getOutputStream() | 用于往 body 中写入二进制格式数据 |
setStatus(int sc)
这个方法如果不显式指出状态码的话,会默认为200。
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("/status") public class HttpStatus extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setStatus(200); } }
这里我们给相应的 Header 中设置一个刷新页面的key-value。
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("/refresh") public class HttpRefresh extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setHeader("refresh","1000");; resp.getWriter().write(System.currentTimeMillis() + " "); } }
postman 不能显示出动态变化的页面,所以我们需要在浏览器中才能看到这个变化。
当在 Header 中添加进去 refresh 键值对的时候,页面会根据后面设置的时间进行刷新。。
setContenType() 和 setCharacterEncoding() 这两个方法作用其实是差不多的,但是我们更建议使用 setContenType() 方法,因为这个方法还可以指定字符编码集。resp.setContentType("test/html; charset=utf8")
sendRedirect(String location)
方法会设置页面重定向到哪里,也就是当状态码返回3xx的时候的重定向位置。
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("/redirect") public class HttpRedirect extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setStatus(302); resp.setHeader("Location", "https://www.baidu.com"); } }
上面设置状态码为3xx,然后设置重定向位置这两个操作可以合并为一个操作。
@WebServlet("/redirect") public class HttpRedirect extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // resp.setStatus(302); // resp.setHeader("Location", "https://www.baidu.com"); resp.sendRedirect("http://www.baidu.com"); } }
getWriter()
和 getOutputStream()
方法则是将响应写入 body 中,这里不过多解释。