DIYTomcat

项目结构

 

conf:放置tomcat配置文件

jre8:java运行环境,使能跨平台运行

lib:第三方jar包,以及diytomcat.jar(diytomcat项目中自己写的类打成的jar包)

webapps:放置web应用程序

work:放置JSP生成的Servlet的class文件

bootstrap.jar:仅包含cn/how2j/diytomcat/Bootstrap.class

cn/how2j/diytomcat/classloader/CommonClassLoader.class

(另:Bootstrap和CommonClassLoader代码中都声明了package cn.how2j...;)

startup.bat:即执行jre8/bin/java -cp bootstrap.jar cn.how2j.diytomcat.Bootstrap命令

 

服务器启动

1. 执行java  -cp  bootstrap.jar  cn.how2j.diytomcat.Bootstrap

让虚拟机执行Bootstrap.class,并将bootstrap.jar添加进classpath路径中,使得Bootstrap.class能被应用类加载器找到并装载后开始执行。

执行过程中发现需要Method类和CommonClassLoader类。Method类会被根类加载器在jre8/lib/rt.jar中找到并装载,CommonClassLoader类会被应用类加载器在bootstrap.jar中找到并装载。

2. 执行Bootstrap

使用CommonClassLoader类装载器装载Server类,该类装载器会去lib目录的所有jar包中寻找Server,并通过反射实例化Server对象并调用其start方法,启动服务器。

(将CommonClassLoader设为当前线程的ContextClassLoader。在后续该线程的执行过程中,所有类的加载,都会调用ContextClassLoader的loadClass方法来进入双亲委派模型中)

 

实例化Server对象

 

 

Context(实现了ServletContext接口)构造函数:

1. 将当前Context所对应的文件夹的路径webAppPath存入成员变量(路径的最后一级,即文件夹名,即当前Context的名字,即该web应用程序的名字)

2. 从webAppPath/WEB-INF/lib/web.xml中解析:

Map<url_pattern,Servlet类名>  (一个url_pattern只能对应一个Servlet类)

Map<Servlet类名,Map<servlet_init_param_name, servlet_init_param_value> >,

Map<url_pattern,List<Filter类名>>  (List<Filter类名>中存的是url_pattern相同的filter)

Map<Filter类名,Map<filter_init_param_name, filter_init_param_value> >,

所有配置了load-on-start-up的Servlet的类名,放进List中。

存入成员变量

3. 创建WebappClassLoader实例,让当前线程的ContextClassLoader,即CommonClassLoader对象作为其父,存入成员变量。

(WebappClassLoader会去webAppPath/WEB-INF/classes和webAppPath/WEB-INF/lib下搜索要求它加载的类的class文件)

4. 解析出web.xml的listener标签中配置的监听器的类名,使用WebappClassLoader去装载这些监听器类,并创建其实例,放进List中,存入成员变量。

(我们只实现了对Context的监听,因此默认所配置的监听器都是ServletContextListener的实现类)

5. 维护一个Map<String, Object> attributesMap,并实现接口中关于attribute的相关get/set方法,用以提供给javaweb程序员使用Context范围的attribute。

6. 使用WebappClassLoader获取所有需要load-on-start-up的Servlet的Class对象,调用getServlet方法,来将这些Servlet放进servletPool中,并使这些Servlet的init方法在未被客户端访问之前就被提前执行。

 

插:diytomcat中保证Servlet单例的方式

依靠Context的

public synchronized HttpServlet getServlet(Class<?> clazz)成员方法和

Map<Class<?>, HttpServlet> servletPool成员变量

 

getServlet方法接收Servlet类的Class对象,返回该Servlet类的唯一实例

servletPool变量的key是Servlet类的Class对象,value是该Class对象反射出的Servlet实例

(value中的Servlet对象,就是key中Servlet类的唯一实例)

 

getServlet方法:

查看clazz是否在servletPool中,若存在则直接返回,不存在则:

--通过反射创建出该clazz的实例,即Servlet对象

--调用Servlet对象的init(ServletConfig sc)方法,

将Map<servlet_init_param_name, servlet_init_param_value>封装成ServletConfig传入

--将Servlet对象放入servletPool中

--返回Servlet实例

(若不加synchronized,当多条线程调用getServlet方法,发现clazz不在servletPool后,将会重复创建Servlet实例)

 

7. 使用WebappClassLoader获取所有Filter的Class对象,调用addFilter方法,来将所有的Filter放进filterPool中,并使这些Filter的init方法被执行。

 

插:diytomcat中保证Filter单例的方式

依靠Context的

public HttpServlet addFilter(Class<?> clazz)成员方法和

Map<Class<?>, Filter> filterPool成员变量

