Servlet 学习笔记

前言

现如今,随着人们生活物质的急剧提高,人们生活场景逐渐丰富,许多的基础设施都已经与科学技术融为一体。

从最初的PC互联网时代,到现今几乎人手一台手机的移动互联网时代,科学技术的发展与丰富多样的应用程序造就了如今方便快捷的生活方式。

终端机器的增加与数据量的极大丰富,越来越多的数据会逐渐的往服务器端转移,或许在可预见的时间内,以后的所有应用程序主体都会被放置在服务端上,客户端仅仅只作为一个显示与交互。

往后的应用程序应当基本上都会归属于 Web 应用程序。绝大多数的业务逻辑与数据存储都会放到后端进行处理。

因此,我们很有必要了解一下后端开发的一些知识。

本文主要针对 Java Web 后端编程进行一些讲解,核心内容就是对 Servlet 的介绍与使用讲解。

Web 应用体系架构

Web 应用程序:指的是通过网络通信进行访问的应用程序。
Web 应用程序通常由 前端后端 两部分组成。

  • 前端:主要指的就是浏览器端(即 HTML,CSS 和 JavaScript 等)以及客户端编程内容。
  • 后端:主要指的就是 Web 组件内容(比如 Servlet,JSP,Filter 等),Web 组件通常都交由 Web 服务器进行调用,并且通过 HTTP 进行请求与响应。

当前,Web 应用软件架构主要是 C/S架构B/S架构

  • C/S架构:即 Client-Server(客户端-服务器)

  • B/S架构:即 Browser-Server(浏览器-服务器)

使用 C/S架构,那么客户端程序就需要我们自己手动进行编写。
使用 B/S架构,客户端程序就是浏览器,因此客户端就无须重新编写个程序了,我们只需关注后端业务就行了。

可以看到,B/S架构 相对于 C/S架构 来说,会更加简单与通用,因此其越来越成为目前最流行的软件架构。

CGI vs Servlet

Web 资源可以分为 静态资源动态资源,最开始的时候,后端响应动态资源都是采用 CGI(Common Gateway Interface)(通用网关接口)进行编程,依据 CGI 的标准,编写外部扩展程序,Web 服务器就可以新建进程调用该外部扩展程序,并传递 HTTP 请求,如下图所示:

CGI

CGI 技术对 每个请求 都会创建一个 新进程 进行响应,因此,其资源占用高,效率低。

而对于 Servlet 来说,Web 服务器对 每个请求 都是通过创建 新线程 进行响应,相对于 CGI 来说,线程比进程有更多优势,比如共享同一块内存,更加轻量,线程间通讯更加方便···如下图所示:

Servlet

Servlet 相对于 CGI 来说,具备如下几大优势:

  • 性能:基于线程响应请求而不是进程。
  • 可移植性:Sevlet 基于 Java 语言编写,而 CGI 程序使用平台相关语言,比如 C/C++,Perl。
  • 健壮性:编写 Servlet,我们无须关心内存泄露,垃圾回收等,全部交由 JVM 负责管理。
    ...

Servlet 简介

A servlet is a small Java program that runs within a Web server. Servlets receive and respond to requests from Web clients, usually across HTTP, the HyperText Transfer Protocol.

从 Oracle 的官方文档中可以看到:Servlet 就是运行在 Web 服务器内的一个小型 Java 程序,可以对 Web 客户端发送的 HTTP 请求进行响应和处理。

更具体来说,JavaEE 为我们提供了一个接口:Servlet

Servlet

对于任何实现了该接口的类,我们都可以将其看作是一个 Servlet

Servlet 生命周期

首先来看下 Servlet 定义的接口方法:

Servlet

Servlet 生命周期有关的方法为:

  • init:在 Servlet 创建时进行初始化。
  • service:响应客户端请求。
  • destroy:在 Web服务器退出时,调用该方法。

