【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是单实例这一点也需要知道,在并发的情况下,可能会有多线程下的并发问题。

相关文章
|
7月前
|
Java
如何在Java中进行多线程编程
Java多线程编程常用方式包括:继承Thread类、实现Runnable接口、Callable接口(可返回结果)及使用线程池。推荐线程池以提升性能,避免频繁创建线程。结合同步与通信机制,可有效管理并发任务。
294 6
|
7月前
|
IDE Java 编译器
java编程最基础学习
Java入门需掌握:环境搭建、基础语法、面向对象、数组集合与异常处理。通过实践编写简单程序,逐步深入学习,打牢编程基础。
401 1
|
8月前
|
SQL Java 数据库
2025 年 Java 从零基础小白到编程高手的详细学习路线攻略
2025年Java学习路线涵盖基础语法、面向对象、数据库、JavaWeb、Spring全家桶、分布式、云原生与高并发技术,结合实战项目与源码分析,助力零基础学员系统掌握Java开发技能,从入门到精通,全面提升竞争力,顺利进阶编程高手。
1255 2
|
8月前
|
安全 Java API
Java Web 在线商城项目最新技术实操指南帮助开发者高效完成商城项目开发
本项目基于Spring Boot 3.2与Vue 3构建现代化在线商城,涵盖技术选型、核心功能实现、安全控制与容器化部署,助开发者掌握最新Java Web全栈开发实践。
767 1
|
7月前
|
安全 前端开发 Java
从反射到方法句柄:深入探索Java动态编程的终极解决方案
从反射到方法句柄,Java 动态编程不断演进。方法句柄以强类型、低开销、易优化的特性,解决反射性能差、类型弱、安全性低等问题,结合 `invokedynamic` 成为支撑 Lambda 与动态语言的终极方案。
304 0
|
9月前
|
安全 Java 数据库连接
2025 年最新 Java 学习路线图含实操指南助你高效入门 Java 编程掌握核心技能
2025年最新Java学习路线图,涵盖基础环境搭建、核心特性(如密封类、虚拟线程)、模块化开发、响应式编程、主流框架(Spring Boot 3、Spring Security 6)、数据库操作(JPA + Hibernate 6)及微服务实战,助你掌握企业级开发技能。
1115 3
|
8月前
|
存储 前端开发 Java
【JAVA】Java 项目实战之 Java Web 在线商城项目开发实战指南
本文介绍基于Java Web的在线商城技术方案与实现,涵盖三层架构设计、MySQL数据库建模及核心功能开发。通过Spring MVC + MyBatis + Thymeleaf实现商品展示、购物车等模块,提供完整代码示例,助力掌握Java Web项目实战技能。(238字)
1010 0
|
8月前
|
Java 开发者
Java并发编程:CountDownLatch实战解析
Java并发编程:CountDownLatch实战解析
583 100
|
9月前
|
JavaScript Java 微服务
现代化 Java Web 在线商城项目技术方案与实战开发流程及核心功能实现详解
本项目基于Spring Boot 3与Vue 3构建现代化在线商城系统,采用微服务架构,整合Spring Cloud、Redis、MySQL等技术,涵盖用户认证、商品管理、购物车功能,并支持Docker容器化部署与Kubernetes编排。提供完整CI/CD流程,助力高效开发与扩展。
1011 64
|
8月前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
522 16