• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
Java Web动态图表编程
 

Java Web动态图表编程

on

  • 4,116 views

 

Statistics

Views

Total Views
4,116
Views on SlideShare
4,116
Embed Views
0

Actions

Likes
0
Downloads
29
Comments
0

0 Embeds 0

No embeds

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

    Java Web动态图表编程 Java Web动态图表编程 Document Transcript

    • 本书向读者展示如何使用 Java Applet、Java Servlet、Java Server Pages(JSP)、JavaBean 以及开放源代码的 Web 图表生成引擎——JFreeChart 和 Cewolf 来开发奇妙的 Web 动态图表 应用程序——以一种跨平台、小巧、结构清晰的模式在 Web 上生成动态图表。 随着计算机网络及编程技术的发展,使得越来越多的应用程序被移植成以 Web 应用程序 (浏览器/服务器)的方式,工作在因特网/局域网环境之中。网络开发人员发现,某些传统 IDE 开发环境,例如:在 Dephi 和 VB 中可以轻而易举地实现的图形界面,而在 Web 程序中却 很难实现,其中一个典型的例子就是 Web 图表(Chart)的生成与处理。即使 Web 程序(ASP、 ASP.NET 等)能够处理一些图表,也基本是采用 ActiveX 或者 COM 组件的结构。这种方法有 三大弊端: ? 开发 ASP 比较简单,但开发 ActiveX 和 COM 却很难; ? 基于 ActiveX 和 COM 构架的 Web 应用程序只能用于微软 Windows 的 Web 服务器 IIS 环境下, 移植性差; ? 由于 ActiveX 和 COM 与 Windows 深度紧密结合,在带来强大功能的同时,也一并带来了 Windows 固有的缺陷——安全性和稳定性差。 PHP 可以用于 Web 图表处理,它具有跨平台、安全性能高、兼容性及扩展性强的优点,但也 有其固有的缺陷,具体表现为以下几点: ? 效率不高,基于解释型,而非编译型,这一点与 ASP 类似。ASP.NET 吸取了 JSP 的优点, 属于编译型,大大提高了运行速度和效率; ? 安装复杂,每一种扩展模块不完全是由 PHP 自身来完成。当图形需要 GD 库,使用类似的扩 展库时,安装和调试是 PHP 的一大问题; ? 因为是开发源代码产品,所以缺乏企业级的商业支持。 JSP 的出现,使得上述弊端不复存在。随着 Java 对二维图形及三维图像的处理能力越来 越强大,利用 JSP 来简单、高效地开发一个 Web 图表应用程序已经不是一件难事了。为了展 示如何编写 Web 图表应用程序,本书中,我们不仅提供了多个 JSP 图表生成的实例,而且还 将从以下两个方面加以说明 Web 图表编程是如何实现的。 1.利用 Java 自身对图形的处理能力,由开发者编写代码来生成 Web 动态图表。将以大量的 例程,不同的方式(例如:Java Applet、Java Servlet、JSP、JavaBean) ,从不同的角度来 展示如何编写 Web 图表的 Java 程序。 2. 借助第三方的图表生成引擎来完成图表, 主要利用一些 Java 开放源代码组织开发的作品, 例如: ? 由 www.jfree.org 推出的 JFreeChart; ? 由 cewolf.sourceforge.net 推出的 Cewolf。 Cewolf 是基于 JFreeChart 的二次开发。 实质上,是基于 JFreeChart 的应用。JFreeChart 没有免费提供开发文档,只有英文版的相关资料,因此 JFreeChart 和 Cewolf 在国内的应用 受到了一定的限制。 根据我们使用及研究 JFreeChart 和 Cewolf 的经验,将在本书中提供大量的例程和详尽 的讲解,帮助读者轻松地掌握上述两种非常优秀的、基于 Java 的 Web 图表生成引擎。相信阅 读本书,并加以必要的练习,读者将会发现,基于 JSP 的 Web 图表编程是可以轻松完成的。 本书结构及内容 本书例程丰富、代码简洁、结构清晰、讲解准确、图文并茂。共分九章,各章具体内容 如下: 第 1 章 Java 概述 本章主要概述 Java 历史及其发展、Java 开发环境的建立,以及编写一个简单的 Java 应 用程序。
    • 第 2 章 Java Applet 与绘图基础 本章简要介绍 Applet 绘图基础,例如:图形环境、图形对象、基本的字体及颜色控制, 以及如何使用 Applet 绘制一些基本的几何图形。通过本章的学习,读者将掌握基本的 Java 图像编程方法。 第 3 章 Java Applet 图表绘制实例 本章综合利用绘制基本几何图形的方法来开发以下常见的图表: ? ·垂直柱状图 ? ·饼图 探讨如何以图表的方式生成以下内容: ? ·在 Web 动态图表中加载外部图像文件 ? ·支票 ? ·如何从 HTML 文档中获取参数生成动态图表 通过本章的学习,读者将会理解任何复杂的图表都可以分解成一些基本的几何图形;通 过排列组合不同的外部图像文件,以及基本的几何图形,就可以生成复杂的图表。 第 4 章 JSP/Servlet 运行环境的搭建 本章主要介绍 JSP Web 服务器的安装。在运行 JSP/Servlet 之前,首先需要安装 JSP/ Servlet 的运行环境,它就是我们平常所说的,支持 JSP/Servlet 的 Web 服务器(容器) 。 目前有很多支持 JSP/Servlet 的 Web 服务器,我们先介绍两种免费的、高性能的、适合 中小企业 JSP/Servlet 的 Web 服务器——Tomcat 和 Resin,并将详细阐述 Tomcat 实现 JSP/Servlet 的运行机制。此外,介绍一种企业级的 J2EE 服务器——Weblogic,学习如何在 这些服务器中部署及发布基于 JSP/Servlet 的 Web 图表应用程序。 第 5 章 基于 Servlet 的 Web 图表编程 本章讲述 Web 请求/响应机制(get 和 post) 、如何部署 Servlet、如何利用 Servlet 生 成 Web 动态图表, 并且提供了模拟网站投票统计、 生成登录验证码、 普通/3D 甘特图等 Servlet 实例。 第 6 章 JSP Web 图表编程基础 本章讲述 JSP 绘图基础。 JSP 的环境下, 在 如何使用 Java.awt.Graphics 类的各种方法, 包括绘制直线、文本(字符串) 、矩形、椭圆和圆、圆弧、多边形、折线,以及如何加载外部 图像文件,等等。 在绘制基本几何图形的基础上,我们将以圆柱体和立方体为例,展示如何通过绘制多个多边 形并将其组合成复杂几何图形的方法;利用 Java.imageio 包中 ImageIO 类的支持,调用 ImageIO 来执行诸如加载图像,以及对图像进行编码输出等工作。 第 7 章 JSP 与 Java2D Web 图表编程 本章阐述如何利用 Java2D API 对于高级二维图形的处理能力,例如:笔划属性的设 定、 图形填充、渐变色、图像的透明度、字体处理、图形边缘的反锯齿、图形对象的转换以及变 形,等等,利用 B/S 应用程序中生成 Web 图表的方法,并以二维及三维图表的形式,包括折 线图、水平直方图、垂直柱状图、堆栈图、饼图,以及二次曲线来进行讲解和说明。 提供了一个绘制复杂图表的例程——股市大盘指数走势图。通过本章的学习,读者可以 不借助任何第三方的程序,编写生成各种风格、复杂的 Web 图表应用程序。 第 8 章 开放源代码与 Web 图表编程 本章详细讲述了 JFreeChart 和 Cewolf 这两个 Web 图表生成引擎的安装、配置及使用方 法。提供了在 JFreeChart 和 Cewolf 的环境下生成以下类型图表的完整例程及讲解: ? 普通/3D 水平及垂直直方图、普通/3D 饼图、普通/3D 堆栈图 ? 线段(曲线)图、区域图、时序图、蜡烛图、移动平均线图
    • ? 罗盘图、温度计图、速度表图、信号图 ? 甘特图、多轴图表、组合图表 此外,还包括如何在 Cewolf 的环境中,创建自定义绘制属性的图表。 第 9 章 Web 图表生成引擎的设计思路与实现 本章讨论如何开发一个图表生成引擎; 如何设计图表生成引擎 (封装成 JavaBean 的形式) 以达到高效、重复使用的目的。图表生成引擎的设计难点是什么?该如何处理?如何优化引 擎?我们将提供具体的实例加以讲解。通过本章的学习,读者将会感受到,图表引擎的设计 原来可以这么轻松地实现! 本书的适用对象 本书适合从事 Java 及 Web 编程的开发人员, JSP/Servlet 应用程序中需要提供图表显 在 示及处理功能的 JSP/Servlet 程序开发者,以及编程爱好者阅读。对于初学入门的应用开发 人员,可以作为自学的实践指导书;对于已经从事 Web 编程工作的人员,可以作为一本供随 时翻阅查询的案头参考书。 建议 我们建议读者在学习期间避免使用可视化的开发工具, 例如: JBuilder、JCreator、KAWA、 Visual Cafe、Eclipse、IntelliJ IDEA、BEA WebLogic Workshop、Oracle JDeveloper 等, 所有工作都可以使用 JSP 服务器(Tomcat/Resin)+ WWW 浏览器(IE/Mozilla/Firefox)+文 本编辑器(EditPlus)来完成。附录中,我们向读者介绍了一种优秀的 Java IDE 开发工具— —Gel。利用 Gel,可以方便地创建、编辑、编译及运行 Java/JSP/Servlet 应用程序。 为了达到最佳的学习效果,我们建议读者在阅读本书的同时,亲自动手按照本书的示例 去操作。 所有的源程序都可以在 www.broadview.com.cn 网站上下载, 但建议读者最好对照源 程序自己动手录入一次,这样可以加深理解和记忆。 因本书的例程非常丰富,为节省篇幅,故基本上不列出每个例程的完整清单。本书详尽 地讲解了每个源程序的设计思路、 运行机制及相对应的核心代码段。 建议读者在阅读本书时, 首先用 EditPlus 之类的文本编辑器来打开相应例程的源程序, 其次阅读其完整的源程序, 最 后再阅读本书的讲解,相信这样会让读者取得事半功倍的效果。 致谢 本书由钟京馗(City University of New York)执笔主编和最后定稿。唐桓(University of Bridgeport)参与本书例程的编写与调试工作。 本书出版之际,特别鸣谢钟志祥、李美行、朱晓蕾、朱正才、胡元妹、黄桂玉、武冠军、唐 光富、彭燕给予的大力支持与协助。 在编写本书的过程中,我们尽力保持内容的科学性、先进性和实用性,同时力求做到讲 解原理,深入浅出,语言通俗易懂。但鉴于作者学识有限,不足之处在所难免,见仁见智, 恳请广大读者指正。我们将在本书的再版中使其更臻完善。  钟京馗 2005 年 9 月于纽约
    • 第 1 章  Java 概述  1  1.1  Java 简介  1  1.1.1  Java 发展简史  1  1.1.2  Java 的体系  2  1.1.3  Java 的优点  3  1.2  Java 开发环境的搭建  4  1.2.1  Java 运行环境的要求  4  1.2.2  Java 的安装和配置  6  1.3  Java/JSP 开发工具  8  1.3.1  EditPlus 简介  9  1.3.2  UltraEdit 简介  11  1.3.3  其他 Java/JSP 开发工具  13  1.4  第一个 Java 程序  15  1.5  本章小结  17  第 2 章  Java Applet 与绘图基础  18  2.1  Java Applet 概述  18  2.2  Java Applet 工作流程  20  2.2.1  组件与容器  20  2.2.2  Applet 的编译与执行  22  2.2.3  包含 Applet 的 HTML 23  2.3  绘制文本(字符串)  23  2.4  绘制线段  29  2.5  色彩管理  31  2.5.1  色彩基础  31  2.5.2  Java 的色彩管理  34  2.6  字体控制  36  2.7  绘制矩形  40  2.7.1  绘制普通矩形  40  2.7.2  绘制 3D 矩形  43  2.8  绘制椭圆  46  2.8.1  绘制普通(空心)椭圆  46  2.8.2  绘制实心椭圆  48  2.8.3  绘制 3D 椭圆  49  2.8.4  绘制圆柱体  50  2.9  绘制圆弧  54  2.9.1  绘制普通(空心)圆弧  55  2.9.2  绘制实心圆弧  56  2.9.3  绘制 3D 圆弧  57  2.10  绘制多边形  59  2.10.1  绘制空心多边形  59  2.10.2  绘制实心多边形  61  2.10.3  绘制折线  61  2.10.4  绘制三角形(箭头)  62  2.10.5  绘制平行四边形及立方体  69
    • 2.11  图像的载入与显示  78  2.11.1  在 Applet 中加载和  2.11.1  显示图像  78  2.11.2  使用 MediaTracker 加载并  2.11.2  显示图像  84  2.11.3  使用双缓冲技术绘制图像  89  2.12  本章小结  93  第 3 章  Java Applet 图表绘制实例  94  3.1  Java Applet 生成 Web  3.1  动态图表  94  3.1.1  垂直柱状图  95  3.1.2  饼图  102  3.2  Java Applet 生成单据  109  3.2.1  带徽标的 Web 动态图表  110  3.2.2  支票的生成  111  3.3  从 HTML 文档获取参数  3.3  生成动态图表  123  3.3.1  传递参数的 HTML 文档  123  3.3.2  获取参数并生成图表  124  3.4  本章小结  126  第 4 章  JSP/Servlet 运行环境的搭建  128  4.1  Tomcat 的安装和配置  128  4.1.1  Tomcat 的安装  129  4.1.2  测试第一个 JSP 程序  132  4.1.3  配置 Tomcat 132  4.1.4  转换后的 JSP 页面文件  145  4.2  Resin 的安装和配置  147  4.2.1  Resin 的安装  147  4.2.2  Resin 的配置  149  4.3  BEA Weblogic 的安装和配置  150  4.3.1  BEA Weblogic 的安装  150  4.3.2  BEA Weblogic 的配置  152  4.3.3  测试 BEA Weblogic  4.3.3  的配置  155  4.3.4  部署第一个 Web  4.3.4  应用程序  156  4.4  本章小结  157  第 5 章 基于 Servlet 的 Web 图表编程  158  5.1  Servlet 简介及其构架  158  5.1.1  Servlet 的特点  158  5.1.2  Servlet 的接口  159  5.1.3  HttpServlet 类简介  160  5.1.4  HttpServletRequest 接口  161  5.1.5  HttpServletResponse 接口  162
    • 5.2  Servlet 处理 HTTP get 请求  163  5.3  Servlet 处理包含数据的 HTTP  5.3  get 请求及解决中文乱码  177  5.4  Servlet 处理 HTTP post 及包含  5.4  数据的 post 请求  187  5.5  Servlet 生成 Web 投票统计图  191  5.6  Servlet 生成登录验证码  198  5.6.1  Servlet 生成登录验证码  5.6.1  实例 1 199  5.6.2  Servlet 生成登录验证码  5.6.2  实例 2 209  5.7  Servlet 高级设置  214  5.7.1  Servlet 初始化参数  214  5.7.2  Servlet 加载优先级  216  5.7.3  Servlet 映射  217  5.8  Servlet 绘制甘特图  218  5.9  Servlet 绘制 3D 甘特图  222  5.10  本章小结  228  第 6 章  JSP Web 图表编程基础  229  6.1  JSP 概述  230  6.1.1  JSP 运行机制  231  6.1.2  JSP 的优点  233  6.2  JSP 语法简介  234  6.2.1  JSP 文件结构  234  6.2.2  JSP 文件中的元素简介  240  6.3  JSP 调用 Servlet 生成  6.3  动态图表  257  6.3.1  JSP 生成验证码  257  6.3.2  JSP 生成甘特图  258  6.3.3  JSP 其他相关知识  258  6.4  JSP 生成基本动态图表  260  6.4.1  JSP 绘制文本和线段  260  6.4.2  JSP 与字体控制  266  6.4.3  JSP 绘制矩形  273  6.4.4  JSP 绘制椭圆  275  6.4.5  JSP 绘制圆弧  276  6.4.6  JSP 绘制多边形和折线  277  6.4.7  JSP 绘制三角形  277  6.4.8  JSP 绘制平行四边形和  6.4.8  立方体  280  6.4.9  JSP 加载并显示图像  281  6.5  本章小结  282  第 7 章  JSP 与 Java2D Web 图表编程  283  7.1  Java2D 概述  283
    • 7.2  Java AWT 与 Java2D 285  7.3  Java2D 与填充属性  287  7.3.1  设置填充属性  287  7.3.2  填充属性的用法  287  7.4  Java2D 与笔划属性  291  7.4.1  线段端点的形状风格  292  7.4.2  线段转折处的形状风格  292  7.4.3  虚线风格  293  7.4.4  BasicStroke 构造器  294  7.4.5  Java2D Web 图表实例  7.4.5  之折线图  294  7.5  创建基本 Java2D 图形  309  7.5.1  Java2D 图形(Shape)  7.5.1  接口概述  309  7.5.2  Point2D 310  7.5.3  Line2D 311  7.5.4  Rectangle2D 312  7.5.5  RoundRectangle2D 314  7.5.6  Java2D Web 图表实例之  7.5.6  柱状图  315  7.5.7  Ellipse2D 333  7.5.8  Arc2D 334  7.6  Java2D 实例饼图类图表  337  7.6.1  普通饼图  338  7.6.2  圆圈图  342  7.6.3  3D 饼图  344  7.7  图形重叠  346  7.8  alpha 复合  348  7.8.1  alpha 复合概述  348  7.8.2  AlphaComposite 类的使用  350  7.8.3  AlphaComposite 应用实例  352  7.9  图形对象的转换  357  7.9.1  图形对象转换(transformation)  7.9.2  概述  357  7.9.2  平移(translation)  359  7.9.3  旋转(rotation)  360  7.9.4  缩放(scale)  362  7.9.5  扭曲(shear)  364  7.10  图形渲染线索  366  7.11  Java2D 与高级文本处理  368  7.11.1  空心文本  368  7.11.2  弯曲文本  369  7.11.3  单行长文本自动分行  371  7.12  Java2D 创建复杂图形  373
    • 7.12.1  Area 374  7.12.2  曲线  375  7.12.3  通用路径  377  7.13  Web 图表实例解析  379  7.13.1  透明 3D 饼图  379  7.13.2  股市指数走势图  381  7.14  本章小结  391  第 8 章 开放源代码作品与 Web 图表编程  392  8.1  开放源代码作品简介  392  8.2  JFreeChart 与 JSP 图表编程  394  8.2.1  JFreeChart 简介  394  8.2.2  JFreeChart 的安装及其  8.2.2  核心类  395  8.2.3  JFreeChart 生成直方图表  398  8.2.4  JFreeChart 生成饼型图表  411  8.2.5  JFreeChart 生成线段图表  416  8.2.6  JFreeChart 生成区域图表  420  8.2.7  JFreeChart 生成时序  8.2.7  (Time Series)图表  424  8.2.8  JFreeChart 生成甘特图表  430  8.2.9  JFreeChart 生成多轴  8.2.9  (Multiple Axis)图表  432  8.2.10  JFreeChart 生成组合  8.2.10  (Combined Axis)图表  435  8.2.11  JFreeChart 生成其他类型  8.2.11  的图表  441  8.3  JFreeChart 与 Servlet  8.3  图表编程  444  8.3.1  简单的 Servlet 图表编程  444  8.3.2  交互式 Servlet 图表编程  446  8.4  Cewolf 与 JSP 图表编程  448  8.4.1  Cewolf 简介  448  8.4.2  Cewolf 的下载与安装  448  8.4.3  Cewolf 生成直方图表  450  8.4.4  Cewolf 生成基于 DefaultCategory  8.4.4  Dataset 数据集的图表  454  8.4.5  Cewolf 生成饼图  460  8.4.6  Cewolf 生成基于 XYDataset  8.4.6  数据集的图表  462  8.4.7  Cewolf 生成基于 OHLCDataset  8.4.7  数据集的图表  465  8.4.8  Cewolf 生成甘特图表  466  8.4.9  Cewolf 生成信号图表  467  8.4.10  Cewolf 生成速度图表  468
    • 8.4.11  Cewolf 生成 OverLay 类型  8.4.11  的图表  468  8.4.12  Cewolf 生成组合图表  470  8.4.13  生成自定义风格的  8.4.13  Cewolf 图表  472  8.5  本章小结  473  第 9 章  Web 图表生成引擎的设计思路与实现  475  9.1  Web 动态图表生成引擎的  9.1  设计思路  475  9.2  Web 动态图表生成引擎的  9.2  设计模型  480  9.2.1  生成普通线段图的  9.3.2  JavaBean 480  9.2.2  生成 3D 线段图的  9.3.2  JavaBean 483  9.2.3  生成普通直方图的  9.3.2  JavaBean 484  9.2.4  生成 3D 直方图的  9.3.2  JavaBean 487  9.2.5  生成普通饼图的  9.3.2  JavaBean 488  9.2.6  生成 3D 饼图的  9.3.2  JavaBean 490  9.3  数据分离  491  9.3.1  创建及调用 CategoryDataset  9.3.2  类数据集对象  491  9.3.2  创建及调用 PieDataset 类  9.3.2  数据集对象  495  9.4  引擎的优化概述  496  9.5  本章小结  498  附录  Gel 使用指南  499
    • 第 1 章  Java 概述  Java 是美国 Sun 微系统公司(Sun  Microsystems,  Inc.)开发,近年来飞速发展的一项崭 新的计算机技术。  1.1  Java 简介  Java 既是一种程序设计语言,也是一个完整的平台。作为一种程序语言,它简洁、面向 对象、 安全、 健壮,以及适用于 Internet 技术;而作为一个平台(JRE,  Java Runtime Environment,  Java 运行环境或者说是 Java 虚拟机)对于符合 Sun 公司 Java 标准的应用程序, , 都可以在 Java  平台上正确运行,与程序运行的操作系统无关。  Java  为什么能够成为目前编写  Web  应用程序的首选设计语言呢?并且具备跨平台的能 力呢?我们先简单地了解一下 Java 的发展历史和体系结构。  1.1.1  Java 发展简史  Java 起初并非叫做 Java,而是叫做 Oak。早期是为了嵌入式系统而设计的一项产品。20  世纪 90 年代初期,  公司预测微处理器的发展将会对家用电器以及智能化电子消费设备起 Sun 到决定性的作用,于是,在 1990 年 12 月,Sun 公司就由 Patrick Naughton、Mike Sheridan 和  James  Gosling 成立了一个叫做 Green  Team 的小组。其主要目标就是开发一种分布式系统架 构,使其能够在智能化电子消费设备作业平台上执行。例如,PDA、手机、信息家电(IA,  Internet/Information  Appliance)等。一开始,准备采用 C++,但 C++太过复杂,安全性差。 最后,基于 C++开发出一种新的语言 Oak,Oak 是一种用于网络、精巧而安全的语言。  1992 年,Green Team 发表了一款名叫 Star Seven(*7)的机器,它有点像现在我们熟悉 的 PDA,但市面上的 PDA 几乎都不是它的对手,更不要说是早在 10 年前那个计算机还不普 及的时代了。Java 语言的前身 Oak 就这样诞生了,主要目的是用来编写在 Star Seven 上的应 用程序。为什么叫 Oak 呢?原因是 James  Gosling 办公室的窗外,正好有一棵橡树(Oak) , 就取了这个名字。至于为什么 Oak 又会改名为 Java 呢?这是因为在 Oak 注册商标时,发现 已经被另外一家公司使用了。那要取什么新名字呢?工程师们边喝着咖啡边讨论着,看着手 中的咖啡,突然灵机一动,就叫  Java  好了。就这样它就变成了大名鼎鼎、如日中天的  Java  了。 但是,Green Team 的项目发展却不尽如人意。智能化电子消费设备的市场发展远远低于  Sun 的预期设想。 Sun 公司曾依此投标一个交互式电视项目, 结果被 SGI 打败了。 可怜的 Oak  几乎无家可归,就在举步维艰,随时会被取消的时刻,情况却发生了巨大变化。1993 年,因 特网在美国开始大规模的发展,基于因特网的 WWW 也爆炸性地流行起来。Sun 公司发现,  Green  Team 的项目研究成果——Java 似乎天生就是为因特网而诞生的,因为恰巧就是 Mark  Andreessen 开发的 Mosaic 和 Netscape 启发了 Oak,他们利用 Java 开发了 HotJava 浏览器,得 到了 Sun 公司首席执行官 ScottMcNealy 的支持,触发了 Java 进军 Internet。 于是,Java 第一次以 Applet 的形式在因特网上出现了。Applet 不但使 WWW 页面显示 静态的内容,而且可以显示动态的内容和动画。同时实行了本地计算机,从远程联网的服务
    • 器上下载资料并正确地显示出来。这些技术在当时引起了巨大的震撼,促使 Java 在因特网上 得以飞速地发展。  1995 年 5 月 23 日,  在 SunWorld’95 上正式发布 Java 和 HotJava 浏览器。  Sun 1.1.2  Java 的体系  Java 发展到今天,已从编程语言发展成为全球第一大通用开发平台。Java 技术已被计算 机行业主要公司所采纳。  1999 年,  公司推出了以 Java2 平台为核心的 J2EE、  Sun J2SE 和 J2ME  三大平台。随着三大平台的迅速推进,全球形成了一股巨大的 Java 应用浪潮。  1.Java 2 Platform, Micro Edition(J2ME)  Java  2 平台微型版。Sun 公司将 J2ME 定义为“一种以广泛的消费性产品为目标、高度 优化的 Java 运行环境”自 1999 年 6 月在 JavaOne Developer Conference 上声明之后,  。 J2ME  进入了小型设备开发的行列。 通过 Java 的特性, 遵循 J2ME 规范开发的 Java 程序可以运行在 各种不同的小型设备上。  2.Java 2 Platform, Standard Edition(J2SE)  Java 2 平台标准版,适用于桌面系统应用程序的开发。本书例程就是利用 J2SE 5.0 版的 相关图形 API 包来开发的。  3.Java 2 Platform, Enterprise Edition(J2EE)  J2EE 是一种利用 Java 2 平台来简化企业解决方案的开发、 部署和管理等相关复杂问题的 体系结构。J2EE 技术的核心就是 Java 平台或 Java 2 平台的标准版,J2EE 不仅巩固了标准版 的许多优点,例如: “一次编写、随处运行”的特性、方便存取数据库的 JDBC API、CORBA  技术,以及能够在  Internet  应用中保护数据的安全模式等,同时还提供了对 EJB(Enterprise  JavaBeans)、Java Servlets API、JSP(Java Server Pages) ,以及 XML 技术的全面支持。 本书第 4 章,阐述了如何将我们开发的 JSP 图表应用程序,在 J2EE 平台上进行部署和 管理。  1.1.3  Java 的优点  Java 是一种面向对象、分布式、解释、健壮、安全、可移植、性能优异,以及多线程的 语言。下面简单介绍其中的几个优点。  1.Write Once, Run Anywhere  “一次编写, 随处运行” 这是程序设计师们喜爱 Java 的原因之一, 。 核心就是 JVM(Java  虚拟机)技术。 编写好一个 Java 程序, 首先,要通过一段翻译程序, 编译成一种叫做字节码的中间代码。 然后经 Java  平台的解释器,翻译成机器语言来执行——平台的核心叫做 JVM。Java  的编译 过程与其他语言不同。例如,C++在编译的时候,是与机器的硬件平台信息密不可分的。 编译程序通过查表将所有指令操作数和操作码等,转换成内存的偏移量,即程序运行时的 内存分配方式,以保证程序运行。而 Java 却是将指令转换成为一种扩展名为 class 的文件, 这种文件不包含硬件的信息。只要安装了 JVM,创立内存布局后,通过查表来确定一条指 令所在的地址,这就保证了 Java 的可移植性和安全性。 上述 Java 程序的编译和运行流程,如图 1.1 所示。
    • Interpreter 编译器 ①:编译 MyApp.java  ②:运行 源文件  Ø 装载类 ① Ø 生成字节码 Ø 及时编译 Ø 解释执行  ② Java  Java  Java  应用 应用 应用 程序  程序  程序  JVM  JVM  JVM  (J2SE)  (J2SE)  (J2ME)  Unix/Linux  MS Windows  Palm OS  图 1.1  Java 的编译和运行流程 
    • 2.简单 纯粹的面向对象,加上数量巨大的类所提供的方法(函数)库的支持,使得利用 Java 开 发各种应用程序,可以说是易如反掌。此外,在程序除错、修改、升级和增加新功能等方面, 因其面向对象的特性,使得这些维护也变得非常容易。  3.网络功能  Java 可以说是借助因特网而重获新生的,自然具备编写网络功能的程序。不论是一般因 特网/局域网的程序,如 Socket、Email、基于 Web  服务器的 Servlet、JSP 程序,甚至连分 布式网络程序,如 CORBA、RMI 等的支持也是非常丰富的,使用起来也很方便。  4.资源回收处理(Garbage Collection)  Garbage  Collection 是由 JVM 对内存实行动态管理的。程序需要多少内存、哪些程序的 内存已经不使用了,需要释放归还给系统,这些烦琐且危险的操作全部交由  JVM  去管理。 让我们能够更专心地编写程序,而不需要担心内存的问题。内存的统一管理,对于跨平台也 有相当大的帮助。  5.异常处理(Exception) 为了使 Java 程式更稳定、更安全,Java 引入了异常处理机制。能够在程序中产生异常情 况的地方,执行相对应的处理,不至于因突发或意外的错误造成执行中断或是死机。通过这 种异常处理,不仅能够清晰地掌握整个程序执行的流程,也使得程序的设计更为严谨。  1.2  Java 开发环境的搭建 现在我们开始搭建 Java 开发环境。本章仅涉及 J2SDK 开发环境的安装与调试,并不涉 及 JSP 及 J2EE 服务器的安装与调试,这部分内容将在第 4 章进行说明。  1.2.1  Java 运行环境的要求 首先,需要了解 Java 对计算机主要硬件要求的最低配置,如表 1.1 所示。该表右边部分 为笔者写作本书时所使用的计算机配置。因为涉及大量在计算机内存中进行绘制各种图表的 运算与操作,建议使用较高配置的计算机以及尽可能多的内存,以便流畅地运行本书中的程 序。 表 1.1  Java 的计算机硬件配置 硬 件 最低要求 笔者的配置  CPU  Intel 或者兼容微处理器,奔腾 166MHz 及其以上  AMD Athlon 3200+  最低 32MB,可以运行图形界面的 Java Application; 512 MB DDR,Kingston 内存 内存 最低 48MB,可以运行 Java Applet;内存不足,将会导 (如果读者希望把本书中的例程运行在 J2EE 的 致系统(尤其是服务器)的性能大幅下降  服务器,如 WebLogic,最好不低于 256 MB) 硬盘  242 MB  80 GB  图形显示卡 无要求  Nvidia Ti 4200 64 MB DDR  网卡 普通网卡  3Com OfficeConnect 10/100 兆位网卡 其他部件,如图形显示卡、光驱及显示器等,Java 并没有特别的要求。因为本书涉及图
    • 形的实时生成, Java 的图形生成是通过图形显示卡调用 OpenGL 功能来加速图像的渲染和 而 处理的。因此,如果系统拥有一块高质量的图形显示卡,将会大大地提高系统的性能。目前 市面上的图形显示卡,无论是在计算机主板上集成的显卡,还是普通的独立图形显示卡,都 具有强大的图形处理功能,足以满足流畅地运行本书中的所有例程。  Java 具有跨平台的特点,支持所有的主流操作系统,如下所示。  1.微软公司  Windows 系列 Ø  Windows 98 / Me  Ø  Windows NT  Ø  Windows 2000 / Windows 2000 Server  Ø  Windows XP  Ø  Windows 2003 Server  2.UNIX 系列 Ø  AIX,  IBM 的 UNIX, 是根据 SVR2 以及一部分 BSD 延伸而来, 加上各种硬件的支持。 具备特有的系统管理功能(SMIT,系统管理接口工具) 。 :基于 4.3BSD,包含许多来自 System  V 的东西。Sun  Ø  SunOS(680x0,Sparc,i386) 公司开发的 UNIX 操作系统,该系统对 UNIX 的贡献是:NFS,OpenLook GUI 标准, 现演变为 Solaris。 Ø  HP­UX(HP) :惠普公司的 UNIX。 Ø  SCO UNIX。  3.类 UNIX 系列 Ø  Linux:包括各种版本的 Linux,如 RedHat、Turbo 和 Mandrake Linux。 Ø  FreeBSD:由美国加州大学伯克利分校计算机系统研究小组设计和维护。
    • 4.其他操作系统 Ø  Mac OS:苹果电脑公司推出的操作系统,主要用于 Power PC。 上述操作系统都可以安装 Java。 笔者的操作系统是微软 Windows XP SP2。本书的所有例 程都是在 Windows XP 环境下进行编写和调试的。  1.2.2  Java 的安装和配置 本书采用 Sun 公司发布的最新版本,也是近年来 Java 最重要的一个升级版本——Java  2  Platform Standard Edition 5.0,即 J2SE 5.0 作为我们的 Java 开发平台。至于 J2SE 5.0 具备哪些 新的功能和特性,请读者自行查阅相关资料。本书将尽量使用 J2SE 5.0 中的新特性来增强代 码的性能,如 Image I/O、Java 2D 等,具体将在其他章节中说明。  J2SE 5.0 的下载地址:http://java.sun.com/j2se/1.5.0/download.jsp。双击下载文件:jdk­1_  5_0­windows­i586.exe 就开始了 J2SE 5.0 开发环境的安装过程,如图 1.2 所示。 默认 图 1.2    Java 的安装过程者一:改变默认的安装路径 安装过程如下: (1) 安装程序经过自解压后, 就会出现安装协议的对话框, 【I accept the terms in the  选择 license agreement】并单击【Next】按钮; (2)在出现选择安装路径的对话框时,我们改变 J2SE  5.0 的默认安装路径。单击右边 的【Change…】按钮,在出现的路径修改对话框中,输入“c:jdk1.5.0”后退回到先前的对话 框; (3)单击【Next】按钮,继续剩余部分的安装。接着 J2SE 5.0 会提示 Java 运行环境(即  JRE 5.0)的安装路径,这里不做任何的改变,采用其默认设置。 然后,连续单击【Next】按钮直到完成安装。最后系统会提示重新启动计算机,再重新 启动计算机,需要对运行 Java 的环境变量进行设置。这是非常重要的一个步骤,如果没有设 置成功,在运行 Java 时会出现错误。 环境变量的设置,如图 1.3、图 1.4 所示。
    • 图 1.3    Java 的安装过程之二:环境参数的设置 (1)进入【控制面板】,单击【系统】 ,在出现的【系统属性】对话框中,单击【高级】 选项。 (2)单击【环境变量】按钮。 (3)在环境变量对话框中,单击位于【系统变量】组中的【新建】按钮。 (4)新建一个系统变量, “变量名”为 JAVA_HOME,“变量值”为 C:JDK1.5.0,然后 单击【确定】按钮,如图 1.4 所示。这个 JAVA_HOME 的值就是 J2SE 5.0 的安装路径。 图 1.4    Java 的安装过程之三:环境参数的设置 (5) 重复第 4 步, “变量名” 在 中输入“CLASSPATH” 在 , “变量值”中输入 “.  %JAVA _HOME%lib”。注意,这里的“.”是要求 JAVA 编译器在查找 JAVA class 文件时,首 先从当前目录开始。 (6)最后是修改系统变量“PATH”的值, PATH”的新值是在原有值前面加上 “ “%JAVA_HOME%bin;”。 提示:在 Java  1.2 版以后,不再需要 CLASSPATH  来设置系统类的路径。CLASSPATH  是为了设置用户编写的类或者第三方开发的类库而设置的。 现在,为确保系统环境变量生效,重新启动计算机。重启之后,测试 J2SE 5.0 的安装与 环境变量设置是否正确。单击【开始】→【所有程序】→【附件】→【命令提示符】 ,启动命 令提示符后,在该窗口中输入如下命令: echo %java_home%  echo %classpath%  echo %path%
    • java –version 在笔者本机上,得到如图 1.5 所示的结果。 图 1.5  测试 Java 运行环境系统参数的设置 如果读者得到如图 1.5 一样的结果,就说明最新版的 Java 开发环境已经安装成功。 提示:读者计算机中“Path”值可能同笔者计算机的值不一样,但其中一定要包含 “C:jdk1.5.0bin”的内容。  1.3  Java/JSP 开发工具 为了获得 Java 图表编程学习的最佳效果, 本书所有源程序都没有使用任何一种可视化的  Java  开发环境来进行开发。建议读者在学习期间不要使用可视化的开发工具,如  JBuilder、  JCreator、KAWA、Visual  Cafe、Eclipse、IntelliJ  IDEA、BEA  WebLogic  Workshop、Oracle  JDeveloper  等。本书所有的操作,都可以用  JSP  服务器(Tomcat/Resin)+WWW  浏览器 (IE/Mozilla/Firefox)+文本编辑器(EditPlus)来轻松完成。这样,我们才可以深刻地理解 系统的运行机制。等到读者对此非常熟悉的时候,再使用可视化开发工具进行开发,定会达 到事半功倍的效果。此外,在本书的附录中,我们将向读者介绍一款优秀的 Java IDE 开发工 具——Gel。利用 Gel,可以方便地创建、编辑、编译及运行 Java/JSP/Servlet 应用程序。建议 读者循序渐进地学习之后,再来学习 Gel,相信一定会喜欢上这款精巧而强大的 Java 开发环 境。 基于文本编辑器的 Java/JSP 开发工具有很多种,如记事本、UltraEdit、EditPlus 等。但最 简单及成本最低廉的非  Windows  自带的记事本(NotePad)莫属了。可以直接使用记事本进 行 Java/JSP 的程序设计, 它具有简单、方便的特点。 但其毕竟不是为程序员准备的开发工具, 因此,并不适用于程序设计。 现在,向读者推荐一款韩国的文本编辑器——EditPlus 作为我们的 Java 及 JSP 编辑器。  1.3.1  EditPlus 简介  EditPlus  是由韩国人编写的一款共享软件,官方网址是  www.editplus.com。最新版本是  EditPlus 2.12。EditPlus 是功能全面的文本、HTML、程序源代码编辑器。主要特点如下: (1)默认支持  HTML、CSS、PHP、ASP、Perl、C/C++、Java、JavaScript  和  VBScript  的语法高亮显示,通过定制语法文件,可以扩展到其他程序语言。 (2)EditPlus 提供了与 Internet 的无缝连接,可以在 EditPlus 的工作区域中打开 Intelnet  浏览窗口。
    • (3)提供了多工作窗口。不用切换到桌面,便可在工作区域中打开多个文档。 (4)正确地配置 Java 的编译器“Javac”以及解释器“Java”后,使用 EditPlus 的菜单 可以直接编译执行 Java 程序。 所谓的文本编辑器,最终还是要归结在文本编辑上。EditPlus  提供了比普通编辑器更加 强大的编辑功能。首先可以无限制地 Undo,其次具有更强大的删除功能,EditPlus 不仅可以 删除一个字,而且还可以删除选择部分、删除整个行等。提供了更强大的复制功能,可以复 制整个行,如单击【Edit】菜单,选择【Duplicate line】命令,即可实现此功能。这些大大地 超越了记事本的功能。  EditPlus 查找/替换功能也是一流的。不但可以进行一般的查找和替换,而且支持以正则 表达式的方式进行查找与替换, 甚至可以在磁盘某一目录的所有文件中, 进行字符串的查找。 比如说,笔者要查找 D:webchart 目录(包括其子目录)下,所有的 jsp 文件中,哪些文件包 含了字符串: “charset=GB2312”,我们就可以单击菜单栏上的【Search】 ,选择【Find in Files】 命令。如图 1.6 所示,在出现的查找对话框中,分别填入以下内容: Ø【Find what】填入“charset=GB2312”,表示要查找的内容。 Ø【File  type】填入“*.jsp” ,表示要查找所有扩展名为  jsp 的文件,如果这一栏不输入 任何条件,则表示在所有的文件中查找,而忽略文件的格式。 Ø【Folder】填入“d:webchart” ,表示希望查找文件位于 d:webchart 这个目录下。 Ø 如果要查找  d:webchart  及其子目录下的所有  jsp  文件,就需要选中该对话框中的 “include subfolders”选项。 填好上述栏目后,单击该对话框中的【Find】按钮,就可以在  EditPlus  下方的输入输出 窗口中,得到查找的结果了。在输出窗口中,会提供一个被查找到的匹配文件的清单。在此 窗口的左侧,指出与被查找的字符串“charset=GB2312”相匹配文件的所在目录以及文件名, 然后给出字符串在该文件中出现的位置,该位置用行列坐标表示。在输出窗口的最底部,  EditPlus 会提供一个统计结果——查找到的匹配的文件总数。 图 1.6    EditPlus 的 Find in Files 功能 双击查找结果清单中出现的任一文件名,EditPlus  会在主窗口中显示出该文件的内容, 并将鼠标指针定位到被查找的字符串第一次出现的位置。 该功能在大量文件中进行查找时,提供了强大的支持。 前面谈到,设置好 Java 的编译器“Javac”和解释器“Java”后,通过 EditPlus 的菜单可
    • 以直接编译执行 Java 程序。现在就来看看如何在 EditPlus 中设置。单击菜单栏中的【Tools】 →【Configure User Tools】→【Preferences】→【User tools】 ,如图 1.7 所示。
    • 运行 Java 的配置 图 1.7    EditPlus 设置编译与运行 Java 程序的功能 Ø【Add Tool】 :表示新增一个用户自定义工具。 Ø【Program】 :表示该工具与程序有关。 Ø【Menu text】 :表示要求为添加的工具命名。这里填入:编译 JAVA。 Ø【Command】 :表示该工具运行时,所调用的可执行程序。Java 中的编译命令是 javac, 所以这里选择 Java 安装目录中 bin 子目录中的 javac.exe 程序。 Ø【Argument】 :表示执行  javac.exe  程序时,所需要的参数。单击旁边的带有向下三角 形的按钮来选择“$(FilePath) ”即可。 Ø 【Initial  directory】:选择初始化目录。单击旁边的带有向下三角形的按钮来选择“$  (FileDir) ”即可。 Ø【Capture output】 :选择捕获屏幕输出。 通过 EditPlus 来编译 Java 程序完成。现在继续增加一个新的工具,设置通过 EditPlus 来 运行编译成功的 Java  程序。方法同前面讲述的设置编译 Java  程序的过程基本相同。按照图  1.7 右下方所示的“运行 Java 的配置”内容,进行设定即可。 正确设置后,无论在 EditPlus 编译还是运行 Java 程序的时候,EditPlus 的输出窗口中都 会显示出相应的编译和运行结果。 关于 EditPlus 就简要介绍到这里。  1.3.2  UltraEdit 简介  UltraEdit 是共享软件,官方网址是 www.ultraedit.com,最新版本是 v11.00a。与 EditPlus  类似,UltraEdit  是一款功能强大的文本、HTML、程序源代码编辑器。作为源代码编辑器, 其默认配置可以对 C/C++、VB、HTML、Java 和 Perl 进行语法高亮显示。设计 Java 程序也 很方便,具有复制、粘贴、剪切、查找、替换、格式控制等编辑功能。  UltraEdit 同样可以在【Advanced】菜单的【Tool Configuration】菜单项配置 Java 编译器  Javac 和解释器 Java,直接编译运行 Java 程序。  UltraEdit 设置 Java 编译器“Javac”的方法如图 1.8 所示。
    • 图 1.8    UltraEdit 设置编译 Java 程序的功能 Ø【Command  Line】 :表示命名行的执行方式。n%表示 Java 源程序的文件名,%e 表示  Java 源程序的扩展名。因为已经正确设置了 Java 的运行路径,所以使用 javac 就可以 了,否则就要用 C:jdk1.5.0binjavac.exe。 Ø【Working Directory】 :表示工作目录。%p 表示工作目录就是当前目录。 Ø【Menu Item Name】 :要求给添加的工具命名。填入:编译 JAVA。 Ø【Save Active File】:表示该工具运行之前,首先保存当前 Java 源程序。 Ø【Output to List Box】:表示将结果输出到列表框中。 Ø【Capture Output】 :选择捕获屏幕输出。 Ø【Insert】:Java 编译的工具菜单就做好了。 同理,设置 Java 解释器“Java”的方法如图 1.9 所示。与前面的步骤类似,只需要更改 第一和第三个步骤即可。 【Command Line】输入“java %n”【Menu Item Name】输入“运行  , Java”。 关于 UltraEdit 就简单介绍到这里。
    • 图 1.9    UltraEdit 设置运行 Java 程序的功能  1.3.3  其他 Java/JSP 开发工具 下面向读者简要介绍两个开放源代码的作品 Eclipse 和 Gel,虽然本书并不使用这两种开 发工具,但笔者觉得有必要向读者介绍一下,目前国外非常流行的 Java IDE 开发工具。  1.Eclipse 简介  Eclipse 是一个开放源代码的项目, 可以在 www.eclipse.org 免费下载 Eclipse 的最新版本。 一般 Eclipse 提供几种下载版本:Release、Stable Build、Integration Build 和 Nightly Build。建 议下载 Release 或 Stable 版本。Eclipse 本身是用 Java 语言编写的,但下载的压缩包中并不包 含 Java 运行环境,需要用户自行安装 JRE,并且需要在操作系统的环境变量中,指明 JRE 中  bin 的路径。如果已经安装了 JDK,就不用安装 JRE 了。 安装 Eclipse 的步骤非常简单,只需将下载的压缩包按原路径直接解压即可。注意,如果 要更新版本,请先删除旧版本重新安装,不能直接解压到原来的路径覆盖旧版本。解压缩之 后,可以到相应的安装路径去找 Eclipse.exe 运行。如果下载的是 Release 或 Stable 版本,并 且 JRE 环境安装正确无误, 一般来说不会有什么问题, 在闪现一个很酷的月蚀图片后,  Eclipse  会显示它的默认界面。  Eclipse 的界面有点像 JBilder,实际操作进程中会发现它更像 IVJ。毕竟开发 Eclipse 的主导 是开发  IVJ  的原班人马(可参考  www.oti.com) 。值得一提的是,Eclipse 项目的参与者除了  IBM  以外,还有 Borland、Rational Software、RedHat、Merant,以及 BEA 等一大批业界的佼佼者,为  Eclipse 的未来奠定了良好的基础。  Eclipse 不仅可以开发 Java 应用程序,还可以安装 Lomboz 插件,并整合 Tomcat 以实现 对 JSP 以及 Java Servlets 的开发。 本书不在这里讨论 Eclipse 的下载、安装、使用、开发、各种插件的安装和使用,以及与  JBoss、Tomcat、WebLogic 等 JSP 或者 J2EE 服务器的整合。有兴趣的读者,请自行查阅相关 资料。 2.Gel 简介  Gel  是由  Gerald  Nunn  发布的一款  Java  IDE  开发工具。Gel  的官方主页是  http://  memescape.co.uk/gexperts/index.html。Gel 是完全免费的,读者可到该网站下载。在本书写作 时,Gel 的最新版本是 RC39。目前该 IDE 工具,在国外众多的 Java、JSP 及 J2EE 程序员中
    • 引起了极大的反响,但国内很少有人使用这款优秀的 Java IDE。  Gel 的运行环境如下: Ø  Windows 95/98/ME/2000/XP  Ø  Internet Explorer 4+  Ø  JDK 1.1+  Gel 的特点如下: (1)编辑器(Editor) Ø 支持对  Java、JSP、HTML、XML、C、C++、Perl、Python,以及更多编程语言的语 法高亮显示。 Ø 无限制地取消及恢复功能。 Ø 整段代码的减少缩进量与增加缩进量。 Ø 高亮显示配对的大括号。 Ø 实时地拼写检查。 Ø 可以显示控制字符。 Ø 整合的源代码管理及控制, 支持 CVS、VSS、Clearcase、Perforce、QVCS、CS­RCS  以 及更多的格式。 Ø 正则表达式搜索。 Ø 文件查找。 Ø 将被选择代码完整地输出为 HTML 及 RTF 文件。 Ø 保持本地程序的更新历史,易于在需要的时候恢复原状。 (2)代码援助(Code Assistants) Ø 自动完成 Java 和 JSP 的编码。 Ø  Java 和 JSP 的参数提示。 Ø  Java 和 JSP 的标识符提示。 Ø 查找声明,并快速跳转到声明所属的变量、方法或类。 Ø 类浏览器。 (3)项目管理(Project Management) Ø 智能文件夹,当系统文件发生改变的时候,自动同步更新。 Ø 可以运行 Applications、Applets 以及应用服务器。 (4)J2EE  Ø 直接预览 Servlets 及 JSP。 Ø 自动完成 JSP 标签,支持 HTML 及自定义 JSP 标签。 (5)其他 Ø 支持集成 ANT。 Ø 支持集成 JUnit。 Ø 定制工具栏及快捷方式。 Ø 支持创建自定义工具。 Ø  Javadoc 浏览器。 总的来说,Gel 虽然小巧,但速度奇快,更重要的是它拥有许多大型 Java IDE 的完整功 能。本书附录,将详细介绍 Gel 的设置及如何利用 Gel 来开发 JSP 程序。  1.4  第一个 Java 程序 前面我们讲解了 J2SE 5.0 的安装及环境变量的设置,现在我们就编写一个很简单的 Java
    • 应用程序 Application 来结束本章的学习。 在 EditPlus 或 UltraEdit 中输入如下代码(chap01Fig1.4WelcomeJavaChart.java): 1: // Fig. 1.4: WelcomeJavaChart.java  2: // 第一个 Java 程序 3:  4: public class WelcomeJavaChart{  5:   // Java 主程序入口 6:  public static void main(String args[]){  7: System.out.println("**************************************");  8:     System.out.println("欢迎来到<<精通 Java Web 动态图表编程>>的世界! ");  9: System.out.println("**************************************");  10:  11:   } //主程序结束 12:  13: } // end class WelcomeJavaChart 该源程序仅仅在命令窗口显示三条文本。可以用传统的方法,在  DOS  下对该文件进行 编译及运行,也可以在 EditPlus 及 UltraEdit 中进行。 将 WelcomeJavaChart.java 拷贝到 D:webchartchap01Fig1.4 子目录下,运行如下命令: D:webchartchap01Fig1.4>javac WelcomeJavaChart.java  D:webchartchap01Fig1.4>java WelcomeJavaChart 编译及运行结果如图 1.10 所示。 在 UltraEdit 中,编译和运行的方法是运行【Advanced】→【编译 JAVA】→【运行 JAVA】 工具,如果全部设置正确,就可以看到如图 1.11 所示的结果。 图 1.10    WelcomeJavaChart.java 的运行结果
    • 图 1.11    UltraEdit 下 WelcomeJavaChart.java 的运行效果 在 EditPlus 中,编译和运行的方法是运行【Tools】→【编译 JAVA】→【运行 JAVA】工 具,如果全部设置正确,就可以看到如图 1.12 所示的结果。
    • 图 1.12    EditPlus 下的 WelcomeJavaChart.java 的运行结果  1.5  本章小结 本章主要介绍 Java 的历史及其运行机制、技术特点,讲解了各种 Java 开发环境的搭建, 包括 JDK 的下载、安装和环境变量的设置,在 EditPlus 和 UltraEdit 中如何设置编译及运行。 此外,简要介绍了两款目前国外流行的 Java IDE 开发环境:Eclipse 和 Gel。最后用一个简单 的 Java Application 来测试搭建的 Java 开发环境是否成功。 只有在正确搭建  Java  开发环境的基础上,才能顺利地搭建  JSP  服务器。下面学习  Java  Applet 的编写和调试,让我们一起进入 Web 图表编程的精彩世界! 第 2 章  Java Applet 与绘图基础  Java 最初在因特网上流行,很大程度上归功于 Java Applet。Java 对图形处理的应用最早 就是从 Applet 开始的,对于希望掌握 Web 动态图表编程的读者来说,从 Applet 着手学习是 一个很好的途径。理解 Applet 的绘图基础,就可以顺利地过渡到如何运用 JSP 来进行图表的 绘制。  2.1    Java Applet 概述  Java 共有两种类型的程序。一种称为 Java  Application,如第 1 章 1.4 节所示的 Welcome
    • JSPChart.java。本章介绍另外一种 Java 程序,这类程序称为 Java Applet。Java Applet 是一种 可以嵌入 HTML 文档(即 Web 页)中并可以被执行的 Java 程序。支持 Java 的浏览器,在浏 览一个包含有 Java Applet 的 Web 页面时,该 Applet 就被下载到该浏览器中,并被 JVM 解释 执行。 目前,支持 Java2 的浏览器有:Mozilla、Netscape7 及其以上版本、Opera 和 FireFox。微 软的 Internet  Explorer 或者早期的 Netscape 版本,需要安装 Java 的插件(Plug­in)才可以正 常浏览 Java Applet 的 Web 页面。 提示:如果读者的计算机中已经安装了 J2SE  5.0 开发环境,那么 IE 就不需要再安装插 件。该插件是作为 J2SE 5.0 的一部分(JRE 5.0)安装在浏览器上的。 我们先来看 J2SE  5.0 中提供的一些 Applet 范例,展示了 Applet 的强大功能。其中每个  Applet 还提供了源代码。通过阅读这些源代码,可以了解 Java 最新的、令人激动的新特性及 其用法。  J2SE  5.0 提供的范例位于 J2SE  5.0 安装目录中的 demo 子目录下。在 MS  Windows 中, 该范例的默认安装位置是:C:jdk1.5.0demoapplets;在 Unix/Linux/Mac  OS 中,该范例的默 认安装位置是:/usr/local/jdk1.5.0/demo/applets。 在 applets 子目录下又包含了一些子目录,每一个子目录对应着一个相应的 Java Applet, 每个 Applet 展示了不同的功能。表 2.1 列出了这些范例及其功能。 表 2.1    applets 目录中的范例 范 例 说 明  Animator  演示了四种不同类型的动画  ArcTest  演示了圆弧的绘制,可以与该 Applet 交互并改变圆弧的显示属性  BarChart  演示了一个简单的条形图  Blink  演示了以不同颜色显示并闪烁的文本  CardTest  演示了几个 GUI 的部件及布局方式  Clock  演示了一个可以显示当前日期和时间的时钟  演示了用图形抖动(dithering)技术进行图形绘制,该技术实现的是从一种颜色逐渐转 DitherTest  变成另一种颜色(渐变色)  DrawTest  演示了使用鼠标在 applet 上,用不同的颜色绘制线段和点  Fractal  演示了绘制一个不规则图形  GraphicsTest  绘制不同的图形,以演示 Java 对图形的处理能力  绘制一个通过直线连接,由许多个节点(用矩形表示)组成的图形。可以拖动其中一个 GraphLayout  节点,观察其他节点是如何重新调整它们位置的情形,以演示复杂的图形交互  演示了一个有热点的图像。当鼠标移动到热点区域,该区域将会高亮显示,并在  ImageMap  appletviewer(或者浏览器)的左下角(状态栏)显示一条消息。当鼠标定位在图像中人像 嘴上的时候,会听见 applet 说“hi”  JumpingBox  在屏幕上随机移动一个矩形,可以试一试用鼠标去捕获它  演示了几个不同化学分子结构的三维视图,可以拖动鼠标从不同的角度来观察这些化学 MoleculeViewer  分子的结构  NervousText  绘制在屏幕上跳动的文本  SimpleGraph  绘制一条复杂的曲线
    • 比较三种不同的排序技术。执行该 Applet 时,会出现三个 appletviewer 窗口。单击其中 SortDemo  任意一个窗口,都会开始排序,不同的排序技术所花费的时间是不同的  SpreadSheet  演示了一个简单的,由行和列组成的电子表格  TicTacToe  演示了一个叫做 TicTacToe 的游戏,由计算机和用户互弈  WireFrame  演示了四个由许多线段组成的三维图形,分别是立方体、恐龙、直升飞机和军舰 运行上述例程的方法有两种。我们以运行表 2.1 中所讲述的 ArcTest Applet 程序为例: (1)用 Java 自带的 Applet 容器 appletview.exe 来运行上述例程。 在【命令提示符】下,输入如下命令: C:Documents and SettingsJKZhong>cd C:jdk1.5.0demoapplets ArcTest  C:jdk1.5.0demoappletsArcTest>appletviewer example1.html (2)会看到 ArcTest 的运行结果,再进行如下操作,会看到如图 2.1 所示的结果。 Ø 在第一个文本框中输入 10,表示圆弧的起始角度。 Ø 在第二个文本框中输入 145,表示圆弧的终止角度。 Ø 按钮【Fill】表示绘制并填充这个圆弧,按钮【Draw】表示仅仅绘制圆弧。单击【Fill】 按钮可以看到,绘制的结果是一个从 10 开始到 145 结束的实心圆弧。 (3)直接用浏览器打开 C:jdk1.5.0demoappletsArcTest 子目录下的 example1.html 文 档,就可以运行上述 Java Applet 程序,这里以微软的 IE 为例。 Ø 如果与笔者相同,使用微软 Windows XP SP2 下的 IE,可能会看到如图 2.2 所示的结 果。IE 可能会有这样一个提示: “为帮助保护您的安全,Internet Explorer 已经限制此 文件显示可能访问您的计算机的活动内容。单击此处查看选项…” 。 Ø 这时无法看到 Java Applet 的运行结果,因为被 IE 屏蔽了。解决的方法是,在 IE 的提 示下单击鼠标右键,在弹出的菜单中选择【允许阻止的内容】 。 Ø 然后会弹出一个【安全警告】对话框,单击【是】按钮,即可看到运行如图 2.2 所示 的 Java Applet 结果。 图 2.1    appltviewer 的运行结果 图 2.2    ArcTest Applet 的运行结果 提示:如果 appletviewer 命令不能工作,或者收到一条消息,表示没有找到 appletviewer  命令,则计算机可能没有正确地设置 PATH 的环境变量。 读者需要按照 1.2.2 节讲述的方法重 新设置 PATH 变量,修改 PATH 变量后,需要重新启动计算机。
    • 2.2    Java Applet 工作流程  2.2.1  组件与容器 一个组件 (Component)代表可以在屏幕上显示出来的一个图形实例, 也就是 Component  类的任意一个子类的对象。图 2.3 是以 Component 类为基础类的继承关系结构图,图中箭头 所指向的类为超类。 从图 2.3 可以看到,JApplet、JFrame、Dialog 等就是一个组件,它们都属于 Component  类的子类。所有从 Container 派生出来的类都可以包含由 Component 类派生出来的任意一个 类的对象,所以我们称之为容器(Container) 。又因为 Container 类是 Component 类的一个子 类,所以任意一个容器对象同时又是一个 Component 对象, 因此一个容器可以包含其他容器。 下面以 Panel 类对象来说明这个关系。我们把一些图形实例如矩形、椭圆等放置在一个 Panel  类的对象——panelA 中, 再把另外一些 Component 对象如菜单、 按钮等放置在另外一个 Panel  类的对象——panelB 中,最后把 panelA 和 panelB 放置在另外一个 Panel 类的对象——panelC  中。这里的例外是,Windows 类及其子类的对象,不能包含在其他容器中,否则,系统会抛 出一个异常。 Component  (抽象类)  Container  (抽象类)  JComponent  (Swing 组件)  Panel  Window  (旧式容器)  (无边框或标题栏)  Applet  Frame  (旧式 Applet 类)  (旧式框架类)  Dialog  (旧式对话框类)  JWindow  JApplets  JFrame  JDialog  (无边框或标题栏)  Applet 的基类 应用程序 对话框的基类 窗口的基类 图 2.3  以 Component 类为基础类的继承关系结构图  JComponent  类是所有在一个窗口中作为  GUI  一部分使用的  Swing  组件的基类。因为  JComponent 也是从 Container 类继承下来的,故此所有的 Swing 组件也是容器。  JApplet 类是所有 Swing Applet 类对象的基类,它由 Component 派生的 Container 类派生 出来。因此,一个 Applet 程序就从 Container 和 Component 类继承所有的方法。此外,它也 继承了旧的 Applet 类的方法。 这里需要注意以下几点: Ø  JApplet 类、JFrame 类、JDialog 类以及 JComponent 类,包含在 javax.swing 包中。 Ø  Applet 类包含在 java.applet 包中。 Ø 除此之外的类,包含在 java.awt 包中。  java.applet  包很小,它只包含这一个类和三个相关的接口,我们很少直接使用该包中的 方法。在  JApplet  类中,对旧的  Applet  类的方法进行了扩充和改进(旧类与  javax.swing  包中的 GUI 组件不完全兼容) 。所以本书所有相关的 Applet 类都扩展于 JApplet 类。 
    • 2.2.2  Applet 的编译与执行 如同 Java Application 程序一样, 在执行前必须对 Applet 类的源程序进行编译, 编译的方 法与 1.4 节讲述的方法相同。  Applet 被嵌入到 HTML 页面中, 并由 Applet 容器(appeltviewer 或者 Web 浏览器)执行。  Applet 的运行由浏览器控制, 不由 Applet 中的代码控制。 当浏览器载入包含有 Applet 的 Web  页面时,它将生成一个 Applet 类的对象,然后利用 Applet 类对象的五个方法控制 Applet 的 执行。这五个方法都是 public 类型,不返回任何结果,以及不需要任何参数的方法。 表 2.2 列出了 5 个方法及其执行的相应功能。 表 2.2    Applet 默认的 5 个方法 方 法 说 明  启动 Applet。浏览器总是调用 Applet 类的默认构造器生成对象,然后调用 init()方法进行初始 void init()  化。一般在这个方法中生成 Applet 运行所需的对象并初始化 Applet 类的所有数据成员  方法由浏览器调用,启动或者重新启动  Applet。当  Applet  第一次启动时,start()方法将紧跟 void start()  在  init()方法后被浏览器调用。如果用户离开当前的  HTML  页面后,在重新返回到当前  HTML  页面时,Start()方法也将被调用。start()方法一般用来启动 applet 需要的任何附加线程  在 init()方法执行结束,  start()方法启动后,就调用此画图方法。另外,每次需要重新绘制 Applet  void paint( Graphics g )  时,也将调用此方法。本方法的典型应用,包括使用  Applet  容器传递给  paint  方法的  Graphics  对象 g 画图  当用户离开包含有该 Applet 的 HTML 页面时,浏览器调用此方法。stop()方法被调用后,将 void stop()  立即停止所有在 start()方法中启动的操作  在终止 Applet 运行时,调用 destroy()方法,以便释放 Applet 占用的,由本地操作系统管理的 void destroy()  任何系统资源。此方法执行之前,总是先调用 stop()方法 我们编写的 Applet 程序都将继承上述 5 种默认的方法。对于每个 Applet,方法调用的顺 序总是 init、start 和 paint,这就保证了每个 Applet 开始执行时都将调用这些方法。但是,并 非每个 Applet 都需要调用 5 种方法。我们可以重载上述的 5 种方法,也可以重载其中的某种 方法,以便 Applet 实现我们所设计的方法。 浏览器通过包含有 Applet 的 HTML 页面来运行 Applet 程序。 那么,在执行 Applet 之前, 首先要创建 HTML 文档。
    • 2.2.3  包含 Applet 的 HTML  通常,HTML 文档以“.html”或者“htm”的扩展名结尾。一个在 HTML 页面中包含有  Applet 的 HTML 文档,必须指明 Applet 容器应该装入和执行哪一个 Applet。 下面是一个完整的包含 Applet 的 HTML 源代码: 1: <!DOCTYPE HTML PUBLIC "­//W3C//DTD HTML 4.0 Transitional//EN">  2: <HTML>  3: <HEAD>  4:  <TITLE>包含有 Applet 的 HTML 页面</TITLE>  5:  </HEAD>  6:  7:  <BODY>  8:          <APPLET  CODE = "MyAppletName.class"  9:  WIDTH = "Applet_width_in_pixels"  10:  HEIGHT = "Applet_height_in_pixels" >  11:         </APPLET>  12:  </BODY>  13: </HTML> 我们可以看到,代码第 8 行~第 11 行是隶属于<applet>和</applet>标记之间的内容,也 是一个包含 Applet 的 HTML 页面所需的最少内容,其余部分都是标准的 HTML 标记。  Applet 标记内的“CODE”属性,表示该 Applet 的.class 文件,它假设 Applet 位于包含 该 Applet 的网页所在的相同目录下。 本例指明 Applet 容器, 载入一个名为 “MyApplet Name” 的  Applet  字节码文件。如果.class  文件和  HTML  文件不在同一目录下,则需要使用一个  CODEBASE 属性, 它拥有一个 URL 值, 指明 applet 源代码的位置,  URL 可以是绝对值 (以 http://  开头) ,也可以是相对值。属性“WIDTH”表示该 Applet 在网页上显示的宽度,用像素表示。属 性“HEIGHT”表示该 Applet 在网页上显示的高度,也用像素表示。 了解前面我们介绍的 Applet 的相关知识,就可以编写一些 Applet 程序了。  2.3  绘制文本(字符串) 现在,让我们来开发一些  Applet。首先介绍一个简单的  Applet  程序——DrawString  Applet.java,就是在 Applet 上绘制一段文本(字符串) “欢迎来到《精通 Java  Web 动态图表 编程》的世界!。  ” DrawStringApplet.java 的源程序清单如下(chap02Fig2.3Fig2.3_01): 1: // Fig. 2.3_01: DrawStringApplet.java  2: // 第一个 Java Applet 程序, 绘制文本(字符串)  3:  4: // 引入相关的包 5: import java.awt.Graphics; // 引入 Graphics 类 6: import javax.swing.JApplet; // 引入 JApplet 类 7:  8: public class DrawStringApplet extends JApplet  9: {  10:  11:   // 在 applet 上绘制文本 12:  public void paint(Graphics g)  13:   {  14:       // 调用父类的 paint 方法 15:  super.paint(g);  16:  17:       g.drawString("欢迎来到 《精通 Java  Web 动态图表编程》的世界!",  25,  25);
    • 18:  19:     } // paint 方法结束 20:  21: } // DrawStringApplet 类结束 提示:本书后面的例程将不再列出完整的源代码清单,请读者从 www.broadview.com.cn 网 站下载本书配套代码,查看相关目录下完整的源代码清单。 包含这个 Applet 的 web 页面文件 DrawStringApplet.html 的源程序清单如下: 1: <!DOCTYPE HTML PUBLIC "­//W3C//DTD HTML 4.0 Transitional//EN">  2: <HTML>  3: <BODY>  4:  <APPLET  CODE  "DrawStringApplet.class"  WIDTH  "320  HEIGHT  "45"  =  =  "  =  >  5:  </APPLET>  6:  </BODY>  7:  </HTML> 提示:本书所有的 Java  Applet 都将使用与本例相类似的 HTML 文档。对于不同的 Java  Applet,读者只需要修改上述代码第四行的内容, 重新设置“CODE”、 “WIDTH” “HEIGHT” 和 的属性值即可。 图 2.4 展示了名为 DrawStringApplet 的 Applet 在四个不同的 Applet 容器中的运行结果。 分别是 Appletviewer、  Netscape Web 浏览器(现在更名为 Mozilla) Opera, 、  以及微软的 Internet  Explorer Web 浏览器。 该程序说明了 Java 的几个重要特性。程序第 17 行完成的实际工作,即在屏幕上绘制一行文 本“欢迎来到《精通 Java Web 动态图表编程》的世界!。下面我们逐行来分析本程序。 ” 程序第 1 行~第 2 行: // Fig. 2.3: DrawStringApplet.java  // 第一个 Java Applet 程序, 绘制文本(字符串) 是注释。第 1 行说明例程的编号,以及源程序的文件名。第 2 行描述程序所实现的功能。
    • x 轴  appletviewer 窗口 y 轴  applet 菜单 绘制区左上角是(0,0) ,绘 制区边缘在状态栏上 x 坐标从 状态栏显示 applet  左向右递增,y 坐标从上向下 当前的运行状态 递增 文本在该处的坐 标是(25,25) 绘制区域在左上角 坐标是(25,25) 状态栏 绘制区域在左上角 坐标是(25,25) 绘制区域在左上角 坐标是(25,25)  状态栏 图 2.4    DrawStringApplet 在四个不同的 Applet 容器中的运行结果 程序第 5 行: import java.awt.Graphics; // 引入 Graphics 类 是一个 import 声明, 表明 Applet 使用 java.awt 包中的 Graphics 类。Java 包含了很多预定 义的类,并按包(类的集合)的形式进行分组。Graphics  类是一个具备图形处理能力的类, 可以让 Java Applet 能够绘制直线、文本、矩形及椭圆形等图形对象。 程序第 6 行: import javax.swing.JApplet; // 引入 JApplet 类 也是一个 import 声明,表明 applet 使用 javax.swing 包中的 JApplet 类。在 Java 2 中创建 一个 Applet 时,必须引进 JApplet 类。 程序第 8 行: public class DrawStringApplet extends Japplet 声明了一个名为 DrawStringApplet 的公有 (public) 并继承了 javax.swing 包中的 JApplet  类, 类。该类的主体从程序第 9 行的左花括号“{”开始,到程序第 21 行的右花括号“}”结束。 在继承关系中,JApplet 类称为超类(也称为基类或父类) ,DrawStringApplet 被称为子类或派 生类。因为,DrawStringApplet 继承了 JApplet 类,所以,它不仅具有 JApplet 类的所有属性 
    • 和方法,而且还可以增加新的方法和属性,以及扩展被继承的方法。 程序第 12 行: public void paint(Graphics g) 声明 Java Applet 的 paint 方法。前面我们提到的 paint 方法是 Java Applet 容器在载入 Applet  时,自动调用三个方法中的一个(其他两个是 init 和 start 方法) 。如果我们不在 Applet 中重 新定义该方法,那么 Applet 容器将调用继承下来的默认超类的 paint 方法。因为,默认超类 (JApplet)的 init、start 和 paint 方法为空(指明该方法的方法体中没有任何语句) ,也就是 说,超类的 paint 方法不会在 Applet 上绘制任何东西,所以在屏幕上也就不会有任何显示。  void 表示 paint 方法执行完后,不返回任何结果。paint 方法需要一个 Graphics 类的对象  g(对象也称为“类实例”或者“实例”。paint 方法使用 Graphics 类的对象 g 在 applet 上绘 ) 制各种图像对象, 如文本及各种几何图形等。  Graphics 类的对象 g 的创建由 Applet 容器负责。 为了实现在屏幕上绘制出一段文本,程序第 13 行~19 行,重载(重新定义)了超类的  paint 方法。 程序第 15 行: super.paint(g); 调用超类 JApplet 的 paint 方法。该语句是 Java Applet 程序中 paint 方法体的第一个语句。 早期的 Java Applet 在没有该语句的情况下仍然可以正常运行, 但在一个拥有众多绘图组件和  GUI 组件的复杂 Java Applet 中, 忽略该语句可能会导致严重的错误。 所以, 在编写 Java Applet  程序的时候,一定在 paint 方法的第一行设置这条语句,这是一个很好的编程习惯。 程序第 17 行: g.drawString(“欢迎来到《精通 Java Web 动态图表编程》的世界!”, 25, 25); 通过 Graphics 类的对象 g 来调用该类的 drawString 方法,以实现在 Applet 上绘制文本的 功能。drawString 方法的具体使用如表 2.3 所示。 表 2.3    drawString 方法 方法  public abstract void drawString(String str, int x, int y)  说明 用当前的字体和颜色,在指定的坐标(x,y)处,绘制字符串 str 的内容 调用的具体方法是 Graphics  类的对象名称(这里是  g)加上一个点操作符“.” ,再加入 所调用的方法名称(这里是 drawString) 。方法名后面跟着一对括号,括号内包含有执行该方 法所需的参数。  drawString 方法需要三个参数,第一个是  String 类的字符串参数,表示要绘制的文本。 最后两个是  int  类型的整数型参数,表示要绘制文本的第一个字符左下角所在的位置,该位 置用 Java 的平面坐标系来表示。 图 2.5 说明了 Java 坐标系。坐标原点位于左上角,以像素为单位。像素是计算机屏幕上 最小的显示单位。在 Java 的坐标系中,第一个是 x 坐标,表示当前位置为水平方向,距离坐 标原点 x 个像素;第二个是 y 坐标,表示当前位置为垂直方向,距离坐标原点 y 个像素。在  Appletviewer 中,坐标原点(0,0)正好位于 Applet 菜单的下方。在 Web 浏览器中,原点(0,  0)位于 Applet 执行的矩形区域的左上角(如图 2.5 所示) 。
    • 坐标原点(0,0)  25 +X  X 轴  25 欢迎来到《精 (x,y) +Y  Y 轴 图 2.5    Java 坐标系 计算机在屏幕上显示的内容都是由屏幕上的每一个像素组成的。例如,计算机显示器的 分辨率是 800´600,表示计算机屏幕上的每一行由 800 个点组成,共有 600 行,整个计算机 屏幕共有  480 000  个像素。现在的计算机可以支持更高的分辨率,也就是说,屏幕上可以显 示更多的像素。Java Applet 在屏幕上的大小,依赖于屏幕的大小与分辨率。对于大小相同的 屏幕来说,分辨率越高,Java Applet 在屏幕上的显示就越小,这是因为屏幕上的像素更多了。 程序第 17 行,将在坐标(25,25)上绘制“欢迎来到《精通 Java Web 动态图表编程》 的世界!”如图 2.4、图 2.5 所示。  Java Applet 可以在 paint 方法中多次调用 drawString 方法,在屏幕的任意位置绘制文本。 我们就可以利用这种方法来显示多行文本。  DrawMultiStringApplet.java  演示了如何绘制多行文本(chap02Fig2.3Fig2.3_02) 。通过 调用该目录下的 DrawMultiStringApplet.html 的运行结果如图 2.6 所示。 坐标(25,25) 坐标(25,40) 图 2.6  在 Java Applet 上绘制多行文本 本例同第一个 Java  Applet 程序相比,仅仅增加了第 18 行。图 2.7 所示的两行文本之所 以能够左对齐, 就是因为第 17 行和第 18 行的 drawString 方法, 都使用了相同的 x 坐标(25)。 此外,两者使用不同的 y 坐标(第 17 行是 25,第 18 行是 40)。因此在 Applet 相同的水平 位置,不同的垂直位置上显示文本,也就实现了左对齐的功能了。如果把第 17 行和第 18 行 的内容相互交换,则运行的结果也是相同的,这说明文本的位置由其坐标决定。 绘制文本时,不能通过换行符(n)来实现文本换行的功能。如  DrawMultiStringApplet2.  java(chap02Fig2.3Fig2.3_03)和 DrawStringApplet.java 相比,主要是第 17 行语句不同,如下 所示: g.drawString(“欢迎来到《精通 Java Web 动态图表编程》的世界!n 不用不知道,Java 真奇妙!”, 25, 25); 即使绘制的文本中包含有换行符(n),运行结果如图 2.7 所示,字符串并没有分行显示。
    • 图 2.7  用n 绘制多行文本 提示: 在某些 Java 版本中,运行结果会在字符串中的换行符位置处显示一个黑色小方框, 表示 drawString 方法不能识别该字符。  2.4  绘制线段 如果要在 Java Applet 中绘制一段直线,就需要调用 Graphics 类的 drawLine 方法。 先来 看 drawLine 的用法,如表 2.4 所示。 表 2.4    drawLine 方法 方法  public abstract void drawLine(int x1, int y1, int x2, int y2)  说明 用当前颜色在端点 1(x1,y1)和端点 2(x2,y2)之间绘制一条直线  drawLine 方法需要四个参数,表示该线段的两个端点。第一个端点的 x 坐标和 y 坐标分 别由第 x1、y1 个参数决定;第二个端点的 x 坐标和 y 坐标分别由第 x2、y2 个参数决定。所 有的坐标值都相对于该 Applet 的左上角坐标(0,0)。drawLine 方法就是在这两个端点之间 绘制一条直线。 程序 drawLineApplet.java(chap02Fig2.4)绘制了 11 条直线、1 个三角形和 1 个矩形。源 程序的运行结果如图 2.8 所示。 坐标(160,10) 坐标(90,10) 坐标(10,10) 坐标(230,10) 坐标(90,110) 坐标(330,10) 坐标(110,110) 坐标(210,110) 坐标(230,110) 坐标(330,110) 图 2.8    drawLineApplet.java  的运行结果 程序第 5 行: import java.awt.Graphics; // 引入 Graphics 类 导入 Applet 所需的 Graphics 类(来自 java.awt 包)实际上, 。 如果总是使用完整的 Graphics  类名(java.awt.Graphics),即包含完整的包名和类名,就不需要第 5 行的 import 语句了。也 就是说,如果没有第 5 条语句,那么第 12 行就可以改写成如下形式:
    • public void paint(java.awt.Graphics g) 程序第 6 行: import javax.swing.*; // 引入 javax.swing 包中所有的类 这里告诉编译器,Applet  可以使用  javax.swing 包中所有的类。星号(*)表示  javax.swing  包中所有的类都可以被编译器所使用。不过,在程序开始运行时,它并不会将该包中所有的 类都载入内存中,而只是装载程序使用的那些类。本例仅加载了 JApplet 类。 程序第 15 行: super.paint(g); 调用超类 JApplet 的 paint 方法。编写 Java  Applet 程序时,在 paint 方法的第一行设置这 条语句。 程序第 18 行~第 21 行: for (int i = 1; i <= 11 ; i++ )  {  g.drawLine(10, 10, 90, i * 10 );  } 运行了一个循环语句,调用了 11 次 drawLine 方法,在 Applet 上绘制了 11 条线段。这  11 条线段第 1 个端点的坐标都是相同的,即(10,10) ,第 2 个端点坐标的 x 坐标也是完全 相同的。只是 y 坐标不同, 每循环一次,y 坐标依次增加 10 像素。该循环的运行结果如图 2.8  中最左边的图形。 程序第 24 行~第 26 行: g.drawLine(160, 10, 110, 110);  g.drawLine(160, 10, 210, 110);  g.drawLine(110, 110, 210, 110); 直接调用 3 次 drawLine 方法,在 Applet 上绘制了由 3 条线段而组成的一个三角形。这 个三角形的三个端点坐标分别是(160,10)(110,110)和(210,110) 、 。用 drawLine 方法 在两个端点之间绘制一条直线,运行结果如图 2.8 中的三角形。 程序第 29 行~第 32 行: g.drawLine(230, 10, 230, 110);  g.drawLine(230, 10, 330, 10);  g.drawLine(330, 10, 330, 110);  g.drawLine(230, 110, 330, 110); 直接调用 4 次 drawLine 方法,在 Applet 上绘制了由 4 条线段组成的一个矩形。这个矩 形的 4 个端点坐标分别是: (230,10)(330,10)(330,110)和(230,110) 、 、 。用 drawLine  方法绘制该矩形的四条边,运行结果如图 2.8 中右边的矩形。
    • 2.5  色彩管理 早期的电脑系统是没有色彩管理这个概念的, 直到 90 年代苹果电脑推出了 ColorSync 1.0  色彩管理,它局限于苹果电脑之间的色彩控制。1993  年国际色彩联盟(Interational  Color  Consortium)成立,制定了开放式色彩管理规则──ICC,使色彩在不同品牌的电脑输出/输 入设备之间能够互相沟通,产生一致的色彩效果。随后,苹果电脑系统、微软的  Windows  系统、Sun 微电脑系统等相继支持 ICC 规则。  2.5.1  色彩基础 当我们从事设计的时候,最想了解的是如何有效地使用色彩。例如,使用什么色彩会产 生什么效果,用什么色彩代表什么性格,色彩如何搭配才美观,等等。要了解这些内容,首 先要认识色彩,知道色彩具有什么物理特性,以及色彩是如何形成的。  1.什么是色彩 我们生活在一个多彩的世界里。白天,在阳光的照耀下,各种色彩争奇斗艳,并随着照 射光的改变而变化无穷。但是,每当黄昏,大地上的景物无论多么鲜艳,都将被夜幕吞没。 在漆黑的夜晚,我们不但看不到物体的颜色,甚至连物体的外形也分辨不清。同样,在暗室 里,我们什么色彩也感觉不到。这些事实告诉我们,没有光就没有色,光是人们感知色彩的 必要条件。色彩来源于光,所以,光是色彩的源泉,色彩是光的表现。要了解色彩产生的原 理,必须对光做进一步的了解。 光的物理性质由它的波长和能量来决定。波长决定了光的颜色,能量决定了光的强度。 光映像到我们的眼睛时,波长不同决定了光的色相不同。波长相同能量不同,则决定了色彩 明暗的不同。 只有波长 380nm~780nm(1nm=10-6 mm)光的辐射才能引起人们的视觉感知,这段光波 叫做可见光。我们看到的色彩,事实上是以光为媒体的一种感觉。色彩是人在接受光的刺激 后,视网膜的兴奋传送到大脑中枢而产生的印象。 英国科学家牛顿在 1666 年发现,把太阳光经过三棱镜折射,然后投射到白色屏幕上,会 显出一条像彩虹一样美丽的色光带谱,从红色开始,依次是橙、黄、绿、青、蓝、紫七色, 如图 2.9 所示。 这条依次排列的彩色光带称为光谱。这种被分解过的色 光,即使再通过三棱镜也不会分解为其他的色光,我们把光 谱中不能再分解的色光叫做单色光。由单色光混合而成的光 叫做复色光。自然界的太阳光,白炽灯和日光灯发出的光都 图 2.9  光谱 是复色光。  2.色彩的三属性 我们不仅观察物体的色彩,同时还会注意到形状、面积、体积,以致该物体的功能和所 处的环境。这些对色彩的感觉都会产生影响。为了寻找规律,人们抽出纯粹色知觉的要素, 认为构成色彩的基本要素是色相、明度、纯度,这就是色彩的三属性。 Ø 色相(Hue) 色相指色的相貌与名称。色轮是表示最基本色相关系的色表。色轮上  90 度角内的几种色彩称作同类色,也叫做近邻色或姐妹色;90 角以外的色 图 2.10  色相
    • 彩称为对比色。色轮上相对位置的色叫做补色,也称为相反色,如图 2.10 所示。 Ø 明度(Value 或 Brightness) 人眼之所以能够看到物体的明暗,是因为物体所反射色光的光量(热量)存在差异。光 量越多,明度越高,反之明度越低。色料的色彩明度,取决于混色中白色和黑色含量的多少, 如图 2.11 所示。 Ø 纯度(Chroma) 所谓色彩的纯度,是指色彩鲜艳与混浊的程度,也称为彩度。色轮上各颜色都是纯色, 纯度最高。色料的混合中,越混合,色彩的纯度越低,如图 2.12 所示。 图 2.11  明度变化 图 2.12  纯度变化  3.混色理论 Ø 原色 无法用其他色彩混合得到的基本色彩称为原色。原色按照一定比例混合可以得到各种色 彩。究竟原色有多少种,又是哪几种颜色?历史上不同的学者有不同的说法,但基本上分为 三原色说和四原色说。 色彩的混色有两种类型:加法混色和减法混色。 Ø 加法混色 色光三原色(红、绿、蓝)按照一定的比例混合可以得到各种色光。三原色光等量混合 可以得到白色。色光混色和色料混色刚好相反,色光混色是越混合越明亮,所以称为加法混 色。我们熟悉的电视机和电脑的 CRT 显示器产生的色彩方式就属于加法混色,如图 2.13A 所 示。 Ø 减法混色 色料三原色(品红、黄、青蓝)按照一定的比例混合可以得到各种色彩。理论上三原色 等量混合可以得到黑色。因为色料越混合越灰暗,所以称为减法混色。水彩、油画、印刷等, 它们产生各种颜色的方法都是减法混色,如图 2.13B 所示。  A:加法混色  B:减法混色 图 2.13  色彩的混合 这两种混色方法,通过对相对应的色彩取反操作,就可以相互转换了。
    • 4.色彩空间及色彩模型 “色彩空间”一词源于英语“Color Space” ,也称做“色域” 。实际上就是各种色彩的集 合,色彩的种类越多,色彩空间越大,能够表现的色彩范围(即色域)越广。对于具体的图 像设备而言,其色彩空间就是它所能表现色彩的总和。 自然界中的色彩千变万化,我们要准确地表示色彩空间就要使用到色彩模型。人们建立 了多种色彩模型,以一维、二维、三维甚至四维空间坐标来规范这些色彩。 最常用的色彩模型有:  HSB、  RGB、  CMYK 以及 CIE Lab 等色彩模型。 其中 RGB、  CMYK  是惟一两种专门面向数码设计和出版印刷的色彩模型。这里我们介绍  RGB、CMYK  色彩模 型,认识这两个色彩模型,有助于我们在设计中准确地把握色彩。 所谓 RGB 就是:红(Red) 、绿(Green) 、蓝(Blue)三种色光原色。RGB 色彩模型的 混色属于加法混色。每种原色的数值越高,色彩越明亮。R、G、B  都为  0  时是黑色,都为  255 时是白色。  RGB 是电脑设计中最直接的色彩表示方法。电脑中的 24 位真彩图像,就是采用  RGB  模型来精确记录色彩的。所以,在电脑中利用 RGB 数值可以精确取得某种颜色。  R、G、B 数值和色彩的三属性没有直接的联系,不能揭示色彩之间的关系。所以在进行 配色设计时,RGB 模型就不是那么适用了。  RGB 色彩模型又有 AdobeRGB、AppleRGB、sRGB 等几种,这些 RGB 色彩模型大多与 显示设备、输入设备(数码相机、扫描仪)相关联。 Ø“sRGB”是所谓的“标准  RGB  色彩模型” 。这种色彩空间由微软公司与惠普公司于  1997 年联合确立,被许多软件、硬件厂商所采用,逐步成为许多扫描仪、低档打印机 和软件的默认色彩空间。同样采用 sRGB 色彩空间的设备之间,可以实现色彩相互模 拟,但却是通过牺牲色彩范围来实现各种设备之间色彩的一致性的,这是所有  RGB  色彩空间中最狭窄的一个。 Ø“Apple RGB”是苹果公司早期为苹果显示器制定的色彩模式。 其色彩范围并不比 sRGB  大多少。这种显示器已经很少使用,这一标准已逐步被淘汰。 Ø“Adobe RGB(1998) ”由 Adobe  公司制定。其最早使用在 Photoshop 5.x 中,被称为  SMPTE­240M。它具备非常大的色彩范围,绝大部分色彩又是设备可呈现的,这一色 彩模型全部包含了  CMYK  的色彩范围,为印刷输出提供了便利,可以更好地还原原 稿的色彩,在出版印刷领域得到了广泛应用。 Ø“ColorMatch RGB”是由 Radius 公司定义的色彩模型,与该公司的 Pressview  显示器 的本机色彩模型相符合。 “Wide Gamut RGB”是用纯谱色原色定义的色彩范围的 RGB  色彩空间。这种空间的色域包括了几乎所有的可见色,比典型的显示器所显示的色域 还要宽。但是,由于这一色彩范围中的很多色彩不能在  RGB  显示器或印刷上准确重 现,所以这一色彩模型并没有太大的实用价值。  RGB 只是光色的表示模型,在印刷行业中,使用另外一种色彩模型 CMYK。CMYK 分 别是青色(Cyan) 、品红(Magenta) 、黄色(Yellow) 、黑色(Black)三种油墨色。每一种颜 色都用百分比来表示,而不是 RGB 那样的 256 级度。 理论上将印刷三原色,即青色(Cyan) 、品红(Magenta) 、黄色(Yellow)混合之后, 应该可以将红绿蓝光全部吸收而得到纯正的黑色,只是现实生活中找不到这种光线吸收,反 射特性都十分完美的颜料,将三种颜色混合后还是会有些许光线反射出来,呈现暗灰色或深 褐色。事实上除了黑色外,用颜料三原色也无法混合出许多暗色系的颜色,为了弥补这个缺 点,在实际印刷的时候,额外加入黑色的颜料,以解决无法产生黑色的问题。因此就有所谓  CMYK 的色彩模式,K 表示黑色(Black) 。
    • 2.5.2  Java 的色彩管理  Java 以跨平台,与硬件无关的方式支持色彩管理。Java 的色彩管理功能来自于 java.awt  包中的 Color 类。Color 类允许在应用程序中指定自己需要的任意色彩,不用担心因计算机的 硬件设备所支持的绘制方式不同而引起的颜色差别。Java 支持 sRGB 色彩模型,将自动找到 最接近的颜色。  Java 对色彩的管理都被封装在 java.awt.Color 类中。  Color 类提供了对颜色控制的常量和方法。表 2.5 列出了 Color 类中部分预定义的颜色常 量及其 RGB 值。 表 2.5  部分 Color 颜色常量及其 RGB 值 颜色常量 颜 色  RGB 值  public final static Color ORANGE/orange  橙色  255, 200, 0  public final static Color PINK/pink  粉红色  255, 175, 175  public final static Color CYAN/cyan  青色  0, 255, 255  public final static Color MAGENTA/magenta  品红色  255, 0, 255  public final static Color YELLOW/yellow  黄色  255, 255, 0  public final static Color RED/red  红色  255, 0, 0  public final static Color BLACK/black  黑色  0, 0, 0  public final static Color WHITE/white  白色  255, 255, 255  public final static Color LIGHT_GRAY/lightGray  淡灰色  192, 192, 192  颜色常量的名称有两种写法,一种是小写,另一种是大写。我们建议采用大写的形式表 达 Java 的颜色常量,因为它们更符合常量的命名约定。  Color  类通过设置不同的三个原色:红、绿、蓝(RGB)的值而创建各种颜色。这三个 原色的合并值称为 RGB 值。RGB 中的每个值又可以表示为 0~255 范围内的某个整数,或者 表示为 0.0~1.0 之间的浮点数。RGB 中的 R 表示颜色中红色的数值,G 表示绿色的数值,而  B 表示蓝色的数值。数值越大,表示相应的色彩含量就越多。RBG 总共可以产生的颜色总数 为 256´256´256,即 167 77 2 16 种颜色。表 2.6 列出了 Color 的方法以及 Graphics 类中与之相 关的方法。 表 2.6  Color 的方法以及 Graphics 类中与之相关的方法  Color 的构造器及部分方法  public Color(int r, int g, int b)      // Color class  创建一种基于红色、绿色和蓝色的颜色,每种颜色的表示方法为 0~255 之间的一个整数  public Color(float r, float g, float b)    // Color class  创建一种基于红色、绿色和蓝色的颜色,每种颜色的表示方法为 0~1 之间的一个浮点数  public int getRed()  // Color class  返回一个 0~255 之间的,表示 RGB 中红色的整数值  public int getGreen()  // Color class  返回一个 0~255 之间的,表示 RGB 中绿色的整数值  public int getBlue()  // Color class  返回一个 0~255 之间的,表示 RGB 中蓝色的整数值
    • 续表  Graphics 类中操作 Color 对象的方法  public abstract Color getColor()    // Graphics class  返回一个表示在图形环境中当前绘图颜色的对象  public abstract void setColor(Color c)      // Graphics class  将当前的绘图颜色设置为 Color 对象 c 所定义的颜色  Color 类提供了两个构造器,一个接收三个 int 参数,另一个接收三个 float 参数。每个参 数分别表示  RGB  中红色、绿色和蓝色的数值。int  的值介于  0~255  之间,float  的值介于  0  和 1 之间。  Color 类的 getRed、getGreen、getBlue 方法都返回一个 0~255 之间的一个整数,该整数 分别代表图像环境中,当前颜色对象的红色、绿色和蓝色的数值。  Graphics 类的 getColor 方法,返回一个代表当前绘制颜色的 Color 对象。setColor 方法设 置当前的绘制颜色。  2.6  字体控制 大多数用于字体控制的方法和常量都属于 Font 类。 表 2.7 列出了 Font 类中部分预定义的 字体常量、方法,以及 Graphics 类中与 Font 类相关的一些方法。 表 2.7    Font 的方法以及 Graphics 类中与之相关的方法  Font 的构造器及部分方法  public final static int PLAIN  // Font class  表示普通字体风格的常量  public final static int HOLD  // Font class  表示粗体字体风格的常量  public final static int ITALIC  // Font class  表示斜体字体风格的常量  public Font(String name, int style, int size)  // Font class  用指定的字体,风格和大小创建一个新的 Font 对象  public int getStyle()  // Font class  返回一个表示当前字体风格的整数值  public int getSize()  // Font class  返回一个表示当前字体大小的整数值  public String getName()  // Font class  续表  Font 的构造器及部分方法 返回一个表示当前字体的字体名称的字符串  public String getFamily()  // Font class  返回一个表示当前字体的字体家族名称的字符串  public boolean isPlain()  // Font class  如果字体的风格为普通字体,则返回真,否则返回假  public boolean isBold()  // Font class
    • 如果字体的风格为粗体字体,则返回真,否则返回假  public boolean isItalic()  // Font class  如果字体的风格为斜体字体,则返回真,否则返回假  Graphics 类中操作 Font 对象的方法  public Font getFont()      // Graphics class  返回一个表示当前字体的 Font 对象引用  public void setFont(Font font)      // Graphics class  将当前字体设置为 Font  对象  font  所定义的字体  Font 构造器接收三个参数:字体名称、字体风格和字体大小。字体名称是运行程序的系 统当前所支持的任意字体,如 Courier、Helvetica 和 TimesRoman(注:字体随操作系统的不 同而相差较大,Java 确保了 Courier、Helvetica、TimesRoman、Dialog 和 DialogInput 在不同 的操作系统中有相同的显示) 。如果应用程序指定的字体不可用,则 Java  将使用系统的默认 字体来取代它。  Font 类的 static 常量 PLAIN、BOLD、ITALIC 用于说明字体风格,分别代表普通、粗体 及斜体字体。字体风格可以组合使用,如 Font.PLAIN+Font.BOLD 等。字体大小用磅(point) 来表示,1 磅是 1/72 英寸(注:这里说的磅,就是计算机显示器上所显示的一个像素。该像 素的大小,与计算机当前设置的分辨率相关) 。  Font 类的 getStyle、getSize、getName 和 getFamily 四个方法,分别返回一个当前字体对 象的风格、字体大小、字体名称及字体隶属的字体家族的结果。字体对象的风格及字体的大 小,返回的结果都是整数值,而字体名称及字体隶属的字体家族返回的结果为字符串。  Font 类的 isPlain、isBold 及 isItalic 方法返回一个布尔值。这三个方法分别测试当前字体 是否是普通、粗体或者斜体风格,并返回相应的结果。  Graphics 类的 getFont 方法用于获得当前字体, 即获得在当前图形环境下绘制文本所用的 字体。Graphics 类的 setFont 方法用于设置当前字体,即指定当前图形环境下绘制文本所用的 字体。setFont 方法以一个 Font 对象为参数。  DrawFontWithColor.java(chap02Fig2.6Fig2.6_01)演示了如何使用不同字体和颜色来绘 制文本,运行结果如图 2.14 所示。 图 2.14  用不同颜色及字体绘制文本 程序第 5 行~第 6 行: import java.awt.*; // 引入 java.awt 包中所有的类 import javax.swing.*; // 引入 javax.swing 包中所有的类 分别引入了 java.awt 包和 javax.swing 包中所有的类。 程序第 16 行, 声明了一个名为 text 的字符串变量, 并初始化它的值为“欢迎来到 Java Web
    • 动态图表编程的世界!”。 程序第 18 行~第 20 行: g.setColor(Color.BLUE);  g.setFont(new Font("宋体", Font.PLAIN, 30));  g.drawString( text, 10, 25 ); 程序第 18 行,使用 Graphics 类的 setColor 方法,设置当前图形环境下,绘制图形对象 或文本对象的颜色为蓝色。颜色的设定方法是直接调用 Color 类里面的静态变量。 程序第 19 行,使用 Graphics 类的 setFont 方法,设置当前图形环境下,绘制文本的字体 为宋体,字体风格为普通字体,字体大小为 30 磅。 程序第 20 行,在 Applet 坐标(10,25)处,绘制文本“欢迎来到 Java Web 动态图表编 程的世界!。该文本的字体为宋体,字体风格为普通字体,大小为 30 磅,颜色为蓝色。 ” 程序第 22 行~第 24 行、第 26 行~第 28 行、第 30 行~第 32 行,使用类似的方法,绘 制了不同颜色及字体的文本。 注意,程序第 22 行: g.setColor(new Color(0.0f, 0.0f, 0.0f)); 设置颜色的方法与第 18 行不同, 采用的是由一个 Color 类接收三个浮点数的构造器所创 建的 Color 类对象。程序第 30 行,采用另外一个接收三个整数的构造器来创建 Color 对象。 每次调用 Color 的构造器后,都将 Color 类的对象传递给 Graphics 类的 setColor 方法,以改 变绘制对象的颜色。程序第 23 行、第 27 行和第 31 行,分别重新设定字体对象,都是调用  Font 类的构造器来创建一个新的 Font 对象,并将该对象传递给 Graphics 类的 setFont 方法。 在调用 Font 类的构造器创建 Font 对象时,都提供了三个参数,分别是字体名称、字体风格 和字体大小。 在设置好绘制图形对象或文本颜色及字体对象后,应用程序就将一直使用定义好的颜色 和字体进行绘制,除非重新定义新的颜色和字体。我们在  DrawFontWithColor.java  程序中一 共设置了四种不同的颜色和字体,运行结果如图 2.15 所示,共绘制了四种不同颜色及字体的 文本。 在创建一个  Font  对象之前,我们需要知道本地操作系统中究竟有哪些是可供使用的字 体。ShowFonts.java  (chap02Fig2.6Fig2.6_02)将列出本地操作系统中所有可供使用的字体清 单,运行结果如图 2.15 所示。 图 2.15  获取本地操作系统的可用字体 我们调用 java.awt 包中 GraphicsEnvironment 类中的 getAllFonts 方法,来获得本地操作系 统中所有可供使用的字体清单。getAllFonts 方法,返回一个 Font 对象数组,其中包含了当前 操作系统中所有可用的字体对象。数组中每个 Font 实例,默认的字体大小为 1 磅,一般需要
    • 重新设置字体大小,否则会因字体太小而无法看清楚。 程序第 10 行~第 12 行:声明了三个变量。 一个用于绘制字符串对象 text,一个 Graphics  Environment 实例 e,以及一个 Font 数组对象 fonts。 程序第 14 行~第 18 行: public void init()  {  e = GraphicsEnvironment.getLocalGraphicsEnvironment();  fonts= e.getAllFonts();  } 在 Java  Applet 默认的第一个调用的 init 方法中,对 GraphicsEnvironment 实例 e,使用  GraphicsEnvironment 的静态方法 getLocalGraphicsEnvironment 进行初始化。然后在 e 中调用  GraphicsEnvironment 类的 getAllFonts 方法, Font 数组对象 fonts 进行初始化。 Java Applet  对 在 类的 paint 方法中,将系统的可用字体全部绘制出来。 程序第 26 行~第 31 行: for (int i = 0; i< fonts.length ­ 1 ; i++)  {  g.setFont(new Font(fonts[i].getName(), Font.PLAIN  ,15));  g.drawString("Font name:" + fonts[i].getName() +  ", 效果:" + text, 40, 30 + 20 * i);  } 使用一个循环,将 fonts 字体数组中的 Font 对象一一取出,并调用 Font 的构造器来创建 新的字体对象。注意,我们设置的字体风格都是普通字体,字体大小为 15 磅。然后用我们 设定的字体,绘制出一段相同的文本,如图 2.15 所示。某些字体并不能正确地显示中文, 在这种情况下,Java 用一个方框来表示无法用当前设定的字体来绘制该字符。  2.7  绘制矩形 在 Web 图表的绘制中,矩形是最基本的几何图形之一。在第 2.4 节,我们阐述了如何利 用 Graphics 类的 drawLine 方法,通过绘制四条直线来组成一个矩形的例子。其实 Graphics  类提供了直接绘制矩形的各种方法,我们直接调用这些方法,就可以方便地绘制矩形。  2.7.1  绘制普通矩形  1.绘制空心矩形 本节提供一些绘制矩形的 Graphics 方法。表 2.8 列出了绘制普通(空心)矩形的方法。 表 2.8  绘制普通矩形的 Graphics 方法  Public void drawRect(int x, int y, int width, int height)  用当前颜色绘制一个矩形,其左上角坐标为(x,y),宽度为 width,  高度为 height(如图 2.16A 所示)  public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight)  用当前颜色绘制一个圆角矩形,其左上角坐标为(x, y),宽度为 width,  高度为 height,arcWidth 以及 arcHeight 指定圆角 的弧宽和弧高(如图 2.16B 所示)
    • 弧宽(arcWidth) (x,y) (height) (x,y) 度高 度高 弧高 (arcHeight)   (height) 宽度(width) 宽度(width) A: drawRect  B: drawRoundRect 图 2.16  普通(空心)矩形的绘制方法 程序 DrawRectApplet.java(chap02Fig2.7Fig2.7_01)绘制了三个矩形:一个方角矩形, 两个圆角矩形。 程序中使用了一种名为“汉真广标”的字体,程序第  17  行,如果读者的计算机中没有 这种字体,Java  将自动调用默认的系统字体(宋体)来代替“汉真广标” ,源程序的运行结 果如图 2.17 所示。 图 2.17  DrawRectApplet.java 的运行结果 程序第 17 行: g.setFont(new Font("汉真广标", Font.PLAIN, 35)); 设置字体为“汉真广标”,字体风格为普通,字体大小为  35  磅。程序第 18  行,用设置 好的“汉真广标”字体在坐标(10,40)处,绘制文本“普通矩形的画法” 。如图 2.17①所示。 程序第 21 行~第 22 行: g.setColor(Color.RED);  g.drawRect(10, 60, 150, 120); 绘制一个左上角顶点坐标为(10,60) ,宽度为 150 磅,高度为 120  磅的矩形,注意, 程序第 21 行,设置当前的绘制颜色为 Color.RED,所以该矩形的边框是红色(如图 2.17②)。 程序第 25 行~第 28 行: g.setColor( Color.LIGHT_GRAY );  g.drawRect( 210, 60, 150, 120 );  g.setColor( Color.BLUE );  g.drawRoundRect( 210, 60, 150, 120, 60, 40); 这里我们绘制了一个圆角矩形。为了说明圆角矩形和方角矩形,以及圆角矩形和圆角的 关系,程序第 25 行~第 26 行,绘制了一个边框为浅灰色,宽度为 150 磅,高度为 120 磅的 普通矩形。程序第 27 行,重新设置当前的绘制颜色为蓝色。程序第 28 行,绘制了一个圆角 矩形。注意,圆角矩形的构造器接收 6 个参数,前面四个参数和程序第 26 行,绘制的矩形语 句完全一样。它定义了圆角矩形左上角坐标,以及宽度和高度,也就是说,圆角矩形的绘制
    • 区域就在这四个参数决定的矩形之中。圆角矩形四个边角的弧宽和弧高,是由最后两个参数 决定的。本例中,圆弧的宽度为 60 磅,高度为 40 磅。从图 2.17③中,可以看到,最后两个 参数实际是上绘制了一个长边长为 60 磅,短边长为 40 磅的一个椭圆。圆角矩形的弧线的形 状正好和这个椭圆的 1/4 相吻合。如果弧宽和弧高的数值相同,则椭圆就变成了一个正圆形。 程序第 25 行~第 28 行: g.setColor( Color.LIGHT_GRAY );  g.drawRect( 410, 60, 150, 120 );  g.setColor( Color.BLUE );  g.drawRoundRect( 410, 60, 150, 120, 80, 80); 这里我们又画了一个圆角矩形,除了圆角矩形左上角顶点坐标不同外,其他部分基本和 前例相同。另外,程序第 28 行的语句中,drawRoundRect 方法最后两个参数,弧宽和弧高是 相同的,所以,本圆角矩形的四个边角弧线就变成正圆形了,如图 2.17④所示。如果 width、  height、arcWidth 和 arcHeight 的取值完全相同,则最终生成一个正圆;如果最后两个参数的 取值同时为 0,则最终生成一个正方形。  2.绘制实心矩形 绘制实心矩形就是填充矩形。表 2.9 列出了 Graphics 类中绘制实心矩形的方法。 表 2.9  绘制实心矩形的 Graphics 方法  Public void fillRect(int x, int y, int width, int height)  用当前颜色绘制一个实心矩形,其左上角坐标为(x, y),宽度为 width,  高度为 height  public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight)  用当前颜色绘制一个实心圆角矩形,其左上角坐标为(x, y),宽度为 width,  高度为 height,arcWidth 以及 arcHeight 指定 圆角的弧宽和弧高 程序 FillRectApplet.java (chap02Fig2.7Fig2.7_02)演示了如何绘制实心矩形,运行结果如 图 2.18 所示。 图 2.18    FillRectApplet.java 的运行结果 本程序和前例几乎相同,仅仅是在程序第 22 行将原来的 drawRect 方法替换成了 fillRect  方法: g.fillRect(10, 60, 150, 120); 将程序第 28 行,drawRoundRect 方法替换成 fillRoundRect 方法:
    • g.fillRoundRect( 210, 60, 150, 120, 60, 40); 程序第 34 行,进行了同样的替换,这样就实现了填充矩形的功能。  2.7.2  绘制 3D 矩形  1.利用 draw3DRect  来绘制空心 3D 矩形 表 2.10 列出了 Graphics 类中绘制空心 3D 矩形的方法。 表 2.10  绘制空心 3D 矩形的 Graphics 方法  Public void draw3DRect(int x, int y, int width, int height, Boolean b)  用当前颜色绘制一个空心 3D 矩形,其左上角坐标为(x,  y),宽度为 width,高度为 height,如果 b 为真,则该矩形向外 凸出;如果 b 为假,则该矩形向内凹陷 程序 Draw3DRectApplet.java  (chap02Fig2.7Fig2.7_03)  演示了如何绘制空心 3D 矩形, 运行结果如图 2.19 所示。  draw3DRect 方法接收 5 个参数。其中前两个表示矩形左上角顶点坐标,接下来的两个参 数表示矩形的宽度和高度。 最后一个参数是个布尔值, 它表示要绘制的矩形是向外凸出 (true) 的,还是向内凹陷(false)的。  draw3DRect 方法,通过用原色绘制并填充两条矩形边,再用稍深的颜色绘制另外两条矩 形边。通过这种方式来表现三维效果。向外凸出的矩形用原色绘制上面和左边两条矩形边, 而向内凹陷(false)的矩形是用原色绘制下面和右边的两条矩形边。注意,某些颜色并不能 体现三维效果。  Draw3DRectApplet.java 演示了上述方法的用法。为了取得比较明显的三维效果,我们在 程序第 20 行,设置当前的绘制颜色为浅灰色,从程序的实际运行结果来看,获得了明显的三 维效果,如图 2.19 所示。  2.利用 fill3DRect 来绘制实心 3D 矩形 绘制实心 3D 矩形与 draw3DRect 方法所接收的参数一样,只是将 draw3DRect 方法名替 换成了 fill3DRect 方法名。表 2.11 列出了 Graphics 类中绘制实心 3D 矩形的方法。 表 2.11  绘制实心 3D 矩形的 Graphics 方法  Public void fill3DRect(int x, int y, int width, int height, Boolean b)  用当前颜色绘制一个实心 3D 矩形,其左上角坐标为(x,  y),宽度为 width,高度为 height,如果 b 为真,则该矩形向外 凸出;如果 b 为假,则该矩形向内凹陷 程序 Fill3DRectApplet.java (chap02Fig2.7Fig2.7_04)  演示了如何绘制实心 3D 矩形,运 行结果如图 2.20 所示。
    • 图 2.19    FillRectApplet.java 的运行结果 图 2.20    Fill3DRectApplet.java 的运行结果  3.利用绘制多个实心矩形来生成 3D 矩形 上节所讲述的  3D  矩形,只是具有一些凸出和凹陷感觉的平面图形。本节提供一个真正 绘制实心 3D 矩形的方法。我们通过绘制由一组红色、重叠在一起的、有凸出感的 3D 矩形所 组成的图形,来实现绘制 3D 矩形的功能。  New3DRectApplet.java(chap02Fig2.7Fig2.7_05)演示了如何绘制不同风格  3D  矩形的方 法,运行结果如图 2.21 所示。 ① ② ③ ④ 图 2.21    New3DRectApplet.java 的运行结果 程序第 20 行, 调用 Color 的构造器, 创建了一个 Color 对象 red。  RGB 取值设定为  255、  255、0,表示红色。 程序第 21 行: g.setColor(red.darker()); //重新设置当前颜色为深红色 调用 Color 类的 darker 方法,重新创建一个比当前颜色更深一些的颜色作为当前颜色。 因为程序第 20 行,创建了一个红色的颜色,所以在程序执行完第 21 行语句后,当前颜色就 重新设置为深红色了。 程序第 23 行: int thickness = 20; //设置矩形的厚度 程序第 25 行~第 31 行: for (int i = thickness ­ 1; i >= 0; i­­)  {  g.fill3DRect(10 + i, 80 ­ i, 150, 120, true);  g.fill3DRect(210 + i, 60 + i, 150, 120, true);
    • g.fill3DRect(30 ­ i, 230 ­ i, 150, 120, true);  g.fill3DRect(230 ­ i, 210 + i, 150, 120, true);  } 调用一个循环方法来绘制 3D 矩形。 循环的次数由程序第 23 行, 声明的整型变量 thickness  决定。 该数值越大,  矩形的厚度就越厚。 3D 在循环的方法体内,共有四条 fill3DRect 绘图语句, 它们分别绘制了四种不同风格的 3D 矩形。这里我们用程序第 27 行,以及如图 2.22 所示的  3D 矩形示意图为例来说明。
    • 第 1 个矩形(30,60) 最后 1 个矩形(10,80)  height 120  thickness 20 weight 150  图 2.22    3D 矩形的画法示意图 第一次循环,i 的值是 20,所以程序第 27 行,执行的是: g.fill3DRect(30, 60, 150, 120, true); 这样就绘制了一个有凸出感的,左上角坐标为(30,60) ,宽度为 150,高度为 120,并 且颜色为深红色的 3D 矩形。之后,每循环一次,就绘制一个这样的 3D 矩形,只是矩形左上 角的坐标不同而已。最后一次循环,i 的值为 0,这时程序第 27 行,实际执行的是: g.fill3DRect(10, 80, 150, 120, true); 这样就绘制了一个有凸出感的,左上角坐标为(10,80) ,宽度为 150,高度为 120,并 且颜色为深红色的 3D 矩形。这二十个重叠在一起的 3D 矩形,最终就形成了如图 2.21①所示 的图形了。 同理,程序第 28 行、第 29 行和第 30 行分别绘制如图 2.21 中②、③、④所示的图形。 读者可以看到,用这种方法绘制的  3D  矩形,既简单又美观。此方法除了可以绘制  3D  矩形以外,还适用于其他 3D 图形的绘制,如椭圆、圆弧、粗线等。这些 3D 图形的绘制方法 将在以后的代码中向读者阐述。 注意,程序第 27 行~第 30 行,生成不同风格的 3D 矩形,同样也可以生成 3D 的直方图 和条形图。  2.8  绘制椭圆 在 Web 图表的绘制中,椭圆也是最基本的几何图形之一。  2.8.1  绘制普通(空心)椭圆 我们直接调用 Graphics 类中提供的绘制椭圆的方法,就可以方便地绘制椭圆。表 2.12  列出了 Graphics 类中绘制椭圆的方法。 表 2.12  绘制椭圆的 Graphics 方法  Public void drawOval(int x, int y, int width, int height)  用当前颜色绘制一个空心椭圆,其所在的边界矩形的左上角顶点坐标为(x,  y),宽度为 width,高度为 height,椭圆与边 界矩形的各边中心内切,如图 2.23 所示 从图 2.24 可以看出,如果椭圆边界矩形的 width 和 height 的取值大小相同,该边界矩形 就会变成正方形,而该边界矩形内的椭圆也将变成正圆形。这样,就实现了绘制正圆图形的 功能。
    • (x,y)  height width  图 2.23  椭圆及其边界矩形 图 2.24    DrawOvalApplet.java 的运行结果 程序 DrawOvalApplet.java(chap02Fig2.8Fig2.8_01)演示了如何绘制椭圆及正圆图形,运 行结果如 2.24 所示。 程序第  21 行~第  22  行,绘制了一个浅灰色的矩形,左上角顶点坐标为(10,60) ,宽 度为 150,高度为 120。 程序第 23 行~第 24 行,绘制了一个边框为红色的椭圆。我们可以看到,程序第 24 行  drawOval 方法和第 22 行 drawRect 方法,接收的参数相同。该椭圆的外接矩形边界和程序第  22 行语句,所绘制的矩形图形完全重叠。图 2.24 展示了椭圆及其边界矩形的关系。 程序第 27 行~第 30 行: g.setColor(Color.LIGHT_GRAY);  g.drawRect(210, 60, 120, 120);  g.setColor(Color.RED);  g.drawOval(210, 60, 120, 120); 这段代码演示了如何绘制正圆形,与程序第 21 行~第 24 行的代码内容基本相同。同样 是先绘制了一个和欲绘制椭圆的外接边界矩形完全重叠的浅灰色矩形。注意,程序第 28 行,  drawRect 方法的最后两个参数的取值相同,这样就改变了矩形的形状为正方形。同理,程序第  24 行的 drawOval 方法的最后两个参数也相同,所以椭圆的形状也就改变成了正圆形。
    • 2.8.2  绘制实心椭圆 绘制实心椭圆就是用某种颜色向一个空心椭圆中进行填充操作。我们直接调用 Graphics  类中提供的绘制实心椭圆的方法绘制实心椭圆。 2.13 列出了 Graphics 类中绘制实心椭圆的 表 方法。 表 2.13  绘制实心椭圆的 Graphics 方法  Public void fillOval(int x, int y, int width, int height)  用当前颜色绘制一个实心椭圆,其所在的边界矩形的左上角顶点坐标为(x,  y),宽度为 width,高度为 height,椭圆与边 界矩形的各边中心内切,如图 2.23 所示 程序  FillOvalApplet.java(chap02Fig2.8Fig2.8_02)演示了如何绘制椭圆及正圆图形运行 结果如图 2.25 所示。 图 2.25    FillOvalApplet.java 的运行结果 程序第  21 行~第  22  行:绘制了一个浅灰色的矩形,左上角顶点坐标为(10,60) ,宽 度为 150 像素,高度为 120 像素。 程序第 23 行~第 24 行, 绘制了一个红色的实心椭圆。我们可以看到程序第 24 行 fillOval  方法和第 22 行 drawRect 方法,接收的参数相同。该椭圆的外接矩形边界和程序第 22 行语句 所绘制的矩形的图形完全重叠。 程序第 27 行~第 30 行: g.setColor(Color.LIGHT_GRAY);  g.drawRect(210, 60, 120, 120);  g.setColor(Color.RED);  g.drawOval(210, 60, 120, 120); 这段代码演示了如何绘制实心正圆形,与程序第 21 行~第 24 行的代码内容基本相同。 也是先绘制了一个与将绘制的椭圆的外接边界矩形完全重叠的浅灰色矩形。注意,程序第 28  行,drawRect 方法的最后两个参数的取值相同,这样就改变了矩形的形状为正方形。同理, 程序第 24 行的 drawOval 方法的最后两个参数也相同,所以椭圆的形状也就改变成了正圆形。  2.8.3  绘制 3D 椭圆 我们通过绘制一组由不同颜色,并重叠在一起的椭圆所组成的图形,来实现绘制  3D  椭 圆的功能。 程序 Draw3DOvalApplet.java (chap02Fig2.8Fig2.8_03)演示了如何绘制 3D 椭圆,运行结
    • 果如图 2.26 所示。 图 2.26    Draw3DOvalApplet.java 的运行结果 程序第 22 行~第 29 行: for (int i = thickness ­ 1; i >= 0; i­­)  {  g.setColor(new Color(255, i * 10, i));  g.fillOval(10 + i, 80 ­ i, 150, 120);  g.fillOval(210 + i, 60 + i, 150, 120);  g.fillOval(30 ­ i, 230 ­ i, 150, 120);  g.fillOval(230 ­ i, 210 + i, 150, 120);  } 循环次数由程序第 20 行定义的整型变量 thickness 决定。该数值越大,3D 椭圆的厚度就 越厚。在该循环体内,程序第 24 行: g.setColor(new Color(255, i * 10, i)); 每循环一次, 当前的绘图颜色会根据 RGB 的值而重新设定一次。 接着一共是 4 条 fillOval  绘图语句,它们分别绘制了 4 种不同风格的 3D 椭圆。这里我们用程序第 24 行和第 25 行为 例,来说明其绘制过程。 第 1 次循环,i 的值是 20,所以,第 24 行和第 25 行执行以下两条语句: g.setColor(new Color(255, 200, 20));  g.fillOval(30, 60, 150, 120); 第 24 行执行完后,当前颜色就被设置成一种近似于土黄色(RGB 值:255,200,20) 的颜色。接着用当前颜色绘制了一个实心椭圆,该椭圆边界矩形的左上角的顶点坐标在(30,  60),宽度为 150,高度为 120。之后,每循环一次,就会重新设置当前的颜色,再绘制一个 这样的椭圆, 只是椭圆的边界矩形的左上角顶点的坐标不同而已。 最后一次循环, i 的值为 0, 这时,第 24 行和第 25 行语句实际执行的是: g.setColor(new Color(255, 0, 0));  g.fillOval(10, 80, 150, 120);  new Color(255, 0, 0)定义了红色为当前的绘图颜色,这样就绘制了边界矩形左上角的坐标 在(10,80) ,宽度为 150,高度为 120,并且颜色为红色的椭圆。20 个重叠在一起的椭圆, 最终形成了如图 2.26①所示的图形。该 3D 椭圆的底部是土黄色,从椭圆的底部到顶部,颜 色逐步过渡到红色。
    • 同理,程序第 26 行、第 27 行和第 28 行分别绘制如图 2.26 中②、③、④所示的图形。  2.8.4  绘制圆柱体 圆柱体可以看成是由上下两个椭圆,再加上一个矩形所组成的图形,如图 2.27 所示。 椭圆的高  上方椭圆外切矩形顶点  ovalHeight  (ovalX1, ovalY1)  矩形左上角顶点  (rectX, rectY)  矩形的高  rectHeight  下方椭圆外切矩形顶点  椭圆的高  (ovalX2, ovalY2)  ovalHeight  width 图 2.27  圆柱体的绘制方法 矩形左上角的顶点坐标是(rectX, rectY)。上下两个椭圆左上角的顶点坐标分别是(ovalX1,  ovalY1)及(ovalX2, ovalY2)。 矩形的宽度和椭圆边界矩形的宽度相同,都是 width,矩形的高度为 rectHeight,椭圆的 边界矩形高度为 ovalHeight。 矩形及上下椭圆边界矩形的左上角坐标之间存在如下关系: Ø 上面一个椭圆边界矩形的左上角坐标 ovalX1 = rectX, ovalY1 = rectY – ovalHeight / 2 ; Ø 下面一个椭圆边界矩形的左上角坐标 ovalX2 = rectX, ovalY2 = ovalY1 + rectHeight ; 所以,只要知道矩形左上角顶点的坐标、矩形的宽度、矩形的高度,以及椭圆的高度就 可以绘制出一个圆柱体了。  DrawCylinderApplet.java(chap02Fig2.8Fig2.8_04)演示了如何利用上面所讲的方法来绘 制一个圆柱体,本程序中使用了下列几个方法: Ø  calculateOvalCoordinate——利用上面的公式计算上、下椭圆的坐标。 Ø  resetRectCoordinate——重新设置矩形左上角顶点的坐标。 Ø  drawCylinder——绘制圆柱体。 Ø  paint——Java Applet 默认的自动调用的方法之一, 在此方法体内, 再调用 draw Cylinder  方法,进行实际圆柱体的绘制工作。  DrawCylinderApplet.java 的运行结果如图 2.28 所示。
    • 图 2.28    DrawCylinderApplet.java 的运行结果 程序第  10  行~第  15  行,定义并初始化了  rectX、rectY、rectWidth、rectHeight,以及  ovalHeight 这 5 个整型变量。分别定义了矩形的左上角坐标,宽度和高度,以及椭圆的高度。 然后声明了 5 个整型变量,ovalX1、ovalY1、ovalX2、ovalY2、ovalWidth。分别代表上面一 个椭圆的边界矩形左上角坐标(ovalX1,ovalY1) ,下面一个椭圆的边界矩形左上角坐标 (ovalX2,ovalY2)以及椭圆的宽度。然后声明并初始化了两个颜色变量,outlineColor  和  fillColor,outlineColor 代表上面一个椭圆的边框颜色,我们设定其默认值为淡灰色。fillColor  代表矩形和下面一个椭圆的填充颜色,我们设定其默认值为红色。 然后,程序执行第 57 行的 paint 方法,程序第 60 行,调用父类的 paint 方法。运行完程 序第 63 行和第 64 行后,在 Applet 上,用 15 磅的“汉真广标”字体绘制了一条文本“圆柱 体画法” 。 之后,程序执行第 67 行: drawCylinder(g); 然后,程序转向执行第 35 行的 drawCylinder 方法,该方法接收一个 Graphics 类的对象 g。 程序第 38 行: calculateOvalCoordinate(); 程序立即转向执行第 18 行的 calculateOvalCoordinate 方法,该方法只有一个目的,就是 根据程序第 10 行~第 12 行, 定义并初始化的 rectX、  rectY、  rectHeight 以及 ovalHeight  rectWidth、  这 5 个整型变量值, 来计算并将计算结果赋值给程序第 13 行声明的 ovalX1、  ovalY1、  ovalX2、  ovalY2、ovalWidth 这 5 个整型变量。 计算过程在程序第 20 行~第 24 行: ovalWidth = this.rectWidth;  ovalX1 = this.rectX;  ovalY1 = this.rectY ­ ovalHeight / 2;  ovalX2 = this.rectX;  ovalY2 = ovalY1 + rectHeight; 程序返回到 drawCylinder 方法中,接着继续执行。 程序第 41 行~第 44 行: g.setColor(fillColor);  g.fillOval(ovalX2, ovalY2, ovalWidth, ovalHeight);
    • g.setColor(outlineColor);  g.drawOval(ovalX2, ovalY2, ovalWidth, ovalHeight); 先将红色(fillColor)设置为当前的绘图颜色,然后用当前颜色填充圆柱体中下面的一个 椭圆。首先绘制最下方的一个椭圆,因为 Java 默认的绘图模式是覆盖模式,也就是说,如果 绘制的图形相互重叠,那么默认图形重叠部分就仅仅显示为最后绘制的图形。这里,椭圆在 调用 calculateOvalCoordinate 方法后,得到的绘制参数为: ovalWidth = 100;  ovalX1 = 10;  ovalY1 = 120 – 40 / 2 = 100;  ovalX2 = 10;  ovalY2 = 100 + 250 = 350; 填充完椭圆后,我们又绘制了一个淡灰色的空心椭圆。注意,这里绘制空心椭圆的参数 和前面绘制实心椭圆的参数完全相同,程序执行完这 4 条语句,就绘制了一个边缘颜色为淡 灰色,其余颜色为红色的实心椭圆。 程序第 47 行~第 48 行: g.setColor(fillColor);  g.fillRect(rectX, rectY, rectWidth, rectHeight); 用红色绘制了一个实心矩形。 程序第 51 行~第 54 行,绘制矩形上方的一个椭圆: g.setColor(g.getColor().darker());  g.fillOval(ovalX1, ovalY1, ovalWidth, ovalHeight);  g.setColor(outlineColor);  g.drawOval(ovalX1, ovalY1, ovalWidth, ovalHeight); 与程序第 41 行~第 44 行,绘制下方椭圆的过程几乎一样,不同的是,为了更好地显示 圆柱体的外观,程序第 51 行,调用了 Graphics 类的 getColor 方法,得到当前的绘图颜色的 对象——红色。然后,调用 Color 类的 darker 方法,生成一个比当前颜色更深(暗)一些的 颜色对象。程序执行完第 51 行后,当前的绘制颜色就转变成了深红色。程序执行完这 4 条语 句,就绘制了一个边缘颜色为淡灰色的深红色实心椭圆。 之后,程序返回到  paint  方法中。这时,一个由上、下两个椭圆及一个矩形组成的圆柱 体就绘制完成了,如图 2.28①所示。 程序继续运行到第 69 行: rectHeight = 350; 对变量  rectHeight  进行直接赋值,以达到重新设置矩形高度的目的。现在,矩形的高度 就被设置为 350 了。 程序第 70 行: resetRectCoordinate(250, 50); 调用 resetRectCoordinate 方法,重新设定矩形左上角顶点坐标。这里,程序转向 到第 28 行的 resetRectCoordinate 方法。 程序第 30 行~第 31 行: this.rectX = rectX;  this.rectY = rectY; 将接收到的两个整型参数分别赋值给程序第 10 行所声明的变量 rectX 和 rectY。这时,左上 角顶点坐标就更新为 250 和 50 了。之后,程序返回到 paint 方法中,继续执行第 72 行: fillColor = Color.BLUE;
    • 重新设定绘图时的填充颜色为蓝色。 程序第 70 行: drawCylinder(g); 再次调用 drawCylinder 方法,程序再次转到第 35 行的 drawCylinder 方法。同理,程序 立即转向执行第 18 行的 calculateOvalCoordinate 方法,根据新的矩形参数来重新计算并将计 算结果再次赋值给程序第 13 行,声明的 ovalX1、ovalY1、ovalX2、ovalY2 和 ovalWidth 这 5  个整型变量。这次,椭圆在调用 calculateOvalCoordinate 方法后,得到的绘制参数为: ovalWidth = 100(没有改变);  ovalX1 = 250;  ovalY1 = 50 – 40 / 2 = 30;  ovalX2 = 250;  ovalY2 = 30 + 350 = 380; 然后,根据新的绘制参数,再次以如下的顺序绘制图形: (1)下方实心椭圆,颜色为蓝色; (2)中间的实心矩形,颜色为蓝色; (3)上方的实心椭圆,颜色为深蓝色,并且该椭圆的边缘为浅灰色。 圆柱体绘制完成后的最终效果如图  2.28②所示。之后,程序再次返回到  paint  方法,并 结束整个程序的运行。  2.9  绘制圆弧 圆弧就是一个空心椭圆的一部分,如图 2.29 所示。 左上角顶点(x,y)  外切矩形高度 height  arcAngle  StartAngle  圆弧起始角度 外切矩形宽度 width  图 2.29  圆弧的绘制方法
    • 2.9.1  绘制普通(空心)圆弧 我们直接调用  Graphics  类中提供的绘制圆弧的方法,来绘制空心圆弧。表  2.14  列出了  Graphics 类中绘制空心圆弧的方法。 表 2.14  绘制圆弧的 Graphics 方法  Public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle)  用当前颜色绘制一段空心圆弧,该圆弧所在椭圆的外切矩形的左上角坐标为(x,  y),宽度为  width,高度为  heigh,以角 度 startAngle 所在的地方为起点,绘制一段角度为 arcAngle 的空心圆弧,如图 2.29 所示 一段圆弧是椭圆或圆的一部分,圆弧的角度通过度数来衡量。一段圆弧在两个角度之间 绘制,一个为起始角度,另一个为圆弧角度,起始角度为圆弧开始的角度,圆弧角度为圆弧 最后的度数。图 2.30 展示了两段圆弧,左边的坐标系显示圆弧从 0°画到大约 110°,以逆 时针方向画过的弧用正角度衡量。右边的坐标系显示圆弧从  0°画到大约-110°,以顺时针 方向画过的圆弧用负角度衡量。如图 2.30 所示的正、负角度圆弧的绘制方向。  drawArc 方法接收 6 个参数。前两个参数指明了包含圆弧的外切矩形的左上角坐标,第  3 个和第 4 个参数定义了外切矩形的宽和高,第 5 个参数定义了圆弧的开始角度,第 6 个参 数定义了圆弧的角度,并用当前颜色绘制圆弧。  DrawArcsApplet.java (chap02Fig2.9Fig2.9_01)演示了圆弧的画法,运行结果如图 2.31 所 示。 正角度  负角度  90°  90°  180°  0°  180°  0°  270°  270° 图 2.30  正、负角度圆弧的绘制方向 图 2.31  几种不同圆弧的绘制方法 程序第 20 行~第 24 行,绘制了两个浅灰色的矩形和两个浅灰色的正方形。 程序第 26 行,重新设置当前的绘图颜色为蓝色。 程序第 29 行: g.drawArc(10, 60, 150, 120, 0, 360); 绘制了一个圆弧。该语句接收 6 个参数中,前 4 个参数和程序第 21 行的 drawRect 方法 的参数完全相同,后两个参数指定圆弧的起始角度在  0°,圆弧的角度为  360°,正好形成 一个首尾封闭的圆弧,最后就形成了一个椭圆,如图 2.31①所示。 程序第 32 行:
    • g.drawArc(210, 60, 120, 120, 90, 270); 这里圆弧所在椭圆的外切矩形的宽度和高度相同,所以变成了一个正方形。最后两个参 数指定圆弧的起始角度在 90°,并按逆时针方向绘制一个角度为 270°的圆弧,所以圆弧的 起始角度在 90°,终点在 90°+270°=360°,如图 2.31②所示。 程序第 34 行: g.setColor(Color.RED); 重新设置当前的绘图颜色为红色。 程序第 37 行: g.drawArc(10, 210, 150, 120, 30, ­ 150); 绘制了一个圆弧。该语句接收 6 个参数中,前 4 个参数和程序第 23 行的 drawRect 方法 的参数完全相同,后两个参数指定圆弧的起始角度在 30°,并按顺时针方向绘制一个角度为  150°的圆弧,如图 2.31③所示。 程序第 40 行: g.drawArc(210, 210, 120, 120,  ­ 90,  ­ 270); 同样,这里圆弧所在椭圆的外切矩形的宽度和高度相同,所以变成了一个正方形。最后 两个参数指定圆弧的起始角度在-90°(也就是 270°) 并按顺时针方向绘制一个角度为 270  , °的圆弧,所以圆弧的起始角度在 270°,终点在 270°-270°=0°,如图 2.31④所示。  2.9.2  绘制实心圆弧  Graphics 类中提供了绘制圆弧的 fillArc 方法,可以非常方便地绘制实心圆弧。表 2.15 列出 了 Graphics 类中绘制实心圆弧的方法。 表 2.15  绘制圆弧的 Graphics 方法  Public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle)  用当前颜色填充一段圆弧,该实心圆弧所在椭圆的外切矩形的左上角坐标为(x,  y),宽度为 width 和高度为 heigh,以角 度 startAngle 所在的地方为起点,绘制一段角度为 arcAngle 的实心圆弧  fillArc 方法同样接收 6 个参数。前两个参数指明了包含圆弧的外切矩形的左上角坐标, 第 3 个和第 4 个参数定义了外切矩形的宽和高,第 5 个参数定义了圆弧的开始角度,第 6 个 参数定义了圆弧的角度,并用当前颜色填充圆弧。  FillArcsApplet.java(chap02Fig2.9Fig2.9_02)演示了实心圆弧的画法,程序运行结果如图  2.32 所示。  FillArcsApplet.java 和前面介绍的 FillArcsApplet.java 程序几乎完全相同, 只是将 drawArc  方法替换成 fillArc 方法。程序第 29 行、第 32 行、第 37 行和第 40 行分别调用 fillArc 方法, 绘制了不同形式的实心圆弧。程序的运行结果如图 2.32 所示。
    • 图 2.32  几种实心圆弧的绘制方法  2.9.3  绘制 3D 圆弧  Java.awt 包中的 Graphics 类中,并没有提供绘制 3D 圆弧的方法,我们可以利用绘制多 个重叠在一起的实心圆弧来实现这一功能。Draw3DArcApplet.java(chap02Fig2.9 Fig2.9 _03)  演示了如何绘制 3D 圆弧,运行结果如图 2.33 所示。
    • 图 2.33    3D 圆弧的绘制方法 程序第 19 行: int thickness = 20; //设置 3D 圆弧的厚度 声明了一个整型变量 thickness,并初始化其值为 20。thickness 代表 3D 圆弧的厚度。 程序第 21 行~第 36 行,是一个循环,该循环的次数由前面设定的变量 thickness 决定, 本例共循环 20 次。 程序第 23 行~第 30 行: if (i == 0)  {  g.setColor(new Color(255, 200, 20));  }  else  {  g.setColor(new Color(255, i * 10, i));  } 每一次循环,就重新设定当前实心圆弧的填充颜色。注意,本例设定最后绘制的实心圆 弧的绘制颜色为 new Color  (255, 200, 20) 其余圆弧的绘制颜色则根据 i 值的变化而变化 , (程 序第 29 行) 。 程序第 33 行~第 35 行,绘制 3D 圆弧的核心语句。 程序第 33 行: g.fillArc(40, 60 + i, 250, 160, 10, 70); 按从下到上的顺序 (因为 i 的值是由大到小递减的)填充圆弧。圆弧的起始角度为 10°, 圆弧的弧度为 70°。绘制结果如图 2.33①所示。同理,程序第 34 行和第 35 行,分别绘制了 两个如图 2.33②及 2.33③所示的不同的 3D 圆弧。  2.10  绘制多边形 我们绘制的很多图形实际上不全都是规则的图形,在这种情况下,Java 绘制多边形和折 线的功能就可以大显身手了。此外,绘制某些 3D 图形时,我们也可以借助多边形来实现。
    • Graphics 类中提供了绘制多边形的 drawPolygon 方法,可以方便地绘制多边形。  2.10.1  绘制空心多边形 表 2.16 列出了 Graphics 类中绘制空心多边形的方法。 表 2.16  绘制空心多边形的 Graphics 方法  public abstract void drawPolygon(int[] xPoints, int[] yPoints, int nPoints)  使用当前颜色绘制  xPoints、yPoints 数组指定的多边形,每个顶点的  x 坐标由 xPoints 数组指定,每个顶点的  y 坐标由  yPoints 数组指定  drawPolygon 方法用来绘制一个多边形,多边形(polygon)是多条边相互连接的封闭图 形。drawPolygon 方法接收 3 个参数:一个包含 x 坐标的整型数组,一个包含 y 坐标的整型 数组,最后是多边形顶点的数目。注意,在多边形中,最后一个顶点会自动连接第一个顶点, 也就是说,最后一个顶点的坐标和第一个顶点的坐标不必相同。  DrawPolygon.java(chap02Fig2.10Fig2.10_01)演示了多边形的画法,运行结果如图 2.34 所 示。 图 2.34    DrawPolygon.java 的运行结果 程序绘制两个多边形,第 1 个多边形是五角星。程序第 21 行~第 36 行: int[] xValues = { 55, 67, 109, 73, 83, 55, 27, 37, 1, 43 };  int[] yValues = { 70, 106, 106, 124, 166, 142, 166, 124, 106, 106 }; 声明了两个数组 xValues 和 yValues,分别表示将绘制的五角星每一个顶点的横坐标及纵 坐标。 程序第 24 行: Polygon fiveStar = new Polygon(xValues, yValues, xValues.length); 调用了 java.lang 包中的 Polygon 的构造器,创建了一个 Polygon 类的实例 fiveStar。该构 造器接收 3 个参数,前面两个参数是程序第 22 行和第 23 行定义好的两个整型数组,代表多 边形上所有顶点的 x 坐标和 y 坐标的集合,最后一个参数是 1 个整型变量,表示该多边形有 多少个顶点。因为五角星共有 10 个顶点,所以程序第 24 行也可写成如下形成: Polygon fiveStar = new Polygon(xValues, yValues, 10); 而数组  xValues  和  yValues  正好包含了  10  个元素,所以我们直接调用数组的静态变量  length,用 xValues.length 的方式,来获得数组中所有元素的个数。 程序第 25 行:
    • g.drawPolygon(fiveStar); 将刚刚创建的 Polygon 对象 fiveStar,传递给 Graphics 类的 drawPolygon 方法。然后,程 序就在这里执行绘制多边形的工作。 程序第 28 行~第 36 行,用另外一种方式绘制了一个多边形。 程序第 28 行: Polygon sharp = new Polygon(); 调用 Polygon 类默认的构造器,创建了一个 Polygon 类的实例 sharp,默认的构造器不接 收任何参数。由 Polygon 类默认的构造器,创建的 Polygon 类的实例是不包括任何顶点的, 需要手工添加该多边形的各个顶点的坐标。 程序第 29 行~第 33 行: sharp.addPoint(185, 45);  sharp.addPoint(195, 110);  sharp.addPoint(290, 160);  sharp.addPoint(220, 180);  sharp.addPoint(150, 140); 调用 Polygon 类的 addPoint 方法,向对象 sharp 中添加顶点,这里共添加了 5 个顶点。 程序第 35 行,重新设定当前的绘图颜色为红色。 程序第 36 行: g.drawPolygon(sharp); 将创建好的 Polygon 实例 sharp 再次传递给 Graphics 类的 drawPolygon 方法,绘制多边形。  2.10.2  绘制实心多边形 表 2.17 列出了 Graphics 类中绘制实心多边形的方法。 表 2.17  绘制实心多边形的 Graphics 方法  public abstract void fillPolygon(int[] xPoints, int[] yPoints, int nPoints)  使用当前颜色绘制并填充由 xPoints、yPoints 数组指定的多边形,每个顶点的 x 坐标由 xPoints 数组指定,每个顶点的 y  坐标由 yPoints 数组指定  fillPolygon  方 法 , 用 当 前 的绘 图 颜色 来 填 充一 个实 心 多 边 形。 fillPolygon  方 法 和  drawPolygon 方法一样, 都接收 3 个参数——一个包含 x 坐标的整型数组, 一个包含 y 坐标的 整型数组,最后是表示该多边形顶点的数目。  FillPolygon.java  (chap02Fig2.10Fig2.10_02)演示了实心多边形的画法,运行结果如图  2.35 所示。
    • 图 2.35    FillPolygon.java 的运行结果  FillPolygon.java 和 DrawPolygon.java 的源程序几乎完全相同。只是在 FillPolygon.java 程 序中,将原来在 DrawPolygon.java 源程序中的 drawPolygon 方法全部替换成了 fillPolygon 方法。 程序其余部分代码没有变化。  2.10.3  绘制折线 使用 drawPolyline 的方法绘制一条折线,折线是一条由多条线段连接起来的首尾不相连 的开放图形。如果折线的第一个顶点和最后一个顶点的坐标相同,那么该折线也就变成了一 个封闭的图形——多边形。 表 2.18 列出了 Graphics 类中绘制折线的方法。 表 2.18  绘制折线的 Graphics 方法  public abstract void drawPolyline (int[] xPoints, int[] yPoints, int nPoints)  使用当前颜色绘制一系列相互连接的线段,线段端点的坐标相应由数组  xPoints  和  yPoints  指定,线段的端点个数由  nPoints 指定  drawPolyline 方法和 drawPolygon 方法一样, 都接收 3 个参数——第 1 个参数包含 x 坐标 的整型数组,第 2 个参数包含 y 坐标的整型数组,最后 1 个参数指明该折线总共有多少个端 点。  DrawPolyline.java(chap02Fig2.10Fig2.10_03)演示了实心多边形的画法,运行结果如图  2.36 所示。 图 2.36    DrawPolylin.java 的运行结果  2.10.4  绘制三角形(箭头) 本节将以绘制 4 种不同风格的等腰三角形(箭头)为例。 假设等腰三角形 ABC 的底边 AB 的边长为 L,底角的度数为a,则等腰三角形 ABC 的 三个顶点的坐标 A、B 和 C 分别可以用以下公式计算,如图 2.37 所示。  1.等腰三角形①:  Ax =x-L/2,Ay =y  Bx =x+L/2,By =y  Cx =x,Cy =y-tga*L/2
    • C  A  中点坐标 a ① (x,y) 底边 L  中点坐标 (x,y) C  a ② a a B  A  B  底边 L  底边 L  中点坐标 A  a a B  (x,y) A  ④ a 底边 L  中点坐标 ③ (x,y) C  a C  B 图 2.37    DrawPolylin.java 的运行结果  2.等腰三角形②:  Ax =x,Ay =y-L/2  Bx =x,By =y +L/2  Cx =x+tga*L/2,Cy =y  3.等腰三角形③:  Ax =x-L/2,Ay =y  Bx =x+L/2,By =y  Cx =x,Cy =y+tga*L/2  4.等腰三角形④:  Ax =x,Ay =y-L/2  Bx =x,By =y+L/2  Cx =x-tga*L/2,Cy =y  DrawTriangleApplet.java(chap02Fig2.10Fig2.10_04)演示了如何利用上述计算来绘制三 角形,运行结果如图 2.38 所示。 源程序共包含了 5 种方法,如下所示: Ø  drawTrigangle  Ø  setBaseLine  Ø  setFillColor  Ø  setOutlineColor  Ø  paint 
    • 图 2.38    DrawPolylin.java 的运行结果  5 种方法分别执行了不同的任务。paint 方法是 Java Applet 自动执行的方法之一,我们在  paint  方法内再调用其他的方法以完成绘制不同大小、不同方向、不同风格的三角形。  drawTrigangle 方法,负责进行计算三角形 3 个顶点坐标,并根据用户提供的参数来决定三角 形的填充颜色、三角形轮廓的绘制颜色,以及三角形的方向等信息来进行图像绘制。  setBaseLine 负责设置等腰三角形底边的长度。  setFillColor 负责设置等腰三角形的填充颜色 (即 绘制实心三角形的颜色) 。setOutlineColor 负责设置绘制等腰三角形轮廓的颜色(即绘制空心 三角形的颜色) 。 程序第 9 行~第 10 行: int[] TriangleXValues = new int[3];  int[] TriangleYValues = new int[3]; 声明并初始化两个整型数组。TriangleXValues 代表三角形 3 个顶点的横坐标,TriangleY  Values 代表三角形 3 个顶点的纵坐标。 程序第 11 行: int alpha = 60; 声明了一个值为  60  的整型变量  alpha。alpha  表示等腰三角形底角。这里设置默认底角  alpha 的度数为 60°。 程序第 12 行: int baseLine = 80; 声明了一个值为 80 的整型变量 baseLine,baseLine 表示等腰三角形底边。这里设置默认 底边 baseLine 的长度为 80。 程序第 15 行: final int UP = 1, RIGHT = 2, DOWN = 3, LEFT = 4; 声明了 4 个整型变量 UP、RIGHT、DOWN、LEFT,代表三角形的方向。分别如图 2.38  所示的向上、向右、向下和向左的三角形。这里我们使用了 final 关键字,表示这些变量的值 在声明后,不能再被修改(相当于常量) 。 程序第 18 行: final int OUTLINE = 1, FILL = 2, MIXED = 3; 这里声明 3 个整型变量 OUTLINE、FILL 和 MIXED,分别代表三角形的绘制风格,分 别表示为: Ø  OUTLINE:绘制三角形轮廓(即绘制空心三角形) ; Ø  FILL:绘制实心三角形;
    • Ø  MIXED:绘制一个带轮廓的实心三角形(即先绘制一个实心三角形,然后在其上再 绘制一个空心三角形) 。 程序第 20 行~第 21 行: Color fillColor = Color.BLACK; // 默认的填充颜色 Color outlineColor = Color.BLACK; //默认的轮廓颜色 声明了两个 Color 类的对象实例,fillColor 和 outlineColor 分别表示绘制实心三角形和三 角形轮廓的颜色。两者的默认绘制颜色被设置为黑色。 当程序运行时,完成上述变量的创建工作后,首先执行程序第 108 行默认的 paint 方法。 在该方法体内,首先进行图形标题的绘制工作,即程序第 115 行,绘制“三角形的画法”这 个文本。 程序第 117 行: setFillColor(Color.RED); 设置填充实心三角形的颜色为红色,注意,这里没有设置绘制三角形轮廓的颜色,所有  outlineColor 对象仍然保持黑色不变。 程序第 120 行: drawTrigangle(50, 120, UP, OUTLINE, g); 调用 drawTrigangle 方法,并传递 5 个参数。前面 4 个参数是整型参数,最后 1 个参数是 由 applet 容器创建的 Graphics 对象 g。前面 4 个参数的具体含义是: Ø  50,100:表示将要绘制的三角形底边中点的坐标为(50,100) ; Ø  UP:表示该三角形的形状是一个向上的三角形; Ø  OUTLINE:表示仅绘制该三角形的轮廓(即空心三角形) 。 然后,程序转向到第 23 行,并执行 drawTrigangle 方法。drawTrigangle 方法,接收刚刚 由 paint 方法调用并传递过来的 5 个参数。 在 drawTrigangle 方法体内,首先运行程序第 27 行: int offset = (int)(baseLine / 2 * Math.tan(alpha * Math.PI / 180)); 定义了一个局部的整型变量 offset。offset 是一个偏移量,它表示三角形顶点 C 到底边中 点的距离。套用图 2.37 所示的计算方法,可知该偏移量等于底角a乘以底边长的一半,即: offset = tgα* L/2 因为,Java.lang.Math 包中的 tan 方法接收 double 类型的弧度值,所以这里使用: alpha * Math.PI / 180 将角度值转化成弧度值。执行: Math.tan(alpha * 180 / Math.PI)) 得到的结果是一个 double 类型的值,再用(int)的方法,将其从双精度型数值强制转换 为整型数值,并将转换后的结果赋值给局部整型变量 offset。 程序第 30 行,根据用户传递过来的第 3 个整型参数,进入一个 switch 多重选择语句: switch (triangleDirection) 根据用户传递过来的 triangleDirection 值,进行下一步操作,即计算三角形 3 个顶点的坐 标。这里程序传入 triangleDirection 的值为 UP。 根据程序第 15 行的定义,UP 的值为 1,所以,程序会执行第 32 行~第 39 行:
    • case 1:  TriangleXValues[0] = x ­ baseLine / 2;  TriangleYValues[0] = y;  TriangleXValues[1] = x + baseLine / 2;  TriangleYValues[1] = y;  TriangleXValues[2] = x;  TriangleYValues[2] = y ­ offset;  break; 根据前面介绍的计算公式,计算出形状向上的三角形的 3 个顶点的坐标,并将这 3 个坐 标分别赋值给数组 TriangleXValues 和  TriangleYValues。计算完 3 个顶点的坐标之后,执行 程序第 39 行的 break 语句。退出当前的 switch 语句后,程序进入第 70 行,另外一个 switch  语句: switch (triangleStryle) 根据用户传递的第 4 个参数 triangleStyle 进行三角形绘制风格的选择。当前用户传递的 是“OUTLINE”,根据程序第 18 行的定义,OUTLINE 的值为 1。所以,程序执行第 72~第  75 行: case 1:  g.setColor(outlineColor);  g.drawPolygon(TriangleXValues, TriangleYValues, 3);  break; 程序第  73  行,先设置当前的绘制颜色为  Color  类的对象  outlineColor  所指定的颜色。  outlineColor 在程序第 21 行声明后并没有改变,仍然是黑色。 程序第 74 行: g.drawPolygon(TriangleXValues, TriangleYValues, 3); 调用 Graphics 类的 drawPloyon 方法,绘制一个空心多边形。执行完程序第 74 行语句后, 程序在 Applet 上绘制出一个形状为向上的空心三角形,颜色为黑色,并且该三角形底边的长 度为默认的 80 磅。实际运行结果如图 2.38①所示。 程序第 75 行是一个 break 语句。在这里,程序跳出 switch 语句,然后,再返回到先前的  paint 方法里。程序运行第 123 行: drawTrigangle(100, 120, RIGHT, FILL, g); 再次调用并转向至程序第 23 行,执行 drawTrigangle 方法。同理,drawTrigangle 方法先 计算三角形顶点 C 距离底边中心的偏离量 offset。 因为 RIGHT 的值为 2(见程序第 15 行) ,所以。程序执行第 30 行的 switch 语句中的第  41 行~第 48 行: case 2:  TriangleXValues[0] = x;  TriangleYValues[0] = y ­ baseLine / 2;  TriangleXValues[1] = x;  TriangleYValues[1] = y + baseLine / 2;  TriangleXValues[2] = x + offset;  TriangleYValues[2] = y;  break; 对数组 TriangleXValues 和 TriangleYValues 再次进行赋值后,又进入程序第 70 行,另外 一个 switch 语句。 因为整型参数 Fill 的值为 2,所以,执行该 switch 语句中的第 77 行~第 80 行: case 2:
    • g.setColor(fillColor);  g.fillPolygon(TriangleXValues, TriangleYValues, 3);  break; 程序第 78 行,重新设置当前的绘图颜色。因为 fillColor 已经在程序第 117 行,被重新设 置成了红色,当前的绘制颜色就已经变成了红色。 程序第 79 行, 调用 Graphics 类的 fillPolygon  方法,再次绘制三角形,这次绘制的三角形是一个形状向右、颜色为红色并且底边长度为 80  磅的实心三角形,实际运行效果如图 2.38②所示。 程序再次返回到 paint 方法,继续执行下面的语句,程序第 125 行: setBaseLine(40); 调用 setBaseLine 方法,程序转向到第 92 行~第 95 行: public void setBaseLine(int baseLine)  {  this.baseLine = baseLine;  }  setBaseLine 方法很简单,只是将用户在 setBaseLine 方法中传递整型参数 baseLine 的值 重新赋值给类的数据成员  baseLine。注意,这里用  this  这个关键字来区分用户提交的参数  baseLine 和类的数据成员 baseLine。这时,类的数据成员变量 baseLine 的值就改变为 40 了。 接着程序返回到 paint 方法中,继续执行下面的语句。 程序第 126 行: setOutlineColor(Color.BLUE); 调用 setOutlineColor 方法,程序会转向到第 102 行~105 行: public void setOutlineColor(Color outlineColor)  {  this.outlineColor = outlineColor;  } 同样地,用 this 关键字将用户传递的 outlineColor 对象赋值给系统变量 outlineColor。此 时,系统 outlineColor 变量就被设置为 Color.BLUE。然后,程序再返回到 paint 方法,执行第  127 行: setFillColor(Color.GREEN); 调用 setOutlineColor 方法,程序转向到第 97 行~第 100 行: public void setFillColor(Color fillColor)  {  this.fillColor = fillColor;  } 同样地,用 this 关键字将用户传递的 fillColor 对象赋值给系统变量 fillColor。这时,系 统变量 fillColor 就被设置成为 Color.GREEN。 然后,程序再返回到 paint 方法,执行第 130 行: drawTrigangle(200, 120, DOWN, MIXED, g); 再次调用 drawTrigangle 方法,重复前面我们所讲的步骤,运行完程序第 130 行,程序会 在 Applet 上绘制一个颜色为绿色、向下的、三角形的轮廓颜色为蓝色、底边长度为 40 磅的 实心三角形。实际运行效果如图 2.38③所示。 同理,程序第 132 行~第 136 行: setBaseLine(60);
    • setFillColor(Color.ORANGE);  // 绘制三角形 4  drawTrigangle(300, 120, LEFT, FILL, g); 根据前面所介绍的绘制过程,我们可以得到一个底边长度为  60  磅、颜色为橙色、方向 向左的实心三角形,如图 2.38④所示。  2.10.5  绘制平行四边形及立方体 平行四边形是创建图形三维效果的一个有效途径, Web 图表的设计中起着非常重要的 在 作用。比如说,一个立方体(如 2.7.2.节“利用绘制多个实心矩形来生成 3D 矩形”所绘制的  3D 矩形)由一个正面的矩形,顶部一个平行四边形以及右边侧面的一个平行四边形组成,如 图 2.39 所示。 图 2.40 是立方体对象 Cube 的结构视图。我们把立方体对象 Cube 分解成 3 部分,如图  2.40 所示。 P5  P3  顶部平行四边形 pTop  a  pRight  P6  P2  侧面平行四边形 高(height)  rFront  P4 基准点 正面矩形  (baseXPts,  厚度  baseYPts) P1  thickness  P0 宽(width)  图 2.39  立方体与平行四边形之间的关系 图 2.40  立方体对象 Cube 的结构视图 我们假设以立方体左下角的顶点为基准点 P0,P0 的坐标为(baseXPts,baseYPts) ,立方 体高度为 height,宽度为 weight,厚度为 thickness,顶部平行四边形 pTop 斜边与水平边的夹 角为a。该立方体各个顶点与基准点 P0 的关系就可以用下面的数学公式来表示。  1.正面矩形 rFront 的 4 个端点分别是:  P1x=baseXPts+width  P1y=baseYPts  P2x=P1x  P2y=baseYPts-height  P6x=baseXPts  P6y=P2y  正面矩形 rFront 可以使用前面所学的 Graphics 类的相关方法完成:  g.drawRect(P6x,P6y,width,height)或者  g.fillRect(P6x,P6y,width,height)或者  g.fill3DRect(P6x,P6y,width,height,true)或者  g.fill3DRect(P6x,P6y,width,height,false)  2.顶部平行四边形 pTop  4 个端点由 P6、P2、P3 和 P5 构成,P2 和 P6 已经计算过了,只 需要计算 P3 和 P5,分别是:
    • P3x=P2x+cosa*thickness  P3y=P2y-sina*thickness  P5x=P6x+cosa*thickness  P5y=P3y  通过前面绘制多边形的方法来绘制这个平行四边形。将 4 个端点 P2、P3、P5 和 P6 的坐标 值分别放入两个整型数组 pTopX 和 pTopY,然后,使用以下方法来绘制:  g. drawPolygon(pTopX,pTopY,4)或者  g. filldrawPolygon(pTopX,pTopY,4)  3.右侧面平行四边形 pRight 的 4 个端点由 P1、P2、P3 和 P4 构成,P1、P2 和 P3 已经计算 过了,这里只需计算 P4  :  P4x=P3x  P4y=P3y+height  同理,将 pRight 的 4 个端点 P1、P2、P3 和 P4 的坐标值分别放入两个整型数组 pRightX  和 pRightY,然后,使用以下方法来绘制:  g. drawPolygon(pRightX,pRightY,4)或者  g. filldrawPolygon(pRightX,pRightY,4)  我们可以按照下面的步骤来绘制这个立方体: (1)先计算出各个端点的坐标; (2)绘制顶部的平行四边形 pTop; (3)绘制右侧面的平行四边形 pRight; (4)绘制正面的矩形 rFront。 源程序 DrawCubeApplet.java(chap02Fig2.10Fig2.10_05)演示了如何绘制一个立方体,运 行结果如图 2.41 所示。该程序采用了同上一节讲述的 DrawTriangleApplet.java 相类似的结构 来完成绘制工作。其中主要的方法、方法名及完成的相应功能如下: 图 2.41    DrawCubeApplet.java 的运行结果 Ø setOutlineColor:设置整个立方体的轮廓颜色。 Ø setFrontFillColor:设置正面实心矩形的填充颜色。 Ø setTopFillColor:设置顶部实心平行四边形的填充颜色。 Ø setRightFillColor:设置右侧部实心平行四边形的填充颜色。 Ø setBasePoint:设置基准点的坐标。
    • Ø setWidth:设置立方体的宽度。 Ø setHeight:设置立方体的高度。 Ø setThickness:设置立方体的厚度。 Ø setAngle:设置平行四边形的夹角a; Ø calculateOffset:计算 cosa* thickness 和 sina*thickness 的值。 Ø buildRFront:计算绘制正面矩形所需的数据。 Ø buildPTop:计算绘制顶部平行四边形所需的数据。 Ø buildPRight:计算绘制右侧部平行四边性所需的数据。 Ø drawFront:绘制正面矩形。 Ø drawTop:绘制顶部平行四边形。 Ø drawRight:绘制右侧部平行四边形。 Ø DrawCube:通过调用上面的方法来绘制完整的平行四边形。 程序第 9 行: int baseXPts = 10, baseYPts = 200; // 立方体基准点坐标 声明了两个整型变量 baseXPts 和 baseYPts,表示立方体左下角那一点的坐标值,就是图  2.40 中所示的点 P0。我们默认设置该点的坐标为(10, 200) 。 程序第 10 行~第 12 行: int width = 100; // 立方体的宽度 int height = 120; // 立方体的高度 int thickness = 30; // 立方体的厚度 声明 3 个整型变量 width、height 和 thickness,分别表示立方体的宽度、高度和厚度。 程序第 14 行~第 17 行: int alpha = 45; // 平行四边形的夹角 int offsetX = 0; // 偏移量 X  int offsetY = 0; // 偏移量 Y  final int PARALLELOGRAM_POINT_NUMBER = 4; // 平行四边形的端点数目 程序第 14 行,声明了整型变量 alpha,将图 2.40 中立方体顶部平行四边形斜边和水平边 的夹角a,设置为 45°。程序第 15 行和第 16 行,声明变量 offsetX,表示图 2.40 中点 P5 和 点 P6 之间水平方向上的距离,offsetY 表示点 P5 和 P6 之间在垂直方向上的距离。同理,点 P3  和  P2  之 间 也 存 在 着 这 种 关 系 。 程 序 第  17  行 , 使 用 了 一 个  final  修 饰 的 整 型 变 量  PARALLELOGRAM_POINT_NUMBER,表示要绘制的多边形的端点数目。因为平行四边形 总共有 4 个端点,所以,这里我们将其值设置为 4。 程序第 19 行~第 27 行: int[] pTopX = new int[PARALLELOGRAM_POINT_NUMBER];  // 顶部平行四边形端点的 Y 坐标数组 int[] pTopY = new int[PARALLELOGRAM_POINT_NUMBER];  // 右侧部平行四边形端点的 X 坐标数组 int[] pRightX = new int[PARALLELOGRAM_POINT_NUMBER];  // 右侧部平行四边形端点的 Y 坐标数组 int[] pRightY = new int[PARALLELOGRAM_POINT_NUMBER]; 程序第 19 行~第 21 行,用整型数组 pTopX 和 pTopY 分别保存顶部平行四边形 4 个端 点的横坐标和纵坐标。程序第 25 行~第 27 行,用整型数组 pRightX 和 pRightY 分别保存右 侧部平行四边形 4 个端点的横坐标和纵坐标。 程序第 33 行:
    • final int OUTLINE = 1, FILL = 2, MIXED = 3; 声明 3 个整型变量 OUTLINE、FILL 和 MIXED,分别代表立方体的绘制风格: Ø  OUTLINE:绘制立方体轮廓(即绘制空心立方体) ; Ø  FILL:绘制实心立方体; Ø  MIXED:绘制一个带轮廓的实心立方体(即先绘制一个实心立方体,然后在其上再 绘制一个空心的立方体) 。 程序第 35 行~第 37 行: Color rFrontFillColor = new Color(178, 0, 0); // 默认的填充颜色 Color pTopFillColor = Color.RED; // 默认的填充颜色 Color pRightFillColor = new Color(124, 0, 0); // 默认的填充颜色 声明并初始化了 3 个 Color 类的对象。分别表示立方体正面的矩形、顶部平行四边形及 右侧部平行四边形默认的填充颜色。RGB 为(178,0,0)和(124,0,0)表示两种红色。 它们都比 Color.RED 所表示的红色要深一些,红色立方体的正面和右侧面如图 2.41①所示。 提示:本书提到的“填充”,表示用当前颜色绘制一个实心的图形对象,而“描绘”表 示用当前颜色绘制一个空心的图形对象(即绘制图形的轮廓)。 程序第 38 行: Color outlineColor = Color.LIGHT_GRAY; //默认的立方体轮廓颜色 设置默认描绘立方体的颜色为淡灰色。 程序首先对上述变量进行初始化,然后,直接调用程序第  190  行的  paint  方法,再回到  paint 的方法体中。 绘制完标题后,程序第 200 行: DrawCube(FILL, g); 调用 DrawCube 方法来绘制一个立方体。之后,程序立即转到第 140 行,执行 DrawCube  方法。DrawCube 方法需要两个参数,第 1 个是整型参数,表示要绘制的平行四边形的风格, 该风格在程序第 33 行已经定义了,FILL 在这里表示要绘制一个实心立方体;第 2 个参数是  Graphics 类的对象 g,它是由 Applet 容器创建的。 程序运行到 DrawCube 的方法,首先执行程序第 142 行: calculateOffset(); 调用程序第 97 行 calculateOffset 方法,利用下列公式: offsetX = (int)(Math.cos(alpha * Math.PI / 180) * thickness + 0.5);  offsetY = (int)(Math.sin(alpha * Math.PI / 180) * thickness + 0.5); 计算偏移量的值。注意,我们在 Math.cos(alpha * Math.PI / 180) * thickness 后面加上了一 个 0.5 的浮点数。原因是 Math.cos 的结果是一个 double 值的小数,而变量 offsetX 是一个整 型变量, 所以我们使用 (int)的方法,强制将 double 值转换成整数, 转换的结果是将 Math.cos  值的小数部分全部丢弃。如:double 类型的 10.999 和 10.0001,都被转换成同样的整数 10, 将其结果加上 0.5 后,凡是小数部分大于 0.5 的,其个位将会递增 1。如 10.999 +0.5=11.499, 转换成整数后,将得到 11 的结果;而 10.0001+0.5=10.5001,转换成整数后,该值仍然是 10。 上面的例子说明,一个浮点数取整的时候,加上 0.5 后,就实现了 4 舍 5 入的功能。  calculateOffset 方法结束后,程序返回到 DrawCube 方法中,继续执行第 143 行: buildRFront();
    • 调用 buildRFront 方法。程序转到第 104 行 buildRFront 方法,在 buildRFront 方法体内, 用以下方法计算出图 2.40 所示的 P1、P2 和 P6 点: pRightX[0] = baseXPts + width;  pRightY[0] = baseYPts;  pRightX[1] = pRightX[0];  pRightY[1] = baseYPts ­ height;  pTopX[0] = baseXPts;  pTopY[0] = pRightY[1];  P0、P1、P2 和 P6 这 4 个点,正好组成正面的矩形。其中 P1 和 P2 两个点分别与右侧面平 行四边形 1P2P3P4) (P 左边两个端点重合, 6 和 P2 这两个点分别同顶部平行四边形 6P2P3  P5) P (P 下方的两个端点相重合。因此,在计算出 P1、P2 和 P6 点后,将点 P1 和 P2 的结果赋值给代表 右侧平行四边形坐标的数组 pRightX,以及 pRightY 第 1 个和第 2 个元素。将点 P6 的结果赋 值给代表顶部平行四边形坐标的数组 pTopX,以及 pTopY 第 1 个元素。 之后,程序回到 DrawCube 方法中,并执行程序第 144 行: buildPTop(); 程序转到第 117 行的 buildPTop 方法。 在 buildPTop 方法体内,用以下方法计算出图 2.40 所示的 P2、P3 和 P5 点: pTopX[1] = pRightX[1];  pTopY[1] = pRightY[1];  pTopX[2] = pTopX[1] + offsetX;  pTopY[2] = pTopY[1] ­ offsetY;  pTopX[3] = pTopX[0] + offsetX;  pTopY[3] = pTopY[2]; 经过计算以及赋值操作后,整型数组 pTopX 和 pTopY 就完成了初始化,保存了点 P6、  P2、P3、P5 坐标的相关信息。 程序返回到 DrawCube 方法中,继续执行第 145 行: buildPRight(); 程序转到并执行第 130 行 buildPRight 方法。 在 buildPRight 方法体内,用以下方法计算出图 2.40 所示的 P3 和 P4 点: pRightX[2] = pTopX[2];  pRightY[2] = pTopY[2];  pRightX[3] = pRightX[2];  pRightY[3] = pRightY[2] + height; 经过计算以及赋值操作后, 整型数组 pRightX 和 pRightY 就完成了初始化,保存了点 P1、  P2、P3、P4 坐标的相关信息。 之后, 程序返回到 DrawCube 方法中,继续执行程序第 147 行的 switch 多重选择语句块: switch (cubeStyle) 程序根据用户传递的整型参数进行选择。 现在用户传递的参数是“FILL” ,根据程序第 33 行的声明,变量 FILL 的值为 2。所以 程序执行标号为 2 的语句,即程序第 158 行~第 168 行。这段代码进行了 3 次填充操作,首 先是程序第 159 行~第 160 行:
    • g.setColor(pTopFillColor);  g.fillPolygon(pTopX, pTopY, PARALLELOGRAM_POINT_NUMBER); 用  Color  类的实例  pTopFillColor  表示的颜色,即红色(Color.RED)对顶部的平行四边 形进行填充的绘制工作。 程序第 162 行~第 163 行: g.setColor(rFrontFillColor);  g.fillRect(pTopX[0], pTopY[0], width, height); 重新设置当前的绘图颜色, Color 类的实例 rFrontFillColor 表示的颜色, 用 即暗红色 (RBG  值:178,0,0)对正面的矩形进行填充的绘制工作。注意,程序第 163 行,我们在这里没有 采用 Graphics 类的 fillPolygon 方法,而是直接调用了 Graphics 类的 fillRect 方法,因为: Ø 可以减少定义两个用于保存正面矩形坐标的整型数组,节省内存; Ø 可以使程序更精简; Ø 得到了绘制矩形所需要的参数,我们可以直接调用 fillRect 方法。 因为矩形的左上角顶点(P6)和顶部平行四边形(P6P2P3P5)的第一个端点相重合,所以 直接使用 pTopX[0]和 pTopY[0]来表示该点。 此外,还可以使用点 P0 和 P6 之间的关系,即两者的横坐标相同,纵坐标相差 height,使 用下列的方法来绘制该矩形: g.fillRect(baseXPts, baseYPts ­ height, width, height); 填充完正面的矩形后,程序继续运行第 165 行~第 166 行: g.setColor(pRightFillColor);  g.fillPolygon(pRightX, pRightY, PARALLELOGRAM_POINT_NUMBER); 与程序第 159 行~第 160 行的代码基本相同。重新设置当前的绘图颜色,即用 Color 类 的实例 rFrontFillColor 表示的颜色,即深红色(RBG 值:124, 0, 0)来填充右侧面这个平行四 边形。 程序执行完第 166 行语句后,通过绘制两个平行四边形和一个矩形就得到了一个默认的 红色的实心立方体,如图 2.41①所示。这里总结一下该立方体的基本参数: Ø 立方体基准点的坐标是:10,200。 Ø 顶部平行四边形的夹角为:45°。 Ø 立方体的长度、宽度和厚度分别为:100 磅、120 磅和 30 磅。 Ø 立方体的风格为实心。 之后,程序执行第 168 行的 break 语句,中断 DrawCube 方法的运行再返回到 paint 方法 中。 程序第 203 行~第 211 行,用蓝色填充了一个如图 2.41②所示的立方体。下面我们来具 体分析。 程序第 203 行~第 207 行: setBasePoint(150, 300);  setWidth(80);  setHeight(220);  setThickness(40);  setAngle(60); 这段代码调用了 5 个 set 方法,重新设置了立方体的基准点坐标为(150,300) 、宽度为  80 磅、高度为 220 磅、厚度为 40 磅、顶部平行四边形的夹角为 60°。上述 set 方法与上一 节所讲述的 DrawTriangleApplet.java 程序中的相关 set 方法的结构和用法完全一致,我们就此 略过。
    • 程序第 208 行~第 210 行: setPTopFillColor(new Color(0, 0, 255));  setFrontFillColor(new Color(0, 0, 178));  setPRightFillColor(new Color(0, 0, 124)); 这段代码调用了 3 个 set 方法,重新设置了立方体的 3 个面,即顶部平行四边形的填充 颜色、正面矩形的填充颜色及右侧面平行四边形的填充颜色。 程序第 211 行: DrawCube(FILL, g); 与程序第 200 行的代码完全相同,再次调用 DrawCube 方法,并提供与程序第 200 行代 码相同的参数。之后,程序再次进入第 140 行的 DrawCube 方法体中,重复前面讲述的计算、 赋值及填充绘制等功能。根据立方体的绘制参数,得到的绘制结果如图 2.41②所示,与默认 的立方体绘制结果(图 2.41①)相比,我们可以清楚地看到两者之间的差别。 程序第 214 行~第 215 行: setBasePoint(280, 300);  setOutlineColor(Color.ORANGE); 重新设置该立方体的基准点坐标为: (280,300) ,然后,重新设置立方体轮廓的描绘颜 色为橙色(默认为浅灰色,见程序第 38 行)。 程序第 216 行: DrawCube(OUTLINE, g); 再次调用 DrawCube 方法。 不过提供参数中的第 1 个整型参数的值由先前的“FILL”改变为“OUTLINE” 。同理, 根据程序第 33 行的定义,OUTLINE 的值为 1,所以这次调用 DrawCube 方法,进入 switch  语句块时,将执行第 149 行~第 156 行: case 1:  g.setColor(outlineColor);  g.drawPolygon(pTopX, pTopY, PARALLELOGRAM_POINT_NUMBER);  g.drawRect(pTopX[0], pTopY[0], width, height);  g.drawPolygon(pRightX, pRightY, PARALLELOGRAM_POINT_NUMBER);  break; 先设置立方体描绘的颜色为 Color 对象 outlineColor 所指定的颜色——橙色,然后,程序 分别用橙色描绘了立方体的 3 个组成部分,即顶部平行四边形、正面的矩形及右侧面的平行 四边形。绘制结果如图 2.41③所示。 该立方体绘制完成后,程序继续执行第 219 行~第 228 行。 我们先看程序第 219 行~第 227 行所进行的操作: setBasePoint(400, 280);  setWidth(120);  setHeight(180);  setThickness(20);  setAngle(30);  setPTopFillColor(Color.GREEN);  setFrontFillColor(Color.MAGENTA);  setPRightFillColor(Color.PINK);  setOutlineColor(Color.BLACK); 这段代码也是调用一些简单的 set 方法,重新设置立方体的基准点、宽度、高度、厚度、 夹角、立方体三个面的填充颜色及轮廓的描绘颜色。这次我们设置其顶部的平行四边形的填
    • 充色为绿色、正面矩形的填充色为品红色、右侧面平行四边形为粉红色、立方体的轮廓描绘 颜色为黑色。 程序第 228 行: DrawCube(MIXED, g); 再次调用 DrawCube 方法,这次提供的参数中的第 1 个整型参数的值改变为“MIXED” 。 同理,根据程序第 33 行的定义,整型变量 MIXED 的值为 3,所以这次调用 DrawCube 方法, 进入 switch 语句块时,将执行第 170 行~第 186 行: case 3:  g.setColor(pTopFillColor);  g.fillPolygon(pTopX, pTopY, PARALLELOGRAM_POINT_NUMBER);  g.setColor(rFrontFillColor);  g.fillRect(pTopX[0], pTopY[0], width, height);  g.setColor(pRightFillColor);  g.fillPolygon(pRightX, pRightY, PARALLELOGRAM_POINT_NUMBER);  g.setColor(outlineColor);  g.drawPolygon(pTopX, pTopY, PARALLELOGRAM_POINT_NUMBER);  g.drawRect(pTopX[0], pTopY[0], width, height);  g.drawPolygon(pRightX, pRightY, PARALLELOGRAM_POINT_NUMBER);  break; 这段代码进行了 3 次图形的填充和 1 次的轮廓描绘操作。用我们设定的立方体 3 个面的 填充颜色,分别对立方体的顶部、正面和右侧面进行填充操作,然后,用设定的轮廓颜色对 立方体的 3 个面依次进行轮廓的描绘操作。实际运行结果如图 2.41④所示。 通过前面的学习,读者对 Java 如何控制颜色、操作字体、绘制基本的几何图形,以及扩 展到三维图形的绘制, 已经有了一个初步的认识。 下一节,我们将学习如何在 Java Applet 中, 加载并显示计算机上的图形文件。  2.11  图像的载入与显示 在现实工作中, 我们希望在 Web 图表最终输出结果中包含一些图标或者图像。这些图标 和图像可以是公司的徽标,也可以是软件的徽标(如微软的 Windows 操作系统的徽标、JSP  Web 服务器 Tomcat 的徽标等),也可能是任意一幅图片。如果这些徽标或者图像都需要我们 自己使用 Java 相关的绘制方法来完成,那么效率实在是太低了。对于一些已经绘制完成的图 像,我们可以直接使用 Java 从文件中读取并显示图像。  2.11.1  在 Applet 中加载和显示图像  Java 中有多种方法都可以加载并显示图像文件,如下所述: Ø 调用 Image 类的相关方法。 Ø 调用 ImageIcon 类的相关方法。  Java J2SE 5.0 版之前的旧版 Java 处理格式基本上分为以下三类: Ø 联合图像专家组(Joint Photographic Experts Group)格式,即 JPG/JPEG 格式。 Ø 图形交错格式(Graphics Interchange Format 格式) ,即 GIF 格式。 Ø 可移植的网络图像(Portable Network Graphics)格式,即 PNG 格式。
    • J2SE 5.0 后的版本就支持了微软的 BMP 格式图像。BMP 图像作为 Windows 环境下主要 的图像格式之一,以其格式简单、适应性强而备受欢迎。因此在 J2SE  5.0 中,Sun 公司也将 其纳入 Java 的支持范围。但 BMP 文件格式,在处理单色图像和真彩色图像的时候,无论图 像数据多么庞大,都不对图像数据进行任何压缩处理,这就使得  BMP  在存储单色图像或者 真彩色图像时,文件体积过于庞大。所以在网络上,一般不采用  BMP  的文件格式来进行传 送。 表 2.19 列出了 java.swing 包中的 ImageIcon 类中的部分构造器。 表 2.19    ImageIcon 类中的部分构造器  ImageIcon()  生成尚未初始化的 ImageIcon 对象,在使用之前必须使用 Image 对象初始化  ImageIcon(String filename)  以 filename 指定的文件生成一个 ImageIcon 对象,filename 既可以表示文件的绝对路径,也可以是当前目录的相对路径  ImageIcon(String filename, String description)  同上面构造器一样,从  filename  指定的文件生成一个  ImageIcon  对象,第二个参数指定图像的说明。该图像将被  MediaTracker 类的对象预先装载用来监视图像的加载状况  ImageIcon(URL location)  以 URL 参数指定的源文件对象创建一个 ImageIcon 对象。URL 类表示统一资源定位器,它指出 World  Wide  Web 上的 资源。这里的 URL 可以是因特网上的地址,也可以是本地计算机  ImageIcon(URL location, String description)  URL 指定图像文件的位置,description 指定文件的名称。这里的 URL 可以是因特网上的地址,也可以是本地计算机  ImageIcon(Image image)  以 Image 的对象来创建一个 ImageIcon 对象  ImageIcon(Image image, String description)  以 Image 的对象来创建一个 ImageIcon 对象,第二个参数提供了图像的说明  ImageIcon 类不是 abstarct 类,所以,程序可以直接创建 ImageIcon 对象。ImageIcon 类提 供了多个构造器,允许程序使用本地计算机上的图像,或者使用存储在  Internet  上的图像来 初始化 ImageIcon 对象。 在这里我们已经涉及到了 URL,所以让我们先简要介绍 URL 和 URL 类。  1.URL  ,用 Internet 提供了许多协议,HTTP 是超文本传输协议(Hyper  Text  Transfer  Protocol) 在 World Wide Web 上,它构成了因特网的基础。它用 URI(Uniform Resource Identifier)统一 资源标识符来标识 Internet 上的数据,用于指定文档位置的 URI(Uniform Resource Locator) 统一资源定位符。一般的 URL 引用文件或目录,也可以引用执行复杂任务的对象,诸如执行 数据库查询和 Internet 搜索。URL 是相对于使用路径来标识数据资源文件标准方法的扩展。  URL 由 4 部分组成,它描述了要访问的资源文件的位置、名称,以及如何与保存该文件的主 机通信而获取文件。URL 中的某些部分可以完全忽略或者是使用其默认值。 表 2.20    URL 组成 协 议 域 名 端 口 路径和文件名  http://  java.sun.com  :80  /index.html  ftp://  ftp.freebsd.org/  :21
    • 协议指定了用来获得文件的通信方法。FTP 是文件传输协议(File Transfer Protocol) 、远 程收取信件的 POP3 或者 IMAP 协议,以及用于发送信件的 SMTP 协议,远程登录的 Telnet  和 SSH 协议等。 端口在计算机上只是标识特定服务的数字,一个计算机可以有多个端口,每个端口提供 不同的服务。通常不需要指定 URL 中的端口,因为标准端口与特定的协议相关,系统将指定 默认的端口。例如,HTTP 协议使用 80 端口、FTP 协议使用 21 端口、POP3 使用 110 端口、  SMTP 协议使用 25 端口、TELNET 协议使用 23 端口、SSH 协议使用 22 端口,等等。 因为域名是数据资源引用的一部分,网络上每个数据源都有惟一的 URL。只要知道数据 资源在因特网上的 URL、储存该数据的计算机已经连接在因特网中并对外提供服务、具有相 应的读取权限,我们就可以获得该数据资源。  2.URL 类  ImageIcon 的构造器中接收了 URL 类的实例,  URL 类封装了 URL, 它隶属于 java.net 包。  Java 的网络功能集成在几个包中。 java.net 包中的类和接口声明了基本的网络功能, 使开发基 于 Internet 和 Web 的应用更为简单。本书讲述的内容(第 3 章以后的内容) ,也是一种基于  Internet/intranet 的 Web 服务器端编程技术。Java 通过该包提供了基于流的通信(stream­based  communication) ,使得应用程序能够把网络视为数据流。此外,java.net 包中的类和接口还提 供了基于数据包的通信(packet­based  communication) ,用于传输单独的信息包,通常是在  Internet 上用来传输音频和视频。 本书对网络的讨论,主要集中在浏览器/服务器(Browser/Server)的两端。浏览器请求 执行某些操作,服务器响应其请求并执行相应的操作,将执行结果返回给浏览器。 程序  LoadImageApplet.java(chap02Fig2.11Fig2.11_01)演示了如何加载并显示几种格式 图像的方法, 除了旧版 Java 支持的 PNG、  GIF、  JPG 外, 我们在程序中还提供了如何加载 BMP  图像文件的方法,运行结果如图 2.42 所示。讲解本程序源代码之前,读者可以在相应的目录 下先浏览本程序将要加载并显示的 4 个不同格式的图像文件。 图 2.42    Applet 加载并显示不同格式的外部图像文件 程序第 6 行: import java.net.*;  ImageIcon 类接收 URL 实例的构造器,URL 类是在 java.net 包中被定义的,所以,这里 必须引入 java.net 包。 程序第 7 行:
    • import javax.imageio.*; 旧版 Java 并不支持 BMP 格式的图像文件,Java 在新版的 javax.imageio 包中的 ImageIO  类中提供对读取 BMP 格式图像文件的支持。 因为例程中涉及到了 BMP 格式图像文件的读取, 所以,这里也必须引入 javax.imageio 这个包。 程序第 11 行: private Image logoJPG, logoBMP, logoPNG; 声明了 3 个 Image 类的变量 logoJPG、logoBMP 和 logoPNG,分别表示加载 JPG、BMP  和 PNG 格式图像文件的 Image 对象。Image 类属于 java.awt 包中 Image 类,可以读取图像文 件。 程序第 12 行: private ImageIcon logoGIF; 定义了一个 ImageIcon 类的对象 logoGIF,表示该对象用于处理 GIF 格式的图像文件。 程序第  15 行~第  36  行,是  Applet  的  init  方法,加载图像、初始化和预处理工作就在  Applet 的 init 方法中完成。 程序第 18 行: logoJPG = getImage(getDocumentBase(), "logo.jpg");  Image 类是一个抽象类 (abstract class)所以 Applet 不能直接创建 Image 类的对象。  , Applet  必须调用一个方法,让 Applet 容器加载并返回程序要使用的 Image 类的对象。JApplet 的超 类 Applet 提供了一个名为 getImage 的方法,该方法将 Image 加载到 Applet 中,方法接收两 个参数——图像文件的位置和图像文件名。在第  1  个参数中,Applet  方法  getDocumentBase  返回一个 URL,表示图像资源在 Internet 中的位置(如果 Applet 是从本地计算机上加载的, 则返回该图像资源在本地计算机中的位置) 。本例中,Java Applet 编译后的.class 文件和调用 该.class  文件的  HTML  文件,以及  4  个不同格式的图像文件都保存在同一个目录下。方法  getDocumentBase 以 URL 类的对象的形式返回图像文件 logo.jpg 在本地计算机中的位置,第  2 个参数指定图像文件的文件名。 从第 18 行开始,程序就从本地计算机加载图像(或者从 Internet 下载图像,视 URL 对 象的内容而定) 。程序使用一个独立的线程去执行加载图像,这样,程序在图像开始加载后, 继续执行下面的语句。 提示:如果 URL 对象指定被请求的文件资源不可用,getDocumentBase 方法并不提示出 错或者抛出异常。 程序第 20 行: logoPNG = getImage(getDocumentBase(), "logo.png"); 该行与程序第 18 行语句,除了加载的图像文件的文件名不同外, 其余完全相同。程序在本 地计算机中加载一个名为 logo.png 的图像文件时,也使用一个独立的线程去执行它。 程序第 26 行: logoGIF = new ImageIcon(new URL(getCodeBase(), "logo.gif"));  ImageIcon 类不是 abstract 类,所以可以直接创建 ImageIcon 的实例。本条语句在执行时, 就创建了 ImageIcon 类的对象 logoGIF,用于加载本地计算机上的文件名为 logo.gif 的图像文 件。从表 2.19 中,我们可以看到 ImageIcon 类提供了多个构造器,允许程序使用本地计算机 上的图像,或者是存储在  Internet  中的图像来初始化  ImageIcon  对象。注意,如果以程序第
    • 26 行的方式, 创建并初始化 ImageIcon 对象时, 系统可能会抛出异常。例如,输入错误的 URL  字符串等,所以我们把该语句放入程序第 23 行~第 35 行之间的 try…catch 语句块中。在这 里发生的异常,可能属于以下两类或者其派生的子类: Ø  IOException  Ø  java.net.UnknowHostException  程序第 29 行: logoBMP = ImageIO.read(new URL(getCodeBase(), "logo.bmp")); 同理,  URL 对象可能产生异常, 所以我们将其也放入程序第 23 行~第 35 行之间的 try…  catch 语句块中。不同的是,这里我们处理的是 BMP 格式的图像文件。传统的 getImage 方法 是不支持 BMP 格式的,我们在此引入了 javax.imageio 包中的 ImageIO 类来处理。logoBMP  是一个 Image 类的对象,Image 是抽象类,不能直接实例化,必须由 Applet 调用另外一个方 法,让 Applet 容器加载并返回程序要使用的 Image 类的对象。ImageIO 的 read 方法,返回一 个 BufferedImage 的对象引用,BufferedImage 是 java.awt.Image 类的子类,而子类的实例对象 是可以被其父类直接使用的,故此,实现了获得 Image 类的实例的目的。  init 的方法执行后,程序执行第 39 行的 paint 方法。paint 方法中的第 42 行~第 48 行, 在 Applet 上绘制了一条橙色的文本后, 继续执行其余语句, 将需要显示的图像一一绘制出来。 程序第 51 行: g.drawImage(logoJPG, 5, 50, this); 调用 Graphics 类的 drawImage 方法,显示 logoJPG 对象。drawImage 方法需要 4 个参数: 第 1 个参数是将要显示的 Image 类的对象引用(这里是 logoJPG) ;第 2 个和第 3 个参数是个 整形参数,表示该 Image 类的对象的引用 logoJPG 在 applet 中绘制所需的 x 和 y 坐标,该坐 标表示图像左上角顶点的坐标位置;最后一个参数是  ImageObserver  对象的引用。通常,  ImageObserver  是 程 序 用 来 显 示 图 像 的 对 象 , ImageObserver  可 以 是 任 何 一 个 实 现  ImageObserver 接口的对象。  Component 类实现了 ImageObserver 接口, 因此, 所有的 Component  都是 ImageObserver。因为 JApplet 也是 Component 类的子类(见第 2.2.1 节组件与容器,图  2.3 所示) ,所以 JApplet 也是 ImageObserver。最后一个参数使用了关键字“this” ,说明当前 这个 ImageObserver 对象就是 JApplet 本身。  ImageObserver 负责处理从 URL 制定的资源上加载图像数据的过程。当这个图像数据在 因特网上,而且图像文件比较大的时候,图像的加载过程就可能有比较长的延迟。为解决此 问题,ImageObserver 接口还声明了 imageUpdate 的方法,该方法会在前面请求有关图像的信 息可用时自动调用,并在组件上绘制该图像。 本语句执行后,将在制定位置显示 Image 类的对象 logoJPG,运行结果如图 2.42①所示。 程序第 52 行和第 54 行: g.drawImage(logoPNG, 160, 50, this);  g.drawImage(logoBMP, 5, 160, this); 同样是调用 Graphics 类的 drawImage 方法来显示图像,除了需要显示的 Image 对象不同 外,其余全部相同。这里是分别绘制一个 PNG 和 BMP 格式的图像文件。运行结果如图 2.42  ②和图 2.42③所示。 程序第 55 行: logoGIF.paintIcon(this, g, 160, 160); 直接调用 ImageIcon 类的 paintIcon 方法来显示图像,该方法需要 4 个参数:Component  对象引用、Graphics 对象引用及图像的绘制坐标(以图像的左上角为基准点) 。本语句的运行
    • 结果如图 2.42④所示。 对比这两种加载及显示图像的方法,我们推荐使用 ImageIcon 方法。因为 ImageIcon 方 法更简单,更直接地创建  ImageIcon  类的实例,而且在显示图像的时候也不需要使用  ImageObserver 的引用。  2.11.2  使用 MediaTracker 加载并显示图像 上节我们阐述了两种加载并显示图像的方法,但是采用异步的方式,即程序在后台完成 图像的加载和显示,而主进程继续执行。这种情况可能在图像还未被完全加载完毕时造成不 完整的显示。在某些情况下,我们需要在图像完全加载完毕后再进行显示,要得到这种功能 就需要了解本节介绍的另外一种加载图像的方法,即调用媒体跟踪器(Media  Tracker)来加 载图像。 媒体跟踪器是 MediaTracker 类型的对象,专门用来跟踪图像的加载。这个类在 java.awt  包中定义,目前只管理图像的载入,以后可能会扩展其功能用来跟踪其他类型的媒体资源的 加载。MediaTracker  只有一个构造器,需要一个组件引用的参数,这个组件对象就是装入图 像的组件。表 2.21 列出了媒体跟踪器的构造器及其相关方法。 表 2.21  媒体跟踪器的构造器及其相关方法  public MediaTracker(Component comp)  构造器:接收一个用于加载图像的 Component 对象  public void addImage(Image image, int id)  续表 把 Image 类的对象 image 添加到当前媒体跟踪器要追踪的图像列表中,整型对象 id 表示该 image 的标识  public void waitForAll()  初始化加载过程并等待所有被跟踪的图像加载  public boolean waitForAll(long ms)  指定初始化加载过程并等待图像加载的时间,时间用毫秒表示  Public boolean checkID(int id)  检查指定的标识为 id 的 Image 对象 image 是否加载完毕,如果已经加载完毕,则返回真  Public boolean checkAll()  检查所有被跟踪的 Image 对象 image 是否加载完毕,如果已经加载完毕,则返回真  Public boolean isErrorAny()  检查所有被跟踪的 Image 对象 image 的错误状况,没有错误则返回假  Public boolean isErrorID(int id)  检查指定的标识为 id 的 Image 对象 image 在加载过程中是否发生错误,没有错误则返回假 媒体跟踪器的使用是在调用  getImage()方法,获得对 Image 的对象引用后,调用跟踪器 的 addImage()方法就将 Image 对象传递给媒体跟踪器。addImage 方法有两个参数:要跟踪的 图像的引用,以及用来跟踪图像的  int  类型的标识。媒体跟踪器可以同时跟踪多个图像,该 标识决定被加载图像的优先级。 程序  MediaTrackerApplet.java(chap02Fig2.11Fig2.11_02)演示了如何在  Applet  中调用  MediaTracker 类以及跟踪图像,运行结果如图 2.43 所示。
    • 图 2.43  用媒体跟踪器(MediaTracker)加载外部图像文件 程序第 9 行~第 11 行: private Image[] images = new Image[3];  private Image bg; // 背景图像 private MediaTracker mt = null; 声明了 1 个 Image 类的数组对象 images,其中包含 3 个 image 对象,1 个 Image 类的对 象 bg,表示本 Applet 将要加载的背景图像,还声明了一个 MediaTracker 类的对象 mt。 程序第 14 行~第 27 行,在 init 方法内,对上述变量进行初始化。 程序第 16 行: bg = getImage(getCodeBase(), "images/background.gif"); 用 Applet 类 getImage 方法得到一个 Image 类的对象引用,并传递给 Applet。注意,在  getImage 的方法内,第 2 个参数的使用方法: "images/background.gif" 表示图像文件的位置在 Java  Applet 当前目录下的 images 子目录中,因此这里表示加载 当前目录中的 images 子目录下的 background.gif 图像文件。 程序第 19 行: mt = new MediaTracker(this); 初始化 MediaTracker 的实例 mt。MediaTracker 只有一个构造器,需要一个组件引用的参 数,这个组件对象就是装入图像的组件。这里的 this 表示本容器,也就是在本 Applet 中创建 图像。 程序第 20 行: mt.addImage(bg, 0); 使用 MediaTracker 类的 addImage 方法,指定要跟踪的图像对象。媒体跟踪器可以同时 跟踪多个图像,多个图像可以拥有相同的标识,标识的值决定加载图像的优先级,具有相对 较低标识值的图像优先加载。这里,图像 bg 的标识指定为 0。 程序第 22 行~第 25 行: for (int i = 0; i < images.length; i++)
    • {  images[i] = getImage(getCodeBase(), "images/logo" + (i + 1) + ".jpg");  mt.addImage(images[i], i + 1);  } 使用了一个循环,在用 getImage 方法获得图像的引用后,将当前目录下的 images 子目 录中 logo1.jpg、logo2 以及 logo3.jpg 图像文件,依次添加到当前媒体跟踪器 mt 要跟踪的图像 列表中。标识在这里分别设定为 1、2、3。  init 方法结束,进入 paint 方法中。 程序第 32 行~第 40 行: try  {  mt.waitForAll();  }  catch (InterruptedException e)  {  System.out.println(e);  return ;  } 程序在第 34 行,调用 waitForAll 方法初始化加载过程,并等待所有被跟踪的图像加载完 毕后返回。如果要加载某个特定标识的图像对象,可以调用跟踪器的  waitForID  方法,并传 递一个标识给该方法,开始加载与某个特定标识相关的所有图像对象,并等待所有与该标识 相关的图像加载完成后才返回。例如,希望跟踪标识为 1 的图像,在上面的语句块中,只需 要更改程序第 34 行: mt.waitForID(1); 如果我们只希望跟踪某一个特定的图像对象,就赋予该媒体跟踪器中所跟踪的每个图像 对象都具有一个惟一的标识。这样,一旦出现异常,我们可以通过相应的方法获得出错图像 的标识,从而找到发生错误的地方。 这里介绍的 waitForID 和 waitForAll 方法将无限期进行等待。因为这些方法可能会抛出 异常,所以将其放入一个 try…catch 语句块中。还有另外一个版本的 waitForAll 方法,该方 法需要一个长整型变量,指定初始化加载过程以及等待图像加载的时间,时间用毫秒表示, 由这个长整型的参数决定。但是使用这种方法,我们并不知道在该方法返回时,图像是否真 的装载完毕了,还是等待的时间到了;也许图像早就加载完毕了,但因为设置的等待时间还 没有结束,因此媒体跟踪器仍旧在等待,这样会严重浪费系统资源。所以我们推荐使用前面 介绍的第 1 和第 2 种方法,而不要随意使用需要接收一个等待时间参数的 waitForAll 方法。 如果 waitForID 和 waitForAll 方法在无限期的等待中抛出异常,将被程序第 36 行~第 40  行的 catch 语句块所捕获,然后在命令控制台输出一条有关的异常信息,之后,主线程返回,  Applet 上没有任何显示。  waitForAll  方法执行完毕后,虽然所有被跟踪的图像都已经加载完毕,但并不说明这些 图像的加载过程中没有任何错误(例如,图像的路径有错、或者图像的文件名有误、或者文 件名和路径都正确,但却是 Java  不支持的图像格式) ,所以,我们必须明确地检查错误,因 此程序第 42 行~第 53 行的代码就负责此项检查工作: if (mt.isErrorAny())  {  for (int i = 0; i <= 4; i++)  {  if (mt.isErrorID(i))  {  g.drawString("图像产生错误状况!文件标志为:" + i, 10, 40 + 20 * i);  }
    • }  return ;  } 程序第  42  行,调用了  isErrorAny 方法,用来检查并确定任何被追踪的图像是否产生了 错误。如果方法返回 false,则说明没有错误发生;如果图像产生了错误,isErrorAny 方法返 回真。我们在程序第 44 行~第 49 行的代码中,对其进行进一步的检查。 程序第 44 行~第 50 行, 编写了一段循环代码来检查并确定到底是哪一个被追踪的图像产 生了错误。程序第 46 行,我们将循环语句中的局部变量 i 作为参数并调用 isErrorID 方法。 如果 isErrorID 方法返回 true,则说明至少在加载一个图像标志与变量 i 相关的图像发生了错 误。因为每个图像都指定了惟一的标识,所以一旦发生错误,可以轻松地找到错误的图像。 找到发生错误的图像后,程序将在 Applet 上绘制出一条与该错误相关的信息。 当确定图像的加载没有错误时,就可以显示这些图像了。 程序第 55 行: g.drawImage(bg, 0, 0, this); 我们前面介绍过的 Graphics 类的 dwawImage 方法, Applet 上绘制图像 bg, 在 如图  2.43  ①所示。 图像 bg (background.gif)的宽度和高度分别是 565 磅和 470 磅。 为达到将该图作为 Applet  背 景 的 目 的 , 在 调 用  MediaTrackerApplet.class  的  HTML  文 档 中 , 我 们 指 定 该  Media  TrackerApplet.class 的宽度和高度正好与图像 bg(background.gif)的宽度和高度相同。例如,  MediaTrackerApplet.html 文档的第 4 行~第 5 行所示: <APPLET CODE = "MediaTrackerApplet.class" WIDTH = "565" HEIGHT = "470">  </APPLET> 程序第 57 行以及第 59 行: g.setFont(new Font("汉真广标", Font.PLAIN, 30));  g.drawString("MediaTracker 的使用", 15, 30); 用“汉真广标” (30 磅,普通风格)这种字体在 Applet 上的坐标(15,30)处绘制文本 “MediaTracker 的使用” 。Java 的绘图模式默认为覆盖模式,所以, “MediaTracker 的使用” 这段文本将显示在已经绘制完成的图像 bg 上,见图 2.43②所示。反之,如果程序第 57、59  行的位置互换,将无法看到文本“MediaTracker 的使用” ,因为图像对象 bg 将它覆盖了。 程序第 61 行~第 64 行: for (int i = 0; i < 3; i++)  {  g.drawImage(images[i], 410, 40+i * 100, this);  } 循环绘制出 Image 的数组对象 images 中的每一个图像对象。因为,每个图像对象的绘制 横坐标都为 410 像素(磅) ,而纵坐标依次递增 100 像素(磅) ,所以,显示效果呈现纵向排 列。如图 2.43③、2.43④、2.43⑤所示。 为了测试 MediaTrackerApplet.java 程序中的异常捕获功能是否正常工作,我们将 images  子目录下的 background.gif,以及 logo2.jpg 分别重新改名为 bf.gif 和 logo2bak.jpg。重新启动  IE,再调用 MediaTrackerApplet.html 文档,我们可以看到,程序中的异常捕获功能已经能够 正常工作了。因为,每个图像对象都定义了惟一的标识,所以在输出结果上,可以看到标志 为“0” “2” 和 的图像对象出现了错误, 而这两个图像文件正是我们刚刚更名前的 background.gif  以及 logo2.jpg(标志分别为 0 和 2,见源程序第 20、25 行) 。出现异常后的程序输出结果,
    • 如图 2.44 所示。 图 2.44  异常的捕获  2.11.3  使用双缓冲技术绘制图像 读者可能已经发现, 在运行前面第 2.8.4 节所讲述的绘制圆柱体的源程序, 以及运行 2.10.5  节所介绍的绘制平行四边形及立方体源程序的时候, 当我们将 IE 从当前窗口转变成非当前窗 口状态,再从非当前窗口恢复到当前窗口状态,有时,某些绘制好的图像会消失,除非我们 重新刷新 IE 窗口,显示才会恢复正常。此外,当我们移动 IE 窗口或者其他的窗口在 IE 上移 动的时候,图像会有些闪烁。但运行 2.11.1 节的加载并绘制图像文件的源程序,却没有这种 现象。这是怎么一回事呢?这就要涉及到 Java  Applet 中的 paint 方法的绘图机制了。产生这 种现象的主要原因是: Ø 由于在显示所绘制的图像时,调用了  repaint  方法。repaint  方法被调用时,需要清除 整个背景,然后才调用 paint 方法显示画面。这样,在清除背景和绘制图像的短暂时 间间隔内被用户看见的就是闪烁。 Ø 由于 paint()方法需要进行复杂的计算,图像中包含着多个图形,不同图形的复杂程度 及其所需要的绘制时间不同,因此,图像中的各个像素值不能同时产生,使得图形的 生成频率低于显示器的刷新频率,从而造成闪烁。 提示:运行 Java 编写的动画程序时,发生不连贯或闪烁现象时,可参考下文所介绍的方 法加以改善。 下面两种方法可以明显地消除或减弱闪烁: Ø 重载 update 方法 当 AWT 接收到 Applet 重新绘制的请求时,调用 Applet 的 update 方法。 默认情况下,  update  方法清除 Applet 的背景,然后调用 paint 方法。重载 update 方法,就可以将以前在 paint 方法 中的绘图代码包含在 update 方法中,从而避免每次重新绘制时将整个区域清除。 Ø 双缓冲技术 双缓冲技术在很多动画 Applet 中被采用。主要原理是创建一幅后台图像,将每一帧画入 图像,然后调用 drawImage 方法,将整个后台图像一次画到屏幕上去。这种方法的优点在于 大部分绘制是在后台进行的。将后台绘制的图像一次绘制到屏幕上。在创建后台图像前,首 先通过调用  createImage  方法生成合适的后台缓冲区,然后获得在缓冲区的绘图环境(即  Graphics 类对象)。 综上所述,改善前面我们写的一些 Java  Applet 源程序的思路是:不直接在 paint 方法中 调用各种绘制方法,而是采用重载 update 方法及双缓冲技术,生成一个图像的缓冲区,获得
    • 该缓冲区中的绘图环境后,将该绘图环境读入内存。paint 方法不再负责图像的绘制工作,即  paint 方法不再装入任何的图像绘制代码。我们在 paint 方法中,直接调用 update 方法,在内 存缓冲区的绘图环境下进行图像的绘制工作,当所有的图像绘制工作完成后,最后将缓冲区 的内容一次性地写入 Applet 并在 Applet 窗口中直接显示出来。 这种方法很巧妙地解决了图像 丢失和闪烁的问题。 现在我们就遵循上面的思路,重新改写第 2.8.4 和第 2.10.5 节所介绍的源程序。 首 先 看 改 写 的  2.8.4  节 介 绍 的 绘 制 圆 柱 体 的 源 程 序  DrawBufferedCylinder.java  (  chap02Fig2.11Fig2.11_03)。  DrawBufferedCylinder.java 与第 2.8.4 节所介绍的源程序 DrawCylinderApplet.java 在绘制 圆柱体时的不同之处在于,前者是先绘制在内存缓冲区中的,然后显示在 Applet 窗口中,而 后者是直接在 Applet 上进行绘制。 程序第 16 行~第 17 行: Image offImage;  Graphics offGraphics; 声明了一个名为 offImage 的 Image 对象,表示缓冲区的 Image 对象。然后声明了一个名 为 offGraphics 的 Graphics 对象,offGraphics 是标识缓冲区的图像绘制环境。这两个系统成 员变量在这里仅仅是声明,并没有初始化,我们将在 init 方法中对其进行初始化。 程序第 19 行: int appltWidth = 370, appletHeight = 420; 增加了两个类的数据成员变量 appletWidth 和 appletHeight, 分别表示本 Applet 的宽度和高 度。appletWidth  和  appletHeight  的值,与调用本  Applet  的  HTML  文档  DrawBuffered  Cylinder.html 中的 Applet 属性中的 Width 和 Height 相同。 程序首先运行第 22 行~第 26 行的 init 方法: public void init()  {  offImage = createImage(appltWidth, appletHeight);  offGraphics = offImage.getGraphics();  } 使用 Component 类的 createImage 方法,创建了一个 Image 类的实例,并将该实例对象 赋给 offImage。Component 类的 createImage 方法,返回的是一个后台的(off­screen),用于 双缓冲的图像对象。它接收两个整型参数,分别指定该图像对象的宽度和高度,如果该  Component 是不可显示的部件,则返回的结果为 null。 接着,用 Image 类的 getGraphics 方法,创建了一个名为 offGraphics 的 Graphics 对象, 并获得图形环境。后台的(off­screen)缓冲图像将由它来产生,因为这里画的是内存缓冲区 图像,所以 Applet 窗口上不会有显示。 然后, 程序执行第 68 行的 paint 方法, 我们强制其执行程序第 73 行~第 91 行的  update  方法。 程序第 75 行~第 77 行: offGraphics.setColor(Color.BLACK);  offGraphics.setFont(new Font("汉真广标", Font.PLAIN, 35));  offGraphics.drawString("圆柱体画法", 10, 40); 前面例程都是调用 paint 方法中的参数 Graphics 对象 g 的相关绘制方法,对文本和其他 图形进行绘制操作。上面的代码是在缓冲区图像的图形环境中绘制,所以,我们不再调用  g  的相关方法,而是调用  offGraphics  的相关方法进行绘图作业。因为  offGraphics  是缓冲区图
    • 像的图形环境对象, 所以每当 Applet 调用 offGraphics 方法时,绘制工作都将在内存中的缓冲 区中进行。这几行代码设置了当前的绘图颜色、字体并绘制一条文本“圆柱体画法” 。 程序第 80 行: drawCylinder(offGraphics); 调用 drawCylinder 方法,并将 offGraphics 作为参数传递给该方法。因此,程序第 46 行~ 第 66 行: public void drawCylinder(Graphics g)  {  // 计算椭圆的坐标 calculateOvalCoordinate();  // 绘制下方椭圆 g.setColor(fillColor);  g.fillOval(ovalX2, ovalY2, ovalWidth, ovalHeight);  g.setColor(outlineColor);  g.drawOval(ovalX2, ovalY2, ovalWidth, ovalHeight);  // 绘制中间的矩形 g.setColor(fillColor);  g.fillRect(rectX, rectY, rectWidth, rectHeight);  // 绘制上方的椭圆 g.setColor(g.getColor().darker());  g.fillOval(ovalX1, ovalY1, ovalWidth, ovalHeight);  g.setColor(outlineColor);  g.drawOval(ovalX1, ovalY1, ovalWidth, ovalHeight);  } 因为,传递的参数是缓冲区的图形绘制环境对象 offGraphics,所以,drawCylinder 方法 同样是在 offGraphics 上进行绘制。这里需要注意的是程序第 90 行,update 方法中的最后一 条语句: g.drawImage(offImage, 0, 0, null); 在调用 drawImage 方法时把 null 作为第 4 个参数,这样可以防止 drawImage 调用 update  方法。因为图像的所有内容都已装入内存,所以,图像在 Applet 窗口的显示就一气呵成了。 现在我们检测程序的运行结果。 将 IE 从当前窗口转变成非当前窗口状态, 再从非当前窗 口恢复到当前窗口状态,我们会发现,绘制好的图像不再消失了,也不需要重新刷新 IE 窗口 了。 重新改写的第  2.10.5  节所介绍的绘制立方体的  DrawBufferedParallelogram.java  (chap02  Fig2.11Fig2.11_04)的源程序我们也不在赘述,请读者自行查阅并运行该程序。
    • 2.12  本章小结 本章主要介绍了色彩的基本知识,Java Applet 运行机制及如何编写、运行 Java Applet 程 序,详细论述了各种基本几何图形在 Java Applet 下的编程工作。 详尽阐述了 Java.awt.Graphics 类的各种方法,包括绘制直线、文本(字符串) 、矩形、椭 圆、圆、圆弧、多边形和折线。在绘制基本的几何图形的基础上,我们以绘制圆柱体和立方 体为例,向读者展示了如何通过绘制多个多边形并将其组合成一个复杂的几何图形的方法。 用多个多边形实现  3D  效果,以及组合各种不同基本几何图形和多边形就能够生成复杂的图 形。 讲述了  Java  Applet  的图形加载及显示的几种方法,并分析了各自的优点。因为  Applet  的局限性,在图像绘制完成的情况下,可能会出现图像丢失或者闪烁的情况,为解决这个问 题,我们提出了如何利用双缓冲技术来改善绘制性能,并采用双缓冲技术重新改写了几个以 前的绘制程序。 在掌握了 Java Applet 绘制各种基本的几何图形及多边形的方法后, 接下来我们将学习使 用 Java Applet 生成 Web 动态图表的几个简单实例。 第 3 章  Java Applet 图表绘制实例 本章将阐述利用 Java  Applet 来绘制 Web 动态图表的一些简单实例。目前,企业中基于  B/S 体系结构的应用程序越来越多,对 B/S 应用程序的要求也越来越高。WWW 的发展使得 基于 B/S 的应用程序不再局限于静态或者简单的动态内容。在工作中,我们经常将所需要的 数据用图表的方法在 Web 页面上表示出来,例如:柱形(直方)图、饼形图、线段图、趋势 图、甘特图等,并且这些图表能够根据数据的改变而自动更新。  Java Applet 是可用于开发的一种轻量级、较简单、基于客户端的 Web 动态图表。  3.1  Java Applet 生成 Web 动态图表 本节介绍几个利用 Java  Applet 来绘制 Web 动态图表的简单实例。参与图表运算的数据 在实际工作中的来源是多种多样的。有通过 HTML 表单提交的,有通过 Applet 传递参数的, 但绝大多数是通过查询或访问数据库而得到的。因为本书不涉及  Java  和数据库编程的相关知 识,所以本书中凡是涉及到 Web 图表运算的实时数据都通过随机数产生。 为让读者能尽快地掌握 Web 动态图表的编程思路和方法,我们在讲述本章的内容时,将 每节的例程都用一个单独的 Java 源程序来编写(Fig.3.02_02:CheckMaker.java 除外) 。我们将 在学习过程中,逐步过渡到用面向对象的编程思路来设计 Web 动态图表。
    • 首先让我们看看 Web 图表的外观结构,如图 3.1 所示。  Web 图表结构可以简单地分成两大区域: Ø 标题区:主要负责绘制图表的标题,表示本图表的名称,一般位于整个图表的上方。 除此之外,还可以将其设置在整个图表的下方、左方及右方。 Ø 图表区:负责绘制具体的图形。如图 3.1 中的折线图及三维饼图,图表区中有时还包 括图例。 图例就是图中所示的三维饼图的右边部分。 图例用来说明图表中各个部分所表示的意义。 在图表区中,某些图表需要坐标系作为参考。如图 3.1 中的折线图,而某些形状的图表 则不需要坐标系,如图  3.1  中所示的三维饼图。同样地,我们在图表区域中,可以设置图例 来说明图表中各部分所代表的含义,如三维饼图所示;也可以不用图表,直接在坐标系中说 明图表中各个部分的含义,如折线图中的坐标系所示。
    • 标题区 图表绘制区 标题区 图表绘制区 图 3.1  图表的布局结构 提示:为使 Web 动态图表的标题更具美观和多样性, 读者可以在本地计算机中安装几套 字库。本书中所使用的字库除 Windows 系统自带的以外,还使用了额外安装的方正字库、经 典字库等。  3.1.1  垂直柱状图 垂直柱状图是使用最频繁的一种 Web 图表。我们就用它作为第一个 Web 动态图表的例 程。现在我们希望用垂直柱状图来表示某书店 5 种计算机编程类书籍在某天销售量的统计情 况。各书籍的销售量从一个  1~100  之间的随机数中产生。如图  3.2  所示,程序  VBar.java  (chap03Fig3.1Fig.3.1_01)的运行结果。 图 3.2 实际上是由三部分组成: (1)图表标题:也就是文本“Java Web 图表设计 Applet 版——柱状图”。 (2)图表区又由两部分组成: Ø 坐标系:横坐标表示编程书籍,纵坐标表示销售数量。 Ø 表示不同书籍销售量的柱状图。 每次我们重新刷新页面时,VBar.java 程序的运行都会重新生成新的、随机的书籍销售数 据。当销售数据发生变化后,图 3.2 中所示了表示各书籍销售量的柱状图也相应地发生变化。
    • 图 3.2  VBar.java 的运行结果(垂直柱状图) 垂直柱状图的设计思路: (1)首先获得每种书籍的销售量。 (2)确定标题区域的范围。 (3)确定图表绘制区域的范围。 (4)绘制标题。 (5)在图表绘制区域内,绘制以下图形对象: Ø 表示书籍销售量的纵坐标。 Ø 表示书籍名称的横坐标。 Ø 用绘制实心矩形来代表垂直的条形,确定相邻矩形的间隔宽度。 Ø 根据每本书的销售量依照一定的比例绘制不同高度的实心矩形。 本章所有代码采用前一章所介绍过的双缓冲技术。程序 VBar.java 第 8 行~第 9 行: Image offImage;  Graphics offGraphics; 声明用于绘制缓冲区图形 Image 对象 offImage 及其在缓冲区的绘图环境 offGraphics。 程序第 12 行: int appltWidth = 600, appletHeight = 500; 声明了两个整型变量 appletWidth 及 appletHeight 并对其初始化,这两个变量分别表示本  Applet 的宽度和高度,它们的值和调用该 Applet 的 HTML 文档中 Applet 的参数相同。注: 如果没有特别说明, 本章中所有的 Java 源程序中与 Applet 相关的宽度和高度的取值与调用该  Applet 的 HTML 文档相同。 程序第 14 行: int bookSales[] = new int[5]; 声明了一个整型数组 bookSales,表示书籍的销售量,我们将在 Applet 中的 init 方法里对 其进行初始化。 程序第 16 行~第 19 行: String bookTitle[] =  {  "Python", "JAVA", "C#", "Perl", "PHP"
    • }; 声明并初始化了一个字符串数组 bookTitle,表示被统计的计算机书籍的书名。这里我们 用了 Python、JAVA、C#、Perl,以及 PHP 这五个字符串来表示这些书名。 程序第 22 行~第 25 行: Color color[] =  {  new Color(99, 99, 0), Color.GREEN, Color.YELLOW, Color.RED, Color.BLUE  }; 声明了包含有五个 Color 对象的 Color 数组,数组中的每种颜色用于表示不同的书籍。 其中 new Color(99, 99, 0)的颜色效果为褐色。 到这里,VBar 类的成员变量就全部声明完毕。那么如何在宽为 600 像素,高为 500 像素 的绘图环境中分配标题区域和图表绘制区域?如何在图表绘制区域中再划分柱状图的绘制区 域?如图 3.3 所示。 标题区域 图表绘制区域 (80,40) (80,65) (480,65) 实心矩形图绘制区 (80,445) 坐标系原点 (480,445) (550,445) 图 3.3  垂直柱状图表的设计思路 我们的绘制思路如下: (1)标题区域占据整个 Applet 最上面高度为 30 像素,宽度为 600 像素的矩形区域。也 就是图 3.3 中最上面的绿色区域。 (2)剩下的区域都是图表绘制区域。除标题外,其余所有图表对象都绘制在此区域内。 除去标题区域,图表绘制区域实际上的宽度为 600 像素,高度为 470 像素。本例中,此区域 内的图形绘制对象又由两部分组成: Ø 坐标系。纵坐标表示书籍的销售量,因为每本书的销售量是从一个  1~100 之间的随 机数中产生的,也就是说,每种书的销售量最大值为 100,如果直接用像素来表示, 仅仅需要 100 像素就够了。现在我们还剩下 470 像素,如果仅用 100 像素,不仅不美 观,而且也浪费了大量的空间。所以我们在垂直方向上用  380  像素来表示销售量为  100  的这个数据,简而言之,也就是每一个单位的销售量就表示纵坐标上  38  像素。 横坐标表示书籍的名称,本例中共有五种不同的书籍。这里我们用 400 像素的宽度来 表示这些书籍,也就是说,实心矩形的绘制区域在宽度为 400 像素,高度为 380 像素 的矩形区域内,而我们最终确定的坐标系原点位于点(80,445) 。两条坐标轴,从美 观和实际的运行效果来看,应该分别比实心矩形绘制区域的宽度和高度的值更大一 些。从图  3.3 我们可以看到,纵轴的最高点比矩形绘制区域的高度大  20  像素,坐标 为 (80,  ; 40) 横轴的端点比实心矩形绘制区域的宽度更多出 70 像素, 坐标位于(550,  445) 。
    • Ø 上述坐标系确定后,就可以确定实心矩形的绘制坐标了。我们先确定所有代表书籍销 售量的实心矩形的底端和横轴相重合,然后就可以推算出除实心矩形的高度后,就可 以推算出实心矩形的左上角顶点的坐标。另外,我们设定所有的实心矩形的宽度为  50 像素。 Ø 绘制的顺序是,先绘制标题区域,然后是绘制代表书籍销售量的  10  条直线,以及代 表各书籍实际销售量的实心矩形,最后是绘制坐标系及其说明文字。 在理清上述 Web 图表的绘制思路后,我们接着讨论源程序的 init 方法: 程序第 28 行~第 38 行: public void init()  {  offImage = createImage(appltWidth, appletHeight);  offGraphics = offImage.getGraphics();  for (int i = 0; i < bookSales.length; i++)  {  bookSales[i] = 1+(int)(Math.random() * 100);  }  } 首先还是创建缓冲区 Image 对象 offImage 及其绘图环境对象 offGraphics,然后初始化整 型数组 bookSales。注意语句: 1+(int)(Math.random() * 100); 正好生成一个 1~100 之间的整数。通过一个循环,将 bookSales 中的每个元素都进行赋 值。接着会运行 paint 方法,然后在 paint 方法中调用第 48 行~第 96 行的 update 方法。 update  在 方法里面的第 51 行~第 53 行: offGraphics.setColor(Color.black);  offGraphics.setFont(new Font("方正粗宋简体", Font.BOLD, 30));  offGraphics.drawstring("Java Web 图表设计 Applet 版——柱状图", 15, 30); 就实现了绘制标题区域的功能。用黑色,粗体,30  磅的“方正粗宋简体”这种字体在  Applet 坐标的点(15,30)处绘制了文本“Java Web 图表设计 Applet 版——柱状图”。 完成标题区域的绘制后,现在进入图表绘制区域。 程序第 56 行: offGraphics.setFont(new Font("SansSerif", Font.PLAIN, 12)); 重新设置当前的绘制字体。 前面说过,实心矩形绘制区域的高度为 380 像素表示 100 个单位的销售量,也就是说, 每 38 像素就相当于 10 个单位的销售量。为了更清晰地表达销售量的显示效果,我们在实心 矩形的绘制区域内再绘制 10 条直线,分别表示 10、20、30、…、90、100 个单位的销售量, 并且在每条直线旁标明该条直线所代表的销售量的数据。 程序第 57 行~第 67 行,实现了上述功能: int salesValue = 0;  for (int i = 418; i > 0; i ­= 38)  {  offGraphics.setColor(Color.black);  offGraphics.drawstring("" + salesValue, 36, (i + 27));  offGraphics.setColor(Color.lightGray);  offGraphics.drawLine(80, (i + 27), 520, (i + 27));  salesValue += 10;
    • } 先声明了一个局部的整型变量 salesValue,并将其值初始化为 0。因为坐标系的原点位置 在点(80,445)处,也就是说,销售量为 0 的直线的纵坐标值都为 445,而销售量为 10 个 单位的直线纵坐标值都为 445-38=407。先从最下面一条直线开始绘制,第 1 次循环,循环变 量 i 的值为 418,注意第 61 行,我们的用法: offGraphics.drawstring("" + salesValue, 36, (i + 27));  drawString 方法接收的第 1 个参数是字符串型,所以我们用: "" + salesValue 这种方式,将一个内容为空的字符串 (注:不是空字符串 null)和其后面的整数 salesValues  连接起来后,整个结果就被 Java 的编译器自动转变成一个字符串。这种用法非常简单,推荐 大家使用。 也就是说,第 1 次循环,程序第 61 行和第 64 行,实际执行的内容是: offGraphics.drawstring("0", 36, 445);  offGraphics.drawLine(80, 445, 520, 445); 之后,salesValue 的值递增 10。 同理,第 2 次绘制,程序第 61 行和第 64 行,实际执行的内容是: offGraphics.drawstring("10", 36, 407);  offGraphics.drawLine(80, 407, 520, 407); 其余步骤,此处不再赘述。程序执行完第 67 行后,绘制完成的图像如图 3.4①所示。 图 3.4  垂直柱状图表的绘制过程 接着绘制表示各种计算机书籍销售量的实心矩形。 程序第 70 行~第 81 行: int drawHigh = 0;  for (int i = 0; i < bookTitle.length; i++)  {  offGraphics.setColor(color[i]);  drawHigh = (int)(Math.ceil(bookSales[i] * 3.8));
    • offGraphics.fill3DRect(110+i * 80, 445­drawHigh, 50, drawHigh, true);  offGraphics.setColor(Color.BLACK);  offGraphics.drawstring(bookTitle[i], 110+i * 80, 465);  } 首先声明了一个局部变量 drawHigh,该变量表示销售量所对应的矩形的绘制高度。接着 我们用一个循环,计算出每种计算机书籍的销售量所对应的绘制高度。然后程序在第 77 行, 绘制实心矩形: offGraphics.fill3Drect(110+i * 80, 445­drawHigh, 50, drawHigh, true); 实心矩形的左上角顶点的绘制参数由下列方法计算: 横坐标:110+i * 80  纵坐标:445–drawHigh  也就是说,第一次循环绘制第 1 个实心矩形左上角的横坐标为 110。而每一个循环后, 实心矩形的横坐标依次增大 80 像素。矩形的纵坐标由其绘制高度通过“445–  drawHigh”得 到,矩形的宽度为  50  像素(即两个相邻之间的实心矩形的间隔距离是  30  像素) ,高度为  drawHigh 像素。这样就保证了所有的实心矩形的下端都和纵坐标为 445 的直线相重合,而矩 形的填充颜色由 Color 对象的数组 Color 决定。 然后重新设置当前绘图颜色为黑色,再通过调用 drawString 方法绘制出该实心矩形所代 表计算机书籍的名称,程序第 80 行: offGraphics.drawString(bookTitle[i], 110+i * 80, 465); 注意,这里的纵坐标都是  465,就保证了所有的说明文字都在同一水平线上(纵坐标为  ,而横坐标正好和矩形左上角顶点的横坐标相同,这样实心矩形和其说明文字也 465 的直线) 就排列整齐了。 程序循环的次数由表示计算机书籍名称的字符串数组 bookTitle 决定。 本段循环结束后,程序绘制的结果如图 3.4②所示。 接着进入坐标系的横轴和纵轴的绘制过程。程序第 84 行~第 86 行: offGraphics.setColor(Color.BLACK);  offGraphics.drawLine(80, 40, 80, 445);  offGraphics.drawLine(80, 445, 550, 445); 首先设置当前的绘制颜色为黑色。横轴和纵轴通过绘制两条直线得到。我们从横轴和纵 轴的绘制参数可以得知:两条直线交点的坐标为(80,445) 。 程序运行完第 86 行后的结果如 3.4③所示。 然后,绘制坐标系中横轴和纵轴的文字说明,程序第 89 行~第 91 行完成该功能: offGraphics.setFont(new Font("黑体", Font.BOLD, 16));  offGraphics.drawString("销售量", 20, 50);  offGraphics.drawString("编程书籍", 500, 465); 用黑体,大小为 16 磅的字体在点(20,50)处绘制文本“销售量” (纵轴的文字说明) , 然后在点(500, 465)处绘制文本“编程书籍” (横轴的文字说明)。 程序第 94 行,完成最后的工作: g.drawImage(offImage, 0, 0, null); 将缓冲区的图像内容全部输出到 Applet 中,并结束 updatefh 方法。至此,垂直柱形图的 所有绘制工作就完成了。程序的实际运行效果如图 3.2 所示。
    • 3.1.2  饼图 饼图也是一种应用非常广泛的图表类型。常用于表示数据之间的对比关系。我们现在用 饼图来表示某书店五种计算机编程类书籍某天的销售量和当天所有书籍销售总量的对比关 系。各书籍的销售量仍旧从一个 1~100 之间的随机数中产生。 程序  Pie.java  演示了如何绘制饼图。同样地,我们先看图  3.5  所示的  Pie.java(chap03  Fig3.1Fig.3.1_02)的运行结果,然后分析该程序的运行流程。 图 3.5    Pie.java 的运行结果 图 3.5 实际上也是由三部分组成的: (1)图表标题:也就是文本“Java Web 图表设计 Applet 版——饼图”。 (2)图表区又由两部分组成: Ø 图 3.5 右边的图例。 Ø 图 3.5 左边部分表示各种计算机书籍销售量及其与销售总量之间比例关系的饼图。饼 图的效果实际上是通过绘制一组颜色各异的实心圆弧而得到的。 每次我们重新刷新页面的时候,  Pie.java 程序的运行都会重新生成新的、 随机的书籍销售 数据。当销售数据发生变化后,图 3.5 中的饼图也会相应地发生变化。 饼图的设计思路: (1)首先获得所有书籍的销售量。 (2)计算所有书籍的销售总量及每本书的销售量占所有书籍销售总量的比例。 (3)确定标题区域的范围。 (4)确定图表绘制区域的范围。 (5)绘制标题。 (6)在图表绘制区域内,再绘制以下图形对象: Ø 图例:图例中又包含 4 个元素,即表示某种图书的实心矩形小方块,书籍名称、书籍 销售量、该销售量占销售总量的比例。 Ø 绘制表示销售量的实心圆弧。实心圆弧的大小(弧度)由其销售量占销售总量的比例 决定。 程序第 6 行,引进了一个新的包: import java.text.*; // 引入 java.text 包中所有的类  java.text  包允许通过与特定语言无关的方式格式化文本消息、日期和数值。本例程将引
    • 用该包中的 DecimalFormat 类,用于格式化浮点数的格式化输出。 与前例相同,程序第 10 行~第 11 行: Image offImage;  Graphics offGraphics; 声明了用于绘制缓冲区图形对象的  Image  对象  offImage  及其在缓冲区的绘图环境  offGraphics。 程序第 13 行: int appltWidth = 600, appletHeight = 500; 声明了两个整型变量 appletWidth 和 appletHeight 并对其初始化,这两个变量分别表示本  applet 的宽度和高度。 程序第 15 行: int bookSales[] = new int[5]; 声明了一个整型数组 bookSales,表示书籍的销售量,我们将在 Applet 中的 init 方法中对 其进行初始化。 程序第 17 行: int totalSales = 0; 声明了一个整型变量 totalSales,表示书籍的销售总量,即各种书籍销售量之合计。默认  totalSales 的值为 0。我们将在 Applet 中的 init 方法中对其进行再次赋值的操作。 程序第 19 行~第 22 行,声明并初始化了一个字符串数组 bookTitle,表示被统计的计算 机书籍的书名。这里我们同样用了 Python、JAVA、C#、Perl,以及 PHP 这 5 个字符串来表示 这些书名。 程序第 25 行~第 28 行,声明了包含有 5 个 Color 对象的 Color 数组,数组中的每种颜 色用于表示不同的书籍。其中 new Color(99, 99, 0)的颜色效果为黄褐色。 到这里,Pie 类的成员变量就全部声明完毕。那么如何在宽为 600 像素,高为 500 像素 的绘图环境中分配标题区域和图表绘制区域?如何在图表绘制区域中划分饼图的绘制区域以 及图例的绘制区域?如图 3.6 所示。 标题区域 图表绘制区域  350  190  (395, 95) (15, 95)  编程类图标 销售数量 所占比例  Python  50      22.94%  JAVA  38      17.43%  300  C#  52      23.85%  Perl  33      15.14%  PHP  45      20.64% 实心圆弧图绘制区 图例绘制区 图 3.6  饼图的设计思路 本例中饼图的绘制思路如下: (1)标题区域占据整个 Applet 最上面高度为 30 像素,宽度为 600 像素的矩形区域。也 就是图 3.3 中最上面的绿色区域。 
    • (2)剩下的区域都是图表绘制区域。除标题外,其余的所有图表对象都绘制在此区域 内。除去标题区域,图表绘制区域的实际宽度为 600 像素,高度为 470 像素。本例中,此区 域内的图形绘制对象又由两部分组成: Ø 左侧的实心圆弧绘制区。一组绘制在一起的实心圆弧就组成了一个饼图的样式。该区 域中,除去蓝色的实心圆弧外,所有圆弧的外切矩形的左上角的顶点坐标为(15, 95) 。 为了美观地显示效果,我们设定其中一个实心圆弧(即蓝色圆弧)的外切矩形的左上 角的顶点坐标为(30,  93),也就是向右水平移动了 15 像素,向上垂直移动了两个像 素,这样的显示效果比较美观。另外,所有实心圆弧的外切矩形的宽度都是 350 像素, 高度为 300 像素。 Ø 确定了圆弧的外切矩形的宽度、高度,以及左上角的顶点坐标后,就可以绘制实心圆 弧了。我们将绘制的第 1 个圆弧的起点角度在 30 度,而圆弧的圆心角的大小由其销 售量决定。 Ø 右侧的图例绘制区。图例被确定在一个蓝色的空心矩形内,该矩形左上角顶点 坐标为(395,95) 左侧的实心圆弧的外切矩形的左上角的顶点坐标相同,处于 ,和 同一高度。其宽度为 190 像素,高度为 300 像素。在图例绘制区内,再依次采用相对 应的颜色绘制 1 个实心的小矩形,以及与其对应的书籍名称、销售数量及其所占销售 总量的比例。 Ø 绘制的顺序是,先绘制标题区域,然后绘制仅有一个空心矩形的不含任何文字说明的 图例,之后是绘制实心圆弧及其相应的图例。 明白饼图的绘制思路后,我们接着讨论 Pie.java 源程序中的 init 方法: 程序第 31 行~第 42 行: public void init()  {  offImage = createImage(appletWidth, appletHeight);  offGraphics = offImage.getGraphics();  for (int i = 0; i < bookSales.length; i++)  {  bookSales[i] = 1+(int)(Math.random() * 100);  totalSales += bookSales[i];  }  } 首先还是创建缓冲区 Image 对象 offImage 及其绘图环境对象 offGraphics。然后初始化整 型数组 bookSales。与前例不同的是,这里我们在对数组 bookSales 赋值的时候,同时也进行 销售总量的计算: totalSales += bookSales[i]; 每循环一次,就将新的、随机生成的数据累加到变量 totalSales 中。循环完毕,则得到了 销售总量的数值。 接着运行 paint 方法,然后在 paint 方法中调用程序第 52 行~第 106 行的 update 方法。 在 update 方法里面,我们首先绘制 Applet 的背景,程序第 55 行~第 56 行: offGraphics.setColor(new Color(144, 255, 255));  offGraphics.fillRect(0, 0, appletWidth, appletHeight); 用蓝色(new  Color(144,  255,  255))填充了整个 Applet 背景。填充背景的功能实际上是 通过填充一个同我们设定的 Applet 宽度和高度都相同的实心矩形而实现的。 程序第 59 行~第 61 行:
    • offGraphics.setColor(Color.BLACK);  offGraphics.setFont(new Font("方正隶变简体", Font.BOLD, 30));  offGraphics.drawString("Java Web 图表设计 Applet 版——饼图", 15, 30); 这里就实现了绘制标题区域的功能。用黑色、粗体、30 磅的“方正隶变简体”这种字体 在 Applet 坐标点(15,30)处绘制了文本“Java Web 图表设计 Applet 版——饼图” 。 完成标题区域的绘制后,现在进入图表绘制区域。 程序第 64 行: offGraphics.drawString("计算机编程类图书销售统计表", 70, 465); 在  Applet  的底部绘制一条文本“计算机编程类图书销售统计表” 。采用的字体和前面的 相同。 程序第 65 行~第 71 行: offGraphics.setFont(new Font("SansSerif", Font.PLAIN, 12));  offGraphics.drawString("编程类图书", 400, 125);  offGraphics.drawString("销售数量", 475, 125);  offGraphics.drawString("所占比例", 535, 125);  offGraphics.setColor(Color.blue);  offGraphics.drawRect(395, 95, 190, 300); 重新设置字体后,绘制了 3 条文本:“编程类图书”“销售数量”及“所占比例” 、 。它们 的纵坐标都为 125,同在一条水平线上。然后绘制一个空心的蓝色矩形,其绘制参数如图 3.6  所示,左上角顶点(395,95) ,宽度和高度分别为 190 像素和 300 像素。 程序运行完第 71 行后,绘制结果如图 3.7 所示。 图 3.7    Pie.java 的运行流程 程序第 74 行: int arcStartAngle = 30, arcAngle = 0; 声明了两个整型变量 arcStartAngle 和 arcAngle。  arcStartAngle 表示绘制圆弧的起始角度, 默认的起始角度是 30 度。arcAngle 表示圆弧两条边线之间的夹角(圆心角) ,默认值为 0。 程序第 75 行:
    • float proportion; 声明了  1  个浮点变量:proportion,该变量表示某种图书销售量与所有图书销售总量之 间的比例关系。 程序第 76 行: DecimalFormat twoDigits = new DecimalFormat ("0.00"); 声明了 1 个 DecimalFormat 类的对象 twoDigits。DecimalFormat 类隶属于 java.text 包。我 们前面讲过,引用该包中的 DecimalFormat 类,用于格式化浮点数的格式化输出。在本例中, 我们决定输出的变量 proportion 值的小数点右边带两位小数(即精确到 0.01)。该语句创建 一个格式为“0.00”进行初始化的 DecimalFormat 对象,其中每个“0”分别指定格式化浮点 数中必不可少的数位。这种特定的格式表明每个用 twoDigits 格式化后的数字,其小数点坐 标至少有 1 位,而小数点右边至少保留两位。 如果被格式化的数字与上面的格式要求不匹配, 则“0”将被自动插入到该数相应的数位上。 然后程序就进入了实心圆弧及其图例说明文字的绘制过程,程序第  78  行~第  101 行实 现了这个过程: for (int i = 0; i < bookTitle.length; i++)  {  arcAngle = (int)(bookSales[i] * 360 / (float)totalSales + 0.5);  proportion = ((float)bookSales[i]) / totalSales * 100;  offGraphics.setColor(color[i]);  if (i < bookTitle.length ­ 1)  {  offGraphics.fillArc(15, 95, 350, 300, arcStartAngle, arcAngle);  }  else  {  offGraphics.fillArc(30, 93, 350, 300, arcStartAngle, arcAngle);  }  arcStartAngle += arcAngle;  offGraphics.fillRect(400, 155+i * 50, 12, 12);  offGraphics.setColor(Color.black);  offGraphics.drawString(bookTitle[i], 420, 167+i * 50);  offGraphics.drawString("" + bookSales[i], 490, 167+i * 50);  offGraphics.drawString("" + twoDigits.format(proportion) + "%", 540,  167+i * 50);  } 在循环体中,程序第  80  行首先计算圆弧的圆心角的度数。注意,我们又用了在浮点数 后面再加上 0.5,然后再取整的操作,达到了小数点后一位数字四舍五入的功能。圆心角的值 等于该种图书的销售量除以销售总量再乘以 360。因为 Java 的除法操作符“/”默认是整除。 因为我们声明的整型数组变量 bookSales 中的元素,以及变量 totalSales 都是整数。所以,在 整型变量 totalSales 前面用了一个关键字“float”就将整型变量 totalSales 的值转变为浮点类 型的数值了。当执行除法操作时,结果就不再是整数,而自动转变为 folat 类型的浮点数了。 然后把得到的浮点类型的结果加上 0.5 再取整后, 实现了小数点后一位数字四舍五入的功能, 最后再把取整后的结果赋值给变量 arcAngle,这样我们就得到了当前圆弧的圆心角的度数。 程序第 81 行, 计算当前销售量占销售总量的百分比。 同样的, 我们强制转换 bookSales  [i]为浮点型。所以当所得的商再乘以 100 后,结果仍是浮点型,然后结果被赋值给 float 变量  proportion。 程序第 83 行,设定当前的绘制颜色。接下来,绘制实心圆弧:
    • if (i < bookTitle.length ­ 1)  {  offGraphics.fillArc(15, 95, 350, 300, arcStartAngle, arcAngle);  }  else  {  offGraphics.fillArc(30, 93, 350, 300, arcStartAngle, arcAngle);  } 上述代码说明,如果绘制的不是最后一个实心圆弧(图  3.5  和图 3.6  中所示的蓝色实心 圆弧) ,则一直执行程序第 87 行。该圆弧的外切矩形的参数都是一样的,即图 3.6 中所示的 左上角顶点坐标为(15,95) ,宽度为 350 像素,高度为 300 像素。第 1 次循环, arcStartAngle  的值为 30,也就是说,第 1 个圆弧的起始位置在 30 度,圆弧的圆心角的大小已经在程序第  80 行计算出了。这时,根据这些参数,就可以绘制出第 1 个实心圆弧。 如果这时局部变量 i 的值等于 bookTitle.length-1,则说明绘制的是最后一个实心圆弧, 执行程序第 91 行。就是圆弧的外切矩形的左上角顶点坐标变更为(30,93) ,宽度和高度保 持不变,分别为 350 像素和 300 像素。 程序第 94 行: arcStartAngle += arcAngle; 重新计算绘制下一个圆弧的起始角度。本条语句实现的功能是,绘制下一个圆弧的时候, 其起始角度正好和当前圆弧的结束位置相同。 绘制完实心圆弧后,接着绘制图例部分。程序第  96  行,先用当前颜色绘制一个宽度和 高度都为 12 像素的实心矩形,然后程序第 97 行,改变当前的绘制颜色为黑色。 程序第 98 行,绘制当前书籍的名称。程序第 99 行,绘制当前书籍的销售量。 程序第 100 行: offGraphics.drawString(twoDigits.format(proportion) + "%", 540, 167+i *  50); 先格式化浮点变量 proportion 的输出格式。注意,这里格式化字符串所使用的方法: twoDigits.format(proportion) 调用 DecimalFormat 类的 format 方法,来格式化所传递参数的输入格式,然后在返回的 结果后再加上一个“%”符号,然后再绘制其百分比。 在绘制书籍名称、 销售数量及销售比例这 3 个字符串的时候, 它们的纵坐标都是相同的, 都是 167+i  *  50。这样就保证了这三个字符串的绘制高度相同。而每循环一次,绘制的新的 这三条字符串的高度会比当前字符串的绘制位置低 50 像素。 程序运行到这里,就结束了一次循环,然后会重复上述过程。 循环语句执行结束后,程序运行第 104 行: g.drawImage(offImage, 0, 0, null); 将缓冲区的图像内容发送到 Applet 上显示,之后便结束了 update 方法,饼图的所有绘制 工作就完成了。程序的实际运行效果如图 3.5 所示。  3.2  Java Applet 生成单据 从前面的例程我们可以看到,用 Applet 来生成 Web 动态图表是非常方便的。有时候,我 们需要生成一些复杂的图表: (1)带有某个公司徽标的图表。 图 3.8    Java 的徽标
    • (2)各种单据: Ø 账本及相关凭证; Ø 支票; Ø 销售发票; Ø 其他单据:如发货单、提货单、入库单、检验单、海关报关单,等等。 下面简单谈谈解决上述问题的思路,先说说第一个问题:比如说要绘制一个附带有如图  3.8 所示的 Java 徽标。 解决方法很简单,上一章我们讨论过在 Java Applet 如何加载并显示图像。那么,我们就 利用这一功能来在 Applet 中载入某个公司的徽标即可。对于第二个问题,以支票的生成为例。 我们认为最高效及简单的解决办法就是:通过扫描一张完整的、清晰的空白支票而得到一张 图像,然后以该图像的宽度和高度生成一个大小相同的 Java Applet,仔细调整相关的绘制参 数,在需要填写的位置上绘制出相关的内容即可。这种方法同样也适用于生成其他各种形式 的单据。 这里,我们提供两个简单的实例,分别展示如何用上述思路来解决这些问题。  3.2.1  带徽标的 Web 动态图表 以本章 3.1 节的 VBar.java 为例, 在该程序的基础上再显示一个如图 3.8 所示的 Java 徽标, 那么该如何修改 VBar.java 源程序呢? 源程序 LogoVBar.java(chap03Fig3.2Fig3.2_01)是 VBar.java 的升级版本,实现了上述 功能。本程序和第 3.1 节的 VBar.java 相比,仅仅改动了几个地方。 在程序第 9 行,新增加了一个 Image 类的对象: Image logoJPG; 在程序第 38 行,init 方法中用 Applet 类的 getImage 方法对其进行初始化: logoJPG = getImage(getDocumentBase(), "javaLogo.jpg"); 在 update 方法中,程序第 52 行: offGraphics.drawImage(logoJPG, 100, 0, this); 在点(100,0)处绘制 Image 对象 logoJPG。 程序第 55 行: offGraphics.drawString("带 Java 徽标的 Web 图表", 135, 40); 重新绘制了一条文本,我们改变了文本的内容和绘制位置。程序的其他部分没有任何改 变,读者可以参考第 3.1 节的解释。 程序的运行结果如图 3.9 所示。
    • 图 3.9  带徽标的 LogoVBar.java 的运行结果
    • 3.2.2  支票的生成 前面提到,生成类似支票这样的单据的最高效及简单的办法就是:通过扫描一张完整、 清晰的空白支票而得到一张图像,然后以该图像的宽度和高度生成一个大小相同的  Java  Applet,在该 Applet 中加载并显示该图像,然后仔细调整相关的绘制参数,在需要填写的位 置上绘制出相关的内容即可。 上述方法的前提是仅仅需要实现扫描一张完整、清晰的空白支票。下面以一张我们自 己制作的转账支票为例,讲述如何生成单据,如图 3.10 所示。 图 3.10  转账支票的外观 简单地说,支票分为左右两部分。左边的部分是支票存根,右边部分是交付给收款人或 银行。需要填写的内容主要有“出票日期”“付款金额”“收款人”“出票人账号”“用途” 、 、 、 、 、 “付款行名称”“科目” 、 ,以及“对方科目”等栏目。似乎只需要调整好上述各栏目绘制内容 的坐标就可以很轻松地完成支票的生成工作了。其实不然,支票的绘制看似简单,实则有几 个不大不小的麻烦需要处理。  1.出票日期的处理 Ø 需要将年、 月、 日提取出来, 并将其分别输入到相应栏目。 另外,在支票右边部分 “出 票日期”一栏,需要输入大写的日期。也就是说,这里涉及到如何将“2004­12­13” 这种日期格式中的年、月、日转换成 “贰零零肆”年“拾贰”月及“拾叁”中文格 式。 Ø 对于“年”的转换,我们可以将阿拉伯数字和中文大写数字进行简单的一对一的转换 操作,如“1999”“2005”可以直接转换成“壹玖玖玖”“贰零零伍” 、 、 ; Ø 对于“月”的转换,却不能直接将阿拉伯数字和中文大写数字进行简单的一一对应的 转换操作,如“10”“11”“12”这三个月份,按简单的一一对应的方法转换,我们 、 、 得到的是 “壹零” 、 “壹壹” “壹贰” 和 的结果, 这不符合我们的习惯称呼。 “10”、“11”、 “12”这三个月份的转换结果应该是“拾”“拾壹”和“拾贰” 、 。但对于 1~9 月份, 则可以直接采用按简单的一一对应的方法进行转换。 Ø 对于“日”的转换,同“月”的转换类似。1~9 之间的“日” ,可以直接转换。 “10”、 “20” “30” 和 这几个日子的转换结果, 应该分别是 “拾” 、“贰拾” “叁拾” 而 和 。 “11”~ “19”之间的日子转换结果应该是“拾壹”~“拾玖”“21”~“29”之间的日子转 ; 换结果应该是“贰拾壹”~“贰拾玖”“31”转换成“叁拾壹” ; 。  2.转账金额的处理 这里涉及到以下几个方面:
    • Ø 如何将小写的转账金额转换成中文的大写金额,即如何将“6 437 192.08”这类的小写 金额转换为“陆佰肆拾叁万柒仟壹佰玖拾贰圆(元)零角捌分”这种中文货币的大写 金额。 Ø 转账金额在支票左边部分的绘制还需要将其格式化成货币的表现形式, 即用小数点将 元和角、分隔开,并在千位,百万,十亿(以千位单位)的数位上用逗号隔开。如“6  437 192.08”应该表示为: “¥6 437 192.08”的格式。 Ø 转账金额在支票右边的绘制内容有两部分,一部分需要在“人民币大写”一栏内,以 中文大写格式填入转账金额, 另一部分是将阿拉伯数字格式的转账金额中的每一个数 字都相应地填写到“人民币大写”一栏右边的小写栏内。注意,在小写栏内转账金额 的最高位数字前还需要绘制一个人民币(¥)的符号。 源程序  CheckMaker.java(chap03Fig3.2Fig3.2_02)演示了如何实现支票的绘制功能。 上面谈到了程序应该对日期及金额进行中文转换的操作,在本例程中,除了没有提供“将小 写的转账金额转换成中文的大写金额”这一功能外,其余所有功能都可以在源程序中通过一 些简单而巧妙的方法实现。我们在 CheckMaker.java 引用了一个自定义的 DateToChinese 类来 实现阿拉伯数字的年、月、日到中文大写日期的转换功能。 我们先看看源程序 CheckMaker.java 的运行结果,如图 3.11 所示。 图 3.11  CheckMaker.java 的运行结果 我们先讨论实现阿拉伯数字的年、 日到中文大写日期转换功能的 DateToChinese. java  月、 (chap03Fig3.2Fig3.2_02)的源程序的运行机制。 程序第 7 行~第 10 行: String[] chineseNumber =  {  "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖", "拾"  }; 定义了一个字符串数组  chineseNumber,该数组中每一个元素分别对应着用中文表示的 阿拉伯数字中的 0~9。 程序第 12 行~第 15 行: public DateToChinese()  {  Super();  } 声明默认的构造器,在其方法体内,用  super  方法来调用其父类默认的构造器方法。程 序第 4 行在声明类的名字为 DateToChinese 的时候,并没有指明本类所继承的父类。在这种 情况下,Java 默认所有未指明父类的类都将继承 Object 类,Object 类位于 java.lang 包中。实
    • 际上,Object 类是所有 Java 类的超类,也就是说,每个 Java 类都是 Object 类的子类或间接 子类,都继承 Object 类所声明的 11 个方法。根据 Java 机制,每个 Java 类的默认构造器中, 不论程序是否显示 super 方法来调用其父类默认的构造器方法, 编译器都将用 super 方法来调 用其父类的默认构造器方法。 类的继承、封装及多态的内容不属于本书讨论的范畴,但因本书中的源程序涉及到这三 个面向对象设计的最重要的概念,所以在这里我们仅做一个很简单的介绍。 除默认的构造器外,DateToChinese 类提供了 5 个方法以实现不同的功能: Ø 程序第 17 行,getDayNumber 方法返回日期中“日”的中文大写转换结果。 Ø 程序第 48 行,getMonthNumber 方法返回日期中“月”的中文大写转换结果。 Ø 程序第 61 行,getCapitalNumber 方法返回日期中“年”的中文大写转换结果。 Ø 程序第 85 行,quotient 方法返回两个整数之间的整除运算结果。 Ø 程序第 90 行,remainder 方法返回两个整数之间的求模(余数)运算结果。 我们先介绍程序第 61 行~第 83 行的 getCapitalNumber 方法。该方法代码如下: public String getCapitalNumber(int number)  {  int divisor = 1, digit;  String output = "";  // 找到“最大的基数” for (int i = 1; i < number; i *= 10)  {  divisor = i;  }  while (divisor >= 1)  {  digit = quotient(number, divisor);  output += chineseNumber[digit];  number = remainder(number, divisor);  divisor = quotient(divisor, 10);  }  return output;  } //分离数字结束  getCapitalNumber  方法接收一个整型参数,以该方法接收到的整型参数“2004”为例, 讨论该方法的实现过程。 程序第 63 行,声明了两个整型变量:divisor 和 digit,其中 divisor 的初始值为 1。 程序第 64 行,声明了 1 个字符串变量:output,初始化为一个内容为空的字符串,表示 要返回的字符串结果。 程序第 67 行~第 70 行,用一个循环找到整数 2004 的“最大基数”“最大基数”在这里的 。 意思是指某个整数最高位数(也就是,从左往右数第 1 个非零数字)的基数,比如说: 2004 = 2 * 1000 + 0 * 100 + 0 * 10 + 4 * 1 所以 2004 的最大基数是 1000。又如: 642589 = 6 * 100000 + 4 * 10000 + 2 * 1000 + 5 * 100 + 8 * 10 + 9 * 1 所以 642 589 的最大基数是 100 000。 计算最大基数后,并将最大基数赋值给变量 divisor,此时 divisor 的值为 1000。 接下来,程序第 72 行~第 80 行的另一个 while 循环: while (divisor >= 1)
    • {  digit = quotient(number, divisor);  output += chineseNumber[digit];  number = remainder(number, divisor);  divisor = quotient(divisor, 10);  }  divisor 的值为 1000,大于 1,所以进入 while 循环,在其循环体内: 程序第 74 行,调用 quotient 方法,并传递两个参数:2004 和 1000。然后程序会跳转至 第 85 行的 quotient 方法: public int quotient(int a, int b)  {  return a / b;  }  quotient  方法接收两个整型参数,并返回这两个整数执行整除运算的结果。这里接收的 是 2004 和 1000,因为 2004/1000=2,所以 digit 的值为 2。然后程序返回 getCapital  Number  方法中的 while 循环中第 76 行,继续执行。 此时,相当于执行: output += chineseNumber[2]; 而 chineseNumber[2]的结果为“贰” ,因此,字符串变量 output 的值现在就变为“贰”。 接下来,程序执行第 78 行,调用 remainder 方法,并提供两个整型参数:2004 和 1000, 然后程序会跳转至 90 行的 remainder 方法: public int remainder(int a, int b)  {  return a % b;  }  remainder 方法也接收两个整型参数,并返回这两个整数执行求模运算的结果。这里接收的 是 2004 和 1000,因为 2004%1000=4,所以返回的结果是 4。然后程序返回 getCapital Number  方法中的 while 循环中第 78 行,将结果 4 赋值给变量 number 后,继续执行程序第 79 行。 此时,相当于执行: divisor = quotient(1000, 10); 再次调用 quotient 方法,并将结果 1000/10=100 赋值给变量 divisor。此时 divisor 的值等 于 100,仍然不小于 1,所以程序继续进行第 2 次循环,同样重复上面的步骤,这次接收的参 数是 4 和 100, 因为 4/100=0, 所以 digit 的值为 0,之后,因为 chineseNumber[0]的值为“零”, 所以 output 的值就变成了“贰零” ,同理,变量 nubmer=4 % 100,所以 number 的值仍然为 4, 而 divisor=100/10,所以 divisor 的值就改变为 10。 现在进入第 3 次循环,同样重复上面的步骤,这次接收的参数是 4 和 10,因为 4/10=0, 所以 digit 的值还是为 0,之后,因为 chineseNumber[0]的值为“零” ,所以 output 的值就变成 了“贰零零” ,同理,变量 nubmer=4%10,所以 number 的值仍然为 4,而 divisor=10/10,所 以现在 divisor 的值就改变为 1 了。 现在进入第 4 次循环,同样重复上面的步骤,这次接收的参数是 4 和 1,因为 4/1=4,所 以 digit 的值改变为 4,之后,因为 chineseNumber[4]的值为“肆” ,所以 output 的值就变成了 “贰零零肆” ,同理,变量 nubmer=4%1,所以 number 的值仍然为 4,而 divisor=1/10,所以 现在 divisor 的值就改变为 0 了,因为 0 小于 1,所以程序结束循环。 最后将字符串变量  output  内容作为  getCapitalNumber  方法的返回值。至此,getCapital  Number 方法结束。
    • 综上所述,getCapitalNumber  方法实现的功能实际上是将给定的一个任意长的整数(当 然该整数被限定在 Java 对整数的处理范围之内)按一一对应的方式把数字转换成中文,并返 回转换后的结果。 现在介绍程序第 48 行~第 58 行的 getMonthNumber 方法。该方法代码如下: public String getMonthNumber(int month)  {  String output = "";  if (month > 10)  {  output += "拾";  month ­= 10;  }  output += getCapitalNumber(month);  return output;  } 顾名思义,该方法返回日期“月”的中文大写转换结果。getMonthNumber 方法接收一个 整型参数 month,程序第 50 行,声明了一个字符串对象:output,并初始化其内容为空。如 果传递的参数在 1~10 之间,则程序跳过第 51 行~第  55 行之间的 if 语句块,执行第 56 行 的语句: output += getCapitalNumber(month); 将变量 month 作为参数再次传递并调用 getCpitalNumber 方法,把 getCpitalNumber 方法 返回的字符串在串接在变量 output 后,最后返回 output。 如果传递的参数大于 10,这里以传入的 month 参数: “12”为例,则程序首先进入第 51  行~第 55 行之间的 if 语句块,执行完第 53 行的语句后,output 值更新为“拾” 。第 54 行, 将 month 参数减去 10 后,再将相减后的结果重新赋值给 month。所以这时,month 的值更改 为 2(12­10)。然后再执行程序第 56 行的语句,这时实际上执行的是: output += getCapitalNumber(2);  getCpitalNumber(2)方法返回的结果是“贰” ,现在字符串变量 output 的值就等于“拾贰” 了,最后再返回 output 的内容。 综上所述,对于“月”的转换,getMonthNumber 方法对“10”“11”“12”这三个月份 、 、 返回的结果是: “拾”“拾壹”和“拾贰” 、 ;对于 1~9 月份,则可以直接采用按简单的一一对 应的方法返回相应的结果。注意,这里我们并没有对大于 12 以上的数进行额外的处理,因为 在程序 CheckMaker.java 中, 我们调用 getMonthNumber 方法时所传递的参数范围就限定在 1~  12 之间了。 现在介绍程序第 17 行~第 46 行的 getDayNumber 方法。该方法代码如下: public String getDayNumber(int day)  {  String output = "";  if (day < 20)  {  output += getMonthNumber(day);  }  else if (day >= 20 && day < 30)  {  output += "贰拾";  day ­= 20;  if (day == 0)  {  return "贰拾";  }
    • output += getCapitalNumber(day);  }  else  {  output += "叁拾";  day ­= 30;  if (day == 0)  {  return "叁拾";  }  output += getCapitalNumber(day);  }  return output;  }  getDayNumber 方法首先对传入的参数 day 进行判断: (1)day 小于 20,则执行程序第 22 行,直接将 day 传递给 getMonthNumber 方法,会 重复执行刚刚我们所讲述的 getMonthNumber 方法的执行过程,然后执行程序第 45 行,并返 回所得到的结果。 (2)day 如果大于或等于 20 且小于 30,则执行程序第 24 行~第 33 行之间的语句块。 这里以 day 等于“27”为例,首先 output 的内容就改变为“贰拾” ,然后 day 减去 20,如果 差为“0” ,说明 day 正好等于 20,则立刻返回字符串“贰拾” ;否则将新的 day 的值传递给  getCpitalNumber 方法,即执行 getCpitalNumber(7)方法,返回的结果“柒”附加在字符串 对象 output 的内容后,这时字符串 output 的内容就改变为“贰拾柒”了。最后执行程序第 45  行,返回 output 的内容。 (3)在其他情况下,则执行程序第  35  行~第  43  行之间的语句块。与前面介绍的第  2  点运行结构过程完全相同,此处不再赘述。同理,注意这里我们并没有对大于 31 以上的数进 行额外的处理,因为在程序 CheckMaker.java 中,调用 getDayNumber 方法时所传递的参数范 围就限定在 1~31 之间了。 现在讨论 CheckMaker.java 的运行过程。 程序第 6 行~第 7 行: import java.util.*; //引入 java.util 包中所有的类 import java.text.*; //引入 java.text 包中的所有的类 分别引进 java.util 包和 java.text 包。java.text 包我们已经介绍过了。java.util 包是一些工 具类的集合,里面包含了一些常用的工具类,如用于日期及时间处理的 Date 类等。 程序第 11 行: Image checkPNG; 声明了一个 Image 类的对象 checkPNG,用于加载和显示背景图像,如图 3.10 所示的空 白支票。 程序第 12 行~第 13 行: Image offImage;  Graphics offGraphics; 声明用于绘制缓冲区图形 对象的  Image  对象  offImage  及其在缓 冲区的绘图环境  offGraphics。 程序第 15 行: int appltWidth = 740, appletHeight = 270; 定义本 Java Applet 的宽度和高度。这里的宽度和高度与背景图像,即空白支票的宽度和
    • 高度相同。 程序第 16 行~第 22 行,创建了一些字符串对象用于空白支票某些栏目的绘制。上述字 符串对象分别表示: “科目”“对方科目”“收款人”“付出行名称”“用途”“会计”以及 、 、 、 、 、 “单位主管”等内容。 程序第 24 行: Date issuedDate = new Date(); 创建了一个 Date 类的实例 issuedDate,表示支票的出票日期。 Date 类隶属于 java.util 包, 主要用于 Java 对日期和时间的处理。 用 new Date()方法创建的 Date 对象表示的是当前的系统 时间。默认的 Date 对象的输出格式如下所示(以 2004 年 12 月 13 日为例) : Mon Dec 13 20:12:50 CST 2004­12­13 程序第 25 行~第 27 行,声明了 3 个整型变量:year、month,以及 day,分别表示支票 出票日期的年、月、日。 程序第 28 行: double amount = 6437192.08d; 创建了 1 个值为 6 437 192.08d 双精度型的变量:amount,表示转账金额。最后一位数字 后面的 8 表示该浮点数为双精度型(这里的 d 可以省略不写,我们的编程习惯是写上“d”表 示 double 型,“f”表示 float 型,以示区别) 。 程序第 30 行: String accountNumber = "渝工行 2004010178930"; 创建字符串对象 accountNumber,表示出票人账号。 程序第 31 行: String chineseAmount = "陆佰肆拾叁万柒仟壹佰玖拾贰圆零角捌分"; 创建了一个字符串变量  chineseAmount,并用“陆佰肆拾叁万柒仟壹佰玖拾贰圆零角捌 分”对其进行初始化。chineseAmount 实际上是程序第 28 行,转账金额 6 437 192.08 的人民 币大写的货币表现格式。 因为本程序并没有涉及到如何将类似于程序第 28 行的数字值自动转 换成相应的人民币大写的货币表现格式的方法, 所以这里我们直接提供了一个与 6 437 192.08  相对应的人民币大写的货币表现格式。对此有兴趣的读者,可以借鉴我们提供的年、月、日 的转换方法来完成这一功能。 程序第 33 行: NumberFormat moneyFormat = NumberFormat.getCurrencyInstance(Locale.  PRC); 创建了 1 个 NumberFormat 的实例对象 accountNumber,并调用 NumberFormat 的静态方 法  getCurrencyInstance 对其进行初始化。该方法返回一个能将数字值格式化为指定的货币值 形式(如表示美元的数字值前,通常加一个“$”符号;表示人民币的数字值前,通常加一个 “¥”符号)的 NumberFormat 对象。方法中的参数 Locale.PRC 表示货币值应该按中国的人 民币格式显示,即以“¥”符号开头,并用小数点将元和角、分隔开,并在千位、百万、十 亿(以千位单位)的数位上用逗号隔开。Locale 类提供了一些静态的常量,用于表示特定国 家的货币值,以便能正确地显示某个国家的货币格式。其他的一些表示国家和地区的静态常 量还有: Ø  Locale. CHINA:中国 Ø  Locale. US:美国
    • Ø  Locale. CANADA:加拿大 Ø  Locale. FRANCE:法国 Ø  Locale. GERMAN:德国 Ø  Locale. UK:英国 Ø  Locale. KOREA:韩国 Ø  Locale. JAPAN:日本 Ø  Locale. ITALY:意大利 Ø  Locale. TAIWAN:中国台湾  NumberFormat 隶属于 java.text 包,而 Locale 类隶属于 java.util 包。两者分别在程序第 6、  7 行引进。 程序第 35 行: DateToChinese dtc = new DateToChinese(); 用 DateToChinese 类的默认构造器创建了 1 个 DateToChinese 类的实例——dtc。注意,我 们并没有引入  DateToChinese  类。如果一个类与使用它的类位于同一个包(目录)中,则不 需要 import。如果程序员没有为类指定一个包,则该类将置于一个没有名称的默认包中,它包含 当前目录中所有已编译、但未显示放置在某个包中的类。这就是我们为什么必须在 Java 程序中, 为了使用 Java API 中的类,就一定要用 import 语句将其引入后才可以使用。 提示:如果程序通过在类名前面添加完全包名来限制类的名称,则可以不需要 import 语 句。例如,程序第 24 行,可改写成以下形式: java.util.Date issuedDate = new java.util.Date(); 至此,本例的变量声明完毕。接下来程序运行第 38 行~第 46 行的 init 方法: public void init()  {  checkPNG = getImage(getDocumentBase(), "check.jpg");  offImage = createImage(appletWidth, appletHeight);  offGraphics = offImage.getGraphics();  year = issuedDate.getYear() + 1900;  month = issuedDate.getMonth() + 1;  day = issuedDate.getDate();  } 关于 Init 方法现在读者都已经很熟悉了。首先还是加载背景图像——空白的支票,然后 创建缓冲图像对象及其绘图环境。之后用 Date 类的相关方法获得程序第 24 行,用 new Date  ()方法创建的 Date 对象:issuedDate 所表示的当前系统时间中的年、月、日等信息。  Date 类的 getYear 方法,返回 Date 对象的年值,该年值是以 1900 为基准点的,也就是 说,公元 2000 年则 getYear 返回 100(2000–1900) 。所以,程序第 43 行,在 getYear 方法后 加上了 1900,就得到了 Date 对象 issuedDate 的实际年值。  Date 类的 getMonth 方法返回 Date 对象的月值,该月值是以 0~11 分别代表 1~12 月。 所以,程序第 44 行,在 getMonth 方法后加上了 1,就得到了 Date 对象 issuedDate 的实际月 值。 程序第 45 行,issuedDate.getDate 返回了 issuedDate 的日值。 之后,我们又会看见熟悉的运行流程,程序先执行 paint 方法,再执行 update 方法,最 后再将缓冲区内的内容显示在 Applet 上。 在 update 方法中,我们首先显示被加载的背景图像,空白的支票。然后,程序在第 142  行: drawLeft();
    • 调用 drawLeft 方法,绘制支票左边部分的内容。程序转到第 56 行~第 85 行的 drawLeft  方法中继续运行: public void drawLeft()  {  offGraphics.setFont(new Font("宋体", Font.PLAIN, 12));  // 绘制科目 offGraphics.drawString(subjectName, 85, 80);  // 绘制对方科目 offGraphics.drawString(partnerSubjectName, 85, 108);  // 绘制出票日期 offGraphics.drawString("" + year, 77, 133);  offGraphics.drawString("" + month, 115, 133);  offGraphics.drawString("" + day, 140, 133);  // 绘制收款人 offGraphics.drawString(receiver, 70, 165);  // 绘制付款金额 offGraphics.drawString(moneyFormat.format(amount), 70, 188);  // 绘制付款用途 offGraphics.drawString(payFor, 70, 208);  // 绘制会计人员 offGraphics.drawString(accountant, 60, 255);  // 绘制主管 offGraphics.drawString(supervisor, 125, 255);  } 这段代码仅仅重复使用了 drawString 的方法,将相关内容绘制到各自对应的栏目中。这 里惟一需要介绍的地方就是程序第 75 行,用 moneyFormat 类的 format 方法,并接收 1 个参 数:即程序第 28 行所声明的双精度型变量 amount,就获得了 amount 的中国货币表现格式的 输出。如果所提供的参数是整数或者只有 1 位小数,则 moneyFormat 类的 format 方法会在相 应的数位上自动添加“0” 。  drawLeft 方法运行完毕后,程序的绘制结果如图 3.12 所示。 我们可以看到“金额”一栏中 amount 的中国货币表现格式的输出效果。 然后,程序返回 update 方法,调用 drawRight 方法,绘制支票右边部分的相关内容。 这时,程序进入第 87 行~第 134 行的 drawRight 方法。实际上,drawRight 方法也是由 一系列的 drawString 方法组成。drawRight 方法运行完成后的绘制结果如图 3.11 右边部分所 示。 在该方法中,要注意图 3.13 圆圈部分是如何绘制的。
    • 图 3.12    drawLeft 方法的运行结果 图 3.13    drawRight 方法的运行结果  drawRight 方法中的第 117 行~第 133 行的代码就进行了圆圈部分的绘制工作: offGraphics.setFont(new Font("宋体", Font.PLAIN, 14));  String amountOutput = moneyFormat.format(amount);  int j = 0;  for (int i = amountOutput.length() ­ 1; i >= 0; i­­)  {  String s = "" + amountOutput.charAt(i);  // 跳过小数点 if (s.equals(".")||s.equals(","))  {  continue;  }  offGraphics.drawString(s, 710­j * 15, 127);  j++;  } 我们首先重新设定当前的绘图字体。 程序第 119 行,创建一个 String 对象 amountOutput,并调用 NumberFormat 类的 format  方法对其进行初始化,amountOutput 的内容如下: ¥6,437,192.08 然后需要将字符串“¥6,437,192.08”中的每一个数字,以及中国货币符号“¥” ,绘制 到圆圈区域内所示的相应位置上。注意,amountOutput  中的逗号“, ”及小数点“.”是不需 要绘制的。 我们的绘制思路是从字符串 amountOutput 的最后 1 个(即倒数第 1 个)字符开始读取, 将其绘制到图 3.13 圆圈区域中“分”的位置;然后读取倒数第 2 个字符,将其绘制在“角” 的位置上;如果读取字符串中当前字符的内容为逗号或小数点,则程序忽略该字符。依此类 推,直到绘制完中国货币符号“¥”为止。  drawRight  方 法 中 的 第  121  行 ~ 第  133  行 , 就 利 用 上 述 思 路 实 现 了 绘 制 字 符 串  amountOutput 的目的。 因为 String 类的 charAt 方法返回的是一个字符,所以在程序第 124 行,用了字符串的串 接方法,创建了一个 String 对象 s,而 s 的内容就是 charAt 方法返回的一个字符。 然后,程序在第 127 行,判断 s 的内容是否是逗号或小数点,如果是,则程序不对 s 做 任何处理,调用 continue 关键字,跳过剩余的部分而直接进入下一次循环。如果 s 的内容不 是逗号或小数点,则运行程序第 131 行:
    • offGraphics.drawString(s, 710­j * 15, 127); 这里的 710 正好是圆圈区域中“分”的横坐标,每循环一次,j 的值会递增 1,也就是说, 下一个对象的绘制位置向左移动 15 像素。 当循环结束的时候,所有的数字以及中国货币符号“¥”就被绘制到相应的位置了。 当 drawRight 方法结束,程序返回到 update 方法中,缓冲区的图像绘制工作也就随之结 束,最后程序在第 147 行,将缓冲区的图像全部显示在 Applet 上,程序就此结束。最终的绘 制结果如图 3.11 所示。 关于如何在 Java  Applet 中加载并显示图像,并利用该功能来生成支票或者其他单据类的 图表,我们就介绍到这里,利用这种思路可以实现其他复杂的票据及单据的绘制工作。  3.3  从 HTML 文档获取参数生成动态图表 前面我们在介绍 Java Applet 中生成动态图表的时候,所需要的数据都是在该 Applet 源程 序中直接指定或者是在该程序中由实时生成的随机数而得来。此前,我们所用到的 HTML 文 档仅仅是加载 Java Applet 的.class 文件,以及设定该 Applet 在浏览器中所占的宽度和高度这 两个最基本的参数。其实除了在 Java 源程序自身可以生成所有需要的数据外,Applet 还可以 从 HTML 文档中读取数据。 当我们希望改变 Web 图表的一些相关信息的时候,以第 3.12 节的 Pie.java 为例,假如我 们更改 Web 图表的标题:“Java Web 图表设计 Applet 版——饼图”为“Java Web 图表设计  Applet & HTML 版” ,就必须修改 Pie.java 源程序第 61 行,用新的内容替换掉旧的内容,然 后再次编译,重新启动浏览器,才可以看到修改后的结果。如果要求在不修改 Java 源程序的 基础上,也同样达到修改 Java Applet 中某些内容的目的,则最简单而有效的办法是从 HTML  文档中读入所需的数据。 下面我们来学习如何通过 HTML 文档向 Java Applet 传递数据。  3.3.1  传递参数的 HTML 文档 利用 Applet 来接收从 HTML 中传递过来的参数,需要在 HTML 文档中的<APPLET>和  </APPLET>标记中,再插入用于向 Java  Applet 传递参数的标记。下面就 HTML 文档中用于  Java Applet 的标记做一个简单说明,如表 3.1 所示。 表 3.1  用于 HTML 文档的 Applet 的标记 标记名称 说 明  CODE  指定 Applet 的类名  WIDTH  指定 Applet 窗口宽度的像素尺寸  HEIGHT  指定 Applet 窗口高度的像素尺寸  指定 Applet 的 URL 地址。它可以是绝对地址 ,如 java.sun.com;也可以是相对于当前 HTML 所在 CODEBASE  目录的相对地址,如/AppletPath/Name。如果 HTML 文件不指定 CODEBASE  标志,浏览器将使用和  HTML 文件相同的 URL  虽然  Java  在  WWW  上很受欢迎,但并非所有浏览器都支持  Java。如果某种浏览器无法运行  Java  ALT  Applet,那么它在遇到 APPLET 语句时将显示 ALT 标志指定的文本信息  ALIGN  用来控制把 Applet 窗口显示在 HTML 文档窗口位置。与 HTML 语法中的<IMG>语句相同,ALIGN
    • 标志指定的值可以是 TOP、MIDDLE 或 BOTTOM  把指定的名字赋予 Applet 的当前实例。当浏览器同时运行两个或多个 Applet 时,各 Applet 可通过 NAME  名字相互引用或交换信息。如果忽略 NAME 标志,Applet 的名字将对应于其类名  PARAM  指定由 Applet 读取的参数值,每个标记指定一个参数 我们重新编写了一个 PieNew.java(chap03Fig3.3)的源程序,这次绘制饼图所需要的数 据全部从 PieNew.html 文档中获得。 我们从 PieNew.html(chap03Fig3.3)文档中可以观察到,同以前的 HTML 文档相比, 增加了一些如下所示的 PARAM 标记: <PARAM NAME = "bookTitle1" VALUE = "Python">  PARAM 指定了由 Java Applet 读取的参数。每个 PARAM 标记(即参数)又包含两个属 性:NAME 和 VALUE。NAME 表示参数的名称,VALUE 表示参数的值。 一般来说,HTML 向 Java  Applet 传递的参数,可以在 Java  Applet 中通过编程的方式获 得这些参数值。通常会在 Java  Applet 的 init 方法中,调用 getParameter 方法来获得某个参数 的值。如果 getParameter 方法中指定某个参数可用,则以字符串的形式返回某个参数的值。 下面阐述如何在 Java Applet 中,获取这些参数的详细用法。  3.3.2  获取参数并生成图表 源程序 PieNew.java 和本章第 3.1.2 节介绍的 Pie.java 的结构完全相同。不同之处在于,  PieNew.java 中生成饼图的数据,没有采用 Pie.java 中的由随机数的方式产生,所有参与生成 饼图的数据全部都是从 PieNew.html 文档中读取相关参数而获得。 我们以 init 方法中,获取书籍的名称及其销售量的参数为例。 程序第 52 行~第 65 行: for (int i = 0; i < 5; i++)  {  bookTitle[i] = getParameter("bookTitle" + (i + 1));  tempBookSales[i] = getParameter("bookSales" + (i + 1));  if (tempBookSales[i] != null)  {  bookSales[i] = Integer.parseInt(tempBookSales[i]);  }  else  {  bookSales[i] = 0;  }  totalSales += bookSales[i];  } 一般来说,HTML 向 Java  Applet 传递的参数,可以在 Java  Applet 中,通过编程的方式 获得这些参数值。通常会在 Java Applet 的 init 方法中,调用 getParameter 方法(程序第 54、  55 行)来获得某个参数值。如果 getParameter 方法中指定的某个参数可用,则以字符串的形 式返回某个参数值。本段程序块总共循环  5  次,调用  getParameter  方法,试图获得参数 “bookTitle1”“bookSales1”“bookTitle2”“bookSales2”“bookTitle3”“bookSales3” 、 、 、 、 、 、 “bookTitle4”“bookSales4”“bookTitle5”和“bookSales5”等参数值。如果在相应的 HTML  、 、 文档中存在同名的参数,则返回该参数值。我们可以看到  NewPie.html  文档中,的确提供了 这些同名的参数(这些参数在 html 中是不区分大小写的) 。因此,会分别获得如“Python” 、 “45”、“Java” 、 “115” 依此类推。 , 记住返回的这些参数值都是字符串类型, 因此参数值 “45” 、 “115”等,虽然它们表示的值都是书籍的销售量,但实际上都是字符串对象,并不能直接用
    • 于数学运算。因为我们需要计算所有书籍的销售总量,所以必须将这些字符串对象转换成可 用于数学计算的数字型的数据类型,如字节型、短整型、整数型、长整型、浮点型和双精度 型等。 在 java.lang 包中,对于每一个基本的数字型数据类型,均设置了一个相应的类,例如:  Integer 类对应于 int,Double 类对应于 double 等。这些类中均提供了一个转换方法,用于实 现字符串向相应数字类型的转换。表 3.2 列出了详细的转换方法。 表 3.2  字符串向数字的转换 目标类型 类 方 法 示 例  byte  Byte  parseByte  byte b = Byte.parseByte(“100”);  short  Short  parseShort  short s = Short.parseShort(“100”);  int  Integer  parseInt  int i = Integer.parseInteger(“100”);  续表 目标类型 类 方 法 示 例  long  Long  ParseLong  long l = Long.parseLong(“100”);  float  Float  parseFloat  float f = Float.parseFloat(“100.173”);  double  Double  parseDouble  double d = Double.parseDouble(“100.173”);  需要注意的是,在使用上述方法将字符串向数字转换的过程中,如果被转换的字符串与 目标类型不匹配,则在转换过程中抛出异常。所以,如果无法肯定被转换的字符串和目标数 据类型是否匹配,我们就必须将转换方法放置在  try…catch  语句块中,捕获这种异常,并做 相应的处理。再调用 getParameter 方法,如果试图获得的参数值在相应的 HTML 文档中并不 存在,则该方法返回一个 null 对象。所以在上面的循环中,程序第 54 行~第 55 行,先分别 用了两个字符串数组:bookTitle、tempBookSales 来存储书籍变量及书籍销售量,然后程序第  56 行,首先判断当前的 tempBookSales 字符串数组中的元素是否为 null,如果不为 null,则执 行 parseInteger 方法,进行字符串对象到整型对象的转换工作。 整个程序的流程和第 3.1.2 节介绍的 Pie.java 完全相同。读者可以参考该节的相关内容, 此处不赘述。  NewPie.java 的运行结果如图 3.14 所示。如果改变 NewPie.java 中的 PARAM 相关参数的 值,则只需要刷新浏览器,就可以看到新的结果了。 图 3.14  NewPie.java 的运行结果
    • 3.4  本章小结 本章通过 5 个不同的实例,讲解了 Java Applet 动态图表的编写方法。 用 Java Applet 来生成动态图表,有如下优点: Ø 简单而高效的开发速度。Java  Applet 是一种简单、快速的开发 Web 动态图表的有效 方法。 Ø 结合图像的加载及显示功能,Java  Applet 使得 Web 动态图表的应用处理能力变得非 常强大,可以完成非常复杂的业务单据。 Ø  Java 设计上的平台无关性,使得 Java  Applet 可以在装有 Java 虚拟机的计算机上很好 地工作。这就保证了代码在  IE  和  Netscape,以及其他多种浏览器之间保持良好的兼 容性。 Ø  Java 良好的安全性, 使得 Java Applet 比 ActiveX 或任何其他的插件都更具安全性。例 如,ActiveX 可以重写文件系统,但在 Java 中是被绝对禁止的。  Java Applet 也有其无法避免的缺点: Ø  Java Applet 是为客户端执行的,也就是说,必须安装 Java 虚拟机(JVM) 。如果没有 安装 Java 虚拟机,则程序无法运行。 Ø  Java  是被设计为与平台无关的,因此,当编译  Java  时,并不像别的语言那样被编译 成针对某种特定平台的机器码,而是被编译成与平台无关的通用二进制代码。当客户 端下载一个含有 Java Applet 的页面时,  Applet 中的这种二进制代码就通过网络传送到 客户端的浏览器,在客户端的浏览器内部装有一个  Java  虚拟机,为了使客户端的机 器能读懂这个 Applet 的内容,浏览器首先要启动这个 Java 虚拟机,然后将 Applet 中 的二进制代码编译成机器语言。这种一次编译,到处运行的特性节省下来的时间就花 费在每个访问者的身上了。 Ø 含有 Applet 的页面在每次请求 class 文件时都需要重新下载,如果网络的通信状况不 好,将会导致程序运行变得异常缓慢。 解决的方法是,Web 动态图表的生成不再由客户端的浏览器生成,当客户端的浏览器在 访问含有 Web 动态图表页面的时候,直接由 Web 服务器生成实时的动态图表(图像) ,然后 将结果返回给客户端。这就是所谓的“服务器端编程技术” 。 基于 Java 的服务器端编程技术,目前主要有三种方法: Ø Java Servlet 技术。 Ø Java Server Pages,即 JSP 技术。 Ø JSP + JavaBean 技术。 本书的重点就是阐述如何通过服务器端编程技术来实现  Web  动态图表,其中重点介绍  JSP 技术和 JSP  +  JavaBean 技术是如何应用于 Web 动态图表编程的。具备了 Java  Applet 的  Web 图表编程基础,就可以进行这方面的学习了。首先,我们必须掌握如何搭建 JSP/Servlet  的运行环境。
    • 第 4 章  JSP/Servlet 运行环境的搭建  Java  Applet 作为 Java 在 Web 上主要的交互式应用模式一直持续了很长的时间。我们概 述了 Java Applet 在应用方面的种种局限性。 对这些局限性及不足之处,解决方案一直到 Servlet  的出现才比较完美。其后,Sun 公司在 1999 年下半年正式推出 Servlet 的升级产品 JSP(Java  Server Pages),以其简单而高效的开发方式、跨平台的能力及卓越的执行性能,使得基于 JSP  的开发及应用在美国得到各大公司的热烈响应和支持。由于 JSP/Servlet 技术也是 J2EE 技术 最重要的组成部分之一,在企业级也得到了广泛的应用。短短几年时间,JSP  已经成为美国 最主要的 Web 开发技术。选择 JSP/Servlet 技术作为基于服务器端的 Web 动态图表的开发是 非常明智的。 在运行及调试 JSP/Servlet 之前,必须安装 JSP/Servlet 的运行环境。JSP/Servlet 的运行 环境也就是我们平常所说的支持 JSP/Servlet 的 Web 服务器。 目前,已经有很多支持 JSP/Servlet 的 Web 服务器,这里我们介绍两种免费、高性能的适 合中小企业的 JSP/Servlet 的 Web 服务器——Tomcat 和 Resin,然后再介绍一种企业级的 J2EE  服务器:Weblogic,学习如何在这些服务器中部署及发布我们编写的基于 JSP/Servlet 的 Web  图表应用程序。 在安装 JSP/Servlet 的 Web 服务器之前,要先安装 JDK 的开发及运行环境。JDK 的安装 请读者参考本书第 1 章。  4.1  Tomcat 的安装和配置  Tomcat  服务器是当今使用最为广泛的 JSP/Servlet  的  Web  服务器,它是  Apache 基金会  Jakarta 项目中的一个核心项目,由 Apache,Sun 和其他一些公司及个人共同开发而成。由于 有了 Sun 的参与和支持,最新的 Servlet 和 JSP 规范总能在 Tomcat 中得到体现。  Tomcat 具有运行稳定,性能优异等特点,而且 Tomcat 是完全免费的开放源代码产品。 是中小企业首选的 JSP/Servlet 的 Web 服务器之一。笔者写作的时候,使用的是 Tomcat 最 新 版 本  5.5.7 , 读 者 可 以 在  Apache  网 站 上 下 载 , URL  如 下 : http://jakarta.apache.  org/site/binindex.cgi  Apache 提供了两种用于 Windows 的 Tomcat 安装程序,一种是 akarta­tomcat­5.5.7.exe 的 可执行文件,另一种是 jakarta­tomcat­5.5.7.zip 的格式,我们建议读者下载 zip 版的服务器程 序。  4.1.1    Tomcat 的安装 我们分别介绍  jakarta­tomcat­5.5.7.exe  及  jakarta­tomcat­5.5.7.zip  版服务器程序的安装过 程。
    • 1.jakarta­tomcat­5.5.7.exe 的安装 (1) 双击下载的 jakarta­tomcat­5.5.7.exe 可执行文件,就会出现如图 4.1 所示的 Apache  Tomcat 安装向导,单击【Next】按钮。 (2)在出现如图 4.2 所示的 Tomcat 授权协议界面后,单击右边的【I Agree】按钮。 图 4.1    Tomcat 的安装向导 图 4.2  授权协议 (3)在出现如图 4.3 所示的 Tomcat 安装组件选择对话框时,我们改变 Tomcat 的默认安 装类型“Normal”为“Full” ,并单击【Next】按钮。 (4)在出现如图 4.4 所示的 Tomcat 的安装路径对话框时,我们同样也改变 Tomcat 的默 认安装路径“C:Program FilesApache Software FoundationTomcat 5.5”为“C:Tomcat 5.5” , 并单击【Next】按钮。 图 4.3  选择安装组件 图 4.4  安装路径 (5)配置 Tomcat,创建一个管理员密码。请记住管理员密码, 以后管理 Tomcat 的时候, 需要使用该密码,单击【Next】按钮,如图 4.5 所示。 (6)选择 Java 虚拟机的路径,如果已经安装了 J2SE  5.0,Tomcat 会自动找到其安装路 径,单击【Next】按钮,继续剩余部分的安装,如图 4.6 所示。
    • 图 4.5  创建管理员密码 图 4.6  选择 JRE  (7)系统会复制相关文件到  Tomcat  的安装目录,最后系统会提示是否运行  Tomcat  服 务器,以及说明文件,单击【Finish】按钮,启动 Tomcat 服务器,如图 4.7 所示。 图 4.7  结束安装 (8)然后我们可以在右下方系统工具栏中,看到 这个图标。右键单击 图标,会 弹出一个菜单。 【Configure…】选项是用于设置 Tomcat 的相关运行参数的。包括启动与关闭  Tomcat 服务器。Tomcat 启动类型:如自动、手动、禁止等,如图 4.9 所示。这里我们保持这 些参数不变。 【Start Service】是启动 Tomcat 服务器, 【Stop Service】是关闭 Tomcat 服务器。 如果【Start Service】显示为灰色,表示 Tomcat 服务器已经启动。 下面测试 Tomcat 服务器是否已经正常运行。启动 Tomcat 有几种方法:其一,可以直接 在 Tomcat 安装目录下(本例:C:tomcat5.5)的 bin 子目录中运行 tomcat5.exe 程序,然后会 弹出一个 DOS 窗口,  Tomcat 以命令行方式并加载相关参数启动服务程序, 如果看到“Server startup  in xxxx ms”的信息,则表示 Tomcat 服务器已经正常启动了,如图 4.8 所示。 图 4.8    Tomcat 的启动方式 1  其二,在 bin 目录下,我们也可以执行 tomcat5w.exe 文件,单击运行界面下方的【Start】 按钮,就可以启动 Tomcat;单击【Stop】按钮,则可以停止 Tomcat 服务器,如图 4.9 所示。 现在启动浏览器,在地址栏内输入:http://localhost:8080 或者 http://127.0.0.1:8080,如果
    • 看到如图 4.10 所示的结果,表示 Tomcat 安装和配置是正确的。 图 4.9    Tomcat 的启动方式 2  图 4.10  默认的 Tomcat 的启动画面  2.jakarta­tomcat­5.5.7.zip 的安装 首先将 jakarta­tomcat­5.5.7.zip 版服务器程序进行解压,如解压到 C:jakarta­tomcat­ 5.5.7  目录下。然后直接运行该目录下 bin 子目录中 startup.bat 批处理文件或者前文介绍的两种启 动方法就可以运行 Tomcat 服务器了。 同样,启动浏览器在地址栏内输入 http://localhost:8080 或者 http://127.0.0.1:8080,如果同 样看到如图 4.10 所示的运行结果,则表示 zip 版 Tomcat 安装和配置也是正确的。 提示:如果不能启动  Tomcat  服务器,或者无法在浏览器中获得相应的结果,几乎都是 因为 J2SE5.0 的安装出现了问题,请参考本书第 1 章的相关内容。
    • 4.1.2  测试第一个 JSP 程序 现在编写一个很简单的 JSP 文件——test.jsp(chap04Fig4.1Fig.4.1_01) ,将 test.jsp 文件 拷贝到 Tomcat 安装目录 (本例:C:tomcat5.5)webappsROOT 子目录下,启动 Tomcat。在浏 览器中地址栏输入  http://localhost:8080/test.jsp  或者  http://127.0.0.1:8080/test.jsp,如果看到如 图 4.11 所示的运行结果,则表示读者第一个 JSP 页面编写及部署成功了。 图 4.11    test.jsp 的运行结果  4.1.3  配置 Tomcat  虽然 Tomcat 现在可以正确运行了,但是读者最好还是参考本书第 1.2.2 节介绍的内容, 新建一个系统变量——CATALINA_HOME,其值为 Tomcat 安装目录,以本书为例,其值为:  C:Tomcat5.5。然后更新系统变量——CLASS_PATH,将原值: .;%JAVA_HOME%lib 更新为, .;%JAVA_HOME%lib;%CATALINA_HOME%lib 同样,请读者不要忘记最前面那个“.” 。  Web  应用程序的安装和部署一直是个令初学者感到非常头痛的难题,但却是必须要掌握的 一个关键内容。很多读者会遇到这样的问题,在本机运行很好的 Web 应用程序,如何将其打包 并在 Web 服务器上进行安装和部署?部署成功,为什么没有得到正确的结果?要想解决这些问 题,必须对 JSP/Servlet 的 Web 服务器的运行机制有一定的了解,这需要专门的著作来对其详细 的讲解。因为本书内容涉及如何将程序发布到不同的 JSP/Servlet 的 Web 服务器上,所以在这里 我们以 Tomcat 为例,简要地探讨其启动过程,以及如何对其进行配置。 让我们来研究一下 Tomcat 目录结构中最重要的几个子目录,如表 4.1 所示。 表 4.1    Tomcat 目录结构 目 录 名 简 介  bin  存放启动和关闭 tomcat 脚本 续表 目 录 名 简 介  conf  包含不同的配置文件,server.xml(Tomcat 的主要配置文件)和 web.xml  work  存放 jsp 编译后产生的 class 文件  webapps  存放应用程序示例,将要部署的应用程序会放到此目录  logs  存放日志文件 其中最重要的是位于 conf 子目录下的两个配置文件——server.xml 和 web.xml。  server.xml 是一个 xml 格式的配置文件,它决定了 Tomcat 服务器运行的各项参数。xml  文档由一系列的标记及元素组成。
    • server.xml 的组织结构如下: (1)顶级元素——<Server>是整个配置文件的根元素。Server  元素代表整个  Catalina  servlet  容器。因此,它必须是  conf/server.xml  配置文件中的最外层元素。其属性代表了整个  servlet 容器的特性, 而<Service>代表与一个引擎 (Engine)相关联的一组连接器 (Connectors); 也就是说,Server 包含了一个或者多个连接器(Connectors) ,以及一个引擎(Engine) ,它负 责处理所有 Connector 所获得的客户请求。 (2)连接器(Connectors)——代表外部客户之间的接口。外部客户向特定的  Service  发送请求,并接收响应。一个  Connector  将在某个指定端口上侦听客户请求,并将获得的请 求交给 Engine 来处理,从 Engine 处获得回应并返回客户。Tomcat 有两个典型的 Connector, 一个直接监听来自 browser 的 http 请求,一个监听来自其他 Web Server 的请求: Ø  Coyote Http/1.1 Connector 在端口 8080 处侦听来自客户 browser 的 http 请求。 Ø  Coyote JK2 Connector 在端口 8009 处侦听来自其他 WebServer(Apache)的 servlet/jsp  代理请求。 (3)容器——代表一些组件,包括:引擎(Engine) 、Host 和 Context。这些组件的功能 是处理进来的请求,生成对应的响应。 Ø 引擎 (Engine) 处理一个 Service 的所有请求。  Engine 下可以配置多个虚拟主机 (Virtual  Host) ,每个虚拟主机都有一个域名。当 Engine 获得一个请求时,它把该请求匹配到 某个 Host 上,然后把该请求交给该 Host 来处理。Engine 有一个默认虚拟主机,当请 求无法匹配到任何一个 Host 上的时候,将交给该默认 Host 来处理。 Ø  Host 处理一个特定虚拟主机的所有请求。 它代表一个虚拟主机, 每个虚拟主机和某个 网络域(Domain  Name)相匹配。每个虚拟主机下都可以部署(deploy)一个或者多 个 Web 应用程序 (Web App) ,每个应用程序对应于一个 Context 和一个 Context path。 当  Host  获得一个请求时,将把该请求匹配到某个  Context  上,然后把该请求交给该  Context 来处理。匹配的方法是“最长匹配” ,所以一个 path 值为""的 Context 将成为 该 Host 默认的 Context,所有无法和其他 Context 的路径名匹配的请求都将最终和该 默认 Context 匹配。 Ø  Context 处理某个特定 Web 应用的所有请求。 一个 Context 对应于一个 Web 应用程序, 一个 Web 应用程序由一个或者多个 Servlet 组成。  Context 在创建的时候将根据配置文 件  CATALINA_HOME/conf/web.xml  和  WEBAPP_HOME/WEB­INF/  web.xml  载入  Servlet 类。当 Context 获得请求时, 将在映射表 (mapping table)中寻找相匹配的 Servlet  类,如果找到,则执行该类,并获得请求的回应,返回相应的结果。 对于 server.xml 文档中的每个元素,对应的文档按照如下方式组织: (1)概述:对这个特定组件的整体描述。每个组件在  org.apache.catalina  包中存在一个 对应的 Java 接口,可能有一个或多个标准实现了这个接口。 (2)属性:该元素的合法属性。一般来说,这又分成公共属性和标准实现属性。公共 属性是所有实现了该 Java  接口的实现都具有的属性。标准实现属性是实现了该 Java  接口的 某个特定 Java 类具有的属性。 (3)嵌套组件:列举了可以合法地嵌在这个元素下的组件。有些元素可以嵌入任何容 器,而另一些元素只能嵌入在 Context 中。 (4)专有特性:描述了该接口的标准实现支持的专有特性的配置,与每个元素类型有 关。 接着简要介绍 Context 的部署配置文件 web.xml,前面我们说过,Context 处理某个特定  web 应用的所有请求。一个 Context 对应于一个 Web 应用程序,一个 Web 应用程序由一个或 者多个 Servlet 组成。当一个 Web App 被初始化的时候,它将用自己的 ClassLoader 对象载入
    • 部署配置文件“web.xml”中定义的每个  servlet  类。它首先载入在  CATALINA_HOME/  conf/web.xml 中部署的 servlet 类, 然后载入在自己的 Web App 根目录下的 WEB­INF/web. xml  中部署的 servlet 类。  web.xml 文件由两部分组成: Ø  servlet 类定义 Ø  servlet 映射定义 每个被载入的 servlet 类都有一个名字,且被注册在该 Context 的映射表(mapping table) 中,和某种 URL  PATTERN 对应,当该 Context 获得请求时,将查询映射表,找到被请求的  servlet,并执行以获得请求回应。 了解了 server.xml 和 web.xml 的结构后,我们就可以来研究一下 Tomcat 的启动过程了。 因为 server.xml 决定了 Tomcat 服务器是如何启动的, 所以我们还是继续关注 server.xml 文档, 通过它来研究 Tomcat 的启动过程。这里我们介绍其中最重要的配置语句: <Server port="8005" shutdown="SHUTDOWN"> 启动 Server,在端口 8005 处监听关闭命令,如果接收到“SHUTDOWN”字符串则关闭 服务器。 <Listener className="org.apache.catalina.mbeans.ServerLifecycleList  ener" />  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLife­  cycleListener" /> 如果一个 Java 对象需要知道 Context 什么时候启动,什么时候停止,可以在这个对象中 嵌套一个 Listener 元素。该 Listener 元素必须已实现了 org.apache.catalina.  Lifecycle  Listener  接口,在发生对应的生命期事件的时候,通知该 Listener。注意,一个 listener 可以具有任意 多的附加属性。属性名与 JavaBean 的属性名相对应,使用标准的属性命名方法。 <!­­ Global JNDI resources ­­>  <GlobalNamingResources>  <!­­ Test entry for demonstration purposes ­­>  <Environment name="simpleValue" type="java.lang.Integer" value="30"/>  <!­­ Editable user database that can also be used by  UserDatabaseRealm to authenticate users ­­>  <Resource name="UserDatabase" auth="Container"  type="org.apache.catalina.UserDatabase"  description="User database that can be updated and saved"  factory="org.apache.catalina.users.MemoryUserDatabase Factory"  pathname="conf/tomcat­users.xml" />  </GlobalNamingResources>  GlobalNamingResources 定义了服务器的全局 JNDI 资源。  JNDI(The Java Naming and Directory Interface,Java 命名和目录接口)是一组在 Java 应 用中访问命名和目录服务的  API。命名服务将名称和对象联系起来,使得我们可以用名称访 问对象。目录服务是一种命名服务,在这种服务里,对象不但有名称,还有属性。 命名或目录服务可以集中存储共有信息,这一点在网络应用中很重要,使应用更协调、 更容易管理。例如,数据库服务设置在目录服务中,以便被有关的数据库应用程序所使用。  JNDI 允许访问文件系统中的文件,定位远程 RMI 注册的对象,访问像 LDAP 这样的目 录服务,定位网络上的 EJB 组件等。JNDI 架构提供了一组标准的独立于命名系统的 API,这 些 API 构建在与命名系统有关的驱动之上。这一层有助于将应用与实际数据源分离,因此不 管应用访问的是 LDAP、RMI、DNS,还是其他的目录服务。换句话说,JNDI 独立于目录服
    • 务的具体实现,只要有目录的服务提供接口(或驱动)就可以使用目录。 我们可以在 GlobalNamingResources 中嵌套<Environment>元素,配置命名的值,这些值 作为环境条目资源(Environment Entry Resource),对整个 Web 应用可见。例如,可以按照如 下方法创建一个环境条目: <GlobalNamingResources ...>  ...  <Environment name="maxExemptions" value="10"  type="java.lang.Integer" override="false"/>  ...  </GlobalNamingResources> 这与在/WEB­INF/web.xml 中包含如下元素是等价的: <env­entry>  <env­entry­name>maxExemptions</param­name>  <env­entry­value>10</env­entry­value>  <env­entry­type>java.lang.Integer</env­entry­type>  </env­entry>  <Environment>元素的有效属性如表 4.2 所示。 表 4.2  <Environment>元素的有效属性 属 性 环境条目的文字描述(可选)  name  环境条目的名称,相对于 java:comp/env context  override  如果不希望/WEB­INF/web.xml 中具有相同名称<env­entry>覆盖指定的值,设为 false。默认值为 true  type  环境条目 Java 类名的全称。在/WEB­INF/web.xml 中,  <env­entry­type>必须是如下的值:  java.lang. Boolean,  java.lang.Byte, java.lang.Character, java.lang.Double, java.lang.Float, java.lang.Integer, java.lang. Long, java.lang.  Short, or java.lang.String  value  通过 JNDI context 请求,返回给应用的参数值。该值必须转换成 type 属性定义的 Java 类型 资源参数用来配置资源管理器(resource  manager  或对象工厂  object  factory) 。在  JNDI  查找时,资源管理器返回查找的对象。在资源可以被访问之前,对<Context>或<Default  Context>元素的每个<Resource>元素, 或者/WEB­INF/web.xml 中定义的每个<resource­ ref>或  <resource­env­ref>元素,都必须定义资源参数。 资源参数是用名称定义的,使用的资源管理器(或者 object  factory)不同,参数名称的 集合也不一样。这些参数名与工厂类的 JavaBeans 属性相对应。JNDI 实现通过调用对应的  JavaBeans 属性设置函数来配置特定的工厂类,然后通过 lookup()调用使得该实例可见。 一个 JDBC 数据源的资源参数可以按照如下方式定义: <GlobalNamingResources ...>  ...  <ResourceParams name="jdbc/EmployeeDB">  <parameter>  <name>driverClassName</name>  <value>org.hsql.jdbcDriver</value>  </parameter>  <parameter>  <name>driverName</name>  </value>jdbc:HypersonicSQL:database</value>  </parameter>  <parameter>  <name>user</name>  <value>dbusername</value>  </parameter>  <parameter>  <name>password</name>
    • <value>dbpassword</value>  </parameter>  </ResourceParams>  ...  </GlobalNamingResources> 如果需要为某个特定的资源类型指定工厂内的 Java 类名,在<ResourceParams>元素中嵌 套一个叫做 factory 的<parameter>条目,如表 4.3 所示。 表 4.3  在<ResourceParams>元素中嵌套 属 性 环境条目的文字描述(可选)  配置的资源名称,相对于 java:comp/env  context。这个名称必须与 CATALINA_HOME/conf/server.xml 中 name  某个<Resource>元素定义的资源名称匹配,或者在/WEB­INF/web.xml  中通过<resource­ref>或者<resource  ­env­ref>元素应用 这段配置保持默认即可。 <Service name="Catalina">  Tomcat 的 Standalone  Service。Service 是一组 Connector 的集合,它们共用一个 Engine  来处理所有 Connector 收到的请求。这里定义 Service 的名称为“Catalina” 。 <Connector port="8080" maxThreads="150"  minSpareThreads="25" maxSpareThreads="75"  enableLookups="false" redirectPort="8443" acceptCount="100"  connectionTimeout="20000" disableUploadTimeout="true" /> 分别说明下列参数: Ø  port:在端口号 8080 处侦听来自客户 browser 的非 SSL 的 HTTP1.1 请求。 Ø  minSpareThreads:该  Connector  先创建  25  个线程等待客户请求,每个请求由一个线 程负责。 Ø  maxSpareThreads:当现有线程不够服务客户请求时,若线程总数不足 75 个,则创建 新线程来处理请求。 Ø  acceptCount:当现有线程已经达到最大数 100 时,为客户请求排队。当队列中请求数 超过 100 时,后来的请求返回 Connection refused 错误。 Ø  redirectport:当客户请求是 https 时,把该请求转发到端口 8443 去,基于 SSL 的 http  请求将会被监听 8443 端口的 Connector 所处理。 Ø  enableLookups:默认为 true。当 Web 应用程序调用 request.getRemoteHost()进行 DNS  查询时,可以获得远程客户端的实际主机名,若为 false 则不进行 DNS 查询,而是返 回其 Ip 地址。因为获取远程客户端的实际主机名对 Web 服务器的性能有很大影响, 所以一般设置为 false。 Ø  connectionTimeout:指定超时的时间数(以毫秒为单位) ,如果不想设定超时的时间 数,则将其值设定为0即可。 <Engine name="Catalina" defaultHost="localhost">  Engine 元素代表与特定的 Catalina Service 相关的所有请求处理机制。 它从一个或者多个  Connector 接受请求并处理, 将完整的响应返回给 Connector, 由 Connector 最终传回给客户端。  defaultHost 指定默认的处理请求的主机名,默认虚拟主机是本地机(localhost) 。 每个 Service 元素必须有且仅有一个 Engine 元素,Engine 元素跟在对应的 Connector 元 素的后面,所有的 Engine 支持如表 4.4 所示的属性。 表 4.4  Engine 支持的属性
    • 属 性 描 述  backgroundProcessorDelay  这个值代表在该 engine  及其子容器(包括所有的  wrappers)上调用  backgroundProcess 方 法的延时,以秒为单位。如果延时值非负,子容器不会被调用,这意味着子容器使用自己的 处理线程。如果该值为正,会创建一个新的线程。在等待指定的时间以后,该线程在 Engine  及其子容器上调用 backgroundProcess 方法。如果没有指定,默认值为 10,代表 10 秒的延时  className  实现的 Java 类名。该类必须实现 org.pache.catalina.Engine 接口。如果没有指定,使用标准 实现  defaultHost  默认主机名。默认主机用来处理不能匹配的任何  Context  路径请求。嵌套在  Engine  中的  Host 元素中,必须有一个 Host 的 name 属性与该名称匹配  jvmRoute  实现了动态负载均衡,并且实现了会话绑定机制,即用户第一次访问的会话将绑定在首次 访问的 tomcat 上,防止会话信息丢失  name  Engine 的逻辑名,用在日志和错误消息中  debug  与这个 Engine 相关联的 Logger 记录的详细程度。数字越大,输出越详细。如果没有指定, 该值为 0  可以在 Engine 元素中嵌入一个或者多个 Host 元素,每个 Host 元素代表一个不同的虚拟 主机。至少需要定义一个  Host,并且嵌套的  Host  元素中,必须有一个名称与上面指定的  defaultHost 属性相匹配。 也可以在 Engine 元素中嵌入一个 DefaultContext 元素(可选) 用来定义自动发布的 Web  , 应用的默认属性。 下列元素可以嵌套在 Engine 元素中,但每种元素至多只能嵌套一次: Ø  Logger:配置一个  logger,用来接收和处理  Engine  的所有日志消息,还包括与这个  Engine 相关的所有 Connector 的消息。另外,Engine 还负责记录嵌套的所有 Host 和  Context 的消息,除非被低层的 Logger 配置覆盖。 Ø  Realm: 配置一个 Realm,允许用户数据库, 以及用户相关角色在所有的 Host 和 Context  之间共享,除非被低层的 Realm 配置覆盖。 <Realm className="org.apache.catalina.realm.UserDatabaseRealm"  resourceName="UserDatabase"/>  Realm 元素是一个包含用户名、密码和用户角色的数据库。角色与 Unix 的 group 类似。  Realm 的不同实现允许将 Catalina 集成认证信息加载到已经被创建和维护的环境中,然后利 用这些信息来实现容器管理的安全性(Container Managed Security) 。 所有 Realm 的实现都支持如表 4.5 所示的属性。 表 4.5  所有 Realm 的实现支持的属性 属 性 描 述  className  实现的 Java 类名。这个类必须实现 org.apache.catalina.Realm 接口 和大多数 Catalina 组件不一样的是,  Realm 有几个标准的实现。所以,必须使用 className  属性来选择读者希望使用的实现。 JDBC Database Realm (org.apache.catalina.realm. UserDatabaseRealm)  JDBC Database Realm 将 Catalina 连接到一个在 JNDI 中注册名字为 UserDatabase 的关系 数据库,通过正确的 JDBC 驱动、访问、用来查询用户名、密码和它们相关的角色。由于查 询是在每次必要的时候完成的,因此数据库的改变会马上反映到用来认证新登录的信息中。 <Host name="localhost" appBase="webapps"  unpackWARs="true" autoDeploy="true"
    • xmlValidation="false" xmlNamespaceAware="false">  Host  元素代表一个虚拟主机,虚拟主机将服务器的域名(比如  www.mycompany.com) 和运行  Catalina  的某个特定服务器联系起来。为了生效,网络名称必须在管理所在  Internet  域的 DNS 服务器进行注册。 在许多情况下,系统管理员可能希望为同一个虚拟主机或应用关联多个域名(比如  www.  mycompany.com 和 company.com) 。这可以利用下面讨论的 Host Name Alias 特征来完成。 在 Engine 元素中可以嵌套一个或多个 Host 元素。在 Host 元素中可以嵌套 context 元素。 在与每个 Engine 相关联的所有 Host 中,必须有一个 Host 的名称与 Engine 的 defaultHost 属 性相匹配。Host 的相关属性如表 4.6 所示。 表 4.6  Host 的相关属性 属 性 描 述  appBase  虚拟主机的 Application Base 目录。这是在该虚拟主机上发布 Web 应用的目录路径。可以 指定绝对路径,或者使用相对于 CATALINA_HOME 的路径  autoDeploy  这个标志表示,在 Tomcat 运行的时候,放到 appBase 目录下的新的 Web 应用程序。默认 为 true。这方面的更多信息请参考应用自动发布  backgroundProcessorDelay  这个值代表在该  host  及其子容器(包括所有的  wrappers)上调用  backgroundProcess 方法 的延时,以秒为单位。如果延时值非负,子容器不会被调用,这意味着子容器使用自己的处 理线程。如果该值为正,会创建一个新的线程。在等待指定的时间以后,该线程在  Host  及 其子容器上调用 backgroundProcess 方法。Host 使用后台处理与 Web 应用实时发布有关的操 作。如果没有指定,默认值是-1,说明 host 依赖其所属的 Engine 的后台处理  className  实现的 Java 类名。该类必须实现 org.apache.catalina.Host 接口。如果没有指定,使用标准实 现  deployOnStartup  此标志表明该 host 的 Web 应用是否由 host configurator  自动发布。默认为 true。这方面的 更多信息请参考自动应用发布  name  虚拟主机的域名,也就是在 DNS 服务器上注册的名称。嵌套在 Engine 的所有 Host 中,必 须有一个 Host 的名字与 Engine 的 defaultHost 属性相同。如果要为同一个虚拟主机指定多个 域名,可以使用主机别名  debug  与 Engine 相关联的 Logger 的调试信息的详细程度。数字越大,输出越详细。如果没有指 定,默认值为 0  deployXML  如果不想使用  Context  XML  配置文件来发布  Web  应用,设为  false。同时也失去了利用  manager  应用程序安装  Web  应用或者“.war”文件的能力(这些  Web 应用或.war  文件不在  Host 的配置基准目录  $CATALINA_HOME/conf/[engine_name]/[host_name]下面)Web  应用使用  catalina  的安全 许可发布,如果需要让不可信的用户管理 Web 应用,这个值可以设为 false。默认为 true  errorReportValveClass  Host 使用的错误报告 valve 的 Java 类名。这个 valve 的责任是输出错误报告。设置这个值 可以定制  Tomcat  产生错误页面的格式。这个类必须实现  org.apache.catalina.Valve  接口。如 果没有指定,则使用默认值  org.apache.catalina.valvees.ErrorReportValve.  unpackWARs  如果希望将位于 appBase 目录下的 WAR 文件解压缩成对应的目录结构,设为 true。如果 希望直接从 WAR 文件运行 Web 应用,设为 false。更多信息请参考应用自动发布 续表 属 性 描 述  workDir  Host 的 Web 应用使用的临时目录的路径。每个应用都有自己的子目录,用于临时的读写。
    • 如果在  Context  中设置了  workDir  属性,它将会覆盖  Host  的  workDir  属性。如  Servlet  Specification 中所述,通过 servlet context 的属性 javax.servlet.context.tempdir,这个目录可以 被 servlet 使用。如果没有指定,使用$CATALINA_HOME/work 下面合适的目录 我们可以在 Host 元素中嵌套一个或者多个 Context 元素,每个 Context 元素代表这个虚 拟主机下的一个不同的 Web 应用。同时,可以嵌套一个 DefaultContext 元素,用来定义后续 发布的 Web 应用的默认值。也可以在 Host 元素中选择嵌套一个 DefaultContext 元素,用来定 义自动发布的 Web 应用的默认特性。 下列元素可以嵌套在 Host 元素中,但至多只能嵌套一个实例: Ø  Logger——配置一个 logger,用来接收和处理 Host 的所有日志消息,以及这个 Host  的所有 Context 的日志消息(除非被低一级的 Logger 配置覆盖)。 Ø  Realm——配置一个 realm,Realm 的用户数据库,以及用户角色被这个 Host 的所有  Context 共享(除非被低一级的 Realm 配置覆盖)。  2.应用自动发布 如果没有改变 Tomcat 默认的 deployOnStartup 属性(默认为 true) ,当 Catalina 第一次启 动时,会自动采取如下步骤: (1) 假定 CATALINA_HOME/conf/[engine_name]/[host_name]目录中的任何 XML 文件都 包含一个 Context 元素(以及它相关的子元素) ,通常情况下,这个<Context>的 docBase 属性 指向一个 Web 应用目录的绝对路径,或者是 WAR 文件的绝对路径。 (2)如果  WAR  文件对应的目录不存在,则这个  WAR  文件会被自动展开,除非  unpackWARs 属性设为 false。在重新发布更新后的 WAR 文件时,重新启动 Tomcat 之前一定 要删除展开后的目录,这样更新后的 WAR 文件才会被重新展开(如果使用 auto deployer,它 会自动完成这项工作) 。 (3)application base 目录下的任何子目录,如果包含/WEB­INF/web.xml 文件,Tomcat  认为是一个展开后的 Web 应用,会为这个目录自动产生一个 Context 元素,即使该目录没有 在 conf/server.xml 文件中出现。产生的 Context 会使用 DefaultContext 中的属性来配置。自动 产生的 Context 的 context 路径是“/”后面跟上目录名,除非目录名是 ROOT,这种情况下  context 路径是空字符串( ) “”。 除了启动时的自动发布以外,在 Tomcat 运行时,当新的 XML 配置文件,WAR 文件或 者子目录(包含新的  Web  应用)放到  appBase  目录下,或者当  XML  配置文件放到  该 CATALINA_HOME/conf/[engine_name]/[host_name]目录的时候, Web 应用被自动发布。  auto  deployer 也负责跟踪 Web 应用的如下变化: (1)如果更新了 WEB­INF/web.xml 文件,会触发 Web 应用的重载。 (2)如果 WAR 文件被更新,并且 WAR 文件已经展开,首先删除展开的 Web 应用,然 后发布更新的 WAR 文件。 (3)如果 XML 配置文件被更新,首先删除该应用(但是不删除任何展开以后的目录) , 然后发布相关的 Web 应用。 其实,对于普通的应用来说,上述配置基本可以保持不变。Tomcat  默认的  Web  应用程 序的根目录文档是位于其安装目录webappsROOT 下。如果希望指定另外一个目录作为我们 的 Web 应用程序的根目录, 该如何修改 server.xml 呢?假设希望将 D:webchart 作为我们 Web  应用程序的根目录,就只需要在 server.xml 的<Host></Host>标记之间,加上以下内容: <Context docBase="d:webchart" path="" debug="0" reloadable="true"  crossContext="true" />  Ø  docBase:应用程序的路径或者是 WAR 文件存放的路径,这里我们指定为 D 盘下的
    • webchart 子目录。 Ø  path:表示此  Web  应用程序的  url  的前缀,这样请求的  url  为  http://localhost:  8080/path/****,注意我们这里的 path 的值为空字符串 (“”) 表示是 ROOT 文档目录。 , Ø  reloadable:该属性非常重要,如果为  true,则  Tomcat  会自动检测应用程序的  /WEB­INF/lib 和/WEB­INF/classes 目录的变化,Tomcat 会自动装载新的应用程序。当 然,如果 jsp 文件也发生变化,Tomcat 也会重新编译该 jsp 文件。这样,我们就可以 在不重新启动 Tomcat 的情况下,让 Tomcat 再次装载应用程序。该特征在开发阶段很 有用,但也大大增加了服务器的开销。因此,在  Web  应用正式发布以后,不推荐使 用。 Ø  crossContext:如果要在应用程序内调用 ServletContext.getContext()来返回在该虚拟主 机上运行的其他 web application 的 request dispatcher, 就需要将其值设为 true。 在安全 性很重要的环境中, 还是设置为 false, 使得 getContext()总是返回 null。默认值为 false。 到这里,Tomcat  的一些设置及工作机制就介绍完了。更多的内容,请读者查阅相关的  Tomcat 文档。 现在来测试我们修改后的 server.xml 是否可以正常工作。首先在 D 盘上新建一个目录:  webchart。然后,将位于chap04Fig4.1Fig.4.1_02目录下的 index.jsp 文件拷贝到 D:webchart  下,重新启动 Tomcat,打开浏览器,并在地址栏输入 http://localhost:8080。如果看到如图 4.12  所示的结果,就说明 Web 应用程序的根目录设置好了。 现在修改 D:webchartindex.jsp 文件的第 25 行~第 27 行的代码,如下所示: 10*6 的积是:&nbsp;<%=getProduct(10,6)%>  <BR><BR>  10/6 的商是:&nbsp;<%=getQuotient(10,6)%> 保存 index.jsp 文件,直接刷新浏览器,可以看到如图 4.13 所示的结果。 可见,在没有重新启动 Tomcat 的情况下,当浏览 index.jsp 文件的时候,Tomcat 检测到  index.jsp 自上次编译以来,源文件发生了变化,于是就自动编译。这样,我们就得到了正确 的结果。 图 4.12  定制 Tomcat  图 4.13  更改后的 index.jsp  讨论完了 server.xml,我们再继续阐述 web.xml 的核心配置。 <servlet>  <servlet­name>default</servlet­name>  <servlet­class>org.apache.catalina.servlets.DefaultServlet  </servlet­class>  <init­param>  <param­name>debug</param­name>  <param­value>0</param­value>  </init­param>  <init­param>  <param­name>listings</param­name>  <param­value>true</param­value>
    • </init­param>  <load­on­startup>1</load­on­startup>  </servlet> 当用户的 HTTP 请求无法匹配任何一个 servlet 的时候,该 servlet 被执行。 <servlet>  <servlet­name>invoker</servlet­name>  <servlet­class>  org.apache.catalina.servlets.InvokerServlet  </servlet­class>  <init­param>  <param­name>debug</param­name>  <param­value>0</param­value>  </init­param>  <load­on­startup>2</load­on­startup>  </servlet> 处理一个  Web  应用程序中的匿名  servlet。当一个  servlet  被编写并编译放入/WEB­  INF/classes/中,却没有在/WEB­INF/web.xml 中定义的时候,该 servlet 被调用,把匿名 servlet  映射成/servlet/ClassName 的形式。传统上这个 servlet 的 URL 被映射为“/servlet/*” 。 <servlet>  <servlet­name>jsp</servlet­name>  <servlet­class>org.apache.jasper.servlet.JspServlet</servlet­  class>  <init­param>  <param­name>fork</param­name>  <param­value>false</param­value>  </init­param>  <init­param>  <param­name>xpoweredBy</param­name>  <param­value>false</param­value>  </init­param>  <load­on­startup>3</load­on­startup>  </servlet> 当请求的是一个 JSP 页面的时候, (*.jsp)将调用该 JSP 页面的 Servlet。它说明将客户 端请求的 JSP 页面首先编译成为 Servlet,然后再执行。这种方式也就是 Tomcat 对 JSP 页面 提供支持的机制。其实所有的 JSP/Servlet 服务器对 JSP 的工作机制都是一样的,即 JSP 会被  JSP/Servlet 服务器首先自动编译成 Serlvet,然后以 Serlvet 的方式工作。 <welcome­file­list>  <welcome­file>index.html</welcome­file>  <welcome­file>index.htm</welcome­file>  <welcome­file>index.jsp</welcome­file>  </welcome­file­list> 设定网站索引文件的优先级。当客户端的请求没有指明是访问哪个页面文件的时候,默 认首先返回 index.html 文件,其次是 index.htm 文件,最后才是 index.jsp 文件。 如果希望默认最先返回的页面索引文件是 index.jsp,则只需要将 index.jsp 的位置放在第 一个 welcome­file 的位置上即可。 最后谈谈 Tomcat 服务器处理一个 http 请求的全过程: 假设来自客户的请求为: http://localhost:8080/chart/chart_index.jsp (1)http 请求被发送到本机端口 8080,被在那里监听的 Coyote  HTTP/1.1  Connector 获 得。 (2)Connector 把该请求交给它所在的 Service 的 Engine 来处理,并等待来自 Engine 的 回应。
    • (3)Engine 获得请求 localhost/chart/chart_index.jsp,匹配它所拥有的所有虚拟主机 Host。 (4)Engine 匹配到名为 localhost 的 Host(即使匹配不到也把请求交给该 Host 处理,因 为该 Host 被定义为该 Engine 的默认主机) 。 (5)localhost Host 获得请求/chart/chart_index.jsp,匹配它拥有的所有 Context。 (6)Host  匹配到路径为/chart  的  Context(如果匹配不到就把该请求交给路径名为""的  Context 去处理) 。 (7)path="/chart"的 Context 获得请求/chart_index.jsp,在它的 mapping table 中寻找对应 的 servlet。 (8)Context 匹配到 URL PATTERN 为*.jsp 的 servlet,对应于 JspServlet 类。 (9)构造 HttpServletRequest 对象和 HttpServletResponse 对象,作为参数调用 JspServlet  的 doGet 或 doPost 方法。 (10)Context 把执行后的 HttpServletResponse 对象返回给 Host。 (11)Host 把 HttpServletResponse 对象返回给 Engine。 (12)Engine 把 HttpServletResponse 对象返回给 Connector。 (13)Connector 把 HttpServletResponse 对象返回给客户 browser。 为进一步了解 JSP 的工作原理,我们以 index.jsp 为例,来阐述 Tomcat 对其编译后的结 果。  4.1.4  转换后的 JSP 页面文件 在 Tomcat 的安装目录下 (本例:C:tomcat5.5) 的 workCatalinalocalhost_orgapache jsp  子目录中,可以找到名为 index_jsp.java 的 java 文件及其编译后的 index_jsp.class 的字节码文 件。上节所讲述的 index.jsp 文件首先被改写为:  index_jsp.java,然后再被编译成字节码的 class  文件。 所以, 当客户端第一次请求 JSP 页面时, 需要一点时间等待, 当该 JSP 页面被编译成 class  文件后,只要 JSP 源文件没有改变,当再次请求该 JSP 页面时,就不再有任何延时了。 1: package org.apache.jsp;  2:  3: import javax.servlet.*;  4: import javax.servlet.http.*;  5: import javax.servlet.jsp.*;  6:  7: public final class index_jsp extends org.apache.jasper.runtime.  HttpJspBase  8:  implements org.apache.jasper.runtime.JspSourceDependent {  9:  10:  11:  int getProduct(int a, int b)  12:  {  13:  14:  return a * b;  15:  }  16:  17:  float getQuotient(int a, int b)  18:  {  19:  return (float)(a) / b;  20:  }  21:  22:  private static java.util.Vector _jspx_dependants;  23:  24:  public java.util.List getDependants() {  25:  return _jspx_dependants;  26:  }  27:
    • 28:  public void _jspService(HttpServletRequest request, HttpServlet  Response response)  29:  throws java.io.IOException, ServletException {  30:  31:  JspFactory _jspxFactory = null;  32:  PageContext pageContext = null;  33:  HttpSession session = null;  34:  ServletContext application = null;  35:  ServletConfig config = null;  36:  JspWriter out = null;  37:  Object page = this;  38:  JspWriter _jspx_out = null;  39:  PageContext _jspx_page_context = null;  40:  41:  42:  try {  43:  _jspxFactory = JspFactory.getDefaultFactory();  44:  response.setContentType("text/html;charset=GB2312");  45:  pageContext = _ jspxFactory. getPageContext (this, request,  response,  46:  null, true, 8192, true);  47:  _jspx_page_context = pageContext;  48:  application = pageContext.getServletContext();  49:  config = pageContext.getServletConfig();  50:  session = pageContext.getSession();  51:  out = pageContext.getOut();  52:  _jspx_out = out;  53:  54:  out.write("<!­­rn");  55:  out.write("tFig. 4.01_02: index.jsprn");  56:  out.write("t 功能: 测试修改后的 server.xml 及 Tomcat 是否正常工作 rn");  57:  out.write("­­>rn");  58:  out.write("rn");  59:  out.write("rn");  60:  out.write("<HTML>rn");  61:  out.write("<HEAD>rn");  62:  out.write("<TITLE> 测试修改后的 server.xml 及 Tomcat 是否正常工作 </TITLE>rn");  63:  out.write("</HEAD>rn");  64:  out.write("rn");  65:  out.write("<BODY>rn");  66:  out.write("rn");  67:  out.write("20*6 的积是:&nbsp;");  68:  out.print(getProduct(20,6));  69:  out.write("rn");  70:  out.write("<BR><BR>rn");  71:  out.write("20/6 的商是:&nbsp;");  72:  out.print(getQuotient(20,6));  73:  out.write("rn");  74:  out.write(" rn");  75:  out.write("</BODY>rn");  76:  out.write("</HTML>rn");  77:  } catch (Throwable t) {  78:  if (!(t instanceof SkipPageException)){  79:  out = _jspx_out;  80:  if (out != null && out.getBufferSize() != 0)  81:  out.clearBuffer();  82:  if (_jspx_page_context != null) _jspx_page_context. handle  PageException(t);  83:  }  84:  } finally {  85:  if (_jspxFactory != null)_jspxFactory.releasePageContext (_  jspx_page_context);  86:  }  87:  }  88: }
    • index_jsp.java 的第 11 行~第 20 行、第 44 行,以及第 54 行~第 76 行就是被 Tomcat 改 写的 index.jsp 文件。 该转换过程是由 Tomcat 自动进行的,我们只需要注意 JSP 源文件的编写就可以了。 至此,Tomcat  运行机制和配置就介绍完了。本书以后的例程,将以本节介绍的  Tomcat  5.5.7 为 JSP/Servlet 服务器。  4.2  Resin 的安装和配置  Resin 是由 Caucho 公司开发的目前运行速度最快的 JSP/Servlets 运行平台。Resin 是一款 十分出色的、商业级的、支持 JSP/Servlets/J2EE 的 Web 服务器。笔者一直非常喜欢 Resin, 个人认为它比 Tomcat 更为优秀,尤其对中文的支持,Resin 比 Tomcat 要完善。在我们开发过 的基于 JSP 的 Browser/Server 的 Web 应用程序中,几乎全部都采用了 Resin 作为 JSP/ Servlets  运行平台。Resin 的下载地址是 http://www.caucho.com/download/index. xtp。 利用 Resin 开发或者学习都是免费的,但是如果把 Resin 作为收费产品发布是需要付费 的。以实际的使用效果来看,我们推荐使用 Resin 3.0.11 版。  4.2.1    Resin 的安装  Resin 的安装很简单,把下载后得到的  Resin­3.0.11.zip 文件解压到任何目录下面即可, 如 c:resin­3.0.11。以下的介绍都假设 Resin 安装在 c:resin­3.0.11 目录下。我们可以看到如图  4.14 所示的目录结构。 这里我们讲述最重要的三个目录: (1)bin 子目录:存放启动与关闭 Resin 服务器的可执行文件,3.0.11 版在 bin 子目录只 存放了 Unix 下的 Resin 的启动脚本,此前的版本,把 Windows 下的启动程序也都放在这个 目录中。 (2)conf 子目录:与 Tomcat 的 conf 子目录类似,存放 Resin 服务器启动的配置文件。 (3)doc 子目录:默认的 Web 应用程序根目录。 现在先试着运行 Resin 服务器。因为 Resin 默认的监听端口和 Tomcat 一样,都是 8080, 所以如果 Tomcat 正在运行中,并且没有修改默认的监听端口 8080,那么会和 Resin 发生冲 突,所以,我们先暂时停止 Tomcat 服务器的运行。 首先进入命令行控制台,然后进入  c:resin­3.0.11目录,在该目录执行  httpd,就可以运 行 Resin 服务器了。Resin 服务器的启动过程如下: (1)弹出一个有 start 和 stop 两个单选按钮和一个 Quit 按钮的对话框。 (2)同时出现一个显示 Resin 的运行相关信息的窗口,如图 4.15 所示。
    • 图 4.14    Resin 的目录结构 图 4.15    Resin 的启动过程 我们从图 4.15 中可以看到,默认的 Resin 服务器监听 8080 端口。点选 Stop,可以停止 当前的 Resin 服务器进程;再点选 Start,又可以开启新的 Resin 服务器进程。关闭该对话框, 则回到 Command 控制台命令符的提示状态下。 启动浏览器,输入 http://localhost:8080,我们可以看到如图 4.16 所示的画面。 图 4.16    Resin 的默认启动页面
    • 4.2.2  Resin 的配置 本书是以 Tomcat 作为 JSP/Servlet 的服务器。 其实 Resin 可以说是比 Tomcat 更优秀的  JSP/Servlet 服务器,为了让读者同时体验这两款优秀的 JSP/Servlet 的服务器的运行性能和特 点,我们可以同时安装并运行 Tomcat 与 Resin 服务器,并将它们的 Web 应用程序的根目录 都设置为  D:webchart。这样,读者可以在实际的工作中体会这两者之间的不同。 (注意:为 让读者可以轻松地配置本书的运行环境,故本书不涉及如何将 Resin 和 IIS/Apache 的集成) 。 下面简要介绍一下 Resin 服务器的配置。Resin 服务器和大多数 Java  Web 服务器一样, 通过一个 xml 格式的 conf 文件进行配置。 进入 c:resin­3.0.11conf 目录,打开 resin.conf。先查找到第 70 行: <http server­id="" host="*" port="8080"/> 用以下的方式修改默认的监听端口 8080: <http server­id="" host="*" port="8888"/> 然后修改默认的应用程序的根目录,找到第 223 行: <web­app id='/' document­directory="webapps/ROOT"/> 修改为: <web­app id='/' document­directory=" d:webchart "/> 其他设置保持不变就可以了,现在保存修改后的  resin.conf  文件。我们甚至不用重新启 动 Resin 服务器,Resin 服务器在监测到其配置文件 resin.conf 发生改变后,会立即重新加载 配 置文 件  resin.conf, 并 自动重 新启 动  resin.conf 。 稍等一 会,就 可以 用以下 的方法 :  http://localhost:8888  来测试 Resin 服务器运行是否成功。如果没有再次修改 D:webchart 下的 index.jsp 文件, 那么就应该看到与图 4.13 所示完全一样的结果,这说明我们的修改是成功的。 如果在 NT4 或者 Win2k/XP/2003 环境下,需要把 Resin 设置成一个服务进程,则按照以 下步骤进行: (1)先按本书第 1 章所示的方法再创建一个系统变量,在“变量名”中输入“RESIN_  HOME” ,在“变量值”中输入“c:resin­3.0.11” 。 (2)在 Command 控制台进入 c:resin­3.0.11bin 目录下,键入如下命令: httpd –install ­conf conf/resin.conf 就可以在【控制面板】的【管理工具】的【服务】下面看到新增的一条“Resin Web Server” 的自动服务。以后只要进入 NT4 或者 Win2k/XP/2003,就可以自动启动 Resin 服务。 (3)如果要卸载该服务,只需要在 Command 控制台进入 c:resin­3.0.11bin 目录下,键 入如下命令即可: httpd –remove  Resin 自带的帮助文档已经非常详尽,读者可以自行阅读。  4.3  BEA Weblogic 的安装和配置 应用服务器(Application Server)是运行 Java 企业组件的平台,在开发企业级的 Web 应 用程序方面能提供更灵活强大的支持,构成应用软件的主要运行环境。当前主流的应用服务
    • 器是 BEA 公司的 Weblogic Server 和 IBM 公司的 Websphere,以及免费的 Jboss。我们推荐使 用 Weblogic,因为它的体系结构更加简洁,开发和部署更加方便,是 Java 企业软件开发人员 首选的开发平台。  Weblogic 是由 BEA 公司开发的目前应用最为广泛的应用服务器。尤其在 BEA 网站上注 册后,BEA 公司提供长达一年免费的 Weblogic 使用期限。对于希望学习 J2EE 的读者来说, 这实在是一个好消息。  Weblogic 的下载地址:http://commerce.bea.com/index.jsp。  Weblogic 的在线文档链接地址:http://edocs.bea.com/。  4.3.1    BEA Weblogic 的安装 安装步骤如下所示: (1)双击  platform813_win32_free.exe  安装程序,安装程序将会自解压并开始安装前的 准备工作,如图 4.17 所示。 图 4.17    BEA Weblogic 的安装过程 1  (2)安装前的准备工作结束后,单击【Next】按钮,如图 4.18 所示。 (3)创建一个新的 BEA Home,这里我们选择默认的 C:bea,然后单击【Next】按钮, 如图 4.19 所示。
    • 图 4.18    BEA Weblogic 的安装过程 2  图 4.19  确定 BEA  的 Home 的目录 (4)选择安装类型,这里我们选择完全安装【Complete】 ,然后单击【Next】按钮,如 图 4.20 所示。 (5)选择 Weblogic 的安装目录,这里仍然选择其默认的安装目录,如图 4.21 所示。 图 4.20  选择 BEA Weblogic 的安装组件 图 4.21  选择 Weblogic 的安装目录 (6)然后是相关文件的拷贝画面,如图 4.22 所示。 图 4.22  拷贝相关文件 (7)没有必要安装 XML Spy 这个 XML 编辑程序和运行快速启动(Run QuickStart) ,单 击【Done】结束安装过程,如图 4.23 所示。
    • 图 4.23  结束 BEA Weblogic 的安装  4.3.2  BEA Weblogic 的配置 在 weblogic7.x 之前, 安装完成 weblogic 会自动创建默认的应用目录 DefaultWebApp。 如 果没有特别的需要,就可以利用这个默认的应用目录部署 Web 应用程序或者 J2EE 系统了。 而在  weblogic8.x  之 后版本中,它不会自动创 建 默认的应用目录。所以我们需要使用  Configuration Wizard 或者 QuickStart 来创建自己的应用目录。 (1)依次单击【开始】→【所有程序】→【BEA WebLogic Platform 8.1(BEA Home 1) 】 →【Configuration Wizard】 ,启动【BEA WebLogic Configuration Wizard】 ,会出现如图 4.24 所 示的配置界面,选择【create a new weblogic configuration】 ,单击【Next】按钮。 图 4.24  创建一个新的配置 (2)在【template】选项中选择【base weblogic server domain】 ,然后单击【Next】按钮, 如图 4.25 所示。
    • 图 4.25  选择【base weblogic server domain】 (3)选择快速配置【Express】,然后单击【Next】按钮。 (4)在创建管理员用户名和密码的界面上输入用户名、密码和创建该服务的描述(请 记住这个用户密码,它是启动这个服务和进入服务控制台的账号) ,然后单击【Next】按钮, 如图 4.26 所示。 图 4.26  创建管理员的用户名和密码 (5)选择 JDK 的版本。目前 Weblogic 自带的 JDK 为 1.4.2 版。其又分为两个版本:一 个是由 Weblogic 公司开发的 JRockit 版,另一个是 Sun 公司的 JDK。Weblogic 的 JRockit 版  JDK 对 Sun 公司的 JDK 在内存的垃圾回收上做了优化,性能更加优良。所以,这里我们选择  JRockit。单击【Next】按钮,如图 4.27 所示。
    • 图 4.27  选择 JDK 的版本 提示: 根据我们的测试,目前版本 Weblogic 8.13 对 J2SE5.0 的支持并不好。 即使选择【Other  Java SDK】,并单击旁边的【Browse】按钮,在弹出的对话框中,选择 C 盘下的【jdk1.5.0】 。 但在其后的启动中,却会出现错误而无法启动成功,所以这里我们还是选择 JDK1.4.2 版。 (6)该界面可以修改创建服务的目录和名称,这里我们保持默认值,然后单击【Create】 按钮开始创建。 图 4.28  结束创建服务
    • 4.3.3  测试 BEA Weblogic 的配置 下面启动 Weblogic 来检查配置是否成功。 依次单击 【开始】 →所有程序】【BEA WebLogic  → Platform 8.1(BEA Home 1)】→【User Projects】→【My Domain】→【Start Server】 如果 , 出现如图 4.29 所示的内容,则表示配置成功。 图 4.29  启动 Weblogic  注意,最后两行要包含如下所示的内容: <Server started in RUNNING mode>  <Thread "ListenThread.Default" listening on port 7001, ip address *.*> 现在打开浏览器,输入地址: http://localhost:7001/console 会出现如图 4.30 所示的登录界面,这里需要我们输入刚刚在创建应用目录时输入的用户 名和密码。 图 4.30  登录 Weblogic 服务器的管理控制台 现在将进入 Weblogic 的控制台。接下来部署我们第一个 Web 应用程序。
    • 4.3.4  部署第一个 Web 应用程序 部署步骤如下所示: (1) Web 程序打包。 将 在硬盘上新建一个目录, Weblogic_Builder, 如 然后将 D:web chart  目录下的  index.jsp  文件及  chap04  子目录中的内容全部拷贝到  Weblogic_Builder  下。在  Weblogic_Builder 目录下新建一个子目录 WEB­INF,再在 WEB­INF 子目录下新建一个名为  web.xml 的文档,内容如下: <?xml version="1.0" encoding="ISO­8859­1"?>  <web­app xmlns="http://java.sun.com/xml/ns/j2ee"  xmlns:xsi="http://www.w3.org/2001/XMLSchema­instance"  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee web­app_2_4.  xsd"  version="2.4">  <web­app>  <welcome­file­list>  <welcome­file>index.jsp</welcome­file>  </web­app> 进入命令行控制台,在 Weblogic_Builder 目录下执行以下命令: jar cvf test.war *.* 将 Weblogic_Builder 目录下的所有文件打包成 test.war 文件; (2)登录 Weblogic 的控制台。 (3) 依次单击 【mydomain】 【Deployments】 【Web Application Modules】 【Deploy  → → → a new Web Application Module…】如图 4.31 所示。 图 4.31  在 Weblogic 下部署 Web 应用程序 (4)然后单击【Upload Your File(s)】→【浏览】,选择打包文件 test.war,单击【Upload】 按钮,返回先前的页面,再单击【myserver】 ,如图 4.32 所示。
    • 图 4.32  上传打包好的 test.war 文件 (5)单击【upload】,在出现的页面中选择【test.war】 ,单击【Target Module】 ,如图 4.33  所示。 (6)在出现的页面中单击【Deploy】选项,进行部署。 (7)当看到如图 4.34 的结果,则说明 Web 应用程序部署成功了。 现在,我们启动浏览器,在地址栏中输入 http://localhost:7001/test  图 4.33  选择已经上传到服务器的 test.war 文件 图 4.34    Web 应用程序部署成功的显示页面 或者输入 http://localhost:7001/test/index.jsp, 应该会看到类似图 4.13 所示的结果, 如果在 地址栏输入 http://localhost:7001/test/chap04/Fig4.1/Fig.4.1_01/test.jsp 则会看到类似图 4.11 所示 的结果。Weblogic 下的 Web 应用程序的部署我们就介绍到这里。  4.4  本章小结 本章先阐述了两个轻量级的 JSP/Servlet 的服务器 Tomcat、Resin 的安装和配置过程,然 后讲述了企业级的 J2EE 应用服务器 Weblogic 的安装、配置和 Web 应用程序的部署。 正确地理解和配置 JSP/Servlet 服务器会对我们今后 Web 应用程序的发布带来很大方便。 本书将在后面的 Web 图表例程中,继续介绍相关的配置工作。 在理解本章的基础上, 我们就可以开发一些基于 JSP/Servlet 的 Web 图表应用程序了。下 一章我们将阐述基于 Servlet 的 Web 图表编程技术。
    • 第 5 章 基于 Servlet 的 Web 图表编程 我们在阐述 Tomcat 安装及配置过程的时候,谈论过 JSP 最终都被 JSP/Servlet 服务器转 换成 Java Servlet 而被执行。JSP 为实现 Web 的请求/响应机制提供了一种方便而强大的方法, 并且开发人员无须了解 Servelt 的底层细节。  Servlet 和 JSP 技术一起构成了 Java 2 企业版(Java  2 Enterprise Edition, J2EE)的 Web 层技术。 本章讲述 Web 的请求/响应机制(主要是 get 和 post 请求) 、如何部署 Servlet、如何利用  Servlet 生成 Web 动态图表。  5.1  Servlet 简介及其构架  Servlet 是利用 Java 技术编写的,在 Web 服务器端执行的程序。其功能有些类似于以前 的 CGI 程序,但是两者的性能和功能却不可同日而语。  Servlet 发展到今天,已经是一种非常成熟的技术了。本章的 Servlet 将说明客户与服务器 之间如何通过 HTTP 协议进行通信。客户端向服务器发送 HTTP 请求,Servlet 服务器(引擎  /容器)收到请求后,将该请求传递给相应的 Servlet 进行处理。Servlet 在进行处理的过程中, 可能包括与数据库或其他服务器端组件进行交互,如其他的 Servlet、  (Enterprise  JSP 或者 EJB  JavaBean)。然后,Servlet  将结果返回给客户,该结果通常是以  HTML、XHTML  或  XML  等文档形式返回给客户端并在浏览器中显示结果。也可以返回其他格式的数据,如图像(图 表)及二进制数据等。本章例程的原理就是利用 Java 对图像的处理能力,在服务器端根据相 应的数据而实时生成图像(图表),并将结果以图像的方式返回给客户端,达到生成动态的  Web 图表的目的。  5.1.1  Servlet 的特点 因为 Servlet 是 Java 的一个类,所以它可以借助 Java 在网络、图像、文件、数据库、多 线程、RMI 等领域的强大处理能力来实现多种功能,如本章就是借助 Java 对图像的处理能力 而在 Web 服务器端生成实时图像,并返回给客户端。  Servlet 的特点如下所示: (1)高性能:Servlet 一旦被 JSP/Servlet 容器加载,只要 JSP/Servlet 容器没有重新启动, 它就驻留在内存中,这样,大大加快了反应速度。在传统的 CGI 中,每个请求都要启动一个 新的进程。虽然 CGI 程序本身执行时间较短,但启动进程所需要的开销很可能反而超过实际 执行时间。而使用 Servlet 时,服务器上仅有一个 Java 虚拟机在运行,只有当 Servlet 被调用 时,它才被加载,而且直到  Servlet  更改时,它才会被要求再次加载。在传统  CGI 中,如果 有 N 个并发的对同一 CGI 程序的请求,则该 CGI 程序的代码在内存中重复装载了 N 次;而 对于 Servlet,处理请求的是 N 个线程,只需要一份 Servlet 类代码。在性能优化方面,Servlet  也比 CGI 有着更多的选择,比如缓冲以前的计算结果,保持数据库连接的活动等等。 (2)通过使用 Servlet API,开发人员不必担心服务器的内部运作方式。表格资料、服务 器头、cookies 等都可以通过 Servlet 处理。
    • (3)可移植性:Servlet 是用 Java 编写的,当然就不必担心操作系统或服务器的类型, 能将其从一个服务器移到另一个服务器以供发布,这一优点充分体现了 Java“一次编写,随 处运行”的优越特性。 (4)安全性:Servlet 共有几种不同层次的安全保障: Ø 首先是基于 Java 的安全框架,因为 Servlet 是利用 Java 编写的。 Ø 因 Servlet 是运行于 JSP/Servlet 容器内的,所以,Servlet 也被 JSP/Servlet 容器的安全 机制所保护。 (5)高度可扩展性与模块化:单个的 Servlet 可以完成一个特定的任务,同时,不同的  Servlet 之间可以相互通信、协作以完成复杂的业务逻辑。  5.1.2  Servlet 的接口 从架构上看,所有基于 Servlet 的类都必须实现 Servlet 接口。与前面我们所讲的 Applet  方法类似,Servlet 接口方法是由 Servlet 容器调用的。该接口声明了 5 个方法,如表 5.1 所示。 表 5.1    Servlet 的接口方法 方 法 说 明  void init (ServletConfig config)  在  Servlet  的执行周期中,Servelt  容器调用该方法一次,以初始化  Servlet。  ServletConfig 参数由执行该 Servlet 的 Servlet 容器提供  ServletConfig getServletConfig()  该方法返回实现  ServletConfig 接口的对象引用。该对象提供了对 Servlet 配置 信息的访问, Servlet 的初始化参数和 ServeltContext, ServeltContext 为 Servlet  如 而 提供了对自身环境的访问(即执行该 Servlet 的 Servlet 容器)  String getServletInfo()  该方法由编写 Servlet 的程序员定义,用于返回包含了诸如 Servlet 的作者、版 本号等 Servlet 信息的字符串  void service(ServletRequest request,  Servlet 容器调用该方法,以响应客户端对 Servlet 的请求 ServletResponse response)  续表 方 法 说 明  void destroy()  当 Servlet 容器终止 Servlet 时,将调用该“注销”方法。该方法将释放 Servlet  所使用的资源,诸如打开的文件及数据库连接等  Servlet 部署在 JSP/Servlet 容器内,其生命周期由 JSP/Servlet 容器管理,这一点与我们以 前讨论过的 Applet 的生命周期由 Applet 的容器——浏览器管理一样。 Ø 加载 Servlet:该操作一般是用容器动态执行。有些服务器提供了相应的管理功能,可 以在启动的时候就加载 Servlet 并能够初始化特定的 Servlet。 Ø 创建一个 Servlet 实例。 Ø 调用 Servlet 的 init 方法。 Ø 根据客户端对 Servlet 的请求,调用 service 方法。 Ø 注销 Servlet:容器通过 destroy 方法来注销 Servlet 以释放系统资源。  Servlet 包定义了两个实现 Servlet 接口的抽象类: Ø GenericServlet 类,该包隶属于 javax.servlet 包。 Ø HttpServlet 类,位于 javax.servlet.http 包。 这两个类为  Servlet  接口的所有方法提供了默认的实现。对于开发服务器端的程序员来 说,主要是继承并扩展 GenericServlet 类和 HttpServlet 类,并重写其中的一些或全部方法。  GenericServlet 类也是抽象类,它的 service 方法也是一个抽象方法,所有继承 GenericServlet
    • 的子类必须直接或间接地实现该方法。 HttpServlet 类通过运行 Servlet 接口,实现了 Http 协议 的功能。 每个 Servlet 的核心方法是 service 方法,它接收一个 ServletRequest 对象和一个 Servlet  Response 对象。这些对象提供了对输入和输出流的访问,使 Servlet 可以读取客户的数据,并 向客户发送数据。这些流既可以基于字节,也可以基于字符。如果在 Servlet 执行过程中发生 错误,则会抛出 ServletException 或者 IOException 异常。  5.1.3  HttpServlet 类简介 基于 Web 的 Servlet 通常扩展 HttpServlet 类。HttpServlet 类重写了 service 方法,以区分 从客户端  Web 浏览器中收到的请求。客户端有两种典型的请求类型(方法)——get  方法和  post 方法。 Ø  get 请求从服务器获取(读取)数据,一般用于从服务器获取 HTML 文档或图像。 Ø  post 请求向服务器发送(传送)数据,一般用于向服务器发送信息,如确认信息和用 户提交的表单中的数据。  HttpServlet 类定义了 doGet 和 doPost 方法, 两者分别响应客户端相应的请求类型。  doGet  和 doPost 方法都由 service 方法调用。 当客户端的请求到达服务器端时, 首先调用的是 service  方法。service 方法会判断客户端的请求类型,然后再调用相应的方法来处理这种请求。  doGet 和 doPost 方法是以一个 HttpServletRequest 对象和一个 HttpServletResponse 对象作 为参数,使得客户端和服务器之间能够进行通信和交互。HttpServletRequest 接口方法使访问 请求所提供的数据更容易;  HttpServletResponse 接口的方法则便于将 servlet 的结果返回给 Web  客户。 除此之外,HttpServlet 类还有其他一些方法,如下所示: Ø  doPut 方法:在响应 HTTP 的 put 请求时调用。这种请求通常用来把文件存储在服务 器上。在一些服务器上,由于固有的安全风险,一般不会提供该方法。客户可能在服 务器上放置可执行的应用程序,如果执行该程序, 可能会删除某些重要的文件或者占 用系统资源,可能导致服务器受到破坏。 Ø  doHead 方法:在响应 HTTP 的 head 请求时调用。如果客户只希望得到响应的头部, 诸如响应的内容类型和内容长度,则通常使用这种请求。 Ø  doOptions 方法:在响应 HTTP 的 options 请求时调用。它将服务器支持的 HTTP 选项 信息返回给客户,诸如 HTTP 的版本号(1.0 版或 1.1 版)和服务器支持的请求方法。 Ø  doDelete 方法:是响应 HTTP 的 delete 请求调用。这种请求通常用于从服务器中删除 文件。在有些服务器上,由于固有的安全风险,一般也不提供该方法,因为客户可能 会删除服务器或者应用程序运行所必需的关键文件。 上述 doPut、doHead、doOptions,以及 doDelete 方法在 Web 应用程序中很少提供具体的 实现方法。这些方法都接收类型为 HttpServletRequest 和 HttpServletResponse 的参数,并且都 返回 void。这部分内容也不在本书的讨论范围之内。一般来说,只需要关注 doGet 及 doPost  方法即可。 下面,我们来讨论 HttpServletRequest 接口和 HttpServletReponse 接口。  5.1.4  HttpServletRequest 接口 该接口代表了 HTTP 请求,继承了 ServletRequest 接口。每次调用 HttpServlet 的 doGet  或 doPost 方法时, 都会接收到一个实现 HttpServletRequest 接口的对象。 执行 Servlet 的  Web  服务器创建了一个 HttpServletReques 对象,并将其传递给 Servlet 的 service 方法,再由 service
    • 方法传递给 doGet 和 doPost 方法。该对象包含客户的请求信息。 在 HttpServletRequest 接口中提供了许多方法,使 Servlet 能够处理客户请求。最常用的  getParameter  方法就是获得客户请求中的参数,这个参数是客户端表单中的数据。该接口中 的某些方法继承于 ServletRequest 接口。表 5.2 列出了几个常用的方法。 表 5.2  HttpServletRequest 接口的部分方法 方 法 说 明  获取作为 get 或 post 请求的一部分而发送给 Servlet 的参数的值。参数 name 代表 String getParameter(String name)  参数的名称  Enumeration  该方法返回作为 post 请求的一部分而发送给 Servlet 的所有参数的名称  getParameterNames()  对于一个具有多个值的参数,该方法返回一个字符串数组,字符串数组包含了指 String[]  getParameter(String  定的 Servlet 参数的值。 该参数的值一般是由客户浏览器表单中的 checkbox 或者 select  name)  对象提交的  返回一个由服务器存储在客户端上的  Cookie  对象数组。Servlet  可以使用  Cookie  Cookie[] getCookies()  对象惟一性来识别客户  HttpSession 对象的用途类似于 Cookie 对象,也利用其惟一性来识别客户端。返回 HttpSession  getSession(Boolean  一个与客户端当前浏览器会话相关联的 HttpSession 对象,如果没有给客户端分配一 create)  个 session,则该方法会创建一个 HttpSession 对象(参数为 true 时)  HttpSession getSession()  返回同客户端相关联的 session,如果没有与客户端相关联的 session,返回 null  读者可以在 http://java.sun.com/j2ee/1.4/docs/api/javax/servlet/http/HttpServletRequest. html 中 查阅到 HttpServletRequest 接口的所有方法。  5.1.5  HttpServletResponse 接口 该接口代表了对客户端的 HTTP 响应, 继承了 ServletResponse 接口。 每次调用 Http Servlet  的 doGet 或 doPost 方法时,都会接收到一个实现 HttpServletResponse 接口的对象。 执行 Servlet  的 Web 服务器创建了一个 HttpServletResponse 对象,并将其传递给 Servlet 的 service 方法, 再由 service 方法传递给 doGet 和 doPost 方法。该对象提供了一些方法,使 Servlet 能够显示 地响应客户。其中一些方法继承于 ServletResponse 接口。表 5.3 列出了几个常用的方法。 表 5.3  HttpServletResponse 接口的部分方法 方 法 说 明  void addCookie(Cookie cookie)  用于将 Cookie 添加到对客户的响应的头部。  Cookie 是否能存储在客户端上, 取决于 Cookie 的生存期和客户端是否接受 Cookie  String encodeURL(String url)  使用 URL 和一个 SessionID 重写这个 URL  ServletOutputStream getOutputStream()  获取一个基于字节的输出流,用于将二进制数据发送给客户端  PrintWriter getWriter()  获取一个基于字符的输出流,用于将文本数据发送给客户端  void setCharacterEncoding  设置响应客户端浏览器的字符编码类型。中国一般使用的字符编码类型为 (String charset)  “GB2312”或者“GBK” 续表 方 法 说 明  void setContentType(String type)  设置响应客户端浏览器的 MIME 类型。  MIME 类型帮助浏览器确定如何显示 数据(或者运行其他应用程序来处理数据) 。例如,MIME 类型为“text/html” , 表示 HTML 文档;MIME 类型为“image/jpeg” ,则表示格式为 jpeg 的图像
    • 读者可以在  http://java.sun.com/j2ee/1.4/docs/api/javax/servlet/http/HttpServletResponse.  html  中查阅到 HttpServletResponse 接口的所有方法。  5.2  Servlet 处理 HTTP get 请求  HTTP get 请求的主要目的是获取指定 URL 服务器上的内容——该内容通常是 HTML 或 者 XHTML 文档。图 5.1 演示了在 Tomcat 服务器下处理一个 HTTP get 请求的 Servlet 运行过 程。 图 5.1    Tomcat 服务器下运行 WelcomeServlet 的结果 在用户单击 “获得 HTML 文档” (图 5.1①所示) 将向 URL 为 按钮 , “/welcome”的 Servlet  发送一个 get 请求。weclome  Servlet 响应该请求,为客户端返回一个动态生成的,显示如图  5.1②“您好,欢迎进入 Servlet 图表编程世界”的 XHTML 文档。 如果要使用  get  请求,在客户端  HTML  文档中的  Form  表单中必须指定请求的类型为 “get”,如: <form action = "/welcome" method = "get">  ….  </form> 调用 Servlet 的 WelcomeServlet.html 文档(chap05Fig5.2Fig.5.2_01)在第 12 行~第 19  行: <form action = "/welcome" method = "get">  <p> 点击按钮调用 Servlet  <input type = "submit" value = "获得 HTML 文档" />  </p>  </form> 提供了一个表单(form) 。表单的 action( “welcome”)指定调用的 Servlet 的 URL 路径, 所以,它向 URL 路径为“welcome”的 Servlet 发送请求信息,而表单的 method 指定发送的 请求类型为“get” ,这就会触发服务器上该 Servlet 的 doGet 方法。  WelcomeServlet.html 文档生成如图 5.1①所示的运行结果。 在运行被调用的  Servlet——WelcomeServlet.java(chap05Fig5.2Fig.5.2_01)之前,请先在  Tomcat/Resin  的  Web  应用程序的根目录(本书的  Web  应用程序根目录为  d:webchart)下的  WEB­INF 中新建一个子目录 classes。 然后在 classes 子目录下新建一个 com 子目录, 接着在 com  子目录下新建一个 fatcat 的子目录,最后在 fatcat 的子目录下新建一个 webchart 的子目录。
    • WelcomeServlet.java 程序第 4 行: package com.fatcat.webchart; 使用了 package 语句,说明本源程序要严格地按照 Java Servlet 的规定,将源程序部署在 相应的目录中。将本源程序 WelcomeServlet.java 拷贝到刚创建的 Tomcat 或者 Resin 的 Web  应用程序根目录下的WEB­INFclassescomfatcatwebchart 子目录下,正确拷贝后的目录结构 如图 5.2 所示。 D:  webchart  WEB­INF  classes  com  fatcat  webchart  WelcomeServelt.java  Web.xml 图 5.2    WelcomeServlet 的目录结构 程序第 6 行~第 7 行: import javax.servlet.*;  import javax.servlet.http.*; 引入了 javax.servlet 和 javax.servlet.http 包。一般来说,  Servlet 都需要使用这些包中的类。  javax.servlet.http 包为 Servlet 提供了超类 HttpServlet,用来处理 HTTP get 请求和 post 请求。 该类实现 javax.servlet.Servlet 接口, 并添加了支持 HTTP 协议请求的方法。 所以程序第 10 行: public class WelcomeServlet extends HttpServlet { 定义了类 WelcomeServlet 并继承了 HttpServlet 类,用来实现 Servlet 的功能。 超类  HttpServlet  提供了  doGet  方法来响应  get  请求。其默认功能是指出“Method  not  allowed”错误。在 Internet Explorer 中,这种错误一般表示为一个显示“This page can not be  displayed”的 Web 页面,而在 Netscape Navigator 中,则显示一个“Error:405”的 Web 页面。 程序第 13 行~第 45 行,重载了 doGet 方法,提供了对 get 请求的处理。doGet 方法有两 个参数——1 个是 HttpServletRequest 对象和 1 个 HttpServletResponse 对象。前面讨论过两者 都隶属于 javax.servlet.http 包。  HttpServletRequest 对象代表客户端的请求,  Http ServletResponse  对 象 代 表 服 务 器 对 客 户 端 的 响 应 。 如 果  doGet  方 法 不 能 处 理 客 户 的 请 求 , 则 抛 出  javax.servlet.ServletException 类型的异常。 如果 doGet 方法在流处理过程中, 即从客户端中读 取数据或向客户写数据的处理过程中发生错误,则会抛出 java.io.IO Exception 异常。 为了演示对 get 请求的响应,本例所示的 Servlet 在接收客户端 get 请求后,就创建了一 个 XHTML 文档,程序第 38 行: out.println( "<h1>您好,<br>欢迎进入 Servlet 图表编程的世界</h1>" ); 该文档包含“您好,<br>欢迎进入 Servlet 图表编程的世界”这样一段文本,中间的这个 “<br>”在 HTML 文档中表示换行符。图 5.1②所示的结果可以看到,这段文本分成两行显 示了。该文本就是服务器对客户端的响应。该响应通过  PrintWriter  对象发送给客户,而  PrintWriter 对象是从 HttpServletResponse 对象中获取的。 程序第 17 行: response.setContentType( "text/html;charset=gb2312" ); 
    • 使用  response 对象的  setContenType 方法,设置返回给客户端响应数据内容的类型。客 户端浏览器则根据服务器返回文档类型来处理文档的内容。内容的类型也被称为数据的  MIME(Multipurpose Internet Mail Extension)类型。本例中,返回数据内容的类型为 text/html, 它是通知浏览器响应的一个 XHTML 文档。客户端的浏览器就必须读取文档中的 XHTML 标 记,并根据标记对文档进行格式化处理,然后将文档显示在浏览器中。 程序第 18 行: PrintWriter out = response.getWriter(); 调用 response 的 getWriter 方法,创建一个 PrintWriter 类的实例 out,通过 out 对象,就 可以向客户发送内容。注意前面我们说过,response 的 getWriter 方法获取的是一个基于字符 的输出流,用于将文本数据发送给客户端。所以,如果我们要发送二进制数据,如我们生成 的图表等,则应该使用 getOutputStream 方法,获取 ServletOutputStream 的对象引用。 程序第 23 行~第 42 行,通过调用 out 对象的 println 方法,将字符串写入该对象中,以 创建 XHTML 文档。该方法在其 String 参数后面输出一个换行符。注意,该换行符仅仅出现 在 XHTML 的源文件中。 如果要在 XHTML 的输出结果中输出一个换行符, 则需要在 XHTML  源文件相应的位置有一个“<br>”内容,如程序第 38 行所示。 程序第 43 行,关闭输出流,更新输出缓冲区并将信息发送给客户。整个请求/响应过程 就结束了。 最后,在运行本程序之前,还应该对本 Servlet 进行部署。  JSP/Servlet 容器中的配置文件(也就是所谓的部署描述符)web.xml 中部署这个 Servlet。 配置文件指定 Servlet 的各种配置参数,如: Ø 调用 Servlet 的名称(即 Servlet 的别名) 。 Ø  Servlet 的描述。 Ø  Servlet 完全合法的路径名及类名。 Ø  Servlet 映射(即 JSP/Servlet 容器调用 Servlet 的 URL 路径)。 将 web.xml(chap05Fig5.2Fig.5.2_01)文档拷贝到 Tomcat 或 Resin 的 Web 应用程序根 目录下的WEB­INF子目录下,现在打开 web.xml,我们可以看见第 1 行~第 5 行: <?xml version="1.0" encoding="GB2312"?>  <!DOCTYPE web­app PUBLIC  "­//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"  "http://java.sun.com/j2ee/dtds/web­app_2_2.dtd"> 指定 Web 应用程序部署描述符的文档类型,以及 XML 文件 DTD 位置。表明这是一个 符合 XML 标准的,编码方式为 GB2312 的 xml 文档。  XML 的精髓是允许文档编写者制定基于信息描述、体现数据之间逻辑关系的自定义标记, 为确保文档具有较强的易读性、清晰的语义和易检索性。一个完全意义上的 xml 文档是必须遵 从 XML 语法的,也就是说,它必须遵守文档类型定义 DTD 中已声明的规定。  DTD 定义了 XML 文档的整体结构以及文档的语法。 简而言之,DTD 规定了一个语法分 析器以解释“有效的”XML 文档所需所有规则的细节。  DTD 实际上可以看做是一个或多个 XML 文件的模板,这些 XML 文件中的元素、元素 的属性、元素的排列方式/顺序、元素包含的内容等,都必须符合 DTD 中的定义。  DTD 可以是一个完全独立的文件,也可以在 XML 文件中直接设定。所以,DTD 分为外 部 DTD(在 XML 文件中调用另外已经编辑好的 DTD)和内部 DTD(在 XML 文件中直接设 定 DTD)两种。 我们这里选择的是外部的 DTD 文档。外部 DTD 是一个独立于 XML 的文件,实际上也
    • 是一个文本文件,只是使用.dtd 为文件扩展名。外部 DTD 独立于 XML 文件,它可以供多个  XML 文件使用,就像用同一个模板可以写出多个不同内容的文件一样。 采用外部 DTD 的好处是,该 DTD 是经过众多公司或组织充分讨论后而形成的,语法严 谨,适用性和通用性强。所以我们采用的外部 DTD 是由 Sun 公司制定的,该 DTD 的存放位 置在:http://java.sun.com/j2ee/dtds/web­app_2_2.dtd  DTD 的语法相当复杂,这里我们仅仅做一个简单的介绍。  web.xml  源文件中的第  7  行~第  22  行的元素“web­app”定义了  Web  应用程序中每个  Servlet 的配置信息,以及每个 Servlet 的 Servlet 映射。 第 9 行~第 13 行: <servlet>  <servlet­name>WelcomeServlet</servlet­name>  <description>简单地处理 get 请求的 Servlet</description>  <servlet­class>com.fatcat.webchart.WelcomeServlet</servlet­class>  </servlet> 元素“servlet”描述 Servlet。 第 10 行的元素“servlet­name”为当前 Servlet 定义的一个名称。该 Servlet 的名称被定义 为“WelcomeServlet”。 第 11 行的元素“description”,用来描述当前 Servlet 的一个说明。 第 12 行的元素“servlet­class”,用来指定当前被加载的 Servlet 完整的、合法的路径及类 的名称。Servlet  类的源程序或其编译后的.class  文件,默认地存放在  JSP/Servlet  容器的  WEB­INF/classes 子目录下。前面讲过,源程序 WelcomeServlet.java 第 4 行: package com.fatcat.webchart; 使用了 package 语句,就表示 WelcomeServlet.java 编译后的.class 文件,应该放在  Tomcat  或者 Resin 的 Web 应用程序根目录下WEB­INFclassescomfatcatwebchart 的子目录下。这就 是为什么必须先在WEB­INFclasses 下创建 com、fatcat 和 webchart 这几个子目录的原因,它 们正好一一对应于 package 中 com.fatcat.webchart 的声明。  web.xml 源文件的第 15 行~第 18 行: <servlet­mapping>  <servlet­name>WelcomeServlet</servlet­name>  <url­pattern>/welcome</url­pattern>  </servlet­mapping> 元素“servlet­mapping”指定 Servlet 的映射。第 16 行中的元素“servlet­name”指定被映 射的 Servlet 名称。该名称要与第 10 行,为当前 Servlet 定义的名称相同,所以这里 “servlet­ name” 的值也是 WelcomeServlet。第 17 行中的元素“url­pattern”指定 JSP/Servlet 容器调用 Servlet  的 URL 路径。我们在这里定义 JSP/Servlet 容器调用 Servlet 的 URL 路径映射为“/welcome”。 当浏览器向 JSP/Servlet 服务器 URL 路径为“/welcome”的 Servlet 发出“get”请求时,  JSP/Servlet  服 务 器会将 该请求转 交给名字 为“ WelcomeServlet ” 的  Servlet  处 理 ,因 为 “WelcomeServlet”是别名,实际上最后会由WEB­INFclassescomfatcatwebchart  子目录下 的 WelcomeServlet.class 所处理。 注意:这里我们在映射“URL”的时候,并没有指定 Servlet 所在服务器的地址及端口, 在这种情况下,JSP/Servlet 服务器会默认该 Servlet 位于本地机。 综上所述,在 web.xml 中部署某个 Servlet 的时候,首先声明 Servlet,指定这个 Servelt  的名字和完整的路径及类,然后映射这个 Servlet,最后通过这个映射来访问 Servlet。 经过一系列比较烦琐和复杂的配置后,我们将所有的文件都放置到正确的目录中,现在
    • 就 可 以 启 动 浏 览 器 , 在 地 址 栏 中 输 入 : http://localhost:8080/chap05/Fig5.2/Fig.5.2_01/  WelcomeServlet.html。 就可以看到如图 5.1 所示的画面,单击“获得 HTML 文档”按钮,就可以得到如图 5.1  ②所示的“您好,欢迎进入 Servlet 图表编程世界”XHTML 文档的结果。 提示:如果读者在 Tomcat 的环境下运行本例程,而并没有得到如图 5.1 所示的运行结果, 最简单的解决方法是:请先退出  Tomcat,这时我们在WEB­INFclassescomfatcat  webchart  中 看 见 只 有  WelcomeServlet.java  的 源 程 序 , 说 明 这 时  Tomcat  并 没 有 生 成 相 对 应 的  WelcomeServlet.class 文件。现在再启动 Resin 来运行本例,可立即获得正确的结果。退出 Resin  后 , 我 们 可 以 看 到 在 WEB­INFclassescomfatcatwebchart  子 目 录 下 生 成 了 所 对 应 的  WelcomeServlet.class 文件。因此,这时再启动 Tomcat,因为已经创建了正确的 class 文件, 所以本例现在也能够正确运行了。另一个解决方法是:不借助  Resin 自动编译功能,而直接 在命令提示符下手动将 Servlet 源程序编译成 class 文件。如果希望手动编译 Servlet 源程序, 则需要将  Tomcat  安装目录下的commonlibservlet­api.jar  文件复制到  JDK  安装目录下的  jrelibext  子目录中。然后进入  Servlet  源程序所在的目录,在命令提示符下输入:javac  WelcomeServlet.java 即可。在本书后续讲解的例程中,如果大家在 Tomcat 的环境下无法获得 正确的运行结果,则可以借鉴本例的解决方法。 ” 单击 IE 的【查看】→【源文件】,我们可以看到文档的源代码如图 5.3 所示。 图 5.3    WelcomeServlet 返回的 HTML 源代码 该源代码正是 WelcomeServlet.java 源程序第 23 行~第 42 行, 通过调用 out 对象的 println  方法,响应客户端的“get”请求而返回的文本内容。 提示:如果读者运行本例程的时候,没有看到相同的结果,请首先检查文件是否按本例 程的介绍拷贝到正确的目录。 当确定文件都已经拷贝到正确的位置后, 仍无法得到正确结果, 请读者重新启动 Tomcat 服务器。因为在修改 web. xml 部署描述符后,必须重新启动 Tomcat  服务器,否则 Tomcat 将不能识别新的 Web 应用程序,而 Resin 服务器则不需要重新启动。 实际上,我们不必通过图 5.1①中所示的 HTML 文档来调用该 Servlet。只要启动 IE,在 浏览器中直接输入如下的 URL:http://localhost:8080/welcome,就可以调用该 Servlet 了,这 种方式同样向服务器发送了一个“get”请求——与通过图  5.1①中所示  HTML  文档调用该  Servlet 的方法完全一致。 通过本例介绍的配置 Servlet 方法,也同样适用于 Resin 服务器。我们在浏览器的地址栏 中输入 http://localhost:8888/chap05/Fig5.2/Fig.5.2_01/WelcomeServlet.html,就可以看到如图 5.4  所示的运行结果。
    • 图 5.4  Resin 服务器下运行 WelcomeServlet  我们可以在浏览器中直接输入如下的  URL  来访问该  Servelt: http://localhost:8888/  welcome,从而得到如图 5.4②所示的结果。 通过我们在 Tomcat 和 Resin 上的运行结果,读者可以看到本例所介绍的 Servlet 配置方 法其实并不麻烦。相反,本配置方法结构清晰,配置文件及 Servlet 源程序正确的存放位置也 非常容易掌握,不容易出错;而且在更换服务器后,任何配置文件都不需要修改,即可成功 运行。所以,我们建议读者在实际的工作中,可以采用本例程所讲述的方法来配置 Servlet, 这样会给我们的工作带来很大方便。 本书讨论的所有 Servlet 的配置和部署都将在现有的这个 web.xml 上进行。我们将通过不 断地修改或更新这个 web.xml 来部署新的 Servlet。 掌握了第一个 Servlet 的编写及配置的方法后,让我们继续讨论如何利用 get 请求从一个  Servlet 获得图像。 利用 Servlet 来生成动态图表,基本思路如下: (1)获得相关的图形或图像的绘制参数。 (2)调用 java.awt.image、java.awt 等 Java 图像类库进行绘制。 (3)将绘制完成的结果,以图像的格式(jpg/jpeg/gif/png)返回给发出请求的客户端。 这里我们先以本书第  2.10.4  节所示的绘制三角形(箭头)程序(Fig.2.10_04:  Draw  TriangleApplet.java)为蓝本,编写一个动态的、能随机生成方向的三角形 Servlet 实例。 当然,在运行本例程之前,需要将例程 Servlet 的源程序 TriangleServlet.java 拷贝到 Tomcat  或者 Resin 的 Web 应用程序根目录下WEB­INFclassescomfatcatwebchart 的子目录下面。 打开子目录中 web.xml 文档,我们可以看到如下的内容: 1: <?xml version="1.0" encoding="GB2312"?>  2:  3: <!DOCTYPE web­app PUBLIC  4:  "­//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"  5:  "http://java.sun.com/j2ee/dtds/web­app_2_2.dtd">  6:  7: <web­app>  8:  9:  <servlet>  10:  <servlet­name>TriangleServlet</servlet­name>  11:  <description>绘制三角形的 Servlet</description>  12: <servlet­class>com.fatcat.webchart.TriangleServlet</servlet­class>  13:  </servlet>  14:  15: <servlet­mapping>  16:  <servlet­name>TriangleServlet</servlet­name>  17:  <url­pattern>/drawTrigangle</url­pattern>
    • 18:  </servlet­mapping>  19:  20: </web­app> 复制本文档中第 9 行~第 18 行内容,然后再打开 Tomcat 或者 Resin 的 Web 应用程序根 目录下的WEB­INFweb.xml  文档,将刚刚复制的内容粘贴到WEB­INFweb.xml  文档中的  <web­app></web­app>标记中。 现在我们只讨论调用生成三角形 Servlet 的 TriangleSevlet.html 文档的源程序的第 12 行, <form action = "/drawTrigangle" method = "get"> 表单标记  form  的属性  action  值为“/drawTrigangle”,method  的值为“get”,说明向  URL 为“/drawTrigangle”的 Servlet 发送 get 请求。 现在重新启动  Tomcat  服务器,运行  IE,在地址栏中输入:http://localhost:8080/chap05/  Fig5.2/Fig.5.2_02/TriangleServlet.html。  TriangleServlet.html 文档的运行结果如图 5.5 所示。
    • 图 5.5  TriangleServlet.html 的运行结果 单击“获得三角形(箭头)”按钮后,我们可以得到如图 5.6 所示的运行结果。 图 5.6  TriangleServlet 生成的随机三角形 刷新该页面,或者退回到 TriangleSevlet.html 再次调用该 Servlet,我们会得到一个新的、 随机产生的三角形,图 5.6 中的②、③、④是我们在刷新该页面后得到的结果。 现在来看看  TriangleServlet.java(chap05Fig5.2Fig.5.2_02)是如何实现动态绘制三角形 (箭头)功能的。 程序第 4 行: package com.fatcat.webchart; 仍然使用了 package 语句,说明本源程序严格地按照 Java Servlet 的规定,将源程序部署 在相应的目录中。我们把源程序 TriangleServlet.java 拷贝到 Tomcat 或者 Resin 的 Web 应用程 序根目录下WEB­INFclassescomfatcatwebchart 的子目录下面,正确拷贝后的目录结构如图  5.7 所示。 D:  webchart  WEB­INF  classes  com  fatcat  webchart  TriangleServlet.java  Web.xml 图 5.7  TriangleServlet Web 应用程序的目录结构 程序第 6 行~第 7 行: import javax.servlet.*;  import javax.servlet.http.*; 引入了 javax.servlet 和 javax.servlet.http 包。我们再次强调 Servle 需要使用这些包中的某 些类。javax.servlet.http 包为 Servlet 提供了超类 HttpServlet,用来处理 HTTP get 和 post 请求。 
    • 程序第 8 行~第 12 行: import java.io.*;  import java.util.*;  import java.awt.image.*;  import java.awt.*;  import com.sun.image.codec.jpeg.*; 是我们熟悉的 Java 相关的输入输出类、工具类、图像类等,注意这里的第 12 行,我们 引进了一个新的 Java 包——com.sun.imgae.codec.jpeg.*。该包是 Java 用来处理 JPEG 的特殊 类,我们在这里使用该包用来处理内存图像的编码格式。 源程序共包含了 7 个方法: Ø  drawTrigangle  Ø  setBaseLine  Ø  setAlpha  Ø  setFillColor  Ø  setOutlineColor  Ø  doGet  Ø  doPost  其中增加 setAlpha、doGet、doPost 方法,但删除了 init 方法,分别执行了不同的任务。 原先  init  方法中实现的功能被移植到  doGet  方法中。我们重载了  doGet  方法,提供了对  get  请求的处理。当客户端发生 get 请求时,就会调用程序第 120 行~第 183 行的 doGet 方法。 再次强调,  doGet 方法有两个参数:一个是 HttpServletRequest 对象,另一个 HttpServletResponse  对象,两者都隶属于  javax.servlet.http  包。HttpServletRequest  对象代表客户端的请求,  HttpServletResponse 对象代表服务器对客户端的响应。如果 doGet 方法不能处理客户的请求, 则抛出 javax.servlet.ServletException 类型的异常。如果 doGet 方法在流处理过程中,即从客 户端中读取数据或向客户写数据的处理过程中发生错误,则会抛出 java.io. IOException 异常。 重载后的 doGet 方法响应了对客户端的请求,在其方法体中分别按顺序又调用了: Ø  setBaseLine  Ø  setAlpha  Ø  setOutlineColor  Ø  setFillColor  Ø  drawTrigangle  这 5 个方法以完成绘制不同大小,不同方向,不同风格的三角形。drawTrigangle 方法负 责进行计算三角形的 3 个顶点坐标,并根据随机生成的参数来决定三角形的填充颜色、三角 形轮廓的绘制颜色以及三角形的方向等信息来进行图像的绘制。setBaseLine 负责设置等腰三 角形底边的长度。setAlpha 负责设置等腰三角形底角的度数。setFillColor 负责设置填充等腰 三角形颜色。setOutlineColor 负责设置描绘等腰三角形轮廓的颜色。 程序第 125 行: response.reset(); 请读者注意, 我们在 doGet 方法中的第 1 条语句, 通过 response 调用了 Response 类的 reset  方法,该方法继承于 ServletResponse,用于清空 response 里缓冲区的内容。另外,在 doGet  或者是 doPost 的方法体中,第 1 条语句一定要调用 reset 方法。 程序第 128 行: response.setContentType("image/jpeg");
    • 使用  response 对象的  setContenType 方法,设置返回给客户端响应数据内容的类型为图 像类,图像的格式为  jpeg  格式。我们讨论过,客户端的浏览器是根据服务器返回文档类型 (MIME)来处理文档内容的。本例中,返回数据内容的类型为 image/jpeg,它通知浏览器响 应一个格式为 jpeg 的图像。于是,客户端浏览器就知道它必须读取该图像,然后将图像显示 在浏览器中。 如果没有程序第 125 行 response 的 reset 方法,而仅仅只有程序第 128 行 setConten Type  方法,即使我们在这里指明了返回的数据类型为  image/jpeg,可实际上,JSP/Servlet  服务器 默认的还是返回 text/html。只有当执行到第 128 行 setContentType “image/jpeg” 时,  ( ) response  缓冲区为空时,JSP/Servlet 服务器才返回 image/jpeg 的图像格式。 但是,无法确定 JSP/Servlet 服务器在执行到 response 的 setContentType( “image/ jpeg”) 方法时,response  缓冲区的内容为空。这样,即使生成的图片内容和格式都没有问题,但因 为返回的数据类型和数据的实际内容不一致,所以,在客户端上也就不会有正确地显示。 但某些 JSP/Servlet 服务器比较“聪明” ,如 Tomcat,如果碰见以下语句: <%@ page contentType="image/jpeg" %> 或 response.setContentType("image/jpeg"); 将会自动调用 response 的 reset 方法。即使没有在程序中显示的声明 response 的 reset 方 法,还是可以得到正确的输出结果。  Resin 服务器在该问题上,不同的版本有着不同的输出结果。Resin 的某些版本与 Tomcat  类似,也会自动调用 response 的 reset 方法,而 Resin 的某些版本却非常忠于源程序,不会自 动调用 response 的 reset 方法,所有输出结果就不正确。 同理,经我们测试,  BEA 公司的 Weblogic 服务器也不会自动调用 response 的 reset 方法, 所以如果缺少了 response 的 reset 方法,也不能得到正确的结果。 综上所述,我们强烈建议读者在编写 JSP/Servlet 的 Web 图表程序时,要在源程序中,首 先调用 response 的 reset 方法,强制清空 response 缓冲区的内容。这样,可以确保在所有的  JSP/Servlet 服务器上运行 Web 图表编程的源程序时,都可以获得正确的结果。 程序第 131 行~第 136 行: int width = 400, height = 300;  BufferedImage image = new BufferedImage(width, height,  BufferedImage.TYPE_INT_RGB);  // 得到图形环境对象 g  Graphics g = image.getGraphics(); 创建了一个宽度和高度分别为  400  像素和  300  像素的缓冲图像  BufferedImage  的对象  image。 程序第 138 行~第 150 行,创建了两个 Color 数组。其中 oColor 数组代表生成的三角形 轮廓的描绘颜色,fColor  数组代表生成的三角形填充的颜色。它们将用于绘制三角形时随机 选取的轮廓描绘颜色和填充颜色。 程序第 153 行~第 154 行: g.setColor(Color.WHITE);  g.fillRect(0, 0, width, height); 填充一个和缓冲图像宽度、高度都相同的白色矩形作为缓冲图像的背景图案。注意,在  JSP/Servlet 中,缓冲图像的默认背景及绘图颜色为黑色。 程序第 156 行~第 158 行:绘制图像的标题。 g.setColor(Color.BLACK);
    • g.setFont(new Font("方正粗宋简体", Font.PLAIN, 30));  g.drawString("随机生成三角形­Servlet 版", 10, 35); 程序第 160 行~第 161 行: int tempBaseLine = 100+(int)(Math.random() * 50);  setBaseLine(tempBaseLine); 定义一个整型变量  tempBaseLine,并用一个  100~149  之间的随机数赋值,然后将其作 为参数传递并调用 setBaseLine 方法。程序会转到第 99 行~第 102 行的 setBaseLine 方法体中 运行: public void setBaseLine(int baseLine)  {  this.baseLine = baseLine;  }  setBaseLine 方法很简单,仅仅是将用户在 setBaseLine 方法中传递整型参数 baseLine 的 值重新赋值给类的数据成员 baseLine。我们假设这时得到的随机数 baseline 为 120,那么,类 的数据成员变量  baseLine  的值,即三角形底边长度就被设置为  120  像素了。接着程序返回  doGet 方法中,继续执行下面的语句。 程序第 163 行~第 164 行: int tempAlpha = 30+(int)(Math.random() * 30);  setAlpha(tempAlpha); 定义一个整型变量 tempAlpha,并用一个 30~59 之间的随机数赋值,然后将其作为参数 传递并调用 setAlpha 方法。程序会转到第 104 行~第 107 行的 setAlpha 方法体中运行: public void setAlpha(int alpha)  {  this.alpha = alpha;  }  setAlpha 方法很简单,是将用户在 setAlpha 方法中传递的整型参数 alpha 值重新赋值给 类的数据成员 alpha。我们假设这时得到的随机数 alpha 为 45,类的数据成员变量 base Line  的值就改变为 45,即三角形底角就被设置为 45 度了。接着程序返回 doGet 方法中,继续执 行。 程序第 166 行~第 170 行: int tempOutlineColorIndex = (int)(Math.random() * 5);  setOutlineColor(oColor[tempOutlineColorIndex]);  int tempfillColorIndex = (int)(Math.random() * 5);  setFillColor(fColor[tempfillColorIndex]); 分别生成了两个  0~4  之间的随机数,并将其作为参数,分别调用了  setOutlineColor  和  setFillColor 方法,用于设定绘制三角形时所需的填充颜色和三角形轮廓的描绘颜色。 程序第 172 行: int tempDirection = 1+(int)(Math.random() * 4); 生成了 1 个 1~4 之间的随机数,该随机数用来表示程序第 22 行: final int UP = 1, RIGHT = 2, DOWN = 3, LEFT = 4; 定义表示三角形方向的 4 个 final 变量之一。UP、RIGHT、DOWN 和 LEFT,代表三角 形的方向,分别表示如本书第 2 章中的图 2.38 所示的向上、向右、向下和向左的三角形。 程序第 173 行:
    • int tempStyle = 1+(int)(Math.random() * 3); 生成了 1 个 1~3 之间的随机数,该随机数用来表示程序第 25 行: final int OUTLINE = 1, FILL = 2, MIXED = 3; 定义表示三角形方向的 3 个 final 变量之一。整型变量 OUTLINE、FILL 和 MIXED,分 别代表三角形的绘制风格。分别表示为: Ø  OUTLINE:描绘三角形轮廓。 Ø  FILL:填充实心三角形。 Ø  MIXED:绘制一个带轮廓的实心三角形(即先填充一个实心三角形,然后再描绘其 轮廓) 。 程序第 176 行: drawTrigangle(180, 150, tempDirection, tempStyle, g); 调用了 drawTrigangle 方法,完成绘制工作。在调用 drawTrigangle 方法的时候,传递 5  个参数。前面 4 个参数是整型参数,最后 1 个参数是由缓冲图像 image 的绘图环境 Graphics  对象 g。 前面 4 个参数的具体含义如下: Ø  180,150:表示欲绘制三角形底边中点的坐标是(180,150) 。 Ø  tempDirection:表示该三角形的方向。 Ø  tempStyle:表示该三角形的绘制风格。 在 drawTrigangle 方法体内,首先运行的是第 34 行: int offset = (int)(baseLine / 2 * Math.tan(alpha * Math.PI / 180)); 定义了一个局部的整型变量 offset。offset 是一个偏移量,它表示三角形顶点 C 到底边中 点的距离。该距离的计算,读者请参考图 2.38 所示的计算方法。 之后,程序接着执行第 37 行,根据传递的随机生成的第 3 个整型参数:tempDirection, 进入一个 switch 多重选择语句: switch (triangleDirection) 这里根据 tempDirection 的值进行下一步的操作,即计算三角形 3 个顶点的坐标。之后, 就会退出 switch 语句。 程序又进入第 77 行,另外一个 switch 语句: switch (triangleStryle) 根据传递的随机生成的第 4 个整型参数 tempStyle 进行三角形绘制风格的选择。 这段代码的处理,同我们在本书第 2.10.4 节所介绍程序的相关方法完全一致,请读者参 阅该部分的内容。 至此,缓冲图像的内容就全部绘制完成了,接下来,需要将该图像的内容输出到客户端 的浏览器中。 程序第 179 行~第 182 行的代码完成了图像的输出工作: ServletOutputStream out = response.getOutputStream();  JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);  encoder.encode(image);  out.close(); 先用 response 的 getOutputStream  方法创建一个 ServletOutputStream 对象 out,然后利用  com.sun.image.codec.jpeg 包中的 JPEGImageEncoder 类来编码图像, 即生成一个 JPEG 格式的
    • 图像。最后调用 ServletOutputStream 对象 out 的 close 方法,关闭输出流并结束此 Servlet。程 序的运行结果如图 5.6 所示。 本例程中使用了 Sun 的 JPEG 特殊包——com.sun.image.codec.jpeg.*。我们认为采用该包 编写的程序的通用性不够好,因为这些代码在 com.sun 包中,不是核心 API 的一部分,也不 是标准的扩展包,因此会影响代码的可移植性。虽然利用该包可以生成正确无误的图表,但 我们完全可以利用 J2SE 5.0 的新特性来对缓冲图像的内容进行解码及编码工作。这样,代码 会更简洁、性能更强大、移植性更好。 我们将在后面的例程中向读者介绍如何利用  J2SE  5.0  新特性来编写用于图表处理的  Servlet。  5.3  Servlet 处理包含数据的 HTTP get 请求及解决中文乱码 在向 Web 服务器发送 get 请求、获取文档或资源时, 经常需要同时提交一些数据。比如, 登录  Web  邮箱时,需要提交登录名和密码;参与一个网络调查/投票的时候,需要向服务器 提交所选择的内容。 在 get 请求中,参数以参数名/参数值成对的形式提交给 JSP/Servlet 服务器。这里我们先 探讨在 HTML 文档中如何向 JSP/Servlet 服务器提交参数,以及如何在 Servlet 中获取并处理 该参数。  GetDataServlet.html 文档(chap05Fig5.3Fig.5.3_01)的源程序的第 14 行~第 48 行,是 一个包含了两个表单对象的表单。 程序第 14 行,表单标记 form  的属性 action 的值为“/getData”,method 的值为“get”, 说明向 URL 为“/getData”的 Servlet 发送 get 请求。 程序第 19 行: <input name="name" type="text" size="40" /> 生成了一个单行文本框的表单对象,对象的名字为“name”,值为用户在该文本字段中 输入的内容,如图 5.8①所示。 程序第 25 行~第 38 行: <input name="level" type="radio" value="初学者" checked="checked" /> 初级学习者,没有参加认证考试 <br />  <input name="level" type="radio" value="Sun 认证移动应用程序开发员" />  Sun Certified Mobile Application Developer <br />  <input name="level" type="radio" value="Sun 认证 Java 程序员" />  Sun Certified Java Programmer <br />  <input name="level" type="radio" value="Sun 认证 Java 开发员" />  Sun Certified Java Developer <br />  <input name="level" type="radio" value="Sun 认证 Java Web 组件开发员" />  Sun Certified Java Web Component Developer <br />  <input name="level" type="radio" value="Sun 认证 J2EE 企业组件开发员" />  Sun Certified J2EE Business Component Developer<br />  <input name="level" type="radio" value="Sun 认证企业构架师" />  Sun Certified Enterprise Architect<br />
    • 图 5.8  GetDataServlet.html 的运行结果 这段代码生成了一组共 7 个单选按钮,如图 5.8②所示。注意,这 7 个单选按钮名字都 是相同的,都叫“level” ,只是每个单选按钮的值不一样,但一组单选按钮一次只能选中一个 选项。注意,第 25 行中包含 checked=“checked”代码,将单选按钮 checked 的属性设置为 “checked”,则该单选按钮在默认状况下,被设置为选中状态。 运行本例程之前,需要将本例程 Servlet 源程序 GetDataServlet.java 拷贝到 Tomcat 或者  Resin 的 Web 应用程序根目录下WEB­INFclassescomfatcatwebchart 的子目录下面。 复制本文档中第 9 行~第 18 行的内容: <servlet>  <servlet­name> GetDataServlet</servlet­name>  <description>提交数据的 HTTP get 请求</description>  <servlet­class>com.fatcat.webchart.GetDataServlet</servlet­class>  </servlet>  <servlet­mapping>  <servlet­name> GetDataServlet</servlet­name>  <url­pattern> /getData</url­pattern>  </servlet­mapping> 然后再打开 Tomcat 或者 Resin 的 Web 应用程序根目录下的WEB­INFweb.xml 文档,将 刚刚复制的内容粘贴到WEB­INFweb.xml 文档中的<web­app></web­app>标记中。 重新启动 Tomcat,运行 IE,在地址栏内输入: http://localhost:8080/chap05/Fig5.3/Fig.5.3_01/GetDataServlet.html 如图 5.8 所示的运行结果,在输入姓名的文本框中输入“唐桓”两个字,然后选中单选 按钮“Sun  Certificated Java Programmer” ,单击【提交】按钮,以调用 GetDataServlet。当用 户按下【提交】按钮后,会将  input  元素(本例中的“name”和“level”两个表单元素)的 值作为请求的一部分放置在参数名/参数值对中,并提交给 JSP/Servlet 服务器。我们可以得 到如图 5.9 所示的结果。 图 5.9  GetData Servlet 的运行结果
    • 注意:图  5.9  所示  GetData 的  Servlet  运行结果中,浏览器地址栏以如下的方式:http://  localhost:8080/getData?name=%CC%C6%BB%B8&level=Sun%C8%CF%D6%A4Java%B3%CC  %D0%F2%D4%B1&Submit=%CC%E1%BD%BB  将参数“name”和“level”及值传递给 JSP/Servlet 服务器。在浏览器地址栏的 URL 参 数中有一个“?”号。这里的“?”将提交的字符串参数,作为  get  请求的组成部分进行传递 数据与 get 请求中 URL 非数据部分区分开。参数名/参数值在 URL 中以“参数名=参数值” 的形式进行传递。如本例 URL 中: name=%CC%C6%BB%B8  level=Sun%C8%CF%D6%A4Java%B3%CC%D0%F2%D4%B1&Submit=%CC%E1%BD%BB 如果同时提交的参数不止一个,则参数之间用“&”符号相互分开。而且参数中内容都 被自动转换成 gb2312 的编码格式。 下面我们来看看 GetDataServlet.java(chap05Fig5.3Fig.5.3_01)这个 Servlet 的源程序,并 讨论它是如何处理这些参数,并得到如图 5.9 所示的运行结果。 程序第 21 行~第 22 行: String tempName = request.getParameter("name");  String tempLevel = request.getParameter("level"); 调用  request  的  getParameter  方法来获取参数的值。该方法需要传递一个参数的名称,  getParameter  方法以字符串的形式返回该参数的值。如果请求的参数不存在,不同的  JSP/  Servlet 服务器会返回 null 或者一个内容为空的字符串。 如果参数具有多个值,则应调用 request 的 getParameterValues 方法,返回一个字符串数 组。另外,调用 request 的 getParameterNames 方法,将返回一个 Enumeration 对象,然后遍 历该 Enumeration 中的所有元素,并取出各元素的值即可。  GetDataServlet 与本章第 5.2 节所讲的 WelcomeServlet 两个 Servlet 的运行流程基本相同。 除了通过调用 request 的 getParameter 方法来获取参数的值外,GetDataServlet.java 中还 提供了一个 convertToChinese 的方法,该方法是用来转换字符串编码格式的。前面我们提到, 运行 GetDataServlet.html 页面的时候,在用户按下【提交】按钮后,会将 input 元素(本例中 的“name”和“level”两个表单元素)的值作为请求的一部分放置在参数名/参数值对中,并 附加在 URL 后提交给 JSP/Servlet 服务器,参数中的内容被自动转换成 gb2312 的编码格式。 既然已经被浏览器自动将参数的内容转换成 gb2312 的格式, 而且在 GetData Servlet.java 的第  19 行: response.setContentType("text/html;charset=gb2312"); 也设置了返回的数据编码方式为 gb2312。通过 request 的 getParameter 方法获得参数值后, 那么在 Servlet 中,是否不用任何处理就可以正常显示中文了呢?实际情况却不是这样。为了 说明,这里我们必须进行一次字符串编码转换的工作,暂时修改 GetData  Servlet.java 中的第  49 行: out.println("<h1>您好," + name + "<br>您提交的信息如下:<br>"); 更改为: out.println("<h1>您好," + tempName + "<br>您提交的信息如下:<br>"); 另外,将程序第 52 行: out.println("您的 Java 编程水平为:<br>" + level + "</h1>"); 更改为:
    • out.println("您的 Java 编程水平为:<br>" + tempLevel + "</h1>"); 重新启动 Tomcat 及 IE 并运行 GetDataServlet.html 文档,同样输入如图 5.8 所示的数据, 单击【提交】按钮,我们就得到如图 5.10 所示的运行结果。 可以看到, 在 GetDataServlet.html 输入并提交的参数 name 的值 “唐桓”以及参数  level  , 的值“Sun 认证 Java 程序员”并没有得到正确的输出。这是什么原因呢? 首先简单回顾一下字符集的编码历史。 任何数据最终都是以二进制的形式在计算机上表示,包括操作系统、应用软件及其开发 工具等,连屏幕上显示的一个字符也是如此。计算机发明的时候,所设计的键盘是以当时广 泛使用的机械式打字机键盘为蓝本。除去 0~9 这 10 个数字键,以及 26 个大小写字母,还有 另外一些特殊的按键,如空格键、回车键、换档键等,这些按键都用一个数字来表示。因为 8 这些键(符号)的数目远远低于  200,所以,一个字节所能表示的数字范围(2  =256)就足 以容纳所有的这些字符。实际上表示这些字符的数字字节最高位(bit)都为  0,也就是说, 这些数字都在 0~127 之间。例如,字符 a 对应的数字为 97,字符 A 对应的数字为 65,字符  z 对应的数字为 122,字符 Z 对应的数字为 90。这种字符与数字对应的编码固定下来后,这 套编码规则被称之为 ASCII 码(美国国家标准信息交换码)。 图 5.10  取消字符串编码转换后的 GetData Servlet 的运行结果 随着计算机逐渐在其他国家的应用和普及,许多国家都把本地的字符集引入计算机,大 大地扩展了计算机字符的范围。 一个字节所能表示的数字范围是不能容纳所有的中文汉字的, 我们国家将每一个中文字符都用两个字节的数字来表示, 原有的 ASCII 字符的编码保持不变, 仍用一个字节表示。为了将一个中文字符与两个 ASCII 码字符相区别,中文字符的每个字节 最高位(bit)都为  1,中国大陆为每一个中文字符都指定了一个对应的数字,并作为标准的 编码固定下来,这套编码规则称为 GBK(国际码) 。后来又在 GBK 的基础上对更多的中文字 符(包括繁体)进行了编码,新的编码系统就是 GB2312,可见 GBK 是 GB2312 的子集。使 用中文的国家和地区很多,同样的一个字符,如“中国”的“中”字,在中国大陆的编码是 十六进制的 D6D0, 而在中国台湾的编码是十六进制的 A4A4,台湾地区对中文字符集的编码 规则称为 BIG5(大五码) 。 这样就出现了一种现象,即同一个字符在不同的国家和地区很可能有着不同的编码 (ASCII 码除外)。随着世界各国的交往越来越密切,全球一体化的趋势越来越明显,不同的 国家和地区间交换越来越多的电子文档和数据,这种不统一的编码方式已经严重制约了国家 和地区间在计算机使用和技术方面的交流。 为了解决各个国家和地区使用本地化字符编码带来的不利影响,人们将全世界所有的符 号进行了统一编码,称之为 Unicode 编码,所有字符不再区分国家和地区,都是人类共有的 符号,如“中国”的“中”这个符号,在全世界的任何角落始终对应的都是一个十六进制的 数字 4e2d。如果所有的计算机系统都使用这种编码方式,在中国大陆的本地化系统中显示的 “中”这个符号,发送到其他国家的本地化系统中,显示的仍然是“中”这个符号。Unicode
    • 编码的字符都占用两个字节, 也就是说全世界所有的字符个数不会超过 2 的 16 次方 (65 536)。 然而,目前 Unicode 一统天下的局面暂时还难以形成,在相当长的一段时期内,人们看 到的都是本地化字符编码与 Unicode 编码共存的景象。既然本地化字符编码与 Unicode 编码 共存,那就少不了涉及两者之间的转化问题,在 Java 中字符使用的都是 Unicode 编码,Java  技术在通过 Unicode 保证跨平台特性的前提下也支持了全扩展的本地平台字符集,我们的键 盘输入和显示输出都是采用本地编码。  Java 中的字符采用的是 Unicode 编码, 每个字符都占用两个字节。  String 类中的 get  Bytes  方法,并不是简单地将字符串中的字节数据存放到一个字节数组中去,而是将 Unicode 码的 字符串中每个字符数字转换成该字符在指定的字符集下的数字,最后这些数字存放到一个字 节数组中返回。 JDK 包中必须含有对应的字符集编码器的类才可以将 Unicode 成功地转换 在 到本地字符集码。 为更好理解导致上述现象的原因, 我们重新编写了一个 Servlet 来测试接收参数的编码格 式。 我们在下载源码中向读者提供了一个输出字符 Unicode 编码的 Servlet。 该 Servlet 的程序 名为——GetCharCodeServlet.java,位于下载源码的 chap05Fig5.3Fig.5.3_02 子目录中,将其 拷贝到 Web 应用程序根目录下的WEB­INFclassescomfatcatwebchart 子目录下面。 打开位于下载源码根目录下的 chap05Fig5.3Fig.5.3_02 子目录中 web.xml 文档,并复制 本文档中的第 9 行~第 18 行的内容: <servlet>  <servlet­name> GetCharCodeServlet</servlet­name>  <description>获取提交数据字符的 Unicode 编码</description>  <servlet­class>com.fatcat.webchart. GetCharCodeServlet</servlet­  class>  </servlet>  <servlet­mapping>  <servlet­name> GetCharCodeServlet</servlet­name>  <url­pattern> /getCharCode</url­pattern>  </servlet­mapping> 打开 Tomcat 或 Resin 的 Web 应用程序根目录下的WEB­INFweb.xml 文档,将刚刚复制 的 内 容粘 贴 到 WEB­INFweb.xml  文 档 中 的 <web­app></web­app> 标 记 中 。因 为  GetChar  CodeServlet.html 文档(chap05Fig5.3Fig.5.3_02)和刚刚介绍的 GetDataServlet.  html 文档除 调用的 Servlet 的 URL 不同外,其余完全相同,故在此略过。 重新启动  Tomcat,运行  IE,在地址栏内输入:http://localhost:8080/chap05/Fig5.3/Fig.  5.3_02/GetCharCodeServlet.html。 在图 5.8 所示的运行结果中,还是在输入姓名的文本框中输入“唐桓”两个字,然后在 选中单选按钮“Sun  Certificated  Java  Programmer” ,单击【提交】按钮,以调用  GetChar  CodeServlet,我们可以看见如图 5.11 所示的结果。 运行  GetDataServlet.html  文档,输入如图  5.8  所示的数据,单击【提交】按钮,我们就 得到如图 5.11 所示的运行结果。
    • 图 5.11  获取字符在编码转换前后各自的 Unicode 代码 可以看到,在程序输出的结果中,字符串被重新编码前,参数 name 的值“唐桓”的 Unicode  编码为: CC C6 BB B8 该码值其实是“唐桓”这个字符串的 gb2312 的编码值,并不是 Unicode 编码值。 我们还可以在 GetDataServlet.html 单击【提交】 按钮后, 发送到 JSP/Servlet 服务器的 URL  的 内 容 中 得 到 验 证 :  http://localhost:8080/getData?name=%CC%C6%BB%B8&  level=  Sun%C8%CF%D6%A4Java%B3%CC%D0%F2%D4%B1&Submit=%CC%E1%BD% BB。 可以清楚地观察到,参数 name=%CC%C6%BB%B8,这里的 CC C6 BB B8 就是 Servlet  中,在字符串被重新编码前,输出的参数 name 的值“唐桓”Unicode 的编码,实际上,字符 串“唐桓”Unicode 的编码是 5510 和 6853。显然,在 JSP/Servlet 服务器接收到该字符串后, 重新编码过程中发生了错误。也就是,JSP/Servlet 服务器(经我们测试 Tomcat 和 Resin,在 这里都出现相同的错误)接收到客户端 HTTP 请求及相关参数的信息后,“唐桓”这个字符 串并没有被正确地转换成 Unicode 的编码,所以,最终出现了错误。 同理,参数 level 的值为“Sun 认证 Java 程序员” 。在 GetDataServlet.html 单击【提交】 按钮后,浏览器也自动将其编码成  gb2312  的格式,我们在  URL  的内容中可以看到:  level=Sun%C8%CF%D6%A4Java%B3%CC%D0%F2%D4%B1  在同时包含有英文字符和中文字符的字符串中,英文字符在被转换成  gb2312  的编码前 后,字符的编码都一样,所以内容保持不变,而非英文字符(如:中文、日文、韩文等)在 转换后,URL 中就显示该字符按照某种格式编码后所得到的码值。 实际上,在 JSP/Servlet 服务器中,响应客户端的 HTTP 请求并返回请求内容的过程中, 进行了两次编码的工作,有关字符编码的默认过程如下: (1)获得当前按默认编码格式,即按照标准的西方英语国家字符集(ISO8859­1)编码 的响应内容。 (2)将当前所有的内容转换成按 Unicode 编码的内容。 (3)将按 Unicode 编码的内容转换成按照指定的字符集(编码格式)编码的内容。 了解这个过程后,我们来看本例中,客户端发送  get  请求后,JSP/Servlet  服务器是如何 工作的: (1)获得参数 name,然后通过调用 request 的 getParameter 方法,获取参数 name 的值 ——“唐桓” 。 (2)将字符串“唐桓”以 Unicode 的方式,进行编码转换。 (3)最后重新编码成 gb2312 输出。 这里在第 2 步,也就是将获得的参数值“唐桓”转换成 Unicode 时,发生了错误。原因 是 JSP/Servlet 服务器认为接收的参数值“唐桓”是按照默认值进行编码的,也就是说,Java  虚拟机认为这时字符串“唐桓”是按 ISO8859­1 的方式进行编码。而实际情况下,在我们单
    • 击【提交】按钮后,浏览器就自动将提交的参数值转换成  gb2312  格式编码的字符了,即在  JSP/Servlet  接收到这些参数之前,这些参数就已经被重新编码过一次了,已经不是默认的  ISO8859­1 编码的字符串了。当 JSP/Servlet 服务器接收到这些参数后,仍然是按照默认的步 骤和方法对这些参数进行再次编码操作,所以出错。 因为英文字符在不同的字符集转换中编码保持不变,所以,转换结果对包含有英文字符 的字符串来说,英语部分的字符是正确的,但其他字符就会出错。 为解决这个问题,我们只需要把接收到的参数再将其反编码成 ISO8859­1 的格式就可以 了 。 这样 ,当  JSP/Servlet  服 务 器 执行 字符 串编码 转 换的 过程 就不 会出 错了 。源 程 序  GetDataServelt  和  GetCharCodeServlet  中的  convertTo8859  方法,就完成了这个功能,将以  gb2312 编码的字符串恢复成以 ISO8859­1 编码的字符串。 现在来看 GetCharCodeServlet.java 的源程序,我们就很清楚了。 程序第 21 行~第 22 行: String tempName = request.getParameter("name");  String tempLevel = request.getParameter("level"); 获得客户端提交的参数。程序第 45 行~第 48 行,直接输出这些参数值及其每个字符的  Unicode 码值: out.println("字符串"" + tempName + ""的 Unicode 编码为:&nbsp;");  out.println(getCharCode(tempName) + "<br>");  out.println("字符串"" + tempLevel + ""的 Unicode 编码为:&nbsp;<br>");  out.println(getCharCode(tempLevel)); 程序中通过调用 getCharCode 方法(第 92 行~第 100 行) ,获得某个字符的 Unicode 码 值: public String getCharCode(String str)  {  String temp = "";  for (int i = 0; i < str.length(); i++)  {  temp += Integer.toHexString((int)str.charAt(i)) + "&nbsp;";  }  return temp;  } 在该方法中,首先调用字符串的 charAt 方法,将字符串中的每个字符分离出来。注意, 在 charAt 方法前面加上了(int),表示将字符转换成其对应的整数值。然后,调用 Integer。 类的静态方法 toHexString,此方法接收一个整数,返回用字符串形式表示的该整数对应的十 六进制的值,这个值就是这个字符的 Unicode 码值。 程序执行完第  48  行后,可以看到,接收到的字符串在没有被重新编码前,英文字符的 输出是正确的,而中文字符则显示为乱码。 程序第 53 行~第 54 行,将获得的这些参数值重新按照 ISO8859­1 的标准进行编码: String name = convertTo8859(tempName);  String level = convertTo8859(tempLevel); 这里通过调用 convertTo8859 的方法(第 76 行~第 89 行) : public String convertTo8859(String strInput)  {  try  {  String tempStr = strInput;  byte[] tempStrBytes = tempStr.getBytes("ISO8859­1");  String strOutput = new String(tempStrBytes);
    • return strOutput;  }  catch (Exception e)  {  return "";  }  } 在这段代码中,首先声明了一个 tempStr 的字符串。然后,声明了一个 tempStrBytes 的 字节数组。String 类的  getBytes  方法,接收一个指定的编码格式的参数,返回该字符串按指 定编码格式转换而得到的字节数组。本例,我们指定编码格式为“ISO­8859­1” ,表示返回字 符串 tempStr 以“ISO­8859­1”编码方式转换而得到的字节数组。注意,因为在字符的编码转 换过程中,可能会出现异常,所以必须将转换过程放置在 try…catch…方法块中。 将字符串的编码转换成 ISO­8859­1 格式后,程序在第 60 行~第 64 行: out.println("<h1>编码后:<br>");  out.println("字符串"" + name + ""的 Unicode 编码为:&nbsp;");  out.println(getCharCode(name) + "<br>");  out.println("字符串"" + level + ""的 Unicode 编码为:&nbsp;<br>");  out.println(getCharCode(level)); 再次输出这些转换编码后的参数值及其每个字符的 Unicode 码值,这次我们就可以得到 正确的输出结果。同理,level 字符串“Sun 认证 Java 程序员”中,英文字符“Sun”“Java” 、 在编码转换前后,其 Unicode 是没有变化的,分别是“53 75 6e”和“4a 61 76 61” 。 综上所述,在 JSP/Servlet 中如果出现中文显示错误的情况,基本上可采用本节介绍的方 法加以解决。但 Java 对字符进行的编码转换操作是一个很底层的问题,所涉及的内容也很复 杂。这里我们仅仅给出了在 JSP/Servlet 中的解决方法,在其他一些 Java 应用程序中,如果出 现中文字符显示乱码的情况,需要读者具体情况具体分析。读者可以借鉴我们的思路来分析 问题,并找出问题的根源及其解决办法。 读者可以直接在浏览器的地址栏中输入如下内容,来发送  get  请求以直接调用这两个  Servlet:http://localhost:8080/getData?name=钟京馗&level=J2EE 企业级开发及应用,运行结 果如图 5.12 所示。 图 5.12  Tomcat 服务器下直接在浏览器的地址栏中输入中文并提交数据的运行结果 或者输入:http://localhost:8888/getCharCode?name=钟京馗&level=J2EE  企业级开发及应 用,运行结果如图 5.13 所示。
    • 图 5.13  Resin 服务器下直接在浏览器的地址栏中输入中文并提交数据的运行结果
    • 5.4    Servlet 处理 HTTP post 及包含数据的 post 请求 当我们采用 HTTP  get 请求并提交一些数据到 JSP/Servlet 服务器的时候,数据的安全性 比较差,提交的信息被转换成 gb2312 的字符后附加在 URL 里,一起传递给 JSP/Servlet 服务 器。任何人都可以在浏览器的地址栏中截取该信息,只要将该信息进行正确的解码转换,就 可以得到原始的文本。所以,get 请求适合在对数据的安全性要求较低的网络环境中运行。当 我们在网上购物、登录 Web 电子邮件等需要提交用户名和密码等机密信息的时候,一般就采 用 post 请求。post 请求的作用与 get 几乎一样,但在发送请求并提交数据的同时,数据也一 并显示在 URL 后,这样,安全性就比 get 请求好得多。  PostDataServlet.html 文档(chap05Fig5.4)源程序的第 19 行: <form action = "/postData" method = "post"> 表单标记 form 的属性 action 的值为“/postData”,method 的值为“post”,说明向 URL  为“/postData”的 Servlet 发送 post 请求。PostDataServlet.html 文档的其余部分和我们介绍过 的 GetDataServlet.html 文档,以及 GetCharCodeServlet.html 文档完全一样,此处不再赘述。 运行结果如图 5.14 所示。 图 5.14  PostDataServlet.html 的运行结果 然后,打开位于下载源码根目录下的 chap05Fig5.4 子目录中 web.xml 文档,同样复制该 文档中的第 9 行~第 18 行的内容: <servlet>  <servlet­name>PostDataServlet</servlet­name>  <description>获取提交数据的 HTTP post 请求</description>  <servlet­class>com.fatcat.webchart.PostDataServlet</servlet­class>  </servlet>  <servlet­mapping>  <servlet­name>PostDataServlet</servlet­name>  <url­pattern>/postData</url­pattern>  </servlet­mapping> 将复制的内容粘贴到WEB­INFweb.xml 文档中的<web­app></web­app>标记中。  PostDataServlet.java 的源代码中第 13 行~第 56 行的 doPost 方法: protected void doPost(HttpServletRequest request, HttpServletResponse  response)throws ServletException, IOException  {  // 发送 XHTML 格式的页面给客户端 response.setContentType("text/html;charset=gb2312");
    • String tempName = request.getParameter("name");  String tempLevel = request.getParameter("level");  PrintWriter out = response.getWriter();  // 开始生成 XHTML 文档 out.println("<?xml version = "1.0"?>");  out.println("<!DOCTYPE html PUBLIC "­//W3C//DTD " +  "XHTML 1.0 Strict//EN""http://www.w3.org"+  "/TR/xhtml1/DTD/xhtml1­strict.dtd">");  out.println("<html xmlns = "http://www.w3.org/1999/xhtml">");  // 生成文档的 head 部分 out.println("<head>");  out.println("<title>处理客户端提交数据的 servlet</title>");  out.println("</head>");  // 生成文档的 body 部分 out.println("<body>");  // 获得正确编码的中文字符串 String name = convertTo8859(tempName);  if (name == null || name.equals(""))  {  name = "Java 爱好者";  }  out.println("<h1>您好," + name + "<br>您提交的信息如下:<br>");  String level = convertTo8859(tempLevel);  out.println("您的 Java 编程水平为:<br>" + level + "</h1>");  out.println("</body>");  // 结束 XHTML 文档 out.println("</html>");  out.close(); // 关闭输出流 } 该  doPost  方法所接收的参数与  doGet  方法相同。一个是表示客户请求的,实现了  Http  ServletRequest  接口的对象;另一个是表示服务器响应的,实现了 HttpServletRequest  接口的 对象。与  doGet  方法一样,如果不能处理客户的请求,doPost  方法,抛出  Servlet  Exception  异常;如果在流过程中出现问题,则抛出 IOException 异常。 将  PostDataServlet.java  复 制 到  Web  应 用 程 序 根 目 录 下 的 WEB­INFclassescom  fatcatwebchart 子目录下面。重新启动 Tomcat 服务器,启动 IE,在地址栏中输入如下 URL: http://localhost:8080/chap05/Fig5.4/PostDataServlet.html 输入“肥猫”及选择“Sun  Certified  Java  Web  Component  Developer” ,可以看到图 5.14  所示的运行结果,单击【提交】按钮,得到如图 5.15 所示的结果。 图 5.15  PostData Servlet 的运行结果
    • 我们看到浏览器的地址栏里,提交的数据没有附在  URL  上,这在一定程度上保证了数 据的安全。 现在分析一下该源程序的工作流程: 单击【提交】按钮后,PostDataServlet.html 向 JSP/Servlet 发出了一个 post 请求。该请求 传递给 Servlet 后,因是一个 post 请求,所以,Servlet 会调用程序第 13 行~第 56 行的 doPost  方法来处理。 该方法与上一节例程所介绍的 doGet 方法完全一致,读者可以参考前例。 注意,程序第 59 行~第 63 行,同样也声明了一个 doGet 方法: protected void doGet(HttpServletRequest request, HttpServletResponse  response)throws ServletException, IOException  {  doPost(request, response);  } 该方法是调用了程序第 13 行~第 56 行的 doPost 方法,来响应客户端的 get 请求。这样  Servlet 就可以同时处理 get 和 post 请求。 本例之前所介绍的例程都没有提供 doPost 方法, 所以无法响应 post 请求, 可以按照上述 方法, 添加一个 doPost 的方法,并在 doPost 方法中调用 doGet 方法就可以了,添加方法如下: protected void doPost(HttpServletRequest request, HttpServletResponse  response)throws ServletException, IOException  {  doGet(request, response);  } 当客户端发出请求时,Servlet 并没有提供与之相应的响应时,Servlet 会返回给客户端一 个代码为“405”的错误,表示被请求的 Servlet 无法响应该请求。 以本例来说明, 将 PostDataServlet.java 第 59 行~第 63 行的 doGet 方法,删掉或注释掉, 再将 PostDataServlet.html 中的第 19 行: <form action = "/postData" method = "post"> 将 method 的属性值由“post”更改为“get”,如下所示: <form action = "/postData" method = "get"> 重新启动  Tomcat,运行  PostDataServlet.html,我们可以看到,在  Tomcat  中提示的内容 是:  HTTP Status 405 – HTTP method GET is not supported by this URL,如图 5.16 所示。 图 5.16  Tomcat 下的代码为 405 的错误提示 而在 Resin 中的提示是:405 GET not supported,如图 5.17 所示。
    • 图 5.17  Resin 下的代码为 405 的错误提示 下面几节,我们将通过下面的内容阐述如何利用 Servlet 来生成 Web 动态图表的一些实 例,如投票统计图、登录验证码、加载图像文件并生成新的图像等。
    • 5.5  Servlet 生成 Web 投票统计图  Web 投票是网站从其访问者那里获得反馈信息的一种方式。当网站的访问者针对某个问 题进行了投票后,可以看到整个投票结果,以及相关的统计信息。 这里,我们编写了一个模拟网站投票并生成投票结果的 Web 图表程序。 本程序向访问者 调查在 Python、Java、C#、Perl 和 PHP 这五种编程语言中,哪种编程语言是其最熟悉的。在 实际的运行环境中, 网站投票的相关信息一般保存在数据库中。 因本书不涉及数据库的编程, 所以,我们考虑将投票的相关信息保存在一个静态类型的数组中。当用户单击【投票】按钮 后,就可以看到投票的结果。平常在页面上按“刷新”按钮时,将会再次提交相关请求及数 据。而本程序则会判断用户提交的数据是否是单击“刷新”按钮而产生的。如果是,将不会 更新数据。投票结果显示为一水平直方图,每种编程语言都将根据用户单击的数量而呈现出 相应的长度。同时,我们可以看到访问者提交数据的详情,如每种编程语言的投票数及其占 总投票数的比率。这样,我们可以直观地观察投票结果,如图 5.19 所示。 先复制 chap05Fig5.5 子目录,web.xml 文档中的第 9 行~第 18 行的内容,将复制的内 容粘贴到WEB­INFweb.xml 文档的<web­app></web­app>标记中。 <servlet>  <servlet­name>Vote</servlet­name>  <description>模拟网站投票并生成结果的 Servlet</description>  <servlet­class>com.fatcat.webchart.VoteServlet</servlet­class>  </servlet>  <servlet­mapping>  <servlet­name>Vote</servlet­name>  <url­pattern>/vote</url­pattern>  </servlet­mapping> 现在来看发送数据及请求的 VoteServlet.html 的源代码的第 12 行: <form action = "/vote" method = "get"> 发送 get 请求给 URL 为 vote 的 Servlet。程序第 16 行~第 20 行,定义了一组单选按钮 名字为“program” 。编程语言 Python、Java、C#、Perl、PHP 的参数值分别为 0~4。 将  VoteServlet.java(chap05Fig5.5) 复 制 到  Web  应 用 程 序 根 目 录 下 的 WEB­INF  classescomfatcatwebchart 子目录下面。现在我们看看其运行结果,如图 5.18 所示。 图 5.19 是多次单击【投票】按钮后的运行结果。 这里假设用户选择的是“Java”编程语言这个选项,并单击【投票】按钮,下面我们来 探讨 VoteServlet.java 程序的流程。
    • 图 5.18  VoteServlet.html 的运行结果 图 5.19  Vote Servlet 的运行结果 程序第 17 行~第 20 行: static int[] voteCounter =  {  0, 0, 0, 0, 0  }; 声明并初始化了一个静态的、全局的整型数组对象 voteCounter,并将其中每个元素的值 初始化为 0。  voteCounter 表示客户选择的相关编程语言的投票数。 注意, 这里变量 voteCounter  使用了关键字 static。这样,在并发的 HTTP 客户端访问中,当任何一个客户端向 Serlvet 提 交数据,并更新 voteCounter 中任意一个元素的值后,所有访问该 Servlet 的客户端均可获得  voteCounter 的最新数据。因为 voteCounter 是一个全局变量,所以,我们把它放在 doGet,以 及 doPost 的方法外面。 该数组中的每个元素分别代表客户端点击的编程语言: Python、JAVA、  C#、Per,以及 PHP 的次数,下标(索引值)分别为 0~4。  VoteServlet.java 里面共有两个方法:  doGet 和 doPost。程序第 129 行~第 133 行的 doPost  方法仅仅是调用 doGet 方法。 程序第 23 行~第 126 行, 定义了 doGet 方法。 doGet 方法实现了统计客户端提交的数据, 并根据这些数据绘制出相应的水平直方图,最后将绘图结果返回给客户端。前面介绍的生成 三角形的例程中使用了 Sun 的 JPEG 特殊类 com.sun.image.codec.jpeg.*。 但我们认为采用该包 编写程序的通用性不够好,因为,这些代码在 com.sun  包中,不是核心 API 的一部分,也不 是标准的扩展包。JPEGImageEncoder 是 Sun 用来实现 JRE 某些功能所使用的特殊的 class。 因此,如果对图像的编码采用 JPEGImageEncoder 类,在其他一些 JRE 上运行会发生异常, 影响代码的可移植性。 在本例中,我们利用 J2SE 5.0 的新特性,来对缓冲图像的内容进行解码及编码工作。这 样代码会更简洁,性能更强大,移植性更好。程序在第 12 行: import javax.imageio.*; 引入了  Java  对图像处理的新包。javax.imageio  包默认可以读入一个  GIF、PNG、JPEG  和 BMP 格式的图像(注:J2SE1.4 版中也提供了该包,但不支持对 BMP 格式图像的读写操 作),以及输出一个 PNG、JPEG 和 BMP 格式的图像,并不支持输出 GIF 格式的图像。原因 是 GIF 格式的图像是有版权的,所以 Sun 公司在其有关图像处理的包中,就只提供了对 GIF  格式图像的读入功能,也就是说,只提供了对 GIF 图像的解码功能,而没有提供对 GIF 格式 图像的输出(即 GIF 图像的编码)功能。本程序将使用该包中的 ImageIO 类,来将缓冲图像 按指定的图像格式进行编码操作。 程序第 27 行:
    • response.reset(); 注意,在输出图像时, 程序的第一个语句要调用 response 的 reset 方法,强制清空 response  缓冲区的内容。 这样,可以保证在所有的 JSP/Servlet 服务器上运行 Web 图表编程的源程序时, 都可以获得正确的结果。 程序第 30 行: response.setContentType("image/png"); 使用  response 对象的  setContenType 方法,设置返回给客户端响应数据内容的类型为图 像,图像的格式为 png 格式。 程序第 33 行: float totalCounter = 0; 声明了一个浮点变量  totalCounter,并将其值初始化为  0。变量  totalCounter  表示客户端 的用户单击【投票】按钮的总次数。 程序第 36 行: String program = request.getParameter("program"); 获得用户 HTTP get 请求中的参数 program。因为,我们选择的单选按钮是 Java,而其对 应的参数值为“1” ,所以,这时字符串 program 的值就为“1”了。 程序第 39 行: String accept = request.getHeader("Accept"); 调用  request  的  getHeader  方法,获取页面头部信息。当正常浏览时,request.getHeader  ( Accept ” 返 回 的 内 容 为 “ image/gif 、 image/x­xbit­map 、 image/jpeg 、 image/pjpeg 、  “ ) application/msword、*/*”的字符串。而刷新页面时,该方法返回字符串的值为“*/*” ,将该 值赋值给字符串变量 accept。 程序第 40 行~第 44 行: if (program != null && !accept.equals("*/*"))  {  int programID = Integer.parseInt(program);  voteCounter[programID]++;  } 本段代码的功能是, 判断字符串变量 proram 是否为空, 以及客户端是否在刷新当前页面。 如果 proram 不为空,而且客户端也没有刷新页面,则更新静态变量整形数组 voteCounter 中, 相对应的元素值。注意,如果客户端是通过按浏览器工具栏上的“刷新”按钮或者是按键盘 的“F5”键来刷新当前页面的时候,request.getHeader( “Accept” )返回的内容为“*/*” ,所 以,我们在 accept.equals(“*/*”)方法前,加了一个取反符“!” 。 程序第 42 行,将字符串 program 的值转换成整数后,并赋值给整型变量 programID。因 此,现在 programID 的值为 1。因为,表示投票数的元素下标值和变量 programID 正好相等, 所以,在程序第 43 行,直接用 voteCounter[programID]++  的方式更新数组 voteCounter 中相 应元素的值。现在元素 voteCounter[1]的值为 1,也就是说,代表 Java 投票数的那个元素的值 为 1,而其他元素的值为 0。 程序第 47 行~第 50 行: for (int i = 0; i < voteCounter.length; i++)  {  totalCounter += voteCounter[i];  }
    • 通过一个简单的循环,计算出所有编程语言的总投票数。 程序第 54 行~第 56 行,创建了一个长度和宽度分别为 600 像素和 330 像素的缓冲图像 对象 image。程序第 59 行,创建了 image 对象的绘图环境 g。 程序第 62 行~第 68 行: g.setColor(Color.YELLOW);  g.fillRect(0, 0, width, height);  g.setColor(Color.white);  g.fillRect(11, 35, 580, 280);  g.setColor(Color.black);  g.drawRect(10, 35, 580, 280); 这里先用黄色填充了整个图形。然后,在 Java 坐标系中的点(10,35)处,黄色的背景 上绘制了一个有黑色边框的白色矩形。该矩形的长度和宽度分别为 580 像素及 280 像素。 程序第 70 行~第 72 行绘制标题。程序执行完第 72 行后的绘制结果如图 5.20①所示。 程序第 74 行~第 77 行,创建了一个字符串数组对象 programName,表示 5 种不同编程 语言的名称。
    • 图 5.20  Vote Servlet 的运行过程 程序第 79 行~第 84 行,创建了一个 Color 类的数组对象 color,分别表示 5 种编程语言 投票数的水平直方图的颜色。 程序第 86 行~第 88 行: g.setFont(new Font("黑体", Font.BOLD, 15));  g.drawString("编程语言", 25, 50);  g.drawString("所占比例", 520, 300); 在坐标系中相应位置绘制了两条字符串“编程语言”和“所占比例” 。程序执行完第  72  行后的绘制结果如图 5.20②所示。 程序第 92 行~第 107 行: int scaleLine = 0;  for (int i = 0; i <= 400; i += 40)  {  g.setColor(Color.black);  g.drawString("" + scaleLine + "%", (i + 80), 300);  g.setColor(Color.lightGray);  g.drawLine((i + 80), 65, (i + 80), 280);  scaleLine += 10;  }  g.setColor(Color.black);  g.drawLine(80, 55, 80, 280);  g.drawLine(80, 280, 550, 280); 为了更清晰地表现各种编程语言投票数占总投票数的比例,我们在坐标系相应位置用淡 灰色绘制了 10 条垂直的直线,每条直线依次表示到达该位置的投票数(用水平直方图表示) 正好占总投票数的 0%、10%、20%……90%、100%。然后,程序第 106 行和第 107 行,在相 关位置绘制了两条黑色的直线。执行完第 107 行后的绘制结果如图 5.20③所示。 程序第 109 行~第 120 行: int drawWidth = 0;  for (int i = 0; i < programName.length; i++)  {  g.setColor(color[i]);  drawWidth = (int)(voteCounter[i] * 400 / totalCounter + 0.5);  g.fill3DRect(80, 78+i * 40, drawWidth, 25, true);  g.setColor(Color.black);  g.drawString(""  voteCounter[i]  " 票数 ("  (int)(voteCounter[i]  100  +  +  +  *  / totalCounter + 0.5) + "%)", 100+drawWidth, 95+i * 40);
    • g.drawString(programName[i], 40, 95+i * 40);  } 根据客户端的投票情况, 计算每种编程语言的投票数, 进而计算出其占总投票数的比例。 然后,根据比例绘制相应长度的一个实心矩形,并绘制出相关的投票信息。例如,客户端对 该种编程语言的投票数及其占总投票数的比例。在本例中,我们对数据做出了如下的处理: Ø 水平直方图最长绘制长度为 400 像素; Ø 客户端对某种编程语言的投票数占总投票数的百分比只计算到个位, 后面数值部分采 取四舍五入的方法。 程序第 109 行,定义了一个整型变量 drawWidth,它表示水平直方图(实心矩形)的绘 制宽度。程序第 114 行,利用如下算法: (int)(voteCounter[i] * 400 / totalCounter + 0.5) 该算法实现了水平直方图(实心矩形)绘制宽度的计算。根据前面的假设, 在程序第 111  行~第 120 行的循环中,当循环变量 i 的值不为 1 时,绘制实心矩形的长度为 0;当循环变量  i 的值为 1 时,计算得到实心矩形的绘制长度为 400 像素,因为在 voteCounter 数组中,只有下 标为 1 的元素,即 voteCounter[1]的值为 1,其余元素的值都为 0,而总投票数也为 1,所以 最后的绘制宽度就是 400 像素。 程序第 117 行~第 118 行: g.drawString(""  voteCounter[i]  " 票数 ("  (int)(voteCounter[i]  100/  +  +  +  *  totalCounter + 0.5) + "%)", 100+drawWidth, 95+i * 40); 实现了绘制该编程语言的投票数及其占总票数的百分比的功能。 重复该循环,就绘制出了所有的图形,然后使用新的 ImageIO 类将其输出到客户端。 程序第 123 行~第 125 行: ServletOutputStream sos = response.getOutputStream();  ImageIO.write(image, "PNG", sos);  sos.close(); 先用 response 的 getOutputStream 方法,创建了一个 ServletOutputStream 对象 sos,然后, 利用 javax.imageio 包中的 ImageIO 类的静态 write 方法来编码图像, 即生成一个 PNG 格式的 图像。ImageIO 类的多种不同版本的 write 方法,都用于对图像进行指定格式的编码或格式转 换操作。本例中,调用的 write 方法,需要接收 3 个参数: Ø 一个实现了 RenderedImage 接口图像的绘图环境对象。该绘图环境可以从一个文件、 输入流或 URL 中获取图像的绘图环境得到。本例中,图像的绘图环境对象就是缓冲 图像 image。 Ø 一个指定了输出图像格式的字符串对象。本例中,指明图像的输出格式为“PNG”格 式。 Ø 一个实现了 ImageOutputStream 接口的输出流对象。本例中,实现该接口的对象就是  ServletOutputStream 类的对象 sos。 最后,调用 ServletOutputStream 对象 sos 的 close 方法,关闭输出流并结束本 Servlet。 如果要输出其他格式的图像,以输出“JPG”格式图像为例。需要改动两个地方。一个 是程序第 30 行: response.setContentType("image/png"); 修改为: response.setContentType("image/jpeg");
    • 另一个需要修改程序第 124 行: ImageIO.write(image, "PNG", sos); 修改为: ImageIO.write(image, "JPG", sos); 如果希望编码成“BMP”格式的图像,修改方法相同,此处不再赘述。 如果在生成图像/图表的同时,还需要处理客户端提交的中文参数,则需要修改程序第  30 行: response.setContentType("image/png; charset=gb2312"); 然后,使用前面例程介绍的编码转换方法进行处理就可以了。 按照我们假设的情况,当用户选择的是“Java”编程语言这个选项,并第一次单击【投 票】按钮后,本 Servlet 绘制出来的结果应该如图 5.21 所示。 单击工具栏上的“刷新”按钮或按键盘上的“F5”键,程序第 40 行~第 44 行,将判断 出事件的发生,不会发生更新数据的情况。读者可以修改程序的判断语句,并观察其运行结 果。
    • 图 5.21  第一次提交数据后的运行结果 当返回投票页面,再次单击【投票】按钮时,就可以看到数据更新了。图 5.19 显示的结 果就是多次单击【投票】按钮后的运行结果。只要不重新启动计算机或  JSP/Servlet,服务器 就会保存所有的投票信息。如果重新启动计算机或 JSP/Servlet 服务器,投票信息将全部被重 置,并重新开始更新。  5.6  Servlet 生成登录验证码 登录验证码技术是一种有效防范利用穷举法或脚本,通过在 Web 登录页面自动输入登录 名及登录密码来猜测及破解用户登录名及密码的技术。它通常是在用户进入  Web  登录页面 后,随机生成验证码,验证码是一幅包含一些数字或字母的图像。用户在登录的时候,必须 同时输入正确的登录名、登录密码以及验证码上所显示的数字或字母组合才可以通过身份校验。 对于一些具有自动填写登录信息的 Web 破解类程序来说,验证码技术对它的防范是相当成功的。 在大多数情况下,这类自动注册程序并不能识别验证码中的字符。 登录验证码技术也是一种有效防范恶意抢注免费邮箱等行为的技术。利用与上述介绍的 类似  Web  自动注册机,如果没有登录验证码技术,可以申请到许多免费的资源,如免费的  Web 电子邮箱、一些实时通讯/聊天软件,如,ICQ、UC、QQ 等。对于这类恶意抢占资源的 行为,在没有使用验证码技术之前,是很难防范的。而采用该技术后,这类恶意抢注行为大 幅减少。 此外,如果不使用登录验证码技术,攻击者除了会使用自动注册程序,注册大量的  Web  服务账户外,还可以使用这些账户影响其他普通用户的正常使用,如发送垃圾邮件或通过反 复登录多个账户来降低服务器的响应速度等。 某些新的 Web 破解类程序,还具有智能识别验证码上所显示的数字或字母的功能,为了 降低验证码被程序自动识别的风险,在生成验证码时,一般采用随机生成一些干扰图像,以 增大 Web 破解类程序自动识别验证码的难度。 可见,登录验证码技术有助于确保普通注册用户而不是自动化程序在填写登录表单,这 是 Web 应用中非常重要的一项技术。 现在大量网站都采取了验证码这类技术。如: Ø 新浪免费邮箱的注册页面(http://mail.sina.com.cn/cgi­bin/register/regMember1.cgi) 。 Ø 腾讯 QQ 首页上的邮箱登录页面(http://www.qq.com/) 。
    • Ø  Hotmail 的 Web 邮箱的申请页面。 设计思路如下: Ø 当客户端每次访问登录页面时,都会由一个  Servlet  生成一个新的验证码,并提供一 个供客户输入验证码的表单。在生成验证码的同时,将该验证码的内容保存在 Session  中。 Ø 用户输入验证码的内容后,单击“校验验证码”按钮后,会将提交的信息传递给另一 个 Servlet 处理。 Ø 该 Servlet 接收到用户提交的信息后,会将用户输入的验证码内容与保存在 Session 中 验证码的内容相对比,根据校验结果,输出不同的内容。 我们将介绍两种应用上述设计思路,编写的生成登录验证码的方法。  5.6.1  Servlet 生成登录验证码实例 1  本节介绍的实例包含三个主要文件。第一个文件是登录页面(validate.html) ,第二个文 件是  Servlet,其中一个  Servlet(CodeMakerServlet.java)用于生成随机登录验证码,最后一 个文件是 Servlet(ValidateServlet.java)用于校验用户输入的验证码和随机生成的验证码是否 相同。本例演示了如何生成一个包含 4 个数字的验证码,以及如何校验用户输入的数据是否 和验证码相同。 复制chap05Fig5.6  Fig.5.6_01 子目录中 web.xml 文档中的第 9 行~第 29 行内容,然后 将复制的内容粘贴到WEB­INFweb.xml 文档中<web­app></web­app>的标记中。 <servlet>  <servlet­name>CodeMakerServlet</servlet­name>  <description>生成验证码的 Servlet</description>  <servlet­class>com.fatcat.webchart.CodeMakerServlet</servlet­  class>  </servlet>  <servlet­mapping>  <servlet­name>CodeMakerServlet</servlet­name>  <url­pattern>/codeMaker</url­pattern>  </servlet­mapping>  <servlet>  <servlet­name>ValidateServlet</servlet­name>  <description>校验验证码的 Servlet</description>  <servlet­class>com.fatcat.webchart.ValidateServlet</servlet­ class>  </servlet>  <servlet­mapping>  <servlet­name>ValidateServlet</servlet­name>  <url­pattern>/validate</url­pattern>  </servlet­mapping> 然后, 将下载源码根目录下的 chap05Fig5.6Fig.5.6_01 子目录中的两个 Servlet 源程 序,  CodeMakerServlet.java  和  ValidateServlet.java  拷 贝 到  Web  应 用 程 序 根 目 录 下 的  WEB­  INFcomfatcatwebchart 子目录下面。重新启动 Tomcat 服务器,运行 IE,在地址栏中输入:  http:// localhost:8080/chap05/Fig5.6/Fig.5.6_01/validate.html。 我们可以看到图 5.22①所示的结果。 在 validate.html 页面中,验证码图片显示的字符为“6943” ,当客户在验证码下面的文本 框中也输入“6943” ,并单击【校验验证码】按钮,validate.html 会向 URL 为/validate 的名为  ValidateServlet 的 Servlet 发出 post 请求。  ValidateServlet 接收到该请求后, 程序会判断客户在文本框中输入内容和验证码图片中显
    • 示字符的内容是否相同,如果相同,则返回如图 5.22②所示的页面。反之,则会返回如图 5.23  ②所示的页面,并提示用户重新输入。 图 5.22  验证成功后的显示页面 图 5.23  验证失败后的显示页面 关于 validate.html(chap05Fig5.6Fig.5.6_01)代码,有三点需要说明: (1)为了确保系统安全,我们设定当客户端每次访问 validate.html 页面时,都会从服务 器重新加载最新的验证码图像。为实现此功能,我们在服务器端和客户端分别采用了一段代 码,强迫浏览器不再缓存 Web 页面,该功能由程序第 7 行~第 10 行的代码实现: <META HTTP­EQUIV="CONTENT­TYPE" CONTENT="text/html; charset=gb2312">  <META HTTP­EQUIV="Pragma" CONTENT="no­cache">  <META HTTP­EQUIV="Cache­Control" CONTENT="no­cache">  <META HTTP­EQUIV="Expires" CONTENT="0"> (2)同样地,为了保证用户提交数据的安全性,我们采用了  post  的请求方法,见程序 第 16 行。 (3)我们在程序的第 19 行,采用了 HTML 加载图像文件的标准语法,如,<img src=”  /codeMaker”/>来加载 Servelt 生成的验证码图像。因为,URL 为/codeMaker 的 Servlet,无论 是对 HTTP 的 get 请求还是 post 请求, 都返回一个图像文件, 因此,我们在 HTML 的元素 img  的属性 src 后,直接给出 Servlet 的 URL 即可。 单击【校验验证码】 按钮后,由 ValidateServlet 进行数据校验。生成验证码图片的 Servlet  的源程序——CodeMakerServlet.java 中共有 4 个方法,如下所示: (1)doGet 方法,负责响应客户端的 HTTP get 请求。方法体中,分别调用 drawCode 和  drawNoise 方法。 (2)doPost 方法,负责响应客户端的 HTTP post 请求,本例中,doPost 仅仅调用 doGet  方法来响应客户端的 post 请求。 (3)drawCode 方法,负责绘制验证码。 (4)drawNoise 方法,负责绘制干扰线。 程序第 16 行~第 22 行: private Font[] codeFont =  {  new Font("Algerian", Font.BOLD, 65),  new Font("Vivaldi", Font.BOLD, 85),  new Font("Broadway", Font.BOLD, 60),  new Font("Forte", Font.BOLD, 75)  }; 创建了包含 4 个 Font 对象的 Font 数组 codeFont,用于设置绘制验证码所需要的字体对 象。
    • 程序第 24 行~第 27 行: private Color[] color =  {  Color.BLACK, Color.RED, Color.DARK_GRAY, Color.BLUE  }; 创建了包含有 4 个 Color 对象的 Color 数组 color,用于设置绘制验证码所需要的颜色对 象。 程序第 29 行: String codeNumbers = “”; 声明了字符串变量 codeNumbers,表示绘制完成的验证码内容。 程序第 31 行:声明了两个整型变量,width 和 height,表示生成验证码图像的宽度和高 度。 程序第 34 行~第 81 行,重新定义了 doGet 方法,以响应客户端 get 请求。 程序第 38 行:调用 response 的 reset 方法,强制清空 response 缓冲区的内容。 程序第 41 行: response.setContentType("image/png"); 使用  response 对象的  setContenType 方法,设置返回给客户端响应数据内容的类型为图 像,图像的格式为 png。 前面讨论过,为了保证系统安全,我们设定当客户端每次访问 validate.html 页面时,都 应从服务器重新加载最新的验证码图像。为实现此功能,我们在服务器端和客户端分别采用 了一段代码,强迫浏览器不再缓存 Web 页面。程序第 44 行~第 46 行的代码,在服务器端实 现了该功能: response.setHeader("Pragma", "No­cache");  response.setHeader("Cache­Control", "no­cache");  response.setDateHeader("Expires", 0); 程序第 49 行~第 50 行,创建一个长度和宽度分别为 250 像素和 70 像素的缓冲图像对 象 image。 程序第 53 行,创建了 image 对象的绘图环境 g。 程序第 56 行~第 57 行: g.setColor(Color.YELLOW);  g.fillRect(0, 0, width, height); 用黄色填充整个图形,使其作为图像的背景色。 程序第 59 行~第 62 行: for (int i = 0; i < 4; i++)  {  drawCode(g, i);  } 在 for 循环体中,4 次调用了 drawCode 方法,用来绘制验证码,每调用一次 drawCode, 就会跳转执行程序第 84 行~第 92 行的 drawCode 方法,并在缓冲图像的相关位置绘制一个 数字字符。 public void drawCode(Graphics graphics, int i)  {  int number = (int)(Math.random() * 10);  graphics.setFont(codeFont[i]);  graphics.setColor(color[i]);
    • graphics.drawString("" + number, 10 + i * 60, 60);  codeNumbers += number;  }  drawCode 方法接收两个参数,一个是 Graphics 对象 graphics,另一个是整型变量 i。程 序第 86 行,定义了一个局部整型变量 number,number 的值是从一个 0~9 之间的随机数中 产生。然后,程序设置当前绘制图像的字体对象(程序第 87 行) ,以及颜色对象(程序第 88  行) 这里的字体对象和颜色对象都是从我们前面定义的字体对象数组 codeFont, 。 以及颜色对 象数组 color 中获取的。程序第 89 行,调用 drawString 方法,在当前缓冲图像的绘图环境中, 绘制刚计算出的随机数。注意,绘制随机数时,绘制参数中横坐标的值与  drawCode  方法传 递的整型参数 i 有关,纵坐标的值总是保持 60 不变。 程序第 91 行, 更新字符串 codeNumbers 内容。一旦调用了 drawCode 方法,code Number  就会将本次 drawCode 方法,产生随机数的内容附加在其内容中。 程序第 92 行,结束 drawCode  方法。 程序第 59 行~第 62 行,循环结束后,在当前缓冲图像的绘图环境中,总共绘制了 4 个 数字字符,同时,字符串 codeNumber 内容就是 4 个数字字符的内容。 验证码绘制完成后,继续绘制干扰线。前面我们讨论过,一些 Web 破解类程序具有智能 识别验证码上所显示数字或字母的功能,为了降低验证码被程序自动识别的风险,在生成验 证码时,通常还要随机生成一些干扰图像,以增大 Web 破解类程序自动识别验证码的难度。 程序第 64 行: drawNoise(g, 30); 调用 drawNoise 方法绘制干扰线, 就是为了达到这个目的。  drawNoise 方法接收两个参数, 一个是 Graphics 对象 graphics,另一个是整型变量 lineNumber。lineNumber 表示要绘制干扰 线的数目,该值越大,则绘制在验证码上的干扰线就越多,干扰效果就越好,但过多的干扰 线会影响普通用户的识别。 这里我们设置 lineNumber 值为 30,即绘制 30 条干扰线。干扰线的实际效果如图 5.22① 以及图 5.22②所示。程序第 95 行~第 106 行 drawNoise 方法,完成该干扰线的绘制工作: public void drawNoise(Graphics graphics, int lineNumber)  {  graphics.setColor(Color.YELLOW);  for (int i = 0; i < lineNumber; i++)  {  int pointX1 = 1 + (int)(Math.random() * width);  int pointY1 = 1 + (int)(Math.random() * height);  int pointX2 = 1 + (int)(Math.random() * width);  int pointY2 = 1 + (int)(Math.random() * height);  graphics.drawLine(pointX1, pointY1, pointX2, pointY2);  }  }  drawNoise 方法定义了 4 个局部的整型变量,并分别用随机数对其赋值。这 4 个变量分 别代表干扰线两个端点的横坐标和纵坐标。 利用这 4 个变量,调用 drawLine 方法,用黄色(程 序第 97 行)在当前绘图环境中绘制 30 条直线。 程序第 67 行~第 68 行: g.setColor(Color.black);  g.drawRect(0, 0, width ­ 1, height ­ 1); 在当前绘图环境中绘制一个黑色的空心矩形,实现了验证码的边框效果。 前面的设计思路中提到,在生成验证码的同时,将验证码的内容保存在  Session  中。那
    • 么什么是 Session?为什么要使用 Session?我们这里做一个简单的回顾。 众所周知,Web 应用程序协议被分成两大类别,无状态(stateless)和有状态(stateful) ; 协议状态指的是它 “记忆”从一个传输到下一个传输信息的能力。  Web 应用程序依赖于  HTTP  (属于无状态协议) ,它不能在各个网页之间存储用户的数据。这导致了在各个 Web 应用程 序中间也无法保存及共享数据。本例中,验证码的生成是由 CodeMakerServlet  生成的,但判 断用户输入和由  CodeMakerServlet  生成的验证码内容是否相同却是由  Validate  Servlet  所负 责。 ValidateServlet 是如何获得 CodeMakerServlet 所生成验证码的内容呢?也就是说, 我们需 要一种可以在 Web 应用程序(JSP/Servlet)以及网页中“维持”状态、 “记忆”及“传递”数 据的机制。 现在有许多方法可以在 HTTP 上模拟有状态连接。一般有以下三种解决方法: (1)Cookie。利用  HTTP  Cookie 来存储有关客户端和服务器之间的会话信息,后继的 各个连接可以查看当前会话,从服务器提取有关该会话的完整信息。这是一种应用最为广泛 的方法。 (2)改写 URL。可以把一些标识会话的数据附加到每个 URL 后面,服务器能够把该会 话标识和它所保存的会话数据关联起来。在浏览器不支持 Cookie 或用户禁用 Cookie 的情况 下也同样有效。然而,大部分使用 Cookie 时所面临的问题同样存在,即服务器端的程序要进 行许多简单且单调冗长的处理。另外,还必须保证每个 URL 后面都附加了必要的信息(包括 非直接的,如通过 Location 给出的重定向 URL) 。如果用户结束会话之后又通过书签返回, 则会话信息将会丢失。 (3)隐藏表单域。HTML  表单中可以包含下面的输入域:<INPUT  TYPE="HIDDEN"  NAME="session" VALUE="...">。当表单被提交时, 隐藏域的名字和数据也被包含到 GET 或 POST  数据里,我们可以利用这一机制来维持会话信息。然而,这种方法有一个很大的缺点,它要求所 有页面都是动态生成的,因为整个问题的核心就是每个会话都要有惟一的标识符。  Java Servlet 提供了一种与众不同的方案——HttpSession。HttpSession 是一个基于 Cookie  或者 URL 改写机制的高级会话状态跟踪接口。如果浏览器支持 Cookie,则使用 Cookie;如 果浏览器不支持 Cookie 或者 Cookie 功能被关闭,则自动使用 URL 改写方法。Servlet 开发者 无须关心细节问题,也无须直接处理 Cookie 或附加到 URL 后面的信息,HttpSession 自动为  Servlet 开发者提供一个可以方便存储会话信息的机制。  HttpSession  类隶属于  javax.servlet.http  包。每个客户端和服务器联机,都有一个对应的  Session 对象。也就是说,如果有 N 个用户同时请求该 Servlet,则 JSP/Servelt 服务器会有 N  个分别对应于每个客户端的 Session 对象。因为,Session 对象和客户端对象是绑定在一起的。 所以,如果将数据保存在  Session  对象中,客户端在联机过程中,在该客户端所请求的  Web  页面(JSP/HTML)以及 Servlet 应用程序中,就可以存取这些数据,从而达到跨网页分享数 据的目的。这就解决了我们刚才提到的 ValidateServlet 如何获得 CodeMaker Servlet 所生成验 证码内容的问题。 现在我们来看本例中, JSP/Servlet 服务器是如何实现 Session 与联机客户端 一一对应的。 当有多个客户端联机到 JSP/Servlet 服务器(调用 validate.html 页面) ,并发出 get/post 请 求给 CodeMakerServlet 以获取验证码图像时(validate.html 程序第 19 行) ,CodeMaker Servlet  为联机的客户端通过调用 HttpServletRequest 的 getSession 方法,自动创建一个会话对象,如 程序第 71 行所示: HttpSession session = request.getSession(true); 如果该客户端并没有被绑定  Session  对象,getSession  方法有可能返回  null。我们给  getSession 方法指定了一个布尔值为真“true”的参数,指定如果 getSession 方法返回结果为  null,则 CodeMakerServlet 为该客户端创建一个 Session 对象,将一个用于识别该 Session 对
    • 象的 sessionID 写入客户端的 Cookie 中,以便 Web 应用程序识别该对象。也就是说,当我们 使用 Session 对象的时候,客户端浏览器不能关闭 Cookie 功能。 程序第 72 行: session. setAttribute("codeNumbers", codeNumbers); 在 Java  Servlet  API  2.1 版或者更早版本的 Servlet  API 中,保存数据对象的使用方法是  putValue。而查看以前保存数据对象则使用 getValue 方法。getValue 返回 Object,因此,必须 把它转换成具体的数据类型。如果 Session 中指定的数据对象不存在,getValue 返回 null。 在 Java Servlet API API 2.2 版及其以后的版本中,Sun 推荐使用 setAttribute 方法来代替  putValue 方法保存数据对象。同样,推荐使用 getAttribute 方法来代替 getValue 方法,获得以 前保存数据对象。  getAttribute 返回 Object, 因此,必须把它转换成具体的数据类型。 如果 Session  中指定的数据对象不存在,getAttribute 返回 null。 这里 setAttribute 方法接收两个参数。第一个参数是字符串变量“codeNumbers” ,表示和 该 Session 属性名字为“codeNumbers” ;第二个参数是指定和属性名字为“code Numbers”绑 定的数据对象。这里我们将字符串对象 codeNumbers 和该 Session 相绑定。注意,Session 可 以绑定  Java  中任何合法的数据对象,不管是基本的数据类型对象,还是某个类(包括  Java  自带的类,以及开发人员自定义的类)的实例,都可以保存在该 Session 对象中。 当客户端每次发出  get  或  post  请求时,该客户端  sessionID  也会同时传递给 JSP/Servlet  服务器。JSP/Servelt 服务器以此 sessionID 来查找保存于服务器上的 Session 对象,如果找到 与该 sessionID 值相同的 Session 对象,则将客户端和该 Session 对象联系起来。如此,就可 以读取、更新、删除,以及保存在该 Session 中的数据对象了。 这就是 JSP/Servlet 服务器通过 Session 实现与联机客户端一一对应的这种机制。 程序第 75 行: codeNumbers = ""; 重新设置字符串变量 codeNumbers 内容,将其内容设置为空。重复该循环,绘制所有的 图形。 现在使用新的 ImageIO 类将其输出到客户端。 程序第 78 行~第 80 行: ServletOutputStream sos = response.getOutputStream();  ImageIO.write(image, "PNG", sos);  sos.close(); 这段代码同前面一个例程完全相同。 也是先用 response 的 getOutputStream 方法创建一个  ServletOutputStream 对象 sos,然后,利用 javax.imageio 包中的 ImageIO 类的静态 write 方法 来编码图像,即生成一个 PNG 格式的图像。 最后,调用 ServletOutputStream 对象 sos 的 close  方法,关闭输出流并结束本 Servlet。 这里有两点需要向读者说明: (1)Session  对象不可能无限期地,无限制地保存在服务器上。如果没有这个限制,任 何高性能的服务器都可能会在很短的时间内崩溃,因为  Session  对象的创建、修改、更新、 查找、跟踪和删除都需要消耗服务器资源。由于 WWW 无状态联机的特性,所以无法确定客 户端什么时候完成诸如网页浏览、登录、发出  get/post  请求,也无法确定客户端什么时候脱 机。因此,也就无法确定 Session 对象何时应该被注销,只有当某个 Session 对象被注销后, 其所占用的系统资源才会被释放,才能被服务器所使用。为解决此问题,一般在  JSP/Servlet  服务器中,对每个  Session  对象都设置了一个默认的、有效的生命周期。Resin  服务器默认  Session 的生命周期是 30 分钟。 当某个联机客户端在创建 Session 对象后, 在 Session 默认的、
    • 有效的生命周期内没有向 JSP/Servlet 服务器发出任何 get 或 post 请求,则该客户端就被认为 是处于脱机状态,与该客户端相对应的 Session 对象也就自动被 JSP/Servlet 服务器所注销。 (2) 前面介绍过, 使用 Cookie 也可以实现跨网页的数据分享, 那么为什么要使用 Session  对象呢?我们只要看看 Cookie 的缺点就清楚了: Ø  Cookie 只能保存文本数据(即字符串对象) ,无法保存其他类型的数据对象。 Ø 要使用 Cookie,必须要有客户端浏览器的支持。 Ø  Cookie 保存在客户端,数据的大小受限制。 Ø  Cookie 在数据的存取上不方便。 Ø  Cookie 是以明文的方式保存在客户端的, 因此,在一些对安全性要求比较高的环境下,  Cookie 就无法满足安全性的要求。 对于 Session 对象,除了要求客户端浏览器必须支持 Cookie 外, 其他 Cookie 的缺点 Session  都没有。 那么如何设置 Session 的生命周期呢?先来看 Tomcat 中的设置方法。 打开 Tomcat 安装目录下 conf/server.xml 文件,在<host></host>标记中增加以下内容: <session­config>  <session­timeout>60</session­timeout>  </session­config> 这里<session­timout>标记就是设置 Session 的生命周期, 60 表示 Session 对象的生命周期 为 60 分钟,该数字是以分钟为计时单位。 同理,在 Resin 中设置 Session 的生命周期的方法是: 修改 Resin 安装目录 conf/resin. conf  文件,在 resin.conf 文件<web­app></web­app>标记中增加以下内容: <session­config>  <!­­ 2 hour timeout ­­>  <session­timeout>120</session­timeout>  </session­config>  <session­timout>标记中的数字,同样也是以分钟为计时单位。这里设置  Session  的生命 周期为 120 分钟。 生成验证码的  Servlet(CodeMakerServlet.java)我们就介绍完了,下面讨论用于校验用 户输入的验证码与随机生成的验证码内容是否相同。该功能由 ValidateServlet.java 程序实现。  ValidateServlet.java 的源程序中共有两个方法,如下所示: Ø  doGet 方法:负责响应客户端 HTTP get 请求。本例中,doGet 仅仅调用 doPost 方法来 响应客户端 get 请求。 Ø  doPost 方法:负责响应客户端 HTTP  post 请求,用来校验保存在 Session 中的验证码 内容是否与用户输入的验证码内容相同,并根据校验结果输出不同的内容。 同样,为了保证系统安全,我们在服务器端使用程序第 20 行~第 22 行的代码,强迫浏 览器不再缓存 Web 页面。 程序第 25 行: String validateCode = request.getParameter("validateCode"); 获得客户端提交的 vaildateCode 参数值。 程序第 28 行~第 29 行: HttpSession session = request.getSession();  String codeNumbers = (String)session.getAttribute("codeNumbers"); 获得先前保存的  Session  对象“codeNumbers”。这里使用的是  Session  类的  getAttribute  方法,来获得先前保存的属性名字为 “codeNumbers”的 Session 对象绑定在一起的数据对象。
    • getAttribute 方法需要接收一个字符串参数,该字符串表示  Session 的属性名字。如果指定属 性名字的 Session 对象不存在,则返回 null 对象。如果指定属性名字的 Session 对象存在,则 返回一个 Object 对象。我们必须手动转换或指定该 Object 对象的类型。 因为,在生成验证码图片的 Servlet 中,我们已经将一个字符串对象( “codeNumbers” ) 和该 Session 绑定(见 CodeMakerServlet.java 源程序第 72 行) 。所以,程序第 29 行,使用了 “(String) ”关键字,强制将返回的  Object  对象转换成  String 对象,然后,使用转换后得到 的 String 对象来初始化字符串对象 codeNumbers。 程序第 33 行~第 36 行: if (codeNumbers == null)  {  response.sendRedirect(url);  return ;  } 这段代码实现的功能是:如果客户端没有通过  validate.html  验证页面而直接访问本  Servlet(ValidateServlet.java),则将浏览器重新导向到指定的页面去处理。如果客户端没有通 过 validate.html 验证页面而直接访问本 Servlet,则此 Servlet 在执行到程序第 28 行~第 29 行 语句时,getAttribute 方法返回对象为 null,此时没有数据对象和该 Session 相绑定。 如果 codeNumbers 对象为 null,则会调用 response 的 sendRedirect 方法,将浏览器重新导 向 到 指 定 的 页 面 去 处 理 。 这 里 指 定 处 理 页 面 的  URL  就 是 “ /chap05/Fig5.6/Fig.5.6_01  (见程序第 31 行) /validate.html” 。因此,如果客户端的浏览器第一次直接访问 Servlet,就会 被 Servlet 重新导向到 JSP/Servlet 服务器根目录下chap05Fig5.6Fig.5.6_01  validate.  html 页 面。程序的其他部分,读者都很熟悉了。现在介绍这个 Servlet 第 59 行~第 69 行的核心代码 部分: if (codeNumbers.equals(validateCode))  {  out.println(  "<h1><font color="green">输入相同,校验成功</font></h1> ");  }  else  {  out.println("<h1><font color="red">输入错误,校验失败</font>  <br>");  out.println("<p>请重新<a href="" + url +  "">输入验证码</a><h1> </p>");  } 这里调用字符串 codeNumbers 的 equals 方法,比较它和字符串 validateCode 的内容是否 相同。如果相同,则返回真值,并输出如图 5.22②所示的结果;否则,返回假值,并输出如 图 5.23②所示的结果。  5.6.2  Servlet 生成登录验证码实例 2  前例所讲述的生成验证码图像 Servlet 有个缺陷。如果我们希望更改验证码图像中数字字 符的字体、色彩、大小等属性,就必须修改源程序,然后,重新编译这个 Servlet 才可以看到 新的执行效果。如果我们对执行效果不满意,就要重复上述过程,这是比较麻烦的。 怎样才可以在不修改源程序的情况下,任意设定验证码的显示效果呢?我们的思路是: 生成随机数后,加载相应的数字字符图像文件,最后,将所有加载的数字字符图像文件合成 为一个验证码图像后再返回给客户端。如果要更改验证码图像中数字字符的显示效果,则只 需要替换数字字符的图像文件即可。这样就达到了在不修改源程序的情况下生成个性化的、
    • 丰富多彩的验证码图像的目的。 复制 chap05Fig5.6 Fig.5.6_02 子目录中 web.xml 文档第 9 行~第 18 行的内容: <servlet>  <servlet­name>ImageCodeMakerServlet</servlet­name>  <description>加载图像文件并生成验证码的 Servlet</description>  <servlet­class>com.fatcat.webchart.ImageCodeMakerServlet</servlet  ­class>  </servlet>  <servlet­mapping>  <servlet­name>ImageCodeMakerServlet</servlet­name>  <url­pattern>/imageCodeMaker</url­pattern>  </servlet­mapping> 并粘贴到WEB­INFweb.xml 文档中<web­app></web­app>标记中。 在读者下载的源码根目录下 chap05Fig5.6Fig.5.6_02 的子目录中还可以看到一个 images  子目录,其下面是  10  个  gif  图像文件,分别表示数字字符  0~9。两个  Servlet  源程序:  ImageCodeMakerServlet.java  和  ValidateServlet.java。同样,也有一个  validate.html  文档,该  HTML 文档作用和前例相同。 这里我们暂时使用上节介绍的校验  Servlet。现在只拷贝  ImageCodeMakerServlet.java  到  Web  应用程序根目录下的WEB­INFcomfatcatwebchart  子目录下。然后,将  images  整个目 录拷贝到 Web 应用程序根目录下面(本书的路径为:d:webchart) 。 这里的 vaildate.html 的源程序与上节介绍的 validate.html 相比, 只是程序的第 19 行不同: <img src="/imageCodeMaker" /><br /> 获取实时生成的验证码图像的 Servlet 不同而已。 现在观察程序的运行结果,重新启动 Tomcat 服务器运行 IE,在其地址栏中输入:http://  localhost:8080/chap05/Fig5.6/Fig.5.6_02/validate.html。 可以看到如图 5.24①所示的结果。 图 5.24  验证成功后的显示页面 客户端输入验证码的校验过程同上节讲述的完全一样,我们在此略过。该验证码图片实 际上由 Web 应用程序根目录下 images 子目录下的 3.gif、9.gif、7.gif 和 6.gif 图像文件合成而 成。下面讨论本例中的图片效果是如何实现的。 生成该验证码的  Servlet(chap05Fig5.6Fig.5.6_02ImageCodeMakerServlet.java)的源程 序与上节介绍的 Servlet 结构几乎相同,不同的只是在程序第 71 行~第 88 行的 drawCode 方 法: public void drawCode(Graphics  graphics, int i,  HttpServletRequest  request)
    • {  int number = (int)(Math.random() * 10);  String imageFilePath = request.getRealPath("images" + number +  ".gif"); File imageFile = new File(imageFilePath);  Image gifFile = null;  try  {  gifFile = ImageIO.read(imageFile);  }  catch (Exception e)  {  System.out.println(e);  }  graphics.drawImage(gifFile, i* 60, 0, null);  codeNumbers += number;  } 首先,方法接收的参数不同。本例中 drawCode 接收 3 个参数,第 1、2 个参数与前例相 同, 但第 3 个参数接收一个 HttpServletRequest 的对象。该对象就是代表客户端请求的 request  对象。 程序第 73 行,定义一个局部的整型变量 number,number 值是从一个 0~9 之间的随机 数中产生。这里我们以图  5.24①所示的验证码为例,来探讨本方法的运行过程。第一次调用  drawCode 方法时,计算出来的随机数的值为 3,然后,将该值赋值给变量 number。 所以,程序第 74 行: String imageFilePath = request.getRealPath("images" + number +  ".gif"); 实际执行的是: String imageFilePath = request.getRealPath("images3.gif"); 先调用 response 的 getRealPath 方法,该方法返回一个字符串对象,表示其参数中指定文 件的实际路径。也就是说,返回的是  Web  应用程序根目录下“images3.gif”文件的实际路 径。这里之所以要使用 “”是因为 在 Java 中是作为转义字符的。 , “” 所以, “images3.gif” 这里 经过编译器解释后就表示字符串“image3.gif” 。 在本机上,该实际路径返回的字符串内容是“d:webchartimages3.gif”这样一个完整的 路径。 注意, 如果是在 UNIX/Linux 下, 返回结果类似于“Web 应用程序根目录/images/3.gif” , 斜杠的方向和 Windows 正好相反。Java 可以做出正确的处理,我们只需要给出正确的参数就 可以了。之后,将返回内容赋值给字符串变量 imageFilePath。 程序第 75 行: File imageFile = new File(imageFilePath); 创建了一个 File 对象 imageFile,并用 File 的构造器对其进行初始化。这里 File 构造器接 收的参数就是刚刚获得的 imageFilePath,也就是 3.gif 这个图像文件的完整路径。 程序第 76 行: Image gifFile = null; 声明了一个 Image 类的对象 gifFile,gifFile 用来表示我们将要加载的文件。Image 类隶 属于 java.awt 包。 程序第 79 行: gifFile = ImageIO.read(imageFile);
    • 这里再次利用 javax.imageio 包中的 ImageIO 类的静态 read 方法,获得一个 Image 对象。  ImageIO 类的静态 read 方法,返回一个 BufferedImaged 类的对象。因为,Buffered Imaged 类 是 Image 类的子类,所以,可以将 ImageIO 类的静态 read 方法返回的 Buffered  Imaged 类的 对象,直接赋值给 Image 对象 gifFile。因为,在加载图像的过程中,可能会发生异常,所以, 将该方法放置在 try…catch…中。 程序第 85 行: graphics.drawImage(gifFile, i* 60, 0, null); 直接调用 drawImage 方法, 在当前缓冲图像的绘图环境中绘制刚刚加载的 3.gif 图像文件。 注意,绘制该图像时,绘制参数中横坐标的值与 drawCode 方法传递的整型参数 i 有关,而纵 坐标总是保持0不变。这里的“i* 60” ,表示下一次绘制图像时的横坐标向右再移动 60 像素。 为什么要再移动 60 像素呢?读者可以使用 ACDSee 之类的软件,查看/images 目录下 gif 图 像文件,其中 2.gif 和 4.gif 两个图像的宽度最大,都为 60 像素。所以,我们这里定义两次绘 图之间的横坐标相差 60 像素。而 gif 图像的高度都相同,都为 60 像素。这也是我们为什么 将验证码图像的宽度设置为 240 像素,  高度为 60 像素的原因(程序第 18 行) 。 注意,在 JSP/Servlet 中调用 Graphics 类的 drawImage 方法时,最后一个参数定义为 null。 程序第 87 行, 更新字符串 codeNumbers 的内容。 一旦调用 drawCode 方法,code Number  就会将本次  drawCode  方法所产生的随机数内容,附加在其内容之中。程序第  88  行,结束  drawCode 方法。 这样,程序第一次循环,调用 drawCode 方法时,就在当前绘图环境中坐标为(0,0)处, 绘制了图像 3.gif。同理: Ø 第二次循环,就在当前的绘图环境中坐标为(60,0)处,绘制图像 9.gif; Ø 第三次循环,就在当前的绘图环境中坐标为(120,0)处,绘制图像 7.gif; Ø 第四次循环,就在当前的绘图环境中坐标为(180,0)处,绘制图像 6.gif。 与前例不同的地方是在程序第 51 行: // drawNoise(g, 30); 将 drawNoise 方法,也就是绘制干扰线的方法注释掉,最终生成的验证码图像上就没有干扰 线(如图 5.24①所示) 。最后,将图像输出到客户端,就完成了生成验证码的全过程。 如果希望生成其他风格的验证码, 只需要将另外风格的数字字符文件覆盖到 Web 应用程 序根目录下 images子目录中就可以了。需要注意以下两点: (1)图像文件的格式为 gif; (2)图像文件的宽度及高度都不超过 60 像素。 如此,我们就不必改变源代码而得到不同风格的验证码。 当前,我们调用校验验证码的 Servlet 与前例完全一样。如果用户输入的数据与验证码内 容相同,则可以看到如图 5.24②所示的结果。如果两者内容不相同,当点击如图 5.23②所示 “ 输 入 验 证 码 ” 的 超 链 接 的 时 候 , validate  Servlet  会 将 页 面 重 新 链 接 到 : http://  localhost:8080/chap05/Fig5.6/Fig.5.6_01/validate.html  , 而 没 有 导 向 如 下 地 址 :  http://  localhost:8080/ chap05/Fig5.6/Fig.5.6_02/ validate.html。 如何让 Servlet 在用户输入错误的情况下,重新导向到各自不同的 validate.htm 页面呢? 其实也很简单,我们只需要在两个文档中做些小小的修改即可。 (1)修改 chap05Fig5.6Fig.5.6_02下的 validate.html 文档。 在该文档第 25 行后,增加一行代码即可: <input type="hidden" name="digits" value="digits"/>
    • 在 validate.html 文档中的表单区域增加一个隐藏的表单对象,该表单对象的名称和值均 为“digits”。在运行本文档的时候。如果客户端单击【校验验证码】按钮时,该隐藏的表单 对象同时提交给相应的 Servlet。 (2)修改WEB­INFcomfatcatwebchartValidateServlet.java,将程序第 67 行~第 68 行 的代码: out.println("<p>请重新<a href="" + url +"">输入验证码</a><h1></p>"); 更改为以下内容即可: String url2 = "/chap05/Fig5.6/Fig.5.6_02/validate.html";  String flag = request.getParameter("digits");  if ( flag == null)  {  out.println("<p>请重新<a  href=""  url  "">输入验证码</a><h1>  +  +  </p>");  }  else  {  out.println("<p>请重新<a href="" + url2 + "">输入验证码</a><h1>  </p>");  } 这里新增了两个字符串变量 url2 和 flag。  从客户端 request 请求中获取 flag “digits”参数。 只有  chap05Fig5.6Fig.5.6_02下  validate.html  文档才会提交这个参数。因此,如果客户端输 入的数据和验证码不符,我们就在这里做简单的判断,如果 flag 为 null,则说明当前 request  请求来自:http://localhost:8080/chap05/Fig5.6/Fig.5.6_01/validate.html,则将页面重新链接到 上 述 地 址 。 反 之 , 如 果  flag  不 为  null , 则 说 明 当 前  request  请 求 来 自 :  http://localhost:8080/chap05/Fig5.6/Fig.5.6_02/validate.html,则将页面重新链接到该地址即可。 重新启动 JSP/Servlet 服务器,现在 vaildate Servlet 就可以正确处理来自不同页面的校验 请求了。如果用户输入与验证码内容不符,则会重新链接到发出该 request 请求填写验证信息 的页面。
    • 5.7  Servlet 高级设置 通过前面的例程我们介绍了关于 Servlet 的设置。本节将介绍一些新的设置方法。  Servlet 的设置包含除前面介绍过的 Servlet 完整路径及类名、别名、描述、映射外,还包 括初始化参数、启动加载优先级等。  5.7.1  Servlet 初始化参数  Servlet 可以配置一些初始化参数,并在 Servlet 中通过 javax.servlet.ServletConfig 接口中 的方法来获取这些参数。表 5.4 列出了该接口中的几个常用方法。 表 5.4    ServletConfig 接口的部分方法 方 法 说 明  public String getInitParameter( String name)  返回指定名字的 Servlet 初始化参数  public Enumeration  返回一个 Enumeration 对象,包含所有的 Servlet 初始化参数的名字  getInitParameterNames()  返回当前 Servlet 实例的名字。该名字可能由服务器管理员命名,也可由  public String getServletName()  Web 应用程序的部署描述符提供,如果当前 Servlet 实例没有被命名,则返 回该 Servlet 的类名  返回  Servlet  上下文对象的引用。它用来定义一个  Servlet  的环境对象。 public ServletContext  也可以认为是多个客户端共享的信息。它与 session 的区别在于应用范围不 getServletContext()  同,session 只对应于一个用户 我们以本章第 5.5 节介绍的投票统计的 Servlet 来说明如何配置及获取初始化参数。在前 面介绍的投票统计的 Servlet 中,编程语言 Python、Java、C#、Perl、PHP 的点击数的初始值 被设置为 0,现在我们在 Servlet 中,设定 Python、Java、CSharp(即 C#) 、Perl、PHP 的点 击数的初始值为 10。用户的点击数在设定初始值的基础上进行增加。  Servlet 的初始化参数是保存在 web.xml 中, 通过 ServletConfig 接口中的 getInit Parameter  方法,获得这些初始化参数。下面来看看具体的例子。 复制 chap05Fig5.7下的 web.html 文档中的第 9 行~第 38 行的内容,然后,将复制的内 容粘贴到WEB­INFweb.xml 文档的<web­app></web­app>标记中。 <servlet>  <servlet­name>InitVote</servlet­name>  <description>调用初始化参数的网站投票 Servlet</description>  <servlet­class>com.fatcat.webchart.InitVoteServlet</servlet­ class>  <init­param>  <param­name>Python</param­name>  <param­value>10</param­value>  </init­param>  <init­param>  <param­name>Java</param­name>  <param­value>10</param­value>  </init­param>  <init­param>  <param­name>CSharp</param­name>  <param­value>10</param­value>  </init­param>  <init­param>  <param­name>Perl</param­name>  <param­value>10</param­value>
    • </init­param>  <init­param>  <param­name>PHP</param­name>  <param­value>10</param­value>  </init­param>  </servlet>  <servlet­mapping>  <servlet­name>InitVote</servlet­name>  <url­pattern>/initVote</url­pattern>  </servlet­mapping> 我们可以看到,每一个初始化的参数都是放置在<servlet></servlet>的标记中。 <servlet>  ...  <init­param>  <param­name>Python</param­name>  <param­value>10</param­value>  </init­param>  ...  <servlet> 而每一个初始化参数的标记是包含在<init­param></init­param>标记及其之间的两个元 素。<param­name>以及<param­value>标记,分别表示参数的名称和值。在上述 web.xml 文件 中,我们指定 Python、Java、CSharp、Perl 和 PHP 参数的参数值为 10。 获取初始化参数的 Servlet 源程序(chap05Fig5.7InitVoteServlet.java)增加了一个 init 方 法(程序第 23 行~第 30 行) ,以获取保存在 web.xml 中的初始化参数。我们在第 5.1 节中介 绍过 Servlet 的运行机制,Servlet 部署在 JSP/Servlet 容器内,其生命周期由 JSP/Servlet 容器 管理,同 Applet 的生命周期由 Applet 的容器——浏览器管理相同:在创建一个 Servlet 实例 后,就调用 Servlet 的 init 方法。 public void init()throws ServletException  {  voteCounter[0] = Integer.parseInt(getInitParameter("Python"));  voteCounter[1] = Integer.parseInt(getInitParameter("Java"));  voteCounter[2] = Integer.parseInt(getInitParameter("CSharp"));  voteCounter[3] = Integer.parseInt(getInitParameter("Perl"));  voteCounter[4] = Integer.parseInt(getInitParameter("PHP"));  } 在上面 init 方法中,获得 web.xml 中的初始化参数的方法是 getInitParameter 方法。该方 法接收一个字符串变量,表示初始化参数的名字,返回结果是一个字符串,表示该初始化参 数的值。程序的其余部分和第 5.5 节的 Servlet 完全相同。 同样,只要不重新启动计算机或 JSP/Servlet 服务器,服务器就会保存所有的投票信息。 如果重新启动计算机或 JSP/Servlet 服务器, 投票信息将全部被重置, 并重新获取初始化参数, 然后,再更新点击数。 调用本  Servlet  的  HTML  文档  InitVoteServlet.html (chap05Fig5.7)和第  5.5  节的  VoteServlet.html 文档,除在程序第 12 行被更改为以下内容: <form action = "/initVote" method = "get"> 其余部分也完全相同。 图 5.25 是启动 IE,运行 http://  chap05/Fig5.7/InitVoteServlet.html,不选择任何单选项, 直接单击【投票】按钮后的运行结果。我们可以看到,5  种语言的投票点击数的初始值都被 设置为 10。说明本 Servlet 已经正确加载了所有的初始化参数。
    • 图 5.25  获取初始化参数的 Servlet  5.7.2  Servlet 加载优先级  Servlet  加载优先级是通过  web.xml  文件来配置的。我们可以在  web.xml  文件中的  <servlet></server>标记中增加一个<load­on­startup></load­on­startup>属性。如下所示: <servlet>  <servlet­name>CodeMakerServlet</servlet­name>  <description>生成认证码的 Servlet</description>  <servlet­class>com.fatcat.webchart.CodeMakerServlet</servlet­  class>  <load­on­startup>20</load­on­startup>  </servlet>  <servlet­mapping>  <servlet­name>CodeMakerServlet</servlet­name>  <url­pattern>/codeMaker</url­pattern>  </servlet­mapping>  <servlet>  <servlet­name>ValidateServlet</servlet­name>  <description>校验认证码的 Servlet</description>  <servlet­class>com.fatcat.webchart.ValidateServlet</servlet­ class>  <load­on­startup>10</load­on­startup>  </servlet>  <servlet­mapping>  <servlet­name>ValidateServlet</servlet­name>  <url­pattern>/validate</url­pattern>  </servlet­mapping>  <servlet>  <servlet­name>ImageCodeMakerServlet</servlet­name>  <description>加载图像文件并生成验证码的 Servlet</description>  <servlet­class>com.fatcat.webchart.ImageCodeMakerServlet</  servlet­class>  <load­on­startup>AnyTime</load­on­startup>  <servlet­mapping>  <servlet­name>ImageCodeMakerServlet</servlet­name>  <url­pattern>/imageCodeMaker</url­pattern>  </servlet­mapping> 属性<load­on­startup></load­on­startup>中,我们分别提供了  3  个不同的值:10、20  和  AnyTime。这里设定,  ValidateServlet 在 CodeMakerServlet 之前先被加载。 属性<load­on­ startup>  的值越小,则说明该 Servlet 的加载优先级越高,也就是说,被 JSP/Servlet 服务器加载的时间
    • 就越早。如果说该属性值为 AnyTime,则说明该 Servlet 可以在服务器启动后的任意时间加载。  5.7.3  Servlet 映射 前面所介绍的每个例程中,我们都对 Servlet 进行了设置。每一个 Servlet 都对应了一个 映射。一个 Servlet 可以设置多个映射。这样就可以通过不同的 URL 来访问同一个 Servlet。 我们以本章第 5.1 节所介绍的 WelcomeServlet 为例,讨论如何为其设定多个映射。 <servlet>  <servlet­name>WelcomeServlet</servlet­name>  <description>简单的、处理 get 请求的 Servlet</description>  <servlet­class>com.fatcat.webchart.WelcomeServlet</servlet­  class>  </servlet>  <servlet­mapping>  <servlet­name>WelcomeServlet</servlet­name>  <url­pattern>/welcome</url­pattern>  </servlet­mapping>  <servlet­mapping>  <servlet­name>WelcomeServlet</servlet­name>  <url­pattern>/welcome/*</url­pattern>  </servlet­mapping>  <servlet­mapping>  <servlet­name>WelcomeServlet</servlet­name>  <url­pattern>/fatcat/welcome</url­pattern>  </servlet­mapping>  <servlet­mapping>  <servlet­name>WelcomeServlet</servlet­name>  <url­pattern>/welcome.jsp</url­pattern>  </servlet­mapping> 通过上述的配置,我们就可以按照 <url­pattern> 属性中指定的  URL  来访问  Welcome  Servlet。下面的访问方式都是正确的: Ø  http://localhost:8080/welcome/msadfaasdfasd  Ø  http://localhost:8080/welcome/welcome&words=fatcat  Ø  http://localhost:8080/welcome/test.html  Ø  http://localhost:8080/welcome/weclome.jsp?name=fatcat  也就是说,只要访问的 URL 是以“/welcome/”开头,都可以正确地访问到这个 Servlet。  5.8  Servlet 绘制甘特图 甘特图又称为“Gantt Chart” ,是美国人甘特在 20 世纪 20 年代率先提出的。如图 5.26  所示。它与我们前面介绍的水平直方图相类似,是一种安排工作进度的有力工具。在甘特图 中,用横轴表示时间刻度,纵轴表示计划,用一条“横条”表示该计划的起始和结束时间。 甘特图的优点是简单、明了、直观、易于绘制,因此,它是 Web 图表编程中经常绘制的图表 之一。 本节介绍如何利用一些基本图形的绘制(直线、实心矩形、文本)来实现甘特图。 本节的例程(chap05Fig5.8GanterServlet.java)中的甘特图,用来表示一个总天数为  10  天的项目进度安排。该项目从 2 月 1 日开始到 2 月 10 日结束, 该项目又可分解为 8 个子计划, 每个计划的开始时间及完成该计划所需的时间从随机数中产生。 程序的运行结果如图 5.26 所 示。我们可以观察到,该项目中各个计划的进度、开始时间、完成各计划所需的时间、结束 时间等信息。
    • 图 5.26  生成甘特图的 Servlet  首先下载源码根目录下的 chap05Fig5.8 子目录中的 web.xml 文档,复制该文档中的第 9  行 ~ 第  18  行的内 容,然 后,将 复制的 内容 粘贴到 WEB­INFweb.xml  文 档 中 的<web­  app></web­app>标记中。 <servlet>  <servlet­name>GanterServlet</servlet­name>  <description>绘制甘特图的 Servlet</description>  <servlet­class>com.fatcat.webchart.GanterServlet</servlet­class>  </servlet>  <servlet­mapping>  <servlet­name>GanterServlet</servlet­name>  <url­pattern>/ganter</url­pattern>  </servlet­mapping> 调用本  Servlet  的  Ganter.html  的源程序很简单,只需要了解程序第  12  行,调用  Servlet  生成甘特图的方法就可以了。然后,将  GanterServlet.java  拷贝到WEB­INFcomfatcatweb  chart下面。  GanterServlet.java 的源程序第 26 行~第 33 行: int totalProcess = 8;  int[] startDay = new int[totalProcess];  int[] processDay = new int[totalProcess];  for (int i = 0; i < totalProcess; i++)  {  startDay[i] = 1+(int)(Math.random() * 9);  processDay[i] = 1+(int)(Math.random() * (11­startDay[i]));  } 整型变量 totalProcess 表示组成该项目子计划的数目, 整型数组 startDay 表示计划的开始 时间, 整型数组 processDay 表示完成计划所需要的时间。 然后,通过一个循环, 分别为 startDay  和 processDay 数组赋值。注意,这里 startDay 中元素的值是从一个 1~9 中的随机数中产生, 而 processDay 中元素的值是从 1 到 11­startDay 之间的数字中产生。 程序第 36 行~第 70 行的代码,绘制的结果如图 5.27①所示。 程序第 72 行~第 78 行: for (int i = 0; i <= 400; i += 40)
    • {  g.setColor(Color.BLACK);  g.drawString("2­" + (1+i / 40), (i + 70), 420);  g.setColor(Color.LIGHT_GRAY);  g.drawLine((i + 80), 65, (i + 80), 400);  } 这段代码绘制了 11 条垂直方向的浅灰色的直线,每条直线表示 2 月 1 日到 2 月 11 日中 的每一天,以及横坐标上表示时间的字段。绘制的结果如图 5.27②所示。 图 5.27  甘特图的绘制过程 1  程序第 81 行~第 83 行: g.setColor(Color.BLACK);  g.fillRect(77, 55, 3, 345);  g.fillRect(80, 397, 420, 3); 绘制坐标轴,与以前我们绘制坐标轴不同之处是:以前绘制的坐标轴是用两条直线来表示。 本例中,是用两个实心矩形来表示。程序执行完此段代码后的运行结果如图 5.28①所示。
    • 图 5.28  甘特图的绘制过程 2  程序第 85 行~第 101 行,是绘制甘特图的核心代码。主要执行了以下操作: Ø 绘制纵坐标上的字段,表示组成本项目的子计划。 Ø 绘制浅灰色的水平直线。 Ø 计算代表每个子计划的实心矩形的绘制参数, 如左上角顶点坐标以及绘制实心矩形的 高度,参与运算的这些数据都从前面的整型数组中获得。计算绘制参数后,用不同的 颜色填充实心矩形。绘制完实心矩形后,用字体为黑体、大小为 15 磅的字体,在实 心矩形上绘制完成该计划所需要的时间。当循环结束后,完成绘制工作。 现在来看程序是如何实现上述操作的。在该循环中,程序第 89 行~第 90 行: g.drawString("计划" + (i + 1), 40, 95+i * 40); 完成了上述第 1 点的绘制工作。程序第 91 行~第 92 行: g.setColor(Color.LIGHT_GRAY);  g.drawLine(80, 90+i * 40, 480, 90+i * 40); 完成了上述第 2 点的绘制工作。程序执行完第 92 行后的绘制效果如图 5.28②所示。 程序第 93 行~第 100 行: g.setColor(color[i]);  drawWidth = processDay[i] * 40;  g.fill3DRect(80+(startDay[i] ­ 1) * 40, 78+i * 40, drawWidth, 25, true);  g.setColor(Color.WHITE);  g.setFont(new Font("黑体", Font.BOLD, 15));  g.drawString("" + processDay[i] + "天", 70+(startDay[i] ­ 1) * 40 +  drawWidth / 2, 95+i * 40);  g.setFont(new Font("SansSerif", Font.PLAIN, 12)); 完成了上述第  3  点的绘制工作。注意,我们这里设定每一天在横坐标上的间隔是  40  像 素。 程序最后部分将缓冲图像输出到客户端,并结束此程序。本  Servlet  最后绘制效果如图  5.26 所示。 到目前为止,我们所介绍的绘制 Web 动态图表例程,都是在单一的 Servlet 中完成。如 何利用多个不同的  Servlet  来完成一幅图表的绘制工作呢?目前我们绘制的坐标轴都是没有 箭头的,如何调用本章 5.2 节所介绍的绘制三角形的 Serlvet 来帮助完善坐标轴的绘制呢?如 何利用绘制平行四边形的方法来绘制 3D 图表呢?
    • 下一节我们将介绍如何让多个 Servlet 协同工作,来绘制比较复杂的 3D 甘特图。  5.9  Servlet 绘制 3D 甘特图 本节将主要介绍,如何让多个 Servlet 协同工作,来完成绘制比较复杂的 3D 图表。我们 以上一节所介绍的绘制甘特图的 Servlet 为蓝本,重新改写这个 Servlet,使其能够绘制 3D 甘 特图。 本例的运行效果,如图 5.29 所示。 图 5.29  生成 3D 甘特图的 Servlet  为完成本例所介绍的  3D 甘特图的绘制工作,我们需要引出本章  5.2  节所介绍的绘制三 角形的  Serlvet(WEB­INFcomfatcatwebchartTriangleServlet.java),另外,我们再编写一个 类,用于绘制各种风格 3D 矩形(chap05Fig5.9DrawParallelogram.java) 。这两个类,将被本 例所讲述的绘制 3D 甘特图 Ganter3DServlet.java 所调用。图 5.29 中横轴最右边的箭头就是调 用 TriangleServlet.java,并由该 Servlet 绘制。图 5.29 中,各种颜色实心矩形的绘制工作是调 用 DrawParallelogram.java 类完成的。 打开下载源码根目录下 chap05Fig5.9 子目录中的 web.xml 文档, 复制该文档中第 9 行~ 第 18 行的内容,然后,将复制的内容粘贴到WEB­INFweb.xml 文档中的<web­app>  </web­  app>标记中。 <servlet>  <servlet­name> Ganter3DServlet</servlet­name>  <description>绘制甘特图的 Servlet</description>  <servlet­class>com.fatcat.webchart.Ganter3DServlet</servlet­ class>  </servlet>  <servlet­mapping>  <servlet­name>Ganter3DServlet</servlet­name>  <url­pattern>/ganter3D</url­pattern>  </servlet­mapping> 调用此 Servlet 的 Ganter3D.html 的源程序也很简单, 只需要了解程序第 12 行调用 Servlet  生 成  3D  甘 特 图 的 方 法 就 可 以 了 。 然 后 将 本 节 所 讨 论 的 两 个  Servlet  源 程 序 ——  DrawParallelogram.java 以及 Ganter3DServlet.java 拷贝到目录WEB­INFcomfatcat  webchart  下。因调用绘制箭头的  TriangleServlet.java  已经保存在该目录中,因此,我们在  Ganter3D  Servlet.java 程序中就可以直接调用 TriangleServlet.java。
    • 有关 TriangleServlet.java 的详细介绍,请看本章第二节的内容。 相信读者在阅读  DrawParallelogram.java  代码的时候,会有一种很熟悉的感觉。不错,  DrawParallelogram.java 的源程序是以本书第 2 章第 2.10.5 节所讨论的绘制平行四边形及立方 体 Applet(位于chap02Fig2.10Fig2.10_05 DrawParallelogramApplet.java)为蓝本。本例程使 用的绘制原理及方法和 DrawParallelogramApplet.java 完全一致。因此,本节不再对如何绘制平行 四边形及立方体做出阐述,请读者参考本书第 2 章第 2.10.5 节的相关内容。 本例程中,我们新增加了一个 setFillColor 的方法,用来设置立方体三个表面的填充颜色。 该方法见 DrawParallelogram.java 的源程序第 70 行~第 75 行: public void setFillColor(Color pTopFillColor)  {  setPTopFillColor(pTopFillColor);  setFrontFillColor(pTopFillColor.darker());  setPRightFillColor(pTopFillColor.darker().darker());  }  setFillColor 的方法,接收一个 Color 类的实例作为参数,该段代码实现的功能如下: Ø 调用程序第 58 行~第 61 行 setPTopFillColor 方法,将参数指定的颜色对象 pTopFill  Color 设置为填充立方体顶部平行四边形的填充颜色。 Ø 调用程序第 52 行~第 55 行 setFrontFillColor 方法, 用指定的颜色对象 pTopFill Color, 更深一些的颜色对象(调用 Color 类的 darker 方法) ,设置为填充立方体正面平行四 边形的填充颜色。 Ø 调用程序第 64 行~第 67 行 setPRightFillColor 方法,再连续两次调用 Color 类的 darker  方法,将获得的 Color 对象,设置为填充立方体右侧面平行四边形的填充颜色。 程序的其他部分与 DrawParallelogramApplet.java 完全相同。 现在讲解调用这两个类的 Ganter3DServlet.java 源程序,我们要调用的类 Draw  Parallelo  gram.java,以及 TriangleServlet.java 都位于本包(WEB­INFcomfatcatwebchart)中,所以, 不需要 import 语句将其引入本程序中。 程序第  1  行~第  69 行代码,就是绘制标题、背景色、图表区域边框等。绘制的结果如 图 5.30①所示。 程序第 70 行~第 80 行: g.setFont(new Font("SansSerif", Font.PLAIN, 12));  // 绘制横坐标上的说明文字,垂直直线和斜线 for (int i = 0; i <= 400; i += 40)  {  g.setColor(Color.black);  g.drawString("2­" + (1+i / 40), (i + 70), 440);  g.setColor(Color.lightGray);  g.drawLine((i + 91), 65, (i + 91), 410);  g.drawLine((i + 81), 420, (i + 91), 410);  } 这段代码首先绘制横坐标上表示时间的字段, “2­1”~“2­11” ,表示 2 月 1 日到 2 月 11  日中的每一天。然后,程序第 78 行,绘制 11 条垂直方向的浅灰色直线。程序第 79 行,绘制 如图 5.30②红圈部分所示的斜线。
    • 图 5.30    3D 甘特图的绘制过程 1  程序第 83 行~第 85 行,绘制坐标轴。同样地,用两个黑色的实心矩形来代表坐标轴: g.setColor(Color.black);  g.fillRect(77, 60, 3, 363);  g.fillRect(80, 420, 420, 3); 绘制效果如图 5.31①所示。 然后,在横轴的最右边绘制了一个方向向右的三角形。绘制该三角形的工作是通过调用  TriangleServlet.java 类来完成的,见源程序代码第 88 行~第 91 行: TriangleServlet ts = new TriangleServlet();  ts.setBaseLine(10);  ts.setAlpha(60);  ts.drawTrigangle(500, 421, 2, 2, g); 程序第 88 行,调用 TriangleServlet 类的默认构造器方法,创建了一个 TriangleServlet 类 的对象  ts。注意,在  TriangleServlet.java  源程序中,我们并没有提供任何构造器方法。既然 没有提供该构造器方法, 为什么可以调用这个构造器的方法呢?这里就需要了解一下 Java 类 的继承以及编译机制了。如果一个类没有使用“extends”关键字指明其父类,则默认继承其 “Object”类(隶属于 java.lang 包) 。无论是否在该类中提供了默认的构造器。Java 编译器在 编译该类的时候,都将自动调用其父类的默认构造器。因此,虽然  TriangleServlet.java  源程 序中并没有提供任何构造器的方法,这里调用其默认的构造器的方法,实际上,就是调用了  java.lang.Object 的默认构造器的方法,所以程序不会出错。 程序第 89 行,  TriangleServlet 类的对象 ts 调用其 setBaseLine 方法 (见 Triangle Servlet.java  源程序第 99 行~第 102 行) ,设置三角形底边的长度为 10 像素。 程序第 90 行,  调用 setAlpha 方法 ts (见 TriangleServlet.java 源程序第 104 行~第 107 行) , 设置三角形底角的角度为 60 度。 程序第 91 行,ts 调用 drawTrigangle 方法(见 TriangleServlet.java 源程序第 30 行~第 97  行) ,绘制三角形。这里为 drawTrigangle 方法提供了 5 个参数。第 1、2 个整形参数表示该三 角形底边中点的坐标,第 3 个参数“2” ,表示三角形的方向向右(见 TriangleServlet.java 源 程序第 22 行) 。第 4 个参数“2” ,表示三角形绘制风格为实心三角形(见 TriangleServlet.java  源程序第 25 行) 。第 5 个参数“g” ,表示把当前绘图环境对象传递给 drawTrigangle 方法,并 在当前的绘图环境中进行图形绘制工作。 执行完程序第 91 行后,绘制结果如图 5.31②所示。 程序第 94 行,调用 DrawParallelogram 类的默认构造器方法,创建了一个 Draw Parallelo  gram 类的对象 dp。同样,在 DrawParallelogram.java 中,也没有提供默认构造器的方法,这
    • 里实际上是调用 java.lang.Object 的默认构造器。 程序第 96 行: int drawWidth = 0; 初始化 3D 矩形(立方体)的绘制长度为 0。 程序第 97 行~第 128 行,通过一个循环,计算所有 3D 矩形的绘制参数,并根据这些参 数绘制 3D 矩形。程序第 100 行~第 106 行: g.setColor(Color.LIGHT_GRAY);  g.drawLine(90, 90+i * 40, 490, 90+i * 40);  if (i == processDay.length ­ 1)  {  g.drawLine(90, 90+(i + 1) * 40, 490, 90+(i + 1) * 40);  }  g.drawLine(80, 100+i * 40, 90, 90+i * 40); 图 5.31    3D 甘特图的绘制过程 2  绘制水平直线、连接水平直线和纵轴之间的斜线。绘制结果如图 5.32 所示。 程序第 109 行~第 111 行: g.setColor(Color.black);  g.setFont(new Font("SansSerif", Font.PLAIN, 12));  g.drawString("计划" + (i + 1), 45, 123+i * 40); 这段代码绘制了纵坐标的说明字段。绘制结果如图 5.32 所示。 图 5.32    3D 甘特图的绘制过程 3  程序第 114 行:
    • drawWidth = processDay[i] * 40; 计算 3D 矩形的绘制长度。3D 矩形表示完成子计划所需要的天数,3D 矩形的长度越长, 说明完成该计划所需要的时间也就越多。同样,在坐标系中,每 40 像素表示一天,所以,这 里用 processDay 数组中的元素再乘以 40 就得到了 3D 矩形的实际绘制长度。 程序第 115 行~第 121 行,调用 DrawParallelogram 类的对象 dp 相关方法,绘制 3D 矩 形。 程序第 115 行: dp.setFillColor(color[i]); 调用前面介绍的 setFillColor 方法,设置 3D 矩形顶面、正面和右侧面的填充颜色。 程序第 116 行: dp.setBasePoint(80+(startDay[i] ­ 1) * 40, 125+i * 40); 调用 setBasePoint 方法,设置 3D 矩形基准点(左下方顶点)的坐标。 程序第 117 行: dp.setWidth(drawWidth); 调用 setWidth 方法,设置 3D 矩形的绘制宽度。这个宽度就是程序第 114 行,计算出来 的 3D 矩形的绘制长度。 程序第 118 行: dp.setHeight(20); 调用 setHeight 方法,设置 3D 矩形的绘制高度。这里设定 3D 矩形的绘制高度为 20 像素。 程序第 119 行: dp.setThickness(15); 调用 setThickness 方法,设置 3D 矩形的绘制厚度。这里设定 3D 矩形的绘制厚度为 15  像素。 程序第 120 行: dp.setAngle(45); 调用 setAngle 方法,设置 3D 矩形的顶部平行四边形的斜边与其水平边的夹角度数。这 里设置该夹角度数为 45 度。 程序第 121 行: dp.drawParallelogram(2, g); 调用 drawParallelogram 方法(见 DrawParallelogram.java 源程序第 152 行~第 200 行) , 绘 制  3D  实心矩 形。第 1个参 数“2 ” ,表 示 该  3D  矩 形的绘 制风格 为实心 矩形( 见  ;第 2 个参数“g” DrawParallelogram.java 源程序第 37 行) ,将当前缓冲图像的绘图环境对象  g 传递给 drawParallelogram 方法,drawParallelogram 方法,将在 g 上绘制 3D 实心矩形。 程序第 124 行~第 127 行: g.setColor(Color.WHITE);  g.setFont(new Font("黑体", Font.BOLD, 15));  g.drawString("" + processDay[i] + "天", 70+(startDay[i] ­ 1) * 40  + drawWidth / 2, 120+i * 40); 在 3D 实心矩形的正面矩形上绘制说明文字,用于说明完成本计划所需要的天数。 最后,程序第 131 行~第 133 行,将缓冲图像输出到客户端,并结束本程序。本 Servlet
    • 最后的绘制效果如图 5.29 所示。 至此,我们阐述了 Ganter3DServlet.java 与其他 Servlet 协同工作来绘制 3D 甘特图表的方 法。可以将复杂的图表分解成一些基本的几何图像。然后,分别编写一些绘制基本几何图像 的类,封装其中的数据对象和具体的计算以及绘制方法,通过调用这些类提供的相关的公共 方法,就可以完成复杂的 Web 图表绘制工作。  5.10  本章小结 本章详细介绍了  Java  Servlet  安装和部署的相关知识。通过几个实例的讲解,了解了  Servlet 生成动态图表的机制及其运行过程。多个 Servlet 可以协同工作完成复杂的 Web 图表 绘制工作。Servlet 异常强大而灵活的功能,非常适合于 Web 应用程序服务器端的编程。它可 以完成复杂的业务逻辑。但 Servlet 在带来强大功能的同时,随之而来的是其固有的弊端,那 就是 Servlet 的安装、编译和部署是非常复杂的。 有鉴于此,  公司在 1999 年的下半年, Sun 推出了基于 Servlet 技术的 JSP  (Java Server Pages) 技术。它在保留 Servlet 高性能的同时,以一种更方便、更快捷的方式开发 Web 应用程序。 下章,我们将开始 JSP 的学习之旅。 第 6 章  JSP Web 图表编程基础 本章将介绍如何利用 JSP 技术生成动态的 Web 图表。JSP 是 Java  Server  Pages(Java 服 务器页面)的缩写。它是由 Sun 公司开发并加以推广,建构在 Servlet 技术之上的 Web 服务 器端编程技术。 为什么 Sun 公司会推出基于 Servlet 技术的 JSP 技术?通过上一章的学习,读者已经非常 清楚 Servlet 在服务器端的强大编程能力。但是 Servlet 在带来强大功能的同时,随之而来的 是其固有的弊端,那就是 Servlet 的安装、编译和部署是非常麻烦的。要成功开发并部署一个  Servlet,需要的步骤如下: (1)编写正确无误的 Servlet 源代码。 (2)更新WEB­INFweb.xml  文档,在  web.xml  文档中指明  JSP/Servlet  容器要加载的  Servlet 的别名、Servlet 完整的路径名及类名、URL 映射、Servlet 描述等。 (3)将 Servlet 源代码放置在相对应的WEB­INFclasses 路径下。 (4)对于某些 JSP/Servlet 容器,还必须重新启动一次才能加载新部署的 Servlet。 如果在此过程中出现错误,将无法调用该 Servlet。尽管 Servlet 具有强大的性能,而且是 一种 Web 应用程序服务器端的首选平台。但 Servlet  的配置和部署,一直是很多开发者望而 却步的一个难关,影响了 Servlet 在全球的推广使用。 除此之外,Servlet 的编写也是一件烦琐的工作。从上一章介绍的例程可以看出,Servlet
    • 有一个最大的缺点:尽管  Servlet  是百分之百的  Java  程序,但为了将最终的输出结果以  HTML/XML 的格式返回给 HTTP 客户端,在 Java 的代码中,我们不得不嵌入大量的 HTML  代码。必须通过一句一句地调用 PrintWriter 类对象 out 的 println 方法,来输出每一句 HTML  代码。下面以第 5 章介绍的 WelcomeServlet.java(chap05Fig5.2 Fig.5.2_01)为例来说明,其 源程序清单如下: 1: // Fig. 5.02_01: WelcomeServlet.java  2: // 简单处理客户端"get"请求的 servlet  3: 4: package com.fatcat.webchart;  5:  6: import javax.servlet.*;  7: import javax.servlet.http.*;  8: import java.io.*;  9:  10: public class WelcomeServlet extends HttpServlet {  11:  12:    // 处理客户端的 "get" 请求 13:  protected void doGet( HttpServletRequest request,  14:       HttpServletResponse response )  15:  throws ServletException, IOException  16:    {  17:       response.setContentType( "text/html;charset=gb2312" );  18:       PrintWriter out = response.getWriter();  19:  20:       // 发送 XHTML 格式的页面给客户端 21:  22:       // 开始生成 XHTML 文档 23:       out.println( "<?xml version = "1.0"?>" );  24:  25:       out.println( "<!DOCTYPE html PUBLIC "­//W3C//DTD " +  26:          "XHTML 1.0 Strict//EN" "http://www.w3.org" +  27:          "/TR/xhtml1/DTD/xhtml1­strict.dtd">" );  28:  29:       out.println( "<html xmlns = "http://www.w3.org/1999/ xhtml  ">" );  30:  31:       // 生成文档的 head 部分 32:       out.println( "<head>" );  33:      out.println( "<title>简单的处理客户端"get"请求的 servlet</  title>" );  34:       out.println( "</head>" );  35:  36:       // 生成文档的 body 部分 37:       out.println( "<body>" );  38:       out.println( "<h1>您好,<br>欢迎进入 Servlet 图表编程的世界</h1>"  );  39:       out.println( "</body>" );  40:  41:       // 结束 XHTML 文档 42:  out.println( "</html>" );  43:       out.close();  // 关闭输出流 44:    }  45: } 在上面的例程中, 使用了大量的 out.println 语句来输出 HTML 内容。实际上,这些 HTML  代码并不是我们需要处理的对象。在 Servlet 中,通过简单方式便可实现的 HTML 代码,开 发者却要通过 out.println 来生成每一行的 HTML 内容。HTML 内容不得不通过代码来实现, 对于一个包含有大量 HTML 代码的 Servlet 来说, 实在是一项繁重而费时的工作, 而且在 servlet  实际应用中也是一个严重问题。 为此,人们开始寻求一种更好的解决方式。在 Sun 正式发布 JSP(Java Server Pages)之
    • 后,这种新的 Web 应用开发技术很快引起了人们的关注。  JSP 为创建高性能的动态 Web 应用, 提供了一个独特的开发环境。  6.1    JSP 概述  JSP 技术是 J2EE 体系结构中的一个重要组成技术。它为开发人员提供了一个 Server 端 框架,基于这个框架,开发人员可以综合使用 HTML、XML、Java 语言,以及其他脚本语言, 灵活、快速地创建动态网页。  JSP 技术基于 Servlet 技术,但它在更高一级的层次上抽象 Servlet。可以让常规、静态的  HTML 与动态产生的内容相结合,看起来像一个 HTML 网页,但却作为 Servlet 来运行。使 用 JSP 要比 Servlet 更简单,是一段标准的 Java 代码。可以放置在任何静态 HTML 文件可以 放置的位置,不用编译,不用打包,也不用进行 ClassPath 的设置,如同访问普通网页那样访 问。  服务器将客户端访问的 JSP 文件, JSP 自动转换成相应的 Servlet, 并由服务器负责该 Servlet  的配置和部署工作。  JSP 技术源自于 Servlet。理论上,凡是 Servlet 可以完成的工作,JSP 同样可以完成,并 且最终以 Servlet 的形式在 Web 服务器端运行。因此,它又继承了 Servlet 的所有优点,例如 高性能、分布式等。 在 JSP 中,编写静态 HTML 更加方便, 不必使用 println 语句来输出每一行 HTML 代码。 更重要的是,借助内容和外观的分离,在页面制作中不同性质的任务可以方便地分开。比如, 由页面设计专家进行 HTML 设计,同时保留出供 Servlet 程序员插入动态内容的空间。 按照 Sun 的说法,JSP 将 Servlet 中的 HTML 代码脱离出来,从而加速了 Web 应用开 发和页面维护。 “JSP 技术应该被视为标准, 而 Servlets 在多数情况下可视为一种补充……” (详见 Sun 发布的“应用开发模型”文档 Section 1.9,1999/12/15 听取意见版) 。 提示:  Servlet 是在 Java 源程序中插入 HTML 代码, JSP 是在 HTML 源程序中插入 Java  而 代码。  6.1.1    JSP 运行机制  JSP 的运行机制,也就是 JSP 如何实现客户端和服务器端交互的基本流程,如图 6.1 所 示。 (1)用户在 HTTP 客户端(浏览器)发出的请求信息,被存储在 Request 对象中并发传 送给 JSP/Servlet 服务器,如图 6.1①所示。 (2)  JSP/Servlet 服务器根据请求信息中指定的 JSP 文件来处理该 Request 对象,如图 6.1  ②所示。 (3)JSP 文件处理该 Request 对象时,一般有以下两种处理途径:  A.在当前的 JSP 文件中处理该请求。 Ø 当前的 JSP 生成响应该请求的 Response 响应对象,将生成 Response 响应对象返回给  JSP/Servlet 服务器,如图 6.1⑤所示。 Ø  JSP/Servlet 服务器在接收到请求指定的 JSP 文件,返回结果后,将接收到的 Response  响应对象生成标准的 HTML/XML 格式内容,再返回给 HTTP 客户端,如图 6.1⑥所 示。
    • 客户端  response 响应 ⑥  ① request 请求  ⑤ JSP/Servlet  response 响应 服务器  JSP  ④ ②  request 请求  文件  response 响应 其他 Web  ③  组件 forward 转发 图 6.1    JSP 与客户端和服务器端交互的基本流程  B.根据实际需要,将 Request 对象转发给其他的服务器端组件处理。 Ø 将客户端请求转发给当前  JSP  文件所指定的其他服务器端组件(如  Servlet  组件、  JavaBean 组件或 EJB 组件等)加以处理,如图 6.1③所示。 Ø 接收到 JSP 页面转发请求的 Request 对象后,服务器端组件生成 Response 响应对象, 并返回给调用它的 JSP 页面,如图 6.1④所示。 Ø  JSP 页面接收到服务器端组件生成 Response 响应对象,并将其返回给 JSP/Servlet 服 务器,如图 6.1⑤所示。 Ø JSP/Servlet 服务器在接收到请求指定的 JSP 文件,返回结果后将接收到的 Response 响 应对象生成标准的 HTML/XML 格式内容,再返回给 HTTP 客户端,如图 6.1⑥所示。  JSP 文件如同一个普通静态 HTML 文件,只不过里面包含了一些 Java 代码。它使用.jsp  的后缀,用来通知服务器该文件需要特殊的处理。当我们访问一个 JSP 页面的时候,该文件 首先会被 JSP 引擎翻译为一个 Java 源文件, 其实就是一个 Servlet 并进行编译, 如同其他 Servlet  一样,由 Servlet 引擎来处理。Servlet 引擎装载这个类,处理来自客户的请求,并把结果返回 给客户。在技术实现细节上,JSP 的实现借助了 Servlet 技术,系统在首次载入 JSP 时,自动 将其编译成内部的 Servlet,JSP 对 Request 对象和 Response 对象(以及其他隐含对象)的处 理,最终都是由其对应的 Servlet 来完成的。 随后的客户在访问 JSP 页面时,只要该文件没有更改,JSP 引擎就直接调用已经编译并 加载的 Servlet。如果已经修改,就会再次执行以上的过程,翻译、编译并加载。这就是所谓 的“第一人惩罚”。
    • 6.1.2    JSP 的优点  JSP 技术是从 Servlet 技术发展起来的。在 Java 服务器端编程中普遍采用的就是 JSP,而 不是 Servlet。因为 JSP 在编写表示页面时远比 Servlet 简单,并且不需要手工编译(由 Servlet  容器自动编译) 目前 Servlet 主要用于视图控制器、 , 处理后台应用等。 由于 JSP 构建在 Servlet  上,所以它具有 Servlet 所有的强大功能。 在开发 JSP 规范的过程中,Sun 公司与许多主要的 Web 服务器、应用服务器和开发工具 供应商积极合作,不断完善技术。现在有很多支持 JSP/Servlet 的服务器,除了我们介绍过的  Sun 公司的 Java  Web  Services  Developer  Pack、轻量级的 Apache 软件基金组织的 Tomcat、  Caucho 公司的 Resin 和企业级 BEA 公司的 Weblogic,还有 Macromedia 公司的 JRun 4、开放 源代码组织的 JBoss、 IBM 公司的 WebSphere、 Borland 公司的 Borland Enterprise Server 5.0.2、  SilverStream 的 Application Server 以及 Fujitsu 公司的 Interstage。 在传统网页 HTML 文件(*.htm,*.html)中,加入 Java 程序片段(Scriptlet)和 JSP 标记, 就构成了 JSP 网页(*.jsp) 。  JSP 基于强大的 Java 语言,具有良好的伸缩性,与 Java  Enterprise  API 集成在一起,在 网络应用开发领域中具有得天独厚的优势。基于 Java 平台构建网络程序,已经被越来越多的 人们认为是最有发展前途的技术。 从 JSP 近几年的发展来看,已经获得了巨大成功。它通过和 EJB 等 J2EE 组件进行集成, 可以编写处理高负载的企业级应用,并且加速了动态 Web 页面的开发。 下面我们来总结一下 JSP 的优点: Ø 将内容的生成和显示进行分离 使用 JSP 技术,Web 页面开发人员可以使用 HTML 或者 XML 标识来设计和格式化最终 页面。使用 JSP 标识或者小脚本来生成页面上的动态内容。生成内容的逻辑被封装在标识和  JavaBeans 的组件中,并且捆绑在小脚本中,所有的脚本在服务器端运行。如果核心逻辑被封 装在标识和 Bean 之中, 那么其他人, 如 Web 管理人员和页面设计者, 都能够编辑和使用 JSP  页面,而不会影响内容的生成。 在服务器端,  JSP 引擎解释标识和小脚本, 生成所请求的内容 (例如, 通过访问 JavaBeans  组件,使用 JDBC 技术访问数据库) ,并且将结果以 HTML 或者 XML 页面形式发送回浏览 器。这样有助于开发者保护自己代码的同时,又保证了任何基于 HTML 的 Web 浏览器的完 全可用性。 Ø 生成可重用的组件 绝大多数 JSP 页面依赖于可重用、跨平台的组件(JavaBeans 或者 Enterprise  JavaBeans  组件)来执行应用程序所要求的更为复杂的处理。开发人员能够共享和交换执行普通操作的 组件或使这些组件为更多的使用者、客户团体所使用。基于组件的方法加速了总体开发,并 使得各种组织在他们现有技能和优化结果上的开发得到平衡。 Ø 采用标识简化页面开发  Web 页面开发人员不一定都是熟悉脚本语言的编程人员。Java  Server  Pages 技术封装了 许多功能,这些功能是易用的、与 JSP 相关的 XML 标识中进行动态内容生成时所需要的。 标准的 JSP 标识能够访问和实例化 JavaBeans 组件,设置或者检索组件属性,下载 Applet 以 及执行其他方法。 通过开发定制标识库,JSP  技术非常容易扩展。第三方开发人员和其他人员可以为常用 功能创建自己的标识库。 这使得 Web 页面开发人员能够使用熟悉的工具和如同标识一样执行 特定功能的构件来工作。
    • Ø  JSP 能提供所有的 Servlets 功能 与 Servlets 相比,JSP 能提供所有的 Servlets 功能,它比使用 Prineln 书写和个性 HTML  更方便。此外,可以更加明确地进行分工。Web 页面设计人员编写 HTML,只需要留出空间 让 Servlets 程序员插入动态部分即可。 Ø 健壮的存储管理和安全性 由于 JSP 页面的内置脚本语言是基于 Java 的编程语言, 并且所有的 JSP 页面都被编译成  Java Servlet。因此,JSP 页面就具有 Java 技术的所有优点,包括健壮的存储管理和安全性。 Ø 一次编写,随处运行 作为 Java 平台的一部分,JSP 拥有 Java 编程语言“一次编写,随处运行”的特点。 Ø  JSP 平台适应性非常广泛 几乎所有的平台都支持 Java。JSP+JavaBeans 的形式令它们可以在任何平台下通行无阻。 从一个平台移植到另一个平台,JSP 和 JavaBeans 甚至不用重新编译,因为 Java 字节码都是 标准的字节码,与平台无关。 其他的优点,如 Java 连接数据库的技术——JDBC(Java  Database  Connectivity)提供了 一种与几乎所有数据库高效、简洁的操作方式,包括数据库连接池、事务处理等。因为这些 内容超出了本书的讨论范围,有兴趣的读者请自行查阅相关资料。 我们将提供更多的实例及详细解释,介绍如何运用 JSP 技术来绘制 Web 动态图表。 在进行 JSP 编程之前,我们还必须对 JSP 的相关知识做一个简要的了解。  6.2    JSP 语法简介  6.2.1    JSP 文件结构 我们在第 4 章 JSP/Servlet 运行环境的搭建中,向读者提供了几个 JSP 源程序,用来测试  JSP/Servlet 运行环境是否搭建成功。但我们并没有对 JSP 文件结构做出解释和分析。现在, 我们以 JSPStructure.jsp 文件(chap06Fig6.1Fig.6.1_01)为例,讨论 JSP 文件的结构。  JSPStructure.jsp 的源程序清单如下: 1: <!­­  2: Fig. 6.01_01: JSPStructure.jsp  3: 功能: JSP 文件结构 4: ­­>  5: <%@ page language="java"  contentType="text/html;charset=GB2312"%>  6: <HTML>  7: <HEAD>  8: <TITLE> JSP 文件结构 </TITLE>  9: </HEAD>  10:  11: <BODY>  12: <%  13: int a = 55, b = 45;  14: %>  15: 整数: 55 + 45 的结果是: <%= a + b%>  16: <BR><BR>  17: 整数: 55 ­ 45 的结果是: <%= a ­ b%>  18: </BODY>
    • 19: </HTML> 从上面的代码清单中可以看到, JSP 页面除了比普通 HTML 页面多一些 Java 代码外, 两 者具有基本相同的结构。Java 代码是通过“<%”和“%>”符号加入到 HTML 代码中间的, 其主要功能就是执行“<%”和“%>”之间的 Java 代码。 打开浏览器,并在地址栏输入:http://localhost:8080/chap06/Fig6.1/Fig.6.1_01/  JSP  Structure.jsp。如果 JSP 源程序没有错误,则会看到如图 6.2 所示的结果。 图 6.2  JSPStructure.jsp 的运行结果 单击 IE 工具栏【查看】→【源文件】 ,可以看到如图 6.3 所示的结果。
    • 图 6.3  JSPStructure.jsp 返回的 HTML 源代码 不难看出,JSPStructure.jsp  的源程序中“<%”和“%>”之间的  Java  代码块,已经被  JSP/Servlet 服务器替换成相应的运行结果。 现在我们将源程序第 13 行中的“;”去掉,变为: int a = 55, b = 45  //遗漏了分号 源程序就会出现错误,如果采用 Tomcat 作为 JSP/Servlet 服务器,就会看到如图 6.4 所示 的错误页面。 图 6.4    Tomcat 服务器下的错误提示 如果采用 Resin 作为 JSP/Servlet 服务器,则会看到如图 6.5 所示的错误页面。 提示:Tomcat 或 Resin 会因不同的版本而显示出不同的错误提示信息。旧版本的 Tomcat  不会直接给出 JSP 源文件出现错误的地方,而是给出该 JSP 源文件被转换成 Servlet,即 java  源程序后,代码出现错误的地方。
    • 图 6.5    Resin 服务器下的错误提示 我们使用的当前版本的 Tomcat 和 Resin 服务器提供的错误信息非常简洁, 直接给出源程 序错误的类型,以及出错代码在 JSPStructure.jsp 源程序的具体位置。注意,Tomcat 和 Resin  指出的错误位置有轻微的差别。如果采用旧版本的 Tomcat 服务器,如 5.0.28 版,则该版本的  Tomcat  就不能正确地指出该出错代码在 JSPStructure.jsp 源程序中的具体位置。Tomcat  指明 出错的位置在第 54 行。这里的第 54 行,并不是指该代码在 JSPStructure.jsp 源程序的位置, 而是指该代码在 JSPStructure.jsp 编译成 Servlet 后,在该 Servlet 的源程序中位置。从 Tomcat  提供的出错信息中可以知道 JSPStructure.jsp 被编译成 Servlet 后,该 Servlet 的源程序名字为  JSPStructure_jsp.java,且保存位置在: C:TomcatworkCatalinalocalhost_orgapachejspchap06Fig6_1Fig_  6_1_005f01JSPStructure_jsp.java 这里的 C:Tomcat 是本书中 Tomcat 的安装目录。我们来看看 JSPStructure_jsp.java 的源程 序清单如下: 1: package org.apache.jsp.chap06.Fig6_1.Fig_6_1_005f01;  2:  3: import javax.servlet.*;  4: import javax.servlet.http.*;  5: import javax.servlet.jsp.*;  6:  7: public final class JSPStructure_jsp extends org.apache.jasper.  runtime.HttpJspBase  8:  implements org.apache.jasper.runtime.JspSourceDependent {  9:  10:  private static java.util.Vector _jspx_dependants;  11:  12:  public java.util.List getDependants() {  13:  return _jspx_dependants;  14:  }  15:  16:  public void _jspService(HttpServletRequest request, HttpServlet­  Response response)  17:  throws java.io.IOException, ServletException {  18:  19:  JspFactory _jspxFactory = null;  20:  PageContext pageContext = null;  21:  HttpSession session = null;  22:  ServletContext application = null;  23:  ServletConfig config = null;  24:  JspWriter out = null;  25:  Object page = this;  26:  JspWriter _jspx_out = null;  27:  PageContext _jspx_page_context = null;  28:  29:
    • 30:  try {  31:  _jspxFactory = JspFactory.getDefaultFactory();  32:  response.setContentType("text/html;charset=GB2312");  33:  pageContext = _jspxFactory.getPageContext(this, request,  response,  34:  null, true, 8192, true);  35:  _jspx_page_context = pageContext;  36:  application = pageContext.getServletContext();  37:  config = pageContext.getServletConfig();  38:  session = pageContext.getSession();  39:  out = pageContext.getOut();  40:  _jspx_out = out;  41:  42:  out.write("<!­­rn");  43:  out.write("tFig. 6.01_01: JSPStructure.jsprn");  44:  out.write("t 功能: JSP 文件结构rn");  45:  out.write("­­>rn");  46:  out.write("rn");  47:  out.write("<HTML>rn");  48:  out.write("<HEAD>rn");  49:  out.write("<TITLE> JSP 文件结构 </TITLE>rn");  50:  out.write("</HEAD>rn");  51:  out.write("rn");  52:  out.write("<BODY>rn");  53:  54:  int a = 55, b = 45  55:  56:  out.write("rn");  57:  out.write("整数: 55 + 45 的结果是: ");  58:  out.print( a + b);  59:  out.write("rn");  60:  out.write("<BR><BR>rn");  61:  out.write("整数: 55 ­ 45 的结果是: ");  62:  out.print( a ­ b);  63:  out.write("rn");  64:  out.write("</BODY>rn");  65:  out.write("</HTML>rn");  66:  } catch (Throwable t) {  67:  if (!(t instanceof SkipPageException)){  68:  out = _jspx_out;  69:  if (out != null && out.getBufferSize() != 0)  70:  out.clearBuffer();  71:  if (_jspx_page_context != null) _jspx_page_context.Handle­  PageException(t);  72:  }  73:  } finally {  74:  if (_jspxFactory != null) _jspxFactory.releasePageContext  (_jspx_page_context);  75:  }  76:  }  77: } 程序第 54 行: int a = 55, b = 45 正是代码发生错误的地方。因此,在程序的开发、调试已经正式运行阶段,采用最新版 本的 Tomcat 或 Resin 服务器,实在是一个明智的选择。 下面分析 JSP 文档的结构。JSP 文档都以“jsp”为扩展名,文档正文又可分为几个部分。 下面以 JSPStructure.jsp 的文档为例,加以说明。 首先是 JSP 指令。它描述页面的基本信息。如所使用的语言、是否维持会话状态、是否 使用缓冲等。JSP 指令由“<%@”开始,到“%>”结束,见程序第 5 行: <%@ page language="java"  contentType="text/html;charset=GB2312" %>
    • 指令中 page language="java",简单地定义了脚本使用语言为 Java 语言。在目前的 JSP 规 范中,language 属性默认值为“java” 。ContentType 属性确定返回客户端响应数据内容的类型 以及字符的编码方式。 “text/html;charset=GB2312”说明返回给客户端的数据类型为 text/html, 它通知客户端浏览器响应一个 XHTML 文档。该 HTML 文档,字符集编码方式为 GB2312。 于是,客户端浏览器就知道它必须读取文档中的 XHTML 标记,并根据标记以及相应的字符 集,对文档进行格式化处理,然后将文档显示在浏览器中。 位于“<%”和“%>”之间的代码块,是描述 JSP 文档处理逻辑的 Java 代码,它被称之 为 Scriptlets。如程序第 12 行~第 14 行所示: <%  int a = 55, b = 45;  %> 最后,位于“<%=”和“%>”之间的代码称为 JSP 表达式,如程序第 15 行的“<%= a +  b%>”以及程序第 17 行的“<%= a-b%>”所示。JSP 表达式提供了一种将 JSP 生成的数值嵌 入 HTML 页面的简单方法。表达式将所得结果转换成字符串形式,然后连接在 HTML 代码 中。 此外,还有一种位于“<%! ”和“%>”之间的代码,称为 JSP 声明。它主要用于在 JSP  页 面 中 声 明 变 量 、 方 法 及 类 。 如 本 书 第  4  章 所 介 绍 的  test.jsp  文 档 ( chap04Fig4.1  Fig.4.1_01test.jsp)中第 8 行~第 21 行所示: 8: <%!  9: SimpleDateFormat shortSDF = new SimpleDateFormat("yyyy­MM­dd");  10: SimpleDateFormat fullSDF = new SimpleDateFormat("yyyy­MM­dd  HH:mm:ss");  11:  12: String gotShortDate(Date date)  13: {  14:  return shortSDF.format(date);  15:  }  16:  17: String gotFullDate(Date date)  18: {  19:  return fullSDF.format(date);  20: }  21: %> 定义了变量、方法及类后,就可以在 JSP 文档的任意地方调用。如 test.jsp 文档中第 35  行、第 37 行所示: 35: (yyyy­MM­dd)格式的转换结果为:&nbsp;<%=gotShortDate(now)%>  36: <BR><BR>  37: (yyyy­MM­dd HH:mm:ss)格式的转换结果为:&nbsp;<%=gotFullDate(now)%> 提示:JSP 代码和 Java 代码都是大小写敏感的。如上述程序第 35 行的调用方法如果写 成:GotShortDate(now),服务器就会给出错误提示。同样,类名、包名、路径名及一些 JSP  标签都不能写错。  6.2.2    JSP 文件中的元素简介  JSP  页面中的元素一般分为以下五类:注释、模板元素、脚本元素、指令元素和动作元 素。下面分别介绍每一类元素。  1.注释 程序 comment.jsp(chap06Fig6.2Fig.6.2_01)演示了在 JSP 中注释的语法。其源程序清
    • 单如下: 1: <!­­  2: Fig. 6.02_01: comments.jsp  3: 功能: JSP 注释语法示例 4: ­­>  5: <%@ page language="java"  contentType="text/html;charset= GB2312" %>  6: <HTML>  7: <HEAD>  8: <TITLE> JSP 注释语法示例 </TITLE>  9: </HEAD>  10:  11: <BODY>  12: <%­­ 本注释将不会在客户端出现 ­­%>  13:  14: <!­­ 本注释的生成时间: <%=(new java.util.Date()).toLocaleString() %>  ­­>  15: <h1>  16: <%  17: // 获得系统的当前时间 18: int time = (new java.util.Date()).getHours();  19:  20: /*  21: 根据时间来输出不同的问候语.  22: 12 点前,输出: 上午好;  23: 12 点到 18 点,输出: 下午好;  24: 18 点后,输出: 晚上好.  25:  */  26:  27: if (time <= 12)  28:  out.print("上午好!");  29: else if (time >= 13 && time <= 18)  30:  out.print("下午好!");  31: else  32:  out.print("晚上好!");  33: %>  34: </h1>  35: </BODY>  36: </HTML> 运行该程序后,在 IE 的【查看源文件】中,我们可以看到如图 6.6 所示的返回结果。 图 6.6  客户端显示的 comment.jsp 的返回结果  JSP 文档中的注释共分三种: Ø HTML/XML 风格的注释
    • 该注释返回给客户端(也就是它可以在【查看源文件】中看到) ,它又分为两种写法:  1)语法 1: <!­­ comment ­­> 如程序 comment.jsp 中第 1 行~第 4 行: <!­­  Fig. 6.02_01: comments.jsp 功能: JSP 注释语法示例 ­­> 如图 6.6①所示,我们可以看到这段注释已经返回给了客户端。  2)语法 2: <!­­ comment [ <%= expression %> ] ­­> 我们可以在注释中加上表达式,如程序 comment.jsp 中第 14 行: <!­­ 本注释生成的时间<%=new java.util.Date()).toLocaleString() %> ­­> 如图 6.6②所示,我们可以看到这段注释已经返回给了客户端。 Ø 隐藏注释 语法: <%­­ comment ­­%> 如程序 comment.jsp 中第 12 行: <%­­ 本注释将不会在客户端出现 ­­%>  JSP  文档在被编译的时候,如果文档中出现隐藏注释的标记,则编译器忽略该标记中所 有的文本。隐藏注释标记中的所有文本也不会显示在客户端的浏览器中。 Ø  Java 注释风格 前面说过,JSP 中的 Java 代码段是标准的 Java 程序。因此,Java 的注释方法也可以在  JSP 中使用,它也分为两种写法:  1)单行注释,语法: //  comment 如例程 comment.jsp 中第 17 行: // 获得系统的当前时间  ,语法: 2)多行注释,在多行语句的首尾处加上以下“/*”和“*/” /*  Any comments  */ 如例程 comment.jsp 中第 20 行~第 25 行: /* 根据时间来输出不同的问候语 12 点前,输出: 上午好;  12 点到 18 点,输出: 下午好;  18 点后,输出: 晚上好.  */
    • 当然,对于 Java 风格的注释不会返回给客户端。  2.模板元素 模板元素是指 JSP 文档中的静态内容,也就是 HTML/XML 代码。如上例中第 6 行~第  11 行、第 15 行、第 34 行~第 36 行,这些代码都遵循 HTML 或者 XML 语法。 模板元素的功能有些类似于网页的框架, 决定了网页结构和最终的外观。模板元素在 JSP  编译的时候,也被编译到 Servlet 里。当客户端请求这个 JSP 文档时,JSP/Servlet 服务器将这 些模板元素原封不动地发送到客户端。 以 JSPStructure.jsp 的源文件为例,程序第 6 行~第 9 行: <HTML>  <HEAD>  <TITLE> JSP 文件结构 </TITLE>  </HEAD> 是标准的 HTML 代码,也就是模板元素。当 JSPStructure.jsp 被编译成 Servlet 时,上面 的代码会在 Servlet 中,被以下代码所替代(JSPStructure_jsp.java 第 47 行~第 50 行) : out.write("<HTML>rn");  out.write("<HEAD>rn");  out.write("<TITLE> JSP 文件结构 </TITLE>rn");  out.write("</HEAD>rn");  3.脚本元素 脚本元素包括前面介绍的声明(Declaration) 、表达式(Expression)和 Scriptlets。 Ø 声明(Declaration) 在 JSP 程序中声明合法的类、方法及变量。  JSP 语法: <%! declaration; [ declaration; ]+ ... %> 可以一次性声明多个变量和方法,以";"结尾。这些声明要在  Java  中是合法的。如本书 第 4 章中所介绍的 test.jsp 文档(chap04Fig4.1 Fig.4.1_01test.jsp)中第 8 行~第 21 行所示。 另外,也可以写成以下形式: <%! int i = 0; %>  <%! int a, b, c; %>  <%! TriangleServlet ts = new TriangleServlet(); %> 直接使用在<% @ page %>中,被包含进来的已经声明的变量和方法, 不需要对它们进行 重新声明。一个声明仅在一个页面中有效。如果每个页面都需要用到一些声明,最好把它们 写成一个单独的文件,然后使用<%@ include %>或<jsp:include >元素包含进来。 Ø 表达式(Expression) 表示一个符合 JSP 语法的表达式。  JSP 语法: <% = expression %>  JSPStructure.jsp 中第 15 行以及第 17 行所示: <%= a + b%>  <%= a ­ b%> 表达式元素是表示一个在脚本语言中被定义的表达式。在运行后被自动转化为字符串,
    • 然后插入到该表达式在 JSP 文件的位置显示。因为表达式的值已经被转化为字符串,所以可 以在一行文本中插入该表达式。 读者在 JSP 中使用表达式时,请注意以下几点: 不能用一个分号(";")来作为表达式的结束符。但同样的表达式用在  Scriptlet  中就需 要以分号来结尾。一个表达式能够变得很复杂,可以由一个或多个表达式组成,这些表达式 的顺序是从左到右。 Ø  Scriptlets  包含一个有效的 Java 程序段。  JSP 语法: <% code fragment %>  JSPStructure.jsp 中第 12 行~第 14 行所示: <%  int a = 55, b = 45;  %> 以及程序 comment.jsp(chap06Fig6.2Fig.6.2_01)第 27 行~第 32 行: <% if (time <= 12)  out.print("上午好!");  else if (time >= 13 && time <= 18)  out.print("下午好!");  else  out.print("晚上好!");  %> 一个 Scriptlet 能够包含多个 JSP 语句、方法、变量以及表达式。因此,在 Scriptlet 中, 可以完成以下工作: Ø 声明将要用到的变量或方法。 Ø 编写 JSP 表达式。 Ø 使用任何隐含的对象和任何用<jsp:useBean>声明过的对象。 Ø 编写 Java 代码。 任何文本、HTML 标记、JSP 元素都必须在 Scriptlet 之外。当 JSP 收到客户的请求时,  Scriptlet 就会被执行。如果 Scriptlet 有显示的内容,这些显示内容就被存在 out 对象中。  4.指令元素  JSP 指令是 JSP 向 JSP 容器发送的消息。用来设置全局值。如类声明、要实现的方法、 输出内容类型等。不向客户端产生任何输出,只影响当前的 JSP 文件。  JSP 语法: <%@ page  [ language="java" ]  [ extends="package.class" ]  [ import="{package.class | package.*}, ..." ]  [ session="true | false" ]  [ buffer="none | 8kb | sizekb" ]  [ autoFlush="true | false" ]  [ isThreadSafe="true | false" ]  [ info="text" ]  [ errorPage="relativeURL" ]  [ contentType="mimeType [ ;charset=characterSet ]" | "text/html ;  charset=ISO­8859­1" ]  [ isErrorPage="true | false" ]  %>
    • 如:comment.jsp 中第 5 行所示: <%@ page language="java"  contentType="text/html;charset=GB2312"%> 此外,还可以写成: <%@ page import="java.util.*, java.lang.*" %>  <%@ page buffer="5kb" autoFlush="false" %>  <%@ page errorPage="error.jsp" %>  <%@ page %>指令作用于当前的整个 JSP 页面,同样包括静态的包含文件。但是<% @  page %>指令不能作用于动态的包含文件,比如<jsp:include>  。 我们可以在一个页面中用多个<% @ page %>指令, 但是其中的属性只能使用一次。 不过 有个例外,那就是 import 属性。因为 import 属性和 Java 中的 import 语句差不多(参照 Java  Language),可以在一个 JSP 文档中多次使用 import 属性。就像在一个 Java 文档中,可以用  import 关键字引入多个包是一样的道理。 无论把<% @ page %>指令放在 JSP 文件的任何地方, 其作用范围都是整个 JSP 页面。 不 过,为了 JSP 程序的可读性,以及培养良好的编程习惯,我们建议读者把它放在 JSP 文件的 顶部。 下面简要介绍上述某些指令元素的功能: Ø <% @  page import =”java.io.*,java.util.*”%>  // 导入包 Ø <% @  buffer=”  ”%>  // 定义对客户输出流的缓冲模型 Ø <% @  info=”  ”%>  // 可以使用 servlet.getServletInfo()得到该字符 Ø <% @  isErrorPage=”  ”%>  // 当前页面是否处理异常或错误 Ø <% @  errorPage=”  ”%>  // 如果发生错误或异常,应该调用哪一个 JSP 页面来处理 Ø <% @  isThreadSafe=” ” %>  // JSP 文件是否能多线程使用 除此之外,指令元素中还有一个重要的 Taglib 指令。  Taglib 指令用来定义一个标签库及其自定义标签的前缀。  JSP 语法: <%@ taglib uri="URIToTagLibrary" prefix="tagPrefix" %> 例如: <%@taglib uri='/WEB­INF/cewolf.tld' prefix='cewolf' %>  <% @ taglib %>指令声明此 JSP 文件使用了自定义的标签,同时引用标签库,指定了它 们标签的前缀。 Ø  uri="URIToTagLibrary"  Uniform Resource Identifier (URI)根据标签前缀,对自定义的标签进行惟一的命名,URI  可以是以下内容:  1)Uniform Resource Locator(URL) 。由 RFC 2396 定义。  2)Uniform Resource Name(URN) 。由 RFC 2396 定义。  3)一个相对或绝对的路径。 因此,uri='/WEB­INF/cewolf.tld'  说明该标签库文件位于  Web  应用程序根目录下/WEB  ­INF/,标签库文件名为 cewolf.tld。 Ø  prefix="tagPrefix"  自定义标签之前的前缀。请不要使用 jsp、jspx、java、javax、servlet、sun 和 sunw 作为 前缀。prefix='cewolf '  说明该标签前缀为 cewolf。 我们必须在使用自定义标签之前使用<% @ taglib %>指令,而且可以在一个页面中多次 使用,但是前缀只能使用一次。
    • 提示:关于标签库的使用,我们将在第 8 章开放源代码作品与 Web 图表编程中向读者介 绍。
    • 5.动作元素  JSP 动作元素是由 XML 语法构成的。它是在请求的处理阶段起作用,影响 JSP 运行的 行为和发送给客户的应答。JSP 规范定义了一系列的标准动作,用 jsp 作为前缀。这些标准动 作可以由任意一个符合 JSP/Servlet 规范的容器所提供,而不管容器是如何实现它们的。  JSP 动作元素的语法是参照 XML 语法的,所以有两种书写方式: <prefix:tag attribute=value attribute­list … /> 以及: <prefix:tag attribute=value attribute­list …>  …  </prefix:tag> 从效果上来说,一个标准动作是能够嵌入到 JSP 页面之中的一个标记。在页面被编译为  当 Servlet 期间, JSP/Servlet 容器遇到该标记时,就用相应于请求的预定义任务的 Java 代码来 代替它。这里我们讨论几个最重要的 JSP 标准动作。 Ø  <jsp:param>  为其他标签提供附加信息。  JSP 语法: <jsp:param name=”paramName” value=”paramValue”/> 附加信息是以“参数名—参数值”成对的方式提供给其他标签的。它与<jsp:include>、  <jsp:forward>以及<jsp:plugin>一起使用。 Ø  <jsp:include>  <jsp:include>元素允许包含动态文件和静态文件,这两种包含文件的结果是不同的。如 果文件是静态的, 那么这种包含只是把包含文件的内容加到 jsp 文件中去, 该文件不会被 JSP  编译器执行;而如果该文件是动态的,那么这个被包含的文件也会被 JSP 编译器执行。 理论上<%  @  include=""  %>与<jsp:include>有所不同。我们把<jsp:include>叫做自动刷 新,<jsp:include>包含的内容是动态改变的,它在被执行时才确定。而<% @ include=""  %>  与包含的内容是固定不变的,一旦经过编译就保持不变。在使用较高版本的 Tomcat 时,这些 功能是一样的。  JSP 语法: <jsp:include page="{relativeURL | <%= expression%>}"   flush="true" /> 或者: <jsp:include page="{relativeURL | <%= expression %>}"   flush="true" >  <jsp:param  name="parameterName"  value="{parameterValue | <%= expression %>}" />+  </jsp:include> 下面简要介绍上述某些指令元素的功能: Ø  page="{relativeURL | <%= expression %>}"  参数为一相对路径,或者代表相对路径的表达式。 Ø  flush="true"  这里必须使用 flush="true",不能使用 false 值。默认值为 false。 Ø  <jsp:param    name="parameterName"  value="{parameterValue  |  <%=  expression  %>}"  />+  注意,这里有个“+”号,这是正则表达式的语法,表示该语句在“<jsp:include>” 、
    • “</jsp:include>”标记中可以出现 1 次或多次,也就是至少要出现 1 次。<jsp:param>可以传 递一个或多个参数给动态文件。name 指定参数名,value 指定参数值。 根据上述语法,以下用法都是合法的: <jsp:include page="drawLine.jsp" />  <jsp:include page="/info/readme.html" />  <jsp:include page="/index.html" />  <jsp:include page="pie.jsp">  <jsp:param name="Python" value="45" />  <jsp:param name="JAVA" value="115" />  <jsp:param name="C#" value="70" />  <jsp:param name="Perl" value="35" />  <jsp:param name="PHP" value="65" />  </jsp:include> 实际上,我们并不能从文件名上判断一个文件是动态的还是静态的,比如 drawData. jsp。 它可能是绘制某些图表的 JSP 页面,也可能只是包含一些绘制图表所需的数据或信息而已, 而并不需要执行。<jsp:include 能够同时处理这两种文件,不需要判断此文件是动态的还是静 态的。 Ø  <jsp:forward>  允许将请求转发到另一个 JSP,Servlet 或者静态资源文件。根据不同的请求,转发到不 同的程序去处理。  JSP 语法: <jsp:forward page={"relativeURL" | "<%= expression %>"} /> 或者: <jsp:forward page={"relativeURL" | "<%= expression %>"} >  <jsp:param name="parameterName"  value="{parameterValue | <%= expression %>}" />+  </jsp:forward> 下面简要介绍上述某些指令元素的功能: Ø  page="{relativeURL | <%= expression %>}"  <jsp:forward>标签从一个 JSP 文件向另一个文件, 传递一个包含用户请求的 request 对象。 而<jsp:forward>标签以下的代码,将不能执行。 Ø  <jsp:param name="parameterName" value="{parameterValue | <%= expression %>}" />+  注意,这里有个“+”号,这也是正则表达式的语法,表示该语句在“<jsp:forward>” 、 “</jsp:forward>”标记中可以出现 1 次或多次,也就是至少要出现 1 次。如果传递多个参数, 则可以在一个 JSP 文件中使用多个<jsp:param>。name 指定参数名,value 指定参数值。 根据上述语法,以下用法都是合法的: <jsp:forward page="drawLine.jsp" />  <jsp:forward page="pie.jsp">  <jsp:param name="Python" value="45" />  <jsp:param name="JAVA" value="115" />  <jsp:param name="C#" value="70" />  <jsp:param name="Perl" value="35" />  <jsp:param name="PHP" value="65" />  </jsp:forward> 注意,请求被转到资源文件必须位于同  JSP  发送请求相同的上下文环境之中。每当  JSP/Servlet 服务器碰到该请求时,就会停止执行当前的 JSP 文档,转而执行被转发的资源文 件。 如果使用了非缓冲输出, 在使用<jsp:forward>时就要小心。如果在使用<jsp:forward>之前,
    • jsp 文件已经有了数据,那么文件执行就会出错。 Ø  <jsp:useBean>  用来实例化 JavaBean,或者定位一个已经存在的  Bean 实例,并且把它赋给一个变量名 (或者 id) ,并给定一个具体的范围来确定对象的生命周期。  JSP 语法: <jsp:useBean  id="beanInstanceName"  scope="page | request | session | application"  {  class="package.class" |  type="typeName" |  beanName="{package.class | <%= expression %>}" |  class="package.class" type="typeName"  }  /> 或者: <jsp:useBean  id="beanInstanceName"  scope="page | request | session | application"  {  class="package.class" |  type="typeName" |  beanName="{beanName | <%= expression %>}" |  class="package.class" type="typeName"  }>  other elements  </jsp:useBean> 下面简要介绍上述某些指令元素的功能: Ø  id="beanInstanceName"  id 是一个大小写敏感的名字, 表示某个 bean 的实例。 如果要使用一个已经创建好的 Bean  对象,那么这个 ID 的值必须与原来定义的 ID 值一致。 Ø  scope="page | request | session | application"  scope 决定 Bean 对象存在的有效范围。Scope 有 4 个可选值,默认值是 page。简单说明 如下: (1)page——表示 Bean 对象与到该页面的特定请求相关联。 ( 2) Request —— 表 示对象 与到该 页面的 特定客户 请求相 联系。 如果请求 被使 用  <jsp:forward>标准动作发送到其他 JSP,或者使用<jsp:include>动作包含了另外的 JSP,则在 所涉及的 JSP 中,该对象是有效的。 (3)Session——当前会话中,由同一个客户发送的任何请求中,该对象都是有效的。 (4)Application——在同一个 Web 应用程序中,任何的 JSP 页面,该对象都是有效的。 Ø  class="package.class "  指定 Bean 类路径和类名。该 package.class 的名字也是大小写敏感的。 Ø  beanName="beanName"  type="typeName"  实例化一个 Bean 对象,同时指定该 Bean 对象的类型。 Ø  type="typeName"  指定 Bean 对象的类型。Type 可以是一个类本身(class) ,也可以是该类的父类,或者是 该类的一个接口。 根据上述语法,以下用法都是合法的: <jsp:useBean id="dP" scope="session" class="com.fatcat.webchart. Draw  Parallelogram" />
    • <jsp:setProperty name="dP" property="width" value="25"/>  <jsp:setProperty name="dP" property="height" value="125"/>  <jsp:setProperty name="dP" property="thickness" value="15"/>  <jsp:setProperty name="dP" property="alpha" value="45"/>  </jsp:useBean>  Ø  <jsp: setProperty>与<jsp: getProperty>  <jsp:setProperty>和  useBean  一起工作,主要用来设置  Bean  的简单属性和索引属性。  <jsp:setProperty>标签使用 Bean 给定的 setXXXX()方法,在 Bean 中设置一个或多个属性值。  JSP 语法: <jsp:setProperty name="beanName"  property="*" |  property="propertyName" param="parameterName" |  property="propertyName" |  property="propertyName" value="{propertyValue|<%=expression %>}"  /> 或者: <jsp:setProperty name="beanName"  property="*" |  property="propertyName" param="parameterName" |  property="propertyName" |  property="propertyName" value="{propertyValue|<%=expression %>}"  >  </jsp:setProperty > 下面简要介绍上述某些指令元素的功能: Ø  name="beanName"  表示某个已经在<jsp:useBean>中,创建 Bean 的实例名字。 Ø  property="*"  储存用户在 JSP 中输入的所有值,用于匹配 Bean 中的属性。Bean 中的属性名字必须和  Request 对象中参数名一致。property="*"是一种设置 Bean 属性的快捷方式。从客户端传到服 务器上的参数值一般都是字符类型(即 String 类) ,这些字符串为了能够在 Bean 中匹配就必 须转换成其他的类型,而转换过程是由 JSP 内在机制自动实现的。 如果 Request 对象的参数值中有空值,那么对应的 Bean 属性将不会设定任何值。同样, 如果 Bean 中有一个属性没有与之相对应的 Request 参数值,那么该属性也同样不会被设置。 Ø  property="propertyName"  使用 Request 中的一个参数值来指定 Bean 中的一个属性值。在这个语法中,property 指 定 Bean 的属性名,而且 Bean 属性和 Request 参数的名字应该相同,否则需要用另一种语法, 即指定 param。propertyName 名字和 Bean 中的 setXXXX 方法中的 XXXX 要完全相同。也就 是说,Bean 中如果有 setWidth(int width)方法,那么 propertyName 的值就应该是“width” 。 Ø  property="propertyName" value="{propertyValue|<%=expression %>}"  使用指定的值来设定  Bean  的相关属性。这个值可以是字符串,也可以是表达式。如果 该值是一个字符串,它会被自动转换成  Bean  属性的类型。如果它是一个表达式,那么它的 类型就必须和设定的属性值类型一致。 如果参数值为空,那么对应的属性值也不会被设定。此外,不能在一个<jsp:setProperty>  中同时使用 param 和 value。 根据上述语法,以下用法都是合法的: <jsp:useBean id="dP" scope="session" class="com.fatcat.webchart. Draw  Parallelogram" />  <jsp:setProperty name="dP" property="baseXPts" value="80"/>  <jsp:setProperty name="dP" property="baseYPts" value="125"/>
    • <jsp:setProperty name="dP" property="thickness" />  <jsp:setProperty name="dP" property="*">  </jsp:useBean> 注意: 如果使用了 property="*", 那么 Bean 的属性就没有必要按 HTML 表单中的顺序排 序。  <jsp:getProperty>和<jsp:setProperty>正好相反,用来获取 Bean 的属性。getProperty 获得  Bean  的属性值都将自动转换为一个字符串对象。如果属性是一个对象,则将调用该对象的  toString 方法。其语法与<jsp:setProperty>相似,我们在此略过。 上述内容对于学习 JSP 文档的编写是非常重要的,我们将在以后的例程中展示其具体的 应用。 Ø <jsp: plugin>  执行一个 Applet 或 Bean,有时还必须下载一个 Java 插件用于执行它。  JSP 语法: <jsp:plugin  type="bean | applet"  code="classFileName"  codebase="classFileDirectoryName"  [ name="instanceName" ]  [ archive="URIToArchive, ..." ]  [ align="bottom | top | middle | left | right" ]  [ height="displayPixels" ]  [ width="displayPixels" ]  [ hspace="leftRightPixels" ]  [ vspace="topBottomPixels" ]  [ jreversion="JREVersionNumber | 1.1" ]  [ nspluginurl="URLToPlugin" ]  [ iepluginurl="URLToPlugin" ] >  [ <jsp:params>  [ <jsp:param name="parameterName"  value="{parameterValue | <%= expression %>}" /> ]+  </jsp:params> ]  [ <jsp:fallback> text message for user </jsp:fallback> ]  </jsp:plugin>  <jsp:plugin>元素用于在浏览器中播放或显示一个对象, 典型的对象就是 Applet 和 Bean, 而这种显示是需要浏览器的 Java 插件。 当  JSP  文档被编译,送往浏览器时,<jsp:plugin>元素会根据浏览器的版本被替换成  <object>或者<embed>元素。注意,<object>用于 HTML4.0 规范,<embed>用于 HTML3.2 规 范。 通常<jsp:plugin>元素会指定对象是 Applet 还是 Bean,也会指定 class 的名字、位置。此 外,还会指定从哪里下载这个 Java 插件。 下面简要介绍上述某些指令元素的功能: Ø  type  ="bean | applet"  将被执行的插件对象类型,必须指定是 Bean 还是 Applet,因为该属性没有默认值。 Ø  code="classFileName"  将被 Java 插件执行的 Java Class 的名字, 必须以.class 结尾。这个文件必须存在于 codebase  属性指定的目录中。 Ø  codebase="classFileDirectoryName"  将被执行的 Java Class 文件的目录 (或者是路径) 如果没有提供此属性, , 那么<jsp:plugin>  的 JSP 文件的当前目录将被使用。 Ø  name="instanceName"
    • 这个 Bean 或 Applet 实例的名字,将在 JSP 其他的地方调用。 Ø  archive="URIToArchive, ..."  一些由逗号分开的路径名,这些路径名用于预加载一些将要使用的  class,这可以提高  applet 的性能。 Ø  align="bottom | top | middle | left | right"  图形、对象、Applet 的位置,有以下的可选值: (1)bottom  (2)top  (3)middle  (4)left  (5)right  Ø  height="displayPixels"  width="displayPixels"  Applet 或 Bean 将要显示长宽的值,此值为数字,单位为像素。 Ø  hspace="leftRightPixels"  vspace="topBottomPixels"  Applet 或 Bean 显示时在屏幕左右、上下所需留下的空间,单位为像素。 Ø  jreversion="JREVersionNumber | 1.1"  Applet 或 Bean 运行所需的 Java Runtime Environment(JRE)的版本,默认值为 1.1。 Ø  nspluginurl="URLToPlugin"  Netscape  Navigator  用户能够使用的  JRE  的下载地址,此值为一个标准的  URL,如:  http://java.sun.com。 Ø  iepluginurl="URLToPlugin"  IE  用户能够使用的  JRE  的下载地址,此值为一个标准的  URL,如  http://java.sun.com/  products/plugin/index.html#download。 Ø  <jsp:params>  [  <jsp:param  name="parameterName"  value="{parameterValue  |  <%=  expression %>}" /> ]+ </jsp:params>  向 Applet 或 Bean 传送的参数或参数值。可以使用表达式。 Ø  <jsp:fallback> text message for user </jsp:fallback>  Java 插件不能启动时显示给用户的一段文字, 如果插件能够启动而 Applet 或 Bean 不能, 那么浏览器会有一个出错信息弹出。 我 们 先 讨 论 一 个 加 载  Applet , 并 传 递 参 数 及 根 据 参 数 绘 制 饼 图 的  JSP  源 程 序  loadApplet.jsp(chap06Fig6.2Fig.6.2_02) 。 源程序第 12 行~第 18 行: <%  int python = 1 + (int)(Math.random()* 50);  int java = 1 + (int)(Math.random()* 50);  int cSharp = 1 + (int)(Math.random()* 50);  int perl = 1 + (int)(Math.random()* 50);  int php = 1 + (int)(Math.random()* 50);  %> 这段 Scriptlets 代码中,定义了 5 个整型变量,在 1~50 之间的随机数中产生。 程序第 19 行: <jsp:plugin type="applet" code="Pie.class" width = "600" height = "500"  > 指定当前加载的类型是 Applet, 类的名字为 Pie.class,该类位于当前目录下,显示 Applet  的宽度为 600 像素,高度为 500 像素。
    • 程序第 20 行~第 43 行: <jsp:params>  <jsp:param name = "titleFontName" value = "经典行书简"/>  <jsp:param name = "titleFontSize" value = "32"/>  <jsp:param name = "chartTitle"  value = "Java Web 图表设计 JSP 版—加载 Applet"/>  <jsp:param name = "bookTitle1" value = "Python"/>  <jsp:param name = "bookSales1" value = "<%=python%>"/>  <jsp:param name = "bookTitle2" value = "JAVA"/>  <jsp:param name = "bookSales2" value = "<%=java%>"/>  <jsp:param name = "bookTitle3" value = "C#"/>  <jsp:param name = "bookSales3" value = "<%=cSharp%>"/>  <jsp:param name = "bookTitle4" value = "Perl"/>  <jsp:param name = "bookSales4" value = "<%=perl%>"/>  <jsp:param name = "bookTitle5" value = "PHP"/>  <jsp:param name = "bookSales5" value = "<%=php%>"/>  <jsp:param name = "subTitle" value = "计算机编程类图书销售统计图"/>  </jsp:params> 向所加载的 Pie.class 传递多个参数。注意,语法必须符合 XML 规范,所有标记必须全 部匹配。 例如,程序第 21 行: <jsp:param name = "titleFontName" value = "经典行书简"/> 也可写成以下的格式: <jsp:param name = "titleFontName" value = "经典行书简" >  </jsp:param> 我们前面讲过,参数的传递有两种方式:一种是直接在 value=“xxx”中指定,另一种是利 用表达式。程序第 28、31、34、37 及第 40 行采用了表达式,利用前面定义的 5 个整型变量 对 value 赋值。 程序第 44 行~第 46 行: <jsp:fallback>  <p><h1>无法加载 Applet</h1></p>  </jsp:fallback> 如果在加载 Applet 的过程中出现错误,则会显示一个出错信息: “无法加载 Applet”。它 是<jsp:plugin>动作的一部分,并且只能在<jsp:plugin>动作中使用。 下面来看看  Pie.java(chap06Fig6.2Fig.6.2_02)的源程序。该程序同本书第  3.3  节所介绍 的 PieNew.java 的结构和用法完全相同,请读者参考该节的相关内容。运行 IE,在地址栏中 输入 http://localhost:8080/chap06/Fig6.2/Fig.6.2_02/loadApplet.jsp,运行结果如图 6.7 所示。 现在再看看执行后的 loadApplet.jsp 返回给客户端的代码: 1: <!­­  2: Fig. 6.02_02_01: loadApplet.jsp  3: 功能: JSP 加载 Applet  4: ­­>  5:  6: <HTML>  7: <HEAD>
    • 8: <TITLE> JSP 加载 Applet </TITLE>  9: </HEAD>  10:  11: <BODY>
    • 图 6.7    JSP 加载 Applet  12:  13:  <OBJECT classid="clsid:8AD9C840­044E­11D1­B3E9­00805F499D93"  width= "600" height="500"codebase="http://java.sun.com/  products/plugin/1.2.2/jinstall­1_2_2­win.cab#Version=1,2,2,0"  >  14: <PARAM name="java_code" value="Pie.class">  15: <PARAM name="type" value="application/x­java­applet;">  16: <PARAM name="titleFontName" value="经典行书简">  17: <PARAM name="titleFontSize" value="32">  18: <PARAM name="chartTitle" value="Java Web 图表设计 JSP 版—加载 Applet">  19: <PARAM name="bookTitle1" value="Python">  20: <PARAM name="bookSales1" value="36">  21: <PARAM name="bookTitle2" value="JAVA">  22: <PARAM name="bookSales2" value="42">  23: <PARAM name="bookTitle3" value="C#">  24: <PARAM name="bookSales3" value="11">  25: <PARAM name="bookTitle4" value="Perl">  26: <PARAM name="bookSales4" value="24">  27: <PARAM name="bookTitle5" value="PHP">  28: <PARAM name="bookSales5" value="27">  29: <PARAM name="subTitle" value="计算机编程类图书销售统计图">  30: <COMMENT>  31: <EMBED type="application/x­java­applet;" width="600" height= "500"  pluginspage="http://java.sun.com/products/plugin/"java_  code="Pie.class" titleFontName="经典行书简" titleFontSize=  "32"chartTitle="Java Web 图表设计 JSP 版—加载 Applet" bookTitle1=  "Python" bookSales1="36" bookTitle2="JAVA" bookSales2="42"  bookTitle3="C#" bookSales3="11" bookTitle4="Perl" bookSales4= "24"  bookTitle5="PHP" bookSales5="27" subTitle="计算机编程类图书销售统计图 "/>  32: <NOEMBED>  33:  34:  <p><h1>无法加载 Applet</h1></p>  35:  36: </NOEMBED>  37: </COMMENT>  38: </OBJECT>  39:  40:
    • 41: </BODY>  42: </HTML> 可以看出,JSP/Servlet 服务器把<jsp:plugin>指令相关的内容,翻译成了浏览器可以识别 的插入代码,通过<jsp:plugin>,使得插入的组件具有更好的动态特征。  6.3    JSP 调用 Servlet 生成动态图表 本节将通过几个例程来演示  JSP  如何调用  Servlet  来生成  Web  动态图表。本节涉及的  Servlet,将采用第 5 章所介绍的 Servlet。  6.3.1    JSP 生成验证码 程序 invokeServlet.jsp(chap06Fig6.3Fig.6.3_01)演示了如何在 JSP 中调用 Servlet 生成  Web 图表,运行结果如图 6.8 所示。 图 6.8    JSP 调用 Servlet  调用方法同我们以前介绍的在  HTML  文档中,调用  Servlet 生成 Web 图表是一样的。  invokeServlet.jsp 里面调用了两个不同的 Servlet,分别 在程序第 14 行: <img src="/codeMaker" /><br /> 以及程序第 19 行: <img src="/imageCodeMaker" /><br /> 我们看到,调用方法实际上是利用 HTML 显示图像的 语法。invokeServlet.jsp 的运行结果如图 6.8 所示。  6.3.2    JSP 生成甘特图 程序 invokeGanterServlet.jsp  (chap06Fig6.3Fig.6.3_ 02) 也是调用了两个  Servlet  来生成甘特图,运行结果如图  6.9  所示。 图 6.9    JSP 调用 Servlet
    • 本例和前例不同的是,在本程序第 14 行、第 19 行。我们将由 Servlet 生成的甘特图默认 的宽度和高度修改为 265 像素和 240 像素。也就是说,在 HTML 中所有<img>图像标记中可 用的属性都可以应用在 Servlet 生成的图像中。  6.3.3    JSP 其他相关知识 这里我们向读者简要介绍一些有关 JSP 的编程要点。  1.JSP 中文乱码问题 在 JSP 的应用开发中,JSP 的中文乱码一直是一个困扰众多开发者的问题。该问题的产 生和解决方法,同本书第 5.3 节所介绍的 Servlet 中文乱码问题的相关知识完全相同。这里就 不再对此问题进行讨论。 对于不同的 JSP/Servlet Web 服务器、同一个 JSP/Servlet Web 服务器的不同版本,以及不 同的 JDK 版本对中文的处理可能不同。在执行相同的 JSP 页面文档时,可能会出现乱码的情 况。解决方法前面已经阐述,此处不再赘述。  2.JSP 内部对象概述  JSP 为简化 Web 应用程序的开发提供了一些内部对象。内部对象包括:  request、  response、  out、session、pageContext、application、config、page、exception 等。这些内部对象不需要由 开发者实例化,它们是由 JSP/Servlet 容器管理来实现的,在所有的 JSP 页面中都能够使用内 部对象。 这些对象同我们前面所讲述的 Servlet 相关对象的作用及用法是一致的。我们简要讨论其 中几个内部对象。 Ø  out 对象  out  对象被封装成  javax.servlet.jsp.JspWriter  接口。它主要用于向客户端发送输出流,也 就是说,由 out 对象向客户端输出数据。  out 是 JSP 中使用最频繁的对象。主要是调用 out 对象的 print 和 println 方法。两者不同 之处在于,println  方法在输出内容后,将在内容的结尾处自动添加一个换行符,即: “n” 。 但换行符“n”在 HTML 语法中是被忽略的。如果要在 HTML 页面上体现换行的效果,就必 须明确使用  out.println(“<br  >”)或者  out.print(“<br>”)来实现。换行符“n”被  HTML  语法所忽略,但在 HTML 的源代码中,我们可以看到,只要是调用了 out.println 方法,下一 条源代码将会出现新的一行被输出。如果调用的是 out.print 方法,即下一条源代码将会紧接 着当前源代码的最后位置被输出。 Ø  request 对象  request  对象代表请求对象,它继承于  HttpServletRequest  接口。来自客户端的请求经  JSP/Servlet 容器处理后,再由 request 对象进行封装。它作为 jspService 方法的一个参数由容 器传递给 JSP 页面。  request 对象的相关方法,读者可以参考我们第 5 章讲述的 Servlet 的相关内容。 Ø  response 对象  response 对象代表响应对象,它继承于 HttpServletResponse 接口。它封装了 JSP 产生的 响应,然后将 response 对象发送到客户端以响应客户的请求。因为输出是缓冲的,所以可以
    • 设置 HTTP 状态码和 response 头信息。  response 对象的相关方法,读者可以参考我们第 5 章讲述的 Servlet 相关内容。 Ø  session 对象  session 对象用在不同页面之间保存共享的信息。一般用于保存每个客户端的信息,以便 跟踪每个用户的操作状态。session 的 ID 保存在客户端的 Cookie 中;session 的信息保存在服 务器中。  session 对象和客户端的会话紧密联系在一起,它由 JSP/Servlet 容器自动创建。  session 对象的相关方法,读者可以参考我们第 5 章所讲述的 Servlet 相关内容。 Ø  application 对象  application 对象为多个 Web 应用程序保存共享信息。对于某一个 JSP/Servlet 容器来说, 每个用户都共同使用一个 application 对象, 也就是对每个客户端而言, 操作的是同一个对象。 而 session 对象,相对于不同的客户端是各自独立的。 application 对象有些类似于 Java 语法中 的 static 类型的数据对象,而 session 有些类似 Java 语法中 private 类型的数据对象。 如果创建了 application 对象,只要服务器没有关闭或者重新启动,该 application 对象就 会一直保持。而 session 对象可以在强制其失效或注销,也可以在超过其默认的失效时间后, 自动注销。这也是两者的主要区别之一。 此外,其他 JSP 的内部对象如:pageContext 被封装成 java.servlet.jsp.PageContext 接口, 它为 JSP 页面包装页面的上下文;config 对象被封装成 javax.servlet.ServletConfig 接口,它表 示 Servlet 的配置、exception 对象是 java.lang.Throwable 类的一个实例,它指的是运行时的异 常,在处理错误页面时使用 exception 对象。  6.4    JSP 生成基本动态图表  JSP 的绘图功能是由 Java 抽象化窗口工具包(AWT, Abstract Windows Toolkit)所支持的。 该包中包含一些子类,如 Font 类,其包含了用于操作字体的方法和一些常量;Color 类,包 含用于操作颜色的方法和常量;Graphics  类,包含了绘制字符串、直线、矩形,以及其他形 状图形的方法。Toolkit 类,提供了从系统中获取可显示字体集等信息的方法。这些类及其一 些方法我们在讲述 Applet 编程的时候已经讨论过了。 在 JSP 程序中,通过引用 Java 抽象化窗口工具包中的 Graphics 类及其方法并结合 Font、  Color 类,就可以轻松地在程序中创建图文并茂、丰富多彩的图表了。 在了解了上述 JSP 的优点、页面结构、运行机制及其相关知识后,就可以用 JSP 来开发 基于 Web 的动态图表应用程序了。同样,任何复杂的图表都可以分解成基本的图表。因此, 掌握 JSP 绘制基本图表是非常重要的。我们在第 2 章以及第 3 章,通过 Applet 向读者介绍了 基本的 Web 图表绘制方法。 本节将把第 2 章使用 Applet 实现的 Web 图表改写成用 JSP 实现, 此外,再扩充一些基于基本图形的应用。  6.4.1    JSP 绘制文本和线段 我们还是从一个简单的 basic.jsp(chap06Fig6.4Fig.6.4_01)图表生成程序开始讲述,在  JSP 中如何引用 AWT 包中的类及方法来生成一些简单的基本图形, 以及如何将生成的图形发 布到 Web 页面上。 类似于我们介绍过的 Servlet,当一个返回给 Web 页面,带有 image/jpeg(或者其他的图
    • 像格式)的 MIME 类型被发送时,客户端浏览器将返回结果当做一个图像,然后浏览器显示 该图像作为页面的一部分或者完全作为图像自身。为设置这个 JSP 页面 MIME 的类型,需要 设置 contentType 属性,如 basic.jsp 源程序第 5 行所示: <%@ page language="java" contentType="image/png;charset=GB2312" 为正确地显示中文,我们制定编码方式为 GB2312。 程序第 6 行~第 8 行: import="java.awt.image.*"  import="java.awt.*"  import="javax.imageio.*" 引入我们熟悉的  Java  图像处理的相关包。请读者注意程序第  8  行,我们仍然没有引进  Sun 的 JPEG 特殊类——com.sun.image.codec.jpeg.*。因为我们认为采用该包编写的程序的通 用性不够好。这些代码在  com.sun  包中,不是  API  核心的一部分,也不是标准的扩展包。  JPEGImageEncoder 是 Sun 用来实现 JRE 某些功能所使用的特殊的 class。因此,如果对图像 的编码采用 JPEGImageEncoder 类,在其他的一些 JRE 上运行会发生异常,影响代码的移植 性。与前一章 Servlet 相同,我们仍然利用 J2SE 5.0 的新特性,来对缓冲图像的内容进行解码 及编码的工作。这样,代码会更简洁,性能更强大,移植性更好。程序第 8 行,引入了 Java  对图像处理的新包。javax.imageio 包默认可以读入一个 GIF、PNG、JPEG 和 BMP 格式的图 像(注意:J2SE1.4  版中也提供了该包,但是不支持对  BMP  格式图像的读写操作) ,以及输 出一个 PNG、JPEG 和 BMP 格式的图像,并不支持输出 GIF 格式的图像。原因是 GIF 格式 的图像是有版权的,所以 Sun 公司在其有关图像处理的包中,只提供了对 GIF 格式图像的读 入功能。 也就是说, 只提供了对 GIF 图像的解码功能, 而没有提供对 GIF 格式图像的输出(即  GIF 图像的编码)功能。本程序将使用该包中的 ImageIO 类,来对缓冲图像按指定的图像格 式进行编码操作。 程序第 13 行: response.reset(); 注意,JSP 的方法体中第 1 条语句,通过 response 调用了 Response 类的 reset 方法,该 方法继承于 ServletResponse。用于清空 response 里的缓冲区的内容。在 JSP 代码中,第 1 条 语句,需要调用 reset 方法,原因和在 Servlet 中介绍的完全相同。 程序第 16 行: response.setContentType("image/png"); 使用  response 对象的  setContenType 方法,设置返回给客户端的响应数据内容的类型为 图像类,图像的格式为  png  格式。我们讨论过,客户端浏览器是根据服务器返回文档类型 (MIME)来处理文档内容的。本例中,返回数据内容的类型为 image/png,它通知浏览器响 应一个格式为  PNG  的图像。于是,客户端浏览器就知道它必须读取该图像,然后将图像显 示在浏览器中。 如果没有程序第 16 行 response 的 reset 方法,而只有程序第 5 行的 setContenType 方法, 即使我们指明了返回的数据类型为  image/png,实际上,JSP/Servlet  服务器默认还是返回的  text/html。只有当执行到程序第 16 行的 setContentType("image/png")时,且 response 的缓 冲区为空时,JSP/Servlet 服务器才会返回 image/jpeg 的图像格式。 但是,无法保证 JSP/Servlet 服务器在执行到 response 的 setContentType("image/png")方 法时,response  缓冲区的内容为空。即使生成的图片内容和格式都没有问题,因为返回的数 据类型和数据的实际内容不一致,在客户端上也不会有正确的显示。
    • 前面介绍过,某些 JSP/Servlet 服务器比较“聪明” ,例如 Tomcat,当遇到以下的语句: <%@ page contentType="image/png" %> 或 response.setContentType("image/png"); 将会自动调用 response 的 reset 方法。这样,即使没有在程序中显示的声明 response 的  reset 方法,还是可以得到正确的输出结果。  Resin 服务器在这个问题上, 不同的版本有着不同的输出结果。  Resin 的某些版本与 Tomcat  类似,也会自动调用 response 的 reset 方法。而 Resin 的某些版本却非常忠于源程序,不会自 动调用 response 的 reset 方法,所有输出结果就不正确。 经我们测试,BEA 公司的 Weblogic 服务器也不会自动调用 response 的 reset 方法,所以 如果缺少了 response 的 reset 方法,就不能得到正确的结果。 综上所述,我们建议读者在编写 JSP/Servlet 的 Web 图表程序时,在源程序中,首先调用  response 的 reset 方法,强制清空 response 缓冲区的内容。这样,可以保证在所有的 JSP/Servlet  服务器上,运行 Web 图表源程序时获得正确的结果。 程序第 19 行~第 23 行: int width=640, height=480;  BufferedImage image =  new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);  Graphics g=image.getGraphics(); 程序第 19 行,设定该图像的宽为 640 像素,高为 480 像素,程序第 21 行~第 22 行, 创建一个 BufferedImage 的实例——image 来绘制动态图像, 然后需要得到图形环境进行绘制。 程序第 16 行,使用 getGraphics()的方法创建了一个 Graphics 对象 g。现在我们就可以绘制图 像内容了。由于 JSP/Servlet 中的缓冲图像的默认背景及绘图颜色为黑色。因此,我们使用一 个与缓冲图像的宽度、 高度都相同的白色矩形作为缓冲图像的背景图案。 见程序第 26 行~第  27 行: g.setColor(Color.WHITE);  g.fillRect(0, 0, width, height); 程序第 30 行,重新设定当前的绘制颜色为红色,然后程序在第 39 行~第 47 行,调用  drawLine 方法,分别绘制了 11 条直线、一个三角形和一个矩形。接着,程序在第 49 行~第  50 行,重新设定字体,并调用 drawString 方法绘制字符串。 程序第 53 行: g.dispose(); 完成图像的绘制工作。调用 Graphics 对象 g  的 dispose 方法来部署(dispose)这个图形 环境。 程序第 56 行~第 58 行: ServletOutputStream sos = response.getOutputStream();  ImageIO.write(image, "PNG", sos);  sos.close(); 使用 response 的 getOutputStream 方法,创建一个 ServletOutputStream 对象 sos,然后利 用 javax.imageio 包中的 ImageIO 类的静态 write 方法来编码图像, 生成一个 PNG 格式的图像。  ImageIO 类有多个不同版本的 write 方法, 都用于对图像进行指定格式的编码或格式转换操作。 本例中调用的 write 方法,需要接收三个参数: Ø 一个实现了 RenderedImage  接口图像的绘图环境对象。该绘图环境可以从一个文件、
    • 输入流或 URL 中获取图像的绘图环境得到。本例中图像的绘图环境对象,就是缓冲 图像 image。 Ø 指定输出图像格式的字符串对象。本例指明图像的输出格式为“PNG”格式。 Ø 一个实现了  ImageOutputStream  接口的输出流对象。本例中实现该接口的对象就是  ServletOutputStream 类的对象 sos。 最后调用 ServletOutputStream 对象 sos 的 close 方法,关闭输出流并结束本 JSP。 启动浏览器, 通过 http://localhost:8080/chap06/Fig6.4/Fig.6.4_01/basic.jsp 来访问本 JSP 文 件,运行结果如图 6.10 所示。 图 6.10    JSP Web 图表的绘制 如果要输出其他格式的图像,以输出“JPG”格式图像为例。需要改动以下几个地方: 程序第 5 行: <%@ page language="java" contentType="image/png;charset=GB2312" 重新设置为: <%@ page language="java" contentType="image/jpeg;charset=GB2312" 程序第 16 行: response.setContentType("image/png"); 重新设置为: response.setContentType("image/jpeg"); 程序第 54 行: ImageIO.write(image, "PNG", sos); 重新修改为: ImageIO.write(image, "JPG", sos); 现在, 不需要重新启动 Tomcat 服务器, 直接在 IE 中刷新就可以看到程序修改后的结果。 如果希望编码成“BMP”格式的图像,修改方法相同,此处不再赘述。 读者可以进行测试,如果以“JPEG”的格式输出,图像质量与“PNG”相比,差距很大, 没有 PNG 格式的图像清晰。此外,我们对保存下来的 basic.jpg 同 basic.png 的文件容量进行 比较: 在本机上,由 basic.jsp 生成的 basic.jpg 的文件容量大小为 14.4K 字节;而 basic.png 只有  3.51K 字节。PNG 格式的显示效果远比 JPG 格式要好,而且生成的图像文件大小也远远低于
    • JPG 格式。 所以,我们也建议读者采用  PNG  格式,作为图表最终的编码格式。基于这个原因,本 书也一直采用 PNG 格式作为我们的编码格式。 下面总结一下 JSP 绘图的基本步骤: (1)设置返回的 MIME 为 image 类型,格式可为 JPEG、BMP、PNG 之一。 (2)调用 Response 类的 reset 方法,强制清空缓冲区。 (3)创建缓冲图像对象并获得其绘图环境。 (4)调用 java.awt 等 Java 图像类库进行绘制。 (5)调用 dispose 方法,部署图像。 (6)将绘制完成的缓冲图像编码,最后以(jpg/jpeg/bmp/png)图像的格式,返回给发 出请求的客户端。 绘制 JSP 图表的代码, 类似前面介绍的 Servlet 绘制图表的 doGet、  doPost 方法中的代码。 使用 JSP 开发 Web 图表程序,其优点也是不言而喻的: Ø 一个 JSP 文件就可以同时响应客户端的 Get 和 Post 请求。 Ø 不用重新启动 JSP/Servlet 服务器, 修改源文件后, 刷新浏览器即可获得修改后的结果。 Ø 不需要修改 web.xml 文档。 在  Tomcat、Resin  服务器中,basic.jsp  程序已经正常工作了。我们再测试一下  basic.jsp  在 Weblogic 中是否运行正常。 如果要在 Weblogic 中运行 basic.jsp 程序,首先需要将其打包,然后部署在 Weblogic 中。 按照我们第 4 章介绍的步骤,为避免混乱,我们首先将 Weblogic_Builder 下所有文件删除掉, 然后将 D:webchartchap06Fig6.4Fig.6.4_01 目录下的 basic.jsp 文件拷贝到 Weblogic_Builder  下。在 Weblogic_Builder 目录下,新建一个子目录 WEB­INF,在 WEB­INF 子目录下,新建 一个名为 web.xml 的文档,内容如下: <?xml version="1.0" encoding="GB2312"?>  <!DOCTYPE web­app PUBLIC  "­//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"  "http://java.sun.com/j2ee/dtds/web­app_2_2.dtd">  <web­app>  <display­name>Basic JSP</display­name>  <description>  Basic JSP  </description>  </web­app> 进入命令行控制台,在 Weblogic_Builder 目录下执行以下命令: jar cvf basic.war *.* 将 Weblogic_Builder 目录下所有的文件打包成 basic.war 文件。步骤如下: (1)登录 Weblogic 的控制台。 (2) 依次单击 【mydomain】 【Deployments】 【Web Application Modules】 【Deploy  → → → a new Web Application Module…】。 (3)单击【Upload Your File(s)】→【浏览】 ,选择打包文件 basic.war,单击【Upload】 按钮,返回先前的页面,然后再单击【myserver】 。 (4)单击【upload】→【basic.war】→【Target Module】 。 (5)在出现的页面中单击【Deploy】选项,完成部署。 现在启动浏览器,在地址栏中输入:
    • http://localhost:7001/basic/basic.jsp 将会看到如图 6.11 所示的结果。 在 Weblogic 中部署其他类似的 JSP 文档时,读者也可以参照此方法。 图 6.11    Weblogic 下 basic.jsp 的运行结果 如果读者还是希望利用 com.sun.image.codec.jpeg 包来对图像进行编码工作,则需要修改 本程序为: Ø 引入 com.sun.image.codec.jpeg 包,如: import="com.sun.image.codec.jpeg.*"  Ø 程序第 57 行,修改为: JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(sos);  encoder.encode(image);  6.4.2    JSP 与字体控制 我们在第 2 章介绍 Applet 的时候,讨论了一些 Java 关于字体控制的一些方法。此前, 我们使用的字体基本都是普通(plain) 、斜体(italic),以及粗体(bold)三种风格。是否只 能使用这几种风格的字体呢?其实,只需要将字体的应用扩展一下,就可以设计出很多丰富 多彩的视觉效果。为达到此目的,我们需要了解关于某种字体的精确信息,如图 6.12 所示。 超出值 高 上升值 基线 下降值 图 6.12  字体规格 字体的高度(height) 、下降值(descent,即字符低于基线的部分) 、上升值(ascent,即 字符高于基线的部分) 、超出值(leading,即字符高度减去下降值和上升值的剩余部分)等。  Java 在绘制文本时,坐标是以字符的基线为标准的。 在 FontMetrics 类中,定义了一些获取字体精确信息的方法。  FontMetrics 类继承于 Object  类,隶属于 java.awt 包。表 6.1 列出了该类,以及 Graphics 类中,一些获取字体精确信息的 方法。 表 6.1    FontMetrics 类及 Graphics 类的相关方法
    • public Font getFont()  // FontMetrics class  返回当前字体对象  public int getAscent()  // FontMetrics class  返回用像素表示字体基线的上升值  public int getDescent()  // FontMetrics class  返回用像素表示字体基线的下降值  public int getLeading()  // FontMetrics class  返回用像素表示字体的超出值  public int getHeight()  // FontMetrics class  返回用像素表示字体的高度  public int stringWidth(String str)  // FontMetrics class  返回一个表示当前字体下,给定的整个字符串长度的整数值  public FontMetrics getFontMetrics()  // Graphics class  返回一个表示当前字体的 FontMetrics 对象  public FontMetrics getFontMetrics(Font font)  // Graphics class  返回一个表示指定字体的 FontMetrics 对象  fontInfo.jsp(chap06Fig6.4Fig.6.4_02)演示了如何使用上述方法,运行结果如图 6.13 所 示。 程序第 30 行~第 32 行: Font[] font = new Font[2];  font[0] = new Font("方正粗宋简体", Font.PLAIN, 20);  font[1] = new Font("华文隶书", Font.BOLD, 18); 定义了一个 Font 数组,并对其进行初始化。 程序第 33 行,声明了一个字符串包含“欢迎来到 Java  Web 图表编程的世界”的一段文 本。程序第 36 行,定义了 5 个整型变量,分别表示字体的高度、下降值、上升值、超出值等, 其中变量 stringLength 表示一个字符串中所有字符的宽度。
    • 图 6.13  获得字符的精确信息 程序第 37 行~第 56 行,通过一个循环,获得当字体不同时,相应字体的高度、下降值、 上升值、超出值以及字符串 title 在应用于不同的字体时,整个字符串宽度的变化情况。 程序第 39 行~第 40 行: g.setFont(font[i]);  ascent = g.getFontMetrics().getAscent();  descent = g.getFontMetrics().getDescent();  fontHeight = g.getFontMetrics().getHeight();  leading = g.getFontMetrics().getLeading();  stringLength = g.getFontMetrics().stringWidth(title); 通过  Graphics  对象  g  的  getFontMetrics  方法,获得一个  FontMetrics  对象,然后调用  FontMetrics 对象的 getAscent、getDescent、getHeight 和 getLeading 方法,获得字符的高度、 下降值、上升值和超出值,这些方法都不需要任何参数。但是 stringWidth 方法,需要一个字 符串参数,返回当前字体下该字符串的宽度。 程序第 47 行~第 55 行,绘制获得的这些信息。 从图 6.13 中我们可以看到,当字体改变时,字符串对象 title 的宽度也发生了变化。第一 次宽度是 312 像素,第二次是 282 像素。 了解如何获得字体精确信息后,现在我们把字体的应用加以扩展,讨论如何设计出丰富 多彩的视觉效果。 首先绘制一段文本,在紧靠该文本再次绘制相同内容的文本。每次绘制时,调用的色彩 是经过精心选择的。 对于绘图区域中任意位置的一个像素来说, 其周围共有 8 个点, 如图 6.14  所示。 西北(Northwest) 北(North) 东北(Northeast) 坐标:X-1,Y-1  坐标:X,Y-1  坐标:X+1,Y-1  西(West) 中央(Center) 东(East) 坐标:X-1,Y  坐标:X,Y  坐标:X+1,Y  西南(Southwest) 南(South) 东南(Southeast) 坐标:X-1,Y+1  坐标:X,Y+1  坐标:X+1,Y+1  图 6.14  点的位置 假设绘图区域中,任意位置的一个像素坐标为 x,y。则其周围 8 个点的坐标分别为: Ø 左上角点(西北方)的坐标:x-1,y-1。
    • Ø 左边点(正西方)的坐标:x-1,y。 Ø 左下角点(西南方)的坐标:x-1,y+1。 Ø 上方点(正北方)的坐标:x,y-1。 Ø 下方点(正南方)的坐标:x,y+1。 Ø 右上方点(东北方)的坐标:x+1,y-1。 Ø 右边点(正东方)的坐标:x+1,y。 Ø 右下角点(东南方)的坐标:x+1,y+1。 下面的例程 fontEffect.jsp(chap06Fig6.4Fig.6.4_02)演示了如何实现字体的多种视觉效 果,运行效果,如图 6.15 所示。 图 6.15  字体效果 从运行结果中可以看出,一共生成了 7 种视觉效果的字体。 首先,fontEffect.jsp 程序第 11 行~第 31 行: <%!  int ShiftNorth(int p, int distance)  {  return (p ­ distance);  }  int ShiftSouth(int p, int distance)  {  return (p + distance);  }  int ShiftEast(int p, int distance)  {  return (p + distance);  }  int ShiftWest(int p, int distance)  {  return (p ­ distance);  }  %> 声明了 4 个方法,这些方法计算出了绘图区域中,任意一个点向其正东方、正西方、正
    • 南方、正北方移动 n 个像素后的位置。本程序中实现的各种视觉效果,即是反复调用这些方 法来实现的。 程序第 48 行~第 50 行: Color bg = new Color(150, 150, 150);  g.setColor(bg);  g.fillRect(0, 0, width, height); 调用 Color 的构造器,创建了一个 Color 类的实例 bg,bg 的 RGB 值同为 150,实际的效 果是浅灰色。 程序第 53 行~第 58 行: int x = 10, y = 100;  g.setFont(new Font("方正粗宋简体", Font.PLAIN, 35));  g.setColor(new Color(50, 50, 50));  g.drawString("欢迎来到 Java Web 图表编程的世界", ShiftEast(x, 2),  ShiftSouth(y, 2));  g.setColor(new Color(220, 220, 220));  g.drawString("欢迎来到 Java Web 图表编程的世界", x, y); 本段代码实现文本的阴影效果,如图 6.15①所示。首先重新设置当前字体对象为“方正 粗宋简体” 、风格为普通字体、大小为 35 像素,然后设置当前颜色(new Color(50,50,50)。 ) 之后,在坐标(12,102)处绘制文本“欢迎来到 Java Web 图表编程的世界” 。这里的坐标 (12,102),是由程序第 56 行中分别调用 ShiftEast 和 ShiftSouth 得到。接着改变当前颜色 为(new Color(220,220,220))所代表的颜色。最后,在坐标(10,100)处,再次绘制相同 内容的文本“欢迎来到 Java Web 图表编程的世界” 。因为两次绘制的文本内容相同,颜色相 异且位置有轻微的错位(第一次绘制文本的位置,距离第二次绘制文本的位置为东南方两个 像素) ,最终两段文本重叠在一起就产生了阴影字体的效果。 程序第 61 行~第 66 行: x = 10;  y = 150;  g.setColor(new Color(220, 220, 220));  g.drawString("欢迎来到 Java Web 图表编程的世界", ShiftEast(x, 1),  ShiftSouth(y, 1));  g.setColor(new Color(50, 50, 50));  g.drawString("欢迎来到 Java Web 图表编程的世界", x, y); 本段代码实现文本的浮雕效果, 如图 6.15②所示。方法与生成阴影效果的文本基本相同, 用不同的颜色,在不同的位置绘制相同内容的文本。 程序第 69 行~第 77 行: x = 10;  y = 200;  g.setColor(Color.RED);  g.drawString("欢迎来到 Java Web 图表编程的世界", ShiftWest(x, 1),  ShiftNorth(y, 1));  g.drawString("欢迎来到 Java Web 图表编程的世界", ShiftWest(x, 1),  ShiftSouth(y, 1));  g.drawString("欢迎来到 Java Web 图表编程的世界", ShiftEast(x, 1),  ShiftNorth(y, 1));  g.drawString("欢迎来到 Java Web 图表编程的世界", ShiftEast(x, 1),  ShiftSouth(y, 1));  g.setColor(Color.YELLOW);  g.drawString("欢迎来到 Java Web 图表编程的世界", x, y); 本段代码实现文本的轮廓效果,如图 6.15③所示。 程序第 72 行~第 75 行,实际执行的是:
    • g.drawString("欢迎来到 Java Web 图表编程的世界", 9, 199);  g.drawString("欢迎来到 Java Web 图表编程的世界", 9, 201);  g.drawString("欢迎来到 Java Web 图表编程的世界", 11, 199);  g.drawString("欢迎来到 Java Web 图表编程的世界", 11, 201); 以点(10,200)为中心,分别在距其西北方、西南方、东北方、东南方 1 像素的位置, 用红色绘制同样内容的一段文本。然后程序用黄色在点(10,200)处,绘制相同内容的一段 文本。这五段文本重叠在一起,就绘制出了文本轮廓的视觉效果。 程序第 80 行~第 88 行: x = 10;  y = 250;  g.setColor(Color.BLACK);  g.drawString("欢迎来到 Java Web 图表编程的世界", ShiftWest(x, 1),  ShiftNorth(y, 1));  g.drawString("欢迎来到 Java Web 图表编程的世界", ShiftWest(x, 1),  ShiftSouth(y, 1));  g.drawString("欢迎来到 Java Web 图表编程的世界", ShiftEast(x, 1),  ShiftNorth(y, 1));  g.drawString("欢迎来到 Java Web 图表编程的世界", ShiftEast(x, 1),  ShiftSouth(y, 1));  g.setColor(bg);  g.drawString("欢迎来到 Java Web 图表编程的世界", x, y); 本段代码实现文本的空心字体效果,如图 6.15④所示。实现原理和生成轮廓效果完全一 样,只是最后一次绘制文本时,将字体的颜色设定为当前背景色(程序第 87 行)。 程序第 91 行~第 102 行: x = 10;  y = 300;  int w = (g.getFontMetrics()).stringWidth("欢迎来到 Java Web 图表编程的世界 ");  int h = (g.getFontMetrics()).getHeight();  int d = (g.getFontMetrics()).getDescent();  g.setColor(new Color(220, 220, 220));  g.drawString("欢迎来到 Java Web 图表编程的世界", x, y);  g.setColor(bg);  for (int i = 0; i < h; i += 3)  {  g.drawLine(x, y + d ­ i, x + w, y + d ­ i);  } 本段代码实现文本的百叶窗效果,类似于 IBM 公司的 logo,如图 6.15⑤所示。首先绘制 一段文本,获得该文本的精确信息,如整个文本的宽度、高度以及下降值等等。从该文本的 底部开始到文本的高度结束,每隔一定的间隔,绘制一条和该文本宽度相同的直线。 程序第 93 行~第 95 行,先调用 g 的 getFontMetrics 方法,获得 FontMetrics 对象后,再 调用 FontMetrics 对象的 stringWidth、getHeight,以及 getDescent 方法来获取字符串“欢迎来 到 Java Web 图表编程的世界”的精确信息,如整个字符串的宽度、高度及下降值等。程序在 第 97 行,绘制了该文本。最后,程序在第 99 行~第 102 行的循环中,每隔 3 像素绘制一条 直线,该直线的宽度被设置成与字符串“欢迎来到 Java Web 图表编程的世界”的宽度相同。 程序第 105 行~第 120 行: x = 10;  y = 350;  Color topColor = new Color(200, 200, 0);  Color sideColor = new Color(100, 100, 0);  for (int i = 0; i < 5; i++)  {  g.setColor(topColor);
    • g.drawString("欢迎来到 Java Web 图表编程的世界", ShiftEast(x, i),  ShiftNorth(ShiftSouth(y, i), 1));  g.setColor(sideColor);  g.drawString("欢迎来到 Java Web 图表编程的世界", ShiftWest(ShiftEast(x, i),  1),ShiftSouth(y, i));  }  g.setColor(Color.YELLOW);  g.drawString("欢迎来到 Java Web 图表编程的世界", ShiftEast(x, 5),  ShiftSouth(y, 5)); 本段代码实现文本的三维立体效果,如图 6.15⑥所示。原理与前面介绍的一样,都是绘 制一系列重叠在一起的文本。需要注意的是,这段代码核心部分在循环体中,实现绘制字体 顶端颜色及其侧面颜色。最后,用黄色绘制相同的文本。 程序第 123 行~第 136 行: x = 300;  y = 400;  int fontSize = 0;  for (int i = 0; i < 20; i++)  {  fontSize = 12+i;  g.setFont(new Font("方正粗宋简体", Font.PLAIN, fontSize));  w = g.getFontMetrics().stringWidth("欢迎来到 Java Web 图表编程的世界");  g.setColor(new Color(0, 65+i * 10, 0));  g.drawString("欢迎来到 Java Web 图表编程的世界", (width ­ w) / 2,  ShiftSouth(y,2 * i));  } 本段代码实现文本的运动字体效果,如图 6.15⑦所示。实现原理是,在一段循环中,每 循环一次,则改变字体的大小、颜色及绘制坐标。然后调用当前字体对象,颜色对象及坐标 绘制文本。注意,在这里设定字符串绘制坐标居中的方法是:(width-w)/2。即使用缓冲图像 的宽度减去整个字符串的宽度,将得到两者的差再除以  2,并以该值作为字符串绘制参数中 的横坐标,这样就实现了字符串居中绘制的功能。 关于基本几何图形的绘制,读者应该非常熟悉了。若没有新的内容,我们将不再对源代 码作出解释,仅罗列出源代码及其运行结果。关于源代码的解释请读者参考本书第二章的相 关内容。  6.4.3    JSP 绘制矩形  1.JSP 绘制普通矩形  (chap06Fig6.4Fig.6.4_03) drawRect.jsp  演示了如何绘制普通矩形,其运行结果如图 6.16  所示。源代码的讲解此处不再赘述。
    • 图 6.16    drawRect.jsp 的运行效果  2.JSP 绘制 3D 矩形  draw3DRect1.jsp(chap06Fig6.4Fig.6.4_03)的源程序演示了如何绘制 3D 矩形,运行结 果如图 6.17 所示。 图 6.17  draw3DRect1.jsp 的运行效果  draw3DRect2.jsp(chap06Fig6.4Fig.6.4_03)的演示了 3D 矩形的另外一种绘制方法,如 图 6.18 所示。源代码的讲解此处不再赘述。
    • 图 6.18  drawRect3D2.jsp 的运行效果  6.4.4    JSP 绘制椭圆  1.JSP 绘制椭圆或正圆  (chap06Fig6.4Fig.6.4_04) drawOval.jsp  演示了如何绘制椭圆或正圆,运行结果如图 6.19  所示。源代码的讲解此处不再赘述。  2.JSP 绘制 3D 椭圆  Draw3DOval.jsp(chap06Fig6.4Fig.6.4_04)的演示了如何绘制 3D 椭圆,运行结果如图  6.20 所示。源代码的讲解此处不再赘述。 图 6.19    drawOval.jsp 的运行效果 图 6.20    draw3DOval.jsp 的运行效果
    • 3.JSP 绘制圆柱体  drawCylinder.jsp(chap06Fig6.4Fig.6.4_04)的演示了如何绘制圆柱体,运行结果如图  6.21 所示。源代码的讲解此处不再赘述。 图 6.21    drawCylinder.jsp 的运行效果  6.4.5    JSP 绘制圆弧  1.JSP 绘制普通圆弧  drawArcs.jsp(chap06Fig6.4Fig.6.4_05)显示了如何绘制普通圆弧,运行结果如图 6.22  所示。 图 6.22    drawArcs.jsp 的运行效果
    • 2.JSP 绘制 3D 圆弧  draw3DArcs.jsp(chap06Fig6.4Fig.6.4_05)显示 了如何绘制 3D 圆弧,运行结果如图 6.23 所示。  6.4.6    JSP 绘制多边形和折线  1.JSP 绘制多边形  drawPolygon.jsp( chap06Fig6.4Fig.6.4_06 ) 演 示了如何绘制多边形,运行结果如图 6.24 所示。  2.JSP 绘制折线  drawPolyline.jsp( chap06Fig6.4Fig.6.4_06 ) 显 图 6.23    draw3DArcs.jsp 的运行效果 示了如何绘制折线,运行效果如图 6.25 所示。 图 6.24    drawPloygon.jsp 的运行效果 图 6.25  drawPolyline.jsp 的运行效果  6.4.7    JSP 绘制三角形 三角形的绘制与 Applet 以及 Servlet 中介绍的绘制方法完全相同。本例在此做了少许修 改,增加了几个数据变量及方法。程序 drawTriangle.jsp 可以接收并处理客户端提交的绘制三 角形的相关参数,如果客户端没有提交相关参数,则调用默认的参数进行绘制。  drawTriangle.jsp(chap06Fig6.4Fig.6.4_07)中关于三角形的绘制过程,请读者参考前面 的相关内容。 程序在第 16 行和第 17 行,新增加了两个成员变量: int basePointX = 80; // 默认的绘制横坐标 int basePointY = 100; // 默认的绘制纵坐标 表示绘制三角形基准点的坐标(即底边中点坐标) 。 程序在第 105 行~第 118 行: public void setAlpha(int alpha)  {  this.alpha = alpha;  }
    • public void setBasePointX(int x)  {  this.basePointX = x;  }  public void setBasePointY(int y)  {  this.basePointY = y;  } 定义了 3 个简单的 set 方法,用于设置三角形的底角度数,以及绘制基准点。 程序在第 141 行~第 179 行: // 获取客户端提交的等腰三角形的基准点参数 String basePointXString = request.getParameter("x");  String basePointYString = request.getParameter("y");  if (basePointXString != null && basePointYString != null)  {  setBasePointX(Integer.parseInt(basePointXString));  setBasePointY(Integer.parseInt(basePointYString));  }  // 获取客户端提交的等腰三角形的底角参数 String alphaString = request.getParameter("alpha");  if (alphaString != null)  {  setAlpha(Integer.parseInt(alphaString));  }  // 获取客户端提交的等腰三角形的底边长度参数 String baseLineString = request.getParameter("baseLine");  if (baseLineString != null)  {  this.baseLine = Integer.parseInt(baseLineString);  }  // 获取客户端提交的等腰三角形的方向参数 String directionString = request.getParameter("direction");  int direction = 1;  if (directionString != null)  {  direction = Integer.parseInt(directionString);  }  // 获取客户端提交的等腰三角形的绘制风格参数 String styleString = request.getParameter("style");  int style = 1;  if (directionString != null)  {  style = Integer.parseInt(styleString);  } 这段代码用来获取客户端提交的参数。如果参数不为空,则调用相应的  set  方法,将客 户端提交的参数,转换成整型对象后赋值给相关的数据变量。这些参数包括:三角形的基准 点坐标、底边的长度、底角的度数、三角形的方向及绘制风格等。 程序第 182 行,调用 drawTrigangle 方法绘制三角形。 现 在 向  IE  的 地 址 栏 中 输 入 :  http://localhost:8080/chap06/Fig6.4/Fig.6.4_07/drawTrian  gle. Jsp, darwTriangle.jsp 的运行结果如图 6.26 所示。 前面讲述过,如果要在一个  HTML 或者 JSP  页 面中, 调用 darwTriangle.jsp 生成的图像, 只需要调用 图 6.26    darwTriangle.jsp 的运行结果
    • 标准的 HTML 语句中,显示图像的标记即可。 例如: <img src="drawTriangle.jsp"> 获得如图 6.26 所示的由 drawTriangle.jsp 生成的默认图像。那么如何才能向其提交参数, 并生成自定义的图像呢?有个简单的解决办法,如下所示: <img src="drawTriangle.jsp?baseLine=60&alpha=45&direction=2&style  =2&x=20&y=60"> 也就是说,直接在  URL  上添加需要提交的参数,包括参数名和参数值。当  HTML/JSP  文档在解析到<img  src=”xxx.jsp?xxxx=xx&xxx=xxx”>这样的标记时,在向服务器请求并加载 图像的同时,这些参数也随请求一并发送给了 JSP/Servlet 服务器。JSP/Servlet 服务器在接收 到这些参数后,按照程序中设定的处理方法进行处理,最后将实时生成的图像再返回给客户 端,达到生成自定义图像的目的。  getTriangle.jsp  (chap06Fig6.4Fig.6.4_07)演示了如何通过上述方法,生成各种自定义的 三角形,源程序清单如下: 1: <!­­  2:  Fig. 6.04_07_02: getTriangle.jsp  3: 功能:获取自定义三角形 4: ­­>  5: <%@ page language="java" contentType="image/png;charset=GB2312"%>  6:  7: <img src="drawTriangle.jsp?baseLine=90&direction=1&style=1&x=100&  y=100">  8: &nbsp;&nbsp;  9: <img src="drawTriangle.jsp?baseLine=60&alpha=45&direction=2&style=  2&x=20&y=60">  10: <BR><BR>  11: <img src="drawTriangle.jsp?baseLine=80&alpha=60&direction=3&  style=3&x=160&y=10">  12: &nbsp;&nbsp;  13: <img src="drawTriangle.jsp?baseLine=100&alpha=60&direction=4&style=  2&x=90&y=70">  getTriangle.jsp 的运行结果如图 6.27 所示。 图 6.27    getTriangle.jsp 的运行结果  6.4.8    JSP 绘制平行四边形和立方体  drawCube.jsp(chap06Fig6.4Fig.6.4_08)的结构及程序运行过程,请读者参考第 2 章相 关内容。DrawCube 的运行结果如图 6.28 所示。
    • 图 6.28    DrawCube.jsp 的运行结果
    • 6.4.9    JSP 加载并显示图像  loadImage.jsp(chap06Fig6.4Fig.6.4_09)演示了如何在 JSP 程序中加载外部图像。运行本 程序之前,请读者先将下载源码 chap06Fig6.4Fig.6.4_09 目录下的 4 个图像文件拷贝到 Web  应用程序根目录下的 images 子目录中(d:webchartimages)。 程序第 38 行~第 45 行: File pngFile =  new File(getServletContext().getRealPath("imageslogo.png"));  File jpgFile =  new File(getServletContext().getRealPath("imageslogo.jpg"));  File gifFile =  new File(getServletContext().getRealPath("imageslogo.gif"));  File bmpFile =  new File(getServletContext().getRealPath("imageslogo.bmp")); 声明了 4 个 File 对象,分别对应“PNG”“JPG”“GIF” 、 、 ,以及“BMP”格式的图像。 注意, 这里我们并没有使用 request.getRealPath 的方法, 来获得文件的完整路径。 Java Servlet  在 2.1 版本以后,Sun 建议不再使用 Request(继承于 javax.servlet 包中的 ServletRequest 接口) 类的 getRealPath,而采用 javax.servlet 包中 ServletContext 接口的 getRealPath 方法。因此,我 们采用 Java 内建对象之一的 config 对象的  getServletContext 方法,获得当前 ServletContext  对象,然后再调用该对象的 getRealPath 方法。getRealPath 方法,需要接收一个字符串参数, 指定文件名。getRealPath 方法,最后返回一个字符串,如果制定的文件存在,则返回该文件 的完整路径。 程序在第 48 行~第 58 行: logoPNG = ImageIO.read(pngFile);  g.drawImage(logoPNG, 10, 50, null);  logoJPG = ImageIO.read(jpgFile);  g.drawImage(logoJPG, 10, 200, null);  logoGIF = ImageIO.read(gifFile);  g.drawImage(logoGIF, 210, 50, null);  logoBMP = ImageIO.read(bmpFile);  g.drawImage(logoBMP, 210, 200, null); 首先, 调用 javax.imageio 包中 ImageIO 类的 read 方法,将创建的 Image 对象赋值给程序 第 36 行,声明的 4 个 Image 对象。然后调用 Graphics 类的 drawImage 方法,将这些 Image  对象的内容显示出来。注意,在 JSP 编程中,drawImage 方法中最后一个参数是“null” ,而 在 Applet 中,drawImage 方法的最后一个参数是“this” 。 使用 Java 的 javax.imageio 包中的 ImageIO 类,来加载及编码图像是非常高效的。所以 我们建议读者采用 ImageIO 类,来处理图像的输入和输出等操作。loadImage.jsp 的运行结果 如图 6.29 所示。
    • 图 6.29    loadImage.jsp 的运行结果  6.5  本章小结 本章主要介绍了 JSP 的优点、结构及运行机制,以及 JSP 绘图的相关知识。简单介绍了  JSP 的语法及其内部对象,并提供了一些简单的实例来说明 JSP 是如何通过读取 HTML/JSP  文档中的数据来绘制统计饼图、如何调用 Servlet 生成不同的验证码、如何加载 Applet,以及 如何绘制甘特图等。 以第 2 章的内容为基础,详尽地阐述了在 JSP 环境下,如何使用 Java.awt.Graphics 类的 各种方法,包括绘制直线、文本(字符串) 、矩形、椭圆和圆、圆弧、多边形和折线。在绘制 基本几何图形的基础上,我们以绘制圆柱体和立方体为例,向读者演示了,如何通过绘制多 个多边形并将其组合成一个复杂几何图形的方法。得益于 Java.imageio 包中的 ImageIO 类的 支持,通过调用 ImageIO 类,来执行加载图像以及对图像进行编码输出等工作,就变得简单, 而且移植性更好了。 到目前为止,我们向读者介绍了基于 Java 对基本几何图形的处理。组合这些基本的几何 图形,我们可以绘制一些普通的、常见的 Web 图表。要绘制复杂的图形,就涉及到我们即将 学习的 JSP 与 Java2D 的相关编程知识了。 第 7 章  JSP 与 Java2D Web 图表编程 本章介绍如何利用 JSP 与 Java2D 技术来生成复杂的 Web 图表。Java2D API 提供了绘制 复杂图形的支持。在  java.awt.geom  包中,提供了很多独特的用以处理圆弧、文字和图像的  API。该包与  java.awt、java.awt.color、java.awt.image、java.awt.font、java.awt.print,以及  java.awt.image.renderable 包中的 API 相结合,就可以实现美观的、复杂的、丰富多彩的 Web  图表了。
    • 7.1    Java2D 概述 顾名思义,Java  2D 就是 Java 对二维图形的支持。Java 的 AWT 中,已经提供了实现简 单的二维图形功能。读者在阅读本书前面部分已经感受到,虽然使用 java.awt 包,可以生成 基本的 Web 图表,但是有一定的局限性,如: Ø 所有线条只能使用单一像素的宽度绘出; Ø 如果要旋转、放大、缩小任何对象,必须自己动手进行数学运算才能达成; Ø 如果要进行渐进色或纹理等特殊着色处理,必须自己动手编写源代码; Ø 只提供最基本的图像显示功能。  Java2D 则提供复杂的、强大灵活的并且独立于图形设备和分辨率的二维图形功能。  Java 2D API 是 JFC  (Java Fundation Classes)的一员,加强了传统 AWT  (Abstract Windowing  Toolkit)图像绘制功能。在 JDK1.2 中,已经支持 Java  2D 的使用。通过 Java  2D  API,我们 可以绘制出任意的几何图形、 运用不同的填色效果、 对图形做平移、 (rotate) 缩放 旋转 、 (scale) 、 扭曲(shear)等。当然,Java 2D API 还有许多增强 AWT 功能的部分,如处理图像文件可以 有不同的滤镜(filter)效果、对于任意的几何图形也能做到碰撞侦测(hit detection) 、图形重 叠复合计算(composite)等功能。本章将对涉及本书内容的 Java2D 相关技术做简要的介绍。  Graphics2D 是 Java2D 的绘图环境。Graphics2D 对象的属性,决定了所绘制图形的所有 信息。绝大部分信息包含在 Graphics2D 对象的 6 个属性内,如下所示: (1)填充属性(paint)  paint 属性,决定图形描绘或填充的颜色,定义填充图形的模式。填充属性是通过调用绘 图环境的 setPaint 方法进行设置。默认的填充属性是当前绘图环境的填充属性。 (2)笔划属性(stroke)  stroke 属性,决定图形描绘所使用的笔划类型,如实线、虚线(注:点划线也属于虚线) 以及线条的粗细。它决定线段端点的形状。笔划属性是通过图形环境中 setStroke 方法进行设 置。默认的笔划属性定义了一个正方形的、粗细为 1 像素的实线,末端为正方形,接口为 45  度斜面。 (3)字体属性(font)  font 属性, 决定绘制文本所使用的字体。 调用图形环境 setFont 方法,即可设置字体属性。 默认字体是当前图形环境所使用的字体。 (4)转换属性(transform)  transform 属性,决定渲染过程中应用的转换方法。通过当前的转换方法,绘制图形可以平 移、旋转、缩放和扭曲。默认的转换方法是恒等转换,即保留原样不进行任何改动。 (5)剪切属性(clip)  clip 属性,定义绘图组件上一个区域的边界。渲染会受到 clip 的限制,只能在 clip 定义 的区域内进行。调用绘图环境 setClip 方法,可以设置剪切属性。默认的剪切属性是整个图形 环境。 (6)复合属性(composite)  composite 属性,决定如何在组件上绘制重叠放置的图形。我们可以修改图形填充颜色的 透明度,使底部被上面图形所覆盖的部分也能显示出来。还可以调用图形环境 setComposite 方 法设置复合属性。默认复合属性可以在任何已有的图形上绘制一个新图形。 所有表示属性的对象都作为引用,存储在 Graphics2D 对象内。因此,我们必须调用相关  set 方法,来设置或修改图形环境中各组件的相关属性。  Graphics2D 渲染图形环境中各个组件时共有 4 种基本的方法: (1)draw(Shape shape)
    • 使用图形环境的当前属性渲染一个图形。这里的渲染,其实与我们学习 Applet 中所说的 描绘图形是相同的意思。两者都是描绘当前图形的外观轮廓。 (2)fill(Shape shape) 使用图形环境的当前属性填充一个图形。这里的填充,其实与我们学习 Applet 中所说的 填充图形是相同的意思。两者都是填充一个实心的当前图形。 (3)drawString(String string) 使用图形环境的当前属性绘制一段文本。 (4)drawImage()  使用图形环境的当前属性渲染(显示)一幅图像。  Graphics2D  类位于  java.awt  包中。因此,要调用  Graphics2D,就必须引入该包。而  Graphics2D  类所渲染的各种  Shape  对象却是位于  java.awt.geom  中。所以必须同时引入  java.awt.geom 包。这里所指的 Shape 对象实现了 Shape 这个接口的任何类的实例。  7.2    Java AWT 与 Java2D  我们先看看调用 Java AWT 相关包来进行 Web 图表绘制的相关方法: public void paint(Graphics g)  {  // 调用父类的 paint 方法 super.paint(g);  // 设置笔划 g.setColor(someColor);  g.setFont(someFont);  // 绘制图形 g.drawString(...);  // 绘制文本 g.drawLine(...);  // 绘制线段 g.drawRect(...);  // 描绘矩形 g.fillRect(...);  // 填充矩形 g.drawOval(...);  // 描绘椭圆 g.fillOval(...);  // 填充椭圆 g.drawPolygon(...);  // 描绘多边形 g.drawPolygon(...);  // 填充多边形 g.drawImage(...);  // 显示图像 ...  } 这些方法读者都已经非常熟悉了。在  Java2D  中,这些代码有些什么不同呢?因为  Graphics2D 类是 Graphics 的子类,所以可以完成本书前面所讲述的所有方法而不需做任何变 动。只需声明如下所示的一个 Graphics2D 的对象引用即可。 Graphics2D g2d  =  (Graphics2D)g 也就是说, 如果要调用 Java2D 的方法, 就必须将 Graphics 的引用 g 强制转换成 Graphics2D  的引用 g2d,然后通过 g2d 来调用 Graphics 的引用 g 的相关方法。上述代码可以改写成如下 形式: public void paint(Graphics g)  {  // 调用父类的 paint 方法 super.paint(g);  Graphics2D g2d = (Graphics2D) g
    • // 设置笔划 g2d.setColor(someColor);  g2d.setFont(someFont);  // 绘制图形 g2d.drawString (...);  // 绘制文本 g2d.drawLine(...);  // 绘制线段 g2d.drawRect(...);  // 描绘矩形 g2d.fillRect(...);  // 填充矩形 g2d.drawOval(...);  // 描绘椭圆 g2d.fillOval(...);  // 填充椭圆 g2d.drawPolygon(...);  // 描绘多边形 g2d.drawPolygon(...);  // 填充多边形 g2d.drawImage(...);  // 显示图像 ...  } 我们着重于 Graphics2D 的新特性。除了示例中调用 Graphics 对象的方法外,Graphics2D  对象还拥有自己独特的新方法。如下所示: public void paintComponent(Graphics g)  {  // 调用父类的 paintComponent 方法 super.paintComponent(g);  Graphics2D g2d = (Graphics2D) g  // 设置绘图环境属性 g2d.setPaint(fillColorOrPattern);  g2d.setStroke(penThicknessOrPattern);  g2d.setComposite(someAlphaComposite);  g2d.setFont(anyFont);  g2d.translate(...);  g2d.rotate(...);  g2d.scale(...);  g2d.shear(...);  g2d.setTransform(someAffineTransform);  // 创建某种图形 SomeShape shape = new SomeShape(...);  // 绘制图形 g2d.draw(shape);  // 描绘图形轮廓 g2d.fill(shape);  // 填充实心图形 ...  } 我们可以看出,在 Graphics2D 中,图形的渲染(绘制)一般分成三步进行: (1)创建所要绘制的图形。 (2)设置该图形绘图环境的相关属性,如笔划粗细、线条类型(实线、虚线、点划线) 、 绘制的颜色、绘制模式、旋转、平移、缩放、扭曲、字体等属性。 (3)根据设置好的相关绘图环境的相关属性,描绘或填充该图形。 在理解上述步骤的基础上,我们就可以编写一些基于 Java2D API 的 Web 图表程序了。 提示:Java2D 本身就是一个博大精深的内容。它需要一本专著来讲述,已经超出本书的 内容。但 Java2D 可以给我们带来强大的图形、图像处理功能,令 Web 图表更加美观。因此, 我们将 Java2D 作为单独的一章进行详尽的讲解。
    • 7.3    Java2D 与填充属性  7.3.1  设置填充属性  paint 属性,决定图形的描绘或填充的颜色,也用它来定义填充图形的模式。填充属性是 通过调用绘图环境的 setPaint 方法进行设置。 默认的 paint 属性的颜色是当前绘图环境的颜色, 如表 7.1 所示的 setPaint 方法。 表 7.1    Java2D 的 setPaint 方法 方 法 说 明  abstract void setPaint(Paint paint)  设置 Graphics2D 图形环境的填充属性  Graphics2D 的 setPaint 方法接收一个参数,该参数为 Paint 类的实例 paint。这里 Paint 类 的对象  paint  可以是任何实现  java.awt.Paint  接口的类的对象。Paint  对象可以是我们熟悉的  java.awt.Color  类的一个实例(Color  类实现了  Paint  接口) ,也可以是  java.awt.GradientPaint  类、java.awt.SystemColor 类、java.awt.TexturePaint 类的一个实例。 其中 SystemColor 类继承于 Color 类。下面分别给出几个实例,说明 setPaint 的用法。  7.3.2  填充属性的用法  1.纯色及渐进色填充  gradientPaint.jsp( chap07Fig7.3Fig.7.3_01 ) 演 示了背景填充的方法,运行结果如图 7.1 所示。 首先,程序在第 8 行: import="java.awt.geom.*" 引进了 Java2D 包。我们在前面说过,geom 包中 定义了 Java2D 所需要的复杂的二维图形。一般来说, 只要是调用 Java2D,就需要引入该包。 程序第 27 行: 图 7.1    gradientPaint.jsp 的运行结果 Graphics2D g2d = (Graphics2D)g; 将 Graphics 的引用 g 强制转换成 Graphics2D 的引用 g2d。这是使用 Graphics2D 对象必 须要做的工作。 程序第 30 行~第 31 行: g2d.setColor(Color.YELLOW);  g2d.fillRect(0, 0, width, height); 这里我们调用 Graphics 类的方法 setColor 设置当前绘制颜色为黄色。然后调用 Graphics  类的 fillRect 方法,填充一个黄色的实心矩形。这两句代码是读者经常见到的,前面的例程是 用来填充绘图环境的背景。 这两句代码演示了 Graphics2D 继承于 Graphics 类的子类和父类的 关系。因此,在 Graphics2D 类的实例中,可以直接调用 Graphics 类的方法。这些方法的用法 和我们以前介绍的 Graphics 类的用法完全相同。因为 Color 类实现了 java.awt.Paint 接口,所 以程序第 30 行,可以改写成如下形式:
    • g2d.setPaint(Color.YELLOW); 程序运行结果也完全相同。 程序第 34 行~第 35 行: GradientPaint gp1 = new GradientPaint(15, 75, Color.RED,  50, 120, Color.GREEN, false); 创建了一个 GradientPaint 类的实例 gp1。GradientPaint 类实现了用渐进色(也称为颜色 梯度)描绘或填充图形组件的效果。这里所调用的 GradientPaint 的构造器共有 7 个参数。第  1、2 个参数指定渐进色的起始坐标,第 3 个参数指定渐进色的起始颜色为红色,第 4、5 个 参数指定渐进色的结束坐标,第 6 个参数指定渐进色的结束颜色为绿色,第 7 个参数是个布 尔值。该布尔值指定渐进色是循环的(true) ,还是非循环的(false) 。 参数中两种坐标指定了渐进色中颜色的变化方向。因为渐进色的结束坐标(50,120)在 其起始坐标(15,75)的右下方,因此,渐进色中颜色的变换方向也是向右下方变化。因为这 里的布尔值为“false”,所以渐进色是非循环的,也就是说,渐进色的颜色从红色渐变到绿色 后,颜色不再进行渐变。 程序第 34 行: g2d.setPaint(gp1); 调用  Graphics2D  类的  setPaint  方法,设置图形环境的填充模式,setPaint  需要接收一个  Paint 类的实例对象。这里我们将 GradientPaint 对象 gp1 作为其参数。 程序第 35 行: g2d.fill(new Rectangle(50, 10, width­100, height­220)); 调用 Graphics2D 类的 fill 方法,填充一个 Shape 对象。前文说过,这里所指的 Shape 对 象,其实就是实现了 Shape 接口的任何类的实例。查看 J2SE 5.0 API 可知以下对象都实现了  Shape 接口,如  Arc2D,  Arc2D.Double,  Arc2D.Float,  Area,  BasicTextUI.BasicCaret,  CubicCurve2D,  CubicCurve2D.Double,  CubicCurve2D.Float,  DefaultCaret,  Ellipse2D,  Ellipse2D.Double,  Ellipse2D.Float,  GeneralPath,  Line2D,  Line2D.Double,  Line2D.Float,  Polygon,  QuadCurve2D,  QuadCurve2D.Double,  QuadCurve2D.Float,  Rectangle,  Rectangle2D,  Rectangle2D.Double,  Rectangle2D.Float,  RectangularShape,  RoundRectangle2D,  RoundRectangle2D.Double,  RoundRectangle2D.Float。 这些对象大部分都位于  java.awt.geom  包中。因此,我们在程序第  35  行,直接调用  java.awt.Rectangle 类的构造器,创建一个 Shape 对象。之后,调用 g2d 的 fill 方法,填充创建 的这个 Shape 对象:左上角坐标为(50,10) ,宽度为 220 像素,高度为 180 像素的矩形。 绘制结果如图 7.1①所示。我们在调用 GradientPaint 的构造器时,第 7 个参数布尔值为 “false”,因此,图  7.1①中可以看到渐进色从红色渐变成绿色后,就一直保持绿色而没有发 生变化。 程序第 39 行~第 42 行: GradientPaint gp2 = new GradientPaint(15, 75, Color.RED,  50, 120, Color.GREEN, true);  g2d.setPaint(gp2);  g2d.fill(new Rectangle(50, 210, width­100, height­220)); 为了对比 GradientPaint 的构造器,第 7 个参数布尔值为“true”时,可以观察到渐进色 从红色渐变为绿色后,又从绿色渐变为红色,然后一直这样循环渐变。同时,渐变色的方向
    • 是向右下方变化的。该段代码绘制结果如图 7.1②所示。  2.纹理填充  texturePaint.jsp(chap07Fig7.3Fig.7.3_02)演示了纹理填充的方法。纹理填充类似于在  Windows 中设置壁纸图像平铺的效果。如果希望以纹理模式进行填充操作,首先必须指定用 于纹理填充操作的缓冲图像, 其次指定使用该缓冲图像中的哪一部分矩形区域作为填充纹理。 创建缓冲图像一般有两种方法:一种是由开发者自己编写,另一种是通过加载外部的图像文 件来创建。本例分别给出这两种创建缓冲图像的方法。 程序首先在第 31 行~第 32 行: BufferedImage bg = new BufferedImage(10, 10,  BufferedImage.TYPE_INT_RGB); 创建了一个宽度和高度均为 10 像素的 BufferedImage  对象 bg。 然后程序在第 34 行: Graphics2D bg2d = bg.createGraphics(); 声明了一个  Graphics2D  的对象  bg2d,调用  bg  的  createGraphics  方法,并用返回的  Graphics2D 的实例对 bg2d 进行初始化。 程序第 35 行~第 42 行: bg2d.setColor(Color.YELLOW);  bg2d.fillRect(0, 0, 10, 10);  bg2d.setColor(Color.RED);  bg2d.fillRect(1, 1, 6, 6);  bg2d.setColor(Color.BLUE);  bg2d.fillRect(2, 2, 4, 4);  bg2d.setColor(Color.BLACK);  bg2d.fillRect(4, 4, 3, 3); 这段代码,绘制了一个 10x10 像素的黄色实心矩形。在该黄色实心矩形中,又覆盖着三 个尺寸大小、颜色不一的实心矩形。 程序第 45 行: TexturePaint tp1 = new TexturePaint(bg, new Rectangle(10,10)); 声明了一个 TexturPaint 对象 tp1。这里我们调用 TexturPaint 的构造器来生成 TexturPaint  的实例。该构造器接收两个参数。正如我们刚才所说,如果希望以纹理模式进行填充操作, 首先必须指定用于纹理填充操作的缓冲图像,其次指定使用该缓冲图像中的哪一部分矩形区 域作为填充纹理。因此,这里构造器接收的第 1 个参数就是我们创建的缓冲图形对象 bg,第  2 个参数是一个 Rectangle2D 的实例。Rectangle2D 位于 java.awt.geom 包中,因为 Rectangle  类(位于  java.awt  包中)是其直接的子类,所以我们可以用  Rectangle  类的实例来代替  Rectangle2D  的实例。注意,这里的  new  Rectangle(10,  10)实际上等同于  new  Rectangle  (0,0,10,10)。本例中 Rectangle 和 bg 的大小正好相同,宽度和高度均为 10 像素。我们也可 以指定 bg 图像中的一部分区域作为纹理。 程序第 46 行: g2d.setPaint(tp1); 设置当前绘图环境的填充模式为纹理填充。 程序第 48 行: g2d.fillRect(0, 0, width, height);
    • 调用 fillRect 方法,利用当前纹理填充模式对整个绘图环境进行纹理填充。 接下来,程序利用加载外部图像文件的方式来创建缓冲图像。请读者将 javaLogo.jpg 文 件(chap07Fig7.3Fig.7.3_02)复制到 Web 应用程序的根目录中。 程序第 52 行~第 57 行: File jpgFile =  new File(getServletContext().getRealPath("javaLogo.jpg"));  BufferedImage im = ImageIO.read(jpgFile);  // 创建纹理填充对象 TexturePaint tp2 =  new TexturePaint(im, new Rectangle(im.getWidth(),im.getHeight ())); 其中程序第 52 行~第 54 行,都是我们熟悉的代码,就是利用 ImageIO 类的 read 方法, 加载外部的图像文件。并用 ImageIO 类的 read 方法,返回的对象来初始化 BufferedImage 类 的对象 im。 程序第 56 行~第 57 行,创建 TexturePaint 对象 tp2。我们指定这里构造器的缓冲图像参 数为 im,同样,用一个 Rectangle 实例来代替 Rectangle2D 实例。Rectangle 的宽度和高度通 过调用 im 的 getWidth 和 getHeight 方法获得,这两个方法分别返回当前图像的宽度和高度。 程序第 60 行~第 61 行: g2d.setPaint(tp2);  g2d.fillOval(50, 50, width ­100, height­100); 设置绘图环境的填充模式为纹理填充对象  tp2  所 表示的模式。然后,按照  tp2  所表示的模式填充一个 外切矩形, 左上角坐标为(50,50) ,宽度为 220 像素, 高度为 300 像素的一个椭圆。  texturePaint.jsp 的运行结果如图 7.2 所示。  7.4    Java2D 与笔划属性 我们在前面讨论过,java.awt 默认的笔划属性,定 义了一个正方形的、粗细为  1  像素的实线,末端为正 方形,接口为 45 度斜面。而在 Java2D 中,stroke 属性 可以自定义实线、虚线、点划线,以及线条的粗细。 它也自定义线段端点的形状风格(end style)及两条线 图 7.2    texturedPaint.jsp 的运行结果 段相交处的形状风格(join style)。 在图形环境中应用笔划属性的基本步骤如下: (1)创建一个 BasicStroke 的实例。BasicStroke 类位于 java.awt 包中。它实现了 Stroke  接口。BasicStroke 有几个不同的构造器,分别用来设置不同的笔划属性。 (2)将创建好的  BasicStroke  的实例作为参数传递给  Graphics2D  的  setStroke  方法,  setStroke 方法,需要接收一个任何实现 Stroke 接口的类的实例。 (3)通过  Graphics2D  的  draw 方法就可以应用自定义的笔划属性了。Graphics2D  根据  Storke 对象的属性来描绘图形的轮廓。
    • 7.4.1  线段端点的形状风格 线段端点有许多种不同形状的风格。Basicstroke 支持三种端点风格,分别由三个常数来 表示,如图 7.3 所示。 public static final int CAP_BUTT 这种端点的形状风格不会在端点加上任何东西 CAP_BUTT  ①  CAP_RNOUD  ②  CAP_SQUARE  ③ 作为修饰,采用这种端点直线段的外观类似于矩形。 图 7.3    BasicStroke 支持三种端点风格  如图 7.3①所示。 Public static final int CAP_RNOUD 这种端点会在线段的端点加上半圆作为修饰,半圆的直径和线段宽度相同。如图  7.3② 所示。 Pubic static final int CAP_SQUARE 这种端点风格会在线段的端点加上矩形作为修饰,矩形的宽为线段宽度的一半,高度和 线段宽度相同。该端点的风格适用于各种线段。如图 7.3③所示。  7.4.2  线段转折处的形状风格  BasicStroke 提供了三种不同的风格,来表示线段转折处笔划的连接方式,如图 7.4 所示。 ① ② ③ 图 7.4    BasicStroke 笔划线段转折处的三种风格 同样,它们也分别由三个常数表示,这些常数也被定义于  BasicStroke  类中,分别是  JOIN_BEVEL、JOIN_MITER 和 JOIN_ROUND。 public static final int JOIN_BEVEL 这种风格的线段会通过如图 7.4①所示的方式连接。 public static final int JOIN_MITER 这种风格的线段,会通过延长两边笔划(延伸它们的外缘)并相交于某一点而生成,不 过线段延伸的长度(miter)却是有限制的。试想一下,如果相交的笔划接近于平行,那么笔 划的延长线可能会在很远的位置才会相交, 这种情况下, 笔划的形状就变形了。 因此,  Java2D  对  miter  的长度有一个限制。如果延伸的长度大于  miter  限制长度的话,系统便自动更改  JOIN_BEVE 风格。JOIN_MITER 的 miter 并未直接指定 miter 的长度,而是通过下面三个值 来指定: Ø  halfLength:线段宽度的一半。 Ø  miterLength:外缘延伸的相交处和内缘相交处的距离。 Ø  miterLimit:外缘延伸长度的上限。 如果 miterLength 大于(miterLimit 乘以 halfLength),系统便自动改用 JOIN_BEVEL,而 不会采用 JOIN_MITER。 (注:miterLimit  的值就是  BasicStroke 器中  miterLimit  参数的值。  HalfLength 及 miterLength 这两个变量的值则是由系统来计算的。 )这样做的目的是要避免在
    • 线段转折处生成夸张的笔划。当两条线段近乎平行时,采用 JOIN_MITER 来生成的线段笔划 会过度地延伸,远离线段的转折处。 public static final int JOIN_ROUND 这种风格的线段,会在结合处添加半圆形作为修饰,与 CAP_ROUND 端点风格差不多。  7.4.3  虚线风格  BasicStroke 还可以设置任意风格的虚线。使用  BasicStroke  对象,可以定义复杂的短划 线图案。在设置虚线风格的时候,需要指定两个用于控制虚线风格的参数: (1) dash: 代表虚线图案的数组。 数组中的元素交替代表虚线线段中 “虚线线段的长度” 及“两条虚线之间空白部分的长度” 。索引值为偶数的数组元素代表着虚线中“虚线线段的长 度”,而索引值为奇数的数组元素代表“两条虚线之间空白部分的长度” 。举例来说: Ø 假设 dash 数组元素为{15,12},表示的意义是虚线段中“虚线线段的长度”及“两条 虚线之间空白部分的长度”分别为 15 像素和 12 像素(注:因为数组的第 1 个元素的 索引值为“0”。 ) Ø 如果 dash 数组元素为{8,4,12,5,7,10},则表示该虚线由一组长短不一的虚线组成(点 划线) 。第一段虚线的长度为 8 像素,紧跟 4 像素的空白,接着是 12 像素的虚线,随 后是 5 像素的空白,然后是 7 像素的虚线,最后是 10 像素的空白。 Ø 如果  dash  数组中只有一个元素,如:{10},则表示虚线的长度和它们之间的间距为 常数,即 10 像素,等同于数组{10,10}。 (2)dash_phase:定义虚线图案开始位置的偏移量。以虚线数组为{15,12}的虚线图 案为例。当虚线的偏移量为 0 时,表示从虚线图案的开头部分开始绘制虚线,接着绘制空 白,以此类推,如图 7.5①所示。当虚线的偏移量为 5 时,虽然还是表示从虚线图案的开 头部分开始绘制虚线,但这里虚线的长度为 10 像素(15­5) ,然后是 12 像素的空白部分, 在此之后,绘制和虚线的偏移量为 0 的虚线图案相同的图案。如图 7.5②所示。 单位:像素  0  15  25  40  50  虚线 1  偏移量 0  虚线 5  偏移量 2  0  10  20  35  45  60 图 7.5  虚线风格  7.4.4    BasicStroke 构造器 在理解上述笔划的端点风格、 转折点风格以及虚线风格的概念后, 创建 BasicStroke 笔划 对象便是比较轻松的事情了。  BasicStroke 笔划对象的创建一般是通过 BasicStroke 类的构造器 进行的。 表 7.2 所示 BasicStroke 的构造器方法。 表 7.2  BasicStroke 的构造器方法 方法:public BasicStroke()  说明:BasicStroke 的默认构造器。根据默认值来创建 BasicStroke 对象:宽度为 1.0、端点风格为 CAP_SQUARE、线段 转折处的风格为 JOIN_MITER、miter 为 10.0 的虚线段 方法:public BasicStroke(float width)  说明:该构造器根据参数指定的值来创建 BasicStroke 对象:宽度为 width 个像素、端点风格为 CAP_SQUARE、线段转
    • 折处风格为 JOIN_MITER、miter 为 10.0 的虚线段 方法:public BasicStroke(float width, int cap, int join)  说明:该构造器根据参数指定的宽度、端点风格以及线段转折处的风格来创建  BasicStroke  对象。如果线段转折处风格 为 JOIN_MITER,则会自动设定 miter 的上限值为 10.0  方法:public BasicStroke(float width, int cap, int join, float miterLimit)  说明:该构造器根据参数指定的宽度、端点风格以及线段转折处的风格来创建  BasicStroke  对象。如果线段转折处风格 为 JOIN_MITER,则设定 miter 上限值为参数 miterLimit 所指定的值 方法:public BasicStroke(float width, int cap, int join, float miterLimit, float[] dash, float dash_phase)  说明:该构造器根据参数指定的宽度、端点风格以及线段转折处的风格、虚线数组以及虚线偏移量来创建  BasicStroke  对象。如果线段转折处风格为 JOIN_MITER,则设定 miter 上限值为参数 miterLimit 所指定的值  7.4.5  Java2D Web 图表实例之折线图  1.普通线段图表 现 在 我 们 来 编 写 一 个 综 合 应 用 所 学  Java2D  API  内 容 的  Web  图 表 。 lineChart.jsp  (chap07Fig7.4Fig.7.4_01)演示了如何利用 Java2D API 来绘制线段图。lineChart.jsp 综合了目 前我们所讨论的 Java AWT、Servlet 和 Java2D 技术。  lineChart.jsp 的运行结果如图  7.6 所示。本程序绘制了两条折线,分别代表 Java  类以及  C#类计算机编程图书在 2004 年的月销售量。月销售量由随机数产生。如图 7.6 所示,书籍的 月销售量 Web 折线图表绘制效果比以前我们绘制的 Web 图表更美观,有了漂亮的背景、立 体感很强的边框、虚线组成的网格线、和谐的坐标轴等等,这些效果是如何实现的呢?我们 将详细讨论整个源程序,将其抽丝剥茧,将其绘制的步骤一一呈现在读者面前。  lineChart.jsp  调用了以前我们讨论过的生成三角形的  Servlet,来生成坐标轴上的箭头。 与以前讲过的 JSP/HTML 文档,调用 Servlet 的方法不同,本例采用了 JavaBean 的形式,来 调用该 Servlet  (关于如何编写 JavaBean, 我们将在本书最后一章进行讲