JavaWeb 学习总结

一、基本概念

1.1 WEB开发相关知识

​ web,网页的意思;用于便是服务器主机上供外界访问的资源

​ Internet上供外界访问的web资源分为

  • 静态 web:指web页面中供人们浏览的数据始终是不变的
    • 静态web开发技术:HTML
  • 动态 web:指 web 页面中供人们浏览的数据是由程序产生的,不同时间点访问web页面看到的内容各不相同
    • 常用动态web开发技术:JSP/Servlet、asp、php

在java中,动态web资源开发技术统称为JavaWeb

1.2 WEB 应用程序

​ web应用程序(web应用)指供浏览器访问的程序,例如有多个 .html web资源,这多个web资源放在一个目录中,并对外提供服务,这就组成了一个web应用

​ 一个web由多个静态 web 资源和动态 web 资源组成,如:html、css、js 文件、jsp、文件、java程序、支持jar包、配置文件等

Web应用开发好后,若想提供外界访问,需要把web应用所在的目录交给 web 服务器管理(如Tomcat),这个过程称为虚拟目录映射

1.3 静态 web

​ *.html 是网页源文件,直接访问服务器上的这些内容,可以获取网页上的信息;整个静态web操作的过程如下:

image-20200409183123296

​ 访问静态web程序的过程中,用户使用客户端浏览器(Chrome、FireFox等),经过网络(NetWord)连接到服务器上,使用HTTP协议发送一个请求(Request),服务器接收到来自客户端的请求,告知 WEB 服务器客户端要所要请求的页面, WEB 服务器接收到所有请求后,根据用户的需要,从文件系统(存放所有静态页面的磁盘)去除内容,通过 WEB 服务器返回给客户端,客户端接收到内容之后经过浏览器渲染解析,得到显示的效果。

静态 WEB 的缺点:

  1. Web页面中的内容无法动态更新,所有用户每个时刻访问看见的内容都是一样的

    静态 web 也可以加上一些动态的特效,使用JavaScript 或 VBScript(微软拿来对标js的),但是这些特效都是基于浏览器实现的,就像css设置网页样式一样,js设置一些特效、行为,所以在服务器上没有任何变化(可以看出区分动态和静态web并不是看网页动或者不动

  2. 无法连接数据库(数据无法持久化),无法实现和用户的交互

1.4 动态 web

​ 同样,动态 web 并不是至页面会动,主要的特性是:web 页面的展示效果因人而异(如:淘宝的‘千人千面’),且动态 web 具有交互性,其页面内容可以动态更新(静态网站内容发生变化往往有一个‘更新首页’的功能,手动更新web页面),动态 web 的操作过程如下:

image-20200409195916875

​ 同静态 web 一样,使用客户端浏览器通过网络连接服务器,并通过HTTP协议发送请求(Request),不同的是所有请求都先经过一个WEB Server Plugin(服务器插件)来处理,区分请求的是静态资源(*.html)还是 动态资源。

​ 静态资源直接交给 web 服务器,之后web服务器从文件系统中取出内容,发送回客户端;

​ 而 动态资源(*.jsp、*.asp、*.php),会先将请求转交给 web Container(web 容器),在 web Container 中连接数据库,在数据库中取出数据等一系列操作后 动态拼接页面的展示内容,然后将展示内容交给 web 服务器,响应返回给客户端浏览器解析执行。

动态 web 缺点:假如服务器的动态web资源出现了错误,就需要重新编写后台程序(java来说就是servlet),重新发布,即 停机维护

​ 优点就是弥补了静态 web 的不足

动态 web 的实现方式:

  • ASP、ASP.NET

    ​ 出自微软,asp已淘汰,ASP.NET效率好,但受限于平台,C#上用得多

  • PHP

    ​ 世界上最好的编程语言...

  • JAVA Servlet/JSP

    ​ B/S架构的语言,不受平台约束,支持多线程处理方式

    B/S 为浏览器和服务器 ; C/S 为 客户端和服务器

二、WEB服务器

1.1 简介

​ Web 服务器是指连接在Internet上的某种类型的计算机程序(给向其发送请求的浏览器提供文档的程序),服务器处理该请求并将文件反馈到该浏览器上,附带的信息(响应头)告诉浏览器如何查看该文件(文件类型、字符集等)。

image-20200409203856151

​ 服务器是一种波动程序,只有当Internet上运行在其他计算机中的浏览器发出请求时,服务器才会响应。

image-20200409203905212

2.2 Web 服务器

​ 常见的 IIS 是Windows自带的,而 Tomcat 服务器是实现了 Java EE 的最小的 web 服务器,性能稳定,开源免费,JavaWeb良品

不管什么 web 资源,想要被远程计算机访问,都必须有一个与之对应的网络通讯程序,当用户来访问时,这个网络通讯程序读取 web 资源数据,并返回给来访者

​ 而Tomcat(web服务器)就是这样一个程序,负责完成底层的网络通讯,开发者只用关注web资源如何写

image-20200409205539036

三、Tomcat服务器

1.1 端口的配置

​ Tomcat 的所有配置都放在 conf 文件夹之中,里面的 server.xml 文件是配置的核心文件。

​ 如果想修改 Tomcat 服务器的启动端口,则可以在 server.xml 配置文件中的 Connector 节点进行的端口修改。

端口修改后重启 Tomcat 生效

1.2 虚拟目录的映射方式

  1. 让Tomcat服务器自动映射

    ​ Tomcat 服务器会自动管理 webapps 目录下的所有web应用,并映射成虚拟目录,即webapps中的 web 应用,外界可以直接访问;

    ​ 所以可以直接将一个web应用整个地 复制 到 webapps 目录下,然后通过对应的路径去访问此web应用。

    修改配置后需要重启Tomcat服务器

  2. 在 servlet.xml 文件的 元素中进行配置

1 <Host name="localhost"  appBase="webapps"
2              unpackWARs="true" autoDeploy="true"
3              xmlValidation="false" xmlNamespaceAware="false">
    <!--下面这条就是配置的内容-->
5          <Context path="/servlet" docBase="D:\JavaWeb\servlet" />
6  </Host>

​ Context 表示上下文,代表整个 JavaWeb 应用

​ path:用来配置虚拟目录,就是浏览器地址栏中输入的访问地址,必须以 ' / ' 开头

​ docBase:配置虚拟目录对应的硬盘上的web应用资源的路径

这里的path 和 docBase 与 Java 中的 Servlet程序 配置web.xml 访问类似

  1. 在 tomcat 服务器的 \ conf\Catalina\localhost 目录下新建一个以 .xml 文件,文件名就为虚拟目录(即上面一种方式的 path),然后只用在xml文件中配置 docBase 即可实现映射:(xml文件名为 path ,其中配置 docBase)
<Context docBase="D:\JavaWeb\servlet" />

此种方式修改配置文件后不用重启Tomcat服务器

1.3 配置虚拟主机

​ 一个主机只能放一个网站,配置多个虚拟主机就可以放多个网站

​ Tomcat 配置虚拟主机:修改 conf文件夹 --> server.xml 配置文件,使用元素进行配置,一对 host 表示一个虚拟主机(就像配置多个Servlet程序一样)

<Host name="www.bibi.com" appBase="D:\">
</Host>

​ 其中 name 此 host 代表的主机,这台主机管理着 appBase 目录下所有的 web 应用(即 JavaWeb 目录并不是代表一个web项目的根目录,而是一个存放的一个或多个web应用的文件下,就像Tomcat服务器的 webapps 目录一样,下面可以放多个web应用)

1.4 Windows 系统中注册域名

​ 配置的主机(网站)要想通过域名被外部访问,必须在DNS服务器 或 Windows系统中注册访问网站时用的域名,即修改 hosts 文件:"C:\Windows\System32\drivers\etc"

image-20200409222603350

​ 编辑此文件,将想要设置域名和IP绑定在一起,如下就可以通过 http://web:8080 去访问web应用了(端口根据自己设置的)

image-20200409222759409

1.5 打包JavaWeb应用

​ 开发完一个 JavaWeb应用后,使用 jar 命令将其打包成一个 war 包(java的包为 jar,JavaWeb为war),用法:

image-20200410080510423

​ 而后将 war 包放到 Tomcat 服务器的 webapps 目录下,运行服务器,Tomcat 服务器在部署 web 应用时会将此 war 包进行解压,生成 web 应用的文件夹

1.6 Tomcat 体系结构

image-20200410081107263

​ Tomcat 服务器的启动时基于 servlet.xml 配置文件,Tomcat 启动时会先启动一个 Server ,Server 会启动 Service(所有请求响应操作都会经过它),Service 里面启动多个 Connector(连接器),连接器不处理请求,负责将用户请求交给 Engine(引擎),由 Engine 解析请求,并将请求交给用户要访问的 Host ,Host 也解析请求,查看用户要访问主机下的哪个 web 应用,一个 Context(上下文)代表一个 web 应用

servlet.xml 中的配置信息:

<?xml version="1.0" encoding="UTF-8"?>
<!-- 服务器 -->
<Server port="8005" shutdown="SHUTDOWN">
<!-- 服务 -->
  <Service name="Catalina">
	<!-- HTTP协议的连接器 -->
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
	<!-- HTTP11 应该就是HTTPS协议的连接器 -->
    <Connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol"
               maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
               clientAuth="false" sslProtocol="TLS" 
                keystoreFile="conf/.keystore" keystorePass="123456"/>
	<!-- AJP协议的连接器 -->
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
	<!-- 引擎 -->
    <Engine name="Catalina" defaultHost="localhost">
		<!-- Host 本地主机 -->
       <Host name="localhost"  appBase="webapps"
             unpackWARs="true" autoDeploy="true">
         <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
                prefix="localhost_access_log." suffix=".txt"
                pattern="%h %l %u %t "%r" %s %b" />
       </Host>
	   <!-- Host 增加的虚拟主机 -->
       <Host name="web" appBase="D:\servlet\hello">
			<!-- web 应用(上下文对象就代表一个web应用) -->
         <Context path="" docBase="D:\servlet\hello"/>
       </Host>
     </Engine>
   </Service>
 </Server>

1.7 互联网上的加密原理

​ Tomcat 服务器启动时会启动多个 Connector(连接器),而 Tomcat 服务器的连接器分为密连接器和非加密连接器

加密方式:

  1. 对称加密(密钥加密)

    ​ 采用单秘钥的加密方式,传输数据的加密方和解密数据的接收方使用统一秘钥进行加解密,所以如何把秘钥安全地传递到解密者手上至关重要

    ​ 常用的对称加密有:DES、IDEA、RC2、RC4、SKIPJACK、RC5、AES 算法等

  2. 非对称加密

    ​ 非对称加密需要两个秘钥:”公共秘钥“ 和 ”私有秘钥“,公钥和私钥是一对,如果用公钥进行加密,那么只有对应的私钥才能解密,如果用私钥进行加密,也只有对应的公钥才能解密

    ​ 由于加密和解密使用的是两个不用的秘钥,所以称为非对称秘钥,实现机密信息交换的基本过程为:甲方生成一对秘钥,并将其中一个作为公共秘钥对外开放,得到该公钥的乙方,使用此秘钥对要加密的信息进行加密,而后传递给甲方,甲方接收到被公钥加密的数据后,使用自己的另一个秘钥,即私钥进行解密;同样的,甲方可以使用乙方的公共秘钥,对数据进行加密签名,而后发送给乙方,乙方使用自己的私钥进行解密验签;

    ​ 简单说就是发送方使用接收方的公钥进行加密,并发送给接收方,接收方收到数据后使用自己的私钥进行解密;即时数据被第三方截取,由于通过数字的手段的加密是一个不可逆的过程,没有对应的私钥也无法解密;

    ​ 但还是存在安全问题:加入A要给B发送数据,那么B需要生成一对秘钥,并将公钥发送给A用来加密数据,但过程中被C截取了,而C也使用B的公钥加密数据,再发给B,B接收到数据后就晕了,分不清这条数据是A发的还是C发的;另一个问题是:在B给A发送公钥的过程中,被C截取后,C生成一对秘钥,将公钥发送给A,A以为这个就是B的公钥,就使用此公钥对信息进行加密,发送给B,过程中被C截获,C可以直接用私钥进行解密,获取数据,而B收到数据后,使用私钥确无法解密;

    ​ 所以,非对称式加密需要确定拿到公钥一定是自己想要的,解决办法:一个第三方机构 CA机构(证书授权中心)来担保,过程:A想给B发送数据,先将公钥发给 CA机构,CA拿到公钥后跑到B家里问:公钥是不是你发的?(确认公钥),B确认后 CA 就会给B公钥做担保,生成一份数字证书给B,数字证书包含了CA的担保认证签名和B的公钥,B拿到CA的数字认证后,就发给A,A拿到数字证书后,看到上面有CA的签名,就可以确定当前拿到的是B的公钥,那么就可以放心使用公钥加密数据,而后发给B了

     ### 1.8 HTTPS 连接器
    

​ 了解了加密原理后,看看浏览器与服务器交互过程的加密:浏览器想要将加密数据发送给服务器,服务器会向浏览器发送一个数字证书,而后浏览器就使用数字证书中的公钥对数据进行加密,再发送给服务器,服务器接收后使用私钥进行解密

​ 所以要实现浏览器与服务器的加密传输,先需要给服务器生成一个数字证书,在配置一下服务器,让服务器收到浏览器的请求后,会向浏览器出示它的数字证书

  1. 生成 Tomcat 服务器的数字证书

    1. jdk的bin目录下有一个keytool.ext 程序就是java提供的制作数字证书的工具

      使用 keytool 生成一个名字为 tomcat 的数字证书,存放在 .keystore 这个秘钥中

      keytool -genkey -alias tomcat -keyalg RSA
      

image-20200410142321188

生成完后操作系统的用户目录下就会生成一个 .keystore 文件

image-20200410142422199

使用命令:keytool -list -keystore .keystore 查看 .keystore 秘钥库里面的所有证书

image-20200410142810600

  1. 配置 HTTPS 连接器

    ​ 将生成的. keystore 密钥库文件拷贝到 Tomcat 服务器的 conf 目录下

    ​ 修改 server.xml 文件,配置 https 连接器,代码如下:

    1  <Connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol"
    2                maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
    3                clientAuth="false" sslProtocol="TLS" 
    4                keystoreFile="conf/.keystore" keystorePass="123456"/>
    

      在 server.xml 文件中配置了一个端口是 8443 的加密连接器,浏览器访问 8443 端口的连接器时,将会以加密的方式来访问 web 服务器,这个连接器收到浏览器的请求后,将会向浏览器出示一份数字证书,浏览器再用数字证书里面的公钥来加密数据,keystoreFile="conf/.keystore" 用来指明密钥库文件的所在路径,服务器从密钥库中提取证书时需要密码,keystorePass="123456" 指明密钥库的访问密码

      使用 "https://localhost:8443/" 访问 8443 的加密连接器

    image-20200410143244215

    ​ 由于密钥库里面的证书是我们手工生成的,没有经过 CA 的认证,所以使用 "https://localhost:8443/" 访问 8443 的加密连接器,浏览器会出现 "证书错误,导航已阻止",浏览器认为当前要访问的这个主机是不安全的,不推荐继续访问,点击img就可以继续访问了,如下图所示:

    image-20200410143316897

  2. 安装数字证书

    ​ 为了让浏览器信任我们生成的数字证书,需要将数字证书安装到浏览器中,以 IE8 浏览器为例进行证书安装说明,安装步骤如下:

    image-20200410143526359

    image-20200410143535359

    image-20200410143555912

    image-20200410143607161

    image-20200410143715092

    image-20200410143741288

    ​ 证书安装成功后,重启浏览器后就可以正常访问了

  3. 删除数字证书

    ​ 以 IE8 为例进行说明,操作步骤如下:工具 ----->Internet 选项

    image-20200410144006980

    ​ 删除之后重启浏览器即可

四、HTTP协议

4.1 简介

  1. 什么是HTTP协议

    ​ HTTP 是 HyperText Transfer Protocol 超文本传输协议,是TCP/IP 协议个一个应用层协议,用于定义 web浏览器 和 web服务器 之间交换数据的过程,规定了客户端与web服务器之间的通讯格式

  2. HTTP协议的版本

    • HTTP/1.0:客户端与 web 服务器建立连接后,只能获得一个 web 资源

    • HTTP/2.0:允许客户端与 web 服务器建立连接后,在一个连接上获取多个 web 应用

4.2 HTTP 请求

客户端连上服务器后,向服务器请求某个web资源,称之为客户端向服务器发送了一个 HTTP 请求

一个完整的 HTTP 请求包含:一个请求行、若干个消息头、及实体内容

访问百度:客户端 --> 发请求(Request) --> 服务器

image-20200410162746105

  1. 请求行

    ​ 上面的称为请求行,可看出请求方式为 GET,请求方式有 POST、GET、HEAD、OPTIONS、DELETE、TRACE、PUT,常用的:GET、POST

    ​ 浏览器向服务器发送请求默认都是 GET 方式,可通过修改表单提交方式改为POST

    ​ 它们都用于向 web 服务器请求某个 web 资源,区别表现在数据传递上:可以在浏览器地址栏输入地址时,在请求的 URL 地址后 加 ?key1=value1&key2=value2... HTTP/1.1表示给服务器发送数据,其属性及值在地址栏中可见,所以不能用于传递敏感信息,且URL地址栏后能附带的参数是有限的,不能1k(不安全,高效)

    ​ POST 方式:可无限制地在请求的实体内容中向服务器发送数据,且内容不会显示在URL地址栏中(安全,不高效)

  2. 消息头

    常用消息头:

    accept:浏览器通过这个头告诉服务器,它所支持的数据类型(格式:大文本/小文本 如:text/html)

    Accept-Charset:浏览器通过这个头告诉服务器它支持哪种字符集

    Accept-Encoding:浏览器通过这个头告诉服务器,支持的压缩格式

    Accept-Language:浏览器通过这个告诉服务器,它的语言环境

    Host:浏览器通过这个头告诉服务器,向访问哪台主机

    if-Modified-Since:浏览器通过这个头告诉服务器,缓存数据的时间

    Referer:浏览器通过这个头告诉服务器,客户机是哪个页面来的(访问来源)

    Connection:浏览器通过这个头告诉服务器,请求完成后是断开连接还是保持连接

    例如百度的:

    image-20200410165645730

Upgrade-Insecure-Requests:客户端向服务器端发送信号表示它支持 upgrade-insecure-requests 的升级机制

User-Agent:识别发起请求的用户代理软件的应用类型、操作系统、软件开发商以及版本号

4.3 HTTP 响应

  1. 一个 HTTP 响应代表服务器向客户端回送数据,包括:一个状态行、若干个消息头、以及实体内容

image-20200410172906263

image-20200410172607318

  1. 状态行

    ​ 包含:HTTP 版本号、状态码、原因叙述

    状态码用于表示服务器对请求的处理结果,分为 5 类:

    image-20200410173218964

  2. 常用响应头(消息头)

    Location:服务器通过这个头,告诉浏览器跳到哪里

    Server:服务器通过这个头,告诉浏览器服务器的型号

    Content-Encoding:服务器通过这个头,告诉浏览器数据的压缩格式

    Content-Length:服务器通过这个头,告诉浏览器回送数据的长度

    Content-Language:服务器通过这个头,告诉浏览器语言环境

    Content-Type:服务器通过这个头,告诉浏览器回送数据的类型

    Refresh:服务器通过这个头,告诉浏览器定时刷新

    Content-Dispostion:服务器通过这个头,告诉浏览器以下载的方式打开数据

    Transfer-Encoding:服务器通过这个头,告诉浏览器数据是以分块的形式回送

    Expires:-1 以下为 控制浏览器不要缓存

    Cache-Control:no-cache

    Pragma:no-cache

4.4 在服务端设置响应头来控制客户端浏览器的行为

  1. 设置 Location 响应头,实现请求重定向
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** 重定向
 * @author YH
 * @create 2020-04-10 18:24
 */
public class Location extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //    通过响应头能设置一系列的浏览器行为
        //通过响应对象:设置状态行 的 状态码 实现重定向(状态码位于状态行,不是响应头,所以设置方法与响应头不同)
        resp.setStatus(302);
        //通过响应对象:设置响应头告诉浏览器跳转到哪里
        //在这里使用 / 代表webapps目录,所以不能使用相对路径在当前的web应用,即request
        resp.setHeader("location","location.html");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
  1. 设置 Content-Encoding 响应头,告诉浏览器数据的压缩格式
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.GZIPOutputStream;

/**
 * @author YH
 * 这个小程序用于演示:
 *  1.使用gzipOutputStream流佬压缩数据
 *  2.设置响应头Content-Encoding来告诉浏览器,服务器发送回去的数据压缩后的格式
 * @create 2020-04-10 18:58
 */
public class Encoding extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String data = null;
        for (int i = 0; i < 5000; i++) {
            data += "abcdefghijk";
        }
        System.out.println("原始数据大小为:" + data.getBytes().length);
		
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        GZIPOutputStream gout = new GZIPOutputStream(bout);
        //将资源写入输入流
        gout.write(data.getBytes());
        //gout流资源如果不关闭,就会占用bout流,使得下面也无法获取bout的数据
        gout.close();
        //得到压缩后的数据
        byte[] g = bout.toByteArray();
//通过响应对象:设置Content-Encoding响应头
        resp.setHeader("Content-Encoding","gzip");
        resp.setHeader("Content-Length",String.valueOf(g.length));
        //将资源写入网页
        resp.getOutputStream().write(g);
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
  1. 设置 Content-type 响应头,指定回送数据的类型
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;

/** 设置 Content-type 响应头,指定回送数据的类型
 * @author YH
 * @create 2020-04-10 19:12
 */
public class Content_type extends HttpServlet {
      /**
      * 浏览器能接收(Accept)的数据类型有:
      * application/x-ms-application,
      * image/jpeg,
      * application/xaml+xml,
      * image/gif,
      * image/pjpeg,
      * application/x-ms-xbap,
      * application/vnd.ms-excel,
      * application/vnd.ms-powerpoint,
      * application/msword,
      */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//        通过响应头,能设置一系列的浏览器的行为
        //通过响应对象:设置 Content-type响应头,指定回送数据的类型
        //回送图片
        resp.setHeader("Content-type","image/jpeg");

        //读取图片(通过java操作外部资源通常都需要进行IO读/写操作,即让java现先获得资源,再去使用资源)
        //通过获取web应用的Context上下文对象,获取整个web应用的资源,将web根目录下的png图片资源转为输入流
        InputStream inputStream = this.getServletContext().getResourceAsStream("web技术体系.png");

        //ServletOutputStream 底层是 OutputBuffer 底层有时间详细了解下-----------------
        ServletOutputStream outputStream = resp.getOutputStream();

        //设置缓存
        int len = 0;
        byte[] buffer = new byte[1024];
        while((len = inputStream.read(buffer)) > 0){
            outputStream.write(buffer,0,len);
        }

//        resp.setHeader("Content-type","text/plain;charset=utf-8"); //同时设置文本字符集
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
  1. 设置 Refresh 响应头,让浏览器定时刷新
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** 设置 Refresh 响应头,让浏览器定时刷新
 * @author YH
 * @create 2020-04-10 19:28
 */
public class Refresh extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//        通过响应头,可以设置一系列的浏览器行为
        //通过响应对象:设置 Refresh 响应头, 2 秒刷新一次
//        resp.setHeader("Refresh","2");
        //设置 Refresh 响应头,让浏览器每隔三秒跳转到 百度(类似登陆后隔几秒跳转)
        resp.setHeader("Refresh","3;url=https://www.baidu.com");
        resp.getWriter().write("hhhhhhhhhh");
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

  1. 设置 Content-Dispostion 响应头,让浏览器下载文件
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLEncoder;

/** 设置 Content-Disposition 响应头,让浏览器下载文件
 * @author YH
 * @create 2020-04-10 19:40
 */
public class Disposition extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("执行到这里");

        //        通过响应头,可以设置一系列浏览器的行为
//        下载网页上的内容需要几个步骤:1、文在文件的路径 2、根据路径获取文件名 3、最后通过响应头设置要下载的文件名
        //获取将被下载的文件路径
        String filePath = "D:\\javacode\\javaweb_Servlet2\\response\\src\\main\\webapp\\哈哈11.xml";
//        通过一些方法,从路径中提取出 文件名:
        //通过获取最后一个 / 的脚标 + 1 确定文件名的第一个字符索引,而后从此索引开始截取字符串,就是文件名
        String fileName = filePath.substring(filePath.lastIndexOf("\\") + 1);
        //通过响应对象:设置 content-disposition 响应头; URLEncoder.encode() 方法用于将字符串转换成特定编码的
        resp.setHeader("Content-Disposition","attachment;filename=" + URLEncoder.encode(fileName,"utf-8"));
//        如果只有上面这些步骤,那么下载到的回事空文件;还需要将文件内的数据,以Stream流的形式写入文件中
        //创建文件输入流,读取下载文件的数据(注意:java要操作任何外部资源都要先获取(读 ),才能进行其他操作(写))
        //注意此处的文件流传递的参数为 文件路径
        FileInputStream fileInputStream = new FileInputStream(filePath);

        //获取响应对象的输出流,用于将下载文件数据写入下载文件内
        ServletOutputStream outputStream = resp.getOutputStream();
        int len;
        byte[] buffer = new byte[1024];
        //将文件输入流中的数据读取缓存数组中
        while((len = fileInputStream.read(buffer)) > 0){
            outputStream.write(buffer,0,len);
        }
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

五、浏览器与服务器交互

5.1 交互过程(访问网站的过程)

image-20200409232149609

​ 当在浏览器中输入URL地址 http://web:8080/servlet/hello.jsp 访问服务器上的 hello.jsp 这个web资源的过程中进行的操作:

  1. 浏览器根据主机名“web” 去操作系统的 Host 文件中查找主机名对应的IP地址
  2. 如果在 Host 文件中没有找到相对应的 IP 地址,就去互联网上的DNS域名服务器上查找是否有“web”这台主机对应的 IP
  3. 浏览器查找到 ”web“这台主机对应的IP地址后,就使用 IP 地址链接上 WEB 服务器(没找到返回”无法访问此网站“提示)
  4. 浏览器连接到web服务器后,使用HTTP协议向服务器发送请求,同时向 web 服务器以 Stream流 的形式传输数据,告诉 web 服务器要访问服务器里面的哪个 web应用(servlet) 下的 web资源 (hello.jsp)
  5. 服务器就收到浏览器传输的数据(请求行、请求头)后,开始等待 web服务器 将浏览器要访问的 web资源 传输给它
  6. 服务器接收到浏览器传输的数据后,开始解析接收到的数据,服务器解析 "GET /servlet/hello.jsp HTTP/1.1" 里面的内容时知道客户端浏览器要访问的是 **servlet应用里面的 heelo.jsp 这个 Web 资源,然后服务器就去读取 hello.jsp 这个 Web 资源里面的内容,将读到的内容再以 Stream(流) 的形式传输给浏览器
  7. 浏览器拿到服务器传输给它的数据之后,就可以显示在浏览器内给用户看,这就是浏览器和服务器的交互过程。

5.2 JavaWeb应用的组成结构

  开发 JavaWeb 应用时,不同类型的文件有严格的存放规则,否则不仅可能会使 web 应用无法访问,还会导致 web 服务器启动报错

image-20200409234440313

  WebRoot :Web 应用所在目录,一般情况下虚拟目录要配置到此文件夹当中

    ┝WEB-INF:此文件夹必须位于 WebRoot 文件夹里面,而且必须以这样的形式去命名,字母都要大写

      ┝web.xml:配置文件,有格式要求,此文件必须以这样的形式去命名,并且必须放置到 WEB-INF 文件夹中

​ web.xml 的格式可在Tomcat 目录下的 webapps\ROOT\WEB-INF 这个目录下的 web.xml 文件中的格式为模板,往往是最新的配置版本,如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                      http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
  version="4.0"
  metadata-complete="true">

</web-app>

​ 这就是 web.xml 文件的格式

六、Maven

​ Maven 翻译为"专家"、"内行",是 Apache 下的一个纯 Java 开发的开源项目。基于项目对象模型(缩写:POM)概念,Maven利用一个中央信息片断能管理一个项目的构建、报告和文档等步骤。

Maven 是一个项目管理工具,可以对 Java 项目进行构建、依赖管理

​ Maven 使用约定大于配置的原则,所以要遵守一定的目录结构

image-20200411081850890

Maven 下载地址:http://maven.apache.org/download.cgi

​ 需要配置环境变量

6.1 Maven POM

​ POM(Project Object Model,项目对象模型)是 Maven 工程的基本工作单元(核心):pom.xml 文件,包含了项目的基本信息,基础配置:

<project xmlns = "http://maven.apache.org/POM/4.0.0"
    xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0
    http://maven.apache.org/xsd/maven-4.0.0.xsd">
 
    <!-- 模型版本 -->
    <modelVersion>4.0.0</modelVersion>
    <!-- 公司或者组织的唯一标志,并且配置时生成的路径也是由此生成, 如com.companyname.project-group,maven会将该项目打成的jar包放本地路径:/com/companyname/project-group -->
    <groupId>com.companyname.project-group</groupId>
 
    <!-- 项目的唯一ID,一个groupId下面可能多个项目,就是靠artifactId来区分的 -->
    <artifactId>project</artifactId>
 
    <!-- 版本号 -->
    <version>1.0</version>
</project>

添加依赖:

<!--该元素描述了项目相关的所有依赖。 这些依赖组成了项目构建过程中的一个个环节。它们自动从项目定义的仓库中下载。要获取更多信息,请看项目依赖机制。 -->
<dependencies>
    <dependency>
        ......
    </dependency>
</dependencies>

Maven 仓库:https://mvnrepository.com/

6.2 配置阿里云镜像

  1. 打开Maven根目录下的conf文件下的 settings.xml

image-20200411091418495

  1. 配置镜像,加速下载依赖等速度,在 mirrors 标签内配置如下:
<mirror>
    <id>aliyunmaven</id>
    <mirrorOf>*</mirrorOf>
    <name>阿里云公共仓库</name>
    <url>https://maven.aliyun.com/repository/public</url>
</mirror>

6.3 本地仓库

在 localRepository 标签内进行配置如下(地址为本地仓库路径):

<localRepository>D:\java_JDK\apache-maven-3.6.3\maven-repo</localRepository>

配置成功会在指定路径下生成一个文件夹,如下:

image-20200411092212461

6.4 在IDEA下使用Maven创建web项目

image-20200411093329592

image-20200411094022531

image-20200411095654341

​ 创建一个Module作为web应用:

image-20200411095909300

image-20200411100232615

image-20200411102053084

创建完成,可以看到目录下自动配置了webapps目录即WEB-INF文件:

image-20200411102212449

由于创建web时会进行一系列初始化操作,下载一些依赖,需要稍作等待,特别是第一时间较长,看见控制台如下所示,表示下载完毕:

image-20200411102509478

web.xml也进行了自动配置:

​ 需要注意的是:自动配置时可能使用的是IDEA默认的配置,需要手动改一下(一劳永逸的办法是修改默认的配置,方法在后面创建完项目后介绍)

七、Servlet 开发

7.1 Servlet调用流程图(生命周期)

总的五大步骤:

image-20200430101922476

加载

初始化:init() ,默认在 servlet 被加载时并实例化后执行

服务:service() ,最终具体体现在 doGet()/doPost() 两个方法

销毁:destroy(),servlet 被系统回收时执行

卸载

具体流程:

Servlet调用流程图

Servlet 容器部分:

image-20200428144113035

小结:   

  1. Servlet何时创建
  默认第一次访问servlet时创建该对象(调用init()方法,设置 属性可修改为 Tomcat启动时执行 init() 方法)
  2. Servlet何时销毁
服务器关闭servlet就销毁了(调用destroy()方法)
  3. 每次访问必须执行的方法

         public void service(ServletRequest req, ServletResponse resp)

7.2 IDEA中开发Servlet

​ Servlet 接口有两个默认实现类:GenericServlet、HttpServlet

​ HttpServlet 重写了 service 方法,且方法体内的代码会自动判断用户的请求方式,GET 调用 doGET(),POST调用 doPOST() 方法,因此我们只需重写 doGET() 和 doPOST() 方法,不用去重写 service()。

创建方式:

image-20200411165554820

然后设置名字即可

​ 但这里不过多赘述,实际还是使用 Maven ,主要关注点应放在配置上。

7.2 Servlet 开发注意项

  1. Servlet 访问 URL 映射配置

    ​ 每一个 servlet 就是一个 Servlet程序,都需要在 web.xml 中进行配置,如:

创建一个让浏览器定时刷新的 servlet 程序:

public class Refresh extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//        通过响应头,可以设置一系列的浏览器行为
        //通过响应对象:设置 Refresh 响应头, 2 秒刷新一次
//        resp.setHeader("Refresh","2");
        //设置 Refresh 响应头,让浏览器每隔三秒跳转到 百度(类似登陆后隔几秒跳转)
        resp.setHeader("Refresh","3;url=https://www.baidu.com");
        resp.getWriter().write("hhhhhhhhhh");
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

就需要到 WEB-INF 目录下的 web.xml 配置文件添加配置:

image-20200411170347114

<servlet>
    <servlet-name>Refresh</servlet-name>
    <servlet-class>com.zuh8.servlet.Refresh</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>Refresh</servlet-name>
    <url-pattern>/refresh</url-pattern>
</servlet-mapping>

是一对标签,共同给此 web 应用配置信息

  • 两个 必须相同,常用定义的类名

  • 用于指定此 web 程序的全类名

  • 表示UTL访问路径 "/" 代表当前 web 应用的根路径

    ​ 进行访问:

    image-20200411171129556

当前 web 应用的根路径为 localhost:8080/response/,此时这个 servlet 程序就被访问了

  • 同一个 servlet 程序,可以被映射到多个 URL 上,即多个 元素的
<servlet>
    <servlet-name>Refresh</servlet-name>
    <servlet-class>com.zuh8.servlet.Refresh</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>Refresh</servlet-name>
    <url-pattern>/refresh</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>Refresh</servlet-name>
    <url-pattern>/refresh222</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>Refresh</servlet-name>
    <url-pattern>/refresh333</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>Refresh</servlet-name>
    <url-pattern>/refresh444</url-pattern>
  </servlet-mapping>

此时就可以通过多个路径访问这一个 servlet 程序了:

​ localhost:8080/response/refresh

​ localhost:8080/response/refresh222

​ localhost:8080/response/refresh333

​ localhost:8080/response/refresh444

  • Servlet 访问 URL 使用 * 通配符映射 

在 Servlet 映射到的 URL 中也可以使用 * 通配符,但是只能有两种固定的格式:一种格式是 "*. 扩展名",另一种格式是以正斜杠(/)开头并以 "/*" 结尾。例如:

image-20200411172506926

当输入的路径符合多个匹配规则时,“谁长得更像就找谁”

对于 servlet 来说构建路径(src 目录)、webapps/WebContent 都属于根目录

web.xml 中 / 代表 web 应用根目录(http://localhost:8888/ServletDemo/)

jsp 中 / 代表服务器根路径http://localhost:8888/)

  1. Servlet 类与普通 Java 类的区别

    ​ Servlet 是一个供其他 Java 程序(Servlet引擎)调用的类,不能独立运行,完全由 Servlet 引擎来控制和调度。

    ​ 针对客户端的多次 Servlet 请求,服务器只会创建创建一个 servlet 实例对象,即首次收到 Servlet 请求后,创建的 servlet 对象会驻留在内存中,为后继其他请求服务,直至 web 容器(Tomcat)退出,servlet 实例才会被销毁。

    ​ 在 Servlet 整个生命周期内,Servlet 的 init() 方法只会被调用一次;而对一个 Servlet 的每次访问请求都会导致 servlet 的 service() 被调用,对于每次访问请求 Servlet 引擎都会创建一个新的 HttpServeltRequest 请求对象(包括请求信息) 和一个新的 HTTPServletResponse 响应对象(目前不存在响应信息,空的),然后将这两个对象作为参数传递给 service() 方法,service 方法再根据请求的方式,调用 doXXX() 方法(也就是我们重写的方法)。

    ​ 如果在 web.xml 中的 元素内配置一个 元素,那么 web 应用程序在启动时就会装载并创建 Servlet 的实例对象、以及调用 Servlet 实例对象的 init() 方法(正常的 servlet 是通过配置的 URL 访问,并向 Servlet 发送请求才会触发引擎去创建 servlet 实例的),配置举例:

    <servlet>
        <servlet-name>invoker</servlet-name>
        <servlet-class>
            org.apache.catalina.servlets.InvokerServlet
        </servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    

    用途:为 web 应用写一个 initServlet(进行一些初始操作的程序),这个 servlet 程序在 web 应用启动时装载创建,可用来给整个 web 应用创建爱必要的数据库表和数据等。

  2. 缺省 Servlet

    ​ 如果某个 Servlet 的映射路径为一个证斜杠(/),那么这个 servlet 就成为当前 web 应用的缺省 servlet。

    ​ 凡是

    访问存在的 web 资源,最终都是访问到缺省 servlet 效果:

    public class Default extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            System.out.println("我是缺省 servlet 我被进入了");
            resp.getWriter().print("Hi baby!");
        }
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            this.doGet(req, resp);
        }
    }
    

