当我们请求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类装载入内存,进行响应客户端的工作。