第5章JSP基础





JSP是一种动态网页开发技术。本章主要介绍了JSP基本语法、内置对象、Servlet、应用监听器、与数据库交互等内容。熟悉JSP,有助于建立B/S模式下前后端交互处理的初步思维,为后续章节的学习做好准备。

5.1JSP概述
5.1.1JSP简介

JSP(Java Server Page,Java服务页面)是由原美国Sun(Sun Microsystems)公司倡导、很多公司(例如Oracle、IBM等)参与建立、基于Java语言的动态Web网页开发技术。
由于JSP是基于Java语言的,先天就具有多平台支持特性,在实践中仍然具有一定市场,例如中国工商银行、华夏基金等官网就是用JSP编写的。
5.1.2JSP基本页面结构
JSP文件的扩展名是.jsp,基本页面结构如下: 


<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<html>

<head>

<title>Title</title>

</head>

<body>

<%

out.print("Hello,JSP!");

%>

</body>

</html>



整个页面可以概要地划分成三大部分: 
(1) JSP指令部分。
第1行代码,属于JSP的页面指令。该指令一般放在第1行,指明了页面内容是纯文本的HTML,字符集编码为UTF8,使用的语言是Java。
(2) JSP代码部分。
粗体部分代码,就是JSP代码,用于执行各种JSP操作。
(3) HTML代码部分。
除了第1行代码、粗体部分代码,其他都属于HTML代码内容。当然,也可以加入前面学习过的CSS和JS等内容。
5.1.3配置Tomcat依赖
为了后续示例方便,先创建好一个Maven Webapp项目chapter5(若忘记创建方法,请回看第1章内容)。
由于我们使用Tomcat作为JSP的应用服务器,因此需要在项目中加入Tomcat依赖。打开项目pom.xml文件,输入Tomcat依赖代码: 


<dependency>

<groupId>org.apache.tomcat</groupId>

<artifactId>tomcat-catalina</artifactId>

<version>9.0.50</version>

</dependency>



注意添加的位置!单击图51中画圈处的Load Maven Changes按钮,重新加载。



图51添加Tomcat依赖


5.2JSP基本语法
5.2.1程序段

这里所说的程序段,是指包含变量、方法、表达式等被<%和%>括起来的部分,不包含前面5.1.2节中所说的JSP指令。如果要在项目中创建test.jsp,代码如下: 


<body>

<%

int sum = 0;

for (int i = 0; i <= 100; i++)

sum += i;

%>

<b>计算结果: </b><%=sum%>

</body>



这段代码中,JSP程序段和HTML代码混在一起。启动Tomcat,在浏览器中打开页面: http://localhost:8080/chapter5/test.jsp,将在浏览器中输出“计算结果: 5050”。
5.2.2表达式
JSP表达式可以将数据转换成字符串,直接输出到页面,其语法格式如下: 


<%=表达式%>


注意: 中间不能有任何空格和分号。
示例: 使用表达式动态输出页面链接。


<body>

<%

String[] url = new String[3];

url[0] = "https://www.hust.edu.cn";

url[1] = "https://www.whu.edu.cn/";

url[2] = "index.jsp";

%>

<a href="<%=url[0]%>" target="_blank">华中科技大学</a>

<a href="<%=url[1]%>" target="_blank">武汉大学</a>

<a href="<%=url[2]%>" target="_self">首页</a>

</body>



5.2.3JSP中的注释
注释是对程序的说明性文字,为程序的可读性及日后系统的维护带来极大方便。JSP注释不会执行,也不会发送给用户,即用户是看不到的。


格式1: <%--注释内容--%>

格式2: <%//注释内容%>


下面是一个完整的JSP页面,包含了两种格式的注释。加粗字体是格式1注释,有下画线的是格式2注释: 


<%--

编写者: 杨过

说明: 示例程序

--%>

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%--导入日期处理相关的类--%>

<%@ page import="java.text.SimpleDateFormat" %>

<%@ page import="java.util.Date" %>

<html><head>

<title>注释示例</title>

</head>

<body>

<%--显示图片、格式化的日期--%>

<%

out.println("<img src='images/m0.png'>");//显示图片

Date today = new Date();

//格式化

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

//输出到页面

out.print("<h2>" + sdf.format(today) + "</h2>");

%>

</body></html>



通过注释,即使不熟悉相关代码的人,也可以很快理解代码的含义,提高了可读性。
5.3JSP内置对象
内置对象是隐含对象,无须声明和创建,可以直接使用。
5.3.1out
out对象主要用于向客户端发送数据,在前面其实已经接触到。out对象提供了很多方法,这里介绍其中的2个常用方法。

 print()/println(): 输出数据/输出数据后换行。

 close(): 关闭输出流。
示例: 页面停顿3s后返回主页。


<body>

<%

out.print("<span style='color:#f00'>请稍候,3秒后返回到主页......</span>");

out.print("<script>");①

out.print("setTimeout(\"location.href='index.jsp'\",3000);");②

out.print("</script>");

out.close();

out.println("<b>欢迎再回来!</b>");③




%>

</body>



从上面代码可以体会到out对象的强大输出功能: 不但可以输出普通文字,还可以输出HTML的各种标签,例如<span>,甚至可以输出JS脚本代码!
①  输出<script>脚本声明。
②  输出JS代码的setTimeout()函数,停顿3s后,通过location.href将页面转向index.jsp。
③  这句欢迎再回来的文字,不会在页面显示,因为前面的out.close()已经关闭输出了。
提示: 尽管可以在JSP代码中用out输出JS,但不建议这样做,这会导致代码的可读性变差,日后的维护也会极其困难。限于我们目前掌握的知识有限,目的只是演示一下out的强大输出能力而已。
5.3.2request
请求对象request可获取通过HTTP传送到客户端的数据。下面是request的常用方法。

 getRequestURI(): 获得所请求的URL地址。

 getServerName(): 获得服务器名称。

 getServerPort(): 获得服务器提供的HTTP服务的端口号。

 getRemoteAddr(): 获得IP地址。

 setCharacterEncoding("编码类型"): 设置请求的编码类型。

 getRemoteHost(): 获得主机名,一般为IP地址。

 setAttribute(String s,Object o): 创建一个新属性并赋值为o。

 getAttribute(String s): 获取指定属性名的值。

 getParameter(String name): 获得客户端传给服务器端的参数值。

 getRequestDispatcher(String var): 将请求转发到由var指定的页面。

 request.getParameterMap(): 获得客户端请求数据的键值对。
