本次学习是自2021年暑假以后重新开始学习 java 的开端

这里只记我认为重要的东西,基本内容来自于https://heavy_code_industry.gitee.io/code_heavy_industry/pro001-javaweb/lecture

新教程升级

image-20220922171057632

image-20220922171221587

image-20220922171413875

image-20220922171525009

Web基础概念介绍

服务器端应用程序

我们要开发的就是服务器端应用程序

./images

业务

项目中的功能就是业务。

项目的逻辑构成

  • 请求:请求是项目中最基本的逻辑单元,就像万事万物都由原子构成

    举例:点超链接跳转到注册页面

  • 功能:一个功能包含很多个请求

    举例:注册用户功能

    • 请求1:点超链接跳转到注册页面
    • 请求2:发送请求获取短信验证码
    • 请求3:检查用户名是否可用
    • 请求4:提交表单完成注册
  • 模块:一个模块包含很多功能

    举例:用户信息管理模块

    • 功能1:用户注册功能
    • 功能2:用户登录功能
    • 功能3:个人中心——账户安全功能
    • 功能4:个人中心——账户绑定功能
    • 功能5:个人中心——收货地址功能
    • 功能6:个人中心——我的银行卡功能
  • 子系统:根据项目规模的不同,子系统这层逻辑概念可能有也可能没有。如果设置了子系统,那么子系统中也必然包含很多模块。其实庞大项目的子系统已经相当于一个项目了,甚至比小型项目整个都大。

    举例:认证中心子系统

    • 模块1:用户信息管理模块
    • 模块2:权限管理模块
    • 模块3:授权管理模块
    • 模块4:权限检查模块
  • 项目:为了解决现实生活中的实际问题开发一个项目,这个项目就是为这个需求提供的一整套解决方案。

    举例:电商项目

    • 子系统1:认证中心子系统
    • 子系统2:商品管理子系统
    • 子系统3:购物车子系统
    • 子系统4:仓储子系统
    • 子系统5:物流子系统
    • 子系统6:订单子系统

./images

架构

#①概念

『架构』其实就是项目的『结构』。只不过『结构』这个词太小了,不适合用来描述项目这么大的东西,所以换了另一个更大的词:架构。所以当我们聊一个项目的架构时,我们聊的是项目是由哪些部分组成的。

#②发展演变历程

#[1]单一架构

一个项目就是一个工程,这样的结构就是单一架构,也叫all in one。我们现在的JavaWeb阶段、SSM阶段都是学习单一架构开发技术。

#[2]分布式架构

一个项目中包含很多工程,每个工程作为一个模块。模块之间存在调用关系。分布式架构阶段的技术分为两类:

  • Java框架:SpringBoot、SpringCloud、Dubbo等等。
  • 中间件:Redis、ElasticSearch、FastDFS、Nginx、Zookeeper、RabbitMQ等等。

./images

#③单一架构技术体系

  • 视图:用户的操作界面+数据的动态显示
    • 前端技术:HTML/CSS/JavaScript
    • 服务器端页面模板技术:Thymeleaf
  • 控制层:处理请求+跳转页面
    • 服务器:Tomcat
    • 控制器:Servlet
    • 域对象:request、session、servletContext
    • 过滤器:Filter
    • 监听器:Listener
    • 异步交互:Ajax
  • 业务逻辑层:业务逻辑计算
  • 持久化层:操作数据库

./images

本阶段技术体系

./images

本阶段案例简介

./images

html介绍

image-20220922171838231

标签

标签名称 功能
h1~h6 1级标题~6级标题
p 段落
a 超链接
ul/li 无序列表
img 图片
div 定义一个前后有换行的块
span 定义一个前后无换行的块

路径

在我们整个Web开发技术体系中,『路径』是一个贯穿始终的重要概念。凡是需要获取另外一个资源的时候都需要用到路径。要想理解路径这个概念,我们首先要认识一个概念:『文件系统』。

①文件系统

我们写代码的时候通常都是在Windows系统来操作,而一个项目开发完成后想要让所有人都能够访问到就必须『部署』到服务器上,也叫『发布』。而服务器通常是Linux系统。

Windows系统和Linux系统的文件系统有很大差别,为了让我们编写的代码不会因为从Windows系统部署到了Linux系统而出现故障,实际开发时不允许使用物理路径

物理路径举例:

D:\aaa\pro01-HTML\page01-article-tag.html

D:\aaa\pro01-HTML\page02-anchor-target.html

幸运的是不管是Windows系统还是Linux系统环境下,目录结构都是树形结构,编写路径的规则是一样的。

./images

所以我们以项目的树形目录结构为依据来编写路径就不用担心操作系统平台发生变化之后路径错误的问题了。有了这个大前提,我们具体编写路径时有两种具体写法:

  • 相对路径
  • 绝对路径(建议使用)

③绝对路径

#[1]通过IDEA服务器打开HTML文件

测试绝对路径的前提是通过IDEA的内置服务器访问我们编写的HTML页面——这样访问地址的组成结构才能和我们以后在服务器上运行的Java程序一致。

./images

./images

#[2]服务器访问地址的组成

./images

#[3]绝对路径的写法

绝对路径要求必须是以『正斜线』开头。这个开头的正斜线在整个服务器访问地址中对应的位置如下图所示:

./images

这里标注出的这个位置代表的是『服务器根目录』,从这里开始我们就是在服务器的内部查找一个具体的Web应用。

所以我们编写绝对路径时就从这个位置开始,按照目录结构找到目标文件即可。拿前面相对路径中的例子来说,我们想在a.html页面中通过超链接访问z.html。此时路径从正斜线开始,和a.html自身所在位置没有任何关系:

./images

1
<a href="/d/e/f/z.html">To z.html</a>

1

#[4]具体例子

编写超链接访问下面的页面:

./images

1
<a href="/aaa/pro01-HTML/animal/cat/miao.html">Cat Page</a>

5、换行

①代码

1
We would like to see as much CSS1 as possible. CSS2 should be limited to widely-supported elements only. The css Zen Garden is about functional, practical CSS and not the latest bleeding-edge tricks viewable by 2% of the browsing public. <br/>The only real requirement we have is that your CSS validates.

②页面显示效果

./images

6、无序列表

①代码

1
2
3
4
5
<ul>
<li>Apple</li>
<li>Banana</li>
<li>Grape</li>
</ul>

②页面显示效果

./images

表单收集

1、什么是表单

在项目开发过程中,凡是需要用户填写的信息都需要用到表单。

./images

2、form标签

在HTML中我们使用form标签来定义一个表单。而对于form标签来说有两个最重要的属性:action和method。

1
2
3
<form action="/aaa/pro01-HTML/page05-form-target.html" method="post">

</form>
#①action属性

用户在表单里填写的信息需要发送到服务器端,对于Java项目来说就是交给Java代码来处理。那么在页面上我们就必须正确填写服务器端的能够接收表单数据的地址。

这个地址要写在form标签的action属性中。但是现在暂时我们还没有服务器端环境,所以先借用一个HTML页面来当作服务器端地址使用。

#②method属性

『method』这个单词的意思是『方式、方法』,在form标签中method属性用来定义提交表单的『请求方式』。method属性只有两个可选值:get或post,没有极特殊情况的话使用post即可。

什么是『请求方式』

浏览器和服务器之间在互相通信时有大量的『数据』需要传输。但是不论是浏览器还是服务器都有很多不同厂商提供的不同产品。

常见的浏览器有:

  • Chrome
  • Firefox
  • Safari
  • Opera
  • Edge

常见的Java服务器有:

  • Tomcat
  • Weblogic
  • WebSphere
  • Glassfish
  • Jetty

这么多不同厂商各自开发的应用程序怎么能保证它们彼此之间传输的『数据』能够被对方正确理解呢?

很简单,我们给这些数据设定『格式』,发送端按照格式发送数据,接收端按照格式解析数据,这样就能够实现数据的『跨平台传输』了。

而这里定义的『数据格式』就是应用程序之间的『通信协议』

在JavaSE阶段的网络编程章节我们接触过TCP/IP、UDP这样的协议,而我们现在使用的『HTTP协议』的底层就是TCP/IP协议。

但是在HTML标签中,点击超链接是GET方式的请求,提交一个表单可以通过form标签的method属性指定GET或POST请求,其他请求方式无法通过HTML标签实现。除了GET、POST之外的其他请求方式暂时我们不需要涉及(到我们学习SpringMVC时会用到PUT和DELETE)。至于GET请求和POST请求的区别我们会在讲HTTP协议的时候详细介绍,现在大家可以从表面现象来观察一下。

3、name和value

在用户使用一个软件系统时,需要一次性提交很多数据是非常正常的现象。我们肯定不能要求用户一个数据一个数据的提交,而肯定是所有数据填好后一起提交。那就带来一个问题,服务器怎么从众多数据中识别出来收货人、所在地区、详细地址、手机号码……?

很简单,给每个数据都起一个『名字』,发送数据时用『名字』携带对应的数据,接收数据时通过『名字』获取对应的数据。

在各个具体的表单标签中,我们通过『name属性』来给数据起『名字』,通过『value属性』来保存要发送给服务器的『值』

但是名字和值之间既有可能是『一个名字对应一个值』,也有可能是『一个名字对应多个值』

这么看来这样的关系很像我们Java中的Map,而事实上在服务器端就是使用Map类型来接收请求参数的。具体的是类型是:**Map<String,String[]>**。

name属性就是Map的键,value属性就是Map的值。

有了上面介绍的基础知识,下面我们就可以来看具体的表单标签了。

4、单行文本框

#①代码
1
个性签名:<input type="text" name="signal"/><br/>
#②显示效果

./images

#5、密码框

#①代码
1
密码:<input type="password" name="secret"/><br/>
#②显示效果

./images

6、单选框

#①代码
1
2
3
4
5
6
7
8
9
10
11
12
你最喜欢的季节是:
<input type="radio" name="season" value="spring" />春天
<input type="radio" name="season" value="summer" checked="checked" />夏天
<input type="radio" name="season" value="autumn" />秋天
<input type="radio" name="season" value="winter" />冬天

<br/><br/>

你最喜欢的动物是:
<input type="radio" name="animal" value="tiger" />路虎
<input type="radio" name="animal" value="horse" checked="checked" />宝马
<input type="radio" name="animal" value="cheetah" />捷豹
#②效果

./images

#③说明
  • name属性相同的radio为一组,组内互斥
  • 当用户选择了一个radio并提交表单,这个radio的name属性和value属性组成一个键值对发送给服务器
  • 设置checked=”checked”属性设置默认被选中的radio

#7、多选框

#①代码
1
2
3
4
5
6
你最喜欢的球队是:
<input type="checkbox" name="team" value="Brazil"/>巴西
<input type="checkbox" name="team" value="German" checked="checked"/>德国
<input type="checkbox" name="team" value="France"/>法国
<input type="checkbox" name="team" value="China" checked="checked"/>中国
<input type="checkbox" name="team" value="Italian"/>意大利
#②效果

./images

#8、下拉列表

#①代码
1
2
3
4
5
6
7
你喜欢的运动是:
<select name="interesting">
<option value="swimming">游泳</option>
<option value="running">跑步</option>
<option value="shooting" selected="selected">射击</option>
<option value="skating">溜冰</option>
</select>
#②效果

./images

#③说明
  • 下拉列表用到了两种标签,其中select标签用来定义下拉列表,而option标签设置列表项。
  • name属性在select标签中设置。
  • value属性在option标签中设置。
  • option标签的标签体是显示出来给用户看的,提交到服务器的是value属性的值。
  • 通过在option标签中设置selected=”selected”属性实现默认选中的效果。

#9、按钮

#①代码
1
2
3
<button type="button">普通按钮</button>
<button type="reset">重置按钮</button>
<button type="submit">提交按钮</button>
#②效果

./images

#③说明
类型 功能
普通按钮 点击后无效果,需要通过JavaScript绑定单击响应函数
重置按钮 点击后将表单内的所有表单项都恢复为默认值
提交按钮 点击后提交表单

10、表单隐藏域

#①代码
1
<input type="hidden" name="userId" value="2233"/>
#②说明

通过表单隐藏域设置的表单项不会显示到页面上,用户看不到。但是提交表单时会一起被提交。用来设置一些需要和表单一起提交但是不希望用户看到的数据,例如:用户id等等。

#11、多行文本框

#①代码
1
自我介绍:<textarea name="desc"></textarea>
#②效果

./images

#③说明

textarea没有value属性,如果要设置默认值需要写在开始和结束标签之间。

CSS

CSS代码语法

  • CSS样式由选择器和声明组成,而声明又由属性和值组成。
  • 属性和值之间用冒号隔开。
  • 多条声明之间用分号隔开。
  • 使用/* … */声明注释。

./images

设置CSS样式的三种方式

#①在HTML标签内设置

仅对当前标签有效

1
<div style="border: 1px solid black;width: 100px; height: 100px;">&nbsp;</div>

./images

#②在head标签内设置

对当前页面有效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<head>
<meta charset="UTF-8">
<title>Title</title>
<style type="text/css">
.one {
border: 1px solid black;
width: 100px;
height: 100px;
background-color: lightgreen;
margin-top: 5px;
}
</style>
</head>
<body>

<div style="border: 1px solid black;width: 100px; height: 100px;">&nbsp;</div>

<div class="one">&nbsp;</div>
<div class="one">&nbsp;</div>
<div class="one">&nbsp;</div>

</body>

./images

#③引入外部CSS样式文件
#[1]创建CSS文件

./images

#[2]编辑CSS文件
1
2
3
4
5
6
7
.two {
border: 1px solid black;
width: 100px;
height: 100px;
background-color: yellow;
margin-top: 5px;
}
#[3]引入外部CSS文件

在需要使用这个CSS文件的HTML页面的head标签内加入:

1
<link rel="stylesheet" type="text/css" href="/aaa/pro01-HTML/style/example.css" />

于是下面HTML代码的显示效果是:

1
2
3
<div class="two">&nbsp;</div>
<div class="two">&nbsp;</div>
<div class="two">&nbsp;</div>

./images

JavaScript简介

特性

#①脚本语言

JavaScript是一种解释型的脚本语言。不同于C、C++、Java等语言先编译后执行, JavaScript不会产生编译出来的字节码文件,而是在程序的运行过程中对源文件逐行进行解释。

#②基于对象

JavaScript是一种基于对象的脚本语言,它不仅可以创建对象,也能使用现有的对象。但是面向对象的三大特性:『封装』、『继承』、『多态』中,JavaScript能够实现封装,可以模拟继承,不支持多态,所以它不是一门面向对象的编程语言。

#③弱类型

JavaScript中也有明确的数据类型,但是声明一个变量后它可以接收任何类型的数据,并且会在程序执行过程中根据上下文自动转换类型。

#④事件驱动

JavaScript是一种采用事件驱动的脚本语言,它不需要经过Web服务器就可以对用户的输入做出响应。

#⑤跨平台性

JavaScript脚本语言不依赖于操作系统,仅需要浏览器的支持。因此一个JavaScript脚本在编写后可以带到任意机器上使用,前提是机器上的浏览器支持JavaScript脚本语言。目前JavaScript已被大多数的浏览器所支持。

JavaScript基本语法

#1、JavaScript代码嵌入方式
#①HTML文档内
  • JavaScript代码要写在script标签内
  • script标签可以写在文档内的任意位置
  • 为了能够方便查询或操作HTML标签(元素)script标签可以写在body标签后面

可以参考简化版的HelloWorld

1
2
3
4
5
6
7
8
9
<!-- 在HBuilderX中,script标签通过打字“sc”两个字母就可以直接完整生成 -->
<script type="text/javascript">

// 下面是同样实现HelloWorld功能的简化版代码
document.getElementById("helloBtn").onclick = function() {
alert("Hello simple");
};

</script>
#②引入外部JavaScript文档

在script标签内通过src属性指定外部xxx.js文件的路径即可。但是要注意以下两点:

  • 引用外部JavaScript文件的script标签里面不能写JavaScript代码
  • 先引入,再使用
  • script标签不能写成单标签

./images

引入方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<body>
</body>

<!-- 使用script标签的src属性引用外部JavaScript文件,和Java中的import语句类似 -->
<!-- 引用外部JavaScript文件的script标签里面不能写JavaScript代码 -->
<!-- 引用外部JavaScript文件的script标签不能改成单标签 -->
<!-- 外部JavaScript文件一定要先引入再使用 -->
<script src="/pro02-JavaScript/scripts/outter.js" type="text/javascript" charset="utf-8"></script>

<script type="text/javascript">

// 调用外部JavaScript文件中声明的方法
showMessage();
</script>
#2、声明和使用变量
#①JavaScript数据类型
  • 基本数据类型

    • 数值型:JavaScript不区分整数、小数

    • 字符串:JavaScript不区分字符、字符串;单引号、双引号意思一样。

    • 布尔型:true、false

      在JavaScript中,其他类型和布尔类型的自动转换。

      true:非零的数值,非空字符串,非空对象

      false:零,空字符串,null,undefined

      例如:”false”放在if判断中

1
2
3
4
5
6
// "false"是一个非空字符串,直接放在if判断中会被当作『真』处理
if("false"){
alert("true");
}else{
alert("false");
}
  • 引用类型
    • 所有new出来的对象
    • 用[]声明的数组
    • 用{}声明的对象
②变量
  • 关键字:var

  • 数据类型:JavaScript变量可以接收任意类型的数据

  • 标识符:严格区分大小写

  • 变量使用规则

    • 如果使用了一个没有声明的变量,那么会在运行时报错

      Uncaught ReferenceError: b is not defined

    • 如果声明一个变量没有初始化,那么这个变量的值就是undefined

JSON

#①JSON格式的用途

在开发中凡是涉及到『跨平台数据传输』,JSON格式一定是首选。

#②JSON格式的说明
  • JSON数据两端要么是**{},要么是[]**
  • **{}**定义JSON对象
  • **[]**定义JSON数组
  • JSON对象的格式是:
1
{key:value,key:value,...,key:value}

1

  • JOSN数组的格式是:
1
[value,value,...,value]

1

  • key的类型固定是字符串
  • value的类型可以是:
    • 基本数据类型
    • 引用类型:JSON对象或JSON数组

正因为JSON格式中value部分还可以继续使用JSON对象或JSON数组,所以JSON格式是可以『多层嵌套』的,所以JSON格式不论多么复杂的数据类型都可以表达。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
"stuId":556,
"stuName":"carl",
"school":{
"schoolId":339,
"schoolName":"atguigu"
},
"subjectList":[
{
"subjectName":"java",
"subjectScore":50
},
{
"subjectName":"PHP",
"subjectScore":35
},
{
"subjectName":"python",
"subjectScore":24
}
],
"teacherMap":{
"aaa":{
"teacherName":"zhangsan",
"teacherAge":20
},
"bbb":{
"teacherName":"zhangsanfeng",
"teacherAge":108
},
"ccc":{
"teacherName":"zhangwuji",
"teacherAge":25
}
}
}
#③JSON对象和JSON字符串互转
#[1]JSON对象转JSON字符串
1
2
3
4
5
var jsonObj = {"stuName":"tom","stuAge":20};
var jsonStr = JSON.stringify(jsonObj);

console.log(typeof jsonObj); // object
console.log(typeof jsonStr); // string
#[2]JSON字符串转JSON对象
1
2
jsonObj = JSON.parse(jsonStr);
console.log(jsonObj); // {stuName: "tom", stuAge: 20}

DOM

#1、概念

#①名词解释

DOM是Document Object Model的缩写,意思是『文档对象模型』——将HTML文档抽象成模型,再封装成对象方便用程序操作。

这是一种非常常用的编程思想:将现实世界的事物抽象成模型,这样就非常容易使用对象来量化的描述现实事物,从而把生活中的问题转化成一个程序问题,最终实现用应用软件协助解决现实问题。而在这其中『模型』就是那个连通现实世界和代码世界的桥梁。

#②DOM树

浏览器把HTML文档从服务器上下载下来之后就开始按照『从上到下』的顺序『读取HTML标签』。每一个标签都会被封装成一个『对象』

而第一个读取到的肯定是根标签html,然后是它的子标签head,再然后是head标签里的子标签……所以从html标签开始,整个文档中的所有标签都会根据它们之间的『父子关系』被放到一个『树形结构』的对象中。

./images

这个包含了所有标签对象的整个树形结构对象就是JavaScript中的一个可以直接使用的内置对象document

例如,下面的标签结构:

./images

会被解析为:

./images

JavaWeb

架构模式简简介

image-20220924120538603

tomcat新建项目-部署-运行-访问

image-20220924121436480

基本项目雏形

image-20220924155054370

以下内容基于黑马程序员javaweb课程,仅供自己学习需要整理。

Maven&MyBatis

目标

  • 能够使用Maven进行项目的管理
  • 能够完成Mybatis代理方式查询数据
  • 能够理解Mybatis核心配置文件的配置

1,Maven

Maven是专门用于管理和构建Java项目的工具,它的主要功能有:

  • 提供了一套标准化的项目结构

  • 提供了一套标准化的构建流程(编译,测试,打包,发布……)

  • 提供了一套依赖管理机制

标准化的项目结构:

项目结构我们都知道,每一个开发工具(IDE)都有自己不同的项目结构,它们互相之间不通用。我再eclipse中创建的目录,无法在idea中进行使用,这就造成了很大的不方便,如下图:前两个是以后开发经常使用的开发工具

image-20210726153521381

而Maven提供了一套标准化的项目结构,所有的IDE使用Maven构建的项目完全一样,所以IDE创建的Maven项目可以通用。如下图右边就是Maven构建的项目结构。

image-20210726153815028

标准化的构建流程:

image-20210726154144488

如上图所示我们开发了一套系统,代码需要进行编译、测试、打包、发布,这些操作如果需要反复进行就显得特别麻烦,而Maven提供了一套简单的命令来完成项目构建。

依赖管理:

依赖管理其实就是管理你项目所依赖的第三方资源(jar包、插件)。如之前我们项目中需要使用JDBC和Druid的话,就需要去网上下载对应的依赖包(当前之前是老师已经下载好提供给大家了),复制到项目中,还要将jar包加入工作环境这一系列的操作。如下图所示

image-20210726154753631

而Maven使用标准的 ==坐标== 配置来管理各种依赖,只需要简单的配置就可以完成依赖管理。

image-20210726154922337

如上图右边所示就是mysql驱动包的坐标,在项目中只需要写这段配置,其他都不需要我们担心,Maven都帮我们进行操作了。

市面上有很多构建工具,而Maven依旧还是主流构建工具,如下图是常用构建工具的使用占比

image-20210726155212733

1.1 Maven简介

==Apache Maven== 是一个项目管理和构建==工具==,它基于项目对象模型(POM)的概念,通过一小段描述信息来管理项目的构建、报告和文档。

官网 :http://maven.apache.org/

通过上面的描述大家只需要知道Maven是一个工具即可。Apache 是一个开源组织,将来我们会学习很多Apache提供的项目。

1.1.1 Maven模型

  • 项目对象模型 (Project Object Model)
  • 依赖管理模型(Dependency)
  • 插件(Plugin)
image-20210726155759621

如上图所示就是Maven的模型,而我们先看紫色框框起来的部分,他就是用来完成 标准化构建流程 。如我们需要编译,Maven提供了一个编译插件供我们使用,我们需要打包,Maven就提供了一个打包插件提供我们使用等。

image-20210726160928515

上图中紫色框起来的部分,项目对象模型就是将我们自己抽象成一个对象模型,有自己专属的坐标,如下图所示是一个Maven项目:

image-20210726161340796

依赖管理模型则是使用坐标来描述当前项目依赖哪儿些第三方jar包,如下图所示

image-20210726161616034

上述Maven模型图中还有一部分是仓库。如何理解仓库呢?

1.1.2 仓库

大家想想这样的场景,我们创建Maven项目,在项目中使用坐标来指定项目的依赖,那么依赖的jar包到底存储在什么地方呢?其实依赖jar包是存储在我们的本地仓库中。而项目运行时从本地仓库中拿需要的依赖jar包。

仓库分类:

  • 本地仓库:自己计算机上的一个目录

  • 中央仓库:由Maven团队维护的全球唯一的仓库

  • 远程仓库(私服):一般由公司团队搭建的私有仓库

    今天我们只学习远程仓库的使用,并不会搭建。

当项目中使用坐标引入对应依赖jar包后,首先会查找本地仓库中是否有对应的jar包:

  • 如果有,则在项目直接引用;

  • 如果没有,则去中央仓库中下载对应的jar包到本地仓库。

image-20210726162605394

如果还可以搭建远程仓库,将来jar包的查找顺序则变为:

本地仓库 –> 远程仓库–> 中央仓库

image-20210726162815045

1.2 Maven安装配置

  • 解压 apache-maven-3.6.1.rar 既安装完成

    image-20210726163219682

    建议解压缩到没有中文、特殊字符的路径下。如课程中解压缩到 D:\software 下。

    解压缩后的目录结构如下:

    image-20210726163518885
    • bin目录 : 存放的是可执行命令。mvn 命令重点关注。
    • conf目录 :存放Maven的配置文件。settings.xml 配置文件后期需要修改。
    • lib目录 :存放Maven依赖的jar包。Maven也是使用java开发的,所以它也依赖其他的jar包。
  • 配置环境变量 MAVEN_HOME 为安装路径的bin目录

    此电脑 右键 –> 高级系统设置 –> 高级 –> 环境变量

    在系统变量处新建一个变量 MAVEN_HOME

    image-20210726164058589

    Path 中进行配置

    image-20210726164146832

    打开命令提示符进行验证,出现如图所示表示安装成功

    image-20210726164306480
  • 配置本地仓库

    修改 conf/settings.xml 中的 为一个指定目录作为本地仓库,用来存储jar包。

    image-20210726164348048
  • 配置阿里云私服

    中央仓库在国外,所以下载jar包速度可能比较慢,而阿里公司提供了一个远程仓库,里面基本也都有开源项目的jar包。

    修改 conf/settings.xml 中的 标签,为其添加如下子标签:

    1
    2
    3
    4
    5
    6
    <mirror>  
    <id>alimaven</id>
    <name>aliyun maven</name>
    <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
    <mirrorOf>central</mirrorOf>
    </mirror>

1.3 Maven基本使用

1.3.1 Maven 常用命令

  • compile :编译

  • clean:清理

  • test:测试

  • package:打包

  • install:安装

命令演示:

资料\代码\maven-project 提供了一个使用Maven构建的项目,项目结构如下:

image-20210726170404545

而我们使用上面命令需要在磁盘上进入到项目的 pom.xml 目录下,打开命令提示符

image-20210726170549907

编译命令演示:

1
compile :编译

执行上述命令可以看到:

  • 从阿里云下载编译需要的插件的jar包,在本地仓库也能看到下载好的插件
  • 在项目下会生成一个 target 目录
image-20210726171047324

同时在项目下会出现一个 target 目录,编译后的字节码文件就放在该目录下

image-20210726171346824

清理命令演示:

1
mvn clean

执行上述命令可以看到

  • 从阿里云下载清理需要的插件jar包
  • 删除项目下的 target 目录
image-20210726171558786

打包命令演示:

1
mvn package

执行上述命令可以看到:

  • 从阿里云下载打包需要的插件jar包
  • 在项目的 terget 目录下有一个jar包(将当前项目打成的jar包)
image-20210726171747125

测试命令演示:

1
mvn test  

该命令会执行所有的测试代码。执行上述命令效果如下

image-20210726172343933

安装命令演示:

1
mvn install

该命令会将当前项目打成jar包,并安装到本地仓库。执行完上述命令后到本地仓库查看结果如下:

image-20210726172709112

1.3.2 Maven 生命周期

Maven 构建项目生命周期描述的是一次构建过程经历经历了多少个事件

Maven 对项目构建的生命周期划分为3套:

  • clean :清理工作。
  • default :核心工作,例如编译,测试,打包,安装等。
  • site : 产生报告,发布站点等。这套声明周期一般不会使用。

同一套生命周期内,执行后边的命令,前面的所有命令会自动执行。例如默认(default)生命周期如下:

image-20210726173153576

当我们执行 install(安装)命令时,它会先执行 compile命令,再执行 test 命令,再执行 package 命令,最后执行 install 命令。

当我们执行 package (打包)命令时,它会先执行 compile 命令,再执行 test 命令,最后执行 package 命令。

默认的生命周期也有对应的很多命令,其他的一般都不会使用,我们只关注常用的:

image-20210726173619353

1.4 IDEA使用Maven

以后开发中我们肯定会在高级开发工具中使用Maven管理项目,而我们常用的高级开发工具是IDEA,所以接下来我们会讲解Maven在IDEA中的使用。

1.4.1 IDEA配置Maven环境

我们需要先在IDEA中配置Maven环境:

  • 选择 IDEA中 File –> Settings

    image-20210726174202898
  • 搜索 maven

    image-20210726174229396
  • 设置 IDEA 使用本地安装的 Maven,并修改配置文件路径

    image-20210726174248050

1.4.2 Maven 坐标详解

什么是坐标?

  • Maven 中的坐标是==资源的唯一标识==
  • 使用坐标来定义项目或引入项目中需要的依赖

Maven 坐标主要组成

  • groupId:定义当前Maven项目隶属组织名称(通常是域名反写,例如:com.itheima)
  • artifactId:定义当前Maven项目名称(通常是模块名称,例如 order-service、goods-service)
  • version:定义当前项目版本号

如下图就是使用坐标表示一个项目:

image-20210726174718176

==注意:==

  • 上面所说的资源可以是插件、依赖、当前项目。
  • 我们的项目如果被其他的项目依赖时,也是需要坐标来引入的。

1.4.3 IDEA 创建 Maven项目

  • 创建模块,选择Maven,点击Next

    image-20210726175049876
  • 填写模块名称,坐标信息,点击finish,创建完成

    image-20210726175109822

    创建好的项目目录结构如下:

    image-20210726175244826

  • 编写 HelloWorld,并运行

1.4.4 IDEA 导入 Maven项目

大家在学习时可能需要看老师的代码,当然也就需要将老师的代码导入到自己的IDEA中。我们可以通过以下步骤进行项目的导入:

  • 选择右侧Maven面板,点击 + 号

    image-20210726182702336
  • 选中对应项目的pom.xml文件,双击即可

    image-20210726182648891
  • 如果没有Maven面板,选择

    View –> Appearance –> Tool Window Bars

    image-20210726182634466

可以通过下图所示进行命令的操作:

image-20210726182902961

配置 Maven-Helper 插件

  • 选择 IDEA中 File –> Settings

    image-20210726192212026
  • 选择 Plugins

    image-20210726192224914
  • 搜索 Maven,选择第一个 Maven Helper,点击Install安装,弹出面板中点击Accept

    image-20210726192244567
  • 重启 IDEA

安装完该插件后可以通过 选中项目右键进行相关命令操作,如下图所示:

image-20210726192430371

1.5 依赖管理

1.5.1 使用坐标引入jar包

使用坐标引入jar包的步骤:

  • 在项目的 pom.xml 中编写 标签

  • 标签中 使用 引入坐标

  • 定义坐标的 groupId,artifactId,version

    image-20210726193105765
  • 点击刷新按钮,使坐标生效

    image-20210726193121384

注意:

快捷方式导入jar包的坐标:

每次需要引入jar包,都去对应的网站进行搜索是比较麻烦的,接下来给大家介绍一种快捷引入坐标的方式

  • 在 pom.xml 中 按 alt + insert,选择 Dependency

    image-20210726193603724
  • 在弹出的面板中搜索对应坐标,然后双击选中对应坐标

    image-20210726193625229
  • 点击刷新按钮,使坐标生效

    image-20210726193121384

自动导入设置:

上面每次操作都需要点击刷新按钮,让引入的坐标生效。当然我们也可以通过设置让其自动完成

  • 选择 IDEA中 File –> Settings

    image-20210726193854438
  • 在弹出的面板中找到 Build Tools

    image-20210726193909276
  • 选择 Any changes,点击 ok 即可生效

1.5.2 依赖范围

通过设置坐标的依赖范围(scope),可以设置 对应jar包的作用范围:编译环境、测试环境、运行环境。

如下图所示给 junit 依赖通过 scope 标签指定依赖的作用范围。 那么这个依赖就只能作用在测试环境,其他环境下不能使用。

image-20210726194703845

那么 scope 都可以有哪些取值呢?

依赖范围 编译classpath 测试classpath 运行classpath 例子
compile Y Y Y logback
test - Y - Junit
provided Y Y - servlet-api
runtime - Y Y jdbc驱动
system Y Y - 存储在本地的jar包
  • compile :作用于编译环境、测试环境、运行环境。
  • test : 作用于测试环境。典型的就是Junit坐标,以后使用Junit时,都会将scope指定为该值
  • provided :作用于编译环境、测试环境。我们后面会学习 servlet-api ,在使用它时,必须将 scope 设置为该值,不然运行时就会报错
  • runtime : 作用于测试环境、运行环境。jdbc驱动一般将 scope 设置为该值,当然不设置也没有任何问题

注意:

  • 如果引入坐标不指定 scope 标签时,默认就是 compile 值。以后大部分jar包都是使用默认值。

2,Mybatis

2.1 Mybatis概述

2.1.1 Mybatis概念

  • MyBatis 是一款优秀的==持久层框架==,用于简化 JDBC 开发

  • MyBatis 本是 Apache 的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis 。2013年11月迁移到Github

  • 官网:https://mybatis.org/mybatis-3/zh/index.html

持久层:

  • 负责将数据到保存到数据库的那一层代码。

    以后开发我们会将操作数据库的Java代码作为持久层。而Mybatis就是对jdbc代码进行了封装。

  • JavaEE三层架构:表现层、业务层、持久层

    三层架构在后期会给大家进行讲解,今天先简单的了解下即可。

框架:

  • 框架就是一个半成品软件,是一套可重用的、通用的、软件基础代码模型
  • 在框架的基础之上构建软件编写更加高效、规范、通用、可扩展

举例给大家简单的解释一下什么是半成品软件。大家小时候应该在公园见过给石膏娃娃涂鸦

image-20210726202410311

如下图所示有一个石膏娃娃,这个就是一个半成品。你可以在这个半成品的基础上进行不同颜色的涂鸦

image-20210726202858441

了解了什么是Mybatis后,接下来说说以前 JDBC代码 的缺点以及Mybatis又是如何解决的。

2.1.2 JDBC 缺点

下面是 JDBC 代码,我们通过该代码分析都存在什么缺点:

image-20210726203656847
  • 硬编码

    • 注册驱动、获取连接

      上图标1的代码有很多字符串,而这些是连接数据库的四个基本信息,以后如果要将Mysql数据库换成其他的关系型数据库的话,这四个地方都需要修改,如果放在此处就意味着要修改我们的源代码。

    • SQL语句

      上图标2的代码。如果表结构发生变化,SQL语句就要进行更改。这也不方便后期的维护。

  • 操作繁琐

    • 手动设置参数

    • 手动封装结果集

      上图标4的代码是对查询到的数据进行封装,而这部分代码是没有什么技术含量,而且特别耗费时间的。

2.1.3 Mybatis 优化

  • 硬编码可以配置到==配置文件==
  • 操作繁琐的地方mybatis都==自动完成==

如图所示

image-20210726204849309

下图是持久层框架的使用占比。

image-20210726205328999

2.2 Mybatis快速入门

需求:查询user表中所有的数据

  • 创建user表,添加数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    create database mybatis;
    use mybatis;

    drop table if exists tb_user;

    create table tb_user(
    id int primary key auto_increment,
    username varchar(20),
    password varchar(20),
    gender char(1),
    addr varchar(30)
    );

    INSERT INTO tb_user VALUES (1, 'zhangsan', '123', '男', '北京');
    INSERT INTO tb_user VALUES (2, '李四', '234', '女', '天津');
    INSERT INTO tb_user VALUES (3, '王五', '11', '男', '西安');
  • 创建模块,导入坐标

    在创建好的模块中的 pom.xml 配置文件中添加依赖的坐标

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    <dependencies>
    <!--mybatis 依赖-->
    <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.5</version>
    </dependency>

    <!--mysql 驱动-->
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.46</version>
    </dependency>

    <!--junit 单元测试-->
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13</version>
    <scope>test</scope>
    </dependency>

    <!-- 添加slf4j日志api -->
    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.20</version>
    </dependency>
    <!-- 添加logback-classic依赖 -->
    <dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
    </dependency>
    <!-- 添加logback-core依赖 -->
    <dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.2.3</version>
    </dependency>
    </dependencies>

    注意:需要在项目的 resources 目录下创建logback的配置文件

  • 编写 MyBatis 核心配置文件 – > 替换连接信息 解决硬编码问题

    在模块下的 resources 目录下创建mybatis的配置文件 mybatis-config.xml,内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>

    <typeAliases>
    <package name="com.itheima.pojo"/>
    </typeAliases>

    <!--
    environments:配置数据库连接环境信息。可以配置多个environment,通过default属性切换不同的environment
    -->
    <environments default="development">
    <environment id="development">
    <transactionManager type="JDBC"/>
    <dataSource type="POOLED">
    <!--数据库连接信息-->
    <property name="driver" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql:///mybatis?useSSL=false"/>
    <property name="username" value="root"/>
    <property name="password" value="1234"/>
    </dataSource>
    </environment>

    <environment id="test">
    <transactionManager type="JDBC"/>
    <dataSource type="POOLED">
    <!--数据库连接信息-->
    <property name="driver" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql:///mybatis?useSSL=false"/>
    <property name="username" value="root"/>
    <property name="password" value="1234"/>
    </dataSource>
    </environment>
    </environments>
    <mappers>
    <!--加载sql映射文件-->
    <mapper resource="UserMapper.xml"/>
    </mappers>
    </configuration>
  • 编写 SQL 映射文件 –> 统一管理sql语句,解决硬编码问题

    在模块的 resources 目录下创建映射配置文件 UserMapper.xml,内容如下:

    1
    2
    3
    4
    5
    6
    7
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="test">
    <select id="selectAll" resultType="com.itheima.pojo.User">
    select * from tb_user;
    </select>
    </mapper>
  • 编码

    • com.itheima.pojo 包下创建 User类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public class User {
      private int id;
      private String username;
      private String password;
      private String gender;
      private String addr;

      //省略了 setter 和 getter
      }
    • com.itheima 包下编写 MybatisDemo 测试类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      public class MyBatisDemo {

      public static void main(String[] args) throws IOException {
      //1. 加载mybatis的核心配置文件,获取 SqlSessionFactory
      String resource = "mybatis-config.xml";
      InputStream inputStream = Resources.getResourceAsStream(resource);
      SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

      //2. 获取SqlSession对象,用它来执行sql
      SqlSession sqlSession = sqlSessionFactory.openSession();
      //3. 执行sql
      List<User> users = sqlSession.selectList("test.selectAll"); //参数是一个字符串,该字符串必须是映射配置文件的namespace.id
      System.out.println(users);
      //4. 释放资源
      sqlSession.close();
      }
      }

解决SQL映射文件的警告提示:

在入门案例映射配置文件中存在报红的情况。问题如下:

image-20210726212621722
  • 产生的原因:Idea和数据库没有建立连接,不识别表信息。但是大家一定要记住,它并不影响程序的执行。
  • 解决方式:在Idea中配置MySQL数据库连接。

