笔记-Java源码阅读-Servlet基础

前言

是什么?

Servlet是 Server Applet的简称,翻译过来就是服务程序,它是一种运行在服务端的,用来处理服务器接收到的请求的小程序,你也可以将它理解为一套处理网络请求的规范,在Java中被抽象封装成我们所熟知的Servlet 接口,Servlet能够将客户端请求和响应内容封装成Java对象,便于开发者读取客户端请求内容,并编辑响应内容。

为什么?

其实Servlet技术其实已经很古老了,并且已经完全成熟,基本上不会有新的变化,现在也基本上不会有直接基于Servlet开发的应用了,但是,学习了解Servlet的运行原理也是很有必要的,因为现在流行的很多框架,诸如 SpringMVC 之类的,底层都是基于Servlet规范,学习并了解Servlet,能让我们遇到疑难杂症的时候,不至于无从下手。

怎么样?

那么,Servlet我们应该学到什么程度呢?我觉得可以学习如下几个方面:

  • Servlet接口的生命周期;
  • GenericServlet和HttpServlet;
  • ServletRequest和ServletResponse;
  • HttpServletRequest;
  • HttpServletResponse;
  • ServletConfig和@WebServlet;

正文

在开始正文前,我们先在本地构建一个简单的Servlet程序,方便我们通过打断点的形式直观观测Servlet的运行过程。我看过几本讲Servlet的书籍,不过基本上要么是结合JSP来讲,要么就独立式Tomcat来讲,对于类似我这种一开始就学习的Spring Boot的开发者而言,真的很难理解,我也不想回过头去再看那些已经被淘汰的技术了,因此,我决定探索一条基于SpringBoot+嵌入式Tomcat+自定义Servlet的实验方案,来探索Servlet的源码奥秘,废话不多数,开始coding!
首先,在idea里初始化一个maven项目,pom.xml中引入的主要依赖如下:

<dependencies>
    <dependency>
        <!-- 嵌入式tomcat-->
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <version>2.7.5</version>
    </dependency>
    <dependency>
        <!-- SpringWeb核心依赖-->
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>5.3.23</version>
    </dependency>
    <dependency>
        <!-- SpringBoot核心依赖-->
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
        <version>2.7.5</version>
    </dependency>
</dependencies>

然后自定义一个Servlet实现,我这里叫做 MyServlet,他的业务逻辑代码就是控制台输出 hello world! ,代码如下:

package indi.demo.api;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(urlPatterns = "demo")
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp){
        System.out.printf("hello world!");
    }
}

最后补上启动类,启动即可,启动类代码:

package indi.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@ServletComponentScan
public class Demo {

    public static void main(String[] args) {
        SpringApplication.run(Demo.class, args);
    }

}

启动此Demo代码,可以在浏览器中访问地址: http://localhost:8080/demo,控制台会打印hello world!
需要注意的是,基于注解形式启动Servlet是Servlet3.0引入的特性,此版本之前无法通过注解来配置和启动Servlet,附赠一张此Demo程序的层次结构说明图:

Servlet的生命周期

Servlet的生命周期可以分为以下几个阶段:

  • 类加载和实例化;
  • Servlet初始化;
  • 处理请求;
  • Servlet销毁;

Servlet的生命周期是通过Servlet接口的各个方法来体现的,打开Servlet接口,可以看到如下代码:

public interface Servlet {
    
    // 此方法对应Servlet的初始化阶段
    public void init(ServletConfig config) throws ServletException;

    // 此方法对应Servlet处理请求
    public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException;

    // 此方法对应Servlet销毁阶段
    public void destroy();

    // 其他方法略
}

Servlet的运行过程(以我正文开始前所述的Demo为例):首先,SpringBoot启动时,会检测当前的服务类型,因为我引入了 spring-web 这个依赖,这里的服务类型就会检测为web服务,这是SpringBoot就回去加载Servlet容器,所谓Servlet容器就是Servlet运行的平台,也就是我们在pom文件里引入的嵌入式Tomcat,当嵌入式Tomcat启动的时候,会寻找合适的Servlet实例加载到内存中,并将其实例化。
那什么样的类会被Tomcat选择中呢?在我这个Demo代码,满足了两个条件:

  • 我们写的这个类必须是Servlet接口的实现类;
  • 启动类需要添加 @ServletComponentScan注解;
  • 这个实现类需要添加 @WebServlet 注解,这个注解主要有两个作用:
    • 将一个类标记为Servlet类,供Tomcat识别;
    • 提供一些Servlet所需配置,比如urlPatterns参数;

