【Java Web编程 八】深入理解Servlet常用对象

简介: 【Java Web编程 八】深入理解Servlet常用对象

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传递的概念了。在客户浏览器路径栏显示的是其重定向的 路径,客户可以观察到地址的变化的。重定向行为是浏览器做了至少两次的访问请求的

二者的主要区别

从以下几个浅层的角度分别来看下二者的区别:

  1. 从地址栏显示的角度来看
  • forward:是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,然后把这些内容再发给浏览器,浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址;
  • redirect:是服务端根据逻辑发送一个状态码告诉浏览器重新去请求那个地址,所以地址栏显示的是新的URL.
  1. 从数据共享的角度来看
  • forward:转发页面和转发到的页面可以共享request里面的数据.
  • redirect:请求页面和重定向页面不可以共享request里面的数据
  1. 从使用场景的角度来看
  • forward: 一般用于用户登陆的时候,根据角色转发到相应的模块.
  • redirect: 一般用于用户注销登陆时返回主页面和跳转到其它的网站等.
  1. 从请求效率的角度来看
  • forward: 高,因为forwar全程依然在一个请求里,所以效率较高
  • redirect: 低,因为至少发生了两次请求,所以效率一般较低
  1. 从使用优先级角度来看:
  • 优先选择转发,因为转发效率更高
  • 在同一个 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是单实例这一点也需要知道,在并发的情况下,可能会有多线程下的并发问题。

相关文章
|
2月前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
2月前
|
安全 Java UED
深入浅出Java多线程编程
【10月更文挑战第40天】在Java的世界中,多线程是提升应用性能和响应能力的关键。本文将通过浅显易懂的方式介绍Java中的多线程编程,从基础概念到高级特性,再到实际应用案例,带你一步步深入了解如何在Java中高效地使用多线程。文章不仅涵盖了理论知识,还提供了实用的代码示例,帮助你在实际开发中更好地应用多线程技术。
78 5
|
1月前
|
Java 程序员
Java编程中的异常处理:从基础到高级
在Java的世界中,异常处理是代码健壮性的守护神。本文将带你从异常的基本概念出发,逐步深入到高级用法,探索如何优雅地处理程序中的错误和异常情况。通过实际案例,我们将一起学习如何编写更可靠、更易于维护的Java代码。准备好了吗?让我们一起踏上这段旅程,解锁Java异常处理的秘密!
|
1月前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
1月前
|
算法 Java 调度
java并发编程中Monitor里的waitSet和EntryList都是做什么的
在Java并发编程中,Monitor内部包含两个重要队列:等待集(Wait Set)和入口列表(Entry List)。Wait Set用于线程的条件等待和协作,线程调用`wait()`后进入此集合,通过`notify()`或`notifyAll()`唤醒。Entry List则管理锁的竞争,未能获取锁的线程在此排队,等待锁释放后重新竞争。理解两者区别有助于设计高效的多线程程序。 - **Wait Set**:线程调用`wait()`后进入,等待条件满足被唤醒,需重新竞争锁。 - **Entry List**:多个线程竞争锁时,未获锁的线程在此排队,等待锁释放后获取锁继续执行。
68 12
|
1月前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
185 2
|
2月前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
2月前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
1月前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
1月前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
68 3