JDBC其实就是 Java 官方提供的一套规范(接口),用于帮助开发人员快速实现不同关系型数据库的连接。
程序运行的时候,数据都是在内存中的。当程序终止的时候,通常都需要将数据保存到磁盘上,无论是保存到本地磁盘,还是通过网络保存到服务器上,最终都会将数据写入磁盘文件。
而如何定义数据的存储格式就是一个大问题。如果我们自己来定义存储格式,比如保存一个班级所有学生的成绩单:
名字 | 成绩 |
---|---|
Michael | 99 |
Bob | 85 |
Bart | 59 |
Lisa | 87 |
你可以用一个文本文件保存,一行保存一个学生,用,隔开:
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代码基本不变。
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 {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
2)获取数据库连接对象
static Connection getConnection(String url, String user, String password);
jdbc:mysql://ip地址(域名):端口号/库名
3. Connection(数据库连接对象)
Statement createStatement();
PreparedStatement prepareStatement(String sql);
4. Statement(SQL 执行对象)
执行 DML 语句:int executeUpdate(String sql);
执行 DQL 语句:ResultSet executeQuery(String sql);
释放资源
5. ResultSet(结果集对象)
boolean next();
XXX getXxx("列名");
String getString("name");
、int getInt("age");
6. PreparedStatement 预编译
SQL 注入:
SQL 注入演示:在登录界面,输入一个错误的用户名或密码,也可以登录成功。
SQL 注入的原理:
PreparedStatement 即预编译 SQL 语句的执行对象,是 SQL 注入的防御手段之一。其原理是:在执行 SQL 语句之前,将 SQL 语句进行提前编译,在明确 SQL 语句的格式(执行计划)后,就不会改变了。因此剩余的内容都会认为是参数。
参数使用?作为占位符:
setXxx(参数 1, 参数 2);
int executeUpdate();
ResultSet executeQuery();
1. JDBC查询
前面我们讲了Java程序要通过JDBC接口来查询数据库。JDBC是一套接口规范,它在哪呢?就在Java的标准库java.sql里放着,不过这里面大部分都是接口。接口并不能直接实例化,而是必须实例化对应的实现类,然后通过接口引用这个实例。那么问题来了:JDBC接口的实现类在哪?
因为JDBC接口并不知道我们要使用哪个数据库,所以,用哪个数据库,我们就去使用哪个数据库的“实现类”,我们把某个数据库实现了JDBC接口的jar包称为JDBC驱动。
因为我们选择了MySQL 5.x作为数据库,所以我们首先得找一个MySQL的JDBC驱动。所谓JDBC驱动,其实就是一个第三方jar包,我们直接添加一个Maven依赖就可以了:
mysql
mysql-connector-java
5.1.47
runtime
注意到这里添加依赖的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://:/?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, BOOL | boolean |
INTEGER | int |
BIGINT | long |
REAL | float |
FLOAT, DOUBLE | double |
CHAR, VARCHAR | String |
DECIMAL | BigDecimal |
DATE | java.sql.Date, LocalDate |
TIME | java.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特性:
数据库事务可以并发执行,而数据库系统从效率考虑,对事务定义了不同的隔离级别。SQL标准定义了4种隔离级别,分别对应可能出现的数据不一致的情况:
Isolation Level | 脏读(Dirty Read) | 不可重复读(Non Repeatable Read) | 幻读(Phantom Read) |
---|---|---|---|
Read Uncommitted | Yes | Yes | Yes |
Read Committed | - | Yes | Yes |
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特性:
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操作。
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 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 findAll() {
ArrayList 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 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 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 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("学生信息删除失败!");
}
}
}
事务一般在 service 层控制管理,因为事务一般与业务耦合,而不是与通用的 dao 层耦合。
import java.util.List;
public interface UserService {
/**
* 批量添加
* @param users
*/
void batchAdd(List users);
}
@Override
public void batchAdd(List 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);
}
}
创建线程是一个昂贵的操作,如果有大量的小任务需要执行,并且频繁地创建和销毁线程,实际上会消耗大量的系统资源,往往创建和消耗线程所耗费的时间比执行任务的时间还长,所以,为了提高效率,可以用线程池。
类似的,在执行JDBC的增删改查的操作时,如果每一次操作都来一次打开连接,操作,关闭连接,那么创建和销毁JDBC连接的开销就太大了。为了避免频繁地创建和销毁JDBC连接,我们可以通过连接池(Connection Pool)复用已经创建好的连接。
数据库连接池是一种复用Connection的组件,它可以避免反复创建新连接,提高JDBC代码的运行效率。
数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个。这项技术能解决建立数据库连接耗费资源和时间的问题,明显提高对数据库操作的性能。
数据库连接池原理:
JDBC连接池有一个标准的接口javax.sql.DataSource,注意这个类位于Java标准库中,但仅仅是接口。要使用JDBC连接池,我们必须选择一个JDBC连接池的实现。
常用的JDBC连接池有:
1. HikariCP
目前使用最广泛的是HikariCP。我们以HikariCP为例,要使用JDBC连接池,先添加HikariCP的依赖如下:
com.zaxxer
HikariCP
2.7.1
紧接着,我们需要创建一个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 等。
使用步骤:
使用示例:
com.mysql.jdbc.Driver
jdbc:mysql://localhost:3306/world
root
admin
5
10
3000
com.mysql.jdbc.Driver
jdbc:mysql://localhost:3306/world
root
admin
5
8
1000
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 基本概念及架构介绍
使用步骤:
示例:
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);
}
}
1. DBUtils 介绍
什么是 DBUtils ?
DBUtils 是 Apache 开源的 Java 编程中的数据库操作实用工具,小巧简单实用。
DBUtils 封装了对 JDBC 的操作,简化了 JDBC 操作,可以少写代码。
DBUtils 的三个核心对象:
QueryRunner 类
:提供对 SQL 语句操作的 API,它主要有三个方法:
update()
:用于执行 insert、update、deletebatch()
:批处理ResultSetHandler 接口
:用于定义 select 操作后怎样封装结果集。DbUtils 类
:一个工具类,定义了关闭资源与事务处理的方法。2. 使用案例
DBUtils 使用步骤:
3. QueryRunner 类(执行对象)
构造函数:
new QueryRunner();
new QueryRunner(DataSource ds);
示例:
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;
}
}
}
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 list = qr.query("select * from student where sid=? and name=?", new BeanListHandler(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 list = qr.query("select * from student", new ResultSetHandler>(){
// 当query方法执行完select语句,就会将结果集以参数的形式传递进来
public List handle(ResultSet rs) throws SQLException {
List list = new ArrayList();
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
5. ThreadLocal(当前线程对象)
作用:调用该类的 get 方法,永远返回当前线程放入的数据(线程局部变量)。
// 模拟 ThreadLocal 的设计,明白其作用
public class ThreadLocal {
private Map container = new HashMap();
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());
}
}
案例:
import java.sql.Connection;
import java.sql.SQLException;
public class ManageThreadLocal {
private static ThreadLocal t1 = new ThreadLocal();
// 得到当前线程中的一个连接
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();
}
}
}
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.class),name);
}
}
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();
} // 关闭
}
}
}
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 操作的问题与解决方案:
问题:
对应的解决方案:
Mybatis 通过 xml 或注解的方式将要执行的各种 statement 配置起来,并将 Java 对象和 statement 中 SQL 的动态参数进行映射,生成最终执行的 SQL 语句。最后 Mybatis 框架执行 SQL 并将结果映射为 Java 对象并返回。
1. Resources(加载资源的工具类)
核心方法:
2. SqlSessionFactoryBuilder(构建器)
SqlSessionFactoryBuilder:获取 SqlSessionFactory 工厂对象的功能类
核心方法:
3. SqlSessionFactory(工厂对象)
SqlSessionFactory:获取 SqlSession 构建者对象的工厂接口
核心方法:
4. SqlSession 会话对象
SqlSession:构建者对象接口,用于执行 SQL、管理事务、接口代理
SqlSession 实例在 MyBatis 中是非常强大的一个类,在这里能看到所有执行语句、提交或回滚事务和获取映射器实例的方法。
MyBatis 开发步骤:
1. 环境搭建
1)导入 MyBatis 的 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 全局的设置和属性信息,如数据库的连接、事务、连接池信息等。
全局配置文件可自定义命名,其配置内容和顺序如下(顺序不能乱):
示例:src 目录下的 MyBatisConfig.xml
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/world?serverTimezone=UTC
username=root
password=admin
加载的顺序:
Mybatis 的默认别名:
Mapper:
(推荐)
3. Mybatis 映射配置文件
映射配置文件包含了数据和对象之间的映射关系以及要执行的 SQL 语句。
映射文件可自定义命名,一般按照规范实体类名Mapper.xml
,这种命名规范是由 ibatis 遗留下来的。
示例:src 目录下的 StudentMapper.xml
SELECT LAST_INSERT_ID()
INSERT INTO student (name, age) VALUES (#{name},#{age})
INSERT INTO student (id,name,age)
VALUES(#{id}, #{name}, #{age})
UPDATE student SET name = #{name}, age = #{age} WHERE id = #{id}
DELETE FROM student WHERE id = #{id}
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 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 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();
}
}
}
}
1. 输入映射:parameterType
1)简单类型
如学生 id 等基本数据类型。
2)POJO 类型
POJO(Plain Ordinary Java Object)即简单的 Java 对象,实际就是普通 Java Beans。
3)包装 POJO 类型
包装 POJO 类型,即 Java 对象中有其他 Java 对象的引用。
需求:
综合查询时,可能会根据用户信息、商品信息、订单信息等作为条件进行查询,用户信息中的查询条件由用户的名称和性别进行查询。
创建包装 POJO 类型:
4)Map 类型
同传递 POJO 对象一样,Map 的 key 相当于 POJO 的属性。
Public void testFindUserByHashmap()throws Exception{
// 获取session
SqlSession session = sqlSessionFactory.openSession();
// 获限mapper接口实例
UserMapper userMapper = session.getMapper(UserMapper.class);
// 构造查询条件Hashmap对象
HashMap map = new HashMap();
map.put("id", 1);
map.put("username", "管理员");
// 传递Hashmap对象查询用户列表
Listlist = userMapper.findUserByHashmap(map);
// 关闭session
session.close();
}
注意:当传递的 map 中的 key 和 sql 中解析的 key 不一致时,程序不会报错,只是通过 key 获取的值为空。
2. 输出映射
1)resultType
使用要求:
2)resultMap
使用要求:
示例需求:
对以下 SQL 查询的结果集进行对象映射:Select id id_, username username_, sex sex_ from user where id = 1;
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 selectByName(String name);
// 3. 添加学生
public void insertBySelectLastId(Student Student);
}
Dao 实现类:
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 selectByName(String name) {
// 创建SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 调用SqlSession的增删改查方法
// 第一个参数:表示statement的唯一标示
List 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 代理的开发规范:
总结:
Mapper 接口开发的方式: 程序员只需定义接口就可以对数据库进行操作。那么具体的对象是怎么创建的?
代码示例:
import com.bean.Student;
import java.util.List;
public interface StudentMapper {
// 查询全部
public abstract List 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 selectCondition(Student stu);
// 根据多个id查询
public abstract List selectByIds(List 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 方式来执行增删改查。
在 Mybatis 中提供了一些动态 SQL 标签,可以让程序员更快的进行 Mybatis 的开发,这些动态 SQL 可以提高 SQL 的可重用性。
动态 SQL 指的就是 SQL 语句可以根据条件或者参数的不同,而进行动态的变化。
常用的动态 SQL 标签有 if 标签、where 标签、SQL 片段、foreach 标签。
1. if、where 标签
if 和 where 标签可用于根据实体类的不同取值,使用不同的 SQL 语句来进行查询。
比如在 id 不为空时可以根据 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);
// 获得MyBatis框架生成的UserMapper接口的实现类
StudentMapper mapper = sqlSession.getMapper( StudentMapper.class);
Student condition = new Student();
condition.setId(1);
Student student = mapper.findByCondition(condition);
2. foreach 标签
使用语法:
获取参数
示例需求:
循环执行 SQL 的拼接操作,例如:SELECT * FROM student WHERE id IN (1, 2, 5);
// 获得MyBatis框架生成的UserMapper接口的实现类
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
List ids = new ArrayList<>();
ids.add(1);
ids.add(2);
List sList = mapper.findByIds(ids);
System.out.println(sList);
3. SQL 片段抽取
将重复的 SQL 提取出来,使用时用 include 引用即可,最终达到 SQL 重用的目的。
使用语法:
需要抽取的 SQL 语句
使用示例:
1. 分页插件介绍
分页功能介绍:
MyBatis 分页插件:
2. 分页插件使用
1)导入 jar 包
2)在 Mybatis 全局配置文件中配置 PageHelper 插件
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 list = mapper.selectAll();
// 6. 处理结果
for (Student student : list) {
System.out.println(student);
}
// 获取分页的相关参数
PageInfo 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();
}
}
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)实体类
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 +
'}';
}
}
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)配置文件
4)Mapper 接口类
import com.bean.Card;
import java.util.List;
public interface OneToOneMapper {
// 查询全部
public abstract List 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 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 students; // 班级中所有学生对象
public Classes() {
}
public Classes(Integer id, String name, List 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 getStudents() {
return students;
}
public void setStudents(List 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)映射文件
4)Mapper 接口
import com.bean.Classes;
import java.util.List;
public interface OneToManyMapper {
// 查询全部
public abstract List 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 = mapper.selectAll();
// 6.处理结果
for (Classes cls : classes) {
System.out.println(cls.getId() + "," + cls.getName());
List 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 courses; // 学生所选择的课程集合
public Student() {
}
public Student(Integer id, String name, Integer age, List courses) {
this.id = id;
this.name = name;
this.age = age;
this.courses = courses;
}
public List getCourses() {
return courses;
}
public void setCourses(List 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)映射文件
4)Mapper 接口
import com.bean.Student;
import java.util.List;
public interface ManyToManyMapper {
// 查询全部
public abstract List 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 students = mapper.selectAll();
//6.处理结果
for (Student student : students) {
System.out.println(student.getId() + "," + student.getName() + "," + student.getAge());
List courses = student.getCourses();
for (Course cours : courses) {
System.out.println("\t" + cours);
}
}
// 7.释放资源
sqlSession.close();
is.close();
}
}
1. 常用注解与案例
近几年来,注解开发越来越流行,Mybatis 也可以使用注解开发方式,这样我们就可以减少编写 Mapper 映射文件了。
案例: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 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 全局配置文件:
测试类:
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 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 注解开发的多表操作
实现复杂关系映射之前我们可以在映射文件中通过配置
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 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 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 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 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 = mapper.selectAll();
// 6.处理结果
for (Classes cls : classes) {
System.out.println(cls.getId() + "," + cls.getName());
List 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 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 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 list = mapper.selectAll();
// 6.处理结果
for (Student student : list) {
System.out.println(student.getId() + "," + student.getName() + "," + student.getAge());
List 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 语句。
查询功能的实现:
新增功能的实现:
修改功能的实现:
删除功能的实现:
案例:
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 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);
}
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 findAll() {
ArrayList 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 体系结构:
Spring 发展历史:
Spring 优势:
1. 耦合与内聚
耦合(Coupling)
:代码书写过程中所使用技术的结合紧密度,用于衡量软件中各个模块之间的互联程度。
内聚(Cohesion)
:代码书写过程中单个模块内部各组成部分间的联系,用于衡量软件中各个功能模块自身内部的功能联系。
程序书写的目标:高内聚,低耦合。即同一个模块内的各个元素之间要高度紧密,但是各个模块之间的相互依存度却不要那么紧密。
2. 工厂模式发展史
3. Spring 发展历程
4. IoC 概念
IoC(Inversion Of Control)控制反转:Spring 反向控制应用程序所需要使用的外部资源。
Spring 控制的资源全部放置在 Spring 容器中,该容器称为 IoC 容器。
5. DI 概念
模拟三层架构中,表现层调用业务层功能。案例步骤如下:
导入 spring 坐标:
org.springframework
spring-context
5.1.9.RELEASE
编写业务层接口与实现类:
public interface UserService {
// 业务方法
void save();
}
import com.service.UserService;
public class UserServiceImpl implements UserService {
@Override
public void save() {
System.out.println("UserService running...");
}
}
创建 spring 配置文件:配置所需资源(UserService)为 Spring 控制的资源。
表现层(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();
}
}
1. bean 标签
类型:标签
归属:beans 标签
作用:定义 spring 中的资源,受此标签定义的资源将受到 spring 控制
基本属性:
id
:bean 的名称,通过 id 值获取 bean 。class
:bean 的类型。name
:bean 的名称,可以通过 name 值获取 bean,用于多人配合时给 bean 起别名。示例:
2. scope 属性
类型:属性
归属:bean标签
作用:定义 bean 的作用范围
示例:
取值:
singleton
:设定创建出的对象保存在 spring 容器中,是一个单例的对象。prototype
:设定创建出的对象不保存在 spring 容器中,是一个非单例的对象。3. bean 生命周期配置
名称:init-method,destroy-method
类型:属性
归属:bean 标签
作用:定义 bean 对象在初始化或销毁时完成的工作
示例:
取值:
当 scope="singleton" 时,spring 容器中有且仅有一个对象,init 方法在创建容器时仅执行一次。
当 scope="prototype" 时,spring 容器要创建同一类型的多个对象,init 方法在每个对象创建时均执行一次。
当 scope="singleton" 时,关闭容器会导致 bean 实例的销毁,调用 destroy 方法一次。
当 scope="prototype" 时,对象的销毁由垃圾回收机制 gc() 控制,destroy 方法将不会被执行。
4. set 方法注入 bean(主流方式)
名称:property
类型:标签
归属:bean 标签
作用:使用 set 方法的形式为 bean 提供资源
示例:
基本属性:
name
:对应 bean 中的属性名,要求该属性必须提供可访问的 set 方法(严格规范为此名称是 set 方法对应名称)
value
:设定非引用类型属性对应的值,不能与 ref 同时使用。
ref
:设定引用类型属性对应 bean 的 id ,不能与value同时使用。
注意:一个 bean 可以有多个 property 标签。
代码示例:
public void save(){
System.out.println("user dao running...");
}
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 标签
作用:注入集合数据类型属性
示例:
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);
}
}
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();
}
}
value1
value2
xiaoming
17
12
34
value1
value2
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 的属性赋值。
username=xiaoming
password=admin123
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);
}
}
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();
}
}
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 中加载其他配置文件。
Spring 容器加载多个配置文件:
new ClassPathXmlApplicationContext("config1.xml", "config2.xml");
Spring 容器中 bean 定义冲突问题:
同 id 的 bean,后定义的覆盖先定义的。
导入配置文件可以理解为将导入的配置文件复制粘贴到对应位置。
导入配置文件的顺序与位置不同可能会导致最终程序运行结果不同。
8. ApplicationContext 分析
ApplicationContext 是一个接口,提供了访问 Spring 容器的 API 。
ClassPathXmlApplicationContext 是一个类,实现了上述功能。
ApplicationContext 的顶层接口是 BeanFactory 。
BeanFactory 定义了 bean 相关的最基本操作。
ApplicationContext 在 BeanFactory 基础上追加了若干新功能。
ApplicationContext 对比 BeanFactory:
BeanFactory 创建的 bean 采用延迟加载形式,使用才创建;
ApplicationContext 创建的 bean 默认采用立即加载形式。
FileSystemXmlApplicationContext:
可以加载文件系统中任意位置的配置文件,而 ClassPathXmlApplicationContext 只能加载类路径下的配置文件。
示例:BeanFactory 创建 Spring 容器。
Resource res = new ClassPathResource("applicationContext.xml");
BeanFactory bf = new XmlBeanFactory(res);
UserService userService = (UserService)bf.getBean("userService");
注解开发的好处:使用注解的形式替代 xml 配置,将繁杂的 Spring 配置文件从工程中彻底消除掉,简化书写。
注解驱动的弊端:
为了达成注解驱动的目的,可能会将原先很简单的书写,变得更加复杂。
XML 中配置第三方开发的资源是很方便的,但使用注解驱动无法在第三方开发的资源中进行编辑,因此会增大开发工作量。
Spring 原始注解:主要是替代
注解 | 说明 |
---|---|
@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 配置文件,还需要使用注解替代的配置如下:
注解 | 说明 |
---|---|
@Configuration | 用于指定当前类是一个 Spring 配置类,当创建容器时会从该类上加载注解。用于指定 Spring 在初始化容器时要扫描的包。 |
@ComponentScan | 作用和在 Spring 的 xml 配置文件中的 |
@Bean | 使用在方法上,标注将该方法的返回值存储到 Spring 容器中。 |
@PropertySource | 用于加载 .properties 文件中的配置。 |
@Import | 用于导入其他配置类。 |
1. 启用注解功能
说明:
在进行包所扫描时,会对配置的包及其子包中所有文件进行扫描。
扫描过程是以文件夹递归迭代的形式进行的。
扫描过程仅读取合法的 java 文件。
扫描时仅读取 spring 可识别的注解。
扫描结束后会将可识别的有效注解转化为 spring 对应的资源加入 IoC 容器。
注意:
无论是注解格式还是 XML 配置格式,最终都是将资源加载到 IoC 容器中,差别仅仅是数据读取方式不同。
从加载效率上来说,注解优于 XML 配置文件。
2. bean 定义:@Component、@Controller、@Service、@Repository
类型:类注解
位置:类定义上方。
作用:设置该类为 spring 管理的 bean 。
示例:
@Component
public class ClassName{}
说明:@Controller、@Service 、@Repository 是 @Component 的衍生注解,功能同 @Component 。
相关属性:
3. bean 的引用类型属性注入:@Autowired、@Qualifier
类型:属性注解、方法注解
位置:属性定义上方,方法定义上方。
作用:设置对应属性的对象或对方法进行引用类型传参。
说明:@Autowired 默认按类型装配,指定 @Qualifier 后则可以指定装配的 bean 的 id 。
相关属性:
4. bean 的引用类型属性注入:@Inject、@Named、@Resource
说明:
@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 属性。
相关属性
8. bean 的生命周期:@PostConstruct、@PreDestroy
类型:方法注解
位置:方法定义上方。
作用:设置该类作为 bean 对应的生命周期方法。
9. 加载第三方资源:@Bean
类型:方法注解
位置:方法定义上方。
作用:设置该方法的返回值作为 spring 管理的 bean 。
范例:
@Bean("dataSource")
public DruidDataSource createDataSource() { return ……; }
说明:
因为第三方 bean 无法在其源码上进行修改,因此可以使用 @Bean 解决第三方 bean 的引入问题。
该注解用于替代 XML 配置中的静态工厂与实例工厂创建 bean,不区分方法是否为静态或非静态。
@Bean 所在的类必须被 spring 扫描加载,否则该注解无法生效。
相关属性
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 依赖:
org.springframework
spring-context
5.1.9.RELEASE
com.alibaba
druid
1.1.16
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;
// 数据源配置类
// 相当于 ,且不能用通配符*
@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") // 相当于
@Import({DataSourceConfig.class}) // 相当于
public class SpringConfig {
}
UserDaoImpl.java:
package com.dao.impl;
import com.dao.UserDao;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
// 相当于
// @Component("userDao")
@Repository("userDao")
public class UserDaoImpl implements UserDao {
@Override
public void save() {
System.out.println("UserDao save...");
}
}
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;
// 相当于
// @Component("userService")
@Service("userService")
public class UserServiceImpl implements UserService {
//
// @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对象的销毁方法
1. 注解整合 Mybatis
注解整合 MyBatis 的开发步骤:
核心内容如下:
org.springframework
spring-context
5.1.9.RELEASE
mysql
mysql-connector-java
8.0.11
org.springframework
spring-jdbc
4.3.8.RELEASE
com.alibaba
druid
1.1.16
org.mybatis
mybatis-spring
1.3.0
org.mybatis
mybatis
3.5.3
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容器的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;
}
}
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;
}
}
package com.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@ComponentScan("com") // 相当于
@Import({JdbcConfig.class, MybatisConfig.class}) // 相当于
public class SpringConfig {
}
2. 注解整合 Junit
注解整合 Junit 的开发步骤:
2.为 Junit 测试用例设定对应的 Spring 容器:
从 Spring 5.0 以后,要求 Junit 的版本必须是 4.12 或以上。
Junit 仅用于单元测试,不能将 Junit 的测试类配置成 Spring 的 bean,否则该配置将会被打包进入工程中。
示例:整合 Junit5。
org.junit.jupiter
junit-jupiter
5.8.2
test
org.springframework
spring-test
5.1.9.RELEASE
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 users = userService.findAll();
Assertions.assertEquals(3, users.size());
}
}
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:设置过滤器类型(过滤策略)
自定义扫描器:
名称: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 文件中使用
使用 @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 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 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 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 容器加载方可运行。
示例:
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工厂制作好了");
}
}
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;
}
}
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 与 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;
}
}
AOP(Aspect Oriented Programming)意为:面向切面编程
,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
AOP 编程思想:
1. AOP 作用
AOP 是 OOP(面向对象编程)的延续,是软件开发中的一个热点,也是 Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。
利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
小结:
2. AOP 底层实现
AOP 底层采用代理机制进行实现。
静态代理:
Proxy Pattern(代理模式),是 23 种常用的面向对象软件的设计模式之一。
代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
优点:
结构:一个是真正的你要访问的对象(目标类),另一个是代理对象,真正对象与代理对象实现同一个接口,先访问代理类再访问真正要访问的对象。
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 代理模式选择
Spirng 可以通过配置的形式控制使用的代理形式,默认使用 JDKProxy,通过配置可以修改为使用 cglib 。
//注解驱动
@EnableAspectJAutoProxy(proxyTargetClass = true)
4)织入时机
3. AOP 术语
target
:目标类,即需要被代理的类。例如:UserServiceproxy
:一个类被 AOP 织入增强后,就成为一个代理类。Joinpoint(连接点)
:所谓连接点是指那些可能被拦截到的方法。例如:所有的方法PointCut(切入点)
:要被增强的连接点。例如:addUser()advice(通知/增强)
:即增强的代码内容。例如:after()、before()Weaving(织入)
:是指把增强(advice)应用到目标对象(target)来创建新的代理对象(proxy)的过程。Aspect(切面)
:是切入点(pointcut)和通知(advice)的结合。
4. AOP 开发明确事项
需要编写的内容:
AOP 技术实现的内容:
AOP 底层使用哪种代理方式:
总结:
AOP:面向切面编程
AOP 底层实现:
AOP 重点概念:
开发明确事项:
AOP 配置开发步骤:
1. 切点表达式
切点表达式的配置语法:
execution([修饰符] 返回值类型 包名.类名.方法名(参数))
示例:
execution(public void com.aop.Target.method())
execution(void com.aop.Target.*(..))
exeaution(* com.aop.*.*(..)) // 常用
exeaution(* com.aop..*.*(..))
execution(* *..*.*(..))
2. 通知类型
通知的配置语法:
名称 | 标签 | 说明 |
---|---|---|
前置通知 | aop:before | 用于配置前置通知。指定增强的方法在切入点方法之前执行 |
后置通知 | aop:after-returning | 用于配置后置通知。指定增强的方法在切入点方法之后执行 |
环绕通知 | aop:around | 用于配置环绕通知。指定增强的方法在切入点方法之前和之后都会执行 |
异常抛出通知 | aop:throwing | 用于配置异常执出通知。指定增强的方法在出现异常时执行 |
最终通知 | aop:after | 用于配置最终通知。无论增强方式执行是否有异常都会执行 |
3. 切点表达式的抽取
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 配置:
测试:
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 ...
基于注解的 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 {
}
测试:
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 ...
J2EE 开发使用分层设计的思想:
Spring 为业务层提供了整套的事务解决方案:
1. PlatformTransactionManager
平台事务管理器实现类:
标准介绍:
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
此接口定义了事务在执行过程中某个时间点上的状态信息及对应的状态操作:
银行转账业务说明:
银行转账操作中,涉及从 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 account set money = money + #{money} where name = #{name}
update account set money = money - #{money} where name = #{name}
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:
使用环绕通知将通知类织入到原始业务对象执行过程中:
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;
}
使用 tx 命名空间配置事务专属通知类:
使用 aop:advisor 在 AOP 配置中引用事务专属通知类:
aop:advice 与 aop:advisor 区别:
aop:advice:配置的通知类可以是普通 java 对象,即不实现接口也不使用继承关系。
aop:advisor:配置的通知类必须实现通知接口。
tx:advice
类型:标签
归属:beans 标签
作用:专用于声明式事务通知
格式:
基本属性:
id:用于配置 aop 时指定通知器的 id
transaction-manager:指定事务管理器 bean
tx:attributes
类型:标签
归属:tx:advice 标签
作用:定义通知属性
格式:
基本属性:
tx:method
类型:标签
归属:tx:attribute 标签
作用:设置具体的事务属性
格式:
说明:
基本属性:
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 标签
作用:开启事务注解驱动,并指定对应的事务管理器
范例:
方式二:纯注解驱动。
名称:@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);
}
}
务传播行为描述的是事务协调员对事务管理员所携带事务的处理态度。
事务传播行为的类型:
事务传播行为的应用:
场景 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 需要新事务)
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);
}
策略模式(Strategy Pattern):根据不同的策略对象来实现不同的行为方式,策略对象的变化导致行为的变化。
示例:装饰模式应用。
1. SSM 三层架构
表现层:负责数据展示
业务层:负责业务处理
数据层:负责数据操作
2. MVC 简介
MVC(Model View Controller)是一种用于设计及创建 Web 应用程序表现层的模式。
Model(模型):数据模型,用于封装数据
View(视图):页面视图,用于展示数据
Controller(控制器):处理用户交互的调度器,用于根据用户需求处理程序逻辑
3. SpringMVC 简介
SpringMVC 是一种基于 Java 实现的、MVC 模型的、轻量级的 Web 框架。
SpringMVC 优点:
4. 入门案例
SpringMVC 工作流程分析:
导入 SpringMVC 相关的 Maven 依赖:
javax.servlet
javax.servlet-api
3.1.0
provided
javax.servlet.jsp
jsp-api
2.1
provided
org.springframework
spring-context
5.1.9.RELEASE
org.springframework
spring-web
5.1.9.RELEASE
org.springframework
spring-webmvc
5.1.9.RELEASE
定义表现层处理器 Controller(等同于 Servlet),并配置成 Spring 的 bean:
@Controller
public class UserController {
public void save(){
System.out.println("user mvc controller is running ...");
}
}
定义 SpringMVC 配置文件(格式与 Spring 配置文件一致):
web.xml 中配置 SpringMVC 核心控制器,用于将请求转发到对应的具体业务处理器 Controller 中(等同于 Servlet 配置):
DispatcherServlet
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath*:spring-mvc.xml
DispatcherServlet
/
设定具体 Controller 的访问路径与返回页面(等同于 Servlet 在 web.xml 中的配置):
// 设定当前方法的访问映射地址
@RequestMapping("/save")
// 设置当前方法返回值类型为String,用于指定请求完成后跳转的页面
public String save(){
System.out.println("user mvc controller is running ...");
// 设定具体跳转的页面
return "success.jsp";
}
5. Spring 技术架构
DispatcherServlet(前端控制器)
:是整体流程控制的中心,由其调用其它组件处理用户的请求,有效降低了组件间的耦合性。Handler(处理器)
:业务处理的核心类,通常由开发者编写,描述具体的业务。View(视图)
:最终产出结果,常用视图如 jsp、html。1. 常规配置
1)Controller 加载控制
SpringMVC 的处理器对应的 bean 必须按照规范格式开发,为了避免加入无效的 bean,可通过 bean 加载过滤器进行包含设定或排除设定。
例如,表现层 bean 标注通常设定为 @Controller,因此可以通过注解名称进行过滤控制:
2)静态资源加载
3)中文乱码处理
web.xml:
CharacterEncodingFilter
org.springframework.web.filter.CharacterEncodingFilter
encoding
UTF-8
CharacterEncodingFilter
/*
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.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, "/*");
}
}
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";
}
4. 对象类型传参
1)POJO
当使用 POJO(简单 Java 对象)时,传参名称与 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;
}
}
// 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 保存对象数据时,要求入参名称与对象层次结构名称保持一致,并使用数组格式描述集合中对象的位置。
public class User {
private String name;
private Integer age;
private List addresses;
}
public class Address {
private String province;
private String city;
private String address;
}
// 访问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 保存对象数据时,要求入参名称与对象层次结构名称保持一致,并使用映射格式描述集合中对象的位置。
public class User {
private String name;
private Integer age;
private Map addressMap;
}
public class Address {
private String province;
private String city;
private String address;
}
// 访问 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 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 接口实现。
标量转换器:
集合、数组相关转换器:
默认转换器:
7. 日期类型格式转换
配置版:声明自定义的转换格式并覆盖系统转换格式。
注解版:
// 形参前
// 访问 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 接口,并制定转换前与转换后的类型:
// 自定义类型转换器,实现Converter接口,接口中指定的泛型即为最终作用的条件
// 本例中的泛型填写的是,最终出现字符串转日期时,该类型转换器生效
public class MyDateConverter implements Converter {
// 重写接口的抽象方法,参数由泛型决定
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 中:
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)
通常,展示页面的保存位置比较固定且结构相似,因此可以设定通用的访问路径,来简化页面配置。
示例:
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 数据
com.fasterxml.jackson.core
jackson-core
2.9.0
com.fasterxml.jackson.core
jackson-databind
2.9.0
com.fasterxml.jackson.core
jackson-annotations
2.9.0
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注解的控制器方法进行结果转换
// 由于返回值为引用类型,自动调用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 al = new ArrayList<>();
al.add(book1);
al.add(book2);
return al; // 返回 [{"name":"SpringMVC入门案例","price":66.66},{"name":"SpringMVC入门案例","price":66.66}]
}
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 数据获取:
// 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 数据获取:
// cookie 数据获取
@RequestMapping("/cookieApi")
public String cookieApi(@CookieValue("JSESSIONID") String jsessionid){
System.out.println(jsessionid);
return "page.jsp";
}
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 数据设置:
@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";
}
}
页面 Ajax.jsp:
<%@page pageEncoding="UTF-8" language="java" contentType="text/html;UTF-8" %>
访问springmvc后台controller
访问springmvc后台controller,传递Json格式POJO
访问springmvc后台controller,传递Json格式List
访问springmvc后台controller,返回字符串数据
访问springmvc后台controller,返回Json数据
访问springmvc后台controller,返回Json数组数据
跨域访问
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 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. 跨域访问
1)跨域环境搭建
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;
}
请求处理过程解析:
1. 拦截器(Interceptor)
定义:是一种动态拦截方法调用
的机制。
作用:
核心原理:AOP 思想
拦截器链:多个拦截器按照一定的顺序,对原始被调用的功能进行增强
拦截器 VS 过滤器:
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)配置拦截器(切入点)
3. 拦截器执行流程
4. 拦截器配置与方法参数
1)前置处理方法
在原始方法之前运行。
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
System.out.println("preHandle");
return true;
}
2)后置处理方法
在原始方法运行后运行,如果原始方法被拦截,则不执行。
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("postHandle");
}
3)完成处理方法
拦截器最后执行的方法,无论原始方法是否执行。
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
System.out.println("afterCompletion");
}
4)拦截器配置项
5. 多拦截器配置
责任链模式:是一种行为模式。
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. 注解开发异常处理器
使用注解实现异常分类管理:
@Component
@ControllerAdvice
public class ExceptionAdvice {
}
使用注解实现异常分类管理:
@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位之间,请重新输入! ");
}
通过自定义异常将所有的异常现象进行分类管理,以统一的格式对外呈现异常消息。
上传文件过程分析:
SpringMVC 的文件上传技术:MultipartResolver 接口。
SpringMVC 文件上传实现:
Maven 依赖:
commons-fileupload
commons-fileupload
1.4
commons-io
commons-io
2.4
页面表单:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
Title
SpringMVC 配置:
控制器:
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";
}
}
文件上传常见问题:
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";
}
}
1. Restful 简介
Rest(REpresentational State Transfer)是一种网络资源的访问风格,定义了网络资源的访问方式。
而 Restful 则是按照 Rest 风格访问网络资源。
优点:
2. Rest 行为常用约定方式
注意:上述行为是约定方式,约定不是规范,可以打破,所以称 Rest 风格,而不是 Rest 规范。
3. Restful开发入门
页面表单:
开启 SpringMVC 对 Restful 风格的访问支持过滤器,即可通过页面表单提交 PUT 与 DELETE 请求:
HiddenHttpMethodFilter
org.springframework.web.filter.HiddenHttpMethodFilter
HiddenHttpMethodFilter
DispatcherServlet
DispatcherServlet
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath*:spring-mvc.xml
控制层:
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";
}
}
1. 表单校验框架介绍
表单校验分类:
表单校验规则:
表单校验框架:
JSR(Java Specification Requests):Java 规范提案
Hibernate 框架中包含一套独立的校验框架 hibernate-validator
org.hibernate
hibernate-validator
6.1.0.Final
注意:
2. 快速入门
页面表单:
设置校验规则:
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 +
'}';
}
}
开启校验,并获取校验错误信息:
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; // 员工年龄
4. 嵌套校验
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. 综合案例
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
Title
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 +
'}';
}
}
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 + '\'' +
'}';
}
}
package com.group;
public interface GroupA {
}
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 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
}
Java Web的基础是Servlet容器,以及标准的Servlet组件:
此外,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工程,引入如下依赖:
以及provided依赖:
这个标准的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的默认查找的配置文件:
%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n
上面给出了一个写入到标准输出的Logback配置,可以基于上述配置添加写入到文件的配置。
在src/main/java中就是我们编写的Java代码了。
使用Spring MVC时,整个Web应用程序按如下顺序启动:
启动后,浏览器发出的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:
dispatcher
org.springframework.web.servlet.DispatcherServlet
contextClass
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
contextConfigLocation
com.itranswarp.learnjava.AppConfig
0
dispatcher
/*
初始化参数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和数据库,用户的注册、登录信息会被持久化:
有了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实现。
使用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":"[email protected]"}' 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 users() {
return userService.getUsers();
}
@GetMapping("/users/{id}")
public User user(@PathVariable("id") long id) {
return userService.getUserById(id);
}
@PostMapping("/signin")
public Map 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":"[email protected]","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":"[email protected]","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)允许输出,不允许输入。
在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中声明即可:
encodingFilter
org.springframework.web.filter.CharacterEncodingFilter
encoding
UTF-8
forceEncoding
true
encodingFilter
/*
...
因为这种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,专门来干这个事情:
authFilter
org.springframework.web.filter.DelegatingFilterProxy
authFilter
/*
...
我们来看实现原理:
web.xml
中读取配置,实例化DelegatingFilterProxy
,注意命名是authFilter
;@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的名字:
basicAuthFilter
org.springframework.web.filter.DelegatingFilterProxy
targetBeanName
authFilter
实际应用时,尽量保持名字一致,以减少不必要的配置。
要使用Basic模式的用户认证,我们可以使用curl命令测试。例如,用户登录名是[email protected],口令是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
<
...HTML输出...
上述响应说明AuthFilter已生效。
注意:Basic认证模式并不安全,这里只用来作为使用Filter的示例。
SSM(Spring + SpringMVC + MyBatis)整合步骤分析:
Spring
MyBatis
Spring 整合 MyBatis
junit 测试业务层接口
SpringMVC
Spring 整合 SpringMVC
其他
表现层数据封装
自定义异常
5 个关键步骤:
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 依赖
org.springframework
spring-context
5.1.9.RELEASE
org.mybatis
mybatis
3.5.3
mysql
mysql-connector-java
8.0.11
org.springframework
spring-jdbc
5.1.9.RELEASE
org.mybatis
mybatis-spring
2.0.3
com.alibaba
druid
1.1.16
com.github.pagehelper
pagehelper
5.1.2
org.springframework
spring-webmvc
5.1.9.RELEASE
javax.servlet
javax.servlet-api
3.1.0
provided
com.fasterxml.jackson.core
jackson-databind
2.9.0
com.fasterxml.jackson.core
jackson-core
2.9.0
com.fasterxml.jackson.core
jackson-annotations
2.9.0
org.junit.jupiter
junit-jupiter
RELEASE
test
org.springframework
spring-test
5.1.9.RELEASE
3. 项目结构
创建项目,组织项目结构,创建包
创建表与实体类
创建三层架构对应的模块、接口与实体类,并建立关联关系
数据层接口(代理自动创建实现类)
项目地址:JavaDemo: 小示例 - Gitee.com
1. Spring 整合 Mybatis
insert into user(userName,password,realName,gender,birthday)values(#{userName},#{password},#{realName},#{gender},#{birthday})
delete from user where uuid = #{uuid}
update user set userName=#{userName},password=#{password},realName=#{realName},gender=#{gender},birthday=#{birthday} where uuid=#{uuid}
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 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 getAll(int page, int size) {
PageHelper.startPage(page, size);
List all = userDao.getAll();
return new PageInfo(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 核心配置
mysql
true
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 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:
DispatcherServlet
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath*:spring-mvc.xml
DispatcherServlet
/
@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;
}
}
contextConfigLocation
classpath*:applicationContext.xml
org.springframework.web.context.ContextLoaderListener
@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;
// 其他编码
}
@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. 自定义异常
设定自定义异常,封装程序执行过程中出现的问题,便于表现层进行统一的异常拦截并进行处理。
自定义异常消息返回时需要与业务正常执行的消息按照统一的格式进行处理。
代码示例:
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());
}
}
项目地址: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(
// 等同于
value = "com",
// 等同于
excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION, classes={Controller.class})
)
// 等同于
@PropertySource("classpath:jdbc.properties")
// 等同于 ,bean的名称默认取transactionManager
@EnableTransactionManagement
@Import({MybatisConfig.class, JdbcConfig.class})
public class SpringConfig {
// 等同于
@Bean("transactionManager")
// 等同于
public DataSourceTransactionManager getTxManager(@Autowired DataSource dataSource){
DataSourceTransactionManager tm = new DataSourceTransactionManager();
// 等同于
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
// 等同于
@ComponentScan("com.controller")
// 功能强大于
@EnableWebMvc
public class SpringMvcConfig {
}
@EnableWebMvc 功能:
项目地址:JavaDemo: 小示例 - Gitee.com
1. 分模块开发
2. 聚合与继承
作用:
相同点:
不同点:
1)聚合
作用:聚合用于快速构建 maven 工程,可以一次性构建多个项目/模块。
制作方式:
pom
../ssm controller
../ssm service
../ssm dao
../ssm pojo
注意事项:参与聚合操作的模块最终执行顺序与模块间的依赖关系有关,与配置顺序无关。
2)继承
作用:通过继承可以实现在子工程中沿用父工程中的配置。
* Maven 中的继承与 java 中的继承相似,在子工程中配置继承关系。
制作方式:
com
ssm
1.0-SNAPSHOT
继承依赖定义:
org.springframework
spring-context
5.1.9.RETFASE
Maven 提供的 dependentcyManagement 元素既能让子模块继承到父模块的依赖配置,又能保证子模块依赖使用的灵活度。在 dependentcyManagement 元素下的依赖声明不会引入实际的依赖,而是定义了依赖的版本,对版本进行同一管理,避免出现版本不一致的情况。
继承依赖使用:
org.springframework
spring-context
可继承的资源:
3. 属性
1)自定义属性
作用:等同于定义变量,方便统一维护。
定义格式
5.1.9.RELEASE
4.12
org.springframework
spring-context
${spring.version}
2)内置属性
作用:使用 Maven 内置属性,快速配置。
调用格式
${basedir}
${version}
3)Setting 属性
作用:使用 Maven 配置文件 setting.xml 中的标签属性,用于动态配置。
调用格式
${settings.localRepository)
4)Java 系统属性
作用:读取 Java 系统属性。
调用格式
${user.home}
mvn help:system
4. 版本管理
工程版本:
SNAPSHOT(快照版本)
RELEASE(发布版本)
工程版本号约定规范:
<主版本>.<次版本>.<增量版本>.<里程碑版本>
5. 资源配置
多文件维护:
配置文件引用 pom 属性:
作用:在任意配置文件中加载 pom 文件中定义的属性。
调用格式
# 在配置文件中调用自定义属性
${jdbc.url}
${project.basedir}/src/main/resources
true
6. 多环境开发配置
多环境配置:
pro env
jdbc:mysql://localhost:3306/world?serverTimezone=UTC
true
dev env
加载指定环境:
# 格式
mvn 指令 -P 定义的环境id
# 范例
mvn install -P pro_env
7. 测试配置
方法一:通过命令跳过测试
mvn 指令 -D skipTests
方法二:通过 IDEA 界面操作跳过测试
方法三:通过 pom 进行测试配置
maven-surefire-plugin
3.0.0-M5
**/**.java