示例1: 在主页index.jsp中打印相关信息。


<body>

<%

String uri = request.getRequestURI();

String serverName = request.getServerName();

int port = request.getServerPort();

String ip = request.getRemoteAddr();

String host = request.getRemoteHost();

out.print(uri + " " + serverName + " " + port + " " + ip + " " + host);

%>

</body>



页面上将显示如下内容: 


/chapter5/index.jsp localhost 8080 127.0.0.1 127.0.0.1


示例2: 制作包含用户名、密码的表单页面login.jsp,提交给doLogin.jsp后显示用户输入的用户名、密码。
先看login.jsp的主要代码: 


<body>

<form name="form1" method="post" action="doLogin.jsp">

用户名<input type="text" name="usr">

密 码<input type="password" name="pwd">

<button>登 录</button>

</form>

</body>



大家对代码应该比较熟悉: 通过表单,以post模式提交用户名、密码数据给doLogin.jsp。再看doLogin.jsp的代码: 


<body>

<%

request.setCharacterEncoding("utf-8");  //必须,否则用户名是中文时,会输出乱码

String username = request.getParameter("usr"); //接收客户端传送的用户名

String password = request.getParameter("pwd"); //接收客户端传送的密码

out.print("你输入的用户名: " + username + " 密码: " + password);

%>

</body>



这个例子,显示了数据在不同页面间传送,以及服务端进行接收的基本方法。
5.3.3response
响应对象response将数据从服务器端发送到客户端,以响应客户端的请求。下面是response的几个常用方法。

 addHeader(String name, String value): 添加指定名称和值的HTTP文件头信息。



图52响应为Word文档

 setHeader(String name, String value): 设置指定名称和值的HTTP文件头信息。

 sendRedirect(String url): 重定向到url地址,即打开url所代表的网页。

 sendError(int code): 发送错误信息编码,例如常见的“404”表示网页找不到。
示例1: 将JSP中的古诗自动下载为Word文档,效果如图52所示。
由于下载为Word文档,所以需要设置HTTP头,指示浏览器,将文件内容作为Word文档下载。首先,需要设置JSP响应内容为“application/msword;charset=GB18030”,其中GB18030在第1章接触过。然后,还需要设置HTTP的ContentDisposition响应头,指示浏览器页面内容的展示形式: inline(内联形式,即作为网页的一部分)、attachment(附件形式,下载并保存到本地)。显然,我们需要设置为附件形式,并指定文件名为poem.doc: 


response.setHeader("Content-Disposition", "attachment;filename=poem.doc");



新建文件mypoem.jsp,主要代码如下: 


<body>

<%

response.setContentType("application/msword;charset=GB18030");

response.setHeader("Content-Disposition", "attachment;filename=poem.doc");

out.print("<b>芙蓉楼送辛渐</b><br/>");

out.print("【唐】王昌龄<br/>");

out.print("寒雨连江夜入吴,<br/>");

out.print("平明送客楚山孤。<br/>");

out.print("洛阳亲友如相问,<br/>");

out.print("一片冰心在玉壶。");

out.close();

%>

</body>



在浏览器地址栏输入http://localhost:8080/chapter5/mypoem.jsp,将自动下载为Word文件poem.doc。
示例2: 根据登录用户名、密码的不同,打开不同页面。修改上5.3.2节中的doLogin.jsp文件: 当用户名为admin、密码为007时,打开admin.jsp页面,否则显示“用户名或密码错误,登录失败!”。
只需要修改doLogin.jsp的代码如下: 


<%

request.setCharacterEncoding("utf-8");

String username = request.getParameter("usr");

String password = request.getParameter("pwd");

if (username.equals("admin") && password.equals("007"))

response.sendRedirect("admin.jsp");

else

out.print("用户名或密码错误,登录失败!");

%>



5.3.4session
会话session代表服务器与客户端所建立的状态信息。由于HTTP协议是无状态协议,即服务器并不知道客户端的状态,例如用户登录后,直接关闭了浏览器,这时候服务器并不知道。另外,购物时,当在A页面购买一双鞋子,再到B页面去购买袜子,这时候因为HTTP无状态性质,无法知道A页面购买了什么,因此需要session来保存客户端状态信息。每个打开网站的用户,都有自己私有的session。下面是session的几个常用方法。

 setAttribute(String s,Object o): 设置session属性名为指定值。

 getAttribute(String s): 获取指定属性名的session值。

 removeAttribute(String s): 删除指定属性名的session。

 invalidate(): 使session失效。

 isNew(): 是否为新的session。

 setMaxInactiveInterval(int time): 设置session的有效期限,单位为秒。
示例: 在主页index.jsp中放置三个链接: 用户登录,链接到login.jsp; 用户注销,链接到logout.jsp; 文件下载,链接到download.jsp,登录成功的用户才可以打开该链接,否则弹框提示需要先登录。
我们用session在不同页面之间分享信息,下面是主要代码。
1. index.jsp


<body>

<a href="login.jsp">用户登录</a>

<a href="logout.jsp">用户注销</a>

<%

Object loginer = session.getAttribute("loginer");//loginer用于记录用户登录状态

if (loginer == null || !loginer.equals("sucess"))//success表示登录成功状态

