什么是 Servlet?
servlet 是运行在 Web 服务器中的 Java 程序. servlet 通常通过 HTTP(超文本传输协议) 接收和响应来自 Web 客户端的请求. Java Web 应用程序中所有的请求 - 响应都是由 Servlet 完成.
Servlet 的工作流程
浏览器与服务器之间的请求和响应都是遵循 HTTP 协议的 (上一篇有介绍 HTTP).Tomcat 会接受并解析 HTTP 请求文本, 然后封装成 HttpServletRequest 对象, 所有的 HTTP 头数据都可以通过 request 相应的方法查询到. Tomcat 同时会把响应的数据封装为 HttpServletResponse 类型的 response 对象, 通过设置 response 属性可以控制输出内容, 然后 Tomcat 会把 request,respnse 作为参数, 调用 Servlet 的相关方法, 例如 doGet,doPost 等.
Java Web 应用程序请求 - 响应的典型过程如图:
编写 Servlet
在编写 Servlet 时, 需要继承 HttpServlet 类, 还要考虑到如何让 Web 容器找到相应的 Servlet.
在 Servlet3.0 之前编写 Servlet 时需要部署配置文件 Web.xml(在上一篇有详细介绍).
Servlet 类的编写:
- package servlet;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import java.io.IOException;
- public class HelloServlet extends HttpServlet {
- protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- }
- protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- }
- }
配置文件的编写:
- HelloServlet
- servlet.HelloServlet
- HelloServlet
- /HeHelloServlet
有没有觉得这样非常麻烦... 所以在 Servlet3.0 以后可以使用 @WebServlet() 注解的方式来告诉 Tomcat 哪些 Servlet 会提供服务以及额外信息, 而不需要配置文件.
- package servlet;
- 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(
- name = "HelloServlet", //Servlet 内部名, 可以随便设置
- urlPatterns = "/login", // 用户请求用的 URL 名称
- loadOnStartup = 1 //Servlet 初始化顺序, 默认为 - 1, 设置为大于 0 点值, 数字越小越先执行
- )
- public class HelloServlet extends HttpServlet {
- protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- }
- protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- }
- }
Servlet 的原理
Servlet API
Servlet
此接口定义了初始化 servlet 的方法, 为请求提供服务的方法和从服务器移除 servlet 的方法, 这些方法称为生命周期方法. 此外该接口还提供了 getServletConfig 方法和 getServletInfo 方法, servlet 可使用前一种方法获得任何启动信息, 而后一种方法允许 servlet 返回有关其自身的基本信息, 比如作者, 版本和版权.
init(ServletConfig config)
生命周期方法, 由 Web 容器调用, 初始化该 servlet.
service(ServletRequest req, ServletResponse res)
生命周期方法, 由 Web 容器调用, 以允许 servlet 响应某个请求. 此方法仅在 servlet 的 init() 方法成功完成之后调用.
destroy()
生命周期方法, 由 Web 容器调用, 销毁该 Servlet.
getServletConfig()
返回 ServletConfig 对象, 该对象包含此 servlet 的初始化和启动参数. 返回的 ServletConfig 对象是传递给 init() 方法的对象. 此接口的实现负责存储 ServletConfig 对象, 以便此方法可以返回该对象. 实现此接口的 GenericServlet 类已经这样做了.
getServletInfo()
返回有关 Servlet 的信息, 比如作者, 版本和版权. 此方法返回的是纯文本, 不应该是任何种类的标记.
GenericServlet
定义一般的, 与协议无关的 servlet.GenericServlet 实现 Servlet 和 ServletConfig 接口. GenericServlet 的主要目的就是将初始化 Servlet 的 Init 方法传入的 ServletConfig 对象封装起来, GenericServlet 也包括了 Servlet 与 ServletConfig 所定义方法的简单实现, 实现内容主要是通过 ServletConfig 来取得一些相关信息. GenericServlet 在实现 Servlet 的 init 方法时, 还调用了无参的 init() 方法, 如果有一些初始时需要运行的操作, 可以在你编写的 Servlet 中重写这个无参 init 方法 (), 而不是重写有参数的 init() 方法, 原因在下面的 Servlet 生命周期里有详细的介绍.
- public void init(ServletConfig config) throws ServletException {
- this.config = config;
- this.init();
- }
- HttpServlet
继承于 GenericServlet, 提供将要被子类化以创建适用于 Web 站点的 HTTP servlet 的抽象类. HttpServlet 的子类至少必须重写一个方法, 该方法通常是以下这些方法之一:
doGet, 如果 servlet 支持 HTTP GET 请求
doPost, 用于 HTTP POST 请求
doPut, 用于 HTTP PUT 请求
doDelete, 用于 HTTP DELETE 请求
init 和 destroy, 用于管理 servlet 的生命周期内保存的资源
getServletInfo,servlet 使用它提供有关其自身的信息
MyServlet
这是你自己编写的 Servlet, 继承 HttpServlet 后重写其中的 doGet 方法或 doPost 方法即可接收和响应客户端的 Http 请求.
Servlet 生命周期
生命周期: Servlet 从创建到消亡的一段时间
Web 容器为 Servlet 创建一个 ServletConfig, 调用 init() 时, servlet 才是一个真正的 Servlet, 之前还只是一个普通的对象 (调用构造函数后). 在调用构造函数和 init 方法之间, Servlet 处在薛定谔状态 (介于两者之间的一种状态), 如果在 Servlet 状态, 你可能有一些 Servlet 初始化代码, 比如得到 Web 应用配置信息, 或请求信息, 那么你就会失败. 但是只要记住一点, Servlet 的构造函数中不放任何东西即可.
init()
默认情况下, Servlet 在第一次被访问的时候, 才会执行 init 方法. 有的时候, 我们可能需要在这个方法里面执行一些初始化工作, 甚至是做一些比较耗时的工作. 那么这个时候, 初次访问, 可能会在 init 方法中逗留太久的时间. 那么有没有方法可以让这个初始化的时机提前一点?
在 Web.xml 中配置, 使用 load-on-startup 元素来指定 Servlet 的启动时机, 初始值为 - 1, 它的值必须是一个整数, 我们设置的时候一般为 0 和大于 0 的整数, 表示在容器启动的时候就加载和初始化 Servlet, 值越小, Servlet 的优先级就越高, 启动的时机就越早, 当值相同时, 由 Web 容器自己选择优先加载.
- HelloServlet
- servlet.HelloServlet
- 2
在注解中添加 loadOnStartup 属性, 与上面一样.
- @WebServlet(
- name = "HelloServlet", //Servlet 内部名, 可以随便设置
- urlPatterns = "/login", // 用户请求用的 URL 名称
- loadOnStartup = 1 //Servlet 初始化顺序, 默认为 - 1, 设置为大于 0 点值, 数字越小越先执行
- )
- service
当一个请求到来的时候, 容器会开始一个新线程, 或者从线程池中分配一个线程, 并调用 service() 方法, 再由 service() 考虑调用哪一个方法响应请求.
destroy
当 Servlet 从服务器中移除或者服务器关闭的时候 Servlet 对象会被销毁, 里面的 destroy 方法就会执行, 然后垃圾回收就会将其回收掉.
四大对象
ServletConfig
Web 容器使用的 servlet 配置对象, 该对象在初始化期间将信息传递给 servlet. 通过 ServletConfig 可以得到一些初始化信息. 每个 Servlet 都有一个属于自己的 ServletConfig.
获取途径: getServletConfig()
ServletConfig 对象只有以下四个方法:
getInitParameter(String name): 根据 name 返回指定初始化参数的值, 如果参数不存在, 则返回 null.
getInitParameterNames(): 以 String 对象的 Enumeration 的形式返回 servlet 的初始化参数的名称, 如果 servlet 没有初始化参数, 则返回一个空的 Enumeration.
getServletContext(): 返回 ServletContext 对象的引用.
getServletName(): 返回此 servlet 实例的名称, 默认是类名.
在使用这些方法之前, 需要知道怎么为 Servlet 添加初始化参数.
Web.xml 中添加参数:
- HelloServlet
- servlet.HelloServlet
- name
- kindleheart
- phone
- 110
- 2
@WebServlet 注解中添加参数:
- @WebServlet(
- name = "HelloServlet", //Servlet 内部名, 可以随便设置
- urlPatterns = "/login", // 用户请求用的 URL 名称
- loadOnStartup = 1, //Servlet 初始化顺序, 默认为 - 1, 设置为大于 0 点值, 数字越小越先执行
- initParams = { // 添加初始化参数
- @WebInitParam(name = "name", value = "kindleheart"),
- @WebInitParam(name = "phone", value = "110")
- }
- )
使用上面四个方法:
- public class HelloServlet extends HttpServlet {
- protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- // 得到 servletConfig 对象
- ServletConfig servletConfig = this.getServletConfig();
- //getInitParameter
- System.out.println(servletConfig.getInitParameter("name"));
- System.out.println(servletConfig.getInitParameter("phone"));
- //getInitParameterNames
- Enumeration<String> names = servletConfig.getInitParameterNames();
- while (names.hasMoreElements()) {
- String name = names.nextElement();
- System.out.println(servletConfig.getInitParameter(name));
- }
- //getServletContext
- ServletContext servletContext = servletConfig.getServletContext();
- //getServletName
- System.out.println(servletConfig.getServletName());
- }
- protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- doPost(request, response);
- }
- }
- ServletContext
Servlet 上下文, 定义一组方法, Servlet 使用这些方法与 Web 容器进行通信, 例如获取文件的 MIME 类型, 分发请求或写入日志文件.
每个 Web 工程下只有一个 ServletContext, 所有 Servlet 共用一个 ServletContext,ServletContext 对象包含在 ServletConfig 对象中, ServletConfig 对象在初始化 servlet 时由 Web 容器提供给 Servlet.
获取方式: getServletContext(),getServletConfig().getServletContext, 这两种方式本质上是一样的, 都是由 ServletConfig 在 GenericServlet 类中返回得到.
ServletRequest 对象的应用
获取全局参数
上面的 init-param 是配置在 servlet 标签下的, 所以只能由这个 Servlet 来读取, 如果想所有的 Servlet 都能读取, 就需要用到上下文参数, context-param 配置在 Web-App 标签下, 同样的在获取全局配置参数之前我们先在 Web.xml 中添加全局参数.
- name
- kindleheart
- phone
- 110
读取方式与 ServletConfig 读取初始化参数完全一样.
获取 Web 应用中的资源
- protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {// 获得要下载的文件的名称
- // 告诉客户端该文件不是直接解析, 而是以附件形式打开 (下载)
- response.setHeader("Content-Disposition", "attachment;filename=" + "tutu.jsp");
- // 获取文件的绝对路径, 再得到输入流
- // String path = this.getServletContext().getRealPath("download/" + "tutu.jsp");
- // InputStream in = new FileInputStream(path);
- // 直接得到输入流
- InputStream in = getServletContext().getResourceAsStream("download/tutu.jsp");
- // 获得输出流, 通过 response 获得的输出流, 用于向客户端写内容
- ServletOutputStream out = response.getOutputStream();
- // 文件拷贝代码
- int len = 0;
- byte[] buffer = new byte[1024];
- while ((len = in.read(buffer)) != -1) {
- out.write(buffer, 0, len);
- }
- in.close();
- out.close();
- }
在 Servlet 上下文存取数据
- protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- // 在 Servlet 上下文添加属性
- getServletContext().setAttribute("name","图图");
- // 在 Servlet 上下文获取这个属性值
- String name = (String) getServletContext().getAttribute("name");
- }
- HttpServletRequest
客户端发来的请求被封装成一个 HttpServletRequest 对象. 所有的信息包括请求的地址, 请求的参数, 提交的数据, 上传的文件, 客户端 IP 地址, 甚至是客户端操作系统都包含在内.
获取方式: 直接使用 request 对象.
HttpServletRequest 对象的应用:
获取请求头信息
- protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- // 获取所有请求头信息
- Enumeration<String> headerNames = request.getHeaderNames();
- while (headerNames.hasMoreElements()) {
- String name = headerNames.nextElement();
- String value = request.getHeader(name);
- System.out.println(name + ":" + value);
- }
- }
获取表单传来的数据
action="login" method="post">
姓名: type="text" name="username">
密码: type="password" name="password">
type="checkbox" name="hobby" value="打球"> 打球
type="checkbox" name="hobby" value="游泳"> 游泳
type="checkbox" name="hobby" value="滑雪"> 滑雪 <br>
- type="submit" value="提交">
- protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- // 设置字符编码为 UTF-8
- request.setCharacterEncoding("utf-8");
- response.setContentType("text/HTML;charset=utf-8");
- // 一个 name 对应一个值
- String username = request.getParameter("username");
- String password = request.getParameter("password");
- // 一个 name 对应多个值
- Map, String[]> map = request.getParameterMap();
- Set keySet = map.keySet();
- for(String key : keySet) {
- System.out.println("key=" + key + "的值的总数有" + map.get(key).length);
- for(String value : map.get(key)) {
- System.out.println(value);
- }
- }
- }
请求转发
在 Web 应用中, 经常需要多个 Servlet 共同处理请求, 那么就需要使用请求转发. 请求转发是通过 RequestDispatcher 对象的 forword(req, res) 来实现的. 这个动作是在 Web 容器中进行的, 浏览器并不知道请求被转发. 当 forward 跳转时, 地址栏的访问地址不改变, 跳转后的 Servlet 或 Jsp 能使用上一个 Servlet 里的属性, 经常用于 Servlet 处理业务逻辑, 然后 Jsp 来显示处理结果.
- protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- // 在 request 域中写入属性
- request.setAttribute("username","kindleheart");
- request.setAttribute("password","123");
- // 请求转发, index.jsp 可以使用该 Servlet 里的属性
- request.getRequestDispatcher("index.jsp").forward(request, response);
- }
- HttpServletResponse
服务器使用 HttpServletResponse 来对浏览器进行响应.
HttpServletResponse 的应用:
设置响应头
在前面的 ServletContext 上下文获取资源时, 浏览器是默认直接显示该资源的, 如果你想下载而不是直接显示, 就会需要设置响应头, 让浏览器知道你要下载而不是显示.
- // 告诉客户端该文件不是直接解析, 而是以附件形式打开 (下载)
- response.setHeader("Content-Disposition", "attachment;filename=" + "tutu.jsp");
使用 getWriter 输出字符
- protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- response.setContentType("text/HTML;charset=utf-8");
- response.getWriter().println("输出字符");
- }
使用 getOutputStream
还是在前面的 ServletContext 获取资源中, 需要把资源以输出流的形式进行下载, 那就需要用到 getOutputStream 来以二进制形式写入.
- // 获得输出流, 通过 response 获得的输出流, 用于向客户端写内容
- ServletOutputStream out = response.getOutputStream();
重定向
HttpServletRequence 的 sendRedirect 要求浏览器重新请求一个 URL, 称为重定向. 重定向是利用服务器返回的状态码决定的, 浏览器在访问服务器的时候, 服务器会返回一个状态码. 可以使用 HttpServletResponse 的 setStatus 方法设置状态码. 如果服务器返回 301, 或者 302, 那么浏览器会到新的网址请求资源, 地址栏上的访问地址会改变. 重定向和请求转发不一样. 因为是重新访问地址, 所以重定向后的 Servlet 或 Jsp 不能使用之前 Servlet 的属性.
状态码 | 意义 |
---|---|
1xx | 信息状态码。表示该请求已经被接受。正在被处理。 |
2xx | 正确状态码。表示该请求已经被正确接受且处理,没有错误发生。例如,200 表示一切正确。 |
3xx | 重定向状态码。例如 301,302 表示该资源已经不存在或换了地址,需要重新定向到一个新的资源。 |
4xx | 请求错误。例如 401 表示没有访问权限,404 表示资源不存在,405 表示访问方式错误。 |
5xx | 服务器错误。例如 500 表示程序出现异常而中途停止运行。 |
使用:
- // 重定向到 index.jsp
- response.sendRedirect("index.jsp");
sendRedirect 方法其实是在响应中设置了 Status 和 Location 标头, 所以使用以下代码就可以实现重定向, 这也是重定向的原理.
- // 设置状态码
- response.setStatus(301);
- // 定位跳转的位置
- response.setHeader("Location", "index.jsp");
自动刷新
response 对象可以使用 setHeader 设置方法响应头来实现自动刷新, 自动刷新不仅可以实现一段时间跳转到其他页面, 还可以实现一段时间后自动刷新本页面.
- response.getWriter().println("注册成功! 3 秒之后会跳转到主页");
- response.setHeader("refresh", "3;index.jsp");
其中 3 代表 3 秒, index.jsp 是指定的跳转网址的 URL, 如果 URL 设置的路径为 Servlet 自己的路径, 就会每隔 3 秒刷新自己一次.
重定向与请求转发的区别
本质区别: 请求转发只发出了一次请求, 而重定向发出了多次请求.
请求转发: 地址栏是初次发出请求的地址
重定向: 地址栏不再是初次发出的请求地址, 地址栏为最后响应的那个地址
请求转发: 在最终的 Servlet 中, request 对象和中转的那个 request 是同一个对象
重定向: 在最终的 Servlet 中, request 对象和中转的那个 request 不是同一个对象
请求转发: 只能转发给当前 Web 应用的资源
重定向: 可以重定向到任何资源
response.sendRedirect("http://www.baidu.com"); 是可以的, 请求转发就不行.
请求转发:/ 代表的是当前 Web 应用的根目录 (http://localhost:8080 / 项目名称 /)
重定向: / 代表的是当前 Web 站点的根目录 (http://localhost:8080/)
注意: 这两条跳转语句不能同时出现在一个页面中, 否则会报 IllegalStateException - if the response was already committed 的错误.
Servlet 中文乱码问题
出现乱码问题的原因是 Web 容器默认是以 ISO-8859-1 进行字符编码的. 而一般使用的过程中都是以 UTF-8 的格式, 那么就会出现乱码问题.
HttpServletRequest 出现中文乱码
POST 请求
- // 设置 request 的字符编码格式为 utf-8
- request.setCharacterEncoding("utf-8");
GET 请求
- String value = request.getParameter("value");
- value = new String(value.getBytes("iso-8859-1"), "utf-8");
- System.out.println(value);
HttpServletResponse 出现中文乱码
- // 设置 response 的字符编码格式为 utf-8
- response.setContentType("text/HTML;charset=utf-8");
如果要接收中文数据, 并在响应时通过浏览器正确显示中文, 那么这两个方法需要同时进行设置.
Servlet 是否为线程安全
线程安全问题指的是多线程在并发执行时会不会出现问题. 由于 Web 容器只会创建一个 Servlet 实例, 所以多个用户发起请求时, 会有多个线程处理 Servlet 代码, 因此 Servlet 是线程不安全的.
考虑以下代码:
- @WebServlet(name = "ThreadSafeServlet", urlPatterns = "/ThreadSafeServlet")
- public class ThreadSafeServlet extends HttpServlet {
- private String name;
- protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- name = request.getParameter("name");
- try {
- Thread.sleep(10000);// 使线程沉睡 10 秒
- } catch (Exception e) {
- e.printStackTrace();
- }
- response.getWriter().println("name:" + name);
- }
- protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- doPost(request, response);
- }
- }
10 秒内在两个不同的浏览器窗口中的表单输入 name 并提交, 假如在 A 浏览器中输入 111,B 浏览器中输入 222, 最后会发现 A 和 B 浏览器显示的 name 都是 222. 这是因为在第一个线程睡眠时, 第二个线程修改了 name 的值, 所有最后显示都是 222, 那么就产生了线程不安全问题.
实际上 Servlet,Context 上下文作用域, HttpSession 都是线程不安全的, 只有 request 请求和局部变量是线程安全的.
来源: http://www.bubuko.com/infodetail-2803212.html