通常情况下,Servlet 由 Web 容器(也即 Web 服务器)进行管理,Web 容器在接收到请求时,会创建相应的 Servlet 实例进行响应,Servlet 的生命从这一刻便开启了。

具体来说,Servlet 的生命周期包含四个阶段:

  1. 在 Web 容器启动或者第一次接收到请求时,Web 容器将加载对应 Servlet 类并将其放入到 Servlet 实例池。
  2. Servlet 实例化后,Web 容器将调用其init方法,让该 Servlet 实例可以进行一些初始化工作。
  3. Web 容器在 Servlet 初始化完成后,会调用其 service方法,让该 Servlet 处理并响应当前客户端请求。
  4. 在 Web 容器关闭时,会调用 Servletdestroy方法,让该 Servlet 进行资源释放操作。

入门案例

下面举个简单的例子:让浏览器访问http://localhost/hello时,后端类MyServlet返回一个Hello Servlet字符串给到浏览器进行显示。

具体操作如下:

  1. 使用 Maven 新建一个 web app工程:
New Project
  1. 在 pom.xml 中导入 Servlet 依赖
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>
  1. IDEA 默认创建的 web 工程目录配置不全,因此我们需要手动进行补全:
  • 补全源代码目录:在 src/main/ 目录下,创建文件夹 java - 右键该文件夹 - Mark Directory as - Sources Root

  • 补全源代码资源目录:在 src/main/ 目录下,创建文件夹 resources - 右键该文件夹 - Mark Directory as - Resources Root

  • 补全测试代码目录:在 src/ 目录下,创建文件夹 test/java - 右键该文件夹 - Mark Directory as - Test Sources Root

  • 补全测试代码资源目录:在 src/test 目录下,创建文件夹 resources - 右键该文件夹 - Mark Directory as - Test Resources Root

  1. 创建类MyServlet,实现 Servlet 接口:
public class MyServlect implements Servlet {
    ...
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        res.setContentType("text/html;charset=UTF-8");
        PrintWriter writer = res.getWriter();
        writer.print("<h1>Hello Servlet</h1>");
    }
    ...
}
  1. webapp/WEB-INF/web.xml 中配置MyServlet及其映射地址:
<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <!--配置Servlet-->
    <servlet>
        <!--配置Servlet名称-->
        <servlet-name>myServlet</servlet-name>
        <!--Servlect类全限定名-->
        <servlet-class>com.yn.MyServlect</servlet-class>
    </servlet>
    
    <!--配置Servlect映射-->
    <servlet-mapping>
        <!--映射的具体Servlet名称-->
        <servlet-name>myServlet</servlet-name>
        <!--映射路径-->
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
</web-app>
  1. 配置 Tomcat 服务器:
tomcat configuration
  1. 运行项目,此时浏览器输入:localhost:8080/hello,就可以看到输出了。

Servlet 执行模型

一个完整的网络请求与响应的过程如下图所示:

request-reponse

具体来说:

  1. 客户端发送一个请求时,Web 容器就会加载对应 ServletServlet 容器池中,并调用其 init方法,完成 Servlet 的初始化工作;
  2. 完成初始化后,Web 容器就会创建一条新的线程,并调用其service方法,同时新建一个请求和响应对象(ServletRequest req,ServletResponse res)作为参数;
  3. 后续客户端再次请求该 Servlet 时,由于 Servlet 已存在于内存中,故无须进行加载与初始化,而是直接创建新的请求和响应对象,并开启一条新线程调用其service方法;
  4. 当 Web 容器即将关闭时,会调用 Servletdestroy方法,让 Servlet 做一些资源释放操作。

以上,便是 Servlet 的整个执行模型。

可以看到,对于 Servlet 来说,默认情况下,Web 容器对相同类别的 Servlet ,在内存中只维持一个(即 Servlet 保持单例),且只有在第一次创建 Servlet 时,才会调用init方法。只有在 Web 容器退出时,才会调用destroy方法。而后续的请求都是直接在新线程中调用其service方法,并且每次都会创建新的请求对象和响应对象作为参数传递给service方法。

