Servlet中有几个常用的对象,如果大家还记得之前在JSP的内置对象中介绍过的内容那么应该会对这几个对象大致有个印象JSP内置对象,分别是下图中的这几个红圈内容:
我们知道JSP编译后就是Servlet,这也再次证明了这一点。
常用对象使用
接下来我们详细介绍下Servlet中使用最频繁的这四个常用对象。
HttpServletRequest接口
HttpServletRequest 接口代表客户端的请求,它包含了客户端提交过来的请求数据:
- HttpServletRequest 接口来自于Servlet规范。
- HttpServletRequest 接口实现类由Http服务器厂商提供。
- HttpServletRequest 接口读取请求协议包中的内容。
- 一般习惯将 HttpServletRequest 接口修饰的对象称为 请求对象。
然后我们探索下主要功能。
request主要功能
主要功能如下:
- HttpServletRequest 接口读取请求包中的请求行中的信息(url、method、uri、scheme)
- HttpServletRequest 接口读取请求包中请求头或者请求体中参数的信息。
- HttpServletRequest 接口代替浏览器向Tomcat索要资源文件【请求转发】。
实现以上功能的方法如下
request常用方法
常用的方法如下,例如获取请求的参数等:
请求头和请求体中的一些数据
request使用示例
一个获取请求中内容的示例如下:
@Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //读取请求行中的url属性信息 String url=request.getRequestURL().toString(); //读取请求行中method的属性信息 String method=request.getMethod(); //读取请求行中的uri属性信息 //uri是从url中截取的一段字符串,格式:"/网站名/资源文件名",可以帮助tomcat进行资源定位 String uri=request.getRequestURI().toString(); //获取请求行中的协议信息 String scheme=request.getScheme(); PrintWriter out = response.getWriter(); out.println("url===" + url); out.println("uri===" + uri); out.println("method===" + method); out.println("scheme===" + scheme); } //请求转发的写法 RequestDispatcher requestDispatcher=request.getRequestDispatcher("/five.html"); requestDispatcher.forward(request,response);
打印结果如下:
HttpServletResponse接口
HttpServletResponse 接口代表向客户端发送的响应,利用response可以向客户 端响应信息或跳转界面:
- HttpServletResponse 接口来自于Servlet规范。
- HttpServletResponse 接口实现类由Http服务器厂商提供。
- HttpServletResponse 接口可以将Servlet中的运行结果写入到响应包。
- 一般习惯将 HttpServletResponse 接口修饰的对象称为 响应对象
然后我们探索下主要功能。
response主要功能
主要功能如下:
- HttpServletResponse 接口负责将Servlet运行结果以二进制形式写入到响应包中的响应体。
- HttpServletResponse 接口负责设置响应包中响应头的content-type属性,控制浏览器采用对应的解析器和编译器对响应体中的二进制数据进行处理。
- HttpServletResponse 接口负责将一个请求地址写入到响应头中的location属性中,来控制浏览器下一次请求的方式【重定向】。
实现这样功能的方法如下
response常用方法
HttpServletResponse常用方法如下:
response使用示例
一个重定向的使用的示例如下:
@Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String url="http://www.baidu.com"; //通过响应对象将地址写入到响应头中的location response.sendRedirect(url); }
这样我们可以重定向到百度页面:
ServletConfig对象
在 Web 容器初始化一个 Servlet 实例时,会为当前的 Servlet 准备一个唯一的 ServletConfig 实例配置对象。ServletConfig 对象能读取配置在 web.xml 文件中对应Servlet 配置的初始化参数。
ServletConfig用于封装servlet的配置信息。从一个servlet被实例化后,对任何客户端在任何时候访问有效,但仅对servlet自身有效,一个servlet的ServletConfig对象不能被另一个servlet访问
ServletConfig只能针对当前配置的Servlet有效,例如我们在注解中定义和通过配置获取代码如下:
@WebServlet(name = "Servlet", value = "/myFirstServlet",initParams = { @WebInitParam(name = "name", value = "tml"), @WebInitParam(name = "age", value = "27") }) public class FirstServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=gbk"); PrintWriter out = response.getWriter(); // 获取 ServletConfig 实例 ServletConfig config = this.getServletConfig(); // 获取指定名称的初始化参数的字符串值 String name = config.getInitParameter("name"); String age = config.getInitParameter("age"); out.println("servlet 初始化参数 name 的值是:" + name + "<br/>"); out.println("servlet 初始化参数 age 的值是:" + age + "<br/>"); }
浏览器的打印结果为:
ServletContext对象
ServletContext可以实现多个Servlet获取相同的初始化参数值,它不属于某一个Servlet所有,而是Web 应用程序的上下文环境的参数。
servlet容器在启动时会加载web应用,并为每个web应用创建唯一的servlet context对象,可以把ServletContext看成是一个Web应用的服务器端组件的共享内存,在ServletContext中可以存放共享数据。ServletContext对象是真正的一个全局对象,凡是web容器中的Servlet都可以访问。 整个web应用只有唯一的一个ServletContext对象
我们可以做个实验,分别在两个Servlet里都加入同样的计数器代码,在HelloServlet中:
package com.example.myfirstweb; import java.io.*; import javax.servlet.ServletContext; import javax.servlet.http.*; import javax.servlet.annotation.*; /** * @author tianmaolin */ @WebServlet(name = "helloServlet", value = "/hello-servlet") public class HelloServlet extends HttpServlet { private String message; @Override public void init() { message = "Hello World!"; } @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/html;charset=gbk"); PrintWriter out = response.getWriter(); // 获取 ServletContext 对象 ServletContext context = this.getServletContext(); // 获取指定名称的 Web 应用程序的上下文初始参数的字符串值 if(null==context.getAttribute("counter")){ context.setAttribute("counter", 1); }else{ int counter=(Integer)context.getAttribute("counter"); context.setAttribute("counter", counter+1); out.println("当前计数器数值为"+counter); } } @Override public void destroy() { } }
在FirstServlet中:
package com.example.myfirstweb.controller; /** * * @Name ${NAME} * * @Description * * @author tianmaolin * * @Data 2021/7/19 */ import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.annotation.*; import java.io.IOException; import java.io.PrintWriter; @WebServlet(name = "Servlet", value = "/myFirstServlet") public class FirstServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=gbk"); PrintWriter out = response.getWriter(); // 获取 ServletContext 对象 ServletContext context = this.getServletContext(); // 获取指定名称的 Web 应用程序的上下文初始参数的字符串值 if(null==context.getAttribute("counter")){ context.setAttribute("counter", 1); }else{ int counter=(Integer)context.getAttribute("counter"); context.setAttribute("counter", counter+1); out.println("当前计数器数值为"+counter); } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { } }
每个Servlet中都可以读取上下文初始化的参数,分别请求两个页面:
他们没有各自累加,而是总体在累加
请求转发和重定向
其实在JSP中的这篇Blog:JSP内置对象中就详细介绍了请求转发和重定向,当时并没有引入Servlet的概念,当我们将Servlet作为请求的目标时这两个概念依然存在:
二者的请求流程
整个执行流程比较好理解,一句话,转发是服务器行为,重定向是客户端行为。为什么这样说呢,这就要看两个动作的工作流程:
- 转发过程:客户浏览器发送http请求----》web服务器接受此请求–》调用内部的一个方法在容器内部完成请求处理和转发动作----》将目标资源 发送给客户;
在这里,转发的路径必须是同一个web容器下的url,其不能转向到其他的web路径上去,中间传递的是自己的容器内的request。在客户浏览器路径栏显示的仍然是其第一次访问的路径,也就是说客户是感觉不到服务器做了转发的。转发行为是浏览器只做了一次访问请求。
- 重定向过程:客户浏览器发送http请求----》web服务器接受后发送302状态码响应及对应新的location给客户浏览器–》客户浏览器发现 是302响应,则自动再发送一个新的http请求,请求url是新的location地址----》服务器根据此请求寻找资源并发送给客户。
在这里 location可以重定向到任意URL,既然是浏览器重新发出了请求,则就没有什么request传递的概念了。在客户浏览器路径栏显示的是其重定向的 路径,客户可以观察到地址的变化的。重定向行为是浏览器做了至少两次的访问请求的
二者的主要区别
从以下几个浅层的角度分别来看下二者的区别:
- 从地址栏显示的角度来看:
- forward:是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,然后把这些内容再发给浏览器,浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址;
- redirect:是服务端根据逻辑发送一个状态码告诉浏览器重新去请求那个地址,所以地址栏显示的是新的URL.
- 从数据共享的角度来看:
- forward:转发页面和转发到的页面可以共享request里面的数据.
- redirect:请求页面和重定向页面不可以共享request里面的数据
- 从使用场景的角度来看:
- forward: 一般用于用户登陆的时候,根据角色转发到相应的模块.
- redirect: 一般用于用户注销登陆时返回主页面和跳转到其它的网站等.
- 从请求效率的角度来看:
- forward: 高,因为forwar全程依然在一个请求里,所以效率较高
- redirect: 低,因为至少发生了两次请求,所以效率一般较低
- 从使用优先级角度来看:
- 优先选择转发,因为转发效率更高
- 在同一个 Web 应用程序的两个请求间传递数据时,采用转发
- 如果需要跳转到其他服务器上的资源,则必须使用重定向
理解了二者的区别我们来看一下写法:
@Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //请求转发的写法 RequestDispatcher requestDispatcher=request.getRequestDispatcher("/first.jsp"); requestDispatcher.forward(request,response); //重定向的写法 response.sendRedirect("/first.jsp"); }
具体的使用区别可以参照:JSP内置对象。
Servlet同步问题
Servlet在多线程下其本身并不是线程安全的。首先明白几个前置概念:
- JSP/Servlet默认是以多线程模式执行的。
- Servlet本身是单实例的,当有多个用户访问某个Servlet会访问该唯一实例中的成员变量。
每个浏览器各自创建的request请求当然是不一样的,都有各自的set和getattribute方法,互不干扰,即使是session也是如此,各自的set和getattribute互不干扰。只有application例外,作用于整个服务器 。但是以上说法皆是基于变量是局部变量,如果是成员变量,由于servlet是单实例的,所以会出现线程紊乱现象
问题示例
例如我们在Servlet里定义一个成员变量:
@WebServlet(name = "Servlet", value = "/myFirstServlet",initParams = { @WebInitParam(name = "name", value = "tml"), @WebInitParam(name = "age", value = "27") }) public class FirstServlet extends HttpServlet { private String username; //成员变量 @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.username=request.getParameter("username"); try { java.lang.Thread.sleep(5000); } catch (Exception e) { e.printStackTrace(); } request.setAttribute("username", this.username); request.getRequestDispatcher("jsp/result.jsp").forward(request, response); }
返回的结果的jsp页面为:
<%-- Created by IntelliJ IDEA. User: tianmaolin Date: 2021/7/14 Time: 11:32 上午 To change this template use File | Settings | File Templates. --%> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>跳转的结果页面</title> </head> <body> <a>我是跳转后的结果页面</a> <%=request.getAttribute("username") %> </body> </html>
当有两个请求同时到达时,第一个用户会发现自己设置的值没有生效(因为休眠了5秒,这里模拟线程并发):
最终两个页面都会显示同一个,也就是后来的那个值。
对于request作用域,一个浏览器的两个请求会发生这个问题,对于session作用域,两个浏览器的两个请求也会发生这个问题。需要注意的是无论是request还是session,他们的作用域都没有改变,出现这种现象的原因是他们获取的usernam都来自Servlet实例,而一个Servlet实例从初始化生成之后直到销毁之前(一般是服务器down)一直存在,所以它们才看起来能像application一样在作用域隔离的情况下还能一直获取到相同的值。需要厘清一个概念,作用域与具体的Servlet无关
而application作用域不会有问题,因为application本来就是服务共享的,在我们的预期之中。
解决方法
很简单,当我们不使用成员变量的时候就不会有这个问题:
public class FirstServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username=request.getParameter("username"); try { java.lang.Thread.sleep(5000); } catch (Exception e) { e.printStackTrace(); } request.setAttribute("username", username); PrintWriter out=response.getWriter(); out.println(request.getAttribute("username")); }
两个请求都获取了自己设置的值。
使用规范
如果在类中定义成员变量,而在service中根据不同的线程对该成员变量进行更改,那么在并发的时候就会引起错误。
最好是在方法中,定义局部变量,而不是类变量或者对象的成员变量,由于方法中的局部变量是在栈中,彼此各自都拥有独立的运行空间而不会互相干扰,因此才做到线程安全
总结一下
其实Servlet中的对象和JSP中的类似,重点掌握request和response就行了,而这两个之中再了解了请求转发和重定向的原理即可。当然Servlet是单实例这一点也需要知道,在并发的情况下,可能会有多线程下的并发问题。