JSP&Servlet学习笔记(二)

一、Servlet

  JSP的本质就是Servlet,而Web容器会将JSP编译成对应的Servlet。自MVC规范出现后,Servlet的责任开始明确下来,仅仅作为控制器使用,不再需要生成页面标签,也不再作为视图层角色使用。

1.1 Servlet的开发

  Servlet是运行在服务器端的程序,用于处理及响应客户端的请求。Servlet是个特殊的Java类必须继承HttpServlet,每个Servlet可以响应客户端的请求。Servlet提供不同的方法用于响应客户端请求:

  • doGet:用于响应客户端的GET请求
  • doPost:用于响应客户端的POST请求
  • doPut&doDelete:基本不用

  事实上,客户端的请求通常只有GET和POST两种,Servlet为了响应这两种请求,必须重写doGet()和doPost()两个方法。

  大部分时候,Servlet对于所有请求的响应都是完全一样的。此时,可以采用重写一个方法来替代上面的几个方法:只需重写service()方法即可响应客户端的所有请求。

另外,HttpServlet还包含两个方法:

  • init(ServletConfig config):创建Servlet实例时,调用该方法的初始化Servlet资源
  • destroy():销毁Servlet实例时,自动调用该方法的回收资源。

通常无须重写上面的两个方法,除非需要在初始化Servlet时,完成某些资源初始化的方法,才考虑冲洗init方法。如果需要在销毁Servlet之前,先完成某些资源的回收,比如关闭数据库连接等,才需要重写destroy方法。

注意:如果重写init()方法,则应该在重写该方法的第一行调用super.init(config)。

Servlet和JSP的区别在于:

  • Servlet中没有内置对象,原来JSP的内置对象都必须有程序显示创建
  • 对于静态的HTML标签,Servlet都必须使用页面输出流逐行输出

1.2 Servlet的配置

  编辑好的Servlet源文件并不能响应用户请求, 还必须将其编译成class文件,如果需要直接使用javac命令编译Servlet类,则必须将Servlet API接口和类添加到系统的CLASSPATH环境变量里。也就是将Tomcat安装目录下的servlet-api.jar和jsp-api.jar添加到CLASSPATH环境变量中,当然使用IDE这些都会自动完成了。

  为了让Servlet能响应用户请求,还必须将Servlet配置在Web应用中。配置Servlet时需要修改web.xml文件。

从Servlet3.0开始,配置Servlet有两种方式:

  • 在Servlet类中使用@WebServlet注解进行配置
  • 通过在web.xml文件中进行配置

开发Servlet类时使用了@WebServlet注解修饰该Servlet,其有如下常用的属性(全都是可选的):

  • name:指定该Servlet的名称
  • urlPatterns / value:这两个属性的作用完全相同,都是指定该Servlet处理的URL
  • asyncSupported:指定该Servlet是否支持异步操作模式
  • display:指定该Servlet的显示名
  • initParams:用于为该Servlet配置参数
  • loadOnStartup:用于将该Servlet配置成load-on-startup的Servlet

如果打算使用注解来配置Servlet,有两点需要指出:

  1. 不要在web.xml文件的根元素()中指定metadata-complete=“true”
  2. 不要在web.xml文件中配置该Servlet

如果打算使用web.xml文件来配置Servlet,则需要配置如下两个部分:

  • 配置Servlet的名字:对应web.xml文件中的元素
  • 配置Servlet的URL:对应web.xml文件中的元素。这一步是可选的,但如果没有为是Servlet配置URL,则该Servlet不能响应用户请求


	
	firstServlet
	
	lee.FirstServlet



	
	firstServlet
	
	/aa

1.3 JSP/Servlet的声明周期

  JSP本质是Servlet,JSP页面将有Web容器编译成对应的Servlet,当Servlet容器中运行时,其实例的创建及销毁等都不是由程序员决定的,而是有Web容器进行控制的。

创建Servlet实例有两个时机:

  • 客户端第一次请求某个Servlet时,系统创建该Servlet的实例:大部分的Servlet都是这种Servlet
  • Web应用启动时立即创建Servlet实例,即load-on-startup Servlet

JSP&Servlet学习笔记(二)_第1张图片

1.4 load-on-startup Servlet

  这总方式的Servlet在Web应用启动时立即创建Servlet实例,通常是用于某些后台服务的Servlet,或者需要拦截很多请求的Servlet;这种Servlet通常作为应用的基础Servlet使用,提供重要的后台服务。

配置load-on-startup的Servlet有两种方式:

  • 在web.xml文件中通过元素的子元素进行配置
  • 通过@WebServlet注解的loadOnStartup属性指定

元素或loadOnStartUp属性都只接收一个整型值,这个整型值越小,Servlet就越优先实例化。

示例:


	
	timerServlet
	
	lee.TimerServlet
	
	1

1.5 访问Servlet的配置参数

  配置Servlet时,还可以增加额外的配置参数。通过使用配置参数,可以实现提供更好的可移植性,避免将参数以硬编码方式写在程序代码中。

为Servlet配置参数有两种方式:

  • 通过@WebServlet的initParams属性来指定
  • 通过在web.xml文件的元素中添加子元素来指定

示例:

@WebServlet(name = "testServlet", urlPatterns = { "/testServlet" }, initParams = {
		@WebInitParam(name = "driver", value = "com.mysql.jdbc.Driver"),
		@WebInitParam(name = "url", value = "jdbc:mysql://localhost:3306/javaee"),
		@WebInitParam(name = "user", value = "root"),
		@WebInitParam(name = "pass", value = "32147") })

访问Servlet配置参数通过ServletConfig独享完成,ServletConfig提供getInitParameter()来获取初始化参数,如下:

// 获取ServletConfig对象
ServletConfig config = getServletConfig();
// 通过ServletConfig对象获取配置参数:dirver
String driver = config.getInitParameter("driver");
// 通过ServletConfig对象获取配置参数:url
String url = config.getInitParameter("url");
// 通过ServletConfig对象获取配置参数:user
String user = config.getInitParameter("user");
// 通过ServletConfig对象获取配置参数:pass
String pass = config.getInitParameter("pass");

  ServletConfig获取配置参数的方法和ServletContext获取配置参数的方法完全一样,只是ServletConfig是获得当前Servlet的配置参数,而ServletContext是获取整个Web应用的配置参数。
在web.xml中为Servlet配置参数(与上面的功能相同):


	
	testServlet
	
	lee.TestServlet
	
	
		driver
		com.mysql.jdbc.Driver
	
	
	
		url
		jdbc:mysql://localhost:3306/javaee
	
	
	
		user
		root
	
	
	
		pass
		32147
	

1.6 使用Servlet作为控制器

使用Servlet作为表现层有如下三个劣势:

  1. 开发效率低,所有的HTML标签都需使用页面输出流完成
  2. 不利于团队协作开发,美工人员无法参与Servlet界面的开发
  3. 程序可维护性差,即使修改一个按钮的标题,都必须重新编辑Java代码,并重新编译

  在标准的MVC模式中,Servlet仅作为控制器使用。Java EE应用框架正式遵循MVC模式的,对于遵循MVC模式的Java EE应用而言,JSP仅作为表现层(View)技术,其作用有两点:

  1. 负责收集用户请求参数
  2. 将应用的处理结果、状态数据呈现给用户

  Servlet则仅充当控制器(Controller)角色,它的作用类似于调度员:所有用户请求都发送给Servlet,Servlet调用Model来处理用户请求,并调用JSP来呈现处理结果;或者Servlet直接调用JSP将应用的状态数据呈现给用户。

