在此特别感谢黑马程序员的精品课程

本文记录内容(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驱动包

image-20210725133015535

2,JDBC快速入门

先来看看通过Java操作数据库的流程

image-20210725163745153

第一步:编写Java代码

第二步:Java代码将SQL发送到MySQL服务端

第三步:MySQL服务端接收到SQL语句并执行该SQL语句

第四步:将SQL语句执行的结果返回给Java代码

2.1 编写代码步骤

  • 创建工程,导入驱动jar包

    image-20210725133015535
  • 注册驱动

    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 具体操作

  1. 创建新的空的项目
  2. 定义项目的名称,并指定位置
  3. 对项目进行设置,JDK版本、编译版本
  4. 创建模块,指定模块的名称及位置
  5. 导入驱动包
  • 将mysql的驱动包放在模块下的lib目录(随意命名)下,并将该jar包添加为库文件
  • 在添加为库文件的时候,有如下三个选项
    • Global Library : 全局有效
    • Project Library : 项目有效
    • Module Library : 模块有效
  1. 在src下创建类

  2. 编写代码如下

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
/**
* JDBC快速入门
*/
public class JDBCDemo {

public static void main(String[] args) throws Exception {
//1. 注册驱动
//Class.forName("com.mysql.jdbc.Driver");
//2. 获取连接
String url = "jdbc:mysql://127.0.0.1:3306/db1";
String username = "root";
String password = "1234";
Connection conn = DriverManager.getConnection(url, username, password);
//3. 定义sql
String sql = "update account set money = 2000 where id = 1";
//4. 获取执行sql的对象 Statement
Statement stmt = conn.createStatement();
//5. 执行sql
int count = stmt.executeUpdate(sql);//受影响的行数
//6. 处理结果
System.out.println(count);
//7. 释放资源
stmt.close();
conn.close();
}
}

3,JDBC API详解

3.1 DriverManager

DriverManager(驱动管理类)作用:

  • 注册驱动

    registerDriver方法是用于注册驱动的,但是我们之前做的入门案例并不是这样写的。而是如下实现

    1
    Class.forName("com.mysql.jdbc.Driver");

    我们查询MySQL提供的Driver类,看它是如何实现的,源码如下:

    image-20210725171635432

    在该类中的静态代码块中已经执行了 DriverManager 对象的 registerDriver() 方法进行驱动的注册了,那么我们只需要加载 Driver 类,该静态代码块就会执行。而 Class.forName("com.mysql.jdbc.Driver"); 就可以加载 Driver 类。

    ==提示:==

    • MySQL 5之后的驱动包,可以省略注册驱动的步骤
    • 自动加载jar包中META-INF/services/java.sql.Driver文件中的驱动类
  • 获取数据库连接

    image-20210725171355278

    参数说明:

    • 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
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
/**
* JDBC API 详解:Connection
*/
public class JDBCDemo3_Connection {

public static void main(String[] args) 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 sql1 = "update account set money = 3000 where id = 1";
String sql2 = "update account set money = 3000 where id = 2";
//4. 获取执行sql的对象 Statement
Statement stmt = conn.createStatement();

try {
// ============开启事务==========
conn.setAutoCommit(false);
//5. 执行sql
int count1 = stmt.executeUpdate(sql1);//受影响的行数
//6. 处理结果
System.out.println(count1);
int i = 3/0;
//5. 执行sql
int count2 = stmt.executeUpdate(sql2);//受影响的行数
//6. 处理结果
System.out.println(count2);

// ============提交事务==========
//程序运行到此处,说明没有出现任何问题,则需求提交事务
conn.commit();
} catch (Exception e) {
// ============回滚事务==========
//程序在出现异常时会执行到这个地方,此时就需要回滚事务
conn.rollback();
e.printStackTrace();
}

//7. 释放资源
stmt.close();
conn.close();
}
}

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
    */
    @Test
    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
    */
    @Test
    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
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
/**
* 执行DQL
* @throws Exception
*/
@Test
public void testResultSet() 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 = "select * from account";
//4. 获取statement对象
Statement stmt = conn.createStatement();
//5. 执行sql
ResultSet rs = stmt.executeQuery(sql);
//6. 处理结果, 遍历rs中的所有数据
/* // 6.1 光标向下移动一行,并且判断当前行是否有数据
while (rs.next()){
//6.2 获取数据 getXxx()
int id = rs.getInt(1);
String name = rs.getString(2);
double money = rs.getDouble(3);

System.out.println(id);
System.out.println(name);
System.out.println(money);

System.out.println("--------------");

}*/
// 6.1 光标向下移动一行,并且判断当前行是否有数据
while (rs.next()){
//6.2 获取数据 getXxx()
int id = rs.getInt("id");
String name = rs.getString("name");
double money = rs.getDouble("money");

System.out.println(id);
System.out.println(name);
System.out.println(money);

System.out.println("--------------");
}

//7. 释放资源
rs.close();
stmt.close();
conn.close();
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 @Test
public void testPreparedStatement() throws Exception {
//2. 获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写
String url = "jdbc:mysql:///db1?useSSL=false";
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);
// 设置?的值
pstmt.setString(1,name);
pstmt.setString(2,pwd);
// 执行sql
ResultSet rs = pstmt.executeQuery();
// 判断登录是否成功
if(rs.next()){
System.out.println("登录成功~");
}else{
System.out.println("登录失败~");
}
//7. 释放资源
rs.close();
pstmt.close();
conn.close();
}

执行上面语句就可以发现不会出现SQL注入漏洞问题了。那么PreparedStatement又是如何解决的呢?它是将特殊字符进行了转义,转义的SQL如下:

1
select * from tb_user where username = 'sjdljfld' and password = '\'or \'1\' = \'1'

3.5.3 PreparedStatement原理

PreparedStatement 好处:

  • 预编译SQL,性能更高
  • 防止SQL注入:==将敏感字符进行转义==
image-20210725195756848

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
    6
    log-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
    */
    @Test
    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对象的复用,如下图

image-20210725210432985

连接池是在一开始就创建好了一些连接(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下并添加为库文件

image-20210725212911980

编写配置文件如下:

1
2
3
4
5
6
7
8
9
10
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql:///db1?useSSL=false&useServerPrepStmts=true
username=root
password=1234
# 初始化连接数量
initialSize=5
# 最大连接数
maxActive=10
# 最大等待时间
maxWait=3000

使用druid的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Druid数据库连接池演示
*/
public class DruidDemo {

public static void main(String[] args) throws Exception {
//1.导入jar包
//2.定义配置文件
//3. 加载配置文件
Properties prop = new Properties();
prop.load(new FileInputStream("jdbc-demo/src/druid.properties"));
//4. 获取连接池对象
DataSource dataSource = DruidDataSourceFactory.createDataSource(prop);

//5. 获取数据库连接 Connection
Connection connection = dataSource.getConnection();
System.out.println(connection); //获取到了连接后就可以继续做其他操作了

//System.out.println(System.getProperty("user.dir"));
}
}

Maven&MyBatis

1,Maven

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

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

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

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

标准化的项目结构:

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

image-20210726153815028

标准化的构建流程:

image-20210726154144488

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

依赖管理:

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

image-20210726154753631

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

image-20210726154922337

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

1.1 Maven简介

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

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

1.1.1 Maven模型

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

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

image-20210726160928515

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

image-20210726161340796

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

image-20210726161616034

1.1.2 仓库

仓库分类:

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

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

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

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

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

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

image-20210726162605394

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

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

image-20210726162815045

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)生命周期如下:

image-20210726173153576

当我们执行 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
2
3
<groupId>com.alibaba</groupId>  <!--隶属组织阿里巴巴-->
<artifactId>druid</artifactId> <!--模块名称是德鲁伊-->
<version>1.1.12</version> <!--版本号是1.1.12-->

==注意:==

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

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

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

注意:

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

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

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

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

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

自动导入设置:

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

  • 选择 IDEA中 File --> Settings

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

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

1.5.2 依赖范围

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

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

image-20210726194703845

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

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

注意:

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

2,Mybatis

2.1 Mybatis概述

2.1.1 Mybatis概念

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

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

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

持久层:

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

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

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

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

框架:

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

2.2 Mybatis快速入门

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

  • 创建user表,添加数据

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

    drop table if exists tb_user;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2.3 Mapper代理开发

2.3.1 Mapper代理开发概述

Mapper 代理方式的目的:

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

2.3.2 使用Mapper代理要求

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

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

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

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

    image-20210726223216517

2.3.3 案例代码实现

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

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

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

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

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

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

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

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

==注意:==

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

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

2.4 核心配置文件

2.4.1 多环境配置

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

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

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

2.4.2 类型别名

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

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

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

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

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

Mybatis练习

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

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

环境准备

  • 数据库表(tb_brand)及数据准备
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 删除tb_brand表
DROP TABLE IF EXISTS tb_brand;
-- 创建tb_brand表
CREATE TABLE tb_brand
(
-- id 主键
id INT PRIMARY KEY AUTO_INCREMENT,
-- 品牌名称
brand_name VARCHAR(20),
-- 企业名称
company_name VARCHAR(20),
-- 排序字段
ordered INT,
-- 描述信息
description VARCHAR(100),
-- 状态:0:禁用 1:启用
STATUS INT
);
-- 添加数据
INSERT INTO tb_brand (brand_name, company_name, ordered, description, STATUS)
VALUES ('三只松鼠', '三只松鼠股份有限公司', 5, '好吃不上火', 0),
('华为', '华为技术有限公司', 100, '华为致力于把数字世界带入每个人、每个家庭、每个组织,构建万物互联的智能世界', 1),
('小米', '小米科技有限公司', 50, 'are you ok', 1);
  • 实体类 Brand,在 com.itheima.pojo 包下创建 Brand 实体类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
public class Brand {
// id 主键
private Integer id;
// 品牌名称
private String brandName;
// 企业名称
private String companyName;
// 排序字段
private Integer ordered;
// 描述信息
private String description;
// 状态:0:禁用 1:启用
private Integer status;


public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getBrandName() {
return brandName;
}

public void setBrandName(String brandName) {
this.brandName = brandName;
}

public String getCompanyName() {
return companyName;
}

public void setCompanyName(String companyName) {
this.companyName = companyName;
}

public Integer getOrdered() {
return ordered;
}

public void setOrdered(Integer ordered) {
this.ordered = ordered;
}

public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

public Integer getStatus() {
return status;
}

public void setStatus(Integer status) {
this.status = status;
}

@Override
public String toString() {
return "Brand{" +
"id=" + id +
", brandName='" + brandName + '\'' +
", companyName='" + companyName + '\'' +
", ordered=" + ordered +
", description='" + description + '\'' +
", status=" + status +
'}';
}
}
  • 编写测试用例,测试代码需要在 test/java 目录下创建包及测试用例。

  • 安装 MyBatisX 插件

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

  • 主要功能

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

查询所有数据

实现该功能我们分以下步骤进行实现:

  • 编写接口方法:Mapper接口
    • 参数:无
      查询所有数据功能是不需要根据任何条件进行查询的,所以此方法不需要参数。
    • 结果:List<Brand>
      我们会将查询出来的每一条数据封装成一个 Brand 对象,而多条数据封装多个 Brand 对象,需要将这些对象封装到List集合中返回。
    • 执行方法、测试

编写接口方法

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

1
2
3
4
5
6
7
8
9
package com.blog.mapper;

import com.blog.pojo.Brand;

import java.util.List;

public interface BrandMapper {
List<Brand> selectAll();
}

编写SQL语句

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

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.blog.mapper.BrandMapper">
<select id="selectAll" resultMap="brandResultMapper">
select *
from tb_brand;
</select>
</mapper>

编写测试方法

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testSelectAll() throws IOException{
//1. 加载MyBatis核心配置文件,获取SqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
List<Brand> brands = brandMapper.selectAll();
System.out.println(brands);
//5. 资源关闭
sqlSession.close();
}

执行测试方法部分结果如下
Brand{id=1, brandName='null', companyName='null', ordered=5, description='好吃不上火', status=0
其中brandName和companyName的值为NULL,究其原因是我们Brand类中的属性名和MySQL中的列名不一致。
这个问题我在上篇JDBC文章中给出了一个比较初级的解决方案,那就是给列取别名,令别名与我们Java中的命名方式相同,这样我们就可以通过获取列的别名,来进行反射赋值。
另一种解决方式是使用resultMap定义字段和属性的映射关系(推荐)

起别名解决上述问题

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

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

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

SQL片段:

  • 将需要复用的SQL片段抽取到 sql 标签中
1
2
3
<sql id="brand_column">
id, brand_name as brandName, company_name as companyName, ordered, description, status
</sql>

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

  • 在原sql语句中进行引用
    使用 include 标签引用上述的 SQL 片段,而 refid 指定上述 SQL 片段的id值
1
2
3
4
5
<select id="selectAll" resultType="brand">
select
<include refid="brand_column" />
from tb_brand;
</select>

使用resultMap解决上述问题

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

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

  • 在映射配置文件中使用resultMap定义 字段属性 的映射关系
1
2
3
4
5
6
7
8
9
10
11
12
13
<!--id用来标识唯一的resultMap,type表示映射类型-->
<resultMap id="brandResultMap" type="brand">
<!--
id:完成主键字段的映射
column:表的列名
property:实体类的属性名
result:完成一般字段的映射
column:表的列名
property:实体类的属性名
-->
<result column="brand_name" property="brandName"/>
<result column="company_name" property="companyName"/>
</resultMap>

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

  • SQL语句正常编写
1
2
3
4
5
<!--使用resultMap属性替换掉原有的resultType属性-->
<select id="selectAll" resultMap="brandResultMap">
select *
from tb_brand;
</select>

查询详情

查看详情功能实现步骤:

  • 编写接口方法:Mapper接口
    • 参数:id
      查看详情就是查询某一行数据,所以需要根据id进行查询。而id以后是由页面传递过来。
    • 结果:Brand
      根据id查询出来的数据只要一条,而将一条数据封装成一个Brand对象即可
  • 编写SQL语句:SQL映射文件
  • 执行方法、进行测试

编写接口方法

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

1
Brand selectById(int id);

编写SQL语句

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

1
2
3
4
5
<select id="selectById" resultMap="brandResultMapper">
select *
from tb_brand
where id = #{id}; <!--这个#{id}是占位符,后面会详细说-->
</select>

编写测试方法

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testSelectById() throws IOException {
//接收参数,该id以后需要传递过来
int id = 1;
//1. 加载MyBatis核心配置文件,获取SqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
Brand brand = brandMapper.selectById(id);
System.out.println(brand);
//5. 资源关闭
sqlSession.close();
}

参数占位符

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

  • #{} :执行SQL时,会将 #{} 占位符替换为?,将来自动设置参数值。#{} 底层使用的是 PreparedStatement
  • ${} :拼接SQL。底层使用的是 Statement,会存在SQL注入问题。

以后开发我们使用 #{} 参数占位符。

parameterType使用

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

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

SQL语句中特殊字段处理

在xml中,”<”、”>”、”&”等字符是不能直接存入的,否则xml语法检查时会报错,如果想在xml中使用这些符号,必须将其转义为实体,如<>&,这样才能保存进xml文档。或者使用<![CDATA[]]>,被这个标记所包含的内容将表示为纯文本

但是严格来说,在XML中只有”<”和”&”是非法的,其它三个都是可以合法存在的,但是,把它们都进行转义是一个好的习惯。

不管怎么样,转义前的字符也好,转义后的字符也好,都会被xml解析器解析,为了方便起见,使用<![CDATA[]]>来包含不被xml解析器解析的内容。但要注意的是:

  • 此部分不能再包含]]>
  • 不允许嵌套使用;
  • ]]>这部分不能包含空格或者换行。