:从 Servlet 执行模型可以看出,Servlet 内部存在线程安全问题(特指service方法)。因此,如果存在共享资源,需要考虑下线程同步,但 Web 应用应当极力避免采用锁同步操作(如synchronized),因为这样做,在高并发环境下,每次只能响应一个请求,这是绝对无法允许的,所以,能尽量避免共享资源就尽量避免。

Servlet 继承体系

Servlet 包含很多接口方法,在实际项目中,很多时候我们不需要对所有方法进行覆写(通常只需覆写service方法),因此,直接实现 Servlet 接口会让代码变得臃肿冗余。

通常我们都会使用 适配器模式 空实现接口方法,后续创建真正的业务类就可以直接通过继承我们自定义的适配器类,并选择覆写所需要的方法即可。

其实这个适配工作,Servlet 文档已经为我们提供了,即:GenericServletHttpServlet

查看 Servlet 继承体系,如下图所示:

Servlet继承体系

简单看下GenericServlet 源码:

public abstract class GenericServlet 
    implements Servlet, ServletConfig, java.io.Serializable
{
    ...
    public void destroy() {
    }
    ...
    public void init() throws ServletException {

    }
    public abstract void service(ServletRequest req, ServletResponse res)
    throws ServletException, IOException;
    ...
}

可以看出,GenericServlet其实就是对 Servlet 的适配器类,其中大部分接口方法都进行空操作,只抽象出service,强制子类进行覆写。

再来看下HttpServlet 的源码:

public abstract class HttpServlet extends GenericServlet
{
    ...
    protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {
        String method = req.getMethod();

        if (method.equals(METHOD_GET)) {
            ...
            doGet(req, resp);
            ...
        } else if (method.equals(METHOD_HEAD)) {
            ...
            doHead(req, resp);

        } else if (method.equals(METHOD_POST)) {
            doPost(req, resp);
        } else if (method.equals(METHOD_PUT)) {
            doPut(req, resp);
        } else if (method.equals(METHOD_DELETE)) {
            doDelete(req, resp);
        } else if (method.equals(METHOD_OPTIONS)) {
            doOptions(req,resp);
        } else if (method.equals(METHOD_TRACE)) {
            doTrace(req,resp);
        } else {
            ...
            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
        }
    }
    ...
    @Override
    public void service(ServletRequest req, ServletResponse res)
        throws ServletException, IOException
    {
        ...
        request = (HttpServletRequest) req;
        response = (HttpServletResponse) res;

        service(request, response);
    }
}
...
}

可以看到,HttpServlet内部主要做的就是对 HTTP 请求方法进行划分,依据具体请求方法将请求重定向到具体方法进行处理,这样的实现方式可以让我们可以更加细致地对具体请求方法进行单独处理。

这里有一点还需要注意的是:HttpServletservice方法的参数为HttpServletRequestHttpServletResponse,其将 Servletservice方法的参数ServletRequestServletResponse进行了强转,提供了更加强大的请求处理和响应功能。

综上,后续进行 Servlet 的开发,建议直接继承 HttpServlet

Web 组件跳转

Java Web 组件包括 Servlet,JSP,Filter 等,有时组件间需要进行通信,则可以采用组件跳转方式。

Web 组件之间的跳转方式可以分为如下 3 种:

  1. 请求转发(forward):又称为 直接转发方式,客户端发送一个请求,服务端直接将该请求转发到另一个 Servlet,如下图所示:
请求转发

对应代码实现:

request.getRequestDispatcher(path).forward(request, response);

