淺談JSP是如何編譯成servlet並提供服務的
目錄
- 概述
- 原始碼分析
概述
服務端對外提供P請求服務的是JspServlet,繼承自HttpServlet。核心服務入口在service方法,大體流程如下:
- 首先獲取請求的jspUri,如果客戶端發起請求:https://xxx.xx.com/jsp/test.jsp,那麼獲取到的jspUri為:/jsp/test.jsp
- 然後檢視快取(Map結構)中是否包含該jspUri的JspServletWrapper,如果沒有就需要建立一個JspServletWrapper並且快取起來,並呼叫JspServletWrapper的service方法
- 如果為development模式,或者首次請求,那麼就需要執行JspCompilationContext.compile() 方法
- 在JspCompilationContext.compile方法中,會根據jsp檔案的lastModified判斷檔案是否已經被更新(out dated),如果被更新過了,就需要刪除之前生成的相關檔案,然後將jspLoader置空(後面需要載入的時候如果jspLoader為空,就會建立一個新的jspLoader),呼叫Compiler.compile方法生成servlet,設定reload為true(預設為true),後面會根據reload引數判斷是否需要重新載入該servlet
- 呼叫JspServletWrapper.getServlet方法獲取最終提供服務的servlet,這個過程會根據reload引數看是否需要過載servlet,如果需要過載,那麼就會獲取jspLoader例項化一個新的servlet(如果前面發現jsp檔案過期,那麼此時獲取的jspLoader為空,則會建立一個新的jspLoader),並且設定reload為false
- 呼叫servlet的service方法提供服務,如果servlet實現了SingleThreadModel介面,那麼會用synchronized做同步控制
原始碼分析
首先看JspServlet的核心邏輯,主要是獲取jspUri和獲取JspServletWrapper,分別是入口service方法和serviceJspFile方法,程式碼如下(部分):
/** * 獲取jspUri,然後呼叫serviceJspFile方法 */ public void service(HttpServletRequest request,HttpServletResponse response) throws ServletException,IOException { String jspUri = this.jspFile; String pathInfo; if (jspUri == null) { pathInfo = (String)request.getAttribute(Constants.JSP_FILE); if (pathInfo != null) { jspUri = pathInfo; request.removeAttribute(Constants.JSP_FILE); } } if (jspUri == null) { jspUri = (String)request.getAttribute("x.servlet.include.servlet_path"); if (jspUri != null) { pathInfo = (String)request.getAttribute("javax.servlet.include.path_info"); if (pathInfo != null) { jspUri = jspUri + pathInfo; } } else { jspUri = request.getServletPath(); pathInfo = request.getPathInfo(); if (pathInfo != null) { jspUri = jspUri + pathInfo; } } } boolean precompile = this.preCompile(request); this.serviceJspFile(request,response,jspUri,precompile); } /** * 主要獲取JspServletWrapper,然後呼叫JspServletWrapper.service方法 */ private void serviceJspFile(HttpServletRequest request,HttpServletResponse response,String jspUri,boolean precompile) throws ServletException,IOException { JspServletWrapper wrapper = this.rctxt.getWrapper(jspUri); if (wrapper == null) { synchronized(this) { wrappewww.cppcns.comr = this.rctxt.getWrapper(jspUri); if (wrapper == null) { if (null == this.context.getResource(jspUri)) { this.handleMissingResource(request,jspUri); return; } wrapper = new JspServletWrapper(this.config,this.options,this.rctxt); this.rctxt.addWrapper(jspUri,wrapper); } } } try { //核心服務方法 wrapper.service(request,precompile); } catch (FileNotFoundException var8) { this.handleMissingResource(request,jspUri); } }
然後進入JspServletWrapper.service方法(部分程式碼):
//注意這個reload是volatile修飾的 private volatile boolean reload = true; public void service(HttpServletRequest request,IOException,FileNotFoundException { Servlet servlet; try { if (this.ctxt.isRemoved()) { throw new FileNotFoundException(this.jspUri); } //判斷development模式和firstTime(首次請求) if (!this.options.getDevelopment() && !this.firstTime) { if (this.compileException != null) { throw this.compileException; } } else { synchronized (this) { this.firstTime = false; //呼叫JspCompilationContext.compile方法 this.ctxt.compile(); } } //獲取最終提供服務的servlet servlet = this.getServlet(); if (precompile) { return; } } try { //根據是否實現SingleThreadModel決定是否需要做同步控制 if (servlet instanceof SingleThreadModel) { synchronized (this) { servlet.service(request,response); } } else { servlet.service(request,response); } } }
這裡主要看JspCompilationContext.complie方法:
public void compile() throws JasperException,FileNotFoundException { this.createCompiler(); if (this.jspCompiler.isOutDated()) { if (this.isRemoved()) { throw new FileNotFoundException(this.jspUri); } try { //清楚檔案資料 this.jspCompiler.removeGeneratedFiles(); //置空jspLoader,現在置null,後面就會建立一個新的JspLoader this.jspLoader = null; //根據jsp生成servlet的邏輯,實現主要有AntCompiler和JDTCompiler,預設JDTCompiler this.jspCompiler.compile(); //設定reload為true,後面根據reload引數判斷是否需要重新載入 this.jsw.setReload(true); this.jsw.setCompilationException((JasperException) null); } } }
要注意對於isOutDated方法的判斷,並不是簡單地每次請求都檢查jsp檔案是否更新,而是有一個間隔時間,如果此次檢查更新的時間在上一次檢查更新+間隔時間之內,也就是沒有超過間隔時間,那麼就不會去檢查jsp檔案的更新。這就是我們說的jsp熱更新延時生效,isOutDated是Compiler的方法,如下(部分程式碼):
public boolean isOutDated(boolean checkClass) { if (this.jsw != null && this.ctxt.getOptions().getModificationTestInterval() > 0) { //getModificationTestInterval就是檢查最短間隔時間,單位為秒 if (this.jsw.getLastModificationTest() + (long)(this.ctxt.getOptions().getModificationTestInterval() * 1000) > System.currentTimeMillis()) { return false; } this.jsw.setLastModificationTest(System.currentTimeMillis()); } Long jspRealLastModified = this.ctxt.getLastModified(this.ctxt.getJspFile()); if (jspRealLastModified < 0L) { return true; } else { long targetLastModified = 0L; File targetFile; if (checkClass) { targetFile = new File(this.ctxt.getClassFileName()); }HcpifIo else { targetFile = new File(this.ctxt.getServletJavaFileName()); } if (!targetFile.exists()) { return true; } else { targetLastModified = targetFile.lastModified(); if (checkClass && this.jsw != null) { this.jsw.setServletClassLastModifiedTime(targetLastModified); } if (targetLastModified != jspRealLastModified) { if (this.log.isDebugEnabled()) { this.log.debug("Compiler: outdated: " + targetFile + " " + targetLastModified); } return true; } else if (this.jsw == null) { return false; } } HcpifIo }
另外,這裡還涉及到JSP的編譯工作,編譯工作主要由org.apache.jasper.compiler.Compiler編譯器負責,Compiler是一個抽象類,apache-jsp中提供了兩種實現:AntCompiler和JDTCompiler,預設使用的編譯器為JDTCompiler。
最後回到JspServletWrapper.getServlet方法:
private volatile boolean reload = true; public Servlet getServlet() throws ServletException { //reload是被volatile修飾的一個boolean變數 //這裡進行雙重檢測 if (this.reload) { synchronized (this) { if (this.reload) { //需要過載 this.destroy(); Servlet servlet; try { InstanceManager instanceManager = InstanceManagerFactory.getInstanceManager(this.config); //建立一個新的serlvet例項物件,注意這裡的getJspLoader方法 servlet = (Servlet) instanceManager.newInstance(this.ctxt.getFQCN(),this.ctxt.getJspLoader()); } catch (Exception var6) { Throwable t = ExceptionUtils.unwrapInvocationTargetException(var6); ExceptionUtils.handleThrowable(t); throw new JasperException(t); } servlet.init(this.config); if (!this.firstTime) { this.ctxt.getRuntimeContext().incrementJspReloadCount(); } this.theServlet = servlet; this.reload = false; } } } return this.theServlet; }
可以看到,方法中使用了雙重檢測機制判斷是否需要過載,reload引數由volatile修飾保證可見性。在建立新的servlet例項的時候,classLoader是通過JspCompilationContext.getJspLoader方法獲取的,看看這個方法的邏輯:
public ClassLoader getJspLoader() { if (this.jspLoader == null) { this.jspLoader = new JasperLoader(new URL[]{this.baseUrl},this.getClassLoader(),this.rctxt.getPermissionCollection()); } return this.jspLoader; }
在前面JspCompilationContext.compHcpifIolie的邏輯中,如果檢測到jsp檔案被更新過(過期),那麼jspLoader會被設定為null,此時就會建立一個新的jspLoader(JasperLoader),然後使用新的loader載入新的servlet,以完成jsp的熱更新,老的classloader在之後會被GC直接回收。
到此這篇關於淺談JSP是如何編譯成servlet並提供服務的的文章就介紹到這了,更多相關JSP編譯成servlet內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!