Top
软件从附着于电脑硬件之日起,就在不断的进行着自我完善和演变。从其使用模式的角度出发,可以简单分为单机程序和网络程序。发展到今时今日仍有大量的不依赖网络的单机程序被我们使用,如记事本、Excel、PPT、ZIP压缩等软件都是大家熟知的装机必备软件。
当电脑越来越多的参与到日常生产生活中,单机程序已经不能满足企业的需要。企业级应用要求能够最大程度的让更多的客户端参与到协同办公之中,所以依赖于网络的程序开始大力发展起来。最早的网络程序是基于主机+终端模式的,也就是整个应用中只有一台大型主机,各个操作地点都是使用一条专线与主机相连,终端不提供任何运算和界面,类似于Unix形式,所有的运算和处理都由主机来完成。主机一般处理能力非常强大,并且稳定,主要机型都是由IBM这样的大公司提供。但主机的高昂的价格以及扩展难、维护费用高等弊端并不是一般企业所能承受,所以除银行、航空订票、证券等大企业在使用以外,大多数企业开始转投CS架构的程序,即客户端服务器架构。
CS架构的发展过程经历了两层CS架构,三层CS架构以及多层CS架构的演变。
两层的CS架构是由客户端和后面的数据库组成的。数据库用于存放数据,并且使用数据库编程语言编写业务逻辑,客户端则使用VB、VC、Delphi这样的可视化编程方便的语言来开发客户端的输入输出界面。用户通过界面向服务器发送请求,服务器发回的数据则通过界面进行显示,服务器的角色就由数据库来充当。这样做的好处就是开发效率高,满足企业需求。但是这种架构存在着很大的弊端,第一是可移植性差,如当数据库从SQL Server更换为Oracle时就必须将业务逻辑用新的语言再重新编写一遍;第二则是大型系统做不了,因为客户端与数据库需要建立持续的连接,而数据库能够支持的最大连接数是有限制的。所以在2000年这样的架构流行之后,慢慢的就开始向三层CS架构转变。
三层的CS架构指的是客户端+应用服务器+数据库,即将混合在数据库端的业务逻辑从中分离出来,放入到应用服务器中,数据库只负责数据的管理、存储及检索。客户端负责界面。三层之中的应用服务器其实也是程序,类似于前面讲过的TCP、Socket编程,任何支持TCP编程的语言都可以作为应用服务器。三层CS架构的工作流程如图 – 1 所示。
用户通过GUI(图形用户界面)进行操作,然后调用客户端的通信模块,通信模块依据自定义协议将请求数据打包,通过网络发送该请求,到达应用服务器时,应用服务器同样也有一个通信模块,将收到的数据包按照协议进行拆包,调用相应的业务处理模块,处理数据,其中可能需要访问数据库来完成数据的获取,将处理完的结果再次发送给通信模块,通信模块将结果按照自定义协议进行打包,然后将数据包发送给客户端的通信模块,客户端进行拆包获取响应数据,将结果显示在界面上,更新界面上的数据显示。
这样的程序结构虽然在一定程度上降低了对数据库编程的依赖,并且能够适应大型的应用程序,但数据通信模块的增加却提升了开发的难度以及整体架构的复杂度。
为了降低三层CS架构中与通信有关的复杂度,BS架构开始成为了网络程序中一大重要的架构类型。
BS架构即Browser + Web Server + DB。其工作流程如图 – 2所示。
由于三层CS架构中,自定义协议提升了整体的复杂度,那么就将自定义协议变成标准的HTTP协议。于是客户端使用HTTP协议进行数据打包拆包的程序即各厂商依据标准开发的浏览器,Web服务器也是基于HTTP协议由一些厂商提供,如IIS,Apache等。这样基于浏览器和服务器的架构中,由于协议已被限定,所以与通信有关的数据打包拆包的过程都不用我们开发人员来编写程序,只需要考虑将HTTP协议解析出来的数据进行业务处理,以及将什么样的结果提供给响应即可。也就是开发过程中只需要考虑7,8,9这三个步骤即可。于是大大降低了网络程序的开发难度,所以这种架构得到了大量的应用。
在BS架构中,早期的Web服务器只能处理静态资源的请求,也就是无法根据请求进行计算后再生成相应的HTML内容。为了补充Web服务器的这个缺陷,于是增强服务器功能的CGI技术最早产生了。CGI(Common Gateway Interface通用网关接口)也是一种规范,可以使用不同的语言来开发,如Perl,C,Java等都可以。当客户端请求静态资源时,Web服务器会自己处理并返回,当客户端请求动态资源时,Web服务器会把请求转交给扩展程序来处理,并将扩展程序的处理结果返回给客户端。但是CGI技术开发复杂,性能较差,只要有一个请求到达,Web服务器就会单独分配一个进程来进行处理,可移植性不好,所以慢慢就由后来的Servlet技术所取代。
Servlet技术是使用Java语言开发的一套组件规范,不再像CGI技术那样需要分配单独的进程来处理请求,而是单独分配一个线程来处理,于是大大提升了处理效率。并且Java语言是跨平台的语言,也提升了Web服务器扩展程序的可移植性,已经取代了CGI技术,成为BS架构中的主流技术。所有后续的BS架构中的主流框架本质上都是基于Servlet来实现的。
组件规范是依靠一套API来实现的,也就是说开发中只要基于Sun公司提供的这套API,按照一定的规则来编写程序,那么就可以实现针对Web服务器的功能扩展。
但是组件只是对部分功能的一个实现,不能单独运行,必须放在一定的环境中才能运行。而这个针对各个组件进行管理、创建、销毁的运行环境即容器。
Servlet作为补充Web服务器功能的组件,需要依赖于Servlet容器才能运行,它的运行原理如图 – 3所示。
在浏览器中输入请求地址后,浏览器会依据IP地址及端口号找到对应的Web服务器,如果请求的是静态资源,Web服务器直接提供响应;如果请求的是动态资源,Web服务器的通信模块会将该请求传递给Servlet容器的通信模块,Servlet容器负责创建Servlet实例,并将请求中的数据解析出来传递给Servlet。在Servlet处理完数据之后,响应结果也是由容器的通信模块负责返回给Web服务器。后续的Servlet的销毁及管理都由容器来负责。
能够充当Servlet容器这个角色的有很多软件,如Tomcat、Weblogic、JBoss等。而这些Servlet容器不仅仅具备了管理Servlet组件的功能,也具备了Web服务器的一些功能,所以很多时候只要安装一个Tomcat软件就同时具备了Web服务器及Servlet容器的双重功能。
Servlet是Web服务器功能的补充,要想能够运行必须依赖Servlet容器的管理才可以,为了进行下一步的Servlet的具体开发,则需要先准备用于运行Servlet的环境,在学习过程中我们选择的是主流的Tomcat作为Web服务器及Servlet容器。
下面,简单介绍下Tomcat的安装步骤:
步骤一:下载并解压安装文件
在浏览器中输入 tomcat.apache.org,在Download菜单中选择Tomcat7.0后会得到如图- 4 所示的下载链接。
选择Core下面符合当前操作系统的版本后下载,下载后的文件为压缩文件,解压到硬盘即可。假定Tomcat解压后的路径为 c:\java\tomcat7.0
步骤二:配置环境变量
选择“我的电脑”右键 (“属性”( “高级系统设置” ( “环境变量” 用于新建及修改一些环境变量,以保证系统记录解压的Tomcat软件的位置。如图-6所示:
点击系统变量下的“新建”按钮,创建“变量名”为 CATALINA_HOME , “变量值“ 为 c:\java\tomcat7.0的环境变量后点击确定按钮,如图- 7所示。
选中系统变量中的“Path“后点击编辑,在原有内容的最前面添加 “ %CATALINA_HOME%\bin; ” 后点击确定按钮。注意:修改值的时候光标放在最前面,并且要用分号将路径与原有Path的值分开。如图- 8所示。
若安装jdk时已配置了CLASS_PATH变量,则选中系统变量中的“CLASS_PATH“后点击编辑,在原有内容的最前面添加 “ %CATALINA_HOME%\lib\servlet-api.jar; ” 后点击确定按钮。注意:修改值的时候光标放在最前面,并且要用分号将路径与原有CLASS_PATH的值分开。如图- 9所示。
至此,环境变量配置完毕。
步骤三:启动Tomcat
为了检测环境变量是否配置成功,以及Tomcat是否能够成功启动,首先进入命令行,windows系统下可以使用 “cmd“命令进入。在命令行窗口下输入“startup”命令,会得到如图 – 10 所示结果。
打开浏览器,输入 “ http://localhost:8080 ”地址后,看到如图 – 11 所示结果。
至此,看到该页面即代表Tomcat启动成功。
步骤四:关闭Tomcat
在命令提示符下输入“shutdown”命令后,Tomcat的命令提示符窗口会关闭,在浏览器中再次输入http://localhost:8080后,看不到 图- 11,代表关闭Tomcat
步骤一:编写一个实现Servlet接口或继承HttpServlet的Java类
在硬盘上新建一个文件名为“HelloServlet.java”的源文件,并编写代码如下:
- package web;
- 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 HelloServlet extends HttpServlet {
- protected void service(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {
- resp.setContentType("text/html");
- PrintWriter out = resp.getWriter();
- out.write("
Hello Servlet"
);
- out.close();
- }
- }
步骤二:使用javac命令编译源文件为字节码文件
在命令行提示符下,进入到HelloServlet.java文件所在的位置,输入如下命令:
- javac HelloServlet.java –cp c:\java\tomcat7.0\lib\servlet-api.jar
在没有报错的情况下会在java源文件的同级目录内出现HelloServlet.class的文件,代表编译成功。
步骤三:将编译完的组件打包
按照如图 – 12 所示的结构将组建打包,并新建web.xml文件,内容如下:
- xml version="1.0" encoding="UTF-8"?>
- <web-app version="2.5"
- xmlns="http://java.sun.com/xml/ns/javaee"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
- http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
- <servlet>
- <servlet-name>helloServletservlet-name>
- <servlet-class>web.HelloServletservlet-class>
- servlet>
- <servlet-mapping>
- <servlet-name>helloServletservlet-name>
- <url-pattern>/sayHiurl-pattern>
- servlet-mapping>
- web-app>
步骤四:部署
将整个firstweb文件夹拷贝到c:\java\tomcat7.0\webapps
步骤五:启动Tomcat,并访问Servlet
在命令提示符下输入“startup”命令启动Tomcat,启动成功后,打开浏览器,在地址栏中输入 “ http://localhost:8080/firstweb/sayHi “回车,得到第一个Web应用程序的运行结果。如图 – 13所示。
在刚开始进行Web应用开发的时候,经常看见页面出现404这个数字,我们一般会称之为运行产生了404错误。类似于404这个数字,还有可能在页面上看到405、500这两个数字,他们都是服务器执行完客户端的请求以后,返回给客户端的一个关于执行结果的状态编码说明。 如果在运行结果页面中没有看到期待的页面,却看到了404、405、500这样的数字,那么代表着服务器告诉客户端运行产生了错误,掌握何种错误情况产生对应的数字将有利于问题的解决。
404产生的原因为Web服务器(容器)根据请求地址找不到对应资源,以下情况都会出现404的错误提示:
具体的解决办法就是根据上面提到的4种情况,逐条进行检查。
当在浏览器中输入 http://localhost:8080/firstweb/sayHi 这个地址后,容器是如何找到 HelloServlet.class这个文件并执行的呢?
首先容器会根据firstweb这个应用名找到位于webapps下面对应的文件夹,然后根据地址中的“/sayHi”到web.xml文件中寻找与之匹配的
405这个错误的产生原因是容器找不到service方法来处理请求。以下情况容器都将视为没有找到service()方法
解决405错误的方法即检查service方法是否存在,签名(方法名、参数、返回值、异常类型)是否与覆盖的父类中的方法一致。
500的错误原因是请求的Servlet在执行service方法的过程中出现了错误,程序不能继续运行了。以下情况容器都将视为500的一种情况,而返回给客户端这个错误说明。
解决500的方法为依据上面三种情况依次进行检测,逐条排除。
HTTP协议是HyperText Transfer Protocol的缩写,即超文本传输协议。是由w3c(万维网联盟)制定的一种应用层协议,用来定义浏览器与web服务器之间如何通信以及通信的数据格式。
因为BS架构中的通信模块就是以HTTP这个协议作为标准协议的,所以对该协议有所了解可以更好的编写程序。
HTTP协议的通信过程可以分为以下四个步骤:
在这个过程中,一次请求对应一次连接,当浏览器再次发请求给服务器时,Web服务器并不知道这就是上次发请求的客户端,这也是HTTP协议的一个特点-无状态协议。这种需要时建立连接,使用结束后立即断开连接的方式使得Web服务器可以利用有限的连接为尽可能多的客户提供服务。也正是具备了这样的特点,才使得BS结构能够承载企业级应用的大量访问。
在HTTP协议控制的数据走向中,既包括客户端发送给服务器端的请求数据也包括服务器端返回给客户端的响应信息,而具有一定规范的数据格式是保证通信标准的第一要素。借助于一些浏览器的插件或者浏览器本身内嵌的功能模块,可以实现对请求数据、响应数据的抓取,熟练掌握这些数据的主要部分,可以有利于理解BS模型中的一些处理问题的方式。通常情况下的请求数据包及响应数据包结构如图 – 14所示。
请求数据包包含三个部分:
请求数据包常见结构如图 – 15 所示。
响应数据包也包含三个部分:
响应数据包常见结构如图 – 16 所示。
HTTP协议在Web容器这端主要表现为通信数据的到达以及响应数据的返回。于是Web容器将这两部分数据解释为两个对象,一个是与请求数据对应的HttpServletRequest对象,一个是与响应数据对应的HttpServletResponse对象。对于Servlet来讲,主要的业务逻辑过程就是从请求对象中获取数据,经过加工后将结果附着在响应对象中发送回客户端。
借助于HttpServletRequest对象可以实现很多操作,如读取请求行、消息头信息,取得路径信息等。使用如下代码可以实现相关功能。
- package web;
- import java.io.IOException;
- import java.io.PrintWriter;
- import java.util.Arrays;
- import java.util.Enumeration;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- public class RequestServlet extends HttpServlet {
-
- protected void service(HttpServletRequest req, HttpServletResponse resp)
- throws ServletException, IOException {
- Enumeration e = req.getHeaderNames ();
- while (e.hasMoreElements ()){
- String headerName = e.nextElement ().toString ();
- System.out.println (headerName+":"+req.getHeader (headerName));
- }
- System.out.println (req.getMethod ());
- System.out.println (req.getProtocol ());
- System.out.println (req.getRequestURI ());
- System.out.println (req.getRequestURL ());
- System.out.println (req.getServletPath ());
- }
- }
HttpServletResponse对象最主要的作用即设置给浏览器的响应内容及浏览器的解码方式,此外设置cookie及重定向也都是响应对象的功能。这些功能对应的代码实现如下所示。
- package web;
- 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 ResponseServlet extends HttpServlet {
-
- protected void service(HttpServletRequest req, HttpServletResponse resp)
- throws ServletException, IOException {
- resp.setContentType("text/html;charset=gbk");
- resp.setStatus(404);
- }
-
- }
Top
请求数据包中的请求行的第一个参数就是请求方式,是客户端向Web服务器发送请求时的意向说明,以此告知服务器该如何处理及解析提交的这些数据,而客户端提交的数据也会因为采用的提交方式不同来处理。后续的一切解析都是基于请求方式的不同而不同。在RFC标准文档中有详细的规定来说明请求方式的种类及作用。
区分请求种类也会为浏览器采用不同的缓存方式处理后续请求提供一个依据,从而提升响应速度。所以对于编程人员来讲,使用正确的请求方式是Web应用的一个重要方面。
根据RFC文档的说明,请求的种类包括如 表 – 1 所示内容。
HTTP协议中定义了的这些方法(有时也叫“动作”)用来表示对指定数据的操作。有些方法(比如HEAD, GET, OPTIONS, and TRACE) 被定义为安全方法,这些方法针对的只是信息的返回,并不会改变服务器的状态(换句话说就是这些方法不会产生副作用)。不安全的方法(例如POST, PUT and DELETE) 应该用特殊的方式向用户展示,通常是按钮而不是链接,这样就可以使用户意识到可能要负的责任(例如一个按钮带来的资金交易。)
使用GET提交数据时,form中的数据将编码到URL中。在使用上,当且仅当请求幂等(字面意思是请求任意次返回同样的结果,本质是请求本身不会改变服务器数据和状态)时使用GET。重复访问时使用GET方法请求的页面,浏览器会使用缓存处理后续请求。当编程人员正确的使用GET后,浏览器会给出很好的缓存配合,响应速度更快。
原则上,服务器端处理GET和POST请求是没有分别的。但由于数据通过不同的方法编码,需要有不同的解码机制。所以,方法变化将导致处理请求的代码变化。比如对于CGI,处理GET时通过环境变量获得参数,处理POST请求时则通过标准输入(stdin) 获得数据。
从使用经验上有如下总结:
如下情况中浏览器会发送GET类型的请求:
POST方式提交的数据是在HTTP协议的Hearder中进行传输的。在使用上,当请求会改变服务器数据或状态时(更新数据,上传文件)应该使用POST。使用POST方法的Form提交时,浏览器基于POST将产生永久改变的假设,将让用户进行提交确认。
提交数据时,Form提交的第一步是创建数据集,并根据ENCTYPE对数据集进行编码。ENCTYPE有两个值:multipart/form-data,application/x-www-form-urlencoded(默认值),前者可同时用于GET、POST,后者只用于POST。进行数据传输时,对于GET方法,数据集使用content type application/x-www-form-urlencoded编码并附在URL后面,在这种模式下,数据严格限制为ASCII码;对于POST,使用content-type编码字符集并将其构造成消息发送
服务器端处理数据时,处理POST请求时则通过标准输入(stdin) 获得数据。
从使用经验上有如下总结:
将表单的Method属性设置为POST时,浏览器会发送POST请求。
Servlet作为Web服务器的补充功能在运行时需要受到Servlet容器的管理,其运行的流程如下:
Java语言在内存当中保存的每一个字符使用的都是Unicode字符集。一个中文字在内存中使用2个字节来表示。
编码:将Unicode字符集对应的字节数组转换成某种本地字符集(比如UTF-8,GBK)对应的字节数组的过程叫做编码
解码:将某种本地字符集对应的字节数组转换成Unicode字符集对应的字节数组的过程。
如果想输出就涉及到编码问题。编码和解码使用的字符集不一致就产生了乱码问题。
out.println(“”)这行代码是编码过程。现在这个out要输出的内容使用的是Unicode字符集,于是转换为默认的iso-8859-1。之所以使用这个格式是因为tomcat默认是英文的。所以调用out.println时,容器采用默认情况下的ISO-8859-1字符集去编码带有中文的输出内容时就产生了乱码。
在service()方法中第一行的位置上添加如下代码:
- response.setContentType(“text/html;charset=UTF-8”);
其中charset可以使用其他支持中文的字符集,如GBK。setContentType()有两个作用:
使用该段代码修改默认的编码方式时,一定要保证在调用print之前编写,所以该段代码尽量放在service方法的第一行的位置。在charset之前使用的是分号隔开,如果写错,则会出现保存文件的界面,原因是浏览器不能识别消息头的值,于是让用户来处理。
能够输出带有中文内容页面的完整代码如下:
- 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 HelloServlet extends HttpServlet{
- public void service(HttpServletRequest request,
- HttpServletResponse response)throws ServletException,
- IOException{
- response.setContentType("text/html;charset=utf-8");
- //获得输出流
- PrintWriter out = response.getWriter();
- out.println(“达内科技欢迎你”);
- //关闭流
- out.close();
- }
- }
对于表单提交的数据,Servlet可以从容器构建的request对象中获取,如下两个方法可以在不同情况下获取表单数据:
主要用于获取表单中控件的数据。其中参数名一定要与客户端表单中的控件name属性相一致。所以在构建表单各元素时,name属性一定要有。而name属性和id属性的区别就在于,id属性一般是作为客户端区分控件的标识,name属性是服务器端区分各控件的标识。如果参数名写错,则该方法会返回null。
用于提供用户输入的HTML页面代码如下:
- <html>
- <head>
- head>
- <body>
- <form action="hello" method="post">
- 用户名:<input name="username"/><br/>
- <input type="submit" value="提交"/>
- form>
- body>
- html>
使用该方法获取表单数据的完整代码如下:
- package web;
- 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 HelloServlet extends HttpServlet{
- public void service(HttpServletRequest
- request,HttpServletResponse response)
- throws ServletException,IOException{
- /*
- * request.getParameter方法:
- * 1.作用:依据请求参数名获取请求参数值。
- */
- String username = request.getParameter("username");
- //处理请求
- String msg = "" +
- "你好 " + username + "
";
- response.setContentType("text/html;charset=utf-8");
- //获得输出流
- PrintWriter out = response.getWriter();
- out.println(msg);
- //关闭流
- out.close();
- }
- }
用于获取表单中一组参数名相同的控件提交的数据组,如复选框,所以该方法返回的是字符串数组。如果用户没有选择这一组同名控件的任何一个,则该方法返回null。
用于提供用户输入用户名和爱好选项的HTML页面完整代码如下:
- <html>
- <head>
- head>
- <body>
- <form action="hello" method="post">
- 用户名:<input name="username"/><br/>
- 爱好:
- 吃饭<input type="checkbox" name="hobby" value="eating" checked="checked"/>
- 睡觉<input type="checkbox" name="hobby" value="sleeping"/>
- 打豆豆<input type="checkbox" name="hobby" value="fighting"/>
- <br/>
- <input type="submit" value="提交"/>
- form>
- body>
- html>
使用该方法获取复选框值的完整代码如下:
- 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 HelloServlet extends HttpServlet{
- public void service(HttpServletRequest
- request,HttpServletResponse response) throws ServletException,IOException{
- String username = request.getParameter("username");
- String[] hobby= request.getParameterValues("hobby");
- //处理请求
- String msg = "" +
- "你好 " + username + "
";
- response.setContentType("text/html;charset=utf-8");
- PrintWriter out = response.getWriter();
- out.println(msg);
- for(int i=0;i<hobby.length;i++){
- out.println("
"
+ hobby[i] +"");
- }
- //关闭流
- out.close();
- }
- }
当表单提交时,浏览器会对表单中的中文参数值进行编码,而使用的编码是打开页面时所使用的字符集,如当前页面使用的UTF-8的字符集进行显示的,那么表单提交的数据就会以UTF-8的方式进行编码后传输,而Web服务器在默认情况下对提交的表单数据会使用ISO-8859-1的字符集来解码,编码与解码的方式不一致就产生了表单提交时的中文乱码问题。
步骤一、确保表单所在的页面按照指定的字符集打开
在HTML页面中使用meta标记可以确保浏览器按照指定的字符集进行解码页面,并限定表单提交时的数据编码方式
- <meta http-equiv="content-type" content="text/html;charset=utf-8">
在服务器端需要在调用getParameter方法读取参数之前,告诉浏览器如何解码,使用如下代码即可完成该设置:
- request.setCharacterEncoding("utf-8");
注意该方法一定要要放在所有request.getParameter方法之前。
这种方式只针对POST请求有效。
步骤一、确保表单所在的页面按照指定的字符集打开
在HTML页面中使用meta标记可以确保浏览器按照指定的字符集进行解码页面,并限定表单提交时的数据编码方式
- <meta http-equiv="content-type" content="text/html;charset=utf-8">
步骤二、完成ISO-8859-1到UTF-8格式的转换
- String username = request.getParameter(“username”);
- username = new String(username.getBytes(“iso-8859-1”),“UTF-8”);
由于GET方式提交的任何数据在服务器端一定会以ISO-8859-1的方式进行解码,所以服务器端可以先按ISO-8859-1的方式获取字节数组,在将该数组转变成UTF-8对应的字符串形式。
解决表单提交时的中文乱码问题的完整代码如下:
- 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 HelloServlet extends HttpServlet{
- public void service(HttpServletRequest request,
- HttpServletResponse response)throws
- ServletException,IOException{
- request.setCharacterEncoding("utf-8");
- String username = request.getParameter("username");
- username=new String(username.getBytes("iso-8859-1"),"utf-8");
- String msg = "" +
- "你好 " + username + "
";
- response.setContentType("text/html;charset=utf-8");
- PrintWriter out = response.getWriter();
- out.println(msg);
- out.close();
- }
- }
步骤一、将JDBC驱动(.jar文件)放到WEB-INF\lib下
原因:ClassLoader找到字节码文件,然后加载到JVM的方法区中,变成一个对象。Tomcat都有自己的类加载器,会去WEB-INF下面lib中找字节码文件。毕竟jar包中都是字节码文件。
步骤二、编写JDBC代码,需要注意异常的处理
Top
在服务器为浏览器提供响应时,回传的数据包中的状态行里面是302状态码,同时在消息头内会增加一个键值对,名称为Location,值是一个新的URL地址。当这个响应到达浏览器的时候,这一次的请求响应过程并未结束,浏览器遇见302状态码之后,会立即按照Location头信息中指定的URL地址发送新的一个请求,这样一个在接到响应后又立即发出请求的过程叫做重定向。对于客户端用户来讲,中间的变化过程不会被察觉,因为这个过程是由浏览器自动完成的。
在重定向的过程中,影响浏览器做出动作的关键点即响应中的状态码及Location这个消息头。302状态就像一道命令一样,使得浏览器做出新的一次请求,而请求的地址会从头信息中查找。由于这个新的请求动作是由浏览器发出的,所以浏览器的地址栏上的地址会变成Location消息头中的地址。
由于发回的响应信息由response对象控制,所以使用如下代码即可实现重定向的过程:
- response.sendRedirect(String url);
该方法的参数值url即Location消息头中的重定向地址。注意,该段代码后面如果还有其他代码的话也会被继续执行的。
由于重定向动作的执行者为浏览器,所以请求的地址可以是任意地址,哪怕是当前应用以外的应用;浏览器发出请求时一定会保持地址栏与目标地址的一致,所以发生重定向时可以从地址栏中看到地址的改变;由于整个跳转过程是在浏览器收到响应后重新发起请求,所以涉及到的Web组件并不会共享同一个request和response。
在图 – 1中,1和4是两个完全不同的请求,如果在1号请求中曾经携带了某些表单数据,但4号这个全新请求中则不会获取到这些表单数据,也就是两次请求涉及到的Web组件不会共享request和response。
在地址栏中输入的请求地址中,端口号之后的部分都是请求资源路径。紧跟端口号的是部署到Web服务器上的应用名(appName),紧跟应用名的则是具体的应用内的组件路径。
浏览器依据地址中的IP和端口号与Web服务器建立连接,服务器会获取到请求资源路径信息。根据端口号后面的应用名找到服务器上对应的应用。默认情况下容器会认为应用名后面的是一个Servlet,所以回到web.xml文件中所有是否有与该值匹配的
容器在进行url-pattern比对的时候是遵循一定的匹配原则的。这些原则主要有:
精确匹配
即具体资源名称与web.xml文件中的url-pattern严格匹配相等才执行。如,配置的内容如下:
- <servlet>
- <servlet-name>someServletservlet-name>
- <servlet-class>test.MyServletservlet-class>
- servlet>
- <servlet-mapping>
- <servlet-name>someServletservlet-name>
- <url-pattern>/abc.htmlurl-pattern>
- servlet-mapping>
则在地址栏中输入 http://ip:port/appName/abc.html 时,服务器就会去执行test.MyServlet这个组件,就算是在应用的根目录下的确有abc.html这个文件,也不会执行。
通配符匹配
使用“*”这个符号来匹配0个或多个字符,已达到路径的批量匹配的效果。
如配置文件中的节点为如下代码所示:
- <servlet>
- <servlet-name>someServletservlet-name>
- <servlet-class>test.MyServletservlet-class>
- servlet>
- <servlet-mapping>
- <servlet-name>someServletservlet-name>
- <url-pattern>/*
则,在地址栏中输入以下任何地址时都是匹配成功的。
- http://ip:port/appName/abc.html
- http://ip:port/appName/abc/def/ghi.html
后缀匹配
在配置url-pattern节点时,不使用斜杠开头,用“*.”开头来匹配任意多个字符的模式叫做后缀匹配。
如配置文件中的节点为如下代码所示:
- <servlet>
- <servlet-name>someServletservlet-name>
- <servlet-class>test.MyServletservlet-class>
- servlet>
- <servlet-mapping>
- <servlet-name>someServletservlet-name>
- <url-pattern>*.dourl-pattern>
- servlet-mapping>
则,在地址栏中输入以下任何地址时都是匹配成功的。
- http://ip:port/appName/abc.do
- http://ip:port/appName/abc/def/ghi.do
在这三种匹配方式中,优先级最高的是精确匹配。如果容器在使用以上原则都不能找到相匹配的资源来执行时,就按照地址到应用中查找对应的文件。此时如果找到文件则返回,找不到资源来执行就返回404错误。
Servlet作为Web应用中最核心的环节是因为这个组件不仅能接受请求,还能够为该请求提供响应,所以Servlet一般都会充当整个应用的控制器来进行请求的分发,为不同的请求找到对应的资源。于是程序中大多只需要一个Servlet完成这个分发工作即可,合并多个Servlet为一个Servlet会让程序的处理逻辑更加明确。
要想完成多个Servlet合并为一个Servlet,需要完成以下两个步骤:
修改web.xml文件,将更多的servlet配置节点删除,只保留一个节点即可,代码如下:
- xml version="1.0" encoding="UTF-8"?>
- <web-app version="2.4"
- 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
- http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
- <servlet>
- <servlet-name>someServletservlet-name>
- <servlet-class>web.SomeServletservlet-class>
- servlet>
- <servlet-mapping>
- <servlet-name>someServletservlet-name>
- <url-pattern>*.dourl-pattern>
- servlet-mapping>
- web-app>
配置完web.xml文件后,不同请求都会发送到Web.SomeServlet来处理,要想起到分发的作用,则需要分析调过来的请求中具体的请求目标是什么。使用如下代码逻辑来完成分发动作。
- package web;
- import java.io.IOException;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- public class SomeServlet extends HttpServlet{
- public void service(HttpServletRequest request,
- HttpServletResponse response) throws
- ServletException,IOException{
- //获得请求资源路径
- String uri = request.getRequestURI();
- System.out.println("uri:" + uri);
- if(uri.equals("/test/list.do")){
- System.out.println("进行员工列表的处理...");
- }else if(uri.equals("/test/add.do")){
- System.out.println("添加员工的处理...");
- }
- }
- }
Servlet容器如何创建Servlet对象、如何为Servlet对象分配、准备资源、如何调用对应的方法来处理请求以及如何销毁Servlet对象的整个过程即Servlet的生命周期。
阶段一、实例化
实例化阶段是Servlet生命周期中的第一步,由Servlet容器调用Servlet的构造器创建一个具体的Servlet对象的过程。而这个创建的时机可以是在容器收到针对这个组件的请求之后,即用了才创建;也可以在容器启动之后立刻创建实例,而不管此时Servlet是否使用的上。使用如下代码可以设置Servlet是否在服务器启动时就执行创建。
- <servlet>
- <servlet-name>someServletservlet-name>
- <servlet-class>test/SomeServletservlet-class>
- <load-on-startup>1load-on-startup>
- servlet>
- <servlet-mapping>
- <servlet-name>someServletservlet-name>
- <url-pattern>/*
配置文件中的load-on-startup节点用于设置该Servlet的创建时机。
当其中的值大于等于0时,表示容器在启动时就会创建实例
小于0时或没有指定时,代表容器在该Servlet被请求时再执行创建
正数的值越小,优先级越高,应用启动时就越先被创建。
阶段二、初始化
Servlet在被加载实例化之后,必须要初始化它。在初始化阶段,init()方法会被调用。这个方法在javax.servlet.Servlet接口中定义。其中,方法以一个ServletConfig类型的对象作为参数。ServletConfig对象由Servlet引擎负责创建,从中可以读取到事先在web.xml文件中通过
一般情况下,init方法不需要编写,因GenericServlet已经提供了init方法的实现,并且提供了getServletConfig方法来获得ServletConfig对象。
注:init方法只被执行一次。
以下代码为在servlet配置中,增加初始化参数
- xml version="1.0" encoding="UTF-8"?>
- <web-app version="2.4"
- 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
- http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
- <servlet>
- <servlet-name>someServletservlet-name>
- <servlet-class>test/SomeServletservlet-class>
- <init-param>
- <param-name>debugparam-name>
- <param-value>trueparam-valule>
- init-param>
- servlet>
- <servlet-mapping>
- <servlet-name>someServletservlet-name>
- <url-pattern>/*
-
使用以下代码可以读取Servlet配置中增加的初始化参数
- package test;
- import java.io.IOException;
- import javax.servlet.ServletConfig;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- public class SomeServlet extends HttpServlet{
- public void service(HttpServletRequest request,
- HttpServletResponse response)
- throws ServletException,IOException{
- System.out.println("SomeServlet's service...");
- ServletConfig config = getServletConfig();
- String debug = config.getInitParameter("debug");
- System.out.println("debug:" + debug);
- }
- }
阶段三、就绪
Servlet被初始化以后就处于能够响应请求的就绪状态。每个对Servlet的请求由一个ServletRequest对象代表,Servlet给客户端的响应由一个ServletResponse对象代表。当客户端有一个请求时,容器就会将请求与响应对象转给Servlet,以参数的形式传给service方法。service方法由javax.servlet.Servlet定义,由具体的Servlet实现。
阶段四、销毁
Servlet容器在销毁Servlet对象时会调用destroy方法来释放资源。通常情况下Servlet容器停止或者重新启动都会引起销毁Servlet对象的动作,但除此之外,Servlet容器也有自身管理Servlet对象的准则,整个生命周期并不需要人为进行干预。
在ServletAPI中最重要的是Servlet接口,所有Servlet都会直接或间接的与该接口发生联系,或是直接实现该接口,或间接继承自实现了该接口的类。
该接口包括以下四个方法:
在最开始制定Servlet规范时,设计者希望这套规范能够支持多种协议的组件开发,所以Servlet接口是最重要的一个接口。虽然我们写的程序中编写的Servlet都是继承自HttpServlet,但本质上都是对该接口的实现,因为HttpServlet就是针对Servlet这个接口的一个抽象的实现类。可以理解为HttpServlet是支持HTTP协议的分支的一部分。设计Servlet接口中的service方法时,也是希望该方法能够处理多种协议下的请求及响应,所以参数类型是ServletRequest,而在HttpServlet这个支持HTTP协议的分支中,service方法的参数则变成了HttpServletRequest和HttpServletResponse,这两个类分别继承于ServletRequest和ServletResponse,也就是对这两个类的一个具体协议的包装,区别是增加了很多与HTTP协议相关的使用API。
制定的这种规范在实际使用中发现,并不会扩展为HTTP协议之外,所以有了过度设计的缺陷,也为在编写HTTP协议的Web应用时添加了一些不必要的操作。
Servlet API中另一个重要的类就是GenericServlet这个抽象类,它对Servlet接口中的部分方法(init和destroy)添加了实现,使得开发时只需要考虑针对service方法的业务实现即可。
HttpServlet又是在继承GenericServlet的基础上进一步的扩展,一个是public voidinit(ServletConfig config),另一个是 public void init()。他们有如下的关系: init(ServletConfig config)方法由tomcat自动调用,它读取web工程下的web.xml,将读取的信息打包传给此参数,此方法的参数同时将接收的信息传递给GenericServlet类中的成员变量config,同时调用init()。以后程序员想重写init方法可以选择init(ServletConfig config)或者init(),但是选择init(ServletConfig config)势必会覆盖此方法已实现的内容,没有为config变量赋值,此后若是调用getServletConfig()方法返回config时会产生空指针异常的,所以想重写init(ServletConfig config)方法,必须在方法体中第一句写上 super.init(config),为了防止程序员忘记重写super.init(config)方法sun公司自动为用户生成一个public void init()的方法。GenericServlet具体的定义如下所示
- GenericServlet{
- ServletConfig config;
- public void init()
- { } //此方法什么也没做,可以说是为编程人员预留的接口
- public void init(ServletConfig config)
- {
- this.config=config;
- this.init();
- }
- getServletConfig()
- {
- return config;
- }
- }
WEB容器在启动时,它会为每个WEB应用程序都创建一个对应的ServletContext对象,它代表当前web应用,是一个全局的环境变量。该应用中的任何组件,在任何时候都可以访问到该对象,所以Servlet上下文具有唯一性。
获取该对象的方式有以下四种:
Servlet上下文的作用:
例如,以下是两个Servlet的完整代码,实现了跨Servlet的数据共享
- import java.io.IOException;
- import java.io.PrintWriter;
- import javax.servlet.*
- import javax.servlet.http.*
- public class SomeServlet extends HttpServlet {
- public void service(HttpServletRequest request,
- HttpServletResponse response)
- throws ServletException, IOException {
- response.setContentType("text/html;charset=utf-8");
- PrintWriter out = response.getWriter();
- ServletContext sctx = getServletContext();
- sctx.setAttribute("name", "Lisa");
- out.close();
- }
- }
- import java.io.IOException;
- import java.io.PrintWriter;
- import javax.servlet.*
- import javax.servlet.http.*
- public class OtherServlet extends HttpServlet {
- public void service(HttpServletRequest request,
- HttpServletResponse response)
- throws ServletException, IOException {
- response.setContentType("text/html;charset=utf-8");
- ServletContext sctx = getServletContext();
- String name = (String) sctx.getAttribute("name");
- out.close();
- }
- }
当浏览器访问服务器的通讯模块SomeServlet时,会启动一个线程T1来进行一系列的创建动作来处理这个请求。一般的web服务器的编程模型如下:
- while(flag){
- Socket s = ss.accept();
- Thread t = new Thread(s);
- t.start();
- }
如果刚好同时也有一个请求来访问SomeServlet,但是服务器只有一个servlet实例,所以服务器会启动线程T2,此时就有可能产生T1和T2同时访问someservlet的情况,如果要修改属性就会有安全隐患
使用synchronized对代码加锁即可。代码结构如下
代码
Top
Servlet技术产生以后,在使用过程中存在一个很大的问题,即为了表现页面的效果而需要输出大量的HTML标签,这些标签在Servlet中表现为一个个的字符串常量。这种输出页面的方式不仅仅增加了开发时对页面的控制难度,也不利于后期的维护。为了能够将Servlet中用于表现的功能分离出来,提高视图的开发效率,推出了JSP这种技术,主要用于将Servlet中负责显示的语句抽取出来。
JSP(Java Server Page)是Sun公司制定的一种服务器端动态页面技术的组件规范,以“.jsp”为后缀的文件中既包含HTML静态标记用于表现页面,也包含特殊的代码,用于生成动态内容。
JSP作为简化Servlet开发的一种技术,实质上最终依然要转变为Servlet才可能运行,只不过这个转变过程由Servlet容器来完成。所以遵循JSP的编写规范才能使得JSP转变为需要的Servlet。
步骤一、创建一个以“jsp”为后缀的文件
步骤二、在文件中添加用于控制页面显示的HTML代码、样式及JavaScript脚本。
步骤三、在需要动态生成的部分添加Java代码的编程逻辑
JSP页面中的HTML代码与静态的HTML页面代码没有区别,可以有基本标记,可以有用于控制样式的CSS,可以有控制页面的JS脚本。这些原本在Servlet中表现为字符串常量的内容最终依然会在JSP转换成Servlet时由系统使用out.write方法自动输出。
在JSP页面中可以添加如下两种类型的注释
- <%-- 注释内容 -->
第一种注释也叫HTML注释,可以出现在JSP页面之中,注释内容中可以包含了一些Java代码,但这些代码会被执行
第二行注释是JSP注释,不允许注释的内容出现Java代码,写了Java代码也会被忽略,不会执行。
JSP页面之中可以嵌入一些Java代码,以此动态生成页面中的一些内容。而JSP规范恰恰是针对这些嵌入的Java代码的一些约束,各种不同类型的代码的作用以及在转变为Servlet一部分时的规则都是不一样的。JSP页面中可以包含如下三种类型的Java代码:
这些Java代码可以在页面的任何位置进行编写,当他们被执行时就可以控制页面上产生可变化的内容了。
使用表达式可以方便的在JSP页面中输出运算的结果,代码形式如下所示:
- <%=3+5%>
- <%=add()%>
- <%=xx.getName()%>
- <%=xx.getName()+“abc”%>
注意:表达式结束不需要写分号。
这种形式的Java代码在转译成Servlet时,会成为service()方法中使用out.print语句的输出。
- out.print(3+5);
- out.print(add());
- out.print(xx.getName());
- out.print(xx.getName()+“abc”));
JSP小脚本可以编写Java代码段,从而实现相对较长的一段运算逻辑。这些Java代码最终都会成为Servlet中Service方法的一部分。由于HTML与Java可以进行混合使用,所以需要注意括号的匹配。
- <table>
- <%
- List
allUser = (List<User>)request.getAttribute(“users“);
- for(User u : allUser){
- %>
- <tr>
- <td> <%=u.getId()%> td>
- <td> <%=u.getName()%> td>
- tr>
- <%
- }
- %>
- table>
以上代码使用小脚本控制生成一个表格。
- public void service(…){
- out.write(“<table>”);
- List
allUser = (List<User>)request.getAttribute(“users“);
- for(User u : allUser){
- out.write(“<tr> <td>”);
- out.print(u.getId());
- out.write(“td><td>”);
- out.print(u.getName());
- out.write(“td>tr>”);
- }
- out.write(“table>”);
- }
JSP声明可以为对应的Servlet添加属性和方法。这种形式的代码使用的很少。语法规则如下:
- <%!
- //属性或方法的声明
- %>
如编写下列代码
- <%!
- public void fun(){
- //… 方法体
- }
- %>
转换为Servlet之后的结果如下:
- public class XXX_JSP extends JSPBase{
- public void fun(){
- // … 方法体
- }
- public void service(… …){
-
- }
- }
指令在JSP页面中通常起到转译成Servlet时的格式控制的作用。基本语法为:
- <%@ 指令名 属性=值 %>
常用指令包含以下三种
这些指令都有自己的属性来实现不同的控制功能。taglib指令会在讲解JSP标签时详细介绍。
page指令可以实现在JSP页面中导入要用到的Java包,也可以对页面的一些属性进行设置。
导包
- <%-- 导包 --%>
- <%@ page import=“java.util.*“%>
- <%@ page import=“java.util.*,java.sql.*“%>
使用page指令导包时,需要用到import属性。如果需要导入多个包,可以分成多条page指令来编写,也可以在一条page指令中,使用“,“逗号作为分隔来实现。注意,page指令要放在页面的最上面编写
设置response.setContentType()方法的参数值
- <%-- 设置response.setConentType方法的参数值 --%>
- <%@ page contentType=“text/html;charset=utf-8“%>
使用page指令可以设置输出内容的编码方式,这样就可以设置浏览器使用正确的解码方式来显示页面。
设置容器读取该文件时的解码方法
- <%-- 设置容器读取该文件时的解码方式 --%>
- <%@ page pageEncoding=“UTF-8“%>
为了保证页面中编写的中文能够保存,以及容器在加载文件时能正确的解码文件中的中文,需要通过page指令的pageEncoding属性来完成。这段代码可以保证页面在加载到内存时正确的解码中文。
include指令主要用于将其他页面包含在另一个页面之中。同一个应用中的很多页面都会有相同的导航头、版权声明,在每一个页面中重复编写是没有必要的,而且如果导航头发生变化时不利于所有页面的更新。如果将这样共用的部分单独做到一个文件中,只需要在不同的页面中添加引用,就可以实现页面内容的重用。
include指令的语法如下:
- <%@ include file=“header.html” %>
由于JSP页面最终要转化为一个Servlet,而作为即将转变为Servlet的一个前期版本,会有很多系统提供给页面能够直接使用的对象,这些对象的创建由系统完成,对象名也有系统命名,根据页面中要实现的功能直接使用即可。具体的隐含对象信息见 表-1。
在Servlet中需要自己创建获取的对象在JSP页面中使用表-1中的对象名即可,原有的方法通过对象来调用。
JSP页面的运行流程遵循如 图 -1 所以的过程。整个过程在Servlet容器的控制下将请求的JSP资源转变为Servlet之后,同样会经历Servlet的例化、初始化、就绪、销毁四部分的生命周期。从图中可以看到,之所以第一次请求一个JSP页面会比之后的请求响应慢一些,就是要经过转译成Servlet,编译成class这样一个过程。
JSP页面最终是要转译为一个Servlet,所以JSP页面中的HTML代码及Java代码、指令等内容都会按照固定的方式变为Servlet中的一部分。
如 图- 2 所示,用于导包的page指令最终成为import语句,page指令中的contentType属性转变为了_JSPService()方法中的response.setContentType()方法,HTML标记都会有out.write方法输出,<%%>的小脚本原封不动成为了_JSPService()方法内的一部分。当HTMl标记与小脚本混合在一起时,转变过程遵循从上至下的顺序逐行转变。
在将一个静态HTML页面转变为动态的JSP页面时,首先需要分析页面的整体结构,找到页面中不变的公共部分,如导航、页脚等信息。将静态页面的脚本拷贝到JSP页面之后,一定要添加page指令pageEncoding属性,保证页面中的中文能够被正确编码。添加page指令的import属性导入页面中需要的Java包。将页面中与目标页面不一致的地方进行修改,如修改表头与实际字段一致。最后将页面中需要动态生成的内容删除,使用小脚本的Java代码来实现运算逻辑。
Top
在Web服务器端处理用户请求的时候,会有需要多个Web组件配合才能完成的情况。一个Web组件(Servlet/JSP)将未完成的处理通过容器转交给另外一个Web组件继续完成,这个转交的过程叫做转发。
常见情况是Servlet负责获取数据,然后将数据转交给JSP进行展现。
实现转发过程可遵循以下三个步骤:
步骤一、绑定数据到request对象
在转交的过程中一定会有数据的传递,并且涉及到的Web组件都是针对同一个请求,所以利用request来保存共同处理的数据不仅仅能让数据共享,也能够随着请求响应的结束而销毁,不会占用服务器更多的资源。使用如下代码可以实现数据的绑定:
- request.setAttribute(String name,Object obj);
setAttribute()方法实现数据绑定,与其对应的还有getAttribute()方法获取绑定的数据,removeAttribute()方法移除绑定的数据。
步骤二、获得转发器
使用如下代码可以获取到转发器,用于说明转交的下一个组件的路径:
- RequestDispatcher rd = request.getRequestDispatcher(String uri);
步骤三、实现转发
使用转发器完成转发的动作,因下一个Web组件要针对同一个请求和响应继续完成后续的工作,所以在转发时要将本次的请求和响应对象作为参数传给下一个Web组件。实现代码如下所示:
- rd.forward(request,response);
其中步骤二和步骤三可以合并为一行代码:
- request.getRequestDispatcher(String uri).forward(request,response);
如图- 1所示,是客户端向服务器请求查看员工列表信息页面时服务器端的处理过程。
1.请求到达服务器(2.根据请求信息创建request和response对象(3.根据请求资源路径找到对应的Servlet执行处理(4.Servlet在处理过程中从数据库获取到结果信息(5.Servlet将结果信息绑定在request对象中(6.Servlet通知容器将request和response对象转交给list.jsp页面继续执行对请求的响应(7. list.jsp页面被调用并执行时从传递过来的request对象中获取绑定的数据生成结果页面(8.服务器将list.jsp页面的执行结果返回给客户端。
在整个的处理过程中,从ActionServlet到list.jsp的这个转换过程就是转发,也就是图示中的6这个步骤。控制这个过程的是服务器,同时ActionServlet和list.jsp共享了同一组请求和响应,两个Web组件共同协作完成了客户端的一次请求。对于客户端来讲,只发了一次请求,并且客户端浏览器也并不知道在服务器端是由两个组件配合提供的响应。
转发过程发生在服务器端,客户端只发送了一个请求,虽然请求到达服务器的指定位置后被容器控制着传到了第二个组件继续完成工作,但浏览器并不知道这个过程,所以转发之后地址栏地址不会发生变化。
转发的目的地必须是同一个应用内部的某个地址,决不能跳出应用。毕竟这个转交过程由容器实现,容器只有访问本应用的权限,而不能控制请求到达应用以外的位置。
转发过程中涉及到的所有Web组件共享同一个request对象和response对象,数据的传递和共享就依赖request对象。
注意:在forward之后的其他语句还是会继续执行完的,只要不报异常。
重定向:浏览器发送请求到容器访问A,A可以发送一个状态码302和一个Location消息头到浏览器,于是浏览器会立即向Location发新的请求。
转发:浏览器发送请求到容器访问A,A可以通知容器去调用B。转发所涉及的各个Web组件会共享同一个request和response对象;而重定向不行。
说明:当请求到达容器,容器会创建request对象和response对象。当响应发送完毕,容器会立即删除request对象和response对象。即,request对象和response对象的生存时间是一次请求与响应期间。
转发之后,浏览器地址栏的地址不变,重定向会变。转发的地址必须是同一个应用内部某个地址,而重定向没有这个限制。转发是一件事情未做完,调用另外一个组件继续做;而重定向是一件事情已经做完,再做另外一件事情。
转发和重定向的执行过程如图- 2所示:
在编写Web类型的应用时,如果程序运行发生了异常就会返回该异常的信息到客户端的浏览器上显示出来,这些信息往往暴露了服务器的一些重要结构,所以要避免异常信息输出到客户端。一般有两种方式来处理异常,编程式的异常处理和声明式的处理。
编程式的异常处理就是在程序中捕获到异常时,使用转发跳转到指定页面进行提示说明。
代码结构如下所示:
- public void service(HttpServletRequest request,
- HttpServletResponse response)
- throws ServletException, IOException {
- request.setCharacterEncoding("utf-8");
- String uri = request.getRequestURI();
- String action = uri.substring(
- uri.lastIndexOf("/"),uri.lastIndexOf("."));
- if (action.equals("/list")) {
- try {
- // 使用dao访问数据库
- //交给jsp来完成
- request.getRequestDispatcher("list3.jsp").forward(request,
- response);
- } catch (Exception e) {
- e.printStackTrace();
- //使用转发的方式来处理异常。
- request.setAttribute("error_msg","系统繁忙,稍后重试");
- request.getRequestDispatcher("error.jsp").forward(request,
- response);
- }
- }
- }
声明式的处理主要依靠容器自己来完成,即产生异常时抛出给容器,但不能让容器将这些底层信息返回给客户端,所以需要在web.xml文件中添加配置说明,通知容器在捕获到异常时应该将什么样的页面返回给客户端。一旦使用声明式处理方式,则该项目下的任意一个文件错误或异常,都会跳到指定的错误处理页面。
具体的实现步骤如下:
步骤一、在代码中捕获到异常直接抛出(注意异常类型,必须是ServletException)
代码结构如下图所示:
- public void service(HttpServletRequest request,
- HttpServletResponse response)
- throws ServletException, IOException {
- request.setCharacterEncoding("utf-8");
- String uri = request.getRequestURI();
- String action = uri.substring(
- uri.lastIndexOf("/"),uri.lastIndexOf("."));
- if (action.equals("/list")) {
- try {
- // 使用dao访问数据库
- //交给jsp来完成
- request.getRequestDispatcher("list3.jsp").forward(request,
- response);
- } catch (Exception e) {
- e.printStackTrace();
- //将异常抛给容器来处理
- throw new ServletException(e);
- }
- }
- }
- xml version="1.0" encoding="UTF-8"?>
- <web-app version="2.4"
- 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
- http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
- <servlet>
- <servlet-name>ActionServletservlet-name>
- <servlet-class>web.ActionServletservlet-class>
- servlet>
- <servlet-mapping>
- <servlet-name>ActionServletservlet-name>
- <url-pattern>*.dourl-pattern>
- servlet-mapping>
-
- <error-page>
- <exception-type>javax.servlet.ServletExceptionexception-type>
- <location>/error.jsplocation>
- error-page>
- web-app>
程序级的异常最好选择编程式的处理方法,利用转发到达指定的错误处理页面。
系统级别的异常最好选择声明式的处理方法。
在JSP页面或Servlet中需要从一个组件到另一个组件的跳转,通常以链接、表单提交、重定向、转发的形式来完成,其中对目标位置的标识即路径。
相对路径指的是相对于当前位置,为了到达目标文件要经过的路径。书写格式上不以“/“开头,如果为了退至上一级目录可以”../“开头。
在图-3中,从index.jsp访问a2.jsp,使用相对路径,具体值为“a/a2.jsp“。
从a1.jsp访问index.jsp,使用相对路径,具体值为“../index.jsp“。
绝对路径指的是,不管当前文档所在的位置在应用的哪里,都会从一个固定的点出发,然后构建到达目标文件所需要经过的路径。通常绝对路径在书写格式上以“/”开头。这个固定的点可能是应用名,也可能是应用名之后。
在图-4中,从index.jsp访问a2.jsp,使用绝对路径,非转发的情况下具体值为“/appName/a/a2.jsp“。
从a1.jsp访问index.jsp,使用绝对路径,非转发的情况下具体值为“/appName/index.jsp“。
对于以下四种常用的需要写路径的代码,在编写绝对路径时起始点是不同的,具体区别如下:
链接地址,表单提交,重定向由于都是浏览器发出的请求,为了到达指定的应用内的资源,所以斜杠后面从应用名开始写;转发位于服务器端,已经在具体的应用内部了,所以斜杠后面从应用名之后开始写。
由于Web应用的真实部署名称和测试时的名称未必一样,所以使用绝对路径时如果应用名写成定值则会带来变更的麻烦。为了获取当前应用的实际部署名称可以使用如下代码来动态获取。
- String request.getContextPath();
Top
Web应用程序使用HTTP协议作为传输数据的标准协议,而HTTP协议是无状态协议,即一次请求对应一次响应,响应结束后连接即断开,同一个用户的不同请求对于服务器端来讲并不会认为这两个请求有什么关联性,并不会以此区分不同的客户端。但实际情况中还是需要服务器端能够区分不同的客户端以及记录与客户端相关的一些数据,所以状态管理能够做到不同客户端的身份识别。
将客户端与服务器之间多次交互当做一个整体来看待,并且将多次交互中涉及的数据保存下来,提供给后续的交互进行数据的管理即状态管理。
这里的状态指的是当前的数据,管理指的是在这个多次交互的过程中对数据的存储、修改、删除。
生活中很多与状态管理类似的案例。如洗车卡记录洗车次数就是很典型的状态管理。洗车卡可以是一张记录简单次数的标示,车主每次携带卡片洗车后由商家修改,车主即可带走这张记录数据的卡片,商家不会保存任何数据,客户自己负责携带需要维护的数据。还有一种处理方式就是商家只给客户一个卡号,每次客户来洗车时自己不会记录洗车次数,只要报上卡号,商家就会从系统中找到与卡号对应的数据,修改后仍然是商家保存,客户带走的只是一个卡号这个标示。
以上两种模式都能实现洗车次数的记录,也就是数据的管理,只是各有利弊,程序中的状态管理与这个案例都采用了同样的处理原理。
状态管理的过程中重要的是数据的保存,只有存下来的数据才能在多次交互中起到记录的作用,所以可以按照管理的数据的存储方式和位置的不同来区分状态管理的模式。
如果将数据存储在客户端,每次向服务器端发请求时都将存在客户端的数据随着请求发送到服务器端,修改后再发回到客户端保存的这种模式叫做Cookie。
如果将数据存储在服务器端,并且为这组数据标示一个编号,只将编号发回给客户端。当客户端向服务器发送请求时只需要将这个编号发过来,服务器端按照这个编号找到对应的数据进行管理的这种模式叫做Session——会话。
一小段文本信息随着请求和响应,在客户端和服务器端之间来回传递。根据设定的时间来决定该段文本在客户端保存时长的这种工作模式叫做Cookie。最初服务器将信息发给客户端时是通过响应数据的Set-Cookie头信息来完成的。
如图-1所示为Cookie的生成及使用原理。
如果客户端向服务器端AddServlet发送请求,遇到创建Cookie的代码时,那么一小段文本信息就会随着response响应中的头信息被传递会客户端。如图中Set-Cookie:uname=xxx就是从服务器端传递回客户端的文本信息。当文本信息到达客户端以后,会被保存在客户端的内存或硬盘上,存在内存中会随着内存的释放而消失,存在硬盘上则会保存更长的时间。
一旦客户端存有服务器发回的文本信息,那么当浏览器再次向服务器发起请求时,如请求FindServlet这个组件,那么存储的文本信息会随着请求数据包的消息头以Cookie:uname=xxx这样的形式将文本信息发送到服务器端。只要Cookie的生命周期没有结束,那么不管是存在内存还是硬盘上的信息都会在客户端向服务器端发出请求时自动的随着消息头发送过去。
Servlet API提供了javax.servlet.http.Cookie这种类型来解释Cookie。其中存储的文本以name-value对的形式进行区分,所以创建Cookie时指定name-value对即可。这个name-value最终是以Set-Cookie这种消息头的形式跟随相应数据包到达客户端,所以要想将数据添加到消息头中需要使用response对象提供的方法。
创建Cookie的代码如下所示:
- Cookie c = new Cookie(String name,String value);
- response.addCookie( c );
代码中的第一行实现了这段name-value对的文本的创建。
代码中的第二行执行的效果就是在响应数据包中追加一个Set-Cookie的消息头。如果发送了相同name的Cookie数据,那么之前的数据会被覆盖。能够创建多少个Cookie存放在客户端与当前浏览器的种类相关。
以下代码实现了创建两个Cookie:
代码
当客户端向服务器发出请求时,服务器端可以尝试着从请求数据包的消息头中获取是否携带了Cookie信息。实现这一功能的代码如下:
- Cookie[] request.getCookies();
由于客户端是可以存放多个Cookie的,所以request提供的获取Cookie的方法的返回值是Cookie数组,如果想进一步获取某一个Cookie信息可以通过遍历数组,分别获取每一个Cookie的name和value。代码如下:
- Cookie[] cookies = request.getCookies();
- if(cookies!=null){
- for(Cookie c : cookies){
- String cookieName = c.getName();
- String cookieValue = c.getValue();
- }
- }
查询Cookie的完整代码如下:
代码
所谓Cookie的修改,本质是获取到要变更值的Cookie,通过setValue方法将新的数据存入到cookie中,然后由response响应对象发回到客户端,对原有旧值覆盖后即实现了修改。主要实现代码:
- Cookie[] cookies = request.getCookies();
- if(cookies!=null){
- for(Cookie c : cookies){
- String cookieName = c.getName();
- if(name.equals(“uname”)){
- c.setValue(“Mark”);
- response.addCookie( c );
- }
- }
其中response.addCookie(c)是非常重要的语句,如果没有这一行代码,那么就算是使用setValue方法修改了Cookie的值,但是不发回到客户端的话,也不会实现数值的改变。所以只要见到response.addCookie这行代码,即服务器端发回了带有Set-Cookie消息头的信息。
默认情况下,Cookie会被浏览器保存在内存中,此时Cookie的生命周期由浏览器决定,只要不关闭浏览器Cookie就会一直存在。
如果希望关闭浏览器后Cookie仍存在,则可以通过设置过期时间使得Cookie存在硬盘上得以保存更长的时间。
设置Cookie的过期时间使用如下代码:
- void setMaxAge(int seconds);
该方法是Cookie提供的实例方法。参数seconds的单位为秒,但精度不是很高。
seconds > 0 :代表Cookie保存在硬盘上的时长
seconds = 0 : 代表Cookie的生命时长为现在,而这一刻稍纵即逝,所以马上Cookie就等同于过了生存时间,所以会被立即删除。这也是删除Cookie的实现方式。
seconds < 0 :缺省值,浏览器会将Cookie保存在内存中。
以下代码实现了Cookie保存在硬盘上40秒:
- import java.io.IOException;
- import java.io.PrintWriter;
- import java.net.URLEncoder;
- import javax.servlet.*;
- import javax.servlet.*;
- public class AddCookieServlet extends HttpServlet {
- public void service(HttpServletRequest request,
- HttpServletResponse response)
- throws ServletException, IOException {
- response.setContentType(
- "text/html;charset=utf-8");
- PrintWriter out = response.getWriter();
- //创建cookie
- Cookie c = new Cookie("username","Lisa");
- c.setMagAge(40);
- Cookie c2 = new Cookie("city","NewYork");
- response.addCookie(c);
- response.addCookie(c2);
- out.close();
- }
- }
Cookie作为在网络传输的一段字符串文本,只能保存合法的ASCII字符,如果要保存中文需要将中文变成合法的ASCII字符,即编码。使用如下代码可以实现将中文保存到Cookie中。
- Cookie c = new Cookie("city",URLEncoder.encode("北京","utf-8"));
完整实现保存用户名和城市信息的代码如下。
代码
服务器读取客户端经过编码之后的信息时,要想能够正确显示需要将信息解码后才能输出。使用URLDecoder的decode()方法即可。实现解码的完整代码如下:
代码
客户端存储Cookie之后,并不是针对同一个应用访问任何资源时都自动发送Cookie到服务器端,而是会进行路径的判断。只有符合路径规范的请求才会发送Cookie到服务器端。
客户端在接受Cookie时会为该Cookie记录一个默认路径,这个路径记录的是添加这个Cookie的Web组件的路径。如,当客户端向 http://localhost:8080/test/file/addCookie.jsp发送请求时创建了cookie,那么该cookie的路径就是 /test/file.
只有当访问的地址是Cookie的路径或者其子路径时,浏览器才发送Cookie到服务器端。
如,Cookie的路径是 /test/file,那么如果访问的是 /test/file/a.jsp 或者 /test/file/b/c.jsp时,都会发送Cookie。
如果访问的是 /test/d.jsp,则浏览器不会发送Cookie。
设置Cookie的路径可以使用Cookie的API方法,setPath(String uri);
如以下代码就实现了设置Cookie的路径为应用的顶级目录,这样所有资源路径要么与此路径相等,要么是子路径,从而实现了客户端发送任何请求时都会发送Cookie。
- Cookie c = new Cookie(“uname”,“jack”);
- c.setPath(“/test”);
- response.addCookie(c);
Cookie由于存放的位置在客户端,所以可以通过修改设置被用户禁止。Cookie本质就是一小段文本,一小段说的是只能保存少量数据,长度是有限制的,一般为4kb左右。文本说的是只能保存字符串,不能保留复杂的对象类型数据。
作为网络中传输的内容,Cookie安全性很低,非常容易通过截取数据包来获取,在没有加密的情况下不要用于存放敏感数据。
就算是能够存放的长度很短,但作为网络中传输的内容也会增加网络的传输量影响带宽。在服务器处理大量请求的时候,Cookie的传递无疑会增加网络的负载量。
Top
服务器为不同的客户端在内存中创建了用于保存数据的Session对象,并将用于标识该对象的唯一Id发回给与该对象对应的客户端。当浏览器再次发送请求时,SessionId也会被发送过来,服务器凭借这个唯一Id找到与之对应的Session对象。在服务器端维护的这些用于保存与不同客户端交互时的数据的对象叫做Session。
如图-1 所示,浏览器第一次访问服务器时,服务器会为该客户端分配一块对象空间,并且使用不同的SID来进行标识,该标识SID会随着响应发回到客户端,且被保存在内存中。当同一个客户端再次发送请求时,标识也会被同时发送到服务器端,而服务器判断要使用哪一个Session对象内的数据时,就会根据客户端发来的这个SID来进行查找。
获得session有两种情况,要么请求中没有SID,则需要创建;要么请求中包含一个SID,根据SID去找对应的对象,但也存在找到找不到的可能。但不管哪种情况都依赖于请求中的这个唯一标识,虽然对于编程人员来讲不需要去查看这个基本不会重复、编号很长的标识,但要想获取到与客户端关联的这个session对象一定要基于请求,所以在Request类型的API中包含获取到session对象的方法,代码如下所示:
- HttpSession s = request.getSession(boolean flag);
- HttpSession s = request.getSession( );
使用第一种获取session对象的方法时,
flag = true:先从请求中找找看是否有SID,没有会创建新Session对象,有SID会查找与编号对应的对象,找到匹配的对象则返回,找不到SID对应的对象时则会创建新Session对象。所以,填写true就一定会得到一个Session对象。
flag= false:不存在SID以及按照SID找不到Session对象时都会返回null,只有根据SID找到对应的对象时会返回具体的Session对象。所以,填写false只会返回已经存在并且与SID匹配上了的Session对象。
request.getSession()方法不填写参数时等同于填写true,提供该方法主要是为了书写代码时更方便,大多数情况下还是希望能够返回一个Session对象的。
Session作为服务器端为各客户端保存交互数据的一种方式,采用name-value对的形式来区分每一组数据。向Session添加数据绑定的代码如下:
- void session.setAttribute(String name,Object obj);
获取绑定数据或移除绑定数据的代码如下:
- void session.getAttribute(String name);
- void session.removeAttribute(String name);
Session对象可以保存更复杂的对象类型数据了,不像Cookie只能保存字符串。
如果客户端想删除SID对应的Session对象时,可以使用Session对象的如下方法:
- void invalidate()
该方法会使得服务器端与该客户端对应的Session对象不再被Session容器管理,进入到垃圾回收的状态。对于这种立即删除Session对象的操作主要应用于不再需要身份识别的情况下,如登出操作。
Session会以对象的形式占用服务器端的内存,过多的以及长期的消耗内存会降低服务器端的运行效率,所以Session对象存在于内存中时会有默认的时间限制,一旦Session对象存在的时间超过了这个缺省的时间限制则认为是Session超时,Session会失效,不能再继续访问。
Web服务器缺省的超时时间设置一般是30分钟。
有两种方式可以修改Session的缺省时间限制,编程式和声明式。
编程式:
- void setMaxInactiveInterval(int seconds)
声明式:
- <session-config>
- <session-timeout>30session-timeout>
- session-config>
使用声明式来修改缺省时间,那么该应用创建的所有Session对象的生命周期都会应用这个规定的时间,单位为分钟。
使用编程式来修改缺省时间只会针对调用该方法的Session对象应用这一原则,不会影响到其他对象,所以更灵活。通常在需要特殊设置时使用这种方式。时间单位是秒,与声明式的时间单位不同。
Session既然区分不同的客户端,所以可以利用Session来实现对访问资源的保护。如,可以将资源划分为登录后才能访问。Session多用于记录身份信息,在保护资源被访问前可以通过判断Session内的信息来决定是否允许。这是依靠Session实现的验证。
使用Session实现验证的步骤如下:
步骤一、为Session对象绑定数据,代码如下:
- HttpSession s = request.getSession();
- s.setAttribute(“uname”,“Rose”);
步骤二、读取Session对象中的绑定值,读取成功代表验证成功,读取失败则跳转回登录页面。
- HttpSession s = request.getSession();
- if(s.getAttribute(“uname”)==null){
- response.sendRedirect(“logIn.jsp”);
- }else{
- //… …
- }
Session对象的数据由于保存在服务器端,并不在网络中进行传输,所以安全一些,并且能够保存的数据类型更丰富,同时Session也能够保存更多的数据,Cookie只能保存大约4kb的字符串。
Session的安全性是以牺牲服务器资源为代价的,如果用户量过大,会严重影响服务器的性能。
Session对象的查找依靠的是SID,而这个ID保存在客户端时是以Cookie的形式保存的。一旦浏览器禁用Cookie,那么SID无法保存,Session对象将不再能使用。
为了在禁用Cookie后依然能使用Session,那么将使用其他的存储方法来完成SID的保存。URL地址在网络传输过程中不仅仅能够起到标示地址的作用,还可以在其后携带一些较短的数据,SID就可以通过URL来实现保存,及URL重写。
浏览器在访问服务器的某个地址时,会使用一个改写过的地址,即在原有地址后追加SessionID,这种重新定义URL内容的方式叫做URL重写。
如:原有地址的写法为http://localhost:8080/test/some
重写后的地址写法为http://localhost:8080/test/some;jsessionid=4E113CB3
生成链接地址和表单提交时,使用如下代码:
- <a href=”<%=response.encodeURL(String url)>”>链接地址a>
如果是重定向,使用如下代码代替response.sendRedirect()
- response.encodeRedirectURL(String url);
验证码技术可以防止对于应用恶意发送数据,因其不规律且不能由机器代劳,所以一定程度上避免了恶意程序对网站的攻击。
验证码本质上是一张图片,图片内容的准确解析不容易用程序来实现,所以能避免内容被快速读取。并且,图片的内容是使用程序随机生成后绘制得到。
注册、登录这样的功能一般都会配备验证码,一定程度上避免恶意代码的攻击。
绘制验证码图片不仅仅需要随机生成要绘制的内容,同时要配合Java中与绘图有关的一套API来完成。绘制API将画板、画笔、颜料、字体等都解释为对象,绘制的过程就是这些对象互相配合完成的。主要涉及Graphics、Font等类型。
绘制图片的基本步骤如下:
以上步骤对应的实现代码如下所示:
代码
配置web.xml文件代码如下:
代码
HTML页面中添加如下代码加入该图片:
- <img src="checkcode"/>
摘要加密的特点:不适用密钥,使用摘要加密算法对明文进行加密之后会得到密文,无法反推出明文。唯一性:不同的明文有不同的密文。不可逆性:即使知道了摘要加密算法,也无法反推出明文。
代码
Top
过滤器是Servlet2.3规范之中一种特殊的Web组件,可以作为Servlet的辅助性插件存在。例如,对信息的筛选是很多Servlet里面的一个必须的前提,但是相同的功能在每个Servlet中都编写不仅仅不利于以后修改过滤逻辑,也不利于功能的重用,这时可以将这一部分非决定性的功能放在一个过滤器中,通过配置由容器控制所有请求在到达真正的处理逻辑Servlet之前先通过过滤器的检查。如果过滤逻辑需要修改,那么只需要修改这一个组件即可。所以这种可随时添加、修改,作为Servlet补充功能的Web组件是非常必要的。
编写过滤器遵循下列步骤:
步骤一、实现Filter接口
Filter是过滤器API中最核心的接口,定义一个Java类实现该接口以提供过滤逻辑。代码如下:
- import javax.servlet.Filter;
- import javax.servlet.FilterChain;
- import javax.servlet.FilterConfig;
- public class CommentFilter implements Filter{
- //… …
- }
步骤二、实现doFilter方法
Filter接口中共定义了三个方法,分别是init,doFilter,destroy方法。init方法在创建Filter时会被调用,且只调用一次,一般在该方法中做一些数据的准备工作,可以通过传入的FilterConfig参数获取在web.xml文件中配置提供给过滤器的初始参数。destroy方法只有在销毁过滤器对象时被调用一次,用于释放一些资源的操作。doFilter方法内编写过滤器的具体处理逻辑,会被多次执行。该方法共有三个参数,请求和响应用于获取数据以及追加数据,FilterChain是过滤器链,负责多过滤器的传递。
实现代码如下:
- import java.io.IOException;
- import java.io.PrintWriter;
- import javax.servlet.*;
- import javax.servlet.http.*;
- public class CommentFilter implements Filter{
- private FilterConfig config;
-
- public void destroy() {
- System.out.println("CommentFilter1's destroy...");
- }
- /*
- * 容器会调用doFilter方法来处理请求(
- * 相当于servlet的service方法)。
- * 容器会将request对象(arg0)和response对象
- * (arg1)作为参数传给doFilter方法。
- * 如果调用了FilterChain(arg2)的doFilter(request,response)方法,
- * 则容器会调用后续的过滤器或者servlet,否则请求处理完毕。
- */
- public void doFilter(ServletRequest arg0,
- ServletResponse arg1, FilterChain arg2)
- throws IOException, ServletException {
- HttpServletRequest request = (HttpServletRequest)arg0;
- HttpServletResponse response =(HttpServletResponse)arg1;
- request.setCharacterEncoding("utf-8");
- response.setContentType("text/html;charset=utf-8");
- PrintWriter out = response.getWriter();
- String content = request.getParameter("content");
- String illegalStr = config.getInitParameter("illegalStr");
- if(content.indexOf(illegalStr) != -1){
- //有敏感字
- out.println("
评论内容包含了敏感字
");
- }else{
- //没有敏感字
- // 执行FilterChain的doFilter会调用后续的过滤器或者servlet。
- arg2.doFilter(arg0, arg1);
- }
- System.out.println("Filter1's doFilter end.");
- }
-
- /*
- * FilterConfig对象可以用来访问过滤器的初始化参数。
- * init方法只会执行一次。
- */
- public void init(FilterConfig arg0) throws ServletException {
- System.out.println("CommentFilter1's init...");
- config = arg0;
- }
- }
步骤三、注册过滤器
在web.xml文件中注册过滤器,代码如下:
- xml version="1.0" encoding="UTF-8"?>
- <web-app version="2.4"
- 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
- http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
-
- <filter>
- <filter-name>filter1filter-name>
- <filter-class>web.CommentFilterfilter-class>
-
- <init-param>
- <param-name>illegalStrparam-name>
- <param-value>胡萝卜param-value>
- init-param>
- filter>
- <filter-mapping>
- <filter-name>filter1filter-name>
- <url-pattern>/commenturl-pattern>
- filter-mapping>
- web-app>
步骤四、部署过滤器
将编译后的过滤器和其他Web组件类合在一起,连同web.xml文件放进应用程序结构中即可。
如图-1所示为过滤器的执行流程。客户端发来请求后,不会直接将请求送达Servlet,而是先走过滤器1的doFilter方法中的code1,当遇到chain.doFilter()方法时,控制权交到service()方法,执行业务逻辑,但执行结束后并不会立即将响应返回给客户端,而是回到过滤器1的doFilter()方法中code2部分,如果该部分有代码就会执行,执行结束后才会将response对象返回给客户端。
从流程中可以看到,过滤器不仅仅对Servlet的执行前起到过滤作用,对于执行后同样有过滤效果。所以,过滤器是对request和response的检查。
在一个Web应用中,可以有多个过滤器,它们的优先级由位于web.xml文件中的声明顺序决定,具体是按照
- xml version="1.0" encoding="UTF-8"?>
- <web-app version="2.4"
- 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
- http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
-
- <filter>
- <filter-name>filter1filter-name>
- <filter-class>filter-class>
- filter>
- <filter>
- <filter-name>filter2filter-name>
- <filter-class>filter-class>
- filter>
- <filter-mapping>
- <filter-name>filter2filter-name>
- <url-pattern>/comment2url-pattern>
- filter-mapping>
- <filter-mapping>
- <filter-name>filter1filter-name>
- <url-pattern>/comment1url-pattern>
- filter-mapping>
- web-app>
如图-2所示,为多个过滤器的执行流程,过滤器1的doFilter的code1 ( 过滤器2的doFilter的code1 ( service()方法 ( 过滤器2的doFilter的code2 ( 过滤器1的doFilter的code2 ( 返回给客户端
在这个动作的传递过程中一定要写 chain.doFilter()
容器启动之后,会创建过滤器实例。通过init方法来完成过滤器的初始化。初始化时可以添加一些配置,提升动态性。而这些参数通过在web.xml文件中的
读取这些name-value对需要使用FilterConfig对象,从web.xml文件到FilterConfig对象的过程由容器完成,并通过参数传入到init方法之中,只需要设定一些成员对象保存数值就可以在doFilter方法中使用。
在web.xml文件中添加过滤器初始化参数的配置代码如下:
- xml version="1.0" encoding="UTF-8"?>
- <web-app version="2.4"
- 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
- http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
-
- <filter>
- <filter-name>filter1filter-name>
- <filter-class>web.CommentFilter1filter-class>
-
- <init-param>
- <param-name>illegalStrparam-name>
- <param-value>胡萝卜param-value>
- init-param>
- filter>
- <filter-mapping>
- <filter-name>filter1filter-name>
- <url-pattern>/commenturl-pattern>
- filter-mapping>
- web-app>
读取初始化参数使用如下代码:
- public class CommentFilter implements Filter{
- private FilterConfig config;
- public void init(FilterConfig arg0) throws ServletException {
- config = arg0;
- }
- public void doFilter(ServletRequest arg0,
- ServletResponse arg1, FilterChain arg2)
- throws IOException, ServletException {
- String illegalStr = config.getInitParameter("illegalStr");
- // … …
- }
- public void destroy() {
- // … …
- }
- }
方便增加或减少某个功能模块,需要添加过滤就多部署一个class修改一下web.xml文件,需要减少某个功能只要删除web.xml中对应的声明即可。
方便修改处理逻辑。当把一些功能独立到某个模块时,如果逻辑变了,只修改这一个文件并更新就可以实现功能的改变,而不用修改每一个使用这个插件的组件。
servlet规范当中定义的一种特殊的组件,用来监听servlet容器产生的事件并进行响应的处理。
容器创建或者销毁request,session,ServletContext(上下文/环境)时产生的事件(统计在线人数)。
调用了以上三个对象(request,response,ServletContext)的setAttribute,removeAttribute方法时产生的事件。
step1,写一个java类,实现相应的监听器接口(共有8个接口)。要依据监听的事件类型来选择相应的监听器接口,比如要监听session对象的创建和销毁,要实现HttpSessionListener。
step2,在监听器接口方法中,实现相应的监听处理逻辑。比如,session对象被删除了,将人数减1。
step3,注册(在web.xml文件中配置即可)。
- public class CountListener implements HttpSessionListener{
-
- private int count = 0;
-
- public void sessionCreated(HttpSessionEvent arg0){
- System.out.println("sessionCreated…");
- count ++;
- }
-
- public sessionDestroyed(HttpSessionEvent arg0){
- System.out.println("session destroyed…");
- count--;
- }
- }
- public void sessionCreated(HttpSessionEvent arg0){
- System.out.print("sessionCreated…");
- HttpSession session = args.getSession();
- ServletContext ctx = session.getServletContext();
- ctx.setAttribute("count",count);
- }
- <listener>
- <listener-class>web.CountListenerlistener-class>
- listener>
系统框架级别的代码经常需要检测容器中数据或对象的变化,以一个不受人为控制因素的触发为执行时机,所以对于需要根据数据变化来做出自动反馈的功能都可以使用到监听器.
Top
JSP的产生在一定程度上将Servlet中负责表现的功能抽取了出来,但JSP页内嵌入的Java代码也破坏了页面中负责表现的页面结构,特别是当运算逻辑稍微复杂一点的话,那么JSP页面中大量的Java代码增加了页面维护的难度。所以使用简单的标签来表现复杂的逻辑以及使用简单的形式表现运算的关系就是EL表达式和JSP标签出现的原因。
一套简单的运算规则,用于给JSTL标签的属性赋值,也可以直接用来输出而脱离标签单独使用。
EL(Expression Language)是从 JavaScript 脚本语言得到启发的一种表达式语言,它借鉴了 JavaScript 多类型转换无关性的特点。在使用 EL 从 scope 中得到参数时可以自动转换类型,因此对于类型的限制更加宽松。 Web 服务器对于 request 请求参数通常会以 String 类型来发送,在得到时使用的 Java 语言脚本就应该是request.getParameter(“XXX”) ,这样的话,对于实际应用还必须进行强制类型转换。而 EL 就将用户从这种类型转换的繁琐工作脱离出来,允许用户直接使用EL 表达式取得的值,而不用关心它是什么类型。
在JSP页面中经常要输出系统定义的对象的属性,而按照以往的写法需要自己去对象域中获取、转换再输出,使用EL表达式可以非常明显的简化过程。
Bean:指的是一个公共的类,按照固定的方式提供属性的get/set访问方式。针对这种特殊类型的属性访问使用EL表达式实现有两种方式,如下。
${user.name}
执行的过程为:从pageContext、request、session、application中依次查找绑定名为“user”的对象,找到后调用“getName”方法,将返回值输出。
假定在session中绑定了一个对象,如下:
- User obj = new User(1,“胡萝卜”);
- session.setAttribute(“user”,obj);
那么${user.name}等价于下面代码:
- <%
- User u = (User)session.getAttribute(“user”);
- out.print(u.getName();
- %>
这种繁琐的取值,转换,输出的过程就都由系统代劳了。而且表达式比以上繁琐代码更会处理null。如果没有为name属性赋过值,页面输出“”,不会输出null。如果取值时绑定名写错,如${obj.name},页面也会输出“”,而不是报空指针异常。但属性名写错会报错,如${user.naaa}.
表达式也支持属性名的动态读取,这时需要采用方式二${user[“name”]}的形式。
假定在Servlet中有如下代码:
- User obj = new User(1,”胡萝卜”);
- session.setAttribute(“user”,obj);
- session.setAttribute(“pName”,”id”);
在JSP中编写如下代码会输出“1”:
- ${sessionScope.user[“id“]}
在JSP中编写如下代码也会输出“1”:
- ${sessionScope.user[sessionScope.pName]}
如果pName在绑定时不指定id,而是name,那么这个表达式就会输出“胡萝卜“,所以这种写法可以认为是表达式中有一个变量。sessionScope.pName 等价于 session.getAttribute(“pName”)。
如果User类型的定义如下:
- package bean;
- public class User {
- private String name;
- private int age;
- private String[] interests;
- private String gender;
- public String getGender() {
- return gender;
- }
- public void setGender(String gender) {
- this.gender = gender;
- }
- public String[] getInterests() {
- return interests;
- }
- public void setInterests(String[] interests) {
- this.interests = interests;
- }
- public int getAge() {
- return age;
- }
- public void setAge(int age) {
- this.age = age;
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- }
那么对于interests这个数组属性的值可以使用如下表达式访问:
- ${user.interests[0]}
在书写表达式时,如果没有指定搜索范围,那么系统会依次调用pageContext、request、session、application的getAttribute()方法。这样不限定查找范围的代码不利于排错,所以这种取值的操作可以限定对象的查找范围。如:
- ${sessionScope.user.name}
一旦指定了对象所在的范围,那么只会在范围内查找绑定对象,不会在找不到的时候再去其他区域中查找了。
sessionScope的位置还可以填写pageScope、requestScope、applicationScope。
使用EL表达式可以单独进行运算得出结果并直接输出,如下代码所示,EL进行算术运算,逻辑运算,关系运算,及empty运算。空运算主要用于判断字符串,集合是否为空,是空或为null及找不到值时都会输出true。
- <%request.getSession().setAttribute("sampleValue", new Integer(10));%>
- ${sessionScope.sampleValue} // 显示 10
- ${sessionScope.sampleValue + 12} <br> // 显示22
- ${(sessionScope.sampleValue + 12)/3} <br> // 显示7.3
- ${(sessionScope.sampleValue + 12) /3==4} <br> // 显示 false
- ${(sessionScope.sampleValue + 12) /3>=5} <br> // 显示 true
- <input type="text" name="sample1" value="${sessionScope.sampleValue + 10}">
- // 显示值为20的 Text 控件
- ${empty null} //显示true
以下两种写法分别等价:
- ${param.username} 与 request.getParameter(“username”);
- ${paramValues.city} 与request.getParameterValues("city");
Sun 公司 Java 标准规范的 JSTL 由 apache组织负责维护。作为开源的标准技术,它一直在不断地完善。 JSTL 的发布包有两个版本: Standard-1.0 Taglib 、 Standard-1.1 Taglib ,它们在使用时是不同的。
Standard-1.0 Taglib ( JSTL1.0 )支持 Servlet2.3 和 JSP1.2 规范, Web 应用服务器 Tomcat4 支持这些规范,而它的发布也在 Tomcat 4.1.24 测试通过了。
Standard-1.1 Taglib ( JSTL1.1 )支持 Servlet2.4 和 JSP2.0 规范, Web 应用服务器 Tomcat5 支持这些规范,它的发布在 Tomcat 5.0.3 测试通过了。
将标签库对应的jar包拷贝到WEB-INF/lib目录下,以便于系统可以加载所需要的类。使用taglib指令在页面上引入标签的命名空间和前缀,帮助系统定位对应的类。
如:
- <%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
使用if标签实现属性判断后的输出。代码如下所示:
- <%
- User user = new User();
- user.setName("胡萝卜");
- user.setGender("f");
- request.setAttribute("user",user);
- %>
- 姓名:${user.name}<br/>
- 性别:
- <c:if test="${user.gender =='m'}" var="rs" scope="request">男c:if>
- <c:if test="${!rs}">女c:if>
使用choose标签简化多个if标签的判断。代码如下所示:
- <%
- User user = new User();
- user.setName("胡萝卜");
- user.setGender("x");
- request.setAttribute("user",user);
- %>
- 性别:
- <c:choose>
- <c:when test="${user.gender == 'm'}">男c:when>
- <c:when test="${user.gender =='f'}">女c:when>
- <c:otherwise>未知c:otherwise>
- c:choose>
使用forEach标签完成对集合的遍历输出。
其中items属性为要遍历的集合,var属性为每次取出来的一个对象,varStatus指定当前迭代的状态。代码如下:
- <table>
- <tr>
- <td>序号td>
- <td>姓名td>
- <td>年龄td>
- tr>
- <c:forEach items="${users}" var="u" varStatus="s">
- <tr>
- <td>${s.count}td>
- <td>${u.name}td>
- <td>${u.age}td>
- tr>
- c:forEach>
- table>
编写一个继承自SimpleTagSupport的Java类:
- import javax.servlet.jsp.tagext.SimpleTagSupport;
- public class HelloTag extends SimpleTagSupport{
-
- }
重写该类的doTag方法,在其中添加处理逻辑:
- import javax.servlet.jsp.JspException;
- import javax.servlet.jsp.JspWriter;
- import javax.servlet.jsp.PageContext;
- import javax.servlet.jsp.tagext.SimpleTagSupport;
- public class HelloTag extends SimpleTagSupport{
- private String info;
- private int qty;
- public void setInfo(String info) {
- this.info = info;
- }
- public void setQty(int qty) {
- this.qty = qty;
- }
- @Override
- public void doTag() throws JspException, IOException {
- PageContext ctx =(PageContext)getJspContext();
- JspWriter out = ctx.getOut();
- for(int i=0;i< qty;i++){
- out.println(info+"
");
- }
- }
-
- }
在WEB-INF下面新建一个tld文件,用于配置标签说明文件。代码如下:
- xml version="1.0" encoding="UTF-8" ?>
- <taglib 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 http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd" version="2.0">
- <tlib-version>1.1tlib-version>
- <short-name>c1short-name>
- <uri>http://www.tarena.com.cn/mytag
- <tag>
- <name>helloname>
- <tag-class>tag.HelloTagtag-class>
- <body-content>emptybody-content>
- <attribute>
- <name>infoname>
- <required>truerequired>
- <rtexprvalue>falsertexprvalue>
- attribute>
- <attribute>
- <name>qtyname>
- <required>truerequired>
- <rtexprvalue>truertexprvalue>
- attribute>
- tag>
- taglib>
标签在页面中被引用时的代码如下:
- <%@taglib uri="http://www.tarena.com.cn/mytag" prefix="c1" %
- //… …
- <c1:hello info="hello kitty" qty="${1+5}"/>
容器依据JSP页面中的uri找到tld文件(依据标签中的