项目结构
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方法,实现请求转发。