IDEA中配置MySQL数据库连接

  • 点击IDEA右边框的 Database ,在展开的界面点击 + 选择 Data Source ,再选择 MySQL

    image-20210726213046072
  • 在弹出的界面进行基本信息的填写

    image-20210726213305893
  • 点击完成后就能看到如下界面

    image-20210726213541418

    而此界面就和 navicat 工具一样可以进行数据库的操作。也可以编写SQL语句

image-20210726213857620

2.3 Mapper代理开发

2.3.1 Mapper代理开发概述

之前我们写的代码是基本使用方式,它也存在硬编码的问题,如下:

image-20210726214648112

这里调用 selectList() 方法传递的参数是映射配置文件中的 namespace.id值。这样写也不便于后期的维护。如果使用 Mapper 代理方式(如下图)则不存在硬编码问题。

image-20210726214636108

通过上面的描述可以看出 Mapper 代理方式的目的:

  • 解决原生方式中的硬编码
  • 简化后期执行SQL

Mybatis 官网也是推荐使用 Mapper 代理的方式。下图是截止官网的图片

image-20210726215339568

2.3.2 使用Mapper代理要求

使用Mapper代理方式,必须满足以下要求:

  • 定义与SQL映射文件同名的Mapper接口,并且将Mapper接口和SQL映射文件放置在同一目录下。如下图:

    image-20210726215946951
  • 设置SQL映射文件的namespace属性为Mapper接口全限定名

    image-20210726220053883
  • 在 Mapper 接口中定义方法,方法名就是SQL映射文件中sql语句的id,并保持参数类型和返回值类型一致

    image-20210726223216517

2.3.3 案例代码实现

  • com.itheima.mapper 包下创建 UserMapper接口,代码如下:

    1
    2
    3
    4
    public interface UserMapper {
    List<User> selectAll();
    User selectById(int id);
    }
  • resources 下创建 com/itheima/mapper 目录,并在该目录下创建 UserMapper.xml 映射配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!--
    namespace:名称空间。必须是对应接口的全限定名
    -->
    <mapper namespace="com.itheima.mapper.UserMapper">
    <select id="selectAll" resultType="com.itheima.pojo.User">
    select *
    from tb_user;
    </select>
    </mapper>
  • com.itheima 包下创建 MybatisDemo2 测试类,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    /**
    * Mybatis 代理开发
    */
    public class MyBatisDemo2 {

    public static void main(String[] args) throws IOException {

    //1. 加载mybatis的核心配置文件,获取 SqlSessionFactory
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    //2. 获取SqlSession对象,用它来执行sql
    SqlSession sqlSession = sqlSessionFactory.openSession();
    //3. 执行sql
    //3.1 获取UserMapper接口的代理对象
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    List<User> users = userMapper.selectAll();

    System.out.println(users);
    //4. 释放资源
    sqlSession.close();
    }
    }

==注意:==

如果Mapper接口名称和SQL映射文件名称相同,并在同一目录下,则可以使用包扫描的方式简化SQL映射文件的加载。也就是将核心配置文件的加载映射配置文件的配置修改为

1
2
3
4
5
6
<mappers>
<!--加载sql映射文件-->
<!-- <mapper resource="com/itheima/mapper/UserMapper.xml"/>-->
<!--Mapper代理方式-->
<package name="com.itheima.mapper"/>
</mappers>

2.4 核心配置文件

核心配置文件中现有的配置之前已经给大家进行了解释,而核心配置文件中还可以配置很多内容。我们可以通过查询官网看可以配置的内容

image-20210726221454927

接下来我们先对里面的一些配置进行讲解。

2.4.1 多环境配置

在核心配置文件的 environments 标签中其实是可以配置多个 environment ,使用 id 给每段环境起名,在 environments 中使用 default='环境id' 来指定使用哪儿段配置。我们一般就配置一个 environment 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<!--数据库连接信息-->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///mybatis?useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="1234"/>
</dataSource>
</environment>

<environment id="test">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<!--数据库连接信息-->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///mybatis?useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="1234"/>
</dataSource>
</environment>
</environments>=

2.4.2 类型别名

在映射配置文件中的 resultType 属性需要配置数据封装的类型(类的全限定名)。而每次这样写是特别麻烦的,Mybatis 提供了 类型别名(typeAliases) 可以简化这部分的书写。

首先需要现在核心配置文件中配置类型别名,也就意味着给pojo包下所有的类起了别名(别名就是类名),不区分大小写。内容如下:

1
2
3
4
<typeAliases>
<!--name属性的值是实体类所在包-->
<package name="com.itheima.pojo"/>
</typeAliases>

通过上述的配置,我们就可以简化映射配置文件中 resultType 属性值的编写

1
2
3
4
5
<mapper namespace="com.itheima.mapper.UserMapper">
<select id="selectAll" resultType="user">
select * from tb_user;
</select>
</mapper>

Mybatis练习

目标

  • 能够使用映射配置文件实现CRUD操作
  • 能够使用注解实现CRUD操作

1,配置文件实现CRUD

image-20210729111159534

如上图所示产品原型,里面包含了品牌数据的 查询按条件查询添加删除批量删除修改 等功能,而这些功能其实就是对数据库表中的数据进行CRUD操作。接下来我们就使用Mybatis完成品牌数据的增删改查操作。以下是我们要完成功能列表:

  • 查询
    • 查询所有数据
    • 查询详情
    • 条件查询
  • 添加
  • 修改
    • 修改全部字段
    • 修改动态字段
  • 删除
    • 删除一个
    • 批量删除

我们先将必要的环境准备一下。

1.1 环境准备

  • 数据库表(tb_brand)及数据准备

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    -- 删除tb_brand表
    drop table if exists tb_brand;
    -- 创建tb_brand表
    create table tb_brand
    (
    -- id 主键
    id int primary key auto_increment,
    -- 品牌名称
    brand_name varchar(20),
    -- 企业名称
    company_name varchar(20),
    -- 排序字段
    ordered int,
    -- 描述信息
    description varchar(100),
    -- 状态:0:禁用 1:启用
    status int
    );
    -- 添加数据
    insert into tb_brand (brand_name, company_name, ordered, description, status)
    values ('三只松鼠', '三只松鼠股份有限公司', 5, '好吃不上火', 0),
    ('华为', '华为技术有限公司', 100, '华为致力于把数字世界带入每个人、每个家庭、每个组织,构建万物互联的智能世界', 1),
    ('小米', '小米科技有限公司', 50, 'are you ok', 1);
  • 实体类 Brand

    com.itheima.pojo 包下创建 Brand 实体类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class Brand {
    // id 主键
    private Integer id;
    // 品牌名称
    private String brandName;
    // 企业名称
    private String companyName;
    // 排序字段
    private Integer ordered;
    // 描述信息
    private String description;
    // 状态:0:禁用 1:启用
    private Integer status;

    //省略 setter and getter。自己写时要补全这部分代码
    }
  • 编写测试用例

    测试代码需要在 test/java 目录下创建包及测试用例。项目结构如下:

    image-20210729112907106
  • 安装 MyBatisX 插件

    • MybatisX 是一款基于 IDEA 的快速开发插件,为效率而生。

    • 主要功能

      • XML映射配置文件 和 接口方法 间相互跳转
      • 根据接口方法生成 statement
    • 安装方式

      点击 file ,选择 settings ,就能看到如下图所示界面

      image-20210729113304743

      注意:安装完毕后需要重启IDEA

    • 插件效果

      image-20210729164450524

      红色头绳的表示映射配置文件,蓝色头绳的表示mapper接口。在mapper接口点击红色头绳的小鸟图标会自动跳转到对应的映射配置文件,在映射配置文件中点击蓝色头绳的小鸟图标会自动跳转到对应的mapper接口。也可以在mapper接口中定义方法,自动生成映射配置文件中的 statement ,如图所示

      image-20210729165337223

1.2 查询所有数据

image-20210729165724838

如上图所示就页面上展示的数据,而这些数据需要从数据库进行查询。接下来我们就来讲查询所有数据功能,而实现该功能我们分以下步骤进行实现:

  • 编写接口方法:Mapper接口

    • 参数:无

      查询所有数据功能是不需要根据任何条件进行查询的,所以此方法不需要参数。

      image-20210729171208737
    • 结果:List

      我们会将查询出来的每一条数据封装成一个 Brand 对象,而多条数据封装多个 Brand 对象,需要将这些对象封装到List集合中返回。

      image-20210729171146911
    • 执行方法、测试

1.2.1 编写接口方法

com.itheima.mapper 包写创建名为 BrandMapper 的接口。并在该接口中定义 List<Brand> selectAll() 方法。

1
2
3
4
5
6
7
public interface BrandMapper {

/**
* 查询所有
*/
List<Brand> selectAll();
}

1.2.2 编写SQL语句

reources 下创建 com/itheima/mapper 目录结构,并在该目录下创建名为 BrandMapper.xml 的映射配置文件

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.itheima.mapper.BrandMapper">
<select id="selectAll" resultType="brand">
select *
from tb_brand;
</select>
</mapper>

1.2.3 编写测试方法

MybatisTest 类中编写测试查询所有的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void testSelectAll() throws IOException {
//1. 获取SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();

//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);

//4. 执行方法
List<Brand> brands = brandMapper.selectAll();
System.out.println(brands);

//5. 释放资源
sqlSession.close();

}

注意:现在我们感觉测试这部分代码写起来特别麻烦,我们可以先忍忍。以后我们只会写上面的第3步的代码,其他的都不需要我们来完成。

执行测试方法结果如下:

image-20210729172544230

从上面结果我们看到了问题,有些数据封装成功了,而有些数据并没有封装成功。为什么这样呢?

这个问题可以通过两种方式进行解决:

  • 给字段起别名
  • 使用resultMap定义字段和属性的映射关系

1.2.4 起别名解决上述问题

从上面结果可以看到 brandNamecompanyName 这两个属性的数据没有封装成功,查询 实体类 和 表中的字段 发现,在实体类中属性名是 brandNamecompanyName ,而表中的字段名为 brand_namecompany_name,如下图所示 。那么我们只需要保持这两部分的名称一致这个问题就迎刃而解。

image-20210729173210433

我们可以在写sql语句时给这两个字段起别名,将别名定义成和属性名一致即可。

1
2
3
4
5
<select id="selectAll" resultType="brand">
select
id, brand_name as brandName, company_name as companyName, ordered, description, status
from tb_brand;
</select>

而上面的SQL语句中的字段列表书写麻烦,如果表中还有更多的字段,同时其他的功能也需要查询这些字段时就显得我们的代码不够精炼。Mybatis提供了sql 片段可以提高sql的复用性。

SQL片段:

  • 将需要复用的SQL片段抽取到 sql 标签中

    1
    2
    3
    <sql id="brand_column">
    id, brand_name as brandName, company_name as companyName, ordered, description, status
    </sql>

    id属性值是唯一标识,引用时也是通过该值进行引用。

  • 在原sql语句中进行引用

    使用 include 标签引用上述的 SQL 片段,而 refid 指定上述 SQL 片段的id值。

    1
    2
    3
    4
    5
    <select id="selectAll" resultType="brand">
    select
    <include refid="brand_column" />
    from tb_brand;
    </select>

1.2.5 使用resultMap解决上述问题

起别名 + sql片段的方式可以解决上述问题,但是它也存在问题。如果还有功能只需要查询部分字段,而不是查询所有字段,那么我们就需要再定义一个 SQL 片段,这就显得不是那么灵活。

那么我们也可以使用resultMap来定义字段和属性的映射关系的方式解决上述问题。

  • 在映射配置文件中使用resultMap定义 字段 和 属性 的映射关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <resultMap id="brandResultMap" type="brand">
    <!--
    id:完成主键字段的映射
    column:表的列名
    property:实体类的属性名
    result:完成一般字段的映射
    column:表的列名
    property:实体类的属性名
    -->
    <result column="brand_name" property="brandName"/>
    <result column="company_name" property="companyName"/>
    </resultMap>

    注意:在上面只需要定义 字段名 和 属性名 不一样的映射,而一样的则不需要专门定义出来。

  • SQL语句正常编写

    1
    2
    3
    4
    <select id="selectAll" resultMap="brandResultMap">
    select *
    from tb_brand;
    </select>

1.2.6 小结

实体类属性名 和 数据库表列名 不一致,不能自动封装数据

  • ==起别名:==在SQL语句中,对不一样的列名起别名,别名和实体类属性名一样
    • 可以定义 片段,提升复用性
  • ==resultMap:==定义 完成不一致的属性名和列名的映射

而我们最终选择使用 resultMap的方式。查询映射配置文件中查询所有的 statement 书写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 <resultMap id="brandResultMap" type="brand">
<!--
id:完成主键字段的映射
column:表的列名
property:实体类的属性名
result:完成一般字段的映射
column:表的列名
property:实体类的属性名
-->
<result column="brand_name" property="brandName"/>
<result column="company_name" property="companyName"/>
</resultMap>



<select id="selectAll" resultMap="brandResultMap">
select *
from tb_brand;
</select>

1.3 查询详情

image-20210729180118287

有些数据的属性比较多,在页面表格中无法全部实现,而只会显示部分,而其他属性数据的查询可以通过 查看详情 来进行查询,如上图所示。

查看详情功能实现步骤:

  • 编写接口方法:Mapper接口

    image-20210729180604529
    • 参数:id

      查看详情就是查询某一行数据,所以需要根据id进行查询。而id以后是由页面传递过来。

    • 结果:Brand

      根据id查询出来的数据只要一条,而将一条数据封装成一个Brand对象即可

  • 编写SQL语句:SQL映射文件

    image-20210729180709318
  • 执行方法、进行测试

1.3.1 编写接口方法

BrandMapper 接口中定义根据id查询数据的方法

1
2
3
4
/**
* 查看详情:根据Id查询
*/
Brand selectById(int id);

1.3.2 编写SQL语句

BrandMapper.xml 映射配置文件中编写 statement,使用 resultMap 而不是使用 resultType

1
2
3
4
<select id="selectById"  resultMap="brandResultMap">
select *
from tb_brand where id = #{id};
</select>

注意:上述SQL中的 #{id}先这样写,一会我们再详细讲解

1.3.3 编写测试方法

test/java 下的 com.itheima.mapper 包下的 MybatisTest类中 定义测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 @Test
public void testSelectById() throws IOException {
//接收参数,该id以后需要传递过来
int id = 1;

//1. 获取SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();

//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);

//4. 执行方法
Brand brand = brandMapper.selectById(id);
System.out.println(brand);

//5. 释放资源
sqlSession.close();
}

执行测试方法结果如下:

image-20210729182223137

1.3.4 参数占位符

查询到的结果很好理解就是id为1的这行数据。而这里我们需要看控制台显示的SQL语句,能看到使用?进行占位。说明我们在映射配置文件中的写的 #{id} 最终会被?进行占位。接下来我们就聊聊映射配置文件中的参数占位符。

mybatis提供了两种参数占位符:

  • #{} :执行SQL时,会将 #{} 占位符替换为?,将来自动设置参数值。从上述例子可以看出使用#{} 底层使用的是 PreparedStatement

  • ${} :拼接SQL。底层使用的是 Statement,会存在SQL注入问题。如下图将 映射配置文件中的 #{} 替换成 ${} 来看效果

    1
    2
    3
    4
    <select id="selectById"  resultMap="brandResultMap">
    select *
    from tb_brand where id = ${id};
    </select>

    重新运行查看结果如下:

    image-20210729184156019

==注意:==从上面两个例子可以看出,以后开发我们使用 #{} 参数占位符。

1.3.5 parameterType使用

对于有参数的mapper接口方法,我们在映射配置文件中应该配置 ParameterType 来指定参数类型。只不过该属性都可以省略。如下图:

1
2
3
4
<select id="selectById" parameterType="int" resultMap="brandResultMap">
select *
from tb_brand where id = ${id};
</select>

1.3.6 SQL语句中特殊字段处理

以后肯定会在SQL语句中写一下特殊字符,比如某一个字段大于某个值,如下图

image-20210729184756094

可以看出报错了,因为映射配置文件是xml类型的问题,而 > < 等这些字符在xml中有特殊含义,所以此时我们需要将这些符号进行转义,可以使用以下两种方式进行转义

  • 转义字符

    下图的 &lt; 就是 < 的转义字符。

    image-20210729185128686
  • image-20210729185030318

1.4 多条件查询

image-20210729203804276

我们经常会遇到如上图所示的多条件查询,将多条件查询的结果展示在下方的数据列表中。而我们做这个功能需要分析最终的SQL语句应该是什么样,思考两个问题

  • 条件表达式
  • 如何连接

条件字段 企业名称品牌名称 需要进行模糊查询,所以条件应该是:

image-20210729204458815

简单的分析后,我们来看功能实现的步骤:

  • 编写接口方法

    • 参数:所有查询条件
    • 结果:List
  • 在映射配置文件中编写SQL语句

  • 编写测试方法并执行

1.4.1 编写接口方法

BrandMapper 接口中定义多条件查询的方法。

而该功能有三个参数,我们就需要考虑定义接口时,参数应该如何定义。Mybatis针对多参数有多种实现

  • 使用 @Param("参数名称") 标记每一个参数,在映射配置文件中就需要使用 #{参数名称} 进行占位

    1
    List<Brand> selectByCondition(@Param("status") int status, @Param("companyName") String companyName,@Param("brandName") String brandName);
  • 将多个参数封装成一个 实体对象 ,将该实体对象作为接口的方法参数。该方式要求在映射配置文件的SQL中使用 #{内容} 时,里面的内容必须和实体类属性名保持一致。

    1
    List<Brand> selectByCondition(Brand brand);
  • 将多个参数封装到map集合中,将map集合作为接口的方法参数。该方式要求在映射配置文件的SQL中使用 #{内容} 时,里面的内容必须和map集合中键的名称一致。

    1
    List<Brand> selectByCondition(Map map);

1.4.2 编写SQL语句

BrandMapper.xml 映射配置文件中编写 statement,使用 resultMap 而不是使用 resultType

1
2
3
4
5
6
7
<select id="selectByCondition" resultMap="brandResultMap">
select *
from tb_brand
where status = #{status}
and company_name like #{companyName}
and brand_name like #{brandName}
</select>

1.4.3 编写测试方法

test/java 下的 com.itheima.mapper 包下的 MybatisTest类中 定义测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Test
public void testSelectByCondition() throws IOException {
//接收参数
int status = 1;
String companyName = "华为";
String brandName = "华为";

// 处理参数
companyName = "%" + companyName + "%";
brandName = "%" + brandName + "%";

//1. 获取SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);

//4. 执行方法
//方式一 :接口方法参数使用 @Param 方式调用的方法
//List<Brand> brands = brandMapper.selectByCondition(status, companyName, brandName);
//方式二 :接口方法参数是 实体类对象 方式调用的方法
//封装对象
/* Brand brand = new Brand();
brand.setStatus(status);
brand.setCompanyName(companyName);
brand.setBrandName(brandName);*/

//List<Brand> brands = brandMapper.selectByCondition(brand);

//方式三 :接口方法参数是 map集合对象 方式调用的方法
Map map = new HashMap();
map.put("status" , status);
map.put("companyName", companyName);
map.put("brandName" , brandName);
List<Brand> brands = brandMapper.selectByCondition(map);
System.out.println(brands);

//5. 释放资源
sqlSession.close();
}

1.4.4 动态SQL

上述功能实现存在很大的问题。用户在输入条件时,肯定不会所有的条件都填写,这个时候我们的SQL语句就不能那样写的

例如用户只输入 当前状态 时,SQL语句就是

1
select * from tb_brand where status = #{status}

而用户如果只输入企业名称时,SQL语句就是

1
select * from tb_brand where company_name like #{companName}

而用户如果输入了 当前状态企业名称 时,SQL语句又不一样

1
select * from tb_brand where status = #{status} and company_name like #{companName}

针对上述的需要,Mybatis对动态SQL有很强大的支撑:

  • if

  • choose (when, otherwise)

  • trim (where, set)

  • foreach

我们先学习 if 标签和 where 标签:

  • if 标签:条件判断

    • test 属性:逻辑表达式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <select id="selectByCondition" resultMap="brandResultMap">
    select *
    from tb_brand
    where
    <if test="status != null">
    and status = #{status}
    </if>
    <if test="companyName != null and companyName != '' ">
    and company_name like #{companyName}
    </if>
    <if test="brandName != null and brandName != '' ">
    and brand_name like #{brandName}
    </if>
    </select>

    如上的这种SQL语句就会根据传递的参数值进行动态的拼接。如果此时status和companyName有值那么就会值拼接这两个条件。

    执行结果如下:

    image-20210729212510291

    但是它也存在问题,如果此时给的参数值是

    1
    2
    3
    4
    Map map = new HashMap();
    // map.put("status" , status);
    map.put("companyName", companyName);
    map.put("brandName" , brandName);

    拼接的SQL语句就变成了

    1
    select * from tb_brand where and company_name like ? and brand_name like ?

    而上面的语句中 where 关键后直接跟 and 关键字,这就是一条错误的SQL语句。这个就可以使用 where 标签解决

  • where 标签

    • 作用:
      • 替换where关键字
      • 会动态的去掉第一个条件前的 and
      • 如果所有的参数没有值则不加where关键字
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <select id="selectByCondition" resultMap="brandResultMap">
    select *
    from tb_brand
    <where>
    <if test="status != null">
    and status = #{status}
    </if>
    <if test="companyName != null and companyName != '' ">
    and company_name like #{companyName}
    </if>
    <if test="brandName != null and brandName != '' ">
    and brand_name like #{brandName}
    </if>
    </where>
    </select>

    注意:需要给每个条件前都加上 and 关键字。

1.5 单个条件(动态SQL)

image-20210729213613029

如上图所示,在查询时只能选择 品牌名称当前状态企业名称 这三个条件中的一个,但是用户到底选择哪儿一个,我们并不能确定。这种就属于单个条件的动态SQL语句。

这种需求需要使用到 choose(when,otherwise)标签 实现, 而 choose 标签类似于Java 中的switch语句。

通过一个案例来使用这些标签

1.5.1 编写接口方法

BrandMapper 接口中定义单条件查询的方法。

1
2
3
4
5
6
/**
* 单条件动态查询
* @param brand
* @return
*/
List<Brand> selectByConditionSingle(Brand brand);

1.5.2 编写SQL语句

BrandMapper.xml 映射配置文件中编写 statement,使用 resultMap 而不是使用 resultType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<select id="selectByConditionSingle" resultMap="brandResultMap">
select *
from tb_brand
<where>
<choose><!--相当于switch-->
<when test="status != null"><!--相当于case-->
status = #{status}
</when>
<when test="companyName != null and companyName != '' "><!--相当于case-->
company_name like #{companyName}
</when>
<when test="brandName != null and brandName != ''"><!--相当于case-->
brand_name like #{brandName}
</when>
</choose>
</where>
</select>

1.5.3 编写测试方法

test/java 下的 com.itheima.mapper 包下的 MybatisTest类中 定义测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Test
public void testSelectByConditionSingle() throws IOException {
//接收参数
int status = 1;
String companyName = "华为";
String brandName = "华为";

// 处理参数
companyName = "%" + companyName + "%";
brandName = "%" + brandName + "%";

//封装对象
Brand brand = new Brand();
//brand.setStatus(status);
brand.setCompanyName(companyName);
//brand.setBrandName(brandName);

//1. 获取SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
List<Brand> brands = brandMapper.selectByConditionSingle(brand);
System.out.println(brands);

//5. 释放资源
sqlSession.close();
}

执行测试方法结果如下:

image-20210729214548756

1.6 添加数据

image-20210729214917317

如上图是我们平时在添加数据时展示的页面,而我们在该页面输入想要的数据后添加 提交 按钮,就会将这些数据添加到数据库中。接下来我们就来实现添加数据的操作。

  • 编写接口方法

    image-20210729215351651

    参数:除了id之外的所有的数据。id对应的是表中主键值,而主键我们是 ==自动增长== 生成的。

  • 编写SQL语句

    image-20210729215537167
  • 编写测试方法并执行

明确了该功能实现的步骤后,接下来我们进行具体的操作。

1.6.1 编写接口方法

BrandMapper 接口中定义添加方法。

1
2
3
4
 /**
* 添加
*/
void add(Brand brand);

1.6.2 编写SQL语句

BrandMapper.xml 映射配置文件中编写添加数据的 statement

1
2
3
4
<insert id="add">
insert into tb_brand (brand_name, company_name, ordered, description, status)
values (#{brandName}, #{companyName}, #{ordered}, #{description}, #{status});
</insert>

1.6.3 编写测试方法

test/java 下的 com.itheima.mapper 包下的 MybatisTest类中 定义测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Test
public void testAdd() throws IOException {
//接收参数
int status = 1;
String companyName = "波导手机";
String brandName = "波导";
String description = "手机中的战斗机";
int ordered = 100;

//封装对象
Brand brand = new Brand();
brand.setStatus(status);
brand.setCompanyName(companyName);
brand.setBrandName(brandName);
brand.setDescription(description);
brand.setOrdered(ordered);

//1. 获取SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//SqlSession sqlSession = sqlSessionFactory.openSession(true); //设置自动提交事务,这种情况不需要手动提交事务了
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
brandMapper.add(brand);
//提交事务
sqlSession.commit();
//5. 释放资源
sqlSession.close();
}

执行结果如下:

image-20210729220348255

1.6.4 添加-主键返回

在数据添加成功后,有时候需要获取插入数据库数据的主键(主键是自增长)。

比如:添加订单和订单项,如下图就是京东上的订单

image-20210729221207962

订单数据存储在订单表中,订单项存储在订单项表中。

  • 添加订单数据

    image-20210729221049462
  • 添加订单项数据,订单项中需要设置所属订单的id

    image-20210729221058898

明白了什么时候 主键返回 。接下来我们简单模拟一下,在添加完数据后打印id属性值,能打印出来说明已经获取到了。

我们将上面添加品牌数据的案例中映射配置文件里 statement 进行修改,如下

1
2
3
4
<insert id="add" useGeneratedKeys="true" keyProperty="id">
insert into tb_brand (brand_name, company_name, ordered, description, status)
values (#{brandName}, #{companyName}, #{ordered}, #{description}, #{status});
</insert>

在 insert 标签上添加如下属性:

  • useGeneratedKeys:是够获取自动增长的主键值。true表示获取
  • keyProperty :指定将获取到的主键值封装到哪儿个属性里

1.7 修改

image-20210729222642700

如图所示是修改页面,用户在该页面书写需要修改的数据,点击 提交 按钮,就会将数据库中对应的数据进行修改。注意一点,如果哪儿个输入框没有输入内容,我们是将表中数据对应字段值替换为空白还是保留字段之前的值?答案肯定是保留之前的数据。

接下来我们就具体来实现

1.7.1 编写接口方法

BrandMapper 接口中定义修改方法。

1
2
3
4
 /**
* 修改
*/
void update(Brand brand);

上述方法参数 Brand 就是封装了需要修改的数据,而id肯定是有数据的,这也是和添加方法的区别。

1.7.2 编写SQL语句

BrandMapper.xml 映射配置文件中编写修改数据的 statement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<update id="update">
update tb_brand
<set>
<if test="brandName != null and brandName != ''">
brand_name = #{brandName},
</if>
<if test="companyName != null and companyName != ''">
company_name = #{companyName},
</if>
<if test="ordered != null">
ordered = #{ordered},
</if>
<if test="description != null and description != ''">
description = #{description},
</if>
<if test="status != null">
status = #{status}
</if>
</set>
where id = #{id};
</update>

set 标签可以用于动态包含需要更新的列,忽略其它不更新的列。

1.7.3 编写测试方法

test/java 下的 com.itheima.mapper 包下的 MybatisTest类中 定义测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Test
public void testUpdate() throws IOException {
//接收参数
int status = 0;
String companyName = "波导手机";
String brandName = "波导";
String description = "波导手机,手机中的战斗机";
int ordered = 200;
int id = 6;

//封装对象
Brand brand = new Brand();
brand.setStatus(status);
// brand.setCompanyName(companyName);
// brand.setBrandName(brandName);
// brand.setDescription(description);
// brand.setOrdered(ordered);
brand.setId(id);

//1. 获取SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//SqlSession sqlSession = sqlSessionFactory.openSession(true);
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
int count = brandMapper.update(brand);
System.out.println(count);
//提交事务
sqlSession.commit();
//5. 释放资源
sqlSession.close();
}

执行测试方法结果如下:

image-20210729224205522

从结果中SQL语句可以看出,只修改了 status 字段值,因为我们给的数据中只给Brand实体对象的 status 属性设置值了。这就是 set 标签的作用。

1.8 删除一行数据

image-20210729224549305

如上图所示,每行数据后面都有一个 删除 按钮,当用户点击了该按钮,就会将改行数据删除掉。那我们就需要思考,这种删除是根据什么进行删除呢?是通过主键id删除,因为id是表中数据的唯一标识。

接下来就来实现该功能。

1.8.1 编写接口方法

BrandMapper 接口中定义根据id删除方法。

1
2
3
4
/**
* 根据id删除
*/
void deleteById(int id);

1.8.2 编写SQL语句

BrandMapper.xml 映射配置文件中编写删除一行数据的 statement

1
2
3
<delete id="deleteById">
delete from tb_brand where id = #{id};
</delete>

1.8.3 编写测试方法

test/java 下的 com.itheima.mapper 包下的 MybatisTest类中 定义测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 @Test
public void testDeleteById() throws IOException {
//接收参数
int id = 6;

//1. 获取SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//SqlSession sqlSession = sqlSessionFactory.openSession(true);
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
brandMapper.deleteById(id);
//提交事务
sqlSession.commit();
//5. 释放资源
sqlSession.close();
}

运行过程只要没报错,直接到数据库查询数据是否还存在。

1.9 批量删除

image-20210729225713894

如上图所示,用户可以选择多条数据,然后点击上面的 删除 按钮,就会删除数据库中对应的多行数据。

1.9.1 编写接口方法

BrandMapper 接口中定义删除多行数据的方法。

1
2
3
4
/**
* 批量删除
*/
void deleteByIds(int[] ids);

参数是一个数组,数组中存储的是多条数据的id

1.9.2 编写SQL语句

BrandMapper.xml 映射配置文件中编写删除多条数据的 statement

编写SQL时需要遍历数组来拼接SQL语句。Mybatis 提供了 foreach 标签供我们使用

foreach 标签

用来迭代任何可迭代的对象(如数组,集合)。

  • collection 属性:
    • mybatis会将数组参数,封装为一个Map集合。
      • 默认:array = 数组
      • 使用@Param注解改变map集合的默认key的名称
  • item 属性:本次迭代获取到的元素。
  • separator 属性:集合项迭代之间的分隔符。foreach 标签不会错误地添加多余的分隔符。也就是最后一次迭代不会加分隔符。
  • open 属性:该属性值是在拼接SQL语句之前拼接的语句,只会拼接一次
  • close 属性:该属性值是在拼接SQL语句拼接后拼接的语句,只会拼接一次
1
2
3
4
5
6
7
8
<delete id="deleteByIds">
delete from tb_brand where id
in
<foreach collection="array" item="id" separator="," open="(" close=")">
#{id}
</foreach>
;
</delete>

假如数组中的id数据是{1,2,3},那么拼接后的sql语句就是:

1
delete from tb_brand where id in (1,2,3);

1.9.3 编写测试方法

test/java 下的 com.itheima.mapper 包下的 MybatisTest类中 定义测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void testDeleteByIds() throws IOException {
//接收参数
int[] ids = {5,7,8};

//1. 获取SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//SqlSession sqlSession = sqlSessionFactory.openSession(true);
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
brandMapper.deleteByIds(ids);
//提交事务
sqlSession.commit();
//5. 释放资源
sqlSession.close();
}

1.10 Mybatis参数传递

Mybatis 接口方法中可以接收各种各样的参数,如下:

  • 多个参数
  • 单个参数:单个参数又可以是如下类型
    • POJO 类型
    • Map 集合类型
    • Collection 集合类型
    • List 集合类型
    • Array 类型
    • 其他类型

1.10.1 多个参数

如下面的代码,就是接收两个参数,而接收多个参数需要使用 @Param 注解,那么为什么要加该注解呢?这个问题要弄明白就必须来研究Mybatis 底层对于这些参数是如何处理的。

1
User select(@Param("username") String username,@Param("password") String password);
1
2
3
4
5
6
7
<select id="select" resultType="user">
select *
from tb_user
where
username=#{username}
and password=#{password}
</select>

我们在接口方法中定义多个参数,Mybatis 会将这些参数封装成 Map 集合对象,值就是参数值,而键在没有使用 @Param 注解时有以下命名规则:

  • 以 arg 开头 :第一个参数就叫 arg0,第二个参数就叫 arg1,以此类推。如:

    map.put(“arg0”,参数值1);

    map.put(“arg1”,参数值2);

  • 以 param 开头 : 第一个参数就叫 param1,第二个参数就叫 param2,依次类推。如:

    map.put(“param1”,参数值1);

    map.put(“param2”,参数值2);

代码验证:

  • UserMapper 接口中定义如下方法

    1
    User select(String username,String password);
  • UserMapper.xml 映射配置文件中定义SQL

    1
    2
    3
    4
    5
    6
    7
    <select id="select" resultType="user">
    select *
    from tb_user
    where
    username=#{arg0}
    and password=#{arg1}
    </select>

    或者

    1
    2
    3
    4
    5
    6
    7
    <select id="select" resultType="user">
    select *
    from tb_user
    where
    username=#{param1}
    and password=#{param2}
    </select>
  • 运行代码结果如下

    image-20210805230303461

    在映射配合文件的SQL语句中使用用 arg 开头的和 param 书写,代码的可读性会变的特别差,此时可以使用 @Param 注解。

在接口方法参数上使用 @Param 注解,Mybatis 会将 arg 开头的键名替换为对应注解的属性值。

代码验证:

  • UserMapper 接口中定义如下方法,在 username 参数前加上 @Param 注解

    1
    User select(@Param("username") String username, String password);

    Mybatis 在封装 Map 集合时,键名就会变成如下:

    map.put(“username”,参数值1);

    map.put(“arg1”,参数值2);

    map.put(“param1”,参数值1);

    map.put(“param2”,参数值2);

  • UserMapper.xml 映射配置文件中定义SQL

    1
    2
    3
    4
    5
    6
    7
    <select id="select" resultType="user">
    select *
    from tb_user
    where
    username=#{username}
    and password=#{param2}
    </select>
  • 运行程序结果没有报错。而如果将 #{} 中的 username 还是写成 arg0

    1
    2
    3
    4
    5
    6
    7
    <select id="select" resultType="user">
    select *
    from tb_user
    where
    username=#{arg0}
    and password=#{param2}
    </select>
  • 运行程序则可以看到错误

    image-20210805231727206

==结论:以后接口参数是多个时,在每个参数上都使用 @Param 注解。这样代码的可读性更高。==

1.10.2 单个参数

  • POJO 类型

    直接使用。要求 属性名参数占位符名称 一致

  • Map 集合类型

    直接使用。要求 map集合的键名参数占位符名称 一致

  • Collection 集合类型

    Mybatis 会将集合封装到 map 集合中,如下:

    map.put(“arg0”,collection集合);

    map.put(“collection”,collection集合;

    ==可以使用 @Param 注解替换map集合中默认的 arg 键名。==

  • List 集合类型

    Mybatis 会将集合封装到 map 集合中,如下:

    map.put(“arg0”,list集合);

    map.put(“collection”,list集合);

    map.put(“list”,list集合);

    ==可以使用 @Param 注解替换map集合中默认的 arg 键名。==

  • Array 类型

    Mybatis 会将集合封装到 map 集合中,如下:

    map.put(“arg0”,数组);

    map.put(“array”,数组);

    ==可以使用 @Param 注解替换map集合中默认的 arg 键名。==

  • 其他类型

    比如int类型,参数占位符名称 叫什么都可以。尽量做到见名知意

2,注解实现CRUD

使用注解开发会比配置文件开发更加方便。如下就是使用注解进行开发

1
2
@Select(value = "select * from tb_user where id = #{id}")
public User select(int id);

==注意:==

  • 注解是用来替换映射配置文件方式配置的,所以使用了注解,就不需要再映射配置文件中书写对应的 statement

Mybatis 针对 CURD 操作都提供了对应的注解,已经做到见名知意。如下:

  • 查询 :@Select
  • 添加 :@Insert
  • 修改 :@Update
  • 删除 :@Delete

接下来我们做一个案例来使用 Mybatis 的注解开发

代码实现:

  • 将之前案例中 UserMapper.xml 中的 根据id查询数据 的 statement 注释掉

    image-20210805235229938
  • UserMapper 接口的 selectById 方法上添加注解

    image-20210805235405070
  • 运行测试程序也能正常查询到数据

我们课程上只演示这一个查询的注解开发,其他的同学们下来可以自己实现,都是比较简单。

==注意:==在官方文档中 入门 中有这样的一段话:

image-20210805234302849

所以,==注解完成简单功能,配置文件完成复杂功能。==

而我们之前写的动态 SQL 就是复杂的功能,如果用注解使用的话,就需要使用到 Mybatis 提供的SQL构建器来完成,而对应的代码如下:

image-20210805234842497

上述代码将java代码和SQL语句融到了一块,使得代码的可读性大幅度降低。

HTTP&Tomcat&Servlet

今日目标:

  • 了解JavaWeb开发的技术栈
  • 理解HTTP协议和HTTP请求与响应数据的格式
  • 掌握Tomcat的使用
  • 掌握在IDEA中使用Tomcat插件
  • 理解Servlet的执行流程和生命周期
  • 掌握Servlet的使用和相关配置

1,Web概述

1.1 Web和JavaWeb的概念

==Web是全球广域网,也称为万维网(www),能够通过浏览器访问的网站。==
在我们日常的生活中,经常会使用浏览器去访问百度京东传智官网等这些网站,这些网站统称为Web网站。如下就是通过浏览器访问传智官网的界面:
1627031023395
我们知道了什么是Web,那么JavaWeb又是什么呢?顾名思义==JavaWeb就是用Java技术来解决相关web互联网领域的技术栈。==
等学习完JavaWeb之后,同学们就可以使用Java语言开发我们上述所说的网站。而国内很多大型网站公司也是首选Java语言来解决web互联网相关的问题。那都有哪些公司的系统是使用Java语言的呢?

使用Java语言开发互联网系统是有很多技术栈需要大家了解,具体都有哪些呢?

1.2 JavaWeb技术栈

了解JavaWeb技术栈之前,有一个很重要的概念要介绍。

1.2.1 B/S架构

什么是B/S架构?
B/S 架构:Browser/Server,浏览器/服务器 架构模式,它的特点是,客户端只需要浏览器,应用程序的逻辑和数据都存储在服务器端。浏览器只需要请求服务器,获取Web资源,服务器把Web资源发送给浏览器即可。大家可以通过下面这张图来回想下我们平常的上网过程:
1627031933553

  • 打开浏览器访问百度首页,输入要搜索的内容,点击回车或百度一下,就可以获取和搜索相关的内容
  • 思考下搜索的内容并不在我们自己的点上,那么这些内容从何而来?答案很明显是从百度服务器返回给我们的
  • 日常百度的小细节,逢年过节百度的logo会更换不同的图片,服务端发生变化,客户端不需做任务事情就能获取最新内容
  • 所以说B/S架构的好处:易于维护升级:服务器端升级后,客户端无需任何部署就可以使用到新的版本。
    了解了什么是B/S架构后,作为后台开发工程师的我们将来主要关注的是服务端的开发和维护工作。在服务端将来会放很多资源,都有哪些资源呢?

1.2.2 静态资源

  • 静态资源主要包含HTML、CSS、JavaScript、图片等,主要负责页面的展示。
  • 我们之前已经学过前端网页制作三剑客(HTML+CSS+JavaScript),使用这些技术我们就可以制作出效果比较丰富的网页,将来展现给用户。但是由于做出来的这些内容都是静态的,这就会导致所有的人看到的内容将是一模一样。
  • 在日常上网的过程中,我们除了看到这些好看的页面以外,还会碰到很多动态内容,比如我们常见的百度登录效果:
    1627037814180
    张三登录以后在网页的右上角看到的是 张三,而李四登录以后看到的则是李四。所以不同的用户访问相同的资源看到的内容大多数是不一样的,要想实现这样的效果,光靠静态资源是无法实现的。

1.2.3 动态资源

  • 动态资源主要包含Servlet、JSP等,主要用来负责逻辑处理。
  • 动态资源处理完逻辑后会把得到的结果交给静态资源来进行展示,动态资源和静态资源要结合一起使用。
  • 动态资源虽然可以处理逻辑,但是当用户来登录百度的时候,就需要输入用户名密码,这个时候我们就又需要解决的一个问题是,用户在注册的时候填入的用户名和密码、以及我们经常会访问到一些数据列表的内容展示(如下图所示),这些数据都存储在哪里?我们需要的时候又是从哪里来取呢?
    1627038674340

1.2.4 数据库

  • 数据库主要负责存储数据。
  • 整个Web的访问过程就如下图所示:
    1627039320220
    (1)浏览器发送一个请求到服务端,去请求所需要的相关资源;
    (2)资源分为动态资源和静态资源,动态资源可以是使用Java代码按照Servlet和JSP的规范编写的内容;
    (3)在Java代码可以进行业务处理也可以从数据库中读取数据;
    (4)拿到数据后,把数据交给HTML页面进行展示,再结合CSS和JavaScript使展示效果更好;
    (5)服务端将静态资源响应给浏览器;
    (6)浏览器将这些资源进行解析;
    (7)解析后将效果展示在浏览器,用户就可以看到最终的结果。
    在整个Web的访问过程中,会设计到很多技术,这些技术有已经学习过的,也有还未涉及到的内容,都有哪些还没有涉及到呢?

1.2.5 HTTP协议

  • HTTP协议:主要定义通信规则
  • 浏览器发送请求给服务器,服务器响应数据给浏览器,这整个过程都需要遵守一定的规则,之前大家学习过TCP、UDP,这些都属于规则,这里我们需要使用的是HTTP协议,这也是一种规则。

1.2.6 Web服务器

  • Web服务器:负责解析 HTTP 协议,解析请求数据,并发送响应数据
  • 浏览器按照HTTP协议发送请求和数据,后台就需要一个Web服务器软件来根据HTTP协议解析请求和数据,然后把处理结果再按照HTTP协议发送给浏览器
  • Web服务器软件有很多,我们课程中将学习的是目前最为常用的==Tomcat==服务器

到这为止,关于JavaWeb中用到的技术栈我们就介绍完了,这里面就只有HTTP协议、Servlet、JSP以及Tomcat这些知识是没有学习过的,所以整个Web核心主要就是来学习这些技术。

1.3 Web核心课程安排

1627043194238

整个Web核心,我们总共有六天的学习内容,分别是:

  • 第一天:HTTP、Tomcat、Servlet
  • 第二天:Request(请求)、Response(响应)
  • 第三天:JSP、会话技术(Cookie、Session)
  • 第四天:Filter(过滤器)、Listener(监听器)
  • 第五天:Ajax、Vue、ElementUI
  • 第六天:综合案例

(1)Request是从客户端向服务端发出的请求对象,

(2)Response是从服务端响应给客户端的结果对象,

(3)JSP是动态网页技术,

(4)会话技术是用来存储客户端和服务端交互所产生的数据,

(5)过滤器是用来拦截客户端的请求,

(6)监听器是用来监听特定事件,

(7)Ajax、Vue、ElementUI都是属于前端技术

这些技术都该如何来使用,我们后面会一个个进行详细的讲解。接下来我们来学习下HTTP、Tomcat和Servlet。

2, HTTP

2.1 简介

HTTP概念

HyperText Transfer Protocol,超文本传输协议,规定了浏览器和服务器之间==数据传输的规则==。

  • 数据传输的规则指的是请求数据和响应数据需要按照指定的格式进行传输。
  • 如果想知道具体的格式,可以打开浏览器,点击F12打开开发者工具,点击Network来查看某一次请求的请求数据和响应数据具体的格式内容,如下图所示:

1627046235092

注意:在浏览器中如果看不到上述内容,需要清除浏览器的浏览数据。chrome浏览器可以使用ctrl+shift+Del进行清除。

==所以学习HTTP主要就是学习请求和响应数据的具体格式内容。==

HTTP协议特点

HTTP协议有它自己的一些特点,分别是:

  • 基于TCP协议: 面向连接,安全

    TCP是一种面向连接的(建立连接之前是需要经过三次握手)、可靠的、基于字节流的传输层通信协议,在数据传输方面更安全。

  • 基于请求-响应模型的:一次请求对应一次响应

    请求和响应是一一对应关系

  • HTTP协议是无状态协议:对于事物处理没有记忆能力。每次请求-响应都是独立的

    无状态指的是客户端发送HTTP请求给服务端之后,服务端根据请求响应数据,响应完后,不会记录任何信息。这种特性有优点也有缺点,

    • 缺点:多次请求间不能共享数据
    • 优点:速度快

    请求之间无法共享数据会引发的问题,如:

    • 京东购物,加入购物车去购物车结算是两次请求,
    • HTTP协议的无状态特性,加入购物车请求响应结束后,并未记录加入购物车是何商品
    • 发起去购物车结算的请求后,因为无法获取哪些商品加入了购物车,会导致此次请求无法正确展示数据

    具体使用的时候,我们发现京东是可以正常展示数据的,原因是Java早已考虑到这个问题,并提出了使用会话技术(Cookie、Session)来解决这个问题。具体如何来做,我们后面会详细讲到。刚才提到HTTP协议是规定了请求和响应数据的格式,那具体的格式是什么呢?

2.2 请求数据格式

2.2.1 格式介绍

请求数据总共分为三部分内容,分别是==请求行==、==请求头==、==请求体==

1627050004221

  • 请求行: HTTP请求中的第一行数据,请求行包含三块内容,分别是 GET[请求方式] /[请求URL路径] HTTP/1.1[HTTP协议及版本]

    请求方式有七种,最常用的是GET和POST

  • 请求头: 第二行开始,格式为key: value形式

    请求头中会包含若干个属性,常见的HTTP请求头有:

    1
    2
    3
    4
    5
    Host: 表示请求的主机名
    User-Agent: 浏览器版本,例如Chrome浏览器的标识类似Mozilla/5.0 ...Chrome/79,IE浏览器的标识类似Mozilla/5.0 (Windows NT ...)like Gecko;
    Accept:表示浏览器能接收的资源类型,如text/*,image/*或者*/*表示所有;
    Accept-Language:表示浏览器偏好的语言,服务器可以据此返回不同语言的网页;
    Accept-Encoding:表示浏览器可以支持的压缩类型,例如gzip, deflate等。

    ==这些数据有什么用处?==

    举例说明:服务端可以根据请求头中的内容来获取客户端的相关信息,有了这些信息服务端就可以处理不同的业务需求,比如:

    • 不同浏览器解析HTML和CSS标签的结果会有不一致,所以就会导致相同的代码在不同的浏览器会出现不同的效果
    • 服务端根据客户端请求头中的数据获取到客户端的浏览器类型,就可以根据不同的浏览器设置不同的代码来达到一致的效果
    • 这就是我们常说的浏览器兼容问题
  • 请求体: POST请求的最后一部分,存储请求参数

    1627050930378

    如上图红线框的内容就是请求体的内容,请求体和请求头之间是有一个空行隔开。此时浏览器发送的是POST请求,为什么不能使用GET呢?这时就需要回顾GET和POST两个请求之间的区别了:

    • GET请求请求参数在请求行中,没有请求体,POST请求请求参数在请求体中
    • GET请求请求参数大小有限制,POST没有

2.2.2 实例演示

代码\http 拷贝到IDEA的工作目录中,比如D:\workspace\web目录,

1627278511902

使用IDEA打开

1627278583127

打开后,可以点击项目中的html\19-表单验证.html,使用浏览器打开,通过修改页面中form表单的method属性来测试GET请求和POST请求的参数携带方式。

1627278725007

小结:

  1. 请求数据中包含三部分内容,分别是请求行、请求头和请求体

  2. POST请求数据在请求体中,GET请求数据在请求行上

2.3 响应数据格式

2.3.1 格式介绍

响应数据总共分为三部分内容,分别是==响应行==、==响应头==、==响应体==

1627053710214

  • 响应行:响应数据的第一行,响应行包含三块内容,分别是 HTTP/1.1[HTTP协议及版本] 200[响应状态码] ok[状态码的描述]

  • 响应头:第二行开始,格式为key:value形式

    响应头中会包含若干个属性,常见的HTTP响应头有:

    1
    2
    3
    4
    Content-Type:表示该响应内容的类型,例如text/html,image/jpeg;
    Content-Length:表示该响应内容的长度(字节数);
    Content-Encoding:表示该响应压缩算法,例如gzip;
    Cache-Control:指示客户端应如何缓存,例如max-age=300表示可以最多缓存300秒
  • 响应体: 最后一部分。存放响应数据

    上图中…这部分内容就是响应体,它和响应头之间有一个空行隔开。

2.3.2 响应状态码

参考: 资料/1.HTTP/《响应状态码.md》

关于响应状态码,我们先主要认识三个状态码,其余的等后期用到了再去掌握:

  • 200 ok 客户端请求成功
  • 404 Not Found 请求资源不存在
  • 500 Internal Server Error 服务端发生不可预期的错误

2.3.3 自定义服务器

在前面我们导入到IDEA中的http项目中,有一个Server.java类,这里面就是自定义的一个服务器代码,主要使用到的是ServerSocketSocket

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package com.itheima;

import sun.misc.IOUtils;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
/*
自定义服务器
*/
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(8080); // 监听指定端口
System.out.println("server is running...");
while (true){
Socket sock = ss.accept();
System.out.println("connected from " + sock.getRemoteSocketAddress());
Thread t = new Handler(sock);
t.start();
}
}
}

class Handler extends Thread {
Socket sock;

public Handler(Socket sock) {
this.sock = sock;
}

public void run() {
try (InputStream input = this.sock.getInputStream()) {
try (OutputStream output = this.sock.getOutputStream()) {
handle(input, output);
}
} catch (Exception e) {
try {
this.sock.close();
} catch (IOException ioe) {
}
System.out.println("client disconnected.");
}
}

private void handle(InputStream input, OutputStream output) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
// 读取HTTP请求:
boolean requestOk = false;
String first = reader.readLine();
if (first.startsWith("GET / HTTP/1.")) {
requestOk = true;
}
for (;;) {
String header = reader.readLine();
if (header.isEmpty()) { // 读取到空行时, HTTP Header读取完毕
break;
}
System.out.println(header);
}
System.out.println(requestOk ? "Response OK" : "Response Error");
if (!requestOk) {
// 发送错误响应:
writer.write("HTTP/1.0 404 Not Found\r\n");
writer.write("Content-Length: 0\r\n");
writer.write("\r\n");
writer.flush();
} else {
// 发送成功响应:

//读取html文件,转换为字符串
BufferedReader br = new BufferedReader(new FileReader("http/html/a.html"));
StringBuilder data = new StringBuilder();
String line = null;
while ((line = br.readLine()) != null){
data.append(line);
}
br.close();
int length = data.toString().getBytes(StandardCharsets.UTF_8).length;

writer.write("HTTP/1.1 200 OK\r\n");
writer.write("Connection: keep-alive\r\n");
writer.write("Content-Type: text/html\r\n");
writer.write("Content-Length: " + length + "\r\n");
writer.write("\r\n"); // 空行标识Header和Body的分隔
writer.write(data.toString());
writer.flush();
}
}
}

