jsp深入分析

JSP核心技术——JSP引擎内幕

  主题:

l  幕后

l  多线程和持久性

l  隐含对象

l  JSP的生命期

l  编译JSP

l  JSP的性能调整

太多的文章都讨论过JSP的背景、语法和元素。很少讨论JSP引擎如何工作的许多细节技术。开发优秀的JSP应用程序或者说要做一个优秀的JSP程序员,至少需要了解JSP引擎是如何工作应该是必备的基本知识。

幕后

JSP引擎接收到对请求时,它将JSP页面的静态数据和动态元素转换成Java代码段。

JSP元素内包含的动态数据已经是Java代码,所以这些段不必修改就可以使用,而静态数据被包装进out.write()方法内——形成java代码;然后,这些Java代码段被顺序放进特殊的包装器类——即jsp被翻译成的类。

JSP包装器由JSP引擎自动创建,它处理支持JSP所设计的大多数工作,而不需开发者干预。包装器通常扩展javax.servlet.Servlet类,这意味着JSP实际上被转换为Java Servlet代码的特殊形式。在许多方面,JSP可以被看作为一种用于创建Java Servlet的宏语言;JSP页面实际上提供了一个到Java Servlet API的以页面为中心的接口。

然后,源代码被编译为功能完全的Java Servelt。这个由JSP引擎创建的新Servlet处理基本的异常处理、输入/输出、线程以及大量与网络和协议相关的其他任务。实际上,是由新生成的Servlet处理请求并生成返回给请求JSP页面的客户的输出。

1.1  重新编译

JSP引擎可以被设计为在接到新的请求时重新编译每个页面。每个请求产生它自己的Servlet来处理相应。幸运的是,JSP采用一种更高效的方式。

JSP页面和Java Servlet为每个页面创建一个实例,而不是为每个请求创建一个实例。当接到新的请求时,只是在已生成的Servlet内创建一个线程。这意味着对JSP页面的第一个请求将生成一个新的Servlet,但以后的请求只是重用第一个请求所创建的Servlet

 

注意:第一个请求时的延迟

当一个JSP页面第一次通过JSP引擎运行时,在收到响应前可能有较长的延迟。出现延迟的原因是,JSP引擎需要将JSP转换为Java代码、进行编译以及将它初始化,然后才能响应第一个请求。

以后的请求会利用已编译的Servlet。第一个请求后的请求应该会更快地得到处理。

 

有些特殊地事件可以通过JSP引擎何时重新编译JSP页面。为了管理重新编译,JSP引擎保持JSP页面代码的记录,并在源代码改变时重新编译页面。JSP的不同实现对于何时重新编译有不同的规则,但所有的引擎必须在JSP源代码改变时重新编译页面。

记住,JSP页面的外部资源,例如JavaBean或者包含(include)的JSP页面可能不会造成页面重新编译。另外,不同的JSP引擎对于何时重新编译页面有不同的规则。

 

注意:预编译协议

JSP 1.1起,规范里定义了一种预编译页面的方式。要想预编译特定的JSP,必须带着jsp_precomplie参数建立对此JSP的HTTP请求。

例如,键入如下URL

http://www.javadesktop.com/core-jsp/catalog.jsp?jsp_precompile="true"

如果此JSP还未编译过或者JSP代码已经改变,那么就会编译它。

1.2  ServletJSP的关系

因为JSP页面被转换为Java Servlet,所以JSP表现出的许多行为与Java Servlet一样。JSPJava Servlet继承了强大的功能和几个特点。

Java Servlet通过创建一个在JVM内运行的持久的应用程序进行工作。处理新的请求的方法实际上是,在这个持久的应用程序内运行一个新的线程。对JSP页面的每个请求在对应的Java Servlet内有一个新线程。

Java Servlet还为JSP开发人员提供几个内建方法和对象。它们提供一个到Servlet的行为和JSP引擎的直接(接口)通道。

 

多线程和持久性

JSPJava Servlet继承了多线程和持久性。持久性允许对象在第一次创建Servlet时初始化,所以JSP Servlet的物理内存内容在请求之间保持不变。可以在持久空间内创建变量,这就允许Servlet执行缓存、会话跟踪以及在无状态环境内没有的其他功能。