out.print("<a href='#' onclick=alert('请先登录!')>文件下载</a>");

else

out.print("<a href='download.jsp')>文件下载</a>");

%>

</body>



大家应该已经注意到,文件下载链接是根据session的值动态生成的。
2. login.jsp
与前面5.3.2节中的一样。
3. doLogin.jsp


<%

request.setCharacterEncoding("utf-8");

String username = request.getParameter("usr");

String password = request.getParameter("pwd");

out.print("<script>");

if (username.equals("admin") && password.equals("007")) {

session.setAttribute("loginer", "sucess"); //用session设置用户登录状态

out.print("alert('登录成功!');");

} else

out.print("alert('用户名或密码错误,登录失败!');");

out.print("location.href='index.jsp'"); //转向打开主页

out.print("</script>");

%>



4. logout.jsp


<%

session.invalidate(); //令session失效

response.sendRedirect("index.jsp"); //转向主页

%>



5.3.5application
服务器启动后,会自动创建application对象。与session属于每个用户独有不同,application存放公共数据,网站的所有用户都可存取其数据,也就是说application是网站所有用户共享的。下面是application的三个常用方法。

 setAttribute(String s,Object o): 设置指定名称、值的application对象。

 getAttribute(String s): 获取指定属性名的application值。

 removeAttribute(String s): 删除指定属性名的application。


图53访问量计数器

示例: 用application实现网站访问量,如图53所示。
这个计数器的每位数字,用<span>显示,并用CSS定义了一个样式numSpan。JSP代码如下: 


<%

if (application.getAttribute("counter") == null) { ①

application.setAttribute("counter", "6357438");

}

String counter = application.getAttribute("counter").toString(); ②

int i = Integer.parseInt(counter);

i++;

application.setAttribute("counter", i); ③

out.print("您是第");

for (int j = 0; j < counter.length(); j++) { ④

char c = counter.charAt(j);

out.println("<span class='numSpan'>" + c + "</span>");

}

out.print("位访问者!");

%>


说明: 
①  服务器启动后,首次访问主页时,counter并不存在,所以设置并赋初值。这里为了演示效果,设置了一个比较大的数。
②  获取当前counter的值,并转换为字符串。注意application.getAttribute("counter")返回的是Object对象。
③  访问量加1后,要记住这个新的值,所以给counter重新赋以新值。
④  遍历总访问量字符串中的每位数字,用CSS修饰过的<span>显示在页面上。
样式numSpan代码如下: 


<style>

.numSpan {

border-radius: 50%;

text-align: center;

display: inline-block;

color: #fff;

background-color: #f15555;

width: 1.8em;

height: 1.7em;

line-height: 1.7em;

font-size: 0.6em;

}

</style>



这个计数器其实有两个问题: 
(1) 一旦服务器停止运行,再次启动时计数器将还原为初值; 
(2) 每刷新一次页面,计数器将加1。第一个问题,可以用以后的数据库知识解决: 将访问量存放到数据库里面,从数据库里面读取,这样一来服务器的启停就毫无影响了。至于第二个问题,用本章前面所学知识,即可完美解决。作为练习,请大家思考完成。
5.4使用Servlet
5.4.1Servlet简介

Servlet是基于多线程技术、运行于服务端的Java类。由于Servlet是编译好的类,运行速度比JSP文件要快,因为JSP文件最终都要被Tomcat服务器编译成Servlet再运行的。这个动态编译的过程,是需要消耗时间的。
在浏览器中打开站点主页index.jsp,实际上Tomcat是这样处理的: 先将index.jsp的内容自动生成名为index_jsp.java的Servlet文件,然后再编译成index_jsp.class,最终运行这个index_jsp.class。
如果将chapter5项目发布到Tomcat的webapps下,启动站点后打开目录D:\apachetomcat9.0.50\work\Catalina\localhost\chapter5\org\apache\jsp,就可以看到index_jsp.java和index_jsp.class这两个文件。
5.4.2Servlet生命周期
Servlet从被服务器创建到销毁的过程称为生命周期。整个生命周期,如图54所示。



图54Servlet生命周期


对读者而言,日常应用会更多地关注于Servlet的响应处理以及输出响应信息的过程。这会涉及Servlet的两个重要方法。
5.4.3doGet()和doPost()方法
doGet()方法用来处理客户端的get方式请求,而doPost()自然是处理来自客户端的post请求。关于get、post这两种方式的区别,请参阅第2章2.4.6节的内容。这两个方法,都是HttpServlet抽象类的内部方法,需要我们覆盖这两个方法。形式如下: 


@Override

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

//在这里完成我们的各种任务

}

@Override

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

doGet(request, response);

}



读者可能注意到: 在doPost()方法里面调用了doGet()方法!这是一种增强Servlet适应性的技巧。这样一来,我们的任务代码,只需要写在doGet()方法里面。无论用户采用get还是post方式提交数据,无须修改代码,Servlet程序都能适应。
5.4.4加入Servlet依赖
要方便创建Servlet,需要加入Servlet依赖。打开pom.xml,加入以下代码并更新Maven: 


<dependency>

<groupId>javax.servlet</groupId>

<artifactId>javax.servlet-api</artifactId>

<version>4.0.1</version>

<scope>provided</scope>

</dependency>



5.4.5创建Servlet


图55新建java文件夹

我们需要创建一个包servlets,用来存放Servlet文件。先来创建java文件夹,这个文件夹是必需的,以后创建其他各种包,都必须放在java文件夹下。创建方法: 在项目的src→main上右击,再选择New→Directory,在弹出的对话框中选择java即可,如图55所示。
现在,在java文件夹上右击鼠标,再选择New→Package,完成servlets包的创建。
示例1: 创建名为GoHome的Servlet,在页面上显示“请稍候,3s后返回主页……”。过了3s,自动打开主页index.jsp。
(1) 在servlets包上右击鼠标,再选择New→Servlet,如图56所示。
(2) 弹出New Servlet对话框。在Name框中输入GoHome,再单击OK按钮完成创建,如图57所示。