上面代码,大家不需要自己写,主要通过上述代码,只需要大家了解到服务器可以使用java完成编写,是可以接受页面发送的请求和响应数据给前端浏览器的,真正用到的Web服务器,我们不会自己写,都是使用目前比较流行的web服务器,比如==Tomcat==

小结

  1. 响应数据中包含三部分内容,分别是响应行、响应头和响应体

  2. 掌握200,404,500这三个响应状态码所代表含义,分布是成功、所访问资源不存在和服务的错误

3, Tomcat

3.1 简介

3.1.1 什么是Web服务器

Web服务器是一个应用程序(==软件==),对HTTP协议的操作进行封装,使得程序员不必直接对协议进行操作,让Web开发更加便捷。主要功能是”提供网上信息浏览服务”。

1627058356051

Web服务器是安装在服务器端的一款软件,将来我们把自己写的Web项目部署到Web Tomcat服务器软件中,当Web服务器软件启动后,部署在Web服务器软件中的页面就可以直接通过浏览器来访问了。

Web服务器软件使用步骤

  • 准备静态资源
  • 下载安装Web服务器软件
  • 将静态资源部署到Web服务器上
  • 启动Web服务器使用浏览器访问对应的资源

上述内容在演示的时候,使用的是Apache下的Tomcat软件,至于Tomcat软件如何使用,后面会详细的讲到。而对于Web服务器来说,实现的方案有很多,Tomcat只是其中的一种,而除了Tomcat以外,还有很多优秀的Web服务器,比如:

1627060368806

Tomcat就是一款软件,我们主要是以学习如何去使用为主。具体我们会从以下这些方向去学习:

  1. 简介: 初步认识下Tomcat

  2. 基本使用: 安装、卸载、启动、关闭、配置和项目部署,这些都是对Tomcat的基本操作

  3. IDEA中如何创建Maven Web项目

  4. IDEA中如何使用Tomcat,后面这两个都是我们以后开发经常会用到的方式

首选我们来认识下Tomcat。

Tomcat

Tomcat的相关概念:

  • Tomcat是Apache软件基金会一个核心项目,是一个开源免费的轻量级Web服务器,支持Servlet/JSP少量JavaEE规范。

  • 概念中提到了JavaEE规范,那什么又是JavaEE规范呢?

    JavaEE: Java Enterprise Edition,Java企业版。指Java企业级开发的技术规范总和。包含13项技术规范:JDBC、JNDI、EJB、RMI、JSP、Servlet、XML、JMS、Java IDL、JTS、JTA、JavaMail、JAF。

  • 因为Tomcat支持Servlet/JSP规范,所以Tomcat也被称为Web容器、Servlet容器。Servlet需要依赖Tomcat才能运行。

  • Tomcat的官网: https://tomcat.apache.org/ 从官网上可以下载对应的版本进行使用。

Tomcat的LOGO

1627176045795

小结

通过这一节的学习,我们需要掌握以下内容:

  1. Web服务器的作用

封装HTTP协议操作,简化开发

可以将Web项目部署到服务器中,对外提供网上浏览服务

  1. Tomcat是一个轻量级的Web服务器,支持Servlet/JSP少量JavaEE规范,也称为Web容器,Servlet容器。

3.2 基本使用

Tomcat总共分两部分学习,先来学习Tomcat的基本使用,包括Tomcat的==下载、安装、卸载、启动和关闭==。

3.2.1 下载

直接从官网下载

1627178001030

大家可以自行下载,也可以直接使用资料中已经下载好的资源,

Tomcat的软件程序 资料/2. Tomcat/apache-tomcat-8.5.68-windows-x64.zip

Tomcat的源码 资料/2. Tomcat/tomcat源码/apache-tomcat-8.5.68-src.zip

3.2.2 安装

Tomcat是绿色版,直接解压即可

  • 在D盘的software目录下,将apache-tomcat-8.5.68-windows-x64.zip进行解压缩,会得到一个apache-tomcat-8.5.68的目录,Tomcat就已经安装成功。

    ==注意==,Tomcat在解压缩的时候,解压所在的目录可以任意,但最好解压到一个不包含中文和空格的目录,因为后期在部署项目的时候,如果路径有中文或者空格可能会导致程序部署失败。

  • 打开apache-tomcat-8.5.68目录就能看到如下目录结构,每个目录中包含的内容需要认识下,

    1627178815892

    bin:目录下有两类文件,一种是以.bat结尾的,是Windows系统的可执行文件,一种是以.sh结尾的,是Linux系统的可执行文件。

    webapps:就是以后项目部署的目录

    到此,Tomcat的安装就已经完成。

3.2.3 卸载

卸载比较简单,可以直接删除目录即可

3.2.4 启动

双击: bin\startup.bat

1627179006011

启动后,通过浏览器访问 http://localhost:8080能看到Apache Tomcat的内容就说明Tomcat已经启动成功。

1627199957728

==注意==: 启动的过程中,控制台有中文乱码,需要修改conf/logging.prooperties

1627199827589

3.2.5 关闭

关闭有三种方式

  • 直接x掉运行窗口:强制关闭[不建议]
  • bin\shutdown.bat:正常关闭
  • ctrl+c: 正常关闭

3.2.6 配置

修改端口

  • Tomcat默认的端口是8080,要想修改Tomcat启动的端口号,需要修改 conf/server.xml

1627200509883

注: HTTP协议默认端口号为80,如果将Tomcat端口号改为80,则将来访问Tomcat时,将不用输入端口号。

启动时可能出现的错误

  • Tomcat的端口号取值范围是0-65535之间任意未被占用的端口,如果设置的端口号被占用,启动的时候就会包如下的错误

    1627200780590

  • Tomcat启动的时候,启动窗口一闪而过: 需要检查JAVA_HOME环境变量是否正确配置

1627201248802

3.2.7 部署

  • Tomcat部署项目: 将项目放置到webapps目录下,即部署完成。

    • 资料/2. Tomcat/hello 目录拷贝到Tomcat的webapps目录下

    • 通过浏览器访问http://localhost/hello/a.html,能看到下面的内容就说明项目已经部署成功。

      1627201572748

      但是呢随着项目的增大,项目中的资源也会越来越多,项目在拷贝的过程中也会越来越费时间,该如何解决呢?

  • 一般JavaWeb项目会被打包称==war==包,然后将war包放到Webapps目录下,Tomcat会自动解压缩war文件

    • 资料/2. Tomcat/haha.war目录拷贝到Tomcat的webapps目录下

    • Tomcat检测到war包后会自动完成解压缩,在webapps目录下就会多一个haha目录

    • 通过浏览器访问http://localhost/haha/a.html,能看到下面的内容就说明项目已经部署成功。

      1627201868752

至此,Tomcat的部署就已经完成了,至于如何获得项目对应的war包,后期我们会借助于IDEA工具来生成。

3.3 Maven创建Web项目

介绍完Tomcat的基本使用后,我们来学习在IDEA中如何创建Maven Web项目,学习这种方式的原因是以后Tomcat中运行的绝大多数都是Web项目,而使用Maven工具能更加简单快捷的把Web项目给创建出来,所以Maven的Web项目具体如何来构建呢?

在真正创建Maven Web项目之前,我们先要知道Web项目长什么样子,具体的结构是什么?

3.3.1 Web项目结构

Web项目的结构分为:开发中的项目和开发完可以部署的Web项目,这两种项目的结构是不一样的,我们一个个来介绍下:

  • Maven Web项目结构: 开发中的项目

    1627202865978

  • 开发完成部署的Web项目

    1627202903750

    • 开发项目通过执行Maven打包命令==package==,可以获取到部署的Web项目目录
    • 编译后的Java字节码文件和resources的资源文件,会被放到WEB-INF下的classes目录下
    • pom.xml中依赖坐标对应的jar包,会被放入WEB-INF下的lib目录下

3.3.2 创建Maven Web项目

介绍完Maven Web的项目结构后,接下来使用Maven来创建Web项目,创建方式有两种:使用骨架和不使用骨架

使用骨架

具体的步骤包含:

1.创建Maven项目

2.选择使用Web项目骨架

3.输入Maven项目坐标创建项目

4.确认Maven相关的配置信息后,完成项目创建

5.删除pom.xml中多余内容

6.补齐Maven Web项目缺失的目录结构

  1. 创建Maven项目

    1627227574092

  2. 选择使用Web项目骨架

    1627227650406

  3. 输入Maven项目坐标创建项目

    1627228065007

  4. 确认Maven相关的配置信息后,完成项目创建

    1627228413280

  5. 删除pom.xml中多余内容,只留下面的这些内容,注意打包方式 jar和war的区别

    1627228584625

  6. 补齐Maven Web项目缺失的目录结构,默认没有java和resources目录,需要手动完成创建补齐,最终的目录结果如下

不使用骨架

具体的步骤包含:

1.创建Maven项目

2.选择不使用Web项目骨架

3.输入Maven项目坐标创建项目

4.在pom.xml设置打包方式为war

5.补齐Maven Web项目缺失webapp的目录结构

6.补齐Maven Web项目缺失WEB-INF/web.xml的目录结构

  1. 创建Maven项目

    1627229111549

  2. 选择不使用Web项目骨架

    1627229137316

  3. 输入Maven项目坐标创建项目

    1627229371251

  4. 在pom.xml设置打包方式为war,默认是不写代表打包方式为jar

    1627229428161

  5. 补齐Maven Web项目缺失webapp的目录结构

    1627229584134

  6. 补齐Maven Web项目缺失WEB-INF/web.xml的目录结构

    1627229676800

  7. 补充完后,最终的项目结构如下:

    1627229478030

上述两种方式,创建的web项目,都不是很全,需要手动补充内容,至于最终采用哪种方式来创建Maven Web项目,都是可以的,根据各自的喜好来选择使用即可。

小结

1.掌握Maven Web项目的目录结构

2.掌握使用骨架的方式创建Maven Web项目

1627204022604

3.掌握不使用骨架的方式创建Maven Web项目

1627204076090

3.4 IDEA使用Tomcat

  • Maven Web项目创建成功后,通过Maven的package命令可以将项目打包成war包,将war文件拷贝到Tomcat的webapps目录下,启动Tomcat就可以将项目部署成功,然后通过浏览器进行访问即可。
  • 然而我们在开发的过程中,项目中的内容会经常发生变化,如果按照上面这种方式来部署测试,是非常不方便的
  • 如何在IDEA中能快速使用Tomcat呢?

在IDEA中集成使用Tomcat有两种方式,分别是==集成本地Tomcat==和==Tomcat Maven插件==

3.4.1 集成本地Tomcat

目标: 将刚才本地安装好的Tomcat8集成到IDEA中,完成项目部署,具体的实现步骤

  1. 打开添加本地Tomcat的面板

    1627229992900

  2. 指定本地Tomcat的具体路径

    1627230313062

  3. 修改Tomcat的名称,此步骤可以不改,只是让名字看起来更有意义,HTTP port中的端口也可以进行修改,比如把8080改成80

    1627230366658

  4. 将开发项目部署项目到Tomcat中

    1627230913259

    扩展内容: xxx.war和 xxx.war exploded这两种部署项目模式的区别?

    • war模式是将WEB工程打成war包,把war包发布到Tomcat服务器上

    • war exploded模式是将WEB工程以当前文件夹的位置关系发布到Tomcat服务器上

    • war模式部署成功后,Tomcat的webapps目录下会有部署的项目内容

    • war exploded模式部署成功后,Tomcat的webapps目录下没有,而使用的是项目的target目录下的内容进行部署

    • 建议大家都选war模式进行部署,更符合项目部署的实际情况

  5. 部署成功后,就可以启动项目,为了能更好的看到启动的效果,可以在webapp目录下添加a.html页面

    1627233265351

  6. 启动成功后,可以通过浏览器进行访问测试

    1627232743706

  7. 最终的注意事项

    1627232916624

至此,IDEA中集成本地Tomcat进行项目部署的内容我们就介绍完了,整体步骤如下,大家需要按照流程进行部署操作练习。

1627205657117

3.4.2 Tomcat Maven插件

在IDEA中使用本地Tomcat进行项目部署,相对来说步骤比较繁琐,所以我们需要一种更简便的方式来替换它,那就是直接使用Maven中的Tomcat插件来部署项目,具体的实现步骤,只需要两步,分别是:

  1. 在pom.xml中添加Tomcat插件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <build>
    <plugins>
    <!--Tomcat插件 -->
    <plugin>
    <groupId>org.apache.tomcat.maven</groupId>
    <artifactId>tomcat7-maven-plugin</artifactId>
    <version>2.2</version>
    </plugin>
    </plugins>
    </build>
  2. 使用Maven Helper插件快速启动项目,选中项目,右键–>Run Maven –> tomcat7:run

1627233963315

==注意:==

  • 如果选中项目并右键点击后,看不到Run Maven和Debug Maven,这个时候就需要在IDEA中下载Maven Helper插件,具体的操作方式为: File –> Settings –> Plugins –> Maven Helper —> Install,安装完后按照提示重启IDEA,就可以看到了。

1627234184076

  • Maven Tomcat插件目前只有Tomcat7版本,没有更高的版本可以使用
  • 使用Maven Tomcat插件,要想修改Tomcat的端口和访问路径,可以直接修改pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<build>
<plugins>
<!--Tomcat插件 -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>80</port><!--访问端口号 -->
<!--项目访问路径
未配置访问路径: http://localhost:80/tomcat-demo2/a.html
配置/后访问路径: http://localhost:80/a.html
如果配置成 /hello,访问路径会变成什么?
答案: http://localhost:80/hello/a.html
-->
<path>/</path>
</configuration>
</plugin>
</plugins>
</build>

小结

通过这一节的学习,大家要掌握在IDEA中使用Tomcat的两种方式,集成本地Tomcat和使用Maven的Tomcat插件。后者更简单,推荐大家使用,但是如果对于Tomcat的版本有比较高的要求,要在Tomcat7以上,这个时候就只能用前者了。

4, Servlet

4.1 简介

1627234763207

  • Servlet是JavaWeb最为核心的内容,它是Java提供的一门==动态==web资源开发技术。

  • 使用Servlet就可以实现,根据不同的登录用户在页面上动态显示不同内容。

  • Servlet是JavaEE规范之一,其实就是一个接口,将来我们需要定义Servlet类实现Servlet接口,并由web服务器运行Servlet

    1627234972853

介绍完Servlet是什么以后,接下来我们就按照快速入门->执行流程->生命周期->体系结构->urlPattern配置->XML配置的学习步骤,一步步完成对Servlet的知识学习,首选我们来通过一个入门案例来快速把Servlet用起来。

4.2 快速入门

==需求分析: 编写一个Servlet类,并使用IDEA中Tomcat插件进行部署,最终通过浏览器访问所编写的Servlet程序。==

具体的实现步骤为:

  1. 创建Web项目web-demo,导入Servlet依赖坐标
1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<!--
此处为什么需要添加该标签?
provided指的是在编译和测试过程中有效,最后生成的war包时不会加入
因为Tomcat的lib目录中已经有servlet-api这个jar包,如果在生成war包的时候生效就会和Tomcat中的jar包冲突,导致报错
-->
<scope>provided</scope>
</dependency>
  1. 创建:定义一个类,实现Servlet接口,并重写接口中所有方法,并在service方法中输入一句话
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.itheima.web;

import javax.servlet.*;
import java.io.IOException;

public class ServletDemo1 implements Servlet {

public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("servlet hello world~");
}
public void init(ServletConfig servletConfig) throws ServletException {

}

public ServletConfig getServletConfig() {
return null;
}

public String getServletInfo() {
return null;
}

public void destroy() {

}
}
  1. 配置:在类上使用@WebServlet注解,配置该Servlet的访问路径
1
@WebServlet("/demo1")
  1. 访问:启动Tomcat,浏览器中输入URL地址访问该Servlet
1
http://localhost:8080/web-demo/demo1
  1. 器访问后,在控制台会打印servlet hello world~ 说明servlet程序已经成功运行。

至此,Servlet的入门案例就已经完成,大家可以按照上面的步骤进行练习了。

4.3 执行流程

Servlet程序已经能正常运行,但是我们需要思考个问题: 我们并没有创建ServletDemo1类的对象,也没有调用对象中的service方法,为什么在控制台就打印了servlet hello world~这句话呢?

要想回答上述问题,我们就需要对Servlet的执行流程进行一个学习。

1627236923139

  • 浏览器发出http://localhost:8080/web-demo/demo1请求,从请求中可以解析出三部分内容,分别是localhost:8080web-demodemo1
    • 根据localhost:8080可以找到要访问的Tomcat Web服务器
    • 根据web-demo可以找到部署在Tomcat服务器上的web-demo项目
    • 根据demo1可以找到要访问的是项目中的哪个Servlet类,根据@WebServlet后面的值进行匹配
  • 找到ServletDemo1这个类后,Tomcat Web服务器就会为ServletDemo1这个类创建一个对象,然后调用对象中的service方法
    • ServletDemo1实现了Servlet接口,所以类中必然会重写service方法供Tomcat Web服务器进行调用
    • service方法中有ServletRequest和ServletResponse两个参数,ServletRequest封装的是请求数据,ServletResponse封装的是响应数据,后期我们可以通过这两个参数实现前后端的数据交互

小结

介绍完Servlet的执行流程,需要大家掌握两个问题:

  1. Servlet由谁创建?Servlet方法由谁调用?

Servlet由web服务器创建,Servlet方法由web服务器调用

  1. 服务器怎么知道Servlet中一定有service方法?

因为我们自定义的Servlet,必须实现Servlet接口并复写其方法,而Servlet接口中有service方法

4.4 生命周期

介绍完Servlet的执行流程后,我们知道Servlet是由Tomcat Web服务器帮我们创建的。

接下来咱们再来思考一个问题:==Tomcat什么时候创建的Servlet对象?==

要想回答上述问题,我们就需要对Servlet的生命周期进行一个学习。

  • 生命周期: 对象的生命周期指一个对象从被创建到被销毁的整个过程。

  • Servlet运行在Servlet容器(web服务器)中,其生命周期由容器来管理,分为4个阶段:

    1. ==加载和实例化==:默认情况下,当Servlet第一次被访问时,由容器创建Servlet对象
    1
    2
    3
    4
    5
    6
    默认情况,Servlet会在第一次访问被容器创建,但是如果创建Servlet比较耗时的话,那么第一个访问的人等待的时间就比较长,用户的体验就比较差,那么我们能不能把Servlet的创建放到服务器启动的时候来创建,具体如何来配置?

    @WebServlet(urlPatterns = "/demo1",loadOnStartup = 1)
    loadOnstartup的取值有两类情况
    (1)负整数:第一次访问时创建Servlet对象
    (2)0或正整数:服务器启动时创建Servlet对象,数字越小优先级越高
    1. ==初始化==:在Servlet实例化之后,容器将调用Servlet的==init()==方法初始化这个对象,完成一些如加载配置文件、创建连接等初始化的工作。该方法只==调用一次==
    2. ==请求处理==:==每次==请求Servlet时,Servlet容器都会调用Servlet的==service()==方法对请求进行处理
    3. ==服务终止==:当需要释放内存或者容器关闭时,容器就会调用Servlet实例的==destroy()==方法完成资源的释放。在destroy()方法调用之后,容器会释放这个Servlet实例,该实例随后会被Java的垃圾收集器所回收
  • 通过案例演示下上述的生命周期

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

    import javax.servlet.*;
    import javax.servlet.annotation.WebServlet;
    import java.io.IOException;
    /**
    * Servlet生命周期方法
    */
    @WebServlet(urlPatterns = "/demo2",loadOnStartup = 1)
    public class ServletDemo2 implements Servlet {

    /**
    * 初始化方法
    * 1.调用时机:默认情况下,Servlet被第一次访问时,调用
    * * loadOnStartup: 默认为-1,修改为0或者正整数,则会在服务器启动的时候,调用
    * 2.调用次数: 1次
    * @param config
    * @throws ServletException
    */
    public void init(ServletConfig config) throws ServletException {
    System.out.println("init...");
    }

    /**
    * 提供服务
    * 1.调用时机:每一次Servlet被访问时,调用
    * 2.调用次数: 多次
    * @param req
    * @param res
    * @throws ServletException
    * @throws IOException
    */
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
    System.out.println("servlet hello world~");
    }

    /**
    * 销毁方法
    * 1.调用时机:内存释放或者服务器关闭的时候,Servlet对象会被销毁,调用
    * 2.调用次数: 1次
    */
    public void destroy() {
    System.out.println("destroy...");
    }
    public ServletConfig getServletConfig() {
    return null;
    }

    public String getServletInfo() {
    return null;
    }


    }

    ==注意:如何才能让Servlet中的destroy方法被执行?==

    1627239292226

在Terminal命令行中,先使用mvn tomcat7:run启动,然后再使用ctrl+c关闭tomcat

小结

这节中需要掌握的内容是:

  1. Servlet对象在什么时候被创建的?

默认是第一次访问的时候被创建,可以使用@WebServlet(urlPatterns = “/demo2”,loadOnStartup = 1)的loadOnStartup 修改成在服务器启动的时候创建。

  1. Servlet生命周期中涉及到的三个方法,这三个方法是什么?什么时候被调用?调用几次?

涉及到三个方法,分别是 init()、service()、destroy()

init方法在Servlet对象被创建的时候执行,只执行1次

service方法在Servlet被访问的时候调用,每访问1次就调用1次

destroy方法在Servlet对象被销毁的时候调用,只执行1次

4.5 方法介绍

Servlet中总共有5个方法,我们已经介绍过其中的三个,剩下的两个方法作用分别是什么?

我们先来回顾下前面讲的三个方法,分别是:

  • 初始化方法,在Servlet被创建时执行,只执行一次
1
void init(ServletConfig config) 
  • 提供服务方法, 每次Servlet被访问,都会调用该方法
1
void service(ServletRequest req, ServletResponse res)
  • 销毁方法,当Servlet被销毁时,调用该方法。在内存释放或服务器关闭时销毁Servlet
1
void destroy() 

剩下的两个方法是:

  • 获取Servlet信息
1
2
3
4
5
String getServletInfo() 
//该方法用来返回Servlet的相关信息,没有什么太大的用处,一般我们返回一个空字符串即可
public String getServletInfo() {
return "";
}
  • 获取ServletConfig对象
1
ServletConfig getServletConfig()

ServletConfig对象,在init方法的参数中有,而Tomcat Web服务器在创建Servlet对象的时候会调用init方法,必定会传入一个ServletConfig对象,我们只需要将服务器传过来的ServletConfig进行返回即可。具体如何操作?

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

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;

/**
* Servlet方法介绍
*/
@WebServlet(urlPatterns = "/demo3",loadOnStartup = 1)
public class ServletDemo3 implements Servlet {

private ServletConfig servletConfig;
/**
* 初始化方法
* 1.调用时机:默认情况下,Servlet被第一次访问时,调用
* * loadOnStartup: 默认为-1,修改为0或者正整数,则会在服务器启动的时候,调用
* 2.调用次数: 1次
* @param config
* @throws ServletException
*/
public void init(ServletConfig config) throws ServletException {
this.servletConfig = config;
System.out.println("init...");
}
public ServletConfig getServletConfig() {
return servletConfig;
}

/**
* 提供服务
* 1.调用时机:每一次Servlet被访问时,调用
* 2.调用次数: 多次
* @param req
* @param res
* @throws ServletException
* @throws IOException
*/
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
System.out.println("servlet hello world~");
}

/**
* 销毁方法
* 1.调用时机:内存释放或者服务器关闭的时候,Servlet对象会被销毁,调用
* 2.调用次数: 1次
*/
public void destroy() {
System.out.println("destroy...");
}

public String getServletInfo() {
return "";
}
}

getServletInfo()和getServletConfig()这两个方法使用的不是很多,大家了解下。

4.6 体系结构

通过上面的学习,我们知道要想编写一个Servlet就必须要实现Servlet接口,重写接口中的5个方法,虽然已经能完成要求,但是编写起来还是比较麻烦的,因为我们更关注的其实只有service方法,那有没有更简单方式来创建Servlet呢?

要想解决上面的问题,我们需要先对Servlet的体系结构进行下了解:

1627240593506

因为我们将来开发B/S架构的web项目,都是针对HTTP协议,所以我们自定义Servlet,会通过继承==HttpServlet==

具体的编写格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@WebServlet("/demo4")
public class ServletDemo4 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//TODO GET 请求方式处理逻辑
System.out.println("get...");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//TODO Post 请求方式处理逻辑
System.out.println("post...");
}
}
  • 要想发送一个GET请求,请求该Servlet,只需要通过浏览器发送http://localhost:8080/web-demo/demo4,就能看到doGet方法被执行了
  • 要想发送一个POST请求,请求该Servlet,单单通过浏览器是无法实现的,这个时候就需要编写一个form表单来发送请求,在webapp下创建一个a.html页面,内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/web-demo/demo4" method="post">
<input name="username"/><input type="submit"/>
</form>
</body>
</html>

启动测试,即可看到doPost方法被执行了。

Servlet的简化编写就介绍完了,接着需要思考两个问题:

  1. HttpServlet中为什么要根据请求方式的不同,调用不同的方法?
  2. 如何调用?

针对问题一,我们需要回顾之前的知识点==前端发送GET和POST请求的时候,参数的位置不一致,GET请求参数在请求行中,POST请求参数在请求体中==,为了能处理不同的请求方式,我们得在service方法中进行判断,然后写不同的业务处理,这样能实现,但是每个Servlet类中都将有相似的代码,针对这个问题,有什么可以优化的策略么?

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

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@WebServlet("/demo5")
public class ServletDemo5 implements Servlet {

public void init(ServletConfig config) throws ServletException {

}

public ServletConfig getServletConfig() {
return null;
}

public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
//如何调用?
//获取请求方式,根据不同的请求方式进行不同的业务处理
HttpServletRequest request = (HttpServletRequest)req;
//1. 获取请求方式
String method = request.getMethod();
//2. 判断
if("GET".equals(method)){
// get方式的处理逻辑
}else if("POST".equals(method)){
// post方式的处理逻辑
}
}

public String getServletInfo() {
return null;
}

public void destroy() {

}
}

要解决上述问题,我们可以对Servlet接口进行继承封装,来简化代码开发。

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

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class MyHttpServlet implements Servlet {
public void init(ServletConfig config) throws ServletException {

}

public ServletConfig getServletConfig() {
return null;
}

public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest)req;
//1. 获取请求方式
String method = request.getMethod();
//2. 判断
if("GET".equals(method)){
// get方式的处理逻辑
doGet(req,res);
}else if("POST".equals(method)){
// post方式的处理逻辑
doPost(req,res);
}
}

protected void doPost(ServletRequest req, ServletResponse res) {
}

protected void doGet(ServletRequest req, ServletResponse res) {
}

