Jsp理论与实践综述

        J2EE是一套规范,而Servlet/Jsp是J2EE规范的一部分,是Tomcat的主要实现部分。在最初的应用实践中,当用户向指定Servlet发送请求时,Servlet利用输出流动态生成HTML页面,这导致Servlet开发效率极为低下。JSP技术通过实现普通静态HTML和动态部分混合编码,使得逻辑内容与外观相分离,大大简化了表示层的实现,提高了开发效率。本文以JSP的本质是Servlet为主线,结合JSP转译后所得的Servlet,详细探讨了JSP的原理、执行过程、脚本元素、编译指令和动作指令,并给出了JSP使用的常见注意事项。


一. JSP 原理


1、JSP 简介 

  

        JSP (Java Server Pages) 是由 Sun 公司倡导、多家公司参与一起建立的一种动态网页技术标准。JSP 文件(*.jsp)实际上是在传统的网页HTML文件(*.htm,*.html)中插入Java程序段(Scriptlet)和 JSP标记(tag)所形成的。 JSP 的本质是 Servlet,当用户向指定 Servlet 发送请求时,Servlet 利用输出流动态生成 HTML 页面。当使用 Servlet 作为表现层(实际上,Servlet 最适合作控制器)时,由于表现层页面往往包括大量的 HTML标签、静态文本和格式等,导致 Servlet开发效率极为低下。如下图所示,JSP 的出现弥补了这种不足,它通过实现普通静态HTML和动态部分混合编码,使得逻辑内容与外观相分离,大大简化了表示层的实现。实际上,在静态的HTML页面中嵌入动态Java脚本即可完成JSP的开发。JSP并没有增加任何本质上不能用Servlet实现的功能,但是在JSP中编写静态HTML更加方便,因为不必再用println语句来输出每一行HTML代码。





2、JSP 的执行过程


        当用户访问一个JSP页面时,容器对JSP页面的处理通常可分为两个时期:转译时期(Translation Time)和请求时期(Request Time)。在转译时期,JSP网页被转译成Servlet类,然后被编译成class文件;在请求时期,容器加载编译后的 Servlet类,并把响应结果返回至客户端,整个流程可分为三个步骤,流程图如下:


        (1)当客户第一次请求JSP页面时,JSP引擎会通过预处理把JSP文件中的静态数据(HTML文本)和动态数据(Java脚本)全部转换为Java代码。转换原则非常直观:对于HTML文本只是简单的用 out.println() 方法包裹起来,而对于Java脚本只是保留或做简单的处理; 

        (2)JSP引擎把生成的.java文件编译成Servlet类文件(.class)。对于Tomcat服务器而言,生成的类文件默认的情况下存放在\work目录; 

        (3)编译后的class对象被加载到容器中,并根据用户的请求生成HTML格式的响应页面。

        为了更好地了解JSP的转译过程和转换原则,我们以下面这个JSP文件为例进行说明。

<%@ page language="java" import="java.util.*" pageEncoding="GB18030"%>
<%-- JSP脚本 --%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <base href="<%=basePath%>">
    <title>Success</title>
    <meta http-equiv="pragma" content="no-cache">
    <meta http-equiv="cache-control" content="no-cache">
    <meta http-equiv="expires" content="0">    
    <meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
    <meta http-equiv="description" content="This is my page">
    <!--
    <link rel="stylesheet" type="text/css" href="styles.css">
    -->
  </head>
  <body>
    Welcome! </br>
    <%! private String declaration = "我是JSP声明..." ;%>
    <%=declaration %>
    <%-- 我是JSP注释... --%>
  </body>
</html>

        JSP引擎会把JSP文件中的静态数据(HTML文本)和动态数据(Java脚本)转换为如下Java代码:

public final class welcome_jsp extends org.apache.jasper.runtime.HttpJspBase
    implements org.apache.jasper.runtime.JspSourceDependent {
  // 动态部分: 通过JSP声明的变量转化为成员变量
  private String declaration = "我是JSP声明..." ;
  private static java.util.List _jspx_dependants;

  public Object getDependants() {
    return _jspx_dependants;
  }

  public void _jspService(HttpServletRequest request, HttpServletResponse response)
        throws java.io.IOException, ServletException {

    //JSP 内置对象
    JspFactory _jspxFactory = null;
    PageContext pageContext = null;
    HttpSession session = null;
    ServletContext application = null;
    ServletConfig config = null;
    JspWriter out = null;
    Object page = this;
    JspWriter _jspx_out = null;
    PageContext _jspx_page_context = null;


    try {       // JSP异常处理机制
      _jspxFactory = JspFactory.getDefaultFactory();
      response.setContentType("text/html;charset=GB18030");
      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');
      out.write('\n');

      // 动态部分: JSP脚本直接转换为Java代码
      String path = request.getContextPath();
      String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";

      // 静态部分: 页面输出流输出静态HTML
      out.write("\r\n");
      out.write("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\r\n");
      out.write("<html>\r\n");
      out.write("  <head>\r\n");
      out.write("    <base href=\"");
      out.print(basePath);
      out.write("\">\r\n");
      out.write("    \r\n");
      out.write("    <title>Success</title>\r\n");
      out.write("\t<meta http-equiv=\"pragma\" content=\"no-cache\">\r\n");
      out.write("\t<meta http-equiv=\"cache-control\" content=\"no-cache\">\r\n");
      out.write("\t<meta http-equiv=\"expires\" content=\"0\">    \r\n");
      out.write("\t<meta http-equiv=\"keywords\" content=\"keyword1,keyword2,keyword3\">\r\n");
      out.write("\t<meta http-equiv=\"description\" content=\"This is my page\">\r\n");
      out.write("\t<!--\r\n");
      out.write("\t<link rel=\"stylesheet\" type=\"text/css\" href=\"styles.css\">\r\n");
      out.write("\t-->\r\n");
      out.write("  </head>\r\n");
      out.write("  \r\n");
      out.write("  <body>\r\n");
      out.write("    Welcome! </br>\r\n");
      out.write("  \t");
      out.write("\r\n");
      out.write("  \t");
      // 动态部分: JSP 输出表达式转换为 out.print()
      out.print(declaration );
      out.write('\r');
      out.write('\n');
      out.write('   ');
      out.write("\r\n");
      out.write("  </body>\r\n");
      out.write("</html>\r\n");
    } catch (Throwable t) {
      if (!(t instanceof SkipPageException)){
        out = _jspx_out;
        if (out != null && out.getBufferSize() != 0)
          out.clearBuffer();
        if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
      }
    } finally {
      if (_jspxFactory != null) _jspxFactory.releasePageContext(_jspx_page_context);
    }
  }
}
        结合以上示例,关于JSP本质和转移准则,我们可以得出以下几点结论:

        1、org.apache.jasper.runtime.HttpJspBase 继承自 javax.servlet.http.HttpServlet,因此 JSP 的本质是 Servlet;

        2、JSP 页面的静态部分(HTML内容)在 _jspService()方法中由页面输出流进行输出

        3、JSP引擎在处理 JSP页面的动态部分时,将JSP声明的变量/方法转换为类的成员变量/方法;将JSP脚本按原顺序插入到_jspService()方法中;将JSP的输出表达式转换为 out.print() 进行输出;将JSP注释进行忽略。

        在请求时期,容器加载编译后的Servlet类,并把响应结果返回至客户端。特别地,如果JSP页面已经被转换为Servlet类(jsp文件转换.java文件)且该Servlet类已经被编译(.java文件编译成.class文件)进而被加载(在第一次被请求时)了,这样再次请求此JSP页面时,将感觉不到延迟



二. JSP 基本语法与脚本元素


        JSP 的脚本元素(Scripting Element)包含三个部分:Scriptlet、Expression(表达式) 和 Declaration(声明)。


Scriptlet 元素


        Scriptlet 中可以包含有效的程序片段,可以包含多个语句、方法调用、变量和表达式等,只要是合乎Java本身的标准语法即可。通常主要的程序也是写在这里,Scriptlet是以 <% 开始, 以%> 结尾。特别需要注意的是:在编译JSP时,编译器在_jspService()方法中只是简单地不做修改地在对应位置包含Scriptlet的内容。因此,在 Scriptlet 内部声明的变量是局部变量而不是成员变量
<%@ page language="java" import="java.util.*" contentType="text/html; charset=utf-8" pageEncoding="utf-8" errorPage="exception.jsp"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>test</title>
    <meta http-equiv="pragma" content="no-cache">
    <meta http-equiv="cache-control" content="no-cache">
    <meta http-equiv="expires" content="0">    
    <meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
    <meta http-equiv="description" content="This is my page">
  </head>
  <body>
    <% for(int i =0; i < 7; i++){
        out.print("<font size='" + i + "'>"); 
    %>
    地球</br></font> <!-- 会打印七遍“地球”-->
    <%}%>
  </body>
</html>


Expression 元素


        JSP 提供了一种简单方法访问可用的Java变量和Java表达式,并生成页面HTML字符串。Expression 元素是以 <%= 开始,以%> 结尾的,其中内容包含一段合法Java的表达式。


Declaration 元素


        Declaration元素用于声明在页面中初始化的变量、方法和类,特别需要注意以下几点:

        (1)编译JSP时,Scriptlet 中定义的变量是_jspService()方法的局部变量,而使用 Declaration 所声明的变量为全局变量

        (2)每一个Declaration声明仅在一个页面中有效,如果要在每个页面都用到一些共同的声明,最好把它们写成一个单独的JSP网页,然后用<%@include%>或元素包含进来

        (3)Declaration元素必须是完整的Java语句,以分号结尾,并且不能产生任何输出。

        此外,JSP注释的格式为 <%– –%>,与 HTML注释 (<!– –>)的区别在于: JSP注释不会输出到客户端。