图56新建Servlet





图57输入Servlet名称


(3) 完整代码如下: 


@WebServlet(name = "GoHome", value = "/toHome")①

public class GoHome extends HttpServlet {②

@Override

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws 
ServletException, IOException {

response.setContentType("text/html;charset=utf-8");③

PrintWriter out = response.getWriter();④

response.setHeader("refresh", "3;URL=index.jsp");⑤

out.println("<span style='color:#ff0000;font-size:0.8em'>请稍候,3s后自动返回主页……</span>");

out.close();

}

@Override

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws 
ServletException, IOException {

doGet(request, response);

}

}


说明: 
①  这是一个注解(有些教程称为注释),用来标注该类是一个Servlet,同时设置GoHome的映射名称为toHome。映射,就是将Servlet虚拟成Web网络访问名,这意味着GoHome类的Web访问地址是: http://localhost:8080/chapter5/toHome。可以将映射名设置成需要的形式,例如,如果将value = "/toHome"修改成value = "/to/go.html",则该Servlet的访问地址变成: http://localhost:8080/chapter5/to/go.html。实际上,go.html在站点中并不存在,这就是所谓的“映射”。后面,我们还会用到各种注解。
②  所有的Servlet类,包括GoHome,都是HttpServlet抽象类的子类。
③  设置响应内容为纯文本HTML,使用utf8编码。
④  这其实就是我们前面在JSP文件中使用的out对象的真正来源。
⑤  5.3.3节用过setHeader()方法,这里通过设置HTTP响应头的refresh,达到3s后自动刷新并打开index.jsp的效果。
示例2: 修改前面5.3.4节的用户登录处理,用Servlet类DoLogin(映射名login.do)来完成登录处理。
(1) 修改login.jsp,将原action="doLogin.jsp"修改为action="login.do"; 
(2) 新建Servlet类DoLogin.java,代码如下: 


@WebServlet(name = "GoHome", value = "/login.do")

public class DoLogin extends HttpServlet {

@Override

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

response.setContentType("text/html;charset=utf-8");

request.setCharacterEncoding("utf-8");

PrintWriter out = response.getWriter();

String username = request.getParameter("usr");

…

request.getSession().setAttribute("loginer", "sucess");

…

}

}



大部分代码与5.3.4节doLogin.jsp中的代码相同,予以省略。加粗代码是Servlet中设置session属性值的方式,与doLogin.jsp中是有差异的,提请注意。
5.5EL表达式语言
5.5.1EL概述

EL(Expression Language,表达式语言)的目的是使JSP更简单、更易读。EL的使用,需要JSTL(JSP Standard Tag Library,JSP标准标签库)的支持。
EL的使用方式非常简单: 


${表达式}


来看看JSP、EL在页面输出“你好,EL!”的差别。JSP是这样的: 


<body><%out.print("你好,EL!");%></body>



而EL则是这样: 


<body>${"你好,EL!"}</body>


5.5.2加入JSTL依赖
在项目pom.xml中加入JSTL依赖: 


<dependency>

<groupId>javax.servlet.jsp.jstl</groupId>

<artifactId>javax.servlet.jsp.jstl-api</artifactId>

<version>1.2.2</version>

</dependency>

<dependency>

<groupId>org.apache.taglibs</groupId>

<artifactId>taglibs-standard-impl</artifactId>

<version>1.2.5</version>

</dependency>



5.5.3内置对象
EL提供了一些可以直接使用的内置对象,下面列出了一些常用的EL内置对象。

 param: 客户端请求参数对象。

 pageScope: 当前页面属性对象。

 requestScope: 本次请求属性对象。

 sessionScope: 会话对象。

 applicationScope: 应用对象。
后面结合EL的条件输出或循环输出,再举例说明对象的使用方法。
5.5.4条件输出
根据条件是否成立来决定内容的显示与否。
单条件表达式: 


<c:if test="表达式">

…

</c:if>




多条件分支表达式: 


<c:choose>

<c:when test="表达式" >

…

</c:when>

……

<c:otherwise>

…

</c:otherwise>

</c:choose>



这里面的表达式,判断的是服务端的某个变量、对象、session等,下面结合示例来学习其用法。
示例: 用EL表达式改写5.4节中的登录示例。
首先,改写5.4.5节中的DoLogin 类,去掉Servlet中输出JS代码的不合理做法; 然后,修改主页index.jsp: 用户登录操作完毕后,用红色文字显示登录成功与否的提示。
1) 修改DoLogin 类的doGet()方法


@Override

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

request.setCharacterEncoding("utf-8");

String username = request.getParameter("usr");

String password = request.getParameter("pwd");

if (username.equals("admin") && password.equals("007"))//登录成功

request.getSession().setAttribute("loginer", "sucess");

else//登录失败

request.getSession().setAttribute("loginer", "fail");

response.sendRedirect("index.jsp"); //转向打开主页

}



现在代码简洁了很多。在服务端用session属性loginer记住用户登录状态: 若登录成功,其值设置为sucess; 登录失败则设置为fail。在前端,JSP页面用EL表达式,取出session的值,并进行判断。
2) 修改主页index.jsp
为了方便与5.3.4节中的代码进行比较,下面给出了完整代码: 


<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%@ page isELIgnored="false" %>①

<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<html>

<head><title>主页</title></head>

<body>

<a href="login.jsp">用户登录</a>

<a href="logout.jsp">用户注销</a>




<c:choose>②

<c:when test="${sessionScope.loginer=='sucess'}">③

<a href='download.jsp'>文件下载</a>

<span style="color: #FF0000"><br/>登录成功!</span>

</c:when>

<c:when test="${sessionScope.loginer!='sucess'}">④

<a href='#' onclick=alert('请先登录!')>文件下载</a>