(diytomcat中保证Filter单例的方式与Servlet完全一致,不同仅在于addFilter为非synchronized的而getServlet为synchronized的,因为该方法是私有的,不需要提供给外界使用,仅在构造Context时使用,不存在多线程的情况)

 

8. 将当前Context封装成ServletContextEvent,调用所有监听器(ServletContextListener)的contextInitialized方法。

 

9. 开启对当前Context文件夹的监听线程。当文件夹下的文件出现任何变化时(新增文件、删除文件、修改文件),都会导致该Context的Host的reload方法被调用。reload方法会调用该Context对象的stop方法(调用servletPool中每个servlet的destroy方法;调用监听器的contextDestroyed方法;关闭各种资源);将其从contextMap中移除;对该文件夹重新构造Context并放入contextMap中。

reload的调用将使得当用户修改了其应用程序中的Servlet类后,diytomcat可以无需重启,直接将更新后的Servlet类加载进来,替换掉之前的,从而实现了热加载。

 

原理:Servlet的class文件一旦被其对应的Context的WebAppClassLoader加载进方法区后,之后每次使用该WebAppClassLoader去加载该Servlet的Class时,加载器会发现之前加载过该类,便会直接返回方法区中存在的Class对象,不会去重新加载。因此,即使该Servlet的class文件发生了修改,虚拟机也是无法察觉的。

而通过上述步骤,当class文件被修改后,将会触发监听器,对该文件夹重新构造Context对象。那么在下次加载该Servlet时,获得到该Servlet对应的Context将是一个新的Context对象,从该Context中获取到的WebAppClassLoader对象也是不同的。这个新的WebAppClassLoader因为未曾加载过该Servlet,所以它将会去读取该Servlet的class文件来生成Class对象,从而使得修改后的Servlet被加载进了虚拟机中(此时方法区中有两个该Servlet类的Class对象,只不过旧的那个将不会被使用了,在full gc时会被回收)。

 

注:当多个事件连续被触发时,监视器中的方法将会被串行的执行(如连续创建两个文件删除两个文件:执行第一次OnCreate,执行完后,再执行第二次OnCreate,执行第一次OnDelete,执行第二次OnDelete)。

而这样将会导致reload方法被多次调用,重复让Host将当前Context卸载掉,并装上新的Context,从而造成错误(事实上,后续的Context文件夹下的变化应当由后续的新建立的Context的监听线程去响应)。为了避免reload的多次调用,可以设置一个标志变量,当reload被调用一次后,就将标志位置false,使得后续直接return,不重复调用reload。

 

Server对象的start方法:

 

public void run() throws IOException {
    ServerSocket ss = new ServerSocket(port);
    while(true) {
        Socket s = ss.accept();
        Runnable r = new Runnable() {
            @Override
            public void run() {
                //...
            }
        };
        ThreadPoolUtil.run(r); //线程池
    }
}

每个Connector线程都会在各自的端口号上进行监听。每当接收到请求,都会从线程池中取出一个线程,交由该线程去处理请求,主线程则继续回到accept处进行监听。

 

请求处理过程

1. 创建Request(实现了HttpServletRequest)实例

//从InputStream inputStream = socket.getInputStream()处获取请求报文

private Socket socket;

//从请求报文中解析

private String method;

//从请求报文中解析出"j2ee",再由此从Host的contextMap中取出对应的Context

private Context context;

//从请求报文中解析(为context名称后的路径)

private String uri;

//从请求报文中解析
private Map<String, String> headerMap;

//从headerMap中key为Cookie的部分解析(Cookie是servlet-api包提供的类,

通过new Cookie(String key, String value)可以构造Cookie)

private Cookie[] cookies;

//若为GET则从URL处解析;若为POST则从request body处解析。
private Map<String, String[]> parameterMap;

//Request范围的attributeMap。据此实现接口中关于attribute的相关get/set方法,提供给javaweb编程人员使用

private Map<String, Object> attributesMap;

//提供set方法,供后面SessionManager类调用注入;提供get方法,供javaweb程序员使用

private HttpSession session;

 

 

HTTP请求报文:

POST /j2ee/hello HTTP/1.1

...

Cookie: name=Gareen(cookie); JSESSIONID=0BDF8846A7DA697EF4A559F3A6769ECB;

 

username=abc&password=def

 

 

2. 创建Response(实现了HttpServletResponse)实例

构造函数部分基本啥都没做。

Response是对HTTP响应报文的封装。我们最后会根据Response对象中各成员变量的值来生成HTTP响应报文。

以下介绍其成员变量

//状态码(默认为200)

private int status;

//响应头中的Content-Type

private String contentType;

//响应头中的Set-Cookie

private List<Cookie> cookies;

//响应头

private Map<String, String> headerMap;

//响应正文

private byte[] body;

//响应正文
private PrintWriter writer;

 

