分类: Java
2008-04-25 15:35:05
到现在为止,我们所写的servlet都返回一个标准的HTML页。但是构成WEB的要素不只是HTML,在本章,我们向大家介绍一下servlet返回的许多有趣的东西。我们先看一下为什么和如何返回不同的MIME类型。最常用的MIME 类型是由servlet(或由servlet中调用applet)产生图象。本章还对何时和如何发送一个压缩的响应,以及用多阶段的Response实现PUSH作出说明。
视觉是人类获得信息的主要途径,读文字信息仅仅是一种方式,其实人们更喜欢看一些直觉的信息(如图片等)。现在的web开发中,几乎没有一个网站是不含任何图片信息的,即使是它们的图象并不是很专业。曾经有人说过:“一张图象所表示的内容要比一千个文字所表示的内容更形象”。
幸运的是一个servlet把一个图象作为其响应输出并不是很难,事实上,我们在第七章中已经看到一个能够完成这一工作的servlet:ViewFile Servlet,这个servlet可以返回在服务器的root下的任意类型的文件。当文件恰好是一个图象时,它通过getMimeType()方法获得文件的类型,并且通过setContentType()方法设置servlet的输出内容的类型,然后再将其原始的字节信息返回到客户端。
这种处理方法需要将所要发送的图片事先存在硬盘上。经常出现的一种情况是:servlet必须产生或处理一副图象以向客户发送。图象,例如一个包含一个类似时钟的图象显示当前时间的网页,必须有720张图象(60分钟*12小时)存储在硬盘上,servlet决定当前时间发送哪张图片。这是一种很不明智的方法,一个优秀的servlet编程者,可以通过程序来动态的产生时钟的表盘和表针图片,或作为一种变通的方式,从硬盘读取表盘的图象,动态产生表针。
这儿有许多别的原因用servlet来产生一个图象。通过产生图象,servlet可以显示当前的库存表,一个可乐机中剩余的可乐的量的图形化报表。servlet可以通过多种途径来对图象进行操作,例如可以更改它们颜色、大小、或外观,可以在图象上画新的图象或者把几张图象组合组成新的图象。
假设你想把一个以原始的像素保存的图象发送给某个人的话,你应该怎么做呢?我们假设它是一个24位真彩色的图象(每个像素点三个字节),图象具有100个像素高、100个像素宽。你可以采用3000个字节流,每次发送一个像素的这种明显方式来发送。但是这就足够了吗?接收者如何对接收到的这3000个字节进行操作呢?为了让接收者明白如何处理接收到的信息,你必须告诉接收者你发送的是原始信息,真彩色点像素值,从左上角开始,一行一行的发送,每行有100个像素宽。为了节约网络的传输数据量,我们考虑对数据内容进行压缩,然后再进行传输的话,你必须告诉接收者你采用何种压缩技术,然后接收者才能解压缩这个图象。这个问题看起来比较复杂起来。
幸运的是,这个问题已经解决了,而且有几种不同的方式。每一种图象格式(GIF、JPEG、TIFF,等)代表一种解决方式。每一种图象格式定义一种标准的方式去对图象进行编码以便于后来对图象进行显示或加工是的解码。每一种编码技术都有一定的优势和局限。例如,在处理计算机产生的图象时,GIF编码格式优于其他格式,但是GIF格式的局限在于它只能处理256色的图象。JPEG压缩方式的优势在于能处理高色彩的真实照片,但是它是一种有“失真”的压缩方式,它在处理图象时会把图象的细节变模糊。
理解图象的编码方式,有利于理解servlet如何处理图象。一个servlet(例如Viewfile)可以通过把图象进行编码,然后把编码的格式原封不动的发送给客户端—浏览器,浏览器把它解码用于显示,来返回一个已经存在的图象。但是一个servlet产生或者加工一个图象,在发送到客户端之前,必须创建一个图象以外的内在的表达方式,对它进行加工、编码。
例8-1是一个简单的产生图象的例子,这个例子产生了一个GIF图象,图象的内容是“Hello CUUG!”,这个servlet的运行结果如图9-1所示。
例8-1 HelloCUUG.java
import java.io.*;
import java.awt.*;
import javax.servlet.*;
import javax.servlet.http.*;
import Acme.JPM.Encoders.GifEncoder;
public class HelloCUUG extends HttpServlet{
public void doGet (HttpServletRequest req,HttpServletResponse res) throws
ServletException,IOException
{
ServletOutputStream out = res.getOutputStream();
Frame frame = null;
Graphics g = null;
try{
//创建一个不显示的Frame
frame = new Frame();
frame.addNotify();
//用上面创建的Frame创建Image对象,获得其graphics区域
Image image = frame.createImage( 400,60);
g = image.getGraphics();
//向这个不显示的graphics区域中画“Hello CUUG!”
g.setFont( new Font("serif",Font.ITALIC,48));
g.drawString(" Hello CUUG!",10,50);
//对Image对象进行GIF编码,并发送向客户端
res.setContentType ("image/gif");
GifEncoder encoder = new GifEncoder(image ,out);
encoder.encode();
}
finally{
//释放资源。
if ( g!= null) g.dispose();
if (frame !=null) frame.removeNotify();
}
}
}
虽然这个servlet用了java.awt包,它即没有在服务器显示一个窗口图象,也没有在客户端显示一个窗口图象。它的全部工作是在一个脱离屏幕的图象数据(上下文文本)中执行,然后由浏览器显示该图象。它的实施策略如下:创建一个脱离屏幕的image对象,获得其上下文文本,向上下文文本中发数据(执行画操作),对处理后的图象数据进行编码然后发向客户。
获得脱离屏幕的图象需要解决一下几个重要问题。在Java中,图行对象必须用java.awt.Image类来表示。不幸的是一个图象对象不能通过构造函数直接来创建,它必须通过其他方法来获得,如Component(组件)中的createImage()方法、Toolkit 中的getImage()方法。在本例中我们是创建一个新图象,所以我们用的是createImage()方法。需注意的是一个Component(组件)创建图象之前,它的本地同位体必须存在,因此,我们在创建Image之前必须创建一个Frame,通过调用addNotify()来创建Frame的本地同位体,后由Frame创建Image对象。如果我们有一个图象,我们可以画到图象的上下文中,然后通过Image的getGraphics()方法来得到。上面的例子只是画了一个简单的字符串。
在向图象的上下文数据中写入数据之后,我们调用了setContentType()来设置MIME的类型为“image/gif”,表示我们将采用GIF的编码格式。在本章的例子中,我们用Jef Poskanzer写的GIF编码格式。在中可以得到一个free的源文件。为了对图象进行编码,我们先创建一个GifEncoder对象,把image对象和servlet的ServletOutStream对象传递给GifEncoder对象,然后调用GifEncoder对象的encoder()方法,图象对象就被编码然后发送到客户端。
在发送了图象以后,为了使系统的性能更好,一个servlet在处理完输出后还应该再作进一步的工作:释放图象资源。这将自动在无用数据中回收资源,立即释放资源对资源有限的系统是非常有益的。释放资源的代码放在最后,以保证代码的执行,甚至是servlet 抛出异常的时候。
运行结果如下图:
前面的例子,向我们讲述了向空图象中画新图象的做法。在本节我们来研究如何向一个已经存在的图象中画新内容或者将几个已经存在的图象进行组合。我们也在返回图象的servlet中进行了错误处理。
有时在一个已经存在的图象上画新内容是非常必要的,而servlet能轻松的处理这种任务。例如用servlet可以做一个定位器来表示每个员工的位置,当要查询某个员工时,它在那个员工的办公位置画一个大红点。
在一个已经存在的图象上画一个新内容的明显的但是错误的做法主要由一下几个步骤:用Toolkit.getDefaultToolkit().getImage(imagename)检索得到Image,用Image的getGraphics()方法得到图象的内容数据,在返回的图象内容数据中画新内容。然而事实并不那么简单,因为如果image不是用Component的createImage()方法创建的,那么就不能用getGraphics()方法。用AWT,你在后台需要一个本地的同位体,用于做实际图象的复制图。
因此,我们要按照下面的步骤来完成:借助于Toolkit.getDefaultToolkit().getImage(imagename) 检索得到事先存在的图象,然后将它画(复制)到用Component的createImage()方法创建(如例8-1)的image对象中,现在就可以使用图象数据来在原始的图象上画新的图象内容了。
例8-2用一个例子说明了这种技术。它是一个servlet在每个图片上写上“Readed by Cuug!”。图象的名称通过在地址栏中把参数传递过来。执行结果如图所示。
例8-2 ReadbyCUUGServlet.java
import java.io.*;
import java.awt.*;
import javax.servlet.*;
import javax.servlet.http.*;
import Acme.JPM.Encoders.GifEncoder;
public class ReadbyCUUGServlet extends HttpServlet{
Frame frame = null ;
Graphics g = null;
public void init(ServletConfig config ) throws ServletException
{
super.init(config);
//创建一个可重用的不显示的Frame
frame = new Frame();
frame.addNotify();
}
public void doGet (HttpServletRequest req,HttpServletResponse res) throws
ServletException,IOException
{
ServletOutputStream out = res.getOutputStream();
try {
//获得图象的位置
String source = req.getPathTranslated();
if ( source == null){
throw new ServletException("extra path must be a gif file");
}
//装载图象(由字节的记录信息转换为Image对象)
MediaTracker mt = new MediaTracker(frame); //frame作为ImageObserver
Image image = Toolkit.getDefaultToolkit().getImage(source);
mt.addImage(image,0);
try{
mt.waitForAll();
}
catch (InterruptedException e)
{
getServletContext().log(e,"Interrupted while loading");
throw new ServletException(e.getMessage());
}
//创建一个和打开的图象尺寸相匹配的grahics数据
int w = image.getWidth(frame);
int h = image.getHeight(frame);
Image offscreen =frame.createImage(w,h);
g = offscreen.getGraphics();
//把已打开的图象数据复制到grahics数据
g.drawImage(image,0,0,frame);
//向图象中添加新内容(在图象上写上Readed by Cuug!)
g.setFont( new Font("serif",Font.BOLD|Font.ITALIC,30));
g.drawString("Readed by Cuug!",10,50);
//对图象进行gif编码并把它传向客户端
res.setContentType ("image/gif");
GifEncoder encoder = new GifEncoder(offscreen ,out);
encoder.encode();
}
finally{
//释放资源
if ( g!= null) g.dispose();
}
}
public void destroy(){
//释放资源
if (frame !=null) frame.removeNotify();
}
}
可以看出,除了部分管理工作外,这个servlet的每一步都按照前面的描述来完成。它用init()方法创建不显示的Frame。一次创建Frame然后多次使用是为了使程清楚而留下来的一种一种最优的方法。在每次request时,它检索图象的文件名通过扩展的地址信息(HttpServletRequest的getPathTranslated()方法获得在servlet名称后和?参数(查询字符串)之前的文件的物理路径,即文件在磁盘的存储的实际路径)。然后通过Toolkit的getImage()方法得到一个对图象的引用,并且借助于MediaTracker将图象物理地调入内存。通常将图象异步的调入内存(部分先调入的部分先显示同时装载另外一部分)是一种好的方案,但是在本例中我们要一次完成对图象的显示操作,因此要保证图象在操作前完全装入内存。然后servlet得到图象的宽和高,创建一个off-screen的图象与之匹配。接下来是关键的一步,将装载的图象复制到新创建的空图象上去。最后servlet向图象上写一个大的“Readed by Cuug!”并且对图象进行编码传输。
注意,在本例中servlet通过抛出意外进行错误处理并且记录错误信息以便服务器的administrator对其进行管理。返回一个图象的最简便的方法是在标签中引用,但是在servlet中运用复杂的操作,可以实现任何你想要对图象进行的操作。
servlet可以把多个图象组合成一个组合图象。利用这种功能,一个建筑物的位置图servlet可以在每个员工的办公室位置显示员工的笑脸,而不仅仅使一个大红点。图象组合技术和在图象上画新内容的技术实现基本相似:先装载待处理的图片,然后将它们画到一个适当创建的图象对象中,然后对图象对象进行编码和传输。
例8-3是一个组合图象的例子。它是一个点击计数器,它用一系列的单个的数字图象组成一个大的图象。它的运行结果如图8-4所示。
例:8-3图象组合
import java.io.*;
import java.awt.*;
import javax.servlet.*;
import javax.servlet.http.*;
import Acme.JPM.Encoders.GifEncoder;
public class GraCounter extends HttpServlet{
public static final String DIR = "/image/number";
public static final String COUNT = "314159";
public void doGet (HttpServletRequest req,HttpServletResponse res) throws ServletException,IOException
{
ServletOutputStream out = res.getOutputStream();
Frame frame = null;
Graphics g = null;
try{
//获得要显示的图象的计数值,在原始的查询字串中必须为
//唯一的一个数值,否则用缺省值
String count = req.getQueryString();
if ( count == null ) count = COUNT;
int countlen = count.length();
Image images[] = new Image[countlen];
for ( int i = 0 ; i < countlen ; i++ ) {
String imageSrc =
req.getRealPath ( DIR + "/" + count.charAt(i) + ".GIF");
images[i] = Toolkit.getDefaultToolkit().getImage(imageSrc);
}
//创建不显示的frame
frame = new Frame();
frame.addNotify();
//装载图象
MediaTracker mt = new MediaTracker(frame);
for ( int i =0 ; i < countlen ; i++ )
{
mt.addImage(images[i],i);
}
try{
mt.waitForAll();
}
catch(InterruptedException e){
getServletContext().log(e,"Interrupted while loading image" );
throw new ServletException(e.getMessage());
}
if (mt.isErrorAny()){
StringBuffer problemChars = new StringBuffer();
for ( int i =0 ; i < countlen ; i++){
if ( mt.isErrorID(i)) {
problemChars.append(count.charAt(i));
}
}
throw new ServletException("could not load an image for these characters:" +
problemChars.toString());
}
int width = 0;
int height = 0;
for ( int i=0 ; i < countlen; i++ ){
width += images[i].getWidth(frame);
height = Math.max(height,images[i].getHeight(frame));
}
Image image = frame.createImage(width,height);
g = image.getGraphics();
int xindex = 0;
for ( int i = 0; i < countlen ; i++){
g.drawImage(images[i],xindex,0,frame);
xindex += images[i].getWidth(frame);
}
res.setContentType("image/gif");
GifEncoder encoder = new GifEncoder(image,out);
encoder.encode();
}
finally{
if ( g!=null) g.dispose();
if (frame !=null) frame.removeNotify();
}
}
}
图
这个servlet通过原始的query方法获得的字符串来获得计数值。对应每个数,servlet通过DIR()返回的路径检索相应的数字图象(DIR通常在server的文档的根目录下,它把虚拟地址动态的转换成实际地址),然后计算这些图象的组合后的宽度和最大高度,以便创建一个off-screen的图象与之匹配,然后按照由左到右的顺序逐个的将各个图片画到off-screen图象对象中。最后它对图象进行编码用于传输。
在实际应用中,这个servlet必须被其它知道点击次数的servlet调用。例如,在一个服务器端的servlet嵌套在一个页中,其使用的语法如下:
750213”>
本例中用的错误处理方法与前面所提的方法一致,通过ServletException抛出一个错误,并且将错误留给服务器以便服务器采取恰当的处理方法。
8.1.3图象特效
通过前面几个例子,我们学会了servlet处理创建和组合图象的方法。接下来我们学习一下servlet对图象进行特效处理的方法。例如,servlet可以在传输之前缩小图象的尺寸以提高图象的传输速度。或者给一个图象加一些阴影,使其看起来向一个可以按下去的按钮。下面,我们看一个将彩色的图象转换成灰度图象的servlet的例子。
例8-4将一个图象返回前将其转换成灰度图象。在这个 servlet中,没有实际创建一个off-screen的图象,而是通过ImageFilter来创建图象。
例8-4 DeColorize.java
import java.awt.*;
import java.awt.image.*;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import Acme.JPM.Encoders.*;
public class DeColorize extends Httpservlet{
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException,IOException{
res.setContentType(“image/gif”);
ServletOutputStream out = res.getOutPutStream();
String source = req.getPathTranslated();
If(source==null){
throw new ServletException(“Extra path information must point to an image”);
}
Frame frame = new frame();
Image image = Toolkit.getDefaultToolkit().getImage(source);
MediaTracker mt = new MediaTracker(frame);
mt.addImage(image ,0);
try{
mt.waitForAll();
}
catch (InterruptedException e){
getServletContext().log(e, “Interrupted while loading image”);
throw new ServletException(e.getMessage());
}
int width = image.getWidth(frame);
int height = image getHeight(frame);
Image filtered = frame.createImage(
new FilteredImageSource(image.getSource(),new GrayscaleImageFilter()));
GifEncoder encoder = new GifEncoder(filtered ,out);
Encoder.encode();
}
}
例8-4的结构与例8-3类似。主要的不同点是:
//Create an image to match,run through a filter
Image filtered = frame.createImage(
new FilteredImageSource (image.getSource(),
new GrayscaleImageFilter()));
本例中没有用我们一直使用的Componet的createImage(int,int)方法。而是用Componet的createImage(ImageProducer)方法。本例中图象的制造者是FilteredImageSource, FilteredImageSource通过GrayscaleImageFilter将图象转换成灰度图象传递给createImage。这个滤色镜函数将彩色图象的每个像素点转换成对应的灰度值,于是图象被创建时就变成了灰度图象。GrayscaleImageFilter的源代码如例8-5所示。
例8-5 GrayscaleImageFilter.java
import java.awt.*;
import java.awt.image.*;
public class GrayscaleImageFilter extends RGBImageFilter{
public GrayscaleImageFilter(){
canFilterIndexColorModel = true:
}
public int filterRGB(int x,int y,int Pixel){
int red = (pixel&0x00ff0000)>>16;
int green = (pixel & 0x0000ff00)>>8;
int blue = pixel & 0x000000ff;
int luma = (int)(0.299*red + 0.587*green + 0.114* blue);
//通常情况下图片被设成不透明
return (0xff<<24|(luma<<16)|(luma<<8)|luma);
}
}
在彩色图中的所有值,这个滤色镜将对每个点的值接收后进行转化返回一个过滤后的值。将变量canFilterIndexColorModel的值设成true,意味着这个滤色镜对整个彩色图象有效而不是对一个像素点。图象的像素点的值通过32位的整形值给出,第一个代表alpha值(透明度),第二个字节是红色的强度,第三个字节是绿色的强度,第四个字节是兰色的强度。把像素的值转换成灰度值,红、绿、蓝的强度必须转化成同一值(灰度)。我们可以将红、绿、蓝的强度值平均,并将平均值作为每种颜色的强度值,这样图象就转化成灰度值。考虑到人对颜色的敏感度(和其它因素),需要一个加权的平均值。红、绿、蓝的权重分别为0.299、0.587、0.114是参照的美国电视委员会(NTSC)为黑白电视制定的标准。