image-20200411191429215

控制台输出:

image-20200411191645172

不仅如此!通过 URL 访问静态 HTML 或图片时,实际上也是在访问 Servlet

访问 web 应用下的静态 HTML 和 图片,示例:

image-20200411192556588

结果就是一顿乱入:

image-20200411192705754

​ 那么如果我们没有定义缺省 servlet 程序,访问静态页面时可以不用调用缺省 servlet 了吗?其实在 Tomcat 安装目录下的 conf/web.xml 文件中,注册了一个名称为 org.apache.catalina.servlets.DefaultServlet 的 servlet,并将这个 Servlet 设置为了缺省 Servlet,摘取如下:

<servlet>
        <servlet-name>default</servlet-name>
        <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
        <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>listings</param-name>
            <param-value>true</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
...
<!-- The mapping for the default servlet -->
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

​ 我尝试将 Tomcat 的这一段代码注释,然后运行:

场面顿时和谐:

Application Server was not connected before run configuration stop,reason:
javax.management.InstanceNotFoundException:Catalina:type=Server

​ 找不到实例异常,连接失败了,无论我本地是否再定义缺省都不能启动服务器了,看来老东家的东西不能乱碰。

  1. Servlet 的线程安全问题

    ​ 当多个客户端并发访问用一个 servlet 时,web 服务器会为每一个客户端的访问请求创建一个线程,并在这个线程上调用 servlet 的 service() 方法,因此 service 方法内如果访问了统一资源的话可能出现线程安全问题。

    ​ 线程安全问题时出现来多个线程同时操作一个数据源的情况下发生的,如下面的情况就不属于操作同一个数据源:

    @Override
    public void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
        int i = 0;
        i++;
    }
    

    ​ 多个线程并发调用 doGet() 时,i 不会存在线程安全的问题,因为 i 在 doget() 中,属于局部变量,每个线程都有一份,不存在共享,如果下面这种情况就会存在线程安全问题了:

    public class ServleTest extends HttpServlet {
        int i = 0;
        @Override
        public void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
            i++;
        }
    }
    

    ​ 此时 i 属于本 servlet 程序的全局变量,所有 doGet() 方法共享一个 i ,一旦发生线程阻塞,就会出现线程安全的情况。

    ​ 而且浏览器访问情况还特殊,使用加锁(synchronized)的方式实现线程同步,如果同时有100个人同时访问这个 servlet,那么这100个人就要按照先后顺序排队轮流访问。

    • Sun公司提供的解决办法:让 servlet 去实现一个 SingThreadModel(单线程模型) 接口,实现它的 servlet 程序,Servlet 引擎将以单线程模式来调用其 service 方法(已过时!!!)

    SingThreadModel 是一个标记接口(没有定义任何方法和常量的接口),仅用于给对象做标志,常见的有 Serializable(可序列化标志接口),Cloneable(对象可被克隆接口)

补充:Servlet API 与源码解析

自定义 Servlet 类继承树:

image-20200430104523463

需要注意 ServletConfig 接口中声明的的两个方法:

  • getServletContext():获取 Servlet 上下文对象

    通过获取到的 ServletContext 对象,可调用如下方法:

    • getContextPath():相对路径
    • getRealkPath():绝对路径
    • get/setAttribute():获取设置属性值
    • getInitParamter(String name):在当前 web 容器范围内,获取初始化参数(注意与下面一个区分)
  • getInitParameter(String name):在当前 Servlet 范围内,获取初始化参数(注意与上面一个区分)

  • Servlet 3.0 方式为注解方式(web.xml 配置方式为 Servlet 2.5,需要注意的是:注解只隶属于某一个具体的 servlet,因此无法为整个 web 容器设置初始化参数,任需要通过 web.xml 方式设置

7.3 ServletConfig 对象

  1. 在 web.xml 中配置 servlet 元素时,可以使用一个或多个 标签为 servlet 配置一些初始化参数

如:

<servlet>
    <servlet-name>InitParam</servlet-name>
    <servlet-class>com.zuh8.servlet.InitParam</servlet-class>
    <init-param>
      <param-name>username</param-name>
      <param-value>root</param-value>
    </init-param>
    <init-param>
      <param-name>password</param-name>
      <param-value>123456</param-value>
    </init-param>
    <init-param>
      <param-name>Charset</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
  </servlet>

标签使用 key-value 的形式配置参数

  1. 通过 ServletConfig 获取 servlet 的初始化参数

    ​ 当 servlet 配置了初始化参数后,web 容器在创建 servlet 实例对象时,会自动将这些初始化参数封装到 ServletConfig 对象中,并在调用 servlet 的 init() 方法时自动将 ServletConfig 对象传递给 servlet。进而,我们通过 ServletConfig 对象就能获取到当前 servlet 的初始化参数信息;

    示例:

    import javax.servlet.ServletConfig;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.Enumeration;
    
    /**
     * @author YH
     * @create 2020-04-11 21:30
     */
    public class InitParam extends HttpServlet {
        /**
         * 定义 ServletConfig 对象,用于接收配置的初始化参数
         */
        private ServletConfig config;
    
        /**
         * 当servlet配置了初始化参数后,web容器在创建servlet实例对象时,
         * 会自动将初始化配置封装到ServletConfig对象中,再调用servlet的
         * init()方法将ServletConfig对象传递给servlet
         * 所以我们通过ServletConfig就可以获取到此servlet的初始化参数信息
         */
        @Override
        public void init(ServletConfig config) throws ServletException {
            //将 ServletConfig 对象传递给此servlet程序
            this.config = config;
        }
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            //控制响应回送的数据编码
            resp.setCharacterEncoding("UTF-8");
            //设置浏览器解码所使用的编码
            resp.setHeader("Content-type","text/html;UTF-8");
    
            //查询指定 key(name) 值的参数
            String paramName = config.getInitParameter("username");
    
            //获取所有初始化参数的 name 集合(key值集合)
            Enumeration<String> initParameter = config.getInitParameterNames();
            //遍历读取并查询其 值(value)
            while(initParameter.hasMoreElements()){
                String s = initParameter.nextElement();
                resp.getWriter().print(s + ":" + config.getInitParameter(s) + "<br/>");
            }
            resp.getWriter().print("单独查询:<br/>");
            resp.getWriter().print("username:" + paramName );
        }
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            this.doGet(req, resp);
        }
    }
    

    image-20200412095012779

    有关编码问题此节点末介绍

    ​ 上面的方式难免有些繁琐,我们看一下它们的继承树:

    image-20200412104842228

    image-20200412104917878

    ​ 原来“爷爷类”里有直接获取 ServletConfig 对象和 ServletText 对象(下面就要用的)的方法,所以上面获取 ServletConfig 对象的操作我们可以这样:

    @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            //获取ServletConfig对象
            ServletConfig config = this.getServletConfig();
            ...
    

7.4 ServletContext 对象

​ Web 容器在启动时,会为每个 web 应用程序都创建一个 ServletContext 对象,它代表此 web 应用。

​ ServletConfig 对象里面维护了 ServletContext 对象的引用,所以可以通过 ServletConfig 对象(getServletContext() 方法)获取 ServletContext 对象。

​ 一个 web 应用中所有的 servlet 共享一个 ServletContext 对象,那么通过 ServletContext 对象就可以实现各个 servlet 之间的通讯;ServletConfig 也被称为 context 对象。

应用:

  1. 多个 servlet 通过 ServletContext 实现数据共享

代码:

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** ServletContext 数据共享演示:存入数据
 * @author YH
 * @create 2020-04-12 11:06
 */
public class ServletContextSet extends HttpServlet {
    /**
     * 获取 ServletContext 对象,存入属性值数据
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //向网页输出的内容包含中文,需要解决字符集的问题
        resp.setCharacterEncoding("utf-8");
        resp.setHeader("Content-type","text/html;charset=utf-8");

        ServletContext context = this.getServletContext();
        //Servlet中的属性值存储是以键值对(key-value)的形式
        context.setAttribute("username1","李雷");
        context.setAttribute("username2","韩梅梅");
        resp.getWriter().print("我向ServletContext中存入数据了!");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;

/** ServletContext 数据共享演示:获取数据
 * @author YH
 * @create 2020-04-12 11:16
 */
public class ServletContextGet extends HttpServlet {
    /**
     * 获取ServletContext对象,读取属性
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //向网页输出的内容包含中文,需要解决字符集的问题
        resp.setCharacterEncoding("utf-8");
        resp.setHeader("Content-type","text/html;charset=utf-8");

        ServletContext context = this.getServletContext();

        String username1 = (String)context.getAttribute("username1");
        String username2 = (String)context.getAttribute("username2");
        resp.getWriter().print(username1 + " and " + username2);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

先访问 用于获取属性的 servlet URL为 /get,结果:

image-20200412114443208

说明此时 ServletContext 对象中没有我们要获取的属性,我们再运行负责存入属性的 servlet 程序,URL为 /set,如下:

image-20200412115945444

再来访问 /get

image-20200412124244737

至此,一个 servlet 获取到另一个 servlet 添加的数据,实现的了交互

  1. 获取 web 应用的初始化参数

代码:

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author YH
 * @create 2020-04-12 13:54
 */
public class ContextParam extends HttpServlet {
    /**
     * 获取整个 web 工程的初始化参数
     * */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ServletContext context = this.getServletContext();
        String url = context.getInitParameter("url");
        resp.getWriter().print(url);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

web.xml 增加:

<!--作用于整个web工程的初始化参数-->
    <context-param>
        <param-name>url</param-name>
        <param-value>jdbc:mysql://localhost:3306/test</param-value>
    </context-param>

访问效果:

image-20200412140300911

