Java web Tomcat (二)GenericServlet源码解析

动力节点JavaWeb视频
从底层源码开始讲起, 讲解的非常详细,对比过尚硅谷的视频,推荐动力节点。
视频是直播的录像,有些时候比较啰嗦,尤其到EL表达式之后,1小时的内容能讲3小时。

系列文章目录

Servlet的生命周期

Servlet对象是由服务器创建的 。
服务器创建的Servlet对象是由服务器的WEB容器管理器统一管理
开发人员不需要对Servlet对象的创建和销毁进行管理

  1. 启动服务器后会读取web.xml中的配置文件
  2. 当用户使用浏览器访问时,服务器会根据配置文件通过反射机制创建Servlet的对象

2.1. 服务器通过对url的解析,找到对应的标签中的标签
2.2. 通过同一组标签的中的名字去标签中查找中查找相同的标签
2.3. 找到通过名字找到对应的class文件的完整路径。通过反射创建对象


3. 创建对象使用的是无参构造方法(所以不能在类中加有参构造方法)
4. 执行对象的init()方法
5. 执行对象的service()方法
6. 当用户再次访问时再次执行service()方法,用户发送多少次请求就再执行多少次service()方法。
7. 关闭服务器时,将会执行destroy()方法,然后销毁对象

总结:

init方法只有在创建对象时执行一次,适合做初始化操作,例如:初始化线程池、初始化数据库连接池等
destroy方法只有在对象销毁之前执行一次,适合做资源关闭、保存数据等。

Servlet协议相关的接口

Servlet 接口

  • 源码:
public interface Servlet {
    public void init(ServletConfig config) throws ServletException;
    
    public ServletConfig getServletConfig();
    
    public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException;
            
    public String getServletInfo();
    
    public void destroy();
}
  • 实现Servlet接口后,共有5个方法,大部分情况都只调用service()一个方法,其他代码虽然不使用但还是要实现,这让代码看起来不够优雅
  • 为了解决这个问题,优化Servlet接口,创建GenericServlet抽象类
自建GenericServlet 抽象类 优化 Servlet接口
  • 自己写的GenericServlet 抽象类,非源码
public abstract class GenericServlet implements Servlet {
    private ServletConfig config;
    
    @Override
    public void init(ServletConfig config) throws ServletException {
        this.config = config;
        this.init();//模板方法设计模式
    }
    public void init(){}
	
    @Override
    //常用的方法作为抽象方法,实现类必须实现此方法,而其他方法则由抽象类全部实现。这样实现类中就只有这一个方法需要实现了。
    public abstract void service(ServletRequest req, ServletResponse res)
    throws ServletException, IOException;
    
	//获取成员变量config的值
    @Override
    public ServletConfig getServletConfig() {
        return config;
    }

    @Override
    public String getServletInfo() {
        return null;
    }
    @Override
    public void destroy() {
        // NOOP by default
    }
  • 通过GenericServlet 抽象类创建Servlet:
public class Implementation extends GenericServlet {
    @Override
    public void init() throws ServletException {
        //初始化,会自动执行
        // TODO: 
    }

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        // TODO: 
    }
}

通过对Servlet接口的优化,Implementation 类中代码已经精简了很多。
总结:

  1. 使用了适配器设计模式,将最常用的service()方法定义为抽象方法,如果要实现GenericServlet 抽象类,则必须重写此方法,而其他不常用的方法,都已经在此抽象类中实现。
  2. 使用了模板方法设计模式,当实现类Implementation中想要用init()方法初始化,则可以重写抽象类中的init()方法,这样就保证了抽象类GenericServlet中的init(ServletConfig config)方法不会被重写。
  3. 当实现类Implementation创建对象时Tomcat会调用抽象类GenericServlet中的init(ServletConfig config)方法,而init(ServletConfig config)方法中会执行this.init();代码,从而也会自动执行实现类Implementation中的init()
  4. init(ServletConfig config)中的config对象,将其从局部变量转变为全局变量。通过getServletConfig()方法可获得该变量的值。

为什么非要在实现类中使用init方法呢?可以不使用init这个名字。只要在抽象类init(ServletConfig config)方法中加入需要调用的方法,实现类去实现就可以了,只是习惯使用init这个名字的方法来初始化某些数据。
不容易理解的点:方法名相同但是参数不同,会被当成时两个方法,对继承没有影响,对于实现类来说就有两个init方法一个有参数一个没有参数。
Tomcat创建实现类对象—>执行抽象类中的init(ServletConfig config)—>this.init();—>实现类中的init()方法

这只是简单的模仿了GenericServlet 的由来,GenericServlet 中还包含实现了ServletConfig,java.io.Serializable这两个接口