最后,说说<![CDATA[]]>和xml转移字符的关系,它们两个看起来是不是感觉功能重复了?

  • 是的,它们的功能就是一样的,只是应用场景和需求有些不同:

    • <![CDATA[]]>不能适用所有情况,转义字符可以;
    • 对于短字符串<![CDATA[]]>写起来啰嗦,对于长字符串转义字符写起来可读性差;
    • <![CDATA[]]>表示xml解析器忽略解析,所以更快。
  • 转义字符

  • CDATA

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

多条件查询

商品查询.png

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

  • 条件表达式
  • 如何连接

当前状态使用status字段表示,企业名称使用company_name表示,品牌名称使用brand_name表示
条件字段 企业名称品牌名称 需要进行模糊查询,所以条件应该是:

1
2
3
4
5
select *
from tb_brand
where `status` = #{status}
and company_name like #{companyName}
and brand_name like #{brandName};

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

  • 编写接口方法
    • 参数:所有查询条件
    • 结果:List<Brand>
  • 在映射配置文件中编写SQL语句
  • 编写测试方法并执行

编写接口方法

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

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

  1. 使用 @Param( "SQL参数占位符名称") 标记每一个参数,在映射配置文件中就需要使用 #{参数名称} 进行占位
1
List<Brand> selectByCondition(@Param("status") int status, @Param("companyName") String companyName, @Param("brandName") String brandName);
  1. 将多个参数封装成一个 实体对象 ,将该实体对象作为接口的方法参数。该方式要求在映射配置文件的SQL中使用 #{内容} 时,里面的内容必须和实体类属性名保持一致。
1
List<Brand> selectByCondition(Brand brand);
  1. 将多个参数封装到map集合中,将map集合作为接口的方法参数。该方式要求在映射配置文件的SQL中使用 #{内容} 时,里面的内容必须和map集合中键的名称一致。map.put(“SQL参数占位符名称”,数值);
1
List<Brand> selectByCondition(Map map);

编写SQL语句

BrandMapper.xml 映射配置文件中编写 statement,注意使用 resultMap 替换 resultType

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

编写测试方法

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

  • Param注解
  • 封装成对象
  • 封装到map集合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void testSelectByCondition() throws IOException {
// 接收参数
int status = 1;
String companyName = "华为";
String brandName = "华为";
// 处理参数
companyName = "%" + companyName + "%";
brandName = "%" + brandName + "%";
//1. 加载MyBatis核心配置文件,获取SqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
List<Brand> brands = brandMapper.selectByCondition(status, companyName, brandName);
System.out.println(brands);
//5. 资源关闭
sqlSession.close();
}

现在的这种查询方式,只有当用户把三个参数都填上的时候才能查询出来,如果另外两个参数没有填,那么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
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="selectByCondition" resultMap="brandResultMapper">
select *
from tb_brand
where
<if test="status != null">
`status` = #{status}
</if>
<if test="companyName != null and companyName != ''">
and company_name like #{companyName}
</if>
<if test="brandName != null and brandName != ''">
and brand_name like #{brandName};
</if>
</select>

如上的这种SQL语句就会根据传递的参数值进行动态的拼接。如果此时status和companyName有值那么就会值拼接这两个条件。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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="selectByCondition" resultMap="brandResultMapper">
select *
from tb_brand
<where>
<if test="status != null">
`status` = #{status}
</if>
<if test="companyName != null and companyName != ''">
and company_name like #{companyName}
</if>
<if test="brandName != null and brandName != ''">
and brand_name like #{brandName};
</if>
</where>
</select>

单个条件(动态SQL)

单条件动态SQL.png
如上图所示,在查询时只能选择 品牌名称当前状态企业名称 这三个条件中的一个,但是用户到底选择哪儿一个,我们并不能确定。这种就属于单个条件的动态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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<select id="selectByConditionSingle" resultType="com.blog.pojo.Brand">
select *
from tb_brand
<where>
<choose>
<when test="status != null">
`status` = #{status}
</when>
<when test="companyName != null and companyName != ''">
companyName = #{companyName}
</when>
<when test="brandName != null and brandName != ''">
brandName = #{brandName}
</when>
</choose>
</where>
</select>

编写测试方法

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

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

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

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

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

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

其实我在测试的时候,一直出现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
2
3
4
<insert id="add" >
insert into tb_brand(brand_name, company_name, ordered, description, status)
VALUES (#{brandName},#{companyName},#{ordered},#{description},#{status})
</insert>

编写测试方法

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Test
public void testAdd() throws IOException {
//接收参数
String brandName = "波导";
String companyName = "波导手机";
Integer ordered = 100;
String description = "手机中的战斗机";
int status = 1;
//封装对象
Brand brand = new Brand();
brand.setBrandName(brandName);
brand.setCompanyName(companyName);
brand.setOrdered(ordered);
brand.setDescription(description);
brand.setStatus(status);
//1. 获取SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
brandMapper.add(brand);
//提交事务
sqlSession.commit();
//5. 释放资源
sqlSession.close();
}

在第2步获取SqlSession对象时,默认是不会自动提交事务的,我们可以在openSession方法中加上true,这样就能自动提交事务了,不用手动调用commit方法

1
//SqlSession sqlSession = sqlSessionFactory.openSession(true); //设置自动提交事务,这种情况不需要手动提交事务了

添加-主键返回

在接收参数的时候,我们没有接收id的参数,而是利用数据库主键自增长来自动赋值,但有时候我们又需要获取这个自增长的id。

解决方案

  • 在 insert 标签上添加如下属性:
    • useGeneratedKeys:是够获取自动增长的主键值。true表示获取
    • keyProperty :指定将获取到的主键值封装到哪儿个属性里
1
2
3
4
<insert id="add" useGeneratedKeys="true" keyProperty="id">
insert into tb_brand(brand_name, company_name, ordered, description, status)
VALUES (#{brandName},#{companyName},#{ordered},#{description},#{status})
</insert>

添加主键返回之后,我们再来测试一下

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
@Test
public void testAdd() throws IOException {
//接收参数
String brandName = "波导";
String companyName = "波导手机";
Integer ordered = 100;
String description = "手机中的战斗机";
int status = 1;
//封装对象
Brand brand = new Brand();
brand.setBrandName(brandName);
brand.setCompanyName(companyName);
brand.setOrdered(ordered);
brand.setDescription(description);
brand.setStatus(status);
//1. 获取SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
brandMapper.add(brand);
System.out.println(brand.getId()); //在这里输出一下id,看看有没有值输出,我这里是有的
//提交事务
sqlSession.commit();
//5. 释放资源
sqlSession.close();
}

修改

编写接口方法

BrandMapper 接口中定义修改方法。int获取修改的行数

1
int update(Brand brand);

编写SQL语句

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

1
2
3
4
5
6
7
8
9
<update id="update">
update tb_brand
set brand_name = #{brandName},
company_name = #{companyName},
ordered = #{ordered},
`description` = #{description},
`status` = #{status}
where id = #{id}
</update>

编写测试方法

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Test
public void testUpdate() throws IOException {
//接收参数
int id = 5;
String brandName = "波导";
String companyName = "波导手机";
Integer ordered = 200;
String description = "波导手机,手机中的战斗机";
int status = 5;
//封装对象
Brand brand = new Brand();
brand.setBrandName(brandName);
brand.setCompanyName(companyName);
brand.setOrdered(ordered);
brand.setDescription(description);
brand.setStatus(status);
brand.setId(id);
//1. 获取SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象,并设置自动提交事务
SqlSession sqlSession = sqlSessionFactory.openSession(true);
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
int updateCount = brandMapper.update(brand);
System.out.println(updateCount);
//5. 释放资源
sqlSession.close();
}

此种修改方式要改只能全部改,如果没有给某一个字段赋值,那么修改之后的值就是null,十分的不方便,所以我们要将其优化成动态的修改字段

修改动态字段

解决方案跟上面的类似,也是用if标签来判断用户的输入,然后用set标签来删除额外的逗号(上面是用where标签去除and或or),防止出现SQL语法错误

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

删除一行数据

我们在App网购的时候,购物车里都会有删除按钮,,当用户点击了该按钮,就会将改行数据删除掉。那我们就需要思考,这种删除是根据什么进行删除呢?是通过主键id删除,因为id是表中数据的唯一标识。
接下来就来实现该功能。

编写接口方法

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

1
void deleteById(int id);

编写SQL语句

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

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

编写测试方法

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testDeleteById() throws IOException {
//接收参数
int id = 6;
//1. 获取SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession(true);
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
brandMapper.deleteById(id);
//5. 释放资源
sqlSession.close();
}

批量删除

我们在删除购物车订单的时候,都会有个多选按钮,可以选中多条记录进行删除,下面我们来实现这个功能

编写接口方法

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

1
2
// 参数是一个数组,数组中存储的是多条数据的id
void deleteByIds(int[] ids);

编写SQL语句

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

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

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

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

假如数组中的id数据是{1,2,3},那么拼接后的sql语句就是:delete from tb_brand where id in (1,2,3);

编写测试方法

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testDeleteByIds() throws IOException {
//接收参数
int[] ids = {1,2,3};
//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接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
brandMapper.deleteByIds(ids);
//5. 释放资源
sqlSession.close();
}

MyBatis参数传递

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

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

多个参数

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

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

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

  • 以 arg 开头 :第一个参数就叫 arg0,第二个参数就叫 arg1,以此类推。如:
    • map.put(“arg0”,参数值1);
    • map.put(“arg1”,参数值2);
  • 以 param 开头 : 第一个参数就叫 param1,第二个参数就叫 param2,依次类推。如:
    • map.put(“param1”,参数值1);
    • map.put(“param2”,参数值2);

下面我们来验证一下

  • UserMapper 接口中定义如下方法
1
User select(String username,String password);
  • UserMapper.xml 映射配置文件中定义SQL
1
2
3
4
5
6
7
<select id="select" resultType="user">
select *
from tb_user
where
username=#{arg0} <!--username=#{param1}-->
and password=#{arg1} <!--and password=#{param2}-->
</select>

运行代码结果如下

1
2
[DEBU6][main] c.i.m.0.select- ==> Preparing: SELECT * FROM tb_user WHERE username = ? AND PASSWORD = ?
[DEBU6] [main] c.i.m.U.select- ==> Parameters: zhangsan(STRING),123(STRING)

在映射配合文件的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
2
@Select(value = "select * from tb_user where id = #{id}")
public User select(int id);

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

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

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

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

  • 代码实现:

    • 将之前案例中 UserMapper.xml 中的 根据id查询数据 的 statement 删掉
    • UserMapper 接口的 selectById 方法上添加注解
    1
    2
    @Select("select * from tb_user where id = #{id}")
    User selectById(int id);
    • 测试
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Test
    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 数据库

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

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

    这些数据有什么用处?

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

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

  • GET请求请求参数在请求行中,没有请求体,POST请求请求参数在请求体中

  • GET请求请求参数大小有限制,POST没有

2.3 响应数据格式

2.3.1 格式介绍

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

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

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

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

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

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

2.3.2 响应状态码

  • 状态码大类
状态码分类 说明
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程序。

具体的实现步骤为:

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

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

public class ServletDemo1 implements Servlet {

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

}

public ServletConfig getServletConfig() {
return null;
}

public String getServletInfo() {
return null;
}

public void destroy() {

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

4.3 执行流程

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

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

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

小结

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

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

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

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

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

4.4 生命周期

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

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

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

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

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

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

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

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

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

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

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

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

    public String getServletInfo() {
    return null;
    }


    }

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

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

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

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

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

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

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

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

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

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

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

4.6 体系结构

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

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

1627240593506

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

具体的编写格式如下:

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

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

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

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

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

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

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


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

public void init(ServletConfig config) throws ServletException {

}

public ServletConfig getServletConfig() {
return null;
}

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

public String getServletInfo() {
return null;
}

public void destroy() {

}
}

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

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

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

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

}

public ServletConfig getServletConfig() {
return null;
}

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

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

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

public String getServletInfo() {
return null;
}

public void destroy() {

}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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
    21
    package com.itheima.web;

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

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

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

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

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

  • urlPattern配置规则

    • 精确匹配
    1
    @WebServlet(urlPatterns = "/user/select")

    访问路径

    http://localhost:8080/web-demo/user/select

    • 目录匹配
    1
    @WebServlet(urlPatterns = "/user/*")

    访问路径

    http://localhost:8080/web-demo/user/任意

    • 扩展名匹配
    1
    @WebServlet(urlPatterns = "*.do")   //注意这里没加斜杠

    访问路径

    http://localhost:8080/web-demo/任意.do

    • 任意匹配
    1
    2
    3
    @WebServlet(urlPatterns = "/")  
    //或
    @WebServlet(urlPatterns = "/*")

    访问路径

    http://localhost:8080/demo-web/任意

