Spring+SpringMVC+Mybatis SSM框架详解

news2024/11/16 9:45:46

一、JDBC编程

1、JDBC 简介

JDBC其实就是 Java 官方提供的一套规范(接口),用于帮助开发人员快速实现不同关系型数据库的连接。

程序运行的时候,数据都是在内存中的。当程序终止的时候,通常都需要将数据保存到磁盘上,无论是保存到本地磁盘,还是通过网络保存到服务器上,最终都会将数据写入磁盘文件。

而如何定义数据的存储格式就是一个大问题。如果我们自己来定义存储格式,比如保存一个班级所有学生的成绩单:

名字成绩
Michael99
Bob85
Bart59
Lisa87

你可以用一个文本文件保存,一行保存一个学生,用,隔开:

Michael,99
Bob,85
Bart,59
Lisa,87

你还可以用JSON格式保存,也是文本文件:

[
    {"name":"Michael","score":99},
    {"name":"Bob","score":85},
    {"name":"Bart","score":59},
    {"name":"Lisa","score":87}
]

你还可以定义各种保存格式,但是问题来了:

存储和读取需要自己实现,JSON还是标准,自己定义的格式就各式各样了;

不能做快速查询,只有把数据全部读到内存中才能自己遍历,但有时候数据的大小远远超过了内存(比如蓝光电影,40GB的数据),根本无法全部读入内存。

为了便于程序保存和读取数据,而且,能直接通过条件快速查询到指定的数据,就出现了数据库(Database)这种专门用于集中存储和查询的软件。

数据库软件诞生的历史非常久远,早在1950年数据库就诞生了。经历了网状数据库,层次数据库,我们现在广泛使用的关系数据库是20世纪70年代基于关系模型的基础上诞生的。

关系模型有一套复杂的数学理论,但是从概念上是十分容易理解的。举个学校的例子:

假设某个XX省YY市ZZ县第一实验小学有3个年级,要表示出这3个年级,可以在Excel中用一个表格画出来:

每个年级又有若干个班级,要把所有班级表示出来,可以在Excel中再画一个表格: 

这两个表格有个映射关系,就是根据Grade_ID可以在班级表中查找到对应的所有班级: 

也就是Grade表的每一行对应Class表的多行,在关系数据库中,这种基于表(Table)的一对多的关系就是关系数据库的基础。

根据某个年级的ID就可以查找所有班级的行,这种查询语句在关系数据库中称为SQL语句,可以写成:

SELECT * FROM classes WHERE grade_id = '1';

结果也是一个表:

---------+----------+----------
grade_id | class_id | name
---------+----------+----------
1        | 11       | 一年级一班
---------+----------+----------
1        | 12       | 一年级二班
---------+----------+----------
1        | 13       | 一年级三班
---------+----------+----------

类似的,Class表的一行记录又可以关联到Student表的多行记录:

使用Java程序访问数据库时,Java代码并不是直接通过TCP连接去访问数据库,而是通过JDBC接口来访问,而JDBC接口则通过JDBC驱动来实现真正对数据库的访问。

例如,我们在Java代码中如果要访问MySQL,那么必须编写代码操作JDBC接口。注意到JDBC接口是Java标准库自带的,所以可以直接编译。而具体的JDBC驱动是由数据库厂商提供的,例如,MySQL的JDBC驱动由Oracle提供。因此,访问某个具体的数据库,我们只需要引入该厂商提供的JDBC驱动,就可以通过JDBC接口来访问,这样保证了Java程序编写的是一套数据库访问代码,却可以访问各种不同的数据库,因为他们都提供了标准的JDBC驱动:

从代码来看,Java标准库自带的JDBC接口其实就是定义了一组接口,而某个具体的JDBC驱动其实就是实现了这些接口的类: 

实际上,一个MySQL的JDBC的驱动就是一个jar包,它本身也是纯Java编写的。我们自己编写的代码只需要引用Java标准库提供的java.sql包下面的相关接口,由此再间接地通过MySQL驱动的jar包通过网络访问MySQL服务器,所有复杂的网络通讯都被封装到JDBC驱动中,因此,Java程序本身只需要引入一个MySQL驱动的jar包就可以正常访问MySQL服务器: 

使用JDBC的好处是:

  • 各数据库厂商使用相同的接口,Java代码不需要针对不同数据库分别开发;

  • Java程序编译期仅依赖java.sql包,不依赖具体数据库的jar包;

  • 可随时替换底层数据库,访问数据库的Java代码基本不变。

2、JDBC API

1. JDBC 快速入门

package com.demo;

import java.sql.*;

public class JdbcDemo {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        // 1. 导入jar包
        // 2. 注册驱动
        // Class.forName("com.mysql.jdbc.Driver");  // Mysql5 驱动
        Class.forName("com.mysql.cj.jdbc.Driver");  // Mysql8 驱动

        // 3. 获取连接对象
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/world?serverTimezone=UTC", "root", "admin");

        // 4. 获取执行对象
        Statement statement = connection.createStatement();

        // 5. 执行SQL语句,并接收结果
        String sql = "select * from city limit 5";
        ResultSet resultSet = statement.executeQuery(sql);

        // 6. 处理结果
        while(resultSet.next()) {
            System.out.println(resultSet.getString("Name") + "\t" + resultSet.getString("Population"));
        }

        // 7. 释放资源
        statement.close();
        connection.close();
    }
}

2. DriverManager(驱动管理对象)

1)注册驱动(告诉程序该使用哪一个数据库驱动)

  • 注册给定的驱动程序:static void registerDriver(Driver driver);
  • 写代码时使用:Class.forName("com.mysql.jdbc.Driver");
  • 通过查看源码发现:在 com.mysql.jdbc.Driver 类中存在以下静态代码块
static {
    try {
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}
  • 我们不需要通过 DriverManager 调用静态方法 registerDriver,因为只要 Driver 类被使用,则会执行其静态代码块完成注册驱动。
  • Mysql5 之后可以省略注册驱动的步骤。因为在驱动 jar 包中,存在一个 javasql.Driver 配置文件,文件中指定了 com.mysqljdbc.Driver。

2)获取数据库连接对象

static Connection getConnection(String url, String user, String password);
  • url:指定连接的路径。语法:jdbc:mysql://ip地址(域名):端口号/库名
  • user:数据库用户名
  • password:数据库密码
  • 返回值 Connection:数据库连接对象

3. Connection(数据库连接对象)

  • 获取执行对象
    • 获取普通执行对象:Statement createStatement();
    • 获取预编译执行对象:PreparedStatement prepareStatement(String sql);
  • 管理事务
    • 开启事务:setAutoCommit(boolean autoCommit); // 参数为 false,则开启事务
    • 提交事务:commit();
    • 回滚事务:rollback();
  • 释放资源
    • 立即释放连接对象:void close();

4. Statement(SQL 执行对象)

  • 执行 DML 语句:int executeUpdate(String sql);

    • 返回值 int:返回影响的行数。
    • 参数 sql:可以执行 insert、update、delete 语句
  • 执行 DQL 语句:ResultSet executeQuery(String sql);

    • 返回值 ResultSet:封装查询的结果。
    • 参数 sql:可以执行select语句。
  • 释放资源

    • 立即释放执行对象:void close();

5. ResultSet(结果集对象)

  • 判断结果集中是否还有数据:boolean next();
    • 有数据则返回 true,并将索引向下移动一行
    • 没有数据则返回 false
  • 获取结果集中的数据:XXX getXxx("列名");
    • XXX 代表要获取的某列数据的类型
    • 例如:String getString("name");int getInt("age");
  • 释放资源
    • 立即释放结果集对象:void close();

6. PreparedStatement 预编译

SQL 注入:

SQL 注入演示:在登录界面,输入一个错误的用户名或密码,也可以登录成功。

SQL 注入的原理:

  • 按照正常道理来说,我们在密码处输入的所有内容,都应该作为密码(这个参数)。
  • 但是现在 Statement 对象在执行 SQL 语句时,将输入的内容当做查询条件来执行了。

PreparedStatement 即预编译 SQL 语句的执行对象,是 SQL 注入的防御手段之一。其原理是:在执行 SQL 语句之前,将 SQL 语句进行提前编译,在明确 SQL 语句的格式(执行计划)后,就不会改变了。因此剩余的内容都会认为是参数。

参数使用?作为占位符:

  • 为参数赋值的方法:setXxx(参数 1, 参数 2);
    • 参数 1:? 的位置编号(编号从 1 开始)
    • 参数 2:? 的实际参数
  • 执行 SQL 语句的方法
    • 执行 insert、update、delete 语句:int executeUpdate();
    • 执行 select 语句:ResultSet executeQuery();

3、JDBC CRUD

1. JDBC查询

前面我们讲了Java程序要通过JDBC接口来查询数据库。JDBC是一套接口规范,它在哪呢?就在Java的标准库java.sql里放着,不过这里面大部分都是接口。接口并不能直接实例化,而是必须实例化对应的实现类,然后通过接口引用这个实例。那么问题来了:JDBC接口的实现类在哪?

因为JDBC接口并不知道我们要使用哪个数据库,所以,用哪个数据库,我们就去使用哪个数据库的“实现类”,我们把某个数据库实现了JDBC接口的jar包称为JDBC驱动。

因为我们选择了MySQL 5.x作为数据库,所以我们首先得找一个MySQL的JDBC驱动。所谓JDBC驱动,其实就是一个第三方jar包,我们直接添加一个Maven依赖就可以了:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
    <scope>runtime</scope>
</dependency>

注意到这里添加依赖的scope是runtime,因为编译Java程序并不需要MySQL的这个jar包,只有在运行期才需要使用。如果把runtime改成compile,虽然也能正常编译,但是在IDE里写程序的时候,会多出来一大堆类似com.mysql.jdbc.Connection这样的类,非常容易与Java标准库的JDBC接口混淆,所以坚决不要设置为compile。

有了驱动,我们还要确保MySQL在本机正常运行,并且还需要准备一点数据。这里我们用一个脚本创建数据库和表,然后插入一些数据:

-- 创建数据库learjdbc:
DROP DATABASE IF EXISTS learnjdbc;
CREATE DATABASE learnjdbc;

-- 创建登录用户learn/口令learnpassword
CREATE USER IF NOT EXISTS learn@'%' IDENTIFIED BY 'learnpassword';
GRANT ALL PRIVILEGES ON learnjdbc.* TO learn@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;

-- 创建表students:
USE learnjdbc;
CREATE TABLE students (
  id BIGINT AUTO_INCREMENT NOT NULL,
  name VARCHAR(50) NOT NULL,
  gender TINYINT(1) NOT NULL,
  grade INT NOT NULL,
  score INT NOT NULL,
  PRIMARY KEY(id)
) Engine=INNODB DEFAULT CHARSET=UTF8;

-- 插入初始数据:
INSERT INTO students (name, gender, grade, score) VALUES ('小明', 1, 1, 88);
INSERT INTO students (name, gender, grade, score) VALUES ('小红', 1, 1, 95);
INSERT INTO students (name, gender, grade, score) VALUES ('小军', 0, 1, 93);
INSERT INTO students (name, gender, grade, score) VALUES ('小白', 0, 1, 100);
INSERT INTO students (name, gender, grade, score) VALUES ('小牛', 1, 2, 96);
INSERT INTO students (name, gender, grade, score) VALUES ('小兵', 1, 2, 99);
INSERT INTO students (name, gender, grade, score) VALUES ('小强', 0, 2, 86);
INSERT INTO students (name, gender, grade, score) VALUES ('小乔', 0, 2, 79);
INSERT INTO students (name, gender, grade, score) VALUES ('小青', 1, 3, 85);
INSERT INTO students (name, gender, grade, score) VALUES ('小王', 1, 3, 90);
INSERT INTO students (name, gender, grade, score) VALUES ('小林', 0, 3, 91);
INSERT INTO students (name, gender, grade, score) VALUES ('小贝', 0, 3, 77);

在控制台输入mysql -u root -p,输入root口令后以root身份,把上述SQL贴到控制台执行一遍就行。如果你运行的是最新版MySQL 8.x,需要调整一下CREATE USER语句。

1)JDBC连接

使用JDBC时,我们先了解什么是Connection。Connection代表一个JDBC连接,它相当于Java程序到数据库的连接(通常是TCP连接)。打开一个Connection时,需要准备URL、用户名和口令,才能成功连接到数据库。

URL是由数据库厂商指定的格式,例如,MySQL的URL是:

jdbc:mysql://<hostname>:<port>/<db>?key1=value1&key2=value2

假设数据库运行在本机localhost,端口使用标准的3306,数据库名称是learnjdbc,那么URL如下:

jdbc:mysql://localhost:3306/learnjdbc?useSSL=false&characterEncoding=utf8

后面的两个参数表示不使用SSL加密,使用UTF-8作为字符编码(注意MySQL的UTF-8是utf8)。

要获取数据库连接,使用如下代码:

// JDBC连接的URL, 不同数据库有不同的格式:
String JDBC_URL = "jdbc:mysql://localhost:3306/test";
String JDBC_USER = "root";
String JDBC_PASSWORD = "password";
// 获取连接:
Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
// TODO: 访问数据库...
// 关闭连接:
conn.close();

核心代码是DriverManager提供的静态方法getConnection()。DriverManager会自动扫描classpath,找到所有的JDBC驱动,然后根据我们传入的URL自动挑选一个合适的驱动。

因为JDBC连接是一种昂贵的资源,所以使用后要及时释放。使用try (resource)来自动释放JDBC连接是一个好方法:

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
    ...
}

2)JDBC查询

获取到JDBC连接后,下一步我们就可以查询数据库了。查询数据库分以下几步:

第一步,通过Connection提供的createStatement()方法创建一个Statement对象,用于执行一个查询;

第二步,执行Statement对象提供的executeQuery("SELECT * FROM students")并传入SQL语句,执行查询并获得返回的结果集,使用ResultSet来引用这个结果集;

第三步,反复调用ResultSet的next()方法并读取每一行结果。

完整查询代码如下:

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
    try (Statement stmt = conn.createStatement()) {
        try (ResultSet rs = stmt.executeQuery("SELECT id, grade, name, gender FROM students WHERE gender=1")) {
            while (rs.next()) {
                long id = rs.getLong(1); // 注意:索引从1开始
                long grade = rs.getLong(2);
                String name = rs.getString(3);
                int gender = rs.getInt(4);
            }
        }
    }
}

注意要点:

Statment和ResultSet都是需要关闭的资源,因此嵌套使用try (resource)确保及时关闭;

rs.next()用于判断是否有下一行记录,如果有,将自动把当前行移动到下一行(一开始获得ResultSet时当前行不是第一行);

ResultSet获取列时,索引从1开始而不是0;

必须根据SELECT的列的对应位置来调用getLong(1),getString(2)这些方法,否则对应位置的数据类型不对,将报错。

3)SQL注入

使用Statement拼字符串非常容易引发SQL注入的问题,这是因为SQL参数往往是从方法参数传入的。

我们来看一个例子:假设用户登录的验证方法如下:

User login(String name, String pass) {
    ...
    stmt.executeQuery("SELECT * FROM user WHERE login='" + name + "' AND pass='" + pass + "'");
    ...
}

其中,参数name和pass通常都是Web页面输入后由程序接收到的。

如果用户的输入是程序期待的值,就可以拼出正确的SQL。例如:name = "bob",pass = "1234":

SELECT * FROM user WHERE login='bob' AND pass='1234'

但是,如果用户的输入是一个精心构造的字符串,就可以拼出意想不到的SQL,这个SQL也是正确的,但它查询的条件不是程序设计的意图。例如:name = "bob' OR pass=", pass = " OR pass='":

SELECT * FROM user WHERE login='bob' OR pass=' AND pass=' OR pass=''

这个SQL语句执行的时候,根本不用判断口令是否正确,这样一来,登录就形同虚设。

要避免SQL注入攻击,一个办法是针对所有字符串参数进行转义,但是转义很麻烦,而且需要在任何使用SQL的地方增加转义代码。

还有一个办法就是使用PreparedStatement。使用PreparedStatement可以完全避免SQL注入的问题,因为PreparedStatement始终使用?作为占位符,并且把数据连同SQL本身传给数据库,这样可以保证每次传给数据库的SQL语句是相同的,只是占位符的数据不同,还能高效利用数据库本身对查询的缓存。上述登录SQL如果用PreparedStatement可以改写如下:

User login(String name, String pass) {
    ...
    String sql = "SELECT * FROM user WHERE login=? AND pass=?";
    PreparedStatement ps = conn.prepareStatement(sql);
    ps.setObject(1, name);
    ps.setObject(2, pass);
    ...
}

所以,PreparedStatement比Statement更安全,而且更快。

使用Java对数据库进行操作时,必须使用PreparedStatement,严禁任何通过参数拼字符串的代码!

我们把上面使用Statement的代码改为使用PreparedStatement:

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
    try (PreparedStatement ps = conn.prepareStatement("SELECT id, grade, name, gender FROM students WHERE gender=? AND grade=?")) {
        ps.setObject(1, "M"); // 注意:索引从1开始
        ps.setObject(2, 3);
        try (ResultSet rs = ps.executeQuery()) {
            while (rs.next()) {
                long id = rs.getLong("id");
                long grade = rs.getLong("grade");
                String name = rs.getString("name");
                String gender = rs.getString("gender");
            }
        }
    }
}

使用PreparedStatement和Statement稍有不同,必须首先调用setObject()设置每个占位符?的值,最后获取的仍然是ResultSet对象。

另外注意到从结果集读取列时,使用String类型的列名比索引要易读,而且不易出错。

注意到JDBC查询的返回值总是ResultSet,即使我们写这样的聚合查询SELECT SUM(score) FROM ...,也需要按结果集读取:

ResultSet rs = ...
if (rs.next()) {
    double sum = rs.getDouble(1);
}

4)数据类型

有的童鞋可能注意到了,使用JDBC的时候,我们需要在Java数据类型和SQL数据类型之间进行转换。JDBC在java.sql.Types定义了一组常量来表示如何映射SQL数据类型,但是平时我们使用的类型通常也就以下几种:

SQL数据类型Java数据类型
BIT, BOOLboolean
INTEGERint
BIGINTlong
REALfloat
FLOAT, DOUBLEdouble
CHAR, VARCHARString
DECIMALBigDecimal
DATEjava.sql.Date, LocalDate
TIMEjava.sql.Time, LocalTime

注意:只有最新的JDBC驱动才支持LocalDate和LocalTime。

总结:

JDBC接口的Connection代表一个JDBC连接;

使用JDBC查询时,总是使用PreparedStatement进行查询而不是Statement;

查询结果总是ResultSet,即使使用聚合查询也不例外。

2. JDBC更新

数据库操作总结起来就四个字:增删改查,行话叫CRUD:Create,Retrieve,Update和Delete。

查就是查询,我们已经讲过了,就是使用PreparedStatement进行各种SELECT,然后处理结果集。现在我们来看看如何使用JDBC进行增删改。

1)插入

插入操作是INSERT,即插入一条新记录。通过JDBC进行插入,本质上也是用PreparedStatement执行一条SQL语句,不过最后执行的不是executeQuery(),而是executeUpdate()。示例代码如下:

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
    try (PreparedStatement ps = conn.prepareStatement(
            "INSERT INTO students (id, grade, name, gender) VALUES (?,?,?,?)")) {
        ps.setObject(1, 999); // 注意:索引从1开始
        ps.setObject(2, 1); // grade
        ps.setObject(3, "Bob"); // name
        ps.setObject(4, "M"); // gender
        int n = ps.executeUpdate(); // 1
    }
}

设置参数与查询是一样的,有几个?占位符就必须设置对应的参数。虽然Statement也可以执行插入操作,但我们仍然要严格遵循绝不能手动拼SQL字符串的原则,以避免安全漏洞。

当成功执行executeUpdate()后,返回值是int,表示插入的记录数量。此处总是1,因为只插入了一条记录。

2)插入并获取主键

如果数据库的表设置了自增主键,那么在执行INSERT语句时,并不需要指定主键,数据库会自动分配主键。对于使用自增主键的程序,有个额外的步骤,就是如何获取插入后的自增主键的值。

要获取自增主键,不能先插入,再查询。因为两条SQL执行期间可能有别的程序也插入了同一个表。获取自增主键的正确写法是在创建PreparedStatement的时候,指定一个RETURN_GENERATED_KEYS标志位,表示JDBC驱动必须返回插入的自增主键。示例代码如下:

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
    try (PreparedStatement ps = conn.prepareStatement(
            "INSERT INTO students (grade, name, gender) VALUES (?,?,?)",
            Statement.RETURN_GENERATED_KEYS)) {
        ps.setObject(1, 1); // grade
        ps.setObject(2, "Bob"); // name
        ps.setObject(3, "M"); // gender
        int n = ps.executeUpdate(); // 1
        try (ResultSet rs = ps.getGeneratedKeys()) {
            if (rs.next()) {
                long id = rs.getLong(1); // 注意:索引从1开始
            }
        }
    }
}

观察上述代码,有两点注意事项:

一是调用prepareStatement()时,第二个参数必须传入常量Statement.RETURN_GENERATED_KEYS,否则JDBC驱动不会返回自增主键;

二是执行executeUpdate()方法后,必须调用getGeneratedKeys()获取一个ResultSet对象,这个对象包含了数据库自动生成的主键的值,读取该对象的每一行来获取自增主键的值。如果一次插入多条记录,那么这个ResultSet对象就会有多行返回值。如果插入时有多列自增,那么ResultSet对象的每一行都会对应多个自增值(自增列不一定必须是主键)。

3)更新

更新操作是UPDATE语句,它可以一次更新若干列的记录。更新操作和插入操作在JDBC代码的层面上实际上没有区别,除了SQL语句不同:

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
    try (PreparedStatement ps = conn.prepareStatement("UPDATE students SET name=? WHERE id=?")) {
        ps.setObject(1, "Bob"); // 注意:索引从1开始
        ps.setObject(2, 999);
        int n = ps.executeUpdate(); // 返回更新的行数
    }
}

executeUpdate()返回数据库实际更新的行数。返回结果可能是正数,也可能是0(表示没有任何记录更新)。

4)删除

删除操作是DELETE语句,它可以一次删除若干列。和更新一样,除了SQL语句不同外,JDBC代码都是相同的:

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
    try (PreparedStatement ps = conn.prepareStatement("DELETE FROM students WHERE id=?")) {
        ps.setObject(1, 999); // 注意:索引从1开始
        int n = ps.executeUpdate(); // 删除的行数
    }
}

总结:

使用JDBC执行INSERT、UPDATE和DELETE都可视为更新操作;

更新操作使用PreparedStatement的executeUpdate()进行,返回受影响的行数。

3. JDBC事务

数据库事务(Transaction)是由若干个SQL语句构成的一个操作序列,有点类似于Java的synchronized同步。数据库系统保证在一个事务中的所有SQL要么全部执行成功,要么全部不执行,即数据库事务具有ACID特性:

  • Atomicity:原子性
  • Consistency:一致性
  • Isolation:隔离性
  • Durability:持久性

数据库事务可以并发执行,而数据库系统从效率考虑,对事务定义了不同的隔离级别。SQL标准定义了4种隔离级别,分别对应可能出现的数据不一致的情况:

Isolation Level脏读(Dirty Read)不可重复读(Non Repeatable Read)幻读(Phantom Read)
Read UncommittedYesYesYes
Read Committed-YesYes
Repeatable Read--Yes
Serializable---

对应用程序来说,数据库事务非常重要,很多运行着关键任务的应用程序,都必须依赖数据库事务保证程序的结果正常。

举个例子:假设小明准备给小红支付100,两人在数据库中的记录主键分别是123和456,那么用两条SQL语句操作如下:

UPDATE accounts SET balance = balance - 100 WHERE id = 123 AND balance >= 100;
UPDATE accounts SET balance = balance + 100 WHERE id = 456;

这两条语句必须以事务方式执行才能保证业务的正确性,因为一旦第一条SQL执行成功而第二条SQL失败的话,系统的钱就会凭空减少100,而有了事务,要么这笔转账成功,要么转账失败,双方账户的钱都不变。

要在JDBC中执行事务,本质上就是如何把多条SQL包裹在一个数据库事务中执行。我们来看JDBC的事务代码:

Connection conn = openConnection();
try {
    // 关闭自动提交:
    conn.setAutoCommit(false);
    // 执行多条SQL语句:
    insert(); update(); delete();
    // 提交事务:
    conn.commit();
} catch (SQLException e) {
    // 回滚事务:
    conn.rollback();
} finally {
    conn.setAutoCommit(true);
    conn.close();
}

其中,开启事务的关键代码是conn.setAutoCommit(false),表示关闭自动提交。提交事务的代码在执行完指定的若干条SQL语句后,调用conn.commit()。要注意事务不是总能成功,如果事务提交失败,会抛出SQL异常(也可能在执行SQL语句的时候就抛出了),此时我们必须捕获并调用conn.rollback()回滚事务。最后,在finally中通过conn.setAutoCommit(true)把Connection对象的状态恢复到初始值。

实际上,默认情况下,我们获取到Connection连接后,总是处于“自动提交”模式,也就是每执行一条SQL都是作为事务自动执行的,这也是为什么前面几节我们的更新操作总能成功的原因:因为默认有这种“隐式事务”。只要关闭了Connection的autoCommit,那么就可以在一个事务中执行多条语句,事务以commit()方法结束。

如果要设定事务的隔离级别,可以使用如下代码:

// 设定隔离级别为READ COMMITTED:
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);

如果没有调用上述方法,那么会使用数据库的默认隔离级别。MySQL的默认隔离级别是REPEATABLE_READ。

数据库事务(Transaction)具有ACID特性:

  • Atomicity:原子性
  • Consistency:一致性
  • Isolation:隔离性
  • Durability:持久性

JDBC提供了事务的支持,使用Connection可以开启、提交或回滚事务。

4. JDBC Batch

使用JDBC操作数据库的时候,经常会执行一些批量操作。

例如,一次性给会员增加可用优惠券若干,我们可以执行以下SQL代码:

INSERT INTO coupons (user_id, type, expires) VALUES (123, 'DISCOUNT', '2030-12-31');
INSERT INTO coupons (user_id, type, expires) VALUES (234, 'DISCOUNT', '2030-12-31');
INSERT INTO coupons (user_id, type, expires) VALUES (345, 'DISCOUNT', '2030-12-31');
INSERT INTO coupons (user_id, type, expires) VALUES (456, 'DISCOUNT', '2030-12-31');
...

实际上执行JDBC时,因为只有占位符参数不同,所以SQL实际上是一样的:

for (var params : paramsList) {
    PreparedStatement ps = conn.preparedStatement("INSERT INTO coupons (user_id, type, expires) VALUES (?,?,?)");
    ps.setLong(params.get(0));
    ps.setString(params.get(1));
    ps.setString(params.get(2));
    ps.executeUpdate();
}

类似的还有,给每个员工薪水增加10%~30%:

UPDATE employees SET salary = salary * ? WHERE id = ?

通过一个循环来执行每个PreparedStatement虽然可行,但是性能很低。SQL数据库对SQL语句相同,但只有参数不同的若干语句可以作为batch执行,即批量执行,这种操作有特别优化,速度远远快于循环执行每个SQL。

在JDBC代码中,我们可以利用SQL数据库的这一特性,把同一个SQL但参数不同的若干次操作合并为一个batch执行。我们以批量插入为例,示例代码如下:

try (PreparedStatement ps = conn.prepareStatement("INSERT INTO students (name, gender, grade, score) VALUES (?, ?, ?, ?)")) {
    // 对同一个PreparedStatement反复设置参数并调用addBatch():
    for (Student s : students) {
        ps.setString(1, s.name);
        ps.setBoolean(2, s.gender);
        ps.setInt(3, s.grade);
        ps.setInt(4, s.score);
        ps.addBatch(); // 添加到batch
    }
    // 执行batch:
    int[] ns = ps.executeBatch();
    for (int n : ns) {
        System.out.println(n + " inserted."); // batch中每个SQL执行的结果数量
    }
}

执行batch和执行一个SQL不同点在于,需要对同一个PreparedStatement反复设置参数并调用addBatch(),这样就相当于给一个SQL加上了多组参数,相当于变成了“多行”SQL。

第二个不同点是调用的不是executeUpdate(),而是executeBatch(),因为我们设置了多组参数,相应地,返回结果也是多个int值,因此返回类型是int[],循环int[]数组即可获取每组参数执行后影响的结果数量。

总结:

使用JDBC的batch操作会大大提高执行效率,对内容相同,参数不同的SQL,要优先考虑batch操作。

4、JDBC 案例

1. student 表

-- 创建student表
CREATE TABLE student(
    sid INT PRIMARY KEY AUTO_INCREMENT,  -- 学生id
    NAME VARCHAR(20),  -- 学生姓名
    age INT,  -- 学生年龄
    birthday DATE  -- 学生生日
);

-- 添加数据
INSERT INTO student VALUES (NULL,'张三',23,'1999-09-23'), (NULL,'李四',24,'1998-08-10'), (NULL,'王五',25,'1996-06-06'), (NULL,'赵六',26,'1994-10-20');

2. JDBC 配置信息

config.properties:

driverClass=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/world?serverTimezone=UTC
username=root
password=admin

3. domain 实体类

Student 实体类:

package com.bean;

import java.util.Date;

public class Student {

    private Integer sid;
    private String name;
    private Integer age;
    private Date birthday;

    public Student() {
    }

    public Student(Integer sid, String name, Integer age, Date birthday) {
        this.sid = sid;
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }

    public Integer getSid() {
        return sid;
    }

    public void setSid(Integer sid) {
        this.sid = sid;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    @Override
    public String toString() {
        return "student{" +
                "sid=" + sid +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", birthday=" + birthday +
                '}';
    }
}

4. JDBC 工具类

package com.utils;

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

// JDBC 工具类
public class JdbcUtil {

    // 私有构造方法
    private JdbcUtil(){}

    // 声明所需要的配置变量
    private static String driverClass;
    private static String url;
    private static String username;
    private static String password;
    private static Connection connection;

