javaweb|Java Web基础知识之JSP(穿上马甲我照样认识你)

之前一直说的是Servlet,但是由于只是做一个Demo,并没有完全显示一个界面,所以它的两个缺点没有显示出来:
【javaweb|Java Web基础知识之JSP(穿上马甲我照样认识你)】

  1. 所有的HTML标签和文本都必须使用一个字符串形式通过ServletResponse的实例写出去;
  2. 所有的文本和样式必须得进行硬编码,也就是说如果你想稍微更改一下前端的显示的话,必须得重新编译Servlet实例;
针对这两个问题,提出了jsp这种技术,一般来说jsp有点像HTML文件,但是可以看到里面还有一些其他的元素,这就是java代码,但是相对于Servlet,它就是一个文本文件,不需要进行编译(其实是你没有看见而已),也就是说当你新添加一个JSP页面时不需要重启Servlet容器,并且该页面文件可以使用任何的文本编辑器编辑。JSP页面一般是在JSP容器中运行的,Servlet容器一般也是JSP容器,所以说Tomcat也可以充当JSP容器。 一、 请求JSP页面 对JSP页面的请求在本质上和请求一个Servlet是类似的,但是我们上面提到JSP页面的一大优点就是不需要编译,其实严格来说这是不对的,它只是不需要静态编译,也就是在正式部署运行之前是不需要编译的,但是在web应用部署运行之后则会动态地进行转换为servlet类并且编译这个类。
  • 当第一次请求JSP页面时,会将转换成一个页面实现类,转换的工作是由servlet容器完成的,所生成的servlet的名称由servlet容器决定,如果转换错误则会发出错误到客户端;如果转换成功则会进一步被servlet容器编译(此处是动态编译)成servlet类,之后容器加载和实例化该类,并执行相应的生命周期方法;
  • 对该页面的后续请求,servlet容器也不傻,每次都转换它也累,所以它会检查该JSP页面自从上一次转换后是否发生修改,如果修改过则会重新转换、编译并执行,如果没有则执行内存中已经存在的JSP类实例。
从上述说明中可以看到,对某个JSP页面的第一次请求是比较耗费时间的,有没有解决办法呢?当然有了!!
  • 配置应用程序启动时就转换和编译所有的JSP页面,而不是在接收第一次请求才做这些工作;
  • 预先编译JSP页面,并将它们以servlet的形式进行部署;
关于这方面是有一些工具提供使用的,还有一些web服务器是提供这方面的配置的,这里就不细说了,但是我是使用的tomcat,没有找到这方面相关的配置,如果哪位大神指教一下,感激不尽啊!! 二、 JSP的本质分析 首先说一下和JSP相关的包,如下:
  • javax.servlet.jsp:包含核心的接口和类,servlet容器使用这些接口和类将JSP页面转换成servlet,最重要的莫过于JspPage和HttpJspPage;
  • javax.servlet.jsp.el:包含支持JSP中的EL表达式的相关的类和接口
  • javax.servlet.jsp.tagext:包含用于开发定制标签的类型,也就是如果你想自己写一个标签必须用到这里的API;
  • javax.el:为Unified EL提供API;
JSP转换生成的页面类必须实现或者间接实现javax.servlet.jsp.JspPage接口,不出你所料,这个接口继承了javax.servlet.Servlet接口,如下: javaweb|Java Web基础知识之JSP(穿上马甲我照样认识你)
文章图片

这样我们就可以相信了,JSP页面确实是一个Servlet(我没有胡诌!!)。为了直观一些,我们直接看JSP页面转换生成之后的servlet类,通过对比进一步解释JSP的本质,如下是一个JSP页面index.jsp:
Hello World!

