赞
踩
第1篇 Web开发与Java Web开发(第1章)
第2篇 JSP语言基础(第2-6章)
第3篇 Java Web整合开发(第7-10章)
第4篇 SSM框架(第11-16章)
第5篇 项目实战(第17章)
第1篇 Web开发与Java Web开发(第1章)
C/S体系结构即Client(客户端)/Server(服务器)
B/S体系结构即Browser(浏览器)/Server(服务器)
当前网络程序开发比较流行的两大主流架构:C/S结构和B/S结构。目前这两种结构都有各自的用武之地,都牢牢占据着自己的市场份额和客户群,在响应速度、用户界面、数据安全等方面,C/S强于B/S,但是在共享、业务扩展和适用万维网的条件下,B/S明显胜过C/S。
用户通过客户端浏览器访问网站或者其他网络资源时,通常需要在客户端浏览器的地址栏中输入URL(Uniform Resource Locator,统一资源定位符),或者通过超链接方式链接到相关网页或网络资源;然后通过域名服务器进行全球域名解析(DNS域名解析),并根据解析结果访问指定IP地址的网站或网页。
为了准确地传输数据,TCP采用了三次握手策略。首先发送一个带SYN(Synchronize)标志的数据包给接收方,接收方收到后,回传一个带有SYN/ACK(Acknowledgement)标志的数据包以示传达确认信息。最后发送方再回传一个带ACK标志的数据包,代表握手结束。在这个过程中,若出现问题导致传输中断了,TCP会再次发送相同的数据包。
在完成TCP后,客户端的浏览器正式向指定IP地址上的Web服务器发送HTTP(HyperText Transfer Protocol,超文本传输协议)请求;通常Web服务器会很快响应客户端的请求,将用户所需的HTML文本、图片和构成该网页的其他一切文件发送给用户。如果需要访问数据库系统中的数据,Web服务器就会将控制权转给应用服务器,根据Web服务器的数据请求读写数据库,并进行相关数据库的访问操作,应用服务器将数据查询响应发送给Web服务器,由Web服务器将查询结果转发给客户端的浏览器;浏览器解析客户端请求的页面内容;最终浏览器根据解析的内容进行渲染,将结果按照预定的页面样式呈现在浏览器上。
说明:Web本意是蜘蛛网和网。现广泛译作网络、互联网等技术,表现为三种形式:超文本(HyperText)、超媒体(Hypermedia)、超文本传输协议(HTTP)
Web客户端主要通过发送HTTP请求并接收服务器响应,最终展现信息内容。也就是说,只要能满足这一目的的程序、工具、脚本,都可以看作是Web客户端。Web客户端技术主要包括HTML语言、Java Applets、脚本程序、CSS、DHTML、插件技术以及VRML技术。
1.HTML语言
HTML语言(Hyper Text Markup Language,超文本标记语言)是Web客户端最主要、最常用的工具。
2.Java Applets
Applet是采用Java编程语言编写的小应用程序。Applets类似于Application,但是它不能单独运行,需要依附在支持Java的浏览器中运行。
3.脚本程序
脚本程序是嵌入HTML文档中的程序,使用脚本程序可以创建动态页面,大大提高交互性。比较常用的脚本程序有JavaScript和VBScript。JavaScript由Netscape公司开发,易用,灵活,无须编译。VBScript由Microsoft公司开发,与JavaScript一样,可用于动态Web页面。
4.CSS
CSS(Cascading Style Sheets,层叠样式表)是一种用来表现HTML或XML等文件样式的计算机语言。CSS不仅可以静态地修饰网页,还可以配合各种脚本语言动态地对网页对象和模型样式进行编辑。
5.DHTML
DHTML(Dynamic HTML,动态HTML)是HTML、CSS和客户端脚本的一种集成。
6.VRML
VRML(Virtual Reality Modeling Language,虚拟现实建模语言)用于建立真实世界的场景模型或人们虚构的三维世界的场景建模语言,具有平台无关性。
Web服务器技术主要包括CGI、PHP、ASP、ASP.NET、Servlet/JSP等。
1.CGI
CGI(Common Gateway Interface,公共网关接口)技术允许服务端的应用程序根据客户端的请求动态生成HTML页面,这使客户端和服务端的动态信息交换成为可能。
2.PHP
PHP原本是Personal Home Page(个人主页)的简称,后更名为PHP:Hypertext Preprocessor(超文本预处理器)。与以往的CGI程序不同,PHP语言将HTML代码和PHP指令合成为完整的服务端动态页面,Web应用的开发者可以用一种更加简便、快捷的方式实现动态Web功能。
3.ASP
Microsoft借鉴PHP的思想,在IIS 3.0中引入了ASP(Active Server Pages,活动服务器页面)技术。ASP使用的脚本语言是VBScript和JavaScript,从而迅速成为Windows系统下Web服务端的主流开发技术。
4.ASP.NET
ASP.NET是使用C#语言代替ASP技术的JavaScript脚本语言,用编译代替了逐句解释,提高了程序的运行效率。
5.Servlet/JSP
Servlet和JSP(Java Server Page)的组合让Java开发者同时拥有了类似CGI程序的集中处理功能和类似PHP的HTML嵌入功能,Java的运行时编译技术也大大提高了Servlet和JSP的执行效率。Servlet和JSP被后来的Java EE平台吸纳为核心技术。
下载JDK并配置环境变量
对于Java Web,4件利器是我们必须掌握的:JDK、Tomcat、IDEA和数据库。JDK是Java开发环境,Tomcat是Web程序部署的服务器,IDEA是开发Java Web的集成工具,可以极大地改善和提高开发效率,数据库负责数据持久化。
Tomcat的目录结构以及部署
常见的Web服务器如下:
· Tomcat(Apache):免费。当前应用最广的Java Web服务器。
· JBoss(Redhat红帽):支持Java EE,应用比较广的EJB容器。
· GlassFish(Oracle):Oracle开发的Java Web服务器,应用不是很广。
· Resin(Caucho):支持Java EE,应用越来越广。
· WebLogic(Oracle):收费。支持Java EE,适合大型项目。
· WebSphere(IBM):收费。支持Java EE,适合大型项目。
Tomcat服务器是一个免费、开源的Web应用服务器,常用在中小型系统和并发访问用户不是很多的场合下,是开发和调试Web程序的首选。
可以看到,这个配置文件中包括3个开启配置的端口和一个注释的端口,其功能如下:
· 8005:关闭Tomcat进程所用的端口。当执行shutdown.sh关闭Tomcat时,连接8005端口执行SHUTDOWN命令,如果8005未开启,则shutdown.sh无法关闭Tomcat。
· 8009:默认未开启。HTTPD等反向代理Tomcat时,可用AJP反向代理到该端口,虽然我们经常使用HTTP反向代理到8080端口,但由于AJP建立TCP连接后一般长时间保持,从而减少了HTTP反复进行TCP连接和断开的开销,因此反向代理中AJP比HTTP高效。
· 8080:默认的HTTP监听端口。
· 8443:默认的HTTPS监听端口。默认未开启,如果要开启,由于Tomcat不自带证书,因此除了取消注释之外,还要自己生成证书并在中指定才可以。
我们通常说的修改端口一般是指修改HTTP对应的8080端口,将图1.9中port的值修改成我们的目标值,如80,然后重启Tomcat(端口修改一定要重启Tomcat才能生效)。
第2篇 JSP语言基础(第2-6章)
SP(Java Server Pages)是Sun公司开发的一种服务器端动态页面生成技术,主要由HTML和少量的Java代码组成,目的是将表示逻辑从Servlet中分离出来,简化了Servlet生成页面。JSP部署在服务器上,可以响应客户端请求,并根据请求内容动态地生成HTML、XML或其他格式文档的Web网页,然后返回给请求者,因此客户端只要有浏览器就能浏览。它使用JSP标签在HTML网页中插入Java代码。标签通常以“<%”开头,以“%>”结束。
其使用具有以下几点特征:
· 跨平台:JSP是基于Java语言的,它能完全兼容Java API,JSP最终文件也会编译成.class文件,所以它跟Java一样是跨平台的。
· 预编译:预编译是指用户在第一次访问JSP页面时,服务器将对JSP页面进行编译,只编译一次。编译好的程序代码会保存起来,用户下一次访问会直接执行编译后的程序。这样不仅减少了服务器的资源消耗,还大大提升了用户访问速度。
· 组件复用:JSP可以利用JavaBean技术编写业务组件、封装业务逻辑或者作为业务模型。这样其他JSP页面可以重复利用该模型,减少重复开发。
· 解耦合:使用JSP开发Java Web可以实现界面的开发与应用程序的开发分离,实现显示与业务逻辑解耦合。界面开发专注界面效果,程序开发专注业务逻辑。最后业务逻辑生成的数据会动态填充到界面进行展示。
JSP的工作模式是请求/响应模式,JSP文件第一次被请求时,JSP容器把文件转换成Servlet,然后Servlet编译成.class文件,最后执行.class文件。
JSP执行的具体步骤如下:
首先是Client发出请求,请求访问JSP文件。
JSP容器将JSP文件转换为Java源代码(Servlet文件)。在转换过程中,如果发现错误,则中断转换并向服务端和客户端返回错误信息,请求结束。
如果文件转换成功,JSP容器会将Java源文件编译成.class文件。
Servlet容器会加载该.class文件并创建Servlet实例,然后执行jspInit()方法。
JSP容器执行jspService()方法处理客户端请求,对于每一个请求,JSP容器都会创建一个线程来处理,对于多个客户端同时请求JSP文件,JSP容器会创建多个线程,使得每一次请求都对应一个线程。
由于首次访问需要转换和编译,因此可能会产生轻微的延迟。另外,当遇到系统资源不足等情况时,Servlet实例可能会被移除。
处理完请求后,响应对象由JSP容器接收,并将HTML格式的响应信息发送回客户端。
JSP指令用来设置整个JSP页面相关的属性,如网页编码和脚本语言。
指令通常以<%@标记开始,以%>标记结束,它的具体语法如下:
<%@ 指令名称 属性1="属性值1" 属性2="属性值2" … 属性n="属性值n" %>
JSP的page指令是页面指令,定义在整个JSP页面范围有效的属性和相关的功能。page指令可以指定使用的脚本语言、导入需要的类、输出内容的类型、处理异常的错误页面以及页面输出缓存的大小,还可以一次性设置多个属性。一个JSP页面可以包含多个page指令,其中的属性只能使用一次(import属性除外)。
<%@ page attribute="value" %>
(attribute:属性)
示例代码:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.util.Date" %>
<%@ page import="java.text.SimpleDateFormat" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%
Date dNow = new Date();
%>
<h2><%= dNow.toString() %></h2>
<%
SimpleDateFormat ft = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
%>
<h2><%= ft.format(dNow) %></h2>
</body>
</html>
JSP的include指令用于通知JSP引擎在编译当前JSP页面时,将其他文件中的内容引入当前JSP页面转换成的Servlet源文件中,这种源文件级别引入的方式称为静态引入。当前JSP页面与静态引入的文件紧密结合为一个Servlet。这些文件可以是JSP页面、HTML页面、文本文件或一段Java代码。
<%@ include file="relativeURL|absoluteURL" %>
示例如下:
jsp_include_01.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%@ include file="jsp_included.jsp" %><!-- 加载 jsp 而不是html 页面 -->
This is the main page ~~
</body>
</html>
jsp_included.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Included Page</title>
</head>
<body>
This is included page~~
</body>
</html>
taglib指令可以引入一个自定义标签集合的标签库,包括库路径、自定义标签。
<%@ taglib uri="uri" prefix="prefixOfTag" %>
JSP脚本标识包括3部分:JSP表达式(Expression)、声明标识(Declaration)和脚本程序(Scriptlet)。
JSP表达式中包含的脚本语言先被转化成String,然后插入表达式出现的地方。表达式元素中可以包含任何符合Java语言规范的表达式,但是不能使用分号来结束表达式。JSP表达式的语法格式如下:
<%= 表达式 %>
注意:“<%”与“=”之间不可以有空格,“=”与其后面的表达式之间可以有空格。
示例代码:
<%
String name = "admin";
%>
<%= name %><br />
5 + 6 = <%= 5+6 %><br />
<p>
<%
String url = "test.jpg";
%>
<img src="images/<%= url %>">
</p>
<p>
<%= (new java.util.Date()).toLocaleString() %>
</p>
一个声明语句可以声明一个或多个变量或方法,供后面的Java代码使用。在JSP文件中,必须先声明这些变量和方法,然后才能使用它们。服务器执行JSP页面时,会将JSP页面转换为Servlet类,在该类中会把使用JSP声明标识定义的变量和方法转换为类的成员和方法。
<%! 声明变量或方法的代码 %>
注意:“<%”与“!”之间不可以有空格,“<%!”与“%>”可以不在同一行。
实例:
<%!
int number = 0; // 声明全局变量
int count() {
number++;
return number;
}
%>
<p>
<meta charset="UTF-8">
页面刷新的次数:<%= count() %>
</p>
<% Java代码或是脚本代码 %>
提示:代码片段与声明标识的区别是,通过声明标识创建的变量和方法在当前JSP页面中有效,它的生命周期是从创建开始到服务器关闭结束;代码片段创建的变量或方法也是在当前JSP页面中有效,但它的生命周期是页面关闭后就会被销毁。
<!-- 注释内容 -->
<!-- HTML注释内容<%=JSP 表达式%>-->
<%--注释内容--%>
// 注释内容
/*
*注释内容
*/
/*
文档的注释内容
*/
以上JSP三种指令标识称为编译指令
前面2.2节学习的JSP三种指令标识称为编译指令,而本节将要学习7种常用的动作标识。JSP动作与JSP指令的不同之处是,JSP页面被执行时首先进入翻译阶段,程序会先查找页面中的指令标识,并将它们转换成Servlet,这些指令标识会先被执行,从而设置整个JSP页面,所以JSP指令是在页面转换时期被编译执行的,且编译一次,而JSP动作是在客户端请求时按照在页面中出现的顺序被执行的,它们只有被执行的时候才会去实现自己所具有的功能,且基本上是客户每请求一次,动作标识就会被执行一次。
<jsp:动作名 属生1="属性值1"…属性n="属性值n" />
或者
<jsp:动作名; 属性1="属性值1"…属性n="属性值n">相关内容</jsp:动作名>
动作标识基本上都是预定义的函数,常用的动作标识如表2.2所示。
jsp:include动作用来包含静态和动态的文件,把指定文件插入正在生成的页面。
<jsp:include page="相对URL地址" flush="true" />
前面介绍include指令的时候,也是用来包含文件的。它们引入文件的时机不一样,include指令是在JSP文件被转换成Servlet的时候引入文件,而jsp:include动作是在页面被请求的时候插入文件。
jsp:include动作在JSP页面执行时引入的方式称为动态引入,主页面程序与被包含文件是彼此独立、互不影响的。
jsp:include动作对动态文件和静态文件的处理方式是不同的。
如果包含的是静态文件,被包含文件的内容将直接嵌入JSP文件中,当静态文件改变时,必须将JSP文件重新保存(重新转译),然后才能访问变化了的文件。
如果包含的是动态文件,则由Web服务器负责执行,把执行后的结果传回包含它的JSP页面中,若动态文件被修改,则重新运行JSP文件时会同步发生变化。
<h2>include 动作示例:</h2>
<jsp:include page="jsp_included.jsp" flush="true" />
前面学习的include指令和jsp:include动作都是用来包含文件的,其作用基本类似。下面介绍一下它们的差异。
1.属性不同
include指令通过文件属性来指定被包含的页面,该属性不支持任何表达式。jsp:include动作是通过页面属性来指定被包含页面的,该属性支持JSP表达式。
2.处理方式不同
使用include指令,被包含文件的内容会原封不动地插入包含页中,JSP编译器对合成的新文件进行编译,最终编译后只生成一个文件。使用jsp:include动作时,只有当该标记被执行时,程序才会将请求转发到(注意是转发而不是请求重定向)被包含的页面,再将其执行结果输出到浏览器,然后重新返回包含页来继续执行后面的代码。因为服务器执行的是两个文件,所以JSP编译器将对这两个文件分别编译。
3.包含方式不同
include指令的包含过程为静态包含,在使用include指令包含文件时,服务器最终执行的是将两个文件合成后由JSP编译器编译成一个Class文件,所以被包含文件的内容是固定不变的;如果改变了被包含的文件,主文件的代码就发生了改变,服务器会重新编译主文件。
jsp:include动作的包含过程为动态包含,通常用来包含那些经常需要改动的文件。因为服务器执行的是两个文件,被包含文件的改动不会影响主文件,所以服务器不会对主文件重新编译,而只需重新编译被包含的文件即可。编译器对被包含文件的编译是在执行时才进行的,只有当jsp:include动作被执行时,使用该标记包含的目标文件才会被编译,否则被包含的文件不会被编译。
4.对被包含文件的约定不同
使用include指令包含文件时,因为JSP编译器是对主文件和被包含文件进行合成后再翻译,所以对被包含文件有约定。例如,被包含的文件中不能使用“、”标记,被包含文件要避免变量和方法在命名上与主文件冲突的问题。
注意:include指令和动作的最终目的是简化复杂页面的代码,提高代码的可读性和可维护性,体现了编程中模块化的思想。
jsp:forward动作把请求转到其他的页面。该动作只有一个属性page。
<jsp:forward page="URL地址" />
jsp:param动作以键-值对的形式为其他标签提供附加信息,通俗地说就是页面传递参数,它常和jsp:include、jsp:forward等一起使用,
<jsp:param name="paramName" value="paramValue" />
其中,name属性用于指定参数名,value属性用于指定参数值。
JSP内置对象又称为隐式对象,是指在JSP页面系统中已经默认内置的Java对象,这些对象不需要显式声明即可使用,也就是可以直接使用。在JSP页面中,可以通过存取JSP内置对象实现与JSP页面和Servlet环境的相互访问,极大地提高了程序的开发效率。
JSP的内置对象主要有以下特点:
· 内置对象是自动载入的,不需要直接实例化。
· 内置对象通过Web容器来实现和管理。
· 所有的JSP页面都可以直接调用内置对象。
· 只有在脚本元素的表达式或代码段中才可以使用(<%=使用内置对象%>或<%使用内置对象%>)。
表3.1所示为JSP的九大内置对象。
request对象是HttpServletRequestWrapper类的实例(笔者这里引入的是Tomcat 10版本的servlet-api.jar)。request对象的继承体系如图3.1所示。
ServletRequest接口的唯一子接口是HttpServletRequest,HttpServletRequest接口的唯一实现类是HttpServletRequestWrapper,可以看出,Java Web标准类库只支持HTTP协议。Servlet/JSP中大量使用了接口而不是实现类,就是面向接口编程的最佳应用。
request内置对象可以用来封装HTTP请求的参数信息、进行属性值的传递以及完成服务端的跳转。
在上一章JSP动作标识示例代码中,我们使用jsp:param传递参数,在接收参数页面,我们其实已经用到了request内置对象。本小节我们来学习使用URL传递参数。
主页示例代码:
<li><a href="jsp_req_param.jsp?name=张三李四&sex=man&id=" rel="external
nofollow">访问请求参数</a></li>
跳转页面示例代码:
name:<%= request.getParameter("name") %><br>
sex:<%= request.getParameter("sex") %><br>
id:<%= request.getParameter("id") %><br>
pwd:<%= request.getParameter("pwd") %><br>
如果指定的参数不存在,则返回null(如pwd参数);如果指定了参数名,但未指定参数值,则返回空的字符串"(如id参数)。
在进行请求转发时,需要把一些数据传递到转发后的页面进行处理,这时需要调用request对象的setAttribute方法将数据保存在request范围内的变量中,转发后的页面则调用getAttribute方法接收数据。
提示:把语句<jsp:forward page=“jsp_req_attr_b.jsp”/>改成response.sendRedirect(“jsp_req_attr_b.jsp”)或者跳转,将得不到request范围内的属性值,页面会出现异常。
Cookie是网络服务器上生成并发送给浏览器的小段文本信息。通过Cookie可以标识一些常用的信息(比如用户身份,记录用户名和密码等),以便跟踪重复的用户。Cookie以键-值对的形式保存在客户端的某个目录下。
通过调用request.getCookies()方法来获得一个jakarta.servlet.http.Cookie对象的数组,然后遍历这个数组,调用getName()方法和getValue()方法来获取每一个Cookie的名称和值。
request对象提供了很多方法获取客户端信息,具体示例如下:
· 客户使用的协议:<%=request.getProtocol() %>
。
· 客户提交信息的方式:<%=request.getMethod() %>
。
· 客户端地址:<%=request.getRequestURL() %>
。
· 客户端IP地址:<%=request.getRemoteAddr() %>
。
· 客户端主机名:<%=request.getRemoteHost() %>
。
· 客户端所请求的脚本文件的文件路径:<%=request.getServletPath() %>
。
· 服务器端口号:<%=request.getServerPort() %>
。
· 服务器名称:<%=request.getServerName() %>
。
· HTTP头文件中Host的值:<%=request.getHeader(“host”) %>
。
· HTTP头文件中User-Agent的值:<%=request.getHeader(“user-agent”) %>
。
· HTTP头文件中accept的值:<%=request.getHeader(“accept”)%>
。
· HTTP头文件中accept-language的值:<%=request.getHeader(“accept-language”)%>
。
JSP国际化是指能同时应对世界不同地区和国家的访问请求,并针对不同地区和国家的访问请求提供符合来访者阅读习惯的页面或数据。
我们先了解几个相关的概念:
· 国际化(I18N):一个页面根据访问者的语言或国家来呈现不同的语言版本。
· 本地化(L10N):向网站添加资源,以使它适应不同的地区和文化。
· 区域:指特定的区域、文化和语言,通常是一个地区的语言标志和国家标志,通过下画线连接起来,比如"en_US"代表美国英语地区。
浏览器可以通过accept-language的HTTP报头向Web服务器指明它所使用的本地语言。java.util.Local类型对象封装了一个国家或地区所使用的一种语言。
提示:不少初学者在中文处理过程中都会碰到中文乱码的情况,这个主要是文件(JSP文件)、工具(IDEA)和服务器(Tomcat)三者编码的问题。目前笔者搭建的Tomcat 10 + IDEA 2022.1.1没有出现过中文乱码的情况。建议读者在创建项目初期,所有的工具和服务器统一使用UTF-8编码。
response对象用于响应客户请求,向客户端输出信息。与request对象类似,response对象是HttpServletResponseWrapper类的实例,它封装了JSP产生的响应客户端请求的有关信息(如回应的Header、HTML的内容以及服务器的状态码等),以提供给客户端。请求的信息可以是各种数据类型的信息,甚至可以是文件。response对象的继承体系如图3.5所示。
在很多情况下,当客户要进行某些操作时,需要将客户引导至另一个页面。例如,当客户输入正确的登录信息时,就需要被引导到登录成功页面,否则被引导到错误显示页面。此时,可以调用response对象的sendRedirect(URL)方法将客户请求重定向到一个不同的页面。重定向会丢失所有的请求参数,使用重定向的效果与在地址栏重新输入新地址再按回车键的效果完全一样,即发送了第二次请求。
response对象的setHeader()方法的作用是设置指定名字的HTTP文件头的值,如果该值已经存在,则新值会覆盖旧值。比较常用的头信息有缓存设置,页面自动刷新或者页面定时跳转。
1.禁用缓存
浏览器通常会对网页进行缓存,目的是提高网页的显示速度。但是很多安全性要求较高的网站(比如支付和个人信息网站)通常需要禁用缓存。
2.自动刷新
3.定时跳转实现页面定时跳转,如10秒后自动跳转到URL所指的页面,设置语句如下:
<% response.setHeader(“refresh”, “10;URL=index.jsp”);%>
一般来说,服务器要输出到客户端的内容不会直接写到客户端,而是先写到输出缓冲区。当满足以下3种情况之一时,就会把缓冲区的内容写到客户端:
· JSP页面的输出信息已经全部写入缓冲区。· 缓冲区已经满了。
· 在JSP页面中调用了response对象的flushBuffer()方法或out对象的flush()方法。
response对象提供了对缓冲区进行配置的方法,如表3.2所示
从表面上看,转发(Forward)动作和重定向(Redirect)动作有些相似:它们都可以将请求传递到另一个页面。但实际上它们之间存在较大的差异。
执行转发动作后依然是上一次的请求;执行重定向动作后生成第二次请求。注意地址栏的变化,执行重定向动作时,地址栏的URL会变成重定向的目标URL。
转发的目标页面可以访问所有原请求的请求参数,转发后是同一次请求,所有原请求的请求参数、request范围的属性全部存在;重定向的目标页面不能访问原请求的请求参数,因为发生了第二次请求,所有原请求的请求参数全部都会失效。
转发地址栏请求的URL不会变;重定向地址栏改为重定向的目标URL,相当于在浏览器地址栏输入新的URL。
Session(会话)表示客户端与服务器的一次对话。从客户端打开浏览器并连接服务器开始,到客户端关闭浏览器离开这个服务器结束,被称为一个会话。设置Session是为了服务器识别客户。由于HTTP是一种无状态协议,即当客户端向服务器发出请求,服务器接收请求,并返回响应后,该连接就结束了,而服务器不保存相关的信息。通过Session可以在的Web页面进行跳转时保存用户的状态,使整个会话一直存在下去,直到服务器关闭。
session对象是HttpSession类的实例。
ession对象提供了setAttribute()和getAttribute()方法创建和获取客户的会话。setAttribute()方法用于设置指定名称的属性值,并将它存储在session对象中(用于获取修改输出)。
其语法格式如下:
session.setAttribute(String name, Object value);
其中,name为属性名称,value为属性值(可以是类,也可以是值)。
调用getAttribute()方法获取与指定属性名name相关联的属性值,返回值为Object类型(所以可能需要转换为String或者Integer类型)。
session.getAttribute(String name);
调用removeAttribute()方法将指定名称的对象移除,即从这个会话中删除与指定名称绑定的对象。其语法格式如下:
session.removeAttribute(String name);
移除对象之后,通过getAttribute()获取的值是null,表示对象已经不存在了。
虽然session对象经历一段时间后会自动消失,但是有时我们也需要手动销毁会话(比如用户登录之后信息存储在会话对象中,退出的时候应该销毁会话对象以保存的用户数据)。
销毁会话有3种方式:
· 调用session.invalidate()方法。
· 会话过期(超时)。
· 服务器重新启动。
通常我们会调用session.invalidate()方法销毁会话。
在session对象中提供了设置会话生命周期的方法,分别说明如下:
· getLastAccessedTime():返回客户端最后一次与会话相关联的请求时间。
· getMaxInactiveInterval():以秒为单位返回一个会话内两个请求之间的最大时间间隔。
· setMaxInactiveInterval():以秒为单位设置会话的有效时间。
例如,设置会话的有效期为1000秒,超出这个范围会话将会失效。
服务器启动后就产生了application对象,当客户在同一个网站的各个页面浏览时,这个application对象都是同一个,它在服务器启动时自动创建,在服务器停止时销毁。与session对象相比,application对象的生命周期更长,类似于系统的全局变量。
一个Web应用程序启动后,会自动创建一个application对象,而且在整个应用程序的运行过程中只有一个application对象,即所有访问该网站的客户都共享一个application对象。
在web.xml文件中,可利用context-param元素来设置系统范围内的初始化参数。
context-param元素应该包含param-name、param-value以及可选的description子元素,
application对象设置的属性在整个程序范围内都有效,即使所有的用户都不发送请求,只要不关闭服务器,在其中设置的属性仍然是有效的。
1.作用范围不同session对象是用户级的对象,而application对象是应用程序级的对象。
一个用户对应一个session对象(客户端对象),每个用户的session对象不同,在用户所访问网站的多个页面之间共享同一个session对象。
一个Web应用程序对应一个application对象(服务端对象),每个Web应用程序的application对象不同,但一个Web应用程序的多个用户之间共享同一个application对象。
在同一网站下,每个用户的session对象不同,每个用户的application对象相同。
在不同网站下,每个用户的session对象不同,每个用户的application对象也不同。
2.生命周期不同session对象的生命周期:用户首次访问网站创建,用户离开该网站(不一定要关闭浏览器)消亡。
application对象的生命周期:启动Web服务器就被创建,关闭Web服务器就被销毁。
out对象是一个输出流,用来向客户端输出各种数据类型的内容。同时它还可以管理应用服务器上的输出缓冲区,缓冲区大小的默认值为8KB,可以通过页面指令page来改变这个默认值。out对象继承自抽象类jakarta.servlet.jsp.JspWriter的实例,在实际应用中,out对象会通过JSP容器变换为java.io.PrintWriter类的对象。
在使用out对象输出数据时,可以对数据缓冲区进行操作,及时清除缓冲区中的残余数据,为其他的输出腾出缓冲空间。数据输出完毕后要及时关闭输出流。
out对象调用print()或println()方法向客户端输出数据。由于客户端是浏览器,因此可以使用HTML中的一些标记控制输出格式。
默认情况下,服务端要输出到客户端的内容不直接写到客户端,而是先写到一个输出缓冲区中。调用out对象的getBufferSize()方法获取当前缓冲区的大小(单位是KB),调用getRemaining()方法获取当前尚剩余的缓冲区的大小(单位是KB)。
pageContext对象是jakarta.servlet.jsp.PageContext类的实例对象。它代表页面上下文,主要用于访问JSP之间的共享数据,使用pageContext可以访问page、request、session、application范围的变量。
1.获得其他对象获得其他对象的几个重要方法说明如下:
· forward(String relativeUrlPath):将当前页面转发到另一个页面或者Servlet组件上。· getRequest():返回当前页面的request对象。
· getResponse():返回当前页面的response对象。
· getServetConfig():返回当前页面的servletConfig对象。
· getServletContext():返回当前页面的ServletContext对象,这个对象是所有页面共享的。
· getSession():返回当前页面的session对象。
· findAttribute():按照页面、请求、会话以及应用程序范围的属性实现对某个属性的搜索。
· setAttribute():设置默认页面范围或特定对象范围中的对象。
· removeAttribute():删除默认页面对象或特定对象范围中已命名的对象。
2.操作作用域对象pageContext对象可以操作所有作用域对象(4个域,request、session、application和pageContext),在getAttribute()、setAttribute()、removeAttribute()三个方法中添加一个参数scope来指定作用域(即范围)。
在PageContext类中包含4个int类型的常量表示4个作用域:
· PAGE_SCOPE:pageContext作用域。
· REQUEST_SCOPE:request作用域。
· SESSION_SCOPE:session作用域。
· APPLICATION_SCOPE:application作用域。
config对象是ServletConfig的实例,它主要用于读取配置的参数,很少在JSP页面使用,常用于Servlet中,因为Servlet需要在web.xml文件中进行配置。
先看一下config内置对象获取servlet名称,代码如下:
<!-- 直接输出config的getServletName的值 -->
<%=config.getServletName()%>
page对象是java.lang.Object类的实例。它指向当前JSP页面本身,有点像类中的this指针,用于设置JSP页面的属性,这些属性将用于和JSP通信,控制所生成的Servlet结构。page对象很少使用,我们调用Object类的一些方法来了解这个对象。
当前page页面对象的字符串描述: <%= page.toString() %><br>
当前page页面对象的class描述: <%= page.getClass() %><br>
page跟this是否等价: <%= page.equals(this) %><br>
提示:page对象虽然是this的引用,但是page的类型是java.lang.Object,所以无法通过page调用实例变量、方法等,只能调用Object类型的一些方法。
JSP引擎在执行过程中可能会抛出种种异常。exception对象表示的就是JSP引擎在执行代码过程中抛出的种种异常。exception对象的作用是显示异常信息,只有在包含isErrorPage="true"的页面中才可以使用,在一般的JSP页面中使用该对象将无法编译JSP文件。
在Java程序中,可以使用try/catch关键字来处理异常情况;如果在JSP页面中出现没有捕获到的异常,就会生成exception对象,并把exception对象传送到在page指令设定的错误页面中,然后在错误页面中处理相应的exception对象。
JavaBean是一个遵循特定写法的Java类。在Java模型中,通过JavaBean可以无限扩充Java程序的功能,通过JavaBean的组合可以快速生成新的应用程序。JavaBean技术使JSP页面中的业务逻辑变得更加清晰,程序中的实体对象及业务逻辑可以单独封装到Java类中。这样不仅提高了程序的可读性和易维护性,还提高了代码的复用性。
JavaBean本质上是一个Java类,一个遵循特定规则的类。当在Web程序中使用时,会以组件的形式出现,并完成特定的逻辑处理功能。使用JavaBean的最大优点在于它可以提高代码的复用性。编写一个成功的JavaBean的宗旨为“一次性编写,任何地方执行,任何地方复用”。
JavaBean按功能可分为可视化JavaBean和不可视JavaBean两类。可视化JavaBean就是具有GUI(图形用户界面)的JavaBean;不可视JavaBean就是没有GUI的JavaBean,最终对用户是不可见的,它更多地被应用在JSP中。
不可视JavaBean又分为值JavaBean和工具JavaBean。
值JavaBean严格遵循了JavaBean的命名规范,通常用来封装表单数据,作为信息的容器。
工具JavaBean可以不遵循JavaBean规范,通常用于封装业务逻辑、数据操作等。例如,连接数据库,对数据库进行增、删、改、查,解决中文乱码等操作。工具JavaBean可以实现业务逻辑与页面显示的分离,提高了代码的可读性与易维护性。
1.实现可序列接口
JavaBean应该直接或间接实现java.io.Serializable接口,以支持序列化机制。
2.公有的无参构造方法
一个JavaBean对象必须拥有一个公有类型以及默认的无参构造方法,从而可以通过new关键字直接对它进行实例化。
3.类的声明是非final类型的
当一个类声明为final类型时,它是不可以更改的,所以JavaBean对象的声明应该是非final类型的。
4.为属性声明访问器
JavaBean中的属性应该设置为私有类型(private),可以防止外部直接访问,它需要提供对应的setXXX()和getXXX()方法来存取类中的属性,方法中的XXX为属性名称,属性的第一个字母应大写。若属性为布尔类型,则可用isXXX()方法代替getXXX()方法。
JavaBean的属性是内部核心的重要信息,当JavaBean被实例化为一个对象时,改变它的属性值也就等于改变了这个Bean的状态。这种状态的改变常常伴随着许多数据处理操作,使得其他相关的属性值也跟着发生变化。
实现java.io.Serializable接口的类实例化的对象被JVM(Java虚拟机)转化为一个字节序列,并且能够将这个字节序列完全恢复为原来的对象,序列化机制可以弥补网络传输中不同操作系统的差异问题。作为JavaBean,对象的序列化也是必需的。使用一个JavaBean时,一般情况下是在设计阶段对它的状态信息进行配置,并在程序启动后期恢复,这种具体工作是由序列化完成的。
相信很多开发者都有这样的经历,比如在开发中经常碰到要在网页录入大量信息(如人力资源管理系统、客户关系管理系统),导致JSP页面代码冗余复杂。此时引入JavaBean技术,可以实现HTML代码和Java代码的分离,可以对代码进行复用和封装,极大地提升开发效率,简化JSP页面,使JSP更易于开发和维护。因此,JavaBean成为JSP程序员必备的利器。
1.导入JavaBean类通过<%@ page import>指令导入JavaBean类。
<%@ page import="com.vincent.bean.UserBean" %>
2.声明JavaBean对象JSP定义了< jsp:useBean>标签用来声明JavaBean对象。
<jsp:useBean id="user" class="com.vincent.bean.UserBean"
scope="session"></jsp:useBean>
说明:属性id的值定义了Bean变量,使之能在后面的程序中使用此变量名来分辨不同的Bean,这个变量名对大小写敏感(区分字母大小写),必须符合所使用的脚本语言的规定。如果Bean已经在别的jsp:useBean标记中创建,则当使用这个已经创建过的Bean时,id的值必须与原来的id值一致;否则意味着创建了同一个类的两个不同的对象。
JSP提供了访问JavaBean属性的标签,如果要将JavaBean的某个属性输出到页面,可以使用jsp:getProperty标签。
有3点需要重点注意的事项:
· class文件必须位于某个包内。
· Bean文件必须有默认无参构造器。
· class文件必须在WEB-INF/classes目录下。
否则,页面会出现UserBean无法解析的错误。
与获取JavaBean属性类似,JSP也提供了给JavaBean属性赋值的标签jsp:setProperty。
在JSP页面中,中文经常会出现乱码的现象,特别是通过表单传递中文数据时。解决办法有很多,如将request的字符集指定为中文字符集,编写JavaBean对乱码字符进行转码等。
解决中文乱码问题的关键在于设置字符集时保持一致,也就是将request请求字符集和页面字符集保持统一。在系统中,我们常用UTF-8字符集来配置页面和类,以避免出现中文乱码的情况。
在程序开发中,我们经常会碰到需要将数组转换成字符串的情况,如表单中的复选框按钮,在提交之后就是一个数组对象,由于数组对象在业务处理中不方便,因此在实际应用中先将它转换成字符串再进行处理。创建JavaBean,并封装将数组转换成字符串的方法。
Servlet是基于Java语言的Web服务器端编程技术,是一种实现动态网页的解决方案,其作用是扩展Web服务器的功能。
Servlet是运行在Servlet容器中的Java类,它能处理Web客户端的HTTP请求,并产生HTTP响应。当浏览器发送一个请求到服务器后,服务器会把请求交给一个特定的Servlet,该Servlet对请求进行处理后会构造一个合适的响应(通常以HTML网页形式)返回给客户端。
Servlet对请求的处理和响应过程可进一步细分为如下几个步骤:
(1)接收HTTP请求。
(2)获取请求信息,包括请求头和请求参数数据。
(3)调用其他Java类方法完成具体的业务功能。
(4)实现到其他Web组件的跳转(包括重定向或请求转发)。
(5)生成HTTP响应(包括HTTP或非HTTP响应)。
浏览器访问Servlet的交互过程如图5.1所示。
首先浏览器向Web服务器发送了一个HTTP请求,Web服务器根据收到的请求会先创建一个HttpServletRequest和HttpServletResponse对象,然后调用相应的Servlet程序。在Servlet程序运行时,它首先会从HttpServletRequest对象中读取数据信息,然后通过service()方法处理请求消息,并将处理后的响应数据写入HttpServletResponse对象中。最后,Web服务器会从HttpServletResponse对象中读取响应数据,并发送给浏览器。
Servlet是使用Servlet API(应用程序设计接口)及相关类和方法的Java程序。它包含两个软件包:jakarta.servlet包和jakarta.servlet.http包。
Servlet是使用Java Servlet API运行在Web服务器上的Java程序,而JSP是一种在Servlet上的动态网页技术,Servlet和JSP存在本质上的区别,其区别主要体现在以下3个方面。
1.承担的角色不同
Servlet承担着客户请求和业务处理的中间角色,处理负责业务逻辑,最后将客户请求内容返回给JSP;而JSP页面HTML和Java代码可以共存,主要负责显示请求结果。
2.编程方法不同
使用Servlet的Web应用程序需要遵循Java标准,需要调用Servlet提供的相关API接口才能对HTTP请求及业务进行处理,在业务逻辑方面的处理功能更为强大;JSP需要遵循一定的脚本语言规范,通过HTML代码和JSP内置对象实现对HTTP请求和页面的处理,在显示界面方面功能强大。
3.执行方式不同
Servlet需要在Java编译器编译后才能运行,如果Servlet在编写完成或修改后没有重新编译,则不能运行或应用修改后的内容;而JSP由JSP容器进行管理,也由JSP容器自动编辑,JSP无论是创建或修改,都无须对它进行编译便可以运行。
Servlet是指HttpServlet对象,声明一个对象为Servlet时需要继承HttpServlet类,然后根据需要重写方法对HTTP请求进行处理。
· init()方法为Servlet初始化的调用方法。
· destroy()方法为Servlet的生命周期结束的调用方法。
· doGet()、doPost()这两个方法分别用来处理Get、Post请求,如表单对象声明的method属性为post,把数据提交到Servlet,由doPost()方法进行处理,Android客户端向Servlet发送Get请求,则由doGet()方法进行处理。
创建Servlet有3种方法,基于上一节我们了解的体系结构以及示例代码,这3种创建Servlet的方法分别是:
(1)创建自定义Servlet实现jakarta.servlet.Servlet接口,实现里面的方法。
(2)创建自定义Servlet继承jakarta.servlet.GenericServlet类,重写service()方法。
(3)创建自定义Servlet继承jakarta.servlet.http.HttpServlet类,重写业务方法。
在实际工作中,我们主要还是继承HttpServlet,因此笔者在讲解Servlet开发的时候主要以第3种方法为主来讲解。具体步骤如下:
(1)新建一个Java类,继承HttpServlet。
(2)重写Servlet生命周期的方法。一般重写doGet()、doPost()、destroy()和service()方法。
(3)编写业务功能代码。
1.配置web.xml文件
在web.xml中需要配置两个类型:servlet和servlet-mapping。
配置好之后,我们可以通过页面访问,访问方式如下:
<li><a href="quickstart">Web.xml配置Servlet</a></li>
浏览器中显示的URL:http://localhost:8080/项目部署的名称/quickstart。
2.使用注解配置
使用注解方式非常简单,只需要写上注解的属性即可:
@WebServlet(name = "quickstart", urlPatterns = "/quickstart")
或
@WebServlet("/quickstart")
使用注解效果跟XML配置是一样的,其主要作用是简化XML配置。
所有的Servlet都必须直接或间接地实现jakarta.servlet.Servlet接口。Servlet接口规定了必须由Servlet类实现并且由Servlet引擎识别和管理的方法集。
Servlet接口中的主要方法及说明如下:
(1)init(ServletConfig config):Servlet的初始化方法。在Servlet实例化后,容器调用该方法进行Servlet的初始化。ServletAPI规定对于任何Servlet实例,init()方法只能被调用一次,如果此方法没有正常结束,就会抛出一个ServletException异常,一旦抛出异常,Servlet将不再执行,而随后对它进行再次调用会导致容器重新载入并再次运行init()方法。
(2)service(ServiceRequest req, ServletResponse resp):Servlet的服务方法。当用户对Servlet发出请求时,容器会调用该方法处理用户的请求。
(3)destroy():Servlet的销毁方法。容器在终止Servlet服务前调用此方法,容器调用此方法前必须给service()线程足够的时间来结束执行,因此接口规定当service()正在执行时,destroy()不被执行。
(4)getServletConfig():此方法可以让Servlet在任何时候获得ServletConfig对象。
(5)getServletInfo():此方法返回一个String对象,该对象包含Servlet的信息,例如开发者、创建日期和描述信息等。在实现Servlet接口时必须实现它这5个方法。
初始化Servlet时,Servlet容器会为这个Servlet创建一个ServletConfig对象,并将它作为参数传递给Servlet。通过ServletConfig对象即可获得当前Servlet的初始化参数信息。一个Web应用中可以存在多个ServletConfig对象,一个Servlet只能对应一个ServletConfig对象。
获取ServletConfig对象一般有两种方式:
· 直接从带参的init()方法中提取。
· 调用GenericServlet提供的getServletConfig()方法获得。
在Servlet API中定义了一个HttpServletRequest接口,它继承自ServletRequest接口。它专门用于封装HTTP请求消息,简称request对象。
HTTP请求消息分为请求行、请求消息头和请求消息体3部分,所以HttpServletRequest接口中定义了获取请求行、请求消息头和请求消息体的相关方法。
1.获取请求行信息
HTTP请求的请求行中包含请求方法、请求资源名、请求路径等信息,HttpServletRequest接口定义了一系列获取请求行信息的方法。
2.获取请求头信息
当浏览器发送请求时,需要通过请求头向服务器传递一些附加信息,例如客户端可以接收的数据类型、压缩方式、语言等。为了获取请求头中的信息,HttpServletRequest接口定义了一系列用于获取HTTP请求头字段的方法。
3.获取form表单的数据
在实际开发中,我们经常需要获取用户提交的表单数据,例如用户名和密码等。为了方便获取表单中的请求参数,ServletRequest定义了一系列获取请求参数的方法。
HttpServletResponse接口继承自ServletResponse接口,主要用于封装HTTP响应消息。由于HTTP响应消息分为响应状态码、响应消息头、响应消息体3部分。因此,在HttpServletResponse接口中定义了向客户端发送响应状态码、响应消息头、响应消息体的方法。
在Servlet API中,ServletResponse接口被定义为用于创建响应消息,ServletResponse对象由Servlet容器在用户每次请求时创建并传入Servlet的service()方法中。
HttpServletResponse接口继承自ServletResponse接口,是专门用于HTTP的子接口,用于封装HTTP响应消息。在HttpServlet类的service()方法中,传入的ServletResponse对象被强制转换为HttpServletResponse对象进行HTTP响应信息的处理。
1.设置响应状态
HttpServletResponse接口提供了如下设置状态码和生成响应状态行的方法:
· setStatus(int sc):以指定的状态码将响应返回给客户端。
· setError(int sc):使用指定的状态码向客户端返回一个错误响应。
· sendError(int sc, String msg):使用指定状态码和状态描述向客户端返回一个错误响应。
· sendRedirect(String location):请求重定向,会设定响应location报头及改变状态码。
通过设置资源暂时转移状态码和location响应头,实现sendRedirect()方法的重定向功能。
2.构建响应消息头
响应头有两种方法:一种是addHeader()方法;另一种是setHeader()方法。addHeader()方法会添加属性,不会覆盖原来的属性;setHeader()会覆盖原来的属性。
3.创建响应正文
在Servlet中,客户端输出的响应数据是通过输出流对象来完成的,HttpServletResponse接口提供了两个获取不同类型输出流对象的方法:
· getOutputStream():返回字节输出流对象ServletOutputStream。
· getWriter():返回字符输出流对象PrintWriter。ServletOutputStream对象主要用于输出二进制字节数据,如配合setContentType()方法响应输出一个图像、视频等。
PrintWriter对象主要用于输出字符文本内容,但其内部实现仍是将字符串转换成某种字符集编码的字节数组后再进行输出。
当向ServletOutputStream或PrintWriter对象中写入数据后,Servlet容器会将这些数据作为响应消息的正文,然后与响应状态行和各响应头组合成完整的响应报文输出到客户端。在Servlet的service()方法结束后,容器还将检查getWriter()或getOutputStream()方法返回的输出流对象是否已经调用过close()方法,如果没有,容器将调用close()方法关闭该输出流对象。
GenericServlet是Servlet接口的实现类,但它是一个抽象类,它唯一的抽象方法就是service()方法,我们可以通过继承GenericServlet来编写自己的Servlet。Servlet每次被访问的时候,Tomcat传递给它一个ServletConfig对象。在所有的方法中,第一个被调用的是init()。
在GenericServlet中定义了一个ServletConfig实例对象,并在init(ServletConfig)方法中以参数方式把ServletConfig赋给了实例变量config。GenericServlet类的很多方法中使用了实例变量config。如果子类覆盖了GenericServlet的init(StringConfig)方法,那么this.config=config这一条语句就会被覆盖,也就是说GenericServlet的实例变量config的值为null,那么所有依赖config的方法都不能使用了。如果真的希望完成一些初始化操作,那么就需要覆盖GenericServlet提供的init()方法,它是没有参数的init()方法,会在init(ServletConfig)方法中被调用。
GenericServlet还实现了ServletConfig接口,所以可以直接调用getInitParameter()、getServletContext()等ServletConfig的方法。
使用GenericService的优点如下:
· 通用Servlet很容易写。
· 有简单的生命周期方法。
· 写通用Servlet只需要继承GenericServlet,重写service()方法。
使用GenericServlet的缺点如下:
· 使用通用Servlet并不是很简单,因为没有类似于HttpServlet中的doGet()、doPost()等方法。
通过前面的知识点和Servlet结构的讲解,我们知道HttpServlet继承了GenericServlet,它包含GenericServlet的所有功能点,同时它也有自己的特殊性。
HttpServlet首先必须读取HTTP请求的内容。Servlet容器负责创建HttpServlet对象,并把HTTP请求直接封装到HttpServlet对象中,大大简化了HttpServlet解析请求数据的工作量。HttpServlet容器响应Web客户请求的流程如下:
(1)Web客户向Servlet容器发出HTTP请求。
(2)Servlet容器解析Web客户的HTTP请求。
(3)Servlet容器创建一个HttpRequest对象,在这个对象中封装HTTP请求信息。
(4)Servlet容器创建一个HttpResponse对象。
(5)Servlet容器调用HttpServlet的service()方法,把HttpRequest和HttpResponse对象作为service()方法的参数传给HttpServlet对象。
(6)HttpServlet调用HttpRequest的有关方法获取HTTP请求信息。
(7)HttpServlet调用HttpResponse的有关方法生成响应数据。
(8)Servlet容器把HttpServlet的响应结果传给Web客户。
过滤器是处于客户端和服务端目标资源之间的一道过滤网,在客户端发送请求时,会先经过过滤器再到Servlet,响应时会根据执行流程再次反向执行过滤器。
过滤器的主要作用是将请求进行过滤处理,然后将过滤后的请求交给下一个资源。其本质是Web应用的一个组成部件,承担了Web应用安全的部分功能,阻止不合法的请求和非法的访问。一般客户端发出请求后会交给Servlet,如果过滤器存在,则客户端发出的请求都先交给过滤器,然后交给Servlet处理。
如果一个Web应用中使用一个过滤器不能解决实际的业务需求,那么可以部署多个过滤器对业务请求多次处理,这样做就组成了一个过滤器链,Web容器在处理过滤器时将按过滤器的先后顺序对请求进行处理,在第一个过滤器处理请求后,会传递给第二个过滤器进行处理,以此类推,直到传递到最后一个过滤器为止,再将请求交给目标资源进行处理,目标资源在处理经过过滤的请求后,其回应信息再从最后一个过滤器依次传递到第一个过滤器,最后传送到客户端。
过滤器的使用场景有登录权限验证、资源访问权限控制、敏感词汇过滤、字符编码转换等,其优势在于代码复用,不必每个Servlet中还要进行相应的操作。
过滤器对象jakarta.servlet.Filter是接口,与其相关的对象还有FilterConfig与FilterChain,分别作为过滤器的配置对象与过滤器的传递工具。在实际开发中,定义过滤器对象只需要直接或间接地实现Filter接口即可。
Servlet过滤器的整体工作流程如图6.2所示
客户端请求访问容器内的Web资源。Servlet容器接收请求,并针对本次请求分别创建一个request对象和response对象。请求到达Web资源之前,先调用Filter的doFilter()方法检查request对象,修改请求头和请求正文,或对请求进行预处理操作。在Filter的doFilter()方法内,调用FilterChain.doFilter()方法将请求传递给下一个过滤器或目标资源。在目标资源生成响应信息返回客户端之前,处理控制权会再次回到Filter的doFilter()方法,执行FilterChain.doFilter()后的语句,检查response对象,修改响应头和响应正文。响应信息返回客户端。
创建一个过滤器对象需要实现Filter接口,同时实现Filter的3个方法。
· init()方法:初始化过滤器。
· destroy()方法:过滤器的销毁方法,主要用于释放资源。
· doFilter()方法:过滤处理的业务逻辑,在请求过滤处理后,需要调用chain参数的doFilter()方法将请求向下传递给下一个过滤器或者目标资源。
过滤器的配置主要分为两个步骤,分别是声明过滤器和创建过滤器映射。
标签用于声明过滤器的对象,在这个标签中必须配置两个元素:和,其中为过滤器的名称,为过滤器的完整类名。
标签用于创建过滤器的映射,其主要作用是指定Web应用中URL应用对应的过滤器处理,在标签中需要指定过滤器的名称和过滤器的URL映射,其中用于定义过滤器的名称,用于指定过滤器应用的URL。
注意:标签中的用于指定已定义的过滤器的名称,必须和标签中的一一对应。
字符编码过滤器,顾名思义就是用于解决字符编码的问题,通俗地讲就是解决Web应用中的中文乱码问题。
前面我们大致提到过解决中文乱码的方法:设置URIEncoding、设置CharacterEncoding、设置ContentType等。这几种解决方案都需要按照一定的规则去配置或者编写代码,一旦出现代码遗漏或者字符设置不一样,都会出现中文乱码问题,所以为了应对这种情况,字符集过滤器应运而生。
过滤器的作用不局限于拦截和筛查,它还可以完成很多其他功能,即过滤器可以在拦截一个请求(或响应)后,对这个请求(或响应)进行其他的处理后再予以放行。
在Servlet技术中定义了一些事件,可以针对这些事件来编写相关的事件监听器,从而对事件做出相应的处理。例如,想要在Web应用程序启动和关闭时执行一些任务(如数据库连接的建立和释放),或者想要监控Session的创建和销毁,就可以通过监听器来实现。
监听器就是一个Java程序,专门用于监听另一个Java对象的方法调用或属性改变,当被监听对象发生上述事件后,监听器某个方法将立即被执行。
详细地说,就是监听器用于监听观察某个事件(程序)的发生情况,当被监听的事件真的发生了,事件发生者(事件源)就会给注册该事件的监听者(监听器)发送消息,告诉监听者某些信息,同时监听者也可以获得一份事件对象,根据这个对象可以获得相关属性和执行相关操作,并做出适当的响应。
监听器可以看成是观察者模式的一种实现。监听器程序中有4种角色:
(1)监听器(监听者):负责监听发生在事件源上的事件,它能够注册在对应的事件源上,当事件发生后会触发执行对应的处理方法(事件处理器)。
(2)事件源(被监听对象,可以产生某些事件的对象):提供订阅与取消监听器的方法,并负责维护监听器列表,以及发送对应的事件给对应的监听器。
(3)事件对象:事件源发生某个动作时,比如某个增、删、改、查的动作,该动作将封装为一个事件对象,并且事件源在通知事件监听器时会把这个事件对象传递过去。
(4)事件处理器:可以作为监听器的成员方法,也可以独立出来注册到监听器中,当监听器接收到对应的事件时,将会调用对应的方法或者事件处理器来处理该事件。
Servlet实现了特定接口的类为监听器,用来监听另一个Java类的方法调用或者属性改变,当被监听的对象发生方法调用或者属性改变后,监听器的对应方法就会立即执行。
Servlet上下文监听器可以监听ServletContext对象的创建、删除以及属性添加、删除和修改操作,该监听器需要用到以下两个接口。
1.ServletContextListener接口
该接口主要实现监听ServletContext的创建和删除。ServletContextListener接口提供了2个方法,它们也被称为Web应用程序的生命周期方法。下面分别进行介绍。
· contextInitialized(ServletContextEvent event)方法:通知正在监听的对象,应用程序已经被加载及初始化。
· contextDestroyed(ServletContextEvent event)方法:通知正在监听的对象,应用程序已经被销毁,即关闭。
2.ServletAttributeListener接口
该接口主要实现监听ServletContext属性的增加、删除和修改。ServletAttributeListener接口提供了以下3个方法。
· attributeAdded(ServletContextAttributeEvent event)方法:当有对象加入application作用域时,通知正在监听的对象。
· attributeReplaced(ServletContextAttributeEvent event)方法:当在application作用域内有对象取代另一个对象时,通知正在监听的对象。
· attributeRemoved(ServletContextAttributeEvent event)方法:当有对象从application作用域内移除时,通知正在监听的对象。
HTTP会话监听(HttpSession)信息有4个接口。
1.HttpSessionListener接口
HttpSessionListener接口实现监听HTTP会话的创建和销毁。HttpSessionListener接口提供了2个方法。
· sessionCreated(HttpSessionEvent event)方法:通知正在监听的对象,会话已经被加载及初始化。
· sessionDestroyed(HttpSessionEvent event)方法:通知正在监听的对象,会话已经被销毁(HttpSessionEvent类的主要方法是getSession(),可以使用该方法回传一个会话对象)。
2.HttpSessionActivationListener接口
HttpSessionActivationListener接口实现监听HTTP会话的active和passivate。HttpSessionActivationListener接口提供了以下3个方法。
· attributeAdded(HttpSessionBindingEvent event)方法:当有对象加入Session的作用域时,通知正在监听的对象。
· attributeReplaced(HttpSessionBindingEvent event)方法:当在Session的作用域内有对象取代另一个对象时,通知正在监听的对象。
· attributeRemoved(HttpSessionBindingEvent event)方法:当有对象从Session的作用域内移除时,通知正在监听的对象(HttpSessionBindingEvent类主要有3个方法:getName()、getSession()和getValues())。
3.HttpBindingListener接口
HttpBindingListener接口实现监听HTTP会话中对象的绑定信息。它是唯一不需要在web.xml中设定监听器的。HttpBindingListener接口提供以下2个方法。
· valueBound(HttpSessionBindingEvent event)方法:当有对象加入Session的作用域内时会被自动调用。
· valueUnBound(HttpSessionBindingEvent event)方法:当有对象从Session的作用域内移除时会被自动调用。
4.HttpSessionAttributeListener接口
HttpSessionAttributeListener接口实现监听HTTP会话中属性的设置请求。HttpSessionAttributeListener接口提供以下2个方法。
· sessionDidActivate(HttpSessionEvent event)方法:通知正在监听的对象,它的会话已经变为有效状态。
· sessionWillPassivate(HttpSessionEvent event)方法:通知正在监听的对象,它的会话已经变为无效状态。
服务端能够在监听程序中获取客户端的请求,然后对请求进行统一处理。要实现客户端的请求和请求参数设置的监听,需要实现两个接口。
1.ServletRequestListener接口
ServletRequestListener接口提供了以下2个方法。
· requestInitalized(ServletRequestEvent event)方法:通知正在监听的对象,ServletRequest已经被加载及初始化。
· requestDestroyed(ServletRequestEvent event)方法:通知正在监听的对象,ServletRequest已经被销毁,即关闭。
2.ServletRequestAttributeListener接口
ServletRequestAttribute接口提供了以下3个方法。
· attributeAdded(ServletRequestAttributeEvent event)方法:当有对象加入request的作用域时,通知正在监听的对象。
· attributeReplaced(ServletRequestAttributeEvent event)方法:当在request的作用域内有对象取代另一个对象时,通知正在监听的对象。
· attributeRemoved(ServletRequestAttributeEvent event)方法:当有对象从request的作用域移除时,通知正在监听的对象。
AsyncListener接口负责管理异步事件,AsyncListener接口提供了4个方法。
· onStartAsync(AsyncEvent event)方法:当异步线程开始时,通知正在监听的对象。
· onError(AsyncEvent event)方法:当异步线程出错时,通知正在监听的对象。
· onTimeout(AsyncEvent event)方法:当异步线程执行超时时,通知正在监听的对象。
· onComplete(AsyncEvent event)方法:当异步线程执行完毕时,通知正在监听的对象。
监听器的作用是监听Web容器的有效事件,它由Servlet容器管理,利用Listener接口监听某个执行程序,并根据该程序的需求做出适当的响应。
Servlet 3.0规范中允许在定义Servlet、Filter与Listener三大组件时使用注解,而不再用web.xml进行注册。Servlet规范允许Web项目没有web.xml配置文件。
Servlet注解方式与传统配置web.xml文件的方式等价,但与之相比,注解方式更清晰、更便利。
Servlet 3.0规范中允许在定义Servlet、Filter与Listener三大组件时使用注解,而不再用web.xml进行注册。Servlet规范允许Web项目没有web.xml配置文件。
Servlet注解方式与传统配置web.xml文件的方式等价,但与之相比,注解方式更清晰、更便利。
1.Servlet注解
Servlet注解用@WebServlet表示,该注解将会在部署时被容器处理,容器将根据具体的属性配置将相应的类部署为Servlet。该注解具有表6.1给出的一些常用属性(以下所有属性均为可选属性,但是value或者urlPatterns通常是必需的,且二者不能共存,如果同时指定,通常会忽略value的取值)。
2.Filter注解
Filter注解用@WebFilter表示,该注解将会在部署时被容器处理,容器将根据具体的属性配置将相应的类部署为过滤器。该注解具有表6.2给出的一些常用属性(以下所有属性均为可选属性,但是value、urlPatterns、servletNames三者必须至少包含一个,且value和urlPatterns不能共存,如果同时指定,通常忽略value的取值)。
3.Listener注解
Listener注解用@WebListener表示,该注解非常简单,只有value一个属性,该属性主要是监听器的描述信息。
Servlet 3.0中提供了对文件上传的原生支持,我们不需要借助任何第三方上传组件,直接使用Servlet 3.0提供的API就能够实现文件上传功能。
@MultipartConfig注解主要是为了辅助Servlet 3.0中HttpServletRequest提供的对上传文件的支持。该注解标注在Servlet上,以表示Servlet希望处理的请求的MIME类型是multipart/form-data。
通常在实际项目中会在服务器固定的目录下配置上传文件存放的路径,这样做避免了因为系统升级(项目部署包升级)导致以前上传的文件丢失的情况。
默认情况下,Web容器会为每个请求分配一个请求处理线程,在响应完成前,该线程资源都不会被释放。也就是说,处理HTTP请求和执行具体业务代码的线程是同一个线程。如果Servlet或Filter中的业务代码处理时间相当长(如数据库操作、跨网络调用等),那么请求处理线程将一直被占用,直到任务结束。这种情况下,随着并发请求数量的增加,可能会导致处理请求线程全部被占用,请求堆积到内部阻塞队列容器中,如果存放请求的阻塞队列也满了,那么后续进来的请求将会遭遇拒绝服务,直到有线程资源可以处理请求为止。
开启异步请求处理之后,Servlet线程不再一直处于阻塞状态以等待业务逻辑的处理,而是启动异步线程之后可以立即返回。异步处理的特性可以帮助应用节省容器中的线程,特别适合执行时间长且用户需要得到响应结果的任务,这将大大减少服务器资源的占用,并且提高并发处理速度。如果用户不需要得到结果,那么直接将一个Runnable对象交给内存中的Executor并立即返回响应即可。
异步处理的步骤如下:
1.开启异步处理
启用异步处理有两种方式:一种是web.xml;另一种是注解形式。为了方便讲解以及学习,笔者后续主要以注解形式为主进行介绍。设置@WebServlet的asyncSupported属性为true,表示支持异步处理:
@WebServlet(asyncSupported = true)
2.启动异步请求
调用req.startAsync(req, resp)方法获取异步处理上下文对象AsyncContext。
3.完成异步处理
在其他线程中执行业务操作,输出结果,并调用asyncContext.complete()完成异步处理。
注意:如果在项目中之前配置过Filter和Listener,就要开启异步支持,否则程序会报错。
Servlet 3.0引入了称为“Web模块部署描述符片段”的web-fragment.xml部署描述文件,该文件必须存放在JAR文件的META-INF目录下,该部署描述文件可以包含一切可以在web.xml中定义的内容。JAR包通常放在WEB-INF/lib目录下,该目录包含所有该模块使用的资源,如.class文件、配置文件等。
现在,为一个Web应用增加一个Servlet配置有如下3种方式:
(1)编写一个类继承HttpServlet,将该类放在classes目录下的对应包结构中,修改web.xml,增加Servlet声明。这是最原始的方式。
(2)编写一个类继承HttpServlet,并且在该类上使用@WebServlet注解将该类声明为Servlet,将该类放在classes目录下的对应包结构中,无须修改web.xml文件。
(3)编写一个类继承HttpServlet,将该类打成JAR包,并在JAR包的META-INF目录下放置一个web-fragment.xml文件,用于声明Servlet配置。
第3篇 Java Web整合开发(第7-10章)
在Java Web开发中,通过页面操作的记录最终都会持久化存储,持久化存储方便管理数据,能保证客户端数据持久化存储在服务端。持久化存储通用的是数据库存储,Java Web通过前端页面与后台数据库交互实现数据互通,常见的数据库操作是JDBC。
JDBC(Java DataBase Connectivity,Java数据库连接)是标准的Java API,是一套客户端程序与数据库交互的规范。JDBC提供了一套通过Java操纵数据库的完整接口,具体就是通过Java连接广泛的数据库,并对表中的数据执行增、删、改、查等操作。
JDBC API库包含与数据库相关的下列功能。
· 制作到数据库的连接。· 创建SQL或MySQL语句。
· 执行SQL或MySQL查询数据库。
· 查看和修改所产生的记录。
从根本上来说,JDBC是一种规范,它提供了一套完整的接口,以方便对底层数据库的访问,因此可以用Java编写不同类型的可执行文件,例如:
· Java应用程序。
· Java Applets。
· Java Servlets。
· Java ServerPages (JSPs)。
· Enterprise JavaBeans (EJBs)。
这些不同的可执行文件通过JDBC驱动程序来访问数据库。
JDBC具有ODBC一样的性能,允许Java程序包含的代码具有不依赖特定数据库的特性,即具有数据库无关性(Database Independent)。
JDBC API是由JDBC驱动程序实现的,不同的数据库对应不同的驱动程序。选择了数据库,就需要使用针对该数据库的JDBC驱动程序,并将对应的JAR包引入项目中。
在调用JDBC API时,JDBC会将请求交给JDBC驱动程序,最终由该驱动程序完成与数据库的交互。此外,数据库驱动程序会提供API操作来实现打开数据库连接、关闭数据库连接以及控制事务等功能。
JDBC的目标是使程序做到“一次编写,到处运行”,使用JDBC API访问数据库,无论是更换数据库还是更换操作系统,都不需要修改调用JDBC API的程序代码,因为JDBC提供了使用相同的API来访问不同的数据库的服务(比如MySQL和Oracle等),这样就可以编写不依赖特定数据库的Java程序。更高层的数据访问框架也是以JDBC为基础构建的。
可以看到,上层应用(Java Application)在使用数据库时,其实不需要知道底层是什么数据库,这部分工作交给JDBC接口完成就行了,因此实现了灵活的接口规范和超强的适配能力。
常见的有关系型数据库MySQL、Oracle、SQL Server、DB2、TiDB等。
在处理过程中,除了在高版本中注册驱动可以省略外,其他步骤都不能省略。尤其是最后的释放资源,否则多次调用极容易导致系统资源耗尽而宕机。
在使用JDBC之前,需要加载和注册JDBC驱动程序。不同厂商对JDBC的实现各不相同,但是它们都遵循JDBC的接口规范,加载和注册驱动程序的代码如下:
Class.forName(driver); //向DriverManager注册驱动程序,即加载驱动程序
注意:MySQL 5之后的驱动JAR包可以省略注册驱动程序的步骤,因为在java.sql.Driver中已经写好了。
DriverManager是驱动程序管理类,负责管理和注册驱动程序,并创建数据库连接。
连接参数说明如下:
· url:不同数据库,定义了不同的字符串连接规范。格式为“协议名:子协议://服务器名或者IP地址:端口号/数据库名?参数=参数值”。例如访问MySQL数据库test的url格式为jdbc:mysql://localhost:3306/test?。如果是本地服务器,访问MySQL的test数据库,url可以简写为:jdbc:mysql:///test(前提必须是本地服务器,且端口必须是3306)。
· user:数据库登录的用户名。
· password:对应user用户名的数据库登录密
Connection接口是特定数据库的连接(会话)接口。在连接上下文中执行SQL语句并返回结果,通过以下方式可以建立连接:
Connection con=DriverManager.getConnection(url, username, password);//建立连接
Connection接口API包含两个重要功能:创建执行SQL语句的对象和事务管理。
1.创建执行SQL语句的对象
JDBC主要用于操作数据库,Connection是抽象的数据库接口,主要是为了获取数据库连接并操作数据库。
2.事务管理
如果JDBC连接处于自动提交模式下,该模式为默认模式,那么每条SQL语句都是在其完成时提交到数据库的。
对简单的应用程序来说这种模式相当好,但有3个原因导致用户可能想关闭自动提交模式,并管理自己的事务:为了提高性能、为了保持业务流程的完整性以及使用分布式事务。
可以通过事务在任意时间来控制以及把更改应用于数据库。它把单条SQL语句或一组SQL语句作为一个逻辑单元,如果其中任一条语句失败,则整个事务失败。
若要启用手动事务模式来代替JDBC驱动程序默认使用的自动提交模式,则调用Connection对象的setAutoCommit()方法。如果把布尔值false传递给setAutoCommit()方法,则表示关闭自动提交模式,如果把布尔值true传递给该方法,则会将再次开启自动提交模式。
Statement代表一条语句对象,用于发送SQL语句给服务器。想完成对数据库的增、删、改、查,只需要通过这个对象向数据库发送增、删、改、查语句即可。
· Statement.executeUpdate方法:用于向数据库发送增、删、改、查的SQL语句。executeUpdate执行完后,将会返回一个整数(增、删、改、查语句导致数据库几行数据发生了变化)。
· Statement.executeQuery方法:用于向数据库发送查询语句,executeQuery方法返回代表查询结果的ResultSet对象。
PreparedStatement是一个特殊的Statement对象,如果只是作为查询数据或者更新数据的接口,用PreparedStatement代替Statement是一个非常理想的选择,因为它有以下优点:
· 简化Statement中的操作。
· 提高执行语句的性能。
· 可读性和可维护性更好。
· 安全性更好。
使用PreparedStatement能够预防SQL注入攻击,所谓SQL注入,指的是通过把SQL命令插入Web表单提交或者输入字段名或页面请求的查询字符串,最终达到欺骗服务器,执行恶意SQL命令的目的。注入只对SQL语句的编译过程有破坏作用,而执行阶段只是把输入串作为数据处理,不再需要对SQL语句进行解析,因此也就避免了类似select * from user where name=‘aa’ and password=‘bb’ or 1=1的SQL注入问题的发生。
作为特殊的Statement对象,这里顺便对两者做个对比。
· 关系:PreparedStatement继承自Statement,都是接口。
· 区别:PreparedStatement可以使用占位符,是预编译的,批处理比Statement效率高。
PreparedStatement使用“?”作为占位符,执行SQL前调用setXX()方法为每个“?”位置的参数赋值。
ResultSet(结果集)封装了数据库返回的结果集。
1.基本的ResultSet
基本的ResultSet的作用就是实现查询结果的存储功能,而且只能读取结果集一次,不能来回地滚动读取。
由于这种结果集不支持滚动读取功能,因此,如果获得这样一个结果集,只能调用结果集的next()方法逐个地读取数据。
2.可滚动的ResultSet
可滚动的ResultSet内部维护一个行游标(在数据库中一行数据即为一条记录,反过来,一条记录即为一行),并提供了一系列方法来移动游标。
· void beforeFirst():把游标放到第一行(即第一条记录)的前面,这也是游标默认的位置。
· void afterLast():把游标放到最后一行(即最后一条记录)的后面。
· boolean first():把游标放到第一行的位置上,返回值表示调控游标是否成功。
· boolean last():把游标放到最后一行的位置上。
· boolean isBeforeFirst():当前游标位置是否在第一行前面。
· boolean isAfterLast():当前游标位置是否在最后一行后面。
· boolean isFirst():当前游标位置是否在第一行上。
· boolean isLast():当前游标位置是否在最后一行上。
· boolean previous():把游标向上挪一行(即向上挪一条记录)。
· boolean next():把游标向下挪一行(即向下挪一条记录)。
· boolean relative(int row):相对位移,当row为正数时,表示向下移动row行,为负数时表示向上移动row行。
· boolean absolute(int row):绝对位移,把游标移动到指定的行上。
· int getRow():返回当前游标所在的行。这些方法分为两类,一类用来判断游标位置,另一类是用来移动游标。
其中两个参数的意义如下:
(1)resultSetType用于设置ResultSet对象的类型为可滚动或者不可滚动,其取值如下:
· ResultSet.TYPE_FORWARD_ONLY:只能向前滚动。
· ResultSet.TYPE_SCROLL_INSENSITIVE和Result.TYPE_SCROLL_SENSITIVE:这两个值用于实现任意前、后滚动(各种移动的ResultSet游标)的方法。二者的区别在于前者对于修改不敏感,而后者对于修改敏感。
(2)resultSetConcurency用于设置ResultSet对象是否能修改,其取值如下:
· ResultSet.CONCUR_READ_ONLY:设置为只读类型的参数。
· ResultSet.CONCUR_UPDATABLE:设置为可修改类型的参数。
所以,如果想要得到具有可滚动类型的ResultSet对象,只要把Statement赋值为ResultSet.CONCUR_READ_ONLY即可。执行这个Statement查询语句得到的就是可滚动的ResultSet对象。
3.可更新的ResultSet
可更新的ResultSet对象可以完成对数据库中表的修改,结果集只是相当于数据库中表的视图,所以并不是所有的ResultSet只要设置了可更新就能够完成更新,能够完成更新的ResultSet的SQL语句必须具备如下属性:
· 只引用了单个表。
· 不含有join或者group by子句。
· 列中要包含主键。
具有上述条件的、可更新的ResultSet可以完成对数据的修改,可更新的ResultSet的创建方法如下:
Statement st = conn.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_UPDATABLE);
执行结果得到的就是可更新的ResultSet。若要更新,把结果集中的游标移动到要更新的行,然后调用updateXXX()方法,其中XXX的含义和getXXX()方法中的XXX是相同的。updateXXX()方法有两个参数!第一个是要更新的列(即数据库的字段),可以是列名或者序号;第二个是要更新的数据,这个数据类型要和XXX相同。每完成一行(一条记录)的更新,都要调用updateRow()完成对数据库的写入,而且是在ResultSet的游标没有离开该修改行之前,否则修改将不会被提交。
调用updateXXX()方法还可以完成插入操作。下面先要介绍其他两个方法:
(1)moveToInsertRow():用于把ResultSet的游标移动到插入行,这个插入行是表中特殊的一行,不需要指定具体哪一行,只要调用这个方法,系统会自动移动到那一行的。
(2)moveToCurrentRow():用于把ResultSet的游标移动到记忆中的某个行,通常是当前行。如果没有执行过insert操作,这个方法就没有什么效果,如果执行过insert操作,这个方法用于返回insert操作之前所在的那一行,即离开插入行回到当前行,当然也可以通过next()、previous()等方法离开插入行返回当前行。
要完成对数据库的插入操作,首先调用moveToInsertRow()把游标移动到插入行,然后调用updateXXX()方法完成对各列数据(即插入记录的各个字段)的更新,与更新操作一样,更新的内容要写到数据库中。不过这里调用的是insertRow(),因此还要保证在该方法执行之前结果集的游标没有离开插入行,否则插入操作不会被执行,并且对插入行的更新操作将被放弃。
4.可保持的ResultSet
正常情况下,如果使用Statement执行完一个查询,又去执行另一个查询,这时第一个查询的结果集就会被关闭,也就是说,所有的Statement查询语句对应的结果集是一个,如果调用Connection的commit()方法也会关闭结果集。可保持性就是指当ResultSet结果集被提交时,该结果集是被关闭还是不被关闭。JDBC 2.0和1.0都是提交后ResultSet结果集就会被关闭。不过在JDBC 3.0中提供了可以设置ResultSet是否关闭的操作。要创建这样的ResultSet对象,生成Statement对象的时候需要有3个参数,
前两个参数及其createStatement()方法中的参数是完全相同的,这里只介绍第3个参数resultSetHoldability,它用于表示在ResultSet提交后是打开还是关闭,该参数的取值有两个:
· ResultSet.HOLD_CURSORS_OVER_COMMIT:表示修改提交时,不关闭ResultSet。
· ResultSet.CLOSE_CURSORS_AT_COMMIT:表示修改提交时ResultSet关闭。
当使用ResultSet对象且查询出来的数据集记录很多时,假如有一千万条时,ResultSet对象是否会占用很多内存?如果记录过多,那么程序会不会耗尽系统的内存呢?答案是不会,ResultSet对象表面上看起来是查询数据库数据记录一个结果集,其实这个对象中只是存储了结果集的相关信息,查询到的数据库记录并没有存放在该对象中,这些内容要等到用户通过next()方法和相关的getXXX()方法提取数据记录的字段内容时才能从数据库中得到,这些相关信息并不会占用内存,只有当用户将记录集中的数据提取出来加入自己的记录集中时才会消耗内存,如果用户没有使用记录集,就不会发生严重消耗内存的情况。
在数据库命令中,使用insert实现添加数据的功能。结合JDBC API中Statement接口的作用,通过JDBC执行SQL语句,即可把数据添加到数据库中。
【例7.6】创建数据库连接
conn = DriverManager.getConnection(url,user,pwd);
stat = conn.createStatement();
insertEmp(stat); // 插入数据
conn.close();
可以看到,上面的代码在中间行调用了insertEmp()方法,该自定义的方法insertEmp()中包含了将添加数据的语句,具体代码如下
查询数据的命令是select,查询的结果是一个数据集合,需要用到前面讲解的ResultSet对象来处理结果集。
【例7.8】查询数据
conn = DriverManager.getConnection(url,user,pwd);
stat = conn.createStatement();
// insertEmp(stat); // 插入数据
queryEmp(stat); // 查询数据
conn.close();
修改数据的命令是update,数据是否真实发生了变化,需要执行上一节查询数据的程序代码来查看。
【例7.9】修改数据
conn = DriverManager.getConnection(url,user,pwd);
stat = conn.createStatement();
// insertEmp(stat); // 插入数据
// queryEmp(stat); // 查询数据
updateEmp(stat);
conn.close();
删除数据的命令是delete,与修改数据一样,数据是否真实发生了变化,删除成功之后需要执行查询数据的程序代码来查看。
【例7.10】删除数据
conn = DriverManager.getConnection(url,user,pwd);
stat = conn.createStatement();
// insertEmp(stat); // 插入数据
// queryEmp(stat); // 查询数据
// updateEmp(stat);
deleteEmp(stat);
conn.close();
批处理是指将关联的SQL语句组合成一个批处理,并将它当成一个调用提交给数据库。
这个批处理包含多条SQL语句,这样做可以减少通信资源的消耗,从而提高程序执行的性能。
JDBC驱动程序不一定支持批处理功能。
调用DatabaseMetaData.supportsBatchUpdates()方法来确定目标数据库是否支持批处理更新。如果JDBC驱动程序支持此功能,则该方法返回值为true。
Statement、PreparedStatement和CallableStatement的addBatch()方法用于添加单条语句到批处理中。
executeBatch()方法用于启动执行所有组合在一起的语句,即批处理。
executeBatch()方法返回一个整数数组,数组中的每个元素代表各自的更新语句的更新数目。
正如可以添加语句到批处理中,也可以调用clearBatch()方法删除批处理,此方法删除所有用addBatch()方法添加的语句。
1.批处理和Statement对象
通过Statement对象使用批处理的典型步骤如下:
(1)调用createStatement()方法创建一个Statement对象。
(2)调用setAutoCommit()方法将自动提交设为false。
(3)Statement对象调用addBatch()方法来添加用户想要的所有SQL语句。
(4)Statement对象调用executeBatch()执行所有的SQL语句。
(5)调用commit()方法提交所有的更改。
2.批处理和PreparedStatement对象
通过prepareStatement对象使用批处理的典型步骤如下:
(1)使用占位符创建SQL语句。
(2)调用任一prepareStatement()方法创建prepareStatement对象。
(3)调用setAutoCommit()方法将自动提交设为false。
(4)Statement对象调用addBatch()方法来添加用户想要的所有SQL语句。
(5)Statement对象调用executeBatch()执行所有的SQL语句。
(6)调用commit()方法提交所有的更改。
存储过程其实是数据库一段代码片段的执行。调用存储过程之前,需要先在数据库中创建存储过程。
1.传统JSP模式
传统的网页HTML文件可以作为页面来浏览。同时,又兼容Java代码,可以用来处理业务逻辑。对于功能单一、需求稳定的项目,可以把页面展示逻辑和业务逻辑都放到JSP中。
· 优点:编程简单,易上手,容易控制。
· 缺点:前后端职责不清晰,可维护性差。
2.Model1模式(JSP + JavaBean)
该模式可以看作是对传统JSP模式的增强,加入了JavaBean或Servlet,将页面展示逻辑和业务逻辑做了分离。JSP只负责显示页面,JavaBean或者Servlet负责收集数据,以及返回处理结果。
· 优点:架构简单,适合中小型项目。
· 缺点:虽然分离出了业务逻辑,但是JSP中仍然包含页面展示逻辑和流程控制逻辑,不利于维护。
3.Model2模式(JSP + Servlet + JavaBean)
该模式也可以看作是传统MVC模式。为了更好地进行职责划分,将流程控制逻辑也分离了出来。JSP负责页面展示,以及与用户的交互——展示逻辑;Servlet负责控制数据显示和状态更新——控制逻辑;JavaBean负责操作和处理数据——业务逻辑。
· 优点:分工明确,层次清晰,能够更好地适应需求的变化,适合大型项目。
· 缺点:相对复杂,严重依赖Servlet API,JavaBean组件类过于庞大。
4.MVC模式
MVC模式是在Model2的基础上,对前后端进一步分工。由于Ajax接口要求业务逻辑被移动到浏览器端,因此浏览器端为了应对更多业务逻辑变得复杂。MVC模式因其明确分工,极受推崇,由此涌现出了很多基于MVC模式的开发框架,如Struts、Spring MVC等。再加上Spring开源框架强大的兼容特性,进而形成了可以适应绝大多数业务需求的经典框架组合,如SSH、SSM等。
前后端可以在约定接口后实现高效并行开发。
前端开发的复杂度控制比较困难。
5.前端为主的MV*模式
为了降低前端开发的复杂度,涌现出了大量的前端框架,如EmberJS、KnockoutJS、AngularJS等。它们的原则是先按照类型分层,如Templates、Controllers、Models等,然后在层内按照业务功能切分。
好处:
· 前后端职责清晰,在各自的工作环境开发,容易测试。
· 前端开发的复杂度可控,通过组件化组织结构。
· 部署相对独立。
不足:
· 前后端代码不能复用。
· 全异步不利于SEO(Search Engine Optimization,搜索引擎优化)。
· 性能并非最佳,尤其是移动互联网的环境下。
· SPA不能满足所有需求。
6.全栈模式
随着Node.js的兴起,全栈开发模式逐步成为主流开发模式,比如MEAN(MongoDB、Express、AngularJS、NodeJS)框架组合、React+Redux等。全栈模式把UI分为前端UI和后端UI。前端UI层处理浏览器层的展现逻辑,主要技术为HTML+CSS+JavaScript。后端UI层处理路由、模板、数据获取等。前端可以自由调控,后端可以专注于业务逻辑层的开发。
好处:
· 前后端的部分代码可以复用。
· 若需要SEO,可以在服务端同步渲染。
· 请求太多导致的性能问题可以通过服务端缓解。
挑战:
· 需要前端对服务端编程有进一步的了解。
· Node层与Java层能否高效通信尚需要验证。
· 需要更多经验才能对部署、运维层面熟悉了解。
分页查询是Java Web开发中经常使用到的技术。在数据库中的数据量非常大的情况下,不适合将所有的数据全部显示到一个页面中,同时为了节约程序以及数据库的资源,需要对数据进行分页查询操作。
通过JDBC实现分页的方法比较多,而且不同的数据库机制的分页方式也不同,这里介绍两种典型的分页方法。
1.通过ResultSet的游标实现分页(伪分页)
该分页方法可以在各种数据库之间通用,但是带来的缺点是占用了大量的资源,不适合在数据量大的情况下使用。
2.通过数据库机制进行分页
很多数据库都会提供这种分页机制,例如SQL Server中提供了top关键字,MySQL数据库中提供了limit关键字,用这些关键字可以设置数据返回的记录数。使用这种分页查询方式可以减少数据库的资源开销,提高程序效率,但是缺点是只适用于一种数据库。
考虑到第一种分页方法在数据量大的情况下效率很低,基本上实际开发中都会选择第二种分页方法。
EL(Expression Language)即表达式语言,通常称为EL表达式。通过EL表达式可以简化在JSP开发中对对象的引用,从而规范页面代码,增加程序的可读性及可维护性。
EL表达式主要是代替JSP页面中的表达式脚本,在JSP页面中输出数据。EL表达式在输出数据的时候,要比JSP的表达式脚本简洁很多。
在JSP页面的任何静态部分均可通过${expression}来获取指定表达式的值。
expression用于指定要输出的内容,可以是字符串,也可以是由EL运算符组成的表达式。
EL表达式的取值是从左到右进行的,计算结果的类型为String,并且连接在一起。例如, 1 + 2 {1+2 } 1+2{2+3}的结果是35。
EL表达式可以返回任意类型的值。如果EL表达式的结果是一个带有属性的对象,则可以利用“[ ]”或者“.”运算符来访问该属性。这两个运算符类似,“[ ]”比较规范,而“.”比较快捷。可以使用以下任意一种形式: o b j e c t [ " p r o p e r t y N a m e " ] 或者 {object["propertyName"]}或者 object["propertyName"]或者{object.propertyName},但是如果propertyName不是有效的Java变量名,则只能用[ ]运算符,否则会导致异常。
EL表达式语法简单,其语法有以下几个要点:
· EL可以与JSTL结合使用,也可以和JavaScript语句结合使用。
· EL可以自动转换类型。如果想通过EL输入两个字符串型数值的和,可以直接通过“+”进行连接,如${num1+num2}。
· EL既可以访问一般的变量,也可以访问JavaBean中的属性和嵌套属性、集合对象。
· EL中可以执行算术运算、逻辑运算、关系运算和条件运算等。
· EL中可以获得命名空间(PageContext对象是页面中所有其他内置对象中作用域最大的集成对象,通过它可以访问其他内置对象)。
· EL中在进行除法运算时,如果除数是0,则返回无穷大(Infinity),而不返回错误。
· EL中可以访问JSP的作用域(request、session、application以及page)。
· 扩展函数可以映射到Java类的静态方法。
目前只要安装的Web服务器能够支持Servlet 2.4/JSP 2.0,就可以在JSP页面中直接使用EL。由于在JSP 2.0以前的版本中没有EL,因此JSP为了和以前的规范兼容,还提供了禁用EL的方法。
1.使用反斜杠“\”符号
只需要在EL的起始标记“$”前加上“\”即可。
2.使用page指令
使用JSP的page指令也可以禁用EL表达式,语法格式如下:
<%@ page isELIgnored="true"%> <!-- true为禁用EL -->
3.在web.xml文件中配置元素
web.xml禁用EL表达式的语法格式如下:
基于当前服务端部署的情况,99%的环境都支持EL表达式,所以极少遇到要兼容低版本的情况。但是,在调试程序的时候,如果遇到了EL表达式无效,应该考虑到可能是版本兼容的问题,这样或许能快速解决问题。
在EL表达式中,经常需要使用一些符号来标记一些名称,如变量名、自定义函数名等,这些符号被称为标识符。EL表达式中的标识符可以由任意顺序的大小写字母、数字和下画线组成,为了避免出现非法的标识符,在定义标识符时还需要遵循以下规范:
· 不能以数字开头。
· 不能是EL中的保留字,如and、or、gt。
· 不能是EL隐式对象,如pageContext。
· 不能包含单引号“'”、双引号“"”、减号“-”和正斜线“/”等特殊字符。
保留字就是编程语言中事先定义并赋予特殊含义的单词,和其他编程语言一样,EL表达式中也定义了许多保留字,如false、not等,接下来就列举EL中所有的保留字。
需要注意的是,EL表达式中的这些保留字不能作为标识符,以免在程序编译时发生错误。
EL获取数据的语法:${标识符},用于获取作用域中的数据,包括简单数据和对象数据。
1.获取简单数据
简单数据指非对象类型的数据,比如String、Integer、基本类型等。获取简单数据的语法:${key},key就是保存数据的关键字或属性名,数据通常要保存在作用域对象中,EL在获取数据时,会依次从page、request、session、application作用域对象中查找,找到了就返回数据,找不到就返回空字符串。
2.获取JavaBean对象数据
EL获取JavaBean对象数据的本质是调用JavaBean对象属性xxx对应的getXxx()方法,例如执行${u.name},就是在调用对象的getName()方法。常见错误:如果在编写JavaBean类时没有提供某个属性xxx对应的getXxx()方法,那么在页面上用EL来获取xxx属性值就会报错:属性xxx无法读取,缺少getXxx()方法。
3.EL访问List集合指定位置的数据
List访问与Java语法的List类似。
EL算术运算与Java基本一样。
EL的“+”运算符与Java的“+”运算符不同,它不能实现两个字符串之间的串接。如果使用该运算符串接两个不可以转换为数值类型的字符串,将抛出异常;如果使用该运算符串接两个可以转换为数值类型的字符串,EL会自动将这两个字符串转换为数值类型,再进行加法运算。
在EL表达式中判断对象是否为空可以通过empty运算符实现,该运算符是一个前缀(Prefix)运算符,即empty运算符位于操作数前方(即操作数左侧),用来确定一个对象或变量是否为null或空。empty运算符的格式如下:
${empty expression}
<% request.setAttribute("name1",""); %>
<% request.setAttribute("name2",null); %>
<h3>EL判断对象是否为空</h3>
对象name1=''是否为空:${empty name1}<br>
对象name2 null是否为空:${empty name2}<br>
注意:一个变量或对象为null或空代表的意义是不同的。null表示这个变量没有指明任何对象,而空表示这个变量所属的对象的内容为空,例如空字符串、空的数组或者空的List容器。empty运算符也可以与not运算符结合使用,用于判断一个对象或变量是否为非空。
逻辑关系运算比较简单。
EL表达式的条件运算使用简单、方便,和Java语言中的用法完全一致,也称三目运算,其语法格式如下:
${条件表达式?表达式1 :表达式2}
EL表达式中定义了11个隐含对象,跟JSP内置对象类似,在EL表达式中可以直接使用。
页面上下文对象为pageContext,用于访问JSP的内置对象中的request、response、out、session、exception、page以及servletContext,获取这些内置对象后就可以获取相关属性值。
EL表达式提供了4个用于访问作用域内的隐含对象,即pageScope、requestScope、sessionScope和applicationScope。指定要查找的标识符的作用域后,系统将不再按照默认的顺序(page、request、session及application)来查找相应的标识符,这4个隐含对象只能用来取得指定作用域内的属性值,而不能取得其他相关信息。
EL表达式剩余的6个隐含对象是访问环境信息的,它们分别是param、paramValues、header、headerValues、cookie和initParam。
EL原本是JSTL 1.0中的技术,但是从JSP 2.0开始,EL就分离出来纳入JSP的标准了,不过EL函数还是和JSTL技术绑定在一起。
自定义和使用EL函数分为以下3个步骤:
(1)编写Java类,并提供公有静态方法,用于实现EL函数的具体功能。
自定义编写的Java类必须是public类中的public static函数,每一个静态函数都可以成为一个EL函数。
(2)编写标签库描述文件,对函数进行声明。
编写TLD(Tag Library Descriptor,标签库描述符)文件,注册EL函数,使之可以在JSP中被识别。文件扩展名为.tld,可以放在WEB-INF目录下,或者是WEB-INF目录下的子目录中。
(3)在JSP页面中添加taglib指令,导入自定义标签库。
用taglib指令导入自定义的EL函数库。注意,taglib的uri填写的是步骤2中tld定义的uri,prefix是tld定义中的function的shortname。
在JSP诞生之初,JSP提供了在HTML代码中嵌入Java代码的特性,这使得开发者可以利用Java语言的优势来完成许多复杂的业务逻辑。但是随着开发者发现在HTML代码中嵌入过多的Java代码,程序员对于动辄上千行的JSP代码基本丧失了维护能力,非常不利于JSP的维护和扩展。基于上述情况,开发者尝试使用一种新的技术来解决上述问题。从JSP 1.1规范后,JSP增加了JSTL标签库的支持,提供了Java脚本的复用性,提高了开发者的开发效率。
JSTL(Java Server Pages Standarded Tag Library,JSP标准标签库)是由JCP(Java Community Process)制定的标准规范,它主要为Java Web开发人员提供标准通用的标签库,开发人员可以利用这些标签取代JSP页面上的Java代码,从而提高程序的可读性,降低程序的维护难度。
JSTL标签是基于JSP页面的,这些标签可以插入JSP代码中,本质上JSTL也是提前定义好的一组标签,这些标签封装了不同的功能。JSTL的目标是简化JSP页面的设计。对于页面设计人员来说,使用脚本语言操作动态数据是比较困难的,而采用标签和表达式语言则相对容易,JSTL的使用为页面设计人员和程序开发人员的分工协作提供了便利。
JSTL标签库极大地减少了JSP页面嵌入的Java代码,使得Java核心业务代码与页面展示JSP代码分离,这比较符合MVC(Model、View、Controller)的设计理念。
核心标签是常用的JSTL标签。引用核心标签库的语法如下:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:out>标签用来显示数据对象(字符串、表达式)的内容或结果。该标签类似于JSP的表达式<%=表达式%>或者EL表达式${expression}。
其语法格式如下:
语法一:
<c:out value="expression" [escapeXml="true|false"] [default="defaultValue"] />
语法二:
<c:out value="expression" [escapeXml="true|false"]>defalultValue</c:out>
参数说明如下:
· value:用于指定将要输出的变量和表达式。该属性的值类似于Object,可以使用EL。
· escapeXml:可选属性,用于指定是否转换特殊字符,可以被转换的字符如表9.1所示。属性值可以为true或false,默认值为true,表示进行转换。例如,将“<”转换为“<”。
· default:可选属性,用于指定value属性值为null时将要显示的默认值。如果没有指定该属性,并且value属性的值为null,该标签将输出空的字符串。
示例代码如下:
<h4><c:out>变量输出标签</h4>
<li><c:out value="out输出示例"></c:out></li>
<li><c:out value="<未进行字符转义>" /></li>
<li><c:out value="<进行字符转义>" escapeXml="false" /></li>
<li><c:out value="${null}">使用了默认值</c:out></li>
<li><c:out value="${null}"></c:out></li>
<c:set>用于在指定作用域内定义保存某个值的变量,或为指定的对象设置属性值。
其语法格式如下:
语法一:
<c:set var="name" value="value" [scope="page|request|session|application"] />
语法二:
<c:set var="name" [scope="page|request|session|application"]>value</c:set>
语法三:
<c:set target="obj" property="name" value="value" />
语法四:
<c:set target="obj" property="name">value</c:set>
参数说明如下:
· var:用于指定变量名。通过该标签定义的变量名,可以通过EL表达式为<c:out>的value属性赋值。
· value:用于指定变量值,可以使用EL表达式。
· scope:用于指定变量的作用域,默认值为page,可选值包括page、request、session或application。
· target:用于指定存储变量值或者标签体的目标对象,可以是JavaBean或Map对象。
<c:remove>标签主要用来从指定的JSP作用域内移除指定的变量。
其语法格式如下:
<c:remove var="name" [scope="page|request|session|application"] />
参数说明如下:
· var:用于指定要移除的变量名。
· scope:用于指定变量的作用域,默认值为page,可选值包括page、request、session或application。
示例代码如下:
<h4><c:remove>变量移除标签</h4>
<li>remove之前name1的值:<c:out value="apple" default="空" /></li>
<c:remove var="name1" />
<li>remove之后name1的值:<c:out value="${name1}" default="空" /></li>
<c:catch>标签用来处理JSP页面中产生的异常,并将异常信息存储起来。
其语法格式如下:
<c:catch var="name1">容易产生异常的代码</c:catch>
参数说明如下:
· var:用户定义存取异常信息的变量的名称。省略后也能够实现异常的捕获,但是不能显式地输出异常信息。
JSTL中提供了4类与URL相关的标签,分别是<c:import>、<c:url>、<c:redirect>和<c:param>。<c:param>标签通常与其他标签配合使用。
<c:import>标签的功能是在一个JSP页面导入另一个资源,资源可以是静态文本,也可以是动态页面,还可以导入其他网站的资源。
其语法格式如下:
<c:import url="" [var="name"] [scope="page|request|session|application"] />
参数说明如下:
· url:待导入资源的URL,可以是相对路径或绝对路径,并且可以导入其他主机资源。
· var:用来保存外部资源的变量。
· scope:用于指定变量的作用域,默认值为page,可选值包括page、request、session或application。
<c:url>标签用于生成一个URL路径的字符串,可以赋予HTML的标记实现URL的链接,或者用它实现网页转发与重定向等。
其语法格式如下:
语法一:指定一个URL不做修改,可以选择把该URL存储在JSP不同的作用域内。
<c:url value="value" [var="name"][scope="page|request|session|application"]
[context="context"]/>
语法二:给URL加上指定参数及参数值,可以选择以name存储该URL。
参数说明如下:
· value:指定要构造的URL。
· context:当要使用相对路径导入同一个服务器下的其他Web应用程序中的URL地址时,context属性用于指定其他Web应用程序的名称。
· var:指定属性名,将构造出的URL结果保存到Web域内的属性中。
· scope:指定URL的作用域,默认值为page,可选值包括page、request、session或application。
<c:redirect>标签用来实现请求的重定向,同时可以在URL中加入指定的参数。例如,对用户输入的用户名和密码进行验证,如果验证不成功,则重定向到登录页面;或者实现Web应用不同模块之间的衔接。
其语法格式如下:
语法一:
<c:redirect url="url" [context="context"]>
语法二:
参数说明:
· url:指定重定向页面的地址,可以是一个String类型的绝对地址或相对地址。
· context:当要使用相对路径重定向到同一个服务器下的其他Web应用程序中的资源时,context属性指定其他Web应用程序的名称。
流程控制标签主要用于对页面简单的业务逻辑进行控制。JSTL中提供了4个流程控制标签:<c:if>标签、<c:choose>标签、<c:when>标签和<c:otherwise>标签。
在程序开发中,经常要用到if语句进行条件判断,同样,JSP页面提供<c:if>标签用于条件判断。
其语法格式如下:
语法一:
<c:if test="cond" var="name" [scope="page|request|session|application"] />
语法二:
<c:if test="cond" var="name" [scope="page|request|session|application"]>
Content</c:if>
参数说明如下:
· test:用于存放判断的条件,一般使用EL表达式来编写。
· var:指定变量名称,用来存放判断的结果为true或false。
· scope:指定变量的作用域,默认值为page,可选值包括page、request、session或application。
<c:choose>、<c:when>和<c:otherwise>三个标签通常是一起使用的,<c:choose>标签作为<c:when>和<c:otherwise>标签的父标签来使用,其语法格式如下:
<c:when>标签是包含在<c:choose>标签中的子标签,它根据不同的条件执行相应的业务逻辑,可以存在多个<c:when>标签来处理不同条件的业务逻辑。
<c:when>的test属性是条件表达式,如果满足条件,即进入相应的业务逻辑处理模块。<c:when>标签必须出现在<c:otherwise>标签之前。
<c:otherwise>标签也是一个包含在<c:choose>标签中的子标签,用于定义<c:choose>标签中的默认条件处理逻辑,如果没有任何一个结果满足<c:when>标签指定的条件,则会执行这个标签主体中定义的逻辑代码。在<c:choose>标签范围内只能存在该标签的一个定义。
循环标签是程序算法中的重要环节,有很多常用的算法都是在循环中完成的,如递归算法、查询算法和排序算法等。同时,循环标签也是十分常用的标签,获取的数据集在JSP页面展示几乎都是通过循环标签来实现的。JSTL标签库中包含<c:forEach>和<c:forTokens>两个循环标签。
<c:forEach>循环标签可以根据循环条件对一个Collection集合中的一系列对象进行迭代输出,并且可以指定迭代次数,从中取出目标数据。如果在JSP页面中使用Java代码来遍历数据,则会使页面非常混乱,不利于维护和分析。使用<c:forEach>循环标签可以使页面更加直观、简洁。
其语法格式如下:
参数说明如下:
· var:也就是保存在Collection集合类中的对象名称。
· items:将要迭代的集合类名。
· varStatus:存储迭代的状态信息,可以访问迭代自身的信息。
· begin:如果指定了begin值,就表示从items[begin]开始迭代,如果没有指定begin值,则从集合的第一个值开始迭代。
· end:表示迭代到集合的end位时结束,如果没有指定end值,则表示一直迭代到集合的最后一位。
· step:指定迭代的步长。
<c:forTokens>标签和Java中的StringTokenizer类的作用非常相似,它通过items属性来指定一个特定的字符串,然后通过delims属性指定一种分隔符(可以同时指定多个分隔符)。通过指定的分隔符把items属性指定的字符串进行分组。和forEach标签一样,forTokens标签也可以指定begin、end以及step属性值。
其语法格式如下:
参数说明如下:
· var:进行迭代的参数名称。
· items:指定进行标签化的字符串。
· varStatus:每次迭代的状态信息。
· delims:使用这个属性指定的分隔符来分隔items指定的字符串。
· begin:开始迭代的位置。
· end:迭代结束的位置。
· step:迭代的步长。
随着我们对Java Web的深入学习,需要掌握的知识点和技术越来越多,比如JSP、HTML、XML、Servlet、EL、JSTL、JavaBean、JDBC等。随着使用的技术越来越多,使用的方法和学习的成本也越来越多。那么,有没有一种集成的技术能解决上述这些问题呢?针对这些问题,Ajax技术应运而生。
Ajax(Asynchronous JavaScript And XML,异步JavaScript和XML)用来描述一种使用现有技术的集合,包括HTML/XHTML、CSS、JavaScript、DOM、XML、XSLT以及最重要的XMLHttpRequest。使用Ajax技术,网页应用能够快速地将增量更新呈现在用户界面上,而不需要重载(刷新)整个页面,这使得程序能够更快地回应用户的操作。之前没有Ajax的时候是加载整个界面,现在Ajax技术在不加载整个界面的情况下,就可以对部分界面的功能进行更新。浏览器通过JavaScript中的Ajax向服务器发送请求,然后将处理过后的数据响应给浏览器,把改变过的部分更新到浏览器界面。
Ajax的主要特性如下:
· 按需取数据,减少了冗余请求和响应对服务器造成的负担。页面不读取无用的冗余数据,而是在用户操作过程中当某项交互需要某部分数据时才会向服务器发送请求。
· 无刷新更新页面,减少用户实际和心理的等待时间。客户端利用XML HTTP发送请求得到服务端的应答数据,在不重新载入整个页面的情况下,用JavaScript操作DOM来更新页面。
· Ajax还能实现预读功能。
1.传统网站带给用户体验不好之处
· 无法局部刷新页面。在传统网站中,当页面发生跳转时,需要重新加载整个页面。但是,一个网站中大部分网页的公共部分(头部、底部和侧边栏)都是一样的,没必要重新加载,这样反而延长了用户的等待时间。
· 页面加载的时间长。用户只能通过刷新页面来获取服务器端的数据,若数据量大、网速慢,则用户等待的时间会很长。
· 表单提交的问题。用户提交表单时,如果用户在表单中填写的内容有一项不符合要求,网页就会重新跳转回表单页面。由于页面发生了跳转,用户刚刚填写的信息都消失了,因此需要重新填写。尤其当填写的信息比较多时,每次失败都要重新填写,用户体验就很差。
2.工作原理差异
3.Ajax开发模式的特点
· 页面只需要部分刷新。页面部分刷新极大地提高了用户体验,减少了用户等待的时间。
· 页面不会重新加载,而只做必要的数据更新。
· 异步访问服务器端,提高了用户体验。
可以看出,Ajax开发模式相较于传统模式有了很大的改善,它综合了多种技术,降低了学习成本,提升了网页响应速度,提升了用户体验。
前面提到,Ajax不是一个新技术,它实际上是几种技术的集合,每种技术都有其独特这处,合在一起就成了一个功能强大的新技术。Ajax包括:
· JavaScript:JavaScript是通用的脚本语言,用来嵌入某些应用中。而Ajax应用程序就是使用JavaScript来编写的。
· CSS:CSS为Web页面元素提供了可视化样式的定义方法。在Ajax应用中,用户界面的样式可以通过CSS独立修改。
· DOM:通过JavaScript修改DOM,Ajax应用程序可以在运用时改变用户界面,或者局部更新页面中的某个节点。
· XMLHttpRequest对象:XMLHttpRequest对象允许Web程序员从Web服务器以后台的方式来获取数据。数据的格式通常是XML或者文本。
· XML:可扩展的标记语言(Extensible Markup Language),具有一种开放的、可扩展的、可自描述的语言结构,它已经成为网上数据和文档传输的标准。它是用来描述数据结构的一种语言,正如它的名字一样。它使得对某些结构化数据的定义更加容易,并且可以通过它和其他应用程序交换数据。
· HTML:超文本标记语言,是一种标识性的语言。它包括一系列标签,通过这些标签可以将网络上的文档格式统一,使分散的Internet资源连接为一个逻辑整体。HTML文本是由HTML命令组成的描述性文本,HTML命令可以是说明文字、图形、动画、声音、表格、链接等。
XMLHttpRequest对象是Ajax技术的核心,通过XMLHttpRequest对象,Ajax可以像桌面应用程序一样,只与服务器进行数据层的交换,而不用刷新页面,也不用每次都将数据处理的工作交给服务器来完成。这样既减轻了服务器的负担,又加快了响应速度,缩短了用户等待的时间。
在使用XMLHttpRequest对象发送请求和处理响应之前,首先需要初始化该对象,由于XMLHttpRequest对象不是一个W3C标准,因此在使用XMLHttpRequest对象发生请求和处理之前,需要先在JavaScript代码中获取该对象。通常情况下,获取XMLHttpRequest对象需要判断浏览器的类型,
XMLHttpRequest对象提供了一些常用的方法,通过这些方法可以对请求进行操作。
1.Open()
open()方法用于设置进行异步请求目标的URL、请求方法以及其他参数信息。其语法如下:
open(method, url, async, username, password)
参数说明如下:
· method:必填参数,用于指定用来发送请求的HTTP方法。按照HTTP规范,该参数要大写。
· url:必填参数,用于指定XMLHttpRequest对象把请求发送到的目的服务器所对应的URI,可以使用绝对路径或者相对路径,该路径会被自动解析为绝对路径,并且可以传递查询字符串。
· async:可选参数,该参数用于指定请求是否是异步的,其默认值为true。如果需要发送一个同步请求,则需要把该参数设置为false。
· username、password:可选参数,如果需要服务器验证访问用户的情况,那么可以设置username和password这两个参数。
调用这个方法是安全的,因为调用这个方法时,通常不会打开一个到Web服务器的网络连接。
2.Send()
send()方法用于向服务器发送请求。如果请求声明为异步,该方法将立即返回,否则等到接收到响应为止。调用open()方法后,就可以通过send()方法按照open()方法设定的参数发送请求。当open()方法中的async参数为true时,在send()调用后立即返回,否则将会中断直到请求返回。需要注意的是,send()方法必须在readyState属性值为1时,即调用open()方法以后才能调用。在调用send()方法以后到接收到响应信息之前,readyState属性值将被设为2;一旦接收到响应消息,readyState属性值将会被设为3;直到响应接收完毕,readyState属性的值才会被设为4。其语法如下:
send(data)
3.abort()
abort()方法用于停止或放弃当前的异步请求,并且将XMLHttpRequest对象设置为初始化状态。其语法如下:
abort()
4.setRequestHeader()
setRequestHeader()方法用于为请求的HTTP头设置值。其语法如下:
setRequestHeader(header,value)
参数说明如下:
· header:用于指定HTTP头。
· value:用于为指定的HTTP头设置值。
注意:setRequestHeader()方法必须在调用open()方法之后才能调用。
5.getRequestHeader()
getResponseHeader()方法用于以字符串形式返回指定的HTTP头信息。其语法如下:
getResponseHeader(headerLabel)
参数说明如下:
· headerLabel:用于指定HTTP头,包括Server、Content-Type和Date等。
6.getAllRequestHeaders()
getAllResponseHeaders()方法用于以字符串形式返回完整的HTTP头信息,其中包括Server、Date、Content-Type和Content-Length。其语法如下:
getAllResponseHeaders()
XMLHttpRequest对象提供了一些常用属性,通过这些属性可以获取服务器的响应状态及响应内容。
1.readyState属性
readyState属性用于获取请求的状态。当一个XMLHttpRequest对象被创建后,readyState属性标识了当前对象处于什么状态,可以通过对该属性的访问来判断此次请求的状态,然后做出相应的操作。该属性共包括5个属性值。
2.responseText属性
responseText属性用于获取服务器的响应信息,采用字符串的形式。responseText属性包含客户端接收到的HTTP响应的文本内容。当readyState属性值为0、1或2时,responseText属性包含一个空字符串;当readyState属性值为3(正在接收)时,响应中包含客户端尚未完成的响应信息;当readyState属性的值为4(已加载)时,responseText属性才包含完整的响应信息。
3.responseXML属性
responseXML属性用于获取服务器的响应,采用XML的形式。这个对象可以解析为一个DOM对象。只有当readyState属性的值为4,并且响应头部的Content-Type的MIME类型被指定为XML(text/XML或者Application/XML)时,该属性才会有值,并解析为一个XML文档,否则该属性值为null。如果回传的XML文档结构有瑕疵或者响应回传未完成,则该属性值也为null。由此可见,responseXML属性用来描述被XMLHttpRequest解析后的XML文档属性。
4.status属性
status属性用于返回服务器的HTTP状态码。
注意:仅当readyState属性的值为3(正在接收中)或4(已加载)时,才能对此属性进行访问。如果在readyState属性值小于3时,试图存取status属性的值,则会发生一个异常。
5.statusText属性
statusText属性用于返回HTTP状态码对应的文本,如“OK”或者“Not Fount”(未找到)等。statusText属性描述了HTTP状态代码文本,并且仅当readyState属性值为3或4时才可以使用。当readyState属性为其他值时,试图存取statusText属性值将引发一个异常。
6.onreadystatechange属性
onreadystatechange属性用于指定状态改变时所触发的事件处理器。在Ajax中,每当readyState属性值发生改变时,就会触发onreadystatechange事件,通常会调用一个JavaScript函数。
Ajax可以通过XMLHttpRequest对象实现采用异步方式在后台发送请求。通常情况下,Ajax发送的请求有两种:一种是GET请求;另一种是POST请求。但是无论发送哪种请求,都需要经过以下4个步骤:
(1)初始化XMLHttpRequest对象。为了提高程序的兼容性,需要创建一个跨浏览器的XMLHttpRequest对象,并且判断XMLHttpRequest对象的实例是否成功,如果不成功,则给予提示。
(2)为XMLHttpRequest对象指定一个返回结果处理函数(回调函数),用于对返回结果进行处理。
(3)创建一个与服务器的连接。在创建时,需要指定发送请求的方式(GET或POST),以及设置是否采用异步方式发送请求。
(4)向服务器发送请求。XMLHttpRequest对象的send()方法用于向服务器发送请求,该方法需要传递一个参数,如果发送的是GET请求,则可以将该参数设置为null,如果发送的是POST请求,则可以通过该参数指定要发送的请求参数。
当向服务器发送请求后,接下来就需要处理服务器响应。在向服务器发送请求时,需要通过XMLHttpRequest对象的onreadystatechange属性指定一个回调函数,用于处理服务器响应。在这个回调函数中,首先需要判断服务器的请求状态,保证请求已完成,然后根据服务器的HTTP状态码判断服务器对请求的响应是否成功,如果成功,则把服务器的响应反馈给客户端。
XMLHttpRequest对象提供了两个用来访问服务器响应的属性:一个是responseText属性,返回字符串响应;另一个是responseXML属性,返回XML响应。
1.处理字符串响应
字符串响应通常应用在响应信息不是特别复杂的情况下。例如,将响应信息显示在提示对话框中,或者响应信息只是显示成功或失败的字符串。
2.处理XML响应
如果在服务器端需要生成特别复杂的响应信息,就需要应用XML响应。应用XMLHttpRequest对象的responseXML属性可以生成一个XML文档,而且当前浏览器已经提供了很好的解析XML文档对象的方法。
将数据提交到服务器有两种方法:一种是使用GET方法提交;另一种是使用POST方法提交。使用不同的方法提交数据,在服务器端接收参数时解决中文乱码的方法是不同的。具体解决方法如下:
(1)当接收使用GET方法提交的数据时,要将编码转换为GBK或UTF-8。
(2)由于应用POST方法提交数据时,默认的字符编码是UTF-8,因此当接收使用POST方法提交的数据时,要将编码转换为UTF-8。
由于Ajax在接收responseText或responseXML的值时是按照UTF-8的编码格式进行解码的,因此如果服务器端传递的数据不是UTF-8格式,在接收responseText或responseXML的值时就可能产生乱码。解决的办法是保证从服务器端传递的数据采用UTF-8的编码格式。
提示:在所有页面,传递参数和接收参数的地方,编码格式都使用UTF-8,可以避免绝大部分乱码异常
Ajax的实现主要依赖于XMLHttpRequest对象,但是在调用它进行异步数据传输时,由于XMLHttpRequest对象的实例在处理完事件后就会被销毁,因此如果不对该对象进行封装处理,在下次需要调用它时就要重新构建,而且每次调用都需要写一大段的代码,使用起来很不方便。虽然现在有很多开源的Ajax框架都提供了对XMLHttpRequest对象的封装方案,但是如果应用这些框架,通常需要加载很多额外的资源,这势必会浪费很多服务器资源。不过JavaScript脚本语言支持面向对象的编码风格,通过它可以将Ajax所必需的功能封装在对象中。
讲到重构,不得不讲设计模式。重构的目的就是减少重复代码,方便使用,解藕合,在尽量少改动或者不改动代码的前提下新增或者修改功能。Ajax重构的步骤大致如下。
1.封装功能和方法
封装功能和方法主要是把Ajax创建和使用的一套复杂的流程独立定义处理,避免重复创建和发送请求的复杂代码。
2.引入封装的脚本
封装的是JavaScript脚本,其引入方式如下
<script style="language: javascript" src ="AjaxSample.js" />
src内容可根据实际文件存放的路径进行调整。
3.实现方法的回调
使用Ajax页面实现函数的回调,在回调方法中可以对获取的数据和结果进行处理,此处的回调类似于Java接口的回调。
Ajax的用途非常广,比较常见的有级联下拉列表。
第4篇 SSM框架
到目前为止,Spring框架可以说已经发展成为一个生态体系或者技术体系,它包含Spring Framework、Spring Boot、Spring Cloud、Spring Data、Spring Security、Spring AMQP等项目。而通常所说的Spring一般意义上指的是Spring Framework,即Spring框架。
Spring框架是一个开源的Java平台,最初是由Rod Johnson编写的,并且于2003年6月首次在Apache 2.0许可下发布。
Spring框架是分层的全栈轻量级开源框架,以IoC和AOP为内核,提供了展现层Spring MVC和业务层事务管理等众多的企业级应用技术,为任何类型的部署平台上的基于Java的现代企业应用程序提供了全面的编程和配置模型,已经成为使用最多的JavaEE企业应用开源框架。
简单来说,Spring是一个免费、开源的框架,为简化企业级项目开发提供全面的开发部署解决方案。
模块化的思想是Spring中非常重要的思想,每个模块既可以单独使用,又可以与其他模块联合使用。在项目中用到某些技术的时候,选择相应的技术模块来使用即可,不需要将其他模块引入进来。
Spring的优点如下:
· Spring是开源的且社区活跃,被世界各地开发人员信任以及使用,也有来自科技界所有大厂的贡献,包括阿里巴巴、亚马逊、谷歌、微软等,不用担心框架没人维护或者被废弃的情况。
· Spring Framework提供了一个简易的开发方式,其基础就是Spring Framework的IoC(Inversion of Control,控制反转)和DI(Dependency Injection,依赖注入)。这种开发方式将避免可能致使底层代码变得繁杂混乱的大量属性文件和帮助类。
· Spring提供了对其他各种优秀框架(Struts、Hibernate、Hessian、Quartz等)的直接支持,不同的框架整合更加流畅。
· Spring是高生产力的。Spring Boot改变了程序员的Java编程方式,约定大于配置的思想以及嵌入式的Web服务器资源,从根本上简化了很多繁杂的工作。同时可以将Spring Boot与Spring Cloud丰富的支持库、服务器、模板相结合,快速地构建微服务项目并完美地实现服务治理。
· Spring是高性能的。使用Spring Boot能够快速启动项目,同时新的Spring 5.x支持非阻塞的响应式编程,能够极大地提升响应效率,并且Spring Boot的DevTools可以帮助开发者快速迭代项目。而对于初学者,甚至可以使用Spring Initializr在几秒之内启动一个新的Spring项目。
· Spring是安全的。Spring Security使用户可以更轻松地与行业标准安全方案集成,并提供默认安全的可信解决方案
Spring已经发展到了第5个大版本,新的Spring 5.x有如下几个模块:
· Core:所有Spring框架组件能够正常运行所依赖的核心技术模块,包括IoC容器(依赖注入、控制反转)、事件、资源、i18n、验证、数据绑定、类型转换、SpEL、AOP等。在使用其他模块的时候,核心技术模块是必需的,它提供了基本的Spring功能支持。
· Testing:测试支持模块,包括模拟对象、TestContext框架、Spring MVC测试、WebTestClient(Mock Objects、TestContext Framework、Spring MVC Test、WebTestClient)。
· Data Access:数据库支持模块,包括事务、DAO支持、JDBC、ORM、编组XML(Transactions、DAO Support、JDBC、O/R Mapping、XML Marshalling)。
· Web Servlet:基于Servlet规范的Web框架支持模块,包括Spring MVC、WebSocket、SockJS、STOMP Messaging。它们是同步阻塞式通信的。
· Web Reactive:基于响应式的Web框架支持模块,包括Spring WebFlux、WebClient、WebSocket。它们是异步非阻塞式(响应式)通信的。
· Integration:第三方功能支持模块,包括远程处理、JMS、JCA、JMX、电子邮件、任务、调度、缓存等服务支持。
· Languages:其他基于JVM的语言支持的模块,包括Kotlin、Groovy等动态语言。· Appendix:Spring属性模块。控制Spring框架某些底层方面的属性的静态持有者。Spring Framework 5.x(以下简称Spring 5.x)版本的代码现在已升级为使用Java 8中的新特性,比如接口的static方法、lambda表达式与stream流。因此,如果想要使用Spring 5.x,那么要求开发人员使用的JDK最低版本为JDK 8。
· Appendix:Spring属性模块。控制Spring框架某些底层方面的属性的静态持有者。
Spring Framework 5.x(以下简称Spring 5.x)版本的代码现在已升级为使用Java 8中的新特性,比如接口的static方法、lambda表达式与stream流。因此,如果想要使用Spring 5.x,那么要求开发人员使用的JDK最低版本为JDK 8。
Spring 5引入了Spring Web Flux,它是一个更优秀的非阻塞响应式Web编程框架,而且能更好地处理大量并发连接,不需要依赖Servlet容器,不调用Servlet API,可以在不是Servlet容器的服务器上(如Netty)运行,希望用它来替代Spring MVC。因为Spring MVC是基于Servlet API构建的同步阻塞式I/O的Web框架,这意味着不适合处理大量并发的情况,但是目前Spring 5.x仍然支持Spring MVC。
目前Spring官网提供的快速开始教程都已被替换成了Spring Boot项目。Spring Boot基于约定大于配置的思想,相比传统的Spring项目,提供了开箱即用的编程体验,大大地减少了开发人员编写配置文件的工作,隐藏了很多原理性的东西。为了学得更加深入,先从手写配置文件开始搭建Spring项目,后面再使用Spring Boot技术。
控制反转(Inversion of Control,IoC)是Spring的核心机制,就是将对象创建的方式、属性设置方式反转,以前是开发人员自己通过new控制对象的创建,自己为对象属性赋值。使用Spring之后,将对象和属性的创建及管理交给了Spring,由Spring来负责对象的生命周期、属性控制以及和其他对象间的关系,达到类与类之间的解耦功能,同时还能实现类实例的复用。
DI(Dependency Injection)即依赖注入。Spring官方文档中说:“IoC is also known as dependency injection(DI)”,即IoC也被称为DI。DI是Martin Fowler在2004年初的一篇论文中首次提出的,用于具体描述一个对象获得依赖对象的方式,不是自己主动查找和设置的(比如new、set),而是被动地通过IoC容器注入(设置)进来的。
Spring中管理对象的容器称为IoC容器,IoC容器负责实例化、配置和组装Bean。org.Springframework.beans和org.Springframework.context包是Springframework的IoC容器的基础。
IoC是一个抽象的概念,具体到Spring中是以代码的形式实现的,Spring提供了许多IoC容器的实现,其核心是BeanFactory接口以及它的实现类。BeanFactory接口可以理解为IoC容器的抽象,提供了IoC容器的基本功能,比如对单个Bean的获取、对Bean的作用域判断、获取Bean类型、获取Bean别名等功能。BeanFactory直译过来就是Bean工厂,实际上IoC容器中Bean的获取就是一种典型的工厂模式,里面的Bean常常是单例的(当然也可以是其他类型的)。
简单地说,IoC容器可以理解为一个大的映射Map(键-值对),通过配置的id、name或者其他唯一标识就可以从容器中获取对应的对象。
在Spring中配置Bean有3种方式,分别说明如下。
1.传统XML配置
传统XML模式配置就是11.1.3节讲解的在applicationContext.xml中配置Bean。
2.工厂模式配置
(1)通过静态工厂方式配置Bean(静态工厂,就是将对象直接放在一个静态区里面,想用的时候直接调用就行)。
(2)通过实例工厂方式配置Bean。实例工厂与静态工厂的区别在于一个是静态的,可以直接调用,另一个需要先实例化工厂,再获取工厂里面的对象。
Setter现在是Spring主流的注入方式,它可以利用Java Bean规范所定义的set和get方法来完成注入,可读性和灵活性高,它不需要使用构造器注入时出现的多个参数,可以把构造方法声明成无参构造器,再使用Setter注入设置相对应的值,其本质上是通过Java反射技术来实现的。
构造器注入主要依赖构造方法来实现,构造方法可以是有参的,也可以是无参的,通常都是通过类的构造方法来创建类对象,以及给它赋值,同样Spring也可以采用反射的方式,通过构造方法来完成注入(赋值)。
组件应用程序的Bean经常需要相互协作以完成应用程序的功能,要求Bean能够相互访问,所以就必须在Bean配置文件中指定Bean的引用。在Bean的配置文件中可以通过元素或者ref属性为Bean的属性或构造器参数指定对Bean的引用。也可以在属性或者构造器中包含Bean的声明,这样的Bean称为内部Bean。
当Bean的实例仅供一个特定的属性使用时,可以将它声明为内部Bean,内部Bean声明直接包含在或元素中,不需要设置任何的id或name属性,内部Bean不能使用在任何其他地方。
自动装配是使用Spring满足Bean依赖的一种方法,根据指定装配规则(属性名称或者属性类型),Spring自动将匹配的属性值注入,不再需要手动装配<property name="xxx"ref=“xxx”>。利用Bean标签中的autowire属性进行设置,常用的有两种类型:按Bean名称装配和按Bean类型装配。
byName:根据属性名称注入,注入值Bean的id值和类属性名称一样。
当一个Bean节点带有autowire byName的属性时:
· 将查找其类中所有的set方法名,例如setCat,获得将set去掉并且首字母小写的字符串,即cat。
· 去Spring容器中寻找是否有此字符串名称id的对象。如果有,就取出注入;如果没有,就报空指针异常。
byType:根据类型,如果有两个指定类型的Bean,则报错。
通过属性的类型查找JavaBean依赖的对象并为其注入。如果容器中存在一个与指定属性类型相同的Bean,那么将与该属性进行自动装配。如果存在多个该类型的Bean,那么将会抛出异常,并指出不能使用byType方式进行自动装配。若没有找到相匹配的Bean,则什么事都不发生,属性也不会被设置。
用法和示例跟byName基本一致,特别注意的是,如果存在多个该类型的Bean,那么将会抛出异常。
官方给出的自动装配一共有4种模式,除了前面讲的两种外,还有no和constructor模式。
· no:不启用自动装配,自动装配默认的值。
· constructor:与byType的方式类似,与byType的区别在于它不是使用setter方法注入,而是使用构造器注入。如果在容器中没有找到与构造器参数类型一致的Bean,那么将会抛出异常。
创建一个Bean定义,其实质是使用该Bean定义对应的类来创建真正实例的模板。把Bean定义看成一个模板很有意义,它与class类似,只根据一个模板就可以创建多个实例。
用户不仅可以控制注入对象中的各种依赖和配置值,还可以控制该对象的作用域。这样可以灵活选择所建对象的作用域,而不必在Java Class级定义作用域。Spring Framework支持两种作用域,下面来介绍一下这两种作用域的用法。
如果Bean的作用域的属性被声明为Singleton,那么Spring IoC容器只会创建一个共享的Bean实例。对于所有的Bean请求,只要id与该Bean定义的相匹配,那么Spring在每次需要时都返回同一个Bean实例。
Singleton是单例类型,就是在创建容器时就同时自动创建了一个Bean的对象,无论用户是否使用,它都存在,每次获取到的对象都是同一个对象。注意,Singleton作用域是Spring中的默认作用域。用户可以在Bean的配置文件中设置作用域的属性为Singleton。
由于SingletonBean是单例的作用域,创建两个SingletonBean对象,第二个对象获取SingletonBean对象中的消息值的时候,即使是由一个新的getBean()方法来获取,不用设置对象中消息的值,就可以直接获取SingletonBean中的消息,因为这时的消息已经由第一个对象初始化了。在单例中,每个Spring IoC容器只有一个实例,无论创建多少个对象,调用多少次getMessage()方法获取它,它总是返回同一个实例。
如果Bean的作用域的属性被声明为Prototype,则表示一个Bean定义对应多个对象实例。声明为Prototype作用域的Bean会导致在每次对该Bean请求(将其注入另一个Bean中,或者以程序的方式调用容器的getBean()方法)时都会创建一个新的Bean实例。Prototype是原型类型,它在创建容器的时候并没有实例化,而是当获取Bean的时候才会去创建一个对象,而且每次获取到的对象都不是同一个对象。一般来说,对有状态的Bean应该使用Prototype作用域,而对无状态的Bean则应该使用Singleton作用域。
在实际开发的时候,经常会遇到在Bean使用之前或者之后做一些必要的操作,Spring对Bean的生命周期的操作提供了支持。
在Spring下实现初始化和销毁方法的主要方式如下:
(1)自定义初始化和销毁方法,声明Bean时通过initMethod、destroyMethod指定。
(2)实现InitializingBean、DisposableBean接口。
(3)实现Spring提供的BeanPostProcessor接口。
以上3种方法优先级逐渐升高,即对象创建后最先调用BeanPostProcessor接口的postProcessBeforeInitialization方法,最后调用自定义的通过initMethod声明的初始化方法。初始化结束后,最先调用BeanPostProcessor接口的postProcessAfterInitialization,最后调用自定义的通过destroyMethod声明的初始化方法。这3种方法只针对某个具体的类,BeanPostProcessor会拦截容器中所有的对象。在单例模式下,在Spring容器关闭时会销毁对象。但是在原型模式下,Spring容器不会再管理这个Bean,如果需要,则要自己调用销毁方法。
容器管理Bean的生命周期,可以自定义初始化和销毁方法,容器在Bean进行到当前生命周期的时候来调用自定义的初始化和销毁方法。
在XML配置中,可以通过init-method和destroy-method指定初始化方法和销毁方法。该方法必须没有参数,但是可以抛出异常。
更常用的是通过@Bean(initMethod=“init”,destroyMethod=“destroy”)的方式指定Bean的初始化方法和销毁方法。接下来主要以注解形式来讲解。
可以看到,非单例模式下,即使容器关闭也不会调用mdestroy()方法。因此,只有单例的Bean,在容器创建时才会实例化并执行初始化方法,在容器关闭时执行销毁方法。对于非单例的Bean,只有在创建Bean的时候才会实例化并执行初始化方法,如果要执行多实例Bean的销毁方法,则需要手动调用。
在Spring配置文件或配置类中,往往通过字面值为Bean各种类型的属性提供设置值:无论是double类型还是int类型,在配置文件中都对应字符串类型的字面值。BeanWrapper填充Bean属性时,如何将这个字面值转换为对应的double或int等内部数据类型呢?这里有一个转换器在其中起作用,这个转换器就是属性编辑器。换言之,就是Spring根据已经注册好的属性编辑器解析这些字符串,实例化成对应的类型。
Spring的属性编辑器没有UI界面,只是将配置文件中的文本配置值转换为Bean属性的对应值。Spring在PropertyEditorRegistrySupport中为常见的属性类型提供了默认属性编辑器,分为三大类。
如果Spring应用定义了特殊类型的属性,并且希望在配置文件中以字面值方式来配置属性值,那么就可以编写自定义属性编辑器并注册到Spring容器的方式来实现。Spring默认的属性编辑器大都扩展自java.beans.PropertyEditorSupport,可以通过扩展PropertyEditorSupport来自定义属性编辑器。在Spring环境下仅需要将配置文件中的字面值转换为属性类型的对象即可,并不需要提供UI界面,所以仅需要覆盖PropertyEditorSupport的setAsText()方法就可以了。
1.自定义属性编辑器的具体步骤
自定义一个实现了PropertyEditorSupport接口的编辑器,重写setAsText()方法,然后注册接口。
2.自定义属性编辑器的场景
3.自定义属性编辑器
自定义一个实现了PropertyEditorSupport接口的编辑器,重写setAsText()方法。
可以看到,通过接口实现类型转换,在转换过程中,虽然格式化输出了日期类型,但是结果输出的并不是格式化配置的结果,因为在转换过程中,其日期格式用了默认日期类型,在getAsText()方法中实现控制字符显示的程序代码。
AOP(Aspect Oriented Programming,面向切面编程)是通过预编译方式和运行期间动态代理实现程序功能统一维护的一种技术。AOP是OOP(Object Oriented Programming,面向对象编程)的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容。
在OOP的编程思维中,基本模块单元是类(Class),OOP将不同的业务对象抽象成一个个类,不同的业务操作抽象成不同的方法,这样的好处是能获得更加清晰、高效的逻辑单元划分。一个完整的业务逻辑是调用不同的对象、方法来组合完成的,每一个步骤都按照顺序执行。这样容易导致业务逻辑之间的耦合关系过于紧密,核心业务的代码之间通常需要手动嵌入大量非核心业务的代码,比如日志记录、事务管理。对于这种跨对象和跨业务的重复的、公共的非核心的程序逻辑,OOP没有特别好的处理方式。
AOP的基本模块单元是切面(Aspect),所谓切面,其实就是对不同业务流水线中的相同业务逻辑进行进一步抽取形成的一个横截面。AOP计数让业务中的核心模块和非核心模块的耦合度进一步降低,实现了代码的复用,减少了代码量,提升了开发效率,并有利于代码未来的可扩展性和可维护性。
简单地说,OOP对业务中每一个功能进行抽取,封装成类和方法,让代码更加模块化,在一定程度上实现了代码的复用。此时,一个完整的业务通过按一定顺序调用对象的方法模块来实现。如果脱离对象层面,基于业务逻辑,站在更高层面来看这种编程方式,带来的缺点是对于业务中的重复代码模块,在源代码中需要在业务的不同阶段重复调用。而AOP则可以对业务中重复调用的模块进行抽取,让业务中的核心逻辑与非核心逻辑进一步解耦,在源代码中不需要手动调用这个重复代码的模块,在更高的层级实现了代码的复用,有利于后续代码的维护和升级。
前面讲了这么多,其实AOP就是在不修改代码的情况下为程序统一添加额外功能的一种技术。AOP可以拦截指定的方法并且对方法增强,而无须侵入业务代码中,让业务与非业务处理逻辑分离。比如Spring的事务,通过事务的注解配置,Spring会自动在业务方法中开启、提交业务,并且在业务处理失败时执行相应的回滚策略。
下面梳理AOP的一些核心概念,方便理解AOP。
1.Joinpoint
Joinpoint(连接点)指的是那些被连接的点,在Spring中指的是可以被拦截的目标类的方法,在Spring AOP中,连接点只支持method execution,即方法执行连接点,并且不能应用于在同一个类中相互调用的方法。
2.Pointcut
Pointcut(切入点)用来匹配要进行切入的Joinpoint集合的表达式,通过切入点表达式(Pointcut Expression,类似于正则表达式)可以确定符合条件的连接点作为切入点。
3.Advice
Advice(通知)是指拦截到连接点之后要做的事,是对切入点增强的内容,也就是切面的具体行为和功能,在Pointcut匹配到的Joinpoint位置,会插入指定类型的Advice。
Advice是基于拦截器进行拦截实现的,表示在Pointcut上要执行的方法,其类型是定义Pointcut执行的时机。
4.Aspect
Aspect(切面)是切入点(Pointcut)和该位置的通知(Advice)的结合,或者说是前面所讲的跨多个业务的被抽离出来的公共业务模块,就像一个横截面一样,对应Java代码中被@AspectJ标注的切面类或者使用XML配置的切面。
5.Target
所有被通知的对象(也可以理解为被代理的对象)都是Target(目标对象)。目标对象被AOP所关注,它的属性的改变会被关注,它的行为的调用也会被关注,它的方法传参的变化仍然会被关注。AOP会注意目标对象的变动,随时准备向目标对象“注入切面”。
6.Weaving
Weaving(编织)是将切面功能应用到目标对象的过程。由代理工厂创建一个代理对象,这个代理对象可以为目标对象执行切面功能。
AOP的织入方式有3种:编译时期(Compile Time)织入、类加载时期(Classload Time)织入、运行时期(Runtime)织入。Spring AOP一般多见于运行时期(Runtime)织入。
7.Introduction
Introduction(引入)就是对于一个已编译完的类(Class),在运行时期,动态地向这个类加载属性和方法。
利用Spring AOP使日志输出与方法分离,使得在调用目标方法之前执行日志输出。传统的做法是把输出语句写在方法体的内部,在调用该方法的时候,用输出语句输出信息来记录方法的执行。AOP可以分离与业务无关的代码。日志输出与方法都做些什么是无关的,它主要的目的是记录方法被执行过。
Spring切入点即Pointcut,用于配置切面的切入位置。Spring中切入点的粒度是方法级的,因此在Spring AOP中切入点的作用是配置哪些类中哪些方法在定义的切入点内,哪些方法应该被过滤排除。
Spring的切入点分为静态切入点、动态切入点和用户自定义切入点3种,其中静态切入点只需要考虑类名、方法名,动态切入点除此之外,还要考虑方法的参数,以便在运行时可以动态地确定切入点的位置。
1.静态切入点
静态往往意味着不变,相对于动态切入点来说,静态切入点具有良好的性能,因为静态切入点只在代理创建时执行一次,而不是在运行期间,每次目标方法执行前都要执行。
优点:由于静态切入点只在代理创建的时候执行一次,然后将结果缓存起来,下一次被调用的时候直接从缓存中获取即可。因此,在性能上静态切入点要远高于动态切入点。静态切入点在第一次织入切面时,首先会计算切入点的位置:它通过反射在程序运行的时候获得调用的方法名,如果这个方法名是定义的切入点,就会织入切面。然后,将第一次计算的结果缓存起来,以后就不需要再进行计算了。这样使用静态切入点的程序性能会好很多。
缺点:虽然使用静态切入点的性能会高一些,但是它也具有一些不足:当需要通知的目标对象的类型多于一种,且需要织入的方法很多时,使用静态切入点编程会很烦琐,而且不是很灵活,性能降低。这时可以选用动态切入点。
2.动态切入点
动态切入点是相对于静态切入点的。静态切入点只能应用在相对不变的位置,而动态切入点应用在相对变化的位置。例如在方法的参数上,由于在程序运行过程中传递的参数是变化的,因此切入点也随之变化,它会根据不同的参数来织入不同的切面。由于每次织入都要重新计算切入点的位置,而且结果不能缓存,因此动态切入点比静态切入点的性能低很多,但是它能够随着程序中参数的变化而织入不同的切面,因而它要比静态切入点灵活很多。
在程序中,静态切入点和动态切入点可以选择使用,当程序对性能要求很高且相对注入不是很复杂时,可以使用静态切入点,当程序对性能要求不是很高且注入比较复杂时,可以使用动态切入点。
静态切入点是在某个方法名上织入切面的,所以在织入程序代码前要进行方法名的匹配,判断当前正在调用的方法是不是已经定义的静态切入点,如果该方法已经被定义为静态切入点,则说明该方法匹配成功,织入切面。如果该方法没有被定义为静态切入点,则匹配失败,不织入切面。这个匹配过程是Spring自动进行的,不需要人为编程的干预。
静态切入点只限于给定的方法和目标类,而不考虑方法的参数。Spring在调用静态切入点时只在第一次调用的时候计算静态切入点的位置,然后缓存起来。通过org.springframework.aop.support.RegexpMethodPointcut可以实现静态切入点,这是一个通用的正则表达式切入点。
掌握Spring切入点底层将有助于更加深刻地理解切入点。Pointcut接口是切入点的定义接口,用它来规定可切入的连接点的属性。通过对此接口的扩展可以处理其他类型的连接点,例如域等。
Spring支持两种切入点:静态切入点和动态切入点。究竟执行静态切入点还是动态切入点,取决于isRuntime()方法的返回值。在匹配切入点之前,Spring会调用isRuntime(),如果返回false,则执行静态切入点,如果返回true,则执行动态切入点。
Spring提供了丰富的切入点,目的是使切面灵活地注入程序中的位置。例如使用流程切入点,可以根据当前调用的堆栈中的类和方法来实施切入。
Aspect是对系统中的对象操作过程中截面逻辑进行模块化封装的AOP概念实体。在通常情况下,Aspect可以包含多个切入点和通知。
AspectJ是Spring框架2.0版本之后增加的新特性,Spring使用了AspectJ提供的一个库来做切入点解析和匹配的工作。但是AOP在运行时仍旧是纯粹的Spring AOP,它并不依赖于AspectJ的编译器或者织入器,在底层中使用的仍然是Spring 2.0之前的实现体系。
在使用AspectJ框架之前,需要导入JAR包:aspectjrt.jar和aspectjweaver.jar。Spring使用AspectJ主要有两种方法:基于XML和基于注解。
使用aop:config标签表明开始AOP的配置。
使用aop:aspect标签表明配置切面:
基于注解,完全不需要XML配置,只需要定义一个配置项即可,代码如下:
@Configuration
@ComponentScan(basePackages = "com.vincent.javaweb")
// Java配置开启AOP注解支持
@EnableAspectJAutoProxy
public class SpringAOPConfiguration {
}
DAO(Data Access Object,数据访问对象)的存在提供了读写数据库中数据的一种方法,这个功能通过接口提供对外服务,程序的其他模块通过这些接口来访问数据库。
使用DAO模式有以下好处:
· 服务对象不再和特定的接口实现绑定在一起,使得它易于测试,因为它提供的是一种服务,在不需要连接数据库的条件下就可以进行单元测试,极大地提高了开发效率。
· 通过使用不依赖持久化技术的方法访问数据库,在应用程序的设计和使用上都有很大的灵活性,对于整个系统无论是在性能上还是应用上都是一个巨大的飞跃。
· DAO的主要目的是将持久性相关的问题与业务规则和工作流隔离开来,它为定义业务层可以访问的持久性操作引入了一个接口,并且隐藏了实现的具体细节,该接口的功能将依赖于采用的持久性技术而改变,但是DAO接口可以基本上保持不变。
· DAO属于O/R Mapping技术的一种。在O/R Mapping技术发布之前,开发者需要直接借助JDBC和SQL来完成与数据库的相互通信,在O/R Mapping技术出现之后,开发者能够使用DAO或其他不同的DAO框架来实现与RDBMS(关系数据库管理系统)的交互。借助O/R Mapping技术,开发者能够将对象属性映射到数据表的字段,将对象映射到RDBMS中,这些Mapping技术能够为应用自动创建高效的SQL语句等,除此之外,O/R Mapping技术还提供了延迟加载、缓存等高级特征,而DAO是O/R Mapping技术的一种实现,因此使用DAO能够大量节省程序开发时间,减少代码量和开发的成本。
Spring提供了一套抽象的DAO类供开发者扩展,这有利于以统一的方式操作各种DAO技术,例如JDO、JDBC等,这些抽象DAO类提供了设置数据源及相关辅助信息的方法,而其中的一些方法与具体DAO技术相关。
目前,Spring DAO提供了以下几种类:
· JdbcDaoSupport:JDBC DAO抽象类,开发者需要为它设置数据源(DataSource),通过其子类,开发者能够获得JdbcTemplate来访问数据库。
· HibernateDaoSupport:Hibernate DAO抽象类。开发者需要为它配置Hibernate SessionFactory。通过其子类,开发者能够获得Hibernate实现。
· JdoDaoSupport:Spring为JDO提供的DAO抽象类,开发者需要为它配置PersistenceManagerFactory,通过其子类,开发者能够获得JdoTemplate。
在使用Spring的DAO框架进行数据库存取的时候,无须使用特定的数据库技术,通过一个数据存取接口来操作即可。
Spring中的事务是基于AOP实现的,而Spring的AOP是方法级别的,所以Spring的事务属性就是对事务应用到方法上的策略描述。这些属性分为传播行为、隔离级别、只读和超时属性。
事务管理在应用程序中起着至关重要的作用,它是一系列任务组成的工作单元,在这个工作单元中,所有的任务必须同时执行,它们只有两种可能的执行结果,要么所有任务全部成功执行,要么所有任务全部执行失败。
事务管理通常分为两种方式,即编程式事务管理和声明式事务管理。在Spring中,这两种事务管理方式被实现得更加优秀。
1.编程式事务管理
在Spring中主要有两种编程式事务的实现方法,即使用PlatformTransactionManager接口的事务管理器实现和使用TransactionTemplate实现。虽然二者各有优缺点,但是推荐使用TransactionTemplate实现方式,因为它符合Spring的模板模式。
TransactionTemplate模板和Spring的其他模板一样,它封装了资源的打开和关闭等常用的重复代码,在编写程序时只需完成需要的业务代码即可。
2.声明式事务管理
Spring的声明式事务不涉及组件依赖关系,它通过AOP实现事务管理,Spring本身就是一个容器,相对而言更为轻便小巧。在使用Spring的声明式事务时不需要编写任何代码,便可实现基于容器的事务管理。Spring提供了一些可供选择的辅助类,这些辅助类简化了传统的数据库操作流程,在一定程度上节省了工作量,提高了编码效率,所以推荐使用声明式事务。
在Spring中常用TransactionProxyFactoryBean完成声明式事务管理。
使用TransactionProxyFactoryBean需要注入它所依赖的事务管理器,设置代理的目标对象、代理对象的生成方式和事务属性。代理对象是在目标对象上生成的包含事务和AOP切面的新的对象,它可以赋予目标的引用来替代目标对象以支持事务或AOP提供的切面功能。
JdbcTemplate类是Spring的核心类之一,可以在org.springframework.jdbc.core包中找到它。JdbcTemplate类在内部已经处理完了数据库资源的建立和释放,并且可以避免一些常见的错误,例如关闭连接、抛出异常等。因此,使用JdbcTemplate类简化了编写JDBC时所使用的基础代码。
JdbcTemplate类可以直接通过数据源的引用实例化,然后在服务中使用,也可以通过依赖注入的方式在ApplicationContext中产生并作为JavaBean的引用供服务使用。
JdbcTemplate类运行了核心的JDBC工作流程,例如应用程序要创建和执行Statement对象,只需在代码中提供SQL语句。这个类还可以执行SQL中的查询、更新或者调用存储过程等操作,同时生成结果集的迭代数据。同时,这个类还可以捕捉JDBC的异常并将它们转换成org.springframework.dao包中定义的通用的能够提供更多信息的异常体系。
JdbcTemplate类中提供了接口来方便地访问和处理数据库中的数据,这些方法提供了基本的选项用于执行查询和更新数据库操作。在数据查询和更新的方法中,JdbcTemplate类提供了很多重载的方法,提高了程序的灵活性。
JdbcTemplate提供了很多常用的数据查询方法,比较常见的如下:
· query。
· queryForObject。
· queryForList。
· queryForMap。
框架通常指的是为了实现某个业界标准或完成特定基本任务的软件组件规范,也指为了实现某个软件组件规范时,提供规范所要求的基础功能的软件产品。比如第12章学习的Spring框架。
使用框架开发极大地提升了开发效率,它有以下优势:
· 省去大量的代码编写,减少开发时间,降低开发难度。· 限制程序员必须使用框架规范开发,可以增强代码的规范性,降低程序员之间的沟通及日后维护的成本。
· 将程序员的注意力从技术中抽离出来,更集中于业务层面。
· 可以直观地把框架比作汽车的零部件,汽车厂商只需要根据模型组合各个零部件就能造出不同性能的车子。
ORM(Object Relational Mapping,对象关系映射)是一种为了解决面向对象与关系数据库存在的互不匹配现象的技术。ORM框架是连接数据库的桥梁,只要提供了持久化类与表的映射关系,ORM框架在运行时就能参照映射文件的信息把对象持久化到数据库中。在具体操作业务对象的时候,不需要再去和复杂的SQL语句打交道,只需简单地操作对象的属性和方法即可。
开发应用程序时可能会编写特别多数据访问层的代码,从数据库保存、删除、读取对象信息,而这些代码都是重复的。使用ORM则会大大减少重复性代码。对象关系映射主要实现程序对象到关系数据库数据的映射。
ORM框架在开发中的作用如图13.1所示,它实现了数据模型(Java代码)与数据库底层的直接接触,封装了数据对象与库的映射,使数据模型不需要关心SQL的实现。
JDBC操作数据库的程序,数据库数据与对象数据的转换代码烦琐、无技术含量。使用ORM框架代替JDBC后,框架可以帮助程序员自动进行转换,只要像平时一样操作对象,ORM框架就会根据映射完成对数据库的操作,极大地增强了开发效率。
MyBatis是一款优秀的基于Java的持久层框架,它内部封装了JDBC,使开发者只需要关注SQL语句本身,而不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。
通过XML或注解的方式将要执行的各种statement配置起来,并通过Java对象和statement中SQL的动态参数进行映射,以生成最终执行的SQL语句,最后由MyBatis框架执行SQL语句并将结果映射为Java对象并返回。
MyBatis采用ORM思想解决了实体和数据库映射的问题,对JDBC进行了封装,屏蔽了JDBC API底层访问细节,使开发时不用与JDBC API打交道,就可以完成对数据库的持久化操作。
作为一款优秀的持久层框架,其优势十分明显:
· 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML中,解除SQL与程序代码的耦合,便于统一管理。
· 提供XML标签,支持编写动态SQL语句,并且可以复用。
· 与JDBC相比,消除了JDBC大量冗余的代码,不需要手动开关连接。
· 数据库兼容性高(因为MyBatis使用JDBC来连接数据库,所以只要是JDBC支持的数据库,MyBatis都支持)。
· 能够与Spring很好地集成。
· 提供映射标签,支持对象与数据库的ORM字段关系映射。
· 提供对象关系映射标签,支持对象关系组件维护。任何框架都不可能尽善尽美,MyBatis也有不足处:
· SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
· SQL语句依赖于数据库,导致数据库的移植性差,不能随意更换数据库。
MyBatis官网提供了MyBatis的使用说明,其包下载托管在GitHub上。
页面有下载链接,有3个资源:第一个是MyBatis框架压缩包,第二个和第三个分别为Windows和Linux系统下的源代码。下载mybatis-3.5.10.zip包,解压缩,目录结构如图13.3所示。
目录说明如下:
· lib:MyBatis依赖包。
· mybatis-3.4.2.jar:MyBatis核心包。
· mybatis-3.4.2.pdf:MyBatis使用手册。
在应用程序中导入MyBatis的核心包以及依赖包即可。
MyBatis应用程序通过SqlSessionFactoryBuilder从mybatis-config.xml配置文件中构建出SqlSessionFactory,然后SqlSessionFactory的实例直接开启一个SqlSession,再通过SqlSession实例获得Mapper对象并运行Mapper映射的SQL语句,完成对数据库的CRUD和事务提交,之后关闭SqlSession。
从图13.4可以看出,MyBatis框架在操作数据库时大致经过了8个步骤。对这8个步骤分析如下:
(1)读取MyBatis的配置文件。mybatis-config.xml为MyBatis的全局配置文件,用于配置数据库连接信息。
(2)加载映射文件。映射文件即SQL映射文件,该文件中配置了操作数据库的SQL语句,需要在MyBatis配置文件mybatis-config.xml中加载。mybatis-config.xml文件可以加载多个映射文件,每个文件对应数据库中的一张表。
(3)构建会话工厂。通过MyBatis的环境配置信息构建会话工厂SqlSessionFactory。
(4)创建会话对象。由会话工厂创建SqlSession对象,该对象中包含执行SQL语句的所有方法。
(5)Executor执行器。MyBatis底层定义了一个Executor接口来操作数据库,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。
(6)MappedStatement对象。在Executor接口的执行方法中有一个MappedStatement类型的参数,该参数是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。
(7)输入参数映射。输入参数类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输入参数映射过程类似于JDBC对preparedStatement对象设置参数的过程。
(8)输出结果映射。输出结果类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输出结果映射过程类似于JDBC对结果集的解析过程。MappedStatement对象在执行SQL语句后,将输出结果映射到Java对象中。这种将输出结果映射到Java对象的过程类似于JDBC编程中对结果的解析处理过程。
每个MyBatis的应用程序都以一个SqlSessionFactory对象的实例为核心。首先获取SqlSessionFactoryBuilder对象,可以根据XML配置文件或者Configuration类的实例构建该对象。然后获取SqlSessionFactory对象,该对象实例可以通过SqlSessionFactoryBuilder对象来获取。有了SqlSessionFactory对象之后,就可以获取SqlSession实例,SqlSession对象中完全包含以数据库为背景的所有执行SQL操作的方法,可以用该实例直接执行已映射的SQL语句。
SqlSessionFactoryBuilder负责构建SqlSessionFactory,并且提供了多个build()方法用于重载。通过build()参数分析,发现配置信息能够以3种形式提供给SqlSessionFactoryBuilder的build()方法,分别是InputStream(字节流)、Reader(字符流)和Configuration(类),由于字节流与字符流都属于读取配置文件的方式,因此从配置信息的来源就很容易想到构建一个SqlSessionFactory有两种方式:读取XML配置文件构建和编程构建。一般习惯采取XML配置文件的方式来构建SqlSessionFactory。
SqlSessionFactoryBuilder的最大特点是阅后即焚。一旦创建了SqlSessionFactory对象,这个类就不再需要存在了,因此SqlSessionFactoryBuilder的最佳作用域就是存在于方法体内,也就是局部变量。
SqlSessionFactory是MyBatis框架中十分重要的对象,它是单个数据库映射关系经过编译后的内存镜像,主要用于创建SqlSession。SqlSessionFactory的实例可以通过SqlSessionFactoryBuilder对象来创建,而SqlSessionFactoryBuilder则可以通过XML配置文件或预先定义好的Configuration实例来创建SqlSessionFactory的实例。其代码如下:
SqlSessionFactory对象是安全的,它一旦被创建,在整个应用执行期间都会存在。如果多次创建同一个数据库的SqlSessionFactory,那么此数据库的资源将容易被耗尽。为了解决此问题,通常每个数据库都会只对应一个SqlSessionFactory,所以在创建SqlSessionFactory实例时,建议使用单列模式。
SqlSession是应用程序与持久层之间执行交互操作的一个单线程对象,其主要作用是执行持久化操作。SqlSession对象包含数据库中所有执行SQL操作的方法,由于其底层封装了JDBC连接,所以直接使用其实例来执行已映射的SQL语句。
每一个线程都应该有一个自己的SqlSession实例,并且该实例是不能被共享的。同时,SqlSession实例也是线程不安全的,因此它的使用范围最好是一次请求或一个方法中,绝对不能将它在一个类的静态字段、实例字段或任何类型的管理范围中使用,使用SqlSession对象之后,要及时关闭它,通常可以将它放在finally块中关闭
这里的配置文件指的是SqlMapConfig.xml文件,MyBatis配置XML文件的层次结构是不能够颠倒顺序的。下面是MyBatis的全部配置:
初看比较复杂,其实理解后还是蛮简单的,而且很多东西在初级的使用中使用默认配置就行了。
是一个配置属性的元素,该元素通常用于将内部的配置外在化,即通过引用外部的配置文件来动态地替换内部定义的属性。例如,数据库的连接等属性,这样更方便程序的运行和部署。
在MyBatis配置文件SqlMapConfig.xml中配置属性:
<properties resource="db.properties"></properties>
这里讲一下优先级的问题:通过参数传递的属性具有最高优先级,然后是resource/url属性中指定的配置文件,最后是properties属性中指定的属性。
元素主要用于改变MyBatis运行时的行为,例如开启二级缓存、开启延迟加载等,虽然不配置< settings>元素,也可以正常运行MyBatis。
元素用于为配置文件中的Java类型设置一个别名。别名的设置与XML配置相关,其使用的意义在于减少全限定类名的冗余。别名的使用忽略字母大小写。
还可以在程序中使用注解,别名为其注解的值:
@Alias("userinfo")
public class UserInfo {
}
@Alias注解将会覆盖配置文件中的定义。
@Alias要和标签配合使用,MyBatis会自动查看指定包内的类别名注解,如果没有这个注解,那么默认的别名就是类的名字,不区分字母大小写。
当MyBatis将一个Java对象作为输入参数执行insert语句时,它会创建一个PreparedStatement对象,并且调用set方法对“?”占位符设置相应的参数值,该参数值的类型可以是Int、String、Date等Java对象属性类型中的任意一个。
当MyBatis框架提供的这些类型处理器不能够满足需求时,还可以通过自定义的方式对类型处理器进行扩展(自定义类型处理器可以通过实现TypeHandler接口或者继承BaseTypeHandler类来定义)。
在MyBatis中,其SQL映射配置文件中的SQL语句所得到的查询结果被动态映射到resultType或其他处理结果集的参数配置对应的Java类型时,其中就有JavaBean等封装类。而objectFactory对象工厂就是用来创建实体对象的类的。
默认的objectFactory要做的就是实例化查询结果对应的目标类,有两种方式可以将查询结果的值映射到对应的目标类:一种是通过目标类的默认构造方法;另一种是通过目标类的有参构造方法。
如果要再新建(new)一个对象(构造方法或者有参构造方法),在得到对象之前需要执行一些处理的程序逻辑,或者在执行该类的有参构造方法时,在传入参数之前,要对参数进行一些处理,这时就可以创建自己的objectFactory来加载该类型的对象。
自定义的对象工厂需要实现ObjectFactory接口,或者继承DefaultObjectFactory类:
public class MyObjectFactory extends DefaultObjectFactory {
}
objectFactory自定义对象类被定义在工程中,在全局配置文件SqlMapConfig.xml中配置。当Resource资源类加载SqlMapConfig.xml文件,并创建出SqlSessionFactory时,会加载配置文件中自定义的objectFactory,并设置配置标签中的property参数。
元素的作用是配置用户所开发的插件。MyBatis对某种方法进行拦截调用的机制被称为Plugin插件。使用plugin方法还能修改或重写方法逻辑。MyBatis中允许使用plugin拦截的方法如下:
在配置文件中,元素用于对环境进行配置。MyBatis的环境配置实际上就是数据源的配置,我们可以通过元素配置多种数据源,即配置多种数据库:
元素是环境配置的根元素,它包含一个default属性,该属性用于指定默认的环境ID。是元素的子元素,它可以定义多个,其id属性用于表示所定义环境的ID值,当有多个environment数据库环境时,可以根据environments的default属性值来指定哪个数据库环境起作用(id属性匹配的那个environment数据库环境起作用),也可以根据后续的代码改变数据库环境。在元素内,包含事务管理和数据源的配置信息,其中元素用于配置事务管理,它的type属性用于指定事务管理的方式,即使用哪种事务管理器;元素用于配置数据源,它的type属性用于指定使用哪种数据源,该元素至少要配置4要素:driver、url、username和password。
MyBatis可以配置两种类型的事务管理器,分别是JDBC和MANAGED。
· JDBC:此配置直接使用了JDBC的提交和回滚设置,它依赖于从数据源得到的连接来管理事务的作用域。
· MANAGED:此配置从来不提交或回滚一个连接,而是让容器来管理事务的整个生命周期。在默认情况下,它会关闭连接,但一些容器并不希望这样,可以将closeConnection属性设置为false来阻止它默认的关闭行为。
在配置文件中,元素用于指定MyBatis映射文件的位置,一般可以使用以下4种方式来引入映射文件:
(1)使用类路径引入:
(2)使用本地文件引入:
(3)使用接口类引入:
(4)使用包名引入:
映射文件是MyBatis框架中十分重要的文件,可以说,MyBatis框架的强大之处就体现在映射文件的编写上。映射文件的命名一般是实体类名+Mapper.xml。这个XML文件中包括类所对应的数据库表的各种增、删、改、查SQL语句。在映射文件中,元素是映射文件的根元素,其他元素都是它的子元素,
元素有一个属性是namespace,它对应着实体类的mapper接口,此接口就是MyBatis的映射接口,它对mapper.xml文件中的SQL语句进行映射。
元素用于映射查询语句,它从数据库中读取数据,并将读取的数据进行封装。例如:
id属性的值要和实体类的Mapper映射接口中的方法名保持一致。resultMap属性为返回的结果集,如果该类没有配置别名,就需要使用全限定名。resultType属性为查询到的结果集中每一行所代表的数据类型。
当没有配置元素且列名和实体类中的set方法去掉set后剩余的部分首字母小写不一致时,就会出现映射不匹配的问题,此时可以通过给列名起别名的方法解决,别名必须和实体类中的set方法去掉set后剩余的部分首字母小写得到的名字一致。其弊端就是只在当前select语句中有效,即只在当前映射方法中有效。配置元素可以有效解决这个问题。
元素常见的属性如表13.3所示。
元素用于映射插入语句,MyBatis执行完一条插入语句后将返回一个整数表示其影响的行数。元素的属性与元素的属性大部分相同,但有几个特有属性,如表13.4所示。
1.自增主键
MyBatis调用JDBC的getGeneratedKeys()将使得如MySQL、PostgreSQL、SQL Server等数据库的表,采用自动递增的字段作为主键,有时可能需要将这个刚刚产生的主键用于关联其他业务。
实现方式一(推荐):
实现方式二:
2.自定义主键
在实际开发中,如果使用的数据库不支持主键自动递增(例如Oracle数据库),或者取消了主键自动递增的规则,则可以使用MyBatis的元素来自定义生成主键。
元素和元素比较简单,它们的属性和元素、元素的属性差不多,执行后也返回一个整数,表示影响了数据库的记录行数。
元素用于映射更新语句,元素用于映射删除语句。
元素标签用来定义可重复使用的SQL代码片段,使用时只需要用include元素标签引用即可,最终达到SQL语句复用的目的。同时它可以被静态地(在加载参数时)参数化,不同的属性值通过包含的实例进行相应的变化。
元素用来封装SQL语句或者SQL片段代码,而元素用来调用封装的代码片段。
元素表示结果映射集,是一个非常重要的元素。它的主要作用是定义映射规则、级联的更新以及定义类型转化器等。它可以引导MyBatis将结果映射为Java对象。示例代码如下:
使用resultMap方式:
元素的type属性表示需要映射的实体对象,id属性是这个resultMap的唯一标识,因为在一个XML文件中resultMap可能有多个。它有一个子元素
配置property和type的原因:通过反射创建类对象。attribute一般是类的成员变量名。
此外,还有和用于处理多表时的关联关系,而元素主要用于处理一个单独的数据库查询返回很多不同数据类型结果集的情况。
MyBatis的强大特性之一便是它的动态SQL,在MyBatis的映射文件中,前面讲的SQL都是比较简单的,当业务逻辑复杂时,SQL常常是动态变化的,前面学习的SQL就不能满足要求了。
使用之前学习的JDBC就能体会到根据不同条件拼接SQL语句有多么痛苦。拼接的时候要确保不能忘了必要的空格,还要注意省掉列名列表最后的逗号。利用动态SQL这一特性可以彻底摆脱这种痛苦。
MyBatis动态SQL常用的标签如表13.5所示。
这些标签在开发过程中无处不在,能够解决用户在数据查询方面的绝大部分问题,正是有了它们,在处理复杂业务逻辑和复杂SQL的时候才更方便高效。
元素在MyBatis开发工作中主要用于where(查询)、insert(插入)和update(更新)3种操作中。通过判断参数值是否为空来决定是否使用某个条件,需要注意的是,此处where 1=1条件不可省略。
有些时候,业务需求并不需要用到所有的条件语句,而只想择其一二。针对这种情况,MyBatis提供了choose元素,它有点像Java中的switch语句。
如果传入了id,那么按照id来查找,如果传入了name,那么按照name来查找,如果两者都传入,那么只按照id来查找,即只有一个条件会生效,且是按照语句顺序执行的。
前面讲解的几种元素都需要在前面添加一个默认的where条件,为了避免这种情况,因此有了元素,可以结合多种元素一起使用。
用于动态更新语句的类似解决方案叫作。元素可以用于动态包含需要更新的列,忽略其他不更新的列。
元素的功能非常强大,它允许用户指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许用户指定开头与结尾的字符串以及集合项迭代之间的分隔符。
可以将任何可迭代对象(如List、Set等)、Map对象或者数组对象作为集合参数传递给foreach。当使用可迭代对象或者数组时,index是当前迭代的序号,item的值是本次迭代获取到的元素。当使用Map对象(或者Map.Entry对象的集合)时,index是键,item是值。
先来看一下标签的参数:
· item:表示集合中每一个元素进行迭代的别名。
· index:指定一个名字,用于表示在迭代过程中,每次迭代到的位置。
· open:表示该语句以什么开始。
· close:表示该语句以什么结束。
· separator:表示每次迭代之间以什么符号作为分隔符。
元素用于处理参数,为参数添加一些修饰。在进行模糊查询时,如果使用“${}”拼接字符串,则无法防止SQL注入问题。如果使用字符串拼接函数或连接符号,不同数据库的字符串拼接函数或连接符号不同,则会导致数据库适配困难。元素使用可以解决此类适配上的问题。
MyBatis动态SQL是开发中经常用到的,它不仅方便了开发,提高了效率,还实现了逻辑和底层数据查询的分离。
在实现复杂关系映射之前,可以在映射文件中通过配置来实现,使用注解开发后,可以使用@Results注解、@Result注解、@One注解、@Many注解组合完成复杂关系的配置。下面来看关系映射注解的使用方法,如表13.6所示。
一对一查询的示例:查询一个订单,与此同时查询出该订单所属的用户。
1.建表导入数据
2.创建实体对象
3.使用注解创建接口
4.Java测试代码
一对多查询的示例:查询一个用户,与此同时查询出该用户具有的订单。在RelaOrderMapper类中添加根据用户ID获取其订单信息的方法,代码如下:
@Select("select * from t_rela_order where cid = #{cid}")
public List<RelaOrder> queryOrderByCustomerId(int cid);
多对多查询的示例:查询用户,同时查询出该用户的所有角色。
1.建表
2.创建实体和Mapper
3.Java测试方法
前面已经分别讲解了Spring和MyBatis,本节将学习MyBatis与Spring的整合,其核心是SqlSessionFactory对象交由Spring来管理。所以只需要将SqlSessionFactory的对象生成器SqlSessionFactoryBean注册在Spring容器中,再将其注入给Dao的实现类即可完成整合。
本次整合采用注解方式配置,读者在学习过程中可以一边学习理解注解的配置方式,一边对照前面XML配置的方式来学习。
创建名为ch13_ms的Module,添加Web框架,并创建相应的目录,主要目录如下:
· resources:存放配置(数据库配置文件)。
· com.vincent.javaweb.config:注解配置类。
· com.vincent.javaweb.entity:存放数据库实体类。
· com.vincent.javaweb.mapper:存放MyBatis映射类。
· com.vincent.javaweb.service:存放数据库逻辑处理类。
· web/WEB-INF/classes:编译的Class文件目录。
· web/WEB-INF/libs:导入的JAR包。
具体目录结构如图13.15所示。
在web/libs中引入MyBatis和Spring的整合需要使用到的JAR包如图13.16所示。
Spring MVC是Spring提供的一个基于MVC设计模式的轻量级Web开发框架,本质上相当于Servlet。Spring MVC角色划分清晰,分工明细。Spring MVC本身就是Spring框架的一部分,可以说和Spring框架是无缝集成的,性能方面具有先天的优势,是当今业界主流、热门的Web开发框架。
一个好的框架要减轻开发者处理复杂问题的负担,内部有良好的扩展,并且有一个支持它的强大的用户群体,恰恰Spring MVC都做到了。
1.三层架构
在第1章介绍过,开发架构一般都是基于两种形式:一种是C/S架构,也就是客户端/服务器;另一种是B/S架构,也就是浏览器/服务器。在Java Web开发中,几乎都是基于B/S架构开发的。在B/S架构中,系统标准的三层架构包括:表现层、业务层和持久层。
· 表现层:也就是Web层。负责接收客户端请求,向客户端响应结果。表现层的设计一般都使用MVC模型。
· 业务层:也就是Service层。负责业务逻辑处理,和开发项目的需求息息相关。Web层依赖业务层,但是业务层不依赖Web层。
· 持久层:也就是DAO层。是数据库的主要操控系统,实现数据的增加、删除、修改、查询等操作,并将操作结果反馈到业务逻辑层。
2.MVC模型
MVC(Model View Controller,模型-视图-控制器)是一种用于设计创建Web应用程序表现层的模式。
· Model:通常指的是数据模型。一般用于封装数据。
· View:通常指的是JSP或者HTML。一般用于展示数据。通常视图是依据模型数据创建的。
· Controller:是应用程序中处理用户交互的部分。一般用于处理程序逻辑。
Spring MVC是一个基于Java语言的、实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model、View、Controller分离,将Web层进行职责解耦,把复杂的Web应用分成逻辑清晰的几部分,以简化开发,减少出错,方便组内开发人员之间的配合。
Spring MVC的执行流程如图14.1所示。
Spring MVC的执行流程具体说明如下:
用户通过浏览器发起一个HTTP请求,该请求会被DispatcherServlet(前端控制器)拦截。
DispatcherServlet调用HandlerMapping(处理映射器)找到具体的处理器(Handler),生成处理对象及处理拦截器(如果有则生成)一并返回前端控制器。
处理映射器HandlerMapping返回Handler(抽象),以HandlerExecutionChain执行链的形式返回给DispatcherServlet。
DispatcherServlet将执行链返回的Handler信息发送给HandlerAdapter(处理适配器)。
HandlerAdapter根据Handler信息找到并执行相应的Handler(Controller控制器)对请求进行处理。
Handler执行完毕后会返回给HandlerAdapter一个ModelAndView对象(Spring MVC的底层对象,包括Model数据模型和View视图信息)。
HandlerAdapter接收到ModelAndView对象后,将其返回给DispatcherServlet。
DispatcherServlet接收到ModelAndView对象后,会请求ViewResolver(视图解析器)对视图进行解析。
ViewResolver解析完成后,会将View(视图)返回给DispatcherServlet。
DispatcherServlet接收到具体的View(视图)后,进行视图渲染,将Model中的模型数据填充到View(视图)中的request域,生成最终的View(视图)。
View(视图)负责将结果显示到浏览器(客户端)。
Spring MVC有以下优势:
· 清晰的角色划分:前端控制器(DispatcherServlet)、处理映射器(HandlerMapping)、处理适配器(HandlerAdapter)、视图解析器(ViewResolver)、处理器或页面控制器(Controller)、验证器(Validator)、命令对象(Command请求参数绑定到的对象就叫命令对象)、表单对象(Form Object提供给表单展示和提交到的对象就叫表单对象)。
· 分工明确,而且扩展点相当灵活,很容易扩展,虽然几乎不需要。
· 由于命令对象就是一个POJO,因此无须继承框架特定API,可以使用命令对象直接作为业务对象。
· 和Spring其他框架无缝集成,是其他Web框架所不具备的。
· 可适配,通过HandlerAdapter可以支持任意的类作为处理器。
· 可定制性,HandlerMapping、ViewResolver等能够非常简单地定制。
· 功能强大的数据验证、格式化、绑定机制。
· 利用Spring提供的Mock对象能够非常简单地进行Web层单元测试。
· 对本地化、主题的解析的支持,更容易进行国际化和主题的切换。
· 强大的JSP标签库,使JSP编写更容易。
由于笔者使用了Tomcat 10、JDK 18.0.0.1,Spring MVC 5.x版本目前不支持Tomcat 10,这里有两个办法处理:一是降低Tomcat和JDK版本;二是升级Spring MVC版本,这里笔者选择使用Spring MVC 6.0.0-M5。
首先创建项目,添加Web框架,然后创建classes、lib目录,项目结构。
Spring MVC是基于Servlet的,DispatcherServlet是整个Spring MVC框架的核心,主要负责截获请求并将其分派给相应的处理器处理。跟所有的Servlet一样,DispatcherServlet也需要在web.xml中进行配置,它才能够正常工作。
默认情况下,所有的Servlet(包括DispatcherServlet)都是在第一次调用时才会被加载。这种机制虽然能在一定程度上降低项目启动的时间,但却增加了用户第一次访问所需的时间,给用户带来不佳的体验。因此,在web.xml中配置标签对Spring MVC前端控制器DispatcherServlet的初始化时间进行了设置,让它在项目启动时就完成了加载。
load-on-startup元素取值规则如下:
· 它的取值必须是一个整数。
· 当值小于0或者没有指定时,表示容器在该Servlet首次被请求时才会被加载。
· 当值大于0或等于0时,表示容器在启动时就加载并初始化该Servlet,取值越小,优先级越高。
· 当取值相同时,容器会自行选择顺序进行加载。
此外,通过将DispatcherServlet映射到“/”,表示DispatcherServlet需要截获并处理该项目的所有URL请求(以.jsp为后缀的请求除外)。
在resources目录下创建名为springmvc-config.xml的配置文件,内容如下:
在上面的配置中定义了一个类型为InternalResourceViewResolver的Bean,这就是视图解析器。通过它可以对视图的编码、视图前缀、视图后缀等进行配置。
DispatcherServlet会拦截用户发送来的所有请求进行统一处理,但不同的请求有着不同的处理过程,例如登录请求和注册请求就分别对应着登录过程和注册过程,因此需要Controller来对不同的请求进行不同的处理。在Spring MVC中,普通的Java类只要标注了@Controller注解,就会被Spring MVC识别成Controller。Controller类中的每一个处理请求的方法被称为“控制器方法”。控制器方法在处理完请求后,通常会返回一个字符串类型的逻辑视图名,Spring MVC需要借助ViewResolver(视图解析器)将这个逻辑视图名解析为真正的视图,最终响应给客户端展示。
根据Spring MVC配置文件中关于InternalResourceViewResolver视图解析器的配置可知,所有的视图文件都应该存放在/WEB-INF/pages目录下且文件名必须以.jsp结尾。
修改index.jsp,在body中添加代码如下:
<h3>Spring MVC 入门示例</h3>
<a href="register">注册</a> | <a href="login">登录</a>
在IDEA中,打开DispatcherServlet类,右击选择Diagrams→Show Diagram,可以查看DispatcherServlet继承的类和实现的接口,如图14.6所示。
从图14.6中的继承关系可以看出,DispatcherServlet本质是一个HttpServlet。
1.DispatcherServlet初始化
Web容器启动时将调用HttpServletBean的init()方法,然后调用FrameworkServlet的initServletBean()和initWebApplicationContext()方法,之后调用DispatcherServlet的onRefresh()、initStrategies()以及各种解析器组件的初始化方法initXXX()。
DispatcherServlet的initStrategies()方法将在WebApplicationContext初始化后自动执行,自动扫描上下文的Bean,根据名称或者类型匹配的机制查找自定义组件,如果没有找到,则会装配一套Spring的默认组件。在org.springframework.web.servlet路径下有一个DispatcherServlet.properties配置文件,该文件指定了DispatcherServlet所使用的默认组件。
DispatcherServlet启动时会对Web层Bean的配置进行检查,如HandlerMapping、HandlerAdapter等,如果没有配置,则会提供默认配置。
整个DispatcherServlet初始化的过程主要做了如下两件事情:
(1)初始化Spring Web MVC使用的Web上下文(ContextLoaderListener加载了根上下文)。
(2)初始化DispatcherServlet使用的策略,如HandlerMapping、HandlerAdapter等。
2.DispatcherServlet处理流程
在配置好DispatcherServlet之后,当请求交由该DispatcherServlet处理时,其处理流程如下:
(1)构造WebApplicationContext作为属性绑定到请求上以备控制器和其他元素使用。绑定的默认key为DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE。
(2)绑定地区解析器到请求上以备解析地区时使用,比如生成视图和准备数据时等。如果不需要解析地区,则无须使用。
(3)绑定主题解析器到请求上以备视图等元素加载主题时使用。
(4)如果配置了文件流解析器,则会检测请求中是否包含文件流,如果包含,则请求会被包装为MultipartHttpServletRequest供其他元素做进一步处理。
(5)搜索合适的处理器处理请求。若找到的话,则与该处理器相关联的执行链(前拦截器、后拦截器、控制器等)会被执行以准备模型数据或生成视图。
(6)如果返回了模型对象,下一步就会进行视图的渲染。如果没有任何模型对象返回,例如因为安全的原因被前拦截器或后拦截器拦截了请求,那么就没有视图会生成,因为该请求已经结束了。
@Controller注解可以将一个普通的Java类标识成控制器(Controller)类。Spring中提供了基于注解的Controller定义方式:@Controller和@RestController注解。基于注解的Controller定义不需要继承或者实现接口,用户可以自由地定义接口签名。以下为Spring Controller定义的示例:
@Controller
public class HelloController {
}
@Controller注解继承了Spring的@Component注解,会把对应的类声明为Spring对应的Bean,并且可以被Web组件管理。@RestController注解是@Controller和@ResponseBody的组合,@ResponseBody表示函数的返回不需要渲染为View,应该直接作为Response的内容写回客户端。
Spring MVC是通过组件扫描机制查找应用中的控制器类的,为了保证控制器能够被Spring MVC扫描到,需要在Spring MVC的配置文件中使用context:component-scan/标签,指定控制器类的基本包(请确保所有控制器类都在基本包及其子包下)。示例代码如下:
<!-- 使用扫描机制扫描控制器类,控制器类都在net.biancheng.controller包及其子包下 -->
<context:component-scan base-package="com.vincent.javaweb.controller" />
Spring MVC的前端控制器(DispatcherServlet)拦截到用户发来的请求后,会通过@RequestMapping注解提供的映射信息找到对应的控制器方法,对这个请求进行处理。
@RequestMapping既可以标注在控制器类上,也可以标注在控制器方法上。
1.修饰方法
当@RequestMapping注解被标注在方法上时,value属性值就表示访问该方法的URL地址。当用户发送过来的请求想要访问该Controller下的控制器方法时,请求路径就必须与这个value值相同。示例代码如下:
2.修饰类
当@RequestMapping注解标注在控制器类上时,value属性的取值就是这个控制器类中的所有控制器方法URL地址的父路径。访问这个Controller下的任意控制器方法都需要带上这个父路径。
在这个控制类中,用户想要访问DecorateClassController中的register()方法,请求的地址就必须带上父路径“/springmvc”,即请求地址必须为“/springmvc/register”。
Spring MVC用于处理视图的两个重要的接口是ViewResolver和View。ViewResolver的主要作用是把一个逻辑上的视图名称解析为一个真正的视图,Spring MVC中用于把View对象呈现给客户端的是View对象本身,而ViewResolver只是把逻辑视图名称解析为对象的View对象。View接口主要用于处理视图,然后返回给客户端。
1.View
View就是用来渲染页面的,它的目的是将程序返回的数据(Model数据)填入页面中,最终生成HTML、JSP、Excel表单、Word文档、PDF文档以及JSON数据等形式的文件,并展示给用户。为了简化视图的开发,Spring MVC提供了许多已经开发好的视图,这些视图都是View接口的实现类。
在Spring MVC中,视图可以划分为逻辑视图和非逻辑视图。逻辑视图最大的特点是,其控制器方法返回的ModelAndView中的view可以不是一个真正的视图对象,而是一个字符串类型的逻辑视图名。对于逻辑视图而言,它需要一个视图解析器(ViewResolver)进行解析,才能得到真正的物理视图对象。非逻辑视图与逻辑视图完全相反,其控制方法返回的是一个真正的视图对象,而不是逻辑视图名,因此这种视图是不需要视图解析器解析的,只需要直接将视图模型渲染出来即可,例如MappingJackson2JsonView就是这样的情况。
2.ViewResolver
Spring MVC提供了一个视图解析器的接口ViewResolver,所有具体的视图解析器必须实现该接口。
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/e5970e5ec245471c9d48cf547da95a3d.png#pic_center= 500x)
Spring MVC提供了很多ViewResolver接口的实现类,它们中的每一个都对应Java Web应用中的某些特定视图技术。在使用某个特定的视图解析器时,需要将它以Bean组件的形式注入Spring MVC容器中,否则Spring MVC会使用默认的InternalResourceViewResolver进行解析。
在数据绑定过程中,Spring MVC框架会通过数据绑定组件(DataBinder)将请求参数串的内容进行类型转换,然后将转换后的值赋给控制器类中方法的形参。这样后台方式就可以正确绑定并获取客户端请求携带的参数。
具体信息处理过程如下:
(1)Spring MVC将ServletRequest对象传递给DataBinder。
(2)将处理方法的入参对象传递给DataBinder。
(3)DataBinder调用ConversionService组件进行数据类型转换、数据格式化等工作,并将ServletRequest对象中的消息填充到参数对象中。
(4)调用Validator组件对已经绑定了请求消息数据的参数对象进行数据合法性校验。
(5)校验完成后会生成数据绑定结果BindingResult对象,Spring MVC会将BindingResult对象中的内容赋给处理方法的相应参数。
当前端请求的参数比较简单时,可以直接使用Spring MVC提供的默认参数类型进行数据绑定。常用的默认参数类型如下:
· HttpServletRequest:通过request对象获取请求消息。
· HttpServletResponse:通过response对象处理响应信息。
· HttpSession:通过session对象得到已保存的对象。
· Model/ModelMap:Model是一个接口,ModelMap是一个接口实现,作用是将Model数据填充到request域。
绑定默认数据类型的示例代码如下:
引用的JSP页面的代码如下:
<li><a href="default?id=1">绑定默认数据类型</a></li>
展示页面的代码(default.jsp)如下:
使用注解方式定义了一个控制器类,同时定义了方法的访问路径。在方法参数中使用了HttpServletRequest类型,并通过该对象的getParameter()方法获取了指定的参数。
绑定简单数据类型指的是Java中几种基本数据类型的绑定,如Int、String、Double等类型。
展示页面的代码(simple.jsp)如下:
有时前端请求中参数名和后台控制器类方法中的形参名不一样,就会导致后台无法正确绑定并接收到前端请求的参数。为此,Spring MVC提供了@RequestParam注解来进行间接数据绑定。
@RequestParam注解主要用于定义请求中的参数,在使用时可以指定它的4个属性:
· value:name属性的别名,这里指入参的请求参数名字,例如value="user_id"表示请求的参数中名字为user_id的参数的值将传入。如果只使用value属性,就可以省略value属性名。
· name:指定请求头绑定的名称。
· required:用于指定参数是否必需,默认是true,表示请求中一定要有相应的参数。
· defaultValue:默认值,表示请求中没有同名参数时的默认值。
在实际应用中,客户端请求可能会传递多个不同类型的参数数据,此时可以使用POJO类型进行数据绑定。POJO类型的数据绑定是将所有关联的请求参数封装在一个POJO(对象)中,然后在方法中直接使用该POJO作为形参来完成数据绑定。
所谓的包装POJO,就是在一个POJO中包含另一个简单的POJO。
绑定集合的方式很多,有List、Set、Map等,此处以List为例进行介绍,其他方式类似。先在JSP页面构建List数据,
Spring MVC在传递数据时,通常需要对数据的类型和格式进行转换。而这些数据不仅可以是常见的String类型,还可以是JSON(JavaScript Object Notation,JS对象标记)等其他类型。
JSON是近些年一种比较流行的数据格式,它与XML相似,也是用来存储数据的,相较于XML,JSON数据占用的空间更小,解析速度更快。因此,使用JSON数据进行前后台的数据交互是一种十分常见的手段。
JSON是一种轻量级的数据交互格式。与XML一样,JSON也是一种基于纯文本的数据格式。JSON不仅能够传递String、Number、Boolean等简单类型的数据,还可以传递数组、Object对象等复杂类型的数据。
为了实现浏览器与控制器类之间的JSON数据交互,Spring MVC提供了一个默认的MappingJackson2HttpMessageConverter类来处理JSON格式请求和响应。通过它既可以将Java对象转换为JSON数据,也可以将JSON数据转换为Java对象。
笔者这里以FastJson为例进行讲解,FastJson采用独创的算法将parse的速度提升到极致,超过了所有JSON库。
1.引入JAR包
2.配置Spring MVC核心配置文件
3.创建Java对象
4.创建Controller控制器
REST实际上是Representational State Transfer的缩写,翻译成中文就是表述性状态转移。
RESTful(REST风格)是一种当前比较流行的互联网软件架构模式,它充分并正确地利用HTTP的特性,规定了一套统一的资源获取方式,以实现不同终端之间(客户端与服务端)的数据访问与交互。
一个满足RESTful的程序或设计应该满足以下条件和约束:
(1)对请求的URL进行规范,在URL中不会出现动词(都是使用名词),而使用动词都是以HTTP请求方式来表示的。
(2)充分利用HTTP方法,HTTP方法名包括GET、POST、PUT、PATCH、DELETE。
前面学习的都是以传统方式操作资源,对比传统方式和RESTful方式,区别如下。
1.传统方式操作资源
2.使用RESTful操作资源
请求访问URL格式的区别:
<h3>Spring MVC 对 RESTful 的支持</h3>
<li><a href="addNor?a=2&b=3">传统风格 add</a></li>
<li><a href="add/2/3">RESTful风格 add</a></li>
RESTful使用了路径变量,其好处如下:
(1)使路径变得更加简洁。
(2)获得参数更加方便,框架会自动进行类型转换。
(3)通过路径变量的类型可以约束访问参数,如果类型不一样,则访问不到对应的请求方法。
Spring MVC的拦截器(Interceptor)可以对用户请求进行拦截,并在请求进入控制器(Controller)之前、控制器处理完请求后甚至是渲染视图后执行一些指定的操作。
在Spring MVC中,拦截器的作用与Servlet中的过滤器类似,它主要用于拦截用户请求并进行相应的处理,例如通过拦截器可以执行权限验证、记录请求信息日志、判断用户是否已登录等操作。
Spring MVC拦截器使用的是可插拔式设计,如果需要某个拦截器,只需在配置文件中启用该拦截器即可;如果不需要这个拦截器,则只要在配置文件中取消应用该拦截器即可。
在Spring MVC中,要使用拦截器,就需要对拦截器类进行定义和配置,通常拦截器类可以通过两种方式来定义:一种是通过实现HandleInterceptor接口,或者继承HandleInterceptor接口的实现类HandleInterceptorAdapter来定义。
自定义的拦截器实现了接口中的3个方法。
1.preHandle()方法
该方法会在控制器方法前执行,其返回值表示是否中断后续操作。当返回值为true时,表示继续向下执行;当返回值为false时,会中断后续的所有操作(包括调用下一个拦截器和执行控制器类中的方法等)。
2.postHandle()方法
该方法会在控制器方法调用之后,解析视图之前执行。可以通过该方法对请求域中的模型和视图做出进一步的修改。
3.afterCompletion()方法
该方法会在整个请求完成(即视图渲染结束)之后执行。可以通过该方法实现资源清理、记录日志信息等工作。
在定义完拦截器后,还需要在Spring MVC的配置文件中使用mvc:interceptors标签及其子标签对拦截器进行配置,这样这个拦截器才会生效。
1.通过子标签配置全局拦截器
可以在Spring MVC的配置文件中,通过mvc:interceptors标签及其子标签将自定义的拦截器配置成一个全局拦截器。该拦截器会对项目内所有的请求进行拦截。
其代码如下:
2.通过子标签配置全局拦截器
除了标签外,还可以在mvc:interceptors标签中通过子标签定义一个全局拦截器引用,对所有的请求进行拦截。其代码如下:
mvc:interceptors标签的子标签不能单独使用,它需要与标签(mvc:interceptors标签内或mvc:interceptors标签外)或@Component等注解配合使用,以保证标签配置的拦截器是Spring IOC容器中的组件。
3.通过mvc:interceptor子标签对拦截路径进行配置
Spring MVC的配置文件中还可以通过mvc:interceptors的子标签mvc:interceptor对拦截器拦截的请求路径进行配置。
其代码如下:
需要注意的是,在mvc:interceptor中,子元素必须按照上述代码的配置顺序进行编写,即以mvc:mapping → mvc:exclude-mapping → 的顺序,否则就会报错。其次,以上这3种配置拦截器的方式,可以根据自身的需求以任意的组合方式进行配置,以实现在mvc:interceptors标签中定义多个拦截器的目的。
在运行程序时,拦截器的执行有一定的顺序,该顺序与配置文件中所定义的拦截器的顺序是相关的。拦截器的执行顺序有两种情况,即单个拦截器和多个拦截器的情况,单个拦截器和多个拦截器的执行顺序是不一样的,略有差别。
1.单个拦截器的执行流程
当只定义了一个拦截器时,它的执行流程如图14.8所示。
单个拦截器的执行流程说明如下:
(1)当请求的路径与拦截器拦截的路径相匹配时,程序会先执行拦截器类(MyInterceptor)的preHandle ()方法。若该方法返回值为true,则继续向下执行Controller(控制器)中的方法,否则将不再向下执行。
(2)控制器方法对请求进行处理。
(3)调用拦截器的postHandle()方法,对请求域中的模型(Model)数据和视图做出进一步的修改。
(4)通过DispatcherServlet的render()方法对视图进行渲染。
(5)调用拦截器的afterCompletion()方法完成资源清理、日志记录等工作。
2.多个拦截器的执行流程
在大型的企业级项目中,通常都不会只有一个拦截器,开发人员可能会定义许多不同的拦截器来实现不同的功能。在程序运行期间,拦截器的执行有一定的顺序,该顺序与拦截器在配置文件中定义的顺序有关。
假设一个项目中包含两个不同的拦截器:Interceptor1和Interceptor2,它们在配置文件中定义的顺序为Interceptor1→Interceptor2。下面通过一个拦截器流程图来描述多个拦截器的执行流程,如图14.9所示。
从上面的执行流程图可以看出,当存在多个拦截器同时工作时,它们的preHandle()方法会按照拦截器在配置文件中的配置顺序执行,但它们的postHandle()和afterCompletion()方法则会按照配置顺序的反序执行。
如果其中有拦截器的preHandle()方法返回false,各拦截器方法的执行情况如下:
(1)第一个preHandle()方法返回false的拦截器以及它之前的拦截器的preHandle()方法都会执行。
(2)所有拦截器的postHandle()方法都不会执行。
(3)第一个preHandle()方法返回false的拦截器之前的拦截器的afterCompletion()方法都会执行。
在进入SSM整合之前,笔者先简单介绍一下Maven,Maven是一种快速构建项目的小工具,它可以解决项目中手动导入包造成的版本不一致、找包困难等问题,同时通过Maven创建的项目都有固定的目录格式,使得约定优于配置,通过固定的目录格式快速掌握项目。
Maven的目录结构说明如下:
· bin存放的是Maven的启动文件,包括两种:一种是直接启动,另一种是通过Debug模式启动,它们之间就差一条指令而已。
· boot存放的是一个类加载器框架,它不依赖于Eclipse的类加载器。
· conf主要存放的是全局配置文件setting.xml,通过它进行配置(所有仓库都拥有的配置)的时候,仓库自身也拥有setting.xml,这个为私有配置,一般推荐使用私有配置,因为全局配置存放于Maven的安装目录中,当进行Maven升级时,要进行重新配置。
· lib存放的是Maven运行需要的各种JAR包。
· LICENSE是Maven的软件使用许可证书。
· NOTICE是Maven包含的第三方软件。
· README.txt是Maven的简单介绍以及安装说明。
SSM框架即将Spring MVC框架、Spring框架、MyBatis框架整合使用,以简化在Web开发中烦琐、重复的操作,让开发人员的精力专注于业务处理开发。
1.Spring MVC框架
Spring MVC框架位于Controller层,主要用于接收用户发起的请求,在接收请求后可进行一定处理(如通过拦截器的信息验证处理)。通过处理后,Spring MVC会根据请求的路径将请求分发到对应的Controller类中的处理方法。处理方法再调用Service层的业务处理逻辑。
2.Spring框架
Spring框架在SSM中充当黏合剂的作用,利用其对象托管的特性将Spring MVC、MyBatis两个独立的框架有机地结合起来。Spring可将Spring MVC中的Controller类和MyBatis中的SqlSession类进行托管,简化了人工管理过程。Spring除了能对Spring MVC和MyBatis的核心类进行管理外,还可以对主要的业务处理的类进行管理。
3.MyBatis框架
MyBatis框架应用于对数据库的操作,其主要功能类SqlSession可对数据库进行具体操作。
第5篇项目实战
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。