注意:``//*的区别?

  1. 当我们的项目中的Servlet配置了 “/”,会覆盖掉tomcat中的DefaultServlet,当其他的url-pattern都匹配不上时都会走这个Servlet

  2. 当我们的项目中配置了"/*",意味着匹配任意访问路径

  3. DefaultServlet是用来处理静态资源,如果配置了"/"会把默认的覆盖掉,就会引发请求静态资源的时候没有走默认的而是走了自定义的Servlet类,最终导致静态资源不能被访问

小结

  1. urlPattern总共有四种配置方式,分别是精确匹配、目录匹配、扩展名匹配、任意匹配

  2. 五种配置的优先级为 精确匹配 > 目录匹配> 扩展名匹配 > /* > / ,无需记,以最终运行结果为准。

4.8 XML配置

前面对应Servlet的配置,我们都使用的是@WebServlet,这个是Servlet从3.0版本后开始支持注解配置,3.0版本前只支持XML配置文件的配置方法。

对于XML的配置步骤有两步:

  • 编写Servlet类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.itheima.web;

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

public class ServletDemo13 extends MyHttpServlet {

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

System.out.println("demo13 get...");
}
@Override
protected void doPost(ServletRequest req, ServletResponse res) {
}
}
  • 在web.xml中配置该Servlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">



<!--
Servlet 全类名
-->
<servlet>
<!-- servlet的名称,名字任意-->
<servlet-name>demo13</servlet-name>
<!--servlet的类全名-->
<servlet-class>com.itheima.web.ServletDemo13</servlet-class>
</servlet>

<!--
Servlet 访问路径
-->
<servlet-mapping>
<!-- servlet的名称,要和上面的名称一致-->
<servlet-name>demo13</servlet-name>
<!-- servlet的访问路径-->
<url-pattern>/demo13</url-pattern>
</servlet-mapping>
</web-app>

这种配置方式和注解比起来,确认麻烦很多,所以建议大家使用注解来开发。但是大家要认识上面这种配置方式,因为并不是所有的项目都是基于注解开发的。

Request&Response

1,Request和Response的概述

Request是请求对象,Response是响应对象。这两个对象在我们使用Servlet的时候有看到:

1
2
3
4
@Override
public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
System.out.println("hello servlet");
}

此时,我们就需要思考一个问题request和response这两个参数的作用是什么?

  • request:获取请求数据
    • 浏览器会发送HTTP请求到后台服务器[Tomcat]
    • HTTP的请求中会包含很多请求数据[请求行+请求头+请求体]
    • 后台服务器[Tomcat]会对HTTP请求中的数据进行解析并把解析结果存入到一个对象中
    • 所存入的对象即为request对象,所以我们可以从request对象中获取请求的相关参数
    • 获取到数据后就可以继续后续的业务,比如获取用户名和密码就可以实现登录操作的相关业务
  • response:设置响应数据
    • 业务处理完后,后台就需要给前端返回业务处理的结果即响应数据
    • 把响应数据封装到response对象中
    • 后台服务器[Tomcat]会解析response对象,按照[响应行+响应头+响应体]格式拼接结果
    • 浏览器最终解析结果,把内容展示在浏览器给用户浏览

对于上述所讲的内容,我们通过一个案例来初步体验下request和response对象的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/demo3")
public class ServletDemo3 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//使用request对象 获取请求数据
String name = request.getParameter("name");//url?name=zhangsan

//使用response对象 设置响应数据
response.setHeader("content-type","text/html;charset=utf-8");
response.getWriter().write("<h1>"+name+",欢迎您!</h1>");
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("Post...");
}
}

启动成功后就可以通过浏览器来访问,并且根据传入参数的不同就可以在页面上展示不同的内容:

小结

在这节中,我们主要认识了下request对象和reponse对象:

  • request对象是用来封装请求数据的对象
  • response对象是用来封装响应数据的对象

目前我们只知道这两个对象是用来干什么的,那么它们具体是如何实现的,就需要我们继续深入的学习。接下来,就先从Request对象来学习,主要学习下面这些内容:

  • request继承体系

  • request获取请求参数

  • request请求转发

2,Request对象

2.1 Request继承体系

在学习这节内容之前,我们先思考一个问题,前面在介绍Request和Reponse对象的时候,比较细心的同学可能已经发现:

  • 当我们的Servlet类实现的是Servlet接口的时候,service方法中的参数是ServletRequest和ServletResponse
  • 当我们的Servlet类继承的是HttpServlet类的时候,doGet和doPost方法中的参数就变成HttpServletRequest和HttpServletReponse

那么,

  • ServletRequest和HttpServletRequest的关系是什么?
  • request对象是有谁来创建的?
  • request提供了哪些API,这些API从哪里查?

首先,我们先来看下Request的继承体系:

1628740441008

ServletRequest和HttpServletRequest是继承关系,并且两个都是接口,接口是无法创建对象,这个时候就引发了下面这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
//接口无法创建对象,那么这个参数是哪儿来的呢
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("username");
resp.setHeader("content-type","text/html;charset=utf-8");
resp.getWriter().write("<h1>"+name+",欢迎访问<h1>");
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("post...");
}

这个时候,我们就需要用到Request继承体系中的RequestFacade:

  • 该类实现了HttpServletRequest接口,也间接实现了ServletRequest接口。
  • Servlet类中的service方法、doGet方法或者是doPost方法最终都是由Web服务器[Tomcat]来调用的,所以Tomcat提供了方法参数接口的具体实现类,并完成了对象的创建
  • 要想了解RequestFacade中都提供了哪些方法,我们可以直接查看JavaEE的API文档中关于ServletRequest和HttpServletRequest的接口文档,因为RequestFacade实现了其接口就需要重写接口中的方法

对于上述结论,要想验证,可以编写一个Servlet,在方法中把request对象打印下,就能看到最终的对象是不是RequestFacade,代码如下:

1
2
3
4
5
6
7
8
9
10
11
@WebServlet("/demo2")
public class ServletDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println(request);
}

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

启动服务器,运行访问http://localhost:8080/request-demo/demo2

小结

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* request 获取请求数据
*/
@WebServlet("/req1")
public class RequestDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// String getMethod():获取请求方式: GET
String method = req.getMethod();
System.out.println(method);//GET
// String getContextPath():获取虚拟目录(项目访问路径):/request-demo
String contextPath = req.getContextPath();
System.out.println(contextPath);
// StringBuffer getRequestURL(): 获取URL(统一资源定位符):http://localhost:8080/request-demo/req1
StringBuffer url = req.getRequestURL();
System.out.println(url.toString());
// String getRequestURI():获取URI(统一资源标识符): /request-demo/req1
String uri = req.getRequestURI();
System.out.println(uri);
// String getQueryString():获取请求参数(GET方式): username=zhangsan
String queryString = req.getQueryString();
System.out.println(queryString);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}

启动服务器,访问http://localhost:8080/request-demo/req1?username=zhangsan&passwrod=123

2.2.2 获取请求头数据

对于请求头的数据,格式为key: value

所以根据请求头名称获取对应值的方法为:

1
String getHeader(String name)

接下来,在代码中如果想要获取客户端浏览器的版本信息,则可以使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* request 获取请求数据
*/
@WebServlet("/req1")
public class RequestDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取请求头: user-agent: 浏览器的版本信息
String agent = req.getHeader("user-agent");
System.out.println(agent);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}

重新启动服务器后,http://localhost:8080/request-demo/req1?username=zhangsan&passwrod=123,获取的结果如下:

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
2
ServletInputStream getInputStream()
该方法可以获取字节
  • 获取字符输入流,如果前端发送的是纯文本数据,则使用该方法
1
BufferedReader getReader()

接下来,大家需要思考,要想获取到请求体的内容该如何实现?

具体实现的步骤如下:

1.准备一个页面,在页面中添加form表单,用来发送post请求

2.在Servlet的doPost方法中获取请求体数据

3.在doPost方法中使用request的getReader()或者getInputStream()来获取

4.访问测试

  1. 在项目的webapp目录下添加一个html页面,名称为:req.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--
action:form表单提交的请求地址
method:请求方式,指定为post
-->
<form action="/request-demo/req1" method="post">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit">
</form>
</body>
</html>
  1. 在Servlet的doPost方法中获取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* request 获取请求数据
*/
@WebServlet("/req1")
public class RequestDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//在此处获取请求体中的数据
}
}
  1. 调用getReader()或者getInputStream()方法,因为目前前端传递的是纯文本数据,所以我们采用getReader()方法来获取

注意

BufferedReader流是通过request对象来获取的,当请求完成后request对象就会被销毁,request对象被销毁后,BufferedReader流就会自动关闭,所以此处就不需要手动关闭流了。

  1. 启动服务器,通过浏览器访问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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@WebServlet("/req1")
public class RequestDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取请求方式
String method = req.getMethod();
//获取请求参数
String params = "";
if("GET".equals(method)){
params = req.getQueryString();
}else if("POST".equals(method)){
BufferedReader reader = req.getReader();
params = reader.readLine();
}
//将请求参数进行打印控制台
System.out.println(params);

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

使用request的getMethod()来获取请求方式,根据请求方式的不同分别获取请求参数值,这样就可以解决上述问题,但是以后每个Servlet都需要这样写代码,实现起来比较麻烦,这种方案我们不采用

解决方案二:

request对象已经将上述获取请求参数的方法进行了封装,并且request提供的方法实现的功能更强大,以后只需要调用request提供的方法即可,在request的方法中都实现了哪些操作?

  1. 根据不同的请求方式获取请求参数,例如:username=zhangsan&password=asd123&hobby=1&hobby=2
  2. 把获取到的内容进行分割,username=zhangsan&password=asd123&hobby=1 -> username=zhangsan password=asd123 hobby=1 hobby=2 -> username zhangsan password asd123 hobby 1 hobby 2
  3. 把分割后端数据,存入到一个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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/request-demo/req2" method="get">
<input type="text" name="username"><br>
<input type="password" name="password"><br>
<input type="checkbox" name="hobby" value="1"> 游泳
<input type="checkbox" name="hobby" value="2"> 爬山 <br>
<input type="submit">

</form>
</body>
</html>

2.在Servlet代码中获取页面传递GET请求的参数值

2.1获取GET方式的所有请求参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* request 通用方式获取请求参数
*/
@WebServlet("/req2")
public class RequestDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//GET请求逻辑
System.out.println("get....");
//1. 获取所有参数的Map集合
Map<String, String[]> map = req.getParameterMap();
for (String key : map.keySet()) {
// username:zhangsan lisi
System.out.print(key+":");

//获取值
String[] values = map.get(key);
for (String value : values) {
System.out.print(value + " ");
}

System.out.println();
}
}

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

2.2获取GET请求参数中的爱好,结果是数组值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* request 通用方式获取请求参数
*/
@WebServlet("/req2")
public class RequestDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//GET请求逻辑
//...
System.out.println("------------");
String[] hobbies = req.getParameterValues("hobby");
for (String hobby : hobbies) {
System.out.println(hobby);
}
}

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

2.3获取GET请求参数中的用户名和密码,结果是单个值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* request 通用方式获取请求参数
*/
@WebServlet("/req2")
public class RequestDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//GET请求逻辑
//...
String username = req.getParameter("username");
String password = req.getParameter("password");
System.out.println(username);
System.out.println(password);
}

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

3.在Servlet代码中获取页面传递POST请求的参数值

3.1将req.html页面form表单的提交方式改成post

3.2将doGet方法中的内容复制到doPost方法中即可

小结

  • req.getParameter()方法使用的频率会比较高

  • 以后我们再写代码的时候,就只需要按照如下格式来编写:

1
2
3
4
5
6
7
8
9
10
11
public class RequestDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//采用request提供的获取请求参数的通用方式来获取请求参数
//编写其他的业务代码...
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req,resp);
}
}

2.3 Request请求转发

  1. 请求转发(forward):一种在服务器内部的资源跳转方式。

(1)浏览器发送请求给服务器,服务器中对应的资源A接收到请求

(2)资源A处理完请求后将请求发给资源B

(3)资源B处理完后将结果响应给浏览器

(4)请求从资源A到资源B的过程就叫请求转发

  1. 请求转发的实现方式:
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
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 请求转发
*/
@WebServlet("/req5")
public class RequestDemo5 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("demo5...");
}

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

(2)创建RequestDemo6类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 请求转发
*/
@WebServlet("/req6")
public class RequestDemo6 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("demo6...");
}

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

(3)在RequestDemo5的doGet方法中进行请求转发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 请求转发
*/
@WebServlet("/req5")
public class RequestDemo5 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("demo5...");
//请求转发
request.getRequestDispatcher("/req6").forward(request,response);
}

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

(4)启动测试

访问http://localhost:8080/request-demo/req5,就可以在控制台看到如下内容:

1
2
demo5...
demo6...

说明请求已经转发到了/req6

  1. 请求转发资源间共享数据:使用Request对象

此处主要解决的问题是把请求从/req5转发到/req6的时候,如何传递数据给/req6

需要使用request对象提供的三个方法:

  • 存储数据到request域[范围,数据是存储在request对象]中
1
void setAttribute(String name,Object o);
  • 根据key获取值
1
Object getAttribute(String name);
  • 根据key删除该键值对
1
void removeAttribute(String name);

接着上个需求来:

1.在RequestDemo5的doGet方法中转发请求之前,将数据存入request域对象中

2.在RequestDemo6的doGet方法从request域对象中获取数据,并将数据打印到控制台

3.启动访问测试

(1)修改RequestDemo5中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/req5")
public class RequestDemo5 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("demo5...");
//存储数据
request.setAttribute("msg","hello");
//请求转发
request.getRequestDispatcher("/req6").forward(request,response);

}

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

(2)修改RequestDemo6中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 请求转发
*/
@WebServlet("/req6")
public class RequestDemo6 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("demo6...");
//获取数据
Object msg = request.getAttribute("msg");
System.out.println(msg);

}

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

(3)启动测试

访问http://localhost:8080/request-demo/req5,就可以在控制台看到如下内容:

1
2
3
demo5...
dem06...
hello