<%@ page language="java" import="java.util.*" contentType="text/html; charset=utf-8" pageEncoding="utf-8" errorPage="exception.jsp"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>test</title>
    <meta http-equiv="pragma" content="no-cache">
    <meta http-equiv="cache-control" content="no-cache">
    <meta http-equiv="expires" content="0">    
    <meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
    <meta http-equiv="description" content="This is my page">
  </head>
  <body>
    <% for(int i =0; i < 7; i++){
        out.print("<font size='" + i + "'>"); 
    %>
    地球</br></font> <!-- 会打印七遍“地球”-->
    <%}%>
  </body>
</html>



JSP 的三个编译指令


        JSP指令负责发送消息到JSP引擎,不包含业务逻辑,不修改out流,只是告诉JSP引擎JSP页面应该如何编译。JSP指令的作用范围仅限于包含指令本身的JSP页面。JSP编译指令有三种类型:page指令、include指令和taglib指令。


1、Page 指令

        Page指令定义了一些属性,通知关于JSP页面一般设置的Servlet引擎的属性。对于page指令,有以下几点需要注意:

        (1)可以在一个页面中引用多个 Page指令,但是除了import属性能多次使用之外,其他的属性都只能用一次

        (2)Page指令可以放在JSP文件的任何地方,但是为了JSP程序的可读性及养成好的编程习惯,最好还是放在JSP文件的顶部

        (3)Page 指令的errorPage属性和isErrorPage属性共同组成了JSP的异常机制,因此 JSP 可以不用像Java那样处理异常,即使是checked异常。前者用于设置该jsp页面出现异常时所要转到的页面,如果没设定,容器将使用当前的页面显示错误信息;后者用于设置该jsp页面是否作为错误显示页面,默认是false,如果设置为true,容器则会在当前页面生成一个 exception 内置对象


2、include 指令

        include指令指出所编译的JSP页面要包含的文件名(以相对URL形式),实质上是页面的静态导入(复制),甚至它会把目标页面的其他编译指令也包含进来(动态include则不会),所以被包括的文件内容会成为当前JSP页面的一部分。需要注意的是,通过include指令包含的文件的操作会在转译时期完成,即将所包含的页面以页面输出流的形式添加到原JSP页面中,如下所示。

<%@ page language="java" import="java.util.*" pageEncoding="utf-8" errorPage=""%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>My JSP 'include.jsp' starting page</title>
    <meta http-equiv="pragma" content="no-cache">
    <meta http-equiv="cache-control" content="no-cache">
    <meta http-equiv="expires" content="0">    
    <meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
    <meta http-equiv="description" content="This is my page">
  </head>
  <body>
    This is my JSP page. <br>
  </body>
  <%@ include file="test.jsp"%>
</html>
        使用include编译指令所包含的JSP文件被转译成如下的java片断:

out.write("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\r\n");
out.write("<html>\r\n");
out.write("  <head>\r\n");
out.write("    <title>test</title>\r\n");
out.write("\t<meta http-equiv=\"pragma\" content=\"no-cache\">\r\n");
out.write("\t<meta http-equiv=\"cache-control\" content=\"no-cache\">\r\n");
out.write("\t<meta http-equiv=\"expires\" content=\"0\">    \r\n");
out.write("\t<meta http-equiv=\"keywords\" content=\"keyword1,keyword2,keyword3\">\r\n");
out.write("\t<meta http-equiv=\"description\" content=\"This is my page\">\r\n");
out.write("  </head>\r\n");
out.write("  \r\n");
out.write("  <body>\r\n");
out.write("  \t");
for(int i =0; i < 7; i++){
out.println("<font size='" + i + "'>");   
out.write("\r\n");
out.write("  \t地球</br></font>\r\n");
out.write("    ");
out.write("\r\n");


JSP中的七个动作指令


        JSP的动作指令与编译指令不同,编译指令是通知JSP引擎的消息,而动作指令只是运行时的动作。编译指令在将JSP编译成Servlet时起作用;而动作指令通常可以替换成JSP脚本,它只是JSP脚本的标准化写法。现将这七个动作指令按使用方式分为以下两组:

        (1)、jsp:forward,jsp:include,jsp:param;

        (2)、jsp:useBean,jsp:setProperty,jsp:getProperty;


1、forward 指令


        执行页面转向,将请求的处理转发到下一个页面。使用 forward 指令进行请求转发时,其并不会重新向新页面发送请求,而只是采用新页面来对用户生成响应。也就是说,请求依然是一次请求,所以不会丢失请求参数,而且用户的请求地址也不会发生改变。此外,在使用forward 指令转发请求时可以增加额外的请求参数,格式如下:

<jsp:forward page="{relativeURL | <%=expression%>}">
        <jsp:param value="***" name="***"/>
</jsp:forward>

        forward指令被转译为Servlet时,会在其对应处添加以下代码片段:

if (true) {
	_jspx_page_context.forward("所forward的页面地址" + (("所forward的页面地址").indexOf('?')>0? '&': '?') + 
    org.apache.jasper.runtime.JspRuntimeLibrary.URLEncode("额外参数名", request.getCharacterEncoding()) + 
    "=" + org.apache.jasper.runtime.JspRuntimeLibrary.URLEncode("对应参数值", request.getCharacterEncoding()));
    return;    // 不执行该指令后面的内容
}
        结合以上说明,我们可以得出以下几个结论:

  • forward指令使用同一个request
  • forward后的语句不会继续发送给客户端;
  • 服务器内部转换 (转发的路径必须是同一个web容器下的url);
  • 可以传递额外的参数


2、include 指令


        用于动态引入一个JSP页面,但它不会导入被include页面的编译指令,仅仅将被导入页面的body内容插入本页面。此外,在使用forward 指令转发请求时可以增加额外的请求参数,格式如下:

<jsp:include page="{relativeURL | <%=expression%>}">
	<jsp:param value="***" name="***"/>
</jsp:include>
        include 指令被转译为Servlet时,会在其对应处添加以下代码片段:

org.apache.jasper.runtime.JspRuntimeLibrary.include(request, response, "被导入页面的地址" + 
	(("被导入页面的地址").indexOf('?')>0? '&': '?') + 
	org.apache.jasper.runtime.JspRuntimeLibrary.URLEncode("额外参数名", request.getCharacterEncoding())+ "=" + 
	org.apache.jasper.runtime.JspRuntimeLibrary.URLEncode("对应参数值", request.getCharacterEncoding()), 
	out, false);
        从以上代码片段我们可以看出,动态导入只是使用一个include()方法来插入目标页面的内容,而不是将目标页面完全融入当前页面中。因此,静态导入和动态导入有如下三点区别:

  • 静态导入是被导入页面的代码完全融入;而动态导入则在Servlet中使用include方法引入被导入页面内容;
  • 静态导入会导入被导入页面的编译指令;而动态导入只是插入被导入页面的body内容;
  • 动态导入还可以增加额外的参数。


useBean、setProperty、getProperty 指令

        useBean指令用于在JSP页面中初始化一个Java对象;setProperty 指令用于为 JavaBean 实例的属性设置值;getProperty 指令用于输出 JavaBean 实例的属性值。格式分别如下:

<jsp:useBean id="name" class="classname" scope="page | request | session | application"/>
<jsp:setProperty name="BeanName" property="propertyName" value="value"/>
<jsp:getProperty name="BeanName" property="propertyName"/>
        useBean、setProperty、getProperty 指令在使用上是并列的(不是嵌套的),形式如下:

<jsp:useBean id="p" class="com.tju.rico.bean.Person" scope="application"/>
<jsp:setProperty property="name" name="p" value="rico"/>
<jsp:getProperty property="name" name="p"/></br>
        形如上面的jsp代码被转译为Servlet时,会在其对应处添加以下代码片段:

//useBean 指令的转译
com.tju.rico.bean.Person p = null;
// 共享资源的序列化访问
synchronized (application) {    
    p = (com.tju.rico.bean.Person) _jspx_page_context.getAttribute("p", PageContext.APPLICATION_SCOPE);
    if (p == null){
        p = new com.tju.rico.bean.Person();
        _jspx_page_context.setAttribute("p", p, PageContext.APPLICATION_SCOPE);
    }
}
//内部调用了JavaBean的 setter
org.apache.jasper.runtime.JspRuntimeLibrary.introspecthelper(_jspx_page_context.findAttribute("p"), 
	"name", "rico", null, null, false);
//调用了JavaBean的 getter
out.write(org.apache.jasper.runtime.JspRuntimeLibrary.
	toString((((com.tju.rico.bean.Person)_jspx_page_context.findAttribute("p")).getName())));
        结合以上说明,我们可以在JSP中使用定义好的Bean,但是 Bean 必须具有以下特点:

  • Bean类应该没有任何公共实例变量多态(Bean中可以不定义属性,但必须提供对应的 getter/setter),也就是说,不允许直接访问实例变量,变量名称首字母必需小写
  • 必须要有一个不带参数的构造器。在JSP元素创建Bean时会调用空构造器 (JSP创建bean的时候使用不带参数的构造器);
  • 通过getter/setter方法来读/写变量的值,并且将对应的变量首字母改成大写。



三.JSP 九大内置对象概述及相关概念说明


        JSP脚本中包含九个内置对象,这九个内置对象都是 Servlet API 接口的实例,并且JSP规范对它们进行了默认初始化(由 JSP 页面对应 Servlet 的 _jspService() 方法来创建并初始化这些实例)。也就是说,它们已经是实例化的对象,可以直接使用。因此,概括地说,下表中所列出的内置对象具有以下四个特点:
  • 由JSP规范提供,不需要使用者来实例化;
  • 通过Web容器实现和管理;
  • 所有JSP页面均可使用;
  • 只有在脚本元素的表达式或代码段中才可使用(<%=使用内置对象%>或<%使用内置对象%>)而不能在JSP声明中使用,因为内置对象都是_jspService()方法的局部变量

内置对象说明类型作用域
pageContext页面上下文对象javax.servlet.jsp.PageContextPage
request请求对象javax.servlet.ServletRequestRequest
session会话对象javax.servlet.http.HttpSessionSession
application应用程序对象javax.servlet.ServletContextApplication
response响应对象javax.servlet.ServletResponsePage
out页面输出对象javax.servlet.jsp.JspWriterPage
config配置对象javax.servlet.ServletConfigPage
exception异常对象javax.lang.ThrowablePage
page页面对象java.lang.ObjectPage


1、JSP内置对象的实质


        我们通过一个JSP实例来认识JSP内置对象的实质,JSP代码片段(isErrorPage=”true”表明这是一个异常处理页面,因为只有异常处理页面才有exception对象)。其JSP代码片段如下:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" isErrorPage="true"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
...
</html>
        对应转译后的Java代码片段:

public final class exception_jsp extends org.apache.jasper.runtime.HttpJspBase
    implements org.apache.jasper.runtime.JspSourceDependent {

    ...

    public void _jspService(HttpServletRequest request, HttpServletResponse response)
        throws java.io.IOException, ServletException {
        //内置对象都是_jspService方法内部的局部变量
        PageContext pageContext = null;
        HttpSession session = null;
        Throwable exception = org.apache.jasper.runtime.JspRuntimeLibrary.getThrowable(request);
        if (exception != null) {
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
        ServletContext application = null;
        ServletConfig config = null;
        JspWriter out = null;
        Object page = this;

        try {
                _jspxFactory = JspFactory.getDefaultFactory();
                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();
                ......
        }
    }
}
        我们可以从上述JSP代码片段转译后的Java代码片段看出,request、response、session、application、out、pagecontext、config、page、exception 这九个内置对象都是_jspService()方法的局部变量(request 和 response 两个内置对象是该方法的形参,实质上也是它的局部变量) ,根据JSP转译规则,我们可以直接在JSP脚本(只有在脚本元素的表达式或代码段中才可使用,不能在JSP声明中使用)中使用这些对象,而无须创建它们。

        特别地,只有设置isErrorPage=”true”的页面才是异常处理页面,而且只有异常处理页面对应的Servlet才会初始化 exception 对象。

        一般地,我们称基于 Web 的应用为 B/S 应用,这些应用一般都是 请求/响应 架构的,即总是先由客户端发送请

求,服务器接收到请求并返回响应数据。现在,我们概述一下基于 请求/响应 架构的应用的运行机制,以及在其中扮

演重要角色的 浏览器 和 Web服务器 各自的作用,其示意图如下:



请求响应架构.png-41.9kB

        浏览器的作用主要有以下几个:

        (1)向远程服务器发送请求; 

        (2)读取远程服务器返回的字符串数据; 

        (3)负责根据返回的字符串数据渲染出一个丰富多彩的页面。

        Web服务器负责接收客户端请求,每当接收到客户端连接请求之后,Web服务器就会单独开启一个线程为该客户端提供服务。对于每次客户端的请求而言,Web 服务器大致需要完成如下几个步骤:

        (1)为客户端请求启动单独的线程; 

        (2)使用 I/O 流读取用户请求的二进制流数据; 

        (3)从请求数据中解析请求参数; 

        (4)处理请求; 

        (5)生成响应数据; 

        (6)使用 I/O 流向客户端发送请求数据。

        上面六个步骤中,第1、2和6步是通用的,由 Web服务器来完成,但第3、4和5步则存在差异。因为不同请求的请求参数不同,处理请求的方式也不同,所生成的响应自然也不同,那么Web服务器如何执行这些步骤呢?

        实际上,Web服务器会调用Servlet的_jspService()方法来处理这些事情。我们知道,在转译时期,JSP中的静态页面和java脚本都会转换为_jspService()方法的执行代码,这些执行代码就负责完成解析请求参数、处理请求、生成响应等功能,而Web服务器则负责完成多线程、网络通信等底层实现。

        Web 服务器执行到第三步得到请求参数后,会根据这些请求参数来创建 HttpServletRequest、HttpServletResponse 等对象,作为调用 _jspService()方法的参数。由此可见,一个Web服务器必须实现Servlet API中绝大部分接口提供实现类(也就是说,Web服务器要实现J2EE中的Servlet、JSP等标准)


2、JSP/Servlet的通信与内置对象的作用域


        我们知道,Web应用中的 JSP、Servlet 等程序都是由Web服务器来调用的,JSP与Servlet之间通常不会相互调用,那么在它们之间如何共享、交换数据以便相互通信就成为了一个极为关键的问题。 

        为了解决这个问题,几乎所有的Web服务器都会提供四个类似Map的结构,即page、request、session 和 application,并允许JSP/Servlet在这四个类似Map的结构中存取数据,而这四个类似Map结构的数据的区别仅在于作用域不同。特别地,JSP中pageContext、request、session和application 四个内置对象分别用于操作page、request、session和application范围内的数据。

        作用域是指变量的有效范围。我们通过一个例子进行简单说明,假设这样一个场景:当我们访问index.jsp 的时候,分别对pageContext、request、 session 和 application四个作用域中的整型变量进行累加。然后,从 index.jsp forward到test.jsp,并在test.jsp里再进行一次累加,然后显示出这四个整数来。从显示的结果来看,我们可以直观的看到:

  • page 范围里的变量无法从index.jsp传递到test.jsp,只要页面跳转了,它们就不见了;
  • request 范围里的变量可以跨越forward的前后的两页,但是只要刷新页面,它们就重新计算了;
  • session 范围里的变量一直在累加,开始还看不出区别,但只要关闭浏览器,再次重启浏览器访问这个页面,它们就重新计算了;
  • application 范围里的变量一直在累加,除非你重启tomcat,否则它们会一直变大。

        (1) 如果我们把变量放到pageContext里,就说明这个变量的作用域是page,它的有效范围只在当前jsp页面里。也就是说,从把变量放到pageContext开始,到jsp页面结束,你都可以使用这个变量。 

        (2) 如果把变量放到request里,就说明这个变量的作用域是request,它的有效范围是当前请求周期。所谓请求周期,就是指从http请求发起,到服务器处理结束并返回响应的整个过程。在这个过程中可能使用forward的方式跳转了多个jsp页面,但由于仍然是同一个请求,因此在这些页面里,我们都可以使用这个变量。 

        (3) 如果把变量放到session里,就说明这个变量的作用域是session,它的有效范围是当前会话。所谓当前会话,就是指从用户打开浏览器开始,到用户关闭浏览器这中间的过程,这个过程可能包含多个请求和响应。也就是说,只要用户不关浏览器,服务器就有办法知道这些请求是一个人发起的,整个过程被称为一个会话(session),而放到会话中的变量,就可以在当前会话范围内使用。 

        (4) 如果把变量放到application里,就说明这个变量的作用域是application,它的有效范围是整个应用。所谓整个应用是指从应用启动,到应用结束。特别地,我们没有说“从服务器启动,到服务器关闭”,这是因为一个服务器可以部署多个应用,只要你结束了当前应用,这个变量就失效了。

        与上述三个作用域不同的是,application 作用域里的变量的存活时间是最长的,如果不进行手工删除,它们就一直可以使用。此外,application里的变量可以被所有用户共用,也就是说,如果用户甲的操作修改了application中的变量,用户乙访问时得到的是修改后的值。这在其他作用域中是不会发生的,page、 request 和 session 都是完全隔离的,无论如何修改都不会影响其他用户。


3、application对象


        application对象代表Web应用本身,因此我们常常使用application来操作 Web 应用层面的相关数据。我们其有如下两个作用:

1) 通过以下两个方法在整个Web应用的多个JSP、Servlet间共享Web应用状态层面的数据:

  • setAttribute(String attrName, Object value)
  • getAttribute(String attrName)
2) 访问获取 Web 应用的配置参数:

        使用 getInitParameter(String attrName) 方法获取 Web 应用的配置参数,这些参数应该在 web.xml 文件中使用 <context-param></context-param>元素进行配置,每个元素配置一个参数,例如:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" 
    xmlns="http://java.sun.com/xml/ns/javaee" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
    http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

  <!-- Web 应用配置参数 -->
  <context-param>
    <param-name>admin</param-name>
    <param-value>Rico</param-value>
  </context-param>
  <context-param>
    <param-name>campus</param-name>
    <param-value>NEU & TJU</param-value>
  </context-param>