public String getServletInfo() {
return null;
}

public void destroy() {

}
}

有了MyHttpServlet这个类,以后我们再编写Servlet类的时候,只需要继承MyHttpServlet,重写父类中的doGet和doPost方法,就可以用来处理GET和POST请求的业务逻辑。接下来,可以把ServletDemo5代码进行改造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@WebServlet("/demo5")
public class ServletDemo5 extends MyHttpServlet {

@Override
protected void doGet(ServletRequest req, ServletResponse res) {
System.out.println("get...");
}

@Override
protected void doPost(ServletRequest req, ServletResponse res) {
System.out.println("post...");
}
}

将来页面发送的是GET请求,则会进入到doGet方法中进行执行,如果是POST请求,则进入到doPost方法。这样代码在编写的时候就相对来说更加简单快捷。

类似MyHttpServlet这样的类Servlet中已经为我们提供好了,就是HttpServlet,翻开源码,大家可以搜索service()方法,你会发现HttpServlet做的事更多,不仅可以处理GET和POST还可以处理其他五种请求方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String method = req.getMethod();

if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < lastModified) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}

} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);

} else if (method.equals(METHOD_POST)) {
doPost(req, resp);

} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);

} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);

} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);

} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);

} else {
//
// Note that this means NO servlet supports whatever
// method was requested, anywhere on this server.
//

String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);

resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}

小结

通过这一节的学习,要掌握:

  1. HttpServlet的使用步骤

继承HttpServlet

重写doGet和doPost方法

  1. HttpServlet原理

获取请求方式,并根据不同的请求方式,调用不同的doXxx方法

4.7 urlPattern配置