JSP开发员接触不到线程开发所涉及的许多问题。JSP引擎处理创建、销毁和管理线程所涉及的大多数工作。这样JSP开发员就不必承担多线程开发的重担。但是JSP开发员需要了解几个影响JSP页面的多线程编程问题。

线程可能不经意地损害其他线程。在这种情况下是,JSP程序员所需要知道何时以及如何使页面不被线程化。

2.1      持久性

<%!

    int counter;//当所在jsp页面被转换为.java文件时,该语句将作为该类的属性!是可持久的

    int j;

    JspWriter out;

    public void a(){//当所在jsp页面被转换为.java文件时,该语句将被作为该类的方法!是可持久的

       try{

           out.println("123465xxx");

       }catch(Exception e){

           e.printStackTrace();

       }

    }

%>

<%

out.println("123465xiaocui");//当所在jsp页面被转换为.java文件时,该语句将被放在_jspService...)方法内并输出!是不可持久的!

%>

因为Servlet只被创建一次,然后作为不变的实例一直运行,所以可以创建持久的变量。同一Servlet的所有线程共享持久的变量。对这些持久变量值的改变被反映到所有线程中。这里的持久变量在java代码里具体体现就是作为该类的属性!试想如果是某个方法所声明的变量,那么每次线程启动时用到得变量值都是最原始的值。

JSP开发员的角度来看,所有在声明标记(<%! … %>)内创建的对象和变量都是持久的。在线程内创建的对象和变量不是持久的。在ScripletExpressionAction标记内的代码将在一个新的请求线程内运行,因此不会创建持久的变量或对象。

拥有持久的对象允许开发者在页面请求之间跟踪数据。这就允许使用内存内的对象实现缓存、计数器、会话数据、数据库连接的缓冲以及许多其他任务。程序清单4-1显示了一个使用持久变量的计数器。页面首次装载时,创建变量counter。因为Servlet一直在内存中运行在,此变量将一直存在直到此Servlet重新启动。每次请求页面时,变量counter递增并显示请求者。每个用户应该看到一个页面计数器,其数字比上次访问此页面时所见到的数字大1

 

程序清单4-1

count.jsp

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">

<%!

  int counter;

%>

<HTML>

 

<STYLE>

.pageFooter {

  position: absolute; top: 590px;

  font-family: Arial, Helvetica, sans-serif;

  font-size: 8pt; text-align: right;

}

</STYLE>

 

<BODY>

<DIV>

This page has been accessed

<%

  counter++;

  out.println(counter);

%>

times since last restarted.

</DIV>

</BODY>

 

</HTML>

 

 

2.2  线程的危险

然而,对象持久性也存在一些潜在的问题。为防止这些问题,JSP开发员需要理解并控制这些问题的出现。

那些使持久性有价值的因素也带来了一个严重问题——线程竞赛(Race Condition)。发生线程竞赛的情况是:一个线程正准备使用数据,而第二个线程在第一个线程使用数据前修改了此数据。

考虑以上的例子(程序清单4-1)有两个线程在运行的情况,仔细注意counter变量的值。

线程1——用户A请求此页面。

线程2——用户B请求此页面。

线程1——counter1

线程2——counter1

线程1——counter被显示给用户A

线程2——counter被显示给用户B

在此情况下,用户A所看到的数据实际是用户B应该看到的。这显然不是想要的结果。

以上例子所造成的问题是微不足道的,只不过是用户A看到了一个错误的页面计数。但是,线程竞赛可能就这样如此轻易造成非常严重的问题——设想一下,如果在给在线购物的用户发帐单时出现线程竞赛,那么会有什么后果。

不论线程竞赛所造成的问题是否重要,都要解决这个问题,这是个良好的编程习惯。线程执行的先后不能预先确定,所以线程竞赛的后果可能无规律地出现。线程竞赛可能难以定位。

 

2.3      线程安全

考虑线程安全时,最重要的是记住线程可以显著提高性能。线程安全几乎总是通过“禁止”代码的后些部分的线程化来实现的。

另一个重点是,线程竞赛只在使用持久变量时出现。如果一个应用程序所用的所有变量和对象都是由线程创建的,那么就不会发生线程竞赛。在这些情况下,不会发生线程化的问题。

