第 12 章 导出文件

注意

超越contentType="text/html",servlet不只可以生成text/html类型的html文本,也可以生成p_w_picpath/jpeg类型的图片,http支持的所有文件格式都可以通过servlet生成。
如果你不满足以下任一条件,请继续阅读,否则请跳过此后的部分,进入下一章: 第 13 章 剖析el表达式
  1. 了解如何使用servlet生成图片。
  2. 了解设置contentType使servlet生成不同格式的文件。

12.1. 图片校验码

进入首页,会显示一个彩×××形验证码,用户根据图片上的文字输入文本框。
jsp-ch-12-01-p_w_picpath-01.png
如果输入错误,会提示输入与图片文字不同,并更新验证码。
jsp-ch-12-01-p_w_picpath-02.png
输入正确会显示成功信息。
jsp-ch-12-01-p_w_picpath-03.png
彩色验证码用来防止恶意程序自动发送垃圾消息,或者是恶意程序循环尝试登录密码。人眼可以根据图片了解验证码的内容,但如果是程序就需要扫描图片分析图片中的内容,为了加大程序分析破解的难度,我们还为图片准备了干扰用的背景颜色,并随便修改文字的颜色。这些都是为了加大程序破解的难度。
现在所有的注意力都集中到如何动态生成校验用的图片,看一下index.jsp中的代码。
<img src="captcha.jpg" />
        
大家可能感到奇怪了,这里img标签对应的是一个静态jpg图片,为什么每次刷新显示的图片内容都不同呢?仔细检查12-01目录下我们也看不到captcha.jpg这个图片,这个图片到底是从哪里得到的呢?
如我们之前所谈到的 第 3.4.2 节 “forward导致找不到图片”,在html里包含的图片,css样式表,js脚本,视频等等外部资源,都需要浏览器再次向服务器发起请求。现在我们进行的请求是一个名叫captcha.jpg的图片,而服务器上并没有这个图片,从web.xml里的配置可以看到如下配置。
<servlet>
    <servlet-name>CaptchaServlet</servlet-name>
    <servlet-class>anni.CaptchaServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>CaptchaServlet</servlet-name>
    <url-pattern>/captcha.jpg</url-pattern>
</servlet-mapping>
        
在这里,名叫/captcha.jpg的请求会交给CaptchaServlet处理。虽然这个请求看起来很像一个实际存在的文件,可服务器接收到这个请求之后并没有去磁盘上找这个文件,而是根据web.xml中配置把request发送给CaptchaServlet并等待它做出响应。
请大家注意,jsp和servlet并不是只能返回html格式的数据,实际上它们可以生成任意格式的数据,比如这里我们就用servlet生成了一个图片。正如我们之前讨论forward时提到的, 第 3.4.2 节 “forward导致找不到图片”,浏览器只是向服务器发送了一个请求,这个请求的地址是/captcha.jpg还是/index.jsp并没有什么区别,在服务器看来他们仅仅是一个字符串而已,接收到请求后服务器先去按照web.xml中的配置做映射,将请求交给对应的servlet处理,如果web.xml中没有对应这个请求的映射,才会去磁盘查找是否有这么一个文件,找到文件则输出到响应中传回客户端,如果找不到就返回经典的404(找不到访问资源)。
其实我们使用的/check.do也是一样的道理,你在服务器上找不到名叫check.do的文件,它只是一个指向CheckServlet的道标,告诉服务器调用CheckServlet处理这个请求,请记住,这些仅仅是请求,你永远不知道服务器会返回给你什么。
代码请大家参考12-01/WEB-INF/src/CaptchaServlet.java,因为img只可能通过GET方式发送请求,所以我们仅仅定义了doGet()方法。
public void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
    //设置页面不缓存
    response.setHeader("Pragma", "No-cache");
    response.setHeader("Cache-Control", "no-cache");
    response.setDateHeader("Expires", 0);
    // 在内存中创建图象
    int width = 60, height = 20;
    BufferedImage p_w_picpath = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
    // 获取图形上下文
    Graphics g = p_w_picpath.getGraphics();
    //生成随机类
    Random random = new Random();
    // 设定背景色
    g.setColor(getRandColor(200, 250));
    g.fillRect(0, 0, width, height);
    //设定字体
    g.setFont(new Font("Times New Roman", Font.PLAIN, 18));
    //画边框
    //g.setColor(new Color());
    //g.drawRect(0, 0, width - 1, height - 1);
    // 随机产生155条干扰线,使图象中的认证码不易被其它程序探测到
    g.setColor(getRandColor(160, 200));
    for (int i = 0; i < 155; i++) {
        int x = random.nextInt(width);
        int y = random.nextInt(height);
        int xl = random.nextInt(12);
        int yl = random.nextInt(12);
        g.drawLine(x,y,x+xl,y+yl);
    }
    // 取随机产生的认证码(4位数字)
    String sRand = "";
    for (int i = 0;i < 4; i++) {
        String rand = String.valueOf(random.nextInt(10));
        sRand += rand;
        // 将认证码显示到图象中
        // 调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成
        g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
        g.drawString(rand, 13 * i + 6, 16);
    }
    // 将认证码存入SESSION
    request.getSession().setAttribute("captcha", sRand);
    // 图象生效
    g.dispose();
    // 输出图象到页面
    ImageIO.write(p_w_picpath, "JPEG", response.getOutputStream());
}
        
