jsp(java server page),其根本是一个简化的Servlet技术,是一种动态网页技术标准。
它是在传统的网页HTML页面中插入java代码段,从而形成jsp文件,后缀为.jsp。
jsp同Servlet一样,是在服务端执行,通常返回给客户端的是一个HTML文件。
这种动态网页技术,主要目的是将逻辑从Servlet中分离,jsp侧重于显示
2.Jsp处理方式
上文说了,Jsp本质就是Servlet,所以java处理Jsp的方式基本同Servlet一样。
java是一门编译型语言,因为应用服务器(tomcat等)首先需要将Jsp页面转换为一个标准java类文件,然后进行编译、加载并实例化。
编译后的java类是一个Servlet实现,负责将我们在jsp页面中编写的内容输出到客户端
1)Jsp页面采用单独的类加载器
因此重新编译不会导致整个应用重新加载,这也是我们可以在运行状态更新Jsp页面的原因
2)提升性能方式
应用服务器会对Jsp类和实例进行缓存,并定时检测Jsp页面的更新情况,如发生变更,将会重新编译
3.Jsp编译(运行时编译)
所谓运行时编译:就是tomcat并不会再启动web应用时自动编译Jsp文件,而是在客户端第一次请求时才编译需要访问的Jsp文件
编译过程分为:
1)获取Jsp文件路径
默认将HttpServletRequest.getServletPath+HttpServletRequest.getPathInfo作为jsp路径
注意:还有其他两种方式,下面会通过源码来分析
2)根据Jsp文件构造JspServletWrapper文件
JspServletWrapper为Jsp引擎的核心,它负责编译、加载Jsp文件并完成请求处理。每个Jsp页面对应一个JspServletWrapper实例。Tomcat会缓存JspServletWrapper对象以提升系统性能
3)调用Servlet的方法完成请求处理
JspServletWrapper判断当前是否首次加载,如果是,则进行编译;如果不是,则直接调用Servlet的方法进行业务处理
4)编译结果处理
通常默认情况下,会存放在%CATALINA_HOME%/work/Engine/Host(一般为localhost)/Context(应用名称)目录下
当然用户也可以通过配置的方式来自定义目录:
// 配置scratchdir ,该参数在默认的Server项目中web.xml中可以找到
scratchdir
web-app/tm/jsp/
4.通过源码来分析一下上述Jsp编译的过程
Jsp本质上就是Servlet
我们创建的是一个.jsp文件,但应用服务器真正使用的是一个Servlet类,是一个.java文件,那么在这个过程中究竟发生了什么呢?
首先有一个默认的知识点:tomcat在默认的web.xml中配置了一个org.apache.jasper.servlet.JspServlet,用于处理所有.jsp或者.jspx结尾的请求,该Servlet实现即为运行时编译的入口。
下面我们就来看下这个类
5.默认web.xml的观察
1)创建SpringMVC项目
笔者创建了一个SpringMVC项目,具体过程不表
然后创建一个Controller类,请求路径为/mvc/hello,返回hello,指向一个jsp文件(hello.jsp),同时在src/main/webapp/WEB-INF/jsp/下创建hello.jsp。
在当前IDE关联tomcat,并将该web项目(命名为springweb)添加到tomcat中。
我们可以在IDE中看到一个Server项目,这个是自动创建的,如下所示
2)观察web.xml文件
该文件是tomcat的默认web.xml,我们来看下其主要的几个项
* DefaultServlet(默认的Servlet,当请求找不到mapping时,就会转发到这)
default
org.apache.catalina.servlets.DefaultServlet
debug
0
listings
false
1
注意:读者可以仔细阅读一下相关源码,可以发现,里面基本做了所有的异常处理,403、404...
* JspServlet(处理.jsp)
jsp
org.apache.jasper.servlet.JspServlet
fork
false
xpoweredBy
false
3
* welcome-list(默认的欢迎页面)
index.html
index.htm
index.jsp
6.org.apache.jasper.servlet.JspServlet源码分析
1)类结构
// The JSP engine (a.k.a Jasper)
public class JspServlet extends HttpServlet implements PeriodicEventListener {
可以看到,JspServlet本质上也是一个Servlet,也符合Servlet的一系列使用规范。
通过上面默认web.xml的分析可以看到,应用服务器启动时就会加载该类,并调用其init方法
2)JspServlet.service()方法
主要的业务处理都在这,我们重点来看下这个方法
@Override
public void service (HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1.jspFile可以通过配置中的init-param来构建(一般来说,我们不配置这个字段)
String jspUri = jspFile;
if (jspUri == null) {
// 2.判断请求中的javax.servlet.include.servlet_path属性是否为空,不为空则设置为jspUri(一般来说,不配置该字段)
jspUri = (String) request.getAttribute(
RequestDispatcher.INCLUDE_SERVLET_PATH);
if (jspUri != null) {
String pathInfo = (String) request.getAttribute(
RequestDispatcher.INCLUDE_PATH_INFO);
if (pathInfo != null) {
jspUri += pathInfo;
}
} else {
// 3.HttpServletRequest.getServletPath+HttpServletRequest.getPathInfo作为jsp路径
jspUri = request.getServletPath();
String pathInfo = request.getPathInfo();
if (pathInfo != null) {
jspUri += pathInfo;
}
}
}
// 通过上面1-3的分析,我们确认了jsp的路径
...
try {
// 4.检查是否预编译,如果没有编译过,则在serviceJSPFile方法会先编译该Jsp
boolean precompile = preCompile(request);
// 5.调用jsp对应的Servlet.service()方法
serviceJspFile(request, response, jspUri, precompile);
} catch (RuntimeException e) {
throw e;
} ...
}
3)serviceJspFile(request, response, jspUri, precompile)
private void serviceJspFile(HttpServletRequest request,
HttpServletResponse response, String jspUri,
boolean precompile)
throws ServletException, IOException {
// 1.判断是否已经加载过,没有则加载
// 加载的主要方式也就是包装一个JspServletWrapper,放入到rctxt中
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 {
// 2.业务处理
wrapper.service(request, response, precompile);
} catch (FileNotFoundException fnfe) {
handleMissingResource(request, response, jspUri);
}
}
总结:
我们将Jsp信息封装为JspServletWrapper,然后将业务处理交给JspServletWrapper处理,下面我们就来看下JspServletWrapper是如何处理的
7.org.apache.jasper.servlet.JspServletWrapper业务处理
service方法主要内容如下:
// JspServletWrapper.service(request, response, precompile)
public void service(HttpServletRequest request,
HttpServletResponse response,
boolean precompile)
throws ServletException, IOException, FileNotFoundException {
Servlet servlet;
try {
...
// 1.如果是第一次访问service访问,则需要先编译Jsp为Servlet
if (options.getDevelopment() || firstTime ) {
synchronized (this) {
firstTime = false;
ctxt.compile();
}
} else {
if (compileException != null) {
// Throw cached compilation exception
throw compileException;
}
}
// 2.获取对应的Servlet
servlet = getServlet();
} catch (ServletException ex) {
...
}
try {
// 3.对已经加载的Jsp进行处理,如果长时间不用则删除之
if (unloadAllowed) {
synchronized(this) {
if (unloadByCount) {
if (unloadHandle == null) {
unloadHandle = ctxt.getRuntimeContext().push(this);
} else if (lastUsageTime < ctxt.getRuntimeContext().getLastJspQueueUpdate()) {
ctxt.getRuntimeContext().makeYoungest(unloadHandle);
lastUsageTime = System.currentTimeMillis();
}
} else {
if (lastUsageTime < ctxt.getRuntimeContext().getLastJspQueueUpdate()) {
lastUsageTime = System.currentTimeMillis();
}
}
}
}
// 4.真正的业务处理,交由具体的Servlet
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) {
...
}
...
}
下面我们逐步来看下这几个方法
1)JspCompilationContext.compile(),创建Jsp compile,主要将Jsp转换为java类,具体过程不表
public void compile() throws JasperException, FileNotFoundException {
// 主要在这里,创建Compile,默认创建org.apache.jasper.compiler.JDTCompiler
createCompiler();
if (jspCompiler.isOutDated()) {
...
}
}
2)getServlet()获取jsp对应的Servlet
public Servlet getServlet() throws ServletException {
// 已经加载过的不会再次加载,直接返回即可
if (reload) {
synchronized (this) {
// Synchronizing on jsw enables simultaneous loading
// of different pages, but not the same page.
if (reload) {
// This is to maintain the original protocol.
destroy();
final Servlet servlet;
try {
// 1.使用InstanceManager生成对应的Servlet类
// 本例中的hello.jsp 生成 org.apache.jsp.WEB_002dINF.jsp.hello_jsp
InstanceManager instanceManager = InstanceManagerFactory.getInstanceManager(config);
servlet = (Servlet) instanceManager.newInstance(ctxt.getFQCN(), ctxt.getJspLoader());
} catch (Exception e) {
Throwable t = ExceptionUtils
.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(t);
throw new JasperException(t);
}
// 2.调用servlet.init方法初始化
servlet.init(config);
if (!firstTime) {
ctxt.getRuntimeContext().incrementJspReloadCount();
}
theServlet = servlet;
reload = false;
// Volatile 'reload' forces in order write of 'theServlet' and new servlet object
}
}
}
return theServlet;
}
3)servlet.service(request, response)到这里就将请求转发给特定的Servlet去处理了
总结:
最终tomcat编译器将hello.jsp编译成了hello_jsp.java,该类继承了HttpServlet。
所以,正验证了开头我们说的:Jsp本质上就是Servlet
8.hello_jsp.java展示
最后我们来展示一下hello.jsp以及生成后的hello_jsp.java类
1)hello.jsp
九九乘法表
请输入两个自然数给您打印乘法表
要求:startNumber <endNumber
startNumber:
endNumber
2)hello_jsp.java(目录为%CATALINA_HOME%\work\Catalina\localhost\springweb\org\apache\jsp\WEB_002dINF\jsp)
/*
* Generated by the Jasper component of Apache Tomcat
* Version: Apache Tomcat/8.5.31
* Generated at: 2018-11-28 01:27:32 UTC
* Note: The last modified time of this file was set to
* the last modified time of the source file after
* generation to assist with modification tracking.
*/
package org.apache.jsp.WEB_002dINF.jsp;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
public final class hello_jsp extends org.apache.jasper.runtime.HttpJspBase
implements org.apache.jasper.runtime.JspSourceDependent,
org.apache.jasper.runtime.JspSourceImports {
private static final javax.servlet.jsp.JspFactory _jspxFactory =
javax.servlet.jsp.JspFactory.getDefaultFactory();
private static java.util.Map _jspx_dependants;
private static final java.util.Set _jspx_imports_packages;
private static final java.util.Set _jspx_imports_classes;
static {
_jspx_imports_packages = new java.util.HashSet();
_jspx_imports_packages.add("javax.servlet");
_jspx_imports_packages.add("javax.servlet.http");
_jspx_imports_packages.add("javax.servlet.jsp");
_jspx_imports_classes = null;
}
private volatile javax.el.ExpressionFactory _el_expressionfactory;
private volatile org.apache.tomcat.InstanceManager _jsp_instancemanager;
public java.util.Map getDependants() {
return _jspx_dependants;
}
public java.util.Set getPackageImports() {
return _jspx_imports_packages;
}
public java.util.Set getClassImports() {
return _jspx_imports_classes;
}
public javax.el.ExpressionFactory _jsp_getExpressionFactory() {
if (_el_expressionfactory == null) {
synchronized (this) {
if (_el_expressionfactory == null) {
_el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory();
}
}
}
return _el_expressionfactory;
}
public org.apache.tomcat.InstanceManager _jsp_getInstanceManager() {
if (_jsp_instancemanager == null) {
synchronized (this) {
if (_jsp_instancemanager == null) {
_jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig());
}
}
}
return _jsp_instancemanager;
}
public void _jspInit() {
}
public void _jspDestroy() {
}
public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
throws java.io.IOException, javax.servlet.ServletException {
final java.lang.String _jspx_method = request.getMethod();
if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method) && !javax.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSPs only permit GET POST or HEAD");
return;
}
final javax.servlet.jsp.PageContext pageContext;
javax.servlet.http.HttpSession session = null;
final javax.servlet.ServletContext application;
final javax.servlet.ServletConfig config;
javax.servlet.jsp.JspWriter out = null;
final java.lang.Object page = this;
javax.servlet.jsp.JspWriter _jspx_out = null;
javax.servlet.jsp.PageContext _jspx_page_context = null;
try {
response.setContentType("text/html;charset=UTF-8");
pageContext = _jspxFactory.getPageContext(this, request, response,
null, true, 8192, true);
_jspx_page_context = pageContext;
application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();
_jspx_out = out;
out.write("\r\n");
out.write("\r\n");
out.write("\r\n");
out.write("\r\n");
out.write(" \r\n");
out.write(" 九九乘法表\r\n");
out.write("\r\n");
out.write("\r\n");
out.write("\r\n");
out.write("\r\n");
out.write(" 请输入两个自然数给您打印乘法表
\r\n");
out.write(" 要求:startNumber <endNumber
\r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" startNumber:\r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" endNumber\r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write(" \r\n");
out.write("\r\n");
out.write("\r\n");
out.write("");
} catch (java.lang.Throwable t) {
if (!(t instanceof javax.servlet.jsp.SkipPageException)){
out = _jspx_out;
if (out != null && out.getBufferSize() != 0)
try {
if (response.isCommitted()) {
out.flush();
} else {
out.clearBuffer();
}
} catch (java.io.IOException e) {}
if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
else throw new ServletException(t);
}
} finally {
_jspxFactory.releasePageContext(_jspx_page_context);
}
}
}
参考:Tomcat架构解析(刘光瑞)