  1. 用 ServletContext 实现请求转发

代码:

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author YH
 * @create 2020-04-12 14:13
 */
public class ServletContextDispatcher1 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //  扩展:指定getBytes() 的解码格式,进行解码,避免乱码问题
        resp.getOutputStream().write("---------------dis1---------------".getBytes("utf-8"));
        
        //现获取请求转发对象;再实现转发
        RequestDispatcher requestDispatcher = this.getServletContext().getRequestDispatcher("/dis2");
        requestDispatcher.forward(req,resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author YH
 * @create 2020-04-12 14:14
 */
public class ServletContextDispatcher2 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//        resp.setCharacterEncoding("utf-8");
//        resp.setHeader("content-type","text/html;charset=utf-8");

        //  扩展:指定getBytes() 的解码格式,进行解码,避免乱码问题
        resp.getOutputStream().write("---------------dis2---------------".getBytes("utf-8"));
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

访问 1 的URL ,显示的确实 2 的 servlet 内容:

image-20200412143921930

  1. 使用 ServletContext 读取资源文件

代码:

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.Properties;

/**
 * 使用 ServletContext 读取资源文件
 * @author YH
 * @create 2020-04-12 15:39
 */
public class ServletContextReader extends HttpServlet {
    /**
     * 读取本地数据要使用流
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setHeader("content-type","text/html;charset=utf-8");

        readConfig1(resp);
        resp.getWriter().print("<hr/>");
        readConfig2(resp);
        resp.getWriter().print("<hr/>");
        readConfig3(resp);
        resp.getWriter().print("<hr/>");
        readConfig4(resp);
    }

    /**
     *
     * @param resp
     * @throws IOException
     */
    private void readConfig1(HttpServletResponse resp) throws IOException {
        //获取文件路径(getRealPath()方法获取的是当前项目的路径,传递的参数是基于此路径的相对路径),即获取相对于整个web应用(ServletContext)的路径,并通过路径创建文件输入流
        String path = this.getServletContext().getRealPath("WEB-INF/classes/db1.properties");

        InputStream in = new FileInputStream(path);
        Properties prop = new Properties();
        //从输入流中读取属性列表(将属性文件中的数据载入Properties对象)
        prop.load(in);
        function(prop,resp);
    }

    /**
     *
     * @param resp
     */
    private void readConfig2(HttpServletResponse resp) throws IOException {
        //直接将文件资源转为输入流的方式
        InputStream in = this.getServletContext().getResourceAsStream("WEB-INF/classes/db2.properties");
        Properties prop = new Properties();
        prop.load(in);
        function(prop,resp);
    }

    /**
     *
     * @param resp
     */
    private void readConfig3(HttpServletResponse resp) throws IOException {
        InputStream in = this.getServletContext().getResourceAsStream("WEB-INF/classes/db3.properties");
        Properties prop = new Properties();
        prop.load(in);
        function(prop,resp);
    }

    /**
     * 读取src目录下webapp
     * @param resp
     */
    private void readConfig4(HttpServletResponse resp) throws IOException {
        InputStream in = this.getServletContext().getResourceAsStream("WEB-INF/classes/db4.properties");
        Properties prop = new Properties();
        prop.load(in);
        function(prop,resp);
    }

    /**
     * 抽取上面重复代码的部分声明的方法
     * @param prop
     * @param resp
     */
    private void function(Properties prop,HttpServletResponse resp) throws IOException {
        String driver = prop.getProperty("driver");
        String url = prop.getProperty("url");
        String username = prop.getProperty("username");
        String password = prop.getProperty("password");
        resp.getWriter().println("读取src目录下包下的db2属性文件");
        resp.getWriter().println(
                //设置数据显示的格式
                MessageFormat.format(
                        "driver={0} , url={1} , username={2} , password={3}",
                        driver,url,username,password));
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

效果:

image-20200412201104778

需要注意的是:使用Maven后(我是在Maven上的) web 应用的路径是基于target 目录下的 web项目目录(上面代码找路径坑了我半天,还是要细心点),如图:

image-20200412205036789

也就是说使用 getRealPath() 方法获取的是当前 web 工程的根路径,就是/serveltContext 获取它下面的资源相对于他来操作(带参的 getRealPath() 方法传递的参数是基于此路径的相对路径)

  1. 使用类加载器读取资源文件

    ​ 通过类获取类加载器,然后用类加载器调用 getResourceAsStream() 资源装换流方法,获取输入流对象,截取上面例子的部分代码来做修改:

    public class ServletContextReader extends HttpServlet {
        private void readConfig2(HttpServletResponse resp) throws IOException {
            //获取类加载器
            ClassLoader loader = ServletContextReader.class.getClassLoader();
            //用类加载器读取配置文件
            InputStream in = loader.getResourceAsStream("WEB-INF/classes/db2.properties");
            Properties prop = new Properties();
            prop.load(in);
            function(prop,resp);
        }
    }
    

类加载器读取资源文件不适合读取大文件,可能出现 jvm 内存溢出的情况。

记一个读取文件名的方法:

...
public void testMethod(){
    String filePath = this.getServletContext().getRealPath("WEB-INF/classes/db1.properties");
    String fileName = filePath.substring(filePath.lastIndex("\\") + 1);
    /**妙就妙在filePath.lastIndex("\\") + 1
    * 获取最后一个 \ 符号的索引 +1,正是文件名第一个 "d"的索引
    * 再substring从这个索引截取到末尾就是文件名了
    */
}
...

7.5 客户端缓存 Servlet 的输出

​ 对于不经常变化的数据,在servlet中可以为其设置合理的缓存时间值,以避免浏览器频繁向服务器发送请求,提升服务器的性能。例如:

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ServletDemo5 extends HttpServlet {
	public void doGet(HttpServletRequest request, HttpServletResponse response)
			 throws ServletException, IOException {
		 String data = "abcddfwerwesfasfsadf";
		 /**
		  * 设置数据合理的缓存时间值,以避免浏览器频繁向服务器发送请求,提升服务器的性能
		  * 这里是将数据的缓存时间设置为1天
		  */
		 response.setDateHeader("expires",System.currentTimeMillis() + 24 * 3600 * 1000);
		 response.getOutputStream().write(data.getBytes());
	}

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

		 this.doGet(request, response);
	}
}

八、HttpServletResponse 对象

image-20200413084004996

​ HTTPServletRequest 对象代表服务器的响应,封装了向客户端发送数据、响应头、响应状态码的方法。

查看 API 相关方法:

  1. 负责向客户端(浏览器)发送数据的方法

image-20200413084923288

分别用于返回响应中的二进制数据(字节流)和 正文数据(字符流),只能选择其一,同时使用会报错

  1. 负责向客户端(浏览器)发送响应头的方法

image-20200413085744436

image-20200413085838288

  1. 负责向客户端(浏览器)发送状态码的方法

image-20200413090040444

  1. 响应状态码的常量

    ​ 当要向客户端发送响应码时,应避免直接使用数字,而使用这些常量

    2xx:请求响应都成功(200)

    3xx:请求重定向(302)

    4xx:请求的资源不存在(404)

    5xx:服务器内部发生错误(如代码错误500、网关错误502)

状态码 200 对应的常量

image-20200413090210143

状态码 302 对应的常量

image-20200413090347171

状态码 404 对应的常量

image-20200413090324875

状态码 500 对应的常量

image-20200413090443458

8.1 Servlet 中文乱码问题

​ 查看我的另一篇博文: Servlet 中文乱码问题解析及详细解决方法

8.2 HTTPServletResponse常见应用

  1. 使用 OutputStream 向浏览器输出中文数据
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** OutputStream响应输出字符编码问题
 * @author YH
 * @create 2020-04-13 9:39
 */
public class ServletOutputStreamTest extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        /**
         * 使用字节输出流输出中文字符,需要一个字符转成字节的过程
         * getBytes()方法将data字符串解码为字节,默认按照本地编码格式进行解码(中系统默认GBK)
         * 可传递参数指定解码类型:utf-8,此时就是按照UTF-8字符集进行解码
         * 那么此时流中的数据时UTF-8解码后的数据,响应将返回这些数据,就需要告诉浏览器它改用什么
         * 编码格式来打开这些数据,怎么告诉浏览器呢? 这就需要设置相关响应头了
         * setHeader()方法第一个参数指定响应头,第二个参数设置此响应头的值(设置了文本类型并指定的打开此文本的编码格式)
         * 还有一个
         */
        resp.setHeader("Content-type","text/html;charset=utf-8");
        ServletOutputStream outputStream = resp.getOutputStream();

        String data = "中国";
        outputStream.write(data.getBytes("utf-8"));
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

效果:

image-20200413102447802

  1. 使用 PrintWriter 向浏览器输出中文数据
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @author YH
 * @create 2020-04-13 15:13
 */
public class PrintWriterTest extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String data = "中国";
        //关键:涉及中文就涉及编码问题
        //设置ContentType响应头信息,告诉浏览器要用utf-8字符集解码
        resp.setContentType("text/html;charset=utf-8");

        //方式一:设置字符以UTF-8的格式回送个浏览器
//        resp.setCharacterEncoding("UTF-8");

        //方式二:通过向网页传递 HTML元素标签设置字符集
        resp.getWriter().write("<mete http-equiv='content-type' content='text/html;charset=utf-8'/>");

        //输出到浏览器
        PrintWriter writer = resp.getWriter();
        writer.write(data);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

image-20200413152302470

​ 以及方式二向浏览器传递的 HTML 元素代码:

响应体内容:

image-20200413152926174

页面 HTML 代码:

image-20200413153013081

OutputStream 可以输出任何数据;PrintWriter 只能输出字符,但也省去了字符转字节数组那一步,使用方便些

  1. 开发过程中,无论使用何种输出方式,浏览器能够显示出来的只能是字符串形式的数据,也就是说直接输出数字、字符浏览器都不能显示出来。

  2. 文件下载

    实现文件下载的思路:

      1.获取要下载的文件的绝对路径

      2.获取要下载的文件名

      3.设置content-disposition响应头控制浏览器以下载的形式打开文件

      4.获取要下载的文件输入流

      5.创建数据缓冲区

      6.通过response对象获取OutputStream流

      7.将FileInputStream流写入到buffer缓冲区

      8.使用OutputStream将缓冲区的数据输出到客户端浏览器

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLEncoder;

/**
 * @author YH
 * @create 2020-04-13 16:00
 */
public class WebDownload extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.获取资源的绝对路径(getRealPath()获取的是项目根目录,传递参数就是将参数拼接到获取的根目录后)
        String filePath = this.getServletContext().getRealPath("阿里前端工程师必读手.pdf");
        System.out.println("==" + filePath + "==");
        //2.获取文件名
        //一个绝妙的获取文件名的方法,filePath.lastIndexOf("\\") + 1 直接获取到文件名的首索引
        //URLEncoder.encode设置文件名的编码格式(解决中文文件名乱码)
        String fileName = filePath.substring(filePath.lastIndexOf("\\") + 1);
        //3.设置content-disposition响应头以下载的形式打开文件
        resp.setHeader("content-disposition","attachment;filename=" + URLEncoder.encode(fileName,"UTF-8"));
        //4.获取要下载的文件输入流
        FileInputStream fileInputStream = new FileInputStream(filePath);
        //5.创建缓冲区
        int len = 0;
        byte[] buffer = new byte[1024];
        //6.通过响应对象获取 OutputStream 流
        ServletOutputStream outputStream = resp.getOutputStream();
        //7.将文件输入流中的数据写入缓冲区
        while((len = fileInputStream.read(buffer)) > 0){
            //8.将数据写入响应输出流
            outputStream.write(buffer,0,len);
        }
        //关闭资源
        fileInputStream.close();
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

image-20200413173053100

注意:

在编写下载文件功能时,要使用 OutputStream 流,避免使用 PrintWriter 流,因为 OutputStream 流是字节流,可以处理任意类型的数据,而 PrintWriter 流是字符流,只能处理字符数据,如果用字符流处理字节数据,会导致数据丢失

5.生成随机图片验证码

​ 生成图片需要使用一个图片类:BufferedImage 类

代码:

![GIF](../GIF.gifimport javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;

/** 生成随机验证码随机
 * @author YH
 * @create 2020-04-13 17:43
 */
public class RandomBufferedImage extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //设置响应头,然浏览器 3 秒刷新一次
        resp.setHeader("refresh","3");

        //1.在内存中创建一张图片
        BufferedImage image = new BufferedImage(80, 30, BufferedImage.TYPE_INT_RGB);
        //2.得到图片
//        image.getGraphics();
        Graphics2D g = image.createGraphics();
        g.setColor(Color.WHITE);//设置背景图片颜色
        g.fillRect(0,0,80,20);//填充背景颜色
        //3.向图片上写数据
        g.setColor(Color.BLUE);//设置图片上字体的颜色
        g.setFont(new Font(null,Font.BOLD,20));
        g.drawString(makeNum(),0,20);
        //4.设置响应头,控制浏览器以图片的方式打开
        resp.setHeader("content-type","image/jpeg");
        //5.设置响应头,控制浏览器不缓存图片
        resp.setDateHeader("expries", -1);
        resp.setHeader("Cache-Control", "no-cache");
        resp.setHeader("Pragma", "no-cache");
        //6.将图片写给浏览器
        ImageIO.write(image,"jpg",resp.getOutputStream());
    }

    /**
     * 生成随机数
     * @return
     */
    private String makeNum(){
        Random random = new Random();
        String ranNum = random.nextInt(9999999) + "";
        StringBuffer sb = new StringBuffer();
        //如果位数不足 7 位 补零
        for (int i = 0; i < 7 - ranNum.length(); i++) {
            sb.append("0");
        }
        ranNum += sb.toString();

        return ranNum;
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

GIF

  1. 其它常用的响应头设置

    • 设置 HTTP响应头控制浏览器禁止换出当前文档内容
    response.setDateHeader("expries", -1);
    response.setHeader("Cache-Control", "no-cache");
    response.setHeader("Pragma", "no-cache");
    
    • 设置 HTTP 响应头控制浏览器定时刷新网页 (refresh)
    response.setHeader("refresh", "3");//设置refresh响应头控制浏览器每隔3秒钟刷新一次
    
    • 通过 response 实现请求重定向:一个 web 资源受到请求后,通知客户端去访问另外一个 web 资源,这称之为请求重定向

      应用场景:用户登录,用户访问登录页面,登录成功后跳转到某个页面,就是一个请求重定向的过程

      实现方式:response.sendRedircet(String location)

      sendRedirect() 内部实现原理:使用 response 设置 302 状态码和设置 location 响应头实现重定向

    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    /** 设置请求重定向的两种方式
     * @author YH
     * @create 2020-04-13 20:09
     */
    public class SendRedirect extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            //1.调用 sendRedirect 方法实现请求重定向
            resp.sendRedirect("ranImg");
    
            //2.使用 response 设置 Location 响应头并设置状态码
    //        resp.setHeader("Location","download");
    //        resp.setStatus(HttpServletResponse.SC_FOUND);
        }
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            this.doGet(req, resp);
        }
    }
    

    GIF

8.3 Web 工程中 URL 地址写法

​ 在 javaweb 中正斜杠 "/":如果 "/" 是给服务器用的,则代表当前的 web 工程,如果 "/" 是给浏览器用的,则代表 webapps 目录

  • "/" 代表当前 web 工程的常见应用:
  1. ServletContext.getRealPath(String path) 获取指定虚拟路径(资源访问路径)的在服务器文件系统上的绝对路径
/**
* ServletContext.getRealPath("/download/1.JPG")是用来获取服务器上的某个资源,
* 那么这个"/"就是给服务器用的,"/"此时代表的就是web工程
 * ServletContext.getRealPath("/download/1.JPG")表示的就是读取web工程下的download文件夹中的1.JPG这个资源
* 只要明白了"/"代表的具体含义,就可以很快写出要访问的web资源的绝对路径
*/
this.getServletContext().getRealPath("/download/1.JPG");
  1. 在服务器端 forward 到其他页面
/**
* 2.forward
* 客户端请求某个web资源,服务器跳转到另外一个web资源,这个forward也是给服务器用的,
* 那么这个"/"就是给服务器用的,所以此时"/"代表的就是web工程
*/
this.getServletContext().getRequestDispatcher("/index.jsp").forward(request, response);
  • "/" 代表 webapps 目录的常见应用:
  1. 使用 sendRedirect 实现请求重定向
response.sendRedirect("/JavaWeb_HttpServlet/index.jsp");

  服务器发送一个 URL 地址给浏览器,浏览器拿到 URL 地址之后,再去请求服务器,所以这个 "/" 是给浏览器使用的,此时 "/" 代表的就是 webapps 目录,"/JavaWeb_HttpServlet/index.jsp" 这个地址指的就是 "webapps\JavaWeb_HttpServlet\index.jsp"

  response.sendRedirect("/ 项目名称 / 文件夹目录 / 页面"); 这种写法是将项目名称写死在程序中的做法,不灵活,万一哪天项目名称变了,此时就得改程序,所以推荐使用下面的灵活写法:

response.sendRedirect("/JavaWeb_HttpServlet/index.jsp");

  这种写法改成

response.sendRedirect(request.getContextPath()+"/index.jsp");

  request.getContextPath() 获取到的内容就是 "/JavaWeb_HttpServlet,这样就比较灵活了,使用 request.getContextPath() 代替 "/ 项目名称",推荐使用这种方式,灵活方便!

  1. 使用超链接跳转
<a href="/JavaWeb_HttpServlet/index.jsp">跳转到首页</a>

  这是客户端浏览器使用的超链接跳转,这个 "/" 是给浏览器使用的,此时 "/" 代表的就是 webapps 目录。

  使用超链接访问 web 资源,绝对路径的写法推荐使用下面的写法改进:

<a href="${pageContext.request.contextPath}/index.jsp">跳转到首页</a>

  这样就可以避免在路径中出现项目的名称,使用 ${pageContext.request.contextPath} 取代 "/JavaWeb_HttpServlet"

  1. Form 表单提交

对于 form 表单提交中 action 属性绝对路径的写法,也推荐使用如下的方式改进:

<form action="${pageContext.request.contextPath}/servlet/CheckServlet" method="post">
         <input type="submit" value="提交">
</form>

综合范例:

<%@page language="java" import="java.util.*" pageEncoding="UTF-8" %>
<!DOCTYPE HTML PUBLIC"-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
    <title>"/"代表webapps目录的常见应用场景</title>
    <%--使用绝对路径的方式引用js脚本--%>
    <script type="text/javascript" src="${pageContext.request.contextPath}/js/index.js"></script>
    <%--${pageContext.request.contextPath}与request.getContextPath()写法是得到的效果是一样的--%>
    <script type="text/javascript" src="<%=request.getContextPath()%>/js/login.js"></script>
    <%--使用绝对路径的方式引用css样式--%>
    <link rel="stylesheet" href="${pageContext.request.contextPath}/css/index.css" type="text/css"/>
</head>
<body>
<%--form表单提交--%>
<form action="${pageContext.request.contextPath}/servlet/CheckServlet" method="post">
    <input type="submit" value="提交">
</form>
<%--超链接跳转页面--%>
<a href="${pageContext.request.contextPath}/index.jsp">跳转到首页</a>
</body>
</html>

8.4 response 细节扩展

​ Servlet 程序向 ServletOutputStream 或 PrintWriter 对象中写入的数据将被 Servlet 引擎从 response 里面获取,Servlet 引擎将这些数据当做响应消息的正文,然后再与响应状态行和各响应头组合后输出到客户端。

​ Servlet 程序的 service 方法结束后,Servlet 引擎将检查 getWriter 或 getOutputStream 方法返回的输出流是否已经调用过 close 方法,如果没有,Servlet 引擎将调用 close 方法关闭该输出流。

Servlet 开发常见问题

Servlet 中文乱码问题解析及详细解决方法

8.5 通过 Servlet 生成验证码图片

  1. 创建一个 DrawImage servlet ,用来生成验证码图片
import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;

/** 生成多种类型的内容验证码
 * @author YH
 * @create 2020-04-14 7:30
 */
public class DrawImage extends HttpServlet {
    private static final long serialVersionUID = 342349254353430483L;
    //图片宽
    public static final int WIDTH = 120;
    //图片高
    public static final int HEIGHT = 30;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //接收客户端传递的createTypeFlag标识(生成那种验证码)
        String createTypeFlag = req.getParameter("createTypeFlag");

        //1.内存中创建一张图片
        BufferedImage image = new BufferedImage(WIDTH, HEIGHT,BufferedImage.TYPE_INT_RGB);
        //2.得到图片
        Graphics2D g = image.createGraphics();
        //3.设置图片的背景色
        setBackGround(g);
        //4.设置图片的边框
        setBorder(g);
        //5.在图片上画干扰线
        drawRandomLine(g);
        //6.写在图片上的随机数
        //生成中文验证码
//        String randomChar = drawRandomNum(g,"ch");
//        //生成数字字母组合验证码
//        String randomChar = drawRandomNum(g,"nl");
//        //生成数字验证码
//        String randomChar = drawRandomNum(g,"n");
//        //生成字母验证码
//        String randomChar = drawRandomNum(g,"l");
        //客户端传递参数选择验证方式
        String randomChar = drawRandomNum(g,createTypeFlag);
        //7.将随机数存在session中
        req.getSession().setAttribute("checkcode",randomChar);
        //8.设置响应头通知浏览器以图片的形式打开
        resp.setContentType("image/jpeg");
        //9.设置响应头通知浏览器不要缓存
        resp.setDateHeader("expries",-1);
        resp.setHeader("Cach-Contro1","no-cache");
        resp.setHeader("Pragma","no-cache");
        //10.将图片写给浏览器
        ImageIO.write(image,"jpg",resp.getOutputStream());
    }

    /**
     * 设置背景颜色
     * @param g
     */
    private void setBackGround(Graphics2D g){
        //设置颜色
        g.setColor(Color.white);
        g.fillRect(0,0,WIDTH,HEIGHT);
    }

    /**
     * 设置边框颜色
     * @param g
     */
    private void setBorder(Graphics2D g){
        //设置颜色
        g.setColor(Color.BLUE);
        //设置边框宽度,要绘制的矩形宽高减掉边框宽度,防止撑开原盒子(前端内容)
        g.drawRect(1,1,WIDTH - 2,HEIGHT -2);
    }

    /**
     * 在图片上画随机数
     * @param g
     */
    private void drawRandomLine(Graphics2D g){
        //设置颜色
        g.setColor(Color.GREEN);
        //设置线条数量
        int lineNum = 5;
        //画线条
        for (int i = 0; i < lineNum; i++) {
            //确定线条起始和结束坐标(在宽高的范围内)
            int x1 = new Random().nextInt(WIDTH);
            int y1 = new Random().nextInt(HEIGHT);
            int x2 = new Random().nextInt(WIDTH);
            int y2 = new Random().nextInt(HEIGHT);
            g.drawRect(x1,y1,x2,y2);
        }
    }