    // 静态代码块:读取配置文件的信息为变量赋值,注册驱动
    static {
        try{
            // 读取配置文件
            InputStream resourceAsStream = JdbcUtil.class.getClassLoader().getResourceAsStream("config.properties");
            Properties properties = new Properties();
            properties.load(resourceAsStream);
            // 赋值
            driverClass = properties.getProperty("driverClass");
            url = properties.getProperty("url");
            username = properties.getProperty("username");
            password = properties.getProperty("password");
            // 注册驱动
            Class.forName(driverClass);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 获取数据库连接对象
    public static Connection getConnection() {
        try {
            connection = DriverManager.getConnection(url, username, password);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return connection;
    }

    // 释放资源
    public static void close(Connection connection, Statement statement, ResultSet resultSet){
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    // 释放资源
    public static void close(Connection connection, Statement statement) {
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

5. Dao 层

StudentDao.java:

package com.dao;

import com.bean.Student;

import java.util.ArrayList;

// Dao层接口
public interface StudentDao {
    // 查询所有学生信息
    public abstract ArrayList<Student> findAll();

    // 根据id条件查询
    public abstract Student findById(Integer id);

    // 新增学生信息
    public abstract int insert(Student student);

    // 修改学生信息
    public abstract int update(Student student);

    // 根据id删除学生信息
    public abstract int delete(Integer id);
}

StudentDaoImpl.java:

package com.dao;

import com.bean.Student;
import com.utils.JdbcUtil;

import java.sql.*;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;


public class StudentDaoImpl implements StudentDao{

    // 查询所有学生信息
    @Override
    public ArrayList<Student> findAll() {
        ArrayList<Student> studentList = new ArrayList<>();
        Connection connection = null;
        Statement statement = null;
        ResultSet resultSet = null;
        try {
            connection = JdbcUtil.getConnection();
            String sql = "select * from student";
            PreparedStatement preparedStatement = connection.prepareStatement(sql);
            resultSet = preparedStatement.executeQuery();
            // 处理结果集
            while (resultSet.next()) {
                Integer sid = resultSet.getInt("sid");
                String name = resultSet.getString("name");
                Integer age = resultSet.getInt("age");
                Date birthday = resultSet.getDate("birthday");
                // 封装Student对象
                Student student = new Student(sid, name, age, birthday);
                // 将student对象保存到集合中
                studentList.add(student);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放资源
            JdbcUtil.close(connection, statement, resultSet);
        }
        // 返回集合对象
        return studentList;
    }

    // 条件查询,根据id查询学生信息
    @Override
    public Student findById(Integer id) {
        Student student = new Student();
        Connection connection = null;
        Statement statement = null;
        ResultSet resultSet = null;

        try {
            connection = JdbcUtil.getConnection();
            String sql = "select * from student where sid=?";
            PreparedStatement preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setInt(1, id);
            resultSet = preparedStatement.executeQuery();
            // 处理结果集
            while (resultSet.next()) {
                Integer sid = resultSet.getInt("sid");
                String name = resultSet.getString("name");
                Integer age = resultSet.getInt("age");
                Date birthday = resultSet.getDate("birthday");
                // 封装Student对象
                student.setSid(sid);
                student.setName(name);
                student.setAge(age);
                student.setBirthday(birthday);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JdbcUtil.close(connection, statement, resultSet);
        }
        return student;
    }

    // 添加学生信息
    @Override
    public int insert(Student student) {
        Connection connection = null;
        Statement statement = null;
        int result = 0;
        try {
            connection = JdbcUtil.getConnection();
            Date raw_birthday = student.getBirthday();
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
            String birthday = simpleDateFormat.format(raw_birthday);
            String sql = "insert into student (sid, name, age, birthday) values (?, ?, ?, ?)";
            PreparedStatement preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setInt(1, student.getSid());
            preparedStatement.setString(2, student.getName());
            preparedStatement.setInt(3, student.getAge());
            preparedStatement.setDate(4, (java.sql.Date) student.getBirthday());
            result = preparedStatement.executeUpdate();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JdbcUtil.close(connection, statement);
        }
        return result;
    }

    // 修改学生信息
    @Override
    public int update(Student student) {
        Connection connection = null;
        Statement statement = null;
        int result = 0;
        try {
            connection = JdbcUtil.getConnection();
            Date raw_birthday = student.getBirthday();
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
            String birthday = simpleDateFormat.format(raw_birthday);
            String sql = "UPDATE student SET name=?, age=?, birthday=? WHERE sid=?";
            PreparedStatement preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, student.getName());
            preparedStatement.setInt(2, student.getAge());
            preparedStatement.setDate(3, (java.sql.Date) student.getBirthday());
            result = preparedStatement.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JdbcUtil.close(connection, statement);
        }
        return result;
    }

    @Override
    public int delete(Integer id) {
        Connection connection = null;
        Statement statement = null;
        int result = 0;
        try {
            connection = JdbcUtil.getConnection();
            String sql = "delete from student where sid=?";
            PreparedStatement preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setInt(1, id);
            result = preparedStatement.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JdbcUtil.close(connection, statement);
        }
        return result;
    }
}

6. Service 层

StudentService.java:

package com.service;

import com.bean.Student;

import java.util.ArrayList;

// service 层接口
public interface StudentService {
    //查询所有学生信息
    public abstract ArrayList<Student> findAll();

    //条件查询,根据id获取学生信息
    public abstract Student findById(Integer id);

    //新增学生信息
    public abstract int insert(Student student);

    //修改学生信息
    public abstract int update(Student student);

    //删除学生信息
    public abstract int delete(Integer id);
}

StudentServiceImpl.java:

package com.service;

import com.bean.Student;
import com.dao.StudentDao;
import com.dao.StudentDaoImpl;

import java.util.ArrayList;

public class StudentServiceImpl implements StudentService {

    private StudentDao dao = new StudentDaoImpl();

    // 查询所有学生信息
    @Override
    public ArrayList<Student> findAll() {
        return dao.findAll();
    }

    // 根据id查询指定学生信息
    @Override
    public Student findById(Integer id) {
        return dao.findById(id);
    }

    // 添加学生信息
    @Override
    public int insert(Student student) {
        return dao.insert(student);
    }

    // 修改学生信息
    @Override
    public int update(Student student) {
        return dao.update(student);
    }

    // 删除学生信息
    @Override
    public int delete(Integer id) {
        return dao.delete(id);
    }
}

7. Controller 层

StudentController.java:

package com.controller;

import com.bean.Student;
import com.service.StudentService;
import com.service.StudentServiceImpl;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.Date;

public class StudentController {

    private StudentService studentService = new StudentServiceImpl();

    // 查询所有学生信息
    @Test
    public void findAll() {
        ArrayList<Student> studentList = studentService.findAll();
        for (Student student: studentList) {
            System.out.println(student);
        }
    }

    // 根据id查询指定学生信息
    @Test
    public void findById() {
        Student student = studentService.findById(3);
        System.out.println("查询成功: "+student);
    }

    // 添加学生信息
    @Test
    public void insert() {
        Student student = new Student(5, "陈七", 19, new Date());
        int result = studentService.insert(student);
        if (result != 0) {
            System.out.println("学生信息添加成功!");
        } else {
            System.out.println("学生信息添加失败!");
        }
    }

    // 修改学生信息
    @Test
    public void update() {
        Student student = studentService.findById(1);
        student.setName("xiaoji");
        int result = studentService.update(student);
        if (result != 0) {
            System.out.println("学生信息修改成功!");
        } else {
            System.out.println("学生信息修改失败!");
        }
    }

    // 删除学生信息
    @Test
    public void delete() {
        int result = studentService.delete(1);
        if (result != 0) {
            System.out.println("学生信息删除成功!");
        } else {
            System.out.println("学生信息删除失败!");
        }
    }
}

5、JDBC事务管理

事务一般在 service 层控制管理,因为事务一般与业务耦合,而不是与通用的 dao 层耦合。

  • Service 接口
import java.util.List;

public interface UserService {
    /**
     * 批量添加
     * @param users
     */
    void batchAdd(List<User> users);
}
  • ServiceImpl 实现类
@Override
public void batchAdd(List<User> users) {
    // 获取数据库连接
    Connection connection = JDBCUtils.getConnection();
    try {
        // 开启事务
        connection.setAutoCommit(false);
        for (User user : users) {
            // 1.创建ID,并把UUID中的-替换
            String uid = UUID.randomUUID().toString().replace("-", "").toUpperCase();
            // 2.给user的uid赋值
            user.setUid(uid);
            // 3.生成员工编号
            user.setUcode(uid);

            // 手动模拟异常
            //int n = 1 / 0;

            // 4.保存
            userDao.save(connection,user);
        }
        // 提交事务
        connection.commit();
    }catch (Exception e){
        try {
            // 若遇到异常,回滚事务
            connection.rollback();
        }catch (Exception ex){
            ex.printStackTrace();
        }
        e.printStackTrace();
    } finally {
        JDBCUtils.close(connection,null,null);
    }
}

6、JDBC 连接池

创建线程是一个昂贵的操作,如果有大量的小任务需要执行,并且频繁地创建和销毁线程,实际上会消耗大量的系统资源,往往创建和消耗线程所耗费的时间比执行任务的时间还长,所以,为了提高效率,可以用线程池。

类似的,在执行JDBC的增删改查的操作时,如果每一次操作都来一次打开连接,操作,关闭连接,那么创建和销毁JDBC连接的开销就太大了。为了避免频繁地创建和销毁JDBC连接,我们可以通过连接池(Connection Pool)复用已经创建好的连接。

数据库连接池是一种复用Connection的组件,它可以避免反复创建新连接,提高JDBC代码的运行效率。

数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个。这项技术能解决建立数据库连接耗费资源和时间的问题,明显提高对数据库操作的性能。

数据库连接池原理:

JDBC连接池有一个标准的接口javax.sql.DataSource,注意这个类位于Java标准库中,但仅仅是接口。要使用JDBC连接池,我们必须选择一个JDBC连接池的实现。

常用的JDBC连接池有:

  • HikariCP
  • C3P0
  • BoneCP
  • Druid

1. HikariCP

目前使用最广泛的是HikariCP。我们以HikariCP为例,要使用JDBC连接池,先添加HikariCP的依赖如下:

<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>2.7.1</version>
</dependency>

紧接着,我们需要创建一个DataSource实例,这个实例就是连接池:

HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/test");
config.setUsername("root");
config.setPassword("password");
config.addDataSourceProperty("connectionTimeout", "1000"); // 连接超时:1秒
config.addDataSourceProperty("idleTimeout", "60000"); // 空闲超时:60秒
config.addDataSourceProperty("maximumPoolSize", "10"); // 最大连接数:10
DataSource ds = new HikariDataSource(config);

注意创建DataSource也是一个非常昂贵的操作,所以通常DataSource实例总是作为一个全局变量存储,并贯穿整个应用程序的生命周期。

有了连接池以后,我们如何使用它呢?和前面的代码类似,只是获取Connection时,把DriverManage.getConnection()改为ds.getConnection():

try (Connection conn = ds.getConnection()) { // 在此获取连接
    ...
} // 在此“关闭”连接

通过连接池获取连接时,并不需要指定JDBC的相关URL、用户名、口令等信息,因为这些信息已经存储在连接池内部了(创建HikariDataSource时传入的HikariConfig持有这些信息)。一开始,连接池内部并没有连接,所以,第一次调用ds.getConnection(),会迫使连接池内部先创建一个Connection,再返回给客户端使用。当我们调用conn.close()方法时(在try(resource){...}结束处),不是真正“关闭”连接,而是释放到连接池中,以便下次获取连接时能直接返回。

因此,连接池内部维护了若干个Connection实例,如果调用ds.getConnection(),就选择一个空闲连接,并标记它为“正在使用”然后返回,如果对Connection调用close(),那么就把连接再次标记为“空闲”从而等待下次调用。这样一来,我们就通过连接池维护了少量连接,但可以频繁地执行大量的SQL语句。

通常连接池提供了大量的参数可以配置,例如,维护的最小、最大活动连接数,指定一个连接在空闲一段时间后自动关闭等,需要根据应用程序的负载合理地配置这些参数。此外,大多数连接池都提供了详细的实时状态以便进行监控。

2. C3P0

C3P0 是一个开源的 JDBC 连接池,使用它的开源项目有 Hibernate、Spring 等。

使用步骤:

  1. 导入 jar 包
  2. 导入配置文件(c3p0-config.xml,文件名不可改)到 src 目录下
  3. 创建 c3p0 连接池对象
  4. 获取数据库连接进行使用

使用示例:

  • c3p0-config.xml
<c3p0-config>
  <!-- 使用默认的配置读取连接池对象 -->
  <default-config>
  	<!--  连接参数 -->
    <property name="driverClass">com.mysql.jdbc.Driver</property>
    <property name="jdbcUrl">jdbc:mysql://localhost:3306/world</property>
    <property name="user">root</property>
    <property name="password">admin</property>
    
    <!-- 连接池参数 -->
    <!--初始化的连接数量-->
    <property name="initialPoolSize">5</property>
    <!--最大连接数量-->
    <property name="maxPoolSize">10</property>
    <!--超时时间-->
    <property name="checkoutTimeout">3000</property>
  </default-config>

  <!-- 自定义连接池对象 -->
  <named-config name="otherc3p0"> 
    <!-- 连接参数 -->
    <property name="driverClass">com.mysql.jdbc.Driver</property>
    <property name="jdbcUrl">jdbc:mysql://localhost:3306/world</property>
    <property name="user">root</property>
    <property name="password">admin</property>
    
    <!-- 连接池参数 -->
    <property name="initialPoolSize">5</property>
    <property name="maxPoolSize">8</property>
    <property name="checkoutTimeout">1000</property>
  </named-config>
</c3p0-config>
  • 测试类
import com.mchange.v2.c3p0.ComboPooledDataSource;

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

public class C3P0Test {

    public static void main(String[] args) throws SQLException {
        // 创建c3p0连接池对象
        DataSource dataSource = new ComboPooledDataSource();

        // 获取数据库连接进行使用
        Connection con = dataSource.getConnection();

        // 查询全部学生信息
        String sql = "SELECT * FROM student";
        PreparedStatement pst = con.prepareStatement(sql);
        ResultSet rs = pst.executeQuery();

        while(rs.next()) {
            System.out.println(rs.getInt("sid") + "\t" + rs.getString("name") + "\t" + rs.getInt("age") + "\t" + rs.getDate("birthday"));
        }

        // 释放资源
        rs.close();
        pst.close();
        con.close();  // 将连接对象归还池中
    }
}

* **优化:抽取工具类**
~~~java
package com.itheima.util;

import java.beans.PropertyVetoException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import javax.sql.DataSource;

import com.mchange.v2.c3p0.ComboPooledDataSource;

public class C3P0Util {
    // 得到一个数据源
    private static DataSource dataSource = new ComboPooledDataSource();
    
    
    public static DataSource getDataSource() {
        return dataSource;
    }

    //从数据源中得到一个连接对象
    public static Connection getConnection(){
        try {
            return dataSource.getConnection();
        } catch (SQLException e) {
            throw new RuntimeException("服务器错误");
        }
    }
    
    public static void close(Connection conn, Statement stmt, ResultSet rs){
        // 关闭资源
        if(rs!=null){
            try {
                rs.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            rs = null;
        }
        if(stmt!=null){
            try {
                stmt.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            stmt = null;
        }
        if(conn!=null){
            try {
                conn.close(); 
            } catch (Exception e) {
                e.printStackTrace();
            }
            conn = null;
        }
    }
}

3. Druid

数据库连接池有很多选择,C3P0、DHCP 等,阿里巴巴开源的 druid 作为一名后起之秀,凭借其出色的性能,也逐渐印入了大家的眼帘。

Druid 基本概念及架构介绍

使用步骤:

  1. 导入 jar 包
  2. 通过 Properties 集合加载配置文件
  3. 通过 Druid 连接池工厂类获取数据库连接池对象
  4. 获取数据库连接,进行使用

示例:

  • druid.properties
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/world
username=root
password=itheima
# 初始化连接数量
initialSize=5
# 最大连接数量
maxActive=10
# 超时时间
maxWait=3000
  • 测试类
import com.alibaba.druid.pool.DruidDataSourceFactory;

import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Properties;

public class DruidTest {
    public static void main(String[] args) throws Exception {
        // 通过Properties集合加载配置文件
        InputStream is = DruidTest.class.getClassLoader().getResourceAsStream("druid.properties");
        Properties prop = new Properties();
        prop.load(is);

        // 通过Druid连接池工厂类获取数据库连接池对象
        DataSource dataSource = DruidDataSourceFactory.createDataSource(prop);

        // 获取数据库连接,进行使用
        Connection con = dataSource.getConnection();

        // 查询全部学生信息
        String sql = "SELECT * FROM student";
        PreparedStatement pst = con.prepareStatement(sql);
        ResultSet rs = pst.executeQuery();

        while(rs.next()) {
            System.out.println(rs.getInt("sid") + "\t" + rs.getString("name") + "\t" + rs.getInt("age") + "\t" + rs.getDate("birthday"));
        }

        // 释放资源
        rs.close();
        pst.close();
        con.close();    // 将连接对象归还池中
    }
}
  • 优化:抽取工具类
public class DataSourceUtils {

    // 1.私有构造方法
    private DataSourceUtils(){}

    // 2.定义DataSource数据源变量
    private static DataSource dataSource;

    // 3.提供静态代码块,完成配置文件的加载和获取连接池对象
    static {
        try{
            // 加载配置文件
            InputStream is = DruidDemo1.class.getClassLoader().getResourceAsStream("druid.properties");
            Properties prop = new Properties();
            prop.load(is);

            // 获取数据库连接池对象
            dataSource = DruidDataSourceFactory.createDataSource(prop);

        } catch(Exception e) {
            e.printStackTrace();
        }
    }

    // 4.提供获取数据库连接的方法
    public static Connection getConnection() {
        Connection con = null;
        try {
            con = dataSource.getConnection();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return con;
    }

    // 5.提供获取数据库连接池的方法
    public static DataSource getDataSource() {
        return dataSource;
    }

    // 6.提供DQL释放资源的方法
    public static void close(Connection con, Statement stat, ResultSet rs) {
        if(con != null) {
            try {
                con.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        if(stat != null) {
            try {
                stat.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        if(rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    // 提供DML释放资源的方法
    public static void close(Connection con, Statement stat) {
        close(con, stat, null);
    }
}

7、DBUtils

1. DBUtils 介绍

什么是 DBUtils ?

DBUtils 是 Apache 开源的 Java 编程中的数据库操作实用工具,小巧简单实用。

DBUtils 封装了对 JDBC 的操作,简化了 JDBC 操作,可以少写代码。

  1. 对于数据表的读操作,他可以把结果转换成 List、Array、Set 等 Java 集合,便于程序员操作。
  2. 对于数据表的写操作,也变得很简单(只需写 SQL 语句)。
  3. 可以使用数据源,使用 JNDI,数据库连接池等技术来优化性能--重用已经构建好的数据库连接对象。

DBUtils 的三个核心对象:

  1. QueryRunner 类:提供对 SQL 语句操作的 API,它主要有三个方法:
    • `query():用于执行 select
    • update():用于执行 insert、update、delete
    • batch():批处理
  2. ResultSetHandler 接口:用于定义 select 操作后怎样封装结果集。
  3. DbUtils 类:一个工具类,定义了关闭资源与事务处理的方法。

2. 使用案例

DBUtils 使用步骤:

  1. 导入 jar 包
    • c3p0-0.9.1.2.jar(其他连接对象均可)
    • commons-logging-1.1.1.jar
    • commons-beanutils-1.8.3.jar
    • commons-dbutils-1.4.jar
  2. 创建 QueryRunner 对象
  3. 使用 query 方法执行 select 语句
  4. 使用 ResultSetHandler 封装结果集
  5. 使用 DbUtils 类释放资源

3. QueryRunner 类(执行对象)

构造函数:

  • new QueryRunner();
    • 其事务可以手动控制。
    • 此对象调用的方法(如 query、update、batrch)参数中要有 Connection 对象。
  • new QueryRunner(DataSource ds);
    • 其事务是自动控制的(一个 SQL 一个事务)。
    • 此对象调用的方法(如 query、update、batrch)参数中无需 Connection 对象。

示例:

  • C3P0Util.java
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import javax.sql.DataSource;

import com.mchange.v2.c3p0.ComboPooledDataSource;

public class C3P0Util {
    // 得到一个数据源
    private static DataSource dataSource = new ComboPooledDataSource();


    public static DataSource getDataSource() {
        return dataSource;
    }

    //从数据源中得到一个连接对象
    public static Connection getConnection(){
        try {
            return dataSource.getConnection();
        } catch (SQLException e) {
            throw new RuntimeException("服务器错误");
        }
    }

    public static void close(Connection conn, Statement stmt, ResultSet rs){
        // 关闭资源
        if(rs!=null){
            try {
                rs.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            rs = null;
        }
        if(stmt!=null){
            try {
                stmt.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            stmt = null;
        }
        if(conn!=null){
            try {
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            conn = null;
        }
    }

}
  • DBUtil
import com.bean.Student;
import org.apache.commons.dbutils.QueryRunner;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.apache.commons.dbutils.ResultSetHandler;
import org.junit.jupiter.api.Test;
import org.apache.commons.dbutils.handlers.BeanListHandler;

public class DBUtil {

    @Test
    public void testDQL1() throws SQLException{
        //创建一个QueryRunner对象
        QueryRunner qr = new QueryRunner(C3P0Util.getDataSource());
        //执行查询语句,并返回结果
        List<Student> list = qr.query("select * from student where sid=? and name=?", new BeanListHandler<Student>(Student.class), 3, "王五");
        for (Student student : list) {
            System.out.println(student);
        }
    }

    @Test
    public void testDQL2() throws SQLException{
        // 创建一个QueryRunner对象
        QueryRunner qr = new QueryRunner(C3P0Util.getDataSource());
        List<Student> list = qr.query("select * from student", new ResultSetHandler<List<Student>>(){
            // 当query方法执行完select语句,就会将结果集以参数的形式传递进来
            public List<Student> handle(ResultSet rs) throws SQLException {
                List<Student> list  = new ArrayList<Student>();
                while(rs.next()){
                    Student student = new Student();
                    student.setSid(rs.getInt(1));
                    student.setName(rs.getString(2));
                    student.setAge(rs.getInt(3));
                    student.setBirthday(rs.getDate(4));
                    list.add(student);
                }
                return list;
            }
        });
        for (Student student : list) {
            System.out.println(student);
        }
    }

    @Test
    public void testDML() throws SQLException{
        //创建一个QueryRunner对象
        QueryRunner qr = new QueryRunner(C3P0Util.getDataSource());
        // 返回影响行数
        qr.update("insert into student (sid,name,age,birthday) values(?,?,?,?)", "6", "王八", "4", new Date());
    }

    @Test
    public void testBatchDQL() throws SQLException{
        //创建一个QueryRunner对象
        QueryRunner qr = new QueryRunner(C3P0Util.getDataSource());
        Object[][] params = new Object[10][];  // 高维代表执行次数;低维代表
        for (int i=0; i<10; i++) {
            params[i] = new Object[]{10+i, "菜"+i, 10+i, new Date()};
        }
        // 返回影响行数
        qr.batch("insert into student (sid,name,age,birthday) values(?,?,?,?)", params);
    }

}

4. ResultSetHandler 接口(结果集对象)

ResultSetHandler 下的所有结果处理器:

对象名说明
ArrayHandler适合取 1 条记录。
把该条记录的每列值封装到一个 Object[] 中
ArrayListHandler适合取多条记录。
把每条记录的每列值封装到一个 Object[] 中,再把数组封装到一个 List 中
ColumnListHandler取某一列的数据。
把该条记录的每列值封装到 List 中
KeyedHandler取多条记录。
每一条记录封装到一个 Map 中,再把这个 Map 封装到另外一个 Map 中,key 为指定的字段值
MapHandler适合取1条记录。
把当前记录的列名和列值放到一个 Map 中
MapListHandler适合取多条记录。
把每条记录封装到一个 Map 中,再把 Map 封装到 List 中
ScalarHandler适合取单行单列数据
BeanHandler取第一行数据
BeanListHandler将每个数据封装到 List 集合中

示例:

import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.ArrayHandler;
import org.apache.commons.dbutils.handlers.ArrayListHandler;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ColumnListHandler;
import org.apache.commons.dbutils.handlers.KeyedHandler;
import org.apache.commons.dbutils.handlers.MapHandler;
import org.apache.commons.dbutils.handlers.MapListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;

import org.junit.Test;

public class ResultSetHandler {
     //ArrayHandler:适合取1条记录。把该条记录的每列值封装到一个数组中Object[]
     @Test
     public void test1() throws SQLException {
          QueryRunner qr = new QueryRunner(c3p0.getDataSource());
          Object[] arr = qr.query("select * from users where id =?",new ArrayHandler(),5);
          for (Object o : arr) {
              System.out.println(o);
          }
     }

     //ArrayListHandler:适合取多条记录。把每条记录的每列值封装到一个数组中Object[],把数组封装到一个List中
     @Test
     public void test2() throws SQLException {
          QueryRunner qr = new QueryRunner(c3p0.getDataSource());
          List<Object[]> list = qr.query("select * from users",new ArrayListHandler());
          for (Object[] o : list) {
              for(Object os : o){
                   System.out.println(os);
              }
              System.out.println("---------");
          }
     }
     
     //ColumnListHandler:取某一列的数据,封装到List中。
     @Test
     public void test3() throws SQLException {
          QueryRunner qr = new QueryRunner(c3p0.getDataSource());
          //参数的列数是指select语句中列的数
          List<Object> list = qr.query("select * from users",new ColumnListHandler(3));
          for (Object o : list) {
              System.out.println(o);
          }
     }
     
     //KeyedHandler:取多条记录,每一条记录封装到一个Map中,再把这个Map封装到另外一个Map中,Key为指定的字段值。
     @Test
     public void test4() throws SQLException {
          QueryRunner qr = new QueryRunner(c3p0.getDataSource());
          //参数指大Map的Key,是表中的某列数据,若不重复,则作为输出的记录数量。
          //小Map中的Key是列名
          Map<Object,Map<String,Object>> map = qr.query("select * from users",new KeyedHandler(1));
          for (Map.Entry<Object, Map<String, Object>> m : map.entrySet()) {
              System.out.println(m.getKey());
              for(Map.Entry<String, Object> mm : m.getValue().entrySet()){
                   System.out.println(mm.getKey()+":"+mm.getValue());
              }
              System.out.println("---------");
          }
     }
     
     //MapHandler:适合取1条记录。把当前记录的列名和列值放到一个Map中
     @Test
     public void test5() throws SQLException {
          QueryRunner qr = new QueryRunner(c3p0.getDataSource());
          Map<String, Object> map = qr.query("select * from users where id=?",new MapHandler(),6);
          for (Map.Entry<String, Object> m : map.entrySet()) {
               System.out.println(m.getKey()+":"+m.getValue());
          }
          System.out.println("---------");
     }
     
     //MapListHandler:适合取多条记录。把每条记录封装到一个Map中,再把Map封装到List中
     @Test
     public void test6() throws SQLException {
          QueryRunner qr = new QueryRunner(c3p0.getDataSource());
          List<Map<String, Object>> list = qr.query("select * from users",new MapListHandler());
          for(Map<String, Object> map : list){
              for (Map.Entry<String, Object> m : map.entrySet()) {
                   System.out.println(m.getKey()+":"+m.getValue());
              }
              System.out.println("---------");
          }
     }
     
     //ScalarHandler:适合取单行单列数据
     @Test
     public void test7() throws SQLException {
          QueryRunner qr = new QueryRunner(c3p0.getDataSource());
          Object o = qr.query("select username from users",new ScalarHandler(1)); //username列第1个
          Object o2 = qr.query("select * from users",new ScalarHandler(2)); //第1行第2列
          Object o3 = qr.query("select count(*) from users",new ScalarHandler());
          System.out.println(o3);
          System.out.println(o.getClass().getName());//查看返回的Object变量是什么类型
     }
     
     //BeanHandler:取第一行数据
     @Test
     public void test8() throws SQLException {
          QueryRunner qr = new QueryRunner(c3p0.getDataSource());
          User u = qr.query("select * from users",new BeanHandler<User>(User.class));
          System.out.println(u);
     }
     
     //BeanListHandler:将每个数据封装到List集合中
     @Test
     public void test9() throws SQLException {
          QueryRunner qr = new QueryRunner(c3p0.getDataSource());
        List<User> list = qr.query("select * from users where id=?", new BeanListHandler<User>(User.class),2);
        //list若取不到值则返回0,不会有空指针异常的问题
        for(User user:list){
            System.out.println(user);
        }
    }   

}

5. ThreadLocal(当前线程对象)

作用:调用该类的 get 方法,永远返回当前线程放入的数据(线程局部变量)。

// 模拟 ThreadLocal 的设计,明白其作用
public class ThreadLocal {

    private Map<Runnable, Object> container = new HashMap<Runnable, Object>();

    public void set(Object value){
        container.put(Thread.currentThread(), value);  // 用当前线程作为key
    }

    public Object get(){
        return container.get(Thread.currentThread());
    }

    public void remove(){
        container.remove(Thread.currentThread());
    }

}

案例:

  • ThreadLocal 工具类
import java.sql.Connection;
import java.sql.SQLException;

public class ManageThreadLocal {

     private static ThreadLocal<Connection> t1 = new ThreadLocal<Connection>();
     
     // 得到当前线程中的一个连接
     public static Connection getConnection(){
          Connection conn = t1.get();  // 从当前线程取出一个连接
          if(conn==null){
              conn = C3P0Util.getConnection();  // 从池中取出一个
              t1.set(conn);  // 把conn对象放入到当前线程对象中
          }
          return conn;
     }
     
     // 开始事务
     public static void startTransaction(){
          try {
              getConnection().setAutoCommit(false);  // 从当前线程对象中取出的连接,并开始事务
          } catch (SQLException e) {
              e.printStackTrace();
          }
     }
     
     // 提交事务
     public static void commit(){
          try {
              getConnection().commit();
          } catch (SQLException e) {
              e.printStackTrace();
          }
     }
     
     // 回滚事务
     public static void rollback(){
          try {
              getConnection().rollback();
          } catch (SQLException e) {
              e.printStackTrace();
          }
     }
     
     public static void close(){
          try {
              getConnection().close();  // 把连接放回池中
              t1.remove();  // 把当前线程对象中的conn移除
          } catch (SQLException e) {
              e.printStackTrace();
          }
     }

}
  • AccountDaoImpl
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import com.dao.AccountDao;
import com.domain.Account;
import com.util.C3P0Util;
import com.util.ManagerThreadLocal;

public class AccountDaoImpl implements AccountDao {

     public void updateAccount(String fromname, String toname, double money) throws Exception {
         // 创建一个QueryRunner对象
         QueryRunner qr = new QueryRunner(C3P0Util.getDataSource());
         qr.update("update account set money=money-? where name=?",money,fromname);
         qr.update("update account set money=money+? where name=?",money,toname);

     }

     public void updateAccout(Account account) throws Exception {
         QueryRunner qr = new QueryRunner();

         return qr.update(ManagerThreadLocal.getConnection(),"update account set money=? where name=?",account.getMoney(),account.getName());

     }

     public Account findAccountByName(String name) throws Exception {
         QueryRunner qr = new QueryRunner();
         return qr.query(ManagerThreadLocal.getConnection(),"select * from account where name=?", new BeanHandler<Account>(Account.class),name);
     }

}
  • AccountServiceImpl

import java.sql.Connection;
import java.sql.SQLException;
import com.dao.AccountDao;
import com.dao.impl.AccountDaoImpl;
import com.domain.Account;
import com.service.AccountService;
import com.util.C3P0Util;
import com.util.ManagerThreadLocal;

public class AccountServiceImpl implements AccountService {
     public void transfer(String fromname, String toname, double money) {
     //   ad.updateAccount(fromname, toname, money);
          AccountDao ad = new AccountDaoImpl();
          
          try {
               ManagerThreadLocal.startTransacation();  // begin
              // 分别得到转出和转入账户对象
              Account fromAccount = ad.findAccountByName(fromname);
              Account toAccount = ad.findAccountByName(toname);
              
              // 修改账户各自的金额
               fromAccount.setMoney(fromAccount.getMoney()-money);
               toAccount.setMoney(toAccount.getMoney()+money);
              
              //完成转账操作
              ad.updateAccout(fromAccount);
//            int i = 10/0;
              ad.updateAccout(toAccount);
              
              ManagerThreadLocal.commit();  // 提交事务
          } catch (Exception e) {
              try {
                   ManagerThreadLocal.rollback();  // 回滚事务
              } catch (Exception e1) {
                   e1.printStackTrace();
              }
          }finally{
              try {
                   ManagerThreadLocal.close();
              } catch (Exception e) {
                   e.printStackTrace();
              }  // 关闭
          }
     }

}

二、Mybatis

1、Mybatis 简介

ORM(Object Relational Mapping,对象关系映射):指的是持久化数据和实体对象的映射模式,为了解决面向对象与关系型数据库存在的互不匹配的现象的技术。

 Mybatis 是一个优秀的基于 Java 的持久层框架,它内部封装了 JDBC,使开发者只需要关注 SQL 语句本身,而不需要花费精力去处理加载驱动、创建连接、创建 statement 等繁杂的过程。

具体地说,Hibernate 是一个完全的 ORM 框架,而 Mybatis 是一个不完全的 ORM 框架。

Mybatis 会将输入参数、输出结果进行映射。

MyBatis 官网地址

原生态 JDBC 操作:

public static void main(String[] args) {

           Connection connection = null;
           PreparedStatement preparedStatement = null;
           ResultSet resultSet = null;

           try {
              // 1、加载数据库驱动
              Class.forName("com.mysql.jdbc.Driver");
              // 2、通过驱动管理类获取数据库链接
              connection =  DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8", "root", "root");
              // 3、定义sql语句 ?表示占位符
           String sql = "select * from user where username = ?";
              // 4、获取预处理statement
              preparedStatement = connection.prepareStatement(sql);
              // 5、设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值
              preparedStatement.setString(1, "王五");
              // 6、向数据库发出sql执行查询,查询出结果集
              resultSet =  preparedStatement.executeQuery();
              // 7、遍历查询结果集
              while(resultSet.next()){
                  User user
                  System.out.println(resultSet.getString("id")+"  "+resultSet.getString("username"));
              }
           } catch (Exception e) {
              e.printStackTrace();
           }finally{
              //8、释放资源
              if(resultSet!=null){
                  try {
                     resultSet.close();
                  } catch (SQLException e) {
                     // TODO Auto-generated catch block
                     e.printStackTrace();
                  }
              }
              if(preparedStatement!=null){
                  try {
                     preparedStatement.close();
                  } catch (SQLException e) {
                     // TODO Auto-generated catch block
                     e.printStackTrace();
                  }
              }
              if(connection!=null){
                  try {
                     connection.close();
                  } catch (SQLException e) {
                     // TODO Auto-generated catch block
                     e.printStackTrace();
                  }
              }
           }
       }

原生态 JDBC 操作的问题与解决方案:

  • 问题:

    1. 频繁创建和销毁数据库的连接会造成系统资源浪费从而影响系统性能。
    2. sql 语句在代码中硬编码,如果要修改 sql 语句,就需要修改 java 代码,造成代码不易维护。
    3. 查询操作时,需要手动将结果集中的数据封装到实体对象中。
    4. 增删改查操作需要参数时,需要手动将实体对象的数据设置到 sql 语句的占位符。
  • 对应的解决方案:

    1. 使用数据库连接池初始化连接资源。
    2. 将 sql 语句抽取到配置文件中。
    3. 使用反射、内省等底层技术,将实体与表进行属性与字段的自动映射。

Mybatis 通过 xml 或注解的方式将要执行的各种 statement 配置起来,并将 Java 对象和 statement 中 SQL 的动态参数进行映射,生成最终执行的 SQL 语句。最后 Mybatis 框架执行 SQL 并将结果映射为 Java 对象并返回。

2、MyBatis API

1. Resources(加载资源的工具类)

核心方法:

2. SqlSessionFactoryBuilder(构建器)

SqlSessionFactoryBuilder:获取 SqlSessionFactory 工厂对象的功能类

核心方法:

3. SqlSessionFactory(工厂对象)

SqlSessionFactory:获取 SqlSession 构建者对象的工厂接口

核心方法:

4. SqlSession 会话对象

SqlSession:构建者对象接口,用于执行 SQL、管理事务、接口代理

SqlSession 实例在 MyBatis 中是非常强大的一个类,在这里能看到所有执行语句、提交或回滚事务和获取映射器实例的方法。

 

3、Mybatis 入门案例

MyBatis 开发步骤:

  1. 添加 MyBatis 的 jar 包
  2. 创建 Student 数据表
  3. 编写 Student 实体类
  4. 编写映射文件 StudentMapper.xml
  5. 编写核心文件 MyBatisConfig.xml
  6. 编写测试类

1. 环境搭建

1)导入 MyBatis 的 jar 包

  • mysql-connector-java-5.1.37-bin.jar
  • mybatis-3.5.3.jar
  • log4j-1.2.17.jar

2)创建 student 数据表

CREATE TABLE student(
    id INT PRIMARY KEY AUTO_INCREMENT,
    NAME VARCHAR(20),
    age INT
);

INSERT INTO student VALUES (NULL, '张三', 23);
INSERT INTO student VALUES (NULL, '李四', 24);
INSERT INTO student VALUES (NULL, '王五', 25);

3)编写 Student 实体类

public class Student {
	
    private Integer id;
    private String name;
    private Integer age;

    public Student() {
    }

    public Student(Integer id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

4)JDBC 配置文件

Mysql 5.X:

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/world
username=root
password=itheima

Mysql 8.X:

driverClass=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/world?serverTimezone=UTC
username=root
password=admin

2. Mybatis 全局配置文件

全局配置文件包含了 MyBatis 全局的设置和属性信息,如数据库的连接、事务、连接池信息等。

全局配置文件可自定义命名,其配置内容和顺序如下(顺序不能乱):

  • Properties(属性)
  • Settings(全局参数设置)
  • typeAliases(类型别名)
  • typeHandlers(类型处理器)
  • objectFactory(对象工厂)
  • plugins(插件)
  • environments(环境信息集合)
    • environment(单个环境信息)
      • transactionManager(事务)
      • dataSource(数据源)
  • mappers(映射器)

示例:src 目录下的 MyBatisConfig.xml

  • jdbc.properties
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/world?serverTimezone=UTC
username=root
password=admin
  • MyBatisConfig.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!-- MyBatis的DTD约束 -->
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">

<!-- 根标签 -->
<configuration>

    <!-- 引入数据库连接的配置文件 -->
    <properties resource="jdbc.properties"/>
    <!--
     <properties resource="db.properties">
           <property name="db.username" value="123" />
     </properties>
     -->

    <!-- 在日常开发过程中,排查问题时难免需要输出 MyBatis 真正执行的 SQL 语句、参数、结果等信息,这时我们就可以借助 LOG4J 的功能来实现执行信息的输出 -->
    <settings>
        <setting name="logImpl" value="log4j"/>
    </settings>

    <!-- 起别名:即全类名与别名的映射 -->
    <typeAliases>
        <!-- 单个别名定义 -->
        <!-- <typeAlias type="com.bean.Student" alias="student"/> -->
        <!-- 批量别名定义(推荐) -->
        <!-- package:为指定包下的所有类声明别名,其默认别名就是类名(首字母大小写都可) -->
        <package name="com.bean" />
    </typeAliases>

    <!-- environments 配置数据库环境:环境可以有多个,default属性指定使用的是哪个 -->
    <!-- 与spring整合后,该信息由spring来管理 -->
    <environments default="mysql">
        <!--environment 配置数据库环境:id属性唯一标识 -->
        <environment id="mysql">
            <!-- 配置JDBC事务控制,由mybatis进行管理 type属性值表示采用JDBC默认的事务 -->
            <transactionManager type="JDBC"></transactionManager>
            <!-- dataSource 数据源信息:type属性指表示采用mybatis连接池 -->
            <dataSource type="POOLED">
                <!-- property获取数据库连接的配置信息 -->
                <property name="driver" value="${driver}" />
                <property name="url" value="${url}" />
                <property name="username" value="${username}" />
                <property name="password" value="${password}" />
            </dataSource>
        </environment>
    </environments>

    <!-- mappers引入映射配置文件 -->
    <mappers>
        <!-- mapper 引入指定的映射配置文件。resource属性指定映射配置文件的名称 -->
        <mapper resource="StudentMapper.xml"/>
    </mappers>
</configuration>

加载的顺序:

  1. 先加载 properties 中 property 标签声明的属性;
  2. 再加载 properties 标签引入的 java 配置文件中的属性;
  3. parameterType 的值和 properties 的属性值会有冲突关系(因此加上 db.)。

Mybatis 的默认别名:

Mapper:

  • <mapper resource=''/>

    • 使用相对于类路径的资源
    • 如:<mapper resource="sqlmap/User.xml" />
  • <mapper url=''/>

    • 使用完全限定路径
    • 如:<mapper url="file:///D:\workspace_spingmvc\mybatis_01\config\sqlmap\User.xml" />
  • <mapper class=''/>

    • 使用mapper接口的全限定名
    • 如:<mapper class="com.mybatis.mapper.UserMapper"/>
    • 注意:此种方法要求 mapper 接口和 mapper 映射文件要名称相同,且放到同一个目录下;
  • <package name=''/>(推荐)

    • 注册指定包下的所有映射文件
    • 如:<package name="com.mybatis.mapper"/>
    • 注意:此种方法要求 mapper 接口和 mapper 映射文件要名称相同,且放到同一个目录下。

3. Mybatis 映射配置文件

映射配置文件包含了数据和对象之间的映射关系以及要执行的 SQL 语句。

映射文件可自定义命名,一般按照规范实体类名Mapper.xml,这种命名规范是由 ibatis 遗留下来的。

示例:src 目录下的 StudentMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!--MyBatis的DTD约束 -->
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!--
    mapper:核心根标签
    namespace:命名空间,对statement的信息进行分类管理(在mapper代理时,它具有特殊及重要的作用)
-->
<mapper namespace="StudentMapper">
    
    <!--
        select:查询功能的标签,表示一个MappedStatement对象
        id属性:statement的唯一标识
        #{}:表示一个占位符(即?)
        #{id}:里面的id表示输入参数的参数名称
        resultType属性:输出结果的所映射的Java类型(单条结果所对应的Java类型)
            这里的student之所以不用写全类名,是因为会在后面的全局配置文件中起别名
        parameterType属性:输入参数的所属Java类型
    -->
    <!-- 查询全量学生信息 -->
    <select id="selectAll" resultType="student">
        SELECT * FROM student
    </select>

    <!-- 根据id查询指定学生信息 -->
    <select id="selectById" resultType="student" parameterType="int">
        SELECT * FROM student WHERE id = #{id}
    </select>

    <!-- 根据名称模糊查询学生列表 -->
    <!-- #{} 表示占位符 ?,#{} 接收简单类型的参数时,里面的名称可以任意 -->
    <!-- ${} 表示拼接符,${} 接收简单类型的参数时,里面的名称必须是 value -->
    <!-- ${} 里面的值会原样输出,不加解析(如果该参数值是字符串,也不会添加引号) -->
    <!-- ${} 存在sql注入的风险,但是有些场景下必须使用,比如排序后面需要动态传入排序的列名 -->
    <select id="selectByName" parameterType="String" resultType="student">
        SELECT * FROM student WHERE name LIKE '%${value}%'
    </select>

    <!-- 添加学生信息 -->
    <!-- selectKey:查询主键,在标签内需要输入查询主键的sql -->
    <!-- order:指定查询主键的sql和insert语句的执行顺序,相当于insert语句来说 -->
    <!-- LAST_INSERT_ID:该函数是mysql的函数,获取自增主键的ID,它必须配合insert语句一起使用 -->
    <insert id="insertBySelectLastId" parameterType="student">
        <selectKey keyProperty="id" resultType="int" order="AFTER">
            SELECT LAST_INSERT_ID()
        </selectKey>
        INSERT INTO student (name, age) VALUES (#{name},#{age})
    </insert>

    <!-- 添加学生信息 -->
    <!-- 只要不是主键自增,order都设置被before -->
    <insert id="insertByKeyproperty" parameterType="student" useGeneratedKeys="true" keyProperty="id">
        <!-- 需要显式地给id赋值,因为该id的值不是通过自增主键来创建的 -->
        INSERT INTO student (id,name,age)
        VALUES(#{id}, #{name}, #{age})
    </insert>

    <!-- 修改学生信息 -->
    <update id="updateById" parameterType="student">
        UPDATE student SET name = #{name}, age = #{age} WHERE id = #{id}
    </update>

    <!-- 删除学生信息 -->
    <delete id="deleteById" parameterType="int">
        DELETE FROM student WHERE id = #{id}
    </delete>

</mapper>

4. 测试代码

import com.bean.Student;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.jupiter.api.Test;

import java.io.InputStream;
import java.util.List;

public class StudentTest {

    // 查询全量结果
    @Test
    public void selectAll() {
        InputStream inputStream = null;
        SqlSession sqlSession = null;
        try {
            // 1. 加载核心配置文件
            inputStream = Resources.getResourceAsStream("MyBatisConfig.xml");
            // 2. 获取SqlSession工厂对象
            SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. 通过SqlSession工厂对象获取SqlSession对象
            // sqlSession 内部的数据区域本身就是一级缓存,是通过 map 来存储的
            sqlSession = ssf.openSession();
            // 4. 执行映射配置文件中的SQL,并获取返回结果
            List<Student> students = sqlSession.selectList("StudentMapper.selectAll");  // 映射配置文件中的namespace属性值.(SQL的)唯一标识
            // 5. 处理返回结果
            for (Student s : students) {
                System.out.println(s);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            sqlSession.close();
            try {
                inputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    // 根据id查询指定结果
    @Test
    public void selectById() {
        InputStream inputStream = null;
        SqlSession sqlSession = null;
        try {
            // 1. 加载核心配置文件
            inputStream = Resources.getResourceAsStream("MyBatisConfig.xml");
            // 2. 获取SqlSession工厂对象
            SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. 通过SqlSession工厂对象获取SqlSession对象
            sqlSession = ssf.openSession();
            // 4. 执行映射配置文件中的SQL,并获取返回结果
            Student student = sqlSession.selectOne("StudentMapper.selectById", 2);
            // 5. 处理返回结果
            System.out.println(student);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            sqlSession.close();
            try {
                inputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    // 根据名称模糊查询结果
    @Test
    public void SelectByName() {
        InputStream inputStream = null;
        SqlSession sqlSession = null;
        try {
            // 1. 加载核心配置文件
            inputStream = Resources.getResourceAsStream("MyBatisConfig.xml");
            // 2. 获取SqlSession工厂对象
            SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. 通过SqlSession工厂对象获取SqlSession对象
            sqlSession = ssf.openSession();
            // 4. 执行映射配置文件中的SQL,并获取返回结果
            List<Student> students = sqlSession.selectList("StudentMapper.selectByName", "五");
            // 5. 处理返回结果
            for (Student s : students) {
                System.out.println(s);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            sqlSession.close();
            try {
                inputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    // 新增数据:根据mysql函数自动获取id
    @Test
    public void insertBySelectLastId() {
        InputStream inputStream = null;
        SqlSession sqlSession = null;
        try {
            // 1. 加载核心配置文件
            inputStream = Resources.getResourceAsStream("MyBatisConfig.xml");
            // 2. 获取SqlSession工厂对象
            SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. 通过SqlSession工厂对象获取SqlSession对象
            sqlSession = ssf.openSession();
            // 4. 执行映射配置文件中的SQL,并获取返回结果
            Student student = new Student(null, "王八", 29);  // id会自动填充
            int result = sqlSession.insert("StudentMapper.insertBySelectLastId", student);
            // 5. 处理返回结果
            if (result==1) {
                System.out.println("insertBySelectLastId 新增成功");
                // 需要手动提交事务
                sqlSession.commit();
            } else {
                System.out.println("insertBySelectLastId 新增失败");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            sqlSession.close();
            try {
                inputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    // 新增数据:根据映射配置属性,自动获取id
    @Test
    public void insertByKeyproperty() {
        InputStream inputStream = null;
        SqlSession sqlSession = null;
        try {
            // 1. 加载核心配置文件
            inputStream = Resources.getResourceAsStream("MyBatisConfig.xml");
            // 2. 获取SqlSession工厂对象
            SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. 通过SqlSession工厂对象获取SqlSession对象
            sqlSession = ssf.openSession();
            // 4. 执行映射配置文件中的SQL,并获取返回结果
            Student student = new Student(null, "史十", 29);  // id会自动填充
            int result = sqlSession.insert("StudentMapper.insertByKeyproperty", student);
            // 5. 处理返回结果
            if (result==1) {
                System.out.println("insertByKeyproperty 新增成功");
                // 需要手动提交事务
                sqlSession.commit();
            } else {
                System.out.println("insertByKeyproperty 新增失败");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            sqlSession.close();
            try {
                inputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    // 修改数据
    @Test
    public void updateById() {
        InputStream inputStream = null;
        SqlSession sqlSession = null;
        try {
            // 1. 加载核心配置文件
            inputStream = Resources.getResourceAsStream("MyBatisConfig.xml");
            // 2. 获取SqlSession工厂对象
            SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. 通过SqlSession工厂对象获取SqlSession对象
            sqlSession = ssf.openSession();
            // 4. 执行映射配置文件中的SQL,并获取返回结果
            Student student = new Student(2, "小二", 29);
            int result = sqlSession.update("StudentMapper.updateById", student);
            // 5. 处理返回结果
            if (result==1) {
                System.out.println("updateById 修改成功");
                // 需要手动提交事务
                sqlSession.commit();
            } else {
                System.out.println("updateById 修改失败");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            sqlSession.close();
            try {
                inputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    // 删除数据
    @Test
    public void deleteById() {
        InputStream inputStream = null;
        SqlSession sqlSession = null;
        try {
            // 1. 加载核心配置文件
            inputStream = Resources.getResourceAsStream("MyBatisConfig.xml");
            // 2. 获取SqlSession工厂对象
            SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. 通过SqlSession工厂对象获取SqlSession对象
            sqlSession = ssf.openSession();
            // 4. 执行映射配置文件中的SQL,并获取返回结果
            int result = sqlSession.delete("StudentMapper.deleteById", 2);
            // 5. 处理返回结果
            if (result==1) {
                System.out.println("updateById 删除成功");
                // 需要手动提交事务
                sqlSession.commit();
            } else {
                System.out.println("updateById 删除失败");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            sqlSession.close();
            try {
                inputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

4、输入/输出映射详解

1. 输入映射:parameterType

1)简单类型

如学生 id 等基本数据类型。

2)POJO 类型

POJO(Plain Ordinary Java Object)即简单的 Java 对象,实际就是普通 Java Beans。

3)包装 POJO 类型

包装 POJO 类型,即 Java 对象中有其他 Java 对象的引用。

  • 需求:

    综合查询时,可能会根据用户信息、商品信息、订单信息等作为条件进行查询,用户信息中的查询条件由用户的名称和性别进行查询。

  • 创建包装 POJO 类型:

  •  映射文件:

  • Mapper 接口:

  • 测试代码:

4)Map 类型

同传递 POJO 对象一样,Map 的 key 相当于 POJO 的属性。

  • 映射文件:
<!-- 传递 hashmap 综合查询用户信息 -->
<select id="findUserByHashmap" parameterType="hashmap" resultType="user">
    <!-- username是hashmap的key -->
    select * from user where id=#{id} and username like '%${username}%'
</select>
  • 测试代码:
Public void testFindUserByHashmap()throws Exception{
    // 获取session
    SqlSession session = sqlSessionFactory.openSession();
    // 获限mapper接口实例
    UserMapper userMapper = session.getMapper(UserMapper.class);
    // 构造查询条件Hashmap对象
    HashMap<String, Object> map = new HashMap<String, Object>();
    map.put("id", 1);
    map.put("username", "管理员");
    
    // 传递Hashmap对象查询用户列表
    List<User>list = userMapper.findUserByHashmap(map);
    // 关闭session
    session.close();
    
}

注意:当传递的 map 中的 key 和 sql 中解析的 key 不一致时,程序不会报错,只是通过 key 获取的值为空。

2. 输出映射

1)resultType

使用要求:

  • 使用 resultType 进行结果映射时,需要查询出的列名和映射的对象的属性名一致,才能映射成功。
  • 如果查询的列名和对象的属性名全部不一致,那么映射的对象为空。
  • 如果查询的列名和对象的属性名有一个一致,那么映射的对象不为空,但是只有映射正确那一个属性才有值。
  • 如果查询的 SQL 的列名有别名,那么这个别名就需要是和属性映射的列名。

2)resultMap

使用要求:

  • 使用 resultMap 进行结果映射时,不需要查询的列名和映射的属性名必须一致,但是需要声明一个 resultMap,来对列名和属性名进行映射。

示例需求:

对以下 SQL 查询的结果集进行对象映射:Select id id_, username username_, sex sex_ from user where id = 1;

  • 映射文件:
<!-- resultMap入门 -->
<!-- id标签:专门为查询结果中唯一列映射 -->
<!-- result标签:映射查询结果中的普通列 -->
<resultMap type="user" id="UserRstMap">
    <id column="id_" property="id" />
    <result column="username_" property="username" />
    <result column="sex_" property="sex" />
</resultMap>
<select id="findUserRstMap" parameterType="int" resultMap="UserRstMap">
    Select id id_,username username_,sex sex_ from user where id = #{id}
</select>
  • Mapper 接口:

  • 测试代码:

5、Mybatis Mapper 代理

1. 传统 Dao 开发方式的问题

原始 Dao 的开发方式,即开发 Dao 接口和 Dao 实现类。

Dao 接口:

import com.bean.Student;

import java.util.List;

public interface StudentDao {

    // 1. 根据学生ID查询学生信息 
    public Student selectById(int id);

    // 2. 根据学生名称模糊查询学生列表 
    public List<Student> selectByName(String name);
	
    // 3. 添加学生 
    public void insertBySelectLastId(Student Student);

}

Dao 实现类:

  • SqlSessionFactory:它的生命周期,应该是应用范围,即全局范围只有一个工厂,因此使用单例模式来实现这个功能(与 Spring 集成之后,由 Spring 来对其进行单例管理)。
  • SqlSession:它内部含有一块数据区域,存在线程不安全的问题,所以应该将 Sqlsession 声明到方法内部。
import com.bean.Student;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;

import java.util.List;

public class StudentDaoImpl implements StudentDao {

    // 依赖注入
    private SqlSessionFactory sqlSessionFactory;

    public StudentDaoImpl(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
    }

    @Override
    public Student selectById(int id) {
        // 创建SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 调用SqlSession的增删改查方法
        // 第一个参数:表示statement的唯一标示
        Student student = sqlSession.selectOne("StudentMapper.selectById", id);
        System.out.println(student);
        // 关闭资源
        sqlSession.close();
        return student;
    }

    @Override
    public List<Student> selectByName(String name) {
        // 创建SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 调用SqlSession的增删改查方法
        // 第一个参数:表示statement的唯一标示
        List<Student> list = sqlSession.selectList("StudentMapper.SelectByName", name);
        System.out.println(list.toString());
        // 关闭资源
        sqlSession.close();
        return list;
    }

    @Override
    public void insertBySelectLastId(Student Student) {
        // 创建SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 调用SqlSession的增删改查方法
        // 第一个参数:表示statement的唯一标示
        sqlSession.insert("StudentMapper.insertBySelectLastId", Student);
        System.out.println(Student.getId());
        // 提交事务
        sqlSession.commit();
        // 关闭资源
        sqlSession.close();
    }

}

测试类:

import com.bean.Student;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;

public class StudentDaoTest {

    private SqlSessionFactory sqlSessionFactory;

    @Before
    public void setUp() throws IOException {
        // 1. 加载核心配置文件
        InputStream inputStream = Resources.getResourceAsStream("MyBatisConfig.xml");
        // 2. 获取SqlSession工厂对象
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    }

    @Test
    public void testSelectStudentById() {
        StudentDao studentDao = new StudentDaoImpl(sqlSessionFactory);
        Student student = studentDao.selectById(1);
        System.out.println(student);
    }

}

思考存在的问题:

  • 有大量的重复的模板代码。
  • 存在硬编码。

2. Mapper 代理的开发方式

采用 Mybatis 的代理开发方式实现 Dao 层的开发,是企业的主流方式。

Mapper 接口开发方法只需要程序员编写 Mapper 接口(相当于 Dao 接口),由 Mybatis 框架根据接口定义创建接口的动态代理对象,代理对象的方法体同上边 Dao 接口实现类方法。

总结代理方式可以让我们只编写接口即可,而实现类对象由 MyBatis 生成

Mapper 代理的开发规范:

  1. Mapper.xml(映射文件)文件中的 namespace 与 mapper 接口的全限定名相同。
  2. Mapper 接口方法名和 Mapper.xml 中定义的每个 statement 的 id 相同。
  3. Mapper 接口方法的输入参数类型和 mapper.xml 中定义的每个 sql 的 parameterType 的类型相同。
  4. Mapper 接口方法的输出参数类型和 mapper.xml 中定义的每个 sql 的 resultType 的类型相同。

总结:

Mapper 接口开发的方式: 程序员只需定义接口就可以对数据库进行操作。那么具体的对象是怎么创建的?

  1. 程序员负责定义接口;
  2. Mybatis 框架根据接口,通过动态代理的方式生成代理对象,负责数据库的 crud 操作。

代码示例:

  • Mapper 接口:
import com.bean.Student;

import java.util.List;

public interface StudentMapper {
    // 查询全部
    public abstract List<Student> selectAll();

    // 根据id查询
    public abstract Student selectById(Integer id);

    // 新增数据
    public abstract Integer insert(Student stu);

    // 修改数据
    public abstract Integer update(Student stu);

    // 删除数据
    public abstract Integer delete(Integer id);

    // 多条件查询
    public abstract List<Student> selectCondition(Student stu);

    // 根据多个id查询
    public abstract List<Student> selectByIds(List<Integer> ids);
}
  • 测试类:
import com.bean.Student;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;

public class StudentMapperTest {

    private SqlSessionFactory sqlSessionFactory;

    @Before
    public void setUp() throws IOException {
        // 加载核心配置文件
        InputStream inputStream = Resources.getResourceAsStream("MyBatisConfig.xml");
        // 获取SqlSession工厂对象
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    }

    @Test
    public void testSelectStudentById() {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 由mybatis通过sqlSession来创建代理对象
        // 创建StudentMapper对象
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
        Student student = mapper.selectById(1);
        System.out.println(student);
        // 关闭资源
        sqlSession.close();
        inputStream.close();
    }

}

3. 动态代理方式源码分析

分析动态代理对象如何生成的?

通过动态代理开发模式,我们只编写一个接口,不写实现类,我们通过 getMapper() 方法最终获取到 org.apache.ibatis.binding.MapperProxy 代理对象,然后执行功能,而这个代理对象正是 MyBatis 使用了 JDK 的动态代理技术,帮助我们生成了代理实现类对象。从而可以进行相关持久化操作。

分析方法是如何执行的?

动态代理实现类对象在执行方法的时候最终调用了 mapperMethod.execute() 方法,这个方法中通过 switch 语句根据操作类型来判断是新增、修改、删除、查询操作,最后一步回到了 MyBatis 最原生的 SqlSession 方式来执行增删改查。

6、动态 SQL

在 Mybatis 中提供了一些动态 SQL 标签,可以让程序员更快的进行 Mybatis 的开发,这些动态 SQL 可以提高 SQL 的可重用性。

动态 SQL 指的就是 SQL 语句可以根据条件或者参数的不同,而进行动态的变化。

常用的动态 SQL 标签有 if 标签、where 标签、SQL 片段、foreach 标签。

1. if、where 标签

if 和 where 标签可用于根据实体类的不同取值,使用不同的 SQL 语句来进行查询。

比如在 id 不为空时可以根据 id 查询,在 username 不为空时还要加入用户名作为条件等。这种情况在我们的多条件组合查询中经常会碰到。

使用格式:

<!-- where:条件标签。如果有动态条件,则使用该标签代替 where 关键字 -->
<where>
    <!-- if:条件判断标签 -->
    <if test="条件判断">
        查询条件拼接
    </if>

示例:

  • 映射文件:
<select id="findByCondition" parameterType="student" resultType="student">
    select * from student
    <where>
        <if test="id!=0">
            and id=#{id}
        </if>
        <if test="username!=null">
            and username=#{username}
        </if>
    </where>
</select>
  • 当查询条件 id 和 username 都存在时:
     // 获得MyBatis框架生成的StudentMapper接口的实现类
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
    Student condition = new Student();
    condition.setId(1);
    condition.setUsername("lucy");
    Student student = mapper.findByCondition(condition);

  • 当查询条件只有 id 存在时:
// 获得MyBatis框架生成的UserMapper接口的实现类
StudentMapper mapper = sqlSession.getMapper( StudentMapper.class);
    Student condition = new Student();
    condition.setId(1);
    Student student = mapper.findByCondition(condition);

2. foreach 标签

<foreach> 即循环遍历标签。适用于多个参数或者的关系。

使用语法:

<foreach collection="" open="" close="" item="" separator="">
    获取参数
</foreach>
  • collection:参数容器类型(list 集合、array 数组)
  • open:开始的 SQL 语句
  • close:结束的 SQL 语句
  • item:参数变量名
  • separator:分隔符

示例需求:

循环执行 SQL 的拼接操作,例如:SELECT * FROM student WHERE id IN (1, 2, 5);

  • 映射文件:
<select id="findByIds" parameterType="list" resultType="student">
    select * from student
    <where>
        <foreach collection="list" open="id in(" close=")" item="id" separator=",">
            #{id}
        </foreach>
    </where>
</select>
  • 测试代码:
// 获得MyBatis框架生成的UserMapper接口的实现类
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
List<Integer> ids = new ArrayList<>();
ids.add(1);
ids.add(2);
List<Student> sList = mapper.findByIds(ids);
System.out.println(sList);

3. SQL 片段抽取

将重复的 SQL 提取出来,使用时用 include 引用即可,最终达到 SQL 重用的目的。

使用语法:

<!-- <sql>:抽取 SQL 语句标签 -->
<sql id="片段唯一标识">需要抽取的 SQL 语句</sql>

<!-- <include>:引入 SQL 片段标签 -->
<include refid="片段唯一标识id" />

使用示例:

<!-- 抽取sql片段 -->
<sql id="selectStudent" select * from student</sql>

<!-- 引入sql片段 -->
<select id="findById" parameterType="int" resultType="student">
    <include refid="selectStudent"></include> where id=#{id}
</select>

<!-- 引入sql片段 -->
<select id="findByIds" parameterType="list" resultType="student">
    <include refid="selectStudent"></include>
    <where>
        <foreach collection="array" open="id in(" close=")" item="id" separator=",">
            #{id}
        </foreach>
    </where>
</select>

7、分页插件

1. 分页插件介绍

分页功能介绍:

  • 分页可以将很多条结果进行分页显示。
  • 如果当前在第一页,则没有上一页。如果当前在最后一页,则没有下一页。
  • 需要明确当前是第几页,这一页中显示多少条结果。

MyBatis 分页插件:

  1. 在企业级开发中,分页也是一种常见的技术。而目前使用的 MyBatis 是不带分页功能的,如果想实现分页的功能,需要我们手动编写 LIMIT 语句。但是不同的数据库实现分页的 SQL 语句也是不同的,所以手写分页的成本较高,这时就可以借助分页插件来帮助我们实现分页功能。
  2. MyBatis 可以使用第三方的插件来对功能进行扩展,如分页插件 PageHelper 就将分页的复杂操作进行了封装,使用简单的方式即可获得分页的相关数据,从而让分页功能变得非常简单。

2. 分页插件使用

1)导入 jar 包

  • pagehelper-5.1.10.jar
  • jsqlparser-3.1.jar

2)在 Mybatis 全局配置文件中配置 PageHelper 插件

<!-- 注意:分页插件的配置位置需在通用 mapper 之前 -->
<plugins>
    <!-- pageHelper 4.0 版本配置
        <plugin interceptor="com.github.pagehelper.PageHelper">
            <property name="dialect" value="mysql"/>
        </plugin>-->
    <!-- pageHelper 5.0 以上版本的配置 -->
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
    </plugin>
</plugins>

3)测试分页数据获取

import com.bean.Student;
import com.github.pagehelper.PageHelper;

import com.github.pagehelper.PageInfo;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class StudentMapperTest {

    private SqlSessionFactory sqlSessionFactory;

    @Before
    public void setUp() throws IOException {
        // 加载核心配置文件
        InputStream inputStream = Resources.getResourceAsStream("MyBatisConfig.xml");
        // 获取SqlSession工厂对象
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    }

    @Test
    public void testPaging() {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 由mybatis通过sqlSession来创建代理对象
        // 创建StudentMapper对象
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

        // 通过分页助手来实现分页功能
        // 第一页:显示3条数据
        // PageHelper.startPage(1, 3);
        // 第二页:显示3条数据
        // PageHelper.startPage(2, 3);
        // 第三页:显示3条数据
        PageHelper.startPage(3, 3);

        // 5.调用实现类的方法,接收结果
        List<Student> list = mapper.selectAll();

        // 6. 处理结果
        for (Student student : list) {
            System.out.println(student);
        }

        // 获取分页的相关参数
        PageInfo<Student> info = new PageInfo<>(list);
        System.out.println("总条数:" + info.getTotal());
        System.out.println("总页数:" + info.getPages());
        System.out.println("每页显示条数:" + info.getPageSize());
        System.out.println("当前页数:" + info.getPageNum());
        System.out.println("上一页数:" + info.getPrePage());
        System.out.println("下一页数:" + info.getNextPage());
        System.out.println("是否是第一页:" + info.isIsFirstPage());
        System.out.println("是否是最后一页:" + info.isIsLastPage());

        // 关闭资源
        sqlSession.close();
    }
    
}

8、Mybatis 多表操作

1. 多表模型介绍

  • 一对一:在任意一方建立外键,关联对方的主键。
  • 一对多:在多的一方建立外键,关联对方(一张表)的主键。
  • 多对多:借助中间表,中间表至少两个字段,分别关联两张表的主键。

2. 一对一

案例:人和身份证,一个人只有一个身份证

1)SQL 数据准备

CREATE TABLE person(
    id INT PRIMARY KEY AUTO_INCREMENT,
    NAME VARCHAR(20),
    age INT
);

INSERT INTO person VALUES (NULL, '张三', 23);
INSERT INTO person VALUES (NULL, '李四', 24);
INSERT INTO person VALUES (NULL, '王五', 25);

CREATE TABLE card(
    id INT PRIMARY KEY AUTO_INCREMENT,
    number VARCHAR(30),
    pid INT,
    CONSTRAINT cp_fk FOREIGN KEY (pid) REFERENCES person(id)
);

INSERT INTO card VALUES (NULL, '12345', 1);
INSERT INTO card VALUES (NULL, '23456', 2);
INSERT INTO card VALUES (NULL, '34567', 3);

2)实体类

  • Person 类:
package com.bean;

public class Person {
    private Integer id;     // 主键id
    private String name;    // 人的姓名
    private Integer age;    // 人的年龄

    public Person() {
    }

    public Person(Integer id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
  • Card 类:
package com.bean;

public class Card {
    private Integer id;
    private Integer number;
    private Person person;  // 所属人的对象

    public Card() {
    }

    public Card(Integer id, Integer number, Person person) {
        this.id = id;
        this.number = number;
        this.person = person;
    }

    public Integer getId() {
        return id;
    }

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

    public Integer getNumber() {
        return number;
    }

    public void setNumber(Integer number) {
        this.number = number;
    }

    public Person getPerson() {
        return person;
    }

    public void setPerson(Person person) {
        this.person = person;
    }

    @Override
    public String toString() {
        return "Card{" +
                "id=" + id +
                ", number=" + number +
                ", person=" + person +
                '}';
    }

}

3)配置文件

  • 全局配置文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!--MyBatis的DTD约束-->
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">

<!--configuration 核心根标签-->
<configuration>

    <!--引入数据库连接的配置文件-->
    <properties resource="jdbc.properties"/>

    <!--配置LOG4J-->
    <settings>
        <setting name="logImpl" value="log4j"/>
    </settings>

    <!--起别名-->
    <typeAliases>
        <package name="com.bean"/>
    </typeAliases>

    <plugins>
        <!-- 注意:分页插件的配置位置需在通用 mapper 之前 -->
<!--        <plugin interceptor="com.github.pagehelper.PageHelper">-->
<!--            &lt;!&ndash; 指定方言 &ndash;&gt;-->
<!--            <property name="dialect" value="mysql"/>-->
<!--        </plugin>-->
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
        </plugin>
    </plugins>

    <!--environments配置数据库环境,环境可以有多个。default属性指定使用的是哪个-->
    <environments default="mysql">
        <!--environment配置数据库环境  id属性唯一标识-->
        <environment id="mysql">
            <!-- transactionManager事务管理。  type属性,采用JDBC默认的事务-->
            <transactionManager type="JDBC"></transactionManager>
            <!-- dataSource数据源信息   type属性 连接池-->
            <dataSource type="POOLED">
                <!-- property获取数据库连接的配置信息 -->
                <property name="driver" value="${driver}" />
                <property name="url" value="${url}" />
                <property name="username" value="${username}" />
                <property name="password" value="${password}" />
            </dataSource>
        </environment>
    </environments>

    <!-- mappers引入映射配置文件 -->
    <mappers>
        <!-- mapper 引入指定的映射配置文件 resource属性指定映射配置文件的名称 -->
        <mapper resource="TableMapper.xml"/>
    </mappers>

</configuration>
  • 映射文件:
<?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.mapper.OneToOneMapper">
    <!--配置字段和实体对象属性的映射关系-->
    <!-- 使用resultMap来进行一对一结果映射,它是将关联对象添加到主信息的对象中,具体说是对象嵌套对象的一种映射方式 -->
    <resultMap id="oneToOne" type="card">
        <!-- id标签:建议在关联查询时必须写上,不写不会报错,但是会影响性能 -->
        <id column="cid" property="id" />
        <result column="number" property="number" />
        <!--
            association:配置被包含对象的映射关系
            property:被包含对象的变量名
            javaType:被包含对象的数据类型
        -->
        <association property="person" javaType="person">
            <id column="pid" property="id" />
            <result column="name" property="name" />
            <result column="age" property="age" />
        </association>
    </resultMap>

    <select id="selectAll" resultMap="oneToOne">
        SELECT c.id cid, number, pid, NAME, age FROM card c, person p WHERE c.pid = p.id
    </select>
</mapper>

4)Mapper 接口类

import com.bean.Card;

import java.util.List;

public interface OneToOneMapper {

    // 查询全部
    public abstract List<Card> selectAll();

}

5)测试类

import com.bean.Card;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class OneToOneTest {

    @Test
    public void testSelectAll() throws IOException {
        // 1.加载核心配置文件
        InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml");

        // 2.获取SqlSession工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

        // 3.通过工厂对象获取SqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession(true);

        // 4.获取OneToOneMapper接口的实现类对象
        OneToOneMapper mapper = sqlSession.getMapper(OneToOneMapper.class);

        // 5.调用实现类的方法,接收结果
        List<Card> list = mapper.selectAll();

        // 6.处理结果
        for (Card c : list) {
            System.out.println(c);
        }

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

3. 一对多

案例:班级和学生,一个班级可以有多个学生

1)SQL 准备

CREATE TABLE classes(
	id INT PRIMARY KEY AUTO_INCREMENT,
	NAME VARCHAR(20)
);
INSERT INTO classes VALUES (NULL,'一班');
INSERT INTO classes VALUES (NULL,'二班');


CREATE TABLE student(
	id INT PRIMARY KEY AUTO_INCREMENT,
	NAME VARCHAR(30),
	age INT,
	cid INT,
	CONSTRAINT cs_fk FOREIGN KEY (cid) REFERENCES classes(id)
);
INSERT INTO student VALUES (NULL,'张三',23,1);
INSERT INTO student VALUES (NULL,'李四',24,1);
INSERT INTO student VALUES (NULL,'王五',25,2);
INSERT INTO student VALUES (NULL,'赵六',26,2);

2)实体类

  • 班级类:
package com.bean;

import java.util.List;

public class Classes {
    
    private Integer id;     // 主键id
    private String name;    // 班级名称
    private List<Student> students;  // 班级中所有学生对象

    public Classes() {
    }

    public Classes(Integer id, String name, List<Student> students) {
        this.id = id;
        this.name = name;
        this.students = students;
    }

    public Integer getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Student> getStudents() {
        return students;
    }

    public void setStudents(List<Student> students) {
        this.students = students;
    }

    @Override
    public String toString() {
        return "Classes{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", students=" + students +
                '}';
    }
}
  • 学生类:
package com.bean;

import java.util.List;

public class Student {

    private Integer id;  // 主键id
    private String name;  // 学生姓名
    private Integer age;  // 学生年龄
    private Classes classes;  // 课程

    public Student() {
    }

    public Student(Integer id, String name, Integer age, Classes classes) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.classes = classes;
    }

    public Classes getClasses() {
        return classes;
    }

    public void setClasses(Classes classes) {
        this.classes = classes;
    }

    public Integer getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", classes=" + classes +
                '}';
    }
}

3)映射文件

<?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.mapper.OneToManyMapper">
    <resultMap id="oneToMany" type="classes">
        <id column="cid" property="id"/>
        <result column="cname" property="name"/>

        <!--
            collection:配置被包含的集合对象映射关系
            property:被包含对象的变量名
            ofType:被包含对象的实际数据类型
        -->
        <collection property="students" ofType="student">
            <id column="sid" property="id"/>
            <result column="sname" property="name"/>
            <result column="sage" property="age"/>
        </collection>
    </resultMap>
    <select id="selectAll" resultMap="oneToMany">
        SELECT c.id cid, c.name cname, s.id sid, s.name sname, s.age sage FROM classes c, student s WHERE c.id=s.cid
    </select>
</mapper>

4)Mapper 接口

import com.bean.Classes;

import java.util.List;

public interface OneToManyMapper {

    // 查询全部
    public abstract List<Classes> selectAll();

}

5)测试类

package com.mapper;

import com.bean.Classes;
import com.bean.Student;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class OneToManyTest {
    @Test
    public void testSelectAll() throws IOException {
        // 1.加载核心配置文件
        InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml");

        // 2.获取SqlSession工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

        // 3.通过工厂对象获取SqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession(true);

        // 4.获取OneToOneMapper接口的实现类对象
        OneToManyMapper mapper = sqlSession.getMapper(OneToManyMapper.class);

        // 5.调用实现类的方法,接收结果
        List<Classes> classes = mapper.selectAll();

        // 6.处理结果
        for (Classes cls : classes) {
            System.out.println(cls.getId() + "," + cls.getName());
            List<Student> students = cls.getStudents();
            for (Student student : students) {
                System.out.println("\t" + student);
            }
        }

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

4. 多对多

案例:学生和课程,一个学生可以选择多门课程、一个课程也可以被多个学生所选择

1)SQL 准备

CREATE TABLE course(
	id INT PRIMARY KEY AUTO_INCREMENT,
	NAME VARCHAR(20)
);
INSERT INTO course VALUES (NULL,'语文');
INSERT INTO course VALUES (NULL,'数学');

CREATE TABLE student_course(
	id INT PRIMARY KEY AUTO_INCREMENT,
	sid INT,
	cid INT,
	CONSTRAINT sc_fk1 FOREIGN KEY (sid) REFERENCES student(id),
	CONSTRAINT sc_fk2 FOREIGN KEY (cid) REFERENCES course(id)
);

INSERT INTO student_course VALUES (NULL,1,1);
INSERT INTO student_course VALUES (NULL,1,2);
INSERT INTO student_course VALUES (NULL,2,1);
INSERT INTO student_course VALUES (NULL,2,2);

2)实体类

  • 学生类:
import java.util.List;

public class Student {
    private Integer id;    // 主键id
    private String name;   // 学生姓名
    private Integer age;   // 学生年龄

    private List<Course> courses;   // 学生所选择的课程集合

    public Student() {
    }

    public Student(Integer id, String name, Integer age, List<Course> courses) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.courses = courses;
    }

    public List<Course> getCourses() {
        return courses;
    }

    public void setCourses(List<Course> courses) {
        this.courses = courses;
    }

    public Integer getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
  • 课程类:
public class Course {
    private Integer id;   // 主键id
    private String name;  // 课程名称

    public Course() {
    }

    public Course(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Course{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

3)映射文件

<?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.mapper.ManyToManyMapper">
    <resultMap id="manyToMany" type="student">
        <id column="sid" property="id"/>
        <result column="sname" property="name"/>
        <result column="sage" property="age"/>

        <collection property="courses" ofType="course">
            <id column="cid" property="id"/>
            <result column="cname" property="name"/>
        </collection>
    </resultMap>
    <select id="selectAll" resultMap="manyToMany">
        SELECT sc.sid, s.name sname, s.age sage, sc.cid, c.name cname 
        FROM student s, course c, student_course sc 
        WHERE sc.sid=s.id AND sc.cid=c.id
    </select>
</mapper>

4)Mapper 接口

import com.bean.Student;

import java.util.List;

public interface ManyToManyMapper {

    // 查询全部
    public abstract List<Student> selectAll();

}

5)测试类

package com.mapper;

import com.bean.Classes;
import com.bean.Course;
import com.bean.Student;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class ManyToManyTest {
    @Test
    public void testSelectAll() throws IOException {
        // 1.加载核心配置文件
        InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml");

        // 2.获取SqlSession工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

        // 3.通过工厂对象获取SqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession(true);

        // 4.获取OneToOneMapper接口的实现类对象
        ManyToManyMapper mapper = sqlSession.getMapper(ManyToManyMapper.class);

        //5.调用实现类的方法,接收结果
        List<Student> students = mapper.selectAll();

        //6.处理结果
        for (Student student : students) {
            System.out.println(student.getId() + "," + student.getName() + "," + student.getAge());
            List<Course> courses = student.getCourses();
            for (Course cours : courses) {
                System.out.println("\t" + cours);
            }
        }

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

9、Mybatis 注解

1. 常用注解与案例

近几年来,注解开发越来越流行,Mybatis 也可以使用注解开发方式,这样我们就可以减少编写 Mapper 映射文件了。

  • @Insert:实现新增
  • @Update:实现更新
  • @Delete:实现删除
  • @Select:实现查询
  • @Result:实现结果集的封装
  • @Results:可以与 @Result 一起使用,以封装多个结果集
  • @One:实现一对一结果集封装
  • @Many:实现一对多结果集封装

案例:student 表的 CRUD

创建 Mapper 接口:

package com.mapper;

import com.bean.Student;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import java.util.List;

public interface StudentMapper {
    //查询全部
    @Select("SELECT * FROM student")
    public abstract List<Student> selectAll();

    //新增操作
    @Insert("INSERT INTO student VALUES (#{id},#{name},#{age})")
    public abstract Integer insert(Student stu);

    //修改操作
    @Update("UPDATE student SET name=#{name},age=#{age} WHERE id=#{id}")
    public abstract Integer update(Student stu);

    //删除操作
    @Delete("DELETE FROM student WHERE id=#{id}")
    public abstract Integer delete(Integer id);
}

修改 Mybatis 全局配置文件:

    <mappers>
        <!--扫描使用注解的类-->
        <mapper class="com.itheima.mapper.UserMapper">
    </mappers>

测试类:

package com.mapper;

import com.bean.Student;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.jupiter.api.Test;

import java.io.InputStream;
import java.util.List;

public class AnnotationTest {

    @Test
    public void selectAll() throws Exception{
        //1.加载核心配置文件
        InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml");

        //2.获取SqlSession工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

        //3.通过工厂对象获取SqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession(true);

        //4.获取StudentMapper接口的实现类对象
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

        //5.调用实现类对象中的方法,接收结果
        List<Student> list = mapper.selectAll();

        //6.处理结果
        for (Student student : list) {
            System.out.println(student);
        }

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

    @Test
    public void insert() throws Exception{
        //1.加载核心配置文件
        InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml");

        //2.获取SqlSession工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

        //3.通过工厂对象获取SqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession(true);

        //4.获取StudentMapper接口的实现类对象
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

        //5.调用实现类对象中的方法,接收结果
        Student stu = new Student(4, "赵六", 26);
        Integer result = mapper.insert(stu);

        //6.处理结果
        System.out.println(result);

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

    @Test
    public void update() throws Exception{
        //1.加载核心配置文件
        InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml");

        //2.获取SqlSession工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

        //3.通过工厂对象获取SqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession(true);

        //4.获取StudentMapper接口的实现类对象
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

        //5.调用实现类对象中的方法,接收结果
        Student stu = new Student(4, "赵六", 36);
        Integer result = mapper.update(stu);

        //6.处理结果
        System.out.println(result);

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

    @Test
    public void delete() throws Exception{
        //1.加载核心配置文件
        InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml");

        //2.获取SqlSession工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

        //3.通过工厂对象获取SqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession(true);

        //4.获取StudentMapper接口的实现类对象
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

        //5.调用实现类对象中的方法,接收结果
        Integer result = mapper.delete(4);

        //6.处理结果
        System.out.println(result);

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

}

2. MyBatis 注解开发的多表操作

实现复杂关系映射之前我们可以在映射文件中通过配置 <resultMap> 来实现,使用注解开发后,我们可以使用 @Results、@Result、@One、@Many 注解组合来完成复杂关系的配置。

1)一对一查询

需求:查询一个用户信息,与此同时查询出该用户对应的身份证信息

 

对应 SQL: 

SELECT * FROM card;

SELECT * FROM person WHERE id=#{id};

创建 PersonMapper 接口:

import com.bean.Person;
import org.apache.ibatis.annotations.Select;

public interface PersonMapper {
    // 根据id查询
    @Select("SELECT * FROM person WHERE id=#{id}")
    public abstract Person selectById(Integer id);
}

使用注解配置 CardMapper:

import com.bean.Card;
import com.bean.Person;
import org.apache.ibatis.annotations.One;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface CardMapper {
    // 查询全部
    @Select("SELECT * FROM card")
    @Results({
            @Result(column="id", property="id"),  // id 列
            @Result(column="number", property="number"),  // number 列
            @Result(  // Card表中的 person id 列
                    property = "person",        // 被包含对象的变量名
                    javaType = Person.class,    // 被包含对象的实际数据类型
                    column = "pid",             // 根据查询出的card表中的pid字段来查询person表
                    /*
                        one、@One 一对一固定写法
                        select属性:指定调用哪个接口中的哪个方法
                     */
                    one = @One(select="com.mapper.PersonMapper.selectById")
            )
    })
    public abstract List<Card> selectAll();
}

测试类:

import com.bean.Card;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class OneToOneTest {

    @Test
    public void testSelectAll() throws IOException {
        // 1.加载核心配置文件
        InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml");

        // 2.获取SqlSession工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

        // 3.通过工厂对象获取SqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession(true);

        // 4.获取OneToOneMapper接口的实现类对象
        CardMapper mapper = sqlSession.getMapper(CardMapper.class);

        // 5.调用实现类的方法,接收结果
        List<Card> list = mapper.selectAll();

        // 6.处理结果
        for (Card c : list) {
            System.out.println(c);
        }

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

执行结果:

Card{id=1, number=12345, person=Person{id=1, name='张三', age=23}}
Card{id=2, number=23456, person=Person{id=2, name='李四', age=24}}
Card{id=3, number=34567, person=Person{id=3, name='王五', age=25}}

2)一对多

需求:查询一个课程,与此同时查询出该课程对应的学生信息

对应的 SQL: 

SELECT * FROM classes

SELECT * FROM student WHERE cid=#{cid}

创建 StudentMapper 接口:

import com.bean.Student;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface StudentMapper {
    //根据cid查询student表
    @Select("SELECT * FROM student WHERE cid=#{cid}")
    public abstract List<Student> selectByCid(Integer cid);
}

使用注解配置 CardMapper:

package com.mapper;

import com.bean.Card;
import com.bean.Person;
import org.apache.ibatis.annotations.One;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface CardMapper {
    // 查询全部
    @Select("SELECT * FROM card")
    @Results({
            @Result(column="id", property="id"),  // id 列
            @Result(column="number", property="number"),  // number 列
            @Result(  // Card表中的 person id 列
                    property = "person",        // 被包含对象的变量名
                    javaType = Person.class,    // 被包含对象的实际数据类型
                    column = "pid",             // 根据查询出的card表中的pid字段来查询person表
                    /*
                        one、@One 一对一固定写法
                        select属性:指定调用哪个接口中的哪个方法
                     */
                    one = @One(select="com.mapper.PersonMapper.selectById")
            )
    })
    public abstract List<Card> selectAll();
}

测试类:

package com.mapper;

import com.bean.Classes;
import com.bean.Student;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class OneToManyTest {
    @Test
    public void testSelectAll() throws IOException {
        // 1.加载核心配置文件
        InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml");

        // 2.获取SqlSession工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

        // 3.通过工厂对象获取SqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession(true);

        // 4.获取OneToOneMapper接口的实现类对象
        ClassesMapper mapper = sqlSession.getMapper(ClassesMapper.class);

        // 5.调用实现类的方法,接收结果
        List<Classes> classes = mapper.selectAll();

        // 6.处理结果
        for (Classes cls : classes) {
            System.out.println(cls.getId() + "," + cls.getName());
            List<Student> students = cls.getStudents();
            for (Student student : students) {
                System.out.println("\t" + student);
            }
        }

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

执行结果:

1,一班
	Student{id=1, name='张三', age=23}
	Student{id=2, name='李四', age=24}
2,二班
	Student{id=3, name='王五', age=25}

3)多对多

需求:查询学生以及所对应的课程信息

对应的 SQL: 

SELECT DISTINCT s.id,s.name,s.age FROM student s,stu_cr sc WHERE sc.sid=s.id
SELECT c.id,c.name FROM stu_cr sc,course c WHERE sc.cid=c.id AND sc.sid=#{id}

创建 CourseMapper 接口:

import com.bean.Course;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface CourseMapper {
    // 根据学生id查询所选课程
    @Select("SELECT c.id, c.name FROM stu_cr sc, course c WHERE sc.cid=c.id AND sc.sid=#{id}")
    public abstract List<Course> selectBySid(Integer id);
}

使用注解配置 StudentMapper 接口:

package com.mapper;

import com.bean.Student;
import org.apache.ibatis.annotations.Many;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface StudentMapper {
    // 查询全部
    @Select("SELECT DISTINCT s.id,s.name,s.age FROM student s,stu_cr sc WHERE sc.sid=s.id")
    @Results({
            @Result(column="id", property="id"),
            @Result(column="name", property="name"),
            @Result(column="age", property="age"),
            @Result(
                    property="courses",   // 被包含对象的变量名
                    javaType=List.class,  // 被包含对象的实际数据类型
                    column="id",          // 根据查询出student表的id来作为关联条件,去查询中间表和课程表
                    /*
                        many、@Many 一对多查询的固定写法
                        select属性:指定调用哪个接口中的哪个查询方法
                     */
                    many = @Many(select="com.mapper.CourseMapper.selectBySid")
            )
    })
    public abstract List<Student> selectAll();
}

测试类:

package com.mapper;

import com.bean.Classes;
import com.bean.Course;
import com.bean.Student;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class ManyToManyTest {
    @Test
    public void testSelectAll() throws IOException {
        // 1.加载核心配置文件
        InputStream is = Resources.getResourceAsStream("MyBatisConfig.xml");

        // 2.获取SqlSession工厂对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

        // 3.通过工厂对象获取SqlSession对象
        SqlSession sqlSession = sqlSessionFactory.openSession(true);

        // 4.获取OneToOneMapper接口的实现类对象
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

        // 5.调用实现类对象中的方法,接收结果
        List<Student> list = mapper.selectAll();

        // 6.处理结果
        for (Student student : list) {
            System.out.println(student.getId() + "," + student.getName() + "," + student.getAge());
            List<Course> courses = student.getCourses();
            for (Course cours : courses) {
                System.out.println("\t" + cours);
            }
        }

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

执行结果:

1,张三,23
	Course{id=1, name='语文'}
	Course{id=2, name='数学'}
2,李四,24
	Course{id=1, name='语文'}
	Course{id=2, name='数学'}

3. 构建 SQL

之前在通过注解开发时,相关 SQL 语句都是自己直接拼写的,一些关键字写起来比较麻烦、而且容易出错。因此,MyBatis 给我们提供了 org.apache.ibatis.jdbc.SQL 功能类,专门用于构建 SQL 语句。

查询功能的实现:

  • 定义功能类并提供获取查询的 SQL 语句的方法。
  • @SelectProvider:生成查询用的 SQL 语句注解。
    • type 属性:生成 SQL 语句功能类对象
    • method 属性:指定调用方法

新增功能的实现:

  • 定义功能类并提供获取新增的 SQL 语句的方法。
  • @InsertProvider:生成新增用的 SQL 语句注解。
    • type 属性:生成 SQL 语句功能类对象
    • method 属性:指定调用方法

修改功能的实现:

  • 定义功能类并提供获取修改的 SQL 语句的方法。
  • @UpdateProvider:生成修改用的 SQL 语句注解。
    • type 属性:生成 SQL 语句功能类对象
    • method 属性:指定调用方法

删除功能的实现:

  • 定义功能类并提供获取删除的 SQL 语句的方法。
  • @DeleteProvider:生成删除用的 SQL 语句注解。
    • type 属性:生成 SQL 语句功能类对象
    • method 属性:指定调用方法

案例:

  • Dao 层:
import com.itheima.domain.Student;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import java.util.ArrayList;

/*
    Dao层接口
 */
public interface StudentDao {
    //查询所有学生信息
    @Select("SELECT * FROM student")
    public abstract ArrayList<Student> findAll();

    //条件查询,根据id获取学生信息
    @Select("SELECT * FROM student WHERE sid=#{sid}")
    public abstract Student findById(Integer sid);
    
    //新增学生信息
    @Insert("INSERT INTO student VALUES (#{sid},#{name},#{age},#{birthday})")
    public abstract int insert(Student stu);
    
    //修改学生信息
    @Update("UPDATE student SET name=#{name},age=#{age},birthday=#{birthday} WHERE sid=#{sid}")
    public abstract int update(Student stu);
    
    //删除学生信息
    @Delete("DELETE FROM student WHERE sid=#{sid}")
    public abstract int delete(Integer sid);

}
  • Dao 实现类:
import com.itheima.dao.StudentDao;
import com.itheima.domain.Student;
import com.itheima.service.StudentService;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * 业务层实现类
 */
public class StudentServiceImpl implements StudentService {

    @Override
    public List<Student> findAll() {
        ArrayList<Student> list = null;
        SqlSession sqlSession = null;
        InputStream is = null;
        try{
            //1.加载核心配置文件
            is = Resources.getResourceAsStream("MyBatisConfig.xml");

            //2.获取SqlSession工厂对象
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

            //3.通过工厂对象获取SqlSession对象
            sqlSession = sqlSessionFactory.openSession(true);

            //4.获取StudentDao接口的实现类对象
            StudentDao mapper = sqlSession.getMapper(StudentDao.class);

            //5.调用实现类对象的方法,接收结果
            list = mapper.findAll();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.释放资源
            if(sqlSession != null) {
                sqlSession.close();
            }
            if(is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        //7.返回结果
        return list;
    }

    @Override
    public Student findById(Integer sid) {
        Student stu = null;
        SqlSession sqlSession = null;
        InputStream is = null;
        try{
            //1.加载核心配置文件
            is = Resources.getResourceAsStream("MyBatisConfig.xml");

            //2.获取SqlSession工厂对象
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

            //3.通过工厂对象获取SqlSession对象
            sqlSession = sqlSessionFactory.openSession(true);

            //4.获取StudentDao接口的实现类对象
            StudentDao mapper = sqlSession.getMapper(StudentDao.class);

            //5.调用实现类对象的方法,接收结果
            stu = mapper.findById(sid);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.释放资源
            if(sqlSession != null) {
                sqlSession.close();
            }
            if(is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        //7.返回结果
        return stu;
    }

    @Override
    public void save(Student student) {
        SqlSession sqlSession = null;
        InputStream is = null;
        try{
            //1.加载核心配置文件
            is = Resources.getResourceAsStream("MyBatisConfig.xml");

            //2.获取SqlSession工厂对象
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

            //3.通过工厂对象获取SqlSession对象
            sqlSession = sqlSessionFactory.openSession(true);

            //4.获取StudentDao接口的实现类对象
            StudentDao mapper = sqlSession.getMapper(StudentDao.class);

            //5.调用实现类对象的方法,接收结果
            mapper.insert(student);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.释放资源
            if(sqlSession != null) {
                sqlSession.close();
            }
            if(is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public void update(Student student) {
        SqlSession sqlSession = null;
        InputStream is = null;
        try{
            //1.加载核心配置文件
            is = Resources.getResourceAsStream("MyBatisConfig.xml");

            //2.获取SqlSession工厂对象
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

            //3.通过工厂对象获取SqlSession对象
            sqlSession = sqlSessionFactory.openSession(true);

            //4.获取StudentDao接口的实现类对象
            StudentDao mapper = sqlSession.getMapper(StudentDao.class);

            //5.调用实现类对象的方法,接收结果
            mapper.update(student);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.释放资源
            if(sqlSession != null) {
                sqlSession.close();
            }
            if(is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public void delete(Integer sid) {
        SqlSession sqlSession = null;
        InputStream is = null;
        try{
            //1.加载核心配置文件
            is = Resources.getResourceAsStream("MyBatisConfig.xml");

            //2.获取SqlSession工厂对象
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

            //3.通过工厂对象获取SqlSession对象
            sqlSession = sqlSessionFactory.openSession(true);

            //4.获取StudentDao接口的实现类对象
            StudentDao mapper = sqlSession.getMapper(StudentDao.class);

            //5.调用实现类对象的方法,接收结果
            mapper.delete(sid);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.释放资源
            if(sqlSession != null) {
                sqlSession.close();
            }
            if(is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

三、Spring IOC

1、Spring 框架简介

Spring 体系结构: 

Spring 发展历史: 

Spring 优势:

  • 方便解耦,简化开发
  • 方便集成各种优秀框架
  • 方便程序的测试
  • AOP 编程的支持
  • 声明式事务的支持
  • 降低 JavaEE API 的使用难度
  • Java 源码是经典学习范例

2、Spring IoC

1. 耦合与内聚

  • 耦合(Coupling):代码书写过程中所使用技术的结合紧密度,用于衡量软件中各个模块之间的互联程度。

  • 内聚(Cohesion):代码书写过程中单个模块内部各组成部分间的联系,用于衡量软件中各个功能模块自身内部的功能联系。

程序书写的目标:高内聚,低耦合。即同一个模块内的各个元素之间要高度紧密,但是各个模块之间的相互依存度却不要那么紧密。

2. 工厂模式发展史

3. Spring 发展历程 

4. IoC 概念

  • IoC(Inversion Of Control)控制反转:Spring 反向控制应用程序所需要使用的外部资源。

  • Spring 控制的资源全部放置在 Spring 容器中,该容器称为 IoC 容器。

5. DI 概念

  • DI(Dependency Injection)依赖注入:应用程序运行依赖的资源由 Spring 为其提供,资源进入应用程序的方式称为注入。

 

  • IoC 与 DI 的关系:同一件事站在不同角度看待问题。

3、入门案例

模拟三层架构中,表现层调用业务层功能。案例步骤如下:

导入 spring 坐标:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.1.9.RELEASE</version>
</dependency>

编写业务层接口与实现类:

  • UserService.java:
public interface UserService {

    // 业务方法
    void save();
}
  • UserServiceImpl.java:
import com.service.UserService;

public class UserServiceImpl implements UserService {

    @Override
    public void save() {
        System.out.println("UserService running...");
    }
}

创建 spring 配置文件:配置所需资源(UserService)为 Spring 控制的资源。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 创建spring控制的资源 -->
    <bean id="userService" class="com.service.impl.UserServiceImpl"/>

</beans>

表现层(UserApp)通过 Spring 获取资源(Service 实例):

import com.service.UserService;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class UserApp {

    public static void main(String[] args) {
        // 加载配置文件,创建Spring容器
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        // 获取资源
        UserService userService = (UserService)context.getBean("userService");
        userService.save();
    }
}

4、IoC 配置

1. bean 标签

  • 类型:标签

  • 归属:beans 标签

  • 作用:定义 spring 中的资源,受此标签定义的资源将受到 spring 控制

  • 基本属性:

    • id:bean 的名称,通过 id 值获取 bean 。
    • class:bean 的类型。
    • name:bean 的名称,可以通过 name 值获取 bean,用于多人配合时给 bean 起别名。
  • 示例:

<beans>
    <bean id="beanId" name="beanName1,beanName2" class="ClassName" />
</beans>

2. scope 属性

  • 类型:属性

  • 归属:bean标签

  • 作用:定义 bean 的作用范围

  • 示例:

<!-- scope用于控制bean创建后的对象是否是单例的 -->
<bean id="userService" scope="prototype" class="com.service.impl.UserServiceImpl" />

取值:

  • singleton:设定创建出的对象保存在 spring 容器中,是一个单例的对象。
  • prototype:设定创建出的对象不保存在 spring 容器中,是一个非单例的对象。
  • request、session、application、websocket(四者不常用):设定创建出的对象放置在 web 容器对应的位置。

3. bean 生命周期配置

  • 名称:init-method,destroy-method

  • 类型:属性

  • 归属:bean 标签

  • 作用:定义 bean 对象在初始化或销毁时完成的工作

  • 示例:

<!-- inti-method 与 destroy-method 用于控制 bean 的生命周期 -->
<bean id="userService" scope="prototype" init-method="init" destroy-method="destroy" class="com.service.impl.UserServiceImpl" />

取值:

  • 当 scope="singleton" 时,spring 容器中有且仅有一个对象,init 方法在创建容器时仅执行一次。

  • 当 scope="prototype" 时,spring 容器要创建同一类型的多个对象,init 方法在每个对象创建时均执行一次。

  • 当 scope="singleton" 时,关闭容器会导致 bean 实例的销毁,调用 destroy 方法一次。

  • 当 scope="prototype" 时,对象的销毁由垃圾回收机制 gc() 控制,destroy 方法将不会被执行。

4. set 方法注入 bean(主流方式)

  • 名称:property

  • 类型:标签

  • 归属:bean 标签

  • 作用:使用 set 方法的形式为 bean 提供资源

  • 示例:

<bean>
    <property name="propertyName" value="propertyValue" ref="beanId"/>
</bean>
  • 基本属性:

    • name:对应 bean 中的属性名,要求该属性必须提供可访问的 set 方法(严格规范为此名称是 set 方法对应名称)

    • value:设定非引用类型属性对应的值,不能与 ref 同时使用。

    • ref:设定引用类型属性对应 bean 的 id ,不能与value同时使用。

  • 注意:一个 bean 可以有多个 property 标签。

代码示例:

  • UserDaoImpl.java:
    public void save(){
        System.out.println("user dao running...");
    }
  • UserServiceImpl.java:
import com.dao.UserDao;
import com.service.UserService;

public class UserServiceImpl implements UserService {

    private UserDao userDao;
    private int num;

    // 对需要进行注入的变量(基本数据类型)添加set方法
    public void setNum(int num) {
        this.num = num;
    }

    // 对需要进行注入的变量(引用数据类型)添加set方法
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void save() {
        System.out.println("user service running..." + num);  // user dao running...666
        userDao.save();  // user dao running...
    }
}

5. 集合类型数据注入

  • 名称:array、list、set、map、props

  • 类型:标签

  • 归属:property 标签或 constructor-arg 标签

  • 作用:注入集合数据类型属性

示例:

  • BookDaoImpl.java:
package com.dao.impl;

import com.dao.BookDao;

import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

public class BookDaoImpl implements BookDao {

    private List al;
    private Properties properties;
    private int[] arr;
    private Set hs;
    private Map hm ;

    public void setAl(List al) {
        this.al = al;
    }

    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    public void setArr(int[] arr) {
        this.arr = arr;
    }

    public void setHs(Set hs) {
        this.hs = hs;
    }

    public void setHm(Map hm) {
        this.hm = hm;
    }

    @Override
    public void save() {
        System.out.println("book dao running...");
        System.out.println("ArrayList:"+al);
        System.out.println("Properties:"+properties);
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
        System.out.println("HashSet:"+hs);
        System.out.println("HashMap:"+hm);
    }
}
  • UserServiceImpl.java:
package com.service.impl;

import com.dao.BookDao;
import com.service.UserService;

public class UserServiceImpl implements UserService {

    private BookDao bookDao;

    public UserServiceImpl() {
    }

    public UserServiceImpl(BookDao bookDao) {
        this.bookDao = bookDao;
    }

    public void setBookDao(BookDao bookDao) {
        this.bookDao = bookDao;
    }

    @Override
    public void save() {
        bookDao.save();
    }

}
  • applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 创建spring控制的资源 -->
    <bean id="userService" class="com.service.impl.UserServiceImpl">
        <property name="bookDao" ref="bookDao"/>
    </bean>

    <bean id="bookDao" class="com.dao.impl.BookDaoImpl">
        <property name="al">
            <list>
                <value>value1</value>
                <value>value2</value>
            </list>
        </property>
        <property name="properties">
            <props>
                <prop key="name">xiaoming</prop>
                <prop key="age">17</prop>
            </props>
        </property>
        <property name="arr">  <!-- 不常用 -->
            <array>
                <value>12</value>
                <value>34</value>
            </array>
        </property>
        <property name="hs">  <!-- 不常用 -->
            <set>
                <value>value1</value>
                <value>value2</value>
            </set>
        </property>
        <property name="hm">  <!-- 不常用 -->
            <map>
                <entry key="name" value="xiaoming"/>
                <entry key="age" value="19"/>
            </map>
        </property>
    </bean>

</beans>
  • UserApp.java(控制层):
package com.servlet;

import com.service.UserService;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class UserApp {

    public static void main(String[] args) {
        // 加载配置文件
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        // 获取资源
        UserService userService = (UserService)context.getBean("userService");
        userService.save();
    }
}
  • 执行结果:
book dao running...
ArrayList:[value1, value2]
Properties:{age=17, name=xiaoming}
12
34
HashSet:[value1, value2]
HashMap:{name=xiaoming, age=19}

6. 加载 properties 文件

Spring 提供了读取外部 properties 文件的机制,使用读取到的数据为 bean 的属性赋值。

  • resource 目录下的 data.properties:
username=xiaoming
password=admin123
  • applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 1.加载context命名空间的支持 -->
    <!-- xmlns:context="http://www.springframework.org/schema/context"
         http://www.springframework.org/schema/context
         https://www.springframework.org/schema/context/spring-context.xsd
    -->
    <!-- 2.加载配置文件:*表示加载全部文件 -->
    <context:property-placeholder location="classpath:*.properties"/>

    <!-- 创建spring控制的资源 -->
    <bean id="userService" class="com.service.impl.UserServiceImpl">
        <property name="userDao" ref="userDao"/>
    </bean>

    <bean id="userDao" class="com.dao.impl.UserDaoImpl">
        <!-- 读取数据使用 ${propertiesName} 格式进行,其中 propertiesName 指 properties 文件中的属性值 -->
        <property name="userName" value="${username}"/>
        <property name="password" value="${password}"/>
    </bean>

</beans>
  • UserDaoImpl.java:
package com.dao.impl;

import com.dao.UserDao;

public class UserDaoImpl implements UserDao {

    private String userName;
    private String password;

    public void setUserName(String userName) {
        this.userName = userName;
    }

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

    @Override
    public void save() {
        System.out.println("userDao running:"+userName+" "+password);
    }
}
  • UserServiceImpl.java:
package com.service.impl;

import com.dao.BookDao;
import com.dao.UserDao;
import com.service.UserService;

public class UserServiceImpl implements UserService {

    private UserDao userDao;

    public UserServiceImpl() {
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    @Override
    public void save() {
        userDao.save();
    }

}
  • UserApp.java(控制层):
package com.servlet;

import com.service.UserService;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class UserApp {

    public static void main(String[] args) {
        // 加载配置文件
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        // 获取资源
        UserService userService = (UserService)context.getBean("userService");
        userService.save();
    }
}
  • 运行结果:
userDao running:juno admin123

7. 团队开发:import 导入配置文件

  • 名称:import

  • 类型:标签

  • 归属:beans 标签

  • 作用:在当前配置文件中导入其他配置文件中的项

示例:在 applicationContext.xml 中加载其他配置文件。

<import resource="applicationContext-user.xml"/>
<import resource="applicationContext-book2.xml"/>
<import resource="applicationContext-book.xml"/>

Spring 容器加载多个配置文件:

new ClassPathXmlApplicationContext("config1.xml", "config2.xml");

Spring 容器中 bean 定义冲突问题:

  • 同 id 的 bean,后定义的覆盖先定义的。

  • 导入配置文件可以理解为将导入的配置文件复制粘贴到对应位置。

  • 导入配置文件的顺序与位置不同可能会导致最终程序运行结果不同。

8. ApplicationContext 分析

  1. ApplicationContext 是一个接口,提供了访问 Spring 容器的 API 。

  2. ClassPathXmlApplicationContext 是一个类,实现了上述功能。

  3. ApplicationContext 的顶层接口是 BeanFactory 。

  4. BeanFactory 定义了 bean 相关的最基本操作。

  5. ApplicationContext 在 BeanFactory 基础上追加了若干新功能。

ApplicationContext 对比 BeanFactory:

  1. BeanFactory 创建的 bean 采用延迟加载形式,使用才创建;

  2. ApplicationContext 创建的 bean 默认采用立即加载形式。

FileSystemXmlApplicationContext:

可以加载文件系统中任意位置的配置文件,而 ClassPathXmlApplicationContext 只能加载类路径下的配置文件。

示例:BeanFactory 创建 Spring 容器。

Resource res = new ClassPathResource("applicationContext.xml");
BeanFactory bf = new XmlBeanFactory(res);
UserService userService = (UserService)bf.getBean("userService");

四、Spring 注解

1、注解简介

注解开发的好处:使用注解的形式替代 xml 配置,将繁杂的 Spring 配置文件从工程中彻底消除掉,简化书写。

注解驱动的弊端:

  • 为了达成注解驱动的目的,可能会将原先很简单的书写,变得更加复杂。

  • XML 中配置第三方开发的资源是很方便的,但使用注解驱动无法在第三方开发的资源中进行编辑,因此会增大开发工作量。

2、常用注解

Spring 原始注解:主要是替代 <Bean> 的配置。

注解说明
@Component使用在类上用于实例化 Bean
@Controller使用在 web 层类上用于实例化 Bean
@Service使用在 service 层类上用于实例化 Bean
@Repository使用在 dao 层类上用于实例化 Bean
@Autowired使用在字段上用于根据类型依赖注入
@Qualifier结合 @Autowired 一起使用,用于根据名称进行依赖注入引用类型
@Resource相当于 @Autowired + @Qualifier,按照名称进行注入引用类型
@Value注入普通类型的属性
@Scope标注 Bean 的作用范围
@PostConstruct使用在方法上标注该方法是 Bean 的初始化方法
@PreDestroy使用在方法上标注该方法是 Bean 的销毁方法

Spring 新注解:

使用上面的注解还不能全部替代 xml 配置文件,还需要使用注解替代的配置如下:

  • 非自定义的 bean 的配置:<bean>
  • 加载 properties 文件的配置:<context:property-placeholder>
  • 组件扫描的配置:<context:component-scan>
  • 引入其他文件:<import>
注解说明
@Configuration用于指定当前类是一个 Spring 配置类,当创建容器时会从该类上加载注解。用于指定 Spring 在初始化容器时要扫描的包。
@ComponentScan作用和在 Spring 的 xml 配置文件中的 <context:component-scan base-package="com.itheima"/> 一样。
@Bean使用在方法上,标注将该方法的返回值存储到 Spring 容器中。
@PropertySource用于加载 .properties 文件中的配置。
@Import用于导入其他配置类。

1. 启用注解功能

  • 启动注解扫描,加载类中配置的注解项:
<context:component-scan base-package="packageName"/>
  • 说明:

    • 在进行包所扫描时,会对配置的包及其子包中所有文件进行扫描。

    • 扫描过程是以文件夹递归迭代的形式进行的。

    • 扫描过程仅读取合法的 java 文件。

    • 扫描时仅读取 spring 可识别的注解。

    • 扫描结束后会将可识别的有效注解转化为 spring 对应的资源加入 IoC 容器。

  • 注意:

    • 无论是注解格式还是 XML 配置格式,最终都是将资源加载到 IoC 容器中,差别仅仅是数据读取方式不同。

    • 从加载效率上来说,注解优于 XML 配置文件。

2. bean 定义:@Component、@Controller、@Service、@Repository

  • 类型:类注解

  • 位置:类定义上方。

  • 作用:设置该类为 spring 管理的 bean 。

  • 示例:

@Component
public class ClassName{}
  • 说明:@Controller、@Service 、@Repository 是 @Component 的衍生注解,功能同 @Component 。

  • 相关属性:

    • value(默认) :定义 bean 的访问 id 。

3. bean 的引用类型属性注入:@Autowired、@Qualifier

  • 类型:属性注解、方法注解

  • 位置:属性定义上方,方法定义上方。

  • 作用:设置对应属性的对象或对方法进行引用类型传参。

  • 说明:@Autowired 默认按类型装配,指定 @Qualifier 后则可以指定装配的 bean 的 id 。

  • 相关属性:

    • required:定义该属性是否允许为 null 。

4. bean 的引用类型属性注入:@Inject、@Named、@Resource

  • 说明:

    • @Inject 与 @Named 是 JSR330 规范中的注解,功能与 @Autowired 和 @Qualifier 完全相同,适用于不同架构场景。
    • @Resource 是 JSR250 规范中的注解,可以简化书写格式。
  • @Resource 相关属性:

    • name:设置注入的 bean 的 id 。

    • type:设置注入的 bean 的类型,接收的参数为 Class 类型。

5. bean 优先级注入:@Primary

  • 类型:类注解

  • 位置:类定义上方。

  • 作用:设置类对应的 bean 按类型装配时优先装配。

  • 说明:@Autowired 默认按类型装配,当出现相同类型的 bean,使用 @Primary 会提高按类型自动装配的优先级,但多个 @Primary 会导致优先级设置无效。

6. bean 的非引用类型属性注入:@Value

  • 类型:属性注解、方法注解

  • 位置:属性定义上方,方法定义上方。

  • 作用:设置对应属性的值或对方法进行传参。

  • 说明:

    • value 值仅支持非引用类型数据,赋值时对方法的所有参数全部赋值。

    • value 值支持读取 properties 文件中的属性值,通过类属性将 properties 中数据传入类中。

    • value 值支持 SpEL 。

    • @value 注解如果添加在属性上方,可以省略 set 方法(set 方法的目的是为属性赋值)。

7. bean 的作用域:@Scope

  • 类型:类注解

  • 位置:类定义上方。

  • 作用:设置该类作为 bean 对应的 scope 属性。

  • 相关属性

    • value(默认):定义 bean 的作用域,默认为 singleton 。

8. bean 的生命周期:@PostConstruct、@PreDestroy

  • 类型:方法注解

  • 位置:方法定义上方。

  • 作用:设置该类作为 bean 对应的生命周期方法。

9. 加载第三方资源:@Bean

  • 类型:方法注解

  • 位置:方法定义上方。

  • 作用:设置该方法的返回值作为 spring 管理的 bean 。

  • 范例:

@Bean("dataSource")
public DruidDataSource createDataSource() {    return ……;    }
  • 说明:

    • 因为第三方 bean 无法在其源码上进行修改,因此可以使用 @Bean 解决第三方 bean 的引入问题。

    • 该注解用于替代 XML 配置中的静态工厂与实例工厂创建 bean,不区分方法是否为静态或非静态。

    • @Bean 所在的类必须被 spring 扫描加载,否则该注解无法生效。

  • 相关属性

    • value(默认):定义 bean 的访问 id 。

10. 加载 properties 文件:@PropertySource

  • 类型:类注解

  • 位置:类定义上方。

  • 作用:加载 properties 文件中的属性值。

  • 范例:

@PropertySource(value="classpath:jdbc.properties")
public class ClassName {
    @Value("${propertiesAttributeName}")
    private String attributeName;
}
  • 说明:不支持*通配格式,一旦加载,所有 spring 控制的 bean 中均可使用对应属性值

  • 相关属性

    • value(默认):设置加载的 properties 文件名。

    • ignoreResourceNotFound:如果资源未找到,是否忽略,默认为 false 。

11. 纯注解开发:@Configuration、@ComponentScan

  • 类型:类注解

  • 位置:类定义上方。

  • 作用:设置当前类为 spring 核心配置加载类(不再需要 spring 配置文件)。

  • 范例:

@Configuration
@ComponentScan("scanPackageName")
public class SpringConfigClassName{
}
  • 说明:

    • 核心配合类用于替换 spring 核心配置文件,此类可以设置空的,不设置变量与属性。

    • bean 扫描工作使用注解 @ComponentScan 替代。

  • 加载纯注解格式上下文对象,需要使用 AnnotationConfigApplicationContext:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);

12. 导入第三方配置:@Import

  • 类型:类注解

  • 位置:类定义上方。

  • 作用:导入第三方 bean 作为 spring 控制的资源。

  • 范例:

@Configuration
@Import(OtherClassName.class)
public class ClassName {
}

说明:

  • @Import 注解在同一个类上,仅允许添加一次,如果需要导入多个,使用数组的形式进行设定。

  • 在被导入的类中可以继续使用 @Import 导入其他资源(了解)。

  • @Bean 所在的类可以使用导入的形式进入 spring 容器,无需声明为 bean 。

13. 综合案例

maven 依赖:

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.1.9.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.16</version>
        </dependency>
    </dependencies>
  • spring 配置类

DataSourceConfig.java:

package com.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;

// 数据源配置类
// 相当于 <context:property-placeholder location="classpath:jdbc.properties"/>,且不能用通配符*
@PropertySource("classpath:jdbc.properties")
public class DataSourceConfig {

    @Value("${jdbc.driver}")
    private static String driver;
    @Value("${jdbc.url}")
    private static String url;
    @Value("${jdbc.username}")
    private static String username;
    @Value("${jdbc.password}")
    private static String password;

    @Bean("dataSource")  // 将方法的返回值放置Spring容器中
    public static DruidDataSource getDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driver);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }
}

SpringConfig.java:

package com.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

// Spring核心配置类
@Configuration
@ComponentScan("com")  // 相当于 <context:component-scan base-package="com"/>
@Import({DataSourceConfig.class})  // 相当于 <import resource=""/>
public class SpringConfig {

}
  • dao 层

UserDaoImpl.java:

package com.dao.impl;

import com.dao.UserDao;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;

// 相当于 <bean id="UserDao" ref="com.dao.impl.UserDaoImpl"/>
// @Component("userDao")
@Repository("userDao")
public class UserDaoImpl implements UserDao {
    @Override
    public void save() {
        System.out.println("UserDao save...");
    }
}
  • service 层

UserServiceImpl.java:

package com.service.impl;

import com.dao.UserDao;
import com.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import javax.sql.DataSource;

// 相当于 <bean id="UserService" ref="com.service.impl.UserServiceImpl"/>
// @Component("userService")
@Service("userService")
public class UserServiceImpl implements UserService {

    // <property name="userDao" ref="userDao"></property>
    // @Autowired  // 可单独使用,按照数据类型从spring容器中进行匹配的(有多个相同数据类型的bean时则会有匹配问题)
    // @Qualifier("userDao")  // 指定bean的id从spring容器中匹配,且要结合@Autowired一起用
    @Resource(name="userDao")  // 相当于 @Autowired+@Autowired
    UserDao userDao;

    @Resource(name="dataSource")
    DataSource dataSource;

    @Value("${jdbc.driver}")  // 读取配置文件中的值
    private String driver;

    /* 使用注解开发可以省略set方法,使用配置文件则不能省略
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
    */

    @Override
    public void save() {
        System.out.println("driver: "+driver);
        System.out.println("dataSource: "+dataSource);
        userDao.save();
    }

    @PostConstruct
    public void init() {
        System.out.println("service对象的初始化方法");
    }

    @PreDestroy
    public void destroy() {
        System.out.println("service对象的销毁方法");
    }
}

controller 层

App.java:

package com.controller;

import com.config.SpringConfig;
import com.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App {

    public static void main(String[] args) {
        // ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        UserService userService = (UserService)context.getBean("userService");
        userService.save();
        context.close();
    }
}

运行结果:

service对象的初始化方法
dataSource: {
    CreateTime:"2021-12-03 01:05:00",
    ActiveCount:0,
    PoolingCount:0,
    CreateCount:0,
    DestroyCount:0,
    CloseCount:0,
    ConnectCount:0,
    Connections:[
    ]
}
UserDao save...
service对象的销毁方法

3、整合第三方技术

1. 注解整合 Mybatis

注解整合 MyBatis 的开发步骤:

  1. 修改 mybatis 外部配置文件格式为注解格式;
  2. 业务类使用 @Component 声明 bean,使用 @Autowired 注入对象;
  3. 建立配置文件 JDBCConfig 与 MyBatisConfig 类,并将其导入到核心配置类 SpringConfig;
  4. 开启注解扫描;
  5. 使用 AnnotationConfigApplicationContext 对象加载配置项。

核心内容如下:

  • Maven 依赖:
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.1.9.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.11</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>4.3.8.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.16</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.3.0</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.3</version>
        </dependency>
  • MybatisConfig.java(Mybatis 配置类):
package com.config;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;

public class MybatisConfig {

    /*
    <!-- spring整合mybatis后,创建连接用的对象 -->
    <bean class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="typeAliasesPackage" value="com.domain"/>
    </bean>

    <!-- 扫描mybatis映射配置,将其作为spring的bean进行管理 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.dao"/>
    </bean>
     */

    // 以下注解代替以上配置文件内容:返回值会作为Spring容器的bean
    @Bean
    public SqlSessionFactoryBean getSqlSessionFactoryBean(@Autowired DataSource dataSource) {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setTypeAliasesPackage("com.domain");
        return sqlSessionFactoryBean;
    }

    @Bean
    public MapperScannerConfigurer getMapperScannerConfigurer() {
        MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
        mapperScannerConfigurer.setBasePackage("com.dao");
        return mapperScannerConfigurer;
    }

}
  • JdbcConfig.java(数据源配置类):
package com.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;

public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String userName;
    @Value("${jdbc.password}")
    private String password;

    @Bean("dataSource")
    public DataSource getDataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(userName);
        ds.setPassword(password);
        return ds;
    }
}
  • SpringConfig.java(Spring 核心配置类):
package com.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@ComponentScan("com")  // 相当于 <context:component-scan base-package="com"/>
@Import({JdbcConfig.class, MybatisConfig.class})  // 相当于 <import resource=""/>
public class SpringConfig {

}

2. 注解整合 Junit

注解整合 Junit 的开发步骤:

  1. Spring 接管 Junit 的运行权,使用 Spring 专用的 Junit 类加载器;

2.为 Junit 测试用例设定对应的 Spring 容器:

  • 从 Spring 5.0 以后,要求 Junit 的版本必须是 4.12 或以上。

  • Junit 仅用于单元测试,不能将 Junit 的测试类配置成 Spring 的 bean,否则该配置将会被打包进入工程中。

示例:整合 Junit5。

  • Maven 依赖:
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.8.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.1.9.RELEASE</version>
        </dependency>
  • 测试类:
package com.service;

import com.config.SpringConfig;
import com.domain.User;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.List;

// 设定spring专用的类加载器
@ExtendWith(SpringExtension.class)
// 设定加载的spring上下文对应的配置
@ContextConfiguration(classes=SpringConfig.class)
public class UserServiceTest {

    @Autowired
    UserService userService;

    @Test
    public void testFindById() {
        User user = userService.findById(1);
        // System.out.println(user);
        Assertions.assertEquals("Mick", user.getName());
    }

    @Test
    public void testFindAll() {
        List<User> users = userService.findAll();
        Assertions.assertEquals(3, users.size());
    }

}

4、IoC底层核心原理

1. IoC 核心接口

2. 组件扫描器:@ComponentScan

组件扫描器:开发过程中,需要根据需求加载必要的 bean 或排除指定 bean。

应用场景:

  • 数据层接口测试
  • 业务层接口测试
  • 各种运行环境设置

配置扫描器:

  • 名称:@ComponentScan

  • 类型:类注解

  • 位置:类定义上方

  • 作用:设置 spring 配置加载类扫描规则

  • 范例:

@Configuration
@ComponentScan(
    value="com",  // 设置基础扫描路径
    excludeFilters =      // 设置过滤规则,当前为排除过滤
    @ComponentScan.Filter(            // 设置过滤器
        type= FilterType.ANNOTATION,  // 设置过滤方式为按照注解进行过滤
        classes=Repository.class)     // 设置具体的过滤项。如不加载所有@Repository修饰的bean
)
public class SpringConfig {

}
  • includeFilters:设置包含性过滤器

  • excludeFilters:设置排除性过滤器

  • type:设置过滤器类型(过滤策略)

    • ANNOTATION
    • ASSIGNABLE_TYPE
    • ASPECTJ
    • REGEX
    • CUSTOM

自定义扫描器:

  • 名称:TypeFilter

  • 类型:接口

  • 作用:自定义类型过滤器

示例:

  • 自定义扫描器
public class MyTypeFilter implements TypeFilter {
    public boolean match(MetadataReader mr, MetadataReaderFactory mrf) throws IOException {
        ClassMetadata cm = metadataReader.getClassMetadata();
        tring className = cm.getClassName();
        if(className.equals("com.itheima.dao.impl.BookDaoImpl")){
            return true;  // 进行过滤(拦截)
        }
        return false;  // 不过滤(放行)
    }
}
  • 配置类
@Configuration
@ComponentScan(
        value = "com",
        excludeFilters = @ComponentScan.Filter(
                type= FilterType.CUSTOM,
                classes = MyTypeFilter.class
        )
)
public class SpringConfig {

}

3. 自定义导入器:ImportSelector

bean 只有通过配置才可以进入 spring 容器,被 spring 加载并控制。配置 bean 的方式如下:

  • XML 文件中使用 <bean/> 标签配置

  • 使用 @Component 及衍生注解配置

企业开发过程中,通常需要配置大量的 bean,因此需要一种快速高效配置大量 bean 的方式。

ImportSelector 注解:

  • 类型:接口

  • 作用:自定义 bean 导入器(导入未加 @Component 注解的 bean)

示例:

  • 自定义导入器:
public class MyImportSelector implements ImportSelector {
    public String[] selectImports(AnnotationMetadata icm) {
        // 返回需要导入的bean数组。该bean即使没加@Component注解也能被扫描识别
        return new String[]{"com.dao.impl.AccountDaoImpl"};
    }
}
  • 配置类:
@Configuration
@ComponentScan("com")
@Import(MyImportSelector.class)  // 导入自定义导入器
public class SpringConfig {
}

自定义导入器的封装工具类:

import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.filter.AspectJTypeFilter;
import org.springframework.core.type.filter.TypeFilter;

import java.io.IOException;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

public class CustomerImportSelector implements ImportSelector {

    private String expression;

    public CustomerImportSelector(){
        try {
            //初始化时指定加载的properties文件名
            Properties loadAllProperties = PropertiesLoaderUtils.loadAllProperties("import.properties");
            //设定加载的属性名
            expression = loadAllProperties.getProperty("path");
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        //1.定义扫描包的名称
        String[] basePackages = null;
        //2.判断有@Import注解的类上是否有@ComponentScan注解
        if (importingClassMetadata.hasAnnotation(ComponentScan.class.getName())) {
            //3.取出@ComponentScan注解的属性
            Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(ComponentScan.class.getName());
            //4.取出属性名称为basePackages属性的值
            basePackages = (String[]) annotationAttributes.get("basePackages");
        }
        //5.判断是否有此属性(如果没有ComponentScan注解则属性值为null,如果有ComponentScan注解,则basePackages默认为空数组)
        if (basePackages == null || basePackages.length == 0) {
            String basePackage = null;
            try {
                //6.取出包含@Import注解类的包名
                basePackage = Class.forName(importingClassMetadata.getClassName()).getPackage().getName();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            //7.存入数组中
            basePackages = new String[] {basePackage};
        }
        //8.创建类路径扫描器
        ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
        //9.创建类型过滤器(此处使用切入点表达式类型过滤器)
        TypeFilter typeFilter = new AspectJTypeFilter(expression,this.getClass().getClassLoader());
        //10.给扫描器加入类型过滤器
        scanner.addIncludeFilter(typeFilter);
        //11.创建存放全限定类名的集合
        Set<String> classes = new HashSet<>();
        //12.填充集合数据
        for (String basePackage : basePackages) {
            scanner.findCandidateComponents(basePackage).forEach(beanDefinition -> classes.add(beanDefinition.getBeanClassName()));
        }
        //13.按照规则返回
        return classes.toArray(new String[classes.size()]);
    }
}

4. 自定义注册器:ImportBeanDefinitionRegistrar

  • 类型:接口

  • 作用:自定义 bean 定义注册器(识别标记了 @Component 的 bean)

示例:

  • 自定义注册器:
// 表示com目录下的bean全部注册
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    public void registerBeanDefinitions(AnnotationMetadata icm, BeanDefinitionRegistry r) {
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(r, false);
        TypeFilter tf = new TypeFilter() {
            public boolean match(MetadataReader mr, MetadataReaderFactory mrf) throws IOException {
                return true;
            }
        };
        scanner.addIncludeFilter(tf);  // 包含
        // scanner.addExcludeFilter(tf);  // 排除
        scanner.scan("com");
    }
}
  • 配置类:
@Configuration
@Import(MyImportBeanDefinitionRegistrar.class)  // 作用等同于 @ComponentScan("com")
public class SpringConfig {
}

封装工具类:

import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.filter.AspectJTypeFilter;
import org.springframework.core.type.filter.TypeFilter;

import java.io.IOException;
import java.util.Map;
import java.util.Properties;

public class CustomeImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    private String expression;

    public CustomeImportBeanDefinitionRegistrar(){
        try {
            //初始化时指定加载的properties文件名
            Properties loadAllProperties = PropertiesLoaderUtils.loadAllProperties("import.properties");
            //设定加载的属性名
            expression = loadAllProperties.getProperty("path");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //1.定义扫描包的名称
        String[] basePackages = null;
        //2.判断有@Import注解的类上是否有@ComponentScan注解
        if (importingClassMetadata.hasAnnotation(ComponentScan.class.getName())) {
            //3.取出@ComponentScan注解的属性
            Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(ComponentScan.class.getName());
            //4.取出属性名称为basePackages属性的值
            basePackages = (String[]) annotationAttributes.get("basePackages");
        }
        //5.判断是否有此属性(如果没有ComponentScan注解则属性值为null,如果有ComponentScan注解,则basePackages默认为空数组)
        if (basePackages == null || basePackages.length == 0) {
            String basePackage = null;
            try {
                //6.取出包含@Import注解类的包名
                basePackage = Class.forName(importingClassMetadata.getClassName()).getPackage().getName();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            //7.存入数组中
            basePackages = new String[] {basePackage};
        }
        //8.创建类路径扫描器
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, false);
        //9.创建类型过滤器(此处使用切入点表达式类型过滤器)
        TypeFilter typeFilter = new AspectJTypeFilter(expression,this.getClass().getClassLoader());
        //10.给扫描器加入类型过滤器
        scanner.addIncludeFilter(typeFilter);
        //11.扫描指定包
        scanner.scan(basePackages);
    }
}

5. bean 初始化过程解析

bean 统一初始化:

  • BeanFactoryPostProcessor

    • 作用:定义了在 bean 工厂对象创建后,bean 对象创建前执行的动作,用于对工厂进行创建后业务处理。

    • 运行时机:当前操作用于对工厂进行处理,仅运行一次。

  • BeanPostProcessor

    • 作用:定义了所有 bean 初始化前后进行的统一动作,用于对 bean 进行创建前业务处理与创建后业务处理。

    • 运行时机:当前操作伴随着每个 bean 的创建过程,每次创建 bean 均运行该操作。

  • InitializingBean

    • 作用:定义了每个 bean 的初始化前进行的动作,属于非统一性动作,用于对 bean 进行创建前业务处理。

    • 运行时机:当前操作伴随着任意一个 bean 的创建过程,保障其个性化业务处理。

  • 注意:上述操作均需要被 spring 容器加载方可运行。

示例:

  • BeanFactoryPostProcessor:
package com.post;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;

public class MyBeanFactory implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        System.out.println("Bean工厂制作好了");
    }
}
  • BeanPostProcessor:
package com.post;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;

public class MyBean implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("bean之前巴拉巴拉");
        System.out.println(beanName);
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("bean之后巴拉巴拉");
        return bean;
    }
}
  • InitializingBean:
package com.service.impl;

import com.dao.UserDao;
import com.domain.User;
import com.service.UserService;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

@Service("userService")
public class UserServiceImpl implements InitializingBean {

    // 定义当前bean初始化操作,功效等同于init-method属性配置
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("UserServiceImpl......bean ...init......");
    }
}
  • 运行结果:
Bean工厂制作好了
bean之前巴拉巴拉
springConfig
bean之后巴拉巴拉
bean之前巴拉巴拉
com.config.DataSourceConfig
bean之后巴拉巴拉
bean之前巴拉巴拉
dataSource
bean之后巴拉巴拉
bean之前巴拉巴拉
getSqlSessionFactoryBean
bean之后巴拉巴拉
bean之后巴拉巴拉
bean之前巴拉巴拉
userDao
bean之后巴拉巴拉
bean之后巴拉巴拉
bean之前巴拉巴拉
userService
UserServiceImpl......bean ...init......
bean之后巴拉巴拉
bean之前巴拉巴拉
com.service.UserServiceTest.ORIGINAL
bean之后巴拉巴拉

6. 单个 bean 初始化

  • FactoryBean:对单一的 bean 的初始化过程进行封装,达到简化配置的目的。

FactoryBean 与 BeanFactory 区别:

  • FactoryBean:封装单个 bean 的创建过程。通常是为了创建另一个 bean 而做的准备工作。

  • BeanFactory:Spring 容器顶层接口,统一定义了 bean 相关的获取操作。

示例:

import org.springframework.beans.factory.FactoryBean;

public class UserServiceImplFactoryBean implements FactoryBean {

    // 重点:返回数据
    @Override
    public Object getObject() throws Exception {
        return new UserServiceImpl();
    }

    @Override
    public Class<?> getObjectType() {
        return null;
    }

    @Override
    public boolean isSingleton() {
        return false;
    }
}

五、Spring AOP

1、Spring AOP 简介

AOP(Aspect Oriented Programming)意为:面向切面编程,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。

AOP 编程思想:

  • 问题一:业务方法日后会很多,会有很多重复的代码。
  • 问题二:已经存在很多的方法,并没有考虑到事务的问题,现在要求加上。

1. AOP 作用

AOP 是 OOP(面向对象编程)的延续,是软件开发中的一个热点,也是 Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。

利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

  • AOP 采取横向抽取机制,取代了传统纵向继承体系的重复性代码。
  • 经典应用:事务管理、性能监视、安全检查、缓存、日志等。

小结:

  • 作用:在程序运行期间,在不修改源码的情况下对方法进行功能增强。
  • 优势:减少重复代码,提高开发效率,且便于维护。

2. AOP 底层实现

AOP 底层采用代理机制进行实现。

静态代理:

  • Proxy Pattern(代理模式),是 23 种常用的面向对象软件的设计模式之一。

  • 代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

  • 优点:

    1. 职责清晰。
      真实的角色就是实现实际的业务逻辑,不用关心其他非本职责的事务,通过后期的代理完成一件完成事务,附带的结果就是编程简洁清晰。
    2. 代理对象可以在客户端和目标对象之间起到中介的作用,这样起到了的作用和保护了目标对象的作用。
    3. 高扩展性。
  • 结构:一个是真正的你要访问的对象(目标类),另一个是代理对象,真正对象与代理对象实现同一个接口,先访问代理类再访问真正要访问的对象。

1)静态代理

装饰者模式(Decorator Pattern):在不惊动原始设计的基础上,为其添加功能。

public class UserServiceDecorator implements UserService{
    private UserService userService;
    public UserServiceDecorator(UserService userService) {
        this.userService = userService;
    }
    public void save() {
        //原始调用
        userService.save();
        //增强功能(后置)
        System.out.println("刮大白");
    }
}

 2)动态代理

  • 动态代理它可以直接给某一个目标对象生成一个代理对象,而不需要代理类存在。
  • 动态代理与代理模式原理是一样的,只是它没有具体的代理类,直接通过反射生成了一个代理对象。

(1)JDK 动态代理

JDK 动态代理是针对对象做代理,要求原始对象具有接口实现,并对接口方法进行增强。

public class UserServiceJDKProxy {
    public UserService createUserServiceJDKProxy(final UserService userService){
        // 获取被代理对象的类加载器
        ClassLoader classLoader = userService.getClass().getClassLoader();
        // 获取被代理对象实现的接口
        Class[] classes = userService.getClass().getInterfaces();
        // 对原始方法执行进行拦截并增强
        InvocationHandler ih = new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // 前置增强内容
                Object ret = method.invoke(userService, args);
                // 后置增强内容
                System.out.println("刮大白2");
                return ret;
            }
        };
        // 使用原始被代理对象创建新的代理对象
        UserService proxy = (UserService) Proxy.newProxyInstance(classLoader,classes,ih);
        return proxy;
    }
}

(2)CGLIB 动态代理

  • CGLIB(Code Generation Library):Code 生成类库。

  • CGLIB 动态代理不限定是否具有接口,可以对任意操作进行增强。

  • CGLIB 动态代理无需要原始被代理对象,动态创建出新的代理对象。

public class UserServiceImplCglibProxy {
    public static UserServiceImpl createUserServiceCglibProxy(Class clazz){
        // 创建Enhancer对象(可以理解为内存中动态创建了一个类的字节码)
        Enhancer enhancer = new Enhancer();
        // 设置Enhancer对象的父类是指定类型UserServerImpl
        enhancer.setSuperclass(clazz);
        Callback cb = new MethodInterceptor() {
            public Object intercept(Object o, Method m, Object[] a, MethodProxy mp) throws Throwable {
                Object ret = mp.invokeSuper(o, a);
                if(m.getName().equals("save")) {
                    System.out.println("刮大白");
                }
                return ret;
            }
        };
        // 设置回调方法
        enhancer.setCallback(cb);
        // 使用Enhancer对象创建对应的对象
        return (UserServiceImpl)enhancer.create();
    }
}

3)Spring 代理模式选择

  • Spring AOP 使用纯 Java 实现,不需要专门的编译过程和类加载器,在运行期通过代理方式向目标类织入增强代码。
  • AspectJ 是一个基于 Java 语言的 AOP 框架。从 Spring2.0 开始,Spring AOP 引入对 Aspect 的支持,AspectJ 扩展了 Java 语言,提供了一个专门的编译器,在编译时提供横向代码的织入。

Spirng 可以通过配置的形式控制使用的代理形式,默认使用 JDKProxy,通过配置可以修改为使用 cglib 。

  • XML 配置:
<!--XMP配置AOP-->
<aop:config proxy-target-class="false"></aop:config>
  • XML 注解支持:
<!--注解配置AOP-->
<aop:aspectj-autoproxy proxy-target-class="false"/>
  • 注解驱动:
//注解驱动
@EnableAspectJAutoProxy(proxyTargetClass = true)

4)织入时机

3. AOP 术语

  1. target:目标类,即需要被代理的类。例如:UserService
  2. proxy:一个类被 AOP 织入增强后,就成为一个代理类。
  3. Joinpoint(连接点):所谓连接点是指那些可能被拦截到的方法。例如:所有的方法
  4. PointCut(切入点):要被增强的连接点。例如:addUser()
  5. advice(通知/增强):即增强的代码内容。例如:after()、before()
  6. Weaving(织入):是指把增强(advice)应用到目标对象(target)来创建新的代理对象(proxy)的过程。
  7. Aspect(切面):是切入点(pointcut)和通知(advice)的结合。
    • 一个线是一个特殊的面。
    • 一个切入点和一个通知,组成一个特殊的面。

4. AOP 开发明确事项

需要编写的内容:

  1. 编写核心业务代码(目标类的目标方法)。
  2. 编写切面类,切面类中有通知(增强功能方法)。
  3. 在配置文件中,配置织入关系,即将哪些通知与哪些连接点进行结合。

AOP 技术实现的内容:

  • Spring 框架监控切入点方法的执行:一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行

AOP 底层使用哪种代理方式:

  • 在 Spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。

总结:

AOP:面向切面编程

AOP 底层实现:

  • 基于 JDK 的动态代理
  • 基于 Cglib 的动态代理

AOP 重点概念:

  • Pointcut(切入点):被增强的方法
  • Advice(通知/增强)封装增强业务逻辑的方法 Aspect(切面):切点+通知
  • Weaving(织入):将切点与通知结合的过程

开发明确事项:

  • 谁是切点(切点表达式配置)
  • 谁是通知(切面类中的增强方法)
  • 将切点和通知进行织入配置

2、AOP 配置

AOP 配置开发步骤:

  1. 导入 AOP 相关坐标;
  2. 创建目标接口和目标类(内部有切点);
  3. 创建切面类(内部有增强方法);
  4. 将目标类和切面类的对象创建权交给 Spring ;
  5. 在 applicationContext.xml 中配置织入关系;
  6. 测试代码。

1. 切点表达式

切点表达式的配置语法:

execution([修饰符] 返回值类型 包名.类名.方法名(参数))

  • 访问修饰符可以省略
  • 返回值类型、包名、类名、方法名可以使用星号 * 表示任意。
  • 包名与类名之间一个点代表当前包下的类,两个点..表示当前包及其子包下的类。
  • 参数列表可以使用两个点表示任意个数,任意类型的参数列表。

示例:

execution(public void com.aop.Target.method())
execution(void com.aop.Target.*(..))
exeaution(* com.aop.*.*(..))  // 常用
exeaution(* com.aop..*.*(..))
execution(* *..*.*(..))

2. 通知类型

通知的配置语法:

<aop:通知类型 method="切面类中的方法名" pointcut="切点表达式"></aop:通知类型>
名称标签说明
前置通知aop:before用于配置前置通知。指定增强的方法在切入点方法之前执行
后置通知aop:after-returning用于配置后置通知。指定增强的方法在切入点方法之后执行
环绕通知aop:around用于配置环绕通知。指定增强的方法在切入点方法之前和之后都会执行
异常抛出通知aop:throwing用于配置异常执出通知。指定增强的方法在出现异常时执行
最终通知aop:after用于配置最终通知。无论增强方式执行是否有异常都会执行

3. 切点表达式的抽取

<aop:config>
    <!--配置公共切入点-->
    <aop:pointcut id="pt1" expression="execution(* *(..))"/>
    <aop:aspect ref="myAdvice">
        <!--配置局部切入点-->
        <aop:pointcut id="pt2" expression="execution(* *(..))"/>
        <!--引用公共切入点-->
        <aop:before method="logAdvice" pointcut-ref="pt1"/>
        <!--引用局部切入点-->
        <aop:before method="logAdvice" pointcut-ref="pt2"/>
        <!--直接配置切入点-->
        <aop:before method="logAdvice" pointcut="execution(* *(..))"/>
    </aop:aspect>
</aop:config>

4. 案例

目标类:

  • 目标接口
package com.aop;

public interface Target {

    public void targetSave();
}
  • 目标实现类
package com.aop;

public class TargetImpl implements Target {

    @Override
    public void targetSave() {
        System.out.println("target save...");
        int i = 1/0;
    }
}

切面类:

package com.aop;

import org.aspectj.lang.ProceedingJoinPoint;

public class MyAspect {

    public void before() {
        System.out.println("MyAspect before ...");
    }

    public void afterReturn() {
        System.out.println("MyAspect afterReturn ...");
    }

    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // ProceedingJoinPoint:正在执行的连接点=切点
        System.out.println("MyAspect around before ...");
        // 切点方法
        Object proceed = proceedingJoinPoint.proceed();
        System.out.println("MyAspect around after ...");
        return proceed;
    }

    public void afterException() {
        System.out.println("MyAspect afterException ...");
    }

    public void after() {
        System.out.println("MyAspect afterFinal ...");
    }

}

Spring 配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
">

    <!-- 目标对象 -->
    <bean id="target" class="com.aop.TargetImpl"></bean>
    <!-- 切面对象 -->
    <bean id="myAspect" class="com.aop.MyAspect"></bean>

    <!-- 配置织入:告诉spring框架 哪些方法(切点)需要进行哪些增强(前置、后置等...) -->
    <!-- 一个beans标签中可以配置多个aop:config标签 -->
    <aop:config>
        <!-- 声明切面 -->
        <!-- 一个aop:config标签中可以配置多个aop:aspect标签,且该标签可以配置在aop:aspect标签内 -->
        <aop:aspect ref="myAspect">
            <!-- 抽取切点表达式 -->
            <aop:pointcut id="myPointCut" expression="execution(* com.aop.*.*(..))"/>
            <!-- 切面=切点+通知 -->
            <!-- 前置增强功能在myAspect的before方法中实现的 -->
<!--            <aop:before method="before" pointcut="execution(public void com.aop.*.*(..))"></aop:before>-->
<!--            <aop:after-returning method="afterReturn" pointcut="execution(* com.aop.*.*(..))"></aop:after-returning>-->
<!--            <aop:around method="around" pointcut="execution(* com.aop.*.*(..))"></aop:around>-->
<!--            <aop:after-throwing method="afterException" pointcut="execution(* com.aop.*.*(..))"></aop:after-throwing>-->
<!--            <aop:after method="after" pointcut="execution(* com.aop.*.*(..))"></aop:after>-->
            <aop:around method="around" pointcut-ref="myPointCut"></aop:around>
            <aop:after-throwing method="afterException" pointcut-ref="myPointCut"></aop:after-throwing>
            <aop:after method="after" pointcut-ref="myPointCut"></aop:after>
        </aop:aspect>
    </aop:config>
</beans>

测试:

package com.aop;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class AspectTest {

    @Autowired
    private Target target;

    @Test
    public void testAspect() {
        target.targetSave();
        /*
        运行结果:
            MyAspect before ...
            target save...
         */
    }
}

运行结果:

MyAspect around before ...
target save...
MyAspect afterException ...
MyAspect afterFinal ...

3、AOP 注解使用

基于注解的 AOP 开发步骤:

  1. 创建目标接口和目标类(内部有切点);
  2. 创建切面类(内部有增强方法);
  3. 将目标类和切面类的对象创建权交给 spring;
  4. 在切面类中使用注解配置织入关系;
  5. 在配置文件中开启组件扫描和 AOP 的自动代理测试(也可以在 Spring 注解配置类中开启 AOP 注解驱动)。

1. 常用注解

名称注解说明
切面@Aspect标注切面类
AOP 自动代理@EnableAspectJAutoProxy设置 Spring 注解配置类开启 AOP 注解驱动的支持,加载AOP注解
前置通知@Before用于配置前置通知。指定增强的方法在切入点方法之前执行
后置通知@AfterReturning用于配置后置通知。指定增强的方法在切入点方法之后执行
环绕通知@Around用于配置环绕通知。指定增强的方法在切入点方法之前和之后都执行
异常抛出通知@AfterThrowing用于配置异常抛出通知。指定增强的方法在出现异常时执行
最终通知@After用于配置最终通知。无论增强方式执行是否有异常都会执行
切点表达式抽取@Pointcut可以引用已抽取的切点表达式

2. 案例

目标类:

  • 目标接口
package com.aop;

public interface Target {

    public void targetSave();
}
  • 目标实现类

切面类:

package com.aop.anno;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component("myAspect")
@Aspect  // 标注是一个切面类
public class MyAspect {

    // 配置前置通知
    @Before("execution(* com.aop.anno.*.*(..))")
    public void before() {
        System.out.println("MyAspect before ...");
    }

    @AfterReturning("execution(* com.aop.anno.*.*(..))")
    public void afterReturn() {
        System.out.println("MyAspect afterReturn ...");
    }

    // @Around("execution(* com.aop.anno.*.*(..))")
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // ProceedingJoinPoint:正在执行的连接点=切点
        System.out.println("MyAspect around before ...");
        // 切点方法
        Object proceed = proceedingJoinPoint.proceed();
        System.out.println("MyAspect around after ...");
        return proceed;
    }

    // @AfterThrowing("execution(* com.aop.anno.*.*(..))")
    public void afterException() {
        System.out.println("MyAspect afterException ...");
    }

    // @After("execution(* com.aop.anno.*.*(..))")
    // 切面类中定义的切入点只能在当前类中使用,如果想引用其他类中定义的切入点使用“类名.方法名()”引用
    @After("MyAspect.pointCut()")
    public void after() {
        System.out.println("MyAspect afterFinal ...");
    }

    // 切入点最终体现为一个方法,无参无返回值,无实际方法体内容,但不能是抽象方法
    @Pointcut("execution(* com.aop.anno.*.*(..))")
    public void pointCut() {
    }
}

Spring 配置:

配置类 or 配置文件(二选一)

  • 配置类
package com.aop.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan("com.aop.anno")
@EnableAspectJAutoProxy
public class SpringConfig {
}
  • 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd
">

    <!-- 组件扫描 -->
    <context:component-scan base-package="com.aop.anno"/>
    <!-- AOP 自动代理 -->
    <aop:aspectj-autoproxy/>
</beans>

测试:

import com.aop.anno.Target;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
//@ContextConfiguration("classpath:applicationContext-anno.xml")
@ContextConfiguration(classes=com.aop.config.SpringConfig.class)
public class AspectTest {

    @Autowired
    private Target target;

    @Test
    public void testAspect() {
        target.targetSave();
    }
}

运行结果:

MyAspect around before ...
MyAspect before ...
target save...
MyAspect around after ...
MyAspect afterFinal ...
MyAspect afterReturn ...

六、Spring 事务

1、Spring 事务核心对象

J2EE 开发使用分层设计的思想:

  • 对于简单的业务层转调数据层的单一操作,事务开启在业务层或者数据层并无太大差别。
  • 当业务中包含多个数据层的调用时,需要在业务层开启事务,即对数据层中多个操作进行组合并归属于同一个事务进行处理。

Spring 为业务层提供了整套的事务解决方案:

  1. PlatformTransactionManager
  2. TransactionDefinition
  3. TransactionStatus

1. PlatformTransactionManager

平台事务管理器实现类:

  • DataSourceTransactionManager:适用于 Spring JDBC 或 MyBatis
  • HibernateTransactionManager:适用于 Hibernate3.0 及以上版本
  • JpaTransactionManager:适用于 JPA
  • JdoTransactionManager:适用于 JDO
  • JtaTransactionManager:适用于 JTA

标准介绍:

  • JPA(Java Persistence API):Java EE 标准之一,为 POJO 提供持久化标准规范,并规范了持久化开发的统一 API,符合 JPA 规范的开发可以在不同的 JPA 框架下运行。

  • JDO(Java Data Object):是 Java 对象持久化规范,用于存取某种数据库中的对象,并提供标准化 API。与 JDBC 相比,JDBC 仅针对关系数据库进行操作,而 JDO 可以扩展到关系数据库、文件、XML、对象数据库(ODBMS)等,可移植性更强。

  • JTA(Java Transaction API):Java EE 标准之一,允许应用程序执行分布式事务处理。与 JDBC 相比,JDBC 事务则被限定在一个单一的数据库连接,而一个 JTA 事务可以有多个参与者,比如 JDBC 连接、JDO 等都可以参与到一个 JTA 事务中。

PlatformTransactionManager 接口定义了事务的基本操作:

// 获取事务
TransactionStatus getTransaction(TransactionDefinition definition)

// 提交事务
void commit(TransactionStatus status) 

// 回滚事务
void rollback(TransactionStatus status)

2. TransactionDefinition

此接口定义了事务的基本信息:

// 获取事务定义名称
String getName()

// 获取事务的读写属性
boolean isReadOnly()

// 获取事务隔离级别
int getIsolationLevel()

// 获事务超时时间
int getTimeout()

// 获取事务传播行为特征
int getPropagationBehavior()

3. TransactionStatus

此接口定义了事务在执行过程中某个时间点上的状态信息及对应的状态操作:

2、事务控制方式

银行转账业务说明:

  • 银行转账操作中,涉及从 A 账户到 B 账户的资金转移操作。

  • 本案例的数据层仅提供单条数据的基础操作,未涉及多账户间的业务操作。

案例环境:本案例环境基于 Spring、Mybatis 整合。

  • 业务层接口提供转账操作:
/**
* 转账操作
* @param outName     出账用户名
* @param inName      入账用户名
* @param money       转账金额
*/
public void transfer(String outName, String inName, Double money);
  • 业务层实现提供转账操作:
public void transfer(String outName, String inName, Double money){
    accountDao.inMoney(outName, money);                                                       accountDao.outMoney(inName, money);
}
  • 数据层提供对应的入账与出账操作:
<update id="inMoney">
    update account set money = money + #{money} where name = #{name}
</update>
<update id="outMoney">
    update account set money = money - #{money} where name = #{name}
</update>

1. 编程式事务

public void transfer(String outName,String inName,Double money){
    //创建事务管理器
    DataSourceTransactionManager dstm = new DataSourceTransactionManager();
    //为事务管理器设置与数据层相同的数据源
    dstm.setDataSource(dataSource);
    //创建事务定义对象
    TransactionDefinition td = new DefaultTransactionDefinition();
    //创建事务状态对象,用于控制事务执行
    TransactionStatus ts = dstm.getTransaction(td);
    accountDao.inMoney(outName,money);
    int i = 1/0;    //模拟业务层事务过程中出现错误
    accountDao.outMoney(inName,money);
    //提交事务
    dstm.commit(ts);
}

使用 AOP 控制事务:

将业务层的事务处理功能抽取出来制作成 AOP 通知,利用环绕通知运行期动态织入。

public Object tx(ProceedingJoinPoint pjp) throws Throwable {
    
    DataSourceTransactionManager dstm = new DataSourceTransactionManager();
    dstm.setDataSource(dataSource);
    TransactionDefinition td = new DefaultTransactionDefinition();
    TransactionStatus ts = dstm.getTransaction(td);
    Object ret = pjp.proceed(pjp.getArgs());
    dstm.commit(ts);
    
    return ret;
}

配置 AOP 通知类,并注入 dataSource:

<bean id="txAdvice" class="com.aop.TxAdvice">
    <property name="dataSource" ref="dataSource"/>
</bean>

使用环绕通知将通知类织入到原始业务对象执行过程中:

<aop:config>
    <aop:pointcut id="pt" expression="execution(* *..transfer(..))"/>
    <aop:aspect ref="txAdvice">
        <aop:around method="tx" pointcut-ref="pt"/>
    </aop:aspect>
</aop:config>

2. 声明式事务(XML)

思考:AOP 配置事务是否具有特例性?如不同的读写操作配置不同的事务类型。

public Object tx(ProceedingJoinPoint pjp) throws Throwable {
    DataSourceTransactionManager dstm = new DataSourceTransactionManager();
    dstm.setDataSource(dataSource);
    TransactionDefinition td = new DefaultTransactionDefinition();
    TransactionStatus ts = dstm.getTransaction(td);
    Object ret = pjp.proceed(pjp.getArgs());
    dstm.commit(ts);

    return ret;
}
<bean id="txAdvice" class="com.aop.TxAdvice">
    <property name="dataSource" ref="dataSource"/>
</bean>

使用 tx 命名空间配置事务专属通知类:

<tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
        <tx:method name="*" read-only="false" />
        <tx:method name="get*" read-only="true" />
        <tx:method name="find*" read-only="true" />
    </tx:attributes>
</tx:advice>

使用 aop:advisor 在 AOP 配置中引用事务专属通知类:

<aop:config>
    <aop:pointcut id="pt" expression="execution(* *..*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/>
</aop:config>

aop:advice 与 aop:advisor 区别:

aop:advice:配置的通知类可以是普通 java 对象,即不实现接口也不使用继承关系。

aop:advisor:配置的通知类必须实现通知接口。

  • MethodBeforeAdvice
  • AfterReturningAdvice
  • ThrowsAdvice
  • ……

tx:advice

  • 类型:标签

  • 归属:beans 标签

  • 作用:专用于声明式事务通知

  • 格式:

<beans>
    <tx:advice id="txAdvice" transaction-manager="txManager">
    </tx:advice>
</beans>
  • 基本属性:

    • id:用于配置 aop 时指定通知器的 id

    • transaction-manager:指定事务管理器 bean

tx:attributes

  • 类型:标签

  • 归属:tx:advice 标签

  • 作用:定义通知属性

  • 格式:

<tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
    </tx:attributes>
</tx:advice>
  • 基本属性:

tx:method

  • 类型:标签

  • 归属:tx:attribute 标签

  • 作用:设置具体的事务属性

  • 格式:

<tx:attributes>
    <tx:method name="*" read-only="false" />
    <tx:method name="get*" read-only="true" />
</tx:attributes>
  • 说明:

    • 通常事务属性会配置多个,包含 1 个读写的全事务属性,1 个只读的查询类事务属性。
  • 基本属性:

3. 声明式事务(注解)

@Transactional

  • 类型:法注解、类注解、接口注解

  • 位置:方法定义上方、类定义上方、接口定义上方

  • 作用:设置当前类/接口中所有方法或具体方法开启事务,并指定相关事务属性

  • 范例:

@Transactional(
    readOnly = false,
    timeout = -1,
    isolation = Isolation.DEFAULT,
    rollbackFor = {ArithmeticException.class, IOException.class},
    noRollbackFor = {},
    propagation = Propagation.REQUIRES_NEW
)
void 接口/类名称();

方式一:配置开启注解驱动(tx:annotation-driven)。

  • 类型:标签

  • 归属:beans 标签

  • 作用:开启事务注解驱动,并指定对应的事务管理器

  • 范例:

<tx:annotation-driven transaction-manager="txManager"/>

方式二:纯注解驱动。

  • 名称:@EnableTransactionManagement

  • 类型:类注解

  • 位置:Spring 注解配置类上方

  • 作用:开启注解驱动,等同XML格式中的注解驱动

  • 范例:

// Spring 核心配置类
@Configuration
@ComponentScan("com")
@PropertySource("classpath:jdbc.properties")
@Import({JDBCConfig.class, MyBatisConfig.class, TransactionManagerConfig.class})  // 引入事务配置类
@EnableTransactionManagement
public class SpringConfig {
}
// 事务配置类
public class TransactionManagerConfig {
    @Bean
    public PlatformTransactionManager getTransactionManager(@Autowired DataSource dataSource){
        return new DataSourceTransactionManager(dataSource);
    }
}

3、事务传播行为

务传播行为描述的是事务协调员对事务管理员所携带事务的处理态度。

事务传播行为的类型: 

事务传播行为的应用:

场景 A:生成订单业务

  • 子业务 S1:记录日志到数据库表 X

  • 子业务 S2:保存订单数据到数据库表 Y

  • 子业务 S3:……

  • 如果 S2 或 S3 或 …… 事务提交失败,此时S1是否回滚?如何控制?

  • (S1 需要新事务)

场景 B:生成订单业务

  • 背景 1:订单号生成依赖数据库中一个专门用于控制订单号编号生成的表 M 获取

  • 背景 2:每次获取完订单号,表 M 中记录的编号自增 1

  • 子业务 S1:从表 M 中获取订单编号

  • 子业务 S2:保存订单数据,订单编号来自于表 M

  • 子业务 S3:……

  • 如果 S2 或 S3 或 …… 事务提交失败,此时 S1 是否回滚?如何控制?

  • (S1 需要新事务)

4、Spring 模板对象

1. 常见的 Spring 模块对象

  • TransactionTemplate

  • JdbcTemplate

  • RedisTemplate

  • RabbitTemplate

  • JmsTemplate

  • HibernateTemplate

  • RestTemplate

2. RedisTemplate

public void changeMoney(Integer id, Double money) {
    redisTemplate.opsForValue().set("account:id:" + id,money);
}
public Double findMondyById(Integer id) {
    Object money = redisTemplate.opsForValue().get("account:id:" + id);
    return new Double(money.toString());
}

3. JdbcTemplate

提供标准的 SQL 语句操作 API 。

public void save(Account account) {
    String sql = "insert into account(name,money)values(?,?)";
    jdbcTemplate.update(sql,account.getName(),account.getMoney());
}

4. NamedParameterJdbcTemplate

提供标准的 SQL 语句操作 API 。

public void save(Account account) {
    String sql = "insert into account(name,money)values(:name,:money)";
    Map pm = new HashMap();
    pm.put("name", account.getName());
    pm.put("money", account.getMoney());
    jdbcTemplate.update(sql, pm);
}

5、Spring 事务底层原理

策略模式(Strategy Pattern):根据不同的策略对象来实现不同的行为方式,策略对象的变化导致行为的变化。

示例:装饰模式应用。

  • JdbcTemplate
  • NamedParameterJdbcTemplate

七、SpringMVC 

1、SpringMVC  简介

1. SSM 三层架构

  • 表现层:负责数据展示

  • 业务层:负责业务处理

  • 数据层:负责数据操作

2. MVC 简介

MVC(Model View Controller)是一种用于设计及创建 Web 应用程序表现层的模式。

  • Model(模型):数据模型,用于封装数据

  • View(视图):页面视图,用于展示数据

    • jsp
    • html
  • Controller(控制器):处理用户交互的调度器,用于根据用户需求处理程序逻辑

    • Servlet
    • SpringMVC

3. SpringMVC 简介

SpringMVC 是一种基于 Java 实现的、MVC 模型的、轻量级的 Web 框架。

SpringMVC 优点:

  1. 使用简单
  2. 性能突出(相比现有的框架技术)
  3. 灵活性强

4. 入门案例

SpringMVC 工作流程分析:

  1. 服务器启动:
    1. 加载 web.xml 中 DispatcherServlet;
    2. 读取 spring-mvc.xml 中的配置,加载所有 com 包中所有标记为 bean 的类;
    3. 读取 bean 中方法上方标注 @RequestMapping 的内容;
  2. 处理请求:
    1. DispatcherServlet 配置拦截所有请求“/”;
    2. 使用请求路径与所有加载的 @RequestMapping 的内容进行比对;
    3. 执行对应的方法;
    4. 根据方法的返回值在 webapp 目录中查找对应的页面并展示。

导入 SpringMVC 相关的 Maven 依赖:

<!-- servlet3.1规范的坐标 -->
<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.1</version>
    <scope>provided</scope>
</dependency>
<!--spring的坐标-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.1.9.RELEASE</version>
</dependency>
<!--spring web的坐标-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>5.1.9.RELEASE</version>
</dependency>
<!--springmvc的坐标-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.1.9.RELEASE</version>
</dependency>

定义表现层处理器 Controller(等同于 Servlet),并配置成 Spring 的 bean:

@Controller
public class UserController {

    public void save(){
        System.out.println("user mvc controller is running ...");
    }
}

定义 SpringMVC 配置文件(格式与 Spring 配置文件一致):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
    <!--扫描加载所有的控制类类-->
    <context:component-scan base-package="com"/>

</beans>

web.xml 中配置 SpringMVC 核心控制器,用于将请求转发到对应的具体业务处理器 Controller 中(等同于 Servlet 配置):

<servlet>
    <servlet-name>DispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:spring-mvc.xml</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>DispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

设定具体 Controller 的访问路径与返回页面(等同于 Servlet 在 web.xml 中的配置):

// 设定当前方法的访问映射地址
@RequestMapping("/save")
// 设置当前方法返回值类型为String,用于指定请求完成后跳转的页面
public String save(){
    System.out.println("user mvc controller is running ...");
    // 设定具体跳转的页面
    return "success.jsp";
}

5. Spring 技术架构

  1. DispatcherServlet(前端控制器):是整体流程控制的中心,由其调用其它组件处理用户的请求,有效降低了组件间的耦合性。
  2. HandlerMapping(处理器映射器):负责根据用户请求找到对应具体的 Handler 处理器。
  3. Handler(处理器:业务处理的核心类,通常由开发者编写,描述具体的业务。
  4. HandlAdapter(处理器适配器):通过它对处理器进行执行。
  5. View Resolver(视图解析器):将处理结果生成 View 视图。
  6. View(视图):最终产出结果,常用视图如 jsp、html。

2、SpringMVC 基础配置

1. 常规配置

1)Controller 加载控制

SpringMVC 的处理器对应的 bean 必须按照规范格式开发,为了避免加入无效的 bean,可通过 bean 加载过滤器进行包含设定或排除设定。

例如,表现层 bean 标注通常设定为 @Controller,因此可以通过注解名称进行过滤控制:

<context:component-scan base-package="com">
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

2)静态资源加载

<!--放行指定类型静态资源配置方式-->
<mvc:resources mapping="/img/**" location="/img/"/>
<mvc:resources mapping="/js/**" location="/js/"/>
<mvc:resources mapping="/css/**" location="/css/"/>

<!--SpringMVC提供的通用资源放行方式-->
<mvc:default-servlet-handler/>

3)中文乱码处理

web.xml:

<!-- 乱码处理过滤器,与Servlet中使用的完全相同,差异之处在于处理器的类由Spring提供 -->
<filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

2. 注解驱动

目标:删除 web.xml 和 spring-mvc.xml 。

注意:

实现示例:

使用注解形式,将 SpringMVC 核心配置文件替换为配置类。

package com.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.Controller;

@Configuration
@ComponentScan(
        value="com",
        includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes={Controller.class})
)
public class SpringMvcConfig implements WebMvcConfigurer {

    // 注解配置放行指定资源格式
    // @Override
    // public void addResourceHandlers(ResourceHandlerRegistry registry) {
    //     registry.addResourceHandler("/img/**").addResourceLocations("/img/");
    //     registry.addResourceHandler("/js/**").addResourceLocations("/js/");
    //     registry.addResourceHandler("/css/**").addResourceLocations("/css/");
    // }

    // 注解配置通用放行资源的格式
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();;
    }
}

替换 web.xml:基于 servlet3.0 规范,自定义 Servlet 容器初始化配置类,加载 SpringMVC 核心配置类。

package com.config;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer;

import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import java.util.EnumSet;
import java.util.Objects;

public class ServletInitConfig extends AbstractDispatcherServletInitializer {
    /*
    创建 Servlet 容器时,使用注解的方式加载 SpringMVC 配置类中的信息,并加载成 Web 专用的 ApplicationContext 对象
    该对象放入了 ServletContext 范围,后期在整个 Web 容器中可以随时获取调用
     */
    @Override
    protected WebApplicationContext createServletApplicationContext() {
        AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
        ctx.register(SpringMvcConfig.class);
        return ctx;
    }

    // 注解配置映射地址方式,服务于 SpringMVC 的核心控制器 DispatcherServlet
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    @Override
    protected WebApplicationContext createRootApplicationContext() {
        return null;
    }

    // 乱码处理作为过滤器,在 servlet 容器启动时进行配置
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        super.onStartup(Objects.<ServletContext>requireNonNull(servletContext));
        CharacterEncodingFilter cef = new CharacterEncodingFilter();
        cef.setEncoding("UTF-8");
        FilterRegistration.Dynamic registration = servletContext.addFilter("characterEncodingFilter", cef);
        registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD,
                DispatcherType.INCLUDE), false, "/*");
    }
}

3、请求

1. 请求映射:@RequestMapping

@RequestMapping 使用:

  • 类型:类注解;方法注解
  • 位置:处理器类定义上方;处理器类中的方法定义上方
  • 作用:为当前处理器中所有方法设定公共的访问路径前缀;绑定请求地址与对应处理方法间的关系
// 示例:方法注解
@Controller
public class UserController {
    // 访问 URI:/user/requestURL1
    @RequestMapping("/requestURL1")
    public String requestURL2() {
        return "page.jsp";
    }
}

// 示例:类注解
@Controller
@RequestMapping("/user")
public class UserController {
    // 访问 URI:/user/requestURL2
    @RequestMapping("/requestURL2")
    public String requestURL2() {
        return "page.jsp";
    }
}

常用属性:

@RequestMapping(
    value="/requestURL3",  // 设定请求路径,与path属性、value属性相同
    method = RequestMethod.GET,  // 设定请求方式
    params = "name",  // 设定请求参数条件
    headers = "content-type=text/*",  // 设定请求消息头条件
    consumes = "text/*",  // 用于指定可以接收的请求正文类型(MIME类型)
    produces = "text/*"  // 用于指定可以生成的响应正文类型(MIME类型)
)
public String requestURL3() {
    return "/page.jsp";
}

2. 普通类型传参

// URL 访问:http://localhost:8080/requestParam1?name=xiaoming&age=14
@RequestMapping("/requestParam1")
public String requestParam1(String name ,String age){
    System.out.println("name="+name+", age="+age);
    return "page.jsp";
}

3. @RequestParam

  • 类型:形参注解
  • 位置:处理器类中的方法形参前方
  • 作用:绑定请求参数与处理方法形参间的关系

示例:

// http://localhost:8080/requestParam2?userName=Jock
@RequestMapping("/requestParam2")
public String requestParam2(@RequestParam(
                            name = "userName",
                            required = true,
                            defaultValue = "xiaohuang") String name) {
    System.out.println("name="+name);
    return "page.jsp";
}
  • 当未传参即直接访问“/requestParam2”时,方法会取默认值“xiaohuang”。
  • 当配置了“required=true”但未配置“defaultValue”时,访问时不传参则会报 400 错。

4. 对象类型传参

1)POJO

当使用 POJO(简单 Java 对象)时,传参名称与 POJO 类属性名保持一致即可。

  • POJO 类
package com.bean;

public class User {

    private String name;
    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}
  • Controller
// URL 访问:http://localhost:8080/requestParam3?name=xiaodong&age=18
@RequestMapping("/requestParam3")
public String requestParam3(User user){
    System.out.println("name="+user.getName());
    return "page.jsp";
}

2)参数冲突

当 POJO 的属性与其他形参出现同名问题时,将被同时赋值。

// 访问 URL:http://localhost:8080/requestParam4?name=xiaoyi&age=14
@RequestMapping("/requestParam4")
public String requestParam4(User user, String age){
    System.out.println("user.age="+user.getAge()+", age="+age);  // user.age=14, age=14
    return "page.jsp";
}

建议使用 @RequestParam 注解进行区分。

3)复杂对象类型

当对象中出现对象属性时,则要求入参名称与对象层次结构名称保持一致。

4)对象集合

当复杂对象中出现用 List 保存对象数据时,要求入参名称与对象层次结构名称保持一致,并使用数组格式描述集合中对象的位置。

  • bean:
public class User {
    private String name;
    private Integer age;
    private List<Address> addresses;
}

public class Address {
    private String province;
    private String city;
    private String address;
}
  • Controller:
// 访问URL:http://localhost:8080/requestParam7?addresses[0].province=bj&addresses[1].province=tj
@RequestMapping("/requestParam7")
public String requestParam7(User user){
    System.out.println("user.addresses="+user.getAddresses());
    return "page.jsp";
}

注意:The valid characters are defined in RFC 7230 and RFC 3986问题解决

当复杂对象中出现用 Map 保存对象数据时,要求入参名称与对象层次结构名称保持一致,并使用映射格式描述集合中对象的位置。

  • bean
public class User {
    private String name;
    private Integer age;
    private Map<String, Address> addressMap;
}

public class Address {
    private String province;
    private String city;
    private String address;
}
  • controller
// 访问 URL:http://localhost:8080/requestParam8?addressMap['home'].province=bj&addressMap['job'].province=tj
@RequestMapping("/requestParam8")
public String requestParam8(User user){
    System.out.println("user.addressMap="+user.getAddressMap());
    return "page.jsp";
}

5. 数组集合类型传参

// 访问 URL:http://localhost:8080/requestParam9?nick=xiaoming1&nick=xiaoming2
@RequestMapping("/requestParam9")
public String requestParam9(String[] nick){
    System.out.println(nick[0]+", "+nick[1]);  // xiaoming1, xiaoming2
    return "page.jsp";
}

// 访问 URL:http://localhost:8080/requestParam10?nick=xiaoming1&nick=xiaoming2
@RequestMapping("/requestParam10")
public String requestParam10(@RequestParam("nick") List<String> nick){
    System.out.println(nick);  // [xiaoming1, xiaoming2]
    return "page.jsp";
}

注意:

  • SpringMVC 默认将 List 作为对象处理,赋值前先创建对象,然后将 nick 作为对象的属性进行处理。而由于 List 是接口,无法创建对象,报无法找到构造方法异常;修复类型为可创建对象的 ArrayList 类型后,对象可以创建,但没有 nick 属性,因此数据为空。

  • 此时需要告知 SpringMVC 的处理器 nick 是一组数据,而不是一个单一数据。

  • 因此通过 @RequestParam 注解,将数量大于 1 个的 names 参数打包成参数数组后, SpringMVC 才能识别该数据格式,并判定形参类型是否为数组或集合,并按数组或集合对象的形式操作数据。

6. 类型转换器

SpringMVC 会对接收的参数进行自动类型转换,该工作通过 Converter 接口实现。

 

标量转换器:

  • StringToBooleanConverter String —> Boolean
  • ObjectToStringConverter Object —> String
  • StringToNumberConverterFactory String —> Number( Integer、Long 等)
  • NumberToNumberConverterFactory Number子类型之间(Integer、Long、Double 等)
  • StringToCharacterConverter String —> java.lang.Character
  • NumberToCharacterConverter Number子类型(Integer、Long、Double 等) —> java.lang.Character
  • CharacterToNumberFactory java.lang.Character —> Number 子类型(Integer、Long、Double 等)
  • StringToEnumConverterFactory String —> enum 类型
  • EnumToStringConverter enum 类型 —> String
  • StringToLocaleConverter String —> java.util.Local
  • PropertiesToStringConverter java.util.Properties —> String
  • StringToPropertiesConverter String —> java.util.Properties

集合、数组相关转换器:

  • ArrayToCollectionConverter 数组 —> 集合(List、Set)
  • CollectionToArrayConverter 集合(List、Set) —> 数组
  • ArrayToArrayConverter(数组间转换)
  • CollectionToCollectionConverter 集合间(List、Set)
  • MapToMapConverter Map间
  • ArrayToStringConverter 数组 —> String 类型
  • StringToArrayConverter String —> 数组(实现方式为 trim 后使用 "," 进行 split)
  • ArrayToObjectConverter 数组 —> Object
  • ObjectToArrayConverter Object —> 单元素数组
  • CollectionToStringConverter 集合(List、Set) —> String
  • StringToCollectionConverter String —> 集合(List、Set)(实现方式为 trim 后使用 "," 进行 split)
  • CollectionToObjectConverter 集合 —> Object
  • ObjectToCollectionConverter Object —> 单元素集合

默认转换器:

  • ObjectToObjectConverter(Object 间转换)
  • IdToEntityConverter Id —> Entity
  • FallbackObjectToStringConverter Object —> String

7. 日期类型格式转换

配置版:声明自定义的转换格式并覆盖系统转换格式。

<!-- 启用自定义Converter -->
<mvc:annotation-driven conversion-service="conversionService"/>
<!-- 1.设定格式类型Converter,注册为Bean,受SpringMVC管理 -->
<bean id="conversionService"
      class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <!-- 2.自定义Converter格式类型设定,该设定使用的是同类型覆盖的思想 -->
    <property name="formatters">
        <!-- 3.使用set保障相同类型的转换器仅保留一个,避免冲突 -->
        <set>
            <!-- 4.设置具体的格式类型 -->
            <bean class="org.springframework.format.datetime.DateFormatter">
                <!-- 5.类型规则 -->
                <property name="pattern" value="yyyy-MM-dd"/>
            </bean>
        </set>
    </property>
</bean>

注解版:

  • 名称:@DateTimeFormat
  • 类型:形参注解、成员变量注解
  • 位置:形参前面或成员变量上方
  • 作用:为当前参数或变量指定类型转换规则
  • 注意:依赖注解驱动支持(<mvc:annotation-driven />)
  • 范例:
// 形参前
// 访问 URL:http://localhost:8080/requestParam11?date=2021-12-12
@RequestMapping("/requestParam11")
public String requestParam11(@DateTimeFormat(pattern="yyyy-MM-dd") Date date){
    System.out.println("date="+date);
    return "page.jsp";
}

// 成员变量上方
@DateTimeFormat(pattern="yyyy-MM-dd")
private Date birthday;

8. 自定义类型转换器

1)实现 Converter 接口,并制定转换前与转换后的类型:

  • 配置
  <!-- 1.将自定义Converter注册为Bean,受SpringMVC管理 -->
  <bean id="myDateConverter" class="com.itheima.converter.MyDateConverter"/>
  <!-- 2.设定自定义Converter服务bean -->
  <bean id="conversionService"
        class="org.springframework.context.support.ConversionServiceFactoryBean">
      <!-- 3.注入所有的自定义Converter,该设定使用的是同类型覆盖的思想 -->
      <property name="converters">
          <!-- 4.set保障同类型转换器仅保留一个,去重规则以Converter<S,T>的泛型为准 -->
          <set>
              <!-- 5.具体的类型转换器 -->
              <ref bean="myDateConverter"/>
          </set>
      </property>
  </bean>
  • 实现类
// 自定义类型转换器,实现Converter接口,接口中指定的泛型即为最终作用的条件
// 本例中的泛型填写的是<String,Date>,最终出现字符串转日期时,该类型转换器生效
public class MyDateConverter implements Converter<String, Date> {
    // 重写接口的抽象方法,参数由泛型决定
    public Date convert(String source) {
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
        Date date = null;
        // 类型转换器无法预计使用过程中出现的异常,因此必须在类型转换器内部捕获,不允许抛出,框架无法预计此类异常如何处理
        try {
            date = df.parse(source);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }
}

2)通过注册自定义转换器,将该功能加入到 SpringMVC 的转换服务 ConverterService 中:

<!-- 开启注解驱动,加载自定义格式化转换器对应的类型转换服务 -->
<mvc:annotation-driven conversion-service="conversionService"/>

4、响应

1. 页面跳转:转发与重定向

    // 转发
    @RequestMapping("/showPage1")
    public String showPage1() {
        System.out.println("user mvc controller is running ...");
        return "forward:page.jsp";  // 支持访问WEB-INF下的页面
    }

    // 重定向
    @RequestMapping("/showPage2")
    public String showPage2() {
        System.out.println("user mvc controller is running ...");
        return "redirect:page.jsp";  // 不支持访问WEB-INF下的页面
    }

请求转发与重定向的区别:

  • 当使用请求转发时,Servlet 容器将使用一个内部的方法来调用目标页面,新的页面继续处理同一个请求,而浏览器将不会知道这个过程(即服务器行为)。与之相反,重定向的含义是第一个页面通知浏览器发送一个新的页面请求。因为当使用重定向时,浏览器中所显示的 URL 会变成新页面的 URL(浏览器行为)。而当使用转发时,该 URL 会保持不变。

  • 重定向的速度比转发慢,因为浏览器还得发出一个新的请求。

  • 同时,由于重定向产生了一个新的请求,所以经过一次重定向后,第一次请求内的对象将无法使用。

总结:

  • 重定向:两次请求,浏览器行为,地址栏改变,请求域中的数据会丢失。
  • 请求转发:一次请求,服务器行为,地址栏不变,请求域中的数据不丢失。

怎么选择是重定向还是转发呢?

  • 通常情况下转发更快,而且能保持请求内的对象,所以它是第一选择。但是由于在转发之后,浏览器中 URL 仍然指向开始页面,此时如果重载当前页面,开始页面将会被重新调用。如果不想看到这样的情况,则选择重定向。

  • 不要仅仅为了把变量传到下一个页面而使用 session 作用域,那会无故增大变量的作用域,转发也许可以帮助解决这个问题。

    • 重定向:以前的请求中存放的变量全部失效,并进入一个新的请求作用域。
    • 转发:以前的请求中存放的变量不会失效,就像把两个页面拼到了一起。

2. 页面访问快捷设定(InternalResourceViewResolver)

通常,展示页面的保存位置比较固定且结构相似,因此可以设定通用的访问路径,来简化页面配置。

示例:

<!-- 设定页面加载的前缀后缀,仅适用于默认形式,不适用于手工标注转发或重定向的方式 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/pages/"/>
    <property name="suffix" value=".jsp"/>
</bean>
public String showPage3() {
    return "page";
}

而如果未设定返回值,使用 void 类型,则默认使用访问路径来拼接前后缀:

// 最简页面配置方式:使用访问路径作为返回的页面名称,省略返回值
@RequestMapping("/showPage5")
public void showPage5() {
    System.out.println("user mvc controller is running ...");
}

3. 带数据页面跳转

  • 实体类:
public class Book {
    private String name;
    private Double price;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}
  • 控制层:
import com.bean.Book;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;

@Controller
public class BookController {

    // 使用原生response对象响应数据
    @RequestMapping("/showData1")
    public void showData1(HttpServletResponse response) throws IOException {
        response.getWriter().write("message");
    }
	
    // 使用原生request对象传递参数
    @RequestMapping("/showPageAndData1")
    public String showPageAndData1(HttpServletRequest request) {
        request.setAttribute("name", "xiaoming");
        return "page";
    }
    
    // 使用Model形参传递参数
    @RequestMapping("/showPageAndData2")
    public String showPageAndData2(Model model) {
        Book book  = new Book();
        book.setName("SpringMVC入门案例");
        book.setPrice(66.66d);
        // 添加数据的方式
        model.addAttribute("name", "xiaoming");
        model.addAttribute("book", book);
        return "page";
    }

    // 使用ModelAndView形参传递参数,该对象还封装了页面信息
    @RequestMapping("/showPageAndData3")
    public ModelAndView showPageAndData3(ModelAndView modelAndView) {
        // ModelAndView mav = new ModelAndView();  // 替换形参中的参数
        Book book = new Book();
        book.setName("SpringMVC入门案例");
        book.setPrice(66.66d);
        // 添加数据的方式
        modelAndView.addObject("book", book);
        modelAndView.addObject("name", "xiaoming");
        // 设置返回页面(若该方法存在多个,则以最后一个为准)
        modelAndView.setViewName("page");
        // 返回值设定成ModelAndView对象
        return modelAndView;
    }

    // ModelAndView对象支持转发的手工设定,该设定不会启用前缀后缀的页面拼接格式
    @RequestMapping("/showPageAndData4")
    public ModelAndView showPageAndData4(ModelAndView modelAndView) {
        modelAndView.setViewName("forward:/WEB-INF/pages/page.jsp");
        return modelAndView;
    }

    // ModelAndView对象支持重定向的手工设定,该设定不会启用前缀后缀的页面拼接格式
    @RequestMapping("/showPageAndData5")
    public ModelAndView showPageAndData6(ModelAndView modelAndView) {
        modelAndView.setViewName("redirect:index.jsp");
        return modelAndView;
    }

}

4. 返回 JSON 数据

  • 导入 maven 坐标:
        <!--json相关坐标3个-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.9.0</version>
        </dependency>
  • Controller:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;


    // 使用@ResponseBody将返回的结果作为响应内容,而非响应的页面名称
    @RequestMapping("/showData2")
    @ResponseBody
    public String showData2(){
        return "{\"name\":\"xiaoming\"}";
    }

    // 使用jackson进行json数据格式转化(会有中文乱码问题)
    @RequestMapping("/showData3")
    @ResponseBody
    public String showData3() throws JsonProcessingException {
        Book book  = new Book();
        book.setName("SpringMVC入门案例");
        book.setPrice(66.66d);
        ObjectMapper om = new ObjectMapper();
        return om.writeValueAsString(book);
    }

    /*
    <!--开启springmvc注解驱动,对@ResponseBody的注解进行格式增强,追加其类型转换的功能,具体实现由MappingJackson2HttpMessageConverter进行-->
    <mvc:annotation-driven/>
     */
    // 使用SpringMVC注解驱动,对标注@ResponseBody注解的控制器方法进行结果转换
    // 由于返回值为引用类型,自动调用jackson提供的类型转换器进行格式转换
    @RequestMapping("/showData4")
    @ResponseBody
    public Book showData4() {
        Book book  = new Book();
        book.setName("SpringMVC入门案例");
        book.setPrice(66.66d);
        return book;
    }

    // 转换集合类型数据
    @RequestMapping("/showData5")
    @ResponseBody
    public List showData5() {
        Book book1 = new Book();
        book1.setName("SpringMVC入门案例");
        book1.setPrice(66.66d);

        Book book2 = new Book();
        book2.setName("SpringMVC入门案例");
        book2.setPrice(66.66d);

        ArrayList<Book> al = new ArrayList<>();
        al.add(book1);
        al.add(book2);
        return al;  // 返回 [{"name":"SpringMVC入门案例","price":66.66},{"name":"SpringMVC入门案例","price":66.66}]
    }

5、Servlet 相关接口

SpringMVC 提供访问原始 Servlet 接口 API 的功能,通过形参声明即可。

    @RequestMapping("/servletApi")
    public String servletApi(HttpServletRequest request,
            HttpServletResponse response, HttpSession session){
        System.out.println(request);  // org.apache.catalina.connector.RequestFacade@6d3a1615
        System.out.println(response);  // org.apache.catalina.connector.ResponseFacade@55405578
        System.out.println(session);  // org.apache.catalina.session.StandardSessionFacade@714a7020
        request.setAttribute("name", "xiaoming");
        System.out.println(request.getAttribute("name"));  // xiaoming
        return "page.jsp";
    }

Header 数据获取:

  • 名称:@RequestHeader
  • 类型:形参注解
  • 位置:处理器类中的方法形参前方
  • 作用:绑定请求头数据与对应处理方法形参间的关系
    // header 数据获取
    @RequestMapping("/headApi")
    public String headApi(@RequestHeader("Accept-Language") String head){
        System.out.println(head);  // zh-CN,zh;q=0.9
        return "page.jsp";
    }

Cookie 数据获取:

  • 名称:@CookieValue
  • 类型:形参注解
  • 位置:处理器类中的方法形参前方
  • 作用:绑定请求 Cookie 数据与对应处理方法形参间的关系
    // cookie 数据获取
    @RequestMapping("/cookieApi")
    public String cookieApi(@CookieValue("JSESSIONID") String jsessionid){
        System.out.println(jsessionid);
        return "page.jsp";
    }

Session 数据获取:

  • 名称:@SessionAttribute
  • 类型:形参注解
  • 位置:处理器类中的方法形参前方
  • 作用:绑定请求 Session 数据与对应处理方法形参间的关系
    // 测试用方法,为下面的试验服务,用于在session中放入数据
    @RequestMapping("/setSessionData")
    public String setSessionData(HttpSession session){
        session.setAttribute("name", "xiaoming");
        return "page";
    }

    // session 数据获取
    @RequestMapping("/sessionApi")
    public String sessionApi(@SessionAttribute("name") String name){
        System.out.println(name);  // 获取session中的name值
        return "page.jsp";
    }

Session 数据设置:

  • 名称:@SessionAttributes
  • 类型:类注解
  • 位置:处理器类上方
  • 作用:声明放入 session 范围的变量名称,适用于 Model 类型数据传参
@Controller
// 设定当前类中名称为age和gender的变量放入session范围(不常用,了解即可)
@SessionAttributes(names={"age","gender"})
public class ServletController {
	
    // 配合 @SessionAttributes(names={"age","gender"}) 使用
    // 将数据放入session存储范围,通过Model对象实现数据set,通过@SessionAttributes注解实现范围设定
    @RequestMapping("/setSessionData2")
    public String setSessionDate2(Model model) {
        model.addAttribute("age",39);
        model.addAttribute("gender","男");
        return "page.jsp";
    }
}

6、异步调用

页面 Ajax.jsp:

<%@page pageEncoding="UTF-8" language="java" contentType="text/html;UTF-8" %>

<a href="javascript:void(0);" id="testAjax">访问springmvc后台controller</a><br/>
<a href="javascript:void(0);" id="testAjaxPojo">访问springmvc后台controller,传递Json格式POJO</a><br/>
<a href="javascript:void(0);" id="testAjaxList">访问springmvc后台controller,传递Json格式List</a><br/>
<a href="javascript:void(0);" id="testAjaxReturnString">访问springmvc后台controller,返回字符串数据</a><br/>
<a href="javascript:void(0);" id="testAjaxReturnJson">访问springmvc后台controller,返回Json数据</a><br/>
<a href="javascript:void(0);" id="testAjaxReturnJsonList">访问springmvc后台controller,返回Json数组数据</a><br/>
<br/>
<a href="javascript:void(0);" id="testCross">跨域访问</a><br/>

<script type="text/javascript" src="${pageContext.request.contextPath}/js/jquery-3.3.1.min.js"></script>

<script type="text/javascript">
    $(function () {
        //为id="testAjax"的组件绑定点击事件
        $("#testAjax").click(function(){
            //发送异步调用
            $.ajax({
               //请求方式:POST请求
               type:"POST",
               //请求的地址
               url:"ajaxController",
               //请求参数(也就是请求内容)
               data:'ajax message',
               //响应正文类型
               dataType:"text",
               //请求正文的MIME类型
               contentType:"application/text",
            });
        });

        //为id="testAjaxPojo"的组件绑定点击事件
        $("#testAjaxPojo").click(function(){
            $.ajax({
               type:"POST",
               url:"ajaxPojoToController",
               data:'{"name":"Jock","age":39}',
               dataType:"text",
               contentType:"application/json",
            });
        });

        //为id="testAjaxList"的组件绑定点击事件
        $("#testAjaxList").click(function(){
            $.ajax({
               type:"POST",
               url:"ajaxListToController",
               data:'[{"name":"Jock","age":39},{"name":"Jockme","age":40}]',
               dataType:"text",
               contentType:"application/json",
            });
        });


        //为id="testAjaxReturnString"的组件绑定点击事件
        $("#testAjaxReturnString").click(function(){
            //发送异步调用
            $.ajax({
               type:"POST",
               url:"ajaxReturnString",
               //回调函数
               success:function(data){
                    //打印返回结果
                    alert(data);
               }
            });
        });

        //为id="testAjaxReturnJson"的组件绑定点击事件
        $("#testAjaxReturnJson").click(function(){
            //发送异步调用
            $.ajax({
               type:"POST",
               url:"ajaxReturnJson",
               //回调函数
               success:function(data){
                    alert(data);
                    alert(data['name']+" ,  "+data['age']);
               }
            });
        });

        //为id="testAjaxReturnJsonList"的组件绑定点击事件
        $("#testAjaxReturnJsonList").click(function(){
            //发送异步调用
            $.ajax({
               type:"POST",
               url:"ajaxReturnJsonList",
               //回调函数
               success:function(data){
                    alert(data);
                    alert(data.length);
                    alert(data[0]["name"]);
                    alert(data[1]["age"]);
               }
            });
        });

        //为id="testCross"的组件绑定点击事件
        $("#testCross").click(function(){
            //发送异步调用
            $.ajax({
               type:"POST",
               url:"http://www.jock.com/cross",
               //回调函数
               success:function(data){
                   alert("跨域调用信息反馈:"+data['name']+" ,  "+data['age']);
               }
            });
        });
    });
</script>

1. 异步请求

  • 注解名称:@RequestBody
  • 类型:形参注解
  • 位置:处理器类中的方法形参前方
  • 作用:将异步提交数据组织成标准请求参数格式,并赋值给形参

//    @RequestMapping("/ajaxController")
//    public String ajaxController(){
//        System.out.println("ajax request is running...");
//        return "page.jsp";
//    }

    @RequestMapping("/ajaxController")
    // 使用@RequestBody注解,可以将请求体内容封装到指定参数中
    public String ajaxController(@RequestBody String message){
        System.out.println("ajax request is running..."+message);
        return "page.jsp";
    }

    @RequestMapping("/ajaxPojoToController")
    // 如果处理参数是POJO,且页面发送的请求数据格式与POJO中的属性对应,@RequestBody注解可以自动映射对应请求数据到POJO中
    // 注意:POJO中的属性如果请求数据中没有,属性值为null,POJO中没有的属性如果请求数据中有,不进行映射
    public String  ajaxPojoToController(@RequestBody User user){
        System.out.println("controller pojo :"+user);
        return "page.jsp";
    }

    @RequestMapping("/ajaxListToController")
    // 如果处理参数是List集合且封装了POJO,且页面发送的数据是JSON格式的对象数组,数据将自动映射到集合参数中
    public String  ajaxListToController(@RequestBody List<User> userList){
        System.out.println("controller list :"+userList);
        return "page.jsp";
    }

2. 异步响应

    // 使用注解@ResponseBody可以将返回的页面不进行解析,直接返回字符串,该注解可以添加到方法上方或返回值前面
    @RequestMapping("/ajaxReturnString")
//    @ResponseBody
    public @ResponseBody String ajaxReturnString(){
        System.out.println("controller return string ...");
        return "page.jsp";
    }

    @RequestMapping("/ajaxReturnJson")
    @ResponseBody
    // 基于jackon技术,使用@ResponseBody注解可以将返回的POJO对象自动转成json格式数据
    public User ajaxReturnJson(){
        System.out.println("controller return json pojo...");
        User user = new User();
        user.setName("Jockme");
        user.setAge(39);
        return user;
    }

    @RequestMapping("/ajaxReturnJsonList")
    @ResponseBody
    // 基于jackon技术,使用@ResponseBody注解可以将返回的保存POJO对象的集合转成json数组格式数据
    public List ajaxReturnJsonList(){
        System.out.println("controller return json list...");
        User user1 = new User();
        user1.setName("Tom");
        user1.setAge(3);

        User user2 = new User();
        user2.setName("Jerry");
        user2.setAge(5);

        ArrayList al = new ArrayList();
        al.add(user1);
        al.add(user2);

        return al;
    }

3. 跨域访问

  • 当通过域名 A 下的操作访问域名 B 下的资源时,称为跨域访问(包括同个 IP 下的不同域名)。
  • 跨域访问时,会出现无法访问的现象:

1)跨域环境搭建

  • 为当前主机添加备用域名
    • 修改 windows 安装目录中的 host 文件
    • 格式:ip 域名
  • 动态刷新 DNS
    • 命令:ipconfig /displaydns
    • 命令:ipconfig /flushdns

2)跨域访问支持

  • 名称:@CrossOrigin
  • 类型:方法注解、类注解
  • 位置:处理器类中的方法上方或类上方
  • 作用:设置当前处理器方法/处理器类中所有方法支持跨域访问
    @RequestMapping("/cross")
    @ResponseBody
    // 使用@CrossOrigin开启跨域访问
    // 标注在处理器方法上方表示该方法支持跨域访问
    // 标注在处理器类上方表示该处理器类中的所有处理器方法均支持跨域访问
    @CrossOrigin
    public User cross(HttpServletRequest request){
        System.out.println("controller cross..."+request.getRequestURL());
        User user = new User();
        user.setName("Jockme");
        user.setAge(39);
        return user;
    }

7、拦截器

请求处理过程解析:

1. 拦截器(Interceptor)

  • 定义:是一种动态拦截方法调用的机制。

  • 作用:

    1. 在指定的方法调用前后执行预先设定后的的代码。
    2. 阻止原始方法的执行。
  • 核心原理:AOP 思想

  • 拦截器链:多个拦截器按照一定的顺序,对原始被调用的功能进行增强

拦截器 VS 过滤器:

  • 归属不同:Filter 属于 Servlet 技术;Interceptor属于 SpringMVC 技术。
  • 拦截内容不同:Filter 对所有访问进行增强;Interceptor 仅针对 SpringMVC 的访问进行增强。

2. 自定义拦截器的开发过程

1)编写 Controller

@Controller
public class InterceptorController {

    @RequestMapping("/handler")
    public String handler() {
        System.out.println("Handler running...");
        return "page.jsp";
    }
}

2)编写自定义拦截器(通知)

// 自定义拦截器需要实现 HandleInterceptor 接口
public class MyInterceptor implements HandlerInterceptor {
    // 处理器运行之前执行
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        System.out.println("前置运行----a1");
        // 返回值为 false 则会拦截原始处理器的运行
        // 如果配置多拦截器,返回值为 false 将终止当前拦截器后面配置的拦截器的运行
        return true;
    }

    // 处理器运行之后执行
    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) throws Exception {
        System.out.println("后置运行----b1");
    }

    // 所有拦截器的后置执行全部结束后,执行该操作
    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) throws Exception {
        System.out.println("完成运行----c1");
    }

    // 三个方法的运行顺序为:preHandle -> postHandle -> afterCompletion
    // 如果 preHandle 返回值为 false,则三个方法仅运行 preHandle
}

3)配置拦截器(切入点)

<mvc:interceptors>
    <mvc:interceptor>
        <!-- 注意:配置顺序为先配置执行位置,后配置执行类 -->
        <mvc:mapping path="/showPage"/>  <!-- Controller上的路径 -->
        <bean class="com.interceptor.MyInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

3. 拦截器执行流程

4. 拦截器配置与方法参数

1)前置处理方法

在原始方法之前运行。

public boolean preHandle(HttpServletRequest request,
                         HttpServletResponse response,
                         Object handler) throws Exception {
    System.out.println("preHandle");
    return true;
}
  • 参数:
    • request:请求对象。
    • response:响应对象。
    • handler:被调用的处理器对象,本质上是一个方法对象,对反射中的 Method 对象进行了再包装。
  • 返回值:
    • 返回值为 false,则被拦截的处理器将不执行。

2)后置处理方法

在原始方法运行后运行,如果原始方法被拦截,则不执行。

public void postHandle(HttpServletRequest request,
                       HttpServletResponse response,
                       Object handler,
                       ModelAndView modelAndView) throws Exception {
    System.out.println("postHandle");
}
  • 参数 modelAndView:如果处理器执行完成具有返回结果,可以读取到对应数据与页面信息,并进行调整。

3)完成处理方法

拦截器最后执行的方法,无论原始方法是否执行。

public void afterCompletion(HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler,
                            Exception ex) throws Exception {
    System.out.println("afterCompletion");
}
  • 参数 ex:如果处理器执行过程中出现异常对象,可以针对异常情况进行单独处理。

4)拦截器配置项

<mvc:interceptors>
    <!-- 开启具体的拦截器的使用,可以配置多个 -->
    <mvc:interceptor>
        <!-- 设置拦截器的拦截路径,支持*通配 -->
        <!-- /**         表示拦截所有映射 -->
        <!-- /*          表示拦截所有/开头的映射 -->
        <!-- /user/*     表示拦截所有/user/开头的映射 -->
        <!-- /user/add*  表示拦截所有/user/开头,且具体映射名称以add开头的映射 -->
        <!-- /user/*All  表示拦截所有/user/开头,且具体映射名称以All结尾的映射 -->
        <mvc:mapping path="/*"/>
        <mvc:mapping path="/**"/>
        <mvc:mapping path="/handleRun*"/>
        <!-- 设置拦截排除的路径,配置/**或/*,达到快速配置的目的 -->
        <mvc:exclude-mapping path="/b*"/>
        <!-- 指定具体的拦截器类 -->
        <bean class="MyInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

5. 多拦截器配置

责任链模式:是一种行为模式。

  • 特征:沿着一条预先设定的任务链顺序执行,每个节点具有独立的工作任务。
  • 优势:
    • 独立性:只关注当前节点的任务,对其他任务直接放行到下一节点。
    • 隔离性:具备链式传递特征,无需知晓整体链路结构,只需等待请求到达后进行处理即可。
    • 灵活性:可以任意修改链路结构动态新增或删减整体链路责任。
    • 解耦:将动态任务与原始任务解耦。
  • 弊端:
    • 链路过长时,处理效率低下。
    • 可能存在节点上的循环引用现象,造成死循环,导致系统崩溃。

8、异常处理

1. 异常处理器

如下实现了 HandlerExceptionResolver 接口的自定义异常处理器后,SpringMVC 就能为框架中的每个异常进行拦截处理。

@Component
public class ExceptionResolver implements HandlerExceptionResolver {
    public ModelAndView resolveException(HttpServletRequest request,
                                         HttpServletResponse response,
                                         Object handler,
                                         Exception ex) {
        System.out.println("异常处理器正在执行中");
        ModelAndView modelAndView = new ModelAndView();
        //定义异常现象出现后,反馈给用户查看的信息
        modelAndView.addObject("msg","出错啦! ");
        //定义异常现象出现后,反馈给用户查看的页面
        modelAndView.setViewName("error.jsp");
        return modelAndView;
    }
}

根据异常的种类不同,进行分门别类的管理,返回不同的信息:

public class ExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request,
                                         HttpServletResponse response,
                                         Object handler,
                                         Exception ex) {
        System.out.println("my exception is running ...."+ex);
        ModelAndView modelAndView = new ModelAndView();
        if( ex instanceof NullPointerException){
            modelAndView.addObject("msg","空指针异常");
        }else if ( ex instanceof  ArithmeticException){
            modelAndView.addObject("msg","算数运算异常");
        }else{
            modelAndView.addObject("msg","未知的异常");
        }
        modelAndView.setViewName("error.jsp");
        return modelAndView;
    }
}

2. 注解开发异常处理器

使用注解实现异常分类管理:

  • 名称:@ControllerAdvice
  • 类型:类注解
  • 位置:异常处理器类上方
  • 作用:设置当前类为异常处理器类
  • 范例:
@Component
@ControllerAdvice
public class ExceptionAdvice {
}  

使用注解实现异常分类管理:

  • 名称:@ExceptionHandler
  • 类型:方法注解
  • 位置:异常处理器类中针对指定异常进行处理的方法上方
  • 作用:设置指定异常的处理方式
  • 说明:处理器方法可以设定多个
  • 范例:
@ExceptionHandler(Exception.class)
@ResponseBody
public String doOtherException(Exception ex){
   return "出错啦,请联系管理员!";
}  

3. 异常处理解决方案

  • 业务异常:
    • 发送对应消息传递给用户,提醒规范操作
  • 系统异常:
    • 发送固定消息传递给用户,安抚用户
    • 发送特定消息给运维人员,提醒维护
    • 记录日志
  • 其他异常:
    • 发送固定消息传递给用户,安抚用户
    • 发送特定消息给编程人员,提醒维护
    • 纳入预期范围内
    • 记录日志

4. 自定义异常

异常定义格式:

// 自定义异常继承RuntimeException,覆盖父类所有的构造方法
public class BusinessException extends RuntimeException {
    public BusinessException() {
    }

    public BusinessException(String message) {
        super(message);
    }

    public BusinessException(String message, Throwable cause) {
        super(message, cause);
    }

    public BusinessException(Throwable cause) {
        super(cause);
    }

    public BusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

异常触发方式:

if(user.getName().trim().length()<4) {
    throw new BusinessException("用户名长度必须在2-4位之间,请重新输入! ");
}

通过自定义异常将所有的异常现象进行分类管理,以统一的格式对外呈现异常消息。

9、文件上传

上传文件过程分析:

SpringMVC 的文件上传技术:MultipartResolver 接口。

  • MultipartResolver 接口定义了文件上传过程中的相关操作,并对通用性操作进行了封装。
  • MultipartResolver 接口底层实现类 CommonsMultipartResovler。
  • CommonsMultipartResovler 并未自主实现文件上传下载对应的功能,而是调用了 apache 的文件上传下载组件。

SpringMVC 文件上传实现:

Maven 依赖:

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.4</version>
</dependency>

 页面表单:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <form action="/fileupload" method="post" enctype="multipart/form-data">
        上传文件: <input type="file" name="file"/><br/>
        <input type="submit" value="上传"/>
    </form>
</body>
</html>

SpringMVC 配置:

<bean id="multipartResolver"
      class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
</bean>

控制器:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;

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

@Controller
public class FileUploadController {

    // 参数中定义 MultipartFile 参数,用于接收页面提交的 type=file 类型的表单(要求表单中的name名称与方法入参名相同)
    @RequestMapping(value="/fileupload")
    public String fileupload(MultipartFile file, HttpServletRequest request) throws IOException {
        //设置保存的路径
        String realPath = request.getServletContext().getRealPath("/images");
        file.transferTo(new File(realPath, "file.png"));  // 将上传的文件保存到服务器
        return "page.jsp";
    }
}

文件上传常见问题:

  1. 文件命名问题
  2. 文件名过长问题
  3. 文件保存路径
  4. 重名问题
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;

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

@Controller
public class FileUploadController {

    @RequestMapping(value="/fileupload")
    public String fileupload(MultipartFile file, MultipartFile file1,
                             MultipartFile file2, HttpServletRequest request) throws IOException {
        // MultipartFile参数中封装了上传的文件的相关信息
        // System.out.println(file.getSize());
        // System.out.println(file.getBytes().length);
        // System.out.println(file.getContentType());
        // System.out.println(file.getName());
        // System.out.println(file.getOriginalFilename());
        // System.out.println(file.isEmpty());

        // 首先判断是否是空文件,也就是存储空间占用为0的文件
        if(!file.isEmpty()) {
            // 如果大小在范围要求内则正常处理;否则抛出自定义异常告知用户(未实现)
            // 获取原始上传的文件名,可以作为当前文件的真实名称保存到数据库中备用
            String fileName = file.getOriginalFilename();
            // 设置保存的路径
            String realPath = request.getServletContext().getRealPath("/images");
            // 保存文件的方法,指定保存的位置和文件名即可,通常文件名使用随机生成策略产生,避免文件名冲突问题
            // String uuid = UUID.randomUUID().toString().replace("-", "").toUpperCase();  // UUID 随机数
            file.transferTo(new File(realPath, file.getOriginalFilename()));
        }
        // 测试一次性上传多个文件
        if(!file1.isEmpty()) {
            String fileName = file1.getOriginalFilename();
            //可以根据需要,对不同种类的文件做不同的存储路径的区分,修改对应的保存位置即可
            String realPath = request.getServletContext().getRealPath("/images");
            file1.transferTo(new File(realPath, file1.getOriginalFilename()));
        }
        if(!file2.isEmpty()) {
            String fileName = file2.getOriginalFilename();
            String realPath = request.getServletContext().getRealPath("/images");
            file2.transferTo(new File(realPath, file2.getOriginalFilename()));
        }
        return "page.jsp";
    }
}

10、Restful

1. Restful 简介

Rest(REpresentational State Transfer)是一种网络资源的访问风格,定义了网络资源的访问方式。

  • 传统风格访问路径
    • http://localhost/user/get?id=1
    • http://localhost/deleteUser?id=1
  • Rest 风格访问路径
    • http://localhost/user/1

而 Restful 则是按照 Rest 风格访问网络资源。

优点:

  • 隐藏资源的访问行为,通过地址无法得知做的是何种操作。
  • 书写简化。

2. Rest 行为常用约定方式

  • GET(查询) http://localhost/user/1 GET
  • POST(保存) http://localhost/user POST
  • PUT(更新) http://localhost/user PUT
  • DELETE(删除) http://localhost/user DELETE

注意:上述行为是约定方式,约定不是规范,可以打破,所以称 Rest 风格,而不是 Rest 规范。

3. Restful开发入门

页面表单:

    <!-- 切换请求路径为restful风格 -->
    <!-- GET请求通过地址栏可以发送,也可以通过设置form的请求方式提交 -->
    <!-- POST请求必须通过form的请求方式提交 -->
    <form action="/user/1" method="post">
        <!-- 当添加了 name 为 _method 的隐藏域时,可以通过设置该隐藏域的值,修改请求的提交方式,切换为 PUT 请求或 DELETE 请求,但是 form 表单的提交方式 method 属性必须填写 post -->
        <!-- 该配置需要配合 HiddenHttpMethodFilter 过滤器使用,单独使用无效,请注意检查 web.xml 中是否配置了对应过滤器 -->
        <!-- 使用隐藏域提交请求类型,参数名称固定为"_method",必须配合提交类型 method=post 使用 -->
        <input type="hidden" name="_method" value="PUT"/>  <!-- value或="DELETE" -->
        <input type="submit"/>
    </form>

开启 SpringMVC 对 Restful 风格的访问支持过滤器,即可通过页面表单提交 PUT 与 DELETE 请求:

    <!-- 配置拦截器,解析请求中的参数_method,否则无法发起PUT请求与DELETE请求,配合页面表单使用 -->
    <filter>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <servlet-name>DispatcherServlet</servlet-name>
    </filter-mapping>
    
    <servlet>
        <servlet-name>DispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath*:spring-mvc.xml</param-value>
        </init-param>
    </servlet>

控制层:

package com.controller;
        
import org.springframework.web.bind.annotation.*;

// 设置rest风格的控制器
@RestController  // 等于 @Controller + @ResponseBody
// 设置公共访问路径,配合下方访问路径使用
@RequestMapping("/user/")
public class RestfulController {

    /**
    // rest风格访问路径完整书写方式
    @RequestMapping("/user/{id}")
    // 使用 @PathVariable 注解获取路径上配置的具名变量,该配置可以使用多次
    public String restLocation(@PathVariable Integer id){
        System.out.println("restful is running ....");
        return "success.jsp";
    }

    // rest风格访问路径简化书写方式,配合类注解@RequestMapping使用
    @RequestMapping("{id}")
    public String restLocation2(@PathVariable Integer id){
        System.out.println("restful is running ....get:"+id);
        return "success.jsp";
    }
    */

    // 接收GET请求配置方式
    // @RequestMapping(value = "{id}", method = RequestMethod.GET)
    // 接收GET请求简化配置方式
    @GetMapping("{id}")
    public String get(@PathVariable Integer id){
        System.out.println("restful is running ....get:"+id);
        return "success.jsp";
    }

    // 接收POST请求配置方式
    // @RequestMapping(value = "{id}", method = RequestMethod.POST)
    // 接收POST请求简化配置方式
    @PostMapping("{id}")
    public String post(@PathVariable Integer id){
        System.out.println("restful is running ....post:"+id);
        return "success.jsp";
    }

    // 接收PUT请求简化配置方式
    // @RequestMapping(value = "{id}", method = RequestMethod.PUT)
    // 接收PUT请求简化配置方式
    @PutMapping("{id}")
    public String put(@PathVariable Integer id){
        System.out.println("restful is running ....put:"+id);
        return "success.jsp";
    }

    // 接收DELETE请求简化配置方式
    // @RequestMapping(value = "{id}", method = RequestMethod.DELETE)
    // 接收DELETE请求简化配置方式
    @DeleteMapping("{id}")
    public String delete(@PathVariable Integer id){
        System.out.println("restful is running ....delete:"+id);
        return "success.jsp";
    }
}

11、表单校验框架

1. 表单校验框架介绍

表单校验分类:

  • 校验位置:
    • 客户端校验
    • 服务端校验
  • 校验内容与对应方式:
    • 格式校验
      • 客户端:使用 JS 技术,利用正则表达式校验
      • 服务端:使用校验框架
    • 逻辑校验
      • 客户端:使用 ajax 发送要校验的数据,在服务端完成逻辑校验,返回校验结果
      • 服务端:接收到完整的请求后,在执行业务操作前,完成逻辑校验

表单校验规则:

  • 长度:例如用户名长度,评论字符数量
  • 非法字符:例如用户名组成
  • 数据格式:例如 Email 格式、IP 地址格式
  • 边界值:例如转账金额上限,年龄上下限
  • 重复性:例如用户名是否重复

表单校验框架:

  • JSR(Java Specification Requests):Java 规范提案

    • JSR 303:提供 bean 属性相关校验规则
  • Hibernate 框架中包含一套独立的校验框架 hibernate-validator

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.1.0.Final</version>
</dependency>

注意:

  • tomcat7:搭配 hibernate-validator 版本 5...Final
  • tomcat8.5 及以上:搭配 hibernate-validator 版本 6...Final

2. 快速入门

页面表单:

<form action="/addemployee" method="post">
    员工姓名:<input type="text" name="name"><span style="color:red">${name}</span><br/>
    员工年龄:<input type="text" name="age"><span style="color:red">${age}</span><br/>
    <input type="submit" value="提交">
</form>

设置校验规则:

  • 名称:@NotNull
  • 类型:属性注解 等
  • 位置:实体类属性上方
  • 作用:设定当前属性校验规则
  • 范例:
    每个校验规则所携带的参数不同,根据校验规则进行相应的调整
    具体的校验规则查看对应的校验框架进行获取
import javax.validation.constraints.NotBlank;

public class Employee {
	
    @NotBlank(message="姓名不能为空")
    private String name;  // 员工姓名
    private Integer age;  // 员工年龄

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

开启校验,并获取校验错误信息:

  • 名称:@Valid、@Validated
  • 类型:形参注解
  • 位置:处理器类中的实体类类型的方法形参前方
  • 作用:设定对当前实体类类型参数进行校验
  • 范例:
import com.bean.Employee;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.validation.Valid;

@Controller
public class EmployeeController {

    // 使用 @Valid 开启校验(使用 @Validated 也可以开启校验)
    // Errors 对象用于封装校验结果,如果不满足校验规则,对应的校验结果封装到该对象中,包含校验的属性名和校验不通过返回的消息
    @RequestMapping(value="/addemployee")
    public String addEmployee(@Valid Employee employee, Errors errors, Model model) {
        System.out.println(employee);
        // 判定Errors对象中是否存在未通过校验的字段
        if(errors.hasErrors()){
            // 获取所有未通过校验规则的信息
            for(FieldError error : errors.getFieldErrors()){
                // 将校验结果信息添加到Model对象中,用于页面显示
                // 实际开发中无需这样设定,返回json数据即可
                model.addAttribute(error.getField(), error.getDefaultMessage());
            }
            // 当出现未通过校验的字段时,跳转页面到原始页面,进行数据回显
            return "employee.jsp";
        }
        return "success.jsp";
    }

}

示例效果:提交表单并返回校验结果。

 

3. 多规则校验

  • 同一个属性可以添加多个校验器
@NotNull(message = "请输入您的年龄")
@Max(value = 60, message = "年龄最大值不允许超过60岁")
@Min(value = 18, message = "年龄最小值不允许低于18岁")
private Integer age;  // 员工年龄
  • 3 种判定空校验器的区别

4. 嵌套校验

  • 名称:@Valid
  • 类型:属性注解
  • 位置:实体类中的引用类型属性上方
  • 作用:设定当前应用类型属性中的属性开启校验
  • 范例:
public class Employee {
    // 实体类中的引用类型通过标注 @Valid 注解,设定开启当前引用类型字段中的属性参与校验
    @Valid
    private Address address;
}
  • 注意:开启嵌套校验后,被校验对象内部需要添加对应的校验规则。

5. 分组校验

同一个模块,根据执行的业务不同,需要校验的属性也会有不同,如新增用户和修改用户时的校验规则不同。

因此,需要对不同种类的属性进行分组,在校验时可以指定参与校验的字段所属的组类别:

// 定义组(通用)
public interface GroupOne {
}
// 为属性设置所属组,可以设置多个
@NotEmpty(message = "姓名不能为空", groups = {GroupOne.class})
private String name;  // 员工姓名
// 开启组校验
public String addEmployee(@Validated({GroupOne.class}) Employee employee){
}

6. 综合案例

  • 页面表单:employee.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <form action="/addemployee" method="post">
        员工姓名:<input type="text" name="name"><span style="color:red">${name}</span><br/>
        员工年龄:<input type="text" name="age"><span style="color:red">${age}</span><br/>
        <!-- 注意,引用类型的校验未通过信息不是通过对象进行封装的,而是直接使用"对象名.属性名"的格式作为整体属性字符串进行保存的,因此需要使用以下获取方法。
        这和使用者的属性传递方式有关,不具有通用性,仅适用于本案例 -->
        省名:<input type="text" name="address.provinceName"><span style="color:red">${requestScope['address.provinceName']}</span><br/>
        <input type="submit" value="提交">
    </form>
</body>
</html>
  • 实体类:Employee.java
package com.bean;

import com.group.GroupA;

import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

public class Employee {

    // 设定校验器,设置校验不通过对应的消息,设定所参与的校验组
    @NotBlank(message="姓名不能为空", groups = {GroupA.class})
    private String name;  // 员工姓名

    // 一个属性可以添加多个校验器
    @NotNull(message = "请输入您的年龄", groups = {GroupA.class})
    @Max(value = 60, message = "年龄最大值不允许超过60岁")
    @Min(value = 18, message = "年龄最小值不允许低于18岁")
    private Integer age;  // 员工年龄

    // 实体类中的引用类型通过标注 @Valid 注解,设定开启当前引用类型字段中的属性参与校验
    @Valid
    private Address address;

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", address=" + address +
                '}';
    }
}
  • 实体类:Address.java
package com.bean;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

// 嵌套校验的实体中,对每个属性正常添加校验规则即可
public class Address {

    @NotBlank(message = "请输入省份名称")
    private String provinceName;  // 省份名称

    @NotBlank(message = "请输入城市名称")
    private String cityName;  // 城市名称

    @NotBlank(message = "请输入详细地址")
    private String detail;  // 详细住址

    @NotBlank(message = "请输入邮政编码")
    @Size(max = 6, min = 6, message = "邮政编码由6位组成")
    private String zipCode;  // 邮政编码

    public String getProvinceName() {
        return provinceName;
    }

    public void setProvinceName(String provinceName) {
        this.provinceName = provinceName;
    }

    public String getCityName() {
        return cityName;
    }

    public void setCityName(String cityName) {
        this.cityName = cityName;
    }

    public String getDetail() {
        return detail;
    }

    public void setDetail(String detail) {
        this.detail = detail;
    }

    public String getZipCode() {
        return zipCode;
    }

    public void setZipCode(String zipCode) {
        this.zipCode = zipCode;
    }

    @Override
    public String toString() {
        return "Address{" +
                "provinceName='" + provinceName + '\'' +
                ", cityName='" + cityName + '\'' +
                ", detail='" + detail + '\'' +
                ", zipCode='" + zipCode + '\'' +
                '}';
    }
}
  • 分组接口:GroupA.java
package com.group;

public interface GroupA {
}
  • 控制层:EmployeeController.java
package com.controller;

import com.bean.Employee;
import com.group.GroupA;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.validation.Valid;
import java.util.List;

@Controller
public class EmployeeController {

    // 应用GroupA的分组校验规则
    @RequestMapping(value="/addemployee")
    // 使用@Valid开启校验,使用@Validated也可以开启校验
    // Errors对象用于封装校验结果,如果不满足校验规则,对应的校验结果封装到该对象中,包含校验的属性名和校验不通过返回的消息
    public String addEmployee(@Validated({GroupA.class}) Employee employee, Errors errors, Model model) {
        System.out.println(employee);
        // 判定Errors对象中是否存在未通过校验的字段
        if(errors.hasErrors()){
            // 获取所有未通过校验规则的信息
            List<FieldError> fieldErrors = errors.getFieldErrors();
            System.out.println(fieldErrors.size());
            for(FieldError error : fieldErrors){
                System.out.println(error.getField());
                System.out.println(error.getDefaultMessage());
                //将校验结果信息添加到Model对象中,用于页面显示,后期实际开发中无需这样设定,返回json数据即可
                model.addAttribute(error.getField(),error.getDefaultMessage());
            }
            // 当出现未通过校验的字段时,跳转页面到原始页面,进行数据回显
            return "employee.jsp";
        }
        return "success.jsp";
    }

    // 不区分校验分组,即全部规则均校验
    @RequestMapping(value="/addemployee2")
    public String addEmployee2(@Valid Employee employee, Errors errors, Model model) {
        System.out.println(employee);
        if(errors.hasErrors()){
            for(FieldError error : errors.getFieldErrors()){
                model.addAttribute(error.getField(), error.getDefaultMessage());
            }
            return "employee.jsp";
        }
        return "success.jsp";
    }
}

7. 实用校验范例

import javax.validation.Valid;
import javax.validation.constraints.*;
import java.io.Serializable;
import java.util.Date;

// 实用的校验范例,仅供参考
public class Employee implements Serializable {

    private String id;  // 员工ID

    private String code;  // 员工编号

    @NotBlank(message = "员工名称不能为空")
    private String name;  // 员工姓名

    @NotNull(message = "员工年龄不能为空")
    @Max(value = 60,message = "员工年龄不能超过60岁")
    @Min(value = 18,message = "员工年里不能小于18岁")
    private Integer age;  // 员工年龄

    @NotNull(message = "员工生日不能为空")
    @Past(message = "员工生日要求必须是在当前日期之前")
    private Date birthday;  // 员工生日

    @NotBlank(message = "请选择员工性别")
    private String gender;  // 员工性别

    @NotEmpty(message = "请输入员工邮箱")
    @Email(regexp = "@", message = "邮箱必须包含@符号")
    private String email;  // 员工邮箱

    @NotBlank(message = "请输入员工电话")
    @Pattern(regexp = "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$", message = "手机号不正确")
    private String telephone;  // 员工电话

    @NotBlank(message = "请选择员工类别")
    private String type;  // 员工类型:正式工为1,临时工为2

    @Valid  // 表示需要嵌套验证
    private Address address;  // 员工住址

    // 省略各 getter、setter
}

八、Spring开发实战

 Java Web的基础是Servlet容器,以及标准的Servlet组件:

  • Servlet:能处理HTTP请求并将HTTP响应返回;
  • JSP:一种嵌套Java代码的HTML,将被编译为Servlet;
  • Filter:能过滤指定的URL以实现拦截功能;
  • Listener:监听指定的事件,如ServletContext、HttpSession的创建和销毁。

此外,Servlet容器为每个Web应用程序自动创建一个唯一的ServletContext实例,这个实例就代表了Web应用程序本身。

在MVC高级开发中,我们实现了一个MVC框架,接口和Spring MVC类似。如果直接使用Spring MVC,我们写出来的代码类似:

@Controller
public class UserController {
    @GetMapping("/register")
    public ModelAndView register() {
        ...
    }

    @PostMapping("/signin")
    public ModelAndView signin(@RequestParam("email") String email, @RequestParam("password") String password) {
        ...
    }
    ...
}

但是,Spring提供的是一个IoC容器,所有的Bean,包括Controller,都在Spring IoC容器中被初始化,而Servlet容器由JavaEE服务器提供(如Tomcat),Servlet容器对Spring一无所知,他们之间到底依靠什么进行联系,又是以何种顺序初始化的?

在理解上述问题之前,我们先把基于Spring MVC开发的项目结构搭建起来。首先创建基于Web的Maven工程,引入如下依赖:

  • org.springframework:spring-context:5.2.0.RELEASE
  • org.springframework:spring-webmvc:5.2.0.RELEASE
  • org.springframework:spring-jdbc:5.2.0.RELEASE
  • javax.annotation:javax.annotation-api:1.3.2
  • io.pebbletemplates:pebble-spring5:3.1.2
  • ch.qos.logback:logback-core:1.2.3
  • ch.qos.logback:logback-classic:1.2.3
  • com.zaxxer:HikariCP:3.4.2
  • org.hsqldb:hsqldb:2.5.0

以及provided依赖:

  • org.apache.tomcat.embed:tomcat-embed-core:9.0.26
  • org.apache.tomcat.embed:tomcat-embed-jasper:9.0.26

这个标准的Maven Web工程目录结构如下:

其中,src/main/webapp是标准web目录,WEB-INF存放web.xml,编译的class,第三方jar,以及不允许浏览器直接访问的View模版,static目录存放所有静态文件。

在src/main/resources目录中存放的是Java程序读取的classpath资源文件,除了JDBC的配置文件jdbc.properties外,我们又新增了一个logback.xml,这是Logback的默认查找的配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<appender name="STDOUT"
		class="ch.qos.logback.core.ConsoleAppender">
		<layout class="ch.qos.logback.classic.PatternLayout">
			<Pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</Pattern>
		</layout>
	</appender>

	<logger name="com.itranswarp.learnjava" level="info" additivity="false">
		<appender-ref ref="STDOUT" />
	</logger>

	<root level="info">
		<appender-ref ref="STDOUT" />
	</root>
</configuration>

上面给出了一个写入到标准输出的Logback配置,可以基于上述配置添加写入到文件的配置。

在src/main/java中就是我们编写的Java代码了。

1、配置Spring MVC

使用Spring MVC时,整个Web应用程序按如下顺序启动:

  1. 启动Tomcat服务器;
  2. Tomcat读取web.xml并初始化DispatcherServlet;
  3. DispatcherServlet创建IoC容器并自动注册到ServletContext中。

启动后,浏览器发出的HTTP请求全部由DispatcherServlet接收,并根据配置转发到指定Controller的指定方法处理。

和普通Spring配置一样,我们编写正常的AppConfig后,只需加上@EnableWebMvc注解,就“激活”了Spring MVC:

@Configuration
@ComponentScan
@EnableWebMvc // 启用Spring MVC
@EnableTransactionManagement
@PropertySource("classpath:/jdbc.properties")
public class AppConfig {
    ...
}

除了创建DataSource、JdbcTemplate、PlatformTransactionManager外,AppConfig需要额外创建几个用于Spring MVC的Bean:

@Bean
WebMvcConfigurer createWebMvcConfigurer() {
    return new WebMvcConfigurer() {
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/static/**").addResourceLocations("/static/");
        }
    };
}

WebMvcConfigurer并不是必须的,但我们在这里创建一个默认的WebMvcConfigurer,只覆写addResourceHandlers(),目的是让Spring MVC自动处理静态文件,并且映射路径为/static/**。

另一个必须要创建的Bean是ViewResolver,因为Spring MVC允许集成任何模板引擎,使用哪个模板引擎,就实例化一个对应的ViewResolver:

@Bean
ViewResolver createViewResolver(@Autowired ServletContext servletContext) {
    PebbleEngine engine = new PebbleEngine.Builder().autoEscaping(true)
            .cacheActive(false)
            .loader(new ServletLoader(servletContext))
            .extension(new SpringExtension())
            .build();
    PebbleViewResolver viewResolver = new PebbleViewResolver();
    viewResolver.setPrefix("/WEB-INF/templates/");
    viewResolver.setSuffix("");
    viewResolver.setPebbleEngine(engine);
    return viewResolver;
}

ViewResolver通过指定prefix和suffix来确定如何查找View。上述配置使用Pebble引擎,指定模板文件存放在/WEB-INF/templates/目录下。

剩下的Bean都是普通的@Component,但Controller必须标记为@Controller,例如:

// Controller使用@Controller标记而不是@Component:
@Controller
public class UserController {
    // 正常使用@Autowired注入:
    @Autowired
    UserService userService;

    // 处理一个URL映射:
    @GetMapping("/")
    public ModelAndView index() {
        ...
    }
    ...
}

如果是普通的Java应用程序,我们通过main()方法可以很简单地创建一个Spring容器的实例:

public static void main(String[] args) {
    ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
}

但是问题来了,现在是Web应用程序,而Web应用程序总是由Servlet容器创建,那么,Spring容器应该由谁创建?在什么时候创建?Spring容器中的Controller又是如何通过Servlet调用的?

在Web应用中启动Spring容器有很多种方法,可以通过Listener启动,也可以通过Servlet启动,可以使用XML配置,也可以使用注解配置。这里,我们只介绍一种最简单的启动Spring容器的方式。

第一步,我们在web.xml中配置Spring MVC提供的DispatcherServlet:

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
        </init-param>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>com.itranswarp.learnjava.AppConfig</param-value>
        </init-param>
        <load-on-startup>0</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

初始化参数contextClass指定使用注解配置的AnnotationConfigWebApplicationContext,配置文件的位置参数contextConfigLocation指向AppConfig的完整类名,最后,把这个Servlet映射到/*,即处理所有URL。

上述配置可以看作一个样板配置,有了这个配置,Servlet容器会首先初始化Spring MVC的DispatcherServlet,在DispatcherServlet启动时,它根据配置AppConfig创建了一个类型是WebApplicationContext的IoC容器,完成所有Bean的初始化,并将容器绑到ServletContext上。

因为DispatcherServlet持有IoC容器,能从IoC容器中获取所有@Controller的Bean,因此,DispatcherServlet接收到所有HTTP请求后,根据Controller方法配置的路径,就可以正确地把请求转发到指定方法,并根据返回的ModelAndView决定如何渲染页面。

最后,我们在AppConfig中通过main()方法启动嵌入式Tomcat:

public static void main(String[] args) throws Exception {
    Tomcat tomcat = new Tomcat();
    tomcat.setPort(Integer.getInteger("port", 8080));
    tomcat.getConnector();
    Context ctx = tomcat.addWebapp("", new File("src/main/webapp").getAbsolutePath());
    WebResourceRoot resources = new StandardRoot(ctx);
    resources.addPreResources(
            new DirResourceSet(resources, "/WEB-INF/classes", new File("target/classes").getAbsolutePath(), "/"));
    ctx.setResources(resources);
    tomcat.start();
    tomcat.getServer().await();
}

上述Web应用程序就是我们使用Spring MVC时的一个最小启动功能集。由于使用了JDBC和数据库,用户的注册、登录信息会被持久化:

2、编写Controller

有了Web应用程序的最基本的结构,我们的重点就可以放在如何编写Controller上。Spring MVC对Controller没有固定的要求,也不需要实现特定的接口。以UserController为例,编写Controller只需要遵循以下要点:

总是标记@Controller而不是@Component:

@Controller
public class UserController {
    ...
}

一个方法对应一个HTTP请求路径,用@GetMapping或@PostMapping表示GET或POST请求:

@PostMapping("/signin")
public ModelAndView doSignin(
        @RequestParam("email") String email,
        @RequestParam("password") String password,
        HttpSession session) {
    ...
}

需要接收的HTTP参数以@RequestParam()标注,可以设置默认值。如果方法参数需要传入HttpServletRequest、HttpServletResponse或者HttpSession,直接添加这个类型的参数即可,Spring MVC会自动按类型传入。

返回的ModelAndView通常包含View的路径和一个Map作为Model,但也可以没有Model,例如:

return new ModelAndView("signin.html"); // 仅View,没有Model

返回重定向时既可以写new ModelAndView("redirect:/signin"),也可以直接返回String:

public String index() {
    if (...) {
        return "redirect:/signin";
    } else {
        return "redirect:/profile";
    }
}

如果在方法内部直接操作HttpServletResponse发送响应,返回null表示无需进一步处理:

public ModelAndView download(HttpServletResponse response) {
    byte[] data = ...
    response.setContentType("application/octet-stream");
    OutputStream output = response.getOutputStream();
    output.write(data);
    output.flush();
    return null;
}

对URL进行分组,每组对应一个Controller是一种很好的组织形式,并可以在Controller的class定义出添加URL前缀,例如:

@Controller
@RequestMapping("/user")
public class UserController {
    // 注意实际URL映射是/user/profile
    @GetMapping("/profile")
    public ModelAndView profile() {
        ...
    }

    // 注意实际URL映射是/user/changePassword
    @GetMapping("/changePassword")
    public ModelAndView changePassword() {
        ...
    }
}

实际方法的URL映射总是前缀+路径,这种形式还可以有效避免不小心导致的重复的URL映射。

可见,Spring MVC允许我们编写既简单又灵活的Controller实现。

3、使用REST 

使用Spring MVC开发Web应用程序的主要工作就是编写Controller逻辑。在Web应用中,除了需要使用MVC给用户显示页面外,还有一类API接口,我们称之为REST,通常输入输出都是JSON,便于第三方调用或者使用页面JavaScript与之交互。

直接在Controller中处理JSON是可以的,因为Spring MVC的@GetMapping和@PostMapping都支持指定输入和输出的格式。如果我们想接收JSON,输出JSON,那么可以这样写:

@PostMapping(value = "/rest",
             consumes = "application/json;charset=UTF-8",
             produces = "application/json;charset=UTF-8")
@ResponseBody
public String rest(@RequestBody User user) {
    return "{\"restSupport\":true}";
}

对应的Maven工程需要加入Jackson这个依赖:com.fasterxml.jackson.core:jackson-databind:2.11.0

注意到@PostMapping使用consumes声明能接收的类型,使用produces声明输出的类型,并且额外加了@ResponseBody表示返回的String无需额外处理,直接作为输出内容写入HttpServletResponse。输入的JSON则根据注解@RequestBody直接被Spring反序列化为User这个JavaBean。

使用curl命令测试一下:

$ curl -v -H "Content-Type: application/json" -d '{"email":"bob@example.com"}' http://localhost:8080/rest      
> POST /rest HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 27
> 
< HTTP/1.1 200 
< Content-Type: application/json;charset=utf-8
< Content-Length: 20
< Date: Sun, 10 May 2020 09:56:01 GMT
< 
{"restSupport":true}

输出正是我们写入的字符串。

直接用Spring的Controller配合一大堆注解写REST太麻烦了,因此,Spring还额外提供了一个@RestController注解,使用@RestController替代@Controller后,每个方法自动变成API接口方法。我们还是以实际代码举例,编写ApiController如下:

@RestController
@RequestMapping("/api")
public class ApiController {
    @Autowired
    UserService userService;

    @GetMapping("/users")
    public List<User> users() {
        return userService.getUsers();
    }

    @GetMapping("/users/{id}")
    public User user(@PathVariable("id") long id) {
        return userService.getUserById(id);
    }

    @PostMapping("/signin")
    public Map<String, Object> signin(@RequestBody SignInRequest signinRequest) {
        try {
            User user = userService.signin(signinRequest.email, signinRequest.password);
            return Map.of("user", user);
        } catch (Exception e) {
            return Map.of("error", "SIGNIN_FAILED", "message", e.getMessage());
        }
    }

    public static class SignInRequest {
        public String email;
        public String password;
    }
}

编写REST接口只需要定义@RestController,然后,每个方法都是一个API接口,输入和输出只要能被Jackson序列化或反序列化为JSON就没有问题。我们用浏览器测试GET请求,可直接显示JSON响应:

要测试POST请求,可以用curl命令:

$ curl -v -H "Content-Type: application/json" -d '{"email":"bob@example.com","password":"bob123"}' http://localhost:8080/api/signin
> POST /api/signin HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 47
> 
< HTTP/1.1 200 
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sun, 10 May 2020 08:14:13 GMT
< 
{"user":{"id":1,"email":"bob@example.com","password":"bob123","name":"Bob",...

注意观察上述JSON的输出,User能被正确地序列化为JSON,但暴露了password属性,这是我们不期望的。要避免输出password属性,可以把User复制到另一个UserBean对象,该对象只持有必要的属性,但这样做比较繁琐。另一种简单的方法是直接在User的password属性定义处加上@JsonIgnore表示完全忽略该属性:

public class User {
    ...

    @JsonIgnore
    public String getPassword() {
        return password;
    }

    ...
}

但是这样一来,如果写一个register(User user)方法,那么该方法的User对象也拿不到注册时用户传入的密码了。如果要允许输入password,但不允许输出password,即在JSON序列化和反序列化时,允许写属性,禁用读属性,可以更精细地控制如下:

public class User {
    ...

    @JsonProperty(access = Access.WRITE_ONLY)
    public String getPassword() {
        return password;
    }

    ...
}

同样的,可以使用@JsonProperty(access = Access.READ_ONLY)允许输出,不允许输入。

4、集成Filter

在Spring MVC中,DispatcherServlet只需要固定配置到web.xml中,剩下的工作主要是专注于编写Controller。

但是,在Servlet规范中,我们还可以使用Filter。如果要在Spring MVC中使用Filter,应该怎么做?

有的童鞋在上一节的Web应用中可能发现了,如果注册时输入中文会导致乱码,因为Servlet默认按非UTF-8编码读取参数。为了修复这一问题,我们可以简单地使用一个EncodingFilter,在全局范围类给HttpServletRequest和HttpServletResponse强制设置为UTF-8编码。

可以自己编写一个EncodingFilter,也可以直接使用Spring MVC自带的一个CharacterEncodingFilter。配置Filter时,只需在web.xml中声明即可:

<web-app>
    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    ...
</web-app>

因为这种Filter和我们业务关系不大,注意到CharacterEncodingFilter其实和Spring的IoC容器没有任何关系,两者均互不知晓对方的存在,因此,配置这种Filter十分简单。

我们再考虑这样一个问题:如果允许用户使用Basic模式进行用户验证,即在HTTP请求中添加头Authorization: Basic email:password,这个需求如何实现?

编写一个AuthFilter是最简单的实现方式:

@Component
public class AuthFilter implements Filter {
    @Autowired
    UserService userService;

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        // 获取Authorization头:
        String authHeader = req.getHeader("Authorization");
        if (authHeader != null && authHeader.startsWith("Basic ")) {
            // 从Header中提取email和password:
            String email = prefixFrom(authHeader);
            String password = suffixFrom(authHeader);
            // 登录:
            User user = userService.signin(email, password);
            // 放入Session:
            req.getSession().setAttribute(UserController.KEY_USER, user);
        }
        // 继续处理请求:
        chain.doFilter(request, response);
    }
}

现在问题来了:在Spring中创建的这个AuthFilter是一个普通Bean,Servlet容器并不知道,所以它不会起作用。

如果我们直接在web.xml中声明这个AuthFilter,注意到AuthFilter的实例将由Servlet容器而不是Spring容器初始化,因此,@Autowire根本不生效,用于登录的UserService成员变量永远是null。

所以,得通过一种方式,让Servlet容器实例化的Filter,间接引用Spring容器实例化的AuthFilter。Spring MVC提供了一个DelegatingFilterProxy,专门来干这个事情:

<web-app>
    <filter>
        <filter-name>authFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>authFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    ...
</web-app>

我们来看实现原理:

  1. Servlet容器从web.xml中读取配置,实例化DelegatingFilterProxy,注意命名是authFilter
  2. Spring容器通过扫描@Component实例化AuthFilter

当DelegatingFilterProxy生效后,它会自动查找注册在ServletContext上的Spring容器,再试图从容器中查找名为authFilter的Bean,也就是我们用@Component声明的AuthFilter。

DelegatingFilterProxy将请求代理给AuthFilter,核心代码如下:

public class DelegatingFilterProxy implements Filter {
    private Filter delegate;
    public void doFilter(...) throws ... {
        if (delegate == null) {
            delegate = findBeanFromSpringContainer();
        }
        delegate.doFilter(req, resp, chain);
    }
}

​这就是一个代理模式的简单应用。我们画个图表示它们之间的引用关系如下:

如果在web.xml中配置的Filter名字和Spring容器的Bean的名字不一致,那么需要指定Bean的名字:

<filter>
    <filter-name>basicAuthFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <!-- 指定Bean的名字 -->
    <init-param>
        <param-name>targetBeanName</param-name>
        <param-value>authFilter</param-value>
    </init-param>
</filter>

实际应用时,尽量保持名字一致,以减少不必要的配置。

要使用Basic模式的用户认证,我们可以使用curl命令测试。例如,用户登录名是tom@example.com,口令是tomcat,那么先构造一个使用URL编码的用户名:口令的字符串:

tom%40example.com:tomcat

对其进行Base64编码,最终构造出的Header如下:

Authorization: Basic dG9tJTQwZXhhbXBsZS5jb206dG9tY2F0

使用如下的curl命令并获得响应如下:

$ curl -v -H 'Authorization: Basic dG9tJTQwZXhhbXBsZS5jb206dG9tY2F0' http://localhost:8080/profile
> GET /profile HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> Authorization: Basic dG9tJTQwZXhhbXBsZS5jb206dG9tY2F0
> 
< HTTP/1.1 200 
< Set-Cookie: JSESSIONID=CE0F4BFC394816F717443397D4FEABBE; Path=/; HttpOnly
< Content-Type: text/html;charset=UTF-8
< Content-Language: en-CN
< Transfer-Encoding: chunked
< Date: Wed, 29 Apr 2020 00:15:50 GMT
< 
<!doctype html>
...HTML输出...

上述响应说明AuthFilter已生效。

注意:Basic认证模式并不安全,这里只用来作为使用Filter的示例。

九、SSM框架整合案例

1、整合流程

SSM(Spring + SpringMVC + MyBatis)整合步骤分析:

  • Spring

    • 框架基础
  • MyBatis

    • mysql + druid + pagehelper
  • Spring 整合 MyBatis

  • junit 测试业务层接口

  • SpringMVC

    • rest 风格(postman 测试请求结果)
    • 数据封装 json(jackson)
  • Spring 整合 SpringMVC

    • Controller 调用 Service
  • 其他

    • 表现层数据封装

    • 自定义异常

5 个关键步骤:

  1. Spring 环境
  2. MyBatis 环境
  3. Spring 整合 MyBatis
  4. SpringMVC 环境
  5. Spring 整合 SpringMVC

2、环境准备

1. 数据表

create table user (
    uuid int(10) not null auto_increment,
    userName varchar(100) default null,
    password varchar(100) default null,
    realName varchar(100) default null,
    gender int(1) default null,
    birthday date default null,
    primary key (uuid)
);

2. Maven 依赖

        <!-- spring环境 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.1.9.RELEASE</version>
        </dependency>

        <!-- mybatis环境 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.3</version>
        </dependency>
        <!-- mysql环境 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.11</version>
        </dependency>
        <!-- spring整合jdbc -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.1.9.RELEASE</version>
        </dependency>
        <!-- spring整合mybatis -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.3</version>
        </dependency>
        <!-- druid连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.16</version>
        </dependency>
        <!-- 分页插件坐标 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.1.2</version>
        </dependency>

        <!-- springmvc环境 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.1.9.RELEASE</version>
        </dependency>
        <!-- servlet环境 -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
        <!-- jackson相关坐标3个 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.9.0</version>
        </dependency>

        <!-- 其他组件 -->
        <!-- junit5单元测试 -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>RELEASE</version>
            <scope>test</scope>
        </dependency>
        <!-- spring整合junit -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.1.9.RELEASE</version>
        </dependency>

3. 项目结构

  • 创建项目,组织项目结构,创建包

  • 创建表与实体类

  • 创建三层架构对应的模块、接口与实体类,并建立关联关系

  • 数据层接口(代理自动创建实现类)

    • 业务层接口 + 业务层实现类
    • 表现层类

3、配置文件+注解开发

项目地址:JavaDemo: 小示例 - Gitee.com

1. Spring 整合 Mybatis

  • Mapper 映射文件
<?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.dao.UserDao">

    <!--添加-->
    <insert id="save" parameterType="user">
        insert into user(userName,password,realName,gender,birthday)values(#{userName},#{password},#{realName},#{gender},#{birthday})
    </insert>

    <!-- 删除 -->
    <delete id="delete" parameterType="int">
        delete from user where uuid = #{uuid}
    </delete>

    <!-- 修改 -->
    <update id="update" parameterType="user">
        update user set userName=#{userName},password=#{password},realName=#{realName},gender=#{gender},birthday=#{birthday} where uuid=#{uuid}
    </update>

    <!-- 查询单个 -->
    <select id="get" resultType="user" parameterType="int">
        select * from user where uuid= #{uuid}
    </select>

    <!-- 分页查询 -->
    <select id="getAll" resultType="user">
        select * from user
    </select>

    <!-- 登录 -->
    <select id="getByUserNameAndPassword" resultType="user" >
        select * from user where userName=#{userName} and password=#{password}
    </select>

</mapper>
  • Dao 接口
package com.dao;

import com.domain.User;
import org.apache.ibatis.annotations.Param;

import java.util.List;

public interface UserDao {

    /**
     * 添加用户
     * @param user
     * @return
     */
    public boolean save(User user);

    /**
     * 修改用户
     * @param user
     * @return
     */
    public boolean update(User user);

    /**
     * 删除用户
     * @param uuid
     * @return
     */
    public boolean delete(Integer uuid);

    /**
     * 查询所有用户
     * 由于会在服务层使用分页插件,因此此处不用传页数和条数
     * @return
     */
    public List<User> getAll();

    /**
     * 查询单个用户
     * @param uuid
     * @return
     */
    public User get(Integer uuid);

    /**
     * 根据用户名密码查询用户信息
     * 注意:数据层操作不要和业务层操作的名称混淆。通常数据层仅反映与数据库间的信息交换,不体现业务逻辑
     * @param username
     * @param password
     * @return
     */
    public User getByUserNameAndPassword(@Param("userName") String username, @Param("password") String password);

}

2. Service 实现类

package com.service.impl;

import com.dao.UserDao;
import com.domain.User;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.service.UserService;
import org.apache.ibatis.annotations.Param;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;

    @Override
    public boolean save(User user) {
        return userDao.save(user);
    }

    @Override
    public boolean update(User user) {
        return userDao.update(user);
    }

    @Override
    public boolean delete(Integer uuid) {
        return userDao.delete(uuid);
    }

    @Override
    public PageInfo<User> getAll(int page, int size) {
        PageHelper.startPage(page, size);
        List<User> all = userDao.getAll();
        return new PageInfo<User>(all);
    }

    @Override
    public User get(Integer uuid) {
        return userDao.get(uuid);
    }

    @Override
    public User login(String username, String password) {
        return userDao.getByUserNameAndPassword(username, password);
    }
}

3. Spring 核心配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!-- 开启bean注解扫描 -->
    <context:component-scan base-package="com"/>

    <!-- 整合Mybatis到spring中 -->
    <bean class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="typeAliasesPackage" value="com.domain"/>
        <!--分页插件-->
        <property name="plugins">
            <array>
                <bean class="com.github.pagehelper.PageInterceptor">
                    <property name="properties">
                        <props>
                            <prop key="helperDialect">mysql</prop>
                            <prop key="reasonable">true</prop>
                        </props>
                    </property>
                </bean>
            </array>
        </property>
    </bean>

    <!-- 数据源 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!-- 加载properties文件,获取数据库连接信息 -->
    <context:property-placeholder location="classpath*:jdbc.properties"/>

    <!-- 映射扫描:加载Mapper配置文件 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.dao"/>
    </bean>

    <!-- 事务管理器 -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 开启事务 -->
    <tx:annotation-driven transaction-manager="txManager"/>

</beans>

4. 整合 junit5

package com.service;

import com.domain.User;
import com.github.pagehelper.PageInfo;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.Date;

// 设定 spring 专用的类加载器
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations="classpath:applicationContext.xml")
class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    public void testSave(){
        User user = new User();
        user.setUserName("Jock");
        user.setPassword("root");
        user.setRealName("Jockme");
        user.setGender(1);
        user.setBirthday(new Date(333333000000L));

        boolean result = userService.save(user);
        assert result;

    }

    @Test
    public void testDelete(){
        User user = new User();
        boolean result = userService.delete(3);
        assert result;
    }

    @Test
    public void testUpdate(){
        User user = new User();
        user.setUuid(1);
        user.setUserName("Jockme");
        user.setPassword("root");
        user.setRealName("JockIsMe");
        user.setGender(1);
        user.setBirthday(new Date(333333000000L));

        boolean result = userService.update(user);
        assert result;
    }

    @Test
    public void testGet(){
        User user = userService.get(1);
        System.out.println(user);
        assert user != null;
    }

    @Test
    public void testGetAll(){
        PageInfo<User> all = userService.getAll(2, 2);
        System.out.println(all);
        assert all.getList().size() == 2;
        System.out.println(all.getList().get(0));
        System.out.println(all.getList().get(1));
    }

    @Test
    public void testLogin(){
        User user = userService.login("Jockme", "root");
        System.out.println(user);
        assert user != null;
    }
}

5. Spring 整合 SpringMVC

SpringMVC:

  • web.xml
<servlet>
    <servlet-name>DispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:spring-mvc.xml</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>DispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>
  • spring-mvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <mvc:annotation-driven/>

    <context:component-scan base-package="com.controller"/>

</beans>
  • controller 层
@RestController  
@RequestMapping("/user")  public class UserController {
    @PostMapping
    public boolean save(User user) {  
        System.out.println("save ..." + user);  return true;
    }
    @PostMapping("/login")
    public User login(String userName,String password){  
        System.out.println("login ..." + userName + " ," +password);
        return null;
    }
}
  • web.xml 加载 Spring 环境
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath*:applicationContext.xml</param-value>
</context-param>

<!--启动服务器时,通过监听器加载spring运行环境-->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
  • Controller 调用 Service
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @PostMapping
    public boolean save(User user){
        return userService.save(user);
    }
}

6. 表现层数据封装

前端接收表现层返回的数据种类各式各样:

数据类型示例格式
操作是否成功true / false格式 A
基本数据数值、字符串格式 B
对象数据json 对象格式 C
集合数据json 数组格式 D

返回的数据格式应统一设计: 

示例代码:

  • 返回数据对象封装
public class Result {
    // 操作结果编码
    private Integer code;
    // 操作数据结果
    private Object data;
    // 消息
    private String message;
    public Result(Integer code) {
        this.code = code;
    }
    public Result(Integer code, Object data) {
        this.code = code;
        this.data = data;
    }
    // 省略展示 getter、setter
}
  • 状态码常量可以根据自己的业务需求设定
public class Code {
    public static final Integer SAVE_OK = 20011;
    public static final Integer SAVE_ERROR = 20010;
    // 其他编码
}
  • controller 调用
@RestController
public class UserController {
    @Autowired
    private UserService userService;
    @PostMapping
    public Result save(User user){
        boolean flag = userService.save(user);
        return new Result(flag ? Code.SAVE_OK:Code.SAVE_ERROR);
    }
    @GetMapping("/{uuid}")
    public Result get(@PathVariable Integer uuid){
        User user = userService.get(uuid);
        return new Result(null != user ?Code.GET_OK: Code.GET_ERROR,user);
    }
}

7. 自定义异常

设定自定义异常,封装程序执行过程中出现的问题,便于表现层进行统一的异常拦截并进行处理。

  • BusinessException
  • SystemException

自定义异常消息返回时需要与业务正常执行的消息按照统一的格式进行处理。

代码示例:

  • 定义 BusinessException
public class BusinessException extends RuntimeException {
    //自定义异常中封装对应的错误编码,用于异常处理时获取对应的操作编码
    private Integer code;

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public BusinessException(Integer code) {
        this.code = code;
    }

    public BusinessException(String message, Integer code) {
        super(message);
        this.code = code;
    }

    public BusinessException(String message, Throwable cause,Integer code) {
        super(message, cause);
        this.code = code;
    }

    public BusinessException(Throwable cause,Integer code) {
        super(cause);
        this.code = code;
    }

    public BusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace,Integer code) {
        super(message, cause, enableSuppression, writableStackTrace);
        this.code = code;
    }
}

  • 控制层
@GetMapping("/{uuid}")
public Result get(@PathVariable Integer uuid){
    User user = userService.get(uuid);
    // 模拟出现异常,使用条件控制,便于测试结果
    if (uuid == 10 ) throw new BusinessException("查询出错啦,请重试!",Code.GET_ERROR);
    return new Result(null != user ? Code.GET_OK : Code.GET_ERROR, user);
}
  • 返回消息兼容异常信息
@Component
@ControllerAdvice
public class ExceptionAdivce {
    @ExceptionHandler(BusinessException.class)
    @ResponseBody
    // 对出现异常的情况进行拦截,并将其处理成统一的页面数据结果格式
    public Result doBusinessException(BusinessException e){
        return new Result(e.getCode(), e.getMessage());
    }
}

4、纯注解开发

项目地址:JavaDemo: 小示例 - Gitee.com

1. 注解替代 applicationContext.xml

package com.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.*;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration
@ComponentScan(
        // 等同于<context:component-scan base-package="com">
        value = "com",
        // 等同于<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION, classes={Controller.class})
)
// 等同于<context:property-placeholder location="classpath*:jdbc.properties"/>
@PropertySource("classpath:jdbc.properties")
// 等同于<tx:annotation-driven/>,bean的名称默认取transactionManager
@EnableTransactionManagement
@Import({MybatisConfig.class, JdbcConfig.class})
public class SpringConfig {
    // 等同于<bean id="txManager"/>
    @Bean("transactionManager")
    // 等同于<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    public DataSourceTransactionManager getTxManager(@Autowired DataSource dataSource){
        DataSourceTransactionManager tm = new DataSourceTransactionManager();
        // 等同于<property name="dataSource" ref="dataSource"/>
        tm.setDataSource(dataSource);
        return tm;
    }
}

2. 注解替代 spring-mvc.xml

package com.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
// 等同于<context:component-scan base-package="com.controller"/>
@ComponentScan("com.controller")
// 功能强大于<mvc:annotation-driven/>
@EnableWebMvc
public class SpringMvcConfig {
}

@EnableWebMvc 功能:

  1. 支持 ConversionService 的配置,可以方便配置自定义类型转换器。
  2. 支持 @NumberFormat 注解格式化数字类型。
  3. 支持 @DateTimeFormat 注解格式化日期数据,包括 Date、Calendar、JodaTime(JodaTime 要导包)。
  4. 支持@Valid的参数校验(需要导入 JSR-303 规范)。
  5. 配合第三方 jar 包和 SpringMVC 提供的注解读写 XML 和 JSON 格式数据。

5、Maven配置

项目地址:JavaDemo: 小示例 - Gitee.com

1. 分模块开发

  • 模块中仅包含当前模块对应的功能类与配置文件。
  • Spring 核心配置文件根据模块功能不同进行独立制作。
  • 当前模块所依赖的模块通过导入坐标的形式加入当前模块后才可以使用。
  • web.xml 需要加载所有的 Spring 核心配置文件。

2. 聚合与继承

作用:

  • 聚合用于快速构建项目继承用于快速配置。

相同点:

  • 聚合与继承的 pom.xml 文件打包方式均为 pom,可以将两种关系制作到同一个 pom 文件中。
  • 聚合与继承均属于设计型模块,并无实际的模块内容。

不同点:

  • 聚合是在当前模块中配置关系,聚合可以感知到参与聚合的模块有哪些。
  • 继承是在子模块中配置关系,父模块无法感知哪些子模块继承了自己。

1)聚合

作用:聚合用于快速构建 maven 工程,可以一次性构建多个项目/模块。

制作方式:

  • 创建一个空模块,打包类型定义为“pom”:
<packaging>pom</packaging>
  • 定义当前模块进行构建操作时关联的其他模块名称
<modules>
    <module>../ssm controller</module>
    <module>../ssm service</module>
    <module>../ssm dao</module>
    <module>../ssm pojo</module>
</modules>

注意事项:参与聚合操作的模块最终执行顺序与模块间的依赖关系有关,与配置顺序无关。

2)继承

作用:通过继承可以实现在子工程中沿用父工程中的配置。

* Maven 中的继承与 java 中的继承相似,在子工程中配置继承关系。

制作方式:

  • 在子工程中声明其父工程坐标与对应的位置
<!--定义该工程的父工程-->
<parent>
    <groupId>com</groupId>
    <artifactId>ssm</artifactId>
    <version>1.0-SNAPSHOT</version>
</parent>

继承依赖定义:

  • 在父工程中定义依赖管理
<!--声明此处进行依赖管理-->
<dependencyManagement>
    <!-- 具体的依赖 -->
    <dependencies>
        <!-- spring环境 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.1.9.RETFASE</version>
        </dependency>
    </dependencies>
<dependencyManagement>

Maven 提供的 dependentcyManagement 元素既能让子模块继承到父模块的依赖配置,又能保证子模块依赖使用的灵活度。在 dependentcyManagement 元素下的依赖声明不会引入实际的依赖,而是定义了依赖的版本,对版本进行同一管理,避免出现版本不一致的情况。

继承依赖使用:

  • 在子工程中定义依赖关系,无需声明依赖版本,版本参照父工程中依赖的版本
<dependencies>
    <!-- spring环境 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
    </dependency>
</dependencies>

可继承的资源:

  • groupld:项目组 ID,项目坐标的核心元素
  • version:项目版本,项目坐标的核心因素
  • description:项目的描述信息
  • organization:项目的组织信息
  • inceptionYear:项目的创始年份
  • url:项目的 URL 地址
  • developers:项目的开发者信息
  • contributors:项目的贡献者信息
  • distributionManagement:项目的部署配置
  • issueManagement:项目的缺陷跟踪系统信息
  • dependencyManagement:项目的依赖管理配置
  • properties:自定义的 Maven 属性
  • dependencies项目的依赖配置
  • build:包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等
  • repositories:项目的仓库配置
  • ciManagement项目的持续集成系统信息
  • scm:项目的版本控制系统信息
  • malilingLists:项目的邮件列表信息
  • reporting:包括项目的报告输出目录配置、报告插件配置等

3. 属性

1)自定义属性

  • 作用:等同于定义变量,方便统一维护。

  • 定义格式

<!-- 定义自定义属性 -->
<properties>
    <spring.version>5.1.9.RELEASE</spring.version>
    <junit.version>4.12</junit.version>
</properties>
  • 调用格式
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>${spring.version}</version>
</dependency>

2)内置属性

  • 作用:使用 Maven 内置属性,快速配置。

  • 调用格式

${basedir}
${version}

3)Setting 属性

  • 作用:使用 Maven 配置文件 setting.xml 中的标签属性,用于动态配置。

  • 调用格式

${settings.localRepository)

4)Java 系统属性

  • 作用:读取 Java 系统属性。

  • 调用格式

${user.home}
  • 系统属性查询方式
mvn help:system

4. 版本管理

工程版本:

  • SNAPSHOT(快照版本)

    • 项目开发过程中,为方便团队成员合作,解决模块间相互依赖和时时更新的问题,开发者对每个模块进行构建的时候,输出的临时性版本叫快照版本(测试阶段版本)。
    • 快照版本会随着开发的进展不断更新。
  • RELEASE(发布版本)

    • 项目开发到进入阶段里程碑后,向团队外部发布较为稳定的版本,这种版本所对应的构件文件是稳定的,即便进行功能的后续开发,也不会改变当前发布版本内容,这种版本称为发布版本。

工程版本号约定规范:

  • <主版本>.<次版本>.<增量版本>.<里程碑版本>
    • 主版本:表示项目重大构的变更,如 spring5 相较于 spring4 的迭代。
    • 次版本:表示有较大的功能增加和变化,或者全面系统地修复漏洞。
    • 增量版本:表示有重大漏洞的修复。
    • 里程碑版本:表明一个版本的里程碑(版本内部)。这样的版本同下一个正式版本相比,相对来说不是很稳定,有待更多的测试。
  • 范例:5.1.9.RELEASE

5. 资源配置

多文件维护:

配置文件引用 pom 属性:

  • 作用:在任意配置文件中加载 pom 文件中定义的属性。

  • 调用格式

# 在配置文件中调用自定义属性
${jdbc.url}
  • 开启配置文件加载 pom 属性
<!-- 配置资源文件对应的信息 -->
<resources>
    <resource>
        <!-- 设定配居文件对应的位造目录,支持使用属性动态没定路径 -->
        <directory>${project.basedir}/src/main/resources</directory>
        <!-- 开启对配置文件的资源加载过滤 -->
        <filtering>true</filtering>
    </resource>
</resources>

6. 多环境开发配置

多环境配置: 

    <!-- 创建多环境 -->
    <profiles>
        <!-- 定义生产环境 -->
        <profile>
            <!-- 定义环境对应的唯一名称 -->
            <id>pro env</id>
            <!-- 定义环境中专用的属性值 -->
            <properties>
                <jdbc.url>jdbc:mysql://localhost:3306/world?serverTimezone=UTC</jdbc.url>
            </properties>
            <!-- 设置默认启动 -->
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
        </profile>
        <!-- 定义开发环境 -->
        <profile>
            <id>dev env</id>
        </profile>
    </profiles>

加载指定环境:

# 格式
mvn 指令 -P 定义的环境id
# 范例
mvn install -P pro_env

7. 测试配置

方法一:通过命令跳过测试

mvn 指令 -D skipTests
  • 注意事项:执行的指令的生命周期必须包含测试环节。

方法二:通过 IDEA 界面操作跳过测试

方法三:通过 pom 进行测试配置 

                <!-- 测试配置 -->
                <plugin>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>3.0.0-M5</version>
<!--                    <configuration>-->
                        <!-- 跳过测试 --> 
<!--                        <skipTests>true</skipTests> -->
<!--                    </configuration> -->
                    <configuration>
                        <!-- 包含指定的测试用例 -->
                        <includes>
                            <!-- 默认测试文件的命名规则:
                                "**/Test*.java"
                                "**/*Test.java"
                                "**/*Tests.java"
                                "**/*TestCase.java"
                                如果现有测试文件不符合以上命名,可以在 pom.xml 添加自定义规则
                            -->
                            <include>**/**.java</include>
                        </includes>
                        <!-- 忽略指定的测试用例 
                            <excludes>
                                <exclude>

                                 </exclude>
                            </excludes>
                        -->
                    </configuration>
                </plugin>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1580.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VScode 官网下载太慢解决方法

用惯了HBuilder 和 WebStorm&#xff0c;发现身边很多人也在用 VSCode&#xff0c;就像尝试下。 但是&#xff0c;官网下载太慢了。https://code.visualstudio.com/ 我可是 500M 的宽带啊~&#xff01;&#xff01;&#xff01; 有小道消息说&#xff0c;VSCode 对中国大陆用…

【Linux】软件包管理器 --- yum

目录&#x1f308;前言Linux 软件包管理器 yum&#x1f681;1、什么是软件包&#x1f682;2、yum指令&#x1f683;3、关于 rzsz&#x1f308;前言 本篇文章进行开发工具的学习&#xff01;&#xff01;&#xff01; Linux 软件包管理器 yum &#x1f681;1、什么是软件包 源…

heapdump 攻击面利用

点击上方蓝字关注我们 一、heapdump案例 1.1 项目中的分析 这个是项目中遇到的一个例子&#xff0c;发现了heapdump泄露&#xff0c;但没有找到可用session&#xff0c;当时就想着内存中应该是有账号密码的&#xff0c;于是就开始找了起来。 用OQL进行搜索&#xff0c;先找…

异地远程群晖NAS教程【cpolar内网穿透】

公网/外网访问群晖NAS&#xff0c;虽然品牌NAS都会有自带DDNS服务&#xff0c;但是公网IPV4很难申请&#xff0c;所以很多用户都会转战内网穿透。网上也有很多教程&#xff0c;比如frp…&#xff0c;但是需要有公网服务器支持来进行搭建&#xff0c;有一定的要求。 cpolar内网穿…

Graphviz安装教程

Graphviz安装教程 官网下载graphviz的安装包 &#xff0c;网址&#xff1a; https://graphviz.org/download/ 进入下面页面。此处我选择这个版本进行下载&#xff0c; 后面就是安装和配置环境变量问题 安装的时候记住安装路径(最好放到anaconda文件夹下&#xff0c;即**\An…

通讯录的实现(静态版本和动态版本和文件版本)

为什们要实现通讯录&#xff1f; 主要是为了让我们将结构体的知识&#xff0c;了解的更加深刻&#xff0c;将结构体应用一下&#xff0c;我们先将静态的通讯录实现&#xff0c;在进行改良&#xff0c;用动态内存的知识再将通讯录改造一边&#xff0c;将动态内容的知识也运用一…

C++ Reference: Standard C++ Library reference: C Library: cstring: strncat

C官网参考链接&#xff1a;https://cplusplus.com/reference/cstring/strncat/ 函数 <cstring> strncat char * strncat ( char * destination, const char * source, size_t num ); 从字符串中追加字符 将source的第一个num个字符追加到destination&#xff0c;并追加…

重型机床热误差补偿温度敏感测点的识别与优化选择

重型机床热误差补偿温度敏感测点的识别与优化选择 动机与主要贡献 ● 热误差预测的性能主要取决于预测模型的准确性和鲁棒性以及输入的温度变量 ● 在大型数控机床热误差补偿方案中&#xff0c;由于温度敏感点的结构和发热机理复杂&#xff0c;目前仍缺乏有效的温度敏感点识别…

java---贪心---区间分组(每日一道算法2022.10.28)

题目&#xff1a; 给定 N 个闭区间 [ai,bi]&#xff0c;请你将这些区间分成若干组&#xff0c;使得每组内部的区间两两之间&#xff08;包括端点&#xff09;没有交集&#xff0c;并使得组数尽可能小 输出最小组数 第一行包含整数 N&#xff0c;表示区间数 接下来 N 行&#x…

Redis篇(2)——main函数

众所周知&#xff0c;redis是C语言写的。那么main函数来一波 int main(int argc, char **argv) {struct timeval tv;int j;//运行测试方法 #ifdef REDIS_TESTif (argc 3 && !strcasecmp(argv[1], "test")) {if (!strcasecmp(argv[2], "ziplist")…

SpringBoot

SpringBoot项目搭建方式1项目搭建方式2SpringBoot文件配置application.propertiesapplication.ymlSpringBoot整合MybatisSpringBoot整合logbackSpringBoot整合pageHelperSpringBoot整合DruidSpringBoot整合FreeMarkerFreeMarker常用指令(遍历List集合)FreeMarker遍历Map集合Spr…

【树莓派不吃灰】Linux系统下必知必会的一些命令

目录1. 前言2. 必知必会命令2.1 ls -al 列出当前目录下的文件2.2 cd 改变目录2.3 pwd 输出当前目录2.4 clear 清屏命令2.5 mkdir 新建目录2.6 rmdir 删除目录2.7 cat 显示或连接文件内容2.8 touch 创建文件 & cat > 创建文件2.9 rm 删除文件2.10 mv 移动/重命名文件/目录…

【小程序开发】事件监听 | 类型划分 | 属性分析

&#x1f373;作者&#xff1a;贤蛋大眼萌&#xff0c;一名很普通但不想普通的程序媛\color{#FF0000}{贤蛋 大眼萌 &#xff0c;一名很普通但不想普通的程序媛}贤蛋大眼萌&#xff0c;一名很普通但不想普通的程序媛&#x1f933; &#x1f64a;语录&#xff1a;多一些不为什么的…

基于SSM实现前后端分离在线考试管理系统

作者主页&#xff1a;编程指南针 作者简介&#xff1a;Java领域优质创作者、CSDN博客专家 、掘金特邀作者、多年架构师设计经验、腾讯课堂常驻讲师 主要内容&#xff1a;Java项目、毕业设计、简历模板、学习资料、面试题库、技术互助 文末获取源码 项目编号&#xff1a;BS-XX-…

[kafka]二.优化(如何保证不丢数据,且不重复[一次且仅一次])

二.优化 1.不丢失数据ack-1 当Producer向Leader发送消息时&#xff0c;可以通过ack的值来设置可靠性级别。 1&#xff09;1&#xff08;默认&#xff09; 意味着ISR中的Leader已成功收到消息并且Producer得到Leader收到消息的确认。如果ISR中的副本数还没有来得及拉取数据就…

Linux 进程替换深剖

目录传统艺能&#x1f60e;概念&#x1f914;细则&#x1f914;原理&#x1f914;exec 函数&#x1f914;execl&#x1f60b;execlp&#x1f60b;execle&#x1f60b;execv&#x1f60b;execvp&#x1f60b;execve&#x1f60b;实现简易 shell&#x1f914;传统艺能&#x1f60…

【python】之常用类型(包括进制)之间的转换

个人主页&#xff1a;天寒雨落的博客_CSDN博客-C,CSDN竞赛,python领域博主 特别标注&#xff1a;仅为自己的学习记录笔记&#xff0c;方便复习和加深记忆&#xff0c;仅供借鉴参考&#xff01; 目录 一、字符和整数之间的转换 1.整数转字符 chr(x) 2.字符转整数 ord(x) 二、…

Git操作复习笔记

Git操作复习笔记一、git基础1.1 安装1.2 简单的命令1.2.1 基本工作流程1.2.3 git使用前配置1.2.4 git提交步骤1.2.5 恢复记录1.2.6 撤销二、git分支操作2.1 分支细分2.2 分支命令2.3 暂时保存更改三、github操作3.1 多人协作开发的流程3.2 创建远程仓库3.3 远程仓库克隆到本地仓…

【K8S系列】Kubernetes的网络模型

目录 一、k8s的三种网络 二、service网络 2.1 netfilter 2.2 iptables 2.3 clustip 一、k8s的三种网络 Node Network: 与外部网络接口 Service Network&#xff1a; ipvs规则当中的网络、路由提供调度 Pod Network&#xff1a; 节点当中pod的内部网络无法与外界通信 其中&…

【python初学者日记】selenium初体验——“秒杀商品”、“清空购物车”技能养成记(一)

【python初学者日记】selenium初体验——“秒杀商品”、“清空购物车”技能养成记&#xff08;一&#xff09;用python解决“清空购物车”、“秒杀商品”问题合集&#xff1a;1、Mac版在pycharm和终端上使用pip显示&#xff1a;pip: command not found Mac2、Mac版在pycharm中*…