JavaWeb笔记
在此特别感谢黑马程序员的精品课程
本文记录内容(JDBC、Maven、Mybatis、HTTP、Tomcat、Servlet、Cookie、Session)
前端部分(html、css、js、jsp、Vue)本文并未记录
JDBC
1,JDBC概述
1.1 JDBC概念
JDBC 就是使用Java语言操作关系型数据库的一套API
全称:( Java DataBase Connectivity ) Java 数据库连接
1.2 JDBC本质
- 官方(sun公司)定义的一套操作所有关系型数据库的规则,即接口
- 各个数据库厂商去实现这套接口,提供数据库驱动jar包
- 我们可以使用这套接口(JDBC)编程,真正执行的代码是驱动jar包中的实现类
1.3 JDBC好处
- 各数据库厂商使用相同的接口,Java代码不需要针对不同数据库分别开发
- 可随时替换底层数据库,访问数据库的Java代码基本不变
以后编写操作数据库的代码只需要面向JDBC(接口),操作哪儿个关系型数据库就需要导入该数据库的驱动包,如需要操作MySQL数据库,就需要再项目中导入MySQL数据库的驱动包。如下图就是MySQL驱动包
2,JDBC快速入门
先来看看通过Java操作数据库的流程
第一步:编写Java代码
第二步:Java代码将SQL发送到MySQL服务端
第三步:MySQL服务端接收到SQL语句并执行该SQL语句
第四步:将SQL语句执行的结果返回给Java代码
2.1 编写代码步骤
-
创建工程,导入驱动jar包
-
注册驱动
1
Class.forName("com.mysql.jdbc.Driver");
-
获取连接
1
Connection conn = DriverManager.getConnection(url, username, password);
Java代码需要发送SQL给MySQL服务端,就需要先建立连接
-
定义SQL语句
1
String sql = “update…” ;
-
获取执行SQL对象
执行SQL语句需要SQL执行对象,而这个执行对象就是Statement对象
1
Statement stmt = conn.createStatement();
-
执行SQL
1
stmt.executeUpdate(sql);
-
处理返回结果
-
释放资源
2.2 具体操作
- 创建新的空的项目
- 定义项目的名称,并指定位置
- 对项目进行设置,JDK版本、编译版本
- 创建模块,指定模块的名称及位置
- 导入驱动包
- 将mysql的驱动包放在模块下的lib目录(随意命名)下,并将该jar包添加为库文件
- 在添加为库文件的时候,有如下三个选项
- Global Library : 全局有效
- Project Library : 项目有效
- Module Library : 模块有效
-
在src下创建类
-
编写代码如下
1 | /** |
3,JDBC API详解
3.1 DriverManager
DriverManager(驱动管理类)作用:
-
注册驱动
registerDriver方法是用于注册驱动的,但是我们之前做的入门案例并不是这样写的。而是如下实现
1
Class.forName("com.mysql.jdbc.Driver");
我们查询MySQL提供的Driver类,看它是如何实现的,源码如下:
在该类中的静态代码块中已经执行了
DriverManager
对象的registerDriver()
方法进行驱动的注册了,那么我们只需要加载Driver
类,该静态代码块就会执行。而Class.forName("com.mysql.jdbc.Driver");
就可以加载Driver
类。==提示:==
- MySQL 5之后的驱动包,可以省略注册驱动的步骤
- 自动加载jar包中META-INF/services/java.sql.Driver文件中的驱动类
-
获取数据库连接
参数说明:
-
url : 连接路径
语法:jdbc:mysql://ip地址(域名):端口号/数据库名称?参数键值对1&参数键值对2…
示例:jdbc:mysql://127.0.0.1:3306/db1
==细节:==
-
如果连接的是本机mysql服务器,并且mysql服务默认端口是3306,则url可以简写为:jdbc:mysql:///数据库名称?参数键值对
-
配置 useSSL=false 参数,禁用安全连接方式,解决警告提示
-
-
user :用户名
-
poassword :密码
-
3.2 Connection
Connection(数据库连接对象)作用:
- 获取执行 SQL 的对象
- 管理事务
3.2.1 获取执行对象
-
普通执行SQL对象
1
Statement createStatement()
入门案例中就是通过该方法获取的执行对象。
-
预编译SQL的执行SQL对象:防止SQL注入
1
PreparedStatement prepareStatement(sql)
通过这种方式获取的
PreparedStatement
SQL语句执行对象是我们一会重点要进行讲解的,它可以防止SQL注入。 -
执行存储过程的对象
1
CallableStatement prepareCall(sql)
通过这种方式获取的
CallableStatement
执行对象是用来执行存储过程的,而存储过程在MySQL中不常用,所以这个我们将不进行讲解。
3.2.2 事务管理
先回顾一下MySQL事务管理的操作:
- 开启事务 : BEGIN; 或者 START TRANSACTION;
- 提交事务 : COMMIT;
- 回滚事务 : ROLLBACK;
MySQL默认是自动提交事务
接下来学习JDBC事务管理的方法。
Connection几口中定义了3个对应的方法:
-
开启事务
参与autoCommit 表示是否自动提交事务,true表示自动提交事务,false表示手动提交事务。而开启事务需要将该参数设为为false。
-
提交事务
-
回滚事务
具体代码实现如下:
1 | /** |
3.3 Statement
3.3.1 概述
Statement对象的作用就是用来执行SQL语句。而针对不同类型的SQL语句使用的方法也不一样。
-
执行DDL、DML语句
-
执行DQL语句
该方法涉及到了
ResultSet
对象,而这个对象我们还没有学习,一会再重点讲解。
3.3.2 代码实现
-
执行DML语句
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/**
* 执行DML语句
* @throws Exception
*/
public void testDML() throws Exception {
//1. 注册驱动
//Class.forName("com.mysql.jdbc.Driver");
//2. 获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写
String url = "jdbc:mysql:///db1?useSSL=false";
String username = "root";
String password = "1234";
Connection conn = DriverManager.getConnection(url, username, password);
//3. 定义sql
String sql = "update account set money = 3000 where id = 1";
//4. 获取执行sql的对象 Statement
Statement stmt = conn.createStatement();
//5. 执行sql
int count = stmt.executeUpdate(sql);//执行完DML语句,受影响的行数
//6. 处理结果
//System.out.println(count);
if(count > 0){
System.out.println("修改成功~");
}else{
System.out.println("修改失败~");
}
//7. 释放资源
stmt.close();
conn.close();
} -
执行DDL语句
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/**
* 执行DDL语句
* @throws Exception
*/
public void testDDL() throws Exception {
//1. 注册驱动
//Class.forName("com.mysql.jdbc.Driver");
//2. 获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写
String url = "jdbc:mysql:///db1?useSSL=false";
String username = "root";
String password = "1234";
Connection conn = DriverManager.getConnection(url, username, password);
//3. 定义sql
String sql = "drop database db2";
//4. 获取执行sql的对象 Statement
Statement stmt = conn.createStatement();
//5. 执行sql
int count = stmt.executeUpdate(sql);//执行完DDL语句,可能是0
//6. 处理结果
System.out.println(count);
//7. 释放资源
stmt.close();
conn.close();
}注意:
- 以后开发很少使用java代码操作DDL语句
3.4 ResultSet
3.4.1 概述
ResultSet(结果集对象)作用:
- ==封装了SQL查询语句的结果。==
而执行了DQL语句后就会返回该对象,对应执行DQL语句的方法如下:
1 | ResultSet executeQuery(sql):执行DQL 语句,返回 ResultSet 对象 |
那么我们就需要从 ResultSet
对象中获取我们想要的数据。ResultSet
对象提供了操作查询结果数据的方法,如下:
boolean next()
- 将光标从当前位置向前移动一行
- 判断当前行是否为有效行
方法返回值说明:
- true : 有效航,当前行有数据
- false : 无效行,当前行没有数据
xxx getXxx(参数):获取数据
- xxx : 数据类型;如: int getInt(参数) ;String getString(参数)
- 参数
- int类型的参数:列的编号,从1开始
- String类型的参数: 列的名称
3.4.2 代码实现
1 | /** |
3.5 PreparedStatement
PreparedStatement作用:
- 预编译SQL语句并执行:预防SQL注入问题
3.5.1 PreparedStatement概述
PreparedStatement作用:
- 预编译SQL语句并执行:预防SQL注入问题
-
获取 PreparedStatement 对象
1
2
3
4// SQL语句中的参数值,使用?占位符替代
String sql = "select * from user where username = ? and password = ?";
// 通过Connection对象获取,并传入对应的sql语句
PreparedStatement pstmt = conn.prepareStatement(sql); -
设置参数值
上面的sql语句中参数使用 ? 进行占位,在之前之前肯定要设置这些 ? 的值。
PreparedStatement对象:setXxx(参数1,参数2):给 ? 赋值
-
Xxx:数据类型 ; 如 setInt (参数1,参数2)
-
参数:
-
参数1: ?的位置编号,从1 开始
-
参数2: ?的值
-
-
-
执行SQL语句
executeUpdate(); 执行DDL语句和DML语句
executeQuery(); 执行DQL语句
==注意:==
- 调用这两个方法时不需要传递SQL语句,因为获取SQL语句执行对象时已经对SQL语句进行预编译了。
3.5.2 使用PreparedStatement改进
1 |
|
执行上面语句就可以发现不会出现SQL注入漏洞问题了。那么PreparedStatement又是如何解决的呢?它是将特殊字符进行了转义,转义的SQL如下:
1 | select * from tb_user where username = 'sjdljfld' and password = '\'or \'1\' = \'1' |
3.5.3 PreparedStatement原理
PreparedStatement 好处:
- 预编译SQL,性能更高
- 防止SQL注入:==将敏感字符进行转义==
Java代码操作数据库流程如图所示:
-
将sql语句发送到MySQL服务器端
-
MySQL服务端会对sql语句进行如下操作
-
检查SQL语句
检查SQL语句的语法是否正确。
-
编译SQL语句。将SQL语句编译成可执行的函数。
检查SQL和编译SQL花费的时间比执行SQL的时间还要长。如果我们只是重新设置参数,那么检查SQL语句和编译SQL语句将不需要重复执行。这样就提高了性能。
-
执行SQL语句
-
接下来我们通过查询日志来看一下原理。
-
开启预编译功能
在代码中编写url时需要加上以下参数。而我们之前根本就没有开启预编译功能,只是解决了SQL注入漏洞。
1
useServerPrepStmts=true
-
配置MySQL执行日志(重启mysql服务后生效)
在mysql配置文件(my.ini)中添加如下配置
1
2
3
4
5
6log-output=FILE
general-log=1
general_log_file="D:\mysql.log"
slow-query-log=1
slow_query_log_file="D:\mysql_slow.log"
long_query_time=2 -
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/**
* PreparedStatement原理
* @throws Exception
*/
public void testPreparedStatement2() throws Exception {
//2. 获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写
// useServerPrepStmts=true 参数开启预编译功能
String url = "jdbc:mysql:///db1?useSSL=false&useServerPrepStmts=true";
String username = "root";
String password = "1234";
Connection conn = DriverManager.getConnection(url, username, password);
// 接收用户输入 用户名和密码
String name = "zhangsan";
String pwd = "' or '1' = '1";
// 定义sql
String sql = "select * from tb_user where username = ? and password = ?";
// 获取pstmt对象
PreparedStatement pstmt = conn.prepareStatement(sql);
Thread.sleep(10000);
// 设置?的值
pstmt.setString(1,name);
pstmt.setString(2,pwd);
ResultSet rs = null;
// 执行sql
rs = pstmt.executeQuery();
// 设置?的值
pstmt.setString(1,"aaa");
pstmt.setString(2,"bbb");
// 执行sql
rs = pstmt.executeQuery();
// 判断登录是否成功
if(rs.next()){
System.out.println("登录成功~");
}else{
System.out.println("登录失败~");
}
//7. 释放资源
rs.close();
pstmt.close();
conn.close();
}
==小结:==
- 在获取PreparedStatement对象时,将sql语句发送给mysql服务器进行检查,编译(这些步骤很耗时)
- 执行时就不用再进行这些步骤了,速度更快
- 如果sql模板一样,则只需要进行一次检查、编译
4,数据库连接池
4.1 数据库连接池简介
数据库连接池是个容器,负责分配、管理数据库连接(Connection)
它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个;
释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏
好处
- 资源重用
- 提升系统响应速度
- 避免数据库连接遗漏
之前我们代码中使用连接是没有使用都创建一个Connection对象,使用完毕就会将其销毁。这样重复创建销毁的过程是特别耗费计算机的性能的及消耗时间的。
而数据库使用了数据库连接池后,就能达到Connection对象的复用,如下图
连接池是在一开始就创建好了一些连接(Connection)对象存储起来。用户需要连接数据库时,不需要自己创建连接,而只需要从连接池中获取一个连接进行使用,使用完毕后再将连接对象归还给连接池;这样就可以起到资源重用,也节省了频繁创建连接销毁连接所花费的时间,从而提升了系统响应的速度。
4.2 数据库连接池实现
-
标准接口:==DataSource==
官方(SUN) 提供的数据库连接池标准接口,由第三方组织实现此接口。该接口提供了获取连接的功能:
1
Connection getConnection()
那么以后就不需要通过
DriverManager
对象获取Connection
对象,而是通过连接池(DataSource)获取Connection
对象。 -
常见的数据库连接池
- DBCP
- C3P0
- Druid
我们现在使用更多的是Druid,它的性能比其他两个会好一些。
-
Druid(德鲁伊)
-
Druid连接池是阿里巴巴开源的数据库连接池项目
-
功能强大,性能优秀,是Java语言最好的数据库连接池之一
-
4.3 Driud使用
- 导入jar包 druid-1.1.12.jar
- 定义配置文件
- 加载配置文件
- 获取数据库连接池对象
- 获取连接
现在通过代码实现,首先需要先将druid的jar包放到项目下的lib下并添加为库文件
编写配置文件如下:
1 | driverClassName=com.mysql.jdbc.Driver |
使用druid的代码如下:
1 | /** |
Maven&MyBatis
1,Maven
Maven是专门用于管理和构建Java项目的工具,它的主要功能有:
-
提供了一套标准化的项目结构
-
提供了一套标准化的构建流程(编译,测试,打包,发布……)
-
提供了一套依赖管理机制
标准化的项目结构:
Maven提供了一套标准化的项目结构,所有的IDE使用Maven构建的项目完全一样,所以IDE创建的Maven项目可以通用。如下图右边就是Maven构建的项目结构。
标准化的构建流程:
如上图所示我们开发了一套系统,代码需要进行编译、测试、打包、发布,这些操作如果需要反复进行就显得特别麻烦,而Maven提供了一套简单的命令来完成项目构建。
依赖管理:
依赖管理其实就是管理你项目所依赖的第三方资源(jar包、插件)。如之前我们项目中需要使用JDBC和Druid的话,就需要去网上下载对应的依赖包,复制到项目中,还要将jar包加入工作环境这一系列的操作。如下图所示
而Maven使用标准的 ==坐标== 配置来管理各种依赖,只需要简单的配置就可以完成依赖管理。
如上图右边所示就是mysql驱动包的坐标,在项目中只需要写这段配置,其他都不需要我们担心,Maven都帮我们进行操作了。
1.1 Maven简介
==Apache Maven== 是一个项目管理和构建==工具==,它基于项目对象模型(POM)的概念,通过一小段描述信息来管理项目的构建、报告和文档。
1.1.1 Maven模型
- 项目对象模型 (Project Object Model)
- 依赖管理模型(Dependency)
- 插件(Plugin)
如上图所示就是Maven的模型,而我们先看紫色框框起来的部分,他就是用来完成 标准化构建流程
。如我们需要编译,Maven提供了一个编译插件供我们使用,我们需要打包,Maven就提供了一个打包插件提供我们使用等。
上图中紫色框起来的部分,项目对象模型就是将我们自己抽象成一个对象模型,有自己专属的坐标,如下图所示是一个Maven项目:
依赖管理模型则是使用坐标来描述当前项目依赖哪儿些第三方jar包,如下图所示
1.1.2 仓库
仓库分类:
-
本地仓库:自己计算机上的一个目录
-
中央仓库:由Maven团队维护的全球唯一的仓库
-
远程仓库(私服):一般由公司团队搭建的私有仓库
当项目中使用坐标引入对应依赖jar包后,首先会查找本地仓库中是否有对应的jar包:
-
如果有,则在项目直接引用;
-
如果没有,则去中央仓库中下载对应的jar包到本地仓库。
如果还可以搭建远程仓库,将来jar包的查找顺序则变为:
本地仓库 --> 远程仓库–> 中央仓库
1.2 Maven安装配置
这部分内容不展开写了,实在不懂的可以自行搜索
- 解压 apache-maven-3.6.1.rar 既安装完成
- 配置环境变量 MAVEN_HOME 为安装路径的bin目录
- 配置本地仓库
- 配置阿里云私服
1.3 Maven基本使用
1.3.1 Maven 常用命令
compile :编译
clean:清理
test:测试
package:打包
install:安装
这部分内容使用IEDA操作非常方便,这里的终端演示就不呈现了。
1.3.2 Maven 生命周期
Maven 构建项目生命周期描述的是一次构建过程经历经历了多少个事件
Maven 对项目构建的生命周期划分为3套:
- clean :清理工作。
- default :核心工作,例如编译,测试,打包,安装等。
- site : 产生报告,发布站点等。这套声明周期一般不会使用。
同一套生命周期内,执行后边的命令,前面的所有命令会自动执行。例如默认(default)生命周期如下:
当我们执行 install
(安装)命令时,它会先执行 compile
命令,再执行 test
命令,再执行 package
命令,最后执行 install
命令。
当我们执行 package
(打包)命令时,它会先执行 compile
命令,再执行 test
命令,最后执行 package
命令。
默认的生命周期也有对应的很多命令,其他的一般都不会使用,我们只关注常用的那五种即可。
1.4 IDEA使用Maven
1.4.1 IDEA配置Maven环境
我们需要先在IDEA中配置Maven环境:
-
选择 IDEA中 File --> Settings
-
搜索 maven
-
设置 IDEA 使用本地安装的 Maven,并修改配置文件路径
1.4.2 Maven 坐标详解
什么是坐标?
- Maven 中的坐标是==资源的唯一标识==
- 使用坐标来定义项目或引入项目中需要的依赖
Maven 坐标主要组成
- groupId:定义当前Maven项目隶属组织名称(通常是域名反写,例如:com.itheima)
- artifactId:定义当前Maven项目名称(通常是模块名称,例如 order-service、goods-service)
- version:定义当前项目版本号
如下图就是使用坐标表示一个项目:
1 | <groupId>com.alibaba</groupId> <!--隶属组织阿里巴巴--> |
==注意:==
- 上面所说的资源可以是插件、依赖、当前项目。
- 我们的项目如果被其他的项目依赖时,也是需要坐标来引入的。
1.4.3 IDEA 创建 Maven项目
-
创建模块,选择Maven,点击Next
-
填写模块名称,坐标信息,点击finish,创建完成
-
编写 HelloWorld,并运行
1.4.4 IDEA 导入 Maven项目
通过以下步骤进行项目的导入:
- 选择右侧Maven面板,点击 + 号
- 选中对应项目的pom.xml文件,双击即可
1.5 依赖管理
1.5.1 使用坐标引入jar包
使用坐标引入jar包的步骤:
-
在项目的 pom.xml 中编写
标签 -
在
标签中 使用 引入坐标 -
定义坐标的 groupId,artifactId,version
-
点击刷新按钮,使坐标生效
注意:
- 具体的坐标我们可以到如下网站进行搜索
- https://mvnrepository.com/
快捷方式导入jar包的坐标:
每次需要引入jar包,都去对应的网站进行搜索是比较麻烦的,接下来给大家介绍一种快捷引入坐标的方式
-
在 pom.xml 中 按 alt + insert,选择 Dependency
-
在弹出的面板中搜索对应坐标,然后双击选中对应坐标
-
点击刷新按钮,使坐标生效
自动导入设置:
上面每次操作都需要点击刷新按钮,让引入的坐标生效。当然我们也可以通过设置让其自动完成
-
选择 IDEA中 File --> Settings
-
在弹出的面板中找到 Build Tools
-
选择 Any changes,点击 ok 即可生效
1.5.2 依赖范围
通过设置坐标的依赖范围(scope),可以设置 对应jar包的作用范围:编译环境、测试环境、运行环境。
如下图所示给 junit
依赖通过 scope
标签指定依赖的作用范围。 那么这个依赖就只能作用在测试环境,其他环境下不能使用。
那么 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
持久层:
-
负责将数据到保存到数据库的那一层代码。
以后开发我们会将操作数据库的Java代码作为持久层。而Mybatis就是对jdbc代码进行了封装。
-
JavaEE三层架构:表现层、业务层、持久层
三层架构在后期会给大家进行讲解,今天先简单的了解下即可。
框架:
- 框架就是一个半成品软件,是一套可重用的、通用的、软件基础代码模型
- 在框架的基础之上构建软件编写更加高效、规范、通用、可扩展
2.2 Mybatis快速入门
需求:查询user表中所有的数据
-
创建user表,添加数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16create 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
<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
<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
9public 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
17public 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();
}
}
-
2.3 Mapper代理开发
2.3.1 Mapper代理开发概述
Mapper 代理方式的目的:
- 解决原生方式中的硬编码
- 简化后期执行SQL
2.3.2 使用Mapper代理要求
使用Mapper代理方式,必须满足以下要求:
-
定义与SQL映射文件同名的Mapper接口,并且将Mapper接口和SQL映射文件放置在同一目录下。如下图:
-
设置SQL映射文件的namespace属性为Mapper接口全限定名
-
在 Mapper 接口中定义方法,方法名就是SQL映射文件中sql语句的id,并保持参数类型和返回值类型一致
2.3.3 案例代码实现
-
在
com.itheima.mapper
包下创建 UserMapper接口,代码如下:1
2
3
4public 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 | <mappers> |
2.4 核心配置文件
2.4.1 多环境配置
在核心配置文件的 environments
标签中其实是可以配置多个 environment
,使用 id
给每段环境起名,在 environments
中使用 default='环境id'
来指定使用哪儿段配置。我们一般就配置一个 environment
即可。
1 | <environments default="development"> |
2.4.2 类型别名
在映射配置文件中的 resultType
属性需要配置数据封装的类型(类的全限定名)。而每次这样写是特别麻烦的,Mybatis 提供了 类型别名
(typeAliases) 可以简化这部分的书写。
首先需要现在核心配置文件中配置类型别名,也就意味着给pojo包下所有的类起了别名(别名就是类名),不区分大小写。内容如下:
1 | <typeAliases> |
通过上述的配置,我们就可以简化映射配置文件中 resultType
属性值的编写
1 | <mapper namespace="com.itheima.mapper.UserMapper"> |
Mybatis练习
一般的产品原型,里面都会包含品牌数据的 查询
、按条件查询
、添加
、删除
、批量删除
、修改
等功能,而这些功能其实就是对数据库表中的数据进行CRUD操作。接下来我们就使用Mybatis完成品牌数据的增删改查操作。以下是我们要完成功能列表:
- 查询
- 查询所有数据
- 查询详情
- 条件查询
- 添加
- 修改
- 修改全部字段
- 修改动态字段
- 删除
- 删除一个
- 批量删除
环境准备
- 数据库表(tb_brand)及数据准备
1 | -- 删除tb_brand表 |
- 实体类 Brand,在
com.itheima.pojo
包下创建 Brand 实体类。
1 | public class Brand { |
-
编写测试用例,测试代码需要在
test/java
目录下创建包及测试用例。 -
安装 MyBatisX 插件
-
MybatisX 是一款基于 IDEA 的快速开发插件,为效率而生。
-
主要功能
- XML映射配置文件 和 接口方法 间相互跳转
- 根据接口方法生成 statement
查询所有数据
实现该功能我们分以下步骤进行实现:
- 编写接口方法:Mapper接口
- 参数:无
查询所有数据功能是不需要根据任何条件进行查询的,所以此方法不需要参数。 - 结果:
List<Brand>
我们会将查询出来的每一条数据封装成一个Brand
对象,而多条数据封装多个Brand
对象,需要将这些对象封装到List集合中返回。 - 执行方法、测试
- 参数:无
编写接口方法
在 com.blog.mapper
包写创建名为 BrandMapper
的接口。并在该接口中定义 List<Brand> selectAll()
方法。
1 | package com.blog.mapper; |
编写SQL语句
在 reources
下创建 com/itheima/mapper
目录结构,并在该目录下创建名为 BrandMapper.xml
的映射配置文件
1 |
|
编写测试方法
在 MybatisTest
类中编写测试查询所有的方法
1 |
|
执行测试方法部分结果如下
Brand{id=1, brandName='null', companyName='null', ordered=5, description='好吃不上火', status=0
其中brandName和companyName的值为NULL,究其原因是我们Brand类中的属性名和MySQL中的列名不一致。
这个问题我在上篇JDBC文章中给出了一个比较初级的解决方案,那就是给列取别名,令别名与我们Java中的命名方式相同,这样我们就可以通过获取列的别名,来进行反射赋值。
另一种解决方式是使用resultMap定义字段和属性的映射关系(推荐)
起别名解决上述问题
我们可以在写sql语句时给这两个字段起别名,将别名定义成和属性名一致即可。
1 | <select id="selectAll" resultType="brand"> |
而上面的SQL语句中的字段列表书写麻烦,如果表中还有更多的字段,同时其他的功能也需要查询这些字段时就显得我们的代码不够精炼。Mybatis提供了sql
片段可以提高sql的复用性。
SQL片段:
- 将需要复用的SQL片段抽取到
sql
标签中
1 | <sql id="brand_column"> |
id属性值是唯一标识,引用时也是通过该值进行引用。
- 在原sql语句中进行引用
使用include
标签引用上述的 SQL 片段,而refid
指定上述 SQL 片段的id值
1 | <select id="selectAll" resultType="brand"> |
使用resultMap解决上述问题
起别名 + sql片段的方式可以解决上述问题,但是它也存在问题。如果还有功能只需要查询部分字段,而不是查询所有字段,那么我们就需要再定义一个 SQL 片段,这就显得不是那么灵活。
那么我们也可以使用resultMap来定义字段和属性的映射关系的方式解决上述问题。
- 在映射配置文件中使用resultMap定义
字段
和属性
的映射关系
1 | <!--id用来标识唯一的resultMap,type表示映射类型--> |
注意:在上面只需要定义 字段名 和 属性名 不一样的映射,而一样的则不需要专门定义出来。
- SQL语句正常编写
1 | <!--使用resultMap属性替换掉原有的resultType属性--> |
查询详情
查看详情功能实现步骤:
- 编写接口方法:Mapper接口
- 参数:id
查看详情就是查询某一行数据,所以需要根据id进行查询。而id以后是由页面传递过来。 - 结果:Brand
根据id查询出来的数据只要一条,而将一条数据封装成一个Brand对象即可
- 参数:id
- 编写SQL语句:SQL映射文件
- 执行方法、进行测试
编写接口方法
在 BrandMapper
接口中定义根据id查询数据的方法
1 | Brand selectById(int id); |
编写SQL语句
在 BrandMapper.xml
映射配置文件中编写 statement
,使用 resultMap
而不是使用 resultType
1 | <select id="selectById" resultMap="brandResultMapper"> |
编写测试方法
在 test/java
下的 com.itheima.mapper
包下的 MybatisTest类中
定义测试方法
1 |
|
参数占位符
mybatis提供了两种参数占位符:
#{}
:执行SQL时,会将 #{} 占位符替换为?,将来自动设置参数值。#{}
底层使用的是PreparedStatement
${}
:拼接SQL。底层使用的是Statement
,会存在SQL注入问题。
以后开发我们使用 #{} 参数占位符。
parameterType使用
对于有参数的mapper接口方法,我们在映射配置文件中应该配置 ParameterType
来指定参数类型。只不过该属性都可以省略。
1 | <select id="selectById" parameterType="int" resultMap="brandResultMap"> |
SQL语句中特殊字段处理
在xml中,”<”、”>”、”&”等字符是不能直接存入的,否则xml语法检查时会报错,如果想在xml中使用这些符号,必须将其转义为实体,如<
、>
、&
,这样才能保存进xml文档。或者使用<![CDATA[]]>
,被这个标记所包含的内容将表示为纯文本
但是严格来说,在XML中只有”<”和”&”是非法的,其它三个都是可以合法存在的,但是,把它们都进行转义是一个好的习惯。
不管怎么样,转义前的字符也好,转义后的字符也好,都会被xml解析器解析,为了方便起见,使用<![CDATA[]]>
来包含不被xml解析器解析的内容。但要注意的是:
- 此部分不能再包含
]]>
; - 不允许嵌套使用;
]]>
这部分不能包含空格或者换行。
最后,说说<![CDATA[]]>
和xml转移字符的关系,它们两个看起来是不是感觉功能重复了?
-
是的,它们的功能就是一样的,只是应用场景和需求有些不同:
<![CDATA[]]>
不能适用所有情况,转义字符可以;- 对于短字符串
<![CDATA[]]>
写起来啰嗦,对于长字符串转义字符写起来可读性差; <![CDATA[]]>
表示xml解析器忽略解析,所以更快。
-
转义字符
-
CDATA
1 | <select id="selectById" resultMap="brandResultMapper"> |
多条件查询
在实际问题中,我们经常会遇到如上图所示的多条件查询,将多条件查询的结果展示在下方的数据列表中。而我们做这个功能需要分析最终的SQL语句应该是什么样,思考两个问题
- 条件表达式
- 如何连接
当前状态使用status
字段表示,企业名称使用company_name
表示,品牌名称使用brand_name
表示
条件字段 企业名称
和 品牌名称
需要进行模糊查询,所以条件应该是:
1 | select * |
简单的分析后,我们来看功能实现的步骤:
- 编写接口方法
- 参数:所有查询条件
- 结果:
List<Brand>
- 在映射配置文件中编写SQL语句
- 编写测试方法并执行
编写接口方法
在 BrandMapper
接口中定义多条件查询的方法。
而该功能有三个参数,我们就需要考虑定义接口时,参数应该如何定义。Mybatis针对多参数有多种实现
- 使用
@Param( "SQL参数占位符名称")
标记每一个参数,在映射配置文件中就需要使用#{参数名称}
进行占位
1 | List<Brand> selectByCondition(int status, String companyName, String brandName); |
- 将多个参数封装成一个
实体对象
,将该实体对象作为接口的方法参数。该方式要求在映射配置文件的SQL中使用#{内容}
时,里面的内容必须和实体类属性名保持一致。
1 | List<Brand> selectByCondition(Brand brand); |
- 将多个参数封装到map集合中,将map集合作为接口的方法参数。该方式要求在映射配置文件的SQL中使用
#{内容}
时,里面的内容必须和map集合中键的名称一致。map.put(“SQL参数占位符名称”,数值);
1 | List<Brand> selectByCondition(Map map); |
编写SQL语句
在 BrandMapper.xml
映射配置文件中编写 statement
,注意使用 resultMap
替换 resultType
1 | <select id="selectByCondition" resultMap="brandResultMap"> |
编写测试方法
在 test/java
下的 com.blog.mapper
包下的 MybatisTest类中
定义测试方法
- Param注解
- 封装成对象
- 封装到map集合
1 |
|
现在的这种查询方式,只有当用户把三个参数都填上的时候才能查询出来,如果另外两个参数没有填,那么SQL语句就会变成
1 | where status = null and company_name = null and brand_name = "%华为%" |
这样显然是不会查询成功的,下面我们来进行优化
动态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 标签:
- if 标签:条件判断
- test 属性:逻辑表达式
1 | <select id="selectByCondition" resultMap="brandResultMapper"> |
如上的这种SQL语句就会根据传递的参数值进行动态的拼接。如果此时status和companyName有值那么就会值拼接这两个条件。SQL语句将变成
1 | select * from tb_brand where status = ? and company_name like ? |
但如果我们只给companyName这一个参数,那么SQL语句会变成下面这样
1 | select * from tb_brand where and company_name like ? |
WHERE关键字后面直接跟了个AND,变成了一条错误的SQL语句,那么最笨的一个解决方案就是在where后面先接一个恒等式
1 | select * from tb_brand where 1 = 1 and company_name like ? |
但MyBatis也料想到了这种情况,所以MyBatis又提供了一个where标签
- where 标签
- 作用:
- 替换where关键字
- 会动态的去掉第一个条件前的 and 或 or
- 如果所有的参数没有值则不加where关键字
- 作用:
1 | <select id="selectByCondition" resultMap="brandResultMapper"> |
单个条件(动态SQL)
如上图所示,在查询时只能选择 品牌名称
、当前状态
、企业名称
这三个条件中的一个,但是用户到底选择哪儿一个,我们并不能确定。这种就属于单个条件的动态SQL语句。
这种需求需要使用到 choose(when,otherwise)标签
实现,分别对应Java中的swtich
,case
,default
编写接口方法
在 BrandMapper
接口中定义单条件查询的方法。
1 | List<Brand> selectByConditionSingle(Brand brand); |
编写SQL语句
在 BrandMapper.xml
映射配置文件中编写 statement
,使用 resultMap
替换 resultType
将where替换成<where>
标签,这样当我们没有选中任何查询方式时,会自动帮我们去掉where,从而查询所有数据
或者保持where不变,在choose中添加<otherwise>
标签,在其中写入一个恒等式,这样当没有选中任何查询方式时,SQL语句会变成select * from tb_brand where true
,同样实现查询所有数据的效果,但还是推荐前者的方式
1 | <select id="selectByConditionSingle" resultType="com.blog.pojo.Brand"> |
编写测试方法
在 test/java
下的 com.blog.mapper
包下的 MybatisTest类中
定义测试方法
1 |
|
其实我在测试的时候,一直出现Preparing: select * from tb_brand WHERE status = ?
,也就是这个status有值啊,哪儿来的值呢?
想了半天发现可能是构造器的status默认值为0吧,因为我那会儿吧status设为的int类型,所以默认值为0,随后恍然大悟,把status的类型改为Integer就行了,以前还真没注意过这个
添加数据
实际开发中,添加数据时会有一个图形化界面,而我们在该页面输入想要的数据后添加 提交
按钮,就会将这些数据添加到数据库中。接下来我们就来实现添加数据的操作。
- 编写接口方法
- 参数:除了id之外的所有的数据。id对应的是表中主键值,而主键我们是 自动增长 生成的。
- 编写SQL语句
- 编写测试方法并执行
编写接口方法
在 BrandMapper
接口中定义添加方法
1 | void add(Brand brand); |
编写SQL语句
在 BrandMapper.xml
映射配置文件中编写添加数据的 statement
1 | <insert id="add" > |
编写测试方法
在 test/java
下的 com.blog.mapper
包下的 MybatisTest类中
定义测试方法
1 |
|
在第2步获取SqlSession对象时,默认是不会自动提交事务的,我们可以在openSession方法中加上true,这样就能自动提交事务了,不用手动调用commit方法
1 | //SqlSession sqlSession = sqlSessionFactory.openSession(true); //设置自动提交事务,这种情况不需要手动提交事务了 |
添加-主键返回
在接收参数的时候,我们没有接收id的参数,而是利用数据库主键自增长来自动赋值,但有时候我们又需要获取这个自增长的id。
解决方案
- 在 insert 标签上添加如下属性:
- useGeneratedKeys:是够获取自动增长的主键值。true表示获取
- keyProperty :指定将获取到的主键值封装到哪儿个属性里
1 | <insert id="add" useGeneratedKeys="true" keyProperty="id"> |
添加主键返回之后,我们再来测试一下
1 |
|
修改
编写接口方法
在 BrandMapper
接口中定义修改方法。int获取修改的行数
1 | int update(Brand brand); |
编写SQL语句
在 BrandMapper.xml
映射配置文件中编写修改数据的 statement
。
1 | <update id="update"> |
编写测试方法
在 test/java
下的 com.blog.mapper
包下的 MybatisTest类中
定义测试方法
1 |
|
此种修改方式要改只能全部改,如果没有给某一个字段赋值,那么修改之后的值就是null,十分的不方便,所以我们要将其优化成动态的修改字段
修改动态字段
解决方案跟上面的类似,也是用if标签来判断用户的输入,然后用set标签来删除额外的逗号(上面是用where标签去除and或or),防止出现SQL语法错误
1 | <update id="update"> |
删除一行数据
我们在App网购的时候,购物车里都会有删除
按钮,,当用户点击了该按钮,就会将改行数据删除掉。那我们就需要思考,这种删除是根据什么进行删除呢?是通过主键id删除,因为id是表中数据的唯一标识。
接下来就来实现该功能。
编写接口方法
在 BrandMapper
接口中定义根据id删除方法。
1 | void deleteById(int id); |
编写SQL语句
在 BrandMapper.xml
映射配置文件中编写删除一行数据的 statement
1 | <delete id="deleteById"> |
编写测试方法
在 test/java
下的 com.blog.mapper
包下的 MybatisTest类中
定义测试方法
1 |
|
批量删除
我们在删除购物车订单的时候,都会有个多选按钮,可以选中多条记录进行删除,下面我们来实现这个功能
编写接口方法
在 BrandMapper
接口中定义删除多行数据的方法。
1 | // 参数是一个数组,数组中存储的是多条数据的id |
编写SQL语句
在 BrandMapper.xml
映射配置文件中编写删除多条数据的 statement
。
编写SQL时需要遍历数组来拼接SQL语句。Mybatis 提供了 foreach
标签供我们使用
foreach 标签
用来迭代任何可迭代的对象(如数组,集合)。
- collection 属性:
- mybatis会将数组参数,封装为一个Map集合。
- 默认:array = 数组
- 使用@Param注解改变map集合的默认key的名称
- mybatis会将数组参数,封装为一个Map集合。
- item 属性:本次迭代获取到的元素。
- separator 属性:集合项迭代之间的分隔符。
foreach
标签不会错误地添加多余的分隔符。也就是最后一次迭代不会加分隔符。 - open 属性:该属性值是在拼接SQL语句之前拼接的语句,只会拼接一次
- close 属性:该属性值是在拼接SQL语句拼接后拼接的语句,只会拼接一次
1 | <delete id="deleteByIds"> |
假如数组中的id数据是{1,2,3},那么拼接后的sql语句就是:delete from tb_brand where id in (1,2,3);
编写测试方法
在 test/java
下的 com.blog.mapper
包下的 MybatisTest类中
定义测试方法
1 |
|
MyBatis参数传递
Mybatis 接口方法中可以接收各种各样的参数,如下:
- 多个参数
- 单个参数:单个参数又可以是如下类型
- POJO 类型
- Map 集合类型
- Collection 集合类型
- List 集合类型
- Array 类型
- 其他类型
多个参数
如下面的代码,就是接收两个参数,而接收多个参数需要使用 @Param
注解,那么为什么要加该注解呢?这个问题要弄明白就必须来研究Mybatis的底层对于这些参数是如何处理的。
1 | User select(; String username, String password) |
我们在接口方法中定义多个参数,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 | <select id="select" resultType="user"> |
运行代码结果如下
1 | [DEBU6][main] c.i.m.0.select- ==> Preparing: SELECT * FROM tb_user WHERE username = ? AND PASSWORD = ? |
在映射配合文件的SQL语句中使用用 arg
开头的和 param
书写,代码的可读性会变的特别差,此时可以使用 @Param
注解。
在接口方法参数上使用 @Param
注解,Mybatis 会将 arg
开头的键名替换为对应注解的属性值。
以后接口参数是多个时,在每个参数上都使用 @Param
注解。这样代码的可读性更高。
单个参数
-
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类型,
参数占位符名称
叫什么都可以。尽量做到见名知意
注解实现CURD
使用注解开发会比配置文件开发更加方便。如下就是使用注解进行开发
1 |
|
注解是用来替换映射配置文件方式配置的,所以使用了注解,就不需要再映射配置文件中书写对应的 statement
Mybatis 针对 CURD 操作都提供了对应的注解,已经做到见名知意。如下:
- 查询 :@Select
- 添加 :@Insert
- 修改 :@Update
- 删除 :@Delete
接下来我们做一个案例来使用 Mybatis 的注解开发
-
代码实现:
- 将之前案例中
UserMapper.xml
中的 根据id查询数据 的statement
删掉 - 在
UserMapper
接口的selectById
方法上添加注解
1
2
User selectById(int id);- 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void testSelect() throws IOException {
//接收参数
int id = 2;
//1. 获取SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession(true);
//3. 获取Mapper接口的代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//4. 执行方法
User user = userMapper.selectById(id);
System.out.println(user);
//5. 释放资源
sqlSession.close();
} - 将之前案例中
注意在官方文档中 入门
中有这样的一段话:
使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。
结论:注解完成简单功能,配置文件完成复杂功能。
HTTP&Tomcat&Servlet
1,Web概述
1.1 Web和JavaWeb的概念
Web是全球广域网,也称为万维网(www),能够通过浏览器访问的网站。
在我们日常的生活中,经常会使用浏览器去访问百度
、知乎
、推特
等这些网站,这些网站统称为Web网站。
我们知道了什么是Web,那么JavaWeb又是什么呢?顾名思义JavaWeb就是用Java技术来解决相关Web互联网领域的技术栈。
1.2 JavaWeb技术栈
了解JavaWeb技术栈之前,有一个很重要的概念要介绍。
1.2.1 B/S架构
什么是B/S架构?
B/S 架构:Browser/Server,浏览器/服务器 架构模式,它的特点是,客户端只需要浏览器,应用程序的逻辑和数据都存储在服务器端。浏览器只需要请求服务器,获取Web资源,服务器把Web资源发送给浏览器即可。
- 打开浏览器访问百度首页,输入要搜索的内容,点击回车或百度一下,就可以获取和搜索相关的内容
- 思考下搜索的内容并不在我们自己的点上,那么这些内容从何而来?答案很明显是从百度服务器返回给我们的
- 日常百度的小细节,逢年过节百度的logo会更换不同的图片,服务端发生变化,客户端不需做任务事情就能获取最新内容
- 所以说B/S架构的好处:易于维护升级:服务器端升级后,客户端无需任何部署就可以使用到新的版本。
了解了什么是B/S架构后,作为后台开发工程师的我们将来主要关注的是服务端的开发和维护工作。在服务端将来会放很多资源,都有哪些资源呢?
1.2.2 静态资源
- 静态资源主要包含HTML、CSS、JavaScript、图片等,主要负责页面的展示。
- 我们之前已经学过前端网页制作
三剑客
(HTML+CSS+JavaScript),使用这些技术我们就可以制作出效果比较丰富的网页,将来展现给用户。但是由于做出来的这些内容都是静态的,这就会导致所有的人看到的内容将是一模一样。 - 在日常上网的过程中,我们除了看到这些好看的页面以外,还会碰到很多动态内容,比如我们常见的百度登录效果:
张三
登录以后在网页的右上角看到的是张三
,而李四
登录以后看到的则是李四
。所以不同的用户访问相同的资源看到的内容大多数是不一样的,要想实现这样的效果,光靠静态资源是无法实现的。
1.2.3 动态资源
- 动态资源主要包含Servlet、JSP等,主要用来负责逻辑处理。
- 动态资源处理完逻辑后会把得到的结果交给静态资源来进行展示,动态资源和静态资源要结合一起使用。
- 动态资源虽然可以处理逻辑,但是当用户来登录百度的时候,就需要输入
用户名
和密码
,这个时候我们就又需要解决的一个问题是,用户在注册的时候填入的用户名和密码、以及我们经常会访问到一些数据列表的内容展示(如下图所示),这些数据都存储在哪里?我们需要的时候又是从哪里来取呢?
1.2.4 数据库
- 浏览器发送一个请求到服务端,去请求所需要的相关资源;
- 资源分为动态资源和静态资源,动态资源可以是使用Java代码按照Servlet和JSP的规范编写的内容;
- 在Java代码可以进行业务处理也可以从数据库中读取数据;
- 拿到数据后,把数据交给HTML页面进行展示,再结合CSS和JavaScript使展示效果更好;
- 服务端将静态资源响应给浏览器;
- 浏览器将这些资源进行解析;
- 解析后将效果展示在浏览器,用户就可以看到最终的结果。
1.2.5 HTTP协议
- HTTP协议:主要定义通信规则
- 浏览器发送请求给服务器,服务器响应数据给浏览器,这整个过程都需要遵守一定的规则,之前学习过TCP、UDP,这些都属于规则,这里我们需要使用的是HTTP协议,这也是一种规则。
1.2.6 Web服务器
- Web服务器:负责解析 HTTP 协议,解析请求数据,并发送响应数据
- 浏览器按照HTTP协议发送请求和数据,后台就需要一个Web服务器软件来根据HTTP协议解析请求和数据,然后把处理结果再按照HTTP协议发送给浏览器
- Web服务器软件有很多,本文使用的是最为常用的
Tomcat
服务器
2, HTTP
2.1 简介
HTTP概念
HyperText Transfer Protocol,超文本传输协议,规定了浏览器和服务器之间数据传输的规则
。
- 数据传输的规则指的是请求数据和响应数据需要按照指定的格式进行传输。
- 如果想知道具体的格式,可以打开浏览器,点击
F12
打开开发者工具,点击Network
来查看某一次请求的请求数据和响应数据具体的格式内容
注意:在浏览器中如果看不到上述内容,需要清除浏览器的浏览数据。chrome浏览器可以使用ctrl+shift+Del进行清除。
HTTP协议特点
HTTP协议有它自己的一些特点,分别是:
-
基于TCP协议: 面向连接,安全
TCP是一种面向连接的(建立连接之前是需要经过三次握手)、可靠的、基于字节流的传输层通信协议,在数据传输方面更安全。
-
基于请求-响应模型的:一次请求对应一次响应
请求和响应是一一对应关系
-
HTTP协议是无状态协议:对于事物处理没有记忆能力。每次请求-响应都是独立的
无状态指的是客户端发送HTTP请求给服务端之后,服务端根据请求响应数据,响应完后,不会记录任何信息。这种特性有优点也有缺点,
- 缺点:多次请求间不能共享数据
- 优点:速度快
请求之间无法共享数据会引发的问题,如:
- 京东购物,
加入购物车
和去购物车结算
是两次请求, - HTTP协议的无状态特性,加入购物车请求响应结束后,并未记录加入购物车是何商品
- 发起去购物车结算的请求后,因为无法获取哪些商品加入了购物车,会导致此次请求无法正确展示数据
具体使用的时候,我们发现京东是可以正常展示数据的,原因是Java早已考虑到这个问题,并提出了使用
会话技术(Cookie、Session)
来解决这个问题。具体如何来做,我们后面会详细讲到。刚才提到HTTP协议是规定了请求和响应数据的格式,那具体的格式是什么呢?
2.2 请求数据格式
2.2.1 格式介绍
请求数据总共分为三部分内容,分别是请求行
、请求头
、请求体
-
请求行: HTTP请求中的第一行数据,请求行包含三块内容,分别是 GET[请求方式] /[请求URL路径] HTTP/1.1[HTTP协议及版本]
请求方式有七种,最常用的是GET和POST
-
请求头: 第二行开始,格式为key: value形式
请求头中会包含若干个属性,常见的HTTP请求头有:
1
2
3
4
5Host: 表示请求的主机名
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请求的最后一部分,存储请求参数
-
GET请求请求参数在请求行中,没有请求体,POST请求请求参数在请求体中
-
GET请求请求参数大小有限制,POST没有
2.3 响应数据格式
2.3.1 格式介绍
响应数据总共分为三部分内容,分别是响应行
、响应头
、响应体
-
响应行:响应数据的第一行,响应行包含三块内容,分别是 HTTP/1.1[HTTP协议及版本] 200[响应状态码] ok[状态码的描述]
-
响应头:第二行开始,格式为key:value形式
响应头中会包含若干个属性,常见的HTTP响应头有:
1
2
3
4Content-Type:表示该响应内容的类型,例如text/html,image/jpeg;
Content-Length:表示该响应内容的长度(字节数);
Content-Encoding:表示该响应压缩算法,例如gzip;
Cache-Control:指示客户端应如何缓存,例如max-age=300表示可以最多缓存300秒 -
响应体: 最后一部分。存放响应数据
上图中…这部分内容就是响应体,它和响应头之间有一个空行隔开。
2.3.2 响应状态码
- 状态码大类
状态码分类 | 说明 |
---|---|
1xx | 响应中——临时状态码,表示请求已经接受,告诉客户端应该继续请求或者如果它已经完成则忽略它 |
2xx | 成功——表示请求已经被成功接收,处理已完成 |
3xx | 重定向——重定向到其它地方:它让客户端再发起一个请求以完成整个处理。 |
4xx | 客户端错误——处理发生错误,责任在客户端,如:客户端的请求一个不存在的资源,客户端未被授权,禁止访问等 |
5xx | 服务器端错误——处理发生错误,责任在服务端,如:服务端抛出异常,路由出错,HTTP版本不支持等 |
状态码大全:https://cloud.tencent.com/developer/chapter/13553
- 常见的响应状态码
状态码 | 英文描述 | 解释 |
---|---|---|
200 | OK |
客户端请求成功,即处理成功,这是我们最想看到的状态码 |
302 | Found |
指示所请求的资源已移动到由Location 响应头给定的 URL,浏览器会自动重新访问到这个页面 |
304 | Not Modified |
告诉客户端,你请求的资源至上次取得后,服务端并未更改,你直接用你本地缓存吧。隐式重定向 |
400 | Bad Request |
客户端请求有语法错误,不能被服务器所理解 |
403 | Forbidden |
服务器收到请求,但是拒绝提供服务,比如:没有权限访问相关资源 |
404 | Not Found |
请求资源不存在,一般是URL输入有误,或者网站资源被删除了 |
428 | Precondition Required |
服务器要求有条件的请求,告诉客户端要想访问该资源,必须携带特定的请求头 |
429 | Too Many Requests |
太多请求,可以限制客户端请求某个资源的数量,配合 Retry-After(多长时间后可以请求)响应头一起使用 |
431 | Request Header Fields Too Large |
请求头太大,服务器不愿意处理请求,因为它的头部字段太大。请求可以在减少请求头域的大小后重新提交。 |
405 | Method Not Allowed |
请求方式有误,比如应该用GET请求方式的资源,用了POST |
500 | Internal Server Error |
服务器发生不可预期的错误。服务器出异常了,赶紧看日志去吧 |
503 | Service Unavailable |
服务器尚未准备好处理请求,服务器刚刚启动,还未初始化好 |
511 | Network Authentication Required |
客户端需要进行身份验证才能获得网络访问权限 |
3, Tomcat
3.1 简介
3.1.1 什么是Web服务器
Web服务器是一个应用程序(软件
),对HTTP协议的操作进行封装,使得程序员不必直接对协议进行操作,让Web开发更加便捷。主要功能是"提供网上信息浏览服务"。
Web服务器是安装在服务器端的一款软件,将来我们把自己写的Web项目部署到Web Tomcat服务器软件中,当Web服务器软件启动后,部署在Web服务器软件中的页面就可以直接通过浏览器来访问了。
Web服务器软件使用步骤
- 准备静态资源
- 下载安装Web服务器软件
- 将静态资源部署到Web服务器上
- 启动Web服务器使用浏览器访问对应的资源
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/ 从官网上可以下载对应的版本进行使用。
关于IDEA创建Maven Web项目和在Tomcat的使用这里就不过多介绍了,各位自行搜索即可
4, Servlet
4.1 简介
-
Servlet是JavaWeb最为核心的内容,它是Java提供的一门
动态
web资源开发技术。 -
使用Servlet就可以实现,根据不同的登录用户在页面上动态显示不同内容。
-
Servlet是JavaEE规范之一,其实就是一个接口,将来我们需要定义Servlet类实现Servlet接口,并由web服务器运行Servlet
4.2 快速入门
需求分析: 编写一个Servlet类,并使用IDEA中Tomcat插件进行部署,最终通过浏览器访问所编写的Servlet程序。
具体的实现步骤为:
- 创建Web项目
web-demo
,导入Servlet依赖坐标
1 | <dependency> |
- 创建:定义一个类,实现Servlet接口,并重写接口中所有方法,并在service方法中输入一句话
1 | package com.itheima.web; |
- 配置:在类上使用@WebServlet注解,配置该Servlet的访问路径
1 |
- 访问:启动Tomcat,浏览器中输入URL地址访问该Servlet
1 | http://localhost:8080/web-demo/demo1 |
- 浏览器访问后,在控制台会打印
servlet hello world~
说明servlet程序已经成功运行。
4.3 执行流程
Servlet程序已经能正常运行,但是我们需要思考个问题: 我们并没有创建ServletDemo1类的对象,也没有调用对象中的service方法,为什么在控制台就打印了servlet hello world~
这句话呢?
要想回答上述问题,我们就需要对Servlet的执行流程进行一个学习。
- 浏览器发出
http://localhost:8080/web-demo/demo1
请求,从请求中可以解析出三部分内容,分别是localhost:8080
、web-demo
、demo1
- 根据
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的执行流程,需要大家掌握两个问题:
- Servlet由谁创建?Servlet方法由谁调用?
Servlet由web服务器创建,Servlet方法由web服务器调用
- 服务器怎么知道Servlet中一定有service方法?
因为我们自定义的Servlet,必须实现Servlet接口并复写其方法,而Servlet接口中有service方法
4.4 生命周期
介绍完Servlet的执行流程后,我们知道Servlet是由Tomcat Web服务器帮我们创建的。
接下来咱们再来思考一个问题:Tomcat什么时候创建的Servlet对象?
要想回答上述问题,我们就需要对Servlet的生命周期进行一个学习。
-
生命周期: 对象的生命周期指一个对象从被创建到被销毁的整个过程。
-
Servlet运行在Servlet容器(web服务器)中,其生命周期由容器来管理,分为4个阶段:
加载和实例化
:默认情况下,当Servlet第一次被访问时,由容器创建Servlet对象
1
2
3
4
5
6默认情况,Servlet会在第一次访问被容器创建,但是如果创建Servlet比较耗时的话,那么第一个访问的人等待的时间就比较长,用户的体验就比较差,那么我们能不能把Servlet的创建放到服务器启动的时候来创建,具体如何来配置?
@WebServlet(urlPatterns = "/demo1",loadOnStartup = 1)
loadOnstartup的取值有两类情况
(1)负整数:第一次访问时创建Servlet对象
(2)0或正整数:服务器启动时创建Servlet对象,数字越小优先级越高初始化
:在Servlet实例化之后,容器将调用Servlet的init()
方法初始化这个对象,完成一些如加载配置文件、创建连接等初始化的工作。该方法只调用一次
请求处理
:每次
请求Servlet时,Servlet容器都会调用Servlet的service()
方法对请求进行处理服务终止
:当需要释放内存或者容器关闭时,容器就会调用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
54package com.itheima.web;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
/**
* Servlet生命周期方法
*/
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方法被执行?
在Terminal命令行中,先使用
mvn tomcat7:run
启动,然后再使用ctrl+c
关闭tomcat
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 | String getServletInfo() |
- 获取ServletConfig对象
1 | ServletConfig getServletConfig() |
ServletConfig对象,在init方法的参数中有,而Tomcat Web服务器在创建Servlet对象的时候会调用init方法,必定会传入一个ServletConfig对象,我们只需要将服务器传过来的ServletConfig进行返回即可。具体如何操作?
1 | package com.itheima.web; |
getServletInfo()和getServletConfig()这两个方法使用的不是很多,大家了解下。
4.6 体系结构
通过上面的学习,我们知道要想编写一个Servlet就必须要实现Servlet接口,重写接口中的5个方法,虽然已经能完成要求,但是编写起来还是比较麻烦的,因为我们更关注的其实只有service方法,那有没有更简单方式来创建Servlet呢?
要想解决上面的问题,我们需要先对Servlet的体系结构进行下了解:
因为我们将来开发B/S架构的web项目,都是针对HTTP协议,所以我们自定义Servlet,会通过继承HttpServle
具体的编写格式如下:
1 |
|
- 要想发送一个GET请求,请求该Servlet,只需要通过浏览器发送
http://localhost:8080/web-demo/demo4
,就能看到doGet方法被执行了 - 要想发送一个POST请求,请求该Servlet,单单通过浏览器是无法实现的,这个时候就需要编写一个form表单来发送请求,在webapp下创建一个
a.html
页面,内容如下:
1 |
|
启动测试,即可看到doPost方法被执行了。
Servlet的简化编写就介绍完了,接着需要思考两个问题:
- HttpServlet中为什么要根据请求方式的不同,调用不同的方法?
- 如何调用?
针对问题一,我们需要回顾之前的知识点前端发送GET和POST请求的时候,参数的位置不一致,GET请求参数在请求行中,POST请求参数在请求体中
,为了能处理不同的请求方式,我们得在service方法中进行判断,然后写不同的业务处理,这样能实现,但是每个Servlet类中都将有相似的代码,针对这个问题,有什么可以优化的策略么?
1 | package com.itheima.web; |
要解决上述问题,我们可以对Servlet接口进行继承封装,来简化代码开发。
1 | package com.itheima.web; |
有了MyHttpServlet这个类,以后我们再编写Servlet类的时候,只需要继承MyHttpServlet,重写父类中的doGet和doPost方法,就可以用来处理GET和POST请求的业务逻辑。接下来,可以把ServletDemo5代码进行改造
1 |
|
将来页面发送的是GET请求,则会进入到doGet方法中进行执行,如果是POST请求,则进入到doPost方法。这样代码在编写的时候就相对来说更加简单快捷。
类似MyHttpServlet这样的类Servlet中已经为我们提供好了,就是HttpServlet,翻开源码,大家可以搜索service()
方法,你会发现HttpServlet做的事更多,不仅可以处理GET和POST还可以处理其他五种请求方式。
1 | protected void service(HttpServletRequest req, HttpServletResponse resp) |
4.7 urlPattern配置
Servlet类编写好后,要想被访问到,就需要配置其访问路径(urlPattern
)
-
一个Servlet,可以配置多个urlPattern
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package com.itheima.web;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
/**
* urlPattern: 一个Servlet可以配置多个访问路径
*/
public class ServletDemo7 extends MyHttpServlet {
protected void doGet(ServletRequest req, ServletResponse res) {
System.out.println("demo7 get...");
}
protected void doPost(ServletRequest req, ServletResponse res) {
}
}在浏览器上输入
http://localhost:8080/web-demo/demo7
,http://localhost:8080/web-demo/demo8
这两个地址都能访问到ServletDemo7的doGet方法。
-
urlPattern配置规则
- 精确匹配
1
访问路径
http://localhost:8080/web-demo/user/select
- 目录匹配
1
访问路径
http://localhost:8080/web-demo/user/任意
- 扩展名匹配
1
//注意这里没加斜杠
访问路径
http://localhost:8080/web-demo/任意.do
- 任意匹配
1
2
3
//或访问路径
http://localhost:8080/demo-web/任意
注意:``/
和/*
的区别?
-
当我们的项目中的Servlet配置了 “/”,会覆盖掉tomcat中的DefaultServlet,当其他的url-pattern都匹配不上时都会走这个Servlet
-
当我们的项目中配置了"/*",意味着匹配任意访问路径
-
DefaultServlet是用来处理静态资源,如果配置了"/"会把默认的覆盖掉,就会引发请求静态资源的时候没有走默认的而是走了自定义的Servlet类,最终导致静态资源不能被访问
小结
-
urlPattern总共有四种配置方式,分别是精确匹配、目录匹配、扩展名匹配、任意匹配
-
五种配置的优先级为 精确匹配 > 目录匹配> 扩展名匹配 > /* > / ,无需记,以最终运行结果为准。
4.8 XML配置
前面对应Servlet的配置,我们都使用的是@WebServlet,这个是Servlet从3.0版本后开始支持注解配置,3.0版本前只支持XML配置文件的配置方法。
对于XML的配置步骤有两步:
- 编写Servlet类
1 | package com.itheima.web; |
- 在web.xml中配置该Servlet
1 |
|
这种配置方式和注解比起来,确认麻烦很多,所以建议大家使用注解来开发。但是大家要认识上面这种配置方式,因为并不是所有的项目都是基于注解开发的。
Request&Response
1,Request和Response的概述
Request是请求对象,Response是响应对象。
这两个对象在我们使用Servlet的时候有看到:
1 |
|
此时,我们就需要思考一个问题request和response这两个参数的作用是什么?
- request:
获取
请求数据- 浏览器会发送HTTP请求到后台服务器[Tomcat]
- HTTP的请求中会包含很多请求数据[请求行+请求头+请求体]
- 后台服务器[Tomcat]会对HTTP请求中的数据进行解析并把解析结果存入到一个对象中
- 所存入的对象即为request对象,所以我们可以从request对象中获取请求的相关参数
- 获取到数据后就可以继续后续的业务,比如获取用户名和密码就可以实现登录操作的相关业务
- response:
设置
响应数据- 业务处理完后,后台就需要给前端返回业务处理的结果即响应数据
- 把响应数据封装到response对象中
- 后台服务器[Tomcat]会解析response对象,按照[响应行+响应头+响应体]格式拼接结果
- 浏览器最终解析结果,把内容展示在浏览器给用户浏览
对于上述所讲的内容,我们通过一个案例来初步体验下request和response对象的使用。
1 |
|
启动成功后就可以通过浏览器来访问,并且根据传入参数的不同就可以在页面上展示不同的内容:
小结
在这节中,我们主要认识了下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的继承体系:
ServletRequest和HttpServletRequest是继承关系,并且两个都是接口,接口是无法创建对象,这个时候就引发了下面这个问题:
1 | //接口无法创建对象,那么这个参数是哪儿来的呢 |
这个时候,我们就需要用到Request继承体系中的RequestFacade
:
- 该类实现了HttpServletRequest接口,也间接实现了ServletRequest接口。
- Servlet类中的service方法、doGet方法或者是doPost方法最终都是由Web服务器[Tomcat]来调用的,所以Tomcat提供了方法参数接口的具体实现类,并完成了对象的创建
- 要想了解RequestFacade中都提供了哪些方法,我们可以直接查看JavaEE的API文档中关于ServletRequest和HttpServletRequest的接口文档,因为RequestFacade实现了其接口就需要重写接口中的方法
对于上述结论,要想验证,可以编写一个Servlet,在方法中把request对象打印下,就能看到最终的对象是不是RequestFacade,代码如下:
1 |
|
启动服务器,运行访问http://localhost:8080/request-demo/demo2
小结
- Request的继承体系为ServletRequest–>HttpServletRequest–>RequestFacade
- Tomcat需要解析请求数据,封装为request对象,并且创建request对象传递到service方法
- 使用request对象,可以查阅JavaEE API文档的HttpServletRequest接口中方法说明
2.2 Request获取请求数据
HTTP请求数据总共分为三部分内容,分别是请求行、请求头、请求体
,对于这三部分内容的数据,分别该如何获取,首先我们先来学习请求行数据如何获取?
2.2.1 获取请求行数据
请求行包含三块内容,分别是请求方式
、请求资源路径
、HTTP协议及版本
对于这三部分内容,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 | /** |
启动服务器,访问http://localhost:8080/request-demo/req1?username=zhangsan&passwrod=123
2.2.2 获取请求头数据
对于请求头的数据,格式为key: value
所以根据请求头名称获取对应值的方法为:
1 | String getHeader(String name) |
接下来,在代码中如果想要获取客户端浏览器的版本信息,则可以使用
1 | /** |
重新启动服务器后,http://localhost:8080/request-demo/req1?username=zhangsan&passwrod=123
,获取的结果如下:
1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36 |
2.2.3 获取请求体数据
浏览器在发送GET请求的时候是没有请求体的,所以需要把请求方式变更为POST
对于请求体中的数据,Request对象提供了如下两种方式来获取其中的数据,分别是:
- 获取字节输入流,如果前端发送的是字节数据,比如传递的是文件数据,则使用该方法
1 | ServletInputStream getInputStream() |
- 获取字符输入流,如果前端发送的是纯文本数据,则使用该方法
1 | BufferedReader getReader() |
接下来,大家需要思考,要想获取到请求体的内容该如何实现?
具体实现的步骤如下:
1.准备一个页面,在页面中添加form表单,用来发送post请求
2.在Servlet的doPost方法中获取请求体数据
3.在doPost方法中使用request的getReader()或者getInputStream()来获取
4.访问测试
- 在项目的webapp目录下添加一个html页面,名称为:
req.html
1 |
|
- 在Servlet的doPost方法中获取数据
1 | /** |
- 调用getReader()或者getInputStream()方法,因为目前前端传递的是纯文本数据,所以我们采用getReader()方法来获取
注意
BufferedReader流是通过request对象来获取的,当请求完成后request对象就会被销毁,request对象被销毁后,BufferedReader流就会自动关闭,所以此处就不需要手动关闭流了。
- 启动服务器,通过浏览器访问
http://localhost:8080/request-demo/req.html
,点击提交
按钮后,就可以在控制台看到前端所发送的请求数据
小结
HTTP请求数据中包含了请求行
、请求头
和请求体
,针对这三部分内容,Request对象都提供了对应的API方法来获取对应的值:
- 请求行
- getMethod()获取请求方式
- getContextPath()获取项目访问路径
- getRequestURL()获取请求URL
- getRequestURI()获取请求URI
- getQueryString()获取GET请求方式的请求参数
- 请求头
- getHeader(String name)根据请求头名称获取其对应的值
- 请求体
- 注意:
浏览器发送的POST请求才有请求体
- 如果是纯文本数据:getReader()
- 如果是字节数据如文件数据:getInputStream()
- 注意:
2.2.4 获取请求参数的通用方式
对于请求参数的获取,常用的有以下两种:
- GET方式:
1 | String getQueryString() |
- POST方式:
1 | BufferedReader getReader(); |
GET请求和POST请求获取请求参数的方式不一样,在获取请求参数这块该如何实现呢?
要想实现,我们就需要思考
:
GET请求方式和POST请求方式区别主要在于获取请求参数的方式不一样,是否可以提供一种统一
获取请求参数的方式,从而统一
doGet和doPost方法内的代码?
解决方案一:
1 |
|
使用request的getMethod()来获取请求方式,根据请求方式的不同分别获取请求参数值,这样就可以解决上述问题,但是以后每个Servlet都需要这样写代码,实现起来比较麻烦,这种方案我们不采用
解决方案二:
request对象已经将上述获取请求参数的方法进行了封装,并且request提供的方法实现的功能更强大,以后只需要调用request提供的方法即可,在request的方法中都实现了哪些操作?
- 根据不同的请求方式获取请求参数,例如:
username=zhangsan&password=asd123&hobby=1&hobby=2
- 把获取到的内容进行分割,
username=zhangsan&password=asd123&hobby=1
->username=zhangsan password=asd123 hobby=1 hobby=2
->username zhangsan password asd123 hobby 1 hobby 2
- 把分割后端数据,存入到一个Map集合中,其中Map集合的泛型为
<String,String[]>
,因为参数的值可能是一个,也可能有多个,所以value的值的类型为String数组。
基于上述理论,request对象为我们提供了如下方法:
- 获取所有参数Map集合
1 | Map<String,String[]> getParameterMap() |
- 根据名称获取参数值(数组)
1 | String[] getParameterValues(String name) |
- 根据名称获取参数值(单个值)
1 | String getParameter(String name) |
接下来,我们通过案例来把上述的三个方法进行实例演示:
1.修改req.html页面,添加爱好选项,爱好可以同时选多个
1 |
|
2.在Servlet代码中获取页面传递GET请求的参数值
2.1获取GET方式的所有请求参数
1 | /** |
2.2获取GET请求参数中的爱好,结果是数组值
1 | /** |
2.3获取GET请求参数中的用户名和密码,结果是单个值
1 | /** |
3.在Servlet代码中获取页面传递POST请求的参数值
3.1将req.html页面form表单的提交方式改成post
3.2将doGet方法中的内容复制到doPost方法中即可
小结
-
req.getParameter()方法使用的频率会比较高
-
以后我们再写代码的时候,就只需要按照如下格式来编写:
1 | public class RequestDemo1 extends HttpServlet { |
2.3 Request请求转发
请求转发(forward):一种在服务器内部的资源跳转方式。
(1)浏览器发送请求给服务器,服务器中对应的资源A接收到请求
(2)资源A处理完请求后将请求发给资源B
(3)资源B处理完后将结果响应给浏览器
(4)请求从资源A到资源B的过程就叫请求转发
- 请求转发的实现方式:
1 | req.getRequestDispatcher("资源B路径").forward(req,resp); |
具体的实现步骤为:
1.创建一个RequestDemo5类,接收/req5的请求,在doGet方法中打印
demo5
2.创建一个RequestDemo6类,接收/req6的请求,在doGet方法中打印
demo6
3.在RequestDemo5的方法中使用
req.getRequestDispatcher(“/req6”).forward(req,resp)进行请求转发
4.启动测试
(1)创建RequestDemo5类
1 | /** |
(2)创建RequestDemo6类
1 | /** |
(3)在RequestDemo5的doGet方法中进行请求转发
1 | /** |
(4)启动测试
访问http://localhost:8080/request-demo/req5
,就可以在控制台看到如下内容:
1 | demo5... |
说明请求已经转发到了/req6
- 请求转发资源间共享数据:使用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); |
接着上个需求来:
1.在RequestDemo5的doGet方法中转发请求之前,将数据存入request域对象中
2.在RequestDemo6的doGet方法从request域对象中获取数据,并将数据打印到控制台
3.启动访问测试
(1)修改RequestDemo5中的方法
1 |
|
(2)修改RequestDemo6中的方法
1 | /** |
(3)启动测试
访问http://localhost:8080/request-demo/req5
,就可以在控制台看到如下内容:
1 | demo5... |
此时就可以实现在转发多个资源之间共享数据。
- 请求转发的特点
-
浏览器地址栏路径不发生变化
虽然后台从
/req5
转发到/req6
,但是浏览器的地址一直是/req5
,未发生变化 -
只能转发到当前服务器的内部资源
不能从一个服务器通过转发访问另一台服务器
-
一次请求,可以在转发资源间使用request共享数据
虽然后台从
/req5
转发到/req6
,但是这个只有一次请求
3,Response对象
- Request:使用request对象来
获取
请求数据 - Response:使用response对象来
设置
响应数据
Reponse的继承体系和Request的继承体系也非常相似:
3.1 Response设置响应数据功能介绍
HTTP响应数据总共分为三部分内容,分别是响应行、响应头、响应体
,对于这三部分内容的数据,respone对象都提供了哪些方法来进行设置?
-
响应行
响应行包含三块内容,分别是 HTTP/1.1[HTTP协议及版本] 200[响应状态码] ok[状态码的描述]
对于响应头,比较常用的就是设置响应状态码:
1 | void setStatus(int sc); |
- 响应头
响应头的格式为key:value形式
设置响应头键值对:
1 | void setHeader(String name,String value); |
- 响应体
对于响应体,是通过字符、字节输出流的方式往浏览器写,
获取字符输出流:
1 | PrintWriter getWriter(); |
获取字节输出流
1 | ServletOutputStream getOutputStream(); |
3.2 Respones请求重定向
Response重定向(redirect):一种资源跳转方式。
(1)浏览器发送请求给服务器,服务器中对应的资源A接收到请求
(2)资源A现在无法处理该请求,就会给浏览器响应一个302的状态码+location的一个访问资源B的路径
(3)浏览器接收到响应状态码为302就会重新发送请求到location对应的访问地址去访问资源B
(4)资源B接收到请求后进行处理并最终给浏览器响应结果,这整个过程就叫重定向
- 重定向的实现方式:
1 | resp.setStatus(302); |
具体的实现步骤为:
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)创建ResponseDemo2类
1 |
|
(3)在ResponseDemo1的doGet方法中给前端响应数据
1 |
|
(4)启动测试
访问http://localhost:8080/request-demo/resp1
,就可以在控制台看到如下内容:
1 | resp1... |
说明/resp1
和/resp2
都被访问到了。到这重定向就已经完成了。
虽然功能已经实现,但是从设置重定向的两行代码来看,会发现除了重定向的地址不一样,其他的内容都是一模一样,所以request对象给我们提供了简化的编写方式为:
1 | resposne.sendRedirect("/request-demo/resp2") |
所以第3步中的代码就可以简化为:
1 |
|
- 重定向的特点
-
浏览器地址栏路径发送变化
当进行重定向访问的时候,由于是由浏览器发送的两次请求,所以地址会发生变化
-
可以重定向到任何位置的资源(服务内容、外部均可)
因为第一次响应结果中包含了浏览器下次要跳转的路径,所以这个路径是可以任意位置资源。
-
两次请求,不能在多个资源使用request共享数据
因为浏览器发送了两次请求,是两个不同的request对象,就无法通过request对象进行共享数据
介绍完请求重定向
和请求转发
以后,接下来需要把这两个放在一块对比下:
重定向特点 | 请求转发特点 |
---|---|
浏览器地址栏路径发生变化 | 浏览器地址栏路径不发生变化 |
可以重定向到任意位置的资源(服务器内部、外部均可) | 只能转发到当前服务器的内部资源 |
两次请求,不能在多个资源使用request共享数据 | 一次请求,可以在转发的资源间使用request共享数据 |
3.3 路径问题
- 问题1:转发的时候路径上没有加
/request-demo
而重定向加了,那么到底什么时候需要加,什么时候不需要加呢?
其实判断的依据很简单,只需要记住下面的规则即可:
- 浏览器使用:需要加虚拟目录(项目访问路径)
- 服务端使用:不需要加虚拟目录
对于转发来说,因为是在服务端进行的,所以不需要加虚拟目录
对于重定向来说,路径最终是由浏览器来发送请求,就需要添加虚拟目录。
掌握了这个规则,接下来就通过一些练习来强化下知识的学习:
1 | Q: |
- 问题2:在重定向的代码中,
/request-demo
是固定编码的,如果后期通过Tomcat插件配置了项目的访问路径,那么所有需要重定向的地方都需要重新修改,该如何优化?
我们可以在代码中动态去获取项目访问的虚拟目录,具体如何获取,我们可以借助前面咱们所学习的request对象中的getContextPath()方法,修改后的代码如下:
1 |
|
重新启动访问测试,功能依然能够实现,此时就可以动态获取项目访问的虚拟路径,从而降低代码的耦合度。
3.4 Response响应字符数据
要想将字符数据写回到浏览器,我们需要两个步骤:
-
通过Response对象获取字符输出流: PrintWriter writer = resp.getWriter();
-
通过字符输出流写数据: writer.write(“aaa”);
接下来,我们实现通过些案例把响应字符数据给实际应用下:
- 返回一个简单的字符串
aaa
1 | /** |
- 返回一串html字符串,并且能被浏览器解析
1 | PrintWriter writer = response.getWriter(); |
注意:
一次请求响应结束后,response对象就会被销毁掉,所以不要手动关闭流。
- 返回一个中文的字符串
你好
,需要注意设置响应数据的编码为utf-8
1 | //设置响应的数据格式及数据的编码 |
3.3 Response响应字节数据
要想将字节数据写回到浏览器,我们需要两个步骤:
-
通过Response对象获取字节输出流:ServletOutputStream outputStream = resp.getOutputStream();
-
通过字节输出流写数据: outputStream.write(字节数据);
接下来,我们实现通过些案例把响应字符数据给实际应用下:
- 返回一个图片文件到浏览器
1 | /** |
上述代码中,对于流的copy的代码还是比较复杂的,所以我们可以使用别人提供好的方法来简化代码的开发,具体的步骤是:
(1)pom.xml添加依赖
1 | <dependency> |
(2)调用工具类方法
1 | //fis:输入流 |
优化后的代码:
1 | /** |
4,用户注册登录案例
4.1 用户登录
4.1.1 需求分析
- 用户在登录页面输入用户名和密码,提交请求给LoginServlet
- 在LoginServlet中接收请求和数据[用户名和密码]
- 在LoginServlt中通过Mybatis实现调用UserMapper来根据用户名和密码查询数据库表
- 将查询的结果封装到User对象中进行返回
- 在LoginServlet中判断返回的User对象是否为null
- 如果为nul,说明根据用户名和密码没有查询到用户,则登录失败,返回"登录失败"数据给前端
- 如果不为null,则说明用户存在并且密码正确,则登录成功,返回"登录成功"数据给前端
4.1.2 环境准备
- 写一个简单的表单提交
1 |
|
- 创建db1数据库,创建tb_user表,创建User实体类
1 | -- 创建数据库 |
1 | package com.blog.pojo; |
- 在项目的pom.xml导入Mybatis和Mysql驱动坐标
1 | <dependency> |
- 创建mybatis-config.xml核心配置文件,UserMapper.xml映射文件,UserMapper接口
1 |
|
4.2 在com.itheima.mapper包下创建UserMapper接口
1 | public interface UserMapper { |
4.3 创建UserMapper.xml文件
1 |
|
至此我们所需要的环境就都已经准备好了,具体该如何实现?
4.1.3 代码实现
- 在UserMapper接口中提供一个根据用户名和密码查询用户对象的方法
1 | /** |
说明
@Param注解的作用:用于传递参数,是方法的参数可以与SQL中的字段名相对应。
- 修改loign.html
1 |
|
- 编写LoginServlet
1 |
|
- 启动服务器测试
如果用户名和密码输入错误,则登陆失败
如果用户名和密码输入正确,则登陆成功
至此用户的登录功能就已经完成了~
4.2 用户注册
4.2.1 需求分析
- 用户在注册页面输入用户名和密码,提交请求给RegisterServlet
- 在RegisterServlet中接收请求和数据[用户名和密码]
- 在RegisterServlet中通过Mybatis实现调用UserMapper来根据用户名查询数据库表
- 将查询的结果封装到User对象中进行返回
- 在RegisterServlet中判断返回的User对象是否为null
- 如果为nul,说明根据用户名可用,则调用UserMapper来实现添加用户
- 如果不为null,则说明用户不可以,返回"用户名已存在"数据给前端
4.2.2 代码编写
- 编写UserMapper提供根据用户名查询用户数据方法和添加用户方法
1 | /** |
- 修改register.html
1 |
|
- 创建RegisterServlet类
1 |
|
- 启动服务器进行测试
如果测试成功,则在数据库中就能查看到新注册的数据
如果用户已经存在,则在页面上展示 用户名已存在
的提示信息
4.3 SqlSessionFactory工具类抽取
上面两个功能已经实现,但是在写Servlet的时候,因为需要使用Mybatis来完成数据库的操作,所以对于Mybatis的基础操作就出现了些重复代码,如下
1 | String resource = "mybatis-config.xml"; |
有了这些重复代码就会造成一些问题:
- 重复代码不利于后期的维护
- SqlSessionFactory工厂类进行重复创建
- 就相当于每次买手机都需要重新创建一个手机生产工厂来给你制造一个手机一样,资源消耗非常大但性能却非常低。所以这么做是不允许的。
那如何来优化呢?
- 代码重复可以抽取工具类
- 对指定代码只需要执行一次可以使用静态代码块
有了这两个方向后,代码具体该如何编写?
1 | public class SqlSessionFactoryUtils { |
工具类抽取以后,以后在对Mybatis的SqlSession进行操作的时候,就可以直接使用
1 | SqlSessionFactory sqlSessionFactory =SqlSessionFactoryUtils.getSqlSessionFactory(); |
这样就可以很好的解决上面所说的代码重复和重复创建工厂导致性能低的问题了。
会话技术(Cookie&Session)
1,会话跟踪技术的概述
对于会话跟踪
这四个词,我们需要拆开来进行解释,首先要理解什么是会话
,然后再去理解什么是会话跟踪
:
-
会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含
多次
请求和响应。- 从浏览器发出请求到服务端响应数据给前端之后,一次会话(在浏览器和服务器之间)就被建立了
- 会话被建立后,如果浏览器或服务端都没有被关闭,则会话就会持续建立着
- 浏览器和服务器就可以继续使用该会话进行请求发送和响应,上述的整个过程就被称之为
会话
。
-
会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间
共享数据
。- 服务器会收到多个请求,这多个请求可能来自多个浏览器
- 服务器需要用来识别请求是否来自同一个浏览器
- 服务器用来识别浏览器的过程,这个过程就是
会话跟踪
- 服务器识别浏览器后就可以在同一个会话中多次请求之间来共享数据
那么我们又有一个问题需要思考,一个会话中的多次请求为什么要共享数据呢?有了这个数据共享功能后能实现哪些功能呢?
-
拿购物车举例:
加入购物车
和去购物车结算
是两次请求,但是后面这次请求要想展示前一次请求所添加的商品,就需要用到数据共享。 -
页面展示用户登录信息:很多网站,登录后访问多个功能发送多次请求后,浏览器上都会有当前登录用户的信息[用户名],比如百度、京东、码云等。
-
网站登录页面的
记住我
功能:当用户登录成功后,勾选记住我
按钮后下次再登录的时候,网站就会自动填充用户名和密码,简化用户的登录操作,多次登录就会有多次请求,他们之间也涉及到共享数据 -
登录页面的验证码功能:生成验证码和输入验证码点击注册这也是两次请求,这两次请求的数据之间要进行对比,相同则允许注册,不同则拒绝注册,该功能的实现也需要在同一次会话中共享数据。
思考:为什么现在浏览器和服务器不支持数据共享呢?
- 浏览器和服务器之间使用的是HTTP请求来进行数据传输
- HTTP协议是
无状态
的,每次浏览器向服务器请求时,服务器都会将该请求视为新的
请求 - HTTP协议设计成无状态的目的是让每次请求之间相互独立,互不影响
- 请求与请求之间独立后,就无法实现多次请求之间的数据共享
如何实现会话跟踪技术呢? 具体的实现方式有:
(1)客户端会话跟踪技术:Cookie
(2)服务端会话跟踪技术:Session
它们之间最大的区别:
Cookie是存储在浏览器端而Session是存储在服务器端
2,Cookie
2.1 Cookie的基本使用
1.概念
Cookie
:客户端会话技术,将数据保存到客户端,以后每次请求都携带Cookie数据进行访问。
2.Cookie的工作流程
- 服务端提供了两个Servlet,分别是ServletA和ServletB
- 浏览器发送HTTP请求1给服务端,服务端ServletA接收请求并进行业务处理
- 服务端ServletA在处理的过程中可以创建一个Cookie对象并将
name=zs
的数据存入Cookie - 服务端ServletA在响应数据的时候,会把Cookie对象响应给浏览器
- 浏览器接收到响应数据,会把Cookie对象中的数据存储在浏览器内存中,此时浏览器和服务端就
建立了一次会话
在同一次会话
中浏览器再次发送HTTP请求2给服务端ServletB,浏览器会携带Cookie对象中的所有数据- ServletB接收到请求和数据后,就可以获取到存储在Cookie对象中的数据,这样同一个会话中的多次请求之间就实现了数据共享
3.Cookie的基本使用
3.1 发送Cookie
- 创建Cookie对象,并设置数据
1 | Cookie cookie = new Cookie("key","value"); |
- 发送Cookie到客户端:使用
response
对象
1 | response.addCookie(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 | <properties> |
(2)编写Servlet类,名称为AServlet
1 |
|
(3)在Servlet中创建Cookie对象,存入数据,发送给前端
1 |
|
(4)启动测试,在浏览器查看Cookie对象中的值
访问http://localhost:8080/cookie-demo/aServlet
名称 username
内容 zs
域名 localhost
路径 /cookie_demo
为何发送 仅限同一网站的连接
脚本可访问 是
创建时间 2023年8月20日星期六 10:47:16
到期时间 浏览会话结束时
3.2 获取Cookie
- 获取客户端携带的所有Cookie,使用
request
对象
1 | Cookie[] cookies = request.getCookies(); |
- 遍历数组,获取每一个Cookie对象:for
- 使用Cookie对象方法获取数据
1 | cookie.getName(); |
介绍完获取Cookie对应的步骤后,接下面再通过一个案例来完成Cookie的获取,具体实现步骤为:
需求:在Servlet中获取前一个案例存入在Cookie对象中的数据
1.编写一个新Servlet类,名称为BServlet
2.在BServlet中使用request对象获取Cookie数组,遍历数组,从数据中获取指定名称对应的值
3.启动测试,在控制台打印出获取的值
(1)编写一个新Servlet类,名称为BServlet
1 |
|
(2)在BServlet中使用request对象获取Cookie数组,遍历数组,从数据中获取指定名称对应的值
1 |
|
(3)启动测试,在控制台打印出获取的值
访问http://localhost:8080/cookie-demo/bServlet
在IDEA控制台就能看到输出的结果:username:zs
2.2 Cookie的原理分析
对于Cookie的实现原理是基于HTTP协议的,其中设计到HTTP协议中的两个请求头信息:
- 响应头:set-cookie
- 请求头: cookie
- 前面的案例中已经能够实现,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[]后,就可以从中获取自己需要的数据
2.3 Cookie的使用细节
2.3.1 Cookie的存活时间
前面让大家思考过一个问题:
(1)浏览器发送请求给AServlet,AServlet会响应一个存有usernanme=zs
的Cookie对象给浏览器
(2)浏览器接收到响应数据将cookie存入到浏览器内存中
(3)当浏览器再次发送请求给BServlet,BServlet就可以使用Request对象获取到Cookie数据
(4)在发送请求到BServlet之前,如果把浏览器关闭再打开进行访问,BServlet能否获取到Cookie数据?
注意:浏览器关闭再打开不是指打开一个新的选显卡,而且必须是先关闭再打开,顺序不能变。
针对上面这个问题,通过演示,会发现,BServlet中无法再获取到Cookie数据,这是为什么呢?
- 默认情况下,Cookie存储在浏览器内存中,当浏览器关闭,内存释放,则Cookie被销毁
这个结论就印证了上面的演示效果,但是如果使用这种默认情况下的Cookie,有些需求就无法实现,比如:
上面这个网站的登录页面上有一个记住我
的功能,这个功能大家都比较熟悉
- 第一次输入用户名和密码并勾选
记住我
然后进行登录 - 下次再登陆的时候,用户名和密码就会被自动填充,不需要再重新输入登录
- 比如
记住我
这个功能需要记住用户名和密码一个星期,那么使用默认情况下的Cookie就会出现问题 - 因为默认情况,浏览器一关,Cookie就会从浏览器内存中删除,对于
记住我
功能就无法实现
所以我们现在就遇到一个难题是如何将Cookie持久化存储
?
Cookie其实已经为我们提供好了对应的API来完成这件事,这个API就是setMaxAge
,
- 设置Cookie存活时间
1 | setMaxAge(int seconds) |
参数值为:
-
正数:将Cookie写入浏览器所在电脑的硬盘,持久化存储。到时间自动删除
-
负数:默认值,Cookie在当前浏览器内存中,当浏览器关闭,则Cookie被销毁
-
零:删除对应Cookie
接下来,咱们就在AServlet中去设置Cookie的存活时间。
1 |
|
修改完代码后,启动测试,访问http://localhost:8080/cookie-demo/aServlet
- 访问一个AServlet后,把浏览器关闭重启后,再去访问
http://localhost:8080/cookie-demo/bServet
,能在控制台打印出username:zs
,说明Cookie没有随着浏览器关闭而被销毁 - 通过浏览器查看Cookie的内容,会发现Cookie的相关信息
名称 username
内容 zs
域名 localhost
路径 /cookie_demo
为何发送 仅限同一网站的连接
脚本可访问 是
创建时间 2023年8月20日星期六 10:47:16
到期时间 2023年8月27日星期六 10:47:16
2.3.2 Cookie存储中文
Cookie是不能直接存储中文的,但是如果有这方面的需求,这个时候该如何解决呢?
我们可以使用URL编码
,所以如果需要存储中文,就需要进行转码,具体的实现思路为:
(1)在AServlet中对中文进行URL编码
1 |
|
(2)在BServlet中获取值,并对值进行解码
1 |
|
3,Session
3.1 Session的基本使用
1.概念
Session
:服务端会话跟踪技术:将数据保存到服务端。
- Session是存储在服务端而Cookie是存储在客户端
- 存储在客户端的数据容易被窃取和截获,存在很多不安全的因素
- 存储在服务端的数据相比于客户端来说就更安全
2.Session的工作流程
- 在服务端的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)创建名为SessionDemo2的Servlet类
1 |
|
(3)SessionDemo1:获取Session对象、存储数据
1 |
|
(4)SessionDemo2:获取Session对象、获取数据
1 |
|
(5)启动测试,
- 先访问
http://localhost:8080/cookie-demo/demo1
,将数据存入Session - 在访问
http://localhost:8080/cookie-demo/demo2
,从Session中获取数据 - 查看控制台 成功输出zs
Session是能够在一次会话中两次请求之间共享数据。
**注意:**Session中可以存储的是一个Object类型的数据,也就是说Session中可以存储任意数据类型。
3.2 Session的原理分析
- Session是基于Cookie实现的
这句话其实不太能详细的说明Session的底层实现,接下来,咱们一步步来分析下Session的具体实现原理:
(1)前提条件
Session要想实现一次会话多次请求之间的数据共享,就必须要保证多次请求获取Session的对象是同一个。
那么它们是一个对象么?要验证这个结论也很简单,只需要在上面案例中的两个Servlet中分别打印下Session对象
SessionDemo1
1 |
|
SessionDemo2
1 |
|
启动测试,分别访问/demo1``/demo2
,输出如下
org.apache.catalina.session.StandardSessionFacade@69186990
org.apache.catalina.session.StandardSessionFacade@69186990
通过打印可以得到如下结论:
- 两个Servlet类中获取的Session对象是同一个
- 把demo1和demo2请求刷新多次,控制台最终打印的结果都是同一个
那么问题又来了,如果新开一个浏览器,访问demo1或者demo2,打印在控制台的Session还是同一个对象么?
注意:在一台电脑上演示的时候,如果是相同的浏览器必须要把浏览器全部关掉重新打开,才算新开的一个浏览器。
测试的结果:如果是不同浏览器或者重新打开浏览器后,打印的Session就不一样了。
所以Session实现的也是一次会话中的多次请求之间的数据共享。
那么最主要的问题就来了,Session是如何保证在一次会话中获取的Session对象是同一个呢?
(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来实现的
这就话,我们就解释完了
3.3 Session的使用细节
3.3.1 Session钝化与活化
首先需要大家思考的问题是:
- 服务器重启后,Session中的数据是否还在?
要想回答这个问题,我们可以先看下下面这幅图,
(1)服务器端AServlet和BServlet共用的session对象应该是存储在服务器的内存中
(2)服务器重新启动后,内存中的数据应该是已经被释放,对象也应该都销毁了
所以session数据应该也已经不存在了。但是如果session不存在会引发什么问题呢?
举个例子说明下,
(1)用户把需要购买的商品添加到购物车,因为要实现同一个会话多次请求数据共享,所以假设把数据存入Session对象中
(2)用户正要付钱的时候接到一个电话,付钱的动作就搁浅了
(3)正在用户打电话的时候,购物网站因为某些原因需要重启
(4)重启后session数据被销毁,购物车中的商品信息也就会随之而消失
(5)用户想再次发起支付,就会出为问题
所以说对于session的数据,我们应该做到就算服务器重启了,也应该能把数据保存下来才对。
那么Tomcat服务器在重启的时候,session数据到底会不会保存以及是如何保存的,我们可以通过实际案例来演示下,测试流程是:
(1)先启动Tomcat服务器
(2)访问http://localhost:8080/cookie-demo/demo1
将数据存入session中
(3)正确停止Tomcat服务器
(4)再次重新启动Tomcat服务器
(5)访问http://localhost:8080/cookie-demo/demo2
查看是否能获取到session中的数据
经过测试,会发现只要服务器是正常关闭和启动,session中的数据是可以被保存下来的。
那么Tomcat服务器到底是如何做到的呢?
具体的原因就是:Session的钝化和活化:
-
钝化:在服务器正常关闭后,Tomcat会自动将Session数据写入硬盘的文件中
- 钝化的数据路径为:
项目目录\target\tomcat\work\Tomcat\localhost\项目名称\SESSIONS.ser
- 钝化的数据路径为:
-
活化:再次启动服务器后,从文件中加载数据到Session中
- 数据加载到Session中后,路径中的
SESSIONS.ser
文件会被删除掉
- 数据加载到Session中后,路径中的
小结
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
<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配置文件中写死的
-
-
-
调用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
public class SessionDemo2 extends HttpServlet {
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);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
} -
启动访问测试,先访问demo1将数据存入到session,再次访问demo2从session中获取数据,会得到如下错误信息
java.lang.IllegalStateException: getAttribute: Session already invalidated
-
该销毁方法一般会在用户退出的时候,需要将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是用来保存用户登录后的数据
4,用户登录注册案例
需求分析
在上篇文章,我们已经完成了对品牌数据的增删改查操作,现在我们继续完善登录功能
需求说明:
- 完成用户登录功能,如果用户勾选"记住用户" ,则下次访问登录页面
自动填充
用户名密码 - 完成注册功能,并实现
验证码
功能
用户登录功能
-
需求:
- 用户登录成功后,跳转到列表页面,并在页面上展示当前登录的用户名称
- 用户登录失败后,跳转回登录页面,并在页面上展示对应的错误信息
-
实现流程分析
- 前端通过表单发送请求和数据给Web层的LoginServlet
- 在LoginServlet中接收请求和数据[用户名和密码]
- LoginServlet接收到请求和数据后,调用Service层完成根据用户名和密码查询用户对象
- 在Service层需要编写UserService类,在类中实现login方法,方法中调用Dao层的UserMapper
- 在UserMapper接口中,声明一个根据用户名和密码查询用户信息的方法
- Dao层把数据查询出来以后,将返回数据封装到User对象,将对象交给Service层
- Service层将数据返回给Web层
- Web层获取到User对象后,判断User对象,如果为Null,则将错误信息响应给登录页面,如果不为Null,则跳转到列表页面,并把当前登录用户的信息存入Session携带到列表页面。
-
具体实现
- 在
db1
数据库下新建tb_user
表
1
2
3
4
5
6
7
8
9
10
11-- 创建用户表
CREATE TABLE tb_user(
id int primary key auto_increment,
username varchar(20) unique,
password varchar(32)
);
-- 添加数据
INSERT INTO tb_user(username,password) values('zhangsan','123'),('lisi','234');
SELECT * FROM tb_user;- 在
com.blog.pojo
下新建User类
根据数据表中的数据类型创建即可
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
41package com.blog.pojo;
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;
}
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}- 在
com.blog.mapper
包下新建UserMapper
接口
我们只需要登录,添加(注册)和查询(检测用户名是否已被占用)这三个功能
1
2
3
4
5
6
7
8
9
10public interface UserMapper {
User login(; String username, String password)
User selectByUsername(String username);
void add(User user);
}- 在
resources/com/itheima/mapper
目录下新建UserMapper.xml
由于我们使用注解开发,所以这里啥也不用写,当SQL语句很复杂的时候,我们才会采用此种方式
1
2
3
4
5
6
7
<mapper namespace="com.blog.mapper.UserMapper">
</mapper>- 在
com.itheima.service
包下,创建UserService类
这里要实现登录的逻辑,上一篇文章也练习过很多遍了
1
2
3
4
5
6
7
8
9
10
11public class UserService {
private SqlSessionFactory sqlSessionFactory = SqlSessionFactoryUtils.getSqlSessionFactory();
public User login(String username, String password) {
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.login(username, password);
sqlSession.close();
return user;
}
}- 写一个登录的页面
login.jsp
我这里就不搞那些花里胡哨的了,平平淡淡才是真,主要实现的是后端的逻辑,前端暂时无所谓了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page isELIgnored="false" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="/brand_demo/loginServlet" method="post">
<h1>登录系统</h1>
用户名:<input name="username" type="text"><br>
密码:<input name="password" type="password"><br>
记住账号:<input name="remember" type="checkbox" value="1"><br>
<input value="登录" type="submit">
<!--这里提前写了一下注册的跳转链接,后面我们会把这个页面也写完-->
<a href="/brand_demo/register.jsp">没有账号?</a>
</form>
</body>
</html>- 创建LoginServlet类
- 实现登录功能,我们要先获取用户输入的用户名和密码
- 然后调用service查询User
- 判断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
30
public class LoginServlet extends HttpServlet {
private UserService userService = new UserService();
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取用户名和密码
String username = request.getParameter("username");
String password = request.getParameter("password");
//调用service查询User
User user = userService.login(username, password);
if (user != null) {
//登陆成功,跳转到查询所有BrandServlet
HttpSession session = request.getSession();
//将user的信息存储到session域中,便于在brand.jsp页面添加提示信息
session.setAttribute("user", user);
response.sendRedirect("/brand_demo/selectAllServlet");
} else {
//登陆失败,将错误信息存储到request域中
request.setAttribute("login_msg", "用户名或密码错误");
//并跳转到login.jsp
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}- 登陆成功,由于我们将user的信息存储到了session域中,我们就可以在
brand.jsp
页面上添加提示信息<h1>${user.username},欢迎你</h1>
当我们登陆成功后,会在页面的最上方用h1标题显示username,欢迎你
- 登陆失败,由于我们将错误提示语句存储到了request域中,所以我们可以在login.jsp中用EL表达式接收一下错误信息,并展示到页面上
1
2
3
4
5
6
7
8
9<form action="/brand_demo/loginServlet" method="post">
<h1>登录系统</h1>
<div>${login_msg}</div> <!--当登陆失败的时候,会在这里显示"用户名或密码错误"-->
用户名:<input name="username" type="text" value="${cookie.username.value}"><br>
密码:<input name="password" type="password" value="${cookie.password.value}"><br>
记住账号:<input name="remember" type="checkbox" value="1"><br>
<input value="登录" type="submit">
<a href="/brand_demo/register.jsp">没有账号?</a>
</form>- 启动,访问测试,输入错误账号密码,会显示
用户名或密码错误
,输入正确的账号密码,会跳转到brand.jsp页面,展示所有商品信息,同时页面最上方有username,欢迎你
字样 - 小结
- 在LoginServlet中,将登录成功的用户数据存入session中,方法在列表页面中获取当前登录用户信息进行展示
- 在LoginServlet中,将登录失败的错误信息存入到request中,如果存入到session中就会出现这次会话的所有请求都有登录失败的错误信息,这个是不需要的,所以不用存入到session中
- 在
记住我-设置Cookie
-
需求:
如果用户勾选"记住用户" ,则下次访问登陆页面自动填充用户名密码。这样可以提升用户的体验。
对应上面这个需求,最大的问题就是: 如何自动填充用户名和密码? -
实现流程分析
-
因为
1
记住我
功能要实现的效果是,就算用户把浏览器关闭过几天再来访问也能自动填充,所以需要将登陆信息存入一个可以长久保存,并且能够在浏览器关闭重新启动后依然有效的地方,就是我们前面讲的Cookie,所以:
- 将用户名和密码写入Cookie中,并且持久化存储Cookie,下次访问浏览器会自动携带Cookie
- 在页面获取Cookie数据后,设置到用户名和密码框中
- 何时写入Cookie?
- 用户必须登陆成功后才需要写
- 用户必须在登录页面勾选了
记住我
的复选框
-
-
具体实现
- 在LoginServlet获取复选框的值并在登录成功后进行设置Cookie
1 |
|
- 启动访问测试,只有当前用户名和密码输入正确,并且勾选了Remeber的复选框,用F12打开开发者工具,监测网络,点击登录,在响应头中可以看得cookie的相关数据
Set-Cookie: username=zhangsan; Expires=Sat, 27-Aug-2022 14:35:56 GMT
Set-Cookie: password=123; Expires=Sat, 27-Aug-2022 14:35:56 GMT
记住我-获取Cookie
-
需求
登录成功并勾选了Remeber后,后端返回给前端的Cookie数据就已经存储好了,接下来就需要在页面获取Cookie中的数据,并把数据设置到登录页面的用户名和密码框中。
如何在页面直接获取Cookie中的值呢? -
实现流程分析
在页面可以使用EL表达式,${cookie.key.value}
key:指的是存储在cookie中的键名称,例如${cookie.username.value}
-
具体实现
- 在login.jsp用户名的表单输入框使用value值给表单元素添加默认值,value可以使用
${cookie.username.value}
- 在login.jsp密码的表单输入框使用value值给表单元素添加默认值,value可以使用
${cookie.password.value}
1
2
3
4
5
6
7
8
9<form action="/brand_demo/loginServlet" method="post">
<h1>登录系统</h1>
<div>${login_msg} ${register_msg}</div>
用户名:<input name="username" type="text" value="${cookie.username.value}"><br>
密码:<input name="password" type="password" value="${cookie.password.value}"><br>
记住账号:<input name="remember" type="checkbox" value="1"><br>
<input value="登录" type="submit">
<a href="/brand_demo/register.jsp">没有账号?</a>
</form>- 访问测试,重新访问登录页面,就可以看得用户和密码已经被填充。
- 在login.jsp用户名的表单输入框使用value值给表单元素添加默认值,value可以使用
用户注册功能
-
需求
- 注册功能:保存用户信息到数据库
- 验证码功能
- 展示验证码:展示验证码图片,并可以点击切换
- 校验验证码:验证码填写不正确,则注册失败
-
实现流程分析
- 前端通过表单发送请求和数据给Web层的RegisterServlet
- 在RegisterServlet中接收请求和数据[用户名和密码]
- RegisterServlet接收到请求和数据后,调用Service层完成用户信息的保存
- 在Service层需要编写UserService类,在类中实现register方法,需要判断用户是否已经存在,如果不存在,则完成用户数据的保存
- 在UserMapper接口中,声明两个方法,一个是根据用户名查询用户信息方法,另一个是保存用户信息方法
- 在UserService类中保存成功则返回true,失败则返回false,将数据返回给Web层
- Web层获取到结果后,如果返回的是true,则提示
注册成功
,并转发到登录页面,如果返回false则提示用户名已存在
并转发到注册页面
-
具体实现
- 在UserService中实现注册逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//返回值告诉我们是否注册成功了
public boolean register(User user) {
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User u = mapper.selectByUsername(user.getUsername());
if (u == null) {
//u为null,说明未找到username对应的用户,即用户名不重复,可以注册
mapper.add(user);
//注册操作记得提交事务
sqlSession.commit();
sqlSession.close();
}
sqlSession.close();
return u == null;
}- 编写一个注册的页面
register.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page isELIgnored="false" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="/brand_demo/registerServlet" method="post">
<h1>欢迎注册</h1>
<!--已有账号的话就跳转至登录页面-->
已有账号?<a href="login.jsp">点击登录</a><br>
用户名:<input name="username" type="text"><br>
密码:<input name="password" type="password"><br>
<input value="注册" type="submit">
</form>
</body>
</html>- 编写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
public class RegisterServlet extends HttpServlet {
private UserService userService = new UserService();
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取客户输入的用户名和密码
String username = request.getParameter("username");
String password = request.getParameter("password");
//封装成一个User
User user = new User();
user.setUsername(username);
user.setPassword(password);
//注册账号,用flag来接收是否注册成功
boolean flag = userService.register(user);
if (flag){
//如果成功注册,将注册成功的提示存入request域中,随后跳转到登录页面
request.setAttribute("register_msg","注册成功请登录");
request.getRequestDispatcher("/login.jsp").forward(request,response);
}else {
//如果注册失败,就是用户名重复了,将错误信息存入request域中,并返回注册页面
request.setAttribute("register_msg","用户名已存在");
request.getRequestDispatcher("/register.jsp").forward(request,response);
}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}- 注册成功,跳转至登录页面,我们需要将注册成功的提示显示在登录页面上,修改
login.jsp
1
2
3
4
5
6
7
8
9
10<form action="/brand_demo/loginServlet" method="post">
<h1>登录系统</h1>
<!--还是放在这里,一个登录的错误提示信息,一个注册成功的提示信息,谁有信息就显示谁-->
<div>${login_msg} ${register_msg}</div>
用户名:<input name="username" type="text" value="${cookie.username.value}"><br>
密码:<input name="password" type="password" value="${cookie.password.value}"><br>
记住账号:<input name="remember" type="checkbox" value="1"><br>
<input value="登录" type="submit">
<a href="/brand_demo/register.jsp">没有账号?</a>
</form>- 注册失败,跳转至注册页面,同时将错误信息展示在注册页面上,修改
register.jsp
1
2
3
4
5
6
7
8
9<form action="/brand_demo/registerServlet" method="post">
<h1>欢迎注册</h1>
已有账号?<a href="login.jsp">点击登录</a><br>
用户名:<input name="username" type="text"><br>
<!--在这里显示一下注册失败的错误信息-->
<div>${register_msg}</div>
密码:<input name="password" type="password"><br>
<input value="注册" type="submit">
</form>- 启动测试,如果是注册的用户信息已经存在,则会显示
用户名已存在
,如果注册成功,会跳转至登录页面,同时也会显示注册成功请登录
提示语句
验证码-展示
-
需求分析
- 展示验证码:展示验证码图片,并可以点击
看不清
切换,验证码的生成是通过工具类来实现的
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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280package com.blog.util;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Random;
/**
* 生成验证码工具类
*/
public class CheckCodeUtil {
public static final String VERIFY_CODES = "123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static Random random = new Random();
/**
* 输出随机验证码图片流,并返回验证码值(一般传入输出流,响应response页面端,Web项目用的较多)
*
* @param w
* @param h
* @param os
* @param verifySize
* @return
* @throws IOException
*/
public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException {
String verifyCode = generateVerifyCode(verifySize);
outputImage(w, h, os, verifyCode);
return verifyCode;
}
/**
* 使用系统默认字符源生成验证码
*
* @param verifySize 验证码长度
* @return
*/
public static String generateVerifyCode(int verifySize) {
return generateVerifyCode(verifySize, VERIFY_CODES);
}
/**
* 使用指定源生成验证码
*
* @param verifySize 验证码长度
* @param sources 验证码字符源
* @return
*/
public static String generateVerifyCode(int verifySize, String sources) {
// 未设定展示源的字码,赋默认值大写字母+数字
if (sources == null || sources.length() == 0) {
sources = VERIFY_CODES;
}
int codesLen = sources.length();
Random rand = new Random(System.currentTimeMillis());
StringBuilder verifyCode = new StringBuilder(verifySize);
for (int i = 0; i < verifySize; i++) {
verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1)));
}
return verifyCode.toString();
}
/**
* 生成随机验证码文件,并返回验证码值 (生成图片形式,用的较少)
*
* @param w
* @param h
* @param outputFile
* @param verifySize
* @return
* @throws IOException
*/
public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException {
String verifyCode = generateVerifyCode(verifySize);
outputImage(w, h, outputFile, verifyCode);
return verifyCode;
}
/**
* 生成指定验证码图像文件
*
* @param w
* @param h
* @param outputFile
* @param code
* @throws IOException
*/
public static void outputImage(int w, int h, File outputFile, String code) throws IOException {
if (outputFile == null) {
return;
}
File dir = outputFile.getParentFile();
//文件不存在
if (!dir.exists()) {
//创建
dir.mkdirs();
}
try {
outputFile.createNewFile();
FileOutputStream fos = new FileOutputStream(outputFile);
outputImage(w, h, fos, code);
fos.close();
} catch (IOException e) {
throw e;
}
}
/**
* 输出指定验证码图片流
*
* @param w
* @param h
* @param os
* @param code
* @throws IOException
*/
public static void outputImage(int w, int h, OutputStream os, String code) throws IOException {
int verifySize = code.length();
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Random rand = new Random();
Graphics2D g2 = image.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 创建颜色集合,使用java.awt包下的类
Color[] colors = new Color[5];
Color[] colorSpaces = new Color[]{Color.WHITE, Color.CYAN,
Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
Color.PINK, Color.YELLOW};
float[] fractions = new float[colors.length];
for (int i = 0; i < colors.length; i++) {
colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
fractions[i] = rand.nextFloat();
}
Arrays.sort(fractions);
// 设置边框色
g2.setColor(Color.GRAY);
g2.fillRect(0, 0, w, h);
Color c = getRandColor(200, 250);
// 设置背景色
g2.setColor(c);
g2.fillRect(0, 2, w, h - 4);
// 绘制干扰线
Random random = new Random();
// 设置线条的颜色
g2.setColor(getRandColor(160, 200));
for (int i = 0; i < 20; i++) {
int x = random.nextInt(w - 1);
int y = random.nextInt(h - 1);
int xl = random.nextInt(6) + 1;
int yl = random.nextInt(12) + 1;
g2.drawLine(x, y, x + xl + 40, y + yl + 20);
}
// 添加噪点
// 噪声率
float yawpRate = 0.05f;
int area = (int) (yawpRate * w * h);
for (int i = 0; i < area; i++) {
int x = random.nextInt(w);
int y = random.nextInt(h);
// 获取随机颜色
int rgb = getRandomIntColor();
image.setRGB(x, y, rgb);
}
// 添加图片扭曲
shear(g2, w, h, c);
g2.setColor(getRandColor(100, 160));
int fontSize = h - 4;
Font font = new Font("Algerian", Font.ITALIC, fontSize);
g2.setFont(font);
char[] chars = code.toCharArray();
for (int i = 0; i < verifySize; i++) {
AffineTransform affine = new AffineTransform();
affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize / 2, h / 2);
g2.setTransform(affine);
g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10);
}
g2.dispose();
ImageIO.write(image, "jpg", os);
}
/**
* 随机颜色
*
* @param fc
* @param bc
* @return
*/
private static Color getRandColor(int fc, int bc) {
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
private static int getRandomIntColor() {
int[] rgb = getRandomRgb();
int color = 0;
for (int c : rgb) {
color = color << 8;
color = color | c;
}
return color;
}
private static int[] getRandomRgb() {
int[] rgb = new int[3];
for (int i = 0; i < 3; i++) {
rgb[i] = random.nextInt(255);
}
return rgb;
}
private static void shear(Graphics g, int w1, int h1, Color color) {
shearX(g, w1, h1, color);
shearY(g, w1, h1, color);
}
private static void shearX(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(2);
boolean borderGap = true;
int frames = 1;
int phase = random.nextInt(2);
for (int i = 0; i < h1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(0, i, w1, 1, (int) d, 0);
if (borderGap) {
g.setColor(color);
g.drawLine((int) d, i, 0, i);
g.drawLine((int) d + w1, i, w1, i);
}
}
}
private static void shearY(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(40) + 10; // 50;
boolean borderGap = true;
int frames = 20;
int phase = 7;
for (int i = 0; i < w1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(i, 0, 1, h1, 0, (int) d);
if (borderGap) {
g.setColor(color);
g.drawLine(i, (int) d, i, 0);
g.drawLine(i, (int) d + h1, i, h1);
}
}
}
}- 验证码就是使用Java代码生成的一张图片
- 验证码的作用:防止机器自动注册,攻击服务器
- 展示验证码:展示验证码图片,并可以点击
-
实现流程分析
- 前端发送请求给CheckCodeServlet
- CheckCodeServlet接收到请求后,生成验证码图片,将图片用Reponse对象的输出流写回到前端
- 思考:如何将图片写回到前端浏览器呢?
- 前面在学Reponse对象的时候,它有一个方法可以获取其字节输出流,getOutputStream()
- 我们可以把写往磁盘的流对象更好成Response的字节流,即可完成图片响应给前端
-
具体实现
- 修改Register.jsp页面,将验证码的图片从后台获取
给看不清
绑定一个点击事件,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<form action="/brand_demo/registerServlet" method="post">
<h1>欢迎注册</h1>
已有账号?<a href="login.jsp">点击登录</a><br>
用户名:<input name="username" type="text"><br>
<div>${register_msg}</div>
密码:<input name="password" type="password"><br>
验证码:<input name="checkCode" type="text">
<img id="checkCodeImg" src="/brand_demo/checkCodeServlet">
<a href="#" id="checkImg">看不清?</a>
<input value="注册" type="submit">
</form>
<script>
document.getElementById("checkImg").onclick = function () {
//路径后面加一个时间戳,能保证生成的图片永远不一样,避免浏览器缓存静态资源
document.getElementById("checkCodeImg").src = "/brand_demo/checkCodeServlet?" + new Date().getMilliseconds();
}
</script>- 编写CheckCodeServlet类,用来接收请求生成验证码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CheckCodeServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//生成验证码
OutputStream os = response.getOutputStream();
String checkCode = CheckCodeUtil.outputVerifyImage(100, 50, os, 4);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}- 完成这些之后,我们启动服务器,就可以看到验证码了,点击
看不清
,也会自动刷新验证码
- 修改Register.jsp页面,将验证码的图片从后台获取
验证码-校验
-
需求
-
判断程序生成的验证码 和 用户输入的验证码 是否一样,如果不一样,则阻止注册
-
验证码图片访问和提交注册表单是
两次
请求,所以要将程序生成的验证码存入Session中 -
思考:为什么要把验证码数据存入到Session中呢?
- 生成验证码和校验验证码是两次请求,此处就需要在一个会话的两次请求之间共享数据
- 验证码属于安全数据类的,所以我们选中Session来存储验证码数据。
-
-
实现流程分析
- 在CheckCodeServlet中生成验证码的时候,将验证码数据存入Session对象
- 前端将验证码和注册数据提交到后台,交给RegisterServlet类
- RegisterServlet类接收到请求和数据后,其中就有验证码,和Session中的验证码进行对比
- 如果一致,则完成注册,如果不一致,则提示错误信息
-
具体实现
- 修改CheckCodeServlet类,将验证码存入Session域
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CheckCodeServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//生成验证码
OutputStream os = response.getOutputStream();
String checkCode = CheckCodeUtil.outputVerifyImage(100, 50, os, 4);
//将验证码存入session域
HttpSession session = request.getSession();
session.setAttribute("checkCodeGen", checkCode);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}- 在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
public class RegisterServlet extends HttpServlet {
private UserService userService = new UserService();
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取客户输入的用户名和密码
String username = request.getParameter("username");
String password = request.getParameter("password");
//多获取一个用户输入的验证码
String checkCode = request.getParameter("checkCode");
//封装成一个User
User user = new User();
user.setUsername(username);
user.setPassword(password);
//从Session中获取验证码
HttpSession session = request.getSession();
String checkCodeGen = (String) session.getAttribute("checkCodeGen");
//如果验证码不一致
if (!checkCodeGen.equals(checkCode)) {
//将错误信息存储到request域中
request.setAttribute("checkCode_msg", "验证码输入错误");
//同时跳转至注册页面
request.getRequestDispatcher("/register.jsp").forward(request, response);
//不允许注册,结束本次操作
return;
}
//注册账号,用flag来接收是否注册成功
boolean flag = userService.register(user);
if (flag) {
//如果成功注册,将注册成功的提示存入request域中,随后跳转到登录页面
request.setAttribute("register_msg", "注册成功请登录");
request.getRequestDispatcher("/login.jsp").forward(request, response);
} else {
//如果注册失败,就是用户名重复了,将错误信息存入request域中,并返回注册页面
request.setAttribute("register_msg", "用户名已存在");
request.getRequestDispatcher("/register.jsp").forward(request, response);
}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}- 验证码错误,需要在注册页面上显示提示信息,修改
register.jsp
1
2
3
4
5
6
7
8
9
10
11
12<form action="/brand_demo/registerServlet" method="post">
<h1>欢迎注册</h1>
已有账号?<a href="login.jsp">点击登录</a><br>
用户名:<input name="username" type="text"><br>
<div>${register_msg}</div>
密码:<input name="password" type="password"><br>
<div>${checkCode_msg}</div> <!--在这里显示一个提示信息-->
验证码:<input name="checkCode" type="text">
<img id="checkCodeImg" src="/brand_demo/checkCodeServlet">
<a href="#" id="checkImg">看不清?</a>
<input value="注册" type="submit">
</form>- 至此,用户的注册登录功能就已经完成了。
Filter&Listener&Ajax&Json
1,Filter
1.1 Filter概述
Filter 表示过滤器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一。
过滤器可以把对资源的请求拦截
下来,从而实现一些特殊的功能。
例如某些网站未登录不能查看评论,不能将商品加入购物车,得把你拦下来,先让你登录,也就是在访问前,先经过Filter。
下面来具体说说,拦截器拦截到后可以做什么功能呢?
过滤器一般完成一些通用的操作。比如每个资源都要写一些代码完成某个功能,我们总不能在每个资源中写这样的代码吧,而此时我们可以将这些代码写在过滤器中,因为请求每一个资源都要经过过滤器。
我们之前做的品牌数据管理的案例中就已经做了登陆的功能,但这个登录功能其实是如同虚设的,我们可以直接访问登录后的页面,所以本文的目标就是完善登录功能,不登录就无法查看数据。
1.2 Filter快速入门
1.2.1 开发步骤
进行 Filter
开发分成以下三步实现
-
定义类,实现 Filter接口,并重写其所有方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//实现Filter接口,重写所有方法
public class FilterDemo1 implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
}
public void destroy() {
}
} -
配置Filter拦截资源的路径:在类上定义
@WebFilter
注解。而注解的value
属性值/*
表示拦截所有的资源1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//这里暂时先拦截所有资源,后面我们会仔细讲拦截路径的配置
public class FilterDemo1 implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
}
public void destroy() {
}
} -
在doFilter方法中输出一句话,并放行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class FilterDemo1 implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//这句话只是来测试是否调用了该方法
System.out.println("doFilter...");
//一定要放行才能访问资源
filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {
}
}上述代码中的
chain.doFilter(request,response);
就是放行,也就是让其访问本该访问的资源。
1.2.2 代码演示
- 创建一个web项目,在
webapp
下创建hello.jsp
页面
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
pom.xml
配置文件内容如下:
1 |
|
在java目录下新建com.blog.web.filter
包,并新建FilterDemo1
类
1 |
|
- 测试
- 将放行的代码注释掉,我们访问
hello.jsp
页面,不会有任何内容,因为被拦截了,且没有放行,控制台会输出doFilter...
- 打开放行的代码,我们访问
hello.jsp
页面,会有h1标签正常输出HELLO FILTER
- 将放行的代码注释掉,我们访问
上述效果说明了FilterDemo1这个过滤器的doFilter
方法被执行了,且必须添加放行的方法才能访问hello.jsp
页面
1.3 Filter执行流程
如上图是使用过滤器的流程,我们通过以下问题来研究过滤器的执行流程:
-
放行后访问对应资源,资源访问完成后,还会回到Filter中吗?
从上图就可以看出肯定
会
回到Filter中 -
如果回到Filter中,是重头执行还是执行放行后的逻辑呢?
如果是重头执行的话,就意味着
放行前逻辑
会被执行两次,肯定不会这样设计了;所以访问完资源后,会回到放行后逻辑
,执行该部分代码。
通过上述的说明,我们就可以总结Filter的执行流程如下:
- 执行放行前逻辑 --> 放行 --> 访问资源 --> 执行放行后逻辑
接下来我们通过代码验证一下,在 doFilter()
方法前后都加上输出语句,如下
1 | public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { |
同时在 hello.jsp
页面加上输出语句,如下
1 | <body> |
执重启服务器,访问hello.jsp
页面,控制台输出结果如下,符合我们的预期结果
1.Filter…
2.Filter…
3.Filter
1.4 Filter拦截路径配置
拦截路径表示 Filter 会对请求的哪些资源进行拦截,使用 @WebFilter
注解进行配置。如:@WebFilter("拦截路径")
拦截路径有如下四种配置方式:
- 拦截具体的资源:/index.jsp:只有访问index.jsp时才会被拦截
- 目录拦截:/user/*:访问/user下的所有资源,都会被拦截
- 后缀名拦截:*.jsp:访问后缀名为jsp的资源,都会被拦截
- 拦截所有:/*:访问所有资源,都会被拦截
1.5 过滤器链
1.5.1 概述
过滤器链是指在一个Web应用,可以配置多个过滤器,这多个过滤器称为过滤器链。
如下图就是一个过滤器链,我们学习过滤器链主要是学习过滤器链执行的流程
上图中的过滤器链执行是按照以下流程执行:
- 执行
Filter1
的放行前逻辑代码 - 执行
Filter1
的放行代码 - 执行
Filter2
的放行前逻辑代码 - 执行
Filter2
的放行代码 - 访问到资源
- 执行
Filter2
的放行后逻辑代码 - 执行
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
public class FilterDemo implements Filter {
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...");
}
public void init(FilterConfig filterConfig) throws ServletException {
}
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
public class FilterDemo2 implements Filter {
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...");
}
public void init(FilterConfig filterConfig) throws ServletException {
}
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
进行测试,在控制台打印内容如下1
2
3
4
51.FilterDemo.
2.FilterDemo
3.hello jsp
4.FilterDemo
5.FilterDemo从结果可以看到确实是按照我们之前说的执行流程进行执行的。
1.5.3 问题
上面代码中为什么是先执行 FilterDemo
,后执行 FilterDemo2
呢?
我们现在使用的是注解配置Filter,而这种配置方式的优先级是按照过滤器类名(字符串)的自然排序。
比如有如下两个名称的过滤器 : BFilterDemo
和 AFilterDemo
。那一定是 AFilterDemo
过滤器先执行。
1.6 案例
1.6.1 需求
访问服务器资源时,需要先进行登录验证,如果没有登录,则自动跳转到登录页面
1.6.2 分析
我们要实现该功能是在每一个资源里加入登陆状态校验的代码吗?显然是不需要的,只需要写一个 Filter
,在该过滤器中进行登陆状态校验即可。而在该 Filter
中逻辑如下:
- 判断访问的是否为登录之后才能看的资源
- 是:放行
- 不是:进行登录验证
- 判断用户是否登录:Session中是否有user对象
- 登录:放行
- 未登录:跳转至登录页面,并给出提示信息
1.6.3 代码实现
1.6.3.1 创建Filter
在 brand-demo
工程创建 com.itheima.web.filter
包,在该下创建名为 LoginFilter
的过滤器
1 |
|
1.6.3.2 编写逻辑代码
在 doFilter()
方法中编写登陆状态校验的逻辑代码。
我们首先需要从 session
对象中获取用户信息,但是 ServletRequest
类型的 requset 对象没有获取 session 对象的方法,所以此时需要将 request对象强转成 HttpServletRequest
对象。
1 | HttpServletRequest req = (HttpServletRequest) request; |
然后完成以下逻辑
- 获取Session对象
- 从Session对象中获取名为
user
的数据 - 判断获取到的数据是否是 null
- 如果不是,说明已经登陆,放行
- 如果是,说明尚未登陆,将提示信息存储到域对象中并跳转到登陆页面
代码如下:
1 |
|
1.6.3.3 测试并抛出问题
重启服务器,访问register.jsp
注册页面,竟然访问不了了,也是直接跳转到了登录页面,而且如果你配置了css文件,css样式也显示不出来了,这是怎么回事呢?
1.6.3.4 问题分析及解决
因为我们配置的是对所有页面进行拦截,但现在需要对所有的登陆和注册相关的资源进行放行。
所以我们需要在判断session中是否包含用户信息之前,应该加上对登陆及注册相关资源放行的逻辑处理
1 | //判断访问资源路径是否和登录注册相关 |
1.6.3.5 过滤器完整代码
1 |
|
2,Listener
2.1 概述
-
Listener 表示监听器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一。
-
监听器可以监听就是在
application
,session
,request
三个对象创建、销毁或者往其中添加修改删除属性时自动执行代码的功能组件。request 和 session 我们学习过。而
application
是ServletContext
类型的对象。ServletContext
代表整个web应用,在服务器启动的时候,tomcat会自动创建该对象。在服务器关闭时会自动销毁该对象。
2.2 分类
JavaWeb 提供了8个监听器:
监听器分类 | 监听器名称 | 作用 |
---|---|---|
servletContext监听 | servletContextListener | 用于对ServletContext对象进行监听(创建、销毁) |
ServletContextAttributeListener | 对ServletContext对象中属性的监听(增删改属性) | |
session监听 | HttpSessionListener | 对Session对象的整体状态的监听(创建、销毁) |
HttpSessionAttributeListener | 对Session对象中的属性监听(增删改属性) | |
HttpSessionBindingListener | 监听对象于Session的绑定和解除 | |
HttpsessionActivationListener | 对Session数据的钝化和活化的监听 | |
Request监听 | servletRequestListener | 对Request对象进行监听(创建、销毁) |
servletRequestAttributeListener | 对Request对象中属性的监听(增删改属性) |
这里面只有 ServletContextListener
这个监听器后期我们会接触到,ServletContextListener
是用来监听 ServletContext
对象的创建和销毁。
ServletContextListener
接口中有以下两个方法
void contextInitialized(ServletContextEvent sce)
:ServletContext
对象被创建了会自动执行的方法void contextDestroyed(ServletContextEvent sce)
:ServletContext
对象被销毁时会自动执行的方法
2.3 代码演示
我们只演示一下 ServletContextListener
监听器
- 定义一个类,实现
ServletContextListener
接口 - 重写所有的抽象方法
- 使用
@WebListener
进行配置
代码如下:
1 |
|
启动服务器,就可以在启动的日志信息中看到 contextInitialized()
方法输出的内容,同时也说明了 ServletContext
对象在服务器启动的时候被创建了。
3,Ajax
3.1 概述
AJAX
(Asynchronous JavaScript And XML):异步的 JavaScript 和 XML。`
3.1.1 作用
AJAX 作用有以下两方面:
- 与服务器进行数据交换:通过AJAX可以给服务器发送请求,服务器将数据直接响应回给浏览器。
- 在之前,我们做功能的流程是,
Servlet
调用完业务逻辑层,然后将数据存储到域对象中,然后跳转到指定的jsp
页面,在页面上使用EL表达式
和JSTL
标签库进行数据的展示。 - 而我们学习了AJAX 后,就可以使用AJAX和服务器进行通信,以达到
使用HTML+AJAX来替换JSP页面
了。浏览器发送请求servlet,servlet 调用完业务逻辑层后将数据直接响应回给浏览器页面,页面使用 HTML 来进行数据展示。
- 在之前,我们做功能的流程是,
- 异步交互:可以在不重新加载整个页面的情况下,与服务器交换数据并更新部分网页的技术,如:搜索联想、用户名是否可用校验,等等…
- 当我们在百度输入一些关键字(例如
奥运
)后就会在下面联想出相关的内容,而联想出来的这部分数据肯定是存储在百度的服务器上,而我们并没有看出页面重新刷新,这就是更新局部页面
的效果。 - 我们在用户名的输入框输入用户名,当输入框一失去焦点,如果用户名已经被占用就会在下方展示提示的信息;在这整个过程中也没有页面的刷新,只是在局部展示出了提示信息,这就是
更新局部页面
的效果。
- 当我们在百度输入一些关键字(例如
3.1.2 同步和异步
知道了局部刷新后,接下来我们再聊聊同步和异步:
- 同步发送请求过程如下
浏览器页面在发送请求给服务器,在服务器处理请求的过程中,浏览器页面不能做其他的操作。只能等到服务器响应结束后才能,浏览器页面才能继续做其他的操作。
-
异步发送请求过程如下
浏览器页面发送请求给服务器,在服务器处理请求的过程中,浏览器页面还可以做其他的操作。
3.2 快速入门
3.2.1 服务端实现
在项目的创建 com.itheima.web.servlet
,并在该包下创建名为 AjaxServlet
的servlet
1 |
|
3.2.2 客户端实现
在 webapp
下创建名为 01-ajax-demo1.html
的页面,在该页面书写 ajax
代码
-
创建核心对象,不同的浏览器创建的对象是不同的
1
2
3
4
5
6
7var 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
6xhttp.onreadystatechange = function() {
if (this.readyState ` 4 && this.status ` 200) {
// 通过 this.responseText 可以获取到服务端响应的数据
alert(this.responseText);
}
};
完整代码如下:
1 |
|
3.2.3 测试
在浏览器地址栏输入 http://localhost:8080/ajax-demo/01-ajax-demo1.html
,在 01-ajax-demo1.html
加载的时候就会发送 ajax
请求,并有一个alert弹窗输出hello ajax~
3.3 案例
需求:在完成用户注册时,当用户名输入框失去焦点时,校验用户名是否在数据库已存在
3.3.1 分析
- 前端完成的逻辑
- 给用户名输入框绑定光标失去焦点事件
onblur
- 发送 ajax请求,携带username参数
- 处理响应:是否显示提示信息
- 给用户名输入框绑定光标失去焦点事件
- 后端完成的逻辑
- 接收用户名
- 调用service查询User。此案例是为了演示前后端异步交互,所以此处我们不做业务逻辑处理
- 返回标记
整体流程如下:
3.3.2 后端实现
在 com.ithiema.web.servlet
包中定义名为 SelectUserServlet
的servlet。代码如下:
1 |
|
3.3.3 前端实现
- 随便写一个注册的页面
register.html
1 |
|
第一步:给用户名输入框绑定光标失去焦点事件 onblur
1 | //1. 给用户名输入框绑定 失去焦点事件 |
第二步:发送 ajax请求,携带username参数
在 第一步
绑定的匿名函数中书写发送 ajax 请求的代码
1 | //2. 发送ajax请求 |
由于我们发送的是 GET 请求,所以需要在 URL 后拼接从输入框获取的用户名数据。而我们在 第一步
绑定的匿名函数中通过以下代码可以获取用户名数据
1 | // 获取用户名的值 |
而携带数据需要将 URL 修改为:
1 | xhttp.open("GET", "http://localhost:8080/ajax-demo/selectUserServlet?username="+username); |
第三步:处理响应:是否显示提示信息
当 this.readyState
4 && this.status 200
条件满足时,说明已经成功响应数据了。
此时需要判断响应的数据是否是 “true” 字符串,如果是说明用户名已经占用给出错误提示;如果不是说明用户名未被占用清除错误提示。代码如下
1 | //判断 |
综上所述,前端完成代码如下:
1 | //1. 给用户名输入框绑定 失去焦点事件 |
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
6axios({
method:"get",
url:"http://localhost:8080/ajax-demo1/aJAXDemo1?username=zhangsan"
}).then(function (resp){
alert(resp.data);
}) -
发送 post 请求
1
2
3
4
5
6
7axios({
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 |
|
4.2.2 前端实现
-
引入 js 文件
1
<script src="js/axios-0.18.0.js"></script>
-
发送 ajax 请求
-
get 请求
1
2
3
4
5
6axios({
method:"get",
url:"http://localhost:8080/ajax-demo/axiosServlet?username=zhangsan"
}).then(function (resp) {
alert(resp.data);
}) -
post 请求
1
2
3
4
5
6
7axios({
method:"post",
url:"http://localhost:8080/ajax-demo/axiosServlet",
data:"username=zhangsan"
}).then(function (resp) {
alert(resp.data);
})
-
整体页面代码如下:
1 |
|
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 | axios.get("http://localhost:8080/ajax-demo/axiosServlet?username=zhangsan").then(function (resp) { |
入门案例中的 post
请求代码可以改为如下:
1 | axios.post("http://localhost:8080/ajax-demo/axiosServlet","username=zhangsan").then(function (resp) { |
5,JSON
5.1 概述
概念:
JavaScript Object Notation。JavaScript 对象表示法.
如下是 JavaScript
对象的定义格式:
1 | { |
接下来我们再看看 JSON
的格式:
1 | { |
通过上面 js 对象格式和 json 格式进行对比,发现两个格式特别像。只不过 js 对象中的属性名可以使用引号(可以是单引号,也可以是双引号);而 json
格式中的键要求必须使用双引号括起来,这是 json
格式的规定。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 |
|
通过浏览器打开,浏览器会有一个弹窗显示`{“name”:“zhangsan”,“age”:18,“addr”:[“北京”,“上海”,“广州”,“深圳”]}
现在我们需要获取到该 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 |
|
5.2.3 发送异步请求携带参数
后面我们使用 axios
发送请求时,如果要携带复杂的数据时都会以 JSON
格式进行传递,如下
1 | axios({ |
请求参数不可能由我们自己拼接字符串,我们可以提前定义一个 js 对象,用来封装需要提交的参数,然后使用 JSON.stringify(js对象)
转换为 JSON
串,再将该 JSON
串作为 axios
的 data
属性值进行请求参数的提交。如下:
1 | var jsObject = {name:"张三"}; |
而 axios
是一个很强大的工具。我们只需要将需要提交的参数封装成 js 对象,并将该 js 对象作为 axios
的 data
属性值进行,它会自动将 js 对象转换为 JSON
串进行提交。如下:
1 | var jsObject = {name:"张三"}; |
注意:
- js 提供的
JSON
对象我们只需要了解一下即可。因为axios
会自动对 js 对象和JSON
串进行想换转换。- 发送异步请求时,如果请求参数是
JSON
格式,那请求方式必须是POST
。因为JSON
串需要放在请求体中。
5.3 JSON串和Java对象的相互转换
在后端我们就需要重点学习以下两部分操作:
- 请求数据:JSON字符串转为Java对象
- 响应数据:Java对象转为JSON字符串
接下来给大家介绍一套 API,可以实现上面两部分操作。这套 API 就是 Fastjson
5.3.1 Fastjson 概述
Fastjson
是阿里巴巴提供的一个Java语言编写的高性能功能完善的 JSON
库,是目前Java语言中最快的 JSON
库,可以实现 Java
对象和 JSON
字符串的相互转换。
5.3.2 Fastjson 使用
Fastjson
使用也是比较简单的,分为以下三步完成
-
导入坐标
1
2
3
4
5<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency> -
Java对象转JSON
1
String jsonStr = JSON.toJSONString(obj);
将 Java 对象转换为 JSON 串,只需要使用
Fastjson
提供的JSON
类中的toJSONString()
静态方法即可。 -
JSON字符串转Java对象
1
User user = JSON.parseObject(jsonStr, User.class);
将 json 转换为 Java 对象,只需要使用
Fastjson
提供的JSON
类中的parseObject()
静态方法即可。
5.3.3 代码演示
- 先创建一个User类
1 | package com.blog.web.pojo; |
- 测试方法
1 | import com.alibaba.fastjson.JSON; |
- 得到输出结果如下
{“id”:1,“password”:“asd123”,“username”:“zhangsan”}
User{id=1, username=‘zhangsan’, password=‘asd123’}