    private String drawRandomNum(Graphics2D g,String... createTypeFlag){
        //设置颜色
        g.setColor(Color.RED);
        //设置字体
        g.setFont(new Font("微软雅黑",Font.BOLD,20));
        //常用的汉字
        String baseChineseChar = "\u7684\u4e00\u4e86\u662f\u6211\u4e0d\u5728\u4eba\u4eec\u6709\u6765\u4ed6\u8fd9\u4e0a\u7740\u4e2a\u5730\u5230\u5927\u91cc\u8bf4\u5c31\u53bb\u5b50\u5f97\u4e5f\u548c\u90a3\u8981\u4e0b\u770b\u5929\u65f6\u8fc7\u51fa\u5c0f\u4e48\u8d77\u4f60\u90fd\u628a\u597d\u8fd8\u591a\u6ca1\u4e3a\u53c8\u53ef\u5bb6\u5b66\u53ea\u4ee5\u4e3b\u4f1a\u6837\u5e74\u60f3\u751f\u540c\u8001\u4e2d\u5341\u4ece\u81ea\u9762\u524d\u5934\u9053\u5b83\u540e\u7136\u8d70\u5f88\u50cf\u89c1\u4e24\u7528\u5979\u56fd\u52a8\u8fdb\u6210\u56de\u4ec0\u8fb9\u4f5c\u5bf9\u5f00\u800c\u5df1\u4e9b\u73b0\u5c71\u6c11\u5019\u7ecf\u53d1\u5de5\u5411\u4e8b\u547d\u7ed9\u957f\u6c34\u51e0\u4e49\u4e09\u58f0\u4e8e\u9ad8\u624b\u77e5\u7406\u773c\u5fd7\u70b9\u5fc3\u6218\u4e8c\u95ee\u4f46\u8eab\u65b9\u5b9e\u5403\u505a\u53eb\u5f53\u4f4f\u542c\u9769\u6253\u5462\u771f\u5168\u624d\u56db\u5df2\u6240\u654c\u4e4b\u6700\u5149\u4ea7\u60c5\u8def\u5206\u603b\u6761\u767d\u8bdd\u4e1c\u5e2d\u6b21\u4eb2\u5982\u88ab\u82b1\u53e3\u653e\u513f\u5e38\u6c14\u4e94\u7b2c\u4f7f\u5199\u519b\u5427\u6587\u8fd0\u518d\u679c\u600e\u5b9a\u8bb8\u5feb\u660e\u884c\u56e0\u522b\u98de\u5916\u6811\u7269\u6d3b\u90e8\u95e8\u65e0\u5f80\u8239\u671b\u65b0\u5e26\u961f\u5148\u529b\u5b8c\u5374\u7ad9\u4ee3\u5458\u673a\u66f4\u4e5d\u60a8\u6bcf\u98ce\u7ea7\u8ddf\u7b11\u554a\u5b69\u4e07\u5c11\u76f4\u610f\u591c\u6bd4\u9636\u8fde\u8f66\u91cd\u4fbf\u6597\u9a6c\u54ea\u5316\u592a\u6307\u53d8\u793e\u4f3c\u58eb\u8005\u5e72\u77f3\u6ee1\u65e5\u51b3\u767e\u539f\u62ff\u7fa4\u7a76\u5404\u516d\u672c\u601d\u89e3\u7acb\u6cb3\u6751\u516b\u96be\u65e9\u8bba\u5417\u6839\u5171\u8ba9\u76f8\u7814\u4eca\u5176\u4e66\u5750\u63a5\u5e94\u5173\u4fe1\u89c9\u6b65\u53cd\u5904\u8bb0\u5c06\u5343\u627e\u4e89\u9886\u6216\u5e08\u7ed3\u5757\u8dd1\u8c01\u8349\u8d8a\u5b57\u52a0\u811a\u7d27\u7231\u7b49\u4e60\u9635\u6015\u6708\u9752\u534a\u706b\u6cd5\u9898\u5efa\u8d76\u4f4d\u5531\u6d77\u4e03\u5973\u4efb\u4ef6\u611f\u51c6\u5f20\u56e2\u5c4b\u79bb\u8272\u8138\u7247\u79d1\u5012\u775b\u5229\u4e16\u521a\u4e14\u7531\u9001\u5207\u661f\u5bfc\u665a\u8868\u591f\u6574\u8ba4\u54cd\u96ea\u6d41\u672a\u573a\u8be5\u5e76\u5e95\u6df1\u523b\u5e73\u4f1f\u5fd9\u63d0\u786e\u8fd1\u4eae\u8f7b\u8bb2\u519c\u53e4\u9ed1\u544a\u754c\u62c9\u540d\u5440\u571f\u6e05\u9633\u7167\u529e\u53f2\u6539\u5386\u8f6c\u753b\u9020\u5634\u6b64\u6cbb\u5317\u5fc5\u670d\u96e8\u7a7f\u5185\u8bc6\u9a8c\u4f20\u4e1a\u83dc\u722c\u7761\u5174\u5f62\u91cf\u54b1\u89c2\u82e6\u4f53\u4f17\u901a\u51b2\u5408\u7834\u53cb\u5ea6\u672f\u996d\u516c\u65c1\u623f\u6781\u5357\u67aa\u8bfb\u6c99\u5c81\u7ebf\u91ce\u575a\u7a7a\u6536\u7b97\u81f3\u653f\u57ce\u52b3\u843d\u94b1\u7279\u56f4\u5f1f\u80dc\u6559\u70ed\u5c55\u5305\u6b4c\u7c7b\u6e10\u5f3a\u6570\u4e61\u547c\u6027\u97f3\u7b54\u54e5\u9645\u65e7\u795e\u5ea7\u7ae0\u5e2e\u5566\u53d7\u7cfb\u4ee4\u8df3\u975e\u4f55\u725b\u53d6\u5165\u5cb8\u6562\u6389\u5ffd\u79cd\u88c5\u9876\u6025\u6797\u505c\u606f\u53e5\u533a\u8863\u822c\u62a5\u53f6\u538b\u6162\u53d4\u80cc\u7ec6";
        //数字和字母的组合
        String baseNumLetter = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        //数字的组合
        String baseNum = "0123456789";
        //字母的组合
        String baseLetter = "ABCDEFGHIGKLMNOPQRSTUVWXYZ";
        //createTypeFlag可变参数底层实现为数组,所以createTypeFlag[0]为第一个参数索引
        String baseChar = baseNumLetter;
        if(createTypeFlag.length > 0 && createTypeFlag[0] != null ) {
            switch (createTypeFlag[0]) {
                case "ch":
                    baseChar = baseChineseChar;
                    break;
                case "nl":
                    baseChar = baseNumLetter;
                    break;
                case "n":
                    baseChar = baseNum;
                    break;
                case "l":
                    baseChar = baseLetter;
                    break;
            }
        }
        return createRandomChar(g,baseChar);
    }

    /**
     * 创建随机字符
     * @param g
     * @param baseChar
     */
    private String createRandomChar(Graphics2D g,String baseChar){
        StringBuffer sb = new StringBuffer();
        // x 坐标的值
        int x = 5;
        String ch = "";
        //控制字数
        for (int i = 0; i < 4; i++) {
            //设置字体旋转角度
            int degree = new Random().nextInt() % 30;
            ch = baseChar.charAt(new Random().nextInt(baseChar.length())) + "";
            sb.append(ch);
            //正向角度
            g.rotate(degree * Math.PI / 180,x,20);
            //写入内容
            g.drawString(ch,x,20);
            //反向角度
            g.rotate(-degree * Math.PI / 180,x,20);
            x += 30;
        }
        return sb.toString();
    }
}

显示各种形式的验证码:

image-20200414094831222

  1. 在 from 表单中使用验证码图片
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
 <head>
   <title>在Form表单中使用验证码</title>
   <script type="text/javascript">
   //刷新验证码
   function changeImg(){
       document.getElementById("randomImage").src="${pageContext.request.contextPath}/drawImage?"+Math.random();
    }
    </script>
  </head>
  <body>
        <form action="${pageContext.request.contextPath}/check" method="post">
            验证码:<input type="text" name="validateCode"/>
            <img alt="验证码看不清,换一张" src="${pageContext.request.contextPath}/drawImage" id="randomImage" onclick="changeImg()">
            <a href="javascript:void(0)" onclick="changeImg()">看不清,换一张</a>
            <br/>
            <input type="submit" value="提交">
        </form>
  </body>
</html>

效果如下:

GIF

  1. 服务器端对 from 表单提交上来的验证码处理
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author YH
 * @create 2020-04-14 11:09
 */
public class CheckServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //获取浏览器提交的用户输入
        String createTypeFlag = req.getParameter("validateCode");
        //获取 DrawImage servlet 生成的验证码内容
        String random = (String)req.getSession().getAttribute("checkcode");
//        this.getServletContext().setAttribute("createTypeFlag",createTypeFlag);
        System.out.println(random.equals(createTypeFlag) ? "输入正确" : "输出错误");
    }
}

GIF

九、HttpServletRequest 对象

​ HttpServletRequest 对象代表客户端的请求,当客户端通过 HTTP 协议访问服务器时,HTTP 请求头中的所有信息都封装在这个对象中,通过这个对象提供的方法可以获得所有的请求信息。

9.1 常用方法

  1. 获取的客户端信息
    • getRequestURL() 返回客户端能发出请求时完整的 RUL
    • getRequestURI() 返回请求行中资源名部分
    • getQueryString() 返回请求行中参数部分
    • getPathInfo() 返回请求 URL 中的额外路径信息的路径信息;指 URL 中位于 servlet 的路径之后和查询参数之前的内容,以"/"开头
    • getRemoteAddr() 返回发出请求的客户机的 IP 地址
    • getRemoteHost() 返回发出请求的客户机的完整主机名
    • getRemotePort() 返回返回客户机所使用的的网络端口号
    • getLocalAddr() 返回 WEB 服务器的 IP 地址
    • getLocalName() 返回 WEB 服务器的主机名

示例:

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @author YH
 * @create 2020-04-14 14:34
 */
public class RequestMethod extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //因为浏览器只能显示字符串类型的数据,所以将获取的所有值都转为字符串
        //获取客户端请求时的完整URL
        String url = req.getRequestURL().toString();
        //获取请求行中资源名部分
        String uri = req.getRequestURI();
        //获取请求行中参数部分
        String param = req.getQueryString();
        //获取请求行中servlet路径之后,参数之前的额外内容
        String pathInfo = req.getPathInfo();
        //获取获取客户机ip
        String address = req.getRemoteAddr();
        //获取客户机主机名
        String host = req.getRemoteHost();
        //获取服务器网络端口
        int remotePort = req.getRemotePort();
//        String remotePort = String.valueOf(req.getRemotePort());
        //web服务器ip
        String localAddr = req.getLocalAddr();
        //web服务器的主机名
        String localName = req.getLocalName();
        //获取请求的方式
        String method = req.getMethod();
        //返回用户的登陆信息
        String user = req.getRemoteUser();

        //设置数据以UTF-8的编码输出到客户端浏览器
        resp.setCharacterEncoding("utf-8");
        //设置响应头,控制浏览器以UTF-8的编码解析收到的响应数据
        resp.setHeader("content-type","text/html;charset=utf-8");

        PrintWriter out = resp.getWriter();

        out.write("获取得到的用户信息如下:");
        out.write("<hr/>");

        out.write("请求的URL地址:" + url);
        out.write("<br/>");
        out.write("请求的资源:" + uri);
        out.write("请求的URL地址中附带的参数" + param);
        out.write("<br/>");
        out.write("请求的额外部分:" + pathInfo);
        out.write("<br/>");
        out.write("客户机ip:" + address);
        out.write("<br/>");
        out.write("客户机主机名:" + host);
        out.write("<br/>");
        out.write("网络端口:" + remotePort);
        out.write("<br/>");
        out.write("web服务器ip:" + localAddr);
        out.write("<br/>");
        out.write("web服务器主机名:" + localName);
        out.write("<br/>");
        out.write("客户端请求方式:" + method);
        out.write("<br/>");
        out.write("用户登录信息:" + user);

    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

效果:

image-20200414154706899

  1. 获取客户端请求头

    • getHeader(String name) 以 String 对象的形式返回指定请求头的值

    • getHeaders(String name) 以 String 对象的 Enumeration 的形式返回指定请求头的所有值(有些请求头参数较多,且可以通过其他请求头指定其中的某一参数)

    • getHeaderNames() 获取所有请求头的名字

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;

/**
 * @author YH
 * @create 2020-04-14 15:56
 */
public class RequestLineMethod extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //设置响应头,控制浏览器以UTF-8的编码解析响应回送的数据
        resp.setHeader("content-type","text/html;charset=utf-8");
        //设置响应数据的编码方式为UTF-8
        resp.setCharacterEncoding("utf-8");

        PrintWriter out = resp.getWriter();

        //指定响应头的 name 获取响应头信息
        String content_type = req.getHeader("Accept-Language");
        out.write("获取到的数据为:");
        out.write("<hr/>");
        out.write("Content-type:" + content_type);
        out.write("<br/>");

        //指定响应头的的 name 获取它所有的信息
        Enumeration<String> headerList = req.getHeaders("Accept");
        while(headerList.hasMoreElements()){
            String headName = headerList.nextElement();
            //根据请求头的名字获取对应的请求头的值
            out.write( "Accept:" + ":" + headName);
            out.write("<br/>");
        }

        //获取请求中所有的响应头的name
        Enumeration<String> headerNames = req.getHeaderNames();
            while(headerNames.hasMoreElements()){
                String headNameList = headerNames.nextElement();
                //根据请求头的名字获取对应的请求头的值
                String headValue = req.getHeader(headNameList);
                out.write("<br/>");
                out.write(headNameList + ":" + headValue);
            }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

效果:

image-20200414183953455

  1. 获得客户机请求参数(客户端提交的数据)
    • getParameter(String) 返回指定请求参数的值
    • getParameterValues(String name) 返回包含给定请求参数拥有的所有值的 String 对象数组(如多选框的值)
    • getParametereNames() 返回所有请求参数的集合
    • getParameterMap() 以键值对的形式返回请求参数信息(编写框架时常用)
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.MessageFormat;
import java.util.Map;

/**
 * @author YH
 * @create 2020-04-14 19:18
 */
public class FromSubmit extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //设置客户端发送请求的数据编码
        req.setCharacterEncoding("utf-8");
        //设置服务器端以utf-8的编码输出数据
        resp.setCharacterEncoding("utf-8");
        //设置响应头,控制浏览器以UTF-8编码解析数据
        resp.setContentType("text/html;charset=utf-8");

        PrintWriter out = resp.getWriter();
//第一种方式:
//        String userId = req.getParameter("userId");
//        String username = req.getParameter("username");
//        String password = req.getParameter("password");
//        String sex = req.getParameter("sex");
//        String department = req.getParameter("department");
////        String hobby = req.getParameter("hobby");
//        String[] hobby = req.getParameterValues("hobby");
//        String textarea = req.getParameter("textarea");
//        String hiddenFile = req.getParameter("hiddenFile");
//        String htmlStr = "<table>" +
//                "<tr><td>填写的编号:</td><td>{0}</td></tr>" +
//                "<tr><td>填写的用户名:</td><td>{1}</td></tr>" +
//                "<tr><td>填写的密码:</td><td>{2}</td></tr>" +
//                "<tr><td>选中的性别:</td><td>{3}</td></tr>" +
//                "<tr><td>选中的部门:</td><td>{4}</td></tr>" +
//                "<tr><td>选中的兴趣:</td><td>{5}</td></tr>" +
//                "<tr><td>填写的说明:</td><td>{6}</td></tr>" +
//                "<tr><td>隐藏域的内容:</td><td>{7}</td></tr>" +
//                "</table>";
//        String hobbyStr = "";
//        //方式出现数组空指针的技巧
//        for (int i = 0; hobby != null && i < hobby.length; i++) {
//            if(i == hobby.length - 1){
//                hobbyStr += hobby[i];
//            }else{
//                hobbyStr += hobby[i] + ",";
//            }
//        }
//        htmlStr = MessageFormat.format(htmlStr,userId,username,password,sex,department,hobbyStr,textarea,hiddenFile);
//
//        out.write(htmlStr);

//第二种方式:
//        Enumeration<String> names = req.getParameterNames();
//        while(names.hasMoreElements()){
//            String s = names.nextElement();
//            String parameter = req.getParameter(s);
//            out.write(s + ":" + parameter + "<br/>");
//        }