<c:if test="${sessionScope.loginer=='fail'}">

<span style="color: #FF0000"><br/>用户名或密码错误,登录失败!</span>

</c:if>

</c:when>

</c:choose>

</body></html>


说明: 
①  这句页面指令,启用EL表达式。紧跟的下面这句指令,则定义使用JSTL标签库。要在JSP页面使用EL,不要忘了放置这两条加粗字体的指令!
②  使用多条件分支表达式,进行登录成功与否判断,也可以改为<c:if>语句。
③  利用EL内置对象中的sessionScope,获取后台Servlet类DoLogin中设置好的loginer值,进行判断。这里也可简写为: <c:when test="${loginer=='sucess'}">。
④   loginer!='sucess',包含两种情况: 用户压根就没进行登录操作,这时候loginer为null值,不能在页面显示登录失败的类似字样; 用户进行了登录操作,但失败,这时候loginer值为fail。


图58登录成功


图58是登录成功后的效果图。使用EL表达式后,index.jsp页面不再有任何JSP代码块,可读性更好。请大家与5.3.4节中的index.jsp进行比较,充分理解代码的含义。
5.5.5循环输出
循环输出语法格式如下: 


<c:forEach var="变量" [items="集合"][begin="起始位置"][end="结束位置"] [step="步长"]>

…

</c:forEach>



中括号[]中的内容是可选的。下面的示例,动态输出5行文本框: 


图59动态生成文本框



<c:forEach var="i" begin="1" end="10" step="2">

文本框${i} <input type="text" name="txt${i}" placeholder=
"EL${i}…"><br/>

</c:forEach>


在网页中的效果如图59所示。

5.6监听器
5.6.1监听器类型

监听器(Listener)是一种特殊的Servlet类,专门用来监听Web应用或特定Java对象的初始化或销毁、方法调用、属性改变等事件。当被监听者发生相应事件后,监听器会立即执行事先实现的方法。主要有以下三种类型的监听器。

 ServletContextListener: Web应用监听器。主要有两个方法——contextInitialized(),Web应用发布时调用此方法,可在此方法中做一些初始化工作; contextDestroyed(),Web应用被注销或服务器停止时被调用,可在此方法中做一些扫尾工作。在第7章会使用这个监听器。

 HttpSessionListener: 会话session监听器。主要有两个方法——sessionCreated(),创建session调用; sessionDestroyed(),销毁session时调用。

 HttpSessionAttributeListener: session属性监听器。主要有三个方法——attributeAdded(),向session中添加新属性时调用; attributeRemoved(),从session中移除属性时被调用; attributeReplaced(),session属性被替换时调用。
5.6.2基于监听器的在线用户统计
用监听器统计在线用户,基本思想是: 先定义一个HashMap对象,存放到application对象里面。当有用户打开站点时,sessionCreated()被触发,


图510创建监听器

就可以在sessionCreated()里面将代表该用户的session id存入HashMap对象里面。
若用户超时导致session失效,会触发sessionDestroyed(),可在此将对应的session id从HashMap对象中移除。这样,HashMap对象的容量大小,就表示在线用户的数量,而JSP页面通过application.getAttribute()即可获取到该数量值。
在servlet包上面右击鼠标,在弹出的快捷菜单中选择New→Web Listener,输入监听器类名称AppListener,如图510所示。
修改AppListener类,主要代码如下: 


@WebListener //注解为监听器

public class AppListener implements ServletContextListener, HttpSessionListener { 
①

private Map<String, HttpSession> map = new ConcurrentHashMap(1);②



@Override

public void contextInitialized(ServletContextEvent sce) {

sce.getServletContext().setAttribute("online", map);③

}

@Override




public void contextDestroyed(ServletContextEvent sce) {

map.clear(); //清除内容

}

@Override

public void sessionCreated(HttpSessionEvent se) {

HttpSession session = se.getSession(); 获取session对象

session.setMaxInactiveInterval(60*20);④

map.put(session.getId(), session);  //当前session存入map

}

@Override

public void sessionDestroyed(HttpSessionEvent se) {

map.remove(se.getSession().getId());//移除session

}

}


说明: 
① 只需要使用两个监听器类: Web应用监听器、会话session监听器。
②  新建一个线程安全、支持高并发的ConcurrentHashMap对象,以便保存session。这里将HashMap对象初始值设为1,后面往HashMap里面put内容时,其容量会自动增长。
③  设置application对象属性online,这样前端JSP页面就可通过EL内置对象applicationScope获取到online的值。
④  设置session的有效期限为20min。在前面学习过HTTP是无状态协议,只好通过session记录状态信息。有些网站在用户登录后,基于安全考虑,会设置了一个有效时间期限,超限则予以自动注销,就是基于设置session有效期限的基本原理。
现在,只需要在JSP页面简单使用下面的语句,即可显示在线用户数。


在线用户数: ${applicationScope.online.size()}


提示: 为了保证在线用户数的准确性,建议大家将站点发布到Tomcat下运行,而不是在IntelliJ IDEA里面运行测试。
5.7与数据库交互
数据库是计算机技术领域最重要的发展产物,是大多数信息管理系统的基础框架,是现代应用最广泛的技术之一。很多网站的背后,都有数据库技术的支持。
5.7.1创建users表并加入数据库依赖
1. 创建users表

先设计一个用户表users,以便后续使用,其结构如图511所示。这些字段分别表示用户名、密码、用户头像、角色(例如teacher、student)、email地址。



图511users表结构


2. 加入数据库依赖
要与数据库交互,需要在pom.xml中加入以下数据库依赖: 


<dependency>

<groupId>org.postgresql</groupId>

<artifactId>postgresql</artifactId>

<version>42.2.23</version>

</dependency>



5.7.2数据库连接
1. 连接原理

按照第1章确定的技术环境,连接数据库的过程如图512所示。



图512数据库连接原理