</web-app>
        通过这种方式可以将一些配置信息放在web.xml文件中配置,从而避免使用硬编码的方式写在代码中,从而更好地提高程序代码的可移植性。


4、pageContext 对象


        pageContext 对象代表JSP页面上下文,其主要用于访问JSP间的共享数据,其可以访问 page、request、session 和 application 范围内的变量。通过以下两个方法访问共享数据:

        (1)、get/setAttribute(String name):取得取得/设置 page 范围内的 name 属性; 

        (2)、get/setAttribute(String name, int scope):取得/设置指定范围内的 name 属性,其中 scope 可以是如下四个值;

  • public static final int PAGE_SCOPE = 1;
  • public static final int REQUEST_SCOPE = 2;
  • public static final int SESSION_SCOPE = 3;
  • public static final int APPLICATION_SCOPE = 4;


5、request 对象


        request 对象是JSP中的重要对象,每个request 对象封装一个用户请求,并且所有的请求参数都被封装到request 对象中,因此request对象是获取请求参数的重要途径。此外,request 可代表本次请求范围,用于操作 request范围的属性。总的来说,该对象有如下两个作用:

(1)获取请求参数/请求头

        Web 应用是请求/响应架构的应用,浏览器发送请求时总会附带一些请求头,还可能包含一些请求参数发送给服务器。实际上,请求头和请求参数都是用户发送到服务器的数据,只不过前者通常由浏览器自动添加,而后者需要开发人员控制添加。服务器端负责解析请求头/请求参数的就是 JSP/Servlet,而 JSP/Servlet 获取请求参数的途径就是 request 对象,其提供如下几个方法来获取请求参数:

  • getParameter(String name)
  • getParameterMap()
  • getParameterNames()
  • getParameterValues(String name)

        此外,客户端发送请求的方式一般有两种:

  • GET 方式的请求:直接在浏览器地址栏输入访问地址所发送的请求或提交表单的默认请求参数发送方式;
  • POST 方式的请求:以 post 方式提交表单,发送请求参数。该种方式一般将请求参数放在HTML HEADER 中传输,因此用户不能在地址栏看到请求参数值,安全性较高

        因此,一般建议以POST的方式发送请求。特别地,对于通过提交提交表单发送请求参数而言,需要注意两点:

  • 只有具有name属性的表单域才生成请求参数;
  • 若多个表单域有相同的name属性,则这些表单域只生成一个请求参数,只是该参数具有多个值。