二、JSP2的自定义标签

JSP2规范简化了标签的开发,在JSP2中开发标签库只需要如下几个步骤:

  1.  开发自定义标签处理类
  2.  建立一个*.tld文件,每个*.tld文件对应一个标签库,每个标签库可包含多个标签
  3. 在JSP文件中使用自定义标签

  标签库是非常重要的技术,通常来说,初学者、普通开发人员都不会编写自己的标签库,因为像MVC框架,如Struts2、SpringMVC、JSF等都提供了丰富的自定义标签。

2.1 开发自定义标签类

自定义标签类应该继承一个父类:javax.servlet.jsp.tagext.SimpleTagSupport,除此之外,JSP自定义标签类还有如下要求:

  • 如果标签类包含属性,每个属性都有对应的getter和setter方法
  • 重写doTag()方法,这个方法负责生成页面内容
public class HelloWorldTag extends SimpleTagSupport {
	// 重写doTag()方法,该方法为标签生成页面内容
	public void doTag() throws JspException, IOException {
		// 获取页面输出流,并输出字符串
		getJspContext().getOut().write("Hello World " + new java.util.Date());
	}
}

2.2 建立TLD文件

  TLD是Tag Library Definition的缩写,即标签库定义,文件的后缀是tld,每个TLD文件对应一个标签库,一个标签库中可包含多个标签。TLD文件也称为标签库定义文件。标签库定义文件的根元素是taglib,他可以包含多个tag子元素,每个tag子元素都定义了一个标签。通常可以到Web容器下复制一个标签库定义文件,在其基础上进行修改即可。例如Tomcat中,在webapps\examples\WEB-INF\jsp2路径下包含了一个示例文件。
  我们可以将tld文件复制到Web应用的WEB-INF路径下(可以是任意子路径下)。

注意:Tomcat 8 所带的示例的TLD文件的根元素所采用的的是标签库2.0规范。

标签库2.0根元素:

标签库2.1根元素:

taglib下有如下三个子元素:

  • tlib-version:指定该标签库实现的版本,这是一个作为标识的内部版本号,对程序没有太大的作用
  • short-name:该标签库的默认短名,该名称通常也没有太大的用处
  • uri:这个属性非常重要,它指定该标签库的URI,相当于指定该标签库的唯一标识。JSP页面中使用标签库时就是根据该URI属性来定位标签库的

taglib元素下可以包含多个tag元素,每个tag元素定义一个标签,tag元素下允许出现如下常用子元素:

  • name:该标签的名称,这个子元素很重要,JSP页面中就是根据该名称来使用此标签的
  • tag-class:指定标签的处理类
  • body-content:这个子元素也很重要,它指定标签体内容。其值可以为:
    • tagdependent:指定标签处理类自己负责处理标签体
    • empty:指定该标签只能作用空标签使用
    • scriptless:指定该标签的标签体可以是静态HTML元素、表达式语言,但不允许出现JSP脚步
    • JSP:指定该标签的标签体可以使用JSP脚本

注:JSP2规范中body-content元素的值不可以是JSP。

注:标签库文件放在Web应用的WEB-INF路径或任意子路径下,Java Web规范会自动加载该文件,则该文件定义的标签库也将生效。

2.3 使用标签库

在JSP页面中确定指定的标签需要两点:

  1. 标签库URI:确定使用哪个标签库
  2. 标签名:确定使用哪个标签

使用标签库分成以下两个步骤:

  1. 导入标签库:使用taglib编译指令导入标签库,就是将标签库和指定前缀关联起来
  2. 使用标签:在JSP页面中使用标签

2.3.1 导入标签库

导入时的语法格式如下:

<%@ taglib uri="tagLibUri" prefix="tagPrefix"%>

其中uri属性指定标签库的URI,这个URI可以确定一个标签库,这个URI就是TLD文件中定义的uri属性对应。而prefix属性指定标签库前缀,即所有使用该前缀的标签将由此标签库处理。

2.3.2 使用标签库

使用标签的语法格式如下:



如果该标签没有标签体,则可以使用如下语法格式:


注:tagprefix其实就是上面定义的prefix属性所定义的值。

示例:

标签库类定义在上面,在tld中定义如下:


	1.0
	mytaglib
	
	http://www.crazyit.org/mytaglib

	
	
		
		helloWorld
		
		lee.HelloWorldTag
		
		empty
	

JSP中导入标签库:


<%@ taglib uri="http://www.crazyit.org/mytaglib" prefix="mytag"%>

JSP中使用标签:

2.4 带属性的标签

  前面的简单标签既没有属性,也没有标签体,用法和功能比较简单。需要注意的是带属性标签必须为每个属性提供对应的setter和getter方法,而带属性标签的配置方法与简单标签也略有差别。示例如下:

自定义带属性的标签类:

public class QueryTag extends SimpleTagSupport {
	// 定义成员变量来代表标签的属性
	private String driver;
	private String url;
	private String user;
	private String pass;
	private String sql;

	// 执行数据库访问的对象
	private Connection conn = null;
	private Statement stmt = null;
	private ResultSet rs = null;
	private ResultSetMetaData rsmd = null;
	
	//省略各成员变量的setter和getter方法
	...

	public void doTag() throws JspException, IOException {
		try {
			// 注册驱动
			Class.forName(driver);
			// 获取数据库连接
			conn = DriverManager.getConnection(url, user, pass);
			// 创建Statement对象
			stmt = conn.createStatement();
			// 执行查询
			rs = stmt.executeQuery(sql);
			rsmd = rs.getMetaData();
			// 获取列数目
			int columnCount = rsmd.getColumnCount();
			// 获取页面输出流
			Writer out = getJspContext().getOut();
			// 在页面输出表格
			out.write("");
			// 遍历结果集
			while (rs.next()) {
				out.write("");
				// 逐列输出查询到的数据
				for (int i = 1; i <= columnCount; i++) {
					out.write("");
				}
				out.write("");
			}
		} catch (ClassNotFoundException cnfe) {
			cnfe.printStackTrace();
			throw new JspException("自定义标签错误" + cnfe.getMessage());
		} catch (SQLException ex) {
			ex.printStackTrace();
			throw new JspException("自定义标签错误" + ex.getMessage());
		} finally {
			// 关闭结果集
			try {
				if (rs != null)
					rs.close();
				if (stmt != null)
					stmt.close();
				if (conn != null)
					conn.close();
			} catch (SQLException sqle) {
				sqle.printStackTrace();
			}
		}
	}
}

  该标签类包含了5个属性,而为标签处理类定义成员变量即可代表标签的属性,而且程序需要为这5个属性提供setter()和getter()方法。该标签输出的内容依然由doTag()方法决定,该方法会根据SQL语句查询数据库,并将查询结果显示在当前页面中。

  对于有属性的标签,需要在tld中为元素增加子元素,每个定义一个标签属性。 子元素通常还需要指定如下几个子元素:

  • name:设置属性名,子元素的值是字符串内容
  • required:设置该属性是否为必须属性,该子元素的值时true或false
  • fragment:设置该属性是否支持JSP脚本、表达式等动态内容,子元素的值true或false

