Servlet 学习笔记

2019-08-20  本文已影响0人  Whyn

前言

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

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

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

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

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

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

Web 应用体系架构

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

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

使用 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 来说,具备如下几大优势:

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 生命周期有关的方法为:

通常情况下,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 工程目录配置不全,因此我们需要手动进行补全:
  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);

特点

  1. 请求包含(include):响应包含资源(如 Servlet,JSP页面,HTML文件)内容,如下图所示:
请求包含

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

对应代码实现:

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

特点

  1. 重定向(redirect):又称为 间接转发方式,客户端第一次请求时,服务端下发重定向请求(响应携带新地址),客户端接收到响应后,再次请求新地址,如下图所示:
重定向

对应代码实现:

response.sendRedirect(String location);

特点

Filter(过滤器)

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

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

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

过滤器

官方提供的接口为: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-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 的相关注解进行讲解:

@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配置更加简洁。

@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注解的相关属性几乎一致。

其他

乱码终极解决方案:按上述分析,对于 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() {

    }
}

参考

上一篇 下一篇

猜你喜欢

热点阅读