Servlet类编写好后,要想被访问到,就需要配置其访问路径(==urlPattern==)

  • 一个Servlet,可以配置多个urlPattern

    1627272805178

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package com.itheima.web;

    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.annotation.WebServlet;

    /**
    * urlPattern: 一个Servlet可以配置多个访问路径
    */
    @WebServlet(urlPatterns = {"/demo7","/demo8"})
    public class ServletDemo7 extends MyHttpServlet {

    @Override
    protected void doGet(ServletRequest req, ServletResponse res) {

    System.out.println("demo7 get...");
    }
    @Override
    protected void doPost(ServletRequest req, ServletResponse res) {
    }
    }

    在浏览器上输入http://localhost:8080/web-demo/demo7,http://localhost:8080/web-demo/demo8这两个地址都能访问到ServletDemo7的doGet方法。

  • ==urlPattern配置规则==

    • 精确匹配

      1627273174144

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      /**
      * UrlPattern:
      * * 精确匹配
      */
      @WebServlet(urlPatterns = "/user/select")
      public class ServletDemo8 extends MyHttpServlet {

      @Override
      protected void doGet(ServletRequest req, ServletResponse res) {

      System.out.println("demo8 get...");
      }
      @Override
      protected void doPost(ServletRequest req, ServletResponse res) {
      }
      }

      访问路径http://localhost:8080/web-demo/user/select

    • 目录匹配

      1627273184095

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      package com.itheima.web;

      import javax.servlet.ServletRequest;
      import javax.servlet.ServletResponse;
      import javax.servlet.annotation.WebServlet;

      /**
      * UrlPattern:
      * * 目录匹配: /user/*
      */
      @WebServlet(urlPatterns = "/user/*")
      public class ServletDemo9 extends MyHttpServlet {

      @Override
      protected void doGet(ServletRequest req, ServletResponse res) {

      System.out.println("demo9 get...");
      }
      @Override
      protected void doPost(ServletRequest req, ServletResponse res) {
      }
      }

      访问路径http://localhost:8080/web-demo/user/任意

      ==思考:==

      1. 访问路径http://localhost:8080/web-demo/user是否能访问到demo9的doGet方法?
      2. 访问路径http://localhost:8080/web-demo/user/a/b是否能访问到demo9的doGet方法?
      3. 访问路径http://localhost:8080/web-demo/user/select是否能访问到demo9还是demo8的doGet方法?

      答案是: 能、能、demo8,进而我们可以得到的结论是/user/*中的/*代表的是零或多个层级访问目录同时精确匹配优先级要高于目录匹配。

    • 扩展名匹配

      1627273194118

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      package com.itheima.web;

      import javax.servlet.ServletRequest;
      import javax.servlet.ServletResponse;
      import javax.servlet.annotation.WebServlet;

      /**
      * UrlPattern:
      * * 扩展名匹配: *.do
      */
      @WebServlet(urlPatterns = "*.do")
      public class ServletDemo10 extends MyHttpServlet {

      @Override
      protected void doGet(ServletRequest req, ServletResponse res) {

      System.out.println("demo10 get...");
      }
      @Override
      protected void doPost(ServletRequest req, ServletResponse res) {
      }
      }

      访问路径http://localhost:8080/web-demo/任意.do

      ==注意==:

      1. 如果路径配置的不是扩展名,那么在路径的前面就必须要加/否则会报错

      1627274483755

      1. 如果路径配置的是*.do,那么在*.do的前面不能加/,否则会报错

      1627274368245

    • 任意匹配

      1627273201370

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      package com.itheima.web;

      import javax.servlet.ServletRequest;
      import javax.servlet.ServletResponse;
      import javax.servlet.annotation.WebServlet;

      /**
      * UrlPattern:
      * * 任意匹配: /
      */
      @WebServlet(urlPatterns = "/")
      public class ServletDemo11 extends MyHttpServlet {

      @Override
      protected void doGet(ServletRequest req, ServletResponse res) {

      System.out.println("demo11 get...");
      }
      @Override
      protected void doPost(ServletRequest req, ServletResponse res) {
      }
      }

      访问路径http://localhost:8080/demo-web/任意

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      package com.itheima.web;

      import javax.servlet.ServletRequest;
      import javax.servlet.ServletResponse;
      import javax.servlet.annotation.WebServlet;

      /**
      * UrlPattern:
      * * 任意匹配: /*
      */
      @WebServlet(urlPatterns = "/*")
      public class ServletDemo12 extends MyHttpServlet {

      @Override
      protected void doGet(ServletRequest req, ServletResponse res) {

      System.out.println("demo12 get...");
      }
      @Override
      protected void doPost(ServletRequest req, ServletResponse res) {
      }
      }

      访问路径`http://localhost:8080/demo-web/任意

      ==注意:==//*的区别?

      1. 当我们的项目中的Servlet配置了 “/“,会覆盖掉tomcat中的DefaultServlet,当其他的url-pattern都匹配不上时都会走这个Servlet

      2. 当我们的项目中配置了”/*”,意味着匹配任意访问路径

      3. DefaultServlet是用来处理静态资源,如果配置了”/“会把默认的覆盖掉,就会引发请求静态资源的时候没有走默认的而是走了自定义的Servlet类,最终导致静态资源不能被访问

小结

  1. urlPattern总共有四种配置方式,分别是精确匹配、目录匹配、扩展名匹配、任意匹配

  2. 五种配置的优先级为 精确匹配 > 目录匹配> 扩展名匹配 > /* > / ,无需记,以最终运行结果为准。

4.8 XML配置

前面对应Servlet的配置,我们都使用的是@WebServlet,这个是Servlet从3.0版本后开始支持注解配置,3.0版本前只支持XML配置文件的配置方法。

对于XML的配置步骤有两步:

  • 编写Servlet类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.itheima.web;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;

public class ServletDemo13 extends MyHttpServlet {

@Override
protected void doGet(ServletRequest req, ServletResponse res) {

System.out.println("demo13 get...");
}
@Override
protected void doPost(ServletRequest req, ServletResponse res) {
}
}
  • 在web.xml中配置该Servlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?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">



<!--
Servlet 全类名
-->
<servlet>
<!-- servlet的名称,名字任意-->
<servlet-name>demo13</servlet-name>
<!--servlet的类全名-->
<servlet-class>com.itheima.web.ServletDemo13</servlet-class>
</servlet>

<!--
Servlet 访问路径
-->
<servlet-mapping>
<!-- servlet的名称,要和上面的名称一致-->
<servlet-name>demo13</servlet-name>
<!-- servlet的访问路径-->
<url-pattern>/demo13</url-pattern>
</servlet-mapping>
</web-app>

这种配置方式和注解比起来,确认麻烦很多,所以建议大家使用注解来开发。但是大家要认识上面这种配置方式,因为并不是所有的项目都是基于注解开发的。

##Request&Response

今日目标

  • 掌握Request对象的概念与使用
  • 掌握Response对象的概念与使用
  • 能够完成用户登录注册案例的实现
  • 能够完成SqlSessionFactory工具类的抽取

1,Request和Response的概述

==Request是请求对象,Response是响应对象。==这两个对象在我们使用Servlet的时候有看到:1628735216156

此时,我们就需要思考一个问题request和response这两个参数的作用是什么?

1628735746602

  • request:==获取==请求数据
    • 浏览器会发送HTTP请求到后台服务器[Tomcat]
    • HTTP的请求中会包含很多请求数据[请求行+请求头+请求体]
    • 后台服务器[Tomcat]会对HTTP请求中的数据进行解析并把解析结果存入到一个对象中
    • 所存入的对象即为request对象,所以我们可以从request对象中获取请求的相关参数
    • 获取到数据后就可以继续后续的业务,比如获取用户名和密码就可以实现登录操作的相关业务
  • response:==设置==响应数据
    • 业务处理完后,后台就需要给前端返回业务处理的结果即响应数据
    • 把响应数据封装到response对象中
    • 后台服务器[Tomcat]会解析response对象,按照[响应行+响应头+响应体]格式拼接结果
    • 浏览器最终解析结果,把内容展示在浏览器给用户浏览

对于上述所讲的内容,我们通过一个案例来初步体验下request和response对象的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/demo3")
public class ServletDemo3 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//使用request对象 获取请求数据
String name = request.getParameter("name");//url?name=zhangsan

//使用response对象 设置响应数据
response.setHeader("content-type","text/html;charset=utf-8");
response.getWriter().write("<h1>"+name+",欢迎您!</h1>");
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("Post...");
}
}

启动成功后就可以通过浏览器来访问,并且根据传入参数的不同就可以在页面上展示不同的内容:

1628738273049

小结

在这节中,我们主要认识了下request对象和reponse对象:

  • request对象是用来封装请求数据的对象
  • response对象是用来封装响应数据的对象

目前我们只知道这两个对象是用来干什么的,那么它们具体是如何实现的,就需要我们继续深入的学习。接下来,就先从Request对象来学习,主要学习下面这些内容:

  • request继承体系

  • request获取请求参数

  • request请求转发

2,Request对象

2.1 Request继承体系

在学习这节内容之前,我们先思考一个问题,前面在介绍Request和Reponse对象的时候,比较细心的同学可能已经发现:

  • 当我们的Servlet类实现的是Servlet接口的时候,service方法中的参数是ServletRequest和ServletResponse
  • 当我们的Servlet类继承的是HttpServlet类的时候,doGet和doPost方法中的参数就变成HttpServletRequest和HttpServletReponse

那么,

  • ServletRequest和HttpServletRequest的关系是什么?
  • request对象是有谁来创建的?
  • request提供了哪些API,这些API从哪里查?

首先,我们先来看下Request的继承体系:

1628740441008

从上图中可以看出,ServletRequest和HttpServletRequest都是Java提供的,所以我们可以打开JavaEE提供的API文档[参考: 资料/JavaEE7-api.chm],打开后可以看到:

1628741839475

所以ServletRequest和HttpServletRequest是继承关系,并且两个都是接口,接口是无法创建对象,这个时候就引发了下面这个问题:

1628742224589

这个时候,我们就需要用到Request继承体系中的RequestFacade:

  • 该类实现了HttpServletRequest接口,也间接实现了ServletRequest接口。
  • Servlet类中的service方法、doGet方法或者是doPost方法最终都是由Web服务器[Tomcat]来调用的,所以Tomcat提供了方法参数接口的具体实现类,并完成了对象的创建
  • 要想了解RequestFacade中都提供了哪些方法,我们可以直接查看JavaEE的API文档中关于ServletRequest和HttpServletRequest的接口文档,因为RequestFacade实现了其接口就需要重写接口中的方法

对于上述结论,要想验证,可以编写一个Servlet,在方法中把request对象打印下,就能看到最终的对象是不是RequestFacade,代码如下:

1
2
3
4
5
6
7
8
9
10
11
@WebServlet("/demo2")
public class ServletDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println(request);
}

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

启动服务器,运行访问http://localhost:8080/request-demo/demo2,得到运行结果:

1628743040046

小结

  • Request的继承体系为ServletRequest–>HttpServletRequest–>RequestFacade
  • Tomcat需要解析请求数据,封装为request对象,并且创建request对象传递到service方法
  • 使用request对象,可以查阅JavaEE API文档的HttpServletRequest接口中方法说明

2.2 Request获取请求数据

HTTP请求数据总共分为三部分内容,分别是==请求行、请求头、请求体==,对于这三部分内容的数据,分别该如何获取,首先我们先来学习请求行数据如何获取?

2.2.1 获取请求行数据

请求行包含三块内容,分别是请求方式请求资源路径HTTP协议及版本

1628748240075

对于这三部分内容,request对象都提供了对应的API方法来获取,具体如下:

  • 获取请求方式: GET
1
String getMethod()
  • 获取虚拟目录(项目访问路径): /request-demo
1
String getContextPath()
  • 获取URL(统一资源定位符): http://localhost:8080/request-demo/req1
1
StringBuffer getRequestURL()
  • 获取URI(统一资源标识符): /request-demo/req1
1
String getRequestURI()
  • 获取请求参数(GET方式): username=zhangsan&password=123
1
String getQueryString()

介绍完上述方法后,咱们通过代码把上述方法都使用下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* request 获取请求数据
*/
@WebServlet("/req1")
public class RequestDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// String getMethod():获取请求方式: GET
String method = req.getMethod();
System.out.println(method);//GET
// String getContextPath():获取虚拟目录(项目访问路径):/request-demo
String contextPath = req.getContextPath();
System.out.println(contextPath);
// StringBuffer getRequestURL(): 获取URL(统一资源定位符):http://localhost:8080/request-demo/req1
StringBuffer url = req.getRequestURL();
System.out.println(url.toString());
// String getRequestURI():获取URI(统一资源标识符): /request-demo/req1
String uri = req.getRequestURI();
System.out.println(uri);
// String getQueryString():获取请求参数(GET方式): username=zhangsan
String queryString = req.getQueryString();
System.out.println(queryString);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}

启动服务器,访问http://localhost:8080/request-demo/req1?username=zhangsan&passwrod=123,获取的结果如下:

1628762794935

2.2.2 获取请求头数据

对于请求头的数据,格式为key: value如下:

1628768652535

所以根据请求头名称获取对应值的方法为:

1
String getHeader(String name)

接下来,在代码中如果想要获取客户端浏览器的版本信息,则可以使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* request 获取请求数据
*/
@WebServlet("/req1")
public class RequestDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取请求头: user-agent: 浏览器的版本信息
String agent = req.getHeader("user-agent");
System.out.println(agent);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}

重新启动服务器后,http://localhost:8080/request-demo/req1?username=zhangsan&passwrod=123,获取的结果如下:

1628769145524

2.2.3 获取请求体数据

浏览器在发送GET请求的时候是没有请求体的,所以需要把请求方式变更为POST,请求体中的数据格式如下:

1628768665185

对于请求体中的数据,Request对象提供了如下两种方式来获取其中的数据,分别是:

  • 获取字节输入流,如果前端发送的是字节数据,比如传递的是文件数据,则使用该方法
1
2
ServletInputStream getInputStream()
该方法可以获取字节
  • 获取字符输入流,如果前端发送的是纯文本数据,则使用该方法
1
BufferedReader getReader()

接下来,大家需要思考,要想获取到请求体的内容该如何实现?

具体实现的步骤如下:

1.准备一个页面,在页面中添加form表单,用来发送post请求

2.在Servlet的doPost方法中获取请求体数据

3.在doPost方法中使用request的getReader()或者getInputStream()来获取

4.访问测试

  1. 在项目的webapp目录下添加一个html页面,名称为:req.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--
action:form表单提交的请求地址
method:请求方式,指定为post
-->
<form action="/request-demo/req1" method="post">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit">
</form>
</body>
</html>
  1. 在Servlet的doPost方法中获取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* request 获取请求数据
*/
@WebServlet("/req1")
public class RequestDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//在此处获取请求体中的数据
}
}
  1. 调用getReader()或者getInputStream()方法,因为目前前端传递的是纯文本数据,所以我们采用getReader()方法来获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* request 获取请求数据
*/
@WebServlet("/req1")
public class RequestDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取post 请求体:请求参数
//1. 获取字符输入流
BufferedReader br = req.getReader();
//2. 读取数据
String line = br.readLine();
System.out.println(line);
}
}

==注意==

BufferedReader流是通过request对象来获取的,当请求完成后request对象就会被销毁,request对象被销毁后,BufferedReader流就会自动关闭,所以此处就不需要手动关闭流了。

  1. 启动服务器,通过浏览器访问http://localhost:8080/request-demo/req.html

1628770516387

点击提交按钮后,就可以在控制台看到前端所发送的请求数据

1628770585480

小结

HTTP请求数据中包含了请求行请求头请求体,针对这三部分内容,Request对象都提供了对应的API方法来获取对应的值:

  • 请求行
    • getMethod()获取请求方式
    • getContextPath()获取项目访问路径
    • getRequestURL()获取请求URL
    • getRequestURI()获取请求URI
    • getQueryString()获取GET请求方式的请求参数
  • 请求头
    • getHeader(String name)根据请求头名称获取其对应的值
  • 请求体
    • 注意: ==浏览器发送的POST请求才有请求体==
    • 如果是纯文本数据:getReader()
    • 如果是字节数据如文件数据:getInputStream()

2.2.4 获取请求参数的通用方式

在学习下面内容之前,我们先提出两个问题:

  • 什么是请求参数?
  • 请求参数和请求数据的关系是什么?

1.什么是请求参数?

为了能更好的回答上述两个问题,我们拿用户登录的例子来说明

1.1 想要登录网址,需要进入登录页面

1.2 在登录页面输入用户名和密码

1.3 将用户名和密码提交到后台

1.4 后台校验用户名和密码是否正确

1.5 如果正确,则正常登录,如果不正确,则提示用户名或密码错误

上述例子中,用户名和密码其实就是我们所说的请求参数。

2.什么是请求数据?

请求数据则是包含请求行、请求头和请求体的所有数据

3.请求参数和请求数据的关系是什么?

3.1 请求参数是请求数据中的部分内容

3.2 如果是GET请求,请求参数在请求行中

3.3 如果是POST请求,请求参数一般在请求体中

对于请求参数的获取,常用的有以下两种:

  • GET方式:
1
String getQueryString()
  • POST方式:
1
BufferedReader getReader();

有了上述的知识储备,我们来实现一个案例需求:

(1)发送一个GET请求并携带用户名,后台接收后打印到控制台

(2)发送一个POST请求并携带用户名,后台接收后打印到控制台

此处大家需要注意的是GET请求和POST请求接收参数的方式不一样,具体实现的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@WebServlet("/req1")
public class RequestDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

String result = req.getQueryString();
System.out.println(result);

}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
BufferedReader br = req.getReader();
String result = br.readLine();
System.out.println(result);
}
}
  • 对于上述的代码,会存在什么问题呢?

1628776252445

  • 如何解决上述重复代码的问题呢?

1628776433318

当然,也可以在doGet中调用doPost,在doPost中完成参数的获取和打印,另外需要注意的是,doGet和doPost方法都必须存在,不能删除任意一个。

==GET请求和POST请求获取请求参数的方式不一样,在获取请求参数这块该如何实现呢?==

要想实现,我们就需要==思考==:

GET请求方式和POST请求方式区别主要在于获取请求参数的方式不一样,是否可以提供一种==统一==获取请求参数的方式,从而==统一==doGet和doPost方法内的代码?

解决方案一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@WebServlet("/req1")
public class RequestDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取请求方式
String method = req.getMethod();
//获取请求参数
String params = "";
if("GET".equals(method)){
params = req.getQueryString();
}else if("POST".equals(method)){
BufferedReader reader = req.getReader();
params = reader.readLine();
}
//将请求参数进行打印控制台
System.out.println(params);

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

使用request的getMethod()来获取请求方式,根据请求方式的不同分别获取请求参数值,这样就可以解决上述问题,但是以后每个Servlet都需要这样写代码,实现起来比较麻烦,这种方案我们不采用

解决方案二:

request对象已经将上述获取请求参数的方法进行了封装,并且request提供的方法实现的功能更强大,以后只需要调用request提供的方法即可,在request的方法中都实现了哪些操作?

(1)根据不同的请求方式获取请求参数,获取的内容如下:

1628778931277

(2)把获取到的内容进行分割,内容如下:

1628779067793

(3)把分割后端数据,存入到一个Map集合中:

1628779368501

注意:因为参数的值可能是一个,也可能有多个,所以Map的值的类型为String数组。

基于上述理论,request对象为我们提供了如下方法:

  • 获取所有参数Map集合
1
Map<String,String[]> getParameterMap()
  • 根据名称获取参数值(数组)
1
String[] getParameterValues(String name)
  • 根据名称获取参数值(单个值)
1
String getParameter(String name)

接下来,我们通过案例来把上述的三个方法进行实例演示:

1.修改req.html页面,添加爱好选项,爱好可以同时选多个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/request-demo/req2" method="get">
<input type="text" name="username"><br>
<input type="password" name="password"><br>
<input type="checkbox" name="hobby" value="1"> 游泳
<input type="checkbox" name="hobby" value="2"> 爬山 <br>
<input type="submit">

</form>
</body>
</html>

1628780937599

2.在Servlet代码中获取页面传递GET请求的参数值

2.1获取GET方式的所有请求参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* request 通用方式获取请求参数
*/
@WebServlet("/req2")
public class RequestDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//GET请求逻辑
System.out.println("get....");
//1. 获取所有参数的Map集合
Map<String, String[]> map = req.getParameterMap();
for (String key : map.keySet()) {
// username:zhangsan lisi
System.out.print(key+":");

//获取值
String[] values = map.get(key);
for (String value : values) {
System.out.print(value + " ");
}

System.out.println();
}
}

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

获取的结果为:

1628780965283

2.2获取GET请求参数中的爱好,结果是数组值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* request 通用方式获取请求参数
*/
@WebServlet("/req2")
public class RequestDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//GET请求逻辑
//...
System.out.println("------------");
String[] hobbies = req.getParameterValues("hobby");
for (String hobby : hobbies) {
System.out.println(hobby);
}
}

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

获取的结果为:

1628781031437

2.3获取GET请求参数中的用户名和密码,结果是单个值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* request 通用方式获取请求参数
*/
@WebServlet("/req2")
public class RequestDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//GET请求逻辑
//...
String username = req.getParameter("username");
String password = req.getParameter("password");
System.out.println(username);
System.out.println(password);
}

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

获取的结果为:

1628781176434

3.在Servlet代码中获取页面传递POST请求的参数值

3.1将req.html页面form表单的提交方式改成post

3.2将doGet方法中的内容复制到doPost方法中即可

小结

  • req.getParameter()方法使用的频率会比较高

  • 以后我们再写代码的时候,就只需要按照如下格式来编写:

1
2
3
4
5
6
7
8
9
10
11
public class RequestDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//采用request提供的获取请求参数的通用方式来获取请求参数
//编写其他的业务代码...
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req,resp);
}
}

2.3 IDEA快速创建Servlet

使用通用方式获取请求参数后,屏蔽了GET和POST的请求方式代码的不同,则代码可以定义如下格式:

1628781419752

由于格式固定,所以我们可以使用IDEA提供的模板来制作一个Servlet的模板,这样我们后期在创建Servlet的时候就会更高效,具体如何实现:

(1)按照自己的需求,修改Servlet创建的模板内容

1628781545912

(2)使用servlet模板创建Servlet类

1628782117420

2.4 请求参数中文乱码问题

问题展示:

(1)将req.html页面的请求方式修改为get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/request-demo/req2" method="get">
<input type="text" name="username"><br>
<input type="password" name="password"><br>
<input type="checkbox" name="hobby" value="1"> 游泳
<input type="checkbox" name="hobby" value="2"> 爬山 <br>
<input type="submit">

</form>
</body>
</html>

(2)在Servlet方法中获取参数,并打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 中文乱码问题解决方案
*/
@WebServlet("/req4")
public class RequestDemo4 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 获取username
String username = request.getParameter("username");
System.out.println(username);
}

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

(3)启动服务器,页面上输入中文参数

1628784323297

(4)查看控制台打印内容

1628784356157

(5)把req.html页面的请求方式改成post,再次发送请求和中文参数

1628784425182

(6)查看控制台打印内容,依然为乱码

1628784356157

通过上面的案例,会发现,不管是GET还是POST请求,在发送的请求参数中如果有中文,在后台接收的时候,都会出现中文乱码的问题。具体该如何解决呢?

2.4.1 POST请求解决方案

  • 分析出现中文乱码的原因:
    • POST的请求参数是通过request的getReader()来获取流中的数据
    • TOMCAT在获取流的时候采用的编码是ISO-8859-1
    • ISO-8859-1编码是不支持中文的,所以会出现乱码
  • 解决方案:
    • 页面设置的编码格式为UTF-8
    • 把TOMCAT在获取流数据之前的编码设置为UTF-8
    • 通过request.setCharacterEncoding(“UTF-8”)设置编码,UTF-8也可以写成小写

修改后的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 中文乱码问题解决方案
*/
@WebServlet("/req4")
public class RequestDemo4 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 解决乱码: POST getReader()
//设置字符输入流的编码,设置的字符集要和页面保持一致
request.setCharacterEncoding("UTF-8");
//2. 获取username
String username = request.getParameter("username");
System.out.println(username);
}

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

重新发送POST请求,就会在控制台看到正常展示的中文结果。

至此POST请求中文乱码的问题就已经解决,但是这种方案不适用于GET请求,这个原因是什么呢,咱们下面再分析。

2.4.2 GET请求解决方案

刚才提到一个问题是POST请求的中文乱码解决方案为什么不适用GET请求?

  • GET请求获取请求参数的方式是request.getQueryString()
  • POST请求获取请求参数的方式是request.getReader()
  • request.setCharacterEncoding(“utf-8”)是设置request处理流的编码
  • getQueryString方法并没有通过流的方式获取数据

所以GET请求不能用设置编码的方式来解决中文乱码问题,那问题又来了,如何解决GET请求的中文乱码呢?

  1. 首先我们需要先分析下GET请求出现乱码的原因:

1628829610823

(1)浏览器通过HTTP协议发送请求和数据给后台服务器(Tomcat)

(2)浏览器在发送HTTP的过程中会对中文数据进行URL==编码==

(3)在进行URL编码的时候会采用页面<meta>标签指定的UTF-8的方式进行编码,张三编码后的结果为%E5%BC%A0%E4%B8%89

(4)后台服务器(Tomcat)接收到%E5%BC%A0%E4%B8%89后会默认按照ISO-8859-1进行URL==解码==

(5)由于前后编码与解码采用的格式不一样,就会导致后台获取到的数据为乱码。

思考: 如果把req.html页面的<meta>标签的charset属性改成ISO-8859-1,后台不做操作,能解决中文乱码问题么?

答案是否定的,因为ISO-8859-1本身是不支持中文展示的,所以改了标签的charset属性后,会导致页面上的中文内容都无法正常展示。

分析完上面的问题后,我们会发现,其中有两个我们不熟悉的内容就是==URL编码==和==URL解码==,什么是URL编码,什么又是URL解码呢?

URL编码

这块知识我们只需要了解下即可,具体编码过程分两步,分别是:

(1)将字符串按照编码方式转为二进制

(2)每个字节转为2个16进制数并在前边加上%

张三按照UTF-8的方式转换成二进制的结果为:

1
1110 0101 1011 1100 1010 0000 1110 0100 1011 1000 1000 1001

这个结果是如何计算的?

使用http://www.mytju.com/classcode/tools/encode_utf8.asp,输入张三

1628833310473

就可以获取张和三分别对应的10进制,然后在使用计算器,选择程序员模式,计算出对应的二进制数据结果:

1628833496171

在计算的十六进制结果中,每两位前面加一个%,就可以获取到%E5%BC%A0%E4%B8%89

当然你从上面所提供的网站中就已经能看到编码16进制的结果了:

1628833310473

但是对于上面的计算过程,如果没有工具,纯手工计算的话,相对来说还是比较复杂的,我们也不需要进行手动计算,在Java中已经为我们提供了编码和解码的API工具类可以让我们更快速的进行编码和解码:

编码:

1
java.net.URLEncoder.encode("需要被编码的内容","字符集(UTF-8)")

解码:

1
java.net.URLDecoder.decode("需要被解码的内容","字符集(UTF-8)")

接下来咱们对张三来进行编码和解码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class URLDemo {

public static void main(String[] args) throws UnsupportedEncodingException {
String username = "张三";
//1. URL编码
String encode = URLEncoder.encode(username, "utf-8");
System.out.println(encode); //打印:%E5%BC%A0%E4%B8%89

//2. URL解码
//String decode = URLDecoder.decode(encode, "utf-8");//打印:张三
String decode = URLDecoder.decode(encode, "ISO-8859-1");//打印:`å¼ ä¸ `
System.out.println(decode);
}
}

到这,我们就可以分析出GET请求中文参数出现乱码的原因了,

  • 浏览器把中文参数按照UTF-8进行URL编码
  • Tomcat对获取到的内容进行了ISO-8859-1的URL解码
  • 在控制台就会出现类上å¼ ä¸‰的乱码,最后一位是个空格
  1. 清楚了出现乱码的原因,接下来我们就需要想办法进行解决

1628846824194

从上图可以看住,

  • 在进行编码和解码的时候,不管使用的是哪个字符集,他们对应的%E5%BC%A0%E4%B8%89是一致的

  • 那他们对应的二进制值也是一样的,为:

    • ```
      1110 0101 1011 1100 1010 0000 1110 0100 1011 1000 1000 1001
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11

      * 为所以我们可以考虑把`å¼ ä¸‰`转换成字节,在把字节转换成`张三`,在转换的过程中是它们的编码一致,就可以解决中文乱码问题。

      具体的实现步骤为:

      >1.按照ISO-8859-1编码获取乱码`å¼ ä¸‰`对应的字节数组
      >
      >2.按照UTF-8编码获取字节数组对应的字符串

      实现代码如下:

      public class URLDemo {

    public static void main(String[] args) throws UnsupportedEncodingException {

      String username = "张三";
      //1. URL编码
      String encode = URLEncoder.encode(username, "utf-8");
      System.out.println(encode);
      //2. URL解码
      String decode = URLDecoder.decode(encode, "ISO-8859-1");
    
      System.out.println(decode); //此处打印的是对应的乱码数据
    
      //3. 转换为字节数据,编码
      byte[] bytes = decode.getBytes("ISO-8859-1");
      for (byte b : bytes) {
          System.out.print(b + " ");
      }
      //此处打印的是:-27 -68 -96 -28 -72 -119
      //4. 将字节数组转为字符串,解码
      String s = new String(bytes, "utf-8");
      System.out.println(s); //此处打印的是张三
    

    }
    }

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

    **说明**:在第18行中打印的数据是`-27 -68 -96 -28 -72 -119`和`张三`转换成的二进制数据`1110 0101 1011 1100 1010 0000 1110 0100 1011 1000 1000 1001`为什么不一样呢?

    其实打印出来的是十进制数据,我们只需要使用计算机换算下就能得到他们的对应关系,如下图:

    ![1628849231208](D:/学习资料汇总(正经/JAVA课程/JavaWeb-资料/day09-Request&Response/ppt/assets/1628849231208.png)

    至此对于GET请求中文乱码的解决方案,我们就已经分析完了,最后在代码中去实现下:

    ```java
    /**
    * 中文乱码问题解决方案
    */
    @WebServlet("/req4")
    public class RequestDemo4 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //1. 解决乱码:POST,getReader()
    //request.setCharacterEncoding("UTF-8");//设置字符输入流的编码

    //2. 获取username
    String username = request.getParameter("username");
    System.out.println("解决乱码前:"+username);

    //3. GET,获取参数的方式:getQueryString
    // 乱码原因:tomcat进行URL解码,默认的字符集ISO-8859-1
    /* //3.1 先对乱码数据进行编码:转为字节数组
    byte[] bytes = username.getBytes(StandardCharsets.ISO_8859_1);
    //3.2 字节数组解码
    username = new String(bytes, StandardCharsets.UTF_8);*/

    username = new String(username.getBytes(StandardCharsets.ISO_8859_1),StandardCharsets.UTF_8);

    System.out.println("解决乱码后:"+username);

    }

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

注意

  • request.setCharacterEncoding("UTF-8")代码注释掉后,会发现GET请求参数乱码解决方案同时也可也把POST请求参数乱码的问题也解决了
  • 只不过对于POST请求参数一般都会比较多,采用这种方式解决乱码起来比较麻烦,所以对于POST请求还是建议使用设置编码的方式进行。

另外需要说明一点的是==Tomcat8.0之后,已将GET请求乱码问题解决,设置默认的解码方式为UTF-8==

小结

  1. 中文乱码解决方案
  • POST请求和GET请求的参数中如果有中文,后台接收数据就会出现中文乱码问题

    GET请求在Tomcat8.0以后的版本就不会出现了

  • POST请求解决方案是:设置输入流的编码

    1
    2
    request.setCharacterEncoding("UTF-8");
    注意:设置的字符集要和页面保持一致
  • 通用方式(GET/POST):需要先解码,再编码

    1
    new String(username.getBytes("ISO-8859-1"),"UTF-8");
  1. URL编码实现方式:
  • 编码:

    1
    URLEncoder.encode(str,"UTF-8");
  • 解码:

    1
    URLDecoder.decode(s,"ISO-8859-1");

2.5 Request请求转发

  1. ==请求转发(forward):一种在服务器内部的资源跳转方式。==

1628851404283

(1)浏览器发送请求给服务器,服务器中对应的资源A接收到请求

(2)资源A处理完请求后将请求发给资源B

(3)资源B处理完后将结果响应给浏览器

(4)请求从资源A到资源B的过程就叫==请求转发==

  1. 请求转发的实现方式:
1
req.getRequestDispatcher("资源B路径").forward(req,resp);

具体如何来使用,我们先来看下需求:

1628854783523

针对上述需求,具体的实现步骤为:

1.创建一个RequestDemo5类,接收/req5的请求,在doGet方法中打印demo5

2.创建一个RequestDemo6类,接收/req6的请求,在doGet方法中打印demo6

3.在RequestDemo5的方法中使用

​ req.getRequestDispatcher(“/req6”).forward(req,resp)进行请求转发

4.启动测试

(1)创建RequestDemo5类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 请求转发
*/
@WebServlet("/req5")
public class RequestDemo5 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("demo5...");
}

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

(2)创建RequestDemo6类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 请求转发
*/
@WebServlet("/req6")
public class RequestDemo6 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("demo6...");
}

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

(3)在RequestDemo5的doGet方法中进行请求转发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 请求转发
*/
@WebServlet("/req5")
public class RequestDemo5 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("demo5...");
//请求转发
request.getRequestDispatcher("/req6").forward(request,response);
}

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

(4)启动测试

访问http://localhost:8080/request-demo/req5,就可以在控制台看到如下内容:

1628855192876

说明请求已经转发到了/req6

  1. 请求转发资源间共享数据:使用Request对象

此处主要解决的问题是把请求从/req5转发到/req6的时候,如何传递数据给/req6

需要使用request对象提供的三个方法:

  • 存储数据到request域[范围,数据是存储在request对象]中
1
void setAttribute(String name,Object o);
  • 根据key获取值
1
Object getAttribute(String name);
  • 根据key删除该键值对
1
void removeAttribute(String name);

接着上个需求来:

1628856995417

1.在RequestDemo5的doGet方法中转发请求之前,将数据存入request域对象中

2.在RequestDemo6的doGet方法从request域对象中获取数据,并将数据打印到控制台

3.启动访问测试

(1)修改RequestDemo5中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/req5")
public class RequestDemo5 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("demo5...");
//存储数据
request.setAttribute("msg","hello");
//请求转发
request.getRequestDispatcher("/req6").forward(request,response);

}

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

(2)修改RequestDemo6中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 请求转发
*/
@WebServlet("/req6")
public class RequestDemo6 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("demo6...");
//获取数据
Object msg = request.getAttribute("msg");
System.out.println(msg);

}

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

(3)启动测试

访问http://localhost:8080/request-demo/req5,就可以在控制台看到如下内容:

1628857213364

此时就可以实现在转发多个资源之间共享数据。

  1. 请求转发的特点
  • 浏览器地址栏路径不发生变化

    虽然后台从/req5转发到/req6,但是浏览器的地址一直是/req5,未发生变化

    1628857365153

  • 只能转发到当前服务器的内部资源

    不能从一个服务器通过转发访问另一台服务器

  • 一次请求,可以在转发资源间使用request共享数据

    虽然后台从/req5转发到/req6,但是这个==只有一次请求==

3,Response对象

前面讲解完Request对象,接下来我们回到刚开始的那张图:

1628857632899

  • Request:使用request对象来==获取==请求数据
  • Response:使用response对象来==设置==响应数据

Reponse的继承体系和Request的继承体系也非常相似:

1628857761317

介绍完Response的相关体系结构后,接下来对于Response我们需要学习如下内容:

  • Response设置响应数据的功能介绍
  • Response完成重定向
  • Response响应字符数据
  • Response响应字节数据

3.1 Response设置响应数据功能介绍

HTTP响应数据总共分为三部分内容,分别是==响应行、响应头、响应体==,对于这三部分内容的数据,respone对象都提供了哪些方法来进行设置?

  1. 响应行

1628858926498

对于响应头,比较常用的就是设置响应状态码:

1
void setStatus(int sc);
  1. 响应头

1628859051368

设置响应头键值对:

1
void setHeader(String name,String value);
  1. 响应体

1628859268095

对于响应体,是通过字符、字节输出流的方式往浏览器写,

获取字符输出流:

1
PrintWriter getWriter();

获取字节输出流

1
ServletOutputStream getOutputStream();

介绍完这些方法后,后面我们会通过案例把这些方法都用一用,首先先来完成下重定向的功能开发。

3.2 Respones请求重定向

  1. ==Response重定向(redirect):一种资源跳转方式。==

1628859860279

(1)浏览器发送请求给服务器,服务器中对应的资源A接收到请求

(2)资源A现在无法处理该请求,就会给浏览器响应一个302的状态码+location的一个访问资源B的路径

(3)浏览器接收到响应状态码为302就会重新发送请求到location对应的访问地址去访问资源B

(4)资源B接收到请求后进行处理并最终给浏览器响应结果,这整个过程就叫==重定向==

  1. 重定向的实现方式:
1
2
resp.setStatus(302);
resp.setHeader("location","资源B的访问路径");

具体如何来使用,我们先来看下需求:

1628861030429

针对上述需求,具体的实现步骤为:

1.创建一个ResponseDemo1类,接收/resp1的请求,在doGet方法中打印resp1....

2.创建一个ResponseDemo2类,接收/resp2的请求,在doGet方法中打印resp2....

3.在ResponseDemo1的方法中使用

​ response.setStatus(302);

​ response.setHeader(“Location”,”/request-demo/resp2”) 来给前端响应结果数据

4.启动测试

(1)创建ResponseDemo1类

1
2
3
4
5
6
7
8
9
10
11
12
@WebServlet("/resp1")
public class ResponseDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("resp1....");
}

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

(2)创建ResponseDemo2类

1
2
3
4
5
6
7
8
9
10
11
12
@WebServlet("/resp2")
public class ResponseDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("resp2....");
}

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

(3)在ResponseDemo1的doGet方法中给前端响应数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/resp1")
public class ResponseDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("resp1....");
//重定向
//1.设置响应状态码 302
response.setStatus(302);
//2. 设置响应头 Location
response.setHeader("Location","/request-demo/resp2");
}

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

(4)启动测试

访问http://localhost:8080/request-demo/resp1,就可以在控制台看到如下内容:

1628861404699

说明/resp1/resp2都被访问到了。到这重定向就已经完成了。

虽然功能已经实现,但是从设置重定向的两行代码来看,会发现除了重定向的地址不一样,其他的内容都是一模一样,所以request对象给我们提供了简化的编写方式为:

1
resposne.sendRedirect("/request-demo/resp2")

所以第3步中的代码就可以简化为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@WebServlet("/resp1")
public class ResponseDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("resp1....");
//重定向
resposne.sendRedirect("/request-demo/resp2");
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
  1. 重定向的特点
  • 浏览器地址栏路径发送变化

    当进行重定向访问的时候,由于是由浏览器发送的两次请求,所以地址会发生变化

    1628861893130

  • 可以重定向到任何位置的资源(服务内容、外部均可)

    因为第一次响应结果中包含了浏览器下次要跳转的路径,所以这个路径是可以任意位置资源。

  • 两次请求,不能在多个资源使用request共享数据

    因为浏览器发送了两次请求,是两个不同的request对象,就无法通过request对象进行共享数据

介绍完==请求重定向==和==请求转发==以后,接下来需要把这两个放在一块对比下:

1628862170296

以后到底用哪个,还是需要根据具体的业务来决定。

3.3 路径问题

  1. 问题1:转发的时候路径上没有加/request-demo而重定向加了,那么到底什么时候需要加,什么时候不需要加呢?

1628862652700

其实判断的依据很简单,只需要记住下面的规则即可:

  • 浏览器使用:需要加虚拟目录(项目访问路径)
  • 服务端使用:不需要加虚拟目录

对于转发来说,因为是在服务端进行的,所以不需要加虚拟目录

对于重定向来说,路径最终是由浏览器来发送请求,就需要添加虚拟目录。

掌握了这个规则,接下来就通过一些练习来强化下知识的学习:

  • <a href='路劲'>
  • <form action='路径'>
  • req.getRequestDispatcher(“路径”)
  • resp.sendRedirect(“路径”)

答案:

1
2
3
4
1.超链接,从浏览器发送,需要加
2.表单,从浏览器发送,需要加
3.转发,是从服务器内部跳转,不需要加
4.重定向,是由浏览器进行跳转,需要加。
  1. 问题2:在重定向的代码中,/request-demo是固定编码的,如果后期通过Tomcat插件配置了项目的访问路径,那么所有需要重定向的地方都需要重新修改,该如何优化?

1628863270545

答案也比较简单,我们可以在代码中动态去获取项目访问的虚拟目录,具体如何获取,我们可以借助前面咱们所学习的request对象中的getContextPath()方法,修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/resp1")
public class ResponseDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("resp1....");

//简化方式完成重定向
//动态获取虚拟目录
String contextPath = request.getContextPath();
response.sendRedirect(contextPath+"/resp2");
}

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

重新启动访问测试,功能依然能够实现,此时就可以动态获取项目访问的虚拟路径,从而降低代码的耦合度。

3.4 Response响应字符数据

要想将字符数据写回到浏览器,我们需要两个步骤:

  • 通过Response对象获取字符输出流: PrintWriter writer = resp.getWriter();

  • 通过字符输出流写数据: writer.write(“aaa”);

接下来,我们实现通过些案例把响应字符数据给实际应用下:

  1. 返回一个简单的字符串aaa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 响应字符数据:设置字符数据的响应体
*/
@WebServlet("/resp3")
public class ResponseDemo3 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");
//1. 获取字符输出流
PrintWriter writer = response.getWriter();
writer.write("aaa");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}

1628863905362

  1. 返回一串html字符串,并且能被浏览器解析
1
2
3
4
PrintWriter writer = response.getWriter();
//content-type,告诉浏览器返回的数据类型是HTML类型数据,这样浏览器才会解析HTML标签
response.setHeader("content-type","text/html");
writer.write("<h1>aaa</h1>");

1628864140820

==注意:==一次请求响应结束后,response对象就会被销毁掉,所以不要手动关闭流。

  1. 返回一个中文的字符串你好,需要注意设置响应数据的编码为utf-8
1
2
3
//设置响应的数据格式及数据的编码
response.setContentType("text/html;charset=utf-8");
writer.write("你好");

1628864390263

3.3 Response响应字节数据

要想将字节数据写回到浏览器,我们需要两个步骤:

  • 通过Response对象获取字节输出流:ServletOutputStream outputStream = resp.getOutputStream();

  • 通过字节输出流写数据: outputStream.write(字节数据);

接下来,我们实现通过些案例把响应字符数据给实际应用下:

  1. 返回一个图片文件到浏览器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 响应字节数据:设置字节数据的响应体
*/
@WebServlet("/resp4")
public class ResponseDemo4 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 读取文件
FileInputStream fis = new FileInputStream("d://a.jpg");
//2. 获取response字节输出流
ServletOutputStream os = response.getOutputStream();
//3. 完成流的copy
byte[] buff = new byte[1024];
int len = 0;
while ((len = fis.read(buff))!= -1){
os.write(buff,0,len);
}
fis.close();
}

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

1628864883564

上述代码中,对于流的copy的代码还是比较复杂的,所以我们可以使用别人提供好的方法来简化代码的开发,具体的步骤是:

(1)pom.xml添加依赖

1
2
3
4
5
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>

(2)调用工具类方法

1
2
3
//fis:输入流
//os:输出流
IOUtils.copy(fis,os);

优化后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 响应字节数据:设置字节数据的响应体
*/
@WebServlet("/resp4")
public class ResponseDemo4 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 读取文件
FileInputStream fis = new FileInputStream("d://a.jpg");
//2. 获取response字节输出流
ServletOutputStream os = response.getOutputStream();
//3. 完成流的copy
IOUtils.copy(fis,os);
fis.close();
}

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

4,用户注册登录案例

接下来我们通过两个比较常见的案例,一个是==注册==,一个是==登录==来对今天学习的内容进行一个实战演练,首先来实现用户登录。

4.1 用户登录

4.1.1 需求分析

1628865728305

  1. 用户在登录页面输入用户名和密码,提交请求给LoginServlet
  2. 在LoginServlet中接收请求和数据[用户名和密码]
  3. 在LoginServlt中通过Mybatis实现调用UserMapper来根据用户名和密码查询数据库表
  4. 将查询的结果封装到User对象中进行返回
  5. 在LoginServlet中判断返回的User对象是否为null
  6. 如果为nul,说明根据用户名和密码没有查询到用户,则登录失败,返回”登录失败”数据给前端
  7. 如果不为null,则说明用户存在并且密码正确,则登录成功,返回”登录成功”数据给前端

4.1.2 环境准备

  1. 复制资料中的静态页面到项目的webapp目录下

参考资料\1. 登陆注册案例\1. 静态页面,拷贝完效果如下:

1628866248169

  1. 创建db1数据库,创建tb_user表,创建User实体类

2.1 将资料\1. 登陆注册案例\2. MyBatis环境\tb_user.sql中的sql语句执行下:

1628866403891

2.2 将资料\1. 登陆注册案例\2. MyBatis环境\User.java拷贝到com.itheima.pojo

1628866560738

  1. 在项目的pom.xml导入Mybatis和Mysql驱动坐标
1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.5</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
</dependency>
  1. 创建mybatis-config.xml核心配置文件,UserMapper.xml映射文件,UserMapper接口

4.1 将资料\1. 登陆注册案例\2. MyBatis环境\mybatis-config.xml拷贝到resources目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--起别名-->
<typeAliases>
<package name="com.itheima.pojo"/>
</typeAliases>

<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<!--
useSSL:关闭SSL安全连接 性能更高
useServerPrepStmts:开启预编译功能
&amp; 等同于 & ,xml配置文件中不能直接写 &符号
-->
<property name="url" value="jdbc:mysql:///db1?useSSL=false&amp;useServerPrepStmts=true"/>
<property name="username" value="root"/>
<property name="password" value="1234"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--扫描mapper-->
<package name="com.itheima.mapper"/>
</mappers>
</configuration>

4.2 在com.itheima.mapper包下创建UserMapper接口

1
2
3
public interface UserMapper {

}

4.3 将资料\1. 登陆注册案例\2. MyBatis环境\UserMapper.xml拷贝到resources目录下

==注意:在resources下创建UserMapper.xml的目录时,要使用/分割==

1628867237329

至此我们所需要的环境就都已经准备好了,具体该如何实现?

4.1.3 代码实现

  1. 在UserMapper接口中提供一个根据用户名和密码查询用户对象的方法
1
2
3
4
5
6
7
8
/**
* 根据用户名和密码查询用户对象
* @param username
* @param password
* @return
*/
@Select("select * from tb_user where username = #{username} and password = #{password}")
User select(@Param("username") String username,@Param("password") String password);

说明

@Param注解的作用:用于传递参数,是方法的参数可以与SQL中的字段名相对应。

  1. 修改loign.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>login</title>
<link href="css/login.css" rel="stylesheet">
</head>

<body>
<div id="loginDiv">
<form action="/request-demo/loginServlet" method="post" id="form">
<h1 id="loginMsg">LOGIN IN</h1>
<p>Username:<input id="username" name="username" type="text"></p>

<p>Password:<input id="password" name="password" type="password"></p>

<div id="subDiv">
<input type="submit" class="button" value="login up">
<input type="reset" class="button" value="reset">&nbsp;&nbsp;&nbsp;
<a href="register.html">没有账号?点击注册</a>
</div>
</form>
</div>

</body>
</html>
  1. 编写LoginServlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@WebServlet("/loginServlet")
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 接收用户名和密码
String username = request.getParameter("username");
String password = request.getParameter("password");

//2. 调用MyBatis完成查询
//2.1 获取SqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2.2 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//2.3 获取Mapper
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//2.4 调用方法
User user = userMapper.select(username, password);
//2.5 释放资源
sqlSession.close();


//获取字符输出流,并设置content type
response.setContentType("text/html;charset=utf-8");
PrintWriter writer = response.getWriter();
//3. 判断user释放为null
if(user != null){
// 登陆成功
writer.write("登陆成功");
}else {
// 登陆失败
writer.write("登陆失败");
}
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
  1. 启动服务器测试

4.1 如果用户名和密码输入错误,则

1628867761245

4.2 如果用户名和密码输入正确,则

1628867801708

至此用户的登录功能就已经完成了~

4.2 用户注册

4.2.1 需求分析

1628867904783

  1. 用户在注册页面输入用户名和密码,提交请求给RegisterServlet
  2. 在RegisterServlet中接收请求和数据[用户名和密码]
  3. 在RegisterServlet中通过Mybatis实现调用UserMapper来根据用户名查询数据库表
  4. 将查询的结果封装到User对象中进行返回
  5. 在RegisterServlet中判断返回的User对象是否为null
  6. 如果为nul,说明根据用户名可用,则调用UserMapper来实现添加用户
  7. 如果不为null,则说明用户不可以,返回”用户名已存在”数据给前端

4.2.2 代码编写

  1. 编写UserMapper提供根据用户名查询用户数据方法和添加用户方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 根据用户名查询用户对象
* @param username
* @return
*/
@Select("select * from tb_user where username = #{username}")
User selectByUsername(String username);

/**
* 添加用户
* @param user
*/
@Insert("insert into tb_user values(null,#{username},#{password})")
void add(User user);
  1. 修改register.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>欢迎注册</title>
<link href="css/register.css" rel="stylesheet">
</head>
<body>

<div class="form-div">
<div class="reg-content">
<h1>欢迎注册</h1>
<span>已有帐号?</span> <a href="login.html">登录</a>
</div>
<form id="reg-form" action="/request-demo/registerServlet" method="post">

<table>

<tr>
<td>用户名</td>
<td class="inputs">
<input name="username" type="text" id="username">
<br>
<span id="username_err" class="err_msg" style="display: none">用户名不太受欢迎</span>
</td>

</tr>

<tr>
<td>密码</td>
<td class="inputs">
<input name="password" type="password" id="password">
<br>
<span id="password_err" class="err_msg" style="display: none">密码格式有误</span>
</td>
</tr>

</table>

<div class="buttons">
<input value="注 册" type="submit" id="reg_btn">
</div>
<br class="clear">
</form>

</div>
</body>
</html>
  1. 创建RegisterServlet类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@WebServlet("/registerServlet")
public class RegisterServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 接收用户数据
String username = request.getParameter("username");
String password = request.getParameter("password");

//封装用户对象
User user = new User();
user.setUsername(username);
user.setPassword(password);

//2. 调用mapper 根据用户名查询用户对象
//2.1 获取SqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2.2 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//2.3 获取Mapper
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

//2.4 调用方法
User u = userMapper.selectByUsername(username);

//3. 判断用户对象释放为null
if( u == null){
// 用户名不存在,添加用户
userMapper.add(user);

// 提交事务
sqlSession.commit();
// 释放资源
sqlSession.close();
}else {
// 用户名存在,给出提示信息
response.setContentType("text/html;charset=utf-8");
response.getWriter().write("用户名已存在");
}

}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
  1. 启动服务器进行测试

4.1 如果测试成功,则在数据库中就能查看到新注册的数据

4.2 如果用户已经存在,则在页面上展示 用户名已存在 的提示信息

4.3 SqlSessionFactory工具类抽取

上面两个功能已经实现,但是在写Servlet的时候,因为需要使用Mybatis来完成数据库的操作,所以对于Mybatis的基础操作就出现了些重复代码,如下

1
2
3
4
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new
SqlSessionFactoryBuilder().build(inputStream);

有了这些重复代码就会造成一些问题:

  • 重复代码不利于后期的维护
  • SqlSessionFactory工厂类进行重复创建
    • 就相当于每次买手机都需要重新创建一个手机生产工厂来给你制造一个手机一样,资源消耗非常大但性能却非常低。所以这么做是不允许的。

那如何来优化呢?

  • 代码重复可以抽取工具类
  • 对指定代码只需要执行一次可以使用静态代码块

有了这两个方向后,代码具体该如何编写?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SqlSessionFactoryUtils {

private static SqlSessionFactory sqlSessionFactory;

static {
//静态代码块会随着类的加载而自动执行,且只执行一次
try {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}


public static SqlSessionFactory getSqlSessionFactory(){
return sqlSessionFactory;
}
}

工具类抽取以后,以后在对Mybatis的SqlSession进行操作的时候,就可以直接使用

1
SqlSessionFactory sqlSessionFactory =SqlSessionFactoryUtils.getSqlSessionFactory();

这样就可以很好的解决上面所说的代码重复和重复创建工厂导致性能低的问题了。

JSP

今日目标:

  • 理解 JSP 及 JSP 原理
  • 能在 JSP中使用 EL表达式JSTL标签
  • 理解 MVC模式三层架构
  • 能完成品牌数据的增删改查功能

1,JSP 概述

==JSP(全称:Java Server Pages):Java 服务端页面。==是一种动态的网页技术,其中既可以定义 HTML、JS、CSS等静态内容,还可以定义 Java代码的动态内容,也就是 JSP = HTML + Java。如下就是jsp代码

1
2
3
4
5
6
7
8
9
10
11
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>JSP,Hello World</h1>
<%
System.out.println("hello,jsp~");
%>
</body>
</html>

上面代码 h1 标签内容是展示在页面上,而 Java 的输出语句是输出在 idea 的控制台。

那么,JSP 能做什么呢?现在我们只用 servlet 实现功能,看存在什么问题。如下图所示,当我们登陆成功后,需要在页面上展示用户名

image-20210818101320935

上图的用户名是动态展示,也就是谁登陆就展示谁的用户名。只用 servlet 如何实现呢?在今天的资料里已经提供好了一个 LoginServlet ,该 servlet 就是实现这个功能的,现将资料中的 LoginServlet.java 拷贝到 request-demo 项目中来演示。接下来启动服务器并访问登陆页面

image-20210818102205544

输入了 zhangsan 用户的登陆信息后点击 登陆 按钮,就能看到如下图效果

image-20210818102313898

当然如果是 lisi 登陆的,在该页面展示的就是 lisi,欢迎您,动态的展示效果就实现了。那么 LoginServlet 到底是如何实现的,我们看看它里面的内容

image-20210818102506754

上面的代码有大量使用到 writer 对象向页面写标签内容,这样我们的代码就显得很麻烦;将来如果展示的效果出现了问题,排错也显得有点力不从心。而 JSP 是如何解决这个问题的呢?在资料中也提供了一个 login.jsp 页面,该页面也能实现该功能,现将该页面拷贝到项目的 webapp下,需要修改 login.html 中表单数据提交的路径为下图

image-20210818103127245

重新启动服务器并进行测试,发现也可以实现同样的功能。那么 login.jsp 又是如何实现的呢?那我们来看看 login.jsp 的代码

image-20210818103352432

上面代码可以看到里面基本都是 HTML 标签,而动态数据使用 Java 代码进行展示;这样操作看起来要比用 servlet 实现要舒服很多。

JSP 作用:简化开发,避免了在Servlet中直接输出HTML标签。

2,JSP 快速入门

接下来我们做一个简单的快速入门代码。

2.1 搭建环境

创建一个maven的 web 项目,项目结构如下:

image-20210818104316457

pom.xml 文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?xml version="1.0" encoding="UTF-8"?>
<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>

<groupId>org.example</groupId>
<artifactId>jsp-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
</plugin>
</plugins>
</build>
</project>

2.2 导入 JSP 依赖

dependencies 标签中导入 JSP 的依赖,如下

1
2
3
4
5
6
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>

该依赖的 scope 必须设置为 provided,因为 tomcat 中有这个jar包了,所以在打包时我们是不希望将该依赖打进到我们工程的war包中。

2.3 创建 jsp 页面

在项目的 webapp 下创建jsp页面

image-20210818105519970

通过上面方式创建一个名为 hello.jsp 的页面。

2.4 编写代码

hello.jsp 页面中书写 HTML 标签和 Java 代码,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>hello jsp</h1>

<%
System.out.println("hello,jsp~");
%>
</body>
</html>

2.5 测试

启动服务器并在浏览器地址栏输入 http://localhost:8080/jsp-demo/hello.jsp,我们可以在页面上看到如下内容

image-20210818110122438

同时也可以看到在 idea 的控制台看到输出的 hello,jsp~ 内容。

3,JSP 原理

我们之前说 JSP 就是一个页面,那么在 JSP 中写 html 标签,我们能理解,但是为什么还可以写 Java 代码呢?

因为 ==JSP 本质上就是一个 Servlet。==接下来我们聊聊访问jsp时的流程

image-20210818111039350
  1. 浏览器第一次访问 hello.jsp 页面
  2. tomcat 会将 hello.jsp 转换为名为 hello_jsp.java 的一个 Servlet
  3. tomcat 再将转换的 servlet 编译成字节码文件 hello_jsp.class
  4. tomcat 会执行该字节码文件,向外提供服务

我们可以到项目所在磁盘目录下找 target\tomcat\work\Tomcat\localhost\jsp-demo\org\apache\jsp 目录,而这个目录下就能看到转换后的 servlet

image-20210818112613589

打开 hello_jsp.java 文件,来查看里面的代码

image-20210818112724462

由上面的类的继承关系可以看到继承了名为 HttpJspBase 这个类,那我们在看该类的继承关系。到资料中的找如下目录: 资料\tomcat源码\apache-tomcat-8.5.68-src\java\org\apache\jasper\runtime ,该目录下就有 HttpJspBase 类,查看该类的继承关系

image-20210818113118802

可以看到该类继承了 HttpServlet ;那么 hello_jsp 这个类就间接的继承了 HttpServlet ,也就说明 hello_jsp 是一个 servlet

继续阅读 hello_jsp 类的代码,可以看到有一个名为 _jspService() 的方法,该方法就是每次访问 jsp 时自动执行的方法,和 servlet 中的 service 方法一样 。

而在 _jspService() 方法中可以看到往浏览器写标签的代码:

image-20210818114008998

以前我们自己写 servlet 时,这部分代码是由我们自己来写,现在有了 jsp 后,由tomcat完成这部分功能。

4,JSP 脚本

JSP脚本用于在 JSP页面内定义 Java代码。在之前的入门案例中我们就在 JSP 页面定义的 Java 代码就是 JSP 脚本。

4.1 JSP 脚本分类

JSP 脚本有如下三个分类:

  • <%…%>:内容会直接放到_jspService()方法之中
  • <%=…%>:内容会放到out.print()中,作为out.print()的参数
  • <%!…%>:内容会放到_jspService()方法之外,被类直接包含

代码演示:

hello.jsp 中书写

1
2
3
4
<%
System.out.println("hello,jsp~");
int i = 3;
%>

通过浏览器访问 hello.jsp 后,查看转换的 hello_jsp.java 文件,i 变量定义在了 _jspService() 方法中

image-20210818123606231

hello.jsp 中书写

1
2
<%="hello"%>
<%=i%>

通过浏览器访问 hello.jsp 后,查看转换的 hello_jsp.java 文件,该脚本的内容被放在了 out.print() 中,作为参数

image-20210818123820571

hello.jsp 中书写

1
2
3
4
<%!
void show(){}
String name = "zhangsan";
%>

通过浏览器访问 hello.jsp 后,查看转换的 hello_jsp.java 文件,该脚本的内容被放在了成员位置

image-20210818123946272

4.2 案例

4.2.1 需求

使用JSP脚本展示品牌数据

image-20210818125203390

说明:

  • 在资料 资料\1. JSP案例素材 中提供了 brand.html 静态页面
  • 在该案例中数据不从数据库中查询,而是在 JSP 页面上写死

4.2.2 实现

  • 将资料 资料\1. JSP案例素材 中的 Brand.java 文件放置到项目的 com.itheima.pojo 包下

  • 在项目的 webapp 中创建 brand.jsp ,并将 brand.html页面中的内容拷贝过来。brand.jsp 内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Title</title>
    </head>
    <body>
    <input type="button" value="新增"><br>
    <hr>
    <table border="1" cellspacing="0" width="800">
    <tr>
    <th>序号</th>
    <th>品牌名称</th>
    <th>企业名称</th>
    <th>排序</th>
    <th>品牌介绍</th>
    <th>状态</th>
    <th>操作</th>

    </tr>
    <tr align="center">
    <td>1</td>
    <td>三只松鼠</td>
    <td>三只松鼠</td>
    <td>100</td>
    <td>三只松鼠,好吃不上火</td>
    <td>启用</td>
    <td><a href="#">修改</a> <a href="#">删除</a></td>
    </tr>

    <tr align="center">
    <td>2</td>
    <td>优衣库</td>
    <td>优衣库</td>
    <td>10</td>
    <td>优衣库,服适人生</td>
    <td>禁用</td>

    <td><a href="#">修改</a> <a href="#">删除</a></td>
    </tr>

    <tr align="center">
    <td>3</td>
    <td>小米</td>
    <td>小米科技有限公司</td>
    <td>1000</td>
    <td>为发烧而生</td>
    <td>启用</td>

    <td><a href="#">修改</a> <a href="#">删除</a></td>
    </tr>
    </table>
    </body>
    </html>

    现在页面中的数据都是假数据。

  • brand.jsp 中准备一些数据

    1
    2
    3
    4
    5
    6
    7
    <%
    // 查询数据库
    List<Brand> brands = new ArrayList<Brand>();
    brands.add(new Brand(1,"三只松鼠","三只松鼠",100,"三只松鼠,好吃不上火",1));
    brands.add(new Brand(2,"优衣库","优衣库",200,"优衣库,服适人生",0));
    brands.add(new Brand(3,"小米","小米科技有限公司",1000,"为发烧而生",1));
    %>

    ==注意:==这里的类是需要导包的

  • brand.jsp 页面中的 table 标签中的数据改为动态的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <table border="1" cellspacing="0" width="800">
    <tr>
    <th>序号</th>
    <th>品牌名称</th>
    <th>企业名称</th>
    <th>排序</th>
    <th>品牌介绍</th>
    <th>状态</th>
    <th>操作</th>

    </tr>

    <%
    for (int i = 0; i < brands.size(); i++) {
    //获取集合中的 每一个 Brand 对象
    Brand brand = brands.get(i);
    }
    %>
    <tr align="center">
    <td>1</td>
    <td>三只松鼠</td>
    <td>三只松鼠</td>
    <td>100</td>
    <td>三只松鼠,好吃不上火</td>
    <td>启用</td>
    <td><a href="#">修改</a> <a href="#">删除</a></td>
    </tr>
    </table>

    上面的for循环需要将 tr 标签包裹起来,这样才能实现循环的效果,代码改进为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    <table border="1" cellspacing="0" width="800">
    <tr>
    <th>序号</th>
    <th>品牌名称</th>
    <th>企业名称</th>
    <th>排序</th>
    <th>品牌介绍</th>
    <th>状态</th>
    <th>操作</th>

    </tr>

    <%
    for (int i = 0; i < brands.size(); i++) {
    //获取集合中的 每一个 Brand 对象
    Brand brand = brands.get(i);
    %>
    <tr align="center">
    <td>1</td>
    <td>三只松鼠</td>
    <td>三只松鼠</td>
    <td>100</td>
    <td>三只松鼠,好吃不上火</td>
    <td>启用</td>
    <td><a href="#">修改</a> <a href="#">删除</a></td>
    </tr>
    <%
    }
    %>

    </table>

    注意:<%%> 里面写的是 Java 代码,而外边写的是 HTML 标签

    上面代码中的 td 标签中的数据都需要是动态的,所以还需要改进

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    <table border="1" cellspacing="0" width="800">
    <tr>
    <th>序号</th>
    <th>品牌名称</th>
    <th>企业名称</th>
    <th>排序</th>
    <th>品牌介绍</th>
    <th>状态</th>
    <th>操作</th>

    </tr>

    <%
    for (int i = 0; i < brands.size(); i++) {
    //获取集合中的 每一个 Brand 对象
    Brand brand = brands.get(i);
    %>
    <tr align="center">
    <td><%=brand.getId()%></td>
    <td><%=brand.getBrandName()%></td>
    <td><%=brand.getCompanyName()%></td>
    <td><%=brand.getOrdered()%></td>
    <td><%=brand.getDescription()%></td>
    <td><%=brand.getStatus() == 1 ? "启用":"禁用"%></td>
    <td><a href="#">修改</a> <a href="#">删除</a></td>
    </tr>
    <%
    }
    %>

    </table>

4.2.3 成品代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<%@ page import="com.itheima.pojo.Brand" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
// 查询数据库
List<Brand> brands = new ArrayList<Brand>();
brands.add(new Brand(1,"三只松鼠","三只松鼠",100,"三只松鼠,好吃不上火",1));
brands.add(new Brand(2,"优衣库","优衣库",200,"优衣库,服适人生",0));
brands.add(new Brand(3,"小米","小米科技有限公司",1000,"为发烧而生",1));

%>


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<input type="button" value="新增"><br>
<hr>
<table border="1" cellspacing="0" width="800">
<tr>
<th>序号</th>
<th>品牌名称</th>
<th>企业名称</th>
<th>排序</th>
<th>品牌介绍</th>
<th>状态</th>
<th>操作</th>
</tr>
<%
for (int i = 0; i < brands.size(); i++) {
Brand brand = brands.get(i);
%>

<tr align="center">
<td><%=brand.getId()%></td>
<td><%=brand.getBrandName()%></td>
<td><%=brand.getCompanyName()%></td>
<td><%=brand.getOrdered()%></td>
<td><%=brand.getDescription()%></td>
<td><%=brand.getStatus() == 1 ? "启用":"禁用"%></td>
<td><a href="#">修改</a> <a href="#">删除</a></td>
</tr>

<%
}
%>
</table>
</body>
</html>

4.2.4 测试

在浏览器地址栏输入 http://localhost:8080/jsp-demo/brand.jsp ,页面展示效果如下

image-20210818145450748

4.3 JSP 缺点

通过上面的案例,我们可以看到 JSP 的很多缺点。

由于 JSP页面内,既可以定义 HTML 标签,又可以定义 Java代码,造成了以下问题:

  • 书写麻烦:特别是复杂的页面

    既要写 HTML 标签,还要写 Java 代码

  • 阅读麻烦

    上面案例的代码,相信你后期再看这段代码时还需要花费很长的时间去梳理

  • 复杂度高:运行需要依赖于各种环境,JRE,JSP容器,JavaEE…

  • 占内存和磁盘:JSP会自动生成.java和.class文件占磁盘,运行的是.class文件占内存

  • 调试困难:出错后,需要找到自动生成的.java文件进行调试

  • 不利于团队协作:前端人员不会 Java,后端人员不精 HTML

    如果页面布局发生变化,前端工程师对静态页面进行修改,然后再交给后端工程师,由后端工程师再将该页面改为 JSP 页面

由于上述的问题, ==JSP 已逐渐退出历史舞台,==以后开发更多的是使用 ==HTML + Ajax== 来替代。Ajax 是我们后续会重点学习的技术。有个这个技术后,前端工程师负责前端页面开发,而后端工程师只负责前端代码开发。下来对技术的发展进行简单的说明

image-20210818150346332
  1. 第一阶段:使用 servlet 即实现逻辑代码编写,也对页面进行拼接。这种模式我们之前也接触过

  2. 第二阶段:随着技术的发展,出现了 JSP ,人们发现 JSP 使用起来比 Servlet 方便很多,但是还是要在 JSP 中嵌套 Java 代码,也不利于后期的维护

  3. 第三阶段:使用 Servlet 进行逻辑代码开发,而使用 JSP 进行数据展示

    image-20210818151232955
  4. 第四阶段:使用 servlet 进行后端逻辑代码开发,而使用 HTML 进行数据展示。而这里面就存在问题,HTML 是静态页面,怎么进行动态数据展示呢?这就是 ajax 的作用了。

那既然 JSP 已经逐渐的退出历史舞台,那我们为什么还要学习 JSP 呢?原因有两点:

  • 一些公司可能有些老项目还在用 JSP ,所以要求我们必须动 JSP
  • 我们如果不经历这些复杂的过程,就不能体现后面阶段开发的简单

接下来我们来学习第三阶段,使用 EL表达式JSTL 标签库替换 JSP 中的 Java 代码。

5,EL 表达式

5.1 概述

EL(全称Expression Language )表达式语言,用于简化 JSP 页面内的 Java 代码。

EL 表达式的主要作用是 ==获取数据==。其实就是从域对象中获取数据,然后将数据展示在页面上。

而 EL 表达式的语法也比较简单,==${expression}== 。例如:${brands} 就是获取域中存储的 key 为 brands 的数据。

5.2 代码演示

  • 定义servlet,在 servlet 中封装一些数据并存储到 request 域对象中并转发到 el-demo.jsp 页面。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @WebServlet("/demo1")
    public class ServletDemo1 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //1. 准备数据
    List<Brand> brands = new ArrayList<Brand>();
    brands.add(new Brand(1,"三只松鼠","三只松鼠",100,"三只松鼠,好吃不上火",1));
    brands.add(new Brand(2,"优衣库","优衣库",200,"优衣库,服适人生",0));
    brands.add(new Brand(3,"小米","小米科技有限公司",1000,"为发烧而生",1));

    //2. 存储到request域中
    request.setAttribute("brands",brands);

    //3. 转发到 el-demo.jsp
    request.getRequestDispatcher("/el-demo.jsp").forward(request,response);
    }

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

    ==注意:== 此处需要用转发,因为转发才可以使用 request 对象作为域对象进行数据共享

  • el-demo.jsp 中通过 EL表达式 获取数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
    <title>Title</title>
    </head>
    <body>
    ${brands}
    </body>
    </html>
  • 在浏览器的地址栏输入 http://localhost:8080/jsp-demo/demo1 ,页面效果如下:

    image-20210818152536484

5.3 域对象

JavaWeb中有四大域对象,分别是:

  • page:当前页面有效
  • request:当前请求有效
  • session:当前会话有效
  • application:当前应用有效

el 表达式获取数据,会依次从这4个域中寻找,直到找到为止。而这四个域对象的作用范围如下图所示

image-20210818152857407

例如: ${brands},el 表达式获取数据,会先从page域对象中获取数据,如果没有再到 requet 域对象中获取数据,如果再没有再到 session 域对象中获取,如果还没有才会到 application 中获取数据。

6,JSTL标签

6.1 概述

JSP标准标签库(Jsp Standarded Tag Library) ,使用标签取代JSP页面上的Java代码。如下代码就是JSTL标签

1
2
3
4
5
6
<c:if test="${flag == 1}">

</c:if>
<c:if test="${flag == 2}">

</c:if>

上面代码看起来是不是比 JSP 中嵌套 Java 代码看起来舒服好了。而且前端工程师对标签是特别敏感的,他们看到这段代码是能看懂的。

JSTL 提供了很多标签,如下图

image-20210818153646575

我们只对两个最常用的标签进行讲解,<c:forEach> 标签和 <c:if> 标签。

JSTL 使用也是比较简单的,分为如下步骤:

  • 导入坐标

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <dependency>
    <groupId>jstl</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
    </dependency>
    <dependency>
    <groupId>taglibs</groupId>
    <artifactId>standard</artifactId>
    <version>1.1.2</version>
    </dependency>
  • 在JSP页面上引入JSTL标签库

    1
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 
  • 使用标签

6.2 if 标签

<c:if>:相当于 if 判断

  • 属性:test,用于定义条件表达式
1
2
3
4
5
6
<c:if test="${flag == 1}">

</c:if>
<c:if test="${flag == 2}">

</c:if>

代码演示:

  • 定义一个 servlet ,在该 servlet 中向 request 域对象中添加 键是 status ,值为 1 的数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @WebServlet("/demo2")
    public class ServletDemo2 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //1. 存储数据到request域中
    request.setAttribute("status",1);

    //2. 转发到 jstl-if.jsp
    数据request.getRequestDispatcher("/jstl-if.jsp").forward(request,response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }
    }
  • 定义 jstl-if.jsp 页面,在该页面使用 <c:if> 标签

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    <html>
    <head>
    <title>Title</title>
    </head>
    <body>
    <%--
    c:if:来完成逻辑判断,替换java if else
    --%>
    <c:if test="${status ==1}">
    启用
    </c:if>

    <c:if test="${status ==0}">
    禁用
    </c:if>
    </body>
    </html>

    ==注意:== 在该页面已经要引入 JSTL核心标签库

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

6.3 forEach 标签

<c:forEach>:相当于 for 循环。java中有增强for循环和普通for循环,JSTL 中的 <c:forEach> 也有两种用法

6.3.1 用法一

类似于 Java 中的增强for循环。涉及到的 <c:forEach> 中的属性如下

  • items:被遍历的容器

  • var:遍历产生的临时变量

  • varStatus:遍历状态对象

如下代码,是从域对象中获取名为 brands 数据,该数据是一个集合;遍历遍历,并给该集合中的每一个元素起名为 brand,是 Brand对象。在循环里面使用 EL表达式获取每一个Brand对象的属性值

1
2
3
4
5
6
7
8
<c:forEach items="${brands}" var="brand">
<tr align="center">
<td>${brand.id}</td>
<td>${brand.brandName}</td>
<td>${brand.companyName}</td>
<td>${brand.description}</td>
</tr>
</c:forEach>

代码演示:

  • servlet 还是使用之前的名为 ServletDemo1

  • 定义名为 jstl-foreach.jsp 页面,内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Title</title>
    </head>
    <body>
    <input type="button" value="新增"><br>
    <hr>
    <table border="1" cellspacing="0" width="800">
    <tr>
    <th>序号</th>
    <th>品牌名称</th>
    <th>企业名称</th>
    <th>排序</th>
    <th>品牌介绍</th>
    <th>状态</th>
    <th>操作</th>
    </tr>

    <c:forEach items="${brands}" var="brand" varStatus="status">
    <tr align="center">
    <%--<td>${brand.id}</td>--%>
    <td>${status.count}</td>
    <td>${brand.brandName}</td>
    <td>${brand.companyName}</td>
    <td>${brand.ordered}</td>
    <td>${brand.description}</td>
    <c:if test="${brand.status == 1}">
    <td>启用</td>
    </c:if>
    <c:if test="${brand.status != 1}">
    <td>禁用</td>
    </c:if>
    <td><a href="#">修改</a> <a href="#">删除</a></td>
    </tr>
    </c:forEach>
    </table>
    </body>
    </html>

6.3.2 用法二

类似于 Java 中的普通for循环。涉及到的 <c:forEach> 中的属性如下

  • begin:开始数

  • end:结束数

  • step:步长

实例代码:

从0循环到10,变量名是 i ,每次自增1

1
2
3
<c:forEach begin="0" end="10" step="1" var="i">
${i}
</c:forEach>

7,MVC模式和三层架构

MVC 模式和三层架构是一些理论的知识,将来我们使用了它们进行代码开发会让我们代码维护性和扩展性更好。

7.1 MVC模式

MVC 是一种分层开发的模式,其中:

  • M:Model,业务模型,处理业务

  • V:View,视图,界面展示

  • C:Controller,控制器,处理请求,调用模型和视图

image-20210818163348642

控制器(serlvlet)用来接收浏览器发送过来的请求,控制器调用模型(JavaBean)来获取数据,比如从数据库查询数据;控制器获取到数据后再交由视图(JSP)进行数据展示。

MVC 好处:

  • 职责单一,互不影响。每个角色做它自己的事,各司其职。

  • 有利于分工协作。

  • 有利于组件重用

7.2 三层架构

三层架构是将我们的项目分成了三个层面,分别是 表现层业务逻辑层数据访问层

image-20210818164301154
  • 数据访问层:对数据库的CRUD基本操作
  • 业务逻辑层:对业务逻辑进行封装,组合数据访问层层中基本功能,形成复杂的业务逻辑功能。例如 注册业务功能 ,我们会先调用 数据访问层selectByName() 方法判断该用户名是否存在,如果不存在再调用 数据访问层insert() 方法进行数据的添加操作
  • 表现层:接收请求,封装数据,调用业务逻辑层,响应数据

而整个流程是,浏览器发送请求,表现层的Servlet接收请求并调用业务逻辑层的方法进行业务逻辑处理,而业务逻辑层方法调用数据访问层方法进行数据的操作,依次返回到serlvet,然后servlet将数据交由 JSP 进行展示。

三层架构的每一层都有特有的包名称:

  • 表现层: com.itheima.controller 或者 com.itheima.web
  • 业务逻辑层:com.itheima.service
  • 数据访问层:com.itheima.dao 或者 com.itheima.mapper

后期我们还会学习一些框架,不同的框架是对不同层进行封装的

image-20210818165439826

7.3 MVC 和 三层架构

通过 MVC 和 三层架构 的学习,有些人肯定混淆了。那他们有什么区别和联系?

image-20210818165808589

如上图上半部分是 MVC 模式,上图下半部分是三层架构。 MVC 模式 中的 C(控制器)和 V(视图)就是 三层架构 中的表现层,而 MVC 模式 中的 M(模型)就是 三层架构 中的 业务逻辑层 和 数据访问层。

可以将 MVC 模式 理解成是一个大的概念,而 三层架构 是对 MVC 模式 实现架构的思想。 那么我们以后按照要求将不同层的代码写在不同的包下,每一层里功能职责做到单一,将来如果将表现层的技术换掉,而业务逻辑层和数据访问层的代码不需要发生变化。

会话技术

今日目标

  • 理解什么是会话跟踪技术

  • 掌握Cookie的使用

  • 掌握Session的使用

  • 完善用户登录注册案例的功能

1,会话跟踪技术的概述

对于会话跟踪这四个词,我们需要拆开来进行解释,首先要理解什么是会话,然后再去理解什么是会话跟踪:

  • 会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含==多次==请求和响应。

    • 从浏览器发出请求到服务端响应数据给前端之后,一次会话(在浏览器和服务器之间)就被建立了
    • 会话被建立后,如果浏览器或服务端都没有被关闭,则会话就会持续建立着
    • 浏览器和服务器就可以继续使用该会话进行请求发送和响应,上述的整个过程就被称之为==会话==。

    用实际场景来理解下会话,比如在我们访问京东的时候,当打开浏览器进入京东首页后,浏览器和京东的服务器之间就建立了一次会话,后面的搜索商品,查看商品的详情,加入购物车等都是在这一次会话中完成。

    思考:下图中总共建立了几个会话?

    1629382713180

    每个浏览器都会与服务端建立了一个会话,加起来总共是==3==个会话。

  • 会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间==共享数据==。

    • 服务器会收到多个请求,这多个请求可能来自多个浏览器,如上图中的6个请求来自3个浏览器
    • 服务器需要用来识别请求是否来自同一个浏览器
    • 服务器用来识别浏览器的过程,这个过程就是==会话跟踪==
    • 服务器识别浏览器后就可以在同一个会话中多次请求之间来共享数据

    那么我们又有一个问题需要思考,一个会话中的多次请求为什么要共享数据呢?有了这个数据共享功能后能实现哪些功能呢?

    • 购物车: 加入购物车去购物车结算是两次请求,但是后面这次请求要想展示前一次请求所添加的商品,就需要用到数据共享。

      1629383655260

    • 页面展示用户登录信息:很多网站,登录后访问多个功能发送多次请求后,浏览器上都会有当前登录用户的信息[用户名],比如百度、京东、码云等。

      1629383767654

    • 网站登录页面的记住我功能:当用户登录成功后,勾选记住我按钮后下次再登录的时候,网站就会自动填充用户名和密码,简化用户的登录操作,多次登录就会有多次请求,他们之间也涉及到共享数据

      1629383921990

    • 登录页面的验证码功能:生成验证码和输入验证码点击注册这也是两次请求,这两次请求的数据之间要进行对比,相同则允许注册,不同则拒绝注册,该功能的实现也需要在同一次会话中共享数据。

      1629384004179

通过这几个例子的讲解,相信大家对会话追踪技术已经有了一定的理解,该技术在实际开发中也非常重要。那么接下来我们就需要去学习下会话跟踪技术,在学习这些技术之前,我们需要思考:为什么现在浏览器和服务器不支持数据共享呢?

  • 浏览器和服务器之间使用的是HTTP请求来进行数据传输
  • HTTP协议是==无状态==的,每次浏览器向服务器请求时,服务器都会将该请求视为==新的==请求
  • HTTP协议设计成无状态的目的是让每次请求之间相互独立,互不影响
  • 请求与请求之间独立后,就无法实现多次请求之间的数据共享

分析完具体的原因后,那么该如何实现会话跟踪技术呢? 具体的实现方式有:

(1)客户端会话跟踪技术:==Cookie==

(2)服务端会话跟踪技术:==Session==

这两个技术都可以实现会话跟踪,它们之间最大的区别:==Cookie是存储在浏览器端而Session是存储在服务器端==

具体的学习思路为:

  • CooKie的基本使用、原理、使用细节
  • Session的基本使用、原理、使用细节
  • Cookie和Session的综合案例

小结

在这节中,我们主要介绍了下什么是会话和会话跟踪技术,需要注意的是:

  • HTTP协议是无状态的,靠HTTP协议是无法实现会话跟踪
  • 想要实现会话跟踪,就需要用到Cookie和Session

这个Cookie和Session具体该如何使用,接下来就先从Cookie来学起。

2,Cookie

学习Cookie,我们主要解决下面几个问题:

  • 什么是Cookie?
  • Cookie如何来使用?
  • Cookie是如何实现的?
  • Cookie的使用注意事项有哪些?

2.1 Cookie的基本使用

1.概念

==Cookie==:客户端会话技术,将数据保存到客户端,以后每次请求都携带Cookie数据进行访问。

2.Cookie的工作流程

1629386230207

  • 服务端提供了两个Servlet,分别是ServletA和ServletB
  • 浏览器发送HTTP请求1给服务端,服务端ServletA接收请求并进行业务处理
  • 服务端ServletA在处理的过程中可以创建一个Cookie对象并将name=zs的数据存入Cookie
  • 服务端ServletA在响应数据的时候,会把Cookie对象响应给浏览器
  • 浏览器接收到响应数据,会把Cookie对象中的数据存储在浏览器内存中,此时浏览器和服务端就==建立了一次会话==
  • ==在同一次会话==中浏览器再次发送HTTP请求2给服务端ServletB,浏览器会携带Cookie对象中的所有数据
  • ServletB接收到请求和数据后,就可以获取到存储在Cookie对象中的数据,这样同一个会话中的多次请求之间就实现了数据共享

3.Cookie的基本使用

对于Cookie的使用,我们更关注的应该是后台代码如何操作Cookie,对于Cookie的操作主要分两大类,本别是==发送Cookie==和==获取Cookie==,对于上面这两块内容,分别该如何实现呢?

3.1 发送Cookie

  • 创建Cookie对象,并设置数据
1
Cookie cookie = new Cookie("key","value");
  • 发送Cookie到客户端:使用==response==对象
1
response.addCookie(cookie);

介绍完发送Cookie对应的步骤后,接下面通过一个案例来完成Cookie的发送,具体实现步骤为:

需求:在Servlet中生成Cookie对象并存入数据,然后将数据发送给浏览器

1.创建Maven项目,项目名称为cookie-demo,并在pom.xml添加依赖

2.编写Servlet类,名称为AServlet

3.在AServlet中创建Cookie对象,存入数据,发送给前端

4.启动测试,在浏览器查看Cookie对象中的值

(1)创建Maven项目cookie-demo,并在pom.xml添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
<!--servlet-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!--jsp-->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<!--jstl-->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
</plugin>
</plugins>
</build>

(2)编写Servlet类,名称为AServlet

1
2
3
4
5
6
7
8
9
10
11
12
@WebServlet("/aServlet")
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

}

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

(3)在Servlet中创建Cookie对象,存入数据,发送给前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@WebServlet("/aServlet")
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//发送Cookie
//1. 创建Cookie对象
Cookie cookie = new Cookie("username","zs");
//2. 发送Cookie,response
response.addCookie(cookie);
}

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

(4)启动测试,在浏览器查看Cookie对象中的值

访问http://localhost:8080/cookie-demo/aServlet

chrome浏览器查看Cookie的值,有两种方式,分布式:

方式一:

1629389317463

方式二:选中打开开发者工具或者 使用快捷键F12 或者 Ctrl+Shift+I

1629390237936

3.2 获取Cookie

  • 获取客户端携带的所有Cookie,使用==request==对象
1
Cookie[] cookies = request.getCookies();
  • 遍历数组,获取每一个Cookie对象:for
  • 使用Cookie对象方法获取数据
1
2
cookie.getName();
cookie.getValue();

介绍完获取Cookie对应的步骤后,接下面再通过一个案例来完成Cookie的获取,具体实现步骤为:

需求:在Servlet中获取前一个案例存入在Cookie对象中的数据

1.编写一个新Servlet类,名称为BServlet

2.在BServlet中使用request对象获取Cookie数组,遍历数组,从数据中获取指定名称对应的值

3.启动测试,在控制台打印出获取的值

(1)编写一个新Servlet类,名称为BServlet

1
2
3
4
5
6
7
8
9
10
11
12
@WebServlet("/bServlet")
public class BServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

}

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

(2)在BServlet中使用request对象获取Cookie数组,遍历数组,从数据中获取指定名称对应的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@WebServlet("/bServlet")
public class BServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取Cookie
//1. 获取Cookie数组
Cookie[] cookies = request.getCookies();
//2. 遍历数组
for (Cookie cookie : cookies) {
//3. 获取数据
String name = cookie.getName();
if("username".equals(name)){
String value = cookie.getValue();
System.out.println(name+":"+value);
break;
}
}

}

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

(3)启动测试,在控制台打印出获取的值

访问http://localhost:8080/cookie-demo/bServlet

1629391020081

在IDEA控制台就能看到输出的结果:

1629391061140

==思考:==测试的时候

  • 在访问AServlet和BServlet的中间把关闭浏览器,重启浏览器后访问BServlet能否获取到Cookie中的数据?

这个问题,我们会在Cookie的使用细节中讲,大家可以动手先试下。

小结

在这节中,我们主要讲解了Cookie的基本使用,包含两部分内容

  • 发送Cookie:
    • 创建Cookie对象,并设置值:Cookie cookie = new Cookie(“key”,”value”);
    • 发送Cookie到客户端使用的是Reponse对象:response.addCookie(cookie);
  • 获取Cookie:
    • 使用Request对象获取Cookie数组:Cookie[] cookies = request.getCookies();
    • 遍历数组
    • 获取数组中每个Cookie对象的值:cookie.getName()和cookie.getValue()

介绍完Cookie的基本使用之后,那么Cookie的底层到底是如何实现一次会话两次请求之间的数据共享呢?

2.2 Cookie的原理分析

对于Cookie的实现原理是基于HTTP协议的,其中设计到HTTP协议中的两个请求头信息:

  • 响应头:set-cookie
  • 请求头: cookie

1629393289338

  • 前面的案例中已经能够实现,AServlet给前端发送Cookie,BServlet从request中获取Cookie的功能
  • 对于AServlet响应数据的时候,Tomcat服务器都是基于HTTP协议来响应数据
  • 当Tomcat发现后端要返回的是一个Cookie对象之后,Tomcat就会在响应头中添加一行数据==Set-Cookie:username=zs==
  • 浏览器获取到响应结果后,从响应头中就可以获取到Set-Cookie对应值username=zs,并将数据存储在浏览器的内存中
  • 浏览器再次发送请求给BServlet的时候,浏览器会自动在请求头中添加==Cookie: username=zs==发送给服务端BServlet
  • Request对象会把请求头中cookie对应的值封装成一个个Cookie对象,最终形成一个数组
  • BServlet通过Request对象获取到Cookie[]后,就可以从中获取自己需要的数据

接下来,使用刚才的案例,把上述结论验证下:

(1)访问AServlet对应的地址http://localhost:8080/cookie-demo/aServlet

使用Chrom浏览器打开开发者工具(F12或Crtl+Shift+I)进行查看==响应头==中的数据

1629393428733

(2)访问BServlet对应的地址`http://localhost:8080/cookie-demo/bServlet

使用Chrom浏览器打开开发者工具(F12或Crtl+Shift+I)进行查看==请求头==中的数据

1629393578667

2.3 Cookie的使用细节

在这节我们主要讲解两个知识,第一个是Cookie的存活时间,第二个是Cookie如何存储中文,首先来学习下Cookie的存活时间。

2.3.1 Cookie的存活时间

前面让大家思考过一个问题:

1629423321737

(1)浏览器发送请求给AServlet,AServlet会响应一个存有usernanme=zs的Cookie对象给浏览器

(2)浏览器接收到响应数据将cookie存入到浏览器内存中

(3)当浏览器再次发送请求给BServlet,BServlet就可以使用Request对象获取到Cookie数据

(4)在发送请求到BServlet之前,如果把浏览器关闭再打开进行访问,BServlet能否获取到Cookie数据?

==注意:浏览器关闭再打开不是指打开一个新的选显卡,而且必须是先关闭再打开,顺序不能变。==

针对上面这个问题,通过演示,会发现,BServlet中无法再获取到Cookie数据,这是为什么呢?

  • 默认情况下,Cookie存储在浏览器内存中,当浏览器关闭,内存释放,则Cookie被销毁

这个结论就印证了上面的演示效果,但是如果使用这种默认情况下的Cookie,有些需求就无法实现,比如:

1629423629887

上面这个网站的登录页面上有一个记住我的功能,这个功能大家都比较熟悉

  • 第一次输入用户名和密码并勾选记住我然后进行登录
  • 下次再登陆的时候,用户名和密码就会被自动填充,不需要再重新输入登录
  • 比如记住我这个功能需要记住用户名和密码一个星期,那么使用默认情况下的Cookie就会出现问题
  • 因为默认情况,浏览器一关,Cookie就会从浏览器内存中删除,对于记住我功能就无法实现

所以我们现在就遇到一个难题是如何将Cookie持久化存储?

Cookie其实已经为我们提供好了对应的API来完成这件事,这个API就是==setMaxAge==,

  • 设置Cookie存活时间
1
setMaxAge(int seconds)

参数值为:

1.正数:将Cookie写入浏览器所在电脑的硬盘,持久化存储。到时间自动删除

2.负数:默认值,Cookie在当前浏览器内存中,当浏览器关闭,则Cookie被销毁

3.零:删除对应Cookie

接下来,咱们就在AServlet中去设置Cookie的存活时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@WebServlet("/aServlet")
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//发送Cookie
//1. 创建Cookie对象
Cookie cookie = new Cookie("username","zs");
//设置存活时间 ,1周 7天
cookie.setMaxAge(60*60*24*7); //易阅读,需程序计算
//cookie.setMaxAge(604800); //不易阅读(可以使用注解弥补),程序少进行一次计算
//2. 发送Cookie,response
response.addCookie(cookie);
}

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