ServletCofing 接口

Servlet 接口的init(ServletConfig config)方法中有一个ServletConfig 对象,Tomcat在每次创建Servlet实现类的对象时都会调用一次init(ServletConfig config),那么方法中的config 传了什么到Servlet中?

  • ServletConfig接口的源代码
public interface ServletConfig {
  	//获取Servlet的name
    public String getServletName();
	//获取整个Webapp应用的配置文件
    public ServletContext getServletContext();
	//获取初始化参数<init-param>中name对应的value
    public String getInitParameter(String name);
	//获取初始化参数<init-param>中的names
    public Enumeration<String> getInitParameterNames();
}
  • web.xml配置文件
<-- servletConfig.getServletContext()可以获得整个配置文件,这个webapp中的所有servlet -->
   <servlet>
   		<-- servletConfig.getServletName()可以得到<servlet-name>标签中的内容 -->
        <servlet-name>configServlet</servlet-name>
        <servlet-class>com.jpowernode.javaweb.servlet.ConfigTest</servlet-class>
        <-- 初始化参数 -->
        <init-param>
        	<-- servletConfig.getInitParameterNames()获得<param-name>标签中的内容 -->
            <param-name>driver</param-name>
            <-- servletConfig.getInitParameter(initParameterName)获得<param-value>标签中的内容 -->
            <param-value>com.mysql.cj.jdbc.Driver</param-value>
        </init-param>
        <init-param>
            <param-name>url</param-name>
            <param-value>jdbc:mysql//localhost:3306/bjpowernode</param-value>
        </init-param>
        <init-param>
            <param-name>user</param-name>
            <param-value>root</param-value>
        </init-param>
        <init-param>
            <param-name>password</param-name>
            <param-value>root</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>configServlet</servlet-name>
        <url-pattern>/config</url-pattern>
    </servlet-mapping>
</servlet>
ServletConfig 接口的四个方法
import jakarta.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;

public class ConfigTest extends GenericServlet {
    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        res.setContentType("text/html");
        PrintWriter out = res.getWriter();

        ServletConfig servletConfig = getServletConfig();
        
        //获得配置文件中<init-param>便签下<param-name>的值
        Enumeration<String> initParameterNames = servletConfig.getInitParameterNames();
        while(initParameterNames.hasMoreElements()){
            String initParameterName = initParameterNames.nextElement();
            //获得配置文件中<init-param>便签下<param-name>对应的<param-value>的值
            String initParameter = servletConfig.getInitParameter(initParameterName);
            out.write("key:&nbsp"+initParameterName+"&nbsp&nbsp&nbsp "+"value:&nbsp;"+initParameter+"</br>");
        }

        //servletName:配置文件中<servlet-name>
        String servletName = servletConfig.getServletName();
        out.write("servletName:&nbsp&nbsp"+servletName+"</br>");

        //servletContext中包含了整个项目的信息,web.xml的全部信息
        ServletContext servletContext = servletConfig.getServletContext();
        String contextPath = servletContext.getContextPath();
        out.write("servletContext:&nbsp&nbsp"+servletContext.getContextPath()+"</br>");
    }
}

输出

key: password    value: root
key: driver    value: com.mysql.cj.jdbc.Driver
key: user    value: root
key: url    value: jdbc:mysql//localhost:3306/bjpowernode
servletName:  configServlet
servletContext:  /servlet_war_exploded

总结:
Tomcat在创建Servlet的对象时,同时会把配置文件中的值都传递给init(ServletConfig config)中的config,这样开发人员就能在及时的获得到配置文件中的配置。

ServletContext 接口

  • Tomcat实现ServletContext接口的类:ApplicationContextFacade.java(.\apache-tomcat-10.0.12-src-源码\java\org\apache\catalina\core)
ServletContext、SerletConfig、Servlet的关系
  1. 一个webapp对应多个Servlet,对应一个配置文件web.xml,对应一个ServletContext
  2. 一个Servlet,对应一组<servlet></servlet>标签,对应一个SerletConfig
  3. 一个webapp里面的所有Servlet共有一个ServletContext

用现实生活举例:
一个班级就是一个webapp,班级里面的学生就是Servlet。
一个班级公用的所有东西就是ServletContext,每个学生的私有物品就是ServletConfig

  1. ServletContext是通过ServletConfig传递到Servlet中,所以在Servlet中很容易得到ServletContext
ServletContext servletContext = getServletConfig().getServletContext();
通过ServletContext获得webapp的全局配置

web.xml配置文件