此时就可以实现在转发多个资源之间共享数据。

  1. 请求转发的特点
  • 浏览器地址栏路径不发生变化

    虽然后台从/req5转发到/req6,但是浏览器的地址一直是/req5,未发生变化

  • 只能转发到当前服务器的内部资源

    不能从一个服务器通过转发访问另一台服务器

  • 一次请求,可以在转发资源间使用request共享数据

    虽然后台从/req5转发到/req6,但是这个只有一次请求

3,Response对象

  • Request:使用request对象来获取请求数据
  • Response:使用response对象来设置响应数据

Reponse的继承体系和Request的继承体系也非常相似:

1628857761317

3.1 Response设置响应数据功能介绍

HTTP响应数据总共分为三部分内容,分别是响应行、响应头、响应体,对于这三部分内容的数据,respone对象都提供了哪些方法来进行设置?

  1. 响应行

    响应行包含三块内容,分别是 HTTP/1.1[HTTP协议及版本] 200[响应状态码] ok[状态码的描述]
    对于响应头,比较常用的就是设置响应状态码:

1
void setStatus(int sc);
  1. 响应头

​ 响应头的格式为key:value形式

​ 设置响应头键值对:

1
void setHeader(String name,String value);
  1. 响应体

​ 对于响应体,是通过字符、字节输出流的方式往浏览器写,

​ 获取字符输出流:

1
PrintWriter getWriter();

​ 获取字节输出流

1
ServletOutputStream getOutputStream();

3.2 Respones请求重定向

  1. Response重定向(redirect):一种资源跳转方式。

(1)浏览器发送请求给服务器,服务器中对应的资源A接收到请求

(2)资源A现在无法处理该请求,就会给浏览器响应一个302的状态码+location的一个访问资源B的路径

(3)浏览器接收到响应状态码为302就会重新发送请求到location对应的访问地址去访问资源B

(4)资源B接收到请求后进行处理并最终给浏览器响应结果,这整个过程就叫重定向

  1. 重定向的实现方式:
1
2
resp.setStatus(302);
resp.setHeader("location","资源B的访问路径");

具体的实现步骤为:

1.创建一个ResponseDemo1类,接收/resp1的请求,在doGet方法中打印resp1....

2.创建一个ResponseDemo2类,接收/resp2的请求,在doGet方法中打印resp2....

3.在ResponseDemo1的方法中使用

​ response.setStatus(302);

​ response.setHeader(“Location”,“/request-demo/resp2”) 来给前端响应结果数据

4.启动测试

(1)创建ResponseDemo1类

1
2
3
4
5
6
7
8
9
10
11
12
@WebServlet("/resp1")
public class ResponseDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("resp1....");
}

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

(2)创建ResponseDemo2类

1
2
3
4
5
6
7
8
9
10
11
12
@WebServlet("/resp2")
public class ResponseDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("resp2....");
}

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

(3)在ResponseDemo1的doGet方法中给前端响应数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/resp1")
public class ResponseDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("resp1....");
//重定向
//1.设置响应状态码 302
response.setStatus(302);
//2. 设置响应头 Location
response.setHeader("Location","/request-demo/resp2");
}

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

(4)启动测试

访问http://localhost:8080/request-demo/resp1,就可以在控制台看到如下内容:

1
2
resp1...
resp2...

说明/resp1/resp2都被访问到了。到这重定向就已经完成了。

虽然功能已经实现,但是从设置重定向的两行代码来看,会发现除了重定向的地址不一样,其他的内容都是一模一样,所以request对象给我们提供了简化的编写方式为:

1
resposne.sendRedirect("/request-demo/resp2")

所以第3步中的代码就可以简化为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@WebServlet("/resp1")
public class ResponseDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("resp1....");
//重定向
resposne.sendRedirect("/request-demo/resp2");
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
  1. 重定向的特点
  • 浏览器地址栏路径发送变化

    当进行重定向访问的时候,由于是由浏览器发送的两次请求,所以地址会发生变化

  • 可以重定向到任何位置的资源(服务内容、外部均可)

    因为第一次响应结果中包含了浏览器下次要跳转的路径,所以这个路径是可以任意位置资源。

  • 两次请求,不能在多个资源使用request共享数据

    因为浏览器发送了两次请求,是两个不同的request对象,就无法通过request对象进行共享数据

介绍完请求重定向请求转发以后,接下来需要把这两个放在一块对比下:

重定向特点 请求转发特点
浏览器地址栏路径发生变化 浏览器地址栏路径不发生变化
可以重定向到任意位置的资源(服务器内部、外部均可) 只能转发到当前服务器的内部资源
两次请求,不能在多个资源使用request共享数据 一次请求,可以在转发的资源间使用request共享数据

3.3 路径问题

  1. 问题1:转发的时候路径上没有加/request-demo而重定向加了,那么到底什么时候需要加,什么时候不需要加呢?

其实判断的依据很简单,只需要记住下面的规则即可:

  • 浏览器使用:需要加虚拟目录(项目访问路径)
  • 服务端使用:不需要加虚拟目录

对于转发来说,因为是在服务端进行的,所以不需要加虚拟目录

对于重定向来说,路径最终是由浏览器来发送请求,就需要添加虚拟目录。

掌握了这个规则,接下来就通过一些练习来强化下知识的学习:

1
2
3
4
5
6
7
8
9
10
Q:
* `<a href='路径'>`
* `<form action='路径'>`
* req.getRequestDispatcher("路径")
* resp.sendRedirect("路径")
A:
1.超链接,从浏览器发送,需要加
2.表单,从浏览器发送,需要加
3.转发,是从服务器内部跳转,不需要加
4.重定向,是由浏览器进行跳转,需要加。
  1. 问题2:在重定向的代码中,/request-demo是固定编码的,如果后期通过Tomcat插件配置了项目的访问路径,那么所有需要重定向的地方都需要重新修改,该如何优化?

我们可以在代码中动态去获取项目访问的虚拟目录,具体如何获取,我们可以借助前面咱们所学习的request对象中的getContextPath()方法,修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/resp1")
public class ResponseDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("resp1....");

//简化方式完成重定向
//动态获取虚拟目录
String contextPath = request.getContextPath();
response.sendRedirect(contextPath+"/resp2");
}

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

重新启动访问测试,功能依然能够实现,此时就可以动态获取项目访问的虚拟路径,从而降低代码的耦合度。

3.4 Response响应字符数据

要想将字符数据写回到浏览器,我们需要两个步骤:

  • 通过Response对象获取字符输出流: PrintWriter writer = resp.getWriter();

  • 通过字符输出流写数据: writer.write(“aaa”);

接下来,我们实现通过些案例把响应字符数据给实际应用下:

  1. 返回一个简单的字符串aaa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 响应字符数据:设置字符数据的响应体
*/
@WebServlet("/resp3")
public class ResponseDemo3 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");
//1. 获取字符输出流
PrintWriter writer = response.getWriter();
writer.write("aaa");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
  1. 返回一串html字符串,并且能被浏览器解析
1
2
3
4
PrintWriter writer = response.getWriter();
//content-type,告诉浏览器返回的数据类型是HTML类型数据,这样浏览器才会解析HTML标签
response.setHeader("content-type","text/html");
writer.write("<h1>aaa</h1>");

注意:一次请求响应结束后,response对象就会被销毁掉,所以不要手动关闭流。

  1. 返回一个中文的字符串你好,需要注意设置响应数据的编码为utf-8
1
2
3
//设置响应的数据格式及数据的编码
response.setContentType("text/html;charset=utf-8");
writer.write("你好");

3.3 Response响应字节数据

要想将字节数据写回到浏览器,我们需要两个步骤:

  • 通过Response对象获取字节输出流:ServletOutputStream outputStream = resp.getOutputStream();

  • 通过字节输出流写数据: outputStream.write(字节数据);

接下来,我们实现通过些案例把响应字符数据给实际应用下:

  1. 返回一个图片文件到浏览器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 响应字节数据:设置字节数据的响应体
*/
@WebServlet("/resp4")
public class ResponseDemo4 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 读取文件
FileInputStream fis = new FileInputStream("d://a.jpg");
//2. 获取response字节输出流
ServletOutputStream os = response.getOutputStream();
//3. 完成流的copy
byte[] buff = new byte[1024];
int len = 0;
while ((len = fis.read(buff))!= -1){
os.write(buff,0,len);
}
fis.close();
}

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

上述代码中,对于流的copy的代码还是比较复杂的,所以我们可以使用别人提供好的方法来简化代码的开发,具体的步骤是:

(1)pom.xml添加依赖

1
2
3
4
5
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>

(2)调用工具类方法

1
2
3
//fis:输入流
//os:输出流
IOUtils.copy(fis,os);

优化后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 响应字节数据:设置字节数据的响应体
*/
@WebServlet("/resp4")
public class ResponseDemo4 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 读取文件
FileInputStream fis = new FileInputStream("d://a.jpg");
//2. 获取response字节输出流
ServletOutputStream os = response.getOutputStream();
//3. 完成流的copy
IOUtils.copy(fis,os);
fis.close();
}

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

4,用户注册登录案例

4.1 用户登录

4.1.1 需求分析

  1. 用户在登录页面输入用户名和密码,提交请求给LoginServlet
  2. 在LoginServlet中接收请求和数据[用户名和密码]
  3. 在LoginServlt中通过Mybatis实现调用UserMapper来根据用户名和密码查询数据库表
  4. 将查询的结果封装到User对象中进行返回
  5. 在LoginServlet中判断返回的User对象是否为null
  6. 如果为nul,说明根据用户名和密码没有查询到用户,则登录失败,返回"登录失败"数据给前端
  7. 如果不为null,则说明用户存在并且密码正确,则登录成功,返回"登录成功"数据给前端

4.1.2 环境准备

  1. 写一个简单的表单提交
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--注意将请求提交给loginServlet-->
<form action="/web_demo_war_exploded/loginServlet" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
</body>
</html>
  1. 创建db1数据库,创建tb_user表,创建User实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 创建数据库
CREATE DATABASE db1;

USE db1;

-- 创建用户表
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;
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
package com.blog.pojo;