修改完代码后,启动测试,访问http://localhost:8080/cookie-demo/aServlet

  • 访问一个AServlet后,把浏览器关闭重启后,再去访问http://localhost:8080/cookie-demo/bServet,能在控制台打印出username:zs,说明Cookie没有随着浏览器关闭而被销毁
  • 通过浏览器查看Cookie的内容,会发现Cookie的相关信息

1629424844041

2.3.2 Cookie存储中文

首先,先来演示一个效果,将之前username=zs的值改成username=张三,把汉字张三存入到Cookie中,看是什么效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@WebServlet("/aServlet")
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//发送Cookie
String value = "张三";
Cookie cookie = new Cookie("username",value);
//设置存活时间 ,1周 7天
cookie.setMaxAge(60*60*24*7);
//2. 发送Cookie,response
response.addCookie(cookie);
}

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

启动访问测试,访问http://localhost:8080/cookie-demo/aServlet会发现浏览器会提示错误信息

1629425945465

通过上面的案例演示,我们得到一个结论:

  • Cookie不能直接存储中文

Cookie不能存储中文,但是如果有这方面的需求,这个时候该如何解决呢?

这个时候,我们可以使用之前学过的一个知识点叫URL编码,所以如果需要存储中文,就需要进行转码,具体的实现思路为:

1.在AServlet中对中文进行URL编码,采用URLEncoder.encode(),将编码后的值存入Cookie中

2.在BServlet中获取Cookie中的值,获取的值为URL编码后的值

3.将获取的值在进行URL解码,采用URLDecoder.decode(),就可以获取到对应的中文值

(1)在AServlet中对中文进行URL编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@WebServlet("/aServlet")
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//发送Cookie
String value = "张三";
//对中文进行URL编码
value = URLEncoder.encode(value, "UTF-8");
System.out.println("存储数据:"+value);
//将编码后的值存入Cookie中
Cookie cookie = new Cookie("username",value);
//设置存活时间 ,1周 7天
cookie.setMaxAge(60*60*24*7);
//2. 发送Cookie,response
response.addCookie(cookie);
}

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

(2)在BServlet中获取值,并对值进行解码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@WebServlet("/bServlet")
public class BServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取Cookie
//1. 获取Cookie数组
Cookie[] cookies = request.getCookies();
//2. 遍历数组
for (Cookie cookie : cookies) {
//3. 获取数据
String name = cookie.getName();
if("username".equals(name)){
String value = cookie.getValue();//获取的是URL编码后的值 %E5%BC%A0%E4%B8%89
//URL解码
value = URLDecoder.decode(value,"UTF-8");
System.out.println(name+":"+value);//value解码后为 张三
break;
}
}

}

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

至此,我们就可以将中文存入Cookie中进行使用。

小结

Cookie的使用细节中,我们讲了Cookie的存活时间存储中文:

  • 存活时间,需要掌握setMaxAage()API的使用

  • 存储中文,需要掌握URL编码和解码的使用

3,Session

Cookie已经能完成一次会话多次请求之间的数据共享,之前我们还提到过Session也可以实现,那么:

  • 什么是Session?
  • Session如何来使用?
  • Session是如何实现的?
  • Session的使用注意事项有哪些?

3.1 Session的基本使用

1.概念

==Session==:服务端会话跟踪技术:将数据保存到服务端。

  • Session是存储在服务端而Cookie是存储在客户端
  • 存储在客户端的数据容易被窃取和截获,存在很多不安全的因素
  • 存储在服务端的数据相比于客户端来说就更安全

2.Session的工作流程

1629427173389

  • 在服务端的AServlet获取一个Session对象,把数据存入其中
  • 在服务端的BServlet获取到相同的Session对象,从中取出数据
  • 就可以实现一次会话中多次请求之间的数据共享了
  • 现在最大的问题是如何保证AServlet和BServlet使用的是同一个Session对象(在原理分析会讲解)?

3.Session的基本使用

在JavaEE中提供了HttpSession接口,来实现一次会话的多次请求之间数据共享功能。

具体的使用步骤为:

  • 获取Session对象,使用的是request对象
1
HttpSession session = request.getSession();
  • Session对象提供的功能:

    • 存储数据到 session 域中

      1
      void setAttribute(String name, Object o)
    • 根据 key,获取值

      1
      Object getAttribute(String name)
    • 根据 key,删除该键值对

      1
      void removeAttribute(String name)

介绍完Session相关的API后,接下来通过一个案例来完成对Session的使用,具体实现步骤为:

需求:在一个Servlet中往Session中存入数据,在另一个Servlet中获取Session中存入的数据

1.创建名为SessionDemo1的Servlet类

2.创建名为SessionDemo2的Servlet类

3.在SessionDemo1的方法中:获取Session对象、存储数据

4.在SessionDemo2的方法中:获取Session对象、获取数据

5.启动测试

(1)创建名为SessionDemo1的Servlet类

1
2
3
4
5
6
7
8
9
10
11
12
@WebServlet("/demo1")
public class SessionDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

}

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

(2)创建名为SessionDemo2的Servlet类

1
2
3
4
5
6
7
8
9
10
11
12
@WebServlet("/demo2")
public class SessionDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

}

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

(3)SessionDemo1:获取Session对象、存储数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@WebServlet("/demo1")
public class SessionDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//存储到Session中
//1. 获取Session对象
HttpSession session = request.getSession();
//2. 存储数据
session.setAttribute("username","zs");
}

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

(4)SessionDemo2:获取Session对象、获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/demo2")
public class SessionDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取数据,从session中
//1. 获取Session对象
HttpSession session = request.getSession();
//2. 获取数据
Object username = session.getAttribute("username");
System.out.println(username);
}

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

(5)启动测试,

  • 先访问http://localhost:8080/cookie-demo/demo1,将数据存入Session
  • 在访问http://localhost:8080/cookie-demo/demo2,从Session中获取数据
  • 查看控制台

1629428292373

通过案例的效果,能看到Session是能够在一次会话中两次请求之间共享数据。

小结

至此Session的基本使用就已经完成了,重点要掌握的是:

  • Session的获取

    1
    HttpSession session = request.getSession();
  • Session常用方法的使用

    1
    2
    void setAttribute(String name, Object o)
    Object getAttribute(String name)

    **注意:**Session中可以存储的是一个Object类型的数据,也就是说Session中可以存储任意数据类型。

介绍完Session的基本使用之后,那么Session的底层到底是如何实现一次会话两次请求之间的数据共享呢?

3.2 Session的原理分析

  • Session是基于Cookie实现的

这句话其实不太能详细的说明Session的底层实现,接下来,咱们一步步来分析下Session的具体实现原理:

(1)前提条件

1629429063101

Session要想实现一次会话多次请求之间的数据共享,就必须要保证多次请求获取Session的对象是同一个。

那么它们是一个对象么?要验证这个结论也很简单,只需要在上面案例中的两个Servlet中分别打印下Session对象

SessionDemo1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/demo1")
public class SessionDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//存储到Session中
//1. 获取Session对象
HttpSession session = request.getSession();
System.out.println(session);
//2. 存储数据
session.setAttribute("username","zs");
}

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

SessionDemo2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@WebServlet("/demo2")
public class SessionDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取数据,从session中
//1. 获取Session对象
HttpSession session = request.getSession();
System.out.println(session);
//2. 获取数据
Object username = session.getAttribute("username");
System.out.println(username);
}

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

启动测试,分别访问

http://localhost:8080/cookie-demo/demo1

http://localhost:8080/cookie-demo/demo2

1629429239409

通过打印可以得到如下结论:

  • 两个Servlet类中获取的Session对象是同一个
  • 把demo1和demo2请求刷新多次,控制台最终打印的结果都是同一个

那么问题又来了,如果新开一个浏览器,访问demo1或者demo2,打印在控制台的Session还是同一个对象么?

1629429788264

==注意:在一台电脑上演示的时候,如果是相同的浏览器必须要把浏览器全部关掉重新打开,才算新开的一个浏览器。==

当然也可以使用不同的浏览器进行测试,就不需要把之前的浏览器全部关闭。

测试的结果:如果是不同浏览器或者重新打开浏览器后,打印的Session就不一样了。

所以Session实现的也是一次会话中的多次请求之间的数据共享。

那么最主要的问题就来了,Session是如何保证在一次会话中获取的Session对象是同一个呢?

1629430754825

(1)demo1在第一次获取session对象的时候,session对象会有一个唯一的标识,假如是id:10

(2)demo1在session中存入其他数据并处理完成所有业务后,需要通过Tomcat服务器响应结果给浏览器

(3)Tomcat服务器发现业务处理中使用了session对象,就会把session的唯一标识id:10当做一个cookie,添加Set-Cookie:JESSIONID=10到响应头中,并响应给浏览器

(4)浏览器接收到响应结果后,会把响应头中的coookie数据存储到浏览器的内存中

(5)浏览器在同一会话中访问demo2的时候,会把cookie中的数据按照cookie: JESSIONID=10的格式添加到请求头中并发送给服务器Tomcat

(6)demo2获取到请求后,从请求头中就读取cookie中的JSESSIONID值为10,然后就会到服务器内存中寻找id:10的session对象,如果找到了,就直接返回该对象,如果没有则新创建一个session对象

(7)关闭打开浏览器后,因为浏览器的cookie已被销毁,所以就没有JESSIONID的数据,服务端获取到的session就是一个全新的session对象

至此,Session是基于Cookie来实现的这就话,我们就解释完了,接下来通过实例来演示下:

(1)使用chrome浏览器访问http://localhost:8080/cookie-demo/demo1,打开开发者模式(F12或Ctrl+Shift+I),查看==响应头(Response Headers)==数据:

1629430891071

(2)使用chrome浏览器再次访问http://localhost:8080/cookie-demo/demo2,查看==请求头(Request Headers)==数据:

1629431299195

小结

介绍完Session的原理,我们只需要记住

  • Session是基于Cookie来实现的

3.3 Session的使用细节

这节我们会主要讲解两个知识,第一个是Session的钝化和活化,第二个是Session的销毁,首先来学习什么是Session的钝化和活化?

3.3.1 Session钝化与活化

首先需要大家思考的问题是:

  • 服务器重启后,Session中的数据是否还在?

要想回答这个问题,我们可以先看下下面这幅图,

1629438984314

(1)服务器端AServlet和BServlet共用的session对象应该是存储在服务器的内存中

(2)服务器重新启动后,内存中的数据应该是已经被释放,对象也应该都销毁了

所以session数据应该也已经不存在了。但是如果session不存在会引发什么问题呢?

举个例子说明下,

(1)用户把需要购买的商品添加到购物车,因为要实现同一个会话多次请求数据共享,所以假设把数据存入Session对象中

(2)用户正要付钱的时候接到一个电话,付钱的动作就搁浅了

(3)正在用户打电话的时候,购物网站因为某些原因需要重启

(4)重启后session数据被销毁,购物车中的商品信息也就会随之而消失

(5)用户想再次发起支付,就会出为问题

所以说对于session的数据,我们应该做到就算服务器重启了,也应该能把数据保存下来才对。

分析了这么多,那么Tomcat服务器在重启的时候,session数据到底会不会保存以及是如何保存的,我们可以通过实际案例来演示下:

==注意:这里所说的关闭和启动应该要确保是正常的关闭和启动。==

那如何才是正常关闭Tomcat服务器呢?

需要使用命令行的方式来启动和停止Tomcat服务器:

==启动==:进入到项目pom.xml所在目录,执行tomcat7:run

1629439800328

==停止==:在启动的命令行界面,输入ctrl+c

1629439879596

有了上述两个正常启动和关闭的方式后,接下来的测试流程是:

(1)先启动Tomcat服务器

(2)访问http://localhost:8080/cookie-demo/demo1将数据存入session中

(3)正确停止Tomcat服务器

(4)再次重新启动Tomcat服务器

(5)访问http://localhost:8080/cookie-demo/demo2 查看是否能获取到session中的数据

1629440018238

经过测试,会发现只要服务器是正常关闭和启动,session中的数据是可以被保存下来的。

那么Tomcat服务器到底是如何做到的呢?

具体的原因就是:Session的钝化和活化:

  • 钝化:在服务器正常关闭后,Tomcat会自动将Session数据写入硬盘的文件中

    • 钝化的数据路径为:项目目录\target\tomcat\work\Tomcat\localhost\项目名称\SESSIONS.ser

      1629440576828

  • 活化:再次启动服务器后,从文件中加载数据到Session中

    • 数据加载到Session中后,路径中的SESSIONS.ser文件会被删除掉

对于上述的整个过程,大家只需要了解下即可。因为所有的过程都是Tomcat自己完成的,不需要我们参与。

小结

Session的钝化和活化介绍完后,需要我们注意的是:

  • session数据存储在服务端,服务器重启后,session数据会被保存

  • 浏览器被关闭启动后,重新建立的连接就已经是一个全新的会话,获取的session数据也是一个新的对象

  • session的数据要想共享,浏览器不能关闭,所以session数据不能长期保存数据

  • cookie是存储在客户端,是可以长期保存

3.3.2 Session销毁

session的销毁会有两种方式:

  • 默认情况下,无操作,30分钟自动销毁

    • 对于这个失效时间,是可以通过配置进行修改的

      • 在项目的web.xml中配置

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        <?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_3_1.xsd"
        version="3.1">

        <session-config>
        <session-timeout>100</session-timeout>
        </session-config>
        </web-app>
      • 如果没有配置,默认是30分钟,默认值是在Tomcat的web.xml配置文件中写死的

        1629441687613

  • 调用Session对象的invalidate()进行销毁

    • 在SessionDemo2类中添加session销毁的方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      @WebServlet("/demo2")
      public class SessionDemo2 extends HttpServlet {
      @Override
      protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      //获取数据,从session中

      //1. 获取Session对象
      HttpSession session = request.getSession();
      System.out.println(session);

      // 销毁
      session.invalidate();
      //2. 获取数据
      Object username = session.getAttribute("username");
      System.out.println(username);
      }

      @Override
      protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      this.doGet(request, response);
      }
      }
    • 启动访问测试,先访问demo1将数据存入到session,再次访问demo2从session中获取数据

      1629441900843

    • 该销毁方法一般会在用户退出的时候,需要将session销毁掉。

Cookie和Session小结

  • Cookie 和 Session 都是来完成一次会话内多次请求间==数据共享==的。

所需两个对象放在一块,就需要思考:

Cookie和Session的区别是什么?

Cookie和Session的应用场景分别是什么?

  • 区别:
    • 存储位置:Cookie 是将数据存储在客户端,Session 将数据存储在服务端
    • 安全性:Cookie不安全,Session安全
    • 数据大小:Cookie最大3KB,Session无大小限制
    • 存储时间:Cookie可以通过setMaxAge()长期存储,Session默认30分钟
    • 服务器性能:Cookie不占服务器资源,Session占用服务器资源
  • 应用场景:
    • 购物车:使用Cookie来存储
    • 以登录用户的名称展示:使用Session来存储
    • 记住我功能:使用Cookie来存储
    • 验证码:使用session来存储
  • 结论
    • Cookie是用来保证用户在未登录情况下的身份识别
    • Session是用来保存用户登录后的数据

介绍完Cookie和Session以后,具体用哪个还是需要根据具体的业务进行具体分析。

4,用户登录注册案例

4.1 需求分析

需求说明:

  1. 完成用户登录功能,如果用户勾选“记住用户” ,则下次访问登录页面==自动==填充用户名密码

  2. 完成注册功能,并实现==验证码==功能

1629442826981

4.2 用户登录功能

  1. 需求:

1629443152010

  • 用户登录成功后,跳转到列表页面,并在页面上展示当前登录的用户名称
  • 用户登录失败后,跳转回登录页面,并在页面上展示对应的错误信息
  1. 实现流程分析

1629443379531

(1)前端通过表单发送请求和数据给Web层的LoginServlet

(2)在LoginServlet中接收请求和数据[用户名和密码]

(3)LoginServlet接收到请求和数据后,调用Service层完成根据用户名和密码查询用户对象

(4)在Service层需要编写UserService类,在类中实现login方法,方法中调用Dao层的UserMapper

(5)在UserMapper接口中,声明一个根据用户名和密码查询用户信息的方法

(6)Dao层把数据查询出来以后,将返回数据封装到User对象,将对象交给Service层

(7)Service层将数据返回给Web层

(8)Web层获取到User对象后,判断User对象,如果为Null,则将错误信息响应给登录页面,如果不为Null,则跳转到列表页面,并把当前登录用户的信息存入Session携带到列表页面。

  1. 具体实现

(1)完成Dao层的代码编写

(1.1)将04-资料\1. 登录注册案例\2. MyBatis环境\UserMapper.java放到com.itheima.mapper`包下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public interface UserMapper {
/**
* 根据用户名和密码查询用户对象
* @param username
* @param password
* @return
*/
@Select("select * from tb_user where username = #{username} and password = #{password}")
User select(@Param("username") String username,@Param("password") String password);

/**
* 根据用户名查询用户对象
* @param username
* @return
*/
@Select("select * from tb_user where username = #{username}")
User selectByUsername(String username);

/**
* 添加用户
* @param user
*/
@Insert("insert into tb_user values(null,#{username},#{password})")
void add(User user);
}

(1.2)将04-资料\1. 登录注册案例\2. MyBatis环境\User.java放到com.itheima.pojo包下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class User {

private Integer id;
private String username;
private String password;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}

(1.3)将04-资料\1. 登录注册案例\2. MyBatis环境\UserMapper.xml放入到resources/com/itheima/mapper`目录下:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.UserMapper">

</mapper>

(2)完成Service层的代码编写

(2.1)在com.itheima.service包下,创建UserService类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class UserService {
//1.使用工具类获取SqlSessionFactory
SqlSessionFactory factory = SqlSessionFactoryUtils.getSqlSessionFactory();
/**
* 登录方法
* @param username
* @param password
* @return
*/
public User login(String username,String password){
//2. 获取SqlSession
SqlSession sqlSession = factory.openSession();
//3. 获取UserMapper
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//4. 调用方法
User user = mapper.select(username, password);
//释放资源
sqlSession.close();

return user;
}
}

(3)完成页面和Web层的代码编写

(3.1)将04-资料\1. 登录注册案例\1. 静态页面拷贝到项目的webapp目录下:

1629444649629

(3.2)将login.html内容修改成login.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>login</title>
<link href="css/login.css" rel="stylesheet">
</head>

<body>
<div id="loginDiv" style="height: 350px">
<form action="/brand-demo/loginServlet" method="post" id="form">
<h1 id="loginMsg">LOGIN IN</h1>
<div id="errorMsg">用户名或密码不正确</div>
<p>Username:<input id="username" name="username" type="text"></p>
<p>Password:<input id="password" name="password" type="password"></p>
<p>Remember:<input id="remember" name="remember" type="checkbox"></p>
<div id="subDiv">
<input type="submit" class="button" value="login up">
<input type="reset" class="button" value="reset">&nbsp;&nbsp;&nbsp;
<a href="register.html">没有账号?</a>
</div>
</form>
</div>
</body>
</html>

(3.3)创建LoginServlet类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@WebServlet("/loginServlet")
public class LoginServlet extends HttpServlet {
private UserService service = new UserService();

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 获取用户名和密码
String username = request.getParameter("username");
String password = request.getParameter("password");

//2. 调用service查询
User user = service.login(username, password);

//3. 判断
if(user != null){
//登录成功,跳转到查询所有的BrandServlet

//将登陆成功后的user对象,存储到session
HttpSession session = request.getSession();
session.setAttribute("user",user);

String contextPath = request.getContextPath();
response.sendRedirect(contextPath+"/selectAllServlet");
}else {
// 登录失败,
// 存储错误信息到request
request.setAttribute("login_msg","用户名或密码错误");
// 跳转到login.jsp
request.getRequestDispatcher("/login.jsp").forward(request,response);

}
}

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

(3.4)在brand.jsp中标签下添加欢迎当前用户的提示信息:

1
<h1>${user.username},欢迎您</h1>

(3.5) 修改login.jsp,将错误信息使用EL表达式来获取

1
2
修改前内容:<div id="errorMsg">用户名或密码不正确</div>
修改后内容: <div id="errorMsg">${login_msg}</div>

(4)启动,访问测试

(4.1) 进入登录页面,输入错误的用户名或密码

1629445376407

(4.2)输入正确的用户和密码信息

1629445415216

小结

  • 在LoginServlet中,将登录成功的用户数据存入session中,方法在列表页面中获取当前登录用户信息进行展示
  • 在LoginServlet中,将登录失败的错误信息存入到request中,如果存入到session中就会出现这次会话的所有请求都有登录失败的错误信息,这个是不需要的,所以不用存入到session中

4.3 记住我-设置Cookie

  1. 需求:

如果用户勾选“记住用户” ,则下次访问登陆页面自动填充用户名密码。这样可以提升用户的体验。

1629445835281

对应上面这个需求,最大的问题就是: 如何自动填充用户名和密码?

  1. 实现流程分析

因为记住我功能要实现的效果是,就算用户把浏览器关闭过几天再来访问也能自动填充,所以需要将登陆信息存入一个可以长久保存,并且能够在浏览器关闭重新启动后依然有效的地方,就是我们前面讲的==Cookie==,所以:

  • 将用户名和密码写入==Cookie==中,并且持久化存储Cookie,下次访问浏览器会自动携带Cookie

  • 在页面获取Cookie数据后,设置到用户名和密码框中

  • 何时写入Cookie?

    • 用户必须登陆成功后才需要写
    • 用户必须在登录页面勾选了记住我的复选框

1629446248511

(1)前端需要在发送请求和数据的时候,多携带一个用户是否勾选Remember的数据

(2)LoginServlet获取到数据后,调用Service完成用户名和密码的判定

(3)登录成功,并且用户在前端勾选了记住我,需要往Cookie中写入用户名和密码的数据,并设置Cookie存活时间

(4)设置成功后,将数据响应给前端

  1. 具体实现

(1)在login.jsp为复选框设置值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>login</title>
<link href="css/login.css" rel="stylesheet">
</head>

<body>
<div id="loginDiv" style="height: 350px">
<form action="/brand-demo/loginServlet" method="post" id="form">
<h1 id="loginMsg">LOGIN IN</h1>
<div id="errorMsg">${login_msg}</div>
<p>Username:<input id="username" name="username" type="text"></p>
<p>Password:<input id="password" name="password" type="password"></p>
<p>Remember:<input id="remember" name="remember" value="1" type="checkbox"></p>
<div id="subDiv">
<input type="submit" class="button" value="login up">
<input type="reset" class="button" value="reset">&nbsp;&nbsp;&nbsp;
<a href="register.html">没有账号?</a>
</div>
</form>
</div>
</body>
</html>

(2)在LoginServlet获取复选框的值并在登录成功后进行设置Cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@WebServlet("/loginServlet")
public class LoginServlet extends HttpServlet {
private UserService service = new UserService();

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 获取用户名和密码
String username = request.getParameter("username");
String password = request.getParameter("password");

//获取复选框数据
String remember = request.getParameter("remember");

//2. 调用service查询
User user = service.login(username, password);

//3. 判断
if(user != null){
//登录成功,跳转到查询所有的BrandServlet

//判断用户是否勾选记住我,字符串写前面是为了避免出现空指针异常
if("1".equals(remember)){
//勾选了,发送Cookie
//1. 创建Cookie对象
Cookie c_username = new Cookie("username",username);
Cookie c_password = new Cookie("password",password);
// 设置Cookie的存活时间
c_username.setMaxAge( 60 * 60 * 24 * 7);
c_password.setMaxAge( 60 * 60 * 24 * 7);
//2. 发送
response.addCookie(c_username);
response.addCookie(c_password);
}

//将登陆成功后的user对象,存储到session
HttpSession session = request.getSession();
session.setAttribute("user",user);

String contextPath = request.getContextPath();
response.sendRedirect(contextPath+"/selectAllServlet");
}else {
// 登录失败,

// 存储错误信息到request
request.setAttribute("login_msg","用户名或密码错误");

// 跳转到login.jsp
request.getRequestDispatcher("/login.jsp").forward(request,response);

}
}

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

(3)启动访问测试,

只有当前用户名和密码输入正确,并且勾选了Remeber的复选框,在响应头中才可以看得cookie的相关数据

1629447232217

4.4 记住我-获取Cookie

  1. 需求

登录成功并勾选了Remeber后,后端返回给前端的Cookie数据就已经存储好了,接下来就需要在页面获取Cookie中的数据,并把数据设置到登录页面的用户名和密码框中。

1629449100282

如何在页面直接获取Cookie中的值呢?

  1. 实现流程分析

在页面可以使用EL表达式,${cookie.==key==.value}

key:指的是存储在cookie中的键名称

1629449234735

(1)在login.jsp用户名的表单输入框使用value值给表单元素添加默认值,value可以使用${cookie.username.value}

(2)在login.jsp密码的表单输入框使用value值给表单元素添加默认值,value可以使用${cookie.password.value}

  1. 具体实现