<web-app ..省略..>
	<-- webapp 的全局配置 -->
    <context-param>
        <param-name>page</param-name>
        <param-value>50</param-value>
    </context-param>
    <context-param>
        <param-name>StartPage</param-name>
        <param-value>1</param-value>
    </context-param>
    
    <-- servlet 的配置 -->
    <servlet>
        <servlet-name>ContextConfig</servlet-name>
        <servlet-class>com.jpowernode.javaweb.servlet.ContextConfig</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>ContextConfig</servlet-name>
        <url-pattern>/contextconfig</url-pattern>
    </servlet-mapping>
</servlet>

代码部分:

public class ContextConfig extends GenericServlet {
    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        res.setContentType("text/html");
        PrintWriter out = res.getWriter();
        //通过ServletConfig获得ServletContext
        ServletContext servletContext = getServletConfig().getServletContext();
        //获得全局配置标签
        Enumeration<String> initParameterNames = servletContext.getInitParameterNames();
        //遍历标签,获得name和value
        while(initParameterNames.hasMoreElements()){
            String name = initParameterNames.nextElement();
            String initParameter = servletContext.getInitParameter(name);
            System.out.println(name+initParameter);
            out.write("context-parm-name:&nbsp&nbsp&nbsp"+name+
                    "&nbsp&nbsp&nbsp&nbsp&nbspcontext-parm-value:&nbsp&nbsp&nbsp"+initParameter+"</br>");
        }
    }
}

输出:

context-parm-name:   StartPage     context-parm-value:   1
context-parm-name:   page     context-parm-value:   50
获取当前项目的根节点
String servletContextName = servletContext.getContextPath();
out.write(servletContextName+"</br>");

输出:

/servlet_war_exploded
获取webapp中文件的绝对路径

参数:从根路径开始

        String realPath = servletContext.getRealPath("/index.html");
        out.write(realPath+"</br>");

输出:

D:\JAVA\javaweb\out\artifacts\servlet_war_exploded\index.html
记录日志

参数:可以只记录信息,也可以记录异常信息

        servletContext.log("日志记录信息", new RuntimeException());
        servletContext.log("日志记录信息");

日志记录的路径
在这里插入图片描述
日志的分类
在这里插入图片描述

共享数据

作用:当所有用户共享一份数据,并且数据量小,很少被修改,可以将数据存放到ServletContext中
涉及占用内存和多线程并发带来的问题,所以要求数据量小,且很少被修改

        //存取和Map很类似,以键值对的方式存入
        servletContext.setAttribute("key", "value");
        // 根据key的名字来取
        Object key = servletContext.getAttribute("key");
        // 根据key的名字删除
        servletContext.removeAttribute("key");

GenericServlet 源码

GenericServlet 抽象类 实现Servlet、Servletconfig、ServletContext接口中常用的方法

public abstract class GenericServlet implements Servlet, ServletConfig,
        java.io.Serializable {
    private static final long serialVersionUID = 1L;
    // 将Tomcat传递来的ServletConfig改变为全局变量,方便使用
    private transient ServletConfig config;
    //GenericServlet的无参构造方法
    public GenericServlet() {}
    // 当Servlet要销毁前执行的方法,实现Servlet接口
    @Override
    public void destroy() {}
    // 获得ServletContext的全局配置的value,实现ServletContext接口
    @Override
    public String getInitParameter(String name) {
        return getServletConfig().getInitParameter(name);
    }
    // 获得ServletContext的全局配置的name,实现ServletContext接口
    @Override
    public Enumeration<String> getInitParameterNames() {
        return getServletConfig().getInitParameterNames();
    }
    // 获得ServletConfig,实现Servlet接口
    @Override
    public ServletConfig getServletConfig() {return config;}
    // 从ServletConfig对象中获取ServletContext,实现ServletConfig接口
    @Override
    public ServletContext getServletContext() {
        return getServletConfig().getServletContext();}
    
    @Override
    public String getServletInfo() {
        return "";
    }
    // 当用户访问Servlet时,Tomcat自动调用的初始化方法,实现Servlet接口
    @Override
    public void init(ServletConfig config) throws ServletException {
        this.config = config;
        this.init();
    }
    // 自有的方法,方便子类继承使用。当init(ServletConfig config)方法执行时会自动调用此方法。
    public void init() throws ServletException {}
    // 日志打印,实现ServletContext接口
    public void log(String message) {
        getServletContext().log(getServletName() + ": " + message);
    }
    // 日志打印,实现ServletContext接口
    public void log(String message, Throwable t) {
        getServletContext().log(getServletName() + ": " + message, t);
    }
    // 最主要的方法,接收用户请求,处理后返回。实现Servlet接口
    @Override
    public abstract void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException;
    // 获取当前webapp的根路径,实现ServletContext接口
    @Override
    public String getServletName() {
        return config.getServletName();
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值