报文中的响应正文部分可以通过body写入,也可以通过response.getWriter.println("..")写入。(getWriter方法是接口中的方法,用来提供给javaweb程序员使用;setBody则在diytomcat内部使用)。

PrintWriter的构造:

this.stringWriter = new StringWriter();
this.writer = new PrintWriter(stringWriter);

(println进来的String全部都存在了StringWriter中,通过调用StringWriter的toString方法可以获取其中存的所有String)

 

3. 为Request注入Session

===============================================================================

插:SessionManager类

public class SessionManager {
   private static Map<String, StandardSession>

sessionMap = new HashMap<>();
   static {
      startSessionOutdateCheckThread();
   }

}

该类维护了服务器中所有的Session,存在sessionMap中。sessionMap的key为sessionId,value为StandardSession(实现了HttpSession。其中记录了该Session的sessionId;该Session的所有键值对Map<String, Object>;该Session的lastAccessTime和maxInactiveInterval)。key对应于某个客户端用户,value对应于该客户端在服务端中存储的所有键值对。

startSessionOutdateCheckThread方法将会启动一个线程。该线程每过一段时间会遍历一遍sessionMap,用系统的当前时间减去每个StandardSession中记录的lastAccessTime,若差值超过了StandardSession中的maxInactiveInterval,则从sessionMap中删除该Session。

这里将startSessionOutdateCheckThread放在static中的原因:只有当SessionManager类被主动使用到时,才会触发类的初始化过程,执行static中的代码;初始化过程只会执行一次且Java会对初始化过程自动进行加锁,保证了startSessionOutdateCheckThread只会被执行一次。

===============================================================================

调用SessionManager的getSession静态方法,获得StandardSession对象,设置进Request中供javaweb程序员使用:

从Request处查看,客户端是否有传来key为JSESSIONID的Cookie。

若没有,或不能从sessionMap中找到(可能过期被删了)。创建一个新的StandardSession,设置其sessionId(随机生成的独一无二的ID),lastAccessTime和maxInactiveInterval;添加进sessionMap中;

若有,且也能从sessionMap中找到。取出对应的StandardSession;更新其lastAccessTime;

 

创建一个key为"JSESSIONID",value为sessionId的Cookie,设置该Cookie的maxAge(即StandardSession的maxInactiveInterval)和path,添加到Response对象中。

 

添加进Response中的Cookie最后会被转换成如下所示的响应头:

 

Expires:浏览器会在到了Expires所示的时间时删掉该Cookie。

Path:当浏览器访问/javaweb或/javaweb/..时,会在请求报文中带上该Cookie。

 

4. 准备责任链

 

4.1. 获取Filters

--从Request中获取Context对象

--从Context对象中的Map<url_pattern,List<Filter类名>>中获取到所有匹配当前Request的uri的Filter的类名

--使用Context对象中的WebAppClassLoader获取到这些Filter类的Class对象

--根据Filter的Class对象从filterPool中取出Filter实例

 

4.2. 获取workingServlet

有三种workingServlet,分别为DefaultServlet(负责处理访问静态文件的请求)、InvokerServlet(负责处理访问Servlet的请求)、JspServlet(负责处理访问jsp的请求)。它们都实现了Servlet接口,且都实现为了懒汉式单例模式。

根据Request的uri选择相应的workingServlet。若Request的uri可以在该Request的Context中的Map<url_pattern,Servlet类名>中找到,则获取InvokerServlet的唯一实例;若Request的uri结尾为.jsp,则获取JspServlet的唯一实例;其余情况,则获取DefaultServlet的唯一实例。

 

 

5. 执行责任链

责任链模式由ApplicationFilterChain类实现(其实现了FilterChain接口,该接口仅doFilter一个方法)。

public class ApplicationFilterChain implements FilterChain{
   
   private Filter[] filters;
   private Servlet servlet;
   int pos;//下一个要执行的Filter在filters数组中的位置
   
   public ApplicationFilterChain(Filter[] filters,Servlet servlet){
      this.filters = filters;
      this.servlet = servlet;
   }
   
   @Override
   public void doFilter(ServletRequest request, ServletResponse response) throwsIOException, ServletException {
      if(pos < filters.length) {
          Filter filter= filters[pos];
          pos = pos + 1;

//调用Filter对象的doFilter方法。

//Filter对象的doFilter方法中会调用FilterChain的doFilter方法,

来使责任链上的传递得以进行。
          filter.doFilter(request, response, this);
      } else {
         servlet.service(request, response);
      }
   }
}

 

filters中每个Filter对象的doFilter方法都会被一一调用。执行完所有filters的doFilter方法后,将会执行workingServlet中的service方法。

 

调用FilterChain的doFilter方法,开启责任链的传递。

 

 

6. 执行workingServlet中的service方法

 