public class User {
private Integer id;
private String username;
private String password;

public User() {
}

public User(Integer id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
  1. 在项目的pom.xml导入Mybatis和Mysql驱动坐标
1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.5</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
</dependency>
  1. 创建mybatis-config.xml核心配置文件,UserMapper.xml映射文件,UserMapper接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--起别名-->
<typeAliases>
<package name="com.itheima.pojo"/>
</typeAliases>

<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<!--
useSSL:关闭SSL安全连接 性能更高
useServerPrepStmts:开启预编译功能
&amp; 等同于 & ,xml配置文件中不能直接写 &符号
-->
<property name="url" value="jdbc:mysql:///db1?useSSL=false&amp;useServerPrepStmts=true"/>
<property name="username" value="root"/>
<property name="password" value="1234"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--扫描mapper-->
<package name="com.itheima.mapper"/>
</mappers>
</configuration>

4.2 在com.itheima.mapper包下创建UserMapper接口

1
2
3
public interface UserMapper {

}

4.3 创建UserMapper.xml文件

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

</mapper>

至此我们所需要的环境就都已经准备好了,具体该如何实现?

4.1.3 代码实现

  1. 在UserMapper接口中提供一个根据用户名和密码查询用户对象的方法
1
2
3
4
5
6
7
8
/**
* 根据用户名和密码查询用户对象
* @param username
* @param password
* @return
*/
@Select("select * from tb_user where username = #{username} and password = #{password}")
User select(@Param("username") String username,@Param("password") String password);

说明

@Param注解的作用:用于传递参数,是方法的参数可以与SQL中的字段名相对应。

  1. 修改loign.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>login</title>
<link href="css/login.css" rel="stylesheet">
</head>

<body>
<div id="loginDiv">
<form action="/request-demo/loginServlet" method="post" id="form">
<h1 id="loginMsg">LOGIN IN</h1>
<p>Username:<input id="username" name="username" type="text"></p>

<p>Password:<input id="password" name="password" type="password"></p>

<div id="subDiv">
<input type="submit" class="button" value="login up">
<input type="reset" class="button" value="reset">&nbsp;&nbsp;&nbsp;
<a href="register.html">没有账号?点击注册</a>
</div>
</form>
</div>

</body>
</html>
  1. 编写LoginServlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@WebServlet("/loginServlet")
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 接收用户名和密码
String username = request.getParameter("username");
String password = request.getParameter("password");

//2. 调用MyBatis完成查询
//2.1 获取SqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2.2 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//2.3 获取Mapper
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//2.4 调用方法
User user = userMapper.select(username, password);
//2.5 释放资源
sqlSession.close();


//获取字符输出流,并设置content type
response.setContentType("text/html;charset=utf-8");
PrintWriter writer = response.getWriter();
//3. 判断user释放为null
if(user != null){
// 登陆成功
writer.write("登陆成功");
}else {
// 登陆失败
writer.write("登陆失败");
}
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
  1. 启动服务器测试

​ 如果用户名和密码输入错误,则登陆失败

如果用户名和密码输入正确,则登陆成功

至此用户的登录功能就已经完成了~

4.2 用户注册

4.2.1 需求分析

  1. 用户在注册页面输入用户名和密码,提交请求给RegisterServlet
  2. 在RegisterServlet中接收请求和数据[用户名和密码]
  3. 在RegisterServlet中通过Mybatis实现调用UserMapper来根据用户名查询数据库表
  4. 将查询的结果封装到User对象中进行返回
  5. 在RegisterServlet中判断返回的User对象是否为null
  6. 如果为nul,说明根据用户名可用,则调用UserMapper来实现添加用户
  7. 如果不为null,则说明用户不可以,返回"用户名已存在"数据给前端

4.2.2 代码编写

  1. 编写UserMapper提供根据用户名查询用户数据方法和添加用户方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 根据用户名查询用户对象
* @param username
* @return
*/
@Select("select * from tb_user where username = #{username}")
User selectByUsername(String username);

/**
* 添加用户
* @param user
*/
@Insert("insert into tb_user values(null,#{username},#{password})")
void add(User user);
  1. 修改register.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>欢迎注册</title>
<link href="css/register.css" rel="stylesheet">
</head>
<body>

<div class="form-div">
<div class="reg-content">
<h1>欢迎注册</h1>
<span>已有帐号?</span> <a href="login.html">登录</a>
</div>
<form id="reg-form" action="/request-demo/registerServlet" method="post">

<table>

<tr>
<td>用户名</td>
<td class="inputs">
<input name="username" type="text" id="username">
<br>
<span id="username_err" class="err_msg" style="display: none">用户名不太受欢迎</span>
</td>

</tr>

<tr>
<td>密码</td>
<td class="inputs">
<input name="password" type="password" id="password">
<br>
<span id="password_err" class="err_msg" style="display: none">密码格式有误</span>
</td>
</tr>

</table>

<div class="buttons">
<input value="注 册" type="submit" id="reg_btn">
</div>
<br class="clear">
</form>

</div>
</body>
</html>
  1. 创建RegisterServlet类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@WebServlet("/registerServlet")
public class RegisterServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 接收用户数据
String username = request.getParameter("username");
String password = request.getParameter("password");

//封装用户对象
User user = new User();
user.setUsername(username);
user.setPassword(password);

//2. 调用mapper 根据用户名查询用户对象
//2.1 获取SqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2.2 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//2.3 获取Mapper
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

//2.4 调用方法
User u = userMapper.selectByUsername(username);

//3. 判断用户对象释放为null
if( u ` null){
// 用户名不存在,添加用户
userMapper.add(user);

// 提交事务
sqlSession.commit();
// 释放资源
sqlSession.close();
}else {
// 用户名存在,给出提示信息
response.setContentType("text/html;charset=utf-8");
response.getWriter().write("用户名已存在");
}

}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
  1. 启动服务器进行测试

​ 如果测试成功,则在数据库中就能查看到新注册的数据

​ 如果用户已经存在,则在页面上展示 用户名已存在 的提示信息

4.3 SqlSessionFactory工具类抽取

上面两个功能已经实现,但是在写Servlet的时候,因为需要使用Mybatis来完成数据库的操作,所以对于Mybatis的基础操作就出现了些重复代码,如下

1
2
3
4
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new
SqlSessionFactoryBuilder().build(inputStream);

有了这些重复代码就会造成一些问题:

  • 重复代码不利于后期的维护
  • SqlSessionFactory工厂类进行重复创建
    • 就相当于每次买手机都需要重新创建一个手机生产工厂来给你制造一个手机一样,资源消耗非常大但性能却非常低。所以这么做是不允许的。

那如何来优化呢?

  • 代码重复可以抽取工具类
  • 对指定代码只需要执行一次可以使用静态代码块

有了这两个方向后,代码具体该如何编写?

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

private static SqlSessionFactory sqlSessionFactory;

static {
//静态代码块会随着类的加载而自动执行,且只执行一次
try {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}


public static SqlSessionFactory getSqlSessionFactory(){
return sqlSessionFactory;
}
}

工具类抽取以后,以后在对Mybatis的SqlSession进行操作的时候,就可以直接使用

1
SqlSessionFactory sqlSessionFactory =SqlSessionFactoryUtils.getSqlSessionFactory();

这样就可以很好的解决上面所说的代码重复和重复创建工厂导致性能低的问题了。

会话技术(Cookie&Session)

1,会话跟踪技术的概述

对于会话跟踪这四个词,我们需要拆开来进行解释,首先要理解什么是会话,然后再去理解什么是会话跟踪:

  • 会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。

    • 从浏览器发出请求到服务端响应数据给前端之后,一次会话(在浏览器和服务器之间)就被建立了
    • 会话被建立后,如果浏览器或服务端都没有被关闭,则会话就会持续建立着
    • 浏览器和服务器就可以继续使用该会话进行请求发送和响应,上述的整个过程就被称之为会话
  • 会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据

    • 服务器会收到多个请求,这多个请求可能来自多个浏览器
    • 服务器需要用来识别请求是否来自同一个浏览器
    • 服务器用来识别浏览器的过程,这个过程就是会话跟踪
    • 服务器识别浏览器后就可以在同一个会话中多次请求之间来共享数据

    那么我们又有一个问题需要思考,一个会话中的多次请求为什么要共享数据呢?有了这个数据共享功能后能实现哪些功能呢?

    • 拿购物车举例: 加入购物车去购物车结算是两次请求,但是后面这次请求要想展示前一次请求所添加的商品,就需要用到数据共享。

    • 页面展示用户登录信息:很多网站,登录后访问多个功能发送多次请求后,浏览器上都会有当前登录用户的信息[用户名],比如百度、京东、码云等。

    • 网站登录页面的记住我功能:当用户登录成功后,勾选记住我按钮后下次再登录的时候,网站就会自动填充用户名和密码,简化用户的登录操作,多次登录就会有多次请求,他们之间也涉及到共享数据

    • 登录页面的验证码功能:生成验证码和输入验证码点击注册这也是两次请求,这两次请求的数据之间要进行对比,相同则允许注册,不同则拒绝注册,该功能的实现也需要在同一次会话中共享数据。

思考:为什么现在浏览器和服务器不支持数据共享呢?

  • 浏览器和服务器之间使用的是HTTP请求来进行数据传输
  • HTTP协议是无状态的,每次浏览器向服务器请求时,服务器都会将该请求视为新的请求
  • HTTP协议设计成无状态的目的是让每次请求之间相互独立,互不影响
  • 请求与请求之间独立后,就无法实现多次请求之间的数据共享

如何实现会话跟踪技术呢? 具体的实现方式有:

(1)客户端会话跟踪技术:Cookie

(2)服务端会话跟踪技术:Session

它们之间最大的区别:Cookie是存储在浏览器端而Session是存储在服务器端

2,Cookie

2.1 Cookie的基本使用

1.概念

Cookie:客户端会话技术,将数据保存到客户端,以后每次请求都携带Cookie数据进行访问。

2.Cookie的工作流程

1629386230207

  • 服务端提供了两个Servlet,分别是ServletA和ServletB
  • 浏览器发送HTTP请求1给服务端,服务端ServletA接收请求并进行业务处理
  • 服务端ServletA在处理的过程中可以创建一个Cookie对象并将name=zs的数据存入Cookie
  • 服务端ServletA在响应数据的时候,会把Cookie对象响应给浏览器
  • 浏览器接收到响应数据,会把Cookie对象中的数据存储在浏览器内存中,此时浏览器和服务端就建立了一次会话
  • 在同一次会话中浏览器再次发送HTTP请求2给服务端ServletB,浏览器会携带Cookie对象中的所有数据
  • ServletB接收到请求和数据后,就可以获取到存储在Cookie对象中的数据,这样同一个会话中的多次请求之间就实现了数据共享

3.Cookie的基本使用

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
<!--servlet-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!--jsp-->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<!--jstl-->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
</plugin>
</plugins>
</build>

(2)编写Servlet类,名称为AServlet

1
2
3
4
5
6
7
8
9
10
11
12
@WebServlet("/aServlet")
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

}

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

(3)在Servlet中创建Cookie对象,存入数据,发送给前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@WebServlet("/aServlet")
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//发送Cookie
//1. 创建Cookie对象
Cookie cookie = new Cookie("username","zs");
//2. 发送Cookie,response
response.addCookie(cookie);
}

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

(4)启动测试,在浏览器查看Cookie对象中的值

访问http://localhost:8080/cookie-demo/aServlet

名称 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
2
cookie.getName();
cookie.getValue();

介绍完获取Cookie对应的步骤后,接下面再通过一个案例来完成Cookie的获取,具体实现步骤为:

需求:在Servlet中获取前一个案例存入在Cookie对象中的数据

1.编写一个新Servlet类,名称为BServlet

2.在BServlet中使用request对象获取Cookie数组,遍历数组,从数据中获取指定名称对应的值

3.启动测试,在控制台打印出获取的值

(1)编写一个新Servlet类,名称为BServlet

1
2
3
4
5
6
7
8
9
10
11
12
@WebServlet("/bServlet")
public class BServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

}

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

(2)在BServlet中使用request对象获取Cookie数组,遍历数组,从数据中获取指定名称对应的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@WebServlet("/bServlet")
public class BServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取Cookie
//1. 获取Cookie数组
Cookie[] cookies = request.getCookies();
//2. 遍历数组
for (Cookie cookie : cookies) {
//3. 获取数据
String name = cookie.getName();
if("username".equals(name)){
String value = cookie.getValue();
System.out.println(name+":"+value);
break;
}
}

}

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

(3)启动测试,在控制台打印出获取的值

访问http://localhost:8080/cookie-demo/bServlet

在IDEA控制台就能看到输出的结果:username:zs

2.2 Cookie的原理分析

对于Cookie的实现原理是基于HTTP协议的,其中设计到HTTP协议中的两个请求头信息:

  • 响应头:set-cookie
  • 请求头: cookie

1629393289338

  • 前面的案例中已经能够实现,AServlet给前端发送Cookie,BServlet从request中获取Cookie的功能
  • 对于AServlet响应数据的时候,Tomcat服务器都是基于HTTP协议来响应数据
  • 当Tomcat发现后端要返回的是一个Cookie对象之后,Tomcat就会在响应头中添加一行数据Set-Cookie:username=zs
  • 浏览器获取到响应结果后,从响应头中就可以获取到Set-Cookie对应值username=zs,并将数据存储在浏览器的内存中
  • 浏览器再次发送请求给BServlet的时候,浏览器会自动在请求头中添加Cookie: username=zs发送给服务端BServlet
  • Request对象会把请求头中cookie对应的值封装成一个个Cookie对象,最终形成一个数组
  • BServlet通过Request对象获取到Cookie[]后,就可以从中获取自己需要的数据

2.3 Cookie的使用细节

2.3.1 Cookie的存活时间

前面让大家思考过一个问题:

1629423321737

(1)浏览器发送请求给AServlet,AServlet会响应一个存有usernanme=zs的Cookie对象给浏览器

(2)浏览器接收到响应数据将cookie存入到浏览器内存中

(3)当浏览器再次发送请求给BServlet,BServlet就可以使用Request对象获取到Cookie数据

(4)在发送请求到BServlet之前,如果把浏览器关闭再打开进行访问,BServlet能否获取到Cookie数据?

注意:浏览器关闭再打开不是指打开一个新的选显卡,而且必须是先关闭再打开,顺序不能变。

针对上面这个问题,通过演示,会发现,BServlet中无法再获取到Cookie数据,这是为什么呢?

  • 默认情况下,Cookie存储在浏览器内存中,当浏览器关闭,内存释放,则Cookie被销毁

这个结论就印证了上面的演示效果,但是如果使用这种默认情况下的Cookie,有些需求就无法实现,比如:

1629423629887

上面这个网站的登录页面上有一个记住我的功能,这个功能大家都比较熟悉

  • 第一次输入用户名和密码并勾选记住我然后进行登录
  • 下次再登陆的时候,用户名和密码就会被自动填充,不需要再重新输入登录
  • 比如记住我这个功能需要记住用户名和密码一个星期,那么使用默认情况下的Cookie就会出现问题
  • 因为默认情况,浏览器一关,Cookie就会从浏览器内存中删除,对于记住我功能就无法实现

所以我们现在就遇到一个难题是如何将Cookie持久化存储?

Cookie其实已经为我们提供好了对应的API来完成这件事,这个API就是setMaxAge,

  • 设置Cookie存活时间
1
setMaxAge(int seconds)

参数值为:

  1. 正数:将Cookie写入浏览器所在电脑的硬盘,持久化存储。到时间自动删除

  2. 负数:默认值,Cookie在当前浏览器内存中,当浏览器关闭,则Cookie被销毁

  3. 零:删除对应Cookie

接下来,咱们就在AServlet中去设置Cookie的存活时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@WebServlet("/aServlet")
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//发送Cookie
//1. 创建Cookie对象
Cookie cookie = new Cookie("username","zs");
//设置存活时间 ,1周 7天
cookie.setMaxAge(60*60*24*7); //易阅读,需程序计算
//cookie.setMaxAge(604800); //不易阅读(可以使用注解弥补),程序少进行一次计算
//2. 发送Cookie,response
response.addCookie(cookie);
}

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

修改完代码后,启动测试,访问http://localhost:8080/cookie-demo/aServlet

  • 访问一个AServlet后,把浏览器关闭重启后,再去访问http://localhost:8080/cookie-demo/bServet,能在控制台打印出username:zs,说明Cookie没有随着浏览器关闭而被销毁
  • 通过浏览器查看Cookie的内容,会发现Cookie的相关信息

名称 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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@WebServlet("/aServlet")
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//发送Cookie
String value = "张三";
//对中文进行URL编码
value = URLEncoder.encode(value, "UTF-8");
System.out.println("存储数据:"+value);
//将编码后的值存入Cookie中
Cookie cookie = new Cookie("username",value);
//设置存活时间 ,1周 7天
cookie.setMaxAge(60*60*24*7);
//2. 发送Cookie,response
response.addCookie(cookie);
}

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

(2)在BServlet中获取值,并对值进行解码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@WebServlet("/bServlet")
public class BServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取Cookie
//1. 获取Cookie数组
Cookie[] cookies = request.getCookies();
//2. 遍历数组
for (Cookie cookie : cookies) {
//3. 获取数据
String name = cookie.getName();
if("username".equals(name)){
String value = cookie.getValue();//获取的是URL编码后的值 %E5%BC%A0%E4%B8%89
//URL解码
value = URLDecoder.decode(value,"UTF-8");
System.out.println(name+":"+value);//value解码后为 张三
break;
}
}

}

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

3,Session

3.1 Session的基本使用

1.概念

Session:服务端会话跟踪技术:将数据保存到服务端。

  • Session是存储在服务端而Cookie是存储在客户端
  • 存储在客户端的数据容易被窃取和截获,存在很多不安全的因素
  • 存储在服务端的数据相比于客户端来说就更安全

2.Session的工作流程

1629427173389

  • 在服务端的AServlet获取一个Session对象,把数据存入其中
  • 在服务端的BServlet获取到相同的Session对象,从中取出数据
  • 就可以实现一次会话中多次请求之间的数据共享了
  • 现在最大的问题是如何保证AServlet和BServlet使用的是同一个Session对象(在原理分析会讲解)

3.Session的基本使用

在JavaEE中提供了HttpSession接口,来实现一次会话的多次请求之间数据共享功能。

具体的使用步骤为:

  • 获取Session对象,使用的是request对象
1
HttpSession session = request.getSession();
  • Session对象提供的功能:

    • 存储数据到 session 域中

      1
      void setAttribute(String name, Object o)
    • 根据 key,获取值

      1
      Object getAttribute(String name)
    • 根据 key,删除该键值对

      1
      void removeAttribute(String name)

介绍完Session相关的API后,接下来通过一个案例来完成对Session的使用,具体实现步骤为:

需求:在一个Servlet中往Session中存入数据,在另一个Servlet中获取Session中存入的数据

1.创建名为SessionDemo1的Servlet类

2.创建名为SessionDemo2的Servlet类

3.在SessionDemo1的方法中:获取Session对象、存储数据

4.在SessionDemo2的方法中:获取Session对象、获取数据

5.启动测试

(1)创建名为SessionDemo1的Servlet类

1
2
3
4
5
6
7
8
9
10
11
12
@WebServlet("/demo1")
public class SessionDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

}

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

(2)创建名为SessionDemo2的Servlet类

1
2
3
4
5
6
7
8
9
10
11
12
@WebServlet("/demo2")
public class SessionDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

}

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

(3)SessionDemo1:获取Session对象、存储数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@WebServlet("/demo1")
public class SessionDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//存储到Session中
//1. 获取Session对象
HttpSession session = request.getSession();
//2. 存储数据
session.setAttribute("username","zs");
}

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

(4)SessionDemo2:获取Session对象、获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/demo2")
public class SessionDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取数据,从session中
//1. 获取Session对象
HttpSession session = request.getSession();
//2. 获取数据
Object username = session.getAttribute("username");
System.out.println(username);
}

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

(5)启动测试,

  • 先访问http://localhost:8080/cookie-demo/demo1,将数据存入Session
  • 在访问http://localhost:8080/cookie-demo/demo2,从Session中获取数据
  • 查看控制台 成功输出zs

Session是能够在一次会话中两次请求之间共享数据。

**注意:**Session中可以存储的是一个Object类型的数据,也就是说Session中可以存储任意数据类型。

3.2 Session的原理分析

  • Session是基于Cookie实现的

这句话其实不太能详细的说明Session的底层实现,接下来,咱们一步步来分析下Session的具体实现原理:

(1)前提条件

1629429063101

Session要想实现一次会话多次请求之间的数据共享,就必须要保证多次请求获取Session的对象是同一个。

那么它们是一个对象么?要验证这个结论也很简单,只需要在上面案例中的两个Servlet中分别打印下Session对象

SessionDemo1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/demo1")
public class SessionDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//存储到Session中
//1. 获取Session对象
HttpSession session = request.getSession();
System.out.println(session);
//2. 存储数据
session.setAttribute("username","zs");
}

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

SessionDemo2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@WebServlet("/demo2")
public class SessionDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取数据,从session中
//1. 获取Session对象
HttpSession session = request.getSession();
System.out.println(session);
//2. 获取数据
Object username = session.getAttribute("username");
System.out.println(username);
}

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

启动测试,分别访问/demo1``/demo2,输出如下

org.apache.catalina.session.StandardSessionFacade@69186990
org.apache.catalina.session.StandardSessionFacade@69186990

通过打印可以得到如下结论:

  • 两个Servlet类中获取的Session对象是同一个
  • 把demo1和demo2请求刷新多次,控制台最终打印的结果都是同一个

那么问题又来了,如果新开一个浏览器,访问demo1或者demo2,打印在控制台的Session还是同一个对象么?

1629429788264

注意:在一台电脑上演示的时候,如果是相同的浏览器必须要把浏览器全部关掉重新打开,才算新开的一个浏览器。

测试的结果:如果是不同浏览器或者重新打开浏览器后,打印的Session就不一样了

所以Session实现的也是一次会话中的多次请求之间的数据共享。

那么最主要的问题就来了,Session是如何保证在一次会话中获取的Session对象是同一个呢?

1629430754825

(1)demo1在第一次获取session对象的时候,session对象会有一个唯一的标识,假如是id:10

(2)demo1在session中存入其他数据并处理完成所有业务后,需要通过Tomcat服务器响应结果给浏览器

(3)Tomcat服务器发现业务处理中使用了session对象,就会把session的唯一标识id:10当做一个cookie,添加Set-Cookie:JESSIONID=10到响应头中,并响应给浏览器

(4)浏览器接收到响应结果后,会把响应头中的coookie数据存储到浏览器的内存中

(5)浏览器在同一会话中访问demo2的时候,会把cookie中的数据按照cookie: JESSIONID=10的格式添加到请求头中并发送给服务器Tomcat

(6)demo2获取到请求后,从请求头中就读取cookie中的JSESSIONID值为10,然后就会到服务器内存中寻找id:10的session对象,如果找到了,就直接返回该对象,如果没有则新创建一个session对象

(7)关闭打开浏览器后,因为浏览器的cookie已被销毁,所以就没有JESSIONID的数据,服务端获取到的session就是一个全新的session对象

至此,Session是基于Cookie来实现的这就话,我们就解释完了

3.3 Session的使用细节

3.3.1 Session钝化与活化

首先需要大家思考的问题是:

  • 服务器重启后,Session中的数据是否还在?

要想回答这个问题,我们可以先看下下面这幅图,

1629438984314

(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数据不能长期保存数据

  • cookie是存储在客户端,是可以长期保存

3.3.2 Session销毁

session的销毁会有两种方式:

  • 默认情况下,无操作,30分钟自动销毁

    • 对于这个失效时间,是可以通过配置进行修改的

      • 在项目的web.xml中配置

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        <?xml version="1.0" encoding="UTF-8"?>
        <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
        version="3.1">

        <session-config>
        <session-timeout>100</session-timeout>
        </session-config>
        </web-app>
      • 如果没有配置,默认是30分钟,默认值是在Tomcat的web.xml配置文件中写死的

  • 调用Session对象的invalidate()进行销毁

    • 在SessionDemo2类中添加session销毁的方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      @WebServlet("/demo2")
      public class SessionDemo2 extends HttpServlet {
      @Override
      protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      //获取数据,从session中

      //1. 获取Session对象
      HttpSession session = request.getSession();
      System.out.println(session);

      // 销毁
      session.invalidate();
      //2. 获取数据
      Object username = session.getAttribute("username");
      System.out.println(username);
      }

      @Override
      protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      this.doGet(request, response);
      }
      }
    • 启动访问测试,先访问demo1将数据存入到session,再次访问demo2从session中获取数据,会得到如下错误信息

      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,用户登录注册案例

需求分析

在上篇文章,我们已经完成了对品牌数据的增删改查操作,现在我们继续完善登录功能

需求说明:

  1. 完成用户登录功能,如果用户勾选"记住用户" ,则下次访问登录页面自动填充用户名密码
  2. 完成注册功能,并实现验证码功能

用户登录功能

  1. 需求:

    • 用户登录成功后,跳转到列表页面,并在页面上展示当前登录的用户名称
    • 用户登录失败后,跳转回登录页面,并在页面上展示对应的错误信息
  2. 实现流程分析

    • 前端通过表单发送请求和数据给Web层的LoginServlet
    • 在LoginServlet中接收请求和数据[用户名和密码]
    • LoginServlet接收到请求和数据后,调用Service层完成根据用户名和密码查询用户对象
    • 在Service层需要编写UserService类,在类中实现login方法,方法中调用Dao层的UserMapper
    • 在UserMapper接口中,声明一个根据用户名和密码查询用户信息的方法
    • Dao层把数据查询出来以后,将返回数据封装到User对象,将对象交给Service层
    • Service层将数据返回给Web层
    • Web层获取到User对象后,判断User对象,如果为Null,则将错误信息响应给登录页面,如果不为Null,则跳转到列表页面,并把当前登录用户的信息存入Session携带到列表页面。
  3. 具体实现

    • 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
    41
    package 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;
    }

    @Override
    public String toString() {
    return "User{" +
    "id=" + id +
    ", username='" + username + '\'' +
    ", password='" + password + '\'' +
    '}';
    }
    }
    • com.blog.mapper包下新建UserMapper接口
      我们只需要登录,添加(注册)和查询(检测用户名是否已被占用)这三个功能
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public interface UserMapper {
    @Select("select * from tb_user where username=#{username} and password=#{password}")
    User login(@Param("username") String username, @Param("password") String password);

    @Select("select * from tb_user where username=#{username}")
    User selectByUsername(String username);

    @Insert("insert into tb_user values (null,#{username},#{password})")
    void add(User user);
    }
    • resources/com/itheima/mapper目录下新建UserMapper.xml
      由于我们使用注解开发,所以这里啥也不用写,当SQL语句很复杂的时候,我们才会采用此种方式
    1
    2
    3
    4
    5
    6
    7
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.blog.mapper.UserMapper">

    </mapper>
    • com.itheima.service包下,创建UserService类
      这里要实现登录的逻辑,上一篇文章也练习过很多遍了
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public 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
    @WebServlet("/loginServlet")
    public class LoginServlet extends HttpServlet {
    private UserService userService = new UserService();

    @Override
    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);
    }
    }

    @Override
    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. 需求:
    如果用户勾选"记住用户" ,则下次访问登陆页面自动填充用户名密码。这样可以提升用户的体验。
    对应上面这个需求,最大的问题就是: 如何自动填充用户名和密码?

  2. 实现流程分析

    • 因为

      1
      记住我

      功能要实现的效果是,就算用户把浏览器关闭过几天再来访问也能自动填充,所以需要将登陆信息存入一个可以长久保存,并且能够在浏览器关闭重新启动后依然有效的地方,就是我们前面讲的Cookie,所以:

      • 将用户名和密码写入Cookie中,并且持久化存储Cookie,下次访问浏览器会自动携带Cookie
      • 在页面获取Cookie数据后,设置到用户名和密码框中
      • 何时写入Cookie?
        • 用户必须登陆成功后才需要写
        • 用户必须在登录页面勾选了记住我的复选框
  3. 具体实现

  • 在LoginServlet获取复选框的值并在登录成功后进行设置Cookie
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
//这里要多获取一个复选框的值,在之前的jsp代码中,我将复选框的值设为了"1"
String remember = request.getParameter("remember");
User user = userService.login(username, password);
if (user != null) {
//当登录成功后,我们再来判断一下复选框选了没
if ("1".equals(remember)) {
//创建Cookie对象
Cookie c_username = new Cookie("username", username);
//设置一下存活时间为7天,更长也行,随便你
c_username.setMaxAge(7 * 24 * 60 * 60);
Cookie c_password = new Cookie("password", password);
c_password.setMaxAge(7 * 24 * 60 * 60);
//发送Cookie
response.addCookie(c_username);
response.addCookie(c_password);
}
HttpSession session = request.getSession();
session.setAttribute("user", user);
response.sendRedirect("/brand_demo/selectAllServlet");
} else {
request.setAttribute("login_msg", "用户名或密码错误");
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
}
  • 启动访问测试,只有当前用户名和密码输入正确,并且勾选了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

  1. 需求
    登录成功并勾选了Remeber后,后端返回给前端的Cookie数据就已经存储好了,接下来就需要在页面获取Cookie中的数据,并把数据设置到登录页面的用户名和密码框中。
    如何在页面直接获取Cookie中的值呢?

  2. 实现流程分析
    在页面可以使用EL表达式,${cookie.key.value}
    key:指的是存储在cookie中的键名称,例如${cookie.username.value}

  3. 具体实现

    • 在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>
    • 访问测试,重新访问登录页面,就可以看得用户和密码已经被填充。

用户注册功能

  1. 需求

    • 注册功能:保存用户信息到数据库
    • 验证码功能
      • 展示验证码:展示验证码图片,并可以点击切换
      • 校验验证码:验证码填写不正确,则注册失败
  2. 实现流程分析

    • 前端通过表单发送请求和数据给Web层的RegisterServlet
    • 在RegisterServlet中接收请求和数据[用户名和密码]
    • RegisterServlet接收到请求和数据后,调用Service层完成用户信息的保存
    • 在Service层需要编写UserService类,在类中实现register方法,需要判断用户是否已经存在,如果不存在,则完成用户数据的保存
    • 在UserMapper接口中,声明两个方法,一个是根据用户名查询用户信息方法,另一个是保存用户信息方法
    • 在UserService类中保存成功则返回true,失败则返回false,将数据返回给Web层
    • Web层获取到结果后,如果返回的是true,则提示注册成功,并转发到登录页面,如果返回false则提示用户名已存在并转发到注册页面
  3. 具体实现

    • 在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
    @WebServlet("/registerServlet")
    public class RegisterServlet extends HttpServlet {
    private UserService userService = new UserService();

    @Override
    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);
    }
    }

    @Override
    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. 需求分析

    • 展示验证码:展示验证码图片,并可以点击看不清切换,验证码的生成是通过工具类来实现的
    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
    280
    package 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代码生成的一张图片
    • 验证码的作用:防止机器自动注册,攻击服务器
  2. 实现流程分析

    • 前端发送请求给CheckCodeServlet
    • CheckCodeServlet接收到请求后,生成验证码图片,将图片用Reponse对象的输出流写回到前端
    • 思考:如何将图片写回到前端浏览器呢?
      • 前面在学Reponse对象的时候,它有一个方法可以获取其字节输出流,getOutputStream()
      • 我们可以把写往磁盘的流对象更好成Response的字节流,即可完成图片响应给前端
  3. 具体实现

    • 修改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
    @WebServlet("/checkCodeServlet")
    public class CheckCodeServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //生成验证码
    OutputStream os = response.getOutputStream();
    String checkCode = CheckCodeUtil.outputVerifyImage(100, 50, os, 4);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }
    }
    • 完成这些之后,我们启动服务器,就可以看到验证码了,点击看不清,也会自动刷新验证码

验证码-校验

  1. 需求

    • 判断程序生成的验证码 和 用户输入的验证码 是否一样,如果不一样,则阻止注册

    • 验证码图片访问和提交注册表单是两次请求,所以要将程序生成的验证码存入Session中

    • 思考:为什么要把验证码数据存入到Session中呢?

      • 生成验证码和校验验证码是两次请求,此处就需要在一个会话的两次请求之间共享数据
      • 验证码属于安全数据类的,所以我们选中Session来存储验证码数据。
  2. 实现流程分析

    • 在CheckCodeServlet中生成验证码的时候,将验证码数据存入Session对象
    • 前端将验证码和注册数据提交到后台,交给RegisterServlet类
    • RegisterServlet类接收到请求和数据后,其中就有验证码,和Session中的验证码进行对比
    • 如果一致,则完成注册,如果不一致,则提示错误信息
  3. 具体实现

    • 修改CheckCodeServlet类,将验证码存入Session域
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @WebServlet("/checkCodeServlet")
    public class CheckCodeServlet extends HttpServlet {
    @Override
    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);
    }

    @Override
    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
    @WebServlet("/registerServlet")
    public class RegisterServlet extends HttpServlet {
    private UserService userService = new UserService();

    @Override
    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);
    }
    }

    @Override
    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
    //这里暂时先拦截所有资源,后面我们会仔细讲拦截路径的配置
    @WebFilter("/*")
    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
    @WebFilter("/*")
    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
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 FILTER</h1>

</body>
</html>

pom.xml 配置文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>filter-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>80</port>
</configuration>
</plugin>
</plugins>
</build>
</project>

在java目录下新建com.blog.web.filter包,并新建FilterDemo1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@WebFilter("/*")
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() {

}
}
  • 测试
    • 将放行的代码注释掉,我们访问hello.jsp页面,不会有任何内容,因为被拦截了,且没有放行,控制台会输出doFilter...
    • 打开放行的代码,我们访问hello.jsp页面,会有h1标签正常输出HELLO FILTER

上述效果说明了FilterDemo1这个过滤器的doFilter方法被执行了,且必须添加放行的方法才能访问hello.jsp页面

1.3 Filter执行流程

Filter执行流程

如上图是使用过滤器的流程,我们通过以下问题来研究过滤器的执行流程:

  • 放行后访问对应资源,资源访问完成后,还会回到Filter中吗?

    从上图就可以看出肯定 回到Filter中

  • 如果回到Filter中,是重头执行还是执行放行后的逻辑呢?

    如果是重头执行的话,就意味着 放行前逻辑 会被执行两次,肯定不会这样设计了;所以访问完资源后,会回到 放行后逻辑,执行该部分代码。

通过上述的说明,我们就可以总结Filter的执行流程如下:

  • 执行放行前逻辑 --> 放行 --> 访问资源 --> 执行放行后逻辑

接下来我们通过代码验证一下,在 doFilter() 方法前后都加上输出语句,如下

1
2
3
4
5
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("1.Filter...");
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("3.Filter...");
}

同时在 hello.jsp 页面加上输出语句,如下

1
2
3
4
5
6
<body>
<h1>HELLO FILTER</h1>
<%
System.out.println("2.Filter...");
%>
</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应用,可以配置多个过滤器,这多个过滤器称为过滤器链。

如下图就是一个过滤器链,我们学习过滤器链主要是学习过滤器链执行的流程

img

上图中的过滤器链执行是按照以下流程执行:

  1. 执行 Filter1 的放行前逻辑代码
  2. 执行 Filter1 的放行代码
  3. 执行 Filter2 的放行前逻辑代码
  4. 执行 Filter2 的放行代码
  5. 访问到资源
  6. 执行 Filter2 的放行后逻辑代码
  7. 执行 Filter1 的放行后逻辑代码

以上流程串起来就像一条链子,故称之为过滤器链。

1.5.2 代码演示

  • 编写第一个过滤器 FilterDemo ,配置成拦截所有资源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @WebFilter("/*")
    public class FilterDemo implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

    //1. 放行前,对 request数据进行处理
    System.out.println("1.FilterDemo...");
    //放行
    chain.doFilter(request,response);
    //2. 放行后,对Response 数据进行处理
    System.out.println("3.FilterDemo...");
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }
    }
  • 编写第二个过滤器 FilterDemo2 ,配置炒年糕拦截所有资源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @WebFilter("/*")
    public class FilterDemo2 implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

    //1. 放行前,对 request数据进行处理
    System.out.println("2.FilterDemo...");
    //放行
    chain.doFilter(request,response);
    //2. 放行后,对Response 数据进行处理
    System.out.println("4.FilterDemo...");
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }
    }

  • 修改 hello.jsp 页面中脚本的输出语句

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
    <title>Title</title>
    </head>
    <body>
    <h1>hello JSP~</h1>
    <%
    System.out.println("3.hello jsp");
    %>
    </body>
    </html>
  • 启动服务器,在浏览器输入 http://localhost/filter-demo/hello.jsp 进行测试,在控制台打印内容如下

    1
    2
    3
    4
    5
    1.FilterDemo.
    2.FilterDemo
    3.hello jsp
    4.FilterDemo
    5.FilterDemo

    从结果可以看到确实是按照我们之前说的执行流程进行执行的。

1.5.3 问题

上面代码中为什么是先执行 FilterDemo ,后执行 FilterDemo2 呢?

我们现在使用的是注解配置Filter,而这种配置方式的优先级是按照过滤器类名(字符串)的自然排序。

比如有如下两个名称的过滤器 : BFilterDemoAFilterDemo 。那一定是 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
2
3
4
5
6
7
8
9
10
11
12
13
@WebFilter("/*")
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {

}

public void init(FilterConfig config) throws ServletException {
}

public void destroy() {
}
}
1.6.3.2 编写逻辑代码

doFilter() 方法中编写登陆状态校验的逻辑代码。

我们首先需要从 session 对象中获取用户信息,但是 ServletRequest 类型的 requset 对象没有获取 session 对象的方法,所以此时需要将 request对象强转成 HttpServletRequest 对象。

1
HttpServletRequest req = (HttpServletRequest) request;

然后完成以下逻辑

  • 获取Session对象
  • 从Session对象中获取名为 user 的数据
  • 判断获取到的数据是否是 null
    • 如果不是,说明已经登陆,放行
    • 如果是,说明尚未登陆,将提示信息存储到域对象中并跳转到登陆页面

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@WebFilter("/*")
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) request;

//1. 判断session中是否有user
HttpSession session = req.getSession();
Object user = session.getAttribute("user");

//2. 判断user是否为null
if(user != null){
// 登录过了
//放行
chain.doFilter(request, response);
}else {
// 没有登陆,存储提示信息,跳转到登录页面

req.setAttribute("login_msg","您尚未登陆!");
req.getRequestDispatcher("/login.jsp").forward(req,response);
}
}

public void init(FilterConfig config) throws ServletException {
}

public void destroy() {
}
}
1.6.3.3 测试并抛出问题

重启服务器,访问register.jsp注册页面,竟然访问不了了,也是直接跳转到了登录页面,而且如果你配置了css文件,css样式也显示不出来了,这是怎么回事呢?

1.6.3.4 问题分析及解决

因为我们配置的是对所有页面进行拦截,但现在需要对所有的登陆和注册相关的资源进行放行。
所以我们需要在判断session中是否包含用户信息之前,应该加上对登陆及注册相关资源放行的逻辑处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//判断访问资源路径是否和登录注册相关
//1,在数组中存储登陆和注册相关的资源路径
String[] urls = {"/login.jsp","/imgs/","/css/","/loginServlet","/register.jsp","/registerServlet","/checkCodeServlet"};
//2,获取当前访问的资源路径
String url = req.getRequestURL().toString();

//3,遍历数组,获取到每一个需要放行的资源路径
for (String u : urls) {
//4,判断当前访问的资源路径字符串是否包含要放行的的资源路径字符串
/*
比如当前访问的资源路径是 /brand-demo/login.jsp
而字符串 /brand-demo/login.jsp 包含了 字符串 /login.jsp ,所以这个字符串就需要放行
*/
if(url.contains(u)){
//找到了,放行
chain.doFilter(request, response);
//break;
return;
}
}
1.6.3.5 过滤器完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@WebFilter("/*")
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) request;

//判断访问资源路径是否和登录注册相关
//1,在数组中存储登陆和注册相关的资源路径
String[] urls = {"/login.jsp","/imgs/","/css/","/loginServlet","/register.jsp","/registerServlet","/checkCodeServlet"};
//2,获取当前访问的资源路径
String url = req.getRequestURL().toString();

//3,遍历数组,获取到每一个需要放行的资源路径
for (String u : urls) {
//4,判断当前访问的资源路径字符串是否包含要放行的的资源路径字符串
/*
比如当前访问的资源路径是 /brand-demo/login.jsp
而字符串 /brand-demo/login.jsp 包含了 字符串 /login.jsp ,所以这个字符串就需要放行
*/
if(url.contains(u)){
//找到了,放行
chain.doFilter(request, response);
//break;
return;
}
}

//1. 判断session中是否有user
HttpSession session = req.getSession();
Object user = session.getAttribute("user");

//2. 判断user是否为null
if(user != null){
// 登录过了
//放行
chain.doFilter(request, response);
}else {
// 没有登陆,存储提示信息,跳转到登录页面

req.setAttribute("login_msg","您尚未登陆!");
req.getRequestDispatcher("/login.jsp").forward(req,response);
}
}

public void init(FilterConfig config) throws ServletException {
}

public void destroy() {
}
}

2,Listener

2.1 概述

  • Listener 表示监听器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一。

  • 监听器可以监听就是在 applicationsessionrequest 三个对象创建、销毁或者往其中添加修改删除属性时自动执行代码的功能组件。

    request 和 session 我们学习过。而 applicationServletContext 类型的对象。

    ServletContext 代表整个web应用,在服务器启动的时候,tomcat会自动创建该对象。在服务器关闭时会自动销毁该对象。

2.2 分类

JavaWeb 提供了8个监听器:

监听器分类 监听器名称 作用
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
2
3
4
5
6
7
8
9
10
11
12
13
@WebListener
public class ContextLoaderListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
//加载资源
System.out.println("ContextLoaderListener...");
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
//释放资源
}
}

启动服务器,就可以在启动的日志信息中看到 contextInitialized() 方法输出的内容,同时也说明了 ServletContext 对象在服务器启动的时候被创建了。

3,Ajax

3.1 概述

AJAX (Asynchronous JavaScript And XML):异步的 JavaScript 和 XML。`

3.1.1 作用

AJAX 作用有以下两方面:

  1. 与服务器进行数据交换:通过AJAX可以给服务器发送请求,服务器将数据直接响应回给浏览器。
    • 在之前,我们做功能的流程是,Servlet 调用完业务逻辑层,然后将数据存储到域对象中,然后跳转到指定的 jsp 页面,在页面上使用 EL表达式JSTL 标签库进行数据的展示。
    • 而我们学习了AJAX 后,就可以使用AJAX和服务器进行通信,以达到使用HTML+AJAX来替换JSP页面了。浏览器发送请求servlet,servlet 调用完业务逻辑层后将数据直接响应回给浏览器页面,页面使用 HTML 来进行数据展示。
  2. 异步交互:可以在不重新加载整个页面的情况下,与服务器交换数据并更新部分网页的技术,如:搜索联想、用户名是否可用校验,等等…
    • 当我们在百度输入一些关键字(例如 奥运)后就会在下面联想出相关的内容,而联想出来的这部分数据肯定是存储在百度的服务器上,而我们并没有看出页面重新刷新,这就是更新局部页面的效果。
    • 我们在用户名的输入框输入用户名,当输入框一失去焦点,如果用户名已经被占用就会在下方展示提示的信息;在这整个过程中也没有页面的刷新,只是在局部展示出了提示信息,这就是更新局部页面的效果。

3.1.2 同步和异步

知道了局部刷新后,接下来我们再聊聊同步和异步:

  • 同步发送请求过程如下

同步请求.png

​ 浏览器页面在发送请求给服务器,在服务器处理请求的过程中,浏览器页面不能做其他的操作。只能等到服务器响应结束后才能,浏览器页面才能继续做其他的操作。

  • 异步发送请求过程如下

    异步请求.png

    浏览器页面发送请求给服务器,在服务器处理请求的过程中,浏览器页面还可以做其他的操作。

3.2 快速入门

3.2.1 服务端实现

在项目的创建 com.itheima.web.servlet ,并在该包下创建名为 AjaxServlet 的servlet

1
2
3
4
5
6
7
8
9
10
11
12
13
@WebServlet("/ajaxServlet")
public class AjaxServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 响应数据
response.getWriter().write("hello ajax~");
}

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

3.2.2 客户端实现

webapp 下创建名为 01-ajax-demo1.html 的页面,在该页面书写 ajax 代码

  • 创建核心对象,不同的浏览器创建的对象是不同的

    1
    2
    3
    4
    5
    6
    7
     var xhttp;
    if (window.XMLHttpRequest) {
    xhttp = new XMLHttpRequest();
    } else {
    // code for IE6, IE5
    xhttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
  • 发送请求

    1
    2
    3
    4
    //建立连接
    xhttp.open("GET", "http://localhost:8080/ajax-demo/ajaxServlet");
    //发送请求
    xhttp.send();
  • 获取响应

    1
    2
    3
    4
    5
    6
    xhttp.onreadystatechange = function() {
    if (this.readyState ` 4 && this.status ` 200) {
    // 通过 this.responseText 可以获取到服务端响应的数据
    alert(this.responseText);
    }
    };

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<script>
//1. 创建核心对象
var xhttp;
if (window.XMLHttpRequest) {
xhttp = new XMLHttpRequest();
} else {
// code for IE6, IE5
xhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
//2. 发送请求
xhttp.open("GET", "http://localhost:8080/ajax-demo/ajaxServlet");
xhttp.send();

//3. 获取响应
xhttp.onreadystatechange = function() {
if (this.readyState ` 4 && this.status ` 200) {
alert(this.responseText);
}
};
</script>
</body>
</html>

3.2.3 测试

在浏览器地址栏输入 http://localhost:8080/ajax-demo/01-ajax-demo1.html ,在 01-ajax-demo1.html加载的时候就会发送 ajax 请求,并有一个alert弹窗输出hello ajax~

3.3 案例

需求:在完成用户注册时,当用户名输入框失去焦点时,校验用户名是否在数据库已存在

3.3.1 分析

  • 前端完成的逻辑
    1. 给用户名输入框绑定光标失去焦点事件 onblur
    2. 发送 ajax请求,携带username参数
    3. 处理响应:是否显示提示信息
  • 后端完成的逻辑
    1. 接收用户名
    2. 调用service查询User。此案例是为了演示前后端异步交互,所以此处我们不做业务逻辑处理
    3. 返回标记

整体流程如下:

image-20210829183854285

3.3.2 后端实现

com.ithiema.web.servlet 包中定义名为 SelectUserServlet 的servlet。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/selectUserServlet")
public class SelectUserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 接收用户名
String username = request.getParameter("username");
//2. 调用service查询User对象,此处不进行业务逻辑处理,直接给 flag 赋值为 true,表明用户名占用
boolean flag = true;
//3. 响应标记
response.getWriter().write("" + flag);
}

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

3.3.3 前端实现

  • 随便写一个注册的页面register.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
<head>
<title>Title</title>
<meta charset="utf-8">
</head>
<body>
<form action="/ajax_demo/selectUserServlet" method="get">
<h1>欢迎注册</h1>
用户名:<input name="username" type="text" id="username">
<span id="username_arr" style="display:none;">用户名已被占用</span><br>
密码:<input name="password" type="password"><br>
<input value="注册" type="submit">
</form>
</body>
</html>

第一步:给用户名输入框绑定光标失去焦点事件 onblur

1
2
3
4
//1. 给用户名输入框绑定 失去焦点事件
document.getElementById("username").onblur = function () {

}

第二步:发送 ajax请求,携带username参数

第一步 绑定的匿名函数中书写发送 ajax 请求的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//2. 发送ajax请求
//2.1. 创建核心对象
var xhttp;
if (window.XMLHttpRequest) {
xhttp = new XMLHttpRequest();
} else {
// code for IE6, IE5
xhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
//2.2. 发送请求
xhttp.open("GET", "http://localhost:8080/ajax-demo/selectUserServlet);
xhttp.send();

//2.3. 获取响应
xhttp.onreadystatechange = function() {
if (this.readyState ` 4 && this.status ` 200) {
//处理响应的结果
}
};

由于我们发送的是 GET 请求,所以需要在 URL 后拼接从输入框获取的用户名数据。而我们在 第一步 绑定的匿名函数中通过以下代码可以获取用户名数据

1
2
// 获取用户名的值
var username = this.value; //this : 给谁绑定的事件,this就代表谁

而携带数据需要将 URL 修改为:

1
xhttp.open("GET", "http://localhost:8080/ajax-demo/selectUserServlet?username="+username);

第三步:处理响应:是否显示提示信息

this.readyState 4 && this.status 200 条件满足时,说明已经成功响应数据了。

此时需要判断响应的数据是否是 “true” 字符串,如果是说明用户名已经占用给出错误提示;如果不是说明用户名未被占用清除错误提示。代码如下

1
2
3
4
5
6
7
8
//判断
if(this.responseText ` "true"){
//用户名存在,显示提示信息
document.getElementById("username_err").style.display = '';
}else {
//用户名不存在 ,清楚提示信息
document.getElementById("username_err").style.display = 'none';
}

综上所述,前端完成代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//1. 给用户名输入框绑定 失去焦点事件
document.getElementById("username").onblur = function () {
//2. 发送ajax请求
// 获取用户名的值
var username = this.value;

//2.1. 创建核心对象
var xhttp;
if (window.XMLHttpRequest) {
xhttp = new XMLHttpRequest();
} else {
// code for IE6, IE5
xhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
//2.2. 发送请求
xhttp.open("GET", "http://localhost:8080/ajax-demo/selectUserServlet?username="+username);
xhttp.send();

//2.3. 获取响应
xhttp.onreadystatechange = function() {
if (this.readyState ` 4 && this.status ` 200) {
//alert(this.responseText);
//判断
if(this.responseText ` "true"){
//用户名存在,显示提示信息
document.getElementById("username_err").style.display = '';
}else {
//用户名不存在 ,清楚提示信息
document.getElementById("username_err").style.display = 'none';
}
}
};
}

4,axios

Axios 对原生的AJAX进行封装,简化书写。

Axios官网是:https://www.axios-http.cn

4.1 基本使用

axios 使用是比较简单的,分为以下两步:

  • 引入 axios 的 js 文件

    1
    <script src="js/axios-0.18.0.js"></script>
  • 使用axios 发送请求,并获取响应结果

    • 发送 get 请求

      1
      2
      3
      4
      5
      6
      axios({
      method:"get",
      url:"http://localhost:8080/ajax-demo1/aJAXDemo1?username=zhangsan"
      }).then(function (resp){
      alert(resp.data);
      })
    • 发送 post 请求

      1
      2
      3
      4
      5
      6
      7
      axios({
      method:"post",
      url:"http://localhost:8080/ajax-demo1/aJAXDemo1",
      data:"username=zhangsan"
      }).then(function (resp){
      alert(resp.data);
      });

axios() 是用来发送异步请求的,小括号中使用 js 对象传递请求相关的参数:

  • method 属性:用来设置请求方式的。取值为 get 或者 post
  • url 属性:用来书写请求的资源路径。如果是 get 请求,需要将请求参数拼接到路径的后面,格式为: url?参数名=参数值&参数名2=参数值2
  • data 属性:作为请求体被发送的数据。也就是说如果是 post 请求的话,数据需要作为 data 属性的值。

then() 需要传递一个匿名函数。我们将 then() 中传递的匿名函数称为 回调函数,意思是该匿名函数在发送请求时不会被调用,而是在成功响应后调用的函数。而该回调函数中的 resp 参数是对响应的数据进行封装的对象,通过 resp.data 可以获取到响应的数据。

4.2 快速入门

4.2.1 后端实现

定义一个用于接收请求的servlet,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@WebServlet("/axiosServlet")
public class AxiosServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("get...");
//1. 接收请求参数
String username = request.getParameter("username");
System.out.println(username);
//2. 响应数据
response.getWriter().write("hello Axios~");
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("post...");
this.doGet(request, response);
}
}