为了配置上面的QueryTag标签,需要在tld文件中增加如下配置片段:


	
	query
	
	lee.QueryTag
	
	empty
	
	
		driver 
		true
		true
	
	
	
		url 
		true
		true
	
	
	
		user 
		true
		true
	
	
	
		pass 
		true
		true
	
	
	
		sql 
		true
		true
	

配置完毕后,就可在页面中使用标签了,先导入标签库,然后使用标签,代码如下:

<%@ taglib uri="http://www.crazyit.org/mytaglib" prefix="mytag"%>

......

2.5 带标签体的标签(重点)

  带标签体的标签,可以在标签内嵌入其他内容(包括静态的HTML内容和动态的JSP内容),通常用于完成一些逻辑运算。示例如下:

标签处理类:

public class IteratorTag extends SimpleTagSupport {
	// 标签属性,用于指定需要被迭代的集合
	private String collection;
	// 标签属性,指定迭代集合元素,为集合元素指定的名称
	private String item;

	// 省略get与set...
	// 省略get与set...
	// 标签的处理方法,标签处理类只需要重写doTag()方法
	public void doTag() throws JspException, IOException {
		// 从page scope中获取名为collection的集合
		Collection itemList = (Collection) getJspContext().getAttribute(collection);
		// 遍历集合
		for (Object s : itemList) {
			// 将集合的元素设置到page范围内
			getJspContext().setAttribute(item, s);
			// 输出标签体
			getJspBody().invoke(null);
		}
	}

	// 标签的处理方法,标签处理类只需要重写doTag()方法
	public void doTag() throws JspException, IOException {
		// 从page scope中获取名为collection的集合
		Collection itemList = (Collection) getJspContext().getAttribute(collection);
		// 遍历集合
		for (Object s : itemList) {
			// 将集合的元素设置到page范围内
			getJspContext().setAttribute(item, s);
			// 输出标签体
			getJspBody().invoke(null);
		}
	}
}

  该标签处理类的doTag()方法首先从page范围内获取了指定名称的Collection对象,然后遍历Collection对象的元素,每次遍历都调用getJspBody()方法,该方法返回该标签所包含的标签体:JSPFragment对象,执行该对象的invoke()方法,即可输出标签体内容。该标签的作用是:遍历指定集合,每遍历一个集合元素,技术处标签体一次。

  因为该标签的标签体不为空,配置该标签时指定body-content为scriptless,该标签的配置代码如下:


	
	iterator
	
	lee.IteratorTag
	
	scriptless
	
	
		collection 
		true
		true
	
	
	
		item 
		true
		true
	

  上面的配置指定该标签的标签体可以是静态HTML内容,也可以是表达式语言,但不允许出现JSP脚本。下面是测试代码:(导入代码省略)


	

带标签体的标签-迭代器标签


<% //创建一个List对象 List a = new ArrayList(); a.add("疯狂Java"); a.add("www.crazyit.org"); a.add("www.fkit.org"); //将List对象放入page范围内 pageContext.setAttribute("a", a); %>
"); out.write(rs.getString(i)); out.write("
${pageScope.item}

测试结果如下:

JSP&Servlet学习笔记(二)_第2张图片
  从输出结果看,使用iterator标签遍历集合元素比使用JSP脚本遍历集合元素要优雅得多,这就是自定义标签的魅力。实际上JSTL标签库提供了一套功能强大的标签,如果有现成的轮子就没必要再造轮子了。

  可能你有所疑惑,这个JSP页面自己先把多个字符串添加到ArrayList,然后再使用这个iterator标签进行迭代输出,好像没有意义。实际上这个标签的用处非常大,在严格的MVC规范下,JSP页面只负责显示数据——而数据通常由控制器(Servlet)放入request范围内,而JSP页面就通过iterator标签迭代出输出request范围内的数据。

2.6 以页面片段作为属性的标签(暂不理解)

  JSP2规范的自定义标签还允许直接将一段“页面片段”作为属性,这种方式给自定义标签提供了更大的灵活性。以“页面片段”为属性的标签与与普通标签区别并不大,只有两个简单的改变:

  1.  标签处理类中定义类型为JSPFragment的属性,该属性代表了“页面片段”
  2. 使用标签库,通过动作指令为标签的属性指定值

定义一个标签处理类,示例:

public class FragmentTag extends SimpleTagSupport {
	private JspFragment fragment;
	//省略fragment的setter和getter方法
	@Override
	public void doTag() throws JspException, IOException {
		JspWriter out = getJspContext().getOut();
		out.println("
"); out.println("

下面是动态传入的JSP片段

"); // 调用、输出“页面片段” fragment.invoke(null); out.println("

  上面的程序中定义了fragment成员变量,该成员变量代表了使用该标签时的“页面片段”,配置该标签与配置普通标签并无任何区别,增加如下配置片段即可。


	
	fragment
	
	lee.FragmentTag
	
	empty
	
	
		fragment
		true
		true
	

  这个自定义标签并没有任何特别之处,就是一个普通的带属性标签,该标签的标签体为空。由于该标签需要一个fragment属性,该属性的类型为JspFragment,因此使用该标签时需要使用动作指令来设置属性值,如下所示:


<%@ taglib uri="http://www.crazyit.org/mytaglib" prefix="mytag"%>
.....

	
<%-- 使用jsp:attribute标签传入fragment参数(该注释不能放在fragment内) -->
	<%-- 下面是动态的JSP页面片段 --%>
	



<%-- 下面是动态的JSP页面片段 --%> ${pageContext.request.remoteAddr}

注意:由于程序指定了fragment标签的标签体为empty,因此程序中fragment开始标签和fragment结束标签之间只能使用子元素,不允许出现其他内容,甚至连注释都不允许。

2.7 动态属性的标签(终点)

  前面的标签的属性个数是确定的,绝大部分情况下这种带属性的标签能处理得很好,但在某些特殊情况下,需要传入自定义标签的属性个数是不确定的,属性名也不确定,这就需要借助于动态属性的标签了。

动态属性标签比普通标签多了两个额外需求:

  1.  标签处理类还需要实现DynamicAttributes接口
  2.  配置标签时通过子元素指定该标签支持动态属性

动态属性标签的处理类,示例:

public class DynaAttributesTag extends SimpleTagSupport implements DynamicAttributes {
	// 保存每个属性名的集合
	private ArrayList keys = new ArrayList();
	// 保存每个属性值的集合
	private ArrayList values = new ArrayList();

	@Override
	public void doTag() throws JspException, IOException {
		JspWriter out = getJspContext().getOut();
		// 此处只是简单地输出每个属性
		out.println("
    "); for (int i = 0; i < keys.size(); i++) { String key = keys.get(i); Object value = values.get(i); out.println("
  1. " + key + " = " + value + "
  2. "); } out.println("
"); } @Override public void setDynamicAttribute(String uri, String localName, Object value) throws JspException { // 添加属性名 keys.add(localName); // 添加属性值 values.add(value); } }

  上面的标签处理类实现了DynamicAttributes接口,就是动态属性标签处理类必须实现的接口,实现该接口必须实现setDynamicAttribute()方法,该方法用于为该标签处理类动态地添加属性名和属性值。标签处理类使用ArrayList类型的Keys属性来保存标签的所有属性名,使用ArrayList类型的values属性来保存标签的所有属性值。
  配置该标签时需要额外地指定子元素,表明该标签时带动态属性的标签,如下:



	dynaAttr
	lee.DynaAttributesTag
	empty
	
	true

  一旦定义了动态属性的标签,接下来在页面中使用该标签时将十分灵活,完全可以为该标签设置任意的属性,如下:


<%@ taglib uri="http://www.crazyit.org/mytaglib" prefix="mytag"%>
...

下面显示的是自定义标签中的内容

指定两个属性


指定四个属性

三、Filter

  Filter主要用于对用户请求进行预处理,也可对HttpServletResponse进行后处理,是个典型的处理链。Filter也可以对用户请求生成响应,这一点与Servlet相同,但实际上很少会使用Filter向用户请求生成响应。使用Filter完整的流程是:Filter对用户请求进行预处理,接着将请求交给Servlet进行处理并生成响应,最后Filter再对服务器响应进行后处理。

Filter有如下几个用处:

  • 在HttpServletRequest到达Servlet之前,拦截客户的HttpServletResponse
  • 根据需要检查HttpServletRequest,也可以修改HttpServletRequest头和数据
  • 在HttpServletResponse到达客户端之前,拦截HttpServletResponse
  • 根据需要检查HttpServletResponse,也可以修改HttpServletResponse头和数据

Filter有如下几个种类:

  • 用户授权的Filter:Filter负责检查用户请求,根据请求过滤用户非法请求
  • 日志Filter:详细记录某些特殊的用户请求
  • 负责解码的Filter:包括对非标准编码的请求解码
  • 能改变XML内容的XSLT Filter等
  • Filter可负责拦截多个请求或相应:一个请求或相应也可被多个Filter拦截

创建一个Filter只需要两个步骤:

  1.  创建Filter处理类
  2.  web.xml文件中配置Filter

3.1 创建Filter类

创建Filter必须实现javax.Servlet.Filter接口,在该接口中定义了如下三个方法:

  • void init(FilterConfig config):用于完成Filter的初始化
  • void destroy():用于Filter销毁前,完成某些资源的回收
  • void doFilter(ServletRequest request , ServletResponse response , FilterChain chain):实现过滤功能,该方法就是对每个请求及响应增加的额外处理

  终点在doFilter方法,实现该方法就可实现对用户请求进行预处理,也可实现对服务器响应进行后处理——它们的分界线为是否调用了chain.doFilter(request, response);方法,执行该方法之前,即对用户请求进行预处理;执行该方法之后,即对服务器响应进行后处理。

示例:(使用注解配置,详细介绍在下面)

@WebFilter(filterName = "log", urlPatterns = { "/*" })
public class LogFilter implements Filter {
	// FilterConfig可用于访问Filter的配置信息
	private FilterConfig config;
	// 实现初始化方法
	public void init(FilterConfig config) {
		this.config = config;
	}
	// 实现销毁方法
	public void destroy() {
		this.config = null;
	}
	// 执行过滤的核心方法
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		// ---------下面代码用于对用户请求执行预处理---------
		// 获取ServletContext对象,用于记录日志
		ServletContext context = this.config.getServletContext();
		long before = System.currentTimeMillis();
		System.out.println("开始过滤...");
		// 将请求转换成HttpServletRequest请求
		HttpServletRequest hrequest = (HttpServletRequest) request;
		// 输出提示信息
		System.out.println("Filter已经截获到用户的请求的地址: " + hrequest.getServletPath());
		// Filter只是链式处理,请求依然放行到目的地址
		chain.doFilter(request, response);//放行用户请求
		// ---------下面代码用于对服务器响应执行后处理---------
		long after = System.currentTimeMillis();
		// 输出提示信息
		System.out.println("过滤结束");
		// 输出提示信息
		System.out.println("请求被定位到" + hrequest.getRequestURI() + "   所花的时间为: " + (after - before));
	}
}

  这个Filter的作用是拦截所有的请求,然后输出请求地址,然后放行这个请求,然后,输出响应地址,并且输出整个响应过程所需要的时间。这只是个简单的拦截器,我们完全也可以在Filter中根据用户请求的HttpSession,判断用户权限是否足够。如果权限不够,直接调用重定向即可,无须调用chain.doFilter(request, response);方法。

3.2 配置Filter

  Filter可以认为是Servlet的“增强版”,因此配置Filter与配置Servlet非常相似,都需要配置如下两个部分:

  • 配置Filter名
  • 配置Filter拦截URL模式

  区别在于:Servlet通常指配置一个URL,而Filter可以同时拦截多个请求的URL。因此,在配置Filter的URL模式时通常会使用模式字符串,使得Filter可以拦截多个请求。与配置Servlet相似的是,配置Filter同样有两种方式:

  • 在Filter类中通过注解进行配置
  • 在web.xml文件中通过配置文件进行配置

  上面Filter类代码使用@WebFilter配置该Filter的名字为log,它会拦截向/*发送的所有请求。而用web.xml文件配置其作用相同,如下:



	
	log
	
	lee.LogFilter 



	
	log
	
	/*

3.2.1 @WebFilter注解常用属性(都是非必须)

  • filterName:指定该Filter的名称
  • urlPatterns / value:这两个属性的作用完全相同。都指定该Filter所拦截的URL
  • initParams:用于为该Filter配置参数
  • servletNames:该属性值指定多个Servlet的名称,用于指定该Filter仅对这几个Servlet执行过滤
  • displayName:指定该Filter的显示名
  • asyncSupported:指定该Filter是否支持异步操作模式。
  • dispatcherTypes:指定该Filter仅对那种dispatcher模式的请求进行过滤。该属性支持ASYNC、ERROR、FORWARD、INCLUDE、REQUEST这5个值的任何组合。默认值为同时过滤5种模式的请求。

3.2.2 使用示例

  假设系统有包含多个Servlet,这些Servlet都需要进行一些的通用处理:比如权限控制、记录日志等,这将导致在这些Servlet的service方法中有部分代码是相同的——为了解决这种代码重复的问题,可以考虑把这些通用处理提取到Filter中完成,这样各Servlet中剩下的只是特定请求相关的处理代码,而通过处理则交给Filter完成。

  由于Filter和Servlet如此相似,所以Filter和Servlet具有完全相同的生命周期行为,且Filter也可以通过元素或@WebFilter的initParams属性来配置初始化参数,获取Filter的初始化参数则使用FilterConfig的getInitParameter()方法。

  下面我们定一个较为实用的Filter,该Filter对用户请求进行过滤,Filter将通过doFilter方法来设置request编码的字符集,从而避免每个JSP、Servlet都需要设置;而且还会验证用户是否登录,如下:

Filter源码:

@WebFilter(filterName = "authority", urlPatterns = { "/*" }, initParams = {
		@WebInitParam(name = "encoding", value = "GBK"), @WebInitParam(name = "loginPage", value = "/login.jsp") })
public class AuthorityFilter implements Filter {
	// FilterConfig可用于访问Filter的配置信息
	private FilterConfig config;
	// 实现初始化方法
	public void init(FilterConfig config) {
		this.config = config;
	}
	// 实现销毁方法
	public void destroy() {
		this.config = null;
	}
	// 执行过滤的核心方法
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		// 获取该Filter的配置参数
		String encoding = config.getInitParameter("encoding");
		String loginPage = config.getInitParameter("loginPage");
		// 设置request编码用的字符集
		request.setCharacterEncoding(encoding); 
		HttpServletRequest requ = (HttpServletRequest) request;
		HttpSession session = requ.getSession(true);
		// 获取客户请求的页面
		String requestPath = requ.getServletPath();
		// 如果session范围的user为null,即表明没有登录
		// 且用户请求的既不是登录页面,也不是处理登录的页面
		if (session.getAttribute("user") == null && !requestPath.endsWith(loginPage)) {
			// forward到登录页面
			request.setAttribute("tip", "您还没有登录");
			request.getRequestDispatcher(loginPage).forward(request, response);
		}
		// "放行"请求
		else {
			chain.doFilter(request, response);
		}
	}
}

  当然也可以使用web.xml文件中配置该Filter,如下:



	
	authority
	
	lee.AuthorityFilter
	
	
		encoding
		GBK
	
	
		loginPage
		/login.jsp
	



	
	authority
	
	/*

login.jsp(可以写几个其它页面,以作测试)


	

登录页面

<% if (request.getAttribute("tip") != null) { out.println("" + request.getAttribute("tip") + ""); } %>
用户名:

  实现的效果是,如果没有的登录,则只能访问/login.jsp,反之则可以自由访问其他页面。

四、使用URL Rewrite实现网站伪静态

  大部分搜索引擎都会优先考虑收录静态的HTML页面,而不是这种动态的*.jsp、*.php页面。但实际上绝大多数网站都是动态的,不可能全部是静态的HTML页面,因此互联网上的大部分网站都会考虑使用伪静态——就是*.jsp、*.php这种动态URL伪装成静态的HTML页面。

  对于Java Web应用来说,要实现伪静态非常简单:可以通过Filter拦截所有发向*.html请求,然后按某种规则将请求forward到实际的*.jsp页面即可。现有的URL Rewrite开源项目为这种思路提供了实现,使用URL Rewrite实现网站伪静态也很简单。

4.1 利用URL Rewrite实现网站伪静态

<1> 下载URL Rewrite jar包

<2>  添加urlrewritefilter-4.0.3.jar 到 WEB-INF/lib目录下,或者添加Maven依赖如下:


    org.tuckey
    urlrewritefilter
    4.0.3

<3> 在web.xml文件中配置启用URL Rewrite Filter,在web.xml文件中添加如下配置:


    UrlRewriteFilter
    org.tuckey.web.filters.urlrewrite.UrlRewriteFilter


    UrlRewriteFilter
    /*
    REQUEST
    FORWARD

注意:需要在所有的servlet mappings的上面。
<4> 在应用的WEB-INF路径下增加urlrewrite.xml文件,该文件定义了伪静态映射规则,这份伪静态规则是基于正则表达式。

添加响应的urlrewrite.xml伪静态规则文件如下:




	
		
		/userinf-(\w*).html
		
		/userinf.jsp?username=$1
	

  上面的规则文件中只定义了一个简单的规则:所有发向/userinf-(\w*).html的请求都将被forward到userinf.jsp,并将(\w*)正则表达式所匹配的内容作为username参数值。根据这个伪静态规则,需要为该应用提供了一个userinf.jsp页面,该页面只是一个模拟了一个显示用户信息的页面,页面代码如下:

<%@ page contentType="text/html; charset=GBK" language="java"
	errorPage=""%>
<%
	// 获取请求参数
	String user = request.getParameter("username");
%>



<%=user%>的个人信息



	<%
		// 此处应该通过数据库读取该用户对应的信息
		// 此处只是模拟,因此简单输出:
		out.println("现在时间是:" + new java.util.Date() + "
"); out.println("用户名:" + user); %>

五、Listener介绍

  Servlet API提供了大量监听器来监听Web项目的内部事件,从而允许当Web内部事件发生时回调事件监听器内的方法。使用Listener只需要两个步骤:

  1. 定义Listener实现类
  2. 通过注解或web.xml文件中配置Listener

5.1 实现Listener类

  常用的Web事件监听器接口有如下几个:

  • ServletContextListener:用于监听Web应用的启动和关闭
  • ServletContextAttributeListener:用于监听ServletContext范围(application)内属性的改变
  • ServletRequestListener:用于监听用户请求
  • ServletRequestAttributeListener:用于监听ServletRequest范围(request)内属性的改变
  • HttpSessionListener:用于监听用户session的开始和结束
  • HttpSessionAttributeListener:用于监听HttpSession范围(session)内属性的改变

5.2 配置Listener

  配置Listener只要向Web应用注册Listener实现类即可,无须配置参数之类的东西,因此十分简单。为Web应用配置Listener也有两种方式:

  • 使用@WebListener修饰Listener实现类即可
  • 在web.xml文档中使用元素进行配置

  使用@WebListener时通常无须指定任何属性,只要使用该注解修饰Listener实现类即可向Web应用注册该监听器。在web.xml中使用元素进行配置时只要配置如下子元素即可。

  • listener-class:指定Listener实现类

5.3 使用ServletContextListener

  ServletContextListener 用于监听Web应用的启动和关闭,该接口包含如下两个方法:

  • contextInitialized(ServletContextEvent sce):启动Web应用时,系统调用Listener的该方法
  • contextDestroyed(ServletContextEvent sce):关闭Web应用时,系统调用Listener的该方法

示例:

@WebListener
public class GetConnListener implements ServletContextListener {
	// 应该启动时,该方法被调用。
	public void contextInitialized(ServletContextEvent sce) {
		try {
			// 取得该应用的ServletContext实例
			ServletContext application = sce.getServletContext();
			// 从配置参数中获取驱动
			String driver = application.getInitParameter("driver");
			// 从配置参数中获取数据库url
			String url = application.getInitParameter("url");
			// 从配置参数中获取用户名
			String user = application.getInitParameter("user");
			// 从配置参数中获取密码
			String pass = application.getInitParameter("pass");
			// 注册驱动
			Class.forName(driver);
			// 获取数据库连接
			Connection conn = DriverManager.getConnection(url, user, pass);
			// 将数据库连接设置成application范围内的属性
			application.setAttribute("conn", conn);
		} catch (Exception ex) {
			System.out.println("Listener中获取数据库连接出现异常" + ex.getMessage());
		}
	}

	// 应该关闭时,该方法被调用。
	public void contextDestroyed(ServletContextEvent sce) {
		// 取得该应用的ServletContext实例
		ServletContext application = sce.getServletContext();
		Connection conn = (Connection) application.getAttribute("conn");
		// 关闭数据库连接
		if (conn != null) {
			try {
				conn.close();
			} catch (SQLException ex) {
				ex.printStackTrace();
			}
		}
	}
}

需要在web.xml文件中配置连接数据库的几个参数:



	driver
	com.mysql.jdbc.Driver



	url
	jdbc:mysql://localhost:3306/ceshi



	user
	root



	pass
	root



	
	lee.GetConnListener

5.4 使用ServletContextAttributeListener

  ServletContextAttributeListener用于监听ServletContext(application)范围内属性的变化,实现该接口的监听器需要实现如下三个方法:

  • attributeAdded(ServletContextAttributeEvent event):当程序把一个属性存入application范围时触发该方法
  • attributeRemoved(ServletContextAttributeEvent event):当程序把一个属性从application范围删除时触发该方法
  • attributeReplaced(ServletContextAttributeEvent event):当程序替换application范围内的属性时将触发该方法

示例:

@WebListener
public class MyServletContextAttributeListener implements ServletContextAttributeListener {
	// 当程序向application范围添加属性时触发该方法
	public void attributeAdded(ServletContextAttributeEvent event) {
		ServletContext application = event.getServletContext();
		// 获取添加的属性名和属性值
		String name = event.getName();
		Object value = event.getValue();
		System.out.println(application + "范围内添加了名为" + name + ",值为" + value + "的属性!");
	}

	// 当程序从application范围删除属性时触发该方法
	public void attributeRemoved(ServletContextAttributeEvent event) {
		ServletContext application = event.getServletContext();
		// 获取被删除的属性名和属性值
		String name = event.getName();
		Object value = event.getValue();
		System.out.println(application + "范围内名为" + name + ",值为" + value + "的属性被删除了!");
	}

	// 当application范围的属性被替换时触发该方法
	public void attributeReplaced(ServletContextAttributeEvent event) {
		ServletContext application = event.getServletContext();
		// 获取被替换的属性名和属性值
		String name = event.getName();
		Object value = event.getValue();
		System.out.println(application + "范围内名为" + name + ",值为" + value + "的属性被替换了!");
	}
}

5.5 使用ServletRequestListener和ServletRequestAttributeListener

  ServletRequestListener用于监听用户请求的到达,实现该接口的监听器需要实现如下两个方法:

  • requestInitialized(ServletRequestEvent sre):用户请求到达、被初始化时触发该方法
  • requestDestroyed(ServletRequestEvent sre):用户请求结束、被销毁时触发该方法

  ServletRequestAttributeListener则用于监听ServletRequest(request)范围内属性的变化,实现该接口的监听器需要实现attributeAdded()、attributeRemoved()、attributeReplaced()三个方法。ServletRequestAttributeListener与ServletContextAttributeListener的作用相似,都用于监听属性的改变,只是ServletRequestAttributeListener监听request范围内属性的改变。

 

@WebListener
public class RequestListener implements ServletRequestListener, ServletRequestAttributeListener {
	// 当用户请求到达、被初始化时触发该方法
	public void requestInitialized(ServletRequestEvent sre) {
		HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
		System.out.println("----发向" + request.getRequestURI() + "请求被初始化----");
	}

	// 当用户请求结束、被销毁时触发该方法
	public void requestDestroyed(ServletRequestEvent sre) {
		HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
		System.out.println("----发向" + request.getRequestURI() + "请求被销毁----");
	}

	// 当程序向request范围添加属性时触发该方法
	public void attributeAdded(ServletRequestAttributeEvent event) {
		ServletRequest request = event.getServletRequest();
		// 获取添加的属性名和属性值
		String name = event.getName();
		Object value = event.getValue();
		System.out.println(request + "范围内添加了名为" + name + ",值为" + value + "的属性!");
	}

	// 当程序从request范围删除属性时触发该方法
	public void attributeRemoved(ServletRequestAttributeEvent event) {
		ServletRequest request = event.getServletRequest();
		// 获取被删除的属性名和属性值
		String name = event.getName();
		Object value = event.getValue();
		System.out.println(request + "范围内名为" + name + ",值为" + value + "的属性被删除了!");
	}

	// 当request范围的属性被替换时触发该方法
	public void attributeReplaced(ServletRequestAttributeEvent event) {
		ServletRequest request = event.getServletRequest();
		// 获取被替换的属性名和属性值
		String name = event.getName();
		Object value = event.getValue();
		System.out.println(request + "范围内名为" + name + ",值为" + value + "的属性被替换了!");
	}
}

5.6 使用HttpSessionListener 和HttpSessionAttributeListener 

  HttpSessionListener 用于监听用户session的创建和销毁,实现该接口的监听器需要实现如下两个方法:

  • sessionCreated(HttpSessionEvent se)
  • sessionDestroyed(HttpSessionEvent se)

示例:

@WebListener
public class OnlineListener implements HttpSessionListener {
	// 当用户与服务器之间开始session时触发该方法
	public void sessionCreated(HttpSessionEvent se) {
		HttpSession session = se.getSession();
		ServletContext application = session.getServletContext();
		// 获取session ID
		String sessionId = session.getId();
		// 如果是一次新的会话
		if (session.isNew()) {
			String user = (String) session.getAttribute("user");
			// 未登录用户当游客处理
			user = (user == null) ? "游客" : user;
			Map online = (Map) application.getAttribute("online");
			if (online == null) {
				online = new Hashtable();
			}
			// 将用户在线信息放入Map中
			online.put(sessionId, user);
			application.setAttribute("online", online);
		}
	}

	// 当用户与服务器之间session断开时触发该方法
	public void sessionDestroyed(HttpSessionEvent se) {
		HttpSession session = se.getSession();
		ServletContext application = session.getServletContext();
		String sessionId = session.getId();
		Map online = (Map) application.getAttribute("online");
		if (online != null) {
			// 删除该用户的在线信息
			online.remove(sessionId);
		}
		application.setAttribute("online", online);
	}
}

六、JSP2特性

JSP2主要增加了如下新特性:

  • 直接配置JSP属性
  • 表达式语言
  • 简化的自定义标签API
  • Tag文件语法

  如果需要使用JSP2语法,其web.xml文件必须使用Servlet2.4以上版本的配置文件。Servlet2.4配置文件的根元素写法如下:

 
 

  我在写博客的时候常用的是Servlet3.1,其根元素写法可参考:web.xml部署描述符的例子

6.1 配置JSP属性

JSP属性定义使用元素配置,主要包括如下4个方面:

  • 是否允许使用表达式语言:使用元素确定,默认值为false,即允许使用表达式语言
  • 是否允许使用JSP脚本:使用元素确定,默认值为false,即允许使用JSP脚本
  • 声明JSP页面的编码:使用元素确定,配置该元素后,可以代替每个页面里page指令contentType属性的charset部分
  • 使用隐士包含:使用元素确定,可以代替在每个页面里使用include编译指令来包含其他页面

注:此处隐式包含的作用与JSP提供的静态包含的作用相似。

6.2 表达式语言&Tag File

  • 表达式语言:可参考《EL表达式—学习笔记》
  • Tag File支持:可参考《标准标签库JSTL——学习笔记》

6.3 Servlet3.0新特性

6.3.1 Servlet3.0的注解

Servlet3.0规范在javax.servlet.annotation包下提供了如下注解:

  • @WebServlet:用于修饰一个Servlet雷,用于部署Servlet类
  • @WebInitParam:用于与@WebServlet或@WebFilter一起使用,为Servlet、Filter配置参数
  • @WebListener:用于修饰Listener类,用于部署Listener类
  • @WebFilter:用于修饰Filter类,用于部署Filter类
  • @MultipartConfig:用于修饰Servlet,指定该Servlet将会负责处理multipart/form-data类型的请求(主要用于文件上传)
  • @ServletSecurity:这是一个与JAAS有关的注解,修饰Servlet指定该Servlet的安全与授权控制
  • @HttpConstraint:用于与@ServletSecurity一起使用,用于指定该Servlet的安全与授权控制
  • @HttpMethodConstraint:用于与@ServletSecurity一起使用,用于指定该Servlet的安全与授权控制

6.3.2 Servlet 3.0 的Web模块支持

  Servlet 3.0为模块化开发提供了良好的支持,Servlet 3.0规范不再要求所有Web组件(如Servlet、Listener、Filter)都部署在web.xml文件中,而是允许采用“Web模块”来部署、管理它们。

一个Web模块通常对应于一个JAR包,这个JAR包有如下文件结构:

JSP&Servlet学习笔记(二)_第3张图片

  从上面的文件结构可以看出,Web模块与普通JAR的最大区别在于需要在META-INF目录下添加一个Web-fragment.xml文件,这个文件也被称为Web模块部署描述符。

  web-fragment.xml文件与web.xml文件的作用、文件结构都基本相似,因为它们都用于部署、管理各种Web组件。只是web-fragment.xml用于部署、管理Web模块而已,但web-fragment.xml可以指定多个下面的两个元素:

  • :用于指定该模块的名称
  • :用于指定加载该Web模块的相对顺序

  Web应用除了可按web-fragment.xml文件中指定的加载顺序来加载Web模块之外,还可以通过web.xml文件指定个Web模块加载的绝对顺序。在web.xml文件中指定的加载顺序将会覆盖Web模块中web-fragment.xml文件所指定的加载顺序。

  假如在Web应用的web.xml文件中增加如下配置片段:


	
	moudle-name
	moudle-name

  Servlet 3.0的Web模块支持为模块化开发、框架使用提供了巨大的方便,例如需要在Web应用中使用Web框架,这就只要将该框架的JAR包复制到Web应用中即可。因为这个JAR包的META-INF目录下可以通过web-fragment.xml文件来配置该框架所需要的Servlet、Listener、Filter等,从而避免修改Web应用的web.xml文件。Web模块支持对于模块化开发也有很大的帮助,开发者可以将不同模块的Web组件部署在不同的web-fragment.xml文件中,从而避免所有模块的配置、部署信息都写在web.xml文件中,这对以后的升级、维护将更加方便。

6.3.3 Servlet3.0提供的异步处理

  在之前的Servlet规范中,如果Servlet作为控制器调用一个耗时的业务方法,nameServlet必须等到业务方法完全返回之后才会生成响应,这将使得Servlet对业务方法的调用变成一种阻塞式的调用,因此效率比较低。

  Servlet 3.0规范引入了异步处理来解决这个问题,异步处理允许Servlet重新发起一条新线程去调用耗时的业务方法,这样就可避免等待。

  Servlet 3.0的异步处理是通过AsyncContext类来处理的,Servlet可通过ServletRequest的如下连个方法开启异步调用、创建AsyncContext对象:

  • AsyncContext startAsync()
  • AsyncContext startAsync(ServletRequest,ServletResponse)

  重复调用上面的方法将得到同一个AsyncContext对象,AsyncContext对象代表异步处理的上下文,它提供了一些工具方法,可完成设置异步调用的超时时长,dispatch用于请求、启动后台线程、获取request、response对象等功能。

示例:

创建异步处理的Servlet类:

@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	@Override
	public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
		response.setContentType("text/html;charset=GBK");
		PrintWriter out = response.getWriter();
		out.println("异步调用示例");
		out.println("进入Servlet的时间:" + new java.util.Date() + ".
"); // 创建AsyncContext,开始异步调用 AsyncContext actx = request.startAsync(); // 设置异步调用的超时时长 actx.setTimeout(60 * 1000); // 启动异步调用的线程,该线程以异步方式执行 actx.start(new GetBooksTarget(actx)); out.println("结束Servlet的时间:" + new java.util.Date() + ".
"); out.flush(); } }

下面创建线程执行体代码:

public class GetBooksTarget implements Runnable {
	private AsyncContext actx = null;
	public GetBooksTarget(AsyncContext actx) {
		this.actx = actx;
	}
	public void run() {
		try {
			// 等待5秒钟,以模拟业务方法的执行
			Thread.sleep(5 * 1000);
			ServletRequest request = actx.getRequest();
			List books = new ArrayList();
			books.add("疯狂Java讲义");
			books.add("轻量级Java EE企业应用实战");
			books.add("疯狂Ajax讲义");
			request.setAttribute("books", books);
			actx.dispatch("/async.jsp");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

  该线程体让线程暂停5秒来模拟调用耗时的业务方法,最后调用AsyncContext的dispatch方法把请求dispatch到指定JSP页面。

  被异步请求dispatch的目标页面需要指定session=“false”,表明该页面不会重新创建session。下面是async.jsp页面代码:

<%@ page contentType="text/html; charset=GBK" language="java"
	session="false"  isELIgnored="false"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
  • ${book}
<% out.println("业务调用结束的时间:" + new java.util.Date()); if (request.isAsyncStarted()) { // 完成异步调用 request.getAsyncContext().complete(); } %>

上面使用注解的方式已经配置好,如果要使用web.xml配置,则需要如下配置:

 
	async 
	lee.AsyncServlet 
	true 
 
 
	async 
	/async 

  对于支持异步调用的Servlet来说,当Servlet以异步方式启用新线程之后,该Servlet的执行不会被阻塞,该Servlet将可以向客户端浏览器生成响应——当新线程执行完成后,新线程生成的响应再次被送往客户端浏览器。

输出如下:

JSP&Servlet学习笔记(二)_第4张图片
  当Servlet启用异步调用的线程之后,该线程的执行过程对开发者是透明的。但在有些情况下,开发者需要了解该异步线程的执行细节,并针对特定的执行结果进行针对性处理,这可借助于Servlet3.0提供的异步监听器来实现。
  异步监听器需要实现AsyncListener接口,实现该接口的监听器类需要实现如下4个方法:

  • onStartAsync(AsyncEvent event):当异步调用开始时触发该方法
  • onComplete(AsyncEvent event):当异步调用完成时触发该方法
  • onError(AsyncEvent event):当异步调用错误时触发该方法
  • onTimeout(AsyncEvent event):当异步调用超时时触发该方法

  设计好异步监听器之后,需要通过AsyncContext来注册监听器,调用该对象的addListener()方法即可注册监听器。

  Filter与Servlet具有很大的相似性,因此Servlet3.0规范支持在Filter中使用异步调用。在Filter中进行异步调用与在Servlet中进行异步调用的效果完全相似,因此不进行叙述。

6.3.4 改进的Servlet API

重大的改进包括:

  • HttpServletRequest增加了对文件上传的支持
  • ServletContext允许通过编程的方式动态注册Servlet、Filter

HttpServletRequest提供了如下两个方法来处理文件上传:

  • Part getPart(String name):根据名称来获取文件上传域
  • Collection getParts():获取所有的文件上传域

  上面两个方法的返回值都涉及一个API:Part,每个Part对象对应于一个文件上传域,该对象提供了大量方法来访问上传文件的文件类型、大小、输入流等,并提供了一个write(String file)方法将上传文件写入服务器磁盘。

  为了向服务器上传文件,需要在表单里使用文件域,这个文件域会在HTML页面上产生一个单行文本框和一个“浏览”按钮,浏览者可通过该按钮选择需要上传的文件。除此之外,上传文件一定要为表单域设置enctype属性。

表单的enctype属性指定的是表单数据的编码方式,该属性有如下三个值:

  • application/x-www-form-urlencoded:这是默认的编码,它只处理表单域里的value属性值,采用这种编码方式的表单会将表单域的值处理成URL编码方式
  • multipart/form-data:这种编码方式会以二进制流的方式来处理表单数据,这种编码方式会把文件域指定文件的内容封装到请求参数里
  • text/plain:这种编码方式当表单的action属性为mailto:URL的形式时比较方便,这种方式主要适用于直接通过表单发送邮件的形式

  如果将enctype设置为application/x-www-form-urlencoded,或不设置enctype属性,提交表单时只会发送文件域的文本框里的字符串,也就是浏览者所选择文件的绝对路径,对服务器获取该文件在客户端上的绝对路径没有任何作用,因为服务器不可能访问客户机的文件系统。

示例如下:

创建上传页面upload.jsp

文件名:
选择文件:

  上面的页面中的表单需要设置enctype=“multipart/form-data”,这表明该表单可用于上传文件。上面表单中定义了两个表单域:一个普通的文本框,它将生成普通请求参数;一个文件上传域,它用于上传文件。对于传统的文件上传需要借助于common-fileupload等工具,处理起来极为复杂,借助于Servlet 3.0的API,处理文件上传将变得十分简单。如下面的Servlet代码:

@WebServlet(name = "upload", urlPatterns = { "/upload" })
@MultipartConfig
public class UploadServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	public void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
		response.setContentType("text/html;charset=GBK");
		PrintWriter out = response.getWriter();
		request.setCharacterEncoding("GBK");
		// 获取普通请求参数
		String name = request.getParameter("name");
		out.println("普通的name参数为:" + name + "
"); // 获取文件上传域 Part part = request.getPart("file"); // 获取上传文件的文件类型 out.println("上传文件的的类型为:" + part.getContentType() + "
"); // 获取上传文件的大小。 out.println("上传文件的的大小为:" + part.getSize() + "
"); // 获取该文件上传域的Header Name Collection headerNames = part.getHeaderNames(); // 遍历文件上传域的Header Name、Value for (String headerName : headerNames) { out.println(headerName + "--->" + part.getHeader(headerName) + "
"); } // 获取包含原始文件名的字符串 String fileNameInfo = part.getHeader("content-disposition"); // 提取上传文件的原始文件名 String fileName = fileNameInfo.substring(fileNameInfo.indexOf("filename=\"") + 10, fileNameInfo.length() - 1); // 将上传的文件写入服务器 part.write(getServletContext().getRealPath("/uploadFiles") + "/" + fileName); // ① } }

  上面Servlet使用了@MultipartConfig修饰,处理文件上传的Servlet应该使用该注解修饰。接下来该Servlet中HttpServletRequest就可通过getPart(String name)方法来获取文件上传域——就像获取普通请求参数一样。

注:上面的Servlet中将会把上传的文件保存到Web应用的根路径下的uploadFiles目录下,因此我们还需要在该Web应用的根目录下创建uploadFiles目录。
  上面Servlet上传时保存的文件名直接使用了上传文件的原始文件名,在实际项目中一般不会这么做,因为可能多个用户可能上传同名的文件,这样将导致后面用户上传的文件覆盖前面用户上传的文件。在实际项目中可借助于java.util.UUID工具类生成文件名。

  ServletContext则提供了如下方法来动态地注册Servlet、Filter,并允许动态设置Web应用的初始化参数:

  • 多个重载的addServlet()方法:动态地注册Servlet
  • 多个重载的addFilter()方法:动态地注册Filter
  • 多个重载的addListener():动态地注册Listener
  • setInitParameter(String name,String value)方法:为Web应用设置初始化参数

6.4 Servlet 3.1 新增的非阻塞式IO

  Servlet 3.1提供的非阻塞IO进行输入、输出,可以更好地提升性能:

  • ServletInputStream:Servlet用于读取数据的输入流
  • ServletOutputStream:Servlet用于输出数据的输出流

  传统读取方式采用阻塞式IO——当Servlet读取浏览器提交的数据时,如果数据暂时不可用,或数据没有读取完成,Servlet当前所在线程将会被阻塞,无法继续向下执行。
  从Servlet 3.1 开始,ServletInputStream新增了一个setReadListener(ReadListener readListener)方法,该方法允许以非阻塞IO读取数据,实现ReadListener监听器需要实现如下三个方法:

  • onAllDataRead():当所有数据读取完成时激发该方法
  • onDataAvailable():当有数据可用时激发该方法
  • onError(Throwable t):读取数据出现错误时激发该方法

类似地ServletOutputStream也提供了SetWriterListener()方法。

在Servlet中使用费阻塞IO非常简单,主要按如下步骤进行即可:

  • 调用ServletRequest的startAsync()方法开启异步模式
  • 通过ServletRequest获取ServletInputStream,并为ServletInputStream设置监听器(ReadListener实现类)
  • 实现ReadListener接口来实现监听器,在该监听器的方法中以非阻塞方式读取数据。

示例如下:

创建Servlet类

@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	public void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
		response.setContentType("text/html;charset=GBK");
		PrintWriter out = response.getWriter();
		out.println("非阻塞IO示例");
		out.println("进入Servlet的时间:" + new java.util.Date() + ".
"); // 创建AsyncContext,开始异步调用 AsyncContext context = request.startAsync(); // 设置异步调用的超时时长 context.setTimeout(60 * 1000); ServletInputStream input = request.getInputStream(); // 为输入流注册监听器 input.setReadListener(new MyReadListener(input, context)); out.println("结束Servlet的时间:" + new java.util.Date() + ".
"); out.flush(); } }

创建实现ReadListener接口的类

public class MyReadListener implements ReadListener {
	private ServletInputStream input;
	private AsyncContext context;

	public MyReadListener(ServletInputStream input, AsyncContext context) {
		this.input = input;
		this.context = context;
	}

	@Override
	public void onDataAvailable() {
		System.out.println("数据可用!!");
		try {
			// 暂停5秒,模拟读取数据是一个耗时操作。
			Thread.sleep(5000);
			StringBuilder sb = new StringBuilder();
			int len = -1;
			byte[] buff = new byte[1024];
			// 采用原始IO方式读取浏览器向Servlet提交的数据
			while (input.isReady() && (len = input.read(buff)) > 0) {
				String data = new String(buff, 0, len);
				sb.append(data);
			}
			System.out.println(sb);
			// 将数据设置为request范围的属性
			context.getRequest().setAttribute("info", sb.toString());
			// 转发到视图页面
			context.dispatch("/async.jsp");
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}

	@Override
	public void onAllDataRead() {
		System.out.println("数据读取完成");
	}

	@Override
	public void onError(Throwable t) {
		t.printStackTrace();
	}
}

下面创建页面:

form.html

用户名:
密码:

async.jsp

<%@ page contentType="text/html; charset=GBK" language="java"
	session="false"%>
浏览器提交数据为:${info}
<%=new java.util.Date()%>

6.5 Tomcat 8的WebSock支持(暂未完成)

赞赏

你可能感兴趣的:(Web开发)