(1)修改login.jsp页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>login</title>
<link href="css/login.css" rel="stylesheet">
</head>

<body>
<div id="loginDiv" style="height: 350px">
<form action="/brand-demo/loginServlet" method="post" id="form">
<h1 id="loginMsg">LOGIN IN</h1>
<div id="errorMsg">${login_msg}</div>
<p>Username:<input id="username" name="username" value="${cookie.username.value}" type="text"></p>

<p>Password:<input id="password" name="password" value="${cookie.password.value}" type="password"></p>
<p>Remember:<input id="remember" name="remember" value="1" type="checkbox"></p>
<div id="subDiv">
<input type="submit" class="button" value="login up">
<input type="reset" class="button" value="reset">&nbsp;&nbsp;&nbsp;
<a href="register.html">没有账号?</a>
</div>
</form>
</div>
</body>
</html>
  1. 访问测试,重新访问登录页面,就可以看得用户和密码已经被填充。

1629449530886

4.5 用户注册功能

  1. 需求
  • 注册功能:保存用户信息到数据库
  • 验证码功能
    • 展示验证码:展示验证码图片,并可以点击切换
    • 校验验证码:验证码填写不正确,则注册失败

1629449648793

  1. 实现流程分析

1629449720005

(1)前端通过表单发送请求和数据给Web层的RegisterServlet

(2)在RegisterServlet中接收请求和数据[用户名和密码]

(3)RegisterServlet接收到请求和数据后,调用Service层完成用户信息的保存

(4)在Service层需要编写UserService类,在类中实现register方法,需要判断用户是否已经存在,如果不存在,则完成用户数据的保存

(5)在UserMapper接口中,声明两个方法,一个是根据用户名查询用户信息方法,另一个是保存用户信息方法

(6)在UserService类中保存成功则返回true,失败则返回false,将数据返回给Web层

(7)Web层获取到结果后,如果返回的是true,则提示注册成功,并转发到登录页面,如果返回false则提示用户名已存在并转发到注册页面

  1. 具体实现

(1)Dao层代码参考资料中的内容完成

(2)编写Service层代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class UserService {
//1.使用工具类获取SqlSessionFactory
SqlSessionFactory factory = SqlSessionFactoryUtils.getSqlSessionFactory();
/**
* 注册方法
* @return
*/

public boolean register(User user){
//2. 获取SqlSession
SqlSession sqlSession = factory.openSession();
//3. 获取UserMapper
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//4. 判断用户名是否存在
User u = mapper.selectByUsername(user.getUsername());

if(u == null){
// 用户名不存在,注册
mapper.add(user);
sqlSession.commit();
}
sqlSession.close();

return u == null;

}
}

(3)完成页面和Web层的代码编写

(3.1)将register.html内容修改成register.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>欢迎注册</title>
<link href="css/register.css" rel="stylesheet">
</head>
<body>
<div class="form-div">
<div class="reg-content">
<h1>欢迎注册</h1>
<span>已有帐号?</span> <a href="login.html">登录</a>
</div>
<form id="reg-form" action="/brand-demo/registerServlet" method="post">
<table>
<tr>
<td>用户名</td>
<td class="inputs">
<input name="username" type="text" id="username">
<br>
<span id="username_err" class="err_msg" style="display:none">用户名不太受欢迎</span>
</td>
</tr>
<tr>
<td>密码</td>
<td class="inputs">
<input name="password" type="password" id="password">
<br>
<span id="password_err" class="err_msg" style="display: none">密码格式有误</span>
</td>
</tr>
<tr>
<td>验证码</td>
<td class="inputs">
<input name="checkCode" type="text" id="checkCode">
<img src="imgs/a.jpg">
<a href="#" id="changeImg" >看不清?</a>
</td>
</tr>
</table>
<div class="buttons">
<input value="注 册" type="submit" id="reg_btn">
</div>
<br class="clear">
</form>
</div>
</body>
</html>

(3.2)编写RegisterServlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@WebServlet("/registerServlet")
public class RegisterServlet extends HttpServlet {
private UserService service = new UserService();

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 获取用户名和密码数据
String username = request.getParameter("username");
String password = request.getParameter("password");

User user = new User();
user.setUsername(username);
user.setPassword(password);

//2. 调用service 注册
boolean flag = service.register(user);
//3. 判断注册成功与否
if(flag){
//注册功能,跳转登陆页面
request.setAttribute("register_msg","注册成功,请登录");
request.getRequestDispatcher("/login.jsp").forward(request,response);
}else {
//注册失败,跳转到注册页面

request.setAttribute("register_msg","用户名已存在");
request.getRequestDispatcher("/register.jsp").forward(request,response);
}


}

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

(3.3)需要在页面上展示后台返回的错误信息,需要修改register.jsp

1
2
修改前:<span id="username_err" class="err_msg" style="display:none">用户名不太受欢迎</span>
修改后:<span id="username_err" class="err_msg">${register_msg}</span>

(3.4)如果注册成功,需要把成功信息展示在登录页面,所以也需要修改login.jsp

1
2
修改前:<div id="errorMsg">${login_msg}</div>
修改后:<div id="errorMsg">${login_msg} ${register_msg}</div>

(3.5)修改login.jsp,将注册跳转地址修改为register.jsp

1
2
修改前:<a href="register.html">没有账号?</a>
修改后: <a href="register.jsp">没有账号?</a>

(3.6)启动测试,

如果是注册的用户信息已经存在:

1629451535605

如果注册的用户信息不存在,注册成功:

1629451567428

4.6 验证码-展示

  1. 需求分析

展示验证码:展示验证码图片,并可以点击切换

1629451646831

验证码的生成是通过工具类来实现的,具体的工具类参考

04-资料\1. 登录注册案例\CheckCodeUtil.java

在该工具类中编写main方法进行测试:

1
2
3
4
5
6
7
8
public static void main(String[] args) throws IOException {
//生成验证码的图片位置
OutputStream fos = new FileOutputStream("d://a.jpg");
//checkCode为最终验证码的数据
String checkCode = CheckCodeUtil.outputVerifyImage(100, 50, fos, 4);
System.out.println(checkCode);
}

生成完验证码以后,我们就可以知晓:

  • 验证码就是使用Java代码生成的一张图片
  • 验证码的作用:防止机器自动注册,攻击服务器
  1. 实现流程分析

1629452623289

(1)前端发送请求给CheckCodeServlet

(2)CheckCodeServlet接收到请求后,生成验证码图片,将图片用Reponse对象的输出流写回到前端

思考:如何将图片写回到前端浏览器呢?

(1)Java中已经有工具类生成验证码图片,测试类中只是把图片生成到磁盘上
(2)生成磁盘的过程中使用的是OutputStream流,如何把这个图片生成在页面呢?
(3)前面在将Reponse对象的时候,它有一个方法可以获取其字节输出流,getOutputStream()
(4)综上所述,我们可以把写往磁盘的流对象更好成Response的字节流,即可完成图片响应给前端

  1. 具体实现

(1)修改Register.jsp页面,将验证码的图片从后台获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<tr>
<td>验证码</td>
<td class="inputs">
<input name="checkCode" type="text" id="checkCode">
<img id="checkCodeImg" src="/brand-demo/checkCodeServlet">
<a href="#" id="changeImg" >看不清?</a>
</td>
</tr>

<script>
document.getElementById("changeImg").onclick = function () {
//路径后面添加时间戳的目的是避免浏览器进行缓存静态资源
document.getElementById("checkCodeImg").src = "/brand-demo/checkCodeServlet?"+new Date().getMilliseconds();
}
</script>

(2)编写CheckCodeServlet类,用来接收请求生成验证码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@WebServlet("/checkCodeServlet")
public class CheckCodeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 生成验证码
ServletOutputStream os = response.getOutputStream();
String checkCode = CheckCodeUtil.outputVerifyImage(100, 50, os, 4);
}

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

4.7验证码-校验

  1. 需求
  • 判断程序生成的验证码 和 用户输入的验证码 是否一样,如果不一样,则阻止注册
  • 验证码图片访问和提交注册表单是==两次==请求,所以要将程序生成的验证码存入Session中

1629452835571

思考:为什么要把验证码数据存入到Session中呢?

  • 生成验证码和校验验证码是两次请求,此处就需要在一个会话的两次请求之间共享数据
  • 验证码属于安全数据类的,所以我们选中Session来存储验证码数据。
  1. 实现流程分析

1629452966499

(1)在CheckCodeServlet中生成验证码的时候,将验证码数据存入Session对象

(2)前端将验证码和注册数据提交到后台,交给RegisterServlet类

(3)RegisterServlet类接收到请求和数据后,其中就有验证码,和Session中的验证码进行对比

(4)如果一致,则完成注册,如果不一致,则提示错误信息

  1. 具体实现

(1)修改CheckCodeServlet类,将验证码存入Session对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@WebServlet("/checkCodeServlet")
public class CheckCodeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

// 生成验证码
ServletOutputStream os = response.getOutputStream();
String checkCode = CheckCodeUtil.outputVerifyImage(100, 50, os, 4);

// 存入Session
HttpSession session = request.getSession();
session.setAttribute("checkCodeGen",checkCode);


}

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

(2)在RegisterServlet中,获取页面的和session对象中的验证码,进行对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.itheima.web;

import com.itheima.pojo.User;
import com.itheima.service.UserService;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;

@WebServlet("/registerServlet")
public class RegisterServlet extends HttpServlet {
private UserService service = new UserService();

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 获取用户名和密码数据
String username = request.getParameter("username");
String password = request.getParameter("password");

User user = new User();
user.setUsername(username);
user.setPassword(password);

// 获取用户输入的验证码
String checkCode = request.getParameter("checkCode");

// 程序生成的验证码,从Session获取
HttpSession session = request.getSession();
String checkCodeGen = (String) session.getAttribute("checkCodeGen");

// 比对
if(!checkCodeGen.equalsIgnoreCase(checkCode)){

request.setAttribute("register_msg","验证码错误");
request.getRequestDispatcher("/register.jsp").forward(request,response);

// 不允许注册
return;
}
//2. 调用service 注册
boolean flag = service.register(user);
//3. 判断注册成功与否
if(flag){
//注册功能,跳转登陆页面

request.setAttribute("register_msg","注册成功,请登录");
request.getRequestDispatcher("/login.jsp").forward(request,response);
}else {
//注册失败,跳转到注册页面

request.setAttribute("register_msg","用户名已存在");
request.getRequestDispatcher("/register.jsp").forward(request,response);
}


}

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

至此,用户的注册登录功能就已经完成了。

Filter&Listener&Ajax

今日目标:

  • 能够使用 Filter 完成登陆状态校验功能
  • 能够使用 axios 发送 ajax 请求
  • 熟悉 json 格式,并能使用 Fastjson 完成 java 对象和 json 串的相互转换
  • 使用 axios + json 完成综合案例

1,Filter

1.1 Filter概述

Filter 表示过滤器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一。Servlet 我们之前都已经学习过了,Filter和Listener 我们今天都会进行学习。

过滤器可以把对资源的请求==拦截==下来,从而实现一些特殊的功能。

如下图所示,浏览器可以访问服务器上的所有的资源(servlet、jsp、html等)

image-20210823184519509

而在访问到这些资源之前可以使过滤器拦截来下,也就是说在访问资源之前会先经过 Filter,如下图

image-20210823184657328

拦截器拦截到后可以做什么功能呢?

==过滤器一般完成一些通用的操作。==比如每个资源都要写一些代码完成某个功能,我们总不能在每个资源中写这样的代码吧,而此时我们可以将这些代码写在过滤器中,因为请求每一个资源都要经过过滤器。

我们之前做的品牌数据管理的案例中就已经做了登陆的功能,而如果我们不登录能不能访问到数据呢?我们可以在浏览器直接访问首页 ,可以看到 查询所有 的超链接

image-20210823185720197

当我点击该按钮,居然可以看到品牌的数据

image-20210823185932418

这显然和我们的要求不符。我们希望实现的效果是用户如果登陆过了就跳转到品牌数据展示的页面;如果没有登陆就跳转到登陆页面让用户进行登陆,要实现这个效果需要在每一个资源中都写上这段逻辑,而像这种通用的操作,我们就可以放在过滤器中进行实现。这个就是==权限控制==,以后我们还会进行细粒度权限控制。过滤器还可以做 统一编码处理敏感字符处理 等等…

1.2 Filter快速入门

1.2.1 开发步骤

进行 Filter 开发分成以下三步实现

  • 定义类,实现 Filter接口,并重写其所有方法

    image-20210823191006878
  • 配置Filter拦截资源的路径:在类上定义 @WebFilter 注解。而注解的 value 属性值 /* 表示拦截所有的资源

    image-20210823191037163
  • 在doFilter方法中输出一句话,并放行

    image-20210823191200201

    上述代码中的 chain.doFilter(request,response); 就是放行,也就是让其访问本该访问的资源。

1.2.2 代码演示

创建一个项目,项目下有一个 hello.jsp 页面,项目结构如下:

image-20210823191855765

pom.xml 配置文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?xml version="1.0" encoding="UTF-8"?>
<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>

<groupId>org.example</groupId>
<artifactId>filter-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>80</port>
</configuration>
</plugin>
</plugins>
</build>
</project>

hello.jsp 页面内容如下:

1
2
3
4
5
6
7
8
9
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>hello JSP~</h1>
</body>
</html>

我们现在在浏览器输入 http://localhost/filter-demo/hello.jsp 访问 hello.jsp 页面,这里是可以访问到 hello.jsp 页面内容的。

image-20210823192353031

接下来编写过滤器。过滤器是 Web 三大组件之一,所以我们将 filter 创建在 com.itheima.web.filter 包下,起名为 FilterDemo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebFilter("/*")
public class FilterDemo implements Filter {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("FilterDemo...");
}

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void destroy() {
}
}

重启启动服务器,再次重新访问 hello.jsp 页面,这次发现页面没有任何效果,但是在 idea 的控制台可以看到如下内容

image-20210823193759365

上述效果说明 FilterDemo 这个过滤器的 doFilter() 方法执行了,但是为什么在浏览器上看不到 hello.jsp 页面的内容呢?这是因为在 doFilter() 方法中添加放行的方法才能访问到 hello.jsp 页面。那就在 doFilter() 方法中添加放行的代码

1
2
//放行
chain.doFilter(request,response);

再次重启服务器并访问 hello.jsp 页面,发现这次就可以在浏览器上看到页面效果。

FilterDemo 过滤器完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@WebFilter("/*")
public class FilterDemo implements Filter {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("1.FilterDemo...");
//放行
chain.doFilter(request,response);
}

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void destroy() {
}
}

1.3 Filter执行流程

image-20210823194830074

如上图是使用过滤器的流程,我们通过以下问题来研究过滤器的执行流程:

  • 放行后访问对应资源,资源访问完成后,还会回到Filter中吗?

    从上图就可以看出肯定 ==会== 回到Filter中

  • 如果回到Filter中,是重头执行还是执行放行后的逻辑呢?

    如果是重头执行的话,就意味着 放行前逻辑 会被执行两次,肯定不会这样设计了;所以访问完资源后,会回到 放行后逻辑,执行该部分代码。

通过上述的说明,我们就可以总结Filter的执行流程如下:

image-20210823195434581

接下来我们通过代码验证一下,在 doFilter() 方法前后都加上输出语句,如下

image-20210823195828596

同时在 hello.jsp 页面加上输出语句,如下

image-20210823200028284

执行访问该资源打印的顺序是按照我们标记的标号进行打印的话,说明我们上边总结出来的流程是没有问题的。启动服务器访问 hello.jsp 页面,在控制台打印的内容如下:

image-20210823200202153

以后我们可以将对请求进行处理的代码放在放行之前进行处理,而如果请求完资源后还要对响应的数据进行处理时可以在放行后进行逻辑处理。

1.4 Filter拦截路径配置

拦截路径表示 Filter 会对请求的哪些资源进行拦截,使用 @WebFilter 注解进行配置。如:@WebFilter("拦截路径")

拦截路径有如下四种配置方式:

  • 拦截具体的资源:/index.jsp:只有访问index.jsp时才会被拦截
  • 目录拦截:/user/*:访问/user下的所有资源,都会被拦截
  • 后缀名拦截:*.jsp:访问后缀名为jsp的资源,都会被拦截
  • 拦截所有:/*:访问所有资源,都会被拦截

通过上面拦截路径的学习,大家会发现拦截路径的配置方式和 Servlet 的请求资源路径配置方式一样,但是表示的含义不同。

1.5 过滤器链

1.5.1 概述

过滤器链是指在一个Web应用,可以配置多个过滤器,这多个过滤器称为过滤器链。

如下图就是一个过滤器链,我们学习过滤器链主要是学习过滤器链执行的流程

image-20210823215835812

上图中的过滤器链执行是按照以下流程执行:

  1. 执行 Filter1 的放行前逻辑代码
  2. 执行 Filter1 的放行代码
  3. 执行 Filter2 的放行前逻辑代码
  4. 执行 Filter2 的放行代码
  5. 访问到资源
  6. 执行 Filter2 的放行后逻辑代码
  7. 执行 Filter1 的放行后逻辑代码

以上流程串起来就像一条链子,故称之为过滤器链。

1.5.2 代码演示

  • 编写第一个过滤器 FilterDemo ,配置成拦截所有资源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @WebFilter("/*")
    public class FilterDemo implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

    //1. 放行前,对 request数据进行处理
    System.out.println("1.FilterDemo...");
    //放行
    chain.doFilter(request,response);
    //2. 放行后,对Response 数据进行处理
    System.out.println("3.FilterDemo...");
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }
    }
  • 编写第二个过滤器 FilterDemo2 ,配置炒年糕拦截所有资源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @WebFilter("/*")
    public class FilterDemo2 implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

    //1. 放行前,对 request数据进行处理
    System.out.println("2.FilterDemo...");
    //放行
    chain.doFilter(request,response);
    //2. 放行后,对Response 数据进行处理
    System.out.println("4.FilterDemo...");
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }
    }

  • 修改 hello.jsp 页面中脚本的输出语句

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
    <title>Title</title>
    </head>
    <body>
    <h1>hello JSP~</h1>
    <%
    System.out.println("3.hello jsp");
    %>
    </body>
    </html>
  • 启动服务器,在浏览器输入 http://localhost/filter-demo/hello.jsp 进行测试,在控制台打印内容如下

    image-20210823221222468

    从结果可以看到确实是按照我们之前说的执行流程进行执行的。

1.5.3 问题

上面代码中为什么是先执行 FilterDemo ,后执行 FilterDemo2 呢?

我们现在使用的是注解配置Filter,而这种配置方式的优先级是按照过滤器类名(字符串)的自然排序。

比如有如下两个名称的过滤器 : BFilterDemoAFilterDemo 。那一定是 AFilterDemo 过滤器先执行。

1.6 案例

1.6.1 需求

访问服务器资源时,需要先进行登录验证,如果没有登录,则自动跳转到登录页面

1.6.2 分析

我们要实现该功能是在每一个资源里加入登陆状态校验的代码吗?显然是不需要的,只需要写一个 Filter ,在该过滤器中进行登陆状态校验即可。而在该 Filter 中逻辑如下:

image-20210823223214525

1.6.3 代码实现

1.6.3.1 创建Filter

brand-demo 工程创建 com.itheima.web.filter 包,在该下创建名为 LoginFilter 的过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
@WebFilter("/*")
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {

}

public void init(FilterConfig config) throws ServletException {
}

public void destroy() {
}
}
1.6.3.2 编写逻辑代码

doFilter() 方法中编写登陆状态校验的逻辑代码。

我们首先需要从 session 对象中获取用户信息,但是 ServletRequest 类型的 requset 对象没有获取 session 对象的方法,所以此时需要将 request对象强转成 HttpServletRequest 对象。

1
HttpServletRequest req = (HttpServletRequest) request;

然后完成以下逻辑

  • 获取Session对象
  • 从Session对象中获取名为 user 的数据
  • 判断获取到的数据是否是 null
    • 如果不是,说明已经登陆,放行
    • 如果是,说明尚未登陆,将提示信息存储到域对象中并跳转到登陆页面

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@WebFilter("/*")
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) request;

//1. 判断session中是否有user
HttpSession session = req.getSession();
Object user = session.getAttribute("user");

//2. 判断user是否为null
if(user != null){
// 登录过了
//放行
chain.doFilter(request, response);
}else {
// 没有登陆,存储提示信息,跳转到登录页面

req.setAttribute("login_msg","您尚未登陆!");
req.getRequestDispatcher("/login.jsp").forward(req,response);
}
}

public void init(FilterConfig config) throws ServletException {
}

public void destroy() {
}
}
1.6.3.3 测试并抛出问题

在浏览器上输入 http://localhost:8080/brand-demo/ ,可以看到如下页面效果

image-20210823224843179

从上面效果可以看出没有登陆确实是跳转到登陆页面了,但是登陆页面为什么展示成这种效果了呢?

1.6.3.4 问题分析及解决

因为登陆页面需要 css/login.css 这个文件进行样式的渲染,下图是登陆页面引入的css文件图解

image-20210823225411925

而在请求这个css资源时被过滤器拦截,就相当于没有加载到样式文件导致的。解决这个问题,只需要对所以的登陆相关的资源进行放行即可。还有一种情况就是当我没有用户信息时需要进行注册,而注册时也希望被过滤器放行。

综上,我们需要在判断session中是否包含用户信息之前,应该加上对登陆及注册相关资源放行的逻辑处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//判断访问资源路径是否和登录注册相关
//1,在数组中存储登陆和注册相关的资源路径
String[] urls = {"/login.jsp","/imgs/","/css/","/loginServlet","/register.jsp","/registerServlet","/checkCodeServlet"};
//2,获取当前访问的资源路径
String url = req.getRequestURL().toString();

//3,遍历数组,获取到每一个需要放行的资源路径
for (String u : urls) {
//4,判断当前访问的资源路径字符串是否包含要放行的的资源路径字符串
/*
比如当前访问的资源路径是 /brand-demo/login.jsp
而字符串 /brand-demo/login.jsp 包含了 字符串 /login.jsp ,所以这个字符串就需要放行
*/
if(url.contains(u)){
//找到了,放行
chain.doFilter(request, response);
//break;
return;
}
}
1.6.3.5 过滤器完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@WebFilter("/*")
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) request;

//判断访问资源路径是否和登录注册相关
//1,在数组中存储登陆和注册相关的资源路径
String[] urls = {"/login.jsp","/imgs/","/css/","/loginServlet","/register.jsp","/registerServlet","/checkCodeServlet"};
//2,获取当前访问的资源路径
String url = req.getRequestURL().toString();

//3,遍历数组,获取到每一个需要放行的资源路径
for (String u : urls) {
//4,判断当前访问的资源路径字符串是否包含要放行的的资源路径字符串
/*
比如当前访问的资源路径是 /brand-demo/login.jsp
而字符串 /brand-demo/login.jsp 包含了 字符串 /login.jsp ,所以这个字符串就需要放行
*/
if(url.contains(u)){
//找到了,放行
chain.doFilter(request, response);
//break;
return;
}
}

//1. 判断session中是否有user
HttpSession session = req.getSession();
Object user = session.getAttribute("user");

//2. 判断user是否为null
if(user != null){
// 登录过了
//放行
chain.doFilter(request, response);
}else {
// 没有登陆,存储提示信息,跳转到登录页面

req.setAttribute("login_msg","您尚未登陆!");
req.getRequestDispatcher("/login.jsp").forward(req,response);
}
}

public void init(FilterConfig config) throws ServletException {
}

public void destroy() {
}
}

2,Listener

2.1 概述

  • Listener 表示监听器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一。

  • 监听器可以监听就是在 applicationsessionrequest 三个对象创建、销毁或者往其中添加修改删除属性时自动执行代码的功能组件。

    request 和 session 我们学习过。而 applicationServletContext 类型的对象。

    ServletContext 代表整个web应用,在服务器启动的时候,tomcat会自动创建该对象。在服务器关闭时会自动销毁该对象。

2.2 分类

JavaWeb 提供了8个监听器:

image-20210823230820586

这里面只有 ServletContextListener 这个监听器后期我们会接触到,ServletContextListener 是用来监听 ServletContext 对象的创建和销毁。

ServletContextListener 接口中有以下两个方法

  • void contextInitialized(ServletContextEvent sce)ServletContext 对象被创建了会自动执行的方法
  • void contextDestroyed(ServletContextEvent sce)ServletContext 对象被销毁时会自动执行的方法

2.3 代码演示

我们只演示一下 ServletContextListener 监听器

  • 定义一个类,实现ServletContextListener 接口
  • 重写所有的抽象方法
  • 使用 @WebListener 进行配置

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@WebListener
public class ContextLoaderListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
//加载资源
System.out.println("ContextLoaderListener...");
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
//释放资源
}
}

启动服务器,就可以在启动的日志信息中看到 contextInitialized() 方法输出的内容,同时也说明了 ServletContext 对象在服务器启动的时候被创建了。

3,Ajax

3.1 概述

==AJAX (Asynchronous JavaScript And XML):异步的 JavaScript 和 XML。==

我们先来说概念中的 JavaScriptXMLJavaScript 表明该技术和前端相关;XML 是指以此进行数据交换。而这两个我们之前都学习过。

3.1.1 作用

AJAX 作用有以下两方面:

  1. 与服务器进行数据交换:通过AJAX可以给服务器发送请求,服务器将数据直接响应回给浏览器。如下图

我们先来看之前做功能的流程,如下图:

image-20210823235114367

如上图,Servlet 调用完业务逻辑层后将数据存储到域对象中,然后跳转到指定的 jsp 页面,在页面上使用 EL表达式JSTL 标签库进行数据的展示。

而我们学习了AJAX 后,就可以==使用AJAX和服务器进行通信,以达到使用 HTML+AJAX来替换JSP页面==了。如下图,浏览器发送请求servlet,servlet 调用完业务逻辑层后将数据直接响应回给浏览器页面,页面使用 HTML 来进行数据展示。

image-20210823235006847
  1. 异步交互:可以在==不重新加载整个页面==的情况下,与服务器交换数据并==更新部分网页==的技术,如:搜索联想、用户名是否可用校验,等等…
image-20210824000706401

上图所示的效果我们经常见到,在我们输入一些关键字(例如 奥运)后就会在下面联想出相关的内容,而联想出来的这部分数据肯定是存储在百度的服务器上,而我们并没有看出页面重新刷新,这就是 ==更新局部页面== 的效果。再如下图:

image-20210824001015706

我们在用户名的输入框输入用户名,当输入框一失去焦点,如果用户名已经被占用就会在下方展示提示的信息;在这整个过程中也没有页面的刷新,只是在局部展示出了提示信息,这就是 ==更新局部页面== 的效果。

3.1.2 同步和异步

知道了局部刷新后,接下来我们再聊聊同步和异步:

  • 同步发送请求过程如下
image-20210824001443897

​ 浏览器页面在发送请求给服务器,在服务器处理请求的过程中,浏览器页面不能做其他的操作。只能等到服务器响应结束后才能,浏览器页面才能继续做其他的操作。

  • 异步发送请求过程如下

    image-20210824001608916

    浏览器页面发送请求给服务器,在服务器处理请求的过程中,浏览器页面还可以做其他的操作。

3.2 快速入门

3.2.1 服务端实现

在项目的创建 com.itheima.web.servlet ,并在该包下创建名为 AjaxServlet 的servlet

1
2
3
4
5
6
7
8
9
10
11
12
13
@WebServlet("/ajaxServlet")
public class AjaxServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 响应数据
response.getWriter().write("hello ajax~");
}

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

3.2.2 客户端实现

webapp 下创建名为 01-ajax-demo1.html 的页面,在该页面书写 ajax 代码

  • 创建核心对象,不同的浏览器创建的对象是不同的

    1
    2
    3
    4
    5
    6
    7
     var xhttp;
    if (window.XMLHttpRequest) {
    xhttp = new XMLHttpRequest();
    } else {
    // code for IE6, IE5
    xhttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
  • 发送请求

    1
    2
    3
    4
    //建立连接
    xhttp.open("GET", "http://localhost:8080/ajax-demo/ajaxServlet");
    //发送请求
    xhttp.send();
  • 获取响应

    1
    2
    3
    4
    5
    6
    xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
    // 通过 this.responseText 可以获取到服务端响应的数据
    alert(this.responseText);
    }
    };

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<script>
//1. 创建核心对象
var xhttp;
if (window.XMLHttpRequest) {
xhttp = new XMLHttpRequest();
} else {
// code for IE6, IE5
xhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
//2. 发送请求
xhttp.open("GET", "http://localhost:8080/ajax-demo/ajaxServlet");
xhttp.send();

//3. 获取响应
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
alert(this.responseText);
}
};
</script>
</body>
</html>

3.2.3 测试

在浏览器地址栏输入 http://localhost:8080/ajax-demo/01-ajax-demo1.html ,在 01-ajax-demo1.html加载的时候就会发送 ajax 请求,效果如下

image-20210824005956117

我们可以通过 开发者模式 查看发送的 AJAX 请求。在浏览器上按 F12 快捷键

image-20210824010247642

这个是查看所有的请求,如果我们只是想看 异步请求的话,点击上图中 All 旁边的 XHR,会发现只展示 Type 是 xhr 的请求。如下图:

image-20210824010438260

3.3 案例

需求:在完成用户注册时,当用户名输入框失去焦点时,校验用户名是否在数据库已存在

image-20210824201415745

3.3.1 分析

  • 前端完成的逻辑
    1. 给用户名输入框绑定光标失去焦点事件 onblur
    2. 发送 ajax请求,携带username参数
    3. 处理响应:是否显示提示信息
  • 后端完成的逻辑
    1. 接收用户名
    2. 调用service查询User。此案例是为了演示前后端异步交互,所以此处我们不做业务逻辑处理
    3. 返回标记

整体流程如下:

image-20210829183854285

3.3.2 后端实现

com.ithiema.web.servlet 包中定义名为 SelectUserServlet 的servlet。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/selectUserServlet")
public class SelectUserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 接收用户名
String username = request.getParameter("username");
//2. 调用service查询User对象,此处不进行业务逻辑处理,直接给 flag 赋值为 true,表明用户名占用
boolean flag = true;
//3. 响应标记
response.getWriter().write("" + flag);
}

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

3.3.3 前端实现

04-资料\1. 验证用户名案例\1. 静态页面 下的文件整体拷贝到项目下 webapp 下。并在 register.html 页面的 body 结束标签前编写 script 标签,在该标签中实现如下逻辑

第一步:给用户名输入框绑定光标失去焦点事件 onblur

1
2
3
4
//1. 给用户名输入框绑定 失去焦点事件
document.getElementById("username").onblur = function () {

}

第二步:发送 ajax请求,携带username参数

第一步 绑定的匿名函数中书写发送 ajax 请求的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//2. 发送ajax请求
//2.1. 创建核心对象
var xhttp;
if (window.XMLHttpRequest) {
xhttp = new XMLHttpRequest();
} else {
// code for IE6, IE5
xhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
//2.2. 发送请求
xhttp.open("GET", "http://localhost:8080/ajax-demo/selectUserServlet);
xhttp.send();

//2.3. 获取响应
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
//处理响应的结果
}
};

由于我们发送的是 GET 请求,所以需要在 URL 后拼接从输入框获取的用户名数据。而我们在 第一步 绑定的匿名函数中通过以下代码可以获取用户名数据

1
2
// 获取用户名的值
var username = this.value; //this : 给谁绑定的事件,this就代表谁

而携带数据需要将 URL 修改为:

1
xhttp.open("GET", "http://localhost:8080/ajax-demo/selectUserServlet?username="+username);

第三步:处理响应:是否显示提示信息

this.readyState == 4 && this.status == 200 条件满足时,说明已经成功响应数据了。

此时需要判断响应的数据是否是 “true” 字符串,如果是说明用户名已经占用给出错误提示;如果不是说明用户名未被占用清除错误提示。代码如下

1
2
3
4
5
6
7
8
//判断
if(this.responseText == "true"){
//用户名存在,显示提示信息
document.getElementById("username_err").style.display = '';
}else {
//用户名不存在 ,清楚提示信息
document.getElementById("username_err").style.display = 'none';
}

综上所述,前端完成代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//1. 给用户名输入框绑定 失去焦点事件
document.getElementById("username").onblur = function () {
//2. 发送ajax请求
// 获取用户名的值
var username = this.value;

//2.1. 创建核心对象
var xhttp;
if (window.XMLHttpRequest) {
xhttp = new XMLHttpRequest();
} else {
// code for IE6, IE5
xhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
//2.2. 发送请求
xhttp.open("GET", "http://localhost:8080/ajax-demo/selectUserServlet?username="+username);
xhttp.send();

//2.3. 获取响应
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
//alert(this.responseText);
//判断
if(this.responseText == "true"){
//用户名存在,显示提示信息
document.getElementById("username_err").style.display = '';
}else {
//用户名不存在 ,清楚提示信息
document.getElementById("username_err").style.display = 'none';
}
}
};
}

4,axios

Axios 对原生的AJAX进行封装,简化书写。

Axios官网是:https://www.axios-http.cn

4.1 基本使用

axios 使用是比较简单的,分为以下两步:

  • 引入 axios 的 js 文件

    1
    <script src="js/axios-0.18.0.js"></script>
  • 使用axios 发送请求,并获取响应结果

    • 发送 get 请求

      1
      2
      3
      4
      5
      6
      axios({
      method:"get",
      url:"http://localhost:8080/ajax-demo1/aJAXDemo1?username=zhangsan"
      }).then(function (resp){
      alert(resp.data);
      })
    • 发送 post 请求

      1
      2
      3
      4
      5
      6
      7
      axios({
      method:"post",
      url:"http://localhost:8080/ajax-demo1/aJAXDemo1",
      data:"username=zhangsan"
      }).then(function (resp){
      alert(resp.data);
      });

axios() 是用来发送异步请求的,小括号中使用 js 对象传递请求相关的参数:

  • method 属性:用来设置请求方式的。取值为 get 或者 post
  • url 属性:用来书写请求的资源路径。如果是 get 请求,需要将请求参数拼接到路径的后面,格式为: url?参数名=参数值&参数名2=参数值2
  • data 属性:作为请求体被发送的数据。也就是说如果是 post 请求的话,数据需要作为 data 属性的值。

then() 需要传递一个匿名函数。我们将 then() 中传递的匿名函数称为 ==回调函数==,意思是该匿名函数在发送请求时不会被调用,而是在成功响应后调用的函数。而该回调函数中的 resp 参数是对响应的数据进行封装的对象,通过 resp.data 可以获取到响应的数据。

4.2 快速入门

4.2.1 后端实现

定义一个用于接收请求的servlet,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@WebServlet("/axiosServlet")
public class AxiosServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("get...");
//1. 接收请求参数
String username = request.getParameter("username");
System.out.println(username);
//2. 响应数据
response.getWriter().write("hello Axios~");
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("post...");
this.doGet(request, response);
}
}

4.2.2 前端实现

  • 引入 js 文件

    1
    <script src="js/axios-0.18.0.js"></script>
  • 发送 ajax 请求

    • get 请求

      1
      2
      3
      4
      5
      6
      axios({
      method:"get",
      url:"http://localhost:8080/ajax-demo/axiosServlet?username=zhangsan"
      }).then(function (resp) {
      alert(resp.data);
      })
    • post 请求

      1
      2
      3
      4
      5
      6
      7
      axios({
      method:"post",
      url:"http://localhost:8080/ajax-demo/axiosServlet",
      data:"username=zhangsan"
      }).then(function (resp) {
      alert(resp.data);
      })

整体页面代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<script src="js/axios-0.18.0.js"></script>
<script>
//1. get
/* axios({
method:"get",
url:"http://localhost:8080/ajax-demo/axiosServlet?username=zhangsan"
}).then(function (resp) {
alert(resp.data);
})*/

//2. post 在js中{} 表示一个js对象,而这个js对象中有三个属性
axios({
method:"post",
url:"http://localhost:8080/ajax-demo/axiosServlet",
data:"username=zhangsan"
}).then(function (resp) {
alert(resp.data);
})
</script>
</body>
</html>

4.3 请求方法别名

