当我们请求jsp页面时,tomcat将请求交由JspServlet来处理,servlet处理请求的方法为service方法,源代码如下(源码太长,这里只保留关键代码)
public void service (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ... try { boolean precompile = preCompile(request); serviceJspFile(request, response, jspUri, precompile); } catch (RuntimeException e) { ... } }preCompile(request)方法的作用就是检查请求jsp页面时有没有带?jsp_precompile查询字符串,然后将检查结果precompile传入serviceJspFile方法(其作用见后面讲解),而对jsp页面的处理主要是通过该方法来实现的,源代码如下。
private void serviceJspFile(HttpServletRequest request, HttpServletResponse response, String jspUri, boolean precompile) throws ServletException, IOException { JspServletWrapper wrapper = rctxt.getWrapper(jspUri); if (wrapper == null) { synchronized(this) { wrapper = rctxt.getWrapper(jspUri); if (wrapper == null) { // Check if the requested JSP page exists, to avoid // creating unnecessary directories and files. if (null == context.getResource(jspUri)) { handleMissingResource(request, response, jspUri); return; } wrapper = new JspServletWrapper(config, options, jspUri, rctxt); rctxt.addWrapper(jspUri,wrapper); } } } try { wrapper.service(request, response, precompile); } catch (FileNotFoundException fnfe) { handleMissingResource(request, response, jspUri); } }
它的主要作用就是检查JspRuntimeContext中是否已经存在与当前jsp文件相对应的JspServletWrapper(如果存在的话,说明这个文件之前已经被访问过了)。有的话就取出来,没有则检查对应的jsp文件是否存在,如果存在的话就新创建一个 JspServletWrapper并添加到JspRuntimeContext中去。随后会进入JspServletWrapper的service方法,源代码如下(同样,保留关键代码)
public void service(HttpServletRequest request, HttpServletResponse response, boolean precompile) throws ServletException, IOException, FileNotFoundException { Servlet servlet; try { ... /* * (1) Compile */ if (options.getDevelopment() || firstTime ) { synchronized (this) { firstTime = false; // The following sets reload to true, if necessary ctxt.compile(); } } else { if (compileException != null) { // Throw cached compilation exception throw compileException; } } /* * (2) (Re)load servlet class file */ servlet = getServlet(); // If a page is to be precompiled only, return. if (precompile) { return; } } catch (ServletException ex) { ... } /* * (4) Service request */ if (servlet instanceof SingleThreadModel) { // sync on the wrapper so that the freshness // of the page is determined right before servicing synchronized (this) { servlet.service(request, response); } } else { servlet.service(request, response); } } catch (UnavailableException ex) { ... } }
主要流程就是编译jsp,若是precompile为真,即请求中带有?jsp_precompile查询字符串,则直接返回,否则进入编译出来的 jsp的service方法,开始执行jsp内的代码。判断是否要对该jsp页面进行编译的关键代码为
if (options.getDevelopment() || firstTime ) { synchronized (this) { firstTime = false; // The following sets reload to true, if necessary ctxt.compile(); } }
这段代码先判断tomcat是否处于development模式中,或者当前jsp是不是第一次被访问。如果是,则会进入
ctxt.compile();ctxt是JspCompilationContext的对象,该对象内封装了与编译jsp相关的所有信息,每一个JspServletWrapper里面都有一个自己的 JspCompilationContext。也就是在compile方法里面,对jsp文件的时间戳以及编译条件进行了判断。
public void compile() throws JasperException, FileNotFoundException { createCompiler(); if (jspCompiler.isOutDated()) { if (isRemoved()) { throw new FileNotFoundException(jspUri); } try { jspCompiler.removeGeneratedFiles(); jspLoader = null; jspCompiler.compile(); jsw.setReload(true); jsw.setCompilationException(null); } catch (JasperException ex) { ... } } }
JspCompilationContext对象内有一个Compile对象jspCompiler,用它来对jsp进行更新检查以及编译。jspCompile.isOutDated方法代码如下:
public boolean isOutDated() { return isOutDated(true); }isOutDated的重载方法如下:
public boolean isOutDated(boolean checkClass) { if (jsw != null && (ctxt.getOptions().getModificationTestInterval() > 0)) { if (jsw.getLastModificationTest() + (ctxt.getOptions().getModificationTestInterval() * 1000) > System .currentTimeMillis()) { return false; } jsw.setLastModificationTest(System.currentTimeMillis()); } Long jspRealLastModified = ctxt.getLastModified(ctxt.getJspFile()); if (jspRealLastModified.longValue() < 0) { // Something went wrong - assume modification return true; } ... }
我们可以看到通过getModificationTestInterval()方法来获取tomcat中web.xml设置的modificationTestInterval参数,来检测是否需要对jsp页面进行重新编译,并通过jsw.setLastModificationTest()方法来记录此次的编译时间,用以下次对jsp的编译条件进行判断。若距离上次编译时间超过了modificationTestInterval的值,则通过jspCompiler的compile()方法来对jsp文件进行编译。
好了,上述就是tomcat在开发模式下,对jsp页面时间戳的检测与编译时机的确定方式,那在生产模式是怎么确定的呢?
我们知道,生产模式下,tomcat会用后台编译的方式来对jsp进行编译,后台编译程序会调用JspRuntimeContext对象的checkCompile()方法来对jsp页面进行检测,源代码如下:
public void checkCompile() { if (lastCompileCheck < 0) { // Checking was disabled return; } long now = System.currentTimeMillis(); if (now > (lastCompileCheck + (options.getCheckInterval() * 1000L))) { lastCompileCheck = now; } else { return; } Object [] wrappers = jsps.values().toArray(); for (int i = 0; i < wrappers.length; i++ ) { JspServletWrapper jsw = (JspServletWrapper)wrappers[i]; JspCompilationContext ctxt = jsw.getJspEngineContext(); // JspServletWrapper also synchronizes on this when // it detects it has to do a reload synchronized(jsw) { try { ctxt.compile(); } catch (FileNotFoundException ex) { ctxt.incrementRemoved(); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); jsw.getServletContext().log("Background compile failed", t); } } } }
可以看到,在该方法中,通过getCheckInterval()方法来获取tomcat中web.xml设置的checkInterval参数,来检测是否需要对jsp页面进行重新编译,并更新lastCompileCheck的值,用以下次编译条件的检测。若距离上次编译时间超过了checkInterval的值,则通过ctxt.compile()方法来对jsp文件进行编译。
tomcat会在work目录里把这个jsp页面转成.java文件,比如将index.jsp转换成index_jsp.java文件,而后编译为index_jsp.class文件,最后tomcat容器通过类加载器把这个index_jsp.class类装载入内存,进行响应客户端的工作。