4.2.2 前端实现

  • 引入 js 文件

    1
    <script src="js/axios-0.18.0.js"></script>
  • 发送 ajax 请求

    • get 请求

      1
      2
      3
      4
      5
      6
      axios({
      method:"get",
      url:"http://localhost:8080/ajax-demo/axiosServlet?username=zhangsan"
      }).then(function (resp) {
      alert(resp.data);
      })
    • post 请求

      1
      2
      3
      4
      5
      6
      7
      axios({
      method:"post",
      url:"http://localhost:8080/ajax-demo/axiosServlet",
      data:"username=zhangsan"
      }).then(function (resp) {
      alert(resp.data);
      })

整体页面代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<script src="js/axios-0.18.0.js"></script>
<script>
//1. get
/* axios({
method:"get",
url:"http://localhost:8080/ajax-demo/axiosServlet?username=zhangsan"
}).then(function (resp) {
alert(resp.data);
})*/

//2. post 在js中{} 表示一个js对象,而这个js对象中有三个属性
axios({
method:"post",
url:"http://localhost:8080/ajax-demo/axiosServlet",
data:"username=zhangsan"
}).then(function (resp) {
alert(resp.data);
})
</script>
</body>
</html>

4.3 请求方法别名

为了方便起见, Axios 已经为所有支持的请求方法提供了别名。如下:

  • get 请求 : axios.get(url[,config])

  • delete 请求 : axios.delete(url[,config])

  • head 请求 : axios.head(url[,config])

  • options 请求 : axios.option(url[,config])

  • post 请求:axios.post(url[,data[,config])

  • put 请求:axios.put(url[,data[,config])

  • patch 请求:axios.patch(url[,data[,config])

而我们只关注 get 请求和 post 请求。

入门案例中的 get 请求代码可以改为如下:

1
2
3
axios.get("http://localhost:8080/ajax-demo/axiosServlet?username=zhangsan").then(function (resp) {
alert(resp.data);
});

入门案例中的 post 请求代码可以改为如下:

1
2
3
axios.post("http://localhost:8080/ajax-demo/axiosServlet","username=zhangsan").then(function (resp) {
alert(resp.data);
})

5,JSON

5.1 概述

概念:JavaScript Object Notation。JavaScript 对象表示法.

如下是 JavaScript 对象的定义格式:

1
2
3
4
5
{
name:"zhangsan",
age:23,
city:"北京"
}

接下来我们再看看 JSON 的格式:

1
2
3
4
5
{
"name":"zhangsan",
"age":23,
"city":"北京"
}

通过上面 js 对象格式和 json 格式进行对比,发现两个格式特别像。只不过 js 对象中的属性名可以使用引号(可以是单引号,也可以是双引号);而 json 格式中的键要求必须使用双引号括起来,这是 json 格式的规定。json 格式的数据有什么作用呢?

作用:由于其语法格式简单,层次结构鲜明,现多用于作为数据载体

5.2 JSON 基础语法

5.2.1 定义格式

JSON 本质就是一个字符串,但是该字符串内容是有一定的格式要求的。 定义格式如下:

1
var 变量名 = '{"key":value,"key":value,...}';

JSON 串的键要求必须使用双引号括起来,而值根据要表示的类型确定。value 的数据类型分为如下

  • 数字(整数或浮点数)
  • 字符串(使用双引号括起来)
  • 逻辑值(true或者false)
  • 数组(在方括号中)
  • 对象(在花括号中)
  • null

示例:

1
var jsonStr = '{"name":"zhangsan","age":23,"addr":["北京","上海","西安"]}'

5.2.2 代码演示

创建一个页面,在该页面的 <script> 标签中定义json字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
//1. 定义JSON字符串
var jsonStr = '{"name":"zhangsan","age":23,"addr":["北京","上海","西安"]}'
alert(jsonStr);

</script>
</body>
</html>

通过浏览器打开,浏览器会有一个弹窗显示`{“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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
//1. 定义JSON字符串
var jsonStr = '{"name":"zhangsan","age":23,"addr":["北京","上海","西安"]}'
alert(jsonStr);

//2. 将 JSON 字符串转为 JS 对象
let jsObject = JSON.parse(jsonStr);
alert(jsObject)
alert(jsObject.name)
//3. 将 JS 对象转换为 JSON 字符串
let jsonStr2 = JSON.stringify(jsObject);
alert(jsonStr2)
</script>
</body>
</html>

5.2.3 发送异步请求携带参数

后面我们使用 axios 发送请求时,如果要携带复杂的数据时都会以 JSON 格式进行传递,如下

1
2
3
4
5
6
7
axios({
method:"post",
url:"http://localhost:8080/ajax-demo/axiosServlet",
data:"username=zhangsan"
}).then(function (resp) {
alert(resp.data);
})

请求参数不可能由我们自己拼接字符串,我们可以提前定义一个 js 对象,用来封装需要提交的参数,然后使用 JSON.stringify(js对象) 转换为 JSON 串,再将该 JSON 串作为 axiosdata 属性值进行请求参数的提交。如下:

1
2
3
4
5
6
7
8
9
var jsObject = {name:"张三"};

axios({
method:"post",
url:"http://localhost:8080/ajax-demo/axiosServlet",
data: JSON.stringify(jsObject)
}).then(function (resp) {
alert(resp.data);
})

axios 是一个很强大的工具。我们只需要将需要提交的参数封装成 js 对象,并将该 js 对象作为 axiosdata 属性值进行,它会自动将 js 对象转换为 JSON 串进行提交。如下:

1
2
3
4
5
6
7
8
9
var jsObject = {name:"张三"};

axios({
method:"post",
url:"http://localhost:8080/ajax-demo/axiosServlet",
data:jsObject //这里 axios 会将该js对象转换为 json 串的
}).then(function (resp) {
alert(resp.data);
})

注意:

  • js 提供的 JSON 对象我们只需要了解一下即可。因为 axios 会自动对 js 对象和 JSON 串进行想换转换。
  • 发送异步请求时,如果请求参数是 JSON 格式,那请求方式必须是 POST。因为 JSON 串需要放在请求体中。

5.3 JSON串和Java对象的相互转换

在后端我们就需要重点学习以下两部分操作:

  • 请求数据:JSON字符串转为Java对象
  • 响应数据:Java对象转为JSON字符串

接下来给大家介绍一套 API,可以实现上面两部分操作。这套 API 就是 Fastjson

5.3.1 Fastjson 概述

Fastjson 是阿里巴巴提供的一个Java语言编写的高性能功能完善的 JSON 库,是目前Java语言中最快的 JSON 库,可以实现 Java 对象和 JSON 字符串的相互转换。

5.3.2 Fastjson 使用

Fastjson 使用也是比较简单的,分为以下三步完成

  1. 导入坐标

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.62</version>
    </dependency>
  2. Java对象转JSON

    1
    String jsonStr = JSON.toJSONString(obj);

    将 Java 对象转换为 JSON 串,只需要使用 Fastjson 提供的 JSON 类中的 toJSONString() 静态方法即可。

  3. JSON字符串转Java对象

    1
    User user = JSON.parseObject(jsonStr, User.class);

    将 json 转换为 Java 对象,只需要使用 Fastjson 提供的 JSON 类中的 parseObject() 静态方法即可。

5.3.3 代码演示

  • 先创建一个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
41
42
43
44
45
46
47
48
49
package com.blog.web.pojo;

public class User {
private Integer id;
private String username;
private String password;

public User() {
}

public User(Integer id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
  • 测试方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.alibaba.fastjson.JSON;
import com.blog.web.pojo.User;

public class FastjsonDemo {
public static void main(String[] args) {
//1. 将Java对象转为JSON字符串
User user = new User();
user.setId(1);
user.setUsername("zhangsan");
user.setPassword("asd123");
String jsonString = JSON.toJSONString(user);
System.out.println(jsonString);
//2. 将JSON字符串转为Java对象
User u = JSON.parseObject("{\"id\":1,\"password\":\"asd123\",\"username\":\"zhangsan\"}", User.class);
System.out.println(u);
}
}
  • 得到输出结果如下

{“id”:1,“password”:“asd123”,“username”:“zhangsan”}
User{id=1, username=‘zhangsan’, password=‘asd123’}