Java Web应用程序中的所有的请求-响应都是由Servlet来完成的。Servlet是Java Web的核心程序,所有的网址最终都交给Servlet来处理。
Servlet工作流程
Servlet接口
Servlet是一种实现了javax.servlet.Servlet接口的类,Servlet接口规定了特定的方法来处理特定的请求,开发者只需要实现Servlet的相关方法,用户访问Web程序的时候,服务器会调用这些方法完成业务处理。Servlet规范是建立在HTTP规范基础上的。HTTP1.1规范支持OPTIONS、GET、POST、HEAD、PUT、DELETE以及TRACE等7种Web访问方式。
其中最常用的是GET和POST,当浏览器以xxx的方式访问网络程序时,Servlet会执行形如void doXxx(HttpServletRequest request, HttpServletResponse response)的方法,例如doGet,doPost等,参数HttpServletRequest ,HttpServletResponse分别为客户端请求与服务器端响应。
Servlet还有一个方法long getLastModified(HttpServletRequest request),返回该文档的最后修改时间,默认为-1,表示该文档永远是最新的
Java Web目录结构
以Tomcat为例,Web程序部署在Tomcat的/webapps下面,一个webapps文件夹可以部署多个不同的Web应用,根据Servlet规范,Web应用程序自己有特定的结构,部署时必须按照这样的结构部署。
实现Servlet
直接实现Servlet接口来编写Servlet很不方便,需要实现的方法太多,在JDK中javax.servlet.*,javax.servlet.http.*包下提供了对Servlet的支持,如javax.servlet.http.HttpServlet类已经实现了Servlet接口的所有方法,编写Servlet时直接继承HttpServlet,并覆盖需要的方法即可。一般只覆盖doGet()与doPost()方法。
下面是一个例子
package com.helloweenvsfei.servlet; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class FirstServlet extends HttpServlet { private static final long serialVersionUID = 2386052823761867369L; /** * 以 GET 方式访问页面时执行该函数。 * 执行 doGet 前会先执行 getLastModified,如果浏览器发现 getLastModified 返回的数值 * 与上次访问时返回的数值相同,则认为该文档没有更新,浏览器采用缓存而不执行 doGet。 * 如果 getLastModified 返回 -1,则认为是时刻更新的,总是执行该函数。 */ public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 调用 HttpServlet 自带的日志函数输出信息到控制台 this.log("执行 doGet 方法... "); // 处理 doGet this.execute(request, response); } /** * 以 POST 方式访问页面时执行该函数。 */ public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.log("执行 doPost 方法... "); // 处理 doPost this.execute(request, response); } /** * 返回该 Servlet 生成的文档的更新时间。对 Get 方式访问有效。 * 返回的时间为相对于 1970年1月1日08:00:00 的毫秒数。 * 如果为 -1 则认为是实时更新。默认为 -1。 */ @Override public long getLastModified(HttpServletRequest request) { this.log("执行 getLastModified 方法... "); return 0; } private void execute(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ response.setCharacterEncoding("UTF-8"); request.setCharacterEncoding("UTF-8"); // 访问该 Servlet 的 URI String requestURI = request.getRequestURI(); // 访问该 Servlet 的方式,是 GET 还是 POST String method = request.getMethod(); // 客户端提交的参数 param 值 String param = request.getParameter("param"); response.setContentType("text/html"); PrintWriter out = response.getWriter(); out.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">"); out.println("<HTML>"); out.println(" <HEAD><TITLE>A Servlet</TITLE></HEAD>"); out.println(" <BODY>"); out.println(" 以 " + method + " 方式访问该页面。取到的 param 参数为:" + param + "<br/>"); out.println(" <form action='" + requestURI + "' method='get'><input type='text' name='param' value=''><input type='submit' value='以 GET 方式访问 RequestServlet'></form>"); out.println(" <form action='" + requestURI + "' method='post'><input type='text' name='param' value=''><input type='submit' value='以 POST 方式访问 RequestServlet'></form>"); // 由客户端浏览器读取该文档的更新时间 out.println(" <script>document.write('本页面最后更新时间:' + document.lastModified + '<br />'); </script>"); out.println(" <script>document.write('本页面URL:' + location + '<br/>' ); </script>"); out.println(" </BODY>"); out.println("</HTML>"); out.flush(); out.close(); } }
配置Servlet
光有Servlet类文件还不行,Web容器必须知道浏览器怎么访问这个Servlet。也就是说需要配置Servlet的类文件与访问方式。这个配置在Web应用程序的描述文件web.xml里完成。首先要配置Servlet的名称以及类名。名称与类名使用标签<servlet>配置。FirstServlet可以简单配置如下:
<servlet> <servlet-name>FirstServlet</servlet-name> <servlet-class> com.helloweenvsfei.servlet.FirstServlet </servlet-class> </servlet>
<servlet>与</servlet>分别为Servlet配置的开始标签与结束标签。中间的部分就是一个Servlet的配置信息。其中<servlet-name>与<servlet-class>属性是必需配置的。<servlet-name>配置Servlet的名称,<servlet-class>配置Servlet的类名。<servlet-name>可以任意取字符串值,但是必须保证该名称在web.xml里唯一。该名称供其他的标签如<servlet-mapping>,<filter>等使用。
<servlet>标签还有一些可选的配置。加入可选配置后的FirstServlet配置如下:
<servlet> <servlet-name>FirstServlet</servlet-name> <servlet-class> com.helloweenvsfei.servlet.FirstServlet </servlet-class> <init-param> <param-name>message</param-name> <param-value>welcome to FirstServlet</param-value> </init-param> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet>
使用<init-param>可以配置初始化参数,包括参数名称(<param-name>)与参数值(<param-value>),一个Servlet可以配置多个初始化参数。Servlet中可以使用方法getServletContext().getInitParam(String paramName)来取得配置的初始化参数值。
标签<load-on-startup>配置该Servlet的加载方式。如果不配置或者为负数,表示请求的时候才加载,如果为0或正整数,表示启动服务器时加载,数越大加载越靠后。实际项目中一些框架如Struts、JSF、Spring都使用该参数来预加载框架中最核心的Servlet
配置<servlet-mapping>
配置好Servlet名称与类名之后还需要配置Servlet的访问方式,访问方式使用标签<servlet-mapping>配置。配置后的FirstServlet如下:
<servlet-mapping> <servlet-name>FirstServlet</servlet-name> <url-pattern>/servlet/FirstServlet</url-pattern> </servlet-mapping>
标签<servlet-name>指明采用该访问方式的Servlet的名称,也就是前面<servlet>里配置的Servlet的名称。<url-pattern>配置该Servlet的访问方式。<url-pattern>值前面加上Web应用程序的路径,再加上服务器域名端口号信息就是访问该Servlet的网址。<url-pattern>中允许使用通配符*与?。*表示任意长度任意字符串,?代表一个字符,Struts、JSF等框架技术中也使用了通配符映射,把形如xxx.do,xxx.jspa等格式的所有URL都映射到某个核心的Servlet上。
从Java EE 5起,<servlet-mapping>标签就可以配置多个<url-pattern>,例如:
<servlet-mapping> <servlet-name>FirstServlet</servlet-name> <url-pattern>/servlet/FirstServlet</url-pattern> <url-pattern>/servlet/FirstServlet.asp</url-pattern> <url-pattern>/servlet/FirstServlet.jsp</url-pattern> <url-pattern>/servlet/FirstServlet.php</url-pattern> <url-pattern>/servlet/FirstServlet.aspx</url-pattern> </servlet-mapping>
这时无论是用.php后缀访问还是用.asp后缀访问,都会正常显示,可以实现隐藏编程语言的目的。客户无法从URL上判断该程序是使用PHP还是ASP写的,有些公司用特定的后缀来声明版权(如公司名缩写等)。
一个完整的Servlet包括Servlet类、<servlet>配置、<servlet-mapping>配置,缺一不可。
请求与响应
客户端浏览器发送一个请求,服务器做出一系列操作后做出一个响应,发送给客户端完成一次Web过程操作。Web编程的过程就是通过请求分析客户需要什么信息或者进行了什么操作,然后进行一系列的处理,最后通过响应结果显示给客户。
获取request变量
客户端浏览器发出的请求被封装成为一个HttpServletRequest对象。所有的信息包括请求的地址,请求的参数,提交的数据,上传的文件,客户端的IP地址甚至客户端操作系统都包含在HttpServletRequest对象中。
看一个例子,从HttpServletRequest对象中采集客户端信息,然后把信息显示给客户端浏览器。
package com.helloweenvsfei.servlet; import java.io.IOException; import java.io.PrintWriter; import java.security.Principal; import java.util.Locale; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.helloweenvsfei.util.IpUtil; public class RequestServlet extends HttpServlet { private static final long serialVersionUID = -7936817351382556277L; /** * @param accept * @return 客户端浏览器接受的文件类型 */ private String getAccept(String accept){ StringBuffer buffer = new StringBuffer(); if(accept.contains("image/gif")) buffer.append("GIF 文件, "); if(accept.contains("image/x-xbitmap")) buffer.append("BMP 文件, "); if(accept.contains("image/jpeg")) buffer.append("JPG 文件, "); if(accept.contains("application/vnd.ms-excel")) buffer.append("Excel 文件, "); if(accept.contains("application/vnd.ms-powerpoint")) buffer.append("PPT 文件, "); if(accept.contains("application/msword")) buffer.append("Word 文件, "); return buffer.toString().replaceAll(", $", ""); } /** * @param locale * @return 语言环境名称 */ private String getLocale(Locale locale) { if(Locale.SIMPLIFIED_CHINESE.equals(locale)) return "简体中文"; if(Locale.TRADITIONAL_CHINESE.equals(locale)) return "繁体中文"; if(Locale.ENGLISH.equals(locale)) return "英文"; if(Locale.JAPANESE.equals(locale)) return "日文"; return "未知语言环境"; } /** * @param ip IP地址 * @return IP地址对应的物理位置 */ private String getAddress(String ip){ return IpUtil.getIpAddress(ip); } /** * @param userAgent * @return 客户端浏览器信息 */ private String getNavigator(String userAgent) { if(userAgent.indexOf("TencentTraveler") > 0) return "腾讯浏览器"; if(userAgent.indexOf("Maxthon") > 0) return "Maxthon浏览器"; if(userAgent.indexOf("MyIE2") > 0) return "MyIE2浏览器"; if(userAgent.indexOf("Firefox") > 0) return "Firefox浏览器"; if(userAgent.indexOf("MSIE") > 0) return "IE 浏览器"; return "未知浏览器"; } /** * @param userAgent * @return 客户端操作系统 */ private String getOS(String userAgent) { if(userAgent.indexOf("Windows NT 5.1") > 0) return "Windows XP"; if(userAgent.indexOf("Windows 98") > 0) return "Windows 98"; if(userAgent.indexOf("Windows NT 5.0") > 0) return "Windows 2000"; if(userAgent.indexOf("Linux") > 0) return "Linux"; if(userAgent.indexOf("Unix") > 0) return "Unix"; return "未知"; } public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); response.setContentType("text/html"); String authType = request.getAuthType(); String localAddr = request.getLocalAddr(); Locale locale = request.getLocale(); String localName = request.getLocalName(); String contextPath = request.getContextPath(); int localPort = request.getLocalPort(); String method = request.getMethod(); String pathInfo = request.getPathInfo(); String pathTranslated = request.getPathTranslated(); String protocol = request.getProtocol(); String queryString = request.getQueryString(); String remoteAddr = request.getRemoteAddr(); int port = request.getRemotePort(); String remoteUser = request.getRemoteUser(); String requestedSessionId = request.getRequestedSessionId(); String requestURI = request.getRequestURI(); StringBuffer requestURL = request.getRequestURL(); String scheme = request.getScheme(); String serverName = request.getServerName(); int serverPort = request.getServerPort(); String servletPath = request.getServletPath(); Principal userPrincipal = request.getUserPrincipal(); String accept = request.getHeader("accept"); String referer = request.getHeader("referer"); String userAgent = request.getHeader("user-agent"); String serverInfo = this.getServletContext().getServerInfo(); PrintWriter out = response.getWriter(); out.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">"); out.println("<HTML>"); // 这里<title></title>之间的信息在浏览器中显示为标题 out.println(" <HEAD><TITLE>Request Servlet</TITLE></HEAD>"); out.println(" <style>body, font, td, div {font-size:12px; line-height:18px; }</style>"); out.println(" <BODY>"); out.println("<b>您的IP为</b> " + remoteAddr + "<b>,位于</b> " + getAddress(remoteAddr) + "<b>;您使用</b> " + getOS(userAgent) + " <b>操作系统</b>," + getNavigator(userAgent) + " <b>。您使用</b> " + getLocale(locale) + "。<br/>"); out.println("<b>服务器IP为</b> " + localAddr + "<b>,位于</b> " + getAddress(localAddr) + "<b>;服务器使用</b> " + serverPort + " <b>端口,您的浏览器使用了</b> " + port + " <b>端口访问本网页。</b><br/>"); out.println("<b>服务器软件为</b>:" + serverInfo + "。<b>服务器名称为</b> " + localName + "。<br/>"); out.println("<b>您的浏览器接受</b> " + getAccept(accept) + "。<br/>"); out.println("<b>您从</b> " + referer + " <b>访问到该页面。</b><br/>"); out.println("<b>使用的协议为</b> " + protocol + "。<b>URL协议头</b> " + scheme + ",<b>服务器名称</b> " + serverName + ",<b>您访问的URI为</b> " + requestURI + "。<br/>" ); out.println("<b>该 Servlet 路径为</b> " + servletPath + ",<b>该 Servlet 类名为</b> " + this.getClass().getName() + "。<br/>"); out.println("<b>本应用程序在硬盘的根目录为</b> " + this.getServletContext().getRealPath("") + ",<b>网络相对路径为</b> " + contextPath + "。 <br/>"); out.println("<br/>"); out.println("<br/><br/><a href=" + requestURI + "> 点击刷新本页面 </a>"); out.println(" </BODY>"); out.println("</HTML>"); out.flush(); out.close(); } }
程序中变量referer(也就是方法request.getHeader("referer")的返回值),是指从哪个网页中单击链接到达本页。如果直接输入网址打开的本页面,则为null,Servlet配置略。运行的结果
response生成图片验证码
服务器对客户端浏览器做出的响应被封装成一个HttpServletResponse对象。要对浏览器进行操作,只需要操作HttpServletResponse对象就可以了。通过HttpServletResponse.getWriter()获得一个PrintWriter对象,该对象对OutputStream的子类。然后使用该对象输出信息就可以了。通过HttpServletResponse获取的PrintWriter对象只能写字符型的数据。如果需要在客户端写二进制数据,可以使用HttpServletResponse.getOutputStream()。方法getWriter()可以看做是方法getOutputStream()的一个封装。
本例将使用Servlet输出图片验证码。原理是服务器生成一个包含随机字符串的图片发给客户端,客户端提交数据时需要填写字符串作为验证。由于字符串保存在图片中,因此机器很难识别,从而达到防止有些人使用计算机程序恶意发送信息的目的。Servlet输出图片时,需要调用getOutputStream输出图片
package com.helloweenvsfei.servlet; import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.Random; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.sun.image.codec.jpeg.JPEGCodec; import com.sun.image.codec.jpeg.JPEGImageEncoder; public class IdentityServlet extends HttpServlet { /** * */ private static final long serialVersionUID = -479885884254942306L; public static final char[] CHARS = { '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' }; public static Random random = new Random(); public static String getRandomString() { StringBuffer buffer = new StringBuffer(); for (int i = 0; i < 6; i++) { buffer.append(CHARS[random.nextInt(CHARS.length)]); } return buffer.toString(); } public static Color getRandomColor() { return new Color(random.nextInt(255), random.nextInt(255), random .nextInt(255)); } public static Color getReverseColor(Color c) { return new Color(255 - c.getRed(), 255 - c.getGreen(), 255 - c .getBlue()); } public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("image/jpeg"); String randomString = getRandomString(); request.getSession(true).setAttribute("randomString", randomString); int width = 100; int height = 30; Color color = getRandomColor(); Color reverse = getReverseColor(color); BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g = bi.createGraphics(); g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); g.setColor(color); g.fillRect(0, 0, width, height); g.setColor(reverse); g.drawString(randomString, 18, 20); for (int i = 0, n = random.nextInt(100); i < n; i++) { g.drawRect(random.nextInt(width), random.nextInt(height), 1, 1); } // 转成JPEG格式 ServletOutputStream out = response.getOutputStream(); JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out); encoder.encode(bi); out.flush(); } public static void main(String[] args) { System.out.println(getRandomString()); } }
代码中利用一个随机数生成器Random与一个char[]类型的字符字典生成随机字符串。字符字典将比较容易混淆的0与O,1与l等都去掉了。然后生成一个长100宽30的图片,利用随机颜色填充背景,利用反色在前面绘制出随机字符,并画了最多100个位置随机的噪音点,增加图片识别的难度。
该Servlet需要配置到web.xml中,代码如下
<servlet> <servlet-name>IdentityServlet</servlet-name> <servlet-class>com.helloweenvsfei.servlet.IdentityServlet</servlet-class> </servlet> <servlet-name>IdentityServlet</servlet-name> <url-pattern>/servlet/IdentityServlet</url-pattern> </servlet-mapping>
然后直接访问该Servlet就可以预览该图片了,为了方便演示,下面用一个HTML文件引用这个图片验证码,主要代码如下
<script>
document.getElementById('btn').disabled = true; document.getElementById('identity').src='servlet/IdentityServlet?ts=' + new Date().getTime(); } </script> <img src="servlet/IdentityServlet" id="identity" onload="btn.disabled = false; " /> <input type=button value=" 换个图片 " onclick="reloadImage()" id="btn">
Servlet不仅能输出文件与图片,还能输出其他格式数据,例如Word、Excel、PDF、MP3等,只要正确设定response输出类型及输出流。不同的输出类型需要声明不同的Context-Type属性,例如JPG图片是"image/jpeg",而Word则是"application/mword"。