特点

  • 一次请求:客户端只发送一次请求,客户端网址不会改变。
  • 响应结果:由 BServlet 负责响应。
  • 资源共享:两个 Web 组件共享请求资源,即通过request.setAttribute设置的资源可以在多个 Web 组件中进行共享。
  • 可以访问 WEB-INF 中的资源WEB-INF 文件夹是 Java Web 应用默认的 安全目录,位于此目录的资源无法直接被浏览器进行请求,只能在服务器端通过请求转发进行间接访问(比如:服务器 Servlet 访问 WEB-INF 下的 JSP 资源目录,并将内容转发给浏览器)。
  • 不支持跨域访问:请求转发只能在同域(协议,域名,端口均相同)间进行。
  1. 请求包含(include):响应包含资源(如 Servlet,JSP页面,HTML文件)内容,如下图所示:
请求包含

请求包含即客户端请求的 Servlet 响应包含有另一个 Servlet 的响应内容。

对应代码实现:

request.getRequestDispatcher(path).include(request, response);

特点

  • 一次请求:客户端只发送一次请求,客户端地址不会改变。
  • 响应结果:响应结果由 AServlet 负责返回,结果包含有 AServlet 和 BServlet 两部分响应内容。
  • 资源共享:两个 Web 组件共享请求资源,即通过request.setAttribute设置的资源可以在多个 Web 组件中进行共享。
  • 不支持跨域访问
  1. 重定向(redirect):又称为 间接转发方式,客户端第一次请求时,服务端下发重定向请求(响应携带新地址),客户端接收到响应后,再次请求新地址,如下图所示:
重定向

对应代码实现:

response.sendRedirect(String location);

特点

  • 客户端累计发送两次请求,浏览器地址栏会改变。
  • 其模式为:请求 - 响应(重定向)- 请求 - 响应。
  • 两次请求没有直接关系,无法进行资源共享。
  • 可以进行跨域访问。

Filter(过滤器)

  • 过滤器(Filter:可以对请求进行预处理和对响应进行后置处理的对象。

过滤器主要用于过滤一些任务,比如转换,日志,压缩,加密解密,输入验证等等。

过滤器是可插拔的,其入口点在web.xml文件中配置,并且只要在web.xml中移除其配置,无须更改其他地方,该过滤器就会自动被移除掉。

过滤器的执行模型如下图所示:

过滤器

官方提供的接口为:Filter,示例代码如下:

  • 首先编写一个过滤器MyFilter实现过滤器接口Filter
public class MyFilter implements Filter {
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        PrintWriter writer = response.getWriter();
        writer.print("Filter is inovked before");
        // 转发到过滤链的下一个Filter,若无,则转发到对应资源
        chain.doFilter(request,response);
        writer.print("Filter is invoked after");
    }

    public void destroy() {

    }
}
  • 然后在web.xml中配置我们定义的过滤器:
<web-app>
    <!--配置Filter-->
    <filter>
        <filter-name>MyFilter</filter-name>
        <filter-class>com.yn.filter.MyFilter</filter-class>
    </filter>
    <!--配置Filter映射-->
    <filter-mapping>
        <filter-name>MyFilter</filter-name>
        <!--拦截所有请求-->
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

现在,无论我们访问哪个资源,都会被我们自定义的过滤器MyFilter拦截到。

注解开发

入门案例中采用 xml 配置的方式配置 ServletServlet 路由映射,其配置还是相对繁琐的。因此,Servlet 3.0 版本为我们提供了更加方便的配置方法:注解

下面我们主要针对 ServletFilter 的相关注解进行讲解:

  • Servlet 注解配置:@WebServlet
    比如,像上面入门案例,我们把 web.xml 中的 <servlet><servlet-mapping>标签去除掉,然后在源码中直接使用注解@WebServlet进行配置:
@WebService("/hello")
public class MyServlect implements Servlet {
    ...
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        res.setContentType("text/html;charset=UTF-8");
        PrintWriter writer = res.getWriter();
        writer.print("<h1>Hello Servlet</h1>");
    }
    ...
}

可以看到,使用注解配置 Servlet 比使用 xml 配置方便快捷了许多。