可以看出这就是由普通的HTML标签组成的文本(你骗我,这不是JSP,这就是HTML),前面说过JSP页面其实就是由HTML元素和java代码组成的。 转换成对应的servlet,index_jsp.java如下,从名字可以看出jtomcat将jsp页面转换成servlet时的名称是由“名称_jsp”组成:
/* * Generated by the Jasper component of Apache Tomcat * Version: Apache Tomcat/7.0.68 * Generated at: 2016-04-16 12:01:36 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; import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.jsp.*; public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent {private static final javax.servlet.jsp.JspFactory _jspxFactory = javax.servlet.jsp.JspFactory.getDefaultFactory(); private static java.util.Map _jspx_dependants; private volatile javax.el.ExpressionFactory _el_expressionfactory; private volatile org.apache.tomcat.InstanceManager _jsp_instancemanager; public java.util.Map getDependants() { return _jspx_dependants; }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 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"); 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("Hello World!\r\n"); out.write("\r\n"); out.write("\r\n"); } 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); } } }

从上面的源代码来看index_jsp继承了org.apache.jasper.runtime.HttpJspBase类,那么这个类是什么?从tomcat的javaDoc来看,如下: javaweb|Java Web基础知识之JSP(穿上马甲我照样认识你)
文章图片
这个类继承了了HttpServlet并且还实现了JspPage和HttpJspPage接口。在JSP中的主体代码被转换成index_jsp类的_jspService()方法中的内容,并且这个方法是由Httpbase中的service()方法调用的,如下:
/** * Entry point into service. */ @Override public final void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { _jspService(request, response); }@Override public abstract void _jspService(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;

当index_jsp继承了Httpbase时,则必须实现_jspService()方法,那么当响应来的时候就可以被httpbase实例调用。
三、 JSP隐式对象 其实之前每次看到这部分的时候,我都会很懵,因为不知道这些隐式对象是怎么起作用的(人类总是对自己无知的事情或者隐藏的事情充满了好奇和恐惧)。这次顺便一起说明一下,也是对自己的督促。我们知道每次servlet初始化或者接受请求的时候servlet容器都会传给生命周期方法一些对象,那么问题来了,JSP页面怎么接受这些对象?是在JSP页面转换成servlet的时候隐式地将这些对象加入到生成的servlet类中的,看上面由JSP页面生成的servlet类可以看出这一点,下面是隐式对象和类型之间的对应关系:
对象 类型
request javax.servlet.http.HttpServletRequest
response javax.servlet.http.HttpServletResponse
session javax.servlet.http.HttpSession
application javax.servlet.ServletContext
config javax.servlet.ServletConfig
exception java.lang.Exception
out javax.servlet.jsp.JspWriter
pageContext javax.servlet.jsp.PageContext
page javax.servlet.jsp.HttpJspPage














对于上述对象可以当做已经存在而直接使用,这里主要说三个对象:
  • out:这个实例的类直接继承自java.io.Writer,所以可以直接将数据写出随响应而返回给客户端,就像我们在servlet中做的那样,response.getWriter().write();
  • page:表示该页面,基本没什么用;
  • pageContext:这个类表示这个页面的上下文环境,可以用来管理和存储在这个页面使用的属性,另外从上面生成的源代码来看,通过这个属性可以获得其他几个对象,如下;
application = pageContext.getServletContext(); config = pageContext.getServletConfig(); session = pageContext.getSession(); out = pageContext.getOut();

另一个重要的作用对于pageContext来说,它可以存储各个作用范围的属性,由于它是直接继承自javax.servlet.jsp.JspContext,所以可以这样操作,有如下4个作用范围:
  1. APPLICATION_SCOPE
  2. SESSION_SCOPE
  3. REQUEST_SCOPE
  4. PAGE_SCOPE
作用范围是从大到小的顺序列出的。对应的API如下,scope则可以使用上述4个范围指定:
abstract public void setAttribute(String name, Object value, int scope)

另外还可以通过设置只在该页使用的属性,即页面作用范围,如下:
abstract public void setAttribute(String name, Object value)

四、 JSP页面内容 JSP页面既包括模板数据,也包括句法元素,之前HTML标签和文本就是模板数据,而句法元素是由包含起来的内容,可以说除了句法元素之外全部是模板数据。关于模板数据我们这里不详细说明,不是我们的重点,这里着重说句法元素。句法元素主要有以下几种: 1、 指令
指令的作用是用来在JSP页面转换成servlet类的时候起到一种指导作用,说白了就是告诉JSP转换器怎么将一个JSP页面转换成servlet类,其中可以包含一些数据用来在转换的过程中加入类中,由于指令实在太多,所以这里我们说明两个比较重要的,page指令和include指令。 Page指令: 就如同这个指令的名字暗示的那样,这个指令是用来说明整个页面包含的信息的,我们怎么使用它呢?语法如下:

它有几个重要的属性,如下:

说明如下:
  • contentType:这个大家就太熟悉了,就是http中的content-type,每次由ServletResponse的实例设置返回;
  • pageEncoding:是该页面的页面编码,如果有中文的话就要小心这个值,如果没有可以直接默认(ISO-8859-1)
  • isErrorPage:说明这个页面是否是一个在发生错误时返回的页面,可以用作进行异常处理时返回的页面;
  • import:从名字就可看出来就是对应于java程序中的import关键字,导入依赖包;
  • session:该属性用来说明该页面是否参与session管理,也就是说session在该页面是否起作用;
  • buffer:指定out对象的缓冲区大小,单位kb,也可以为none,表示不使用缓存;
  • autoFlush;说明缓冲区满的时候是否自动刷新,true时则会自动刷新,如果是false,则必须手动调用response.getWriter().flush()进行强制刷新,这个属性如果看源码的话是在PrintWriter中定义的;
关于这个指令还有如下几个方面要注意:
  • page指令可以放在页面的任何位置,但是注意,对于上面属性中涉及编码的必须放在最前面;
  • page指令可以多次出现,但是如果在多个指令中出现同一属性则它们的值必须相同,但是import例外,它是进行累加的;
include指令: 这个指令使用比较简单,它是用来包含另外一个文件在当前页面中,如果你的一个页面比较大的话想分成多个表示不同区域的页面则可以使用这个指令来做,另外如果部分内容在多个页面中都被引用则可以做成一个独立的页面然后使用该指令包含在其他页面中。比较熟悉的使用如下:

使用这个指令有以下两个问题要注意:
  • 包含的文件的路径:如果file属性以"/"开头则说明,该文件的路径是以web应用程序在服务器上的根路径开始,也就是一个绝对路径;如果不是,则该路径被解释成相对于该页面的路径,也就是说被包含的页面文件必须和指令使用的页面在同一个文件夹下;
  • 包含的文件的类型:可以是JSP文件,或者是jspf文件,还可以是静态文件html
2、 脚本元素
脚本元素就是将java代码转成一个JSP页面,主要有三种形式:
  • Scriptlet
这就是一小段嵌入在HTML元素中的Java代码,使用包含起来,如下:

注意:定义在其中的变量在之后的Scriptlet是可见的,也就是说可以直接使用。
  • 表达式
表达式其实就是Scriptlet的封装,是通过包装起来的,它其实是把java的运算结果或者说返回值直接赋值给了隐式对象out的print()方法,避免了我们在Scriptlet中使用out输出,使用如下:
Now is

  • 声明
声明主要是用来声明一下在JSP页面中使用的变量和方法,不建议这么使用,但是作为脚本元素的一部分还是要提一下,语法是通过包含对应的声明部分。

其实我们上边对脚本元素说了很多,在实际开发中并不常用,稍微懂一点web开发的都知道MVC模式,使用脚本元素明显违反了这种模式,鉴于之后又出现EL这种工具,所以更没有必要使用脚本元素了(不过自己做一些demo还是可以使用的)。 我们可以通过在部署描述符中配置来禁止使用脚本元素,如下:
*.jsp true

在jsp-config中还可以配置很多属性,这里就不一一介绍了,如果有兴趣的可以去看部署描述符的schema文件。
3、 动作
动作会被编译成执行某个操作的Java代码,有点类似于通过文本的配置然后在接受请求时却会转换成可执行的代码。动作有很多种,它们是以标签的形式存在,除了标准的标准,还有一些定制的标签,这里就不说了,实在是太多了,如果想了解的话可以去查看JSTL相关的信息,也可以了解一下怎么编写自定义标签。 这里作为举例,说明两个即可: include: 该动作用于动态地包含另一个资源,这个可以是JSP页面,servlet或者是HTML静态页面。语法如下:

注意它和上述include指令之间的区别。主要如下:
  • include指令是发生在JSP页面转换成servlet类的时候,因此include指令适合静态页面的包含,而include动作则是发生在每次请求的时候,因此可以使用include动作传递参数,同时还因为include动作是发生在接受请求的时候,每次的被包含页面的响应根据业务逻辑可以是最新的,但是由于include指令是在页面转换时作用的,所以如果被包含的页面不变化,那么返回的页面内容总是相同的;
  • 如果想要使用include动作的这种在接受请求时返回最新响应的机制,则被包含的页面必须是以jsp结尾的;
关于include指令和include动作的实现原理其实是,include指令是包含一个页面进去,而inclu动作其实包含进去一个URL地址,所以使用include动作其实在底层是向该被包含的页面发送一个请求(自己理解),具体的原理看如下链接文章。 forward:
这个我们之前在 Java Web基础知识之Filter:过滤一切你不想看到的事情中遇到过,通过它可以转向不同的资源,包括JSP和servlet,关于这个标签的底层实现是通过RequestDispatcher.forward()实现的,这个标签的使用如下:

在转发的时候可以添加一些新的请求参数如上所示,关于转发和重定向的区别见如下链接

    推荐阅读