// 第三种方式
        Map<String, String[]> parameterMap = req.getParameterMap();
        for(Map.Entry<String,String[]> entry : parameterMap.entrySet()){
            String paramName = entry.getKey();
            String paramValue = "";
            String[] paraValueArr = entry.getValue();
            for (int i = 0; paraValueArr != null && i < paraValueArr.length; i++) {
                if(i == paraValueArr.length - 1){
                    //最后一个不加 , 号
                    paramValue += paraValueArr[i];
                }else{
                    //不是最后一个加 , 号
                    paramValue += paraValueArr[i] + ",";
                }
            }
            out.write(MessageFormat.format("{0} : {1}<br/>",paramName,paramValue));
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

image-20200415082515156

第一种方式 使用 getParamter() 和 getParamterValues():

使用getParamter()读取多选框:image-20200415083005305

使用getParamterValues()读取多选框:image-20200415082758444

第二种方式 使用 getParamterNames():

image-20200415082549897

第三种方式 使用 getParamterMap():

image-20200416082528744

补充:之所以会产生乱码,就是因为服务器和客户端沟通的编码不一致造成的,因此解决的办法是:在*客户端*和服务器之间设置一个统一的编码,之后就按照此编码进行数据的传输和接收。

请求乱码问题详见:https://www.cnblogs.com/csyh/p/12691421.html

9.2 Request 对象是实现请求转发

​ 指一个 web 资源收到客户端请求后,通知服务器去调用另外一个 web 资源进行处理

​ 应用场景:MVC 设计模式

  1. 通过 ServletContext 的 getRequestDispatcher(String path) 方法,获取一个 RequestDispatcher 对象,调用这个对象的 forward 方法可以实现请求转发
RequestDispatcher reqDispatcher =this.getServletContext().getRequestDispatcher("/test.jsp");
reqDispatcher.forward(request, response);
  1. 通过 request 对象提供的 getRequestDispatche(String path) 方法,获取一个 RequestDispatcher 对象,调用这个对象的 forward 方法实现请求转发
request.getRequestDispatcher("/test.jsp").forward(request, response);

​ request 对象同时也是一个域对象(Map 容器),开发人员通过 request 对象在实现转发时,把数据通过 request 对象带给其他 web 资源处理

示例:在 request 对象中存入数据,转发页面中获取此数据

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** 将数据存入 request 对象,把 request 对象当做一个 Map 容器来使用
 * @author YH
 * @create 2020-04-16 10:36
 */
public class RequestMapData extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //向 request 对象中存入数据
        req.setAttribute("data","requestData");
        //当浏览器访问此 servlet 后,将请求转发至前端页面
        req.getRequestDispatcher("req.jsp").forward(req,resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    使用普通方式取出存储在 request 对象中的数据
    <h3><%=(String)request.getAttribute("data")%></h3>
    使用EL表达式取出存储在request对象中的数据
    <h3>${data}</h3>
</body>
</html>

9.3 请求转发和请求重定向的区别

​ 一个 web 资源收到客户端请求后,通知服务器去调用另一个 web 资源进行处理,为请求转发(307)

​ 一个 web 资源收到客户端请求后,通知浏览器去访问另一个 web 资源进行处理,为请求重定向(302)

十、Cookie 会话管理

​ 简单理解:用户开一个浏览器,点击多个超链接访问服务器的多个资源,然后关闭浏览器,这个整个过程称为一个会话

​ 有状态会话:一个同学来过教室,下次再来教室,我们会知道这个同学曾经来过

10.1 会话过程中要解决的问题?

​ 每个用户在使用浏览器与服务器进行会话的过程中,不可避免各自会产生一些数据,程序要想办法为每个用户保存这些数据

10.2 保存会话的两种技术

​ Cookie 是客户端技术,是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通俗点说:程序把每个用户的数据以 cookie 的形式写给用户各自的浏览器,再去访问服务器中的 web 资源时,就会带着各自的数据去。这样 web 资源处理的就是用户各自的数据了。

​ 通常用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。

10.2.2 Session

​ Session 是服务器端技术,利用这个技术,服务器在运行时可以为每一个用户的浏览器创建一个其独享的 session 对象,由于 session 为用户浏览器独享,所以用户在访问服务器的 web 资源时,可以把各自的数据放在各自的 session 中,当用户再去访问服务器中的其他 web 资源时,其他 web 资源再从用户各自的 session 中取出数据为用户服务。

​ Java 中的 javax.servlet.http.Cookie 类用于创建一个 Cookie

方法 类型 描述
1 Cookie(String name,String value) 构造方法 实例化 Cookie 对象,传入 cookie 名称后 cookie 的值
2 public String getName() 普通方法 取得 Cookie 的名字
3 public String getValue() 普通方法 取得 Cookie 的值
4 public void setValue() 普通方法 设置 Cookie 的值
5 public void setMaxAge(int expiry) 普通方法 设置 Cookie 的最大保存时间,即 Cookie 的有效期,当服务器给浏览器回送一个 cookie 时,如果在服务器端没有调用 setMaxAge 方法设置 cookie 的有效期,那么 cookie 的有效期只在一次会话过程中有效,用户开一个浏览器,点击多个超链接,访问服务器多个 web 资源,然后关闭浏览器,整个过程称之为一次会话,当关闭浏览器,会话就结束了,此时 cookie 就会失效,如果在服务器端使用 setMaxAge 方法设置了 cookie 的有效期,比如设置了 30 分钟,那么当服务器把 cookie 发送给浏览器时,cookie 就会存储在客户端的硬盘上30分钟,在30分钟内即时浏览器关闭了,cookie 依然存在,只要打开浏览器访问服务器,浏览器都会带上 cookie ,这样就可以在服务器端获取到客户端浏览器传递过来的 cookie 里面的信息(这就是 cookie 是否设置 maxAge 的区别),不设置 maxAge,那么 cookie 就只在一次会话中有效,一旦用户关闭了浏览器(会话),那么 cookie 就没有了。浏览器是如何做到这一点的呢?我们启动一个浏览器就相当于启动一个应用程序,而服务器回送的 cookie 首先是存在浏览器的缓存中的,当浏览器关闭时,浏览器的缓存自然就没有了,所以存在缓存中的 cookie 也被被清理掉了,而设置了 maxAge,即设置 cookie 的有效期后,浏览器在关闭时就会将缓存中的 cookie 写到硬盘上存储起来,这样 cookie 就能够一直存在了
6 public int getMaxAge() 普通方法 获取 Cookies 的有效期
7 public void setPath(String uri) 普通方法 设置 cookie 的有效路径,比如把 cookie 的有效路径设置为 “/ck",那么浏览器访问”xdp"目录下的 web 资源时,都会带上 cookie,再比如把 cookie 的有效路径设置为“/ck/yh",那么浏览器只有在访问”ck"目录下的“yh"这个目录里的 web 资源时才会带上 cookie一起访问,而当访问”ck“目录下的 web 资源时,浏览器是不带”cookie“的
8 public String getPath() 普通方法 获取 cookie 的有效路径
9 public void setDomain(String pattern) 普通方法 设置 cookie 的有效路径
10 public void getDomain() 普通方法 获取 cookie 的有效路径

​ response 接口中也定义了一个 addCookie() 方法,用于在其响应头中增加一个相应的 Set-Cookie 头字段。同样,request 接口中也定义了一个 getCookies() 方法,它用于获取客户端提交的 Cookie

实例 1:使用 cookie 记录用户上一次访问的时间

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;

/** cookie实例:获取用户的上一次访问时间
 * @author YH
 * @create 2020-04-19 9:14
 */
public class CookieDemo extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //设置服务端以UTF-8的编码进行输出
        resp.setCharacterEncoding("UTF-8");
        //控制浏览器以UTF-8的编码进行输出
        resp.setContentType("text/html;charset=utf-8");
        PrintWriter out = resp.getWriter();
        //获取浏览器其访问服务器传递过来的 Cookie 的数组
        Cookie[] cookies = req.getCookies();
        //如果用户是第一次访问,那么cookie为null
        if(cookies != null){
            out.write("上一次的访问时间为:");
            //对cookie数组进行遍历,获取name为lastAccessTime的属性的值,就是上次访问时间
            for (int i = 0; i < cookies.length; i++) {
                Cookie cookie = cookies[i];
                if(cookie.getName().equals("lastAccessTime")){
                    Long lastAccessTime = Long.parseLong(cookie.getValue());
                    //转为日期格式
                    Date date = new Date(lastAccessTime);
                    out.write(date.toString());
                }
            }
        }else{
            out.write("你是第一次访问!");
        }

        //用户访问过之后重新设置用户的访问时间,存储到 cookie 中,然后发送到客户端浏览器
        Cookie cookie = new Cookie("lastAccessTime",System.currentTimeMillis() + "");
        //将 cookie 对象添加到response对象中,这样服务器在输出response对象内容时就会把cookie传递给浏览器
        resp.addCookie(cookie);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

第一次访问,服务器通过响应对象回送cookie数据给浏览器:

image-20200419110730449

刷新一下,相当于第二次访问:

image-20200419110958117

实例 2:使用 Cookie 实现记住用户名

首次访问注册页面:

image-20200420081812814

访问测试页面,将用户提交的用户名信息以 Cookie 的形式回传给客户端(此时浏览器的其他网页也共享这个 cookie):

image-20200420082235679

再次访问注册页面:

image-20200420083008474

  • 一个 Cookie 只能标识一种信息,它至少含有一个标识该信息的名称(Name)和设置值(Value)
  • 一个 web 站点可以给一个 web 浏览器发送多个 Cookie,一个 web 浏览器也可以存储多个 web 站点提供的 Cookie
  • 浏览器一般可以存储300个Cookie(好几年前的数据),每个站点最多存放20个 Cookie(几年前的数据),每个 Cookie 的大小最小为 4k(4096字节)
  • 如果创建一个 Cookie 并将它发送到浏览器,默认情况下它是一个会话级别的 cookie(即存储在浏览器的内存中),用户退出浏览器之后被删除。若希望浏览器将给 cookie 存储在磁盘上,则需要使用 maxAge,并给出一个以秒为单位的时间。将最大时效设为 0 则是命令浏览器将该 Cookie 删除
  1. 删除 Cookie

注意:删除 cookie 时,path 必须一致,否则不会删除

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author YH
 * @create 2020-04-19 11:37
 */
public class DeleteCookie extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //创建一个名字为 lastAccessTime的cookie
        Cookie cookie = new Cookie("lastAccessTime",System.currentTimeMillis() + "");
        //将cookie的有效期设置为0,命令浏览器删除该cookie
        cookie.setMaxAge(0);
        resp.addCookie(cookie);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
  1. Cookie 中存取中文

    ​ 要想在 cookie 中存取中文,那么必须使用 URLEncoder 类里面的 encode(String s,String enc) 方法进行中文转码,例如:

    Cookie cookie = new Cookie("userName",URLEncoder.encode("云翯","UTF-8"));
    response.addCookie(cookie);
    

    ​ 在获取 cookie 中的中文数据时,再使用 URLDecoder 类里面的 decode(String s,String enc) 进行解码,例如:

    URLDecoder.decode(cookies[i].getValue(),"UTF-8");
    

十一、Session

​ 在 WEB 开发中,服务器可以为每个用户浏览器创建一个会话对象(session 对象)一个浏览器独占一个 session 对象(默认情况下)。因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的 session 中,当用户使用浏览器访问其他程序时,其他程序可以从用户的 session 中取出该用户的数据,为用户服务。

  • Cookie 存储在客户端,是把用户的数据写给用户的浏览器

  • Session 存储在服务端,是把用户的数据写到用户独占的 session 中(同一会话共享一个 session )

  • Session 对象由服务器创建,开发人员可以调用 request 对象的 getSession 方法得到 session 对象

11.2 Session 实现原理

  1. 服务器时如何实现一个 session 为一个用户浏览器服务的?

Session 机制:

image-20200420095343794

​ 客户端第一次访问服务端时(会通过匹配 JSESSIONID和sessionId判断是否第一次访问),服务端会产生一个 session 对象(用于保存该用户的信息),且每个 session 对象都有一个唯一的 sessionId(用于区分其他的 session),服务端会产生一个 name=JSESSIONID,Value=服务端 sessionId 的 Cookie,服务端会在响应客户端的同时,将该 Cookie 发送给客户端,至此,客户端就有了一个 Cookie(JSESSIONID);

​ 因此,客户端的 Cookie 就可以和服务端的 session 一一对应(JSESSIONID - sessionId)。

​ 客户端再次访问服务端时:客户端请求中就会带上 Cookie,而服务端会先用客户端 cookie 中的 JSESSIONID 去服务端的 session 中匹配 sessionId,如果没有匹配到,说明第一次访问,过程如上;如果过匹配到,说明不是第一访问,则直接通过对应的 session 获取用户信息,为之服务(如无需再次登录)

例子:商场存包

​ 顾客(客户端)

​ 商场(服务端)

​ 顾客第一次来存包处,商场判断顾客是否有钥匙,判断是否是新顾客(没钥匙),给新顾客分配一把钥匙,钥匙 与 柜子 一一对应;

​ 顾客第二次来存包,顾客有钥匙,说明是老顾客,则不需要分配钥匙,客户凭钥匙去对应的柜子取包。

可以用如下的代码证明:

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
 * @author YH
 * @create 2020-04-19 14:55
 */
public class SessionDemo extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("utf-8");
        resp.setContentType("text/html;charset=utf-8");
        //使用request对象的getSession()获取session,如果session不存在将创建一个
        HttpSession session = req.getSession();
        //将数据存储到session中
        session.setAttribute("data","加油!奥利给!");
        //使用session对象的getId()获取session的id
        String id = session.getId();
        //判断session是不是新创建的
        if(session.isNew()){
            resp.getWriter().print("session创建成功,session的id为:" + id);
        }else{
            resp.getWriter().print("服务器已存在session,session的id是:" + id);
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

服务器创建session后,把session的Id以cookie的形式存储到用户的客户端:

image-20200419153250093

刷新浏览器,再次向服务器发送请求,可以看到浏览器将session的id存储到cookie中,一起传到服务器:

image-20200419153700015

11.3 session 常用方法

String getId() 获取sessionId

boolean isNew() 判断是否是新用户(第一次访问)

void invalidate() 使 session 失效(退出登录、注销)

设置 session 属性(存入数据):

​ void setAttribute()

​ Object getAttribute()

void setMaxInactiveInterval(秒):设置最大有效非活动时间(如,多长时间未操作退出登录)

int getMaxInactiveInterval():获取最大有效非活动时间

注意:request 请求(作用域)只在同一次有效(如在地址栏再次回车或点刷新属于二次发送请求,是无效的),而按 F5(强制刷新)不同,浏览器会自动重复刚才的行为。

11.4 session 共享问题

​ 同一会话中(用户在站点上从打开到关闭的一次操作),访问站点上所有 web 资源时,共享一个 session

实例:

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** session共享:网页登陆示例
 * @author YH
 * @create 2020-04-20 14:24
 */
public class SessionDemo2 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setCharacterEncoding("utf-8");
        //模拟数据库存储账号密码
        String nameData = "YH";
        String pswdData = "123456";
        //进行登陆验证
        String username = req.getParameter("username");
        String password = req.getParameter("password");

        if (username.equals(nameData) && password.equals(pswdData)){
            //密码正确,跳转到登陆成功页面(验证登陆成功后可以直接访问登陆成功页面)
            //登陆成功创建 session
            req.getSession().setAttribute("username",username);
            req.getSession().setAttribute("password",password);
            //请求转发至登陆成功页面,可以获取到数据且地址栏没有改变
            req.getRequestDispatcher("login.jsp").forward(req,resp);
        }else{
            //密码错误,重定向回登陆界面(数据会丢失,且地址栏变化)
            System.out.println("密码错误");
            resp.sendRedirect("index.jsp");
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
<%--登陆页面--%>
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<html>
<body>
<h2>Hello World!</h2>
<form action="${pageContext.request.contextPath}/session2" method="get">
    <input type="text" name="username" />
    <input type="password" name="password" />
    <input type="submit" value="注册" />
</form>
</body>
</html>
<%--登陆成功页面--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<%
    String name = (String)session.getAttribute("username");
    //如果用户没有登陆成功而直接访问此页面,则name必然为null
    if(name != null){
        response.getWriter().print("登陆成功!用户名:" + name);
        //10秒无操作注销session(退出登陆)
        session.setMaxInactiveInterval(10);
    }else{
        //重定向回登录页
        response.sendRedirect("index.jsp");
    }
%>
<a href="invalidate.jsp">注销</a>
<h2>登陆成功!</h2>
</body>
</html>
<%--实现注销(销毁 session)功能的页面--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <%
        //注销 session
        session.invalidate();
        //返回登陆页
        response.sendRedirect("login.jsp");
    %>
</body>
</html>

输入错误的账号密码,直接跳回登陆页:

GIF

​ 尝试未登陆成功的情况下去访问登陆后所跳转的页面,跳回登陆页:

GIF

输入正确的登陆账号密码:

GIF

​ 登陆成功后,访问跳转后的页面(如访问其它需要登陆后才能访问的页面效果同样):

GIF

​ 注销后跳转回登陆界面,并尝试访问登陆成功界面,但跳回登录页:

GIF

GIF

10 秒无操作后再次访问登录页,已经退出了:

GIF

小结:

​ 用户的登陆信息可以保存在 session 中,通过 session 可以控制用户登入登出等用户独享的信息,而同一用户登陆成功后(匹配上 session 后),可以获取 session 的所有信息(即 登陆后的页面共享这个 session)。

11.5 使用 session 防止表单重复提交

​ 场景:遇到网络延迟时,用户提交表单,服务器半天没有反应,那么用户可能多次进行提交操作,造成重复提交表单的情况,开发中需要避免。

  1. 表单重复提交的常见应用场景

有如下 jsp 页面:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <form action="${pageContext.request.contextPath}/form" method="post">
        用户名:<input type="text" name="username">
        <input type="submit" value="提交">
    </form>
</body>
</html>
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author YH
 * @create 2020-04-22 20:06
 */
public class DoFormServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //浏览器默认是以UTF-8编码传输到服务器的,所以需要设置服务器以UTF-8的编码进行接收,避免乱码
        req.setCharacterEncoding("UTF-8");
        String username = req.getParameter("username");
        try{
            //让当前线程沉睡3秒,模拟网络延迟
            Thread.sleep(3*1000);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(username + "向数据库中插入数据");

    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

场景一:网络延迟使用户可以多次提交表单(用 Chrome 浏览器重复提价也只会显示一次,IE浏览器多次点击 提交按钮会显示多次)

GIF1

场景二:提交表单后出现网络延迟,用户进行刷新操作,也会重复提交表单(同样貌似 Chrome 浏览器做了优化,反复刷新也只提交一次,IE 刷新就会提交一次)

GIF2

场景三:场景二中的延迟出现时,进行后退有可能也会出现重复提价表单的情况

  1. 解决办法

    • 方式一:利用 JavaScript 防止表单重复提交

      针对网络延迟的情况下用户有时间多次点击 submit 按钮按钮导致表单重复提交,js 的解决思路是:用 JS 控制 form 表单只能提交一次

      检测是否提交过表单来控制提交:

      <script type="text/javascript">
          var isCommitted = false;//表单是否已经提交标识,默认为false
          function dosubmit(){
              if(isCommitted==false){
                   isCommitted = true;//提交表单后,将表单是否已经提交标识设置为true
                   return true;//返回true让表单正常提交
               }else{
                   return false;//返回false那么表单将不提交
               }
           }
      </script>
      

      还可以在表单提交后将按钮设置为不可用:

      function dosubmit(){
          //获取表单提交按钮
          var btnSubmit = document.getElementById("submit");
          //将表单提交按钮设置为不可用,这样就可以避免用户再次点击提交按钮
          btnSubmit.disabled= "disabled";
          //返回true让表单可以正常提交
          return true;
      }
      

      使用 JS 只能解决场景一出现的情况。

  • 方式二:利用 Session 防止表单重复提交

具体思路:

​ 在服务器生成一个唯一的随机标识,专业术语称为 Token(令牌),同时在当前用户的 session 域中保存这个 Token;然后将 Token 发送到客户端的 Form 表单中,在 Form 表单中使用隐藏域来存储这个 Token,表单提交的时候连同这个 Token 一起提交到服务器,然后在服务器端判断提交上来的 Token 与服务器生成的 Token 是否一致,如果不一致,那就是重复提交了,此时服务器端就可以不处理重复的表单;如果相同则处理提交的表单,处理完后清除当前用户的 session 域中存储的 Token 标识;

​ 服务器拒绝处理用户提交的表单请求的情况:

  • 存储 session 域中的 Token(令牌) 与表单提交的 Token(令牌) 不同 ----- 多次点击提交按钮;
  • 当前用户的 session 中不存在 Token(令牌) ----- 第一次提交成功后,服务器会清除当前用户 session 中存储的 Token;
  • 用户提交的表单数据中没有 Token(令牌) ----- 点击后退时

具体代码:

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 获取并向 session 中存储 token令牌的 servlet程序
 * @author YH
 * @create 2020-04-22 22:02
 */
public class TokenServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //生成 ToKen,创建令牌
        String token = TokenProccessor.getInstance().makeToken();
        System.out.println("在FormServlet中生成的token:"+token);
        //在服务器使用session保存token(令牌)
        req.getSession().setAttribute("token", token);
        //跳转到form表单页面
        req.getRequestDispatcher("/form.jsp").forward(req,resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
import sun.misc.BASE64Encoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;

/**
 * 单例设计模式创建生成 Token令牌 的工具类
 * @author YH
 * @create 2020-04-22 22:04
 */
public class TokenProccessor {
    private TokenProccessor(){}
    private static final TokenProccessor instance = new TokenProccessor();

    /**
     * 返回类对象实例
     * @return
     */
    public static TokenProccessor getInstance(){
        return instance;
    }

    /**
     * 生成Token
     * @return
     */
    public String makeToken(){
        String token = (System.currentTimeMillis() + new Random().nextInt(999999999) + "");
        //数据指纹 128位长 16个字节 md5
        try{
            MessageDigest md = MessageDigest.getInstance("md5");
            byte md5[] = md.digest(token.getBytes());
            //base64编码--任意而精致编码明文字符
            BASE64Encoder encoder = new BASE64Encoder();
            return encoder.encode(md5);
        }catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }
}
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 判断是否重复提交
 * @author YH
 * @create 2020-04-22 20:06
 */
public class DoFormServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //判断用户是否重复提交
        boolean b = isRepeatSubmit(req);
        if(b == true){
            System.out.println("请不要重复提交");
            return;
        }
        //移除session中的token
        req.getSession().removeAttribute("token");
        System.out.println("处理用户提交请求!");

        //浏览器默认是以UTF-8编码传输到服务器的,所以需要设置服务器以UTF-8的编码进行接收,避免乱码
        req.setCharacterEncoding("UTF-8");
        String username = req.getParameter("username");
        try{
            //让当前线程沉睡3秒,模拟网络延迟
            Thread.sleep(3*1000);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(username + "向数据库中插入数据");
    }

    /**
     * 判断客户端提交上来的令牌和服务器端生成的令牌是否一致
     * @param req
     * @return true 重复提交 false 没有重复
     */
    private boolean isRepeatSubmit(HttpServletRequest req) {
        String client_token = req.getParameter("token");
//        1.如果用户提交的表单数据中没有token,则用户是重复提交了表单
        if(client_token == null){
            return true;
        }
        //取出存储在Session中的token
        String session_token = (String)req.getSession().getAttribute("token");
//        2.如果session中不存在token,则表单重复提交
        if(session_token == null){
            return true;
        }
//        3.存储在session的Token 与 表单提交的Token不同,则用户是重复提交了表单
        if(!client_token.equals(session_token)){
            return true;
        }
        return false;
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <form action="${pageContext.request.contextPath}/form" method="post">
<%--        使用隐藏域存储生成的token--%>
<%--        <input type="hidden" name="token" value="<%=session.getAttribute("token")%>">--%>
<%--    使用EL表达式取出存储在session中的token--%>
    <input type="hidden" name="token" value="${token}"/>

        用户名:<input type="text" name="username">
        <input type="submit" value="提交">
    </form>
</body>
</html>

首次访问服务器生成token存入专属的session内:

image-20200422224017221

最终效果:

GIF

从运行效果中可以看到,通过这种方式处理表单重复提交,可以解决上述的场景二和场景三中出现的表单重复提交问题。

补充:cookie 及四种对象作用域

​ 客户端在第一次请求服务端时,如果服务端发现此请求没有 JSESSIONID,则会创建一个 name=JSESSIONID 的cookie,并返回给客户端。

四种范围对象(小 -> 大)

pageContext JSP 页面容器 当前页面有效(页面跳转后无效)

request 请求对象 同一次请求有效,其他请求无效(请求转发一直都是一次请求 则有效,重定向后会丢失原请求数据发起新请求 则无效)

session 会话对象 同一次会话有效(无论怎么跳转都有效,关闭/切换浏览器后无效;即从 登陆 - 退出 之间 全部有效)

appliation 全局对象 全局有效(整个项目运行期间都有效;重启服务、其他项目无效)

以上四个对象:通过 setAttribute() 赋值 getAttribute() 获取属性值;作用范围越大的,性能开销也越大

十二、JavaBean 及 JSP 简单了解

12.1 JavaBean

Bean:在计算机英语中,有可重用组件的含义

JavaBean:用 Java 语言编写的可重用组件(JavaBean 所表示的范围 > 实体类)

​ JavaBean 遵循特定的写法,特点如下:

  • 这个类必须有一个无参构造器
  • 属性必须私有化
  • 私有化的属性必须通过 public 类型的方法暴露给其他程序,且方法的命名需遵循规范
  • 使用层面,Java 可分为 2 大类:
    • 封装业务逻辑的 JavaBean(如封装 jdbc 操作的 DAO)
    • 封装数据的 JavaBean(如对应于数据库中一张表的实体类)

范例:

package yh.javabean;

public class Person {
    //-----------属性私有化-----------
    private String name;
    private char sex;
    private int age;
    //-----------无参构造器-----------
    public class(){}
    //-----------设置/获取属性的public权限的方法-----------
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
    public char getSex(){
        return sex;
    }
    public void setSex(char sex){
        this.sex = sex;
    }
    public int getAge(){
        return age;
    }
    public void setAge(int age){
        this.age = age;
    }
}

​ JavaBean 在 J2EE 开发中,通常用于封装数据,对于遵循以上写法的 JavaBean 组件,其他程序可以通过反射技术实例化 JavaBean 对象,并且通过反射那些遵循命名规范的方法,从而获得 JavaBean 的属性,进而调用其保存数据。

​ JavaBean 的属性可以是任意类型,且可以有多个属性。每个属性需要有对应的 getter、setter 方法(称为属性修改器 和 属性访问器)。

​ 命名规范:遵循驼峰命名法,get/set 为小写。

​ 属性值也可以只有 get 或 set 方法,称为只读、只写属性。

12.2 JSP 原理

JSP 本质上是 Servlet,浏览器向服务器发送请求时,不管访问的是什么资源,起始都是在访问 Servlet。

​ 所以当访问一个 jsp 页面时,其实也是在访问一个 Servlet,服务器在执行 jsp 的时候,首先把 jsp 翻译成一个 Servlet,所以我们访问 jsp 时,其实不是在访问 jsp,而是在访问 jsp 翻译后的哪个 Servlet。

访问 jsp 页面时服务器执行流程图:

img

第一次执行:

  1. 客户端通过电脑连接服务器,因为是请求是动态的,所以所有的请求交给 WEB 容器来处理
  2. 在容器中找到需要执行的 *.jsp 文件
  3. 之后 *.jsp 文件通过转换变为 *.java 文件
  4. *.java 文件经过编译后,形成 *.class 文件
  5. 最终服务器要执行形成的 *.class 文件

第二次执行:

  1. 因为已经存在了 *.class 文件,所以不在需要转换和编译的过程

修改后执行:

​ \1. 源文件已经被修改过了,所以需要重新转换,重新编译。

​ 每个 jsp 页面就是一个 java 源文件,且继承 HttpJspBase 类,而 HttpJspBase 类又继承于 HTTPServlet,可见 HttpJspBase 类本身就是一个 servlet,继承它的 jsp 类也是。

  • 既然本质是 java 中的 Servlet,那么其中的 HTML 代码又是如何实现的呢?

​ 在 jsp 中编写的 java 代码和 html 代码都会被翻译到_jspService 方法中去,java 代码原封不动地翻译为 java 代码,而 HTML 代码则通过 out.write(""); 输出流,向网页输出 HTML 代码,最终通过浏览器解析展现

  • Web 服务器在调用 jsp 实例时,会给 jsp 提供一些什么 java 对象?

八大对象:

PageContext pageContext;
HttpSession session;
ServletContext application;
ServletConfig config;
JspWriter out;
Object page = this;
HttpServletRequest request,
HttpServletResponse response

其中 page/request/response 已经完成实例化,其他 5 个对象的实例化方式:

pageContext = _jspxFactory.getPageContext(this, request, response,null, true, 8192, true);
application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();

十三、JDBC

13.1简介

  1. 数据库驱动

​ 安装数据库后,我们的程序是不可以直接进行数据库连接的,需要对应的数据驱动(就像我们电脑的声卡、网卡也不是插上就能用的,也需要驱动),通过驱动去和数据库打交道,如图:

image-20200423121143850

  1. JDBC

    ​ SUN 公司为了简化、统一对数据的凑在哦、定义了一套 Java 操作数据库的规范(接口),称之为 JDBC(Java DataBase Connection)。这套接口由数据库厂商去实现,这样开发人员只需学习 jdbc 接口,并通过 jdbc 加载具体的驱动,就可以操作数据库。

    image-20200423121303771

    组成 JDBC 的两个包:java.sql、javax.sql,开发 JDBC 应用还需要导入相应数据库的驱动。

  2. 相关API

    • DriverManager驱动管理类
    • Connection连接对象接口(通过 DriverManager 获取对象)
      • createStatement():生成命令对象
      • preparedStatement():生成预编译命令对象
    • Statement命令对象接口(通过 Connection 获取对象)
      • executeQuery():执行SQL增删改查语句,返回首影响行数
      • executeUpdate():执行SQL查询语句,返回结果集
      • execure():执行任何SQL语句,返回boolean
    • prearedStatement预编译命令对象接口(Statement 子类,也通过 Connection 获取对象)
      • executeQuery():执行SQL增删改查语句,返回首影响行数
      • executeUpdate():执行SQL查询语句,返回结果集
      • execure():执行任何SQL语句,返回boolean
      • setXxxx(占位符索引,占位符的值):设置对应索引的占位符的值,类型为XX类型
      • setObject(占位符索引,占位符的值):设置对应索引的占位符的值,类型为Object类型
    • CallableStatement : 调用数据库中的 存储过程/存储函数(Statement 子类,也通过 Connection 获取对象)
    • ResultSet结果集对象接口(通过 Statement 获取对象)
      • next():下移一行返回当前行是否有值(类似java迭代器)
      • previous():上移一行,返回当前行是否有值
      • getXX(列索引|列名|别名):返回对应列的值,接收类型为XX
      • getObject(列索引|列名|别名):返回对应列的值,接收类型为Object
        afterLast() :移动到resultSet的最后面。

各个 类、API 解释:

  1. DriverManager 类

    JDBC 程序中 DriverManager 用于加载驱动,并创建与数据库的连接,这个 API 的常用方法:

    • DriverManager.registerDriver(new Driver())
    • DriverManager.getConnection(url,user,password)

注意:实际开发中并不采用 registerDriver 方法注册驱动,原因:

  1. 查看 Driver 源码可以看到,如果采用此种方式,会导致驱动程序注册两次,在内存中创建两个 Driver 对象
  2. 程序依赖 mysql 的 api,脱离 mysql 的 jar 包,程序将无法编译,将来程序切花底层数据库将会非常麻烦

​ 推荐方式:Class.forName("com.mysql.jdbc.Driver");

此种方式不会导致驱动对象在内存中重复出现,并且采用此种方式,程序只需要一个字符串,不需要依赖具体的驱动,使程序的灵活性更高。

  1. 数据库 URL

    URL 用于标识数据库的位置,通过 URL 地址告诉 JDBC 程序连接哪个数据库,URL 的写法为:

    image-20200423135844554

    常用数据库 URL 地址的写法:

    • MySQL 写法:jdbc:mysql://localhost:3306/data
    • Oracle 写法:jdbc:oracle:thin:@localhost:1521:sid
    • SqlServer 写法:jdbc:microsoft:sqlserver://localhost:1433; DatabaseName=sid
  2. Connection 类

    JDBC 程序中的 Connection 用于代表数据库的连接,Connection 是数据编程中最重要的一个对象,客户端与数据库所有交互都是通过 Connection 对象完成的,常用方法:

    • createStatement():创建向数据库发送 SQL 的 statement 对象
    • prepareStatement(sql):创建向数据库发送预编译 SQL 的 PrepareStatement 对象
    • prepareCall(sql):创建执行存储过程的 callableStatement 对象
    • setAutoCommit(boolean autoCommit):设置事务是否自动提交
    • commit():在链接上提交事务
    • rollback():在此连接上回滚事务
  3. Statement 类

    JDBC 程序中的 Statement 对选哪个用于向数据库发送 SQL 语句,常用方法:

    • executeQuery(String sql):用于向数据库发送查询语句
    • executeUpdate(String sql):用于向数据库发送 insert、update 或 delete 语句
    • execute(String sql):用于向数据库发送任意 SQL 语句
    • addBatch(String sql):把多条 SQL 语句放到一个批处理中
    • executeBatch():向数据库发送一批 SQL 语句执行
  4. PreparedStatement 类(常用)

​ PreparedStatement 是 Statement 的子类,它的实例对象可以通过 Connection.preparedStatement() 方法获得,相对于 Statement 对象而言:Statement 对象而言:PreparedStatement 可以避免 sql 注入的问题。
​ Statement 会使数据库频繁编译 sql,可能造成数据库缓冲区溢出;而 PreparedStatement 可对 SQL 进行预编译,从而提高数据库的执行效率。并且 PreparedStatement 对于 SQL 中的参数,允许使用占位符的形式进行替换,简化 sql 语句的编写。

  • 相比 Statement,PreparedStatement 有哪些优势:
    1. 解决 Statement 的 SQL 语句拼串问题,防止 SQL 注入
    2. PreparedStatment 可以操作 Blod(而进制)数据,Statement不行
    3. PreparedStatement 可以实现更高效的批量操作
  1. ResultSet 类

JDBC 程序中用于表示 SQL 语句执行结果的对象,ResultSet 封装执行结果时,采用的类似于表格的方式,内部维护了一个指向该表格数据行的游标,初始位置位于第一行数据的前一个,配合 next() 方法,移动游标并判断是否为空。

​ 既然是结果集,所以提供的都是 get 方法

​ 获取任意类型的数据:

​ getXxx(int index)

​ getXxx(String columnName)

​ ResultSet 还提供了对结果集进行滚动的方法:

​ next():移动到下一行

​ Previous():移动到前一行

​ absolute(int row):移动到指定行

​ beforeFirst():移动 resultSet 的最前面

​ afterLast():移动到 resultSet 的最后面

  1. 释放资源

  Jdbc 程序运行完后,切记要释放程序在运行过程中,创建的那些与数据库进行交互的对象,这些对象通常是 ResultSet, Statement 和 Connection 对象,特别是 Connection 对象,它是非常稀有的资源,用完后必须马上释放,如果 Connection 不能及时、正确的关闭,极易导致系统宕机。Connection 的使用原则是尽量晚创建,尽量早的释放。

​ 为确保资源释放代码能运行,资源释放代码也一定要放在 finally 语句中。

13.2 使用 JDBC 进行增删改查

​ JDBC连接步骤:

  1. 读取配置文件
  2. 注册加驱动
  3. 创建连接
  4. 执行增删改查
  5. 关闭资源

将其中1/2/3/5封装成工具类

对JDBC连接过程通用部分封装为工具类:

package jdbc;

import java.io.FileInputStream;
import java.sql.*;
import java.util.Properties;

/**
 * JDBC工具类
 * 功能:获取连接、释放资源
 * @author YH
 * @create 2020-04-23 16:00
 */
public class JDBCUtils {
    //连接数据库需要的URL参数
    static String user;
    static String password;
    static String url;
    static String driver;
    //读取配置文件(属于共有操作,随着类加载只执行一次,提升效率)
    static{
        //工具类中对可能出现的异常进行初步处理,省去调用者处理
        try {
            Properties properties = new Properties();
            properties.load(new FileInputStream("JDBC/src/main/resources/data.properties"));
            user = properties.getProperty("user");
            password = properties.getProperty("password");
            url = properties.getProperty("url");
            driver = properties.getProperty("driver");
            //注册驱动
            Class.forName(driver);
        } catch (Exception e) {
            //将编译时异常转为运行时异常(提示的异常信息更具体)
            throw new RuntimeException(e);
        }
    }

    /**
     * 获取连接
     * @return
     */
    public static Connection getConnection(){
        try {
            return DriverManager.getConnection(url,user,password);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 功能:释放资源
     * 没有用到的资源参数留null
     * @param connection
     * @param statement 可以接收其子类对象
     * @param resultSet
     */
    public static void close(Connection connection, Statement statement, ResultSet resultSet){
        try {
            if(connection != null){
                connection.close();
            }
            if(statement != null){
                statement.close();
            }
            if(resultSet != null){
                resultSet.close();
            }
        } catch(SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

配置文件内容:

user=root
password=rootMySQL
url=jdbc:mysql://localhost:3306/class7
driver=com.mysql.jdbc.Driver

JDBC连接MySQL进行CRUD操作:

package jdbc;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 *
 * @author YH
 * @create 2020-04-23 15:42
 */
public class JDBCTest {
    public static void main(String[] args) throws SQLException {
        JDBCTest jdbcTest = new JDBCTest();
//        jdbcTest.select();
        jdbcTest.cud();
    }

    /**
     * 查询操作
     */
    public void select() throws SQLException {
        Connection connection = JDBCUtils.getConnection();

        System.out.println("连接成功!");

        PreparedStatement statement = null;

        //查询操作
        try {
            String sql = "SELECT * FROM usertest WHERE id BETWEEN 2 AND ?";
            statement = connection.prepareStatement(sql);
            //设置通配符的值
            statement.setString(1,"6");
            ResultSet set = statement.executeQuery();
            while(set.next()){
                String id = set.getString(1);
                String name = set.getString(2);
                String sex = set.getString(3);
                String age = set.getString(4);
                String password = set.getString(5);
                String telephone = set.getString(6);
                System.out.println("编号\t姓名\t性别\t年龄\t密码\t电话");
                System.out.println(" " + id + "\t\t" + name + "\t" + sex + "\t\t" + age+"\t\t"
                        + password+"\t" + telephone);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            //关闭资源
            JDBCUtils.close(connection,statement,null);
        }
    }

    /**
     * 增删改操作
     */
    public void cud(){
        //获取连接
        Connection connection = JDBCUtils.getConnection();

        System.out.println("连接成功!");

        PreparedStatement cStatement = null;
        PreparedStatement uStatement = null;
        PreparedStatement dStatement = null;
        try {
            //增删改操作
            //sql语句
            String cSql = "insert into usertest(id,name,sex) values(null,'张三','女'),(null,'李四','男');";
            String uSql = "update usertest set name='麻子',sex='女' where id=1;";
            String dSql = "delete from usertest where id=2;";

            //预编译SQL语句
            cStatement = connection.prepareStatement(cSql);
            uStatement = connection.prepareStatement(uSql);
            dStatement = connection.prepareStatement(dSql);
            //执行SQL语句
//            int cLine = cStatement.executeUpdate();
//            System.out.println(cLine > 0 ? "创建并添加数据成功!" : "创建失败!");

//            int uLine = uStatement.executeUpdate();
//            System.out.println(uLine > 0 ? "更新数据成功!" : "更新失败!");
//
            int dLine = dStatement.executeUpdate();
            System.out.println(dLine > 0 ? "删除数据成功!" : "删除失败");


        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(connection,cStatement,null);
            JDBCUtils.close(null,uStatement,null);
            JDBCUtils.close(null,dStatement,null);
        }
    }
}

原表中数据:

image-20200423210741833

查询结果:

image-20200423172700026

增、改、删效果:

image-20200423210956743

image-20200423211038538

image-20200423211120446

13.3 查询结果集的处理

​ 查询之所以与 CUD 区别开来关键在于查询返回的是从数据库获取的数据,查询不同的表就返回不同的结果集,我们需要对这些结果集进行妥善处理。

​ 对于查询结果集可以直接输出 或 存储在数组、集合内使用,但是更好的方式是将数据封装在 JavaBean 对象中,无论是调用还是传递都更加方便,且更符合 Java 万事万物皆对象的理念。

  • ORM 编程思想(Object Relational mapping 对象关系映射)

    • 一个数据表对应一个 java 类
    • 表中的一条记录(行)对应 java 类的一个对象
    • 表中的一个字段(本行的列)对应 java 类的一个属性
  • Java与SQL对应数据类型转换表

    Java类型 SQL类型
    boolean BIT
    byte TINYINT
    short SMALLINT
    int INTEGER
    long BIGINT
    String CHAR,VARCHAR,LONGVARCHAR
    byte array BINARY , VAR BINARY
    java.sql.Date DATE
    java.sql.Time TIME
    java.sql.Timestamp TIMESTAMP

1. ResultSet

  • 常用方法:

    • next():判断下一行是否有数据,true 向下移动一行
    • getXxx():返回指定类型的数据,Xxx 为数据类型
  • 查询需要调用 PreparedStatement 的 executeQuery() 方法,查询结果是一个 ResultSet 对象;

  • ResultSet 对象以逻辑表格的形式封装了执行数据库操作的结果集,ResultSet 接口由数据库厂商实现;

  • ResultSet 返回的实际是一张数据表,有一个指针指向数据表的第一条记录的前面。初始时这个游标位于第一条记录前(类似迭代器),通过其 next() 方法检测是否有下一行,有则移动到下一行(相当于 Iterator 的 hasnext() 和 next() 结合,只是没有返回当前指向数据值,它返回的是 boolean)

1555580152530

  • 当指正指向一行时,可以通过调用 getXxx(int index) 或 getXxx(int columnName) 获取每一列的值。
    • 如:getInt(1),getString("name")

注意:Java 与数据库的交互涉及到的相关 Java API 中的索引都从 1 开始

2. ResultSetMetaData 元数据

​ 围绕数据进行解释说明的信息的称为元数据。ResultSetMetaData 可用于获取关于 ResultSet 对象中列的类型和属性信息的对象

​ ResultSetMetadata meta = ResultSet.getMetaData();

​ 常用方法:

  • getColumnCount():返回当前 ResultSet 对象的列数
  • getColumnName(int column):获得指定列的名称
  • isNullable(int column):指示指定列中的值是否可以为 null
  • getColumnLabel(int column):获取指定列的别名
  • getColumnTypeName(int column):获得指定列的类型
  • isAutoIncrement(int column):指示是否自动为指定列进行编号,这样这些列仍然是只读的
  • getColumnDisplaySize(int column):指示指定列的最大标准宽度,以字符为单位

1555579494691

问题1:得到结果集后,如何知道该结果集中有哪些列?列名是什么?

​ 需要使用一个描述 ResultSet 的对象,即 ResultSetMetadata,配合 next() 通过 meta.getConlumnName(int column) 依次获得列名(索引从 1 开始)

问题2:关于 ResultSetMetaData

1. 如何获取 ResultSetMetaData:调用 ResultSet() 的 getMetaData() 方法即可
2. 获取 ResultSet 中有多少列:调用 ResultSetMetaData 的 getColumnCount() 方法即可
3. 获取 ResultSet 每一列的别名是什么:调用 ResultSetMetaData 的 getColumnLabel() 方法

1555579816884

  1. 编写通用的查询方法
package jdbc;

import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * 不同表格通用的查询方法
 * @author YH
 * @create 2020-04-25 11:53
 */
public class ForQuery {
    /**
     * 测试方法
     */
    public static void main(String[] args){
        ForQuery f = new ForQuery();
        String sql = "SELECT user_id id,user_name name,sex,birthday FROM table_1 WHERE user_id=?;";
        Table_1 instance = f.getInstance(Table_1.class, sql, 1);
        System.out.println(instance.toString());
        System.out.println("-------------------------------------");

        String sql2 = "SELECT user_id id,user_name name,sex,birthday FROM table_1;";
        List<Table_1> list = f.getInstances(Table_1.class, sql2);
        //使用Lambda表达式(方法引用)
        list.forEach(System.out::println);
        }
    /**
     * 通用查询表格单条数据的方法
     * @param clazz 表格对应的类的Class对象
     * @param sql   SQL执行语句
     * @param params  占位符参数
     * @param <T>   表格要封装成的对象类型
     * @return
     */
    public <T> T getInstance(Class<T> clazz,String sql,Object... params){
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            //1.获取连接
            conn = JDBCUtils.getConnection();
            //2.预编译SQL语句,获得PreparedStatement对象
            statement = conn.prepareStatement(sql);
            //3.填充占位符
            for (int i = 0; i < params.length; i++) {
                statement.setObject(i + 1,params[i]);
            }
            //4.执行查询,获得结果集
            resultSet = statement.executeQuery();
            //5.获取结果集对象的元数据对象
            ResultSetMetaData metaData = resultSet.getMetaData();
            //获取查询结果集中数据的列数
            int columnCount = metaData.getColumnCount();

            if(resultSet.next()){
                //每有一条查询记录,生成一个对应的对象
                T t = clazz.newInstance();
                for (int i = 0; i < columnCount; i++) {
                    //获取列值
                    Object columnValue = resultSet.getObject(i + 1);
                    //获取列的别名(需要SQL语句中设置别名,否则获取的就是原列名,总之就是获取结果集中体现的列名)
                    String columnLabel = metaData.getColumnLabel(i + 1);
                    //6.利用反射获取本对象的属性对象,并进行赋值
                    //获取field属性对象
                    Field field = clazz.getDeclaredField(columnLabel);
                    //可访问私有权限属性
                    field.setAccessible(true);
                    //给本对象的此属性设置值
                    field.set(t,columnValue);
                }
                return t;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn,statement,resultSet);
        }
        return null;
    }
    /**
     * 用List的形式返回查询到的每条数据的对象集合
     * @param clazz 表格对应的类的Class对象
     * @param sql   SQL执行语句
     * @param params  占位符参数
     * @param <T>   表格要封装成的对象类型
     * @return
     */
    public <T> List<T> getInstances(Class<T> clazz,String sql,Object... params){
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            //1.获取连接
            conn = JDBCUtils.getConnection();
            //2.预编译SQL语句,获得PreparedStatement对象
            statement = conn.prepareStatement(sql);
            //3.填充占位符
            for (int i = 0; i < params.length; i++) {
                statement.setObject(i + 1,params[i]);
            }
            //4.执行查询,获得结果集
            resultSet = statement.executeQuery();
            //5.获取结果集对象的元数据对象
            ResultSetMetaData metaData = resultSet.getMetaData();
            //获取查询结果集中数据的列数
            int columnCount = metaData.getColumnCount();
            List<T> list = new ArrayList<>();
            while(resultSet.next()){
                //每有一条查询记录,生成一个对应的对象
                T t = clazz.newInstance();
                for (int i = 0; i < columnCount; i++) {
                    //获取列值
                    Object columnValue = resultSet.getObject(i + 1);
                    //获取列的别名(需要SQL语句中设置别名,否则获取的就是原列名,总之就是获取结果集中体现的列名)
                    String columnLabel = metaData.getColumnLabel(i + 1);
                    //6.利用反射获取本对象的属性对象,并进行赋值
                    //获取field属性对象
                    Field field = clazz.getDeclaredField(columnLabel);
                    //可访问私有权限属性
                    field.setAccessible(true);
                    //给本对象的此属性设置值
                    field.set(t,columnValue);
                }
                list.add(t);
            }
            return list;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn,statement,resultSet);
        }
        return null;
    }
}

单条数据查询效果:

image-20200425142204353

image-20200425142036851

多条数据查询效果:

image-20200425145944396

image-20200425145906345

​ 需要注意的是:数据库中的列名往往和我们定义的对象属性名不同,我们需要将他们对应起来,这个中间桥梁就是:ResultSet 结果集

​ 针对表的字段名与类的属性名不相同的情况:

  1. 必须在声明 SQL 时,使用类的属性名来命名字段的别名

  2. 使用 ResultSetMetaData 时,需要使用 getColumnLabel() 来替换 getColumnName() 获取列的别名

    如果 SQL 中没有给字段起别名,getColumnLabel() 获取的就是列名

我们要让查询结果所组成的结果集的列名与我们自定义类的属性一一对应,那么就要将执行的 SQL 语句所查询的字段都设置上别名,且与属性名对应,最终查询的结果的列名就与属性名对应上了,而后就可以通过反射直接设置列名对应属性的值

查询操作流程图:

image-20200425104425004

JDBC API 小结

  • 两种思想

    • 面向接口编程思想

      • 无论是获取连接,执行 sql 语句等,都是接口引用我们获取的对象,而实际调用方法的是接口的实现类对象
    • ORM 思想(Object Relational Mapping)

      • 一个数据表对应一个java类
      • 表中的一条记录对应java类的一个对象
      • 表中的一个字段对应java类的一个属性

      sql 需要结合列名和对象的属性名来写,要起别名。

  • 两种技术

    • JDBC 结果集的元数据:ResultSetMetaData
      • 获取列数:getColumn()
      • 获取列的别名:getColumn()
    • 通过反射,创建指定类的对象,获取指定的属性并赋值

13.4 批量插入

​ Java 支持批量更新机制,运行多条语句一次性提价给数据库批量处理。通常比单独提交处理更有效率

JDBC 的批量处理语句包括下面三个方法:

  • addBatch(String):添加需要批量处理的 SQL 语句或是参数;
  • executeBatch():执行批量处理语句
  • clearBatch():清空缓存的数据

通常我们会遇到两种批量执行 SQL 语句的情况:

  • 多条 SQL 语句的批量处理
  • 一个 SQL 语句的批量传参

实现高效的批量操作

package jdbc;

import org.junit.Test;

import java.sql.Connection;
import java.sql.PreparedStatement;

/**
 * 高效的批量插入操作
 * 1:使用 addBatch() / executeBatch() / clearBatch() 方法
 * 2.mysql 服务器默认是关闭批处理的,我们需要通过一个参数,让 mysql 开启批处理的支持
 *      ?rewriteBatchedStatements=true 下载配置文件的url后面
 * 3.使用更新的 mysql 驱动:mysql-connector-java-5.1.37-bin.jar
 * @author YH
 * @create 2020-04-26 9:27
 */
public class InsertsTest {
    @Test
    public void inserts() throws Exception {
        //记录程序开始执行的时间戳
        long start = System.currentTimeMillis();

        //获取连接
        Connection conn = JDBCUtils.getConnection();

        //1.设置不自动提交
        conn.setAutoCommit(false);

        String sql = "INSERT INTO tb_2(name) VALUES(?)";
        PreparedStatement statement = conn.prepareStatement(sql);
        for (int i = 1; i <= 1000000; i++) {
            statement.setString(1,"name_" + i);
            //1.攒sql
            statement.addBatch();
            //每攒500条sql语句执行一次
            if(i % 500 == 0){
                //2.执行sql
                statement.executeBatch();
                //2.清空缓存
                statement.clearBatch();
            }
        }
        //2.提交请求
        conn.commit();

        //记录程序执行完毕的时间戳,并得出最终用时
        long end = System.currentTimeMillis();
        System.out.println("花费时间为:" + (end - start));
    }
}

执行结果:

image-20200426112633237

​ **掉坑警告!不要手贱在 sql 语句后加分号 ; **,调试研究了半天才网上找到的解答,使用批处理时,不要用!,既然不用也可以,为了养成习惯 sql 语句都不要用;结尾了,会出现java.sql.BatchUpdateException异常,如下:

image-20200426110143163

13.5 数据库事务

  1. 数据库事务介绍

一组逻辑单元(如 一个或多个DML操作),时数据从一种状态变换到另一种状态的过程称为事务

事务处理(事务操作):保证所有事务都作为一个工作单元来执行,即时出现了故障,也不能改变这种执行方式;当一个事务中执行多个操作时,要么所有的事务都被提交(commit),则这些修改就永久地保存下来;要么数据库管理系统将放弃所有的修改,整个事务回滚(rollback)到最初状态。

​ 这个过程需要确保一致性,数据的操纵应当是离散的成组的逻辑单元:当它全部完成时,数据的一致性可以保持;而当这个单元中的一部分操作失败,整个事务应全部视为错误,所有起始点以后的操作应全部回退到开始状态。

  1. JDBC 事务处理
  • 数据一旦提交,就不可回滚

  • 数据什么时候意味着提交?

    • 当一个连接对象被创建时,默认情况下是自动提交事务,每次执行一个 sql 语句时,如果执行成功,就会向数据库自动提交,而不能回滚;
    • 关闭数据库连接,数据就会自动提交。如果多个操作,每个操作使用的是自己单独的连接,则无法保证事务,即同一个事务的多个操作必须在同一个连接下。
  • JDBC 程序中为了让多个 SQL 语句作为一个事务执行:

    • 调用 Connection 对象的 setAutoCommit(false); 以取消自动提交事务;
    • 在所有的 SQL 语句都成执行后,调用 commit(); 方法提交事务;
    • 在出现异常时,调用 rollback(); 方法回滚事务;

    若此时 Connection 连接没有被关闭,还可能被重复使用,那么需要恢复其自动提交状态 setAutoCommit(true)。尤其是在使用数据库连接池技术时,执行 close() 方法前,最好恢复自动提交状态。

【案例:用户 AA 向用户 BB 转账 100】

/**
     * JDBC处理数据转账案例
     */
    @Test
    public void transfer(){
        Connection conn = null;
        try {
            //1.获取连接
            conn = JDBCUtils.getConnection();
            //2.取消自动提交事务
            conn.setAutoCommit(false);
            //3.进行数据库操作
            String subSql = "update user_table set balance=balance-100 where user=?";
            //将执行修改数据的操作封装成方法来调用
            update(conn,subSql,"AA");

            //模拟出现异常
//            System.out.println(1 / 0);

            String addSql = "update user_table set balance=balance+100 where user=?";
            //将执行修改数据的操作封装成方法来调用
            update(conn,addSql,"BB");

            //4.没有出现异常,提交事务
            conn.commit();
        //注意异常接收的类型,如果不能捕获到出现的异常,也就不能处理,也就执行代码块内的回滚操作
        } catch (Exception e) {
            e.printStackTrace();
            //5.出现了异常,则回滚事务
            try {
                conn.rollback();
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        } finally {
            //6.关闭资源前恢复每次DML操作的自动提交功能(针对使用数据库连接池时)
            try {
                conn.setAutoCommit(true);
            } catch (SQLException e) {
                e.printStackTrace();
            }
            //7.关闭资源
            JDBCUtils.close(conn,null,null);
        }
    }

执行增删改操作的方法:

/**
     * @description 使用事务后的通用更新操作
     * @param conn 连接
     * @param sql 执行的sql语句
     * @param params 占位符的参数
     */
    public void update(Connection conn,String sql,Object... params){
        PreparedStatement ps = null;
        try {
            ps = conn.prepareStatement(sql);

            //填充占位符
            for (int i = 0; i < params.length; i++) {
                ps.setObject(i + 1,params[i]);
            }
            ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(null,ps,null);
        }
    }
}

扩展:事务的 ACID 属性

  1. 原子性(Atomicity)

    原子性是指事务是一个不可分割的一个工作单位,事务中的操作要么都发生,要么都不发生

  2. 一致性(Consistency)

    事务必须使数据库从一个一致性状态变换到另一个一致性状态

  3. 隔离性(Isolation)

    事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间互不干扰

  4. 持久性(Durability)

    持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响

  • 数据库的并发问题
    • 对于同时运行的多个事务,当这些事务访问数据库中相同的数据时,如果没有采取必要的隔离措施机制,就会导致各种并发问题:
      • 脏读:对于两个事务 T1、T2, T1 读取了已经被 T2 更新但还没有被提交的字段。之后, 若 T2 回滚, T1读取的内容就是临时且无效的。
      • 不可重复读: 对于两个事务T1, T2, T1 读取了一个字段, 然后 T2 更新了该字段。之后, T1再次读取同一个字段, 值就不同了。
      • 幻读: 对于两个事务T1, T2, T1 从一个表中读取了一个字段, 然后 T2 在该表中插入了一些新的行。之后, 如果 T1 再次读取同一个表, 就会多出几行。
  • 数据库事务的隔离性:数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。
  • 一个事务与其他事务的隔离程度称为隔离级别。数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱
    • 四种隔离级别:

1555586275271

​ Oracle 支持的 2 种事务隔离级别:READ COMMITED,SERIALIZABLE。 Oracle 默认的事务隔离级别为: READ COMMITED

​ Mysql 支持 4 种事务隔离级别。Mysql 默认的事务隔离级别为: REPEATABLE READ。

  • 在 MySQL 中设置隔离级别

    ​ 每启动一个 mysql 程序,就会获得一个单纯的数据库连接,每个数据库连接都有一个全局变量 @@tx_isolation,表示当前的事务隔离级别。

    • 查看当前的隔离级别:

      SELECT @@tx_isolation;
      
    • 设置当前 mysql 连接的隔离级别:

      set  transaction isolation level read committed;
      
    • 设置数据库系统的全局隔离级别:

      set global transaction isolation level read committed;
      
    • 补充操作:[ tom 为用户名]

      • 创建 mysql 数据库用户:

        create user tom identified by 'abc123';
        
      • 授予权限

        #授予通过网络方式登录的tom用户,对所有库所有表的全部权限,密码设为abc123.
        grant all privileges on *.* to tom@'%'  identified by 'abc123'; 
        
         #给tom用户使用本地命令行方式,授予atguigudb这个库下的所有表的插删改查的权限。
        grant select,insert,delete,update on atguigudb.* to tom@localhost identified by 'abc123'; 
        
        

13.6 DAO 及相关实现类

DAO(Data Access Object): 访问数据信息的类和接口,包括了对数据的 CRUD(Create Retrival、Update、Delete),而不包括任何业务相关的信息。也称为 BaseDAO

作用:为了实现功能的模块化,更有利于代码的维护和升级。

操作范例:

针对数据表 customers 进行操作:(如图为被 CRUD 后的数据)

image-20200427152443593

BaseDAO.java :

package dao;

import jdbc.JDBCUtils;

import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * @Description 封装针对数据表通用的操作
 * @author YH
 * @create 2020-04-27 6:47
 */
public class BaseDAO {
    /**
     * @description 通用的增删改操作(考虑上事务)
     * @param conn 数据库连接对选哪个
     * @param sql 执行的SQL语句
     * @param params 占位符参数
     * @return
     */
    public int update(Connection conn,String sql, Object...params) {
        PreparedStatement ps = null;
        try {
            //1.预编译sql语句,获取PrepareStatement的实例
            ps = conn.prepareStatement(sql);
            for (int i = 0; i < params.length; i++) {
                //2.填充占位符
                ps.setObject(i + 1,params[i]);
            }
            //执行并返回受影响行数
            return  ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(null,ps,null);
        }
        return 0;
    }

    /**
     * 通用查询操作,用于返回封装数据表中一条记录的对象(考虑上事务)
     * @param conn 数据库连接
     * @param clazz 表格JavaBean类的Class对象
     * @param sql 要执行的SQL语句
     * @param params 占位符参数
     * @param <T> 表格JavaBean类型
     * @return
     */
    public <T> T getInstance(Connection conn,Class<T> clazz,String sql,Object...params){
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            //1.预编译SQL语句,获取PrepareStatement实例
            ps = conn.prepareStatement(sql);
            for (int i = 0; i < params.length; i++) {
                //2.设置占位符
                ps.setObject(i + 1,params[i]);
            }
            //3.执行,获取结果集
            rs = ps.executeQuery();
            //4.获取结果集元数据对象
            ResultSetMetaData metaData = rs.getMetaData();
            //5.获取结果集的列数
            int columnCount = metaData.getColumnCount();
            //6.判断结果集中是否有记录
            if(rs.next()) {
                //6.1 创建对象实例
                T t = clazz.newInstance();
                //遍历结果集中的每一列
                for (int i = 0; i < columnCount; i++) {
                    //6.2获取列值
                    Object columnValue = rs.getObject(i + 1);
                    //6.3获取列的别名(列名)
                    String columnLabel = metaData.getColumnLabel(i + 1);
                    //7.利用反射获取本对象的field对象
                    Field field = clazz.getDeclaredField(columnLabel);
                    //7.1设置私有属性可被访问
                    field.setAccessible(true);
                    //7.2给本对象此属性赋值
                    field.set(t,columnValue);
                }
                return t;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(null,ps,rs);
        }
        return null;
    }
    /**
     * 通用查询操作,用于返回封装数据表中所有记录的对象集合(考虑上事务)
     * @param conn 数据库连接
     * @param clazz 表格JavaBean类的Class对象
     * @param sql 要执行的SQL语句
     * @param params 占位符参数
     * @param <T> 表格JavaBean类型
     * @return
     */
    public <T> List<T> getForList(Connection conn, Class<T> clazz, String sql, Object...params){
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            //1.预编译SQL语句,获取PrepareStatement实例
            ps = conn.prepareStatement(sql);
            for (int i = 0; i < params.length; i++) {
                //2.设置占位符
                ps.setObject(i + 1,params[i]);
            }
            //3.执行,获取结果集
            rs = ps.executeQuery();
            //4.获取结果集元数据对象
            ResultSetMetaData metaData = rs.getMetaData();
            //5.获取结果集的列数
            int columnCount = metaData.getColumnCount();
            //6.声明一个存储JavaBean对象的List
            List<T> list = new ArrayList<>();
            //6.判断结果集中是否有记录
            while(rs.next()) {
                //6.1 创建对象实例
                T t = clazz.newInstance();
                //遍历结果集中的每一列
                for (int i = 0; i < columnCount; i++) {
                    //6.2获取列值
                    Object columnValue = rs.getObject(i + 1);
                    //6.3获取列的别名(列名)
                    String columnLabel = metaData.getColumnLabel(i + 1);
                    //7.利用反射获取本对象的field对象
                    Field field = clazz.getDeclaredField(columnLabel);
                    //7.1设置私有属性可被访问
                    field.setAccessible(true);
                    //7.2给本对象此属性赋值
                    field.set(t,columnValue);
                }
                list.add(t);
            }
            return list;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(null,ps,rs);
        }
        return null;
    }

    /**
     * 用于查询特殊值的通用方法(如最大值、总数、平均值等)
     * @param conn 数据库连接
     * @param sql 要执行的SQL语句
     * @param params 占位符参数
     * @param <E> 返回的数据类型
     * @return
     */
    public <E> E getValue(Connection conn,String sql,Object...params){
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            ps = conn.prepareStatement(sql);
            for (int i = 0; i < params.length; i++) {
                ps.setObject(i + 1,params[i]);
            }
            rs = ps.executeQuery();
            if(rs.next()){
                return (E) rs.getObject(1);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(null,ps,rs);
        }
        return null;
    }
}

CustomerDAO.java :

package dao;

import bean.Customer;

import java.sql.Connection;
import java.util.Date;
import java.util.List;

/**
 * @description 此接口用于规范针对于customers表的常用操作
 * @author YH
 * @create 2020-04-27 10:03
 */
public interface CustomerDAO {
    /**
     * @description 将cust对象添加到数据库中
     * @param conn
     * @param cust
     */
    void insert(Connection conn, Customer cust);

    /**
     * 通过指定id删除记录
     * @param conn
     * @param id
     */
    void deleteById(Connection conn,int id);

    /**
     * 针对内存中的cust对象,去修改数据表中指定的记录
     * @param conn
     * @param cust
     */
    void update(Connection conn,Customer cust);

    /**
     * 通过指定id查询得到对应的Customer对象
     * @param conn
     * @param id
     * @return
     */
    Customer getCustomerById(Connection conn,int id);

    /**
     * 查询返回表中所有记录的对象构成的List集合
     * @param conn
     * @return
     */
    List<Customer> getAll(Connection conn);

    /**
     * 查询表中所有数据的条目数
     * @param conn
     * @return
     */
    long getCount(Connection conn);

    /**
     * 返回数据表中最大的生日
     * @param conn
     * @return
     */
    Date getMaxBirth(Connection conn);
}

CustomerDAOImp.java

package dao;

import bean.Customer;

import java.sql.Connection;
import java.sql.Date;
import java.util.List;

/**
 * 针对Customer表的具体实现
 * @author YH
 * @create 2020-04-27 10:13
 */
public class CustomerDAOImp extends BaseDAO implements CustomerDAO {

    @Override
    public void insert(Connection conn, Customer cust) {
        //向数据库插入数据的sql语句
        String sql = "insert into customers(name,email,birth) values(?,?,?)";
        //使用父类BaseDAO通用的更新方法
        update(conn,sql,cust.getName(),cust.getEmail(),cust.getBirth());
    }

    @Override
    public void deleteById(Connection conn,int id){
        String sql = "delete from customers where id=?";
        update(conn,sql,id);
    }

    @Override
    public void update(Connection conn,Customer cust){
        String sql = "update customers set name=?,email=?,birth=? where id=?";
        update(conn,sql,cust.getName(),cust.getEmail(),cust.getBirth(),cust.getId());
    }

    @Override
    public Customer getCustomerById(Connection conn, int id){
        String sql = "select id,name,email,birth from customers where id=?";
        return getInstance(conn,Customer.class,sql,id);
    }

    @Override
    public List<Customer> getAll(Connection conn){
        String sql = "select id,name,email,birth from customers";
        return getForList(conn,Customer.class,sql);
    }

    @Override
    public long getCount(Connection conn){
        String sql = "select count(*) from customers";
        return getValue(conn,sql);
    }

    @Override
    public Date getMaxBirth(Connection conn){
        String sql = "select max(birth) from customers";
        return getValue(conn,sql);
    }

    /**
     * 添加中文时出现SQL异常并提示字符串值不正确,需修改编码解决
     * @param conn
     * @param character
     */
    public void charcter(Connection conn,String character){
        String sql = "alter table customers change name name varchar(20) character set ?";
        update(conn,sql,character);
    }

    /**
     *
     */

}

测试代码:

package junit;

import bean.Customer;
import dao.CustomerDAOImp;
import jdbc.JDBCUtils;
import org.junit.Test;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Date;
import java.util.List;

/**
 * @author YH
 * @create 2020-04-27 11:28
 */
public class CustomerDAOImpTest {
    CustomerDAOImp dao = new CustomerDAOImp();

    @Test
    public void testCharset(){
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            dao.charcter(conn,"utf8");
            System.out.println("编码修改成功");
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn,null,null);
        }
    }

    @Test
    public void testInert(){
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            Customer cust = new Customer(1, "云", "yunhe@qq.com", new Date(43534646435L));
            dao.insert(conn, cust);
            System.out.println("添加成功!");
        } catch (SQLException e) {
            e.printStackTrace();
        }finally{
            JDBCUtils.close(conn,null,null);
        }
    }

    @Test
    public void testDeleteById(){
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            dao.deleteById(conn,1);
            System.out.println("删除成功");
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn,null,null);
        }
    }

    @Test
    public void testUpdate(){
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            dao.update(conn,new Customer(5,"云翯","yunhe@qq.com",new Date(349803493049L)));
            System.out.println("修改成功");
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn,null,null);
        }
    }

    @Test
    public void testGetCustomerById(){
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            Customer customer = dao.getCustomerById(conn, 5);
            System.out.println(customer.toString());
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn,null,null);
        }
    }

    @Test
    public void testGetAll(){
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            List<Customer> list = dao.getAll(conn);
            list.forEach(System.out::println);
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn,null,null);
        }
    }

    @Test
    public void testGetCount(){
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            System.out.println(dao.getCount(conn));
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn,null,null);
        }
    }

    @Test
    public void testGetMaxBirth(){
        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection();
            System.out.println(dao.getMaxBirth(conn).toString());
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.close(conn,null,null);
        }
    }

}

