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 请求,如下图所示:

Servlet 学习笔记_第1张图片
CGI

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

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

Servlet 学习笔记_第2张图片
Servlet

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

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

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 学习笔记_第3张图片
Servlet

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

Servlet 生命周期

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

Servlet 学习笔记_第4张图片
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容器关闭时,会调用 Servlet 的destroy方法,让该 Servlet 进行资源释放操作。

入门案例

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

具体操作如下:

  1. 使用 Maven 新建一个 web app工程:
Servlet 学习笔记_第5张图片
New Project
  1. 在 pom.xml 中导入 Servlet 依赖:


    javax.servlet
    javax.servlet-api
    3.1.0
    provided

  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

Servlet 学习笔记_第6张图片
  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("

Hello Servlet

"); } ... }
  1. webapp/WEB-INF/web.xml 中配置MyServlet及其映射地址:



    
    
        
        myServlet
        
        com.yn.MyServlect
    
    
    
    
        
        myServlet
        
        /hello
    

  1. 配置 Tomcat 服务器:
Servlet 学习笔记_第7张图片
tomcat configuration
  1. 运行项目,此时浏览器输入:localhost:8080/hello,就可以看到输出了。

Servlet 执行模型

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

Servlet 学习笔记_第8张图片
request-reponse

具体来说:

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

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

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

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

Servlet 继承体系

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

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

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

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

Servlet 学习笔记_第9张图片
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,其将 Servlet 的service方法的参数ServletRequestServletResponse进行了强转,提供了更加强大的请求处理和响应功能。

Servlet 学习笔记_第10张图片

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

Web组件跳转

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

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

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

对应代码实现:

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 学习笔记_第12张图片
请求包含

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

对应代码实现:

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

特点

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

对应代码实现:

response.sendRedirect(String location);

特点

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

Filter(过滤器)

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

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

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

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

Servlet 学习笔记_第14张图片
过滤器

官方提供的接口为: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中配置我们定义的过滤器:

    
    
        MyFilter
        com.yn.filter.MyFilter
    
    
    
        MyFilter
        
        /*
    

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

注解开发

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

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

  • Servlet 注解配置:@WebServlet
    比如,像上面入门案例,我们把 web.xml 中的 标签去除掉,然后在源码中直接使用注解@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("

Hello Servlet

"); } ... }

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

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

下面对注解WebServlet进行讲解:

Servlet 学习笔记_第15张图片
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中的标签去除掉,然后在源码中直接使用注解@WebFilter进行配置:
@WebFilter("/*")
public class MyFilter implements Filter {
...
}

下面对WebFilter进行讲解:

Servlet 学习笔记_第16张图片
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 格式接收请求,此时就无须进行编码转换了。

2)对于 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() {

    }
}

参考

  • Servlet Tutorial
  • 初学Java Web(3)——第一个Servlet

你可能感兴趣的:(Servlet 学习笔记)