HTTP(HyperText Transfer Protocol,超文本传输协议)协议是Web应用所使用的最主要的协议。以浏览器为界面的Web应用程序均是以HTTP协议为基础的请求相应模式。浏览器作为客户端向服务器发送一个请求,服务器收到请求后,将响应返回给客户端。图7-1显示了浏览器访问http://www.sina.com.cn/的请求和响应。
图7-1
HTTP是一个无状态协议,浏览器和服务器的交互包括以下步骤。
① 浏览器向服务器请求建立TCP连接。
② 连接建立后,浏览器发送HTTP请求给服务器。
③ 服务器将响应内容发送给浏览器。
④ 双方关闭TCP连接。
如果服务器支持HTTP 1.1版本,则第 ②、③ 步可以多次执行,以便减少TCP连接的次数,从而提高网络效率。
HTTP请求由请求方式、URL和数据三部分构成,最常见的HTTP请求是GET请求和POST请求。
GET请求仅仅给服务器发送一个URL,可以在URL中包含参数,然后期待服务器返回相应的内容。一个完整的GET请求的URL格式如下。
http://www.livebookstore.net/listBooks.jspx
与GET请求相比,POST请求的参数不包含在URL中,而是以附加的消息体发送给服务器。POST请求的数据不会显示在浏览器的地址栏,因此用户无法看到。
由于HTTP协议是无状态的,而Web应用程序常常需要跟踪用户的身份,因此,服务器通常使用以下两种方式来保存用户状态。
(1)使用Cookie来标识用户。浏览器在第一次请求服务器时将获得服务器传递给它的Cookie,此后的请求中,浏览器将Cookie附加在请求中,服务器就可以识别出用户身份。
(2)通过URL重写的方式来跟踪用户。服务器通过将响应页面中的URL链接附加上一个特定的标识符,就可以跟踪用户身份。
对于一个用户来说,在浏览器和服务器之间的反复的请求响应被称为一个会话。由于服务器的资源是有限的,因此,会话有一个超时设置。如果用户长时间没有通过浏览器请求服务器,服务器就认为此会话结束。选择一个合适的会话超时是必要的,过短的会话会导致用户操作不便,过长的会话会导致服务器负担过重。通常,JavaEE服务器的默认会话超时(例如,30分钟)是一个比较合理的设置。
在对HTTP协议有基本了解后,我们需要了解JavaEE的两种Web组件标准:Servlet和JSP。它们是整个JavaEE Web应用程序的基础。
7.1.2 Servlet组件
Servlet组件是JavaEE中最核心的Web标准。Servlet运行于Web容器中,按照请求/ 响应模式为用户提供服务。一个典型的Servlet代码如下。
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
PrintWriter pw = response.getWriter();
pw.print("<html><boay><h1>Hello, world!</h1></body></html>");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
doGet方法和doPost方法分别对应HTTP的GET请求和POST请求。Servlet API定义了HttpServletRequest对象和HttpServletResponse对象,Web容器负责将这两个对象传递给Servlet组件,开发人员需要从HttpServletRequest对象中获取需要的参数,然后将生成的页面写入HttpServletResponse对象的输出流中,即完成了一次完整的请求/响应。
由于Servlet组件必须运行在Web容器中,因此,要运行上面的示例,必须将其部署到Web服务器上。我们以Resin服务器为例,建立一个HelloServlet项目,结构如图7-2所示。
图7-2
web目录是网站的根目录,WEB-INF目录(注意:区分大小写)存放了Web应用程序所需的全部文件,在Web应用程序运行期,服务器不允许用户直接通过URL访问WEB-INF目录下的任何文件,这就保证了服务器端代码不会被客户端所获得。在WEB-INF目录下,classes目录存放编译好的class文件,这里我们直接设置项目输出路径为HelloServlet/web/WEB-INF/classes,如图7-3所示。
图7-3
为了编译HelloServlet,需要将servlet-api.jar引入到Java Build Path中。注意,这个文件仅在编译时需要用到,在Web应用程序的运行期不需要它,因为Web服务器内置了所有的Servlet API。
web/WEB-INF/lib目录存放了所有用到的第三方库文件,在这个简单的项目中我们没有用到任何第三方库,在后面的章节中,如果用到了第三方库,都需要将其加入到工程的Java Build Path中。web.xml是整个Web应用程序最基本的配置文件。要让HelloServlet工作,需要在web.xml中定义它,并为其配置URL映射。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>helloServlet</servlet-name>
<servlet-class>example.chapter7.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>helloServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
</web-app>
在启动Resin前,请配置好环境变量JAVA_HOME和RESIN_HOME,然后编写一个resin.conf配置文件,为了方便启动,我们还编写了一个start_resin.bat批处理文件来启动Resin。读者可以导入本书配套光盘中的配置文件。
PATH=%JAVA_HOME%/bin;%PATH%
set RESIN_CONF=%CD%/resin.conf
CD web
%RESIN_HOME%/httpd -server-root %CD% -conf %RESIN_CONF%
运行start_resin.bat启动Resin,由于我们配置的HelloServlet的映射路径为/hello,因此,在浏览器地址栏中输入“http://localhost:8080/hello”,即可看到HelloServlet在浏览器中运行的效果,如图7-4所示。
图7-4
在每个HTTP请求到来时,Web服务器都会创建HttpServletRequest对象和HttpServletResponse对象来封装HTTP请求和输出,然后传递给Servlet组件处理。除了HttpServlet作为核心的Web组件用于处理HTTP请求外,Web容器还提供了ServletContext对象和Session对象来简化Web应用程序的开发。ServletContext对象在一个Web应用程序中有且仅有一个,它封装了应用程序所需的常用信息;Session对象负责管理一个会话,Web容器为每一个客户端创建一个独立的Session,Web应用程序可以将客户端的相关信息放入其各自的Session中,以便管理。
7.1.3 JSP组件
由于在Servlet中输出HTML页面极其困难且难以维护,因此,JavaEE还提供了另一种以HTML为主的JSP组件。在一个JSP页面中,大部分为HTML标签,仅用<% ... %>嵌入小部分Java代码,因此,JSP降低了网页设计的难度。
当用户请求一个JSP页面时,JSP页面首先要被Web容器转化为Servlet源代码,然后被编译为class文件并执行。通常,编译过程在首次请求时被执行。如果原始的JSP文件做了更新,则Web容器会检测到更新并自动对其重新编译。
以下是一个典型的JSP页面。
<html>
<body>
<h1><% out.print("Hello, world!"); %></h1>
</body>
</html>
和Servlet相比,JSP页面的部署就非常简单,不需要在web.xml中定义,直接在地址栏输入JSP页面的路径即可。可以从本书的配套光盘中导入HelloJsp项目,如图7-5所示。
然后启动Resin,输入“http://localhost:8080/hello.jsp”,其执行效果和7.1.2节的Servlet示例完全一样,如图7-6所示。
图7-5 图7-6
由于JSP在执行前将首先被转化为Servlet源代码,然后被编译为class文件并载入执行。第一次请求JSP时,需要一个转化和编译的过程,因此时间较长,而后续的请求就可以跳过转化和编译过程,因此速度会快得多。可以在/WEB-INF/work目录下找到由hello.jsp页面转化的Servlet源代码和编译后的class文件。
JSP和Servlet另一个不同之处在于,服务器会自动检测JSP页面的改动。若发现更改,则自动重新编译,而改动Servlet的class文件后,只有重新启动服务器或者重新加载Web应用程序后才会生效。
7.1.4 JSP标签
JSP页面的输出通常是调用隐含的out对象的print()方法,这样做会导致复杂的Java代码。为了简化输出,SUN又在JSP中引入了标签的概念。标签就是封装了一些功能并能够输出HTML片段的Java类,使用起来却和HTML代码差不多。例如,使用了JSTL(JSP Standard Tag Library)标签库的JSP页面输出“Hello, world!”和直接使用out对象的JSP页面相比,格式更简洁,但是必须在页面开头处声明使用的标签库。
<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
<html>
<body>
<h1><c:out value="Hello, world!" /></h1>
</body>
</html>
JSP标签的功能相当强大,不但可以输出HTML片段,还可以执行任意Java代码,甚至包括执行SQL语句等,但是,滥用JSP标签将造成页面逻辑混乱,给后期维护造成极大的困难,此外,自己开发定制标签库也不是一件容易的事情。我们强烈建议,对标签库的使用应该严格限制那些只用于显示输出结果的标签,不要使用诸如执行SQL查询之类的标签。那么,处理请求的逻辑应该在放在何处呢?MVC模式正是为了解决逻辑处理和显示输出相分离而提出的一种设计模式。稍后我们还将介绍MVC模式的原理和在Web应用中的实现方式。
7.1.5 Filter
从Servlet 2.3规范开始,还引入了另一个激动人心的特性——Filter(过滤器)。Filter组件有能力在请求被传递给Servlet或JSP组件处理前截获请求,通过修改HttpServletRequest或HttpServletResponse,实现诸如安全检查、定制输出等许多功能。此外,可以将多个Filter组合在一起形成过滤链,使请求能被链上的每一个Filter依次处理。
一个最简单的Filter实现如下。
public class SimpleFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
// 在此初始化Filter
}
public void destroy() {
// 在此释放资源
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 千万不要忘了调用chain.doFilter(),否则请求无法被继续处理:
chain.doFilter(request, response);
}
}
过滤器在doFilter方法中处理请求,在SimpleFilter中,我们简单地调用chain.doFilter()将请求直接传递给下一个过滤器。要特别注意,如果忘记了调用chain.doFilter(),那么请求处理就会到此结束,客户端很可能得不到任何输出。
下面的例子演示了如何利用Filter来实现安全检查。这个SecurityFilter将向客户端返回403禁止访问的错误。
public class SecurityFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {}
public void destroy() {}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
((HttpServletResponse)response).sendError(403);
// 没有调用chain.doFilter(),因为已经向客户端发送了403错误
}
}
在web.xml中,我们规定SecurityFilter将过滤/security目录下的所有资源,即访问/security目录下的任何资源,都会得到一个403禁止访问的错误。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<filter>
<filter-name>securityFilter</filter-name>
<filter-class>example.chapter7.SecurityFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>securityFilter</filter-name>
<url-pattern>/security/*.jsp</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
</web-app>
我们创建如下的SecurityFilter工程,在web目录下包含两个jsp文件,其中,security.jsp放在/security目录下,如图7-7所示。
图7-7
打开浏览器,访问/normal.jsp页面是没有问题的,如图7-8所示。
如果访问/security/security.jsp,则会得到一个403错误,这说明SecurityFilter起作用了,如图7-9所示。
图7-8 图7-9
Filter不仅能对请求实现预处理,还能定制输出。利用这个特性,可以对输出进行后处理,例如,对输出的内容进行GZip压缩,以加快网络传输。下面的GZipFilter的例子演示了如何对输出内容进行压缩,可以从本书的配套光盘中找到工程源代码。
实现定制输出的关键是对HttpServletResponse进行包装,截获所有的输出,等到过滤器链处理完毕后,再对截获的输出进行处理,并写入到真正的HttpServletResponse对象中。JavaEE框架已经定义了一个HttpServletResponseWrapper类使得包装HttpServletResponse更加容易。我们扩展这个HttpServletResponseWrapper,截获所有的输出,并保存到ByteArrayOutputStream中。
public class Wrapper extends HttpServletResponseWrapper {
public static final int OT_NONE = 0, OT_WRITER = 1, OT_STREAM = 2;
private int outputType = OT_NONE;
private ServletOutputStream output = null;
private PrintWriter writer = null;
private ByteArrayOutputStream buffer = null;
public Wrapper(HttpServletResponse resp) throws IOException {
super(resp);
buffer = new ByteArrayOutputStream();
}
public PrintWriter getWriter() throws IOException {
if(outputType==OT_STREAM)
throw new IllegalStateException();
else if(outputType==OT_WRITER)
return writer;
else {
outputType = OT_WRITER;
writer = new PrintWriter(new OutputStreamWriter(buffer, get CharacterEncoding()));
return writer;
}
}
public ServletOutputStream getOutputStream() throws IOException {
if(outputType==OT_WRITER)
throw new IllegalStateException();
else if(outputType==OT_STREAM)
return output;
else {
outputType = OT_STREAM;
output = new WrappedOutputStream(buffer);
return output;
}
}
public void flushBuffer() throws IOException {
if(outputType==OT_WRITER)
writer.flush();
if(outputType==OT_STREAM)
output.flush();
}
public void reset() {
outputType = OT_NONE;
buffer.reset();
}
public byte[] getResponseData() throws IOException {
flushBuffer();
return buffer.toByteArray();
}
class WrappedOutputStream extends ServletOutputStream {
private ByteArrayOutputStream buffer;
public WrappedOutputStream(ByteArrayOutputStream buffer) {
this.buffer = buffer;
}
public void write(int b) throws IOException {
buffer.write(b);
}
public byte[] toByteArray() {
return buffer.toByteArray();
}
}
}
然后,在GZipFilter的doFilter()方法中对输出进行GZip压缩。
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletResponse resp = (HttpServletResponse)response;
Wrapper wrapper = new Wrapper(resp);
chain.doFilter(request, wrapper);
byte[] gzipData = gzip(wrapper.getResponseData());
resp.addHeader("Content-Encoding", "gzip");
resp.setContentLength(gzipData.length);
ServletOutputStream output = response.getOutputStream();
output.write(gzipData);
output.flush();
}
gzip()方法负责将一个byte[]数组的内容进行GZip压缩。
private byte[] gzip(byte[] data) {
ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(10240);
GZIPOutputStream output = null;
try {
output = new GZIPOutputStream(byteOutput);
output.write(data);
}
catch (IOException e) {}
finally {
try {
output.close();
}
catch (IOException e) {}
}
return byteOutput.toByteArray();
}
我们在web.xml中配置GZipFilter过滤的URL为*.html,然后通过一个HTML文件进行测试,在使用GZipFilter和不使用GZipFilter的情况下,通过ieHTTPHeaders这个插件查看IE浏览器获得的服务器返回,如图7-10和图7-11所示。
图7-10 图7-11
可以看到,没有使用GZipFilter时,原始HTML文件的大小是188,535字节,使用GZipFilter压缩后的文件大小是43,318字节,压缩了大约77%,效果是非常显著的。
如果读者对于Apache的mod_rewrite熟悉就一定知道URL Rewrite在Apache中是非常强大的功能,它能很容易地使动态URL以.html的静态形式更友好地展示给用户。在JavaEE Web应用程序中,应用Filter同样可以实现这一强大功能。
事实上,Resin服务器已经自带了一个实现URL重写的Filter,不过它依赖于Resin的某些jar包。不过,我们可以从Resin的源代码中获得这个RewriteFilter的源代码,然后稍做修改,就可以立刻应用在任何JavaEE Web应用程序中。
从Resin的官方站点http://www.caucho.com/下载Resin 3.0.x的源代码,解压后将RewriteFilter.java提取出来,然后在Eclipse中建立以下UrlRewrite工程,如图7-12所示。
图7-12
我们修改RewriteFilter.java,主要将其与Resin的jar包依赖的类去掉,用StringBuffer替换com.caucho.util.CharBuffer(CharBuffer是StringBuffer的一个非线程安全版本,因此性能会稍微高一点),用org.apache.commons.logging.Log替换com.caucho.log.Log,此外,Resin的RewriteFilter是以非标准方式配置Rewrite规则的,我们将使用/WEB-INF/ rewrite.properties作为Rewrite规则的配置文件。整个RewriteFilter代码如下。
public class RewriteFilter implements Filter {
private final Log log = LogFactory.getLog(RewriteFilter.class);
private ServletContext _app;
private ArrayList<RewriteEntry> _entries = new ArrayList<RewriteEntry>();
public void init(FilterConfig config) throws ServletException {
_app = config.getServletContext();
// 读取配置文件:
File conf_file = new File(
_app.getRealPath("/WEB-INF/rewrite. properties"));
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(conf_file));
for(;;) {
String s = reader.readLine();
if(s==null)
break;
if(s.trim().equals(""))
continue;
if(s.trim().startsWith("#"))
continue;
String[] ss = s.trim().split(" ", 2);
if(ss.length!=2) {
log.warn("Invaild rule: " + s);
}
else {
log.info("Add rewrite entry: " + s);
_entries.add(new RewriteEntry(ss[0], ss[1]));
}
}
}
catch(IOException ioe) {
log.warn("Exception in init RewriteFilter.", ioe);
}
finally {
if(reader!=null) {
try {
reader.close();
}
catch(IOException e) {}
}
}
}
public void destroy() {}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain nextFilter)throws ServletException, IOException
{
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
String url = req.getRequestURI();
for (int i = 0; i < _entries.size(); i++) {
RewriteEntry entry = _entries.get(i);
Pattern pattern = entry.getRegexp();
Matcher matcher = pattern.matcher(url);
if (! matcher.find(0))
continue;
String replacement = replace(matcher, entry.getTarget());
String query = req.getQueryString();
if (query != null) {
if (replacement.indexOf('?') > 0)
replacement = replacement + '&' + query;
else
replacement = replacement + '?' + query;
}
log.info("forwarding '" + url + "' to '" + replacement + "'");
if (replacement.startsWith("/")) {
RequestDispatcher disp = _app.getRequestDispatcher(replacement);
disp.forward(request, response);
return;
}
else {
res.sendRedirect(res.encodeRedirectURL(replacement));
return;
}
}
nextFilter.doFilter(request, response);
}
private String replace(Matcher matcher, String target) {
StringBuffer cb = new StringBuffer(512);
for (int i = 0; i < target.length(); i++) {
char ch = target.charAt(i);
if (ch != '$' || i == target.length() - 1)
cb.append(ch);
else {
ch = target.charAt(i + 1);
if (ch >= '0' && ch <= '9') {
int group = ch - '0';
cb.append(matcher.group(group));
i++;
}
else if (ch == '$') {
cb.append('$');
i++;
}
else
cb.append('$');
}
}
return cb.toString();
}
public static class RewriteEntry {
private Pattern _pattern;
private String _target;
public RewriteEntry(String pattern, String target) {
_pattern = Pattern.compile(pattern);
_target = target;
}
public Pattern getRegexp() { return _pattern; }
public String getTarget() { return _target; }
}
}
URL的重写规则和Apache的完全一致,但是不支持域名的重写。为了测试RewriteFilter,我们编写了两个jsp文件,放在web目录下,然后在/WEB-INF/rewrite. properties中定义了两条URL重写规则。
^//index/-(.*)/.html$ /index.jsp?id=$1
^//hello/-(.*)/.html$ /hello.jsp?name=$1
在web.xml中配置好RewriteFilter后,启动Resin,打开浏览器测试,如图7-13和图7-14所示。
可以看到,后缀为.html的URL被自动重写为带有参数的.jsp的URL,但是JSP页面获得的URL是重写后的URL,因此不会影响JSP页面的正常运行。
这个RewriteFilter虽然简单,但是在Web应用程序中是极其有用的。使用RewriteFilter可以使开发阶段不必集成Apache即可测试Web应用程序。将来如果需要集成Apache,只需要在Apache中配置好mod_rewrite模块,然后简单地将RewriteFilter移除即可。
图7-13 图7-14
从上面3个例子可以看出,Filter可以实现对输入和输出的定制,其设计类似于AOP的切面,能非常方便地插入需要的功能而不修改核心Web组件的代码。