Sun Java System Application Server 7 Web应用开发者指南

上一部分 | 目录 | 下一部分


使用servlet

这部分将介绍如何通过创建高效的servlet(包括标准servlet)来控制运行在 Sun Java System Application Server 上的应用的交互。另外,这部分还介绍了Sun Java System Application Server用于对标准进行扩充的一些特性。

本模块包括以下几节:

关于servlet


servletapplet一样,是一种可再用的Java应用。不过,servlet需要运行在应用服务器或Web服务器上,而不能直接在Web浏览器中运行。

Sun Java System Aplication Server所支持的servlet是基于v2.3Java Servlet 规范。所有相关的规范都可以通过install_dir/docs/index.htm访问到,其中install_dir是指Sun Java System Application Server的安装路径。

servlet可用于应用的表现逻辑。servlet充当的是应用的中心调度器的角色,它负责处理表单输入、调用被封装在EJB组件中的业务逻辑组件,以及使用JSP来格式化网页的输出。 servlet在对用户请求作出的响应中生成内容,以此来控制应用流从对一个用户的交互转到对下一个用户的交互。

Servlet的基本特征有:

  •   servlet的创建和运行时管理是由Sun Java System Application Serverservlet引擎完成的
  •    servlet对封装在request对象中的数据进行操作
  •    servlet用封装在一个response对象中的数据作为对一个询问的响应
  • servlet调用EJB组件来履行业务逻辑功能
  •    Servlets call JSPs to perform page layout functions.
  •    servlet调用JSP来布局页面
  • servlet是可扩展的。可以通过随Sun Java System Application Server一起提供的API来扩展其功能
  •    servlet在交互中持续地为用户提供session信息
  • servlet可作为一个应用的一部分,或者也可以直接驻留在应用服务器中,以供增强应用的功能
  •   当服务器正在运行时,可以动态地装载servlet
  • servlet可通过URL来访问,应用页面上的一些按钮往往是指向servlet
  •   servlet还可以调用其它的servlet

Servlet数据流

当用户点击了 Submit(提交)按钮时,输入在显示页面中的信息将被交付给servlet。这个servlet会处理这些输入的数据,并通过生成内容来编制出一个响应,内容的生成通常是由业务逻辑组件(EJB组件)来完成的。一旦生成了内容,servlet就创建一个响应页面——一般是通过将内容转交给JSP。最后该响应被返回给客户,这样就可以开始下一次的用户交互。

下面的示例展示了进出servlet的信息流:

  1.      servlet处理客户的请求
  2.        servlet生成内容
  3.        servlet创建一个响应,要么:

    a.       直接返回给客户端

    或者

    b.       将此项任务分派给JSP

servlet仍驻留在内存中,以供处理其它的请求。

Servlet数据流的流程

Servlet的分类

servlet主要有两种:

  • 通用 servletgeneric servlet
    •       扩展了javax.servlet.GenericServlet
    •       协议无关,不带HTTP或任何其它传输协议的支持
  •    HTTP servlet
    •       扩展了 javax.servlet.HttpServlet
    •       内置HTTP协议支持,在 Sun Java System Application Server 环境下更加宜用

这两种类型的servlet都实现了构造器方法init()和毁坏器方法destroy(),分别用于初始化和回收资源。

所有的servlet都必须实现一个service()方法,由该方法来负责处理servlet请求。在通用servlet中,只需通过简单地重载service方法来提供处理请求的例程。而HTTP servlet提供了一个service方法,该方法自动地将请求发送给本servlet中的另外一个方法,而在这个另外的方法中使用了HTTP传输方法。因此,在HTTP servlet中,要处理POST请求就重载doPost方法,要处理GET请求就重载doGet方法,以此类推。

创建servlet


要创建一个servlet,需执行以下操作:

  • servlet设计到你的应用中,或者,如果是以一种通常的方式来访问的话,则将它设计成不需访问任何应用数据
  •    创建一个继承GenericServlet HttpServlet的类,对于不同的请求处理,重载适当的方法
  •   使用Sun Java System Application Server Administration界面来创建一个Web应用部署描述文件。详情请参考装配和部署Web组件