从图可知,ODBC或JDBC驱动相当于Web应用与数据库之间的“桥梁”。由于ODBC(Open Database Connectivity,开放数据库连接)连接数据库的效率低于JDBC,且影响了跨平台特性,现实中使用较少,这里就不做介绍了。JDBC(Java DataBase Connectivity,Java 数据库连接)是由原Sun公司提供的统一的标准应用程序编程接口,能够与各种数据库进行交互。
2. 驱动程序
JDBC驱动程序通常是一个或多个jar文件,它实际上是封装数据库访问的类库。驱动程序名一般采用“包名.类名”的形式。例如: 

 org.postgresql.Driver: PostgreSQL数据库。其中,org.postgresql是包名,而Driver才是真正的驱动程序类。

 com.mysql.jdbc.Driver: MySQL数据库。

 com.microsoft.jdbc.sqlserver.SQLServerDriver: SQL Server数据库。
驱动程序需要进行如下加载后才能使用。


Class.forName("org.postgresql.Driver");


3. 连接地址
建立连接还需提供数据库地址URL、能够访问数据库的用户(含用户名、密码)。URL一般由三部分组成,各部分间用冒号分隔,格式如下: 


jdbc:<子协议>:<子名称>


<子协议>是指数据库连接机制名。不同数据库厂商的数据库连接机制名是不同的,例如MySQL数据库使用的是mysql,PostgeSQL数据库使用的是postgresql。<子名称>提供定位数据库的更详细信息,包括服务器、端口、数据库名等。例如: 

 jdbc:postgresql://localhost:5432/tamsdb: 通过5432端口连接本地服务器上的PostgreSQL数据库tamsdb。

 jdbc:mysql://localhost:3306/mydb: 通过3306端口连接本地服务器上的MySQL数据库mydb。

 jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=jspdb: 通过1433端口连接本地服务器上的SQL Server数据库jspdb。
5.7.3JDBC应用
1. 驱动管理器DriverManager

DriverManager是JDBC的管理层,作用于用户和驱动程序之间,负责加载驱动程序,建立数据库和驱动程序之间的连接。DriverManager有三种使用格式: 

 DriverManager.getConnection(String url)

 DriverManager.getConnection(String url, String user, String password)

 DriverManager.getConnection(String url,Properties info)
示例: 


Class.forName("org.postgresql.Driver");

String url = "jdbc:postgresql://localhost:5432/tamsdb";

Connection conn = null;

conn = DriverManager.getConnection(url, "admin", "007");



或者: 


Properties prop = new Properties();

prop.put("user", "admin");

prop.put("password", "007");

conn = DriverManager.getConnection(url, prop);



2. 连接Connection
一个连接对象代表与特定数据库的会话。Connection的重要方法如下。

 close(): 关闭连接。

 createStatement(): 创建会话声明对象。

 prepareStatement(String sql): 预编译SQL语句。

 setAutoCommit(boolean autoCommit): 设置Connection是否处于自动提交模式,默认为自动提交。如果是自动提交,则所有的SQL语句将被立即执行。否则,SQL语句整体作为一个数据库的事务,由commit()提交,由rollback()撤销。

 commit(): 提交事务。

 rollback(): 回滚事务。撤销当前事务处理所做的任何改变,回到最初的状态。
3. 会话Statement和PreparedStatement
Statement代表发送SQL语句的一次会话,常用方法有以下几种。

 close(): 关闭会话。

 executeQuery(String sql): 执行SQL语句并返回结果集。

 setFetchSize(int rows): 设置从数据库预读取rows条记录。

 setMaxRows(int max): 设置从数据库返回结果中结果集的最大行号数为max。

 executeUpdate(String sql): 执行数据更新(update、delete、insert)SQL语句。

 addBatch(String sql): 将SQL语句加入批量更新列表。批量更新可以提高程序执行效率,优化数据处理过程。

 executeBatch(): 执行批量更新。
PreparedStatement是Statement的子类,可存储一条预编译的SQL语句,并可高效、多次执行该语句。PreparedStatement在一些场景应用中比Statement执行效率更高。实践中,对于重复性发生的业务,使用预编译有性能上的优势,例如用户登录,这是一种操作没变只是操作的数据发生变化的业务,建议使用预编译方法。
使用Statement: 


conn = DriverManager.getConnection(url, "admin", "007");

Statement s = conn.createStatement();



第2句代码可以换成预编译方式: 


String sql = " select username,logo,role from users";

PreparedStatement ps = conn.prepareStatement(sql);



4. 结果集
通过SQL语句查询数据库后,获得需要的数据结果(结果集,或称记录集),然后再根据业务要求进行各种处理后,将数据返回给前端JSP页面进行显示。获取结果集ResultSet的方法如下: 


Statement s = conn.createStatement();

ResultSet rs = s.executeQuery("select username,logo,role from users");


或者: 


String sql = "select username,logo,role from users";

PreparedStatement ps = conn.prepareStatement(sql);

ResultSet rs = ps.executeQuery();


拿到结果集后,就可以对其中的每个对象(对应于数据表中的每条记录)进行操作。ResultSet提供了许多getXXX()方法,来获取当前行对象的属性值。这里的XXX,表示要根据属性的类型,选择相应的方法。例如:  


String sname =rs.getString(1); //获取sname姓名

String sex =rs.getString("sex"); //获取性别

int score= rs.getInt(3); //获取score分数,也可以用rs.getInt("score")



一般来说,使用索引方式(获取姓名代码)而非名称(获取性别代码)执行速度更快。与此相对应,ResultSet提供了许多setXXX()方法来设置属性值。除了getXXX()、setXXX()方法外,表51列出了ResultSet其他常用方法。


表51ResultSet常用方法




方法功 能 说  明


boolean next()移动到下一行并判断是否到末尾
boolean absolute(int row)移动到结果集指定行
void close()关闭结果集
void deleteRow()删除当前行
void first()移动到结果集的第1行
int getRow()获得当前行的行号
void insertRow()插入新行
void updateRow()更新当前行