个人体会:

  1. 首先封装一个通用操作的 DAO(或BaseDAO)作为一个实现数据库和 Java 之间交互的基础,封装了针对数据表的通用操作;
  2. 建立针对特定表的规范,即提供对应表格的接口并声明常用操作的方法;
  3. 最后创建继承 DAO(或 BaseDAO)、且实现表格对应接口的实现类,进行具体的交互(即 CRUD 等操作)

13.7 数据库连接池

  • JDBC 数据库连接池的必要性

    在使用开发基于数据库的 web 程序时,传统模式基本是:在主程序(如servlet、beans)中建立连接、进行 sql 操作、断开数据库连接,这三个步骤。这种模式开发,存在的问题:

    • 每次向数据库要求一个连接都要经过将 Connection 加载到内存,在验证用户名和密码(花费0.05s~1s),执行完后在断开连接,这种方式会消耗大量数据库资源,数据库连接的资源并没有得到很好的重复利用。若同时又几百人甚至几千人在线可能会导致服务器崩溃。
    • 对于每一次数据库连接,使用完成都得断开。如果未能关闭,将会导致数据库系统中的内存泄漏。
    • 不能控制被创建的连接对象树。系统资源会被毫无顾忌的分配出去,如连接过多也可能导致内存泄漏,服务器崩溃。
  • 数据库库连接池技术
  1. 使用数据库连接池优化程序性能

    数据库连接池的基本思想就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。如图:

    1555593464033

    工作原理:

    1555593598606

    ​ 数据库在初始化时就创建一定量的连接放到数据库连接池,通过设置最小数据库连接数(和最大数据库连接数来限定数据库连接池的连接数量,当应用程序向数据库请求的连接数量大于数据库连接池中的数量时,这些请求将被加入等待队列中。

    ​ 数据库中的最小连接数和最大连接数的设置要考虑的因素:

    • 最小连接数:是连接池一直保持的数据库连接,所以如果应用程序对数据库连接的使用量不大,未被使用的数据库连接将被浪费;
    • 最大连接数:是连接池能申请的最大连接数,如果数据库连接请求超过次数,后面的数据库连接请求将被加入到等待队列中,这会影响后来的数据库操作;

数据库连接池的优点:

  1. 资源重用

    数据库连接得以重用,避免了频繁创建、释放连接所要开销,同时增加了系统运行环境的平稳性。

  2. 更快的反应速度

    数据库连接池初始化过程中往往已经创建了若干连接备用,所以对于业务请求处理而言,可以直接利用现有的连接,避免了数据库连接初始化和释放过程的时间开销。

  3. 新的资源分配手段

    对于多重应用共享同一数据库而言,可在应用层通过数据库连接池的配置,实现某一应用最大可用数据库连接数的限制,避免某一应用独占所有的数据库资源。

  4. 同一的连接管理,避免数据库连接泄漏

    可预设占用超时参数,强制回收被占用连接,从而避免了常规数据库连接操作中可能出现的资源泄漏。

  5. 使用开源的数据库连接池

    开发的数据库连接池是第三方对 DataSoruce 的实现(编写连接池需实现 java.sql.DataSource 接口),即连接池的实现,也称为数据源

    注意:

    • 数据源和数据库连接不同,数据源无需创建多个,它是产生数据库连接的工厂,因此整个应用只需要一个数据源即可。

    • 当数据库访问结束后,程序还是像以前一样关闭数据库连接:conn.close(); 但conn.close()并没有关闭数据库的物理连接,它仅仅把数据库连接释放,归还给了数据库连接池。

    一些开源组织提供的数据源的独立实现:

    • DBCP
    • C3P0 Tomcat 服务器默认
    • Druid(德鲁伊)
  6. Druid(德鲁伊)数据源

    Druid 是阿里开源平台上的一个数据库连接池实现,有强大的日志监控功能,据说是目前最好的连接池。

    使用范例:

    import com.alibaba.druid.pool.DruidDataSourceFactory;
    
    import javax.sql.DataSource;
    import java.io.FileInputStream;
    import java.sql.Connection;
    import java.util.Properties;
    /**
     * 使用Druid连接池获取连接
     * @author YH
     * @create 2020-04-24 10:31
     */
    public class DruidTest {
        public static void main(String[] args) throws Exception{
            Properties pro = new Properties();
            pro.load(new FileInputStream("JDBC/src/main/resources/druid.properties"));
            DataSource ds = DruidDataSourceFactory.createDataSource(pro);
            int i = 0;
            while(i < 100){
                Connection conn = ds.getConnection();
                System.out.println("第" + i + "个连接:" + conn);
                i++;
            }
        }
    }
    
    url=jdbc:mysql://localhost:3306/class7?rewriteBatchedStatements=true
    username=root
    password=rootMySQL
    driverClassName=com.mysql.jdbc.Driver
    initialSize=10
    maxActive=20
    maxWait=100
    filters=wall
    

    可以看出,配置文件中设置了maxActive 最大活跃数后,只能同时获取20个连接

    image-20200424105154116

如果及时释放连接:

image-20200424115253063

  1. DruidDataSource 配置属性:
配置 缺省值 说明
name 配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。如果没有配置,将会生成一个名字,格式是:"DataSource-" + System.identityHashCode(this). 另外配置此属性至少在1.0.5版本中是不起作用的,强行设置name会出错。详情-点此处
url 连接数据库的url,不同数据库不一样。例如: mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto
username 连接数据库的用户名
password 连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter。详细看这里
driverClassName 根据url自动识别 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName
initialSize 0 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
maxActive 8 最大连接池数量
maxIdle 8 已经不再使用,配置了也没效果
minIdle 最小连接池数量
maxWait 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
poolPreparedStatements false 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
maxPoolPreparedStatementPerConnectionSize -1 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
validationQuery 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
validationQueryTimeout 单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法
testOnBorrow true 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnReturn false 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testWhileIdle false 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
keepAlive false (1.0.28) 连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作。
timeBetweenEvictionRunsMillis 1分钟(1.0.14) 有两个含义: 1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。 2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
numTestsPerEvictionRun 30分钟(1.0.14) 不再使用,一个DruidDataSource只支持一个EvictionRun
minEvictableIdleTimeMillis 连接保持空闲而不被驱逐的最小时间
connectionInitSqls 物理连接初始化的时候执行的sql
exceptionSorter 根据dbType自动识别 当数据库抛出一些不可恢复的异常时,抛弃连接
filters 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat 日志用的filter:log4j 防御sql注入的filter:wall
proxyFilters 类型是List<com.alibaba.druid.filter.Filter>,如果同时配置了filters和proxyFilters,是组合关系,并非替换关系

13.8 Apache-DBUtils 实现 CRUD 操作

​ commons-dbutils 是 Apache 组织提供的一个开源 JDBC 工具类,它是对 JDBC 的简单封装

  • API
    • org.apache.commons.dbutils.QueryRunner :提供数据库操作的一系列重载的 update() 和 query() 操作(类似上面的 BaseDAO);
    • 接口 org.apache.commons.dbutils.ResultSetHandler:此接口用于处理查询返回的结果集,不同的结果集情形由其 不同的子类来实现(类似上面针对特定表格规范的接口);
    • 工具类:org.apache.commons.dbutils.DbUtils:提供如关闭连接、装载JDBC驱动程序等常规工作的工具类,里面的所有方法都是静态的。主要方法如下
      • public static void close(...) throws java.sql.SQLException:DbUtils类提供了三个重载的关闭方法。这些方法检查所提供的参数是不是NULL,如果不是的话,它们就关闭Connection、Statement和ResultSet。
      • public static void closeQuietly(...):这一类方法不仅能在Connection、Statement和ResultSet为NULL情况下避免关闭,还能隐藏一些在程序中抛出的SQLEeception。
      • public static void commitAndClose(Connection conn) throws SQLException:用来提交连接的事务,然后关闭连接。
      • public static commitAndCloseQuietly(Connection conn):用来提交连接,然后关闭连接,并且在关闭连接时不抛出SQL异常。
      • public static rollback(Connection conn) throws SQLException:回滚给定连接所做的修改(允许 conn 为 null,因为方法内部做了判断)
      • public static void rollbackAndClose(Connection conn)throws SQLException:回滚给定连接所做的修改并关闭资源
      • public static void rollbackAndCloseQuietly(Connection):回滚给定连接所做的修改并关闭资源,并且在关闭连接时不抛出SQL异常
      • public static boolean loadDriver(java.lang.String driverClassName):这一方装载并注册JDBC驱动程序,如果成功就返回true。使用该方法,你不需要捕捉这个异常ClassNotFoundException。
package dbutils;

import java.sql.Connection;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.ResultSetHandler;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.MapHandler;
import org.apache.commons.dbutils.handlers.MapListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import org.junit.Test;

import com.atguigu2.bean.Customer;
import com.atguigu4.util.JDBCUtils;

/*
 * commons-dbutils 是 Apache 组织提供的一个开源 JDBC工具类库,封装了针对于数据库的增删改查操作
 * 
 */
public class QueryRunnerTest {
	
	//测试插入
	@Test
	public void testInsert() {
		Connection conn = null;
		try {
			QueryRunner runner = new QueryRunner();
			conn = JDBCUtils.getConnection3();
			String sql = "insert into customers(name,email,birth)values(?,?,?)";
			int insertCount = runner.update(conn, sql, "蔡徐坤","caixukun@126.com","1997-09-08");
			System.out.println("添加了" + insertCount + "条记录");
		} catch (SQLException e) {
			e.printStackTrace();
		}finally{
			JDBCUtils.closeResource(conn, null);
		}
	}
	
	//测试查询
	/*
	 * BeanHander:是ResultSetHandler接口的实现类,用于封装表中的一条记录。
	 */
	@Test
	public void testQuery1(){
		Connection conn = null;
		try {
			QueryRunner runner = new QueryRunner();
			conn = JDBCUtils.getConnection3();
			String sql = "select id,name,email,birth from customers where id = ?";
			BeanHandler<Customer> handler = new BeanHandler<>(Customer.class);
			Customer customer = runner.query(conn, sql, handler, 23);
			System.out.println(customer);
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally{
			JDBCUtils.closeResource(conn, null);
		}
	}
	
	/*
	 * BeanListHandler:是ResultSetHandler接口的实现类,用于封装表中的多条记录构成的集合。
	 */
	@Test
	public void testQuery2() {
		Connection conn = null;
		try {
			QueryRunner runner = new QueryRunner();
			conn = JDBCUtils.getConnection3();
			String sql = "select id,name,email,birth from customers where id < ?";
			
			BeanListHandler<Customer>  handler = new BeanListHandler<>(Customer.class);

			List<Customer> list = runner.query(conn, sql, handler, 23);
			list.forEach(System.out::println);
		} catch (SQLException e) {
			e.printStackTrace();
		}finally{
			JDBCUtils.closeResource(conn, null);
		}
	}
	
	/*
	 * MapHander:是ResultSetHandler接口的实现类,对应表中的一条记录。
	 * 将字段及相应字段的值作为map中的key和value
	 */
	@Test
	public void testQuery3(){
		Connection conn = null;
		try {
			QueryRunner runner = new QueryRunner();
			conn = JDBCUtils.getConnection3();
			String sql = "select id,name,email,birth from customers where id = ?";
			MapHandler handler = new MapHandler();
			Map<String, Object> map = runner.query(conn, sql, handler, 23);
			System.out.println(map);
		} catch (SQLException e) {
			e.printStackTrace();
		}finally{
			JDBCUtils.closeResource(conn, null);	
		}
		
	}
	
	/*
	 * MapListHander:是ResultSetHandler接口的实现类,对应表中的多条记录。
	 * 将字段及相应字段的值作为map中的key和value。将这些map添加到List中
	 */
	@Test
	public void testQuery4(){
		Connection conn = null;
		try {
			QueryRunner runner = new QueryRunner();
			conn = JDBCUtils.getConnection3();
			String sql = "select id,name,email,birth from customers where id < ?";
		
			MapListHandler handler = new MapListHandler();
			List<Map<String, Object>> list = runner.query(conn, sql, handler, 23);
			list.forEach(System.out::println);
		} catch (SQLException e) {
			e.printStackTrace();
		}finally{
			JDBCUtils.closeResource(conn, null);	
		}
		
	}
	/*
	 * ScalarHandler:用于查询特殊值
	 */
	@Test
	public void testQuery5(){
		Connection conn = null;
		try {
			QueryRunner runner = new QueryRunner();
			conn = JDBCUtils.getConnection3();
			
			String sql = "select count(*) from customers";
			
			ScalarHandler handler = new ScalarHandler();
			
			Long count = (Long) runner.query(conn, sql, handler);
			System.out.println(count);
		} catch (SQLException e) {
			e.printStackTrace();
		}finally{
			JDBCUtils.closeResource(conn, null);	
		}
	}
	@Test
	public void testQuery6(){
		Connection conn = null;
		try {
			QueryRunner runner = new QueryRunner();
			conn = JDBCUtils.getConnection3();
			
			String sql = "select max(birth) from customers";
			
			ScalarHandler handler = new ScalarHandler();
			Date maxBirth = (Date) runner.query(conn, sql, handler);
			System.out.println(maxBirth);
		} catch (SQLException e) {
			e.printStackTrace();
		}finally{
			JDBCUtils.closeResource(conn, null);	
		}
	}

	/*
	 * 自定义ResultSetHandler的实现类
	 */
	@Test
	public void testQuery7(){
		Connection conn = null;
		try {
			QueryRunner runner = new QueryRunner();
			conn = JDBCUtils.getConnection3();
			
			String sql = "select id,name,email,birth from customers where id = ?";
			ResultSetHandler<Customer> handler = new ResultSetHandler<Customer>(){

				@Override
				public Customer handle(ResultSet rs) throws SQLException {
//					System.out.println("handle");
//					return null;
//					return new Customer(12, "成龙", "Jacky@126.com", new Date(234324234324L));
					
					if(rs.next()){
						int id = rs.getInt("id");
						String name = rs.getString("name");
						String email = rs.getString("email");
						Date birth = rs.getDate("birth");
						Customer customer = new Customer(id, name, email, birth);
						return customer;
					}
					return null;
				}
			};
			Customer customer = runner.query(conn, sql, handler,23);
			System.out.println(customer);
		} catch (SQLException e) {
			e.printStackTrace();
		}finally{
			JDBCUtils.closeResource(conn, null);
		}
	}
}

十四、Filter 过滤器

14.1 Filter 简介

​ Filter 也称为过滤器,通过 Filter 技术可对 web 服务器管理所有的 web 资源:例如 jsp、Servlet 静态图片文件或静态 HTML 文件等进行拦截,从而实现一些特殊的功能。例如实现 URL 级别的权限访问控制、过滤敏感词汇、压缩响应信息等一些高级功能。

​ Servlet API 中提供了一个 Filter 接口,开发 web 应用时,如果编写的 Java 类实现了这个接口,则把这个 java 类称之为过滤器 Filter。通过 Filter 技术,开发人员可实现用户在访问某个目标资源之前,对访问的请求和响应进行拦截,如下所示:

image-20200428103042792

  • 过滤器位于客户端和web应用程序之间,用于检查和修改两者之间流过的请求和响应;
  • 在请求到达Servlet/JSP之前,过滤器截获请求;
  • 在响应送给客户端之前,过滤器截获响应;
  • 最先截获客户端请求的过滤器将最后截获Servlet/JSP的响应信息;

多个过滤器形成一个过滤器链,过滤器链中不同过滤器的先后顺序由部署文件web.xml中过滤器映射的顺序决定,过程如下:

image-20200428114707920

14.2 Filter 是如何实现拦截

​ Filter 接口中有一个 doFilter 方法,当我们编写好 Filter,并配置对哪个 web 资源进行拦截后,web 服务器每次在调用 web 资源的 service 方法之前,都会先调用一下 Filter 的 doFilter() 方法,因此该方法内编写的编写的代码可以达到如下目的:

  • 调用目标资源之前,让一段代码执行;

  • 是否调用目标资源,即是否让用户访问 web 资源;

  • 调用目标资源后,让一段代码执行;

    web 服务器在调用 doFilter() 方法时,会传递一个 filterChain 对象进来,filterChain 对象是 filter 接口中最重要的一个对象,它也提供了一个 doFilter() 方法,开发人员可以根据需求决定是否调用此方法,调用该方法,web 服务器就调用 web 资源的 service() 方法,即 web 资源被访问;反之不会被访问。

14.3 Filter 开发

Filter 开发步骤

  • 编写 java 类实现 Filter 接口,并实现其 doFilter() 方法;
  • 在 web.xml 文件中使用 元素对编写的 filter 类进行注册,并设置它所能拦截的资源;

范例:

web.xml 配置:

<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                      http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0"
         metadata-complete="true">
    
<!--  配置过滤器-->
  <filter>
    <description>FilterDemo</description>
    <filter-name>FilterDemo</filter-name>
    <filter-class>filter.FilterDemo</filter-class>
  </filter>
    
<!--针对一个URL pattern做过滤-->
  <filter-mapping>
    <filter-name>FilterDemo</filter-name>
<!--    表示拦截所有请求-->
    <url-pattern>/*</url-pattern>
  </filter-mapping>
    
<!--  针对一个Servlet做过滤
  <filter-mapping>
    <filter-name>FilterDemo</filter-name>
    <url-pattern>FilterDemo</url-pattern>
  </filter-mapping>-->
</web-app>

Filter 接口实现类:

package filter;

import javax.servlet.*;

import java.io.IOException;

/**
 * Filter 过滤器使用
 * @author YH
 * @create 2020-04-28 11:00
 */
    public class FilterDemo implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //对request和response做一些预处理
        servletRequest.setCharacterEncoding("utf-8");
        servletResponse.setCharacterEncoding("utf-8");
        servletResponse.setContentType("text/html;charset=utf-8");
        System.out.println("FilterDemo过滤器执行前");
        //让目标资源执行(放行)
        filterChain.doFilter(servletRequest, servletResponse);
        System.out.println("FilterDemo过滤器执行后");
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("过滤器初始化...");
    }

    @Override
    public void destroy() {
        System.out.println("过滤器销毁");
    }
}