为了方便起见, Axios 已经为所有支持的请求方法提供了别名。如下:

  • get 请求 : axios.get(url[,config])

  • delete 请求 : axios.delete(url[,config])

  • head 请求 : axios.head(url[,config])

  • options 请求 : axios.option(url[,config])

  • post 请求:axios.post(url[,data[,config])

  • put 请求:axios.put(url[,data[,config])

  • patch 请求:axios.patch(url[,data[,config])

而我们只关注 get 请求和 post 请求。

入门案例中的 get 请求代码可以改为如下:

1
2
3
axios.get("http://localhost:8080/ajax-demo/axiosServlet?username=zhangsan").then(function (resp) {
alert(resp.data);
});

入门案例中的 post 请求代码可以改为如下:

1
2
3
axios.post("http://localhost:8080/ajax-demo/axiosServlet","username=zhangsan").then(function (resp) {
alert(resp.data);
})

5,JSON

5.1 概述

==概念:JavaScript Object Notation。JavaScript 对象表示法.==

如下是 JavaScript 对象的定义格式:

1
2
3
4
5
{
name:"zhangsan",
age:23,
city:"北京"
}

接下来我们再看看 JSON 的格式:

1
2
3
4
5
{
"name":"zhangsan",
"age":23,
"city":"北京"
}

通过上面 js 对象格式和 json 格式进行对比,发现两个格式特别像。只不过 js 对象中的属性名可以使用引号(可以是单引号,也可以是双引号);而 json 格式中的键要求必须使用双引号括起来,这是 json 格式的规定。json 格式的数据有什么作用呢?

作用:由于其语法格式简单,层次结构鲜明,现多用于作为==数据载体==,在网络中进行数据传输。如下图所示就是服务端给浏览器响应的数据,这个数据比较简单,如果现需要将 JAVA 对象中封装的数据响应回给浏览器的话,应该以何种数据传输呢?

image-20210830232718632

大家还记得 ajax 的概念吗? 是 ==异步的 JavaScript 和 xml==。这里的 xml就是以前进行数据传递的方式,如下:

1
2
3
4
5
<student>
<name>张三</name>
<age>23</age>
<city>北京</city>
</student>

再看 json 描述以上数据的写法:

1
2
3
4
5
{	
"name":"张三",
"age":23,
"city":"北京"
}

上面两种格式进行对比后就会发现 json 格式数据的简单,以及所占的字节数少等优点。

5.2 JSON 基础语法

5.2.1 定义格式

JSON 本质就是一个字符串,但是该字符串内容是有一定的格式要求的。 定义格式如下:

1
var 变量名 = '{"key":value,"key":value,...}';

JSON 串的键要求必须使用双引号括起来,而值根据要表示的类型确定。value 的数据类型分为如下

  • 数字(整数或浮点数)
  • 字符串(使用双引号括起来)
  • 逻辑值(true或者false)
  • 数组(在方括号中)
  • 对象(在花括号中)
  • null

示例:

1
var jsonStr = '{"name":"zhangsan","age":23,"addr":["北京","上海","西安"]}'

5.2.2 代码演示

创建一个页面,在该页面的 <script> 标签中定义json字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
//1. 定义JSON字符串
var jsonStr = '{"name":"zhangsan","age":23,"addr":["北京","上海","西安"]}'
alert(jsonStr);

</script>
</body>
</html>

通过浏览器打开,页面效果如下图所示

image-20210831223339530

现在我们需要获取到该 JSON 串中的 name 属性值,应该怎么处理呢?

如果它是一个 js 对象,我们就可以通过 js对象.属性名 的方式来获取数据。JS 提供了一个对象 JSON ,该对象有如下两个方法:

  • parse(str) :将 JSON串转换为 js 对象。使用方式是: ==var jsObject = JSON.parse(jsonStr);==
  • stringify(obj) :将 js 对象转换为 JSON 串。使用方式是:==var jsonStr = JSON.stringify(jsObject)==

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
//1. 定义JSON字符串
var jsonStr = '{"name":"zhangsan","age":23,"addr":["北京","上海","西安"]}'
alert(jsonStr);

//2. 将 JSON 字符串转为 JS 对象
let jsObject = JSON.parse(jsonStr);
alert(jsObject)
alert(jsObject.name)
//3. 将 JS 对象转换为 JSON 字符串
let jsonStr2 = JSON.stringify(jsObject);
alert(jsonStr2)
</script>
</body>
</html>

5.2.3 发送异步请求携带参数

后面我们使用 axios 发送请求时,如果要携带复杂的数据时都会以 JSON 格式进行传递,如下

1
2
3
4
5
6
7
axios({
method:"post",
url:"http://localhost:8080/ajax-demo/axiosServlet",
data:"username=zhangsan"
}).then(function (resp) {
alert(resp.data);
})

请求参数不可能由我们自己拼接字符串吧?肯定不用,可以提前定义一个 js 对象,用来封装需要提交的参数,然后使用 JSON.stringify(js对象) 转换为 JSON 串,再将该 JSON 串作为 axiosdata 属性值进行请求参数的提交。如下:

1
2
3
4
5
6
7
8
9
var jsObject = {name:"张三"};

axios({
method:"post",
url:"http://localhost:8080/ajax-demo/axiosServlet",
data: JSON.stringify(jsObject)
}).then(function (resp) {
alert(resp.data);
})

axios 是一个很强大的工具。我们只需要将需要提交的参数封装成 js 对象,并将该 js 对象作为 axiosdata 属性值进行,它会自动将 js 对象转换为 JSON 串进行提交。如下:

1
2
3
4
5
6
7
8
9
var jsObject = {name:"张三"};

axios({
method:"post",
url:"http://localhost:8080/ajax-demo/axiosServlet",
data:jsObject //这里 axios 会将该js对象转换为 json 串的
}).then(function (resp) {
alert(resp.data);
})

==注意:==

  • js 提供的 JSON 对象我们只需要了解一下即可。因为 axios 会自动对 js 对象和 JSON 串进行想换转换。
  • 发送异步请求时,如果请求参数是 JSON 格式,那请求方式必须是 POST。因为 JSON 串需要放在请求体中。

5.3 JSON串和Java对象的相互转换

学习完 json 后,接下来聊聊 json 的作用。以后我们会以 json 格式的数据进行前后端交互。前端发送请求时,如果是复杂的数据就会以 json 提交给后端;而后端如果需要响应一些复杂的数据时,也需要以 json 格式将数据响应回给浏览器。

image-20210831104901912

在后端我们就需要重点学习以下两部分操作:

  • 请求数据:JSON字符串转为Java对象
  • 响应数据:Java对象转为JSON字符串

接下来给大家介绍一套 API,可以实现上面两部分操作。这套 API 就是 Fastjson

5.3.1 Fastjson 概述

Fastjson 是阿里巴巴提供的一个Java语言编写的高性能功能完善的 JSON 库,是目前Java语言中最快的 JSON 库,可以实现 Java 对象和 JSON 字符串的相互转换。

5.3.2 Fastjson 使用

Fastjson 使用也是比较简单的,分为以下三步完成

  1. 导入坐标

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.62</version>
    </dependency>
  2. Java对象转JSON

    1
    String jsonStr = JSON.toJSONString(obj);

    将 Java 对象转换为 JSON 串,只需要使用 Fastjson 提供的 JSON 类中的 toJSONString() 静态方法即可。

  3. JSON字符串转Java对象

    1
    User user = JSON.parseObject(jsonStr, User.class);

    将 json 转换为 Java 对象,只需要使用 Fastjson 提供的 JSON 类中的 parseObject() 静态方法即可。

5.3.3 代码演示

  • 引入坐标

  • 创建一个类,专门用来测试 Java 对象和 JSON 串的相互转换,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class FastJsonDemo {

    public static void main(String[] args) {
    //1. 将Java对象转为JSON字符串
    User user = new User();
    user.setId(1);
    user.setUsername("zhangsan");
    user.setPassword("123");

    String jsonString = JSON.toJSONString(user);
    System.out.println(jsonString);//{"id":1,"password":"123","username":"zhangsan"}


    //2. 将JSON字符串转为Java对象
    User u = JSON.parseObject("{\"id\":1,\"password\":\"123\",\"username\":\"zhangsan\"}", User.class);
    System.out.println(u);
    }
    }

VUE&Element

今日目标:

  • 能够使用VUE中常用指令和插值表达式
  • 能够使用VUE生命周期函数 mounted
  • 能够进行简单的 Element 页面修改
  • 能够完成查询所有功能
  • 能够完成添加功能

1,VUE

1.1 概述

接下来我们学习一款前端的框架,就是 VUE。

==Vue 是一套前端框架,免除原生JavaScript中的DOM操作,简化书写。==

我们之前也学习过后端的框架 MybatisMybatis 是用来简化 jdbc 代码编写的;而 VUE 是前端的框架,是用来简化 JavaScript 代码编写的。前一天我们做了一个综合性的案例,里面进行了大量的DOM操作,如下

image-20210831112115508

学习了 VUE 后,这部分代码我们就不需要再写了。那么 VUE 是如何简化 DOM 书写呢?

==基于MVVM(Model-View-ViewModel)思想,实现数据的双向绑定,将编程的关注点放在数据上。==之前我们是将关注点放在了 DOM 操作上;而要了解 MVVM 思想,必须先聊聊 MVC 思想,如下图就是 MVC 思想图解

image-20210831113940588

C 就是咱们 js 代码,M 就是数据,而 V 是页面上展示的内容,如下图是我们之前写的代码

image-20210831114227585

MVC 思想是没法进行双向绑定的。双向绑定是指当数据模型数据发生变化时,页面展示的会随之发生变化,而如果表单数据发生变化,绑定的模型数据也随之发生变化。接下来我们聊聊 MVVM 思想,如下图是三个组件图解

image-20210831114805052

图中的 Model 就是我们的数据,View 是视图,也就是页面标签,用户可以通过浏览器看到的内容;ModelView 是通过 ViewModel 对象进行双向绑定的,而 ViewModel 对象是 Vue 提供的。接下来让大家看一下双向绑定的效果,下图是提前准备的代码,输入框绑定了 username 模型数据,而在页面上也使用 {{}} 绑定了 username 模型数据

image-20210831115645528

通过浏览器打开该页面可以看到如下页面

image-20210831115902537

当我们在输入框中输入内容,而输入框后面随之实时的展示我们输入的内容,这就是双向绑定的效果。

1.2 快速入门

Vue 使用起来是比较简单的,总共分为如下三步:

  1. 新建 HTML 页面,引入 Vue.js文件

    1
    <script src="js/vue.js"></script>
  2. 在JS代码区域,创建Vue核心对象,进行数据绑定

    1
    2
    3
    4
    5
    6
    7
    8
    new Vue({
    el: "#app",
    data() {
    return {
    username: ""
    }
    }
    });

    创建 Vue 对象时,需要传递一个 js 对象,而该对象中需要如下属性:

    • el : 用来指定哪儿些标签受 Vue 管理。 该属性取值 #app 中的 app 需要是受管理的标签的id属性值
    • data :用来定义数据模型
    • methods :用来定义函数。这个我们在后面就会用到
  3. 编写视图

    1
    2
    3
    4
    <div id="app">
    <input name="username" v-model="username" >
    {{username}}
    </div>

    {{}} 是 Vue 中定义的 插值表达式 ,在里面写数据模型,到时候会将该模型的数据值展示在这个位置。

整体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input v-model="username">
<!--插值表达式-->
{{username}}
</div>
<script src="js/vue.js"></script>
<script>
//1. 创建Vue核心对象
new Vue({
el:"#app",
data(){ // data() 是 ECMAScript 6 版本的新的写法
return {
username:""
}
}

/*data: function () {
return {
username:""
}
}*/
});

</script>
</body>
</html>

1.3 Vue 指令

指令:HTML 标签上带有 v- 前缀的特殊属性,不同指令具有不同含义。例如:v-if,v-for…

常用的指令有:

指令 作用
v-bind 为HTML标签绑定属性值,如设置 href , css样式等
v-model 在表单元素上创建双向数据绑定
v-on 为HTML标签绑定事件
v-if 条件性的渲染某元素,判定为true时渲染,否则不渲染
v-else
v-else-if
v-show 根据条件展示某元素,区别在于切换的是display属性的值
v-for 列表渲染,遍历容器的元素或者对象的属性

接下来我们挨个学习这些指令

1.3.1 v-bind & v-model 指令

image-20210831150101736
  • v-bind

    该指令可以给标签原有属性绑定模型数据。这样模型数据发生变化,标签属性值也随之发生变化

    例如:

    1
    <a v-bind:href="url">百度一下</a>

    上面的 v-bind:" 可以简化写成 : ,如下:

    1
    2
    3
    4
    <!--
    v-bind 可以省略
    -->
    <a :href="url">百度一下</a>
  • v-model

    该指令可以给表单项标签绑定模型数据。这样就能实现双向绑定效果。例如:

    1
    <input name="username" v-model="username">

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<a v-bind:href="url">点击一下</a>
<a :href="url">点击一下</a>
<input v-model="url">
</div>

<script src="js/vue.js"></script>
<script>
//1. 创建Vue核心对象
new Vue({
el:"#app",
data(){
return {
username:"",
url:"https://www.baidu.com"
}
}
});
</script>
</body>
</html>

通过浏览器打开上面页面,并且使用检查查看超链接的路径,该路径会根据输入框输入的路径变化而变化,这是因为超链接和输入框绑定的是同一个模型数据

image-20210831150945931

1.3.2 v-on 指令

image-20210831151231955

我们在页面定义一个按钮,并给该按钮使用 v-on 指令绑定单击事件,html代码如下

1
<input type="button" value="一个按钮" v-on:click="show()">

而使用 v-on 时还可以使用简化的写法,将 v-on: 替换成 @,html代码如下

1
<input type="button" value="一个按钮" @click="show()">

上面代码绑定的 show() 需要在 Vue 对象中的 methods 属性中定义出来

1
2
3
4
5
6
7
8
new Vue({
el: "#app",
methods: {
show(){
alert("我被点了");
}
}
});

==注意:v-on: 后面的事件名称是之前原生事件属性名去掉on。==

例如:

  • 单击事件 : 事件属性名是 onclick,而在vue中使用是 v-on:click
  • 失去焦点事件:事件属性名是 onblur,而在vue中使用时 v-on:blur

整体页面代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input type="button" value="一个按钮" v-on:click="show()"><br>
<input type="button" value="一个按钮" @click="show()">
</div>
<script src="js/vue.js"></script>
<script>
//1. 创建Vue核心对象
new Vue({
el:"#app",
data(){
return {
username:"",
}
},
methods:{
show(){
alert("我被点了...");
}
}
});
</script>
</body>
</html>

1.3.3 条件判断指令

image-20210831151904081

接下来通过代码演示一下。在 Vue中定义一个 count 的数据模型,如下

1
2
3
4
5
6
7
8
9
//1. 创建Vue核心对象
new Vue({
el:"#app",
data(){
return {
count:3
}
}
});

现在要实现,当 count 模型的数据是3时,在页面上展示 div1 内容;当 count 模型的数据是4时,在页面上展示 div2 内容;count 模型数据是其他值时,在页面上展示 div3。这里为了动态改变模型数据 count 的值,再定义一个输入框绑定 count 模型数据。html 代码如下:

1
2
3
4
5
6
7
<div id="app">
<div v-if="count == 3">div1</div>
<div v-else-if="count == 4">div2</div>
<div v-else>div3</div>
<hr>
<input v-model="count">
</div>

整体页面代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<div v-if="count == 3">div1</div>
<div v-else-if="count == 4">div2</div>
<div v-else>div3</div>
<hr>
<input v-model="count">
</div>

<script src="js/vue.js"></script>
<script>
//1. 创建Vue核心对象
new Vue({
el:"#app",
data(){
return {
count:3
}
}
});
</script>
</body>
</html>

通过浏览器打开页面并在输入框输入不同的值,效果如下

image-20210831154300325

然后我们在看看 v-show 指令的效果,如果模型数据 count 的值是3时,展示 div v-show 内容,否则不展示,html页面代码如下

1
2
3
<div v-show="count == 3">div v-show</div>
<br>
<input v-model="count">

浏览器打开效果如下:

image-20210831154547780

通过上面的演示,发现 v-showv-if 效果一样,那它们到底有什么区别呢?我们根据浏览器的检查功能查看源代码

image-20210831154759672

通过上图可以看出 v-show 不展示的原理是给对应的标签添加 display css属性,并将该属性值设置为 none ,这样就达到了隐藏的效果。而 v-if 指令是条件不满足时根本就不会渲染。

1.3.4 v-for 指令

image-20210831155204829

这个指令看到名字就知道是用来遍历的,该指令使用的格式如下:

1
2
3
<标签 v-for="变量名 in 集合模型数据">
{{变量名}}
</标签>

==注意:需要循环那个标签,v-for 指令就写在那个标签上。==

如果在页面需要使用到集合模型数据的索引,就需要使用如下格式:

1
2
3
4
<标签 v-for="(变量名,索引变量) in 集合模型数据">
<!--索引变量是从0开始,所以要表示序号的话,需要手动的加1-->
{{索引变量 + 1}} {{变量名}}
</标签>

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<div v-for="addr in addrs">
{{addr}} <br>
</div>

<hr>
<div v-for="(addr,i) in addrs">
{{i+1}}--{{addr}} <br>
</div>
</div>

<script src="js/vue.js"></script>
<script>

//1. 创建Vue核心对象
new Vue({
el:"#app",
data(){
return {
addrs:["北京","上海","西安"]
}
}
});
</script>
</body>
</html>

通过浏览器打开效果如下

image-20210831155837801

1.4 生命周期

生命周期的八个阶段:每触发一个生命周期事件,会自动执行一个生命周期方法,这些生命周期方法也被称为钩子方法。

image-20210831160239294

下图是 Vue 官网提供的从创建 Vue 到效果 Vue 对象的整个过程及各个阶段对应的钩子函数

image-20210831160335496

看到上面的图,大家无需过多的关注这张图。这些钩子方法我们只关注 mounted 就行了。

mounted:挂载完成,Vue初始化成功,HTML页面渲染成功。而以后我们会在该方法中==发送异步请求,加载数据。==

1.5 案例

1.5.1 需求

使用 Vue 简化我们在前一天ajax学完后做的品牌列表数据查询和添加功能

image-20210831161040800

此案例只是使用 Vue 对前端代码进行优化,后端代码无需修改。

1.5.2 查询所有功能

image-20210831161346678
  1. 在 brand.html 页面引入 vue 的js文件

    1
    <script src="js/vue.js"></script>
  2. 创建 Vue 对象

    • 在 Vue 对象中定义模型数据
    • 在钩子函数中发送异步请求,并将响应的数据赋值给数据模型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    new Vue({
    el: "#app",
    data(){
    return{
    brands:[]
    }
    },
    mounted(){
    // 页面加载完成后,发送异步请求,查询数据
    var _this = this;
    axios({
    method:"get",
    url:"http://localhost:8080/brand-demo/selectAllServlet"
    }).then(function (resp) {
    _this.brands = resp.data;
    })
    }
    })
  3. 修改视图

    • 定义 <div id="app"></div> ,指定该 div 标签受 Vue 管理

    • body 标签中所有的内容拷贝作为上面 div 标签中

    • 删除表格的多余数据行,只留下一个

    • 在表格中的数据行上使用 v-for 指令遍历

      1
      2
      3
      4
      5
      6
      7
      8
      9
      <tr v-for="(brand,i) in brands" align="center">
      <td>{{i + 1}}</td>
      <td>{{brand.brandName}}</td>
      <td>{{brand.companyName}}</td>
      <td>{{brand.ordered}}</td>
      <td>{{brand.description}}</td>
      <td>{{brand.statusStr}}</td>
      <td><a href="#">修改</a> <a href="#">删除</a></td>
      </tr>

整体页面代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<a href="addBrand.html"><input type="button" value="新增"></a><br>
<hr>
<table id="brandTable" border="1" cellspacing="0" width="100%">
<tr>
<th>序号</th>
<th>品牌名称</th>
<th>企业名称</th>
<th>排序</th>
<th>品牌介绍</th>
<th>状态</th>
<th>操作</th>
</tr>
<!--
使用v-for遍历tr
-->
<tr v-for="(brand,i) in brands" align="center">
<td>{{i + 1}}</td>
<td>{{brand.brandName}}</td>
<td>{{brand.companyName}}</td>
<td>{{brand.ordered}}</td>
<td>{{brand.description}}</td>
<td>{{brand.statusStr}}</td>
<td><a href="#">修改</a> <a href="#">删除</a></td>
</tr>
</table>
</div>
<script src="js/axios-0.18.0.js"></script>
<script src="js/vue.js"></script>

<script>
new Vue({
el: "#app",
data(){
return{
brands:[]
}
},
mounted(){
// 页面加载完成后,发送异步请求,查询数据
var _this = this;
axios({
method:"get",
url:"http://localhost:8080/brand-demo/selectAllServlet"
}).then(function (resp) {
_this.brands = resp.data;
})
}
})
</script>
</body>
</html>

1.5.3 添加功能

页面操作效果如下:

image-20210831163001830

整体流程如下

image-20210831163035298

==注意:前端代码的关键点在于使用 v-model 指令给标签项绑定模型数据,利用双向绑定特性,在发送异步请求时提交数据。==

  1. 在 addBrand.html 页面引入 vue 的js文件

    1
    <script src="js/vue.js"></script>
  2. 创建 Vue 对象

    • 在 Vue 对象中定义模型数据 brand
    • 定义一个 submitForm() 函数,用于给 提交 按钮提供绑定的函数
    • submitForm() 函数中发送 ajax 请求,并将模型数据 brand 作为参数进行传递
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    new Vue({
    el: "#app",
    data(){
    return {
    brand:{}
    }
    },
    methods:{
    submitForm(){
    // 发送ajax请求,添加
    var _this = this;
    axios({
    method:"post",
    url:"http://localhost:8080/brand-demo/addServlet",
    data:_this.brand
    }).then(function (resp) {
    // 判断响应数据是否为 success
    if(resp.data == "success"){
    location.href = "http://localhost:8080/brand-demo/brand.html";
    }
    })

    }
    }
    })
  3. 修改视图

    • 定义 <div id="app"></div> ,指定该 div 标签受 Vue 管理

    • body 标签中所有的内容拷贝作为上面 div 标签中

    • 给每一个表单项标签绑定模型数据。最后这些数据要被封装到 brand 对象中

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <div id="app">
      <h3>添加品牌</h3>
      <form action="" method="post">
      品牌名称:<input id="brandName" v-model="brand.brandName" name="brandName"><br>
      企业名称:<input id="companyName" v-model="brand.companyName" name="companyName"><br>
      排序:<input id="ordered" v-model="brand.ordered" name="ordered"><br>
      描述信息:<textarea rows="5" cols="20" id="description" v-model="brand.description" name="description"></textarea><br>
      状态:
      <input type="radio" name="status" v-model="brand.status" value="0">禁用
      <input type="radio" name="status" v-model="brand.status" value="1">启用<br>

      <input type="button" id="btn" @click="submitForm" value="提交">
      </form>
      </div>

整体页面代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>添加品牌</title>
</head>
<body>
<div id="app">
<h3>添加品牌</h3>
<form action="" method="post">
品牌名称:<input id="brandName" v-model="brand.brandName" name="brandName"><br>
企业名称:<input id="companyName" v-model="brand.companyName" name="companyName"><br>
排序:<input id="ordered" v-model="brand.ordered" name="ordered"><br>
描述信息:<textarea rows="5" cols="20" id="description" v-model="brand.description" name="description"></textarea><br>
状态:
<input type="radio" name="status" v-model="brand.status" value="0">禁用
<input type="radio" name="status" v-model="brand.status" value="1">启用<br>

<input type="button" id="btn" @click="submitForm" value="提交">
</form>
</div>
<script src="js/axios-0.18.0.js"></script>
<script src="js/vue.js"></script>
<script>
new Vue({
el: "#app",
data(){
return {
brand:{}
}
},
methods:{
submitForm(){
// 发送ajax请求,添加
var _this = this;
axios({
method:"post",
url:"http://localhost:8080/brand-demo/addServlet",
data:_this.brand
}).then(function (resp) {
// 判断响应数据是否为 success
if(resp.data == "success"){
location.href = "http://localhost:8080/brand-demo/brand.html";
}
})
}
}
})
</script>
</body>
</html>

通过上面的优化,前端代码确实简化了不少。但是页面依旧是不怎么好看,那么接下来我们学习 Element,它可以美化页面。

2,Element

Element:是饿了么公司前端开发团队提供的一套基于 Vue 的网站组件库,用于快速构建网页。

Element 提供了很多组件(组成网页的部件)供我们使用。例如 超链接、按钮、图片、表格等等~

如下图左边的是我们编写页面看到的按钮,上图右边的是 Element 提供的页面效果,效果一目了然。

image-20210831170943892

我们学习 Element 其实就是学习怎么从官网拷贝组件到我们自己的页面并进行修改,官网网址是

1
https://element.eleme.cn/#/zh-CN

进入官网能看到如下页面

image-20210831171456559

接下来直接点击 组件 ,页面如下

image-20210831171552844

2.1 快速入门

  1. 将资源 04-资料\02-element 下的 element-ui 文件夹直接拷贝到项目的 webapp 下。目录结构如下

    image-20210831171856768
  2. 创建页面,并在页面引入Element 的css、js文件 和 Vue.js

    1
    2
    3
    <script src="vue.js"></script>
    <script src="element-ui/lib/index.js"></script>
    <link rel="stylesheet" href="element-ui/lib/theme-chalk/index.css">
  3. .创建Vue核心对象

    Element 是基于 Vue 的,所以使用Element时必须要创建 Vue 对象

    1
    2
    3
    4
    5
    <script>
    new Vue({
    el:"#app"
    })
    </script>
  4. 官网复制Element组件代码

    image-20210831180730287

    在左菜单栏找到 Button 按钮 ,然后找到自己喜欢的按钮样式,点击 显示代码 ,在下面就会展示出对应的代码,将这些代码拷贝到我们自己的页面即可。

整体页面代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">


<el-row>
<el-button>默认按钮</el-button>
<el-button type="primary">主要按钮</el-button>
<el-button type="success">成功按钮</el-button>
<el-button type="info">信息按钮</el-button>
<el-button type="warning">警告按钮</el-button>
<el-button type="danger">删除</el-button>
</el-row>
<el-row>
<el-button plain>朴素按钮</el-button>
<el-button type="primary" plain>主要按钮</el-button>
<el-button type="success" plain>成功按钮</el-button>
<el-button type="info" plain>信息按钮</el-button>
<el-button type="warning" plain>警告按钮</el-button>
<el-button type="danger" plain>危险按钮</el-button>
</el-row>

<el-row>
<el-button round>圆角按钮</el-button>
<el-button type="primary" round>主要按钮</el-button>
<el-button type="success" round>成功按钮</el-button>
<el-button type="info" round>信息按钮</el-button>
<el-button type="warning" round>警告按钮</el-button>
<el-button type="danger" round>危险按钮</el-button>
</el-row>

<el-row>
<el-button icon="el-icon-search" circle></el-button>
<el-button type="primary" icon="el-icon-edit" circle></el-button>
<el-button type="success" icon="el-icon-check" circle></el-button>
<el-button type="info" icon="el-icon-message" circle></el-button>
<el-button type="warning" icon="el-icon-star-off" circle></el-button>
<el-button type="danger" icon="el-icon-delete" circle></el-button>
</el-row>
</div>

<script src="js/vue.js"></script>
<script src="element-ui/lib/index.js"></script>
<link rel="stylesheet" href="element-ui/lib/theme-chalk/index.css">

<script>
new Vue({
el:"#app"
})
</script>

</body>
</html>

2.2 Element 布局

Element 提供了两种布局方式,分别是:

  • Layout 布局
  • Container 布局容器

2.2.1 Layout 局部

通过基础的 24 分栏,迅速简便地创建布局。也就是默认将一行分为 24 栏,根据页面要求给每一列设置所占的栏数。

image-20210831182349672

在左菜单栏找到 Layout 布局 ,然后找到自己喜欢的按钮样式,点击 显示代码 ,在下面就会展示出对应的代码,显示出的代码中有样式,有html标签。将样式拷贝我们自己页面的 head 标签内,将html标签拷贝到 <div id="app"></div> 标签内。

整体页面代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>

<style>
.el-row {
margin-bottom: 20px;
}
.el-col {
border-radius: 4px;
}
.bg-purple-dark {
background: #99a9bf;
}
.bg-purple {
background: #d3dce6;
}
.bg-purple-light {
background: #e5e9f2;
}
.grid-content {
border-radius: 4px;
min-height: 36px;
}
.row-bg {
padding: 10px 0;
background-color: #f9fafc;
}
</style>
</head>
<body>
<div id="app">
<el-row>
<el-col :span="24"><div class="grid-content bg-purple-dark"></div></el-col>
</el-row>
<el-row>
<el-col :span="12"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="12"><div class="grid-content bg-purple-light"></div></el-col>
</el-row>
<el-row>
<el-col :span="8"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="8"><div class="grid-content bg-purple-light"></div></el-col>
<el-col :span="8"><div class="grid-content bg-purple"></div></el-col>
</el-row>
<el-row>
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple-light"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple-light"></div></el-col>
</el-row>
<el-row>
<el-col :span="4"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="4"><div class="grid-content bg-purple-light"></div></el-col>
<el-col :span="4"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="4"><div class="grid-content bg-purple-light"></div></el-col>
<el-col :span="4"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="4"><div class="grid-content bg-purple-light"></div></el-col>
</el-row>
</div>
<script src="js/vue.js"></script>
<script src="element-ui/lib/index.js"></script>
<link rel="stylesheet" href="element-ui/lib/theme-chalk/index.css">

<script>
new Vue({
el:"#app"
})
</script>
</body>
</html>

现在需要添加一行,要求该行显示8个格子,通过计算每个格子占 3 栏,具体的html 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--
添加一行,8个格子 24/8 = 3
-->
<el-row>
<el-col :span="3"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="3"><div class="grid-content bg-purple-light"></div></el-col>
<el-col :span="3"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="3"><div class="grid-content bg-purple-light"></div></el-col>
<el-col :span="3"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="3"><div class="grid-content bg-purple-light"></div></el-col>
<el-col :span="3"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="3"><div class="grid-content bg-purple-light"></div></el-col>
</el-row>

2.2.2 Container 布局容器

用于布局的容器组件,方便快速搭建页面的基本结构。如下图就是布局容器效果。

如下图是官网提供的 Container 布局容器实例:

image-20210831183433892

该效果代码中包含了样式、页面标签、模型数据。将里面的样式 <style> 拷贝到我们自己页面的 head 标签中;将html标签拷贝到 <div id="app"></div> 标签中,再将数据模型拷贝到 vue 对象的 data() 中。

整体页面代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>

<style>
.el-header {
background-color: #B3C0D1;
color: #333;
line-height: 60px;
}

.el-aside {
color: #333;
}
</style>
</head>
<body>
<div id="app">
<el-container style="height: 500px; border: 1px solid #eee">
<el-aside width="200px" style="background-color: rgb(238, 241, 246)">
<el-menu :default-openeds="['1', '3']">
<el-submenu index="1">
<template slot="title"><i class="el-icon-message"></i>导航一</template>
<el-menu-item-group>
<template slot="title">分组一</template>
<el-menu-item index="1-1">选项1</el-menu-item>
<el-menu-item index="1-2">选项2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="分组2">
<el-menu-item index="1-3">选项3</el-menu-item>
</el-menu-item-group>
<el-submenu index="1-4">
<template slot="title">选项4</template>
<el-menu-item index="1-4-1">选项4-1</el-menu-item>
</el-submenu>
</el-submenu>
<el-submenu index="2">
<template slot="title"><i class="el-icon-menu"></i>导航二</template>
<el-submenu index="2-1">
<template slot="title">选项1</template>
<el-menu-item index="2-1-1">选项1-1</el-menu-item>
</el-submenu>
</el-submenu>
<el-submenu index="3">
<template slot="title"><i class="el-icon-setting"></i>导航三</template>
<el-menu-item-group>
<template slot="title">分组一</template>
<el-menu-item index="3-1">选项1</el-menu-item>
<el-menu-item index="3-2">选项2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="分组2">
<el-menu-item index="3-3">选项3</el-menu-item>
</el-menu-item-group>
<el-submenu index="3-4">
<template slot="title">选项4</template>
<el-menu-item index="3-4-1">选项4-1</el-menu-item>
</el-submenu>
</el-submenu>
</el-menu>
</el-aside>

<el-container>
<el-header style="text-align: right; font-size: 12px">
<el-dropdown>
<i class="el-icon-setting" style="margin-right: 15px"></i>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>查看</el-dropdown-item>
<el-dropdown-item>新增</el-dropdown-item>
<el-dropdown-item>删除</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<span>王小虎</span>
</el-header>

<el-main>
<el-table :data="tableData">
<el-table-column prop="date" label="日期" width="140">
</el-table-column>
<el-table-column prop="name" label="姓名" width="120">
</el-table-column>
<el-table-column prop="address" label="地址">
</el-table-column>
</el-table>
</el-main>
</el-container>
</el-container>
</div>
<script src="js/vue.js"></script>
<script src="element-ui/lib/index.js"></script>
<link rel="stylesheet" href="element-ui/lib/theme-chalk/index.css">

<script>
new Vue({
el:"#app",
data() {
const item = {
date: '2016-05-02',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
};
return {
tableData: Array(20).fill(item)
}
}
})
</script>
</body>
</html>

2.3 案例

其他的组件我们通过完成一个页面来学习。

我们要完成如下页面效果

image-20210831185223141

要完成该页面,我们需要先对这个页面进行分析,看页面由哪儿几部分组成,然后到官网进行拷贝并修改。页面总共有如下组成部分

image-20210831185505106

还有一个是当我们点击 新增 按钮,会在页面正中间弹出一个对话框,如下

image-20210831185612905

2.3.1 准备基本页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">

</div>

<script src="js/vue.js"></script>
<script src="element-ui/lib/index.js"></script>
<link rel="stylesheet" href="element-ui/lib/theme-chalk/index.css">

<script>
new Vue({
el: "#app"
})
</script>
</body>
</html>

2.3.2 完成表格展示

使用 Element 整体的思路就是 ==拷贝 + 修改==。

2.3.2.1 拷贝

image-20210831185937618

在左菜单栏找到 Table 表格并点击,右边主体就会定位到表格这一块,找到我们需要的表格效果(如上图),点击 显示代码 就可以看到这个表格的代码了。

将html标签拷贝到 <div id="app"></div> 中,如下:

image-20210831190328825

将css样式拷贝到我们页面的 head 标签中,如下

image-20210831190419248

将方法和模型数据拷贝到 Vue 对象指定的位置

image-20210831190534720

拷贝完成后通过浏览器打开可以看到表格的效果

image-20210831191234876

表格效果出来了,但是显示的表头和数据并不是我们想要的,所以接下来就需要对页面代码进行修改了。

2.3.2.2 修改
  1. 修改表头和数据

    下面是对表格代码进行分析的图解。根据下图说明修改自己的列数和列名

    image-20210831192032118

    修改完页面后,还需要对绑定的模型数据进行修改,下图是对模型数据进行分析的图解

    image-20210831192429806
  2. 给表格添加操作列

    从之前的表格拷贝一列出来并对其进行修改。按钮是从官网的 Button 按钮 组件中拷贝并修改的

    image-20210831192809304
  3. 给表格添加复选框列和标号列

    给表格添加复选框和标号列,效果如下

    image-20210831193216143

    此效果也是从 Element 官网进行拷贝,先找到对应的表格效果,然后将其对应代码拷贝到我们的代码中,如下是复选框列官网效果图和代码

    image-20210831193601788

    这里需要注意在 <el-table> 标签上有一个事件 @selection-change="handleSelectionChange" ,这里绑定的函数也需要从官网拷贝到我们自己的页面代码中,函数代码如下:

    image-20210831194013986

    从该函数中又发现还需要一个模型数据 multipleSelection ,所以还需要定义出该模型数据

标号列也用同样的方式进行拷贝并修改。

2.3.3 完成搜索表单展示

在 Element 官网找到横排的表单效果,然后拷贝代码并进行修改

image-20210831194300357

点击上面的 显示代码 后,就会展示出对应的代码,下面是对这部分代码进行分析的图解

image-20210831194835721

然后根据我们要的效果修改代码。

2.3.4 完成批量删除和新增按钮展示

从 Element 官网找具有着色效果的按钮,并将代码拷贝到我们自己的页面上

image-20210831214602954

2.3.5 完成对话框展示

在 Element 官网找对话框,如下:

image-20210831214818516

下面对官网提供的代码进行分析

image-20210831215609729

上图分析出来的模型数据需要在 Vue 对象中进行定义。

2.3.6 完成分页条展示

在 Element 官网找到 Pagination 分页 ,在页面主体部分找到我们需要的效果,如下

image-20210831220034775

点击 显示代码 ,找到 完整功能 对应的代码,接下来对该代码进行分析

image-20210831220446390

上面代码属性说明:

  • page-size :每页显示的条目数

  • page-sizes : 每页显示个数选择器的选项设置。

    :page-sizes="[100,200,300,400]" 对应的页面效果如下:

    image-20210831220820557
  • currentPage :当前页码。我们点击那个页码,此属性值就是几。

  • total :总记录数。用来设置总的数据条目数,该属性设置后, Element 会自动计算出需分多少页并给我们展示对应的页码。

事件说明:

  • size-change :pageSize 改变时会触发。也就是当我们改变了每页显示的条目数后,该事件会触发。
  • current-change :currentPage 改变时会触发。也就是当我们点击了其他的页码后,该事件会触发。

2.3.7 完整页面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.el-table .warning-row {
background: oldlace;
}
.el-table .success-row {
background: #f0f9eb;
}
</style>
</head>
<body>
<div id="app">
<!--搜索表单-->
<el-form :inline="true" :model="brand" class="demo-form-inline">
<el-form-item label="当前状态">
<el-select v-model="brand.status" placeholder="当前状态">
<el-option label="启用" value="1"></el-option>
<el-option label="禁用" value="0"></el-option>
</el-select>
</el-form-item>

<el-form-item label="企业名称">
<el-input v-model="brand.companyName" placeholder="企业名称"></el-input>
</el-form-item>

<el-form-item label="品牌名称">
<el-input v-model="brand.brandName" placeholder="品牌名称"></el-input>
</el-form-item>

<el-form-item>
<el-button type="primary" @click="onSubmit">查询</el-button>
</el-form-item>
</el-form>

<!--按钮-->
<el-row>
<el-button type="danger" plain>批量删除</el-button>
<el-button type="primary" plain @click="dialogVisible = true">新增</el-button>
</el-row>

<!--添加数据对话框表单-->
<el-dialog
title="编辑品牌"
:visible.sync="dialogVisible"
width="30%">
<el-form ref="form" :model="brand" label-width="80px">
<el-form-item label="品牌名称">
<el-input v-model="brand.brandName"></el-input>
</el-form-item>

<el-form-item label="企业名称">
<el-input v-model="brand.companyName"></el-input>
</el-form-item>

<el-form-item label="排序">
<el-input v-model="brand.ordered"></el-input>
</el-form-item>

<el-form-item label="备注">
<el-input type="textarea" v-model="brand.description"></el-input>
</el-form-item>

<el-form-item label="状态">
<el-switch v-model="brand.status"
active-value="1"
inactive-value="0"
></el-switch>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="addBrand">提交</el-button>
<el-button @click="dialogVisible = false">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>

<!--表格-->
<template>
<el-table
:data="tableData"
style="width: 100%"
:row-class-name="tableRowClassName"
@selection-change="handleSelectionChange">
<el-table-column
type="selection"
width="55">
</el-table-column>
<el-table-column
type="index"
width="50">
</el-table-column>
<el-table-column
prop="brandName"
label="品牌名称"
align="center">
</el-table-column>
<el-table-column
prop="companyName"
label="企业名称"
align="center">
</el-table-column>
<el-table-column
prop="ordered"
align="center"
label="排序">
</el-table-column>
<el-table-column
prop="status"
align="center"
label="当前状态">
</el-table-column>
<el-table-column
align="center"
label="操作">
<el-row>
<el-button type="primary">修改</el-button>
<el-button type="danger">删除</el-button>
</el-row>
</el-table-column>

</el-table>
</template>

<!--分页工具条-->
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[5, 10, 15, 20]"
:page-size="5"
layout="total, sizes, prev, pager, next, jumper"
:total="400">
</el-pagination>

</div>
<script src="js/vue.js"></script>
<script src="element-ui/lib/index.js"></script>
<link rel="stylesheet" href="element-ui/lib/theme-chalk/index.css">
<script>
new Vue({
el: "#app",
methods: {
tableRowClassName({row, rowIndex}) {
if (rowIndex === 1) {
return 'warning-row';
} else if (rowIndex === 3) {
return 'success-row';
}
return '';
},
// 复选框选中后执行的方法
handleSelectionChange(val) {
this.multipleSelection = val;

console.log(this.multipleSelection)
},
// 查询方法
onSubmit() {
console.log(this.brand);
},
// 添加数据
addBrand(){
console.log(this.brand);
},
//分页
handleSizeChange(val) {
console.log(`每页 ${val} 条`);
},
handleCurrentChange(val) {
console.log(`当前页: ${val}`);
}
},
data() {
return {
// 当前页码
currentPage: 4,
// 添加数据对话框是否展示的标记
dialogVisible: false,

// 品牌模型数据
brand: {
status: '',
brandName: '',
companyName: '',
id:"",
ordered:"",
description:""
},
// 复选框选中数据集合
multipleSelection: [],
// 表格数据
tableData: [{
brandName: '华为',
companyName: '华为科技有限公司',
ordered: '100',
status: "1"
}, {
brandName: '华为',
companyName: '华为科技有限公司',
ordered: '100',
status: "1"
}, {
brandName: '华为',
companyName: '华为科技有限公司',
ordered: '100',
status: "1"
}, {
brandName: '华为',
companyName: '华为科技有限公司',
ordered: '100',
status: "1"
}]
}
}
})
</script>
</body>
</html>