6.1. JspServlet中的service方法

===============================================================================插:JspClassLoaderManager类

public class JspClassLoaderManager {
    private static Map<String, JspClassLoader> map = new HashMap<>();


    public static synchronized JspClassLoader getJspClassLoader(String key) {...}
    public static void invalidJspClassLoader(String key) {...}

}

--该类维护了一个Map。key为context名+uri(如j2ee/xxx/hello.jsp);value为JspClassLoader。

--getJspClassLoader方法会根据key从Map中取出对应的JspClassLoader,若不存在,则会新建一个JspClassLoader放进map后返回。

--invalidJspClassLoader方法则从Map中删除对应的JspClassLoader

 

JspClassLoader会去diytomcat/work/context_name下搜索要求它加载的类的class文件。

 

每个jsp都有专属于自己的JspClassLoader,用以实现Jsp生成的servlet的热加载。

原理:和普通servlet的热加载原理是一样的(重新加载一个类的原理即,使用一个新的类加载器去加载这个类)。

关于为什么每个jsp都对应一个ClassLoader,而不是每个context下的所有jsp都对应一个ClassLoader(不知道..)。我认为它们的区别仅在于:a).每个context下的所有jsp都对应一个ClassLoader。当任意一个jsp发生了改动,需要替换掉该jsp对应的ClassLoader以实现热加载。同时,因为该ClassLoader又是context下其它jsp的类加载器,则ClassLoader的替换会导致其它没被改动过的jsp也需要被重新加载。b).每个jsp对应于一个ClassLoader。任意一个jsp的改动,不会影响到其它任何jsp。

===============================================================================
插:compileJsp方法(该方法直接使用的tomcat中的代码,并未自己实现)

以访问j2ee/xxx/hello.jsp为例,

该方法会在work/context_name/org/apache/jsp/xxx下生成两个文件:

hello_jsp.java(由jsp文件转化生成的servlet类)

hello_jsp.class(生成的servlet类的class文件)

===============================================================================

--查看访问的jsp文件是否存在(从Request中获取到要访问的jsp在服务端硬盘中的绝对路径),若不存在则设置Response对象中的status为404。

--查看访问的jsp文件是否已经生成了对应的class文件,若该文件不存在,则调用compileJsp方法生成。

--对比jsp文件和其生成的class文件的修改日期。若jsp的修改日期晚于其class文件的修改日期,表示该jsp文件经过了修改,则调用compileJsp方法重新生成其class文件,且从JspClassLoaderManager的Map中删除该jsp的JspClassLoader。

(现在为止已经确保了访问的jsp的class文件是存在的且是最新的)

 

--从JspClassLoaderManager中取出该jsp的ClassLoader,

--使用ClassLoader加载该jsp的class,获取到该jsp的Class对象

--使用该Class对象作为key去Context中取出Servlet实例

--调用Servlet的service方法

 

6.2. InvokerServlet中的service方法

--从Request中获取Context对象

--从Context对象中的Map<url_pattern,List<Servlet类名>>中获取到当前uri访问的Servlet类名

--使用Context对象中的WebAppClassLoader获取到Servlet类的Class对象

--根据Servlet的Class对象从servletPool中取出Servlet实例

--调用Servlet的service方法

 

6.3. DefaultServlet中的service方法

--查看访问的静态文件是否存在(从Request中获取到要访问的静态文件在服务端硬盘中的绝对路径),若不存在则设置Response对象中的status为404。

--获取静态文件的后缀名(如.exe),在配置文件中查找该后缀名对应的mimeType名

--在response的headerMap中增加一个key为Content-Type的响应头,设置其值为该mimeType

--将静态文件读取成byte数组,设置进response的body中。

 

7. 若处理请求的过程中,catch到任何异常,则设置Response对象的status为500

 

8. 根据Response对象生成HTTP响应报文,并将报文转化为byte数组,写进Request的Socket对象的outputStream中。

 

 

另:

 

request.sendRedirect(String redirectUri)的实现:

若javaweb程序员在其servlet中调用了request.sendRedirect(String redirectUri)方法,则Response的headerMap中将会被添加一个key为Location,value为redirectUri的响应头,status会被设置为302。浏览器看到该信息便会重新发起对redirectUri的访问。

 

request.getRequestDispatcher.forward(String forwardUri)的实现

===============================================================================

插:ApplicationRequestDispatcher类(实现了RequestDispatcher接口)

该类通过传递一个String forwardUri进行构造,其forward(request, response)方法实现了请求转发的功能。forward方法中将request中的uri重新设置成forwardUri后,回到请求处理的第三步重新开始该请求的处理。

===============================================================================

Request的getRequestDispatcher方法将会返回使用forwardUri构造的

ApplicationRequestDispatcher实例。调用其forward方法,实现请求转发。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值