servlet生命周期详解--【结合源码】

Servlet源码讲解

通过一个项目,衍生出Java Servlet的源码分析与讲解,结合官方文档,力求准确无误

第一个项目

没有配置Tomcat的朋友可以看看这篇文章BS架构和CS架构 + Tomcat安装及配置

一个基础JavaWeb项目包含三个方面:表现层(Presentation layer)、业务逻辑层(Business Logic Layer)、数据访问层(Data access layer),也就是JavaEE里面的三层架构,此demo项目里只有表现层,也就是servlet + html

servlet:

public class DemoServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String username = request.getParameter("username");
        String age = request.getParameter("age");
        System.out.println(username);
        System.out.println(age);
    }
}

html:

<form action="/demo" method="post">
    姓名: <input type="text" name="username" /> <br/>
    年龄: <input type="text" name="age" /> <br/>
    <input type="submit" value="提交"/>
</form>

web.xml

<servlet>
    <servlet-name>demo</servlet-name>
    <servlet-class>com.servlet.DemoServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>demo</servlet-name>
    <url-pattern>/demo</url-pattern>
</servlet-mapping>
servlet字符编码

启动后,html页面会显示一个表单,输入内容,点击提交按钮,即可提交表单数据,DemoServlet通过request对象获取表单提交的数据,然后打印到控制台。

如果你输入中文,表单提交后你会发现,控制台显示的是一串乱码,我们先debug看一下:

乱码1

发现获取到的就是乱码,如何解决:

这就涉及到了servlet的编码设置:

  • tomcat8之前的编码
    • get请求
      • 获取字符串:str = request.getParameter(“”);
      • 将获取的字符串打散成编码格式为ISO-8859-1的字节数组: bytes = str.getBytes(“ISO-8859-1”);
      • 再将字节数组转换成指定编码的字符串: str = new String(bytes, “utf-8”);
    • post请求
      • request.setCharacterEncoding(“utf-8”);
  • tomcat8及之后的编码
    • tomcat8之后的编码设置只需要针对post请求
    • request.setCharacterEncoding(“utf-8”);
  • 注意:设置编码要在获取参数之前

如果,你在设置编码后,还是出现了乱码,就像这样

乱码2

接收到的是正常字符串,Tomcat控制台输出的时候,显示了乱码

  • 如果你启动Tomcat时,日志文件也显示了乱码处理方法
  • 按照上述方法修改后,正常情况下,应该是没有问题了,如果全试了没有解决,可以下方留言,看到会回复,或者直接百度

乱码3

servlet继承关系

public class DemoServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String username = request.getParameter("username");
        String age = request.getParameter("age");
        System.out.println(username);
        System.out.println(age);
    }
}

仔细观察不难发现,我这个DemoServlet类是继承HttpServlet类的,里面的方法也是重写的,也就是说,我只是写了方法的实现而已,其他的都是HttpServlet这个类帮我实现的,所以我们看看这个类

public abstract class HttpServlet extends GenericServlet

我们发现HttpServlet类是是一个抽象类,同时继承了GenericServlet类,我们看看GenericServlet

public abstract class GenericServlet implements Servlet, ServletConfig, Serializable

我们发现,GenericServlet类也是一个抽象类,它同时实现了三个接口:

  • Servlet
  • ServletConfig
  • Serializable

Serializable这个我们知道它是一个序列化接口,ServletConfig这个接口看名字像是一个配置文件接口,Servlet接口没见过但看名字就不简单(我们学servlet它就叫Servlet)

重头戏留到后面,我们先看一下ServletConfig接口,我们不妨看名字猜一下

public interface ServletConfig {
    # 获取Servlet的名称		-----> 可能和web.xml里面的 servlet-name 有关系
    String getServletName();
  	# 获取一个ServletContext对象
    ServletContext getServletContext();
    # 获取初始化参数
    String getInitParameter(String var1);
    # 获取初始化的参数列表,以枚举的形式返回
    Enumeration<String> getInitParameterNames();
}

看完这个接口我们发现都是和Servlet配置有关的,除了一个ServletContext对象我们不清楚以外,没有和运行有关的方法,所以我们看Servlet接口,同样的看名字猜用处:

public interface Servlet {
    # 初始化方法
    void init(ServletConfig var1) throws ServletException;
    # 获取ServletConfig对象
    ServletConfig getServletConfig();
    # service(服务)
    void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
    # 获取servlet的信息
    String getServletInfo();
    # destroy(销毁)
    void destroy();
}
init()和destroy()

我们发现了什么?init()、service()、destroy()这三个方法,正如我们猜测的那样,他们的功能就是初始化、提供服务、销毁servlet实例

通过阅读HttpServletGenericServletServlet这三个类的源码,其中init()和destroy()在源码中,并没有逻辑代码,为什么?