(2)操作 request范围的属性

        使用 request对象的如下两个方法可以设置/获取request范围的属性(特别注意的是,forward前后仍是同一个请求):

  • setAttribute(String name, Object o)
  • getAttribute(String name)

(3)执行 forward/include 操作

        HttpServletRequest 提供了getRequestDispatcher(String path)方法去执行 forward/include 操作,也就是代替JSP所提供的 forward/include 动作指令,其中参数path就是希望 forward/include 的路径。具体方式如下:

request.getRequestDispatcher("目标路径").forward/include(request, response);
        上述代码的语义就是 将请求forward到了目标页面或将目标页面包含到了当前页面 。需要注意的是,以该种方式进行 forward/include 时, path参数必须以 “/” 开头



6、session 对象


        session 对象代表一次用户会话,具体指从用户打开浏览器开始,到用户关闭浏览器,这个可能包含多个请求和响应的过程。因此,我们常常使用 session 来跟踪用户的会话信息,如判断用户是否登录,或者在包含购物车的应用中用于追踪用户购买的商品等。我们可以通过以下两个方法进行存取session范围内的属性:
  • setAttribute(String attrName, Object value)
  • getAttribute(String attrName)

        特别地,通常只应该把与用户会话状态相关的信息放入session范围内,而不应该仅仅为了两个页面间交换信息,就将该信息放入session范围内Session 的属性值可以是任何可序列化的 Java 对象。