剩下的几节讨论以下话题:

  • 创建类声明
  • 重载方法
  •    访问参数和存储数据
  •    处理会话(Session)和安全(Security
  • 访问业务逻辑组件
  • 处理线程问题
  •   发送客户结果

创建类声明

创建一个servlet,需要写一个public Java类,该类应包含基本I/O的支持,还需要引入javax.servlet包。这个类必须继承GenericServlet HttpServlet。由于Since Sun Java System Application Serverservlet是在HTTP环境下的,所以这里推荐使用后一类的servlet(即HTTP servlet,译者注)。如果这个servlet属于一个package(包)的一部分,你还必须声明这个package的名字,以便类装载器可以正确地找到这个package

下面的头文件的例子展示了一个名为myServletHTTP servlet类的声明:

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class myServlet extends HttpServlet {
   ...servlet methods...
}

重载方法

接着,重载一个或更多的方法,从而为servlet执行不同的任务提供了相应的指令。servlet执行的所有处理都是基于request-by-request方式的,这些处理都在service方法中进行,在通用servlet中就是service()方法,而对于HTTP servlet,就是doOperation()方法。service方法接收进来的请求,根据所提供的指令处理这些请求,然后作出适当的输出。你还可以在servlet中创建其它的方法。

为了执行一次事务,业务逻辑可能还需访问数据库或者将请求传给一个EJB组件。

重载初始化方法

重载类初始化方法init(),在init()方法中为servlet实例的生命周期初始化或分配资源,例如一个counter(计数器)。init()方法将在servlet初始化之后、接收任何请求之前运行。如果想了解更多的信息,请参考servlet API规范说明书。

注意

所有 init() 方法都必须调用super.init(ServletConfig),以设置它们的scope作用域)。这样,其它的servlet方法就可以使用该servletconfiguration对象。如果忽略了这个调用,则在servlet开始运行时浏览器将显示一个500 SC_INTERNAL_SERVER_ERROR 的错误信息。

注意

在初始化时,如果Web应用的任何一个组件,例如一个filter(过滤器)抛出一个ServletException 异常,那么这个Web应用将不能开始运行。这样做是为了确保如果Web应用的某一部分可以运行的话,那么它的所有部分都应该可以运行。如果要求当安全组件不能用时Web应用也不能用,那么这一点尤其重要。

下面例子中的init()方法通过创建一个名为thisMany的整型变量来初始化一个counter

public class myServlet extends HttpServlet {
   int thisMany;

   public void init (ServletConfig config) throws ServletException
   {
      super.init(config);
      thisMany = 0;
   }
}

现在,其它的servlet方法就可以访问这个变量了。

重载毁坏器方法

重载类毁坏器destroy()方法是为了记录日志消息或释放在servlet的生命周期内打开了的资源。资源必须正确地关闭并且释放,这样才可以被重新使用或由垃圾收集器收回。destroy()方法在servlet本身正要退出内存之前运行。如果想知道更多的信息,请参考servlet API规范说明书。

举个例子,根据前面重载初始化方法中的例子,destroy()方法可以写如下所示的日志消息:

out.println("myServlet was accessed " + thisMany " times.\n");

重载ServiceGetPost方法

当有请求发生时,Sun Java System Application Server将收到的数据传给servlet引擎,由servlet引擎来处理这个请求。请求包括表单数据、cookiesession信息,以及URL name-value对,所有这些都包含在一个称为request对象的HttpServletRequest对象中。客户端元数据被封装成一个称为response对象的HttpServletResponse对象中。Servlet引擎将这两个对象都传给servletservice()方法作为参数。

HTTP servlet中,默认的service()方法会将请求发送给另外一个基于HTTP传输方法(POSTGET等)的方法。这样就使得servlet可以通过传输方法来执行不同的request数据处理。由于请求的发送是在service()方法中进行的,所以在HTTP servlet中通常不必重载service()方法。但是,需要重载doGet()、doPost()等方法,视预计的请求类型而定。

HTTP servlet中,请求的自动转送是通过简单地调用request.getMethod()来实现的,该方法提供了HTTP传输方法。在Sun Java System Application Server中,request数据在到达servlet之前就已经被预处理成一个name-value表的形式,因此在HTTP servlet中只需重载service()方法,而不用担心会降低其功能性。然而,这样一来servlet就缺乏了可移植性,因为它现在依赖于经过预处理的数据。

为执行需要对请求作出应答的任务,需要重载service()方法(在通用servlet中)或者doGet()或doPost()方法(在HTTP servlet中)。往往这就意味着要通过访问EJB组件来执行业务处理,选取需要的信息(这些信息在request对象或JDBC ResultSet对象中),然后将新生成的内容传递给JSP进行格式化并返回给客户端。

大多数涉及到表单的操作都需要使用GETPOST操作中的一种,因此,在大多数servlet中都需要重载doGet()或doPost()方法。注意,实现这两个方法时,都需要为它们提供输入类型或者直接将request对象传给一个集中处理方法,如下例所示:

public void doGet (HttpServletRequest request,
                 HttpServletResponse response)
         throws ServletException, IOException {
   doPost(request, response);
}

HTTP servlet中,所有的request-by-request事务都在适当的doOperation()中处理,包括session管理、用户鉴别、调度EJB组件和JSP,以及使用Sun Java System Application Server的一些特殊功能等。