:使用注解开发甚至连web.xml文件都不需要了。

下面对注解WebServlet进行讲解:

WebServlet

WebServlet注解的各个属性含义如下:

Attribute Description
name Servlet 名称
value URL 路由映射
urlPatterns URL 路由映射
loadOnStartup 启动加载配置
initParams Servlet 初始参数配置
asyncSupported Servlet 支持异步操作配置
small 配置小图标
largeIcon 配置大图标
description Servlet 描述
displayName Servlet 显示名称

其中,最重要的属性就是urlPatterns,可以为 Servlet 配置一个或多个路由映射。
valueurlPatterns效果等同,使用value配置更加简洁。

  • Filter 注解配置:@WebFilter
    比如,像上面过滤器例子,我们把web.xml中的<filter><filter-mapping>标签去除掉,然后在源码中直接使用注解@WebFilter进行配置:
@WebFilter("/*")
public class MyFilter implements Filter {
...
}

下面对WebFilter进行讲解:

WebFilter
Attribute Description
filterName 过滤器名称
value URL 路由映射
urlPatterns URL 路由映射
dispatcherTypes 指定调度器(Request/Response)类型
servletNames 提供 Servlet 名称(数组)
displayName 过滤器名称
description 过滤器描述
initParams 过滤器初始参数配置
asyncSupported 过滤器支持异步操作配置
smallIcon 配置小图标
largeIcon 配置大图标

WebFilterWebServlet注解的相关属性几乎一致。

其他

  • 中文乱码问题:tomcat 8 之前, tomcat 服务器在接收请求时,默认采用的编码方式为 ISO-8859-1,该编码向下兼容 ASCII,是单字节编码,故不支持中文(两个字节),此时:

    1. 对于 Get 请求,参数位于请求行,需要先将 ISO-8859-1 的字符串进行解码,再编码成 UTF-8 格式:
    String name = request.getParameter("name");
    byte[] data = name.getBytes("ISO-8859-1");
    name = new String(data,"UTF-8");
    

    :在 tomcat 8 以后,统一采用 UTF-8 格式接收请求,此时就无须进行编码转换了。

    1. 对于 Post 请求,参数位于请求体,请求体编码由请求头 Content-Type 决定,官方提供了相关 api 可以自动根据请求体的解码方式解析出 post body 内容,解决乱码问题:
     request.setCharacterEncoding("UTF-8");
    

    setCharacterEncoding方法必须在读取请求参数(getParameter)或者读取输入流(getReader)之前进行调用,否则没有效果。

乱码终极解决方案:按上述分析,对于 Get 请求,tomcat 8 之后不会存在乱码(前提:请求页面使用的是 UTF-8 编码)。对于 Post 请求,使用setCharacterEncoding即可,为了统一设置所有 Servlet 编码,新建一个过滤器 Filter 设置编码最为方便:

@WebFilter("/*")
public class EncodingFilter implements Filter {
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        request.setCharacterEncoding("utf-8");
        chain.doFilter(request,response);
    }

    public void destroy() {

    }
}

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Based on Java™ Servlet Specification v3.1 [TOC] Servlet和S...
    0x70e8阅读 1,261评论 0 7
  • IOC 控制反转容器控制程序对象之间的关系,而不是传统实现中,有程序代码之间控制,又名依赖注入。All 类的创建,...
    irckwk1阅读 868评论 0 0
  • Servlet过滤器是 Servlet 程序的一种特殊用法,主要用来完成一些通用的操作,如编码的过滤、判断用户的登...
    重山杨阅读 1,158评论 0 12
  • 这部分主要是与Java Web和Web Service相关的面试题。 96、阐述Servlet和CGI的区别? 答...
    杂货铺老板阅读 1,344评论 0 10
  • Servlet接口 Servlet规范的核心接口即是Servlet接口,它是所有Servlet类必须实现的接口,在...
    java日记阅读 1,792评论 0 2