init(): 我查看了官方的解释

Called by the servlet container to indicate to a servlet that the servlet is being placed into service.
The servlet container calls the init method exactly once after instantiating the servlet. The init method must complete successfully before the servlet can receive any requests.
The servlet container cannot place the servlet into service if the init method
	Throws a ServletException
	Does not return within a time period defined by the Web server
翻译:
由servlet容器调用,以向servlet指示servlet正在投入服务。
servlet容器在实例化servlet之后只调用init方法一次。init方法必须成功完成,servlet才能接收任何请求。
如果init方法出现以下的两种情况,它将无法servlet容器将无法将servlet投入使用
    抛出ServletExceptionWeb服务器定义的时间段内不返回规定的值

通过这个解释相信大家都有发现,init()方法是在servlet实例创建后才调用的,并不是由它创建servlet实例

destroy(): 直接上官方解释

Called by the servlet container to indicate to a servlet that the servlet is being taken out of service. This method is only called once all threads within the servlet's service method have exited or after a timeout period has passed. After the servlet container calls this method, it will not call the service method again on this servlet.
This method gives the servlet an opportunity to clean up any resources that are being held (for example, memory, file handles, threads) and make sure that any persistent state is synchronized with the servlet's current state in memory.
翻译:
由servlet容器调用,以向servlet指示servlet正在停止服务。只有当servlet的服务方法中的所有线程都退出或超时后,才会调用此方法。servlet容器调用此方法后,将不再在此servlet上调用服务方法。
该方法使servlet有机会清理所持有的任何资源(例如,内存、文件句柄、线程)并确保任何持久状态与servlet在内存中的当前状态同步。

通过阅读官方的解释,我们知道这两个方法主要的还是Servlet容器去调用,用来通知Called byservlet会被执行或销毁

重头戏又在后面,三个有两个不是让我们调用的,那只剩下service方法了,阅读源码我们发现,在GenericServletServlet两个

类中service()都是没有逻辑代码的,而在HttpServlet类中,对其进行了实现,看看源码:

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String method = req.getMethod();
    long lastModified;
    if (method.equals("GET")) {
        lastModified = this.getLastModified(req);
        if (lastModified == -1L) {
            this.doGet(req, resp);
        } else {
            ...
            if (ifModifiedSince < lastModified / 1000L * 1000L) {
                this.maybeSetLastModified(resp, lastModified);
                this.doGet(req, resp);
            } else {
                resp.setStatus(304);
            }
        }
    } else if (method.equals("HEAD")) {
        ....
        this.doHead(req, resp);
    } 
    ...
    else {
        String errMsg = lStrings.getString("http.method_not_implemented");
        Object[] errArgs = new Object[]{method};
        errMsg = MessageFormat.format(errMsg, errArgs);
        resp.sendError(501, errMsg);
    }
}

通过源码我们发现,源码中先是获取请求方式String method = req.getMethod();,然后通过一系列的if去调用对应的doXxx()

方法。我们阅读doXxx()方法。会发现,这些方法都是这个形式

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String msg = lStrings.getString("http.method_get_not_supported");
    this.sendMethodNotAllowed(req, resp, msg);
}
private void sendMethodNotAllowed(HttpServletRequest req, HttpServletResponse resp, String msg) throws IOException {
    String protocol = req.getProtocol();
    if (protocol.length() != 0 && !protocol.endsWith("0.9") && !protocol.endsWith("1.0")) {
    	resp.sendError(405, msg);
    } else {
    	resp.sendError(400, msg);
    }
}

我们发现,要么返回405,要么返回400,这其实就是告诉我们,我们没有写这个方法,去响应对应的请求

405

我们接着看它的方法体:

# 从lStrings里面通过key去获取字符串
String msg = lStrings.getString("http.method_get_not_supported");
# lStrings是一个配置文件,javax.servlet.http.LocalStrings这个地方
private static final ResourceBundle lStrings = ResourceBundle.getBundle("javax.servlet.http.LocalStrings");

# 我在jar包中找到了LocalStrings.properties
err.cookie_name_blank=Cookie name may not be null or zero length
err.cookie_name_is_token=Cookie name [{0}] is a reserved token
err.io.indexOutOfBounds=Invalid offset [{0}] and / or length [{1}] specified for array of size [{2}]
err.io.nullArray=Null passed for byte array in write method
err.io.short_read=Short Read
err.state.commit=Not permitted once response has been committed

http.method_delete_not_supported=HTTP method DELETE is not supported by this URL
http.method_get_not_supported=HTTP method GET is not supported by this URL
http.method_not_implemented=Method [{0}] is not implemented by this Servlet for this URI
http.method_post_not_supported=HTTP method POST is not supported by this URL
http.method_put_not_supported=HTTP method PUT is not supported by this URL
http.non_http=Non HTTP request or response
doXxx()和service()