GenericServlet和HttpServlet

虽然可以直接实现Servlet接口,就能构造一个简单的Servlet应用,但是我们一般不会直接使用Servlet接口的,更常用的是几次HttpServelt这个父类,来间接实现Servlet接口,那为什么要这么做呢?
Servlet接口是一个具有通用性质的接口,但是在一个服务端应用中,我们可能需要很多个这样的Servlet实现,这就意味着,如果我们直接使用Servlet接口的话,需要对Servlet的中每个方法给出具体实现,但其实,实际使用中,我们更加关注的是其中的service方法,大多数情况下,其他方法都是默认给个空的默认实现即可,所以为了降低冗余,就将除service方法以外的方法给出默认实现,只留service给开发者定制化,这个就是GenericServlet的设计目的。
GenericServlet的核心代码如下:

public abstract class GenericServlet implements Servlet, ServletConfig,
        java.io.Serializable {
    
    // 核心抽象方法留给子类实现
    @Override
    public abstract void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException;

    // 其他方法,给出了默认实现,一般是空方法体
    
}

说完GenericServlet,我们再来看HttpServlet,它是前者的子类,顾名思义,他就是为了Http协议的请求定制化的Servlet实现。在实际开发中,一个请求一般会按照method(方法)的不同,而走不同的业务逻辑,这就意味着,如果仅用GenericServlet的话,我们会经常在service方法里,根据请求的method的不同,而让代码走不同的逻辑或者方法,于是,HttpServlet便应运而生,它重写了service方法,根据method的不同取值,去调用不同的protected方法,开发者根据自身需求直接复写对应方法即可,进而节省了开发者的时间,降低了代码冗余。

public abstract class HttpServlet extends GenericServlet {
    
    // 一些HTTP的method常量
    private static final String METHOD_DELETE = "DELETE";
    private static final String METHOD_HEAD = "HEAD";
    private static final String METHOD_GET = "GET";
    private static final String METHOD_OPTIONS = "OPTIONS";
    private static final String METHOD_POST = "POST";
    private static final String METHOD_PUT = "PUT";
    private static final String METHOD_TRACE = "TRACE";

    // 实现GenericServlet方法
    protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {

        String method = req.getMethod();

        
        if (method.equals(METHOD_GET)) {
            // 如果method=GET,调用doGet方法
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                doGet(req, resp);
            } else {
                long ifModifiedSince;
                try {
                    ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                } catch (IllegalArgumentException iae) {
                    ifModifiedSince = -1;
                }
                if (ifModifiedSince < (lastModified / 1000 * 1000)) {
                    maybeSetLastModified(resp, lastModified);
                    doGet(req, resp);
                } else {
                    resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                }
            }

        } else if (method.equals(METHOD_HEAD)) {
            // 如果method=HEAD,调用doHead方法
            long lastModified = getLastModified(req);
            maybeSetLastModified(resp, lastModified);
            doHead(req, resp);

        } else if (method.equals(METHOD_POST)) {
            // 如果method=POST,调用doPost方法
            doPost(req, resp);

        } else if (method.equals(METHOD_PUT)) {
            // 如果method=PUT,调用doPut
            doPut(req, resp);

        } else if (method.equals(METHOD_DELETE)) {
            // 如果method=DELETE,调用doDelete方法
            doDelete(req, resp);

        } else if (method.equals(METHOD_OPTIONS)) {
            // 如果method=OPTIONS,调用doOptions方法
            doOptions(req,resp);

        } else if (method.equals(METHOD_TRACE)) {
            // 如果method=TRACE,调用doTrace方法
            doTrace(req,resp);

        } else {
        	// 其他情况,返回错误信息
            String errMsg = lStrings.getString("http.method_not_implemented");
            Object[] errArgs = new Object[1];
            errArgs[0] = method;
            errMsg = MessageFormat.format(errMsg, errArgs);

            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
        }
    }

    // 当method为GET的时候,service方法调用此 doGet方法
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {
        // 默认情况下,这个方法会返回不支持调用的错误信息,通过复写以实现自定义输出
        String msg = lStrings.getString("http.method_get_not_supported");
        sendMethodNotAllowed(req, resp, msg);
    }

    // 此处只举例method=GET时调用doGet方法,其他方法大致同理,略。
}

ServletRequest和ServletResponse

ServletRequest和ServletResponse是Servlet接口的service方法的主要参数,它们分别封装了Http请求的请求头数据和响应头数据。
先来讲讲ServletRequest,它是对请求报文的封装,核心源码如下:

public interface ServletRequest {

    // 获取指定属性
    public Object getAttribute(String name);
    
    // 获取所有属性
    public Enumeration<String> getAttributeNames();
    
    // 获取字符编码格式
    public String getCharacterEncoding();
    
    // 设置字符编码格式
    public void setCharacterEncoding(String env) throws java.io.UnsupportedEncodingException;
    
    // 获取报文内容长度,返回int结果
    public int getContentLength();
    
    // 获取报文内容长度,返回long结果
    public long getContentLengthLong();
    
	// 获取内容类型
    public String getContentType();
    
    // 获取输入流
    public ServletInputStream getInputStream() throws IOException;
    
    // 获取指定参数的第一个值
    public String getParameter(String name);
    
    // 获取参数名称枚举
    public Enumeration<String> getParameterNames();
    
    // 获取指定参数的所有的值
    public String[] getParameterValues(String name);
    
    // 获取所有参数及其参数值
    public Map<String, String[]> getParameterMap();
    
    // 获取协议
    public String getProtocol();
    
    // 获取URL的主机名
    public String getServerName();
    
    // 获取URL的主机端口
    public int getServerPort();
    
    // 获取报文Body部分的Reader
    public BufferedReader getReader() throws IOException;
    
    // 获取客户端地址,一般是IP
    public String getRemoteAddr();
    
    // 获取客户端主机名称,一般是IP
    public String getRemoteHost();
    
    // 获取客户端通信端口
    public int getRemotePort();
    
    // 获取语言环境
    public Locale getLocale();
    
    // 获取Servlet上下文
    public ServletContext getServletContext();
}

补充说明:

  • **getAttribute **:attribute在Request中是以Map形式存储的,getAttributeNames 方法就是获取这个Map的key的枚举,通过这个枚举值,调用 getAttribute 方法就能获取到attribute值;
  • getContentLength :返回值的单位是byte,代表着Content包含的byte数量,若无Content,则返回 -1 ,如果上传的内容过大,比如文件之类的,建议使用 getContentLengthLong 方法;
  • getContentType :可以获取到Http请求头的 Content-Type 属性的值;
  • getParameter :parameter在Request中是以 Map<String,ArrayList> 的形式存储的,getParameterNames 方法获取Map的key组成的枚举,getParameter 方法可以根据入参匹配Map的key,如果匹配到,则取List的第一个数据返回;getParameterValues 则不是选择第一个,而是全部值,将其转换成字符串数字返回;getParameterMap 则直接返回了所有参数及其参数值;
  • getProtocol :返回协议及其版本,比如 HTTP/1.1 ;

ServletResponse 是对响应报文的封装,主要代码如下:

public interface ServletResponse {
    
    // 获取字符编码格式,默认 ISO-8859-1
    public String getCharacterEncoding();
    
    // 告诉客户端返回内容采用的字符编码格式
    public void setCharacterEncoding(String charset);
    
    // 获取内容类型,比如 text/xml 
    public String getContentType();
    
    // 设置内容类型,体现在Head的Content-Type属性
    public void setContentType(String type);
    
    // 获取输出流,方便向客户端写入二进制数据
    public ServletOutputStream getOutputStream() throws IOException;
    
    // 获取打印流,方便向客户端写入文字字符
    public PrintWriter getWriter() throws IOException;
    
    // 设置响应内容的长度,体现在Head的Content-Length属性
    public void setContentLength(int len);
    
    // 功能同 setContentLength,能够表示的范围更大
    public void setContentLengthLong(long length);
    
    // 设置语言环境
    public void setLocale(Locale loc);

    // 获取置语言环境
    public Locale getLocale();
    
}

HttpServletRequest

我们前面讲了SerlvetRequest之后,不知道你有没有发现,他没有任何与Http协议相关的方法,比如获取Cookie、Session、Header、请求的方法method值等等方法,因为ServletRequest抽象的是网络请求,并非特指Http请求,所以它只包含多个网络协议的公共逻辑,比如字符编码格式之类的。针对HTTP协议的请求,有另外两个类与之对应,就是HttpServletRequest和HttpServletResponse。
我们先来看看HttpServletRequest类,主要代码如下:

public interface HttpServletRequest extends ServletRequest {

    // 获取所有Cookie对象
	public Cookie[] getCookies();

    // 在Header中,获取指定name下的一个值
    public String getHeader(String name);

    // 在Header中,获取指定name下的所有值
    public Enumeration<String> getHeaders(String name);

