2.10.2 配置Listener

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

  • 使用@Weblistener修饰Listener实现类即可。
  • web.xml文档中使用<listener>元素进行配置。

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

  • listener-class:指定Listener实现类。

若将ServletContextlistener配置在web容器中,且Web容器(支持Servlet2.3以上规范)支持Listener,则该ServletContextListener将可以监听Web应用的启动、关闭。
如果选择web.xml文件来配置Listener,则应在web.xml文档中增加如下配置片段:

1
2
3
4
<listener>
<!--指定 Listener的实现类-->
<listener-class>lee.GetConnListener</listener-class>
</listener>

上面的配置片段向Web应用注册了一个Listener,其实现类为lee.GetConnListener。当Web应用被启动时,该ListenercontextInitialized方法被触发,该方法会获取一个JDBCConnection,并放入application范围内,这样所有JSP页面都可通过application获取数据库连接,从而可以非常方便地进行数据库访问。

本例中的ServletContextListener把一个数据库连接(Connection实例)设置成application属性,这样将导致所有页面都使用相同的Connection实例,实际上这种做法的性能非常差。较为实用的做法是:应用启动时将一个数据源(javax.sql.DataSource实例)设置成application属性,而所有JSP页面都通过DataSource实例来取得数据库连接,再进行数据库访问,这样就会好得多。关于数据库连接池的介绍请参看疯狂Java体系的《疯狂Java讲义》一书的13.8节

2.10.4 使用ServletRequestListener和ServletRequestAttributeListener

ServletRequestListener

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

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

ServletRequestAttributeListener

ServletRequestAttributeListener则用于监听ServletRequest(request)范围内属性的变化,实现该接口的监听器需要实现

  • attributeAdded()
  • attributeRemoved()
  • attributeReplaced()

这三个方法。

由此可见,ServletRequestAttributeListenerServletContextAttributeListener的作用相似,都用于监听属性的改变,
只是ServletrequestAttributeListener监听request范围内属性的改变,
ServletContextAttributeListener监听的是application范围内属性的改变。

RequestListener.java

需要指出的是,应用程序完全可以采用一个监听器类来监听多种事件,只要让该监听器实现类同时实现多个监听器接口即可,如以下代码所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package lee;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;

@WebListener
public class RequestListener implements ServletRequestListener, ServletRequestAttributeListener {
// ------ 实现ServletRequestListener方法 -------------------------------------
// 当用户请求到达、被初始化时触发该方法
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() + "请求被销毁----");
}
// ------ 实现ServletRequestAttributeListener方法 ------------------------------
// 当程序向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 + "的属性被替换了!");
}
}

上面的监听器实现类同时实现了ServletRequestListener接口和ServletRequestAttributerListener接口,因此它既可以监听用户请求的初始化和销毁,也可监听request范围内属性的变化。

实现系统日志

由于实现了ServletRequestListener接口的监听器可以非常方便地监听到每次请求的创建、销毁,因此Web应用可通过实现该接口的监听器来监听访问该应用的每个请求,从而实现系统日志。

2.10.3 使用ServletContextAttributeListener

ServletContextAttributeListener

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

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

MyServletContextAttributeListener.java

下面是一个监听ServletContext范围内属性改变的Listener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package lee;

import javax.servlet.*;
import javax.servlet.annotation.*;

@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 + "的属性被替换了!");
}
}

@WebListener注解 注册Listener

上面的ServletcontextListener使用了@WebListener注解修饰,这就是向Web应用中注册了该Listener,该Listener实现了attributeAddedattributeRemovedattributeReplaced方法,因此当application范围内的属性被添加、删除、替换时,这些对应的监听器方法将会被触发。

2.8.1 开发自定义标签类

JSP页面使用一个简单的标签时,底层实际上由标签处理类提供支持,从而可以通过简单的标签来封装复杂的功能,从而使团队更好地协作开发(能让美工人员更好地参与JSP页面的开发)。

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

  • 如果标签类包含属性,每个属性都有对应的gettersetter方法
  • 重写doTag()方法,这个方法负责生成页面内容。

下面开发一个最简单的自定义标签,该标签负责在页面上输出HelloWorld

1
2
3
4
5
6
7
8
9
10
11
12
13
package lee;

import javax.servlet.jsp.tagext.*;
import javax.servlet.jsp.*;
import java.io.*;