1.  SingleThreadModel

获取线程安全的最简单方法也带来了性能下降。这个方法就是关闭整个页面的线程化。关闭线程化是个糟糕的选择,在正常情况下应该避免,因为它是通过牺牲许多优势来避免潜在的问题。

JSP通过page指令属性isThreadSafe='false'提供了关闭线程化的方法。这使页面在SingleThreadModel下创建,SingleThreadMode只允许页面每次处理一个请求。

这里的siThreadSafe=true’相当于你的servlet实现了接口

SingleThreadModel,这个接口的作用是这样的:当请求的实例数大于当前实例数的时候才会重新去创建个实例,然后把这个实例放到Stack中(实例池)中。换句话就是说当请求的实例数小于等于当前的实例数时它会从statck中取一个空闲的实例来完成请求!我们可以看出:servlet自身的非静态属性有安全隐患(因为你可能两次请求使用的是一个servlet对象),静态属性一定有安全隐患(所有的请求都公用这个属性),servlet之外的类也可能存在安全隐患

这个选项还不能百分之百有效。在sessionapplication范围内创建的变量可能仍然受多个线程的影响。但是servlet类的属性

2.       synchronized( )

一个保护变量不受线程竞赛影响的更实用而且高效的方法是,使用Java的同步接口。这个接口显露一个锁定机制,这个机制每次只允许一个线程处理某个代码块。

可以同步整个方法使其不受线程竞赛影响,办法是在方法的识别标志中使用synchronized关键字。这将保护在此方法内访问的所有持久变量。在此情况下,每次只有一个线程可以访问此方法。

也可以通过将代码包装在synchronized( )块内来使代码同步。在此情况下,需要有一个参数表示对象被锁定。

注意:同步的标志

派生自java.lang.Object的对象可以用作同步块的参数。每个对象有一个“块标志”,synchronized( )使用这个标志管理线程化(替代C程序中所用的互斥和信号量)。Java中的原始数据类型(8中基本类型)没有此标志,因此不能用作同步块的锁。

在创建同步块时,使用代表被同步数据的对象通常更高效。例如,如果一个写磁盘的同步块被编写成foo对象,最好使用synchronized(foo)。当然,也可以使用thispage对象,但同步块每次运行时都堵塞整个页面,这回造成瓶颈。

清单4-2是一个新的计数器页面例子,它使用同步块。在此例中,两行代码被放在同步块内。每次只有一个线程可以处理这个块。每个页面将锁定页面对象,递增变量,显示变量,然后解锁这个页面对象。

 

程序清单4-2

count2.jsp

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">

<%!

  int counter;

%>

<HTML>

 

<STYLE>

.pageFooter {

  position: absolute; top: 590px;

  font-family: Arial, Helvetica, sans-serif;

  font-size: 8pt; text-align: right;

}

</STYLE>

 

<BODY>

<DIV>

This page has been accessed

<%

  synchronized (page){

    counter++;

    out.println(counter);

  }

%>

times since last restarted.

</DIV>

</BODY>

 

</HTML>

 

 

隐含对象

Servlet还创建几个由JSP引擎使用的对象。这些对象中的大多数被显露给JSP开发员,并且可以直接调用而不必显式声明。

 

3.1  out对象

JSP的主要功能是描述发送到客户请求的响应输出流中的数据。这个输出流通过隐含的out对象显露给JSP开发员。

Out对象是javax.servlet.jsp.JspWriter对象的实例。这个对象可以代表输出流、经过滤的输出流或来自其他JSP页面的嵌套的JspWriter对象。但是输出应该不被直接发送到输出流,因为在JSP的生命期内可能有多个输出流。

根据页面是否被缓冲,初始的JspWriter对象的初始化有所不同。缺省情况下,每个JSP页面都打开了缓冲,这可以提高性能。缓冲功能很容易关闭,只要使用page指令的buffered='false'属性即可。

缓冲的out对象以块为单位收集和发送数据,这通常会提供最好的总体吞吐量。使用缓冲时,PrintWriter在第一个块被发送时创建,也就是在第一次调用flush()时。

如果不缓冲输出,将立即创建PrintWriter对象并引用out对象。在此情况下,发送到out对象的数据立即被发送到输出流。创建PrintWriter对象时将使用由服务器决定的缺省设置和头信息。

 

