Java 服务端页面 JSP

作者: 善倾 | 来源:发表于2018-09-01 23:50 被阅读0次

    JSP ( java server page ) java 服务端页面,从名字就可以看出它是运行在服务端的 java 代码,由 Tomcat 来执行的。早期是没有 JSP 这种东西的,动态网页技术是通过在服务端输出字符串的方式拼接出 HTML 代码,然后将其发送给浏览器,由浏览器去渲染出可视化界面。但是这样难度太大了,稍不留神就会出错,简直让人崩溃。后来就出现了 JSP 技术,它能够在 HTML 页面中嵌入 java 代码,然后由 Tomcat 去执行,转成正式的 HTML 页面,发送到客户端浏览器去运行。

    JSP 的执行流程

    首先客户端浏览器向服务端发送一个请求的页面地址,服务端在收到用户请求后找到对应的 xxx.jsp 文件,然后将其转成 xxx_jsp.java 文件,再编译成 xxx_jsp.class 字节码文件,然后在堆区生成对应的 JSP 对象。以后客户端每次访问 xxx.jsp 都是相当于直接访问堆区的这个 JSP 对象。所以才会有第一次访问 JSP 页面的时候,执行速度会很慢,之后速度就会变快,就是因为这中间存在一个转换过程。

    严格来说,这里讲到的 JSP 对象应该称为 Servlet 对象。以 welcome.jsp 为例,Tomcat 会把 welcome.jsp 转成 welcome_jsp.java 文件。查看转成 .java 文件的源码后发现,这是一个 Servlet 类,此类继承了 org.apache.jasper.runtime.HttpJspBase 类,这是一个继承自 HttpServlet 类抽象类。继承自HttpServlet,继承自HttpServlet,继承自HttpServlet,重要的事情说三遍啊。这还不能证明 JSP 就是 Servlet 吗? 以后提起 JSP 只需要记住一句话,它间接继承了 HttpServlet 类。

    这足以证明 JSP 本质上还是一个 Servlet ,只是因为程序员直接用 Servlet 做 HTML 元素的字符串拼接太麻烦了,然后就提供了一种更为简单的方式供程序员使用编写代码,然后将具体 HTML 字符串拼接的工作交给 JSP 转换器自动去完成。本质并没有改变,只是程序员用得更爽了。

    每次访问 JSP 页面前,Servlet 容器都会去检查 JSP 页面是否改动了,如果改动就重新转成 .java 文件,再编译成 .class 文件,如果没有改动过,就直接使用已经存在的这个对象。这也就是说,改动 JSP 页面,不需要重启 Servlet 容器,就可以看到新的改变。

    学习 JavaSE 的时候,主角是 JVM ,但是到了学习 JavaWeb 的阶段,JVM 就只是作为一个配角了,Tomcat 才是时刻存在、需要关注的主角。

    JSP 注释

    JSP 页面中有两种注释,一种是 JSP 注释,一种是 HTML 注释。

    • `` JSP 页面中用来给 HTML 代码做注释的,Tomcat 并不会处理这种注释,会把它发给浏览器去处理。但是因为是注释,所以注释中的内容不会被浏览器渲染出来,但是会存在 HTML 源代码中。 web.xml 文件中也是用的这种方式做注释,但是当然是 Tomcat 来处理的啊。通过配置信息来告诉框架程序该怎么执行,这就是 java 中应用的极为广泛的声明式编程。
    • <%--这是JSP的注释--%> JSP 中的注释,Tomcat 在将其转成 .java 文件时就已经把注释部分的内容给去掉了,更不可能被发送到客户端。而且在<% %>这种标签中的 java 代码中还可以用 //单行注释/*多行注释*/

    JSP 中的脚本元素

    JSP 的脚本元素有三种类型,Scriptlet 、声明及表达式。

    • Scriptlet 用<% %>表示,在此标签中可以直接添加 java 代码,但是通常不建议这样做,会导致 JSP 页面的可读性简直差到了极点。而且同一个 JSP 页面中的多个 Scriptlet 中的 java 代码最后都会被 Tomcat 放在转成 .java 文件的void _jspService(HttpServletRequest request,HttpServletResponse response)方法中,所以这种 Scriptlet 中的变量本质上属于局部变量,每调用一次就在对应线程栈中开辟一个方法栈帧,方法结束后,就释放内存空间。在写代码的时候,想象下正在编写的 JSP 页面转成 .java 文件的时候,代码是如何组织的,就能够很好的理解这种复杂的逻辑了。JSP 中的 HTML 元素在转成对应 .java 文件时,最终还是通过 out.print() 方法进行字符串输出的,所以本质没有任何改变,只是提供了一个简易的方式给程序员使用而已。
    • 声明用 <%! %>表示,在这里面定义的变量属于全局变量,在 JSP 页面对应的位置属于全局变量,也就是在内存中是在堆区对象的内存空间中。学习启示:手动思考下 JavaSE 部分学习的内存划分知识,不要学了 web 就忘了之前学的了,把不同抽象层次的东西建立联系,才能够真正做到融会贯通。
    • 表达式用 <%= %>表示,它后面跟的是一个变量或者常量,不需要加;。它对应到 .java 文件中,就是使用了out.print()方法进行输出而已,方法内的参数变量肯定不能加上;啊。

    JSP 中的指令

    JSP 中的指令(Directive) 作用是,指示 JSP 转换器应该如何将 JSP 页面转换成 Servlet 的命令。最常用的就是 page 和 include 了!

    page 指令

    page 指令是用来定义页面的相关属性的,常用的属性有 contentType 、errorPage 及 import 等。

    • contentType 属性,一般用法如下<%@page contentType = "text/html;charset=utf-8"%>,这种利用 charset 指定编码的方式,指的是服务端发送给客户端内容的编码,所以发送到客户端仍然极有可能会出现乱码问题。真正要解决乱码问题,还是应该用 page 指令的 pageEncoding 属性,它指的是整个 JSP 页面的编码方式。
    • pageEncoding属性,一般用法如下:<%@page contentType = "text/html" pageEncoding = "utf-8"%>它指的是整个 JSP 页面的编码方式。所以,以后 JSP 中的页面用这种方式来指定编码,并配合 HTML 属性 <meta charset = "utf-8">指定浏览器使用这种方式读取 HTML 页面即可。

    include 指令

    通常一个 JSP 页面有多个模块组成,比如顶部的菜单栏,任何界面都需要此菜单,所以把它做成一个单独的页面,然后其他页面去引用这个文件即可。这时候就需要用到 include 指令了!

    使用方式如下:<%@include file = "menu.html"%>,file 属性对应的 url 如果加上/,那么在服务端就会被解读为一条绝对路径,如果不加上/就会被解读为当前 JSP 页面的相对路径。无论包含进来的是哪种文件,这个 include 指令的作用就是直接把对应文件的内容原封不动的拷贝过来,然后再转成 .java 文件,进行编译,所以可能会存在冲突问题。

    JSP 标签库

    在 JSP 中使用原生 java 代码会造成页面可读性很差,难以维护。所以就想出了使用标签来解决这个问题。标签的思想就是,使用的一种看起来更简洁、用起来方便的一种符号来替代大量的 java 代码,使得编写 JSP 页面变的更简单。通过查看源码即可证明,事实如此。

    对于 JSP 标签,只需要理解它的思想即可,因为实际编程中最多只会用到 JSTL 标签或者 web MVC 框架的标签。而且 JSP 这种东西早就不用了,基于它之上的标签就更不用说了。

    这几个标签之前也花费了好多时间去学习,但是现在发现,学这东西根本就没卵用啊。老师真的是有毒,明明知道用不上还教,搞得自己还傻不拉几的花了这么多时间在这上面

    所谓的 JSP 标签其实也就是 JSP 设计者提供给程序员而已。使用标签能够少写好多行代码啊,用着超级爽的,而且在框架中还有特殊的用途,对于标签要做到掌握 JSTL 和相应的 web 框架的标签就可以了。JSP 引擎还是会把这些标签转换成原生 Java 代码的,查看源码即可证明。

    JSP 中还有三个专门用来操作 JavaBean 的标签,点此查看对应笔记

    include 标签

    include 动作具体使用方式如下:<jsp:include page = "info.jsp">,利用这种 include 动作包含页面和 include 指令包含不同点在于:include 指令无论包含的是静态页面还是动态页面,都是直接将被包含页面的内容原封不动的复制过来,然后 JSP 转换器再将其转成 .java 文件,最后编译成 .class 文件。而 include 指令则是先对被包含页面进行动态处理,将输出信息包含进来,如果是静态页面当然也是直接包含进来。

    所以,以后在需要进行包含页面时,利用 include 动作而不是 include 指令,这样更好。

    还可以利用 include 动作给被包含页面传递参数,因为本质上这是一次 http 请求,所以页面之间共享的是同一个 request 对象,使用方式如下:

    <jsp:include page = "menu.jsp">
        <jsp:param name="info" value = "www.tencent.com"/>
    </jsp:include>
    

    上述代码的执行过程如下:先给 request 对象添加属性 name 和 value 这样的键值对元素,然后去执行被包含页面(被包含页面可以通过 request 对象得到传递过来的属性),被包含页面执行完后的结果被包含进来。

    forward 标签

    forward 动作属于服务端跳转,也就是说浏览器并没有第二次发送请求,从头到尾都只是发送了一次请求,当前页面和后续的跳转页面都是共享 request 等对象的(要理解这一点,前提是要理解 http 协议的执行过程)。所以在客户端浏览器的地址栏中 url 并没有改变,还是原来的请求 url 。

    具体使用方式如下,同 include 动作一样,也可以通过嵌套 param 动作来传递数据到后续页面。

    <jsp:forward page = "success.jsp">
        <jsp:param name = "userName" value = "password"/>
    </jsp:forward>
    

    JSP 脚本元素中的 9 个内置对象

    JSP 中虽然号称有 9 大内置对象,但是其实只要死死的理解了以下 6 个就可以了,另外 3 个就当它不存在吧。

    • request
    • response
    • session
    • application
    • config
    • page 此对象值在 JSP 中有用

    在编写 JSP 页面时 ,它有 9 个不同功能的内置对象,不需要 JSP 程序员显示进行初始化,就能够直接使用。这给程序员提供了很大的便利,但无论是书本上还是老师上课都没有讲过这些内置对象是哪来的,上来就讲内置对象有什么用、怎么用?给人感觉很虚,对内置对象的理解停留在非常表面的层次,知识始终是掌握的不牢靠的。

    其实所谓的内置对象本质上就是 Tomcat 负责创建和管理的封装了一系列参数信息的堆区的很普通的对象而已,JSP 中能够使用它是因为 jsp_Service() 方法中定义了几个按照规定命名的局部变量,然后拿到了这些对象在堆区的地址而已。而且这些内置对象不仅仅是只能在 JSP 中用到,在 Servlet 类中是一样使用的,这有什么奇怪的吗?没有啊,同样都是在操作系统管理的一个 JVM 进程中的堆区内存中运行的对象,为什么不能够使用呢?不要因为内置对象是在学习 JSP 内容的时候了解到的,就以为内置对象只有 JSP 才能够使用。

    如果再说的深入一点,JSP 本质就是 Servlet 啊,所以内置对象本质上其实是给 Servlet 准备的。只是在 JSP 中直接拿到了这几个对象并且按照约定俗成的名字命名好了给程序员使用(这部分代码不许要客户端程序员自己使用而已)而在 Servlet 中并没有预先指定命名,需要自己去拿到这些内置对象,自己命名变量指向这些堆区的对象,仅此而已。

    JSP 的 9 个内置对象到底是哪来的?

    前面已经知道了 xxx.jsp 文件会被转成对应的 xxx_jsp.java 文件,脚本元素<% %><%= %>中的代码都会被放在对应 Servlet 类的 _jspService() 方法中。也只有在这两种脚本元素内才能够使用 JSP 的内置对象,它背后本质的原因是,_jspService() 方法中有 8 个局部变量,并且进行了相应的初始化,查看源码即可发现,就是 JSP 中的除开 exception 对象的另外 8 个内置对象

    这说明什么呢?因为这两种脚本元素中的代码最后是被放在同一个 _jspService() 方法中的,而 JSP 页面中使用的 9 个内置对象的引用变量也是在这里进行初始化的,指向了堆区的内存对象。所以现在转换成了什么问题呢?一个方法内的局部变量的作用范围肯定只在这个方法内有效啊,所以只有在这两个脚本元素中才能使用这 9 个内置对象,在<%! %>这个脚本元素中则不可以使用内置对象,因为这里面的变量和方法会被当成全局变量看待,已经离开了内置对象的作用范围了。

    其实已经很清晰明了了,想要有更直观的感受,就看看_jspServeice()方法的源码吧,那一目了然了。此处限于篇幅,删去了部分不重要代码

    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;
      }
    

    现在回过头来看,上面的讲述过程好混乱。直接看最后 _jspService() 方法的源码就可以知道,8 个内置对象的引用就是在这里进行初始化的,两种脚本元素中的 java 代码最后也是被包含在这个方法中。所以,他们是在同一个方法,使用本方法的局部变量,这不是一件再正常不过的事情了吗?每一次请求结束,此方法就会退栈,局部变量都会消失,但不代表堆区的对象也会消失。至于堆区的内存对象到底是什么时候创建的,什么时候消失的,取决于具体的内置对象,并不相同(内置对象的创建和管理是由 Tomcat 负责的)。比如 application 对象就会在 Tomcat 服务器启动时创建(无论是哪次请求拿到的都是堆区的同一个内存对象),request 和 response 就会在每次有新的请求时创建,请求结束就立马被销毁(每次请求都是两个新的对象)。这就是涉及到内置对象的生命周期的问题了,点此查看具体内置对象的笔记

    学习启示:在学习 JSP 的过程中,千万不要停留在 JSP 页面本身来考虑问题,而是要去想它转成对应的 java 类后是怎么组织源码的,然后这不就是你在学习 JavaSE 部分的知识点了吗?这一块基础还算扎实吧,所以说,Java 基础学得有多好,决定了未来能走多远,其他看似很牛逼的东西,本质上都是对 JavaSE 部分的上层应用而已。

    application 对象

    点此查看 application 对象相关笔记

    config 对象

    config 对象是 javax.servlet.ServletConfig 接口的实例对象,代表当前 JSP 页面的配置信息,但是通常 JSP 页面不需要显示的定义配置的信息,那是因为关于 JSP 的配置信息在 conf/web.xml 中已经配置好了,通常 JSP 页面中不会用到它。

    但是在自定义的 Servlet 中用的非常多,因为每一个 Servlet 类都需要显示的在 WEB-INF/web.xml 中进行访问 url 的配置,所以在对应的<servlet></servlet>元素中还可以进行参数的配置。Tomcat 会为每一个 Servlet 对象都创建一个单独的 config 对象,也就是说每个 JSP 页面也都会有一个唯一的 config 对象,它在 Tomcat 启动时被创建,关闭时被销毁。

    并且自动的把这里面配置的参数名和参数值放到 config 对象中去。通过 ServletConfig 接口的getInitParam(String name)方法就可以得到配置文件中的属性值了。

    <servlet>
        <servlet-name>Simple</servlet-name>
        <servlet-class>lee.SimpleServlet</servlet-class>
        <init-param>
            <param-name>port</param-name>
            <param-value>3366</param-value>
        </init-param>
    </servlet>
    

    out 对象

    JSP 页面中可以使用 out 对象来进行输出,但是这样会导致页面可读性变差,通常是用<%= %>来替代。尽管在转成 .java 文件后,也是采用的 out 对象进行输出,但是这个代码又不需要程序员来看,所以不用理睬这个问题, JSP 页面才需要程序员阅读。

    page

    JSP 四个内置对象域中有一个就是 page ,一般在设定属性的生存范围时会用到。它在 JSP 转成 .java 源码中是这么写的Object page = this,就是说这个 page 引用指向的就是当前的这个 JSP 对象。JSP 中用的还比较多,Servlet 中基本不会用到它。

    pageContext 对象

    pageContext 是 javax.servlet.jsp.PageContext 的实例对象, 它在 Servlet 中基本不会用,但是在 JSP 中是超级有用。查看 JSP 转成 .java 后的源码就可知道,如下:

    application = pageContext.getServletContext();
    config = pageContext.getServletConfig();
    session = pageContext.getSession();
    out = pageContext.getOut();
    

    JSP 中的 9 个内置对象,除了 exception 比较特殊外,其他 8 个都是在这里面进行声明的。其中 request 和 response 是通过依赖注入的方式由 Tomcat 传递过来的,上述的 4 个内置对象都是通过 pageContext 对象进行依赖查找的到的对象地址。由此可见在 JSP 中 pageContext 有多重要,但是在 Servlet 中通常是通过 request 对象得到这些内置对象实例地址的。

    request 对象

    点击这里查看 request 对象的笔记

    response 对象

    点击这里查看 response 对象的笔记

    session 对象

    点次查看 session 对象的笔记

    JSP 内置对象的 4 种作用域

    这其实就是内置对象的生命周期的问题了,不仅仅是这个对象,而且还包括依赖于这个对象而存在的属性,比如将表单 Bean 对象作为这几个内置对象的属性存在,那么这些 Bean 对象的生存周期就是依赖于这些 JSP 内置对象。详情参考 JavaBean 相关笔记

    这四个内置对象分别是 page 、request 、session 和 application 。

    • page,只在当前 JSP 页面内有效
    • request,在客户端发送过来的一次请求周期内有效,服务端跳转属于一次请求
    • session,在和服务端连接的一次会话周期内有效,也就是说系统Cookie 在浏览器的运行内存中存在时有效,因为这就是一次会话。
    • application,在服务器启动到关闭周期内,对于本 web 应用内有效。

    相关文章

      网友评论

        本文标题:Java 服务端页面 JSP

        本文链接:https://www.haomeiwen.com/subject/ybjpwftx.html