public class HelloWorldTag extends SimpleTagSupport {
// 重写doTag()方法,该方法为标签生成页面内容
public void doTag() throws JspException, IOException {
// 获取页面输出流,并输出字符串
getJspContext().getOut().write("Hello World " + new java.util.Date());
}
}

上面这个标签处理类非常简单,它继承了SimpleTagSupport父类,并重写doTag()方法,而doTag()方法则负责输出页面内容。该标签没有属性,因此无须提供settergetter方法。

2.8.2 建立TLD文件

TLDTag Library Definition的缩写,即标签库定义,文件的后缀是td,每个TLD文件对应一个标签库,一个标签库中可包含多个标签。TLD文件也称为标签库定义文件.
标签库定义文件的根元素是taglib,它可以包含多个tag子元素,每个tag子元素都定义一个标签。通常可以到web容器下复制一个标签库定义文件,并在此基础上进行修改即可。例如Tomcat8.5,在\webapps\examples\WEB-INF\jsp2\路径下包含了一个jsp2-example-taglib.tld文件,这就是一个TLD文件的范例。
将该文件复制到Web应用的WEB-INF路径,或WEB-NF的任意子路径下,并对该文件进行简单修改,修改后的mytaglib.tld文件代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="GBK"?>

<taglib xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-jsptaglibrary_2_1.xsd"
version="2.1">
<tlib-version>1.0</tlib-version>
<short-name>mytaglib</short-name>
<!-- 定义该标签库的URI -->
<uri>http://www.crazyit.org/mytaglib</uri>

<!-- 定义第一个标签 -->
<tag>
<!-- 定义标签名 -->
<name>helloWorld</name>
<!-- 定义标签处理类 -->
<tag-class>lee.HelloWorldTag</tag-class>
<!-- 定义标签体为空 -->
<body-content>empty</body-content>
</tag>
</taglib>

上面的标签库定义文件也是一个标准的XML文件,该XML文件的根元素是taglib元素,因此每次编写标签库定义文件时都直接添加该元素即可.

taglib标签子元素

taglib下有如下三个子元素。

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

tag标签子元素

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

  • name:该标签的名称,这个子元素很重要,JSP页面中就是根据该名称来使用此标签的。
  • tag-class:指定标签的处理类,毋庸置疑,这个子元素非常重要,它指定了标签由哪个标签处理类来处理。
  • body-content:这个子元素也很重要,它指定标签体内容。该子元素的值可以是如下几个。
    • tagdependent:指定标签处理类自己负责处理标签体。
    • empty:指定该标签只能作为空标签使用。
    • scriptless:指定该标签的标签体可以是静态HTML元素、表达式语言,但不允许出现JSP脚本
    • JSP:指定该标签的标签体可以使用JSP脚本。
  • dynamic-attributes:指定该标签是否支持动态属性。只有当定义动态属性标签时才需要该子元素。

因为JSP2规范不再推荐使用JSP脚本,所以JSP2自定义标签的标签体中不能包含JSP脚本。所以,实际上tag元素的body-content子元素的值不可以是JSP.
定义了上面的标签库定义文件后,将标签库文件放在web应用的WEB-INF路径或任意子路径下java Web规范会自动加载该文件,则该文件定义的标签库也将生效。

2.8.5 带标签体的标签

带标签体的标签,可以在标签内嵌入其他内容(包括静态的HTML内容和动态的JSP内容),通常用于完成一些逻辑运算,例如判断和循环等。下面以一个迭代器标签为示例,介绍带标签体标签的开发过程。

IteratorTag.java

一样先定义一个标签处理类,该标签处理类的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package lee;

import javax.servlet.jsp.tagext.*;
import javax.servlet.jsp.*;
import java.io.*;
import java.sql.*;
import java.util.*;

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

// collection的setter和getter方法
public void setCollection(String collection) {
this.collection = collection;
}

public String getCollection() {
return this.collection;
}

// item的setter和getter方法
public void setItem(String item) {
this.item = item;
}

public String getItem() {
return this.item;
}

// 标签的处理方法,标签处理类只需要重写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);
}
}
}