7、 response 对象


        response 对象代表服务器对客户端的响应。但大部分情况下,程序无须使用 response 对象来响应客户端请求,因为有一个更为简单的响应对象—— out,它代表页面输出流,直接使用out生成响应更简单。不过,out是JSPWriter 的实例,是字符流输出对象,无法输出非字符内容。因此,若需要在JSP页面动态生成一副位图或者输出一个PDF文档,则必须使用 response 作为响应输出,此不赘述。此外,response 对象还有两个重要作用,即重定向和操作Cookie。


(1) 重定向

        请求重定向与 forward 不同,重定向会丢失所有的请求参数和 request范围内的属性。因为重定向将生成一个全新的请求,与前一次请求不在同一个 request 范围内,所以会丢失原有请求的所有请求参数和request范围内的属性。我们通过以下方式进行重定向:

response.sendRedirect("request/MyJsp.jsp");  
        特别需要注意的是, 请求重定向方法 sendRedirect() 的path参数不必以 “/” 开头(若以 “/” 开头,其代表的是 “http://localhost:8080/“)


(2) 操作Cookie

        Cookie 通常用于网站记录客户的某些信息,例如客户的用户名等。一旦用户下次登录,网站就可以获取到客户的相关信息,根据这些客户信息,网站可以对客户提供更为友好的服务。Cookie 与 Session的最大不同在于:session会随着浏览器的关闭而失效,但Cookie会一直存放在客户端的机器上,除非超出Cookie的生命周期(Cookie的默认生命周期就是整个会话)。通常,如下面的例子所示,在客户端增加一个 Cookie 分为如下三个步骤:

  • 创建Cookie实例,其构造器为 Cookie(String name, String value);
  • 设置Cookie的生命周期;
  • 向客户端增加Cookie。

// 获取请求参数
String name = request.getParameter("name");
// 以获取到的请求参数为值,创建一个Cookie对象
Cookie c = new Cookie("username" , name);
// 设置Cookie对象的生存期限
c.setMaxAge(24 * 3600);
// 向客户端增加Cookie对象
response.addCookie(c);
out.print(name);
        访问客户端的Cookie时,需要使用request对象 。request对象提供了getCookies()方法,该方法将返回客户端机器上所有Cookie组成的数组,遍历该数组的每个元素,找到希望访问的Cookie即可,例如:

Cookie[] cookies = request.getCookies();
for(Cookie cookie : cookies){
    out.print(cookie.getName() + " : " + cookie.getValue() + "</br>");
}
        特别地,Cookie值不允许出现中文字符,如果需要值为中文内容的Cookie时,可以借助于 java.NET.URLEncoder/URLDecoder 进行转换。


8、config 对象


        config 对象代表当前 JSP 配置信息,但JSP 页面通常无需配置,因此也就不存在配置信息,所以在 JSP 页面较少使用这个对象。但是,其在 Servlet 中经常使用(javax.servlet.ServletConfig 的实例)。因为Servlet需要在web.xml中进行配置,可以指定配置参数。因此,我们常常使用 getInitParameter(String paramName) 来获取Servlet的配置参数,例如:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" 
    xmlns="http://java.sun.com/xml/ns/javaee" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
    http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

  <!-- Servlet 配置 -->
  <servlet>
    <servlet-name>config</servlet-name>
    <servlet-class>com.edu.tju.rico.servlet.configServlet</servlet-class>
    <init-param>
        <param-name>name</param-name>
        <param-value>Rico</param-value>
    </init-param>
    <init-param>
        <param-name>age</param-name>
        <param-value>24</param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>config</servlet-name>
    <url-pattern>/servlet/config</url-pattern>
  </servlet-mapping>
</web-app>


9、exception 对象


        exception 对象是Throwable的实例,代表JSP脚本中产生的异常和错误 ,是JSP页面异常机制的一部分,并且只有当isErrorPage属性设为true时才可以访问exception内置对象。我们知道,在JSP脚本中通常无需处理异常,即使是checked异常。事实上,JSP脚本所有可能出现的异常都可以交给错误处理页面处理。看下面非异常处理页面转译成Servlet后的例子(片段):

public void _jspService(HttpServletRequest request, HttpServletResponse response)
        throws java.io.IOException, ServletException {
    // 内置对象的声明
    try {
      // 内置对象的初始化
      out.write("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\r\n");
      out.write("<html>\r\n");
      //... ...
      out.write("  </body>\r\n");
      out.write("</html>\r\n");
    } catch (Throwable t) {
      if (!(t instanceof SkipPageException)){
        out = _jspx_out;
        if (out != null && out.getBufferSize() != 0)
          out.clearBuffer();
        if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
      }
    } finally {
      if (_jspxFactory != null) 
        _jspxFactory.releasePageContext(_jspx_page_context);
}
        我们可以看到, 之所以在JSP脚本中通常无需处理异常,包括checked异常,是因为当JSP被转译成Servlet后,其静态HTML和动态java脚本都将置于 try 语句块中 。特别地, exception 对象只有在异常处理页面中才有效 。看下面异常处理页面转译成Servlet后的例子(片段):

public void _jspService(HttpServletRequest request, HttpServletResponse response)
        throws java.io.IOException, ServletException {
    // 内置对象的声明
    PageContext pageContext = null;
    //...
    PageContext _jspx_page_context = null;
    Throwable exception = org.apache.jasper.runtime.JspRuntimeLibrary.getThrowable(request);
    if (exception != null) {
      response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    }
    
    try {

      // 内置对象的初始化
      pageContext = _jspxFactory.getPageContext(this, request, response,
                null, true, 8192, true);
      //...
      //使用内置对象pageContext给_jspx_page_context赋值
      _jspx_page_context = pageContext;

      out.write("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\r\n");
      out.write("<html>\r\n");
      ... ...
      out.write("</html>\r\n");
    } catch (Throwable t) {
      if (!(t instanceof SkipPageException)){
        out = _jspx_out;
        if (out != null && out.getBufferSize() != 0)
          out.clearBuffer();
        if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
      }
    } finally {
      if (_jspxFactory != null) _jspxFactory.releasePageContext(_jspx_page_context);
    }
}
        如上面代码片段所示, 一旦 try 语句块 捕获到JSP脚本的异常,并且 _jspx_page_context 不为 null,就会由该对象来处理异常 。实际上,该对象的处理逻辑也很简单:如果该页面的page指令指定了errorPage属性,则将请求forward到errorPage属性所指定的页面,否则使用系统页面来输出异常信息即可。特别地,JSP声明部分仍然需要处理checked异常。



本文内容主要转载自:

http://blog.csdn.net/justloveyou_/article/details/55824500

http://blog.csdn.net/justloveyou_/article/details/57154560


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值