如果试图在servlet中调用RequestDispatcher方法include()或forward(),要清楚request信息不再是以HTTP POSTGET等形式发送的。换句话说,如果servlet重载了doPost(),那么当其它servlet调用该方法时,该方法处理不了任何东西,即使在调用servlet时正好通过HTTP GET收到了这个请求的数据。因此,正如上面所解释的那样,应该确保对各种可能的输入类型都要实现例程。RequestDispatcher方法总是会调用service()方法。

要了解更多的信息,请参考"Calling a Servlet Programmatically"

注意

任意的二进制数据,如上载的文件或图象,都可能会出现问题,因为Web连接器(connector)会默认地将传入的数据转换成name-value对的形式。你可以对连接器进行编程,令其适当地处理这种数据,并适当地将它们打包到request对象中。

访问参数和存储数据

传进来的数据是封装在一个request对象中的。对于HTTP servlet来说,request对象的类型是HttpServletRequest;对于通用servlet来说,request对象的类型是ServletRequestRequest对象包含了所有的请求参数,包括名为attributes的自定义的request数值。

要访问所有传进来的request参数,可以通过getParameter()方法。例如:

String username = request.getParameter("username");

可以通过setAttribute()和getAttribute()分别对request对象赋值和从中取值。例如:

request.setAttribute("favoriteDwarf", "Dwalin");

上述例子向我们展示了一种将数据传给JSP的方法,既然request对象是JSP的内建beanJSP自然可以访问得到。

处理会话(session)和安全性(security)

从一个Web或应用服务器的角度来看,一个Web应用实际上就是一系列互不相干的服务器命中事件。如果某个用户曾经访问过这个站点,甚至用户的最后一次交互就发生在几秒钟之前,都不会自动地在服务器上留下记载。而session(会话)却可以通过记住应用的状态来支持多个用户的交互。在每次交互中,客户端通过一个cookie来对自己进行确认,如果浏览器是不带cookie的,也可以通过将session标识符放在URL中来进行确认。

session对象可以用于存放对象,如表列数据、关于应用当前状态的信息以及关于当前用户的信息。绑定在session中的对象还可以被其它使用了同一个session的组件访问。

如果想了解更多信息,请参考"创建和管理用户会话"

登录成功后,你应该引导servlet在一个称为session对象的标准对象中建立用户角色,包括关于当前会话的信息,用户用来登录的名字和所有需要保留的附加信息。这样一来,应用组件就可以查询session对象,从而对用户进行确认。

如果需要为应用提供安全的用户session,请参考"Web应用安全性"

访问业务逻辑组件

Sun Java System Application Server编程模式下,你可以通过EJB组件实现业务逻辑,包括数据库或姓名地址录的事务处理以及复杂的计算。在处理特定任务时,需要将一个request对象的引用作为参数来传递。

将从访问数据库得来的结果存放在JDBC ResultSet对象中,并将该对象的一个引用传给其它的组件,从而进行格式化并发送给客户。另外,通过request.setAttribute()方法可以将request对象得到的结果保存下来,对于session,可以使用session.setAttribute()方法。存放在request对象中的对象必须与请求的长度相匹配,换句话说,要和这种特定的servlet线程相匹配。存放在session对象中的对象可以维持到session的整个生命周期,在这段时间内,可以经历很多的用户交互。

下面的例子展示了一个访问了一个名为ShoppingCart EJB组件的servlet。这个servlet在导入cart的远程接口后,通过获取用户的session ID创建了一个名为cartcart的句柄,该句柄被存放在用户的session中。

import cart.ShoppingCart;
   
   // Get the user's session and shopping cart
   HttpSession session = request.getSession(true);
   ShoppingCart cart =
      (ShoppingCart)session.getAttribute(session.getId());

   // If the user has no cart, create a new one
   if (cart == null) {
      String jndiNm = "java:comp/env/ejb/ShoppingCart";
      javax.naming.Context initCtx = null;
      Object home = null;
      try {
         initCtx = new javax.naming.InitialContext(env);
         java.util.Properties props = null;
         home = initCtx.lookup(jndiNm);
         cart = ((IShoppingCartHome) home).create();
         session.setValue(session.getId(),cart);
      }
      catch (Exception ex) {
      .....
      .....
      }
   }

servlet中通过Java命名目录接口(Java Naming Directory InterfaceJNDI)访问EJB组件,建立一个EJB组件的句柄,或者是代理。接着,像一个正规的对象那样引用EJB组件,具体操作由bean的容器来管理。

以下是JNDI查找shopping cart(购物车)的代理的例子:

String jndiNm = "java:comp/env/ejb/ShoppingCart";
javax.naming.Context initCtx;
Object home;
   try
   {
      initCtx = new javax.naming.InitialContext(env);
   }
   catch (Exception ex)
   {
      return null;
   }
   try
   {
      java.util.Properties props = null;
      home = initCtx.lookup(jndiNm);
   }
   catch(javax.naming.NameNotFoundException e)
   {
      return null;
   }
   catch(javax.naming.NamingException e)
   {
      return null;
   }
   try
   {
      IShoppingCart cart = ((IShoppingCartHome) home).create();
   
}
catch (...) {...}