当浏览器访问 web 资源时,会经过过滤器,执行结果:

image-20200428145148767

可以看出 Filter 过滤器的 doFilter() 调用流程基本与 service() 一致。

补充:可以看出,执行了两次 Filter 的 doFilter() 方法(service() 也是),个人理解是请求的时候调用一次,响应时又调用一次;过程:浏览器发送请求 --- WEB服务器(web 容器)--- 第一次调用 --- 访问 servlet/jsp 程序 --- 第二次调用 --- WEB服务器发送响应 --- 浏览器接收

  • Filter 链

    在一个 web 应用中,可以开发编写多个 Filter,这些 Filter 组合起来称之为一个 Filter 链。

web 服务器根据 Filter 在 web.xml 文件中的注册顺序,决定先调用哪个 Filter,第一个 Filter 的 doFilter)() 方法被调用时,web 服务器会创建一个代表 Filter 链的 FilterChain 对象传递给该方法,在 doFilter() 方法中,开发人员如果调用了 FilterChain 对象 doFilter() 方法,则 web 服务器会检查 FilterChain 对象中是否还有 Filter,如果有,则调用第 2 个 Filter,否则调用目标资源。

过滤器逻辑与Servlet逻辑不同,它不依赖于任何用户状态信息,因为一个过滤器实例可能同时处理多个完全不同的请求。