    // 获取Header的name枚举
    public Enumeration<String> getHeaderNames();

    // 在Header中,获取指定name下的所有值,并转换成int类型数据
    public int getIntHeader(String name);

    // 获取Http的Method
    public String getMethod();

    // 获取请求路径
    public String getPathInfo();

    // 获取查询参数
    public String getQueryString();

    // 获取Session
    public HttpSession getSession();

    // 更改sessionId并返回
    public String changeSessionId();

    // 获取RequestURI
    public String getRequestURI();

    // 获取RequestURL
    public StringBuffer getRequestURL();

    // 获取ContextPath
    public String getContextPath();

    // 获取form表单上传的文件集合,文件以Part形式存在
    public Collection<Part> getParts() throws IOException,ServletException;

    // 获取指定文件的Part对象
    public Part getPart(String name) throws IOException,ServletException;
    
}

我以一个Demo代码,让大家直观看到这些参数值和输出格式:

@WebServlet(name = "demoServlet", urlPatterns = "/demo/*")
public class MyServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 打印 getCookies
        System.out.println("request.getCookies ->");
        for (Cookie cookie : req.getCookies()) {
            System.out.println(String.format("%15s : %s", cookie.getName(), cookie.getValue()));
        }
        // 打印 getHeader
        Enumeration<String> hNames = req.getHeaderNames();
        System.out.println("request.getHeader ->");
        while (hNames.hasMoreElements()) {
            String hName = hNames.nextElement();
            System.out.println(String.format("%15s : %s", hName, req.getHeader(hName)));
        }
        // 打印 getMethod
        System.out.println("request.getMethod -> "+req.getMethod());
        // 打印 getRequestURL
        System.out.println("request.getRequestURL -> "+req.getRequestURL());
        // 打印 getRequestURI
        System.out.println("request.getRequestURI -> "+req.getRequestURI());
        // 打印 getPathInfo
        System.out.println("request.getPathInfo -> "+req.getPathInfo());
        // 打印 getQueryString
        System.out.println("request.getQueryString -> "+req.getQueryString());
    }
}

Postman请求为 http://127.0.0.1/demo/test?param1=p1_value&param2=p2_value ,控制台输出内容如下:

request.getCookies ->
             c1 : c1_value
             c2 : c2_value
request.getHeader ->
     user-agent : PostmanRuntime/7.29.2
         accept : */*
  postman-token : 1e36217c-31d9-4f79-b38a-bff91a5f85cf
           host : 127.0.0.1
accept-encoding : gzip, deflate, br
     connection : keep-alive
         cookie : c1=c1_value; c2=c2_value
request.getMethod -> GET
request.getRequestURL -> http://127.0.0.1/demo/test
request.getRequestURI -> /demo/test
request.getPathInfo -> /test
request.getQueryString -> param1=p1_value&param2=p2_value

HttpServletResponse

针对 HttpServletResponse,我们也是先讲源码,再举例子。
HttpServletResponse的主要源码如下:

public interface HttpServletResponse extends ServletResponse {
    
    // 添加Cookie
    public void addCookie(Cookie cookie);

    // 发送错误消息,使用自定义消息
    public void sendError(int sc, String msg) throws IOException;

    // 发送错误消息
    public void sendError(int sc) throws IOException;

    // 响应重定向
    public void sendRedirect(String location) throws IOException;

    // 设置响应头
    public void setHeader(String name, String value);

    // 添加响应头
    public void addHeader(String name, String value);

    // 获取响应头指定name的一个值
    public String getHeader(String name);

    // 获取响应头指定name的所有值
    public Collection<String> getHeaders(String name);

    // 设置响应状态
    public void setStatus(int sc);

    // 获取响应状态
    public int getStatus();

    /*
    * 以下是 HTTP Status Code,即Http状态码
    */
	public static final int SC_CONTINUE = 100;
    public static final int SC_SWITCHING_PROTOCOLS = 101;
    public static final int SC_OK = 200;
    public static final int SC_CREATED = 201;
    public static final int SC_ACCEPTED = 202;
    public static final int SC_NON_AUTHORITATIVE_INFORMATION = 203;
    public static final int SC_NO_CONTENT = 204;
    public static final int SC_RESET_CONTENT = 205;
    public static final int SC_PARTIAL_CONTENT = 206;
    public static final int SC_MULTIPLE_CHOICES = 300;
	public static final int SC_MOVED_PERMANENTLY = 301;
    public static final int SC_MOVED_TEMPORARILY = 302;
    public static final int SC_FOUND = 302;
    public static final int SC_SEE_OTHER = 303;
    public static final int SC_NOT_MODIFIED = 304;
    public static final int SC_USE_PROXY = 305;
    public static final int SC_TEMPORARY_REDIRECT = 307;
    public static final int SC_BAD_REQUEST = 400;
    public static final int SC_UNAUTHORIZED = 401;
    public static final int SC_PAYMENT_REQUIRED = 402;
    public static final int SC_FORBIDDEN = 403;
    public static final int SC_NOT_FOUND = 404;
    public static final int SC_METHOD_NOT_ALLOWED = 405;
    public static final int SC_NOT_ACCEPTABLE = 406;
    public static final int SC_PROXY_AUTHENTICATION_REQUIRED = 407;
    public static final int SC_REQUEST_TIMEOUT = 408;
    public static final int SC_CONFLICT = 409;
    public static final int SC_GONE = 410;
    public static final int SC_LENGTH_REQUIRED = 411;
    public static final int SC_PRECONDITION_FAILED = 412;
    public static final int SC_REQUEST_ENTITY_TOO_LARGE = 413;
    public static final int SC_REQUEST_URI_TOO_LONG = 414;
    public static final int SC_UNSUPPORTED_MEDIA_TYPE = 415;
    public static final int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
    public static final int SC_EXPECTATION_FAILED = 417;
    public static final int SC_INTERNAL_SERVER_ERROR = 500;
    public static final int SC_NOT_IMPLEMENTED = 501;
    public static final int SC_BAD_GATEWAY = 502;
    public static final int SC_SERVICE_UNAVAILABLE = 503;
    public static final int SC_GATEWAY_TIMEOUT = 504;
    public static final int SC_HTTP_VERSION_NOT_SUPPORTED = 505;
    
}