注意:HTTP头和缓冲

HTTP使用响应头描述服务器以及定义发往客户的数据的某些方面信息。这可能包括页面的MIME内容类型、新的cookie、转发URL或其他HTTP“动作”。

JSP允许开发者在创建OutputStream(即PrintWriter前改变响应头的内容。一旦建立了OutputStream,头信息就不能改变了,因为它已被发送到客户。

在缓冲的out对象的情况下,直至缓冲区第一次刷新时才建立OutputStream。缓冲区被刷出很大程度上取决于page指令的autoFlushbufferSize属性。通常,最好在有任何数据被发送到out对象前设置头信息。

对无缓冲的out对象,很难设置页面头。当无缓冲的页面建立时,几乎立即就建立了OutputStream

在建立OutputStream后发送的头可能会造成大量不正常的结果。一些头被简单地忽略,其他的头可能产生异常,例如IllegalStateException

 

JspWriter包含的方法大多数与java.io.PrintWriter类一样。但是JspWriter有另外几个用于处理缓冲的方法。与PrintWriter对象不同,JspWriter抛出IOExceptions。在JSP中,这些异常需要显式捕获和处理。

 

注意:autoFlush( )

JSP中,缺省的缓冲行为是在缓冲区满了时自动刷出缓冲区。但是,有时JSP实际上是直接和另一个应用程序通信。在此情况下,不“何时”的行为可能是在超出缓冲区时抛出异常。

设置page指令的属性autoFlush='false'将造成缓冲区溢出而抛出异常。

 

3.2  request对象

客户每次请求页面时,JSP引擎创建一个代表此请求的新对象。这个对象是javax.servlet.http.HttpServletRequest的实例,并且具有描述此请求的参数。这个对象通过request对象显露给JSP开发员。

通过request对象,JSP页面可以对从客户机那儿接收的输入做出反应。请求参数被存储在特殊的键值对中,可以使用request.getParameter(name)方法获取请求参数。

request对象还提供了几个用于获取头信息和cookie数据的方法。它提供了认识客户和服务器的方式。

requst对象被限制在request范围内。不论page指令如何设置页面的范围,request对象总是为每个请求重新创建。对于来自一个客户的请求,都有一个request对象与之对应。

关于request的对象及其方法的可以自己再查找一些相关资料。

 

3.3  response对象

服务器要创建request对象,也要创建一个来代表对客户的响应。这个对象是javax.servlet.http.HttpServletResponse的实例,并且作为response对象显露给JSP开发员。

response对象处理返回给客户的数据流。out对象与response对象关系非常密切。response对象还定义了创建新的HTTP头的接口。通过response对象,JSP开发员可以添加新的cookie或数据标记,改变页面的MIME内容类型,或者开始“服务器推”方法。JSP页面还包含关于HTTP的信息,从而能够返回HTTP状态码,例如使页面重定向。

关于response的对象及其方法的可以自己再查找一些相关资料。

3.4  pageContext对象

pageContext对象用于代表整个JSP页面。它适于作为访问关于页面的信息的方法,同时回避了大多数实现细节。

这个对象为每个请求存储request对象和response对象的引用。applicationconfigsessionout对象通过访问此对象的属性派生出来。PageContext对象还包含关于发给JSP页面的指令的信息,这包括缓冲信息、errorPageURL和页面范围。

pageContext对象不只是作为数据资料库。它还管理嵌套的JSP页面,执行forwardinclude动作所涉及的大多数工作。pageContext对象还处理未被捕获的异常。

JSP作者的角度来看,这个对象在获取关于当前JSP页面环境的信息方面很有用。如果你要创建组件,而组件的行为根据JSP page指令的不同而有所变化,那么pageContext对象特别有用。

3.5  session对象

session对象用于在使用无状态连接协议(如HTTP)的情况下跟踪关于某个客户的信息。会话可以用于在客户请求之间存储任意信息。

每个会话应该只对应于一个客户,并且可以跨多个请求。会话通常通过URL重写或cookie来跟踪,但是跟踪进行请求的客户的方法对于session对象并不重要。

session对象是javax.servlet.http.HttpSession的实例,其行为方式与Java Servlet下的session对显完全相同。

关于session的对象及其方法的可以自己再查找一些相关资料。

3.6  application对象

application对象是产生的ServletServletContext的直接包装器。它所具有的方法和接口与Java Servlet编程中的ServletContext对象一样。

这个对象在JSP页面的整个生命期内代表此页面。当JSP页面初始化时,创建这个对象;当使用jspDestory( )方法删除JSP页面时,或者JSP页面重新编译时,或者JVM崩溃时,将删除这个对象。JSP页面内所用的所有对象都可以使用此对象中存储的信息。

application对象还提供JSP与服务器进行通信的方法,此过程不涉及“请求”。这有助于寻找关于文件MIME类型的信息,直接向服务器发送日志信息,或者与其他服务器通信。

3.7  config对象

config对象是javax.servlet.ServletConfig的实例。这个对象是产生的ServletServletConfig对象的直接包装器。它所具有的方法和接口与Java Servlet编程中的ServletConfig对象一样。

这个对象允许JSP开发员访问ServletJSP引擎的初始化参数。它有助于获取标准的全局信息,例如路径或文件位置。

3.8  page对象

这个对象是对页面对象的实例的实际引用。它可以看作是代表整个JSP页面的对象。

JSP页面第一次被初始化时,通过获取对this对象的引用来创建page对象。所以,page对象实际上是this对象的直接同义词。

但是,在JSP的生命期内,this对象不能引用页面本身。在JSP页面的环境内,page对象将保持不变,并且总是代表整个JSP页面。

3.9  exception元素

exception对象是包含从前一个页面抛出的异常的包装器。它通常用于根据错误条件产生合适的响应。

前一个页面有未被捕获处理的异常而且使用了<%@ page errorPage=" … " %>标记时,可以使用这个对象。

4  JSP的生命期

JSP引擎有三个方法用于管理JSP页面和它产生的Servlet的生命期。

JSP页面的核心通过使用产生的_jspService方法来处理。它由JSP引擎自身创建和管理_jspService不应该由JSP开发者管理;如果这么做,会造成灾难性后果。_jspService方法代表JSP页面处理所有请求和响应。实际上,所有线程调用_jspService方法。

注意:保留的名称

JSP规范特别保留了以jsp_jspjspx_jspx开头的方法和变量。JSP开发者可以访问具有这些名称的方法和变量,但是不能创建这样的新方法和新变量。JSP引擎期望自己控制这些方法和变量,所以改变它们或者创建新的方法和变量可能够了会造成奇怪的结果。

 

另两个方法, jspInit( )jspDestroy( ),可由JSP开发员进行覆盖。实际上,如果JSP开发员不创建它们,它们并不存在。它们在管理JSP页面的生命期方面扮演特殊角色。另外你创建或者不创建jspInit( )jspDestroy( )jsp都会创建

_jspInit( )_jspDestroy( ),这两个方法也是用于初始化和销毁的!~

4.1  jspInit( )

方法原型:void jspInit( )

jspInit( )方法只在JSP页面第一次被请求时运行一次。JspInit( )方法保证在任何请求被处理前被处理完。它与Java ServletJava Applet中的init()方法一样。

jspInit( )方法允许JSP作者为每个请求创建和装载所需的对象。这有助于装载状态信息、创建数据库连接缓冲池,以及执行只需在JSP页面首次启动时执行一次其他任务。

4.2  jspDestroy( )

方法原型:void jspDestroy( )

ServletJVM卸载时,服务器调用jspDestroy( )方法。它与Java ServletJava Applet中的destroy( )方法一样。

jspInit( )方法不同,不保证这个方法被执行。服务器在每个线程后尽力尝试运行这个方法。因为这个方法在处理完成后调用,有些情况可能使它不被执行。如服务器崩溃。

JspDestroy( )允许JSP开发员在Servlet完成前执行代码。它通常用于释放资源或关闭仍然打开着的连接,也可以用于释放存储状态信息或其他应该在实例之间存储的信息。

4.3  JSP的生命期概述

在第一次请求或预编译时,调用jspInit( ),此时页面开始运行,等待请求。现在,_jspService处理大多数事务、获取请求、运行线程并且产生响应。最后,当接到关闭信号时,调用jspDestroy( )方法。JSP页面的整个生命期见图4-1

 

4.4  使用jspInit( )jspDestroy( )的计数器

前面的页面计数器例子所用的变量只存储在运行的Servlet的内存中。它没有被写入磁盘,所以如果JSP页面重新启动,变量将被重置。

在程序清单4-3所示的例子中,jspInit( )方法在页面首次启动时装载变量的值以恢复此变量。此例子还使用jspDestroy()方法写此变量的值,以便JSP页面下一次启动时恢复。

 

程序清单4-3

count3.jsp

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">

 

<%@ import="java.util.*,java.sql.*" %>

 

<%!

  int counter;

 

  public void jspInit() {

    try {

      FileInputStream countFile =

                       new FileInputStream ("counter.dat");

      DataInputStream countData =

                       new DataInputStream (countFile);

      counter = countData.readInt();

    }

    catch (FileNotFoundException ignore) {

 // No file indicates a new conter.

    }

  }

 

  public void jspDestroy() {

    try{

      FileOutputStream countFile =

                     new FileOutputStream ("counter.dat");

      DataOutputStream countData =

                     new DataOutputStream (countFile);

      countData.writeInt(counter);

    }

    catch(IOException e) {

      e.printStrackTrace();

    }

  }

%>

<HTML>

 

<STYLE>

.pageFooter {

  position: absolute; top: 590px;

  font-family: Arial, Helvetica, sans-serif;

  font-size: 8pt; text-align: right;

}

</STYLE>

 

<BODY>

<DIV>

This page has been accessed

<%

  synchronized (page){

    counter++;

    out.println(counter);

  }

%>

times since last restarted.

</DIV>

</BODY>

 

</HTML>

 

 

编译JSP

大多数JSP引擎将它们创建的Servlet源代码放在一个工作目录中。在许多引擎中,这是一个必须显式地打开的选项,但是这个简单的任务。

通读产生的源代码有助于调试JSP页面中的问题。另外,这些源代码可以帮助有经验的Java开发员进一步了解JSP实现的内部原理。

程序清单4-4显示来自上一个计数器的编译的源代码。此清单中的源代码由Apache Jakarta ProjectTomcat 4.1.21产生;其他JSP引擎可能会产生略有差别的源代码。此源代码还被略微修改过,以便阅读方便。

 

程序清单4-4

count.jsp

 

 

6  JSP的性能调整

Java编程的几个方面会严重影响JSP页面的性能。

这些方面中的一些不是JSP特有的,而是Java编程的普遍问题。下面列出的是最常见的效率错误。

6.1  避免串联追加

在开发时,使用串联操作符(+,有的书称为“重载操作符”)将String对象联结起来是很简单的。例如:

    String output;

    output += "Item: " + item + " ";

    output += "Price: " + price + " ";

    println (output);

然而,很容易忘记String对象是不可变的,并且不能包含可改变的数据。StringBuffer对象设计为用于操作字符串。载改变String对象时,实际上是创建一个新的StringBuffer对象,旧的字符串被StringBuffer.toString( )的结果替代。

在处理以上代码时,将会创建几个新的String对象和StringBuffer对象。如果将String对象转换为StringBuffer对象或一开始就使用StringBuffer对象,效率通常会高得多。例如:

    StringBuffer output;

    output.append(new String("Item: "));

    output.append(item);

    output.append(new String(" "));

    output.append(new String("Price: "));

    output.append(price);

    output.append(new String(" "));

    println (output.toString());

6.2  小心使用synchronized( )

保护对象不受线程竞赛问题的影响是很重要的,但是同步过程容易造成性能瓶颈。

确保被同步的块包含尽可能少的代码行。在代码阻塞的线程中,哪怕减少一行代码,也会产生很大的不同。另外,尽可能同步大多数相应的锁定对象。通常,最好同步那些受到线程竞赛威胁的对象。尽可能避免使用thispage对象作为锁定对象。

总体上,JSP引擎你用了Java Servlet的非常强大的体系结构。线程化显著提高了性能,同时要注意防止线程竞赛。基于Java Servlet的其他调整技术可以用于调整JSP页面和底层Java Servlet的性能。

 

猜你喜欢

转载自chtblyl0920052505.iteye.com/blog/2268017