代码最先设置response(响应)中的头部配置,告诉浏览器不要缓存对/captcha.jpg的请求结果,这样才能保证每次刷新页面都看到最新生成的图片,要是设置了缓存很可能每次看到的都是最先请求看到的图片。
中间一大段代码实现动态生成图片的功能,我们先随机获得几个数字,然后写到BufferedImage中,最后就可以把图片数据写到response,因为图片是二进制数据,所以我们使用了response.getOutputStream()而不是response.getWriter()。
为了达到验证的功能,每次生成图片之后要记得讲随机得到的数字保存到session中,session中的变量可以跨越多个请求周期存在,等用户输入验证码提交后就能与session中的数据做比较了,这些是在CheckServlet中实现的。
public void doPost(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
    HttpSession session = request.getSession();
    String requestCaptcha = request.getParameter("captcha");
    String sessionCaptcha = (String) session.getAttribute("captcha");
    if (sessionCaptcha != null && sessionCaptcha.equals(requestCaptcha)) {
        session.removeAttribute("captcha");
        request.getRequestDispatcher("/success.jsp").forward(request, response);
    } else {
        request.setAttribute("message", "验证码输入错误");
        request.getRequestDispatcher("/index.jsp").forward(request, response);
    }
}
        
这样我们便获得了彩×××性校验码的功能,例子在12-01目录下。

12.2. 图片校验码 - jsp版

使用servlet既需要编译源代码,又要在web.xml加入对应的请求映射。如果只是需要简单的验证码功能,我们也可以使用jsp实现。
我们已经知道jsp就是servlet的另一种形式,servlet能做的事情jsp也可以实现,下面就是实现CaptchaServlet功能的captcha.jsp。
<%@ page contentType="p_w_picpath/jpeg" import="java.awt.*,java.awt.p_w_picpath.*,java.util.*,javax.p_w_picpathio.*" %>
<%!
// 给定范围获得随机颜色
Color getRandColor(int fc,int bc) {
    Random random = new Random();
    if(fc > 255) {
        fc = 255;
    }
    if(bc > 255) {
        bc = 255;
    }
    int r = fc + random.nextInt(bc - fc);
    int g = fc + random.nextInt(bc - fc);
    int b = fc + random.nextInt(bc - fc);
    return new Color(r, g, b);
}
%>
<%
//设置页面不缓存
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
// 在内存中创建图象
int width = 60, height = 20;
BufferedImage p_w_picpath = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 获取图形上下文
Graphics g = p_w_picpath.getGraphics();
//生成随机类
Random random = new Random();
// 设定背景色
g.setColor(getRandColor(200,250));
g.fillRect(0, 0, width, height);
//设定字体
g.setFont(new Font("Times New Roman", Font.PLAIN, 18));
//画边框
//g.setColor(new Color());
//g.drawRect(0,0,width-1,height-1);
// 随机产生155条干扰线,使图象中的认证码不易被其它程序探测到
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
    int x = random.nextInt(width);
    int y = random.nextInt(height);
    int xl = random.nextInt(12);
    int yl = random.nextInt(12);
    g.drawLine(x,y,x+xl,y+yl);
}
// 取随机产生的认证码(4位数字)
String sRand = "";
for (int i = 0;i < 4; i++) {
    String rand = String.valueOf(random.nextInt(10));
    sRand += rand;
    // 将认证码显示到图象中
    // 调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成
    g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
    g.drawString(rand, 13 * i + 6, 16);
}
// 将认证码存入SESSION
session.setAttribute("captcha", sRand);
// 图象生效
g.dispose();
// 输出图象到页面
ImageIO.write(p_w_picpath, "JPEG", response.getOutputStream());
out.clear();
out = pageContext.pushBody();
%>
        
captcha.jsp中的代码与servlet基本相同,不过需要注意两点。
  1. 在定义Color getRandColor(int fc,int bc)的时候用到了<%!%>的写法,比普通<%%>多了一个叹号,这是在jsp中定义方法时必要的步骤,方法必须写到<%!%>中才能正常执行,感兴趣的话可以去tomcat的work下找对应生成的java代码来研究,看看<%!%>和<%%>的代码分别生成到哪里。
  2. 在jsp中使用response.getOutputStream()很可能引起一个问题。
    getOutputStream() has already been called for this response
                    
    在tomcat5下jsp中出现此错误,一般都是在jsp中使用了输出流(如输出图片验证码,文件下载等)后没有妥善处理好。
    具体的原因就是在tomcat中,jsp转换成servlet之后在函数_jspService(HttpServletRequest request, HttpServletResponse response)的最后有一段这样的代码
    finally {
        if (_jspxFactory != null) _jspxFactory.releasePageContext(_jspx_page_context);
    }
                    
    这里是在释放在jsp中使用的对象,会调用response.getWriter(),因为这个方法是和response.getOutputStream()相冲突的,所以会出现以上这个异常。
    然后当然是要提出解决的办法,其实挺简单的(并不是和某些朋友说的那样--将jsp内的所有空格和回车符号所有都删除掉),在使用完输出流以后调用以下两行代码即可:
    out.clear();
    out = pageContext.pushBody();
                    
我们还需要修改index.jsp,让其中的img标签请求captcha.jsp。
<img src="include/captcha.jsp">
        
这里又一次证明了html写的仅仅是请求而已,src="include/captcha.jsp"怎么看都不像一个图片的名称,但是它确实可以正确显示图片,因为服务器将请求交给captcha.jsp,处理后得到是一个图片的数据,可以在浏览器里正常显示。
captcha.jsp放在12-02/include目录下,小项目中可以直接用它了。