如果想了解更多关于EJB组件的信息,请参考Sun Java System Application Server开发者指南中Enterprise JavaBeans 技术部分的内容。

注意

为了避免与JNDI中的其它enterprise资源的名称相冲突,同时还为了避免移植性问题,Sun Java System Application Server 所有应用中的名称都要以字符串java:comp/env开头。

处理线程问题

在默认情况下,servlet不具备线程安全性。一个单独的servlet实例中的方法通常并发地执行多次(视可用的内存而定)。每次方法的执行都是通过servlet引擎仅有的一个servlet拷贝而在一个不同的线程中发生。

虽然这样可以有效地利用系统资源,但却是危险的,因为Java管理内存的方式决定了这样做的不安全性。由于属于servlet类的变量是通过引用来传递的,不同的线程会以覆盖的方式写同一块内存中的内容,这就是上述做法的副作用。要使一个servlet(或一个servlet中的某个程序块)是线程安全的,可以选用以下介绍的方法中的一种:

  • 同步(Synchronize)所有对实例变量的写访问操作,例如可以用public synchronized void method()(同步整个方法)或者是用synchronizedthis{…} (同步程序块)。由于同步操作会使反应速度大大变慢,因此只对程序块同步,或者如果可以确信程序块不需要同步时,最好就不用同步。

举个例子,假设该servletdoGet()中有一个线程安全的程序块,以及一个线程安全的方法mySafeMethod():

   import java.io.*;
   import javax.servlet.*;
   import javax.servlet.http.*;

   public class myServlet extends HttpServlet {

   public void doGet (HttpServletRequest request,
                    HttpServletResponse response)
            throws ServletException, IOException {
      //pre-processing
      synchronized (this) {
         //code in this block is thread-safe
      }
      //other processing;
      }

   public synchronized int mySafeMethod (HttpServletRequest request)
      {
      //everything that happens in this method is thread-safe
      }
   }

  • 使用SingleThreadModel类来创建一个单线程的servlet。对于在Sun Java System Application Server中的一个单线程servletservlet引擎创建一个servlet实例池,用于接收传进来的请求(在内存中的同一个servlet的多个拷贝)。你可以通过设置在Sun Java System Application Server的特定的web应用的deployment descriptor(部署描述文件)中的singleThreadedServletPoolSize属性来改变servlet实例池中servlet实例的个数。如果想知道更多的关于Sun Java System Application Server的特定的 web应用的deployment descriptor文件的信息,请参考"装配和部署Web组件"。一个单线程servlet装载起来会更慢些,因为新的请求必须要等到一个空闲的实例才可以被处理,但是对于分布式的、load-balanced(负载均衡)的应用,这就不是什么问题了。因为在这种应用中,servlet的装载已经自动地变成一个不那么繁忙的任务了。

下面的例子中,servlet是完全单线程的:

   import java.io.*;
   import javax.servlet.*;
   import javax.servlet.http.*;

   public class myServlet extends HttpServlet
      implements SingleThreadModel {
      servlet methods...
   }

发送客户结果

用户交互的最后一个活动是提交一个响应页面给客户。响应页面可以通过两种方式提交:

创建一个 Servlet 响应页面

可以通过将内容写入output流的方式来创建servlet的输出页面。具体应采取的方式取决于输出的类型。

在进行任何输出之前,都要通过使用setContentType()方法来指定输出的MIME类型。如下所示:

response.setContentType("text/html");

对于一些文本输出,如简单的HTML,可以创建一个PrintWriter对象并通过println语句将文本写入该对象中。例如:

PrintWriter output = response.getWriter();
output.println("Hello, World\n");

对于二进制输出,可以直接写入到输出流中。先创建一个ServletOutputStream对象,然后通过print()语句将内容写入到该对象中。例如:

ServletOutputStream output = response.getOutputStream();
output.print(binary_data);

创建JSP响应页面

servlet可以通过两种方式调用JSP

  •    通过RequestDispatcher接口中的include()方法调用一个JSP,返回后再继续处理交互。在一个给定的servlet中,可以多次调用include()方法。

下面的例子展示了一个使用include()方法的JSP

   RequestDispatcher dispatcher =
      getServletContext().getRequestDispatcher("JSP_URI");
   dispatcher.include(request, response);
   ... //processing continues

  •   RequestDispatcher接口中的forward()方法负责处理JSP交互的控制。在调用了forward()方法之后,servlet就不再牵涉到当前交付的输出,因此,在一个特定的servlet中,只需调用一次forward()方法。