现在我们不难发现,这个HttpServlet中实现的service方法以及一系列的doXxx()方法并不能处理业务,那么也就是说这就是我们要自

己重写这些方法去,实现业务的处理,那么我们到底是重写service()方法还是doXxx()方法呢?

我想告诉你的是都可以,但我建议大家重写doXxx()方法,无论你是重写service()方法还是doXxx()方法都可以实现,你想要的效果,但是其中有一些差别

  • 如果你是重写doXxx()方法,那么service()方法还是使用HttpServlet类中写的,那么它在遇到你意料之外的请求方法发送来的请求时(你只写了doGet(),但是来了一个Post请求),会调用它自己写的,也就是返回405或400
  • 如果你重写service()方法,那么除非你也像HttpServlet类中写的一样,把所有的情况都考虑到,不然就会白屏(我试的,不知道对不对),浏览器什么都不显示,那时候直接尬住
  • 其次,在service()方法中,帮我们实现了缓存协商机制(大家好奇的话,可以自己查一下,不急的话我也会写的时候,看我的,点点关注自动通知),你重写的话也没有了。

然后,我们再看看官方对service()方法的解释:

Called by the servlet container to allow the servlet to respond to a request.
This method is only called after the servlet's init() method has completed successfully.
The status code of the response always should be set for a servlet that throws or sends an error.
Servlets typically run inside multithreaded servlet containers that can handle multiple requests concurrently. Developers must be aware to synchronize access to any shared resources such as files, network connections, and as well as the servlet's class and instance variables.
翻译:
由servlet容器调用,以允许servlet响应请求。
只有在servlet的init()方法成功完成后,才会调用此方法。
应始终为引发或发送错误的servlet设置响应的状态代码。
servlet通常在可以同时处理多个请求的多线程servlet容器内运行。开发人员必须知道同步访问任何共享资源,如文件、网络连接以及servlet的类和实例变量。

通过这些解释我们可以从中知道:

  • service方法也是有servlet容器去调用
  • service方法只有init()方法成功后才能调用
  • 我们要给一个错误的请求,返回一个响应状态码(这也告诉我们要重新doXxx()方法,而不是service()
  • service方法一般运行在多线程的环境中,但它不是线程安全的,要确保线程安全,我们要自己对数据继续同步

综上,也是我推荐大家重写doXxx()方法的原因。

总结

  • servlet继承关系
    • Servlet接口 —> GenericServlet抽象类 —> HttpServlet抽象子类
  • 相关方法:
    • Servlet接口:
      • 主要的方法:
        • init(ServletConfig): 初始化方法
        • service(ServletRequest, ServletResponse):请求分发 – 抽象方法
        • destroy():销毁方法
    • GenericServlet抽象类
      • service(ServletRequest, ServletResponse):请求分发 – 抽象方法
    • HttpServlet抽象子类
      • service(ServletRequest, ServletResponse):请求分发 – 实现方法

servlet生命周期

  • 生命周期:从出生到死亡,就是生命周期,servlet中的相关函数:init(), service(), destroy()
  • 默认情况下:
    • 第一次接收请求时创建实例(调用构造器),初始化对象(init()方法),然后调用服务(service()方法)
    • 第二次请求开始就直接调用service()方法
    • 当servlet容器被关闭时,servlet实例会被销毁
    • 注:servlet实例tomcat只会创建一个,所有的请求都是这个实例去响应。默认情况下,第一次请求时tomcat会创建servlet实例
  • servlet的初始化
    • 默认情况下,servlet实例会在第一次请求的时候,被tomcat创建
    • 我们可以通过<load-on-startup>来,设置servlet的初始化时机
      The load-on-startup element indicates that this servlet should be loaded (instantiated and have its init() called) 
      on the startup of the web application. The optional contents of these element must be an integer  indicating
      the order in which the servlet should be loaded. If the value is a negative integer, or the element is not 
      present, the container is free to load the servlet whenever it chooses. If the value is a positive integer or
      0, the container must load and initialize the servlet as the application is deployed. The container must 
      guarantee that servlets marked with lower integers are loaded before servlets marked with higher integers. 
      The container may choose the order of loading of servlets with the same load-on-start-up value.
      翻译:
      load-on-startup元素表示这个servlet应该在web应用程序启动时加载(实例 化并调用其init()。这些元素的可选内容必须是一个整数,
      指示servlet的加 载顺序。如果值是负整数,或者元素不存在,则容器可以随时加载  servlet。如果该值为正整数或0,则容器必须在部署应用
      程序时加载并初始 化servlet。容器必须保证标记为低整数的servlet在标记为高整数的servlet 之前加载。容器可以选择具有相同启动负载
      值的servlet的加载顺序。
      
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学编程的小猫猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值