14.4 Filter 生命周期

  1. 创建

    Filter 的创建和销毁由 web 服务器负责,web 应用程序启动时,web 服务器将创建 Filter 的实例对象,并调用其 init() 方法,完成对象的初始化功能,从而为后继的用户请求做好拦截的准备,filter 对象只会创建一次,init() 方法也只会执行一次。通过 init() 方法的参数,可获得当前 filter 配置信息的 FilterConfig 对象。

  2. 销毁

    ​ Web 容器调用 destroy() 方法销毁 Filter。destroy() 方法在 Filter 的生命周期中仅执行一次,在 destroy() 方法中可以释放过滤器使用的资源。

  3. FilterConfig 接口

    ​ 用户在配置 filter 时,可以使用 为 Filter 配置一些初始化参数,当 web 容器实例化 Filter 对象,调用其 init 方法时,会把封装了 filter 初始化参数的 filterConfig 对象传递进来。因此开发人员在编写 filter 时,通过 filterConfig 对象的方法,就可获得:

    ​ String getFilterName():得到 filter 的名称;

    ​ String getInitParameter(String name):返回在部署描述中指定名称的初始化参数的值,不存在返回 null;

    ​ Enumeration getInitParameterNames():返回过滤器的所有初始化参数的的名字的枚举集合;

    ​ public ServletContext getServletContext():返回 Servlet 上下文对象的引用;

范例:利用 FilterConfig 得到 filter 配置信息

web.xml 文件中的配置:

...
<filter>
  <filter-name>FilterDemo</filter-name>
  <filter-class>filter.FilterDemo</filter-class>+
  <init-param>
    <param-name>YH</param-name>
    <param-value>study java</param-value>
  </init-param>
  <init-param>
    <param-name>content</param-name>
    <param-value>JavaWeb</param-value>
  </init-param>
</filter>
...

filter 实现类代码:

package filter;

import javax.servlet.*;

import java.io.IOException;
import java.util.Enumeration;

/**
 * Filter 过滤器使用
 * @author YH
 * @create 2020-04-28 11:00
 */
    public class FilterDemo implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //对request和response做一些预处理
        servletRequest.setCharacterEncoding("utf-8");
        servletResponse.setCharacterEncoding("utf-8");
        servletResponse.setContentType("text/html;charset=utf-8");
        System.out.println("FilterDemo过滤器执行前");
        //让目标资源执行(放行)
        filterChain.doFilter(servletRequest, servletResponse);
        System.out.println("FilterDemo过滤器执行后");
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("过滤器初始化...");
        //获取过滤器的名字
        String filterName = filterConfig.getFilterName();
        //获取在web.xml文件中配置的初始化参数
        String initParam1 = filterConfig.getInitParameter("YH");
        String initParam2 = filterConfig.getInitParameter("content");
        //返回过滤器的所有初始化参数的名字的枚举集合
        Enumeration<String> initParameterNames = filterConfig.getInitParameterNames();
        System.out.println(filterName);
        System.out.println(initParam1);
        System.out.println(initParam2);
        while(initParameterNames.hasMoreElements()){
            String paramName = (String)initParameterNames.nextElement();
            System.out.println(paramName);
        }
    }

    @Override
    public void destroy() {
        System.out.println("过滤器销毁");
    }
}

image-20200428205241168

14.5 Filter 的部署

分为两个步骤:

  1. 注册 Filter
  2. 映射 Filter
  • 注册 Filter

    开发好 Filter 之后,需要在 web.xml 文件中进行注册,这样才能够被 web 服务器调用

    web.xml 配置文件中注册 Filter 的范例:

    <filter>
        <description>FilterDemo过滤器</description>
        <filter-name>FilterDemo</filter-name>
        <filter-class>filter.FilterDemo</filter-class>+
        <init-param>
          <description>配置FilterDemo过滤器的初始化参数1</description>
          <param-name>YH</param-name>
          <param-value>study java</param-value>
        </init-param>
        <init-param>
          <description>配置FilterDemo过滤器的初始化参数2</description>
          <param-name>content</param-name>
          <param-value>JavaWeb</param-value>
        </init-param>
      </filter>
    

    用于添加描述信息,该元素的内容可为空,也可以不配置

    用于为过滤器指定一个名字,该元素的内容不能为空

    用于指定过滤器完整的限定类名(全类名)

    用于为过滤器指定初始化参数,其子元素以键值对的形式指定 key 和 value;可以使用 FilterConfig 接口对象来访问初始化参数。

  • 映射 Filter

    <!--针对一个URL pattern做过滤-->
      <filter-mapping>
        <filter-name>FilterDemo</filter-name>
    	<!-- 表示拦截(过滤)所有请求-->
        <url-pattern>/*</url-pattern>
      </filter-mapping>
    
    <!--  针对一个servlet做过滤-->
      <filter-mapping>
        <filter-name>FilterDemo</filter-name>
        <servlet-name>FilterDemo</servlet-name>
      </filter-mapping>
    

    用于设置一个 filter 所负责拦截的资源,一个 Filter 拦截的资源可通过两种方式来指定:servlet 名称资源访问的请求路径

    用于设置 filter 的注册名称,必须与 中声明的 name 一样

    设置 filter 所拦截的 web 资源的请求访问路径

    设置 filter所 拦截的 servlet 名称

    指定过滤器所拦截的资源被 Servlet 容器调用的方式,可以是 RRQUEST,INCLUDE,FORWARD 和 ERROR 之一,默认 REQUEST 。可以设置多个 子元素用来指定 Filter 对资源的多种调用方式进行拦截,如下:

    <!--  指定过滤器所拦截指定 Servlet 容器调用方式的资源-->
      <filter-mapping>
        <filter-name>FilterDemo</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
      </filter-mapping>
    

    子元素可以设置的值及其意义:

    • REQUEST:当用户直接访问页面时,Web 容器将会调用过滤器。如果目标资源是通过 RequestDispatcher 的 include() 或 forward() 方法访问时,那么该过滤器就不会被调用。

    • INCLUDE:如果目标资源是通过 RequestDispatcher 的 include() 方法访问时,那么该过滤器将被调用。除此之外,该过滤器不会被调用。

    • FORWARD:如果目标资源是通过 RequestDispatcher 的 forward() 方法访问时,那么该过滤器将被调用,除此之外,该过滤器不会被调用。

    • ERROR:如果目标资源是通过声明式异常处理机制调用时,那么该过滤器将被调用。除此之外,过滤器不会被调用。

十五、Listener 监听器

​ 监听器在 JavaWeb 开发中用得比较多,监听器(Listener)在开发中的常见应用:

15.1 统计当前在线人数

​ 在 JavaWeb 应用开发中,有时候我们需要统计当前在线的用户数,此时就可以使用监听器技术来实现这个功能了:

package listener;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

/**
 * 监听器:统计当前在线人数
 * @author YH
 * @create 2020-04-29 7:18
 */
public class ListenerDemo1 implements HttpSessionListener {
    /**
     * 监听服务器被访问的情况进行增加在线人数
     * @param se
     */
    @Override
    public void sessionCreated(HttpSessionEvent se) {
        ServletContext context = se.getSession().getServletContext();
        Integer onLineCount = (Integer) context.getAttribute("onLineCount");
        if(onLineCount == null){
            context.setAttribute("onLineCount",1);
        } else{
            onLineCount++;
            context.setAttribute("onLineCount",onLineCount);
        }
        System.out.println("连接数:" + onLineCount);
    }

    /**
     * 监听服务器被访问的情况进行减少在线人数
     * @param se
     */
    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        ServletContext context = se.getSession().getServletContext();
        Integer onLineCount = (Integer) context.getAttribute("onLineCount");
        if(onLineCount == null){
            context.setAttribute("onLineCount",1);
        } else{
            onLineCount--;
            context.setAttribute("onLineCount",onLineCount);
        }
        System.out.println("连接数:" + onLineCount);
    }
}

web.xml 中的配置

<listener>
  <display-name>ListenerDemo1</display-name>
  <listener-class>listener.ListenerDemo1</listener-class>
</listener>

15.2 自定义 Session 扫描器

​ 当一个 Web 应用创建的 Session 很多时,为了避免 Session 占用太多的内存,我们可以选择手动将这些内存中的 session 销毁,那么此时也可以借助监听器技术来实现:

package listener;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import java.util.*;

/**
 * 自定义 Session 扫描器
 * @author YH
 * @create 2020-04-29 7:22
 */
public class ListenerDemo2 implements HttpSessionListener, ServletContextListener {
    /**
     * @Field:list
     *      定义一个集合存储服务器创建的 HttpSession
     *      LinkeList不是一个线程安全的集合
     */
    /*
    * private List<HttpSession> list = new LinkedList<>();
    * 这样写涉及到线程安全问题,SessionScanerListener 对象在内存中只有一个
    * 但sessionCreated 可能被多人调用
    * 当有多个人并发访问站点时,服务器同时为这些并发访问的人创建session
    * 那么sessionCreate方法在某一时刻内会被几个线程同时调用,几个现场并发调用sessionCreated方法
    * 但是其内部处理的是向一个集合内添加已经创建好的session,那么add(session)时就会涉及到
    * 几个 session同时抢夺集合中的一个位置的情况,所以向集合中添加session时,要确保线程是安全的
    * 解决:使用 Collections.synchronizedList(List<T> list)方法将不是线程安全的list集合
    * 包装成线程安全的list集合
    * */

    private List<HttpSession> list = null;
            {
        //将LinkedList包装成线程安全的集合
        list = Collections.synchronizedList(new LinkedList<HttpSession>());
    }

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        System.out.println("session被创建了!");
        HttpSession session = se.getSession();
        //加锁:向集合添加session 和 遍历集合操作不能同时进行
        synchronized(this){
            list.add(session);
        }
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        System.out.println("session 被销毁了");
    }

    /**
     * web应用启动时触发这个事件
     * @param sce
     */
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("web应用初始化");
        //创建定时器
        Timer timer = new Timer();
        //定时每隔30秒执行任务
        timer.schedule(new MyTask(list,this),0,1000*30);
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("web应用关闭");
    }
}

/**
 * 定时要执行的任务
 */
class MyTask extends TimerTask{
    /**
     * 存储HttpSession的list集合
     */
    private List<HttpSession> list;
    /**
     * 存储传递过来的锁
     */
    private Object lock;

    public MyTask(List<HttpSession> list,Object lock){
        this.list = list;
        this.lock = lock;
    }

    @Override
    public void run(){
        //将该操作加锁进行锁定
        synchronized(lock){
            System.out.println("定时器执行");
            //进行迭代操作
            Iterator<HttpSession> it = list.iterator();
            while(it.hasNext()){
                HttpSession session = it.next();
                // 30秒未有操作销毁session
                if(System.currentTimeMillis() - session.getLastAccessedTime() > 1000*30){
                    //手动销毁
                    session.invalidate();
                    //移除集合中已经被销毁的session
                    it.remove();
                }
            }
        }
    }
}

web.xml 中的配置

<listener>
  <display-name>ListenerDemo2</display-name>
  <listener-class>listener.ListenerDemo2</listener-class>
</listener>

执行结果:

启动web服务器时的运行可以看出执行顺序:监听器 -- 过滤器 -- servle一系列操作

image-20200429095508088

我们开始访问网页,新用户访问就会创建一个session对象,同时连接数增加:

image-20200429095945106

监视器的定时器 30 秒监控一次,有30秒未操作的session(用户),就销毁此session:

image-20200429100145705

监控器的定时任务依旧进行:

image-20200429100401946

关闭服务器时,过滤器先关闭,再关闭监听器:

image-20200429100500454

十六、MVC 设计模式与三层架构

16.3 MVC 设计模式

image-20200504081303509

Model:模型,一个功能,用 JavaBean 实现(可细分为 处理业务逻辑的JavaBean 和 封装数据的JavaBean)

View:视图,用于展示信息以及实现用户交互(通过前端技术实现)

Controller:控制器,接收请求,将请求跳转到模型进行处理,模型处理完毕后,再将处理的结果返回给请求处(servlet 实现

16. 2 三层架构

​ 与 MVC 设计模式的目标一致:都是为了解耦合、提高代码复用度;但是二者对项目的理解角度不同。

image-20200504094237082

项目结构:

image-20200507162908137

表现层往往不需要定义接口

三层组成

​ 表示层 USL (User Show Layer) 一般称为:视图层

​ 业务逻辑层 BLL (Business Logic Layer) 一般称为:Service 层

​ 数据访问层 DAL (Data Access Layer) DAO层

三层的关系:

​ 上层依赖下层:上层将请求传递给下层,下层处理后返回给上层;(依赖:代码存在先后的理解,如有A的前提是先有B)

三层优化:

加入接口

​ 面向接口编程:先定义接口-再定义实现类

​ 类命名规范:

​ 接口(interface) :I实体类Service 如:IStudentService

​ 实现类(implements):实体类ServiceImpl 如:StudentServiceImpl

​ 包命名规范:

​ 接口所在包:如 xxx.service

​ 实现类所在包:如 xxx.service.impl (作为接口包的子包)

​ 并利用多态使用接口类型接收接口实现类的引用 如:接口 i = new 实现类();

使用 DBUtil 解决代码冗余:

范例:学生管理系统

代码流程图:

image-20200505091612041

参考:孤傲苍狼http://www.cnblogs.com/xdp-gacl/tag/

posted @ 2020-05-19 09:00  "无问西东"  阅读(1227)  评论(1编辑  收藏  举报
网络创业项目 123how出海导航