Servlet和JSP的工作原理
Servlet是Java Web的基础,JSP是做Java Web开发时,每个开发人员都会用到的东东,但是对于这两者的工作原理以及他们之间的联系,我却一直云里雾里。
前两天项目中遇到了一个蹊跷的问题:我们的项目是以war包部署在Jetty中的,刚部署时运行良好,过了一段时间,请求war包中的有些页面时(如index.jsp),Jetty无法找到这些JSP页面了,于是直接返回404错误。当时没找出问题的原因,于是直接重启Jetty作为权宜之际。为了找出问题的根源,感觉很有必要弄清楚Jetty是如何处理Servlet、JSP、war包的。
Servlet和JSP的用法示例
先来看一下示例程序的文件结构:
运行src中的ServletServer.java会启动嵌入式的Jetty,然后接收Servlet请求;运行JspServer.java也会启动嵌入式的Jetty,然后接收JSP请求。要运行servlet,只需要引入lib/jetty目录下的jar包就可以了,要运行JSP,还需要引入lib/jsp目录下的jar包。
ServletServer.java代码如下:
1 public class ServletServer { 2 public static void main(String[] args) throws Exception { 3 Server server = new Server(8080); 4 ServletHandler handler = new ServletHandler(); 5 server.addHandler(handler); 6 ServletHolder holder = new ServletHolder(new FirstServlet()); 7 handler.addServletWithMapping(holder, "/first"); 8 server.start(); 9 server.join(); 10 } 11 }
它指定在8080端口启动Jetty,然后向Jetty中添加一个Servlet,该Servlet为FirstServlet.java,并且配置访问该Servlet的路径为:http://localhost:8080/first。
FirstServlet.java代码如下:
1 public class FirstServlet extends HttpServlet{ 2 3 public void doGet(HttpServletRequest request, HttpServletResponse response) 4 throws ServletException, IOException { 5 response.setContentType("text/html"); 6 response.getWriter().println("<h1>hello</h1>"); 7 } 8 9 }
这个Servlet很简单,就是向浏览器返回一串HTML代码,于是浏览器就能看到结果了,就是显示一个字符串hello。理论上说,有了Servlet我们就能进行Java Web编程了,但是直接使用Servlet实在很不方便,因为Servlet是在java代码中嵌入HTML代码,于是就有了JSP。
接下来看看JspServer.java的代码:
1 public class JspServer { 2 public static void main(String[] args) { 3 Server serverHttp = new Server(8080); 4 5 serverHttp.setSendServerVersion(false); 6 serverHttp.setSendDateHeader(false); 7 serverHttp.setStopAtShutdown(true); 8 9 WebAppContext wac = new WebAppContext(); 10 wac.setWar("test.war"); 11 12 serverHttp.setHandler(wac); 13 try { 14 serverHttp.start(); 15 } catch (Exception e) { 16 e.printStackTrace(); 17 } 18 } 19 }
它也指定在8080端口启动jetty,然后指定jetty可以处理webapps目录下的JSP文件。目前webapps目录下只有一个JSP文件,hello.jsp:
1 <h1><%="hello"%></h1>
浏览器请求http://localhost:8080/hello.jsp时得到的返回也是一个字符串hello。
比较FirstServlet.java和hello.jsp可以发现,Servlet是在java代码中嵌入HTML,而JSP是在HTML中嵌入java。为了实现java和HTML分离,还可以引入JSTL和Spring MVC等,这是后话。
Servlet的工作原理(以Jetty为例)
当我们在浏览器中请求一个Servlet时,会发生什么呢?回头看看前面ServletServer.java的代码,其中有这样一行:ServletHandler handler = new ServletHandler()。ServletHander就是用来处理Servlet请求的,它管理着Servlet的整个生命周期。Servlet的生命周期包括:初始化(init)、运行(service)和销毁(destroy),这个从javax.servlet.Servlet的接口就可以清楚的看出来。Servlet对HTTP请求的响应,是在service()方法中完成的。
示例中的FirstServlet继承自javax.servlet.http.HttpServlet,并覆盖了doGet()方法。HttpServlet的service()方法会调用doGet()方法。
下面分别来看看Servlet初始化和运行的流程。Servlet的初始化是在Jetty启动时进行的,时序图为:
Jetty启动后就可以接收并处理Servlet请求,Servlet运行的时序图如下所示:
JSP到Servlet的转换(以Jetty为例)
JSP改变了Servlet在Java代码中写HTML代码的方式,极大的方便了开发人员,但是Jetty是只能处理Servlet的,所以必然会先把JSP转变成Servlet。示例中的hello.jsp转变成Servlet后的文件名为:hello_jsp.java,大致代码如下:
1 public final class hello_jsp extends org.apache.jasper.runtime.HttpJspBase 2 implements org.apache.jasper.runtime.JspSourceDependent { 3 4 public void _jspService(HttpServletRequest request, HttpServletResponse response) 5 throws java.io.IOException, ServletException { 6 7 //局部变量定义 8 ... 9 10 try { 11 ... 12 13 //返回HTML 14 out.write("<html>\r\n"); 15 ...... 16 } catch (Throwable t) { 17 } finally { 18 } 19 } 20 }
Hello_jsp继承了抽象类HttpJspBase并实现了_jspService方法,而HttpJspBase又继承了HttpServlet,HttpJspBase的service()方法在执行时会调用_jspService()方法,这样Jetty就可以处理JSP请求了。
下面来看看Jetty是如何将JSP转变成Servlet的,Jetty处理JSP请求的时序图如下所示:
其中JspServlet和JspServletWapper位于org.apache.jasper.servlet包中,所以Jetty是调用jasper来处理JSP的。下面是JspServletWrapper.service()方法的代码片段:
1 /* 2 * (1) Compile 3 */ 4 if (options.getDevelopment() || firstTime ) { 5 synchronized (this) { 6 firstTime = false; 7 8 // The following sets reload to true, if necessary 9 ctxt.compile(); 10 } 11 } else { 12 if (compileException != null) { 13 // Throw cached compilation exception 14 throw compileException; 15 } 16 } 17 18 /* 19 * (2) (Re)load servlet class file 20 */ 21 getServlet(); 22 23 // If a page is to be precompiled only, return. 24 if (precompile) { 25 return; 26 } 27 28 /* 29 * (3) Service request 30 */ 31 if (theServlet instanceof SingleThreadModel) { 32 // sync on the wrapper so that the freshness 33 // of the page is determined right before servicing 34 synchronized (this) { 35 theServlet.service(request, response); 36 } 37 } else { 38 theServlet.service(request, response); 39 }
从上述代码可以清楚的看到JSP的处理流程:先将JSP编译成Servlet,再通过反射机制获取这个Servlet的实例,最后调用这个Servlet的service方法。
至此,Servlet和JSP的工作流程就很清楚了,现在回过头来看看文章开始处提到的问题:为什么war包中的jsp文件找不到了?Jetty对war包的处理很简单,在Jetty启动时,将war解压出来(文件名随机生成,类似于:Jetty_0_0_0_0_8080_test.war____.hcx133),放在JETTY_HOME的work目录下,如果JETTY_HOME没有work目录,就会放在系统的temp目录下。所以对于war中本来存在的JSP文件,Jetty包404错误,原因应该就是解压出来的文件被删除了。
[后记]问题重现了,果然是war包解压出来的文件被删了,linux会自动删除/tmp内超过10天没有更新的文件。