ServletConfig与@WebServlet

ServletConfig 是Servlet的配置类,在初始化阶段,作为init方法的参数,传递进Servlet内部,ServletConfig的作用主要有:

  • 获取Servlet程序的别名;
  • 获取初始化参数;
  • 获取ServletContext对象;

ServletConfig的主要代码如下:

public interface ServletConfig {

    // 获取Servlet程序的名称
	public String getServletName();

    // 获取ServletContext对象
    public ServletContext getServletContext();

    // 获取初始化参数值
    public String getInitParameter(String name);

    // 获取初始化参数的参数名枚举
    public Enumeration<String> getInitParameterNames();
    
}
在SpringBoot中,我们可以不使用web.xml形式配置Servlet了,可以直接使用@WebServlet注解里提供的属性来配置Servlet了,我们先来看看这个注解的源码:
package javax.servlet.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebServlet {
    
    // Servlet名称
    String name() default "";

    // URL匹配模式
    tring[] urlPatterns() default {};

    // 同 urlPatterns
    String[] value() default {};

    // 启动优先级
    int loadOnStartup() default -1;

    // 初始化参数
    WebInitParam[] initParams() default {};

	// 是否支持异步
    boolean asyncSupported() default false;

    // 小图标
    String smallIcon() default "";

    // 大图标
    String largeIcon() default "";

    // 描述
    String description() default "";

    // 显示名称
    String displayName() default "";

}
其中:
  1. loadOnStartup是Servlet启动的优先级,如果值 <= 0 时,Servlet被首次请求时才会被加载;如果值 > 0 时,表示Servlet容器启动时加载,取值越小,优先级越高 ;
  2. initParams 对应于ServletConfig 的 initParameter ;
  3. name 属性对应于ServletConfig 的 servletName ;

我这里提供了一个小小的Demo代码,可以结合起来理解一下:

@WebServlet(
        name = "demoServlet",
        urlPatterns = "/demo",
        initParams = {
                @WebInitParam(name = "param1", value = "1+1=2"),
                @WebInitParam(name = "param2", value = "0+0=0")
        }
)
public class MyServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ServletConfig config = this.getServletConfig();
        // 打印ServletName
        System.out.println("ServletName -> "+ config.getServletName());
        // 打印initParams
        Enumeration<String> paramNames = config.getInitParameterNames();
        while (paramNames.hasMoreElements()) {
            String paramName = paramNames.nextElement();
            System.out.println(String.format("%s => %s", paramName, config.getInitParameter(paramName)));
        }
    }
}

/*
* 正常情况下,控制台输出如下:
* ServletName -> demoServlet
* param1 => 1+1=2
* param2 => 0+0=0
*/
  • 18
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值