示例1: 循环遍历users表的查询结果集并输出。


String sql = "select username,password from users";

PreparedStatement ps = conn.prepareStatement(sql);

ResultSet rs = ps.executeQuery();

while (rs.next()) {//循环遍历

out.print(rs.getString(1) + " " + rs.getString(2)); //输出用户名、密码

}

rs.close(); //关闭结果集

ps.close();



示例2: 修改用户aaa的密码为123456。


String sql="update users set password='123456' where username='aaa'";

PreparedStatement ps=myconn.prepareStatement(sql);

ps.executeUpdate();



示例3: 修改5.5.4节DoLogin 类的doGet()方法,实现基于数据库的用户登录。
只需要修改doGet()方法的代码如下: 


protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {

request.setCharacterEncoding("utf-8");

String username = request.getParameter("usr");

String password = request.getParameter("pwd");

request.getSession().setAttribute("loginer", "fail");①

String url = "jdbc:postgresql://localhost:5432/tamsdb";

Connection conn = null;

try {

Class.forName("org.postgresql.Driver");//加载JDBC驱动程序

conn = DriverManager.getConnection(url, "admin", "007");//建立连接

String sql = "select * from users where username=? and password=?";②

PreparedStatement ps = conn.prepareStatement(sql);

ps.setString(1, username);③

ps.setString(2, password);

ResultSet rs = ps.executeQuery();

if (rs.next()) //查找到记录,登录成功

request.getSession().setAttribute("loginer", "sucess");

rs.close();

ps.close();

} catch (Exception e) { e.printStackTrace();

} finally {

if (conn != null) {

try { conn.close();

} catch (SQLException e) { e.printStackTrace();}

}

response.sendRedirect("index.jsp");

}

}


说明: 
①  用session记录登录状态,默认假定用户登录失败。
②  定义查询的SQL语句。这里的问号?是占位符,不妨这样理解: 当不清楚用户名、密码的具体值时,用问号占位,等待后续传值过来。
③  将前面接收的用户名,传值给第1个问号占位符。





视频讲解


5.8场景应用示例
5.8.1文件上传

文件上传是Web应用中很常见的功能,一般采用第三方上传组件来实现。读者也可以自力更生实现文件上传。
1. 上传数据的编码格式
浏览器在上传文件时,是按照一定数据编码格式进行的: 传送数据时在头尾部分附加了额外信息,具体如下: 


-----------------------------7d429871607fe

Content-Disposition: form-data;name="表单控件名";filename="上传文件的路径和文件名"

Content-Type: text/plain

上传文件的内容……

-----------------------------7d429871607fe 

Content-Disposition: form-data; name="filename" Content-Type: application/octet-stream

-----------------------------7d429871607fe--



在传送数据的头部、尾部都辅以分割线,并附加了ContentDisposition信息,例如formdata、表单文件控件名、上传文件名等。如果自己编写文件上传处理,将数据写入服务器时,要忽略掉这些附加数据。
2. 编写上传JSP文件upload.jsp


<Script language="javascript">

function check() {

if (document.uploadForm.myfile.value == "") {

alert("请选择待上传文件!");

return false;

}

return true;

}

</Script>

<form name="uploadForm" method="post" action="upload.do"

enctype="multipart/form-data" onsubmit="return check()">

请选择待上传文件<input type="file" id="myfile" name="myfile"><br>

<input value="开始上传" type="submit">

</form>



这个表单与常规纯文本处理的表单稍有不同,请注意粗体部分的代码,用于定义文件传输的HTTP信息。另外,<input>的类型是file。
3. 编写Servlet类FileUpload


@WebServlet(name = "FileUpload", value = "/upload.do")//映射为upload.do

@MultipartConfig //配置上传文件支持

public class FileUpload extends HttpServlet {

@Override

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

request.setCharacterEncoding("utf-8");

response.setContentType("text/html;charset=UTF-8");

String destPath = request.getServletContext().getRealPath("/upfiles");①

Part part = request.getPart("myfile"); // myfile对应表单的文件输入框

String header = part.getHeader("content-disposition");

String fileName = getFileName(header);//获得文件名

part.write(destPath + File.separator + fileName);//写入服务器

response.getWriter().print("文件上传成功!");

}

private String getFileName(String header) {//获取文件名

String fieldName = null;

if (header != null && header.startsWith("form-data")) {

int start = header.indexOf("filename=\"");②

int end = header.indexOf('"', start + 10); ③

if (start != -1 && end != -1) {

fieldName = header.substring(start + 10, end);

}

}

return fieldName;

}

}


说明: 
①  上传的文件都存放到站点upfiles文件夹下,事先需要在webapp下创建好该文件夹。
②  找到头部附加信息中“filename=”的位置,目的是从filename值中获取到文件名。
③  从“filename=”后面一个字符开始(因为“filename=”长度为9),找到filename属性值字符串,即文件名。
5.8.2在页面中显示Excel表格 
使用Servlet显示上一节上传的Excel表格文件。图513为上传的Excel文件reimb.xls,图514则是Servlet输出到网页后的效果。二者样式上会有些许差异,但整体还算不错。



图513Excel报销单





图514网页显示Excel报销单


1. 加入POI依赖
对Excel的处理,需要用到Apache软件基金会的著名Excel处理工具POI。POI提供了最完整、最正确的Excel格式读取实现,并采用了内存优化方式处理Excel表格,可以帮助读者轻松读写Excel文件。POI免费、开源,可到其官网下载。在pom.xml中添加依赖项: 


<dependency>

<groupId>org.apache.poi</groupId>

<artifactId>poi</artifactId>

<version>5.0.0</version>




</dependency>

<dependency>

<groupId>org.apache.poi</groupId>