上面的标签处理类与前面的处理类并没有太大的不同,该处理类包含两个成员变量(代表标签的属性),并为这两个成员变量提供了settrgetter方法。
标签处理类的doTage()方法首先从page范围内获取了指定名称的Collection对象,然后遍历Collection对象的元素,每次遍历都调用了getJspBody()方法,该方法返回该标签所包含的标签体:JspFragment对象,执行JspFragment对象的invoke()方法,即可输出标签体内容。
该标签的作用是:遍历指定集合,每遍历一个集合元素,即输出标签体一次。

因为该标签的标签体不为空,配置该标签时指定body-contentscriptless,该标签的配置代码片段如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- 定义第三个标签 -->
<tag>
<!-- 定义标签名 -->
<name>iterator</name>
<!-- 定义标签处理类 -->
<tag-class>lee.IteratorTag</tag-class>
<!-- 定义标签体不允许出现JSP脚本 -->
<body-content>scriptless</body-content>
<!-- 配置标签属性:collection -->
<attribute>
<name>collection</name>
<required>true</required>
<fragment>true</fragment>
</attribute>
<!-- 配置标签属性:item -->
<attribute>
<name>item</name>
<required>true</required>
<fragment>true</fragment>
</attribute>
</tag>

上面的配置片段中的代码:

1
<body-content>scriptless</body-content>

指定该标签的标签体可以是静态HTML内容,也可以是表达式语言,但不允许出现JSP脚本.
为了测试在JSP页面中使用该标签的效果,下面先将一个List对象设置成page范围的属性,然后使用该标签来迭代输出List集合的全部元素.

iteratorTag.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

<%@ page contentType="text/html; charset=UTF-8" language="java" errorPage="" %>
<%@ page import="java.util.*"%>
<!-- 导入标签库,指定mytag前缀的标签,
由http://www.crazyit.org/mytaglib的标签库处理 -->
<%@ taglib uri="http://www.crazyit.org/mytaglib" prefix="mytag"%>
<!DOCTYPE html>
<html>
<head>
<title> 带标签体的标签-迭代器标签 </title>
</head>
<body>
<h2>带标签体的标签-迭代器标签</h2><hr/>
<%
//创建一个List对象
List<String> a = new ArrayList<String>();
a.add("疯狂Java");
a.add("www.crazyit.org");
a.add("www.fkit.org");
//将List对象放入page范围内
pageContext.setAttribute("a" , a);
%>
<table border="1" bgcolor="#aaaadd" width="300">
<!-- 使用迭代器标签,对a集合进行迭代 -->
<mytag:iterator collection="a" item="item">
<tr>
<td>${pageScope.item}</td>
<tr>
</mytag:iterator>
</table>
</body>
</html>

上面的页面代码中代码:

1
2
3
4
5
6
<!-- 使用迭代器标签,对a集合进行迭代 -->
<mytag:iterator collection="a" item="item">
<tr>
<td>${pageScope.item}</td>
<tr>
</mytag:iterator>

可实现通过Iterator标签来遍历指定集合,浏览该页面即可看到如图2.36所示的界面。
图2.36显示了使用Iterator标签遍历集合元素的效果,从iteratorTag.jsp页面的代码来看,使用Iterator标签遍历集合元素比使用JSP脚本遍历集合元素要优雅得多,这就是自定义标签的魅力。
实际上JSTL标签库提供了一套功能非常强大的标签,例如普通的输出标签,就像刚刚介绍的迭代器标签,还有用于分支判断的标签等,JSTL都有非常完善的实现。

2.11 JSP2特性

目前Servlet3.1对应于JSP2.3规范,JSP2.3也被统称为JSP2。相比JSP1.2,JSP2主要增加了如下新特性。

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

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

<?xml version="1.0" encoding="GBK"?>
<web-app xmlns="http://xmlns.jcp.org/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_4.xsd" version="2.4">

</web-app>

本书所给出的Web应用都是使用Servlet3.1规范,也就是对应于JSP2.3规范,因此完全支持JSP2的特性。Servlet3.1规范的web-app元素的写法如下:

<?xml version="1.0" encoding="GBK"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
    http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1">

</web-app>

2.11.1 配置JSP属性

JSP属性定义使用<jsp-property-group>元素配置,主要包括如下4个方面。

  1. 是否允许使用表达式语言:使用<el-ignored>元素确定,默认值为flase,即允许使用表达式语言。
  2. 是否允许使用JSP小脚本:使用<scripting-invalid>元素确定,默认值为false,即允许使用JSP小脚本。
  3. 声明JSP页面的编码:使用<page-encoding>元素确定,配置该元素后,可以代替每个页面里page指令contentType属性的charset部分。
  4. 使用隐式包含:使用<include-prelude><include-coda>元素确定,可以代替在每个页面里使用include编译指令来包含其他页面。(此处隐式包含的作用与JSP提供的静态包含的作用相似。)

下面的web.xml文件配置了该应用下的系列属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?xml version="1.0" encoding="GBK"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<!-- 关于JSP的配置信息 -->
<jsp-config>
<jsp-property-group>
<!-- 对哪些文件应用配置 -->
<url-pattern>/noscript/*</url-pattern>
<!-- 忽略表达式语言 -->
<el-ignored>true</el-ignored>
<!-- 页面编码的字符集 -->
<page-encoding>GBK</page-encoding>
<!-- 不允许使用Java脚本 -->
<scripting-invalid>true</scripting-invalid>
<!-- 隐式导入页面头 -->
<include-prelude>/inc/top.jspf</include-prelude>
<!-- 隐式导入页面尾 -->
<include-coda>/inc/bottom.jspf</include-coda>
</jsp-property-group>
<jsp-property-group>
<!-- 对哪些文件应用配置 -->
<url-pattern>*.jsp</url-pattern>
<el-ignored>false</el-ignored>
<!-- 页面编码字符集 -->
<page-encoding>GBK</page-encoding>
<!-- 允许使用Java脚本 -->
<scripting-invalid>false</scripting-invalid>
</jsp-property-group>
<jsp-property-group>
<!-- 对哪些文件应用配置 -->
<url-pattern>/inc/*</url-pattern>
<el-ignored>false</el-ignored>
<!-- 页面编码字符集 -->
<page-encoding>GBK</page-encoding>
<!-- 不允许使用Java脚本 -->
<scripting-invalid>true</scripting-invalid>
</jsp-property-group>
</jsp-config>

<context-param>
<param-name>author</param-name>
<param-value>yeeku</param-value>
</context-param>

</web-app>

上面的配置文件中配置了三个<jsp-property-group>元素,每个元素配置一组JSP属性,用于指定哪些JSP页面应该满足怎样的规则。例如,第一个jsp-property-group元素指定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<jsp-property-group>
<!-- 对哪些文件应用配置 -->
<url-pattern>/noscript/*</url-pattern>
<!-- 忽略表达式语言 -->
<el-ignored>true</el-ignored>
<!-- 页面编码的字符集 -->
<page-encoding>GBK</page-encoding>
<!-- 不允许使用Java脚本 -->
<scripting-invalid>true</scripting-invalid>
<!-- 隐式导入页面头 -->
<include-prelude>/inc/top.jspf</include-prelude>
<!-- 隐式导入页面尾 -->
<include-coda>/inc/bottom.jspf</include-coda>
</jsp-property-group>

/noscript/*下的所有页面应该使用GBK字符集进行编码,且不允许使用JSP脚本,忽略表达式语言,并隐式包含页面头、页面尾。
如果在不允许使用JSP脚本的页面中使用JSP脚本,则该页面将出现错误。即/noscript/*下的页面中使用JSP脚本将引起错误。

noscript\test1.jsp

看下面的JSP页面代码,为test1.jsp页面代码。

1
2
3
4
5
6
7
8
9
10
11
12
<%@ page contentType="text/html; charset=UTF-8" language="java" errorPage="" %>
<!DOCTYPE html>
<html>
<head>
<title> 页面配置1 </title>
</head>
<body>
<h2>页面配置1</h2>
下面是表达式语言输出:<br/>
${1 + 2}
</body>
</html>

上面的页面中粗体字代码就是表达式语言,关于表达式语言请看下一节介绍。但由于在web.xml文件中配置了表达式语言无效,所以浏览该页面将看到系统直接输出表达式语言。在浏览器中浏览该页面的效果如图2.44所示。

从图2.44中可以看出,test1.jsp的表达式语言不能正常输出,这是因为配置了忽略表达式语言
上面页面中看到隐式include的页面头分别是inc\top.jspfinc\bottom.jspf,这两个文件依然是JSP页面,只是将文件名后缀改为了jspf而已。

test2.jsp

而位于应用根路径下的JSP页面则支持表达式语言和JSP脚本,但没有使用隐式include包含页面头和页面尾。应用根路径下的test2.jsp页面代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<%@ page contentType="text/html; charset=UTF-8" language="java" errorPage="" %>
<!DOCTYPE html>
<html>
<head>
<title> 页面配置2 </title>
</head>
<body>
<h2>页面配置2</h2>
下面是表达式语言输出:<br/>
${1 + 2}<br/>
下面是小脚本输出:<br/>
<%out.println("hello Java");%>
</body>
</html>

上面的页面中两行粗体字代码正是嵌套在JSP页面中的JSP脚本和表达式语言,浏览该页面将看到如图2.45所示的效果。

图2.45中椭圆形圈出的3就是${1+2}的结果这就是表达式语言的计算结果。

2.10.5 使用HttpSessionListener和HttpSessionAttributeListener

HttpSessionListener

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

  • sessionCreated(HttpsessionEvent se):用户与服务器的会话开始、创建时触发该方法
  • sessionDestroyed(HttpsessionEvent se):用户与服务器的会话断开、销毁时触发该方法。

HttpSessionAttributeListener

HttpSessionAttributeListener则用于监听HttpSession(session内置对象)范围内属性的变化,实现该接口的监听器需要实现

  • attributeAdded()
  • attributeRemoved()
  • attributeReplaced()

这三个方法。

由此可见,HttpSessionAttributeListenerServletContextAttributeListener的作用相似,都用于监听属性的改变,只是HttpSessionAttributeListener监听session范围内属性的改变,
ServletContextAttributeListener监听的是application范围内属性的改变。

程序 监听系统在线用户

实现HttpSessionListener接口的监听器可以监听每个用户会话的开始和断开,因此应用可以通过该监听器监听系统的在线用户。

OnlineListener.java

下面是该监听器的实现类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package lee;

import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;
import java.util.*;

@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<String, String> online = (Map<String, String>) application.getAttribute("online");
if (online == null) {
online = Collections.synchronizedMap(new HashMap<String, String>());
}
// 将用户在线信息放入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<String, String> online = (Map<String, String>) application.getAttribute("online");
if (online != null) {
// 删除该用户的在线信息
online.remove(sessionId);
}
application.setAttribute("online", online);
}
}

上面的监听器实现类实现了HttpSessionListener接口,该监听器可用于监听用户与服务器之间session的开始、关闭.

  • 当用户与服务器之间的session开始时,如果该session是一次新的session,程序就将当前用户的sessionId、用户名存入application范围的Map中;
  • 当用户与服务器之间的session关闭时,程序从application范围的Map中删除该用户的信息。

通过上面的方式,application范围内的Map就记录了当前应用的所有在线用户。

online.jsp

显示在线用户的页面代码很简单,只要迭代输出application范围的Map即可,如以下代码所

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %>
<%@ page import="java.util.*" %>
<!DOCTYPE html>
<html>
<head>
<title> 用户在线信息 </title>
</head>
<body>
在线用户:
<table width="400" border="1">
<%
Map<String , String> online = (Map<String , String>)application
.getAttribute("online");
for (String sessionId : online.keySet())
{%>
<tr>
<td><%=sessionId%>
<td><%=online.get(sessionId)%>
</tr>
<%}%>
</body>
</html>

如果在本机启动三个不同的浏览器来模拟三个用户访问该应用,访问online.jsp页面将看到如图2.42所示的页面效果。

程序 监听用户的详细信息

需要指出的是,采用HttpSessionListener监听用户在线信息比较“粗糙”,只能监听到有多少人在线,每个用户的sessionId等基本信息。如果应用需要监听到每个用户停留在哪个页面、本次在线的停留时间、用户的访问IP等信息,则应该考虑定时检查HttpServletRequest来实现。

通过检查HttpservletRequest的做法可以更精确地监控在线用户的状态,这种做法的思路是:

  • 定义一个ServletRequestListener,这个监听器负责监听每个用户请求,当用户请求到达时,系统将用户请求的sessionID、用户名、用户IP、正在访问的资源、访问时间记录下来
  • 启动一条后台线程,这条后台线程每隔一段时间检查上面的每条在线记录,如果某条在线记录的访问时间与当前时间相差超过了指定值,将这条在线记录删除即可。这条后台线程应随着Web应用的启动而启动,可考虑使用ServletContextListener来完成。

RequestListener.java

下面先定义一个ServletRequestListener,它负责监听每次用户请求:
每次用户请求到达时,

  • 如果是新的用户会话,将相关信息插入数据表:
  • 如果是老的用户会话,则更新数据表中已有的在线记录。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package lee;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.sql.*;

@WebListener
public class RequestListener implements ServletRequestListener {
// 当用户请求到达、被初始化时触发该方法
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
HttpSession session = request.getSession();
// 获取session ID
String sessionId = session.getId();
// 获取访问的IP和正在访问的页面
String ip = request.getRemoteAddr();
String page = request.getRequestURI();
String user = (String) session.getAttribute("user");
// 未登录用户当游客处理
user = (user == null) ? "游客" : user;
try {
DbDao dd = new DbDao("com.mysql.jdbc.Driver", "jdbc:mysql://localhost:3306/online_inf", "root", "32147");
ResultSet rs = dd.query("select * from online_inf where session_id=?", true, sessionId);
// 如果该用户对应的session ID存在,表明是旧的会话
if (rs.next()) {
// 更新记录
rs.updateString(4, page);
rs.updateLong(5, System.currentTimeMillis());
rs.updateRow();
rs.close();
} else {
// 插入该用户的在线信息
dd.insert("insert into online_inf values(? , ? , ? , ? , ?)", sessionId, user, ip, page,
System.currentTimeMillis());
}
} catch (Exception ex) {
ex.printStackTrace();
}
}

// 当用户请求结束、被销毁时触发该方法
public void requestDestroyed(ServletRequestEvent sre) {
}
}

上面的程序中粗体字代码控制用户会话是新的session,还是已有的session,新的session将插入数据表;旧的session将更新数据表中对应的记录。

接下来定义一个ServletContextListener,它负责启动一条后台线程,这条后台线程将会定期检查在线记录,并删除那些长时间没有重新请求过的记录。

OnlineListener.java

Listener代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package lee;

import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;
import java.sql.*;
import java.awt.event.*;

@WebListener
public class OnlineListener implements ServletContextListener {
// 超过该时间(10分钟)没有访问本站即认为用户已经离线
public final int MAX_MILLIS = 10 * 60 * 1000;

// 应用启动时触发该方法
public void contextInitialized(ServletContextEvent sce) {
// 每5秒检查一次
new javax.swing.Timer(1000 * 5, new ActionListener() {
public void actionPerformed(ActionEvent e) {
try {
DbDao dd = new DbDao("com.mysql.jdbc.Driver", "jdbc:mysql://localhost:3306/online_inf", "root",
"32147");
ResultSet rs = dd.query("select * from online_inf", false);
StringBuffer beRemove = new StringBuffer("(");
while (rs.next()) {
// 如果距离上次访问时间超过了指定时间
if ((System.currentTimeMillis() - rs.getLong(5)) > MAX_MILLIS) {
// 将需要被删除的session ID添加进来
beRemove.append("'");
beRemove.append(rs.getString(1));
beRemove.append("' , ");
}
}
// 有需要删除的记录
if (beRemove.length() > 3) {
beRemove.setLength(beRemove.length() - 3);
beRemove.append(")");
// 删除所有“超过指定时间未重新请求的记录”
dd.modify("delete from online_inf where session_id in " + beRemove.toString());
}
dd.closeConn();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}).start();
}

public void contextDestroyed(ServletContextEvent sce) {
}
}

上面的程序中粗体字代码负责收集系统中“超过指定时间未访问”的在线记录,然后程序通过一条SQL语句删除这些在线记录。
需要指出的是,上面程序启动的后台线程定期检查的时间间隔为5秒,实际项目中这个时间应该适当加大,尤其是在线用户较多时,否则应用将会频繁地检查online_Inf数据表中的全部记录,这将导致系统开销过大

online.jsp

显示在线用户的页面十分简单,只要查询online_Inf表中全部记录,并将这些记录显示出来即可。
以下是该页面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %>
<%@ page import="java.sql.*,lee.*" %>
<!DOCTYPE html>
<html>
<head>
<title> 用户在线信息 </title>
</head>
<body>
在线用户:
<table width="640" border="1">
<%
DbDao dd = new DbDao("com.mysql.jdbc.Driver"
, "jdbc:mysql://localhost:3306/online_inf"
, "root"
, "32147");
// 查询online_inf表(在线用户表)的全部记录
ResultSet rs = dd.query("select * from online_inf" , false);
while (rs.next())
{%>
<tr>
<td><%=rs.getString(1)%>
<td><%=rs.getString(2)%>
<td><%=rs.getString(3)%>
<td><%=rs.getString(4)%>
</tr>
<%}%>
</body>
</html>

启动不同浏览器访问该应用的不同页面,然后访问online.jsp页面将可看到如图2.43所示的页面效果。

对于应用中所有需要统计在线用户页面,只要将上面的online.jsp页面包含到页面中即可。

data.sql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DROP DATABASE IF EXISTS online_inf;

CREATE DATABASE online_inf;

USE online_inf;

CREATE TABLE online_inf
(
session_id VARCHAR(255) PRIMARY KEY,
username VARCHAR(255),
user_ip VARCHAR(255),
access_res VARCHAR(255),
last_access BIGINT
);
;

2.10 Listener介绍

Web应用在web容器中运行时,Web应用内部会不断地发生各种事件:
web应用被启动web应用被停止,用户session开始、用户session结束、用户请求到达等,通常来说,这些web事件对开发者是透明的。
实际上,Servlet API提供了大量监听器来监听Web应用的内部事件,从而允许当Web内部事件发生时回调事件监听器内的方法.

使用Listener步骤

使用Listener只需要两个步骤。

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

2.10.1 实现Listener类

AWT事件编程完全相似,监听不同web事件的监听器也不相同。常用的web事件监听器接口有如下几个。

监听器 描述
ServletContextListener 用于监听Web应用的启动和关闭
ServletContextAttributeListener 用于监听Servletcontext范围(application)内属性的改变。
ServletRequestListener 用于监听用户请求
ServletRequestAttributeListener 用于监听Servletrequest范围(request)内属性的改变。
HttpSessionListener 用于监听用户session的开始和结束
HttpSessionAttributeListener 用于监听Httpsession范围(session)内属性的改变。

下面先以ServletContextListener为例来介绍Listener的开发和使用,ServletContextListener用于监听Web应用的启动和关闭

ServletContextListener接口方法

Listener类必须实现ServletContextListener接口,该接口包含如下两个方法.

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

通过上面的介绍不难看出,ServletContextListener的作用有点类似于load-on-startup Servlet,都可用于在Web应用启动时,回调方法来启动某些后台程序,这些后台程序负责为系统运行提供支持。

GetConnListener.java

下面将创建一个获取数据库连接的Listener,该Listener会在应用启动时获取数据库连接,并将获取到的连接设置成application范围内的属性。
下面是该Listener的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package lee;

import java.sql.*;
import javax.servlet.*;
import javax.servlet.annotation.*;

@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();
}
}
}
}

上面的程序中重写了ServletContextListenercontextInitializedcontextDestroyed方法,这两个方法分别在应用启动、关闭时被触发。
上面ServletContextListener的两个方法分别实现获取数据库连接、关闭数据库连接的功能,这些功能都是为整个Web应用提供服务的。

程序中,contextInitialized()方法中代码:

1
2
3
4
5
6
7
8
9
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");

用于获取配置参数,细心的读者可能已经发现ServletContextListener获取的是web应用的配置参数,而不是像ServletFilter获取本身的配置参数。这是因为配置Listener时十分简单,只要简单地指定Listener实现类即可,不能配置初始化参数:

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="GBK"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1">
<!-- 配置第一个参数:driver -->
<context-param>
<param-name>driver</param-name>
<param-value>com.mysql.jdbc.Driver</param-value>
</context-param>
<!-- 配置第二个参数:url -->
<context-param>
<param-name>url</param-name>
<param-value>jdbc:mysql://localhost:3306/javaee</param-value>
</context-param>
<!-- 配置第三个参数:user -->
<context-param>
<param-name>user</param-name>
<param-value>root</param-value>
</context-param>
<!-- 配置第四个参数:pass -->
<context-param>
<param-name>pass</param-name>
<param-value>32147</param-value>
</context-param>

<listener>
<!-- 指定Listener的实现类 -->
<listener-class>lee.GetConnListener</listener-class>
</listener>

</web-app>

2.9.3 使用URL Rewrite实现网站伪静态

对于以JSP为表现层开发的动态网站来说,用户访问的URL通常有如下形式

1
xxx.jsp?param=value

大部分搜索引擎都会优先考虑收录静态的HTML页面,而不是这种动态的*.jsp*.php页面。但实际上绝大部分网站都是动态的,不可能全部是静态的HTML页面,因此互联网上的大部分网站都会考虑使用伪静态——就是将*.jsp*.php这种动态URL伪装成静态的HTML页面。
对于Java Web应用来说,要实现伪静态非常简单:可以通过Filter拦截所有发向*.html请求,然后按某种规则将请求forward到实际的*.jsp页面即可。
现有的URL Rewrite开源项目为这种思路提供了实现,使用URL Rewrite实现网站伪静态也很简单。
下面详细介绍如何利用URL Rewrite实现网站伪静态。
登录http://www.tuckey.org/urlrewrite站点下载UrlRewrite的最新版本,本书成书时,该项目的最新版本是4.0.3,建议读者也下载该版本的UrlRewrite.或者到https://github.com/paultuckey/urlrewritefilter/releases下载也是可以的.

  1. 下载URL Rewrite,直接下载它的urlrewritefilter-4.0.3.jar即可,并将该JAR包复制到web应用
  2. 下载URL Rewrite,直接下载它的urlrewritefilter-4.0.3.jar即可,并将该JAR包复制到Web应用的WEB-FLib目录下。
  3. web.xml文件中配置启用URL Rewrite Filter,在web.xml文件中增加如下配置片段。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!-- 配置Url Rewrite的Filter -->
    <filter>
    <filter-name>UrlRewriteFilter</filter-name>
    <filter-class>org.tuckey.web.filters.urlrewrite.UrlRewriteFilter</filter-class>
    </filter>
    <!-- 配置Url Rewrite的Filter拦截所有请求 -->
    <filter-mapping>
    <filter-name>UrlRewriteFilter</filter-name>
    <url-pattern>/*</url-pattern>
    </filter-mapping>
    上面的配置片段指定使用URL RewriteFilter拦截所有的用户请求。
    在应用的WEB-INF路径下增加urlrewrite.xml文件,该文件定义了伪静态映射规则,这份伪静态规则是基于正则表达式的。

    urlrewrite.xml

    下面是本应用所使用的urlrewrite.xml伪静态规则文件。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <?xml version="1.0" encoding="GBK"?>
    <!DOCTYPE urlrewrite PUBLIC "-//tuckey.org//DTD UrlRewrite 3.2//EN"
    "http://tuckey.org/res/dtds/urlrewrite3.2.dtd">
    <urlrewrite>
    <rule>
    <!-- 所有配置如下正则表达式的请求 -->
    <from>/userinf-(\w*).html</from>
    <!-- 将被forward到如下JSP页面,其中$1代表
    上面第一个正则表达式所匹配的字符串 -->
    <to type="forward">/userinf.jsp?username=$1</to>
    </rule>
    </urlrewrite>
    上面的规则文件中只定义了一个简单的规则:
    所有发向/userinf-(\w*).html的请求都将被forward/userinf.jsp?页面,并将(\w*)正则表达式所匹配的内容作为username参数值。

    userinfo.jsp

    根据这个伪静态规则,需要为该应用提供一个userinfo.jsp页面,该页面只是一个模拟了一个显示用户信息的页面,该页面代码如下。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %>
    <%
    // 获取请求参数
    String user = request.getParameter("username");
    %>
    <!DOCTYPE html>
    <html>
    <head>
    <title> <%=user%>的个人信息 </title>
    </head>
    <body>
    <%
    // 此处应该通过数据库读取该用户对应的信息
    // 此处只是模拟,因此简单输出:
    out.println("现在时间是:" + new java.util.Date() + "<br/>");
    out.println("用户名:" + user);
    %>
    </body>
    </html>
    上面的页面中粗体字代码username请求参数来输出用户信息,但因为系统使用了URL Rewrite,因此用户可以请求类似于userinf-xxx.Html页面,图2.41显示了“伪静态”示意。