<artifactId>poi-scratchpad</artifactId>

<version>5.0.0</version>

</dependency>



2. 编写Servlet处理Excel


@WebServlet(name = "WebExcel", value = "/excel")   //地址映射为excel

public class WebExcel extends HttpServlet {



@Override

protected void doGet(HttpServletRequest request, HttpServletResponse response)

throws IOException {

String excelFile = request.getServletContext().getRealPath("upfiles/reimb.xls");

HSSFWorkbook wb = ExcelToHtmlUtils.loadXls(new File(excelFile));

wb.setSheetName(0, " ");//去掉Excel Sheet名称

writeToHTML(response, wb);

wb.close();

}



private void writeToHTML(HttpServletResponse response, HSSFWorkbook wb) {

response.setContentType("text/html;charset=UTF-8");

try {

Document doc = XMLHelper.newDocumentBuilder().newDocument();

ExcelToHtmlConverter converter = new ExcelToHtmlConverter(doc);

converter.setOutputColumnHeaders(false);//不显示列头

converter.setOutputRowNumbers(false);//不显示行号

converter.processWorkbook(wb);



Properties prop = new Properties();

prop.setProperty("encoding", "UTF-8");//设置编码

prop.setProperty("indent", "yes");//缩进

prop.setProperty("method", "html");//转换模式: html

Transformer tf = TransformerFactory.newInstance().newTransformer();

tf.setOutputProperties(prop);

tf.transform(new DOMSource(converter.getDocument()),

new StreamResult(response.getOutputStream())); //数据流输出

} catch (Exception e) {

e.printStackTrace();

}

}

}



这里只是结合Servlet,用一个小示例对POI抛砖引玉。实际上,POI还可以帮助创建、读取或输出Excel文件,具有强大的Excel处理能力,读者需要在实践中不断探索、掌握其用法。
注意: 上述方法只能处理xls文件,对于xlsx则需要更复杂的方法。


视频讲解


5.8.3用PDF显示古诗
1. 应用需求

在前面已经领略到了Servlet强大的处理能力,接下来拓展其应用、加深理解: 用Servlet在页面以PDF方式显示一首古诗。PDF文件的应用非常广泛,很多网站常常使用PDF向用户展示内容。若要方便处理PDF文档,需要iText配合使用。
2. iText简介
iText是面向Java、.NET的强大的PDF处理工具,提供了一套通用、可编程和企业级PDF解决方案,并可嵌入到其他各类应用中,例如与Servlet结合。
iText官网提供了开源免费版和商业版两种形式。
3. 加入iText依赖
要使用iText,需先在pom.xml中加入依赖: 


<dependency>

<groupId>com.itextpdf</groupId>

<artifactId>itext7-core</artifactId>

<version>7.1.16</version>

<type>pom</type>

</dependency>



4. 中文问题
iText对中文支持不尽如人意。尽管官方也提供了处理中文的方法,但是略显遗憾的是,少数情况下某些中文会出现乱码。一个可行的解决方法是: 使用Windows自带的字体来支持中文显示。只需要以下两个步骤。
(1) 在项目webapp下新建文件夹font,用于存放字体文件; 
(2) 将C:\Windows\Fonts文件夹下的某个字体文件,例如华文彩云(STCAIYUN.TTF),复制到font文件夹下。
下面,就可以用这个字体文件,以PDF文件格式显示一首古诗。
5. 具体实现
在servlets包下新建Servlet类PdfPoem.java,主要代码如下: 


@WebServlet(name = "PdfPoem", value = "/pdf")  //地址映射为pdf

public class PdfPoem extends HttpServlet {

@Override

protected void doGet(HttpServletRequest request, HttpServletResponse response) 

throws IOException {

ServletOutputStream stream = response.getOutputStream();

PdfWriter writer = new PdfWriter(stream);

PdfDocument pdfDoc = new PdfDocument(writer); //创建PDF文档对象




Document doc = new Document(pdfDoc, PageSize.A4); //页面A4大小

//获取字体文件的绝对路径,getRealPath()返回绝对路径

String font = request.getServletContext().getRealPath("/font/STCAIYUN.TTF"); 

PdfFont fontChinese = PdfFontFactory.createFont(font,

PdfEncodings.IDENTITY_H); //创建PDF中文字体对象

Paragraph header = new Paragraph("秋夕\n 【唐】 杜牧"); //Pdf段落,\n为换行

header.setFont(fontChinese); //使用华文彩云字体

header.setFontSize(16);

header.setTextAlignment(TextAlignment.CENTER);

doc.add(header);

List poem = new List().setListSymbol("\u2022"); //无序列表

poem.setFont(fontChinese);

poem.setFontSize(14);

poem.setTextAlignment(TextAlignment.CENTER);

poem.add(new ListItem("银烛秋光冷画屏,"));//每句诗作为列表项添加到列表

poem.add(new ListItem("轻罗小扇扑流萤。"));

poem.add(new ListItem("天阶夜色凉如水,"));

poem.add(new ListItem("卧看牵牛织女星。"));

doc.add(poem);

doc.close();

}

@Override

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {

doGet(request, response);

}

}



从上述处理过程来看,iText的代码还是非常简洁、清晰、易读的。在浏览器地址打开http://localhost:8080/chapter5/pdf,将显示如图515所示的PDF文件。



图515PDF古诗(1)


注意: 这里在导入类的时候,若有多个选择,一般请选择属于iText的类。不少人出错,很大程度上是因为导入了错误的类。

5.9场景任务挑战——有背景图的PDF古诗
在数据库中创建存放古诗的poems表,请合理设计出该表的相应字段。新建Servlet类LatestPoem,地址映射为/poem,每次读取poems表中最新录入的那首古诗,然后用6行1列的无边框表格,显示整首诗。整个表格的背景需要设置成一张图片,并将字体改用其他字体,例如华文隶书,如图516所示。



图516PDF古诗(2)