JavaWeb-JDBC-Mybatis-Junit-Maven-Lombok
Java與資料庫
初識JDBC
JDBC是什麼?
JDBC英文名為:Java Data Base Connectivity(Java資料庫連接),官方解釋它是Java程式語言和廣泛的資料庫之間獨立於資料庫的連接標準的Java API,根本上說JDBC是一種規範,它提供的介面,一套完整的,允許便捷式訪問底層資料庫。
可以用JAVA來寫不同類型的可執行文件:JAVA應用程式、JAVA Applets、Java Servlet、JSP等,不同的可執行文件都能通過JDBC訪問資料庫,又兼備存儲的優勢。簡單說它就是Java與資料庫的連接的橋樑或者插件,用Java程式碼就能操作資料庫的增刪改查、存儲過程、事務等。
我們可以發現,JDK自帶了一個java.sql
包,而這裡面就定義了大量的介面,不同類型的資料庫,都可以通過實現此介面,編寫適用於自己資料庫的實現類。而不同的資料庫廠商實現的這套標準,我們稱為資料庫驅動
。
準備工作
那麼我們首先來進行一些準備工作,以便開始JDBC的學習:
- 將idea連接到我們的資料庫,以便以後調試。
- 將mysql驅動jar依賴導入到項目中(推薦6.0版本以上,這裡用到是8.0)
- 向Jetbrians申請一個學生/教師授權,用於激活idea終極版(進行JavaWeb開發需要用到,一般申請需要3-7天時間審核)不是大學生的話…emmm…懂的都懂。
- 教育授權申請地址://www.jetbrains.com/shop/eform/students
一個Java程式並不是一個人的戰鬥,我們可以在別人開發的基礎上繼續向上開發,其他的開發者可以將自己編寫的Java程式碼打包為jar
,我們只需要導入這個jar
作為依賴,即可直接使用別人的程式碼,就像我們直接去使用JDK提供的類一樣。
使用JDBC連接資料庫
注意:6.0版本以上,不用手動載入驅動,我們直接使用即可!
//1. 通過DriverManager來獲得資料庫連接
try (Connection connection = DriverManager.getConnection("連接URL","用戶名","密碼");
//2. 創建一個用於執行SQL的Statement對象
Statement statement = connection.createStatement()){ //注意前兩步都放在try()中,因為在最後需要釋放資源!
//3. 執行SQL語句,並得到結果集
ResultSet set = statement.executeQuery("select * from 表名");
//4. 查看結果
while (set.next()){
...
}
}catch (SQLException e){
e.printStackTrace();
}
//5. 釋放資源,try-with-resource語法會自動幫助我們close
其中,連接的URL如果記不住格式,我們可以打開idea的資料庫連接配置,複製一份即可。(其實idea本質也是使用的JDBC,整個idea程式都是由Java編寫的,實際上idea就是一個Java程式)
了解DriverManager
我們首先來了解一下DriverManager是什麼東西,它其實就是管理我們的資料庫驅動的:
public static synchronized void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {
/* Register the driver if it has not already been added to our list */
if(driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da)); //在剛啟動時,mysql實現的驅動會被載入,我們可以斷點調試一下。
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
我們可以通過調用getConnection()來進行資料庫的鏈接:
@CallerSensitive
public static Connection getConnection(String url,
String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}
return (getConnection(url, info, Reflection.getCallerClass())); //內部有實現
}
我們可以手動為驅動管理器添加一個日誌列印:
static {
DriverManager.setLogWriter(new PrintWriter(System.out)); //這裡直接設定為控制台輸出
}
現在我們執行的資料庫操作日誌會在控制台實時列印。
了解Connection
Connection是資料庫的連接對象,可以通過連接對象來創建一個Statement用於執行SQL語句:
Statement createStatement() throws SQLException;
我們發現除了普通的Statement,還存在PreparedStatement:
PreparedStatement prepareStatement(String sql)
throws SQLException;
在後面我們會詳細介紹PreparedStatement的使用,它能夠有效地預防SQL注入式攻擊。
它還支援事務的處理,也放到後面來詳細進行講解。
了解Statement
我們發現,我們之前使用了executeQuery()
方法來執行select
語句,此方法返回給我們一個ResultSet對象,查詢得到的數據,就存放在ResultSet中!
Statement除了執行這樣的DQL語句外,我們還可以使用executeUpdate()
方法來執行一個DML或是DDL語句,它會返回一個int類型,表示執行後受影響的行數,可以通過它來判斷DML語句是否執行成功。
也可以通過excute()
來執行任意的SQL語句,它會返回一個boolean
來表示執行結果是一個ResultSet還是一個int,我們可以通過使用getResultSet()
或是getUpdateCount()
來獲取。
執行DML操作
我們通過幾個例子來向資料庫中插入數據。
執行DQL操作
執行DQL操作會返回一個ResultSet對象,我們來看看如何從ResultSet中去獲取數據:
//首先要明確,select返回的數據類似於一個excel表格
while (set.next()){
//每調用一次next()就會向下移動一行,首次調用會移動到第一行
}
我們在移動行數後,就可以通過set中提供的方法,來獲取每一列的數據。
執行批處理操作
當我們要執行很多條語句時,可以不用一次一次地提交,而是一口氣全部交給資料庫處理,這樣會節省很多的時間。
public static void main(String[] args) throws ClassNotFoundException {
try (Connection connection = DriverManager.getConnection();
Statement statement = connection.createStatement()){
statement.addBatch("insert into user values ('f', 1234)");
statement.addBatch("insert into user values ('e', 1234)"); //添加每一條批處理語句
statement.executeBatch(); //一起執行
}catch (SQLException e){
e.printStackTrace();
}
}
將查詢結果映射為對象
既然我們現在可以從資料庫中獲取數據了,那麼現在就可以將這些數據轉換為一個類來進行操作,首先定義我們的實體類:
public class Student {
Integer sid;
String name;
String sex;
public Student(Integer sid, String name, String sex) {
this.sid = sid;
this.name = name;
this.sex = sex;
}
public void say(){
System.out.println("我叫:"+name+",學號為:"+sid+",我的性別是:"+sex);
}
}
現在我們來進行一個轉換:
while (set.next()){
Student student = new Student(set.getInt(1), set.getString(2), set.getString(3));
student.say();
}
注意:列的下標是從1開始的。
我們也可以利用反射機制來將查詢結果映射為對象,使用反射的好處是,無論什麼類型都可以通過我們的方法來進行實體類型映射:
private static <T> T convert(ResultSet set, Class<T> clazz){
try {
Constructor<T> constructor = clazz.getConstructor(clazz.getConstructors()[0].getParameterTypes()); //默認獲取第一個構造方法
Class<?>[] param = constructor.getParameterTypes(); //獲取參數列表
Object[] object = new Object[param.length]; //存放參數
for (int i = 0; i < param.length; i++) { //是從1開始的
object[i] = set.getObject(i+1);
if(object[i].getClass() != param[i])
throw new SQLException("錯誤的類型轉換:"+object[i].getClass()+" -> "+param[i]);
}
return constructor.newInstance(object);
} catch (ReflectiveOperationException | SQLException e) {
e.printStackTrace();
return null;
}
}
現在我們就可以通過我們的方法來將查詢結果轉換為一個對象了:
while (set.next()){
Student student = convert(set, Student.class);
if(student != null) student.say();
}
實際上,在後面我們會學習Mybatis框架,它對JDBC進行了深層次的封裝,而它就進行類似上面反射的操作來便於我們對資料庫數據與實體類的轉換。
自己寫一個Mybatis。
實現登陸與SQL注入攻擊
在使用之前,我們先來看看如果我們想模擬登陸一個用戶,我們該怎麼去寫:
try (Connection connection = DriverManager.getConnection("URL","用戶名","密碼");
Statement statement = connection.createStatement();
Scanner scanner = new Scanner(System.in)){
ResultSet res = statement.executeQuery("select * from user where username='"+scanner.nextLine()+"'and pwd='"+scanner.nextLine()+"';");
while (res.next()){
String username = res.getString(1);
System.out.println(username+" 登陸成功!");
}
}catch (SQLException e){
e.printStackTrace();
}
用戶可以通過自己輸入用戶名和密碼來登陸,乍一看好像沒啥問題,那如果我輸入的是以下內容呢:
Test
1111' or 1=1; --
# Test 登陸成功!
1=1一定是true,那麼我們原本的SQL語句會變為:
select * from user where username='Test' and pwd='1111' or 1=1; -- '
我們發現,如果允許這樣的數據插入,那麼我們原有的SQL語句結構就遭到了破壞,使得用戶能夠隨意登陸別人的帳號。
因此我們可能需要限制用戶的輸入來防止用戶輸入一些SQL語句關鍵字,但是關鍵字非常多,這並不是解決問題的最好辦法。
使用PreparedStatement
我們發現,如果單純地使用Statement來執行SQL命令,會存在嚴重的SQL注入攻擊漏洞!而這種問題,我們可以使用PreparedStatement來解決:
public static void main(String[] args) throws ClassNotFoundException {
try (Connection connection = DriverManager.getConnection("URL","用戶名","密碼");
PreparedStatement statement = connection.prepareStatement("select * from user where username= ? and pwd=?;");
Scanner scanner = new Scanner(System.in)){
statement.setString(1, scanner.nextLine());
statement.setString(2, scanner.nextLine());
System.out.println(statement); //列印查看一下最終執行的
ResultSet res = statement.executeQuery();
while (res.next()){
String username = res.getString(1);
System.out.println(username+" 登陸成功!");
}
}catch (SQLException e){
e.printStackTrace();
}
}
我們發現,我們需要提前給到PreparedStatement一個SQL語句,並且使用?
作為佔位符,它會預編譯一個SQL語句,通過直接將我們的內容進行替換的方式來填寫數據。
使用這種方式,我們之前的例子就失效了!我們來看看實際執行的SQL語句是什麼:
com.mysql.cj.jdbc.ClientPreparedStatement: select * from user where username= 'Test' and pwd='123456'' or 1=1; -- ';
我們發現,我們輸入的參數一旦出現'
時,會被變為轉義形式\'
,而最外層有一個真正的'
來將我們輸入的內容進行包裹,因此它能夠有效地防止SQL注入攻擊!
管理事務
JDBC默認的事務處理行為是自動提交,所以前面我們執行一個SQL語句就會被直接提交(相當於沒有啟動事務),所以JDBC需要進行事務管理時,首先要通過Connection對象調用setAutoCommit(false) 方法, 將SQL語句的提交(commit)由驅動程式轉交給應用程式負責。
con.setAutoCommit(); //關閉自動提交後相當於開啟事務。
// SQL語句
// SQL語句
// SQL語句
con.commit();或 con.rollback();
一旦關閉自動提交,那麼現在執行所有的操作如果在最後不進行commit()
來提交事務的話,那麼所有的操作都會丟失,只有提交之後,所有的操作才會被保存!也可以使用rollback()
來手動回滾之前的全部操作!
public static void main(String[] args) throws ClassNotFoundException {
try (Connection connection = DriverManager.getConnection("URL","用戶名","密碼");
Statement statement = connection.createStatement()){
connection.setAutoCommit(false); //關閉自動提交,現在將變為我們手動提交
statement.executeUpdate("insert into user values ('a', 1234)");
statement.executeUpdate("insert into user values ('b', 1234)");
statement.executeUpdate("insert into user values ('c', 1234)");
connection.commit(); //如果前面任何操作出現異常,將不會執行commit(),之前的操作也就不會生效
}catch (SQLException e){
e.printStackTrace();
}
}
我們來接著嘗試一下使用回滾操作:
public static void main(String[] args) throws ClassNotFoundException {
try (Connection connection = DriverManager.getConnection("URL","用戶名","密碼");
Statement statement = connection.createStatement()){
connection.setAutoCommit(false); //關閉自動提交,現在將變為我們手動提交
statement.executeUpdate("insert into user values ('a', 1234)");
statement.executeUpdate("insert into user values ('b', 1234)");
connection.rollback(); //回滾,撤銷前面全部操作
statement.executeUpdate("insert into user values ('c', 1234)");
connection.commit(); //提交事務(注意,回滾之前的內容都沒了)
}catch (SQLException e){
e.printStackTrace();
}
}
同樣的,我們也可以去創建一個回滾點來實現定點回滾:
public static void main(String[] args) throws ClassNotFoundException {
try (Connection connection = DriverManager.getConnection("URL","用戶名","密碼");
Statement statement = connection.createStatement()){
connection.setAutoCommit(false); //關閉自動提交,現在將變為我們手動提交
statement.executeUpdate("insert into user values ('a', 1234)");
Savepoint savepoint = connection.setSavepoint(); //創建回滾點
statement.executeUpdate("insert into user values ('b', 1234)");
connection.rollback(savepoint); //回滾到回滾點,撤銷前面全部操作
statement.executeUpdate("insert into user values ('c', 1234)");
connection.commit(); //提交事務(注意,回滾之前的內容都沒了)
}catch (SQLException e){
e.printStackTrace();
}
}
通過開啟事務,我們就可以更加謹慎地進行一些操作了,如果我們想從事務模式切換為原有的自動提交模式,我們可以直接將其設置回去:
public static void main(String[] args) throws ClassNotFoundException {
try (Connection connection = DriverManager.getConnection("URL","用戶名","密碼");
Statement statement = connection.createStatement()){
connection.setAutoCommit(false); //關閉自動提交,現在將變為我們手動提交
statement.executeUpdate("insert into user values ('a', 1234)");
connection.setAutoCommit(true); //重新開啟自動提交,開啟時把之前的事務模式下的內容給提交了
statement.executeUpdate("insert into user values ('d', 1234)");
//沒有commit也成功了!
}catch (SQLException e){
e.printStackTrace();
}
通過學習JDBC,我們現在就可以通過Java來訪問和操作我們的資料庫了!為了更好地銜接,我們還會接著講解主流持久層框架——Mybatis,加深JDBC的記憶。
使用Lombok
我們發現,在以往編寫項目時,尤其是在類進行類內部成員欄位封裝時,需要編寫大量的get/set方法,這不僅使得我們類定義中充滿了get和set方法,同時如果欄位名稱發生改變,又要挨個進行修改,甚至當欄位變得很多時,構造方法的編寫會非常麻煩!
通過使用Lombok(小辣椒)就可以解決這樣的問題!
我們來看看,使用原生方式和小辣椒方式編寫類的區別,首先是傳統方式:
public class Student {
private Integer sid;
private String name;
private String sex;
public Student(Integer sid, String name, String sex) {
this.sid = sid;
this.name = name;
this.sex = sex;
}
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 String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
而使用Lombok之後:
@Getter
@Setter
@AllArgsConstructor
public class Student {
private Integer sid;
private String name;
private String sex;
}
我們發現,使用Lombok之後,只需要添加幾個註解,就能夠解決掉我們之前長長的一串程式碼!
配置Lombok
- 首先我們需要導入Lombok的jar依賴,和jdbc依賴是一樣的,放在項目目錄下直接導入就行了。可以在這裡進行下載://projectlombok.org/download
- 然後我們要安裝一下Lombok插件,由於IDEA默認都安裝了Lombok的插件,因此直接導入依賴後就可以使用了。
- 重啟IDEA
Lombok是一種插件化註解API,是通過添加註解來實現的,然後在javac進行編譯的時候,進行處理。
Java的編譯過程可以分成三個階段:
- 所有源文件會被解析成語法樹。
- 調用註解處理器。如果註解處理器產生了新的源文件,新文件也要進行編譯。
- 最後,語法樹會被分析並轉化成類文件。
實際上在上述的第二階段,會執行lombok.core.AnnotationProcessor,它所做的工作就是我們上面所說的,修改語法樹。
使用Lombok
我們通過實戰來演示一下Lombok的實用註解:
- 我們通過添加
@Getter
和@Setter
來為當前類的所有欄位生成get/set方法,他們可以添加到類或是欄位上,注意靜態欄位不會生成,final欄位無法生成set方法。- 我們還可以使用@Accessors來控制生成Getter和Setter的樣式。
- 我們通過添加
@ToString
來為當前類生成預設的toString方法。 - 我們可以通過添加
@EqualsAndHashCode
來快速生成比較和哈希值方法。 - 我們可以通過添加
@AllArgsConstructor
和@NoArgsConstructor
來快速生成全參構造和無參構造。 - 我們可以添加
@RequiredArgsConstructor
來快速生成參數只包含final
或被標記為@NonNull
的成員欄位。 - 使用
@Data
能代表@Setter
、@Getter
、@RequiredArgsConstructor
、@ToString
、@EqualsAndHashCode
全部註解。- 一旦使用
@Data
就不建議此類有繼承關係,因為equal
方法可能不符合預期結果(尤其是僅比較子類屬性)。
- 一旦使用
- 使用
@Value
與@Data
類似,但是並不會生成setter並且成員屬性都是final的。 - 使用
@SneakyThrows
來自動生成try-catch程式碼塊。 - 使用
@Cleanup
作用與局部變數,在最後自動調用其close()
方法(可以自由更換) - 使用
@Builder
來快速生成建造者模式。- 通過使用
@Builder.Default
來指定默認值。 - 通過使用
@Builder.ObtainVia
來指定默認值的獲取方式。
- 通過使用
認識Mybatis
在前面JDBC的學習中,雖然我們能夠通過JDBC來連接和操作資料庫,但是哪怕只是完成一個SQL語句的執行,都需要編寫大量的程式碼,更不用說如果我還需要進行實體類映射,將數據轉換為我們可以直接操作的實體類型,JDBC很方便,但是還不夠方便,我們需要一種更加簡潔高效的方式來和資料庫進行交互。
再次強調:學習厲害的框架或是厲害的技術,並不是為了一定要去使用它,而是它們能夠使得我們在不同的開發場景下,合理地使用這些技術,以靈活地應對需要解決的問題。
MyBatis 是一款優秀的持久層框架,它支援訂製化 SQL、存儲過程以及高級映射。
MyBatis 避免了幾乎所有的 JDBC 程式碼和手動設置參數以及獲取結果集。MyBatis 可以使用簡單的 XML 或註解來配置和映射原生資訊,將介面和 Java 的 POJOs(Plain Ordinary Java Object,普通的 Java對象)映射成資料庫中的記錄。
我們依然使用傳統的jar依賴方式,從最原始開始講起,不使用Maven,有關Maven內容我們會在後面統一講解!全程圍繞官方文檔講解!
這一塊內容很多很雜,再次強調要多實踐!
XML語言概述
在開始介紹Mybatis之前,XML語言發明最初是用於數據的存儲和傳輸,它可以長這樣:
<?xml version="1.0" encoding="UTF-8" ?>
<outer>
<name>阿偉</name>
<desc>怎麼又在玩電動啊</desc>
<inner type="1">
<age>10</age>
<sex>男</sex>
</inner>
</outer>
如果你學習過前端知識,你會發現它和HTML幾乎長得一模一樣!但是請注意,雖然它們長得差不多,但是他們的意義卻不同,
HTML主要用於通過編排來展示數據,而XML主要是存放數據,它更像是一個配置文件!當然,瀏覽器也是可以直接打開XML文件的。
一個XML文件存在以下的格式規範:
- 必須存在一個根節點,將所有的子標籤全部包含。
- 可以但不必須包含一個頭部聲明(主要是可以設定編碼格式)
- 所有的標籤必須成對出現,可以嵌套但不能交叉嵌套
- 區分大小寫。
- 標籤中可以存在屬性,比如上面的
type="1"
就是inner
標籤的一個屬性,屬性的值由單引號或雙引號包括。
XML文件也可以使用注釋:
<?xml version="1.0" encoding="UTF-8" ?>
<!-- 注釋內容 -->
通過IDEA我們可以使用Ctrl
+/
來快速添加註釋文本(不僅僅適用於XML,還支援很多種類型的文件)
那如果我們的內容中出現了<
或是>
字元,那該怎麼辦呢?我們就可以使用XML的轉義字元來代替:
如果嫌一個一個改太麻煩,也可以使用CD來快速創建不解析區域:
<test>
<name><![CDATA[我看你<><><>是一點都不懂哦>>>]]></name>
</test>
那麼,我們現在了解了XML文件的定義,現在該如何去解析一個XML文件呢?比如我們希望將定義好的XML文件讀取到Java程式中,這時該怎麼做呢?
JDK為我們內置了一個叫做org.w3c
的XML解析庫,我們來看看如何使用它來進行XML文件內容解析:
// 創建DocumentBuilderFactory對象
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 創建DocumentBuilder對象
try {
DocumentBuilder builder = factory.newDocumentBuilder();
Document d = builder.parse("file:mappers/test.xml");
// 每一個標籤都作為一個節點
NodeList nodeList = d.getElementsByTagName("test"); // 可能有很多個名字為test的標籤
Node rootNode = nodeList.item(0); // 獲取首個
NodeList childNodes = rootNode.getChildNodes(); // 一個節點下可能會有很多個節點,比如根節點下就囊括了所有的節點
//節點可以是一個帶有內容的標籤(它內部就還有子節點),也可以是一段文本內容
for (int i = 0; i < childNodes.getLength(); i++) {
Node child = childNodes.item(i);
if(child.getNodeType() == Node.ELEMENT_NODE) //過濾換行符之類的內容,因為它們都被認為是一個文本節點
System.out.println(child.getNodeName() + ":" +child.getFirstChild().getNodeValue());
// 輸出節點名稱,也就是標籤名稱,以及標籤內部的文本(內部的內容都是子節點,所以要獲取內部的節點)
}
} catch (Exception e) {
e.printStackTrace();
}
當然,學習和使用XML只是為了更好地去認識Mybatis的工作原理,以及如何使用XML來作為Mybatis的配置文件,這是在開始之前必須要掌握的內容(使用Java讀取XML內容不要求掌握,但是需要知道Mybatis就是通過這種方式來讀取配置文件的)
不僅僅是Mybatis,包括後面的Spring等眾多框架都會用到XML來作為框架的配置文件!
初次使用Mybatis
那麼我們首先來感受一下Mybatis給我們帶來的便捷,就從搭建環境開始,
中文文檔網站://mybatis.org/mybatis-3/zh/configuration.html
我們需要導入Mybatis的依賴,Jar包需要在github上下載,如果卡得一匹,自己%%%。
依賴導入完成後,我們就可以編寫Mybatis的配置文件了(現在不是在Java程式碼中配置了,而是通過一個XML文件去配置,這樣就使得硬編碼的部分大大減少,項目後期打包成Jar運行不方便修復,但是通過配置文件,我們隨時都可以去修改,就變得很方便了,同時程式碼量也大幅度減少,配置文件填寫完成後,我們只需要關心項目的業務邏輯而不是如何去讀取配置文件)我們按照官方文檔給定的提示,在項目根目錄下新建名為mybatis-config.xml
的文件,並填寫以下內容:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"//mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${驅動類(含包名)}"/>
<property name="url" value="${資料庫連接URL}"/>
<property name="username" value="${用戶名}"/>
<property name="password" value="${密碼}"/>
</dataSource>
</environment>
</environments>
</configuration>
我們發現,在最上方還引入了一個叫做DTD(文檔類型定義)的東西,它提前幫助我們規定了一些標籤,我們就需要使用Mybatis提前幫助我們規定好的標籤來進行配置(因為只有這樣Mybatis才能正確識別我們配置的內容)
通過進行配置,我們就告訴了Mybatis我們鏈接資料庫的一些資訊,包括URL、用戶名、密碼等,這樣Mybatis就知道該鏈接哪個資料庫、使用哪個帳號進行登陸了(也可以不使用配置文件,這裡不做講解,還請各位小夥伴自行閱讀官方文檔)
配置文件完成後,我們需要在Java程式啟動時,讓Mybatis對配置文件進行讀取並得到一個SqlSessionFactory
對象:
public static void main(String[] args) throws FileNotFoundException {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(new FileInputStream("mybatis-config.xml"));
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)){
//暫時還沒有業務
}
}
直接運行即可,雖然沒有幹什麼事情,但是不會出現錯誤,如果之前的配置文件編寫錯誤,直接運行會產生報錯!那麼現在我們來看看,SqlSessionFactory
對象是什麼東西:
每個基於 MyBatis 的應用都是以一個 SqlSessionFactory 的實例為核心的,我們可以通過SqlSessionFactory
來創建多個新的會話,SqlSession
對象,每個會話就相當於我不同的地方登陸一個帳號去訪問資料庫,你也可以認為這就是之前JDBC中的Statement
對象,會話之間相互隔離,沒有任何關聯。
而通過SqlSession
就可以完成幾乎所有的資料庫操作,我們發現這個介面中定義了大量資料庫操作的方法,因此,現在我們只需要通過一個對象就能完成資料庫交互了,極大簡化了之前的流程。
我們來嘗試一下直接讀取實體類,讀取實體類肯定需要一個映射規則,比如類中的哪個欄位對應資料庫中的哪個欄位,在查詢語句返回結果後,Mybatis就會自動將對應的結果填入到對象的對應欄位上。首先編寫實體類,,直接使用Lombok是不是就很方便了:
import lombok.Data;
@Data
public class Student {
int sid; //名稱最好和資料庫欄位名稱保持一致,不然可能會映射失敗導致查詢結果丟失
String name;
String sex;
}
在根目錄下重新創建一個mapper文件夾,新建名為TestMapper.xml
的文件作為我們的映射器,並填寫以下內容:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"//mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="TestMapper">
<select id="selectStudent" resultType="com.test.entity.Student">
select * from student
</select>
</mapper>
其中namespace就是命名空間,每個Mapper都是唯一的,因此需要用一個命名空間來區分,它還可以用來綁定一個介面。我們在裡面寫入了一個select標籤,表示添加一個select操作,同時id作為操作的名稱,resultType指定為我們剛剛定義的實體類,表示將資料庫結果映射為Student
類,然後就在標籤中寫入我們的查詢語句即可。
編寫好後,我們在配置文件中添加這個Mapper映射器:
<mappers>
<mapper url="file:mappers/TestMapper.xml"/>
<!-- 這裡用的是url,也可以使用其他類型,我們會在後面講解 -->
</mappers>
最後在程式中使用我們定義好的Mapper即可:
public static void main(String[] args) throws FileNotFoundException {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(new FileInputStream("mybatis-config.xml"));
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)){
List<Student> student = sqlSession.selectList("selectStudent");
student.forEach(System.out::println);
}
}
我們會發現,Mybatis非常智慧,我們只需要告訴一個映射關係,就能夠直接將查詢結果轉化為一個實體類。
配置Mybatis
在了解了Mybatis為我們帶來的便捷之後,現在我們就可以正式地去學習使用Mybatis了!
由於SqlSessionFactory
一般只需要創建一次,因此我們可以創建一個工具類來集中創建SqlSession
,這樣會更加方便一些:
public class MybatisUtil {
//在類載入時就進行創建
private static SqlSessionFactory sqlSessionFactory;
static {
try {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(new FileInputStream("mybatis-config.xml"));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
/**
* 獲取一個新的會話
* @param autoCommit 是否開啟自動提交(跟JDBC是一樣的,如果不自動提交,則會變成事務操作)
* @return SqlSession對象
*/
public static SqlSession getSession(boolean autoCommit){
return sqlSessionFactory.openSession(autoCommit);
}
}
現在我們只需要在main方法中這樣寫即可查詢結果了:
public static void main(String[] args) {
try (SqlSession sqlSession = MybatisUtil.getSession(true)){
List<Student> student = sqlSession.selectList("selectStudent");
student.forEach(System.out::println);
}
}
之前我們演示了,如何創建一個映射器來將結果快速轉換為實體類,但是這樣可能還是不夠方便,我們每次都需要去找映射器對應操作的名稱,而且還要知道對應的返回類型,再通過SqlSession
來執行對應的方法,能不能再方便一點呢?
現在,我們可以通過namespace
來綁定到一個介面上,利用介面的特性,我們可以直接指明方法的行為,而實際實現則是由Mybatis來完成。
public interface TestMapper {
List<Student> selectStudent();
}
將Mapper文件的命名空間修改為我們的介面,建議同時將其放到同名包中,作為內部資源:
<mapper namespace="com.test.mapper.TestMapper">
<select id="selectStudent" resultType="com.test.entity.Student">
select * from student
</select>
</mapper>
作為內部資源後,我們需要修改一下配置文件中的mapper定義,不使用url而是resource表示是Jar內部的文件:
<mappers>
<mapper resource="com/test/mapper/TestMapper.xml"/>
</mappers>
現在我們就可以直接通過SqlSession
獲取對應的實現類,通過介面中定義的行為來直接獲取結果:
public static void main(String[] args) {
try (SqlSession sqlSession = MybatisUtil.getSession(true)){
TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
List<Student> student = testMapper.selectStudent();
student.forEach(System.out::println);
}
}
那麼肯定有人好奇,TestMapper明明是一個我們自己定義介面啊,Mybatis也不可能提前幫我們寫了實現類啊,那這介面怎麼就出現了一個實現類呢?我們可以通過調用getClass()
方法來看看實現類是個什麼:
TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
System.out.println(testMapper.getClass());
我們發現,實現類名稱很奇怪,名稱為com.sun.proxy.$Proxy4
,它是通過動態代理生成的,相當於動態生成了一個實現類,而不是預先定義好的,有關Mybatis這一部分的原理,我們放在最後一節進行講解。
接下來,我們再來看配置文件,之前我們並沒有對配置文件進行一個詳細的介紹:
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/study"/>
<property name="username" value="test"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/test/mapper/TestMapper.xml"/>
</mappers>
</configuration>
首先就從environments
標籤說起,一般情況下,我們在開發中,都需要指定一個資料庫的配置資訊,包含連接URL、用戶、密碼等資訊,而environment
就是用於進行這些配置的!
實際情況下可能會不止有一個資料庫連接資訊,比如開發過程中我們一般會使用本地的資料庫,而如果需要將項目上傳到伺服器或是防止其他人的電腦上運行時,我們可能就需要配置另一個資料庫的資訊,因此,我們可以提前定義好所有的資料庫資訊,該什麼時候用什麼即可!
在environments
標籤上有一個default屬性,來指定默認的環境,當然如果我們希望使用其他環境,可以修改這個默認環境,也可以在創建工廠時選擇環境:
sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(new FileInputStream("mybatis-config.xml"), "環境ID");
我們還可以給類型起一個別名,以簡化Mapper的編寫:
<!-- 需要在environments的上方 -->
<typeAliases>
<typeAlias type="com.test.entity.Student" alias="Student"/>
</typeAliases>
現在Mapper就可以直接使用別名了:
<mapper namespace="com.test.mapper.TestMapper">
<select id="selectStudent" resultType="Student">
select * from student
</select>
</mapper>
如果這樣還是很麻煩,我們也可以直接讓Mybatis去掃描一個包,並將包下的所有類自動起別名(別名為首字母小寫的類名)
<typeAliases>
<package name="com.test.entity"/>
</typeAliases>
也可以為指定實體類添加一個註解,來指定別名:
@Data
@Alias("lbwnb")
public class Student {
private int sid;
private String name;
private String sex;
}
當然,Mybatis也包含許多的基礎配置,通過使用:
<settings>
<setting name="" value=""/>
</settings>
所有的配置項可以在中文文檔處查詢,本文不會進行詳細介紹,在後面我們會提出一些比較重要的配置項。
有關配置文件的介紹就暫時到這裡為止,我們討論的重心應該是Mybatis的應用,而不是配置文件,所以省略了一部分內容的講解。
增刪改查
在了解了Mybatis的一些基本配置之後,我們就可以正式來使用Mybatis來進行資料庫操作了!
在前面我們演示了如何快速進行查詢,我們只需要編寫一個對應的映射器既可以了:
<mapper namespace="com.test.mapper.TestMapper">
<select id="studentList" resultType="Student">
select * from student
</select>
</mapper>
當然,如果你不喜歡使用實體類,那麼這些屬性還可以被映射到一個Map上:
<select id="selectStudent" resultType="Map">
select * from student
</select>
public interface TestMapper {
List<Map> selectStudent();
}
Map中就會以鍵值對的形式來存放這些結果了。
通過設定一個resultType
屬性,讓Mybatis知道查詢結果需要映射為哪個實體類,要求欄位名稱保持一致。那麼如果我們不希望按照這樣的規則來映射呢?我們可以自定義resultMap
來設定映射規則:
<resultMap id="Test" type="Student">
<result column="sid" property="sid"/>
<result column="sex" property="name"/>
<result column="name" property="sex"/>
</resultMap>
通過指定映射規則,我們現在名稱和性別一欄就發生了交換,因為我們將其映射欄位進行了交換。
如果一個類中存在多個構造方法,那麼很有可能會出現這樣的錯誤:
### Exception in thread "main" org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: org.apache.ibatis.executor.ExecutorException: No constructor found in com.test.entity.Student matching [java.lang.Integer, java.lang.String, java.lang.String]
### The error may exist in com/test/mapper/TestMapper.xml
### The error may involve com.test.mapper.TestMapper.getStudentBySid
### The error occurred while handling results
### SQL: select * from student where sid = ?
### Cause: org.apache.ibatis.executor.ExecutorException: No constructor found in com.test.entity.Student matching [java.lang.Integer, java.lang.String, java.lang.String]
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
...
這時就需要使用constructor
標籤來指定構造方法:
<resultMap id="test" type="Student">
<constructor>
<arg column="sid" javaType="Integer"/>
<arg column="name" javaType="String"/>
</constructor>
</resultMap>
值得注意的是,指定構造方法後,若此欄位被填入了構造方法作為參數,將不會通過反射給欄位單獨賦值,而構造方法中沒有傳入的欄位,依然會被反射賦值,有關resultMap
的內容,後面還會繼續講解。
如果資料庫中存在一個帶下劃線的欄位,我們可以通過設置讓其映射為以駝峰命名的欄位,比如my_test
映射為myTest
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
如果不設置,默認為不開啟,也就是默認需要名稱保持一致。
我們接著來看看條件查詢,既然是條件查詢,那麼肯定需要我們傳入查詢條件,比如現在我們想通過sid欄位來通過學號查找資訊:
Student getStudentBySid(int sid);
<select id="getStudentBySid" parameterType="int" resultType="Student">
select * from student where sid = #{sid}
</select>
我們通過使用#{xxx}
或是${xxx}
來填入我們給定的屬性,實際上Mybatis本質也是通過PreparedStatement
首先進行一次預編譯,有效地防止SQL注入問題,但是如果使用${xxx}
就不再是通過預編譯,而是直接傳值,因此我們一般都使用#{xxx}
來進行操作。
使用parameterType
屬性來指定參數類型(非必須,可以不用,推薦不用)
接著我們來看插入、更新和刪除操作,其實與查詢操作差不多,不過需要使用對應的標籤,比如插入操作:
<insert id="addStudent" parameterType="Student">
insert into student(name, sex) values(#{name}, #{sex})
</insert>
int addStudent(Student student);
我們這裡使用的是一個實體類,我們可以直接使用實體類裡面對應屬性替換到SQL語句中,只需要填寫屬性名稱即可,和條件查詢是一樣的。
複雜查詢
一個老師可以教授多個學生,那麼能否一次性將老師的學生全部映射給此老師的對象呢,比如:
@Data
public class Teacher {
int tid;
String name;
List<Student> studentList;
}
映射為Teacher對象時,同時將其教授的所有學生一併映射為List列表,顯然這是一種一對多的查詢,那麼這時就需要進行複雜查詢了。而我們之前編寫的都非常簡單,直接就能完成映射,因此我們現在需要使用resultMap
來自定義映射規則:
<select id="getTeacherByTid" resultMap="asTeacher">
select *, teacher.name as tname from student inner join teach on student.sid = teach.sid
inner join teacher on teach.tid = teacher.tid where teach.tid = #{tid}
</select>
<resultMap id="asTeacher" type="Teacher">
<id column="tid" property="tid"/>
<result column="tname" property="name"/>
<collection property="studentList" ofType="Student">
<id property="sid" column="sid"/>
<result column="name" property="name"/>
<result column="sex" property="sex"/>
</collection>
</resultMap>
可以看到,我們的查詢結果是一個多表聯查的結果,而聯查的數據就是我們需要映射的數據(比如這裡是一個老師有N個學生,聯查的結果也是這一個老師對應N個學生的N條記錄),其中id
標籤用於在多條記錄中辨別是否為同一個對象的數據,比如上面的查詢語句得到的結果中,tid
這一行始終為1
,因此所有的記錄都應該是tid=1
的教師的數據,而不應該變為多個教師的數據,如果不加id進行約束,那麼會被識別成多個教師的數據!
通過使用collection來表示將得到的所有結果合併為一個集合,比如上面的數據中每個學生都有單獨的一條記錄,因此tid相同的全部學生的記錄就可以最後合併為一個List,得到最終的映射結果,當然,為了區分,最好也設置一個id,只不過這個例子中可以當做普通的result
使用。
了解了一對多,那麼多對一又該如何查詢呢,比如每個學生都有一個對應的老師,現在Student新增了一個Teacher對象,那麼現在又該如何去處理呢?
@Data
@Accessors(chain = true)
public class Student {
private int sid;
private String name;
private String sex;
private Teacher teacher;
}
@Data
public class Teacher {
int tid;
String name;
}
現在我們希望的是,每次查詢到一個Student對象時都帶上它的老師,同樣的,我們也可以使用resultMap
來實現(先修改一下老師的類定義,不然會很麻煩):
<resultMap id="test2" type="Student">
<id column="sid" property="sid"/>
<result column="name" property="name"/>
<result column="sex" property="sex"/>
<association property="teacher" javaType="Teacher">
<id column="tid" property="tid"/>
<result column="tname" property="name"/>
</association>
</resultMap>
<select id="selectStudent" resultMap="test2">
select *, teacher.name as tname from student left join teach on student.sid = teach.sid
left join teacher on teach.tid = teacher.tid
</select>
通過使用association
進行關聯,形成多對一的關係,實際上和一對多是同理的,都是對查詢結果的一種處理方式罷了。
事務操作
我們可以在獲取SqlSession
關閉自動提交來開啟事務模式,和JDBC其實都差不多:
public static void main(String[] args) {
try (SqlSession sqlSession = MybatisUtil.getSession(false)){
TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
testMapper.addStudent(new Student().setSex("男").setName("小王"));
testMapper.selectStudent().forEach(System.out::println);
}
}
我們發現,在關閉自動提交後,我們的內容是沒有進入到資料庫的,現在我們來試一下在最後提交事務:
sqlSession.commit();
在事務提交後,我們的內容才會被寫入到資料庫中。現在我們來試試看回滾操作:
try (SqlSession sqlSession = MybatisUtil.getSession(false)){
TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
testMapper.addStudent(new Student().setSex("男").setName("小王"));
testMapper.selectStudent().forEach(System.out::println);
sqlSession.rollback();
sqlSession.commit();
}
回滾操作也印證成功。
動態SQL
動態 SQL 是 MyBatis 的強大特性之一。如果你使用過 JDBC 或其它類似的框架,你應該能理解根據不同條件拼接 SQL 語句有多痛苦,例如拼接時要確保不能忘記添加必要的空格,還要注意去掉列表最後一個列名的逗號。利用動態 SQL,可以徹底擺脫這種痛苦。
我們直接使用官網的例子進行講解。
快取機制
MyBatis 內置了一個強大的事務性查詢快取機制,它可以非常方便地配置和訂製。
其實快取機制我們在之前學習IO流的時候已經提及過了,我們可以提前將一部分內容放入快取,下次需要獲取數據時,我們就可以直接從快取中讀取,這樣的話相當於直接從記憶體中獲取而不是再去向資料庫索要數據,效率會更高。
因此Mybatis內置了一個快取機制,我們查詢時,如果快取中存在數據,那麼我們就可以直接從快取中獲取,而不是再去向資料庫進行請求。
Mybatis存在一級快取和二級快取,我們首先來看一下一級快取,默認情況下,只啟用了本地的會話快取,它僅僅對一個會話中的數據進行快取(一級快取無法關閉,只能調整),我們來看看下面這段程式碼:
public static void main(String[] args) throws InterruptedException {
try (SqlSession sqlSession = MybatisUtil.getSession(true)){
TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
Student student1 = testMapper.getStudentBySid(1);
Student student2 = testMapper.getStudentBySid(1);
System.out.println(student1 == student2);
}
}
我們發現,兩次得到的是同一個Student對象,也就是說我們第二次查詢並沒有重新去構造對象,而是直接得到之前創建好的對象。如果還不是很明顯,我們可以修改一下實體類:
@Data
@Accessors(chain = true)
public class Student {
public Student(){
System.out.println("我被構造了");
}
private int sid;
private String name;
private String sex;
}
我們通過前面的學習得知Mybatis在映射為對象時,在只有一個構造方法的情況下,無論你構造方法寫成什麼樣子,都會去調用一次構造方法,如果存在多個構造方法,那麼就會去找匹配的構造方法。我們可以通過查看構造方法來驗證對象被創建了幾次。
結果顯而易見,只創建了一次,也就是說當第二次進行同樣的查詢時,會直接使用第一次的結果,因為第一次的結果已經被快取了。
那麼如果我修改了資料庫中的內容,快取還會生效嗎:
public static void main(String[] args) throws InterruptedException {
try (SqlSession sqlSession = MybatisUtil.getSession(true)){
TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
Student student1 = testMapper.getStudentBySid(1);
testMapper.addStudent(new Student().setName("小李").setSex("男"));
Student student2 = testMapper.getStudentBySid(1);
System.out.println(student1 == student2);
}
}
我們發現,當我們進行了插入操作後,快取就沒有生效了,我們再次進行查詢得到的是一個新創建的對象。
也就是說,一級快取,在進行DML操作後,會使得快取失效,也就是說Mybatis知道我們對資料庫裡面的數據進行了修改,所以之前快取的內容可能就不是當前資料庫裡面最新的內容了。還有一種情況就是,當前會話結束後,也會清理全部的快取,因為已經不會再用到了。但是一定注意,一級快取只針對於單個會話,多個會話之間不相通。
public static void main(String[] args) {
try (SqlSession sqlSession = MybatisUtil.getSession(true)){
TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
Student student2;
try(SqlSession sqlSession2 = MybatisUtil.getSession(true)){
TestMapper testMapper2 = sqlSession2.getMapper(TestMapper.class);
student2 = testMapper2.getStudentBySid(1);
}
Student student1 = testMapper.getStudentBySid(1);
System.out.println(student1 == student2);
}
}
注意:一個會話DML操作只會重置當前會話的快取,不會重置其他會話的快取,也就是說,其他會話快取是不會更新的!
一級快取給我們提供了很高速的訪問效率,但是它的作用範圍實在是有限,如果一個會話結束,那麼之前的快取就全部失效了,但是我們希望快取能夠擴展到所有會話都能使用,因此我們可以通過二級快取來實現,二級快取默認是關閉狀態,要開啟二級快取,我們需要在映射器XML文件中添加:
<cache/>
可見二級快取是Mapper級別的,也就是說,當一個會話失效時,它的快取依然會存在於二級快取中,因此如果我們再次創建一個新的會話會直接使用之前的快取,我們首先根據官方文檔進行一些配置:
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
我們來編寫一個程式碼:
public static void main(String[] args) {
Student student;
try (SqlSession sqlSession = MybatisUtil.getSession(true)){
TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
student = testMapper.getStudentBySid(1);
}
try (SqlSession sqlSession2 = MybatisUtil.getSession(true)){
TestMapper testMapper2 = sqlSession2.getMapper(TestMapper.class);
Student student2 = testMapper2.getStudentBySid(1);
System.out.println(student2 == student);
}
}
我們可以看到,上面的程式碼中首先是第一個會話在進行讀操作,完成後會結束會話,而第二個操作重新創建了一個新的會話,再次執行了同樣的查詢,我們發現得到的依然是快取的結果。
那麼如果我不希望某個方法開啟快取呢?我們可以添加useCache屬性來關閉快取:
<select id="getStudentBySid" resultType="Student" useCache="false">
select * from student where sid = #{sid}
</select>
我們也可以使用flushCache=”false”在每次執行後都清空快取,通過這這個我們還可以控制DML操作完成之後不清空快取。
<select id="getStudentBySid" resultType="Student" flushCache="true">
select * from student where sid = #{sid}
</select>
添加了二級快取之後,會先從二級快取中查找數據,當二級快取中沒有時,才會從一級快取中獲取,當一級快取中都還沒有數據時,才會請求資料庫,因此我們再來執行上面的程式碼:
public static void main(String[] args) {
try (SqlSession sqlSession = MybatisUtil.getSession(true)){
TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
Student student2;
try(SqlSession sqlSession2 = MybatisUtil.getSession(true)){
TestMapper testMapper2 = sqlSession2.getMapper(TestMapper.class);
student2 = testMapper2.getStudentBySid(1);
}
Student student1 = testMapper.getStudentBySid(1);
System.out.println(student1 == student2);
}
}
得到的結果就會是同一個對象了,因為現在是優先從二級快取中獲取。
讀取順序:二級快取 => 一級快取 => 資料庫
雖然快取機制給我們提供了很大的性能提升,但是快取存在一個問題,我們之前在電腦組成原理
中可能學習過快取一致性問題,也就是說當多個CPU在操作自己的快取時,可能會出現各自的快取內容不同步的問題,而Mybatis也會這樣,我們來看看這個例子:
public static void main(String[] args) throws InterruptedException {
try (SqlSession sqlSession = MybatisUtil.getSession(true)){
TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
while (true){
Thread.sleep(3000);
System.out.println(testMapper.getStudentBySid(1));
}
}
}
我們現在循環地每三秒讀取一次,而在這個過程中,我們使用IDEA手動修改資料庫中的數據,將1號同學的學號改成100,那麼理想情況下,下一次讀取將無法獲取到小明,因為小明的學號已經發生變化了。
但是結果卻是依然能夠讀取,並且sid並沒有發生改變,這也證明了Mybatis的快取在生效,因為我們是從外部進行修改,Mybatis不知道我們修改了數據,所以依然在使用快取中的數據,但是這樣很明顯是不正確的。
因此,如果存在多台伺服器或者是多個程式都在使用Mybatis操作同一個資料庫,並且都開啟了快取,需要解決這個問題,要麼就得關閉Mybatis的快取來保證一致性:
<settings>
<setting name="cacheEnabled" value="false"/>
</settings>
<select id="getStudentBySid" resultType="Student" useCache="false" flushCache="true">
select * from student where sid = #{sid}
</select>
要麼就需要實現快取共用,也就是讓所有的Mybatis都使用同一個快取進行數據存取,在後面,我們會繼續學習
Redis、Ehcache、Memcache等快取框架,通過使用這些工具,就能夠很好地解決快取一致性問題。
使用註解開發
在之前的開發中,我們已經體驗到Mybatis為我們帶來的便捷了,我們只需要編寫對應的映射器,並將其綁定到一個介面上,即可直接通過該介面執行我們的SQL語句,極大的簡化了我們之前JDBC那樣的程式碼編寫模式。
那麼,能否實現無需xml映射器配置,而是直接使用註解在介面上進行配置呢?
答案是可以的,也是現在推薦的一種方式(也不是說XML就不要去用了,由於Java 註解的表達能力和靈活性十分有限,可能相對於XML配置某些功能實現起來會不太好辦,但是在大部分場景下,直接使用註解開發已經綽綽有餘了)
首先我們來看一下,使用XML進行映射器編寫時,我們需要現在XML中定義映射規則和SQL語句,然後再將其綁定到一個介面的方法定義上,然後再使用介面來執行:
<insert id="addStudent">
insert into student(name, sex) values(#{name}, #{sex})
</insert>
int addStudent(Student student);
而現在,我們可以直接使用註解來實現,每個操作都有一個對應的註解:
@Insert("insert into student(name, sex) values(#{name}, #{sex})")
int addStudent(Student student);
當然,我們還需要修改一下配置文件中的映射器註冊:
<mappers>
<mapper class="com.test.mapper.MyMapper"/>
<!-- 也可以直接註冊整個包下的 <package name="com.test.mapper"/> -->
</mappers>
通過直接指定Class,來讓Mybatis知道我們這裡有一個通過註解實現的映射器。
我們接著來看一下,如何使用註解進行自定義映射規則:
@Results({
@Result(id = true, column = "sid", property = "sid"),
@Result(column = "sex", property = "name"),
@Result(column = "name", property = "sex")
})
@Select("select * from student")
List<Student> getAllStudent();
直接通過@Results
註解,就可以直接進行配置了,此註解的value是一個@Result
註解數組,每個@Result
註解都都一個單獨的欄位配置,其實就是我們之前在XML映射器中寫的:
<resultMap id="test" type="Student">
<id property="sid" column="sid"/>
<result column="name" property="sex"/>
<result column="sex" property="name"/>
</resultMap>
現在我們就可以通過註解來自定義映射規則了。那麼如何使用註解來完成複雜查詢呢?我們還是使用一個老師多個學生的例子:
@Results({
@Result(id = true, column = "tid", property = "tid"),
@Result(column = "name", property = "name"),
@Result(column = "tid", property = "studentList", many =
@Many(select = "getStudentByTid")
)
})
@Select("select * from teacher where tid = #{tid}")
Teacher getTeacherBySid(int tid);
@Select("select * from student inner join teach on student.sid = teach.sid where tid = #{tid}")
List<Student> getStudentByTid(int tid);
我們發現,多出了一個子查詢,而這個子查詢是單獨查詢該老師所屬學生的資訊,而子查詢結果作為@Result
註解的一個many結果,代表子查詢的所有結果都歸入此集合中(也就是之前的collection標籤)
<resultMap id="asTeacher" type="Teacher">
<id column="tid" property="tid"/>
<result column="tname" property="name"/>
<collection property="studentList" ofType="Student">
<id property="sid" column="sid"/>
<result column="name" property="name"/>
<result column="sex" property="sex"/>
</collection>
</resultMap>
同理,@Result
也提供了@One
子註解來實現一對一的關係表示,類似於之前的assocation
標籤:
@Results({
@Result(id = true, column = "sid", property = "sid"),
@Result(column = "sex", property = "name"),
@Result(column = "name", property = "sex"),
@Result(column = "sid", property = "teacher", one =
@One(select = "getTeacherBySid")
)
})
@Select("select * from student")
List<Student> getAllStudent();
如果現在我希望直接使用註解編寫SQL語句但是我希望映射規則依然使用XML來實現,這時該怎麼辦呢?
@ResultMap("test")
@Select("select * from student")
List<Student> getAllStudent();
提供了@ResultMap
註解,直接指定ID即可,這樣我們就可以使用XML中編寫的映射規則了,這裡就不再演示了。
那麼如果出現之前的兩個構造方法的情況,且沒有任何一個構造方法匹配的話,該怎麼處理呢?
@Data
@Accessors(chain = true)
public class Student {
public Student(int sid){
System.out.println("我是一號構造方法"+sid);
}
public Student(int sid, String name){
System.out.println("我是二號構造方法"+sid+name);
}
private int sid;
private String name;
private String sex;
}
我們可以通過@ConstructorArgs
註解來指定構造方法:
@ConstructorArgs({
@Arg(column = "sid", javaType = int.class),
@Arg(column = "name", javaType = String.class)
})
@Select("select * from student where sid = #{sid} and sex = #{sex}")
Student getStudentBySidAndSex(@Param("sid") int sid, @Param("sex") String sex);
得到的結果和使用constructor
標籤效果一致,這裡就不多做講解了。
我們發現,當參數列表中出現兩個以上的參數時,會出現錯誤:
@Select("select * from student where sid = #{sid} and sex = #{sex}")
Student getStudentBySidAndSex(int sid, String sex);
Exception in thread "main" org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: org.apache.ibatis.binding.BindingException: Parameter 'sid' not found. Available parameters are [arg1, arg0, param1, param2]
### Cause: org.apache.ibatis.binding.BindingException: Parameter 'sid' not found. Available parameters are [arg1, arg0, param1, param2]
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:153)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:145)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:76)
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:87)
at org.apache.ibatis.binding.MapperProxy$PlainMethodInvoker.invoke(MapperProxy.java:145)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:86)
at com.sun.proxy.$Proxy6.getStudentBySidAndSex(Unknown Source)
at com.test.Main.main(Main.java:16)
原因是Mybatis不明確到底哪個參數是什麼,因此我們可以添加@Param
來指定參數名稱:
@Select("select * from student where sid = #{sid} and sex = #{sex}")
Student getStudentBySidAndSex(@Param("sid") int sid, @Param("sex") String sex);
探究:要是我兩個參數一個是基本類型一個是對象類型呢?
System.out.println(testMapper.addStudent(100, new Student().setName("小陸").setSex("男")));
@Insert("insert into student(sid, name, sex) values(#{sid}, #{name}, #{sex})")
int addStudent(@Param("sid") int sid, @Param("student") Student student);
那麼這個時候,就出現問題了,Mybatis就不能明確這些屬性是從哪裡來的:
### SQL: insert into student(sid, name, sex) values(?, ?, ?)
### Cause: org.apache.ibatis.binding.BindingException: Parameter 'name' not found. Available parameters are [student, param1, sid, param2]
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:196)
at org.apache.ibatis.session.defaults.DefaultSqlSession.insert(DefaultSqlSession.java:181)
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:62)
at org.apache.ibatis.binding.MapperProxy$PlainMethodInvoker.invoke(MapperProxy.java:145)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:86)
at com.sun.proxy.$Proxy6.addStudent(Unknown Source)
at com.test.Main.main(Main.java:16)
那麼我們就通過參數名稱.屬性的方式去讓Mybatis知道我們要用的是哪個屬性:
@Insert("insert into student(sid, name, sex) values(#{sid}, #{student.name}, #{student.sex})")
int addStudent(@Param("sid") int sid, @Param("student") Student student);
那麼如何通過註解控制快取機制呢?
@CacheNamespace(readWrite = false)
public interface MyMapper {
@Select("select * from student")
@Options(useCache = false)
List<Student> getAllStudent();
使用@CacheNamespace
註解直接定義在介面上即可,然後我們可以通過使用@Options
來控制單個操作的快取啟用。
探究Mybatis的動態代理機制
在探究動態代理機制之前,我們要先聊聊什麼是代理:其實顧名思義,就好比我開了個大棚,裡面栽種的西瓜,那麼西瓜成熟了是不是得去賣掉賺錢,而我們的西瓜非常多,一個人肯定賣不過來,肯定就要去多找幾個開水果攤的幫我們賣,這就是一種代理。實際上是由水果攤老闆在幫我們賣瓜,我們只告訴老闆賣多少錢,而至於怎麼賣的是由水果攤老闆決定的。
那麼現在我們來嘗試實現一下這樣的類結構,首先定義一個介面用於規範行為:
public interface Shopper {
//賣瓜行為
void saleWatermelon(String customer);
}
然後需要實現一下賣瓜行為,也就是我們要告訴老闆賣多少錢,這裡就直接寫成成功出售:
public class ShopperImpl implements Shopper{
//賣瓜行為的實現
@Override
public void saleWatermelon(String customer) {
System.out.println("成功出售西瓜給 ===> "+customer);
}
}
最後老闆代理後肯定要用自己的方式去出售這些西瓜,成交之後再按照我們告訴老闆的價格進行出售:
public class ShopperProxy implements Shopper{
private final Shopper impl;
public ShopperProxy(Shopper impl){
this.impl = impl;
}
//代理賣瓜行為
@Override
public void saleWatermelon(String customer) {
//首先進行 代理商討價還價行為
System.out.println(customer + ":哥們,這瓜多少錢一斤啊?");
System.out.println("老闆:兩塊錢一斤。");
System.out.println(customer + ":你這瓜皮子是金子做的,還是瓜粒子是金子做的?");
System.out.println("老闆:你瞅瞅現在哪有瓜啊,這都是大棚的瓜,你嫌貴我還嫌貴呢。");
System.out.println(customer + ":給我挑一個。");
impl.saleWatermelon(customer); //討價還價成功,進行我們告訴代理商的賣瓜行為
}
}
現在我們來試試看:
public class Main {
public static void main(String[] args) {
Shopper shopper = new ShopperProxy(new ShopperImpl());
shopper.saleWatermelon("小強");
}
}
這樣的操作稱為靜態代理,也就是說我們需要提前知道介面的定義並進行實現才可以完成代理,而Mybatis這樣的是無法預知代理介面的,我們就需要用到動態代理。
JDK提供的反射框架就為我們很好地解決了動態代理的問題,在這裡相當於對JavaSE階段反射的內容進行一個補充。
public class ShopperProxy implements InvocationHandler {
Object target;
public ShopperProxy(Object target){
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String customer = (String) args[0];
System.out.println(customer + ":哥們,這瓜多少錢一斤啊?");
System.out.println("老闆:兩塊錢一斤。");
System.out.println(customer + ":你這瓜皮子是金子做的,還是瓜粒子是金子做的?");
System.out.println("老闆:你瞅瞅現在哪有瓜啊,這都是大棚的瓜,你嫌貴我還嫌貴呢。");
System.out.println(customer + ":行,給我挑一個。");
return method.invoke(target, args);
}
}
通過實現InvocationHandler來成為一個動態代理,我們發現它提供了一個invoke方法,用於調用被代理對象的方法並完成我們的代理工作。現在就可以通過 Proxy.newProxyInstance
來生成一個動態代理類:
public static void main(String[] args) {
Shopper impl = new ShopperImpl();
Shopper shopper = (Shopper) Proxy.newProxyInstance(impl.getClass().getClassLoader(),
impl.getClass().getInterfaces(), new ShopperProxy(impl));
shopper.saleWatermelon("小強");
System.out.println(shopper.getClass());
}
通過列印類型我們發現,就是我們之前看到的那種奇怪的類:class com.sun.proxy.$Proxy0
,因此Mybatis其實也是這樣的來實現的(肯定有人問了:Mybatis是直接代理介面啊,你這個不還是要把介面實現了嗎?)那我們來改改,現在我們不代理任何類了,直接做介面實現:
public class ShopperProxy implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String customer = (String) args[0];
System.out.println(customer + ":哥們,這瓜多少錢一斤啊?");
System.out.println("老闆:兩塊錢一斤。");
System.out.println(customer + ":你這瓜皮子是金子做的,還是瓜粒子是金子做的?");
System.out.println("老闆:你瞅瞅現在哪有瓜啊,這都是大棚的瓜,你嫌貴我還嫌貴呢。");
System.out.println(customer + ":行,給我挑一個。");
return null;
}
}
public static void main(String[] args) {
Shopper shopper = (Shopper) Proxy.newProxyInstance(Shopper.class.getClassLoader(),
new Class[]{ Shopper.class }, //因為本身就是介面,所以直接用就行
new ShopperProxy());
shopper.saleWatermelon("小強");
System.out.println(shopper.getClass());
}
我Mybatis屬於半自動框架,SQL語句依然需要我們自己編寫,雖然存在一定的麻煩,但是會更加靈活,而後面我們還會學習JPA,它是全自動的框架,你幾乎見不到SQL的影子!
使用JUnit進行單元測試
首先一問:我們為什麼需要單元測試?
隨著我們的項目逐漸變大,我們都是邊在寫邊在測試,而我們當時使用的測試方法,就是直接在主方法中運行測試。
但是,在很多情況下,我們的項目可能會很龐大,不可能每次都去完整地啟動一個項目來測試某一個功能,這樣顯然會降低我們的開發效率。
因此,我們需要使用單元測試來幫助我們針對於某個功能或是某個模組單獨運行程式碼進行測試,而不是啟動整個項目。
同時,在我們項目的維護過程中,難免會涉及到一些原有程式碼的修改,很有可能出現改了程式碼導致之前的功能出現問題(牽一髮而動全身),而我們又不一定能立即察覺到。
因此,我們可以提前保存一些測試用例,每次完成程式碼後都可以跑一遍測試用例,來確保之前的功能沒有因為後續的修改而出現問題。
我們還可以利用單元測試來評估某個模組或是功能的耗時和性能,快速排查導致程式運行緩慢的問題,這些都可以通過單元測試來完成,可見單元測試對於開發的重要性。
嘗試JUnit
首先需要導入JUnit依賴,我們在這裡使用Junit4進行介紹,最新的Junit5放到Maven板塊一起講解,Jar包已經放在影片下方簡介中,直接去下載即可。同時IDEA需要安裝JUnit插件(默認是已經捆綁安裝的,因此無需多餘配置)
現在我們創建一個新的類,來編寫我們的單元測試用例:
public class TestMain {
@Test
public void method(){
System.out.println("我是測試用例1");
}
@Test
public void method2(){
System.out.println("我是測試用例2");
}
}
我們可以點擊類前面的測試按鈕,或是單個方法前的測試按鈕,如果點擊類前面的測試按鈕,會執行所有的測試用例。
運行測試後,我們發現控制台得到了一個測試結果,顯示為綠色表示測試通過。
只需要通過打上@Test
註解,即可將一個方法標記為測試案例,我們可以直接運行此測試案例,但是我們編寫的測試方法有以下要求:
- 方法必須是public的
- 不能是靜態方法
- 返回值必須是void
- 必須是沒有任何參數的方法
對於一個測試案例來說,我們肯定希望測試的結果是我們所期望的一個值,因此,如果測試的結果並不是我們所期望的結果,那麼這個測試就應該沒有成功通過!
我們可以通過斷言工具類來進行判定:
public class TestMain {
@Test
public void method(){
System.out.println("我是測試案例!");
Assert.assertEquals(1, 2); //參數1是期盼值,參數2是實際測試結果值
}
}
通過運行程式碼後,我們發現測試過程中拋出了一個錯誤,並且IDEA給我們顯示了期盼結果和測試結果,那麼現在我們來測試一個案例,比如我們想查看冒泡排序的編寫是否正確:
@Test
public void method(){
int[] arr = {0, 4, 5, 2, 6, 9, 3, 1, 7, 8};
//錯誤的冒泡排序
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if(arr[j] > arr[j + 1]){
int tmp = arr[j];
arr[j] = arr[j+1];
// arr[j+1] = tmp;
}
}
}
Assert.assertArrayEquals(new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, arr);
}
通過測試,我們發現得到的結果並不是我們想要的結果,因此現在我們需要去修改為正確的冒泡排序,修改後,測試就能正確通過了。我們還可以再通過一個案例來更加深入地了解測試,現在我們想測試從資料庫中取數據是否為我們預期的數據:
@Test
public void method(){
try (SqlSession sqlSession = MybatisUtil.getSession(true)){
TestMapper mapper = sqlSession.getMapper(TestMapper.class);
Student student = mapper.getStudentBySidAndSex(1, "男");
Assert.assertEquals(new Student().setName("小明").setSex("男").setSid(1), student);
}
}
那麼如果我們在進行所有的測試之前需要做一些前置操作該怎麼辦呢,一種辦法是在所有的測試用例前面都加上前置操作,但是這樣顯然是很冗餘的,因為一旦發生修改就需要挨個進行修改,因此我們需要更加智慧的方法,我們可以通過@Before
註解來添加測試用例開始之前的前置操作:
public class TestMain {
private SqlSessionFactory sqlSessionFactory;
@Before
public void before(){
System.out.println("測試前置正在初始化...");
try {
sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(new FileInputStream("mybatis-config.xml"));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
System.out.println("測試初始化完成,正在開始測試案例...");
}
@Test
public void method1(){
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)){
TestMapper mapper = sqlSession.getMapper(TestMapper.class);
Student student = mapper.getStudentBySidAndSex(1, "男");
Assert.assertEquals(new Student().setName("小明").setSex("男").setSid(1), student);
System.out.println("測試用例1通過!");
}
}
@Test
public void method2(){
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)){
TestMapper mapper = sqlSession.getMapper(TestMapper.class);
Student student = mapper.getStudentBySidAndSex(2, "女");
Assert.assertEquals(new Student().setName("小紅").setSex("女").setSid(2), student);
System.out.println("測試用例2通過!");
}
}
}
同理,在所有的測試完成之後,我們還想添加一個收尾的動作,那麼只需要使用@After
註解即可添加結束動作:
@After
public void after(){
System.out.println("測試結束,收尾工作正在進行...");
}
JUL日誌系統
首先一問:我們為什麼需要日誌系統?
我們之前一直都在使用System.out.println
來列印資訊,但是,如果項目中存在大量的控制台輸出語句,會顯得很凌亂,而且日誌的粒度是不夠細的,假如我們現在希望,項目只在debug的情況下列印某些日誌,而在實際運行時不列印日誌,採用直接輸出的方式就很難實現了,因此我們需要使用日誌框架來規範化日誌輸出。
而JDK為我們提供了一個自帶的日誌框架,位於java.util.logging
包下,我們可以使用此框架來實現日誌的規範化列印,使用起來非常簡單:
public class Main {
public static void main(String[] args) {
// 首先獲取日誌列印器
Logger logger = Logger.getLogger(Main.class.getName());
// 調用info來輸出一個普通的資訊,直接填寫字元串即可
logger.info("我是普通的日誌");
}
}
我們可以在主類中使用日誌列印,得到日誌的列印結果:
十一月 15, 2021 12:55:37 下午 com.test.Main main
資訊: 我是普通的日誌
我們發現,通過日誌輸出的結果會更加規範。
JUL日誌講解
日誌分為7個級別,詳細資訊我們可以在Level類中查看:
- SEVERE(最高值)- 一般用於代表嚴重錯誤
- WARNING – 一般用於表示某些警告,但是不足以判斷為錯誤
- INFO (默認級別) – 常規消息
- CONFIG
- FINE
- FINER
- FINEST(最低值)
我們之前通過info
方法直接輸出的結果就是使用的默認級別的日誌,我們可以通過log
方法來設定該條日誌的輸出級別:
public static void main(String[] args) {
Logger logger = Logger.getLogger(Main.class.getName());
logger.log(Level.SEVERE, "嚴重的錯誤", new IOException("我就是錯誤"));
logger.log(Level.WARNING, "警告的內容");
logger.log(Level.INFO, "普通的資訊");
logger.log(Level.CONFIG, "級別低於普通訊息");
}
我們發現,級別低於默認級別的日誌資訊,無法輸出到控制台,我們可以通過設置來修改日誌的列印級別:
public static void main(String[] args) {
Logger logger = Logger.getLogger(Main.class.getName());
//修改日誌級別
logger.setLevel(Level.CONFIG);
//不使用父日誌處理器
logger.setUseParentHandlers(false);
//使用自定義日誌處理器
ConsoleHandler handler = new ConsoleHandler();
handler.setLevel(Level.CONFIG);
logger.addHandler(handler);
logger.log(Level.SEVERE, "嚴重的錯誤", new IOException("我就是錯誤"));
logger.log(Level.WARNING, "警告的內容");
logger.log(Level.INFO, "普通的資訊");
logger.log(Level.CONFIG, "級別低於普通訊息");
}
每個Logger
都有一個父日誌列印器,我們可以通過getParent()
來獲取:
public static void main(String[] args) throws IOException {
Logger logger = Logger.getLogger(Main.class.getName());
System.out.println(logger.getParent().getClass());
}
我們發現,得到的是java.util.logging.LogManager$RootLogger
這個類,它默認使用的是ConsoleHandler,且日誌級別為INFO,由於每一個日誌列印器都會直接使用父類的處理器,因此我們之前需要關閉父類然後使用我們自己的處理器。
我們通過使用自己日誌處理器來自定義級別的資訊列印到控制台,當然,日誌處理器不僅僅只有控制台列印,我們也可以使用文件處理器來處理日誌資訊,我們繼續添加一個處理器:
//添加輸出到本地文件
FileHandler fileHandler = new FileHandler("test.log");
fileHandler.setLevel(Level.WARNING);
logger.addHandler(fileHandler);
注意,這個時候就有兩個日誌處理器了,因此控制台和文件的都會生效。如果日誌的列印格式我們不喜歡,我們還可以自定義列印格式,比如我們控制台處理器就默認使用的是SimpleFormatter
,而文件處理器則是使用的XMLFormatter
,我們可以自定義:
//使用自定義日誌處理器(控制台)
ConsoleHandler handler = new ConsoleHandler();
handler.setLevel(Level.CONFIG);
handler.setFormatter(new XMLFormatter());
logger.addHandler(handler);
我們可以直接配置為想要的列印格式,如果這些格式還不能滿足你,那麼我們也可以自行實現:
public static void main(String[] args) throws IOException {
Logger logger = Logger.getLogger(Main.class.getName());
logger.setUseParentHandlers(false);
//為了讓顏色變回普通的顏色,通過程式碼塊在初始化時將輸出流設定為System.out
ConsoleHandler handler = new ConsoleHandler(){{
setOutputStream(System.out);
}};
//創建匿名內部類實現自定義的格式
handler.setFormatter(new Formatter() {
@Override
public String format(LogRecord record) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
String time = format.format(new Date(record.getMillis())); //格式化日誌時間
String level = record.getLevel().getName(); // 獲取日誌級別名稱
// String level = record.getLevel().getLocalizedName(); // 獲取本地化名稱(語言跟隨系統)
String thread = String.format("%10s", Thread.currentThread().getName()); //執行緒名稱(做了格式化處理,留出10格空間)
long threadID = record.getThreadID(); //執行緒ID
String className = String.format("%-20s", record.getSourceClassName()); //發送日誌的類名
String msg = record.getMessage(); //日誌消息
//\033[33m作為顏色程式碼,30~37都有對應的顏色,38是沒有顏色,IDEA能顯示,但是某些地方可能不支援
return "\033[38m" + time + " \033[33m" + level + " \033[35m" + threadID
+ "\033[38m --- [" + thread + "] \033[36m" + className + "\033[38m : " + msg + "\n";
}
});
logger.addHandler(handler);
logger.info("我是測試消息1...");
logger.log(Level.INFO, "我是測試消息2...");
logger.log(Level.WARNING, "我是測試消息3...");
}
日誌可以設置過濾器,如果我們不希望某些日誌資訊被輸出,我們可以配置過濾規則:
public static void main(String[] args) throws IOException {
Logger logger = Logger.getLogger(Main.class.getName());
//自定義過濾規則
logger.setFilter(record -> !record.getMessage().contains("普通"));
logger.log(Level.SEVERE, "嚴重的錯誤", new IOException("我就是錯誤"));
logger.log(Level.WARNING, "警告的內容");
logger.log(Level.INFO, "普通的資訊");
}
實際上,整個日誌的輸出流程如下:
Properties配置文件
Properties文件是Java的一種配置文件,我們之前學習了XML,但是我們發現XML配置文件讀取實在是太麻煩,那麼能否有一種簡單一點的配置文件呢?我們可以使用Properties文件:
name=Test
desc=Description
該文件配置很簡單,格式為配置項=配置值
,我們可以直接通過Properties
類來將其讀取為一個類似於Map一樣的對象:
public static void main(String[] args) throws IOException {
Properties properties = new Properties();
properties.load(new FileInputStream("test.properties"));
System.out.println(properties);
}
我們發現,Properties
類是繼承自Hashtable
,而Hashtable
是實現的Map介面,也就是說,Properties
本質上就是一個Map一樣的結構,它會把所有的配置項映射為一個Map,這樣我們就可以快速地讀取對應配置的值了。
我們也可以將已經存在的Properties對象放入輸出流進行保存,我們這裡就不保存文件了,而是直接列印到控制台,我們只需要提供輸出流即可:
public static void main(String[] args) throws IOException {
Properties properties = new Properties();
// properties.setProperty("test", "lbwnb"); //和put效果一樣
properties.put("test", "lbwnb");
properties.store(System.out, "????");
//properties.storeToXML(System.out, "????"); 保存為XML格式
}
我們可以通過System.getProperties()
獲取系統的參數,我們來看看:
public static void main(String[] args) throws IOException {
System.getProperties().store(System.out, "系統資訊:");
}
編寫日誌配置文件
我們可以通過進行配置文件來規定日誌列印器的一些默認值:
# RootLogger 的默認處理器為
handlers= java.util.logging.ConsoleHandler
# RootLogger 的默認的日誌級別
.level= CONFIG
我們來嘗試使用配置文件來進行配置:
public static void main(String[] args) throws IOException {
//獲取日誌管理器
LogManager manager = LogManager.getLogManager();
//讀取我們自己的配置文件
manager.readConfiguration(new FileInputStream("logging.properties"));
//再獲取日誌列印器
Logger logger = Logger.getLogger(Main.class.getName());
logger.log(Level.CONFIG, "我是一條日誌資訊"); //通過自定義配置文件,我們發現默認級別不再是INFO了
}
我們也可以去修改ConsoleHandler
的默認配置:
# 指定默認日誌級別
java.util.logging.ConsoleHandler.level = ALL
# 指定默認日誌消息格式
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# 指定默認的字符集
java.util.logging.ConsoleHandler.encoding = UTF-8
其實,我們閱讀ConsoleHandler
的源碼就會發現,它就是通過讀取配置文件來進行某些參數設置:
// Private method to configure a ConsoleHandler from LogManager
// properties and/or default values as specified in the class
// javadoc.
private void configure() {
LogManager manager = LogManager.getLogManager();
String cname = getClass().getName();
setLevel(manager.getLevelProperty(cname +".level", Level.INFO));
setFilter(manager.getFilterProperty(cname +".filter", null));
setFormatter(manager.getFormatterProperty(cname +".formatter", new SimpleFormatter()));
try {
setEncoding(manager.getStringProperty(cname +".encoding", null));
} catch (Exception ex) {
try {
setEncoding(null);
} catch (Exception ex2) {
// doing a setEncoding with null should always work.
// assert false;
}
}
}
使用Lombok快速開啟日誌
我們發現,如果我們現在需要全面使用日誌系統,而不是傳統的直接列印,那麼就需要在每個類都去編寫獲取Logger的程式碼,這樣顯然是很冗餘的,能否簡化一下這個流程呢?
前面我們學習了Lombok,我們也體會到Lombok給我們帶來的便捷,我們可以通過一個註解快速生成構造方法、Getter和Setter,同樣的,Logger也是可以使用Lombok快速生成的。
@Log
public class Main {
public static void main(String[] args) {
System.out.println("自動生成的Logger名稱:"+log.getName());
log.info("我是日誌資訊");
}
}
只需要添加一個@Log
註解即可,添加後,我們可以直接使用一個靜態變數log,而它就是自動生成的Logger。我們也可以手動指定名稱:
@Log(topic = "打工是不可能打工的")
public class Main {
public static void main(String[] args) {
System.out.println("自動生成的Logger名稱:"+log.getName());
log.info("我是日誌資訊");
}
}
Mybatis日誌系統
Mybatis也有日誌系統,它詳細記錄了所有的資料庫操作等,但是我們在前面的學習中沒有開啟它,現在我們學習了日誌之後,我們就可以嘗試開啟Mybatis的日誌系統,來監控所有的資料庫操作,要開啟日誌系統,我們需要進行配置:
<setting name="logImpl" value="STDOUT_LOGGING" />
logImpl
包括很多種配置項,包括 SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING,而默認情況下是未配置,也就是說不列印。我們這裡將其設定為STDOUT_LOGGING表示直接使用標準輸出將日誌資訊列印到控制台,我們編寫一個測試案例來看看效果:
public class TestMain {
private SqlSessionFactory sqlSessionFactory;
@Before
public void before(){
try {
sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(new FileInputStream("mybatis-config.xml"));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
@Test
public void test(){
try(SqlSession sqlSession = sqlSessionFactory.openSession(true)){
TestMapper mapper = sqlSession.getMapper(TestMapper.class);
System.out.println(mapper.getStudentBySidAndSex(1, "男"));
System.out.println(mapper.getStudentBySidAndSex(1, "男"));
}
}
}
我們發現,兩次獲取學生資訊,只有第一次打開了資料庫連接,而第二次並沒有。
現在我們學習了日誌系統,那麼我們來嘗試使用日誌系統輸出Mybatis的日誌資訊:
<setting name="logImpl" value="JDK_LOGGING" />
將其配置為JDK_LOGGING表示使用JUL進行日誌列印,因為Mybatis的日誌級別都比較低,因此我們需要設置一下logging.properties
默認的日誌級別:
handlers= java.util.logging.ConsoleHandler
.level= ALL
java.util.logging.ConsoleHandler.level = ALL
程式碼編寫如下:
@Log
public class TestMain {
private SqlSessionFactory sqlSessionFactory;
@Before
public void before(){
try {
sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(new FileInputStream("mybatis-config.xml"));
LogManager manager = LogManager.getLogManager();
manager.readConfiguration(new FileInputStream("logging.properties"));
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void test(){
try(SqlSession sqlSession = sqlSessionFactory.openSession(true)){
TestMapper mapper = sqlSession.getMapper(TestMapper.class);
log.info(mapper.getStudentBySidAndSex(1, "男").toString());
log.info(mapper.getStudentBySidAndSex(1, "男").toString());
}
}
}
但是我們發現,這樣的日誌資訊根本沒法看,因此我們需要修改一下日誌的列印格式,我們自己創建一個格式化類:
public class TestFormatter extends Formatter {
@Override
public String format(LogRecord record) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
String time = format.format(new Date(record.getMillis())); //格式化日誌時間
return time + " : " + record.getMessage() + "\n";
}
}
現在再來修改一下默認的格式化實現:
handlers= java.util.logging.ConsoleHandler
.level= ALL
java.util.logging.ConsoleHandler.level = ALL
java.util.logging.ConsoleHandler.formatter = com.test.TestFormatter
現在就好看多了,當然,我們還可以繼續為Mybatis添加文件日誌,這裡就不做演示了。
使用Maven管理項目
注意:開始之前,看看你C盤空間夠不夠,最好預留2GB空間以上!
吐槽:很多電腦預裝系統C盤都給得巨少,就算不裝軟體,一些軟體的快取文件也能給你塞滿,建議有時間重裝一下系統重新分配一下磁碟空間。
Maven 翻譯為”專家”、”內行”,是 Apache 下的一個純 Java 開發的開源項目。基於項目對象模型(縮寫:POM)概念,Maven利用一個中央資訊片斷能管理一個項目的構建、報告和文檔等步驟。Maven 是一個項目管理工具,可以對 Java 項目進行構建、依賴管理。Maven 也可被用於構建和管理各種項目,例如 C#,Ruby,Scala 和其他語言編寫的項目。Maven 曾是 Jakarta 項目的子項目,現為由 Apache 軟體基金會主持的獨立 Apache 項目。
通過Maven,可以幫助我們做:
- 項目的自動構建,包括程式碼的編譯、測試、打包、安裝、部署等操作。
- 依賴管理,項目使用到哪些依賴,可以快速完成導入。
我們之前並沒有講解如何將我們的項目打包為Jar文件運行,同時,我們導入依賴的時候,每次都要去下載對應的Jar包,這樣其實是很麻煩的,並且還有可能一個Jar包依賴於另一個Jar包,就像之前使用JUnit一樣,因此我們需要一個更加方便的包管理機制。
Maven也需要安裝環境,但是IDEA已經自帶了Maven環境,因此我們不需要再去進行額外的環境安裝(無IDEA也能使用Maven,但是配置過程很麻煩,並且我們現在使用的都是IDEA的集成開發環境,所以這裡就不講解Maven命令行操作了)我們直接創建一個新的Maven項目即可。
Maven項目結構
我們可以來看一下,一個Maven項目和我們普通的項目有什麼區別:
那麼首先,我們需要了解一下POM文件,它相當於是我們整個Maven項目的配置文件,它也是使用XML編寫的:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="//maven.apache.org/POM/4.0.0"
xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="//maven.apache.org/POM/4.0.0 //maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>MavenTest</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
我們可以看到,Maven的配置文件是以project
為根節點,而modelVersion
定義了當前模型的版本,一般是4.0.0,我們不用去修改。
groupId
、artifactId
、version
這三個元素合在一起,用於唯一區別每個項目,別人如果需要將我們編寫的程式碼作為依賴,那麼就必須通過這三個元素來定位我們的項目,我們稱為一個項目的基本坐標,所有的項目一般都有自己的Maven坐標,因此我們通過Maven導入其他的依賴只需要填寫這三個基本元素就可以了,無需再下載Jar文件,而是Maven自動幫助我們下載依賴並導入。
groupId
一般用於指定組名稱,命名規則一般和包名一致,比如我們這裡使用的是org.example
,一個組下面可以有很多個項目。artifactId
一般用於指定項目在當前組中的唯一名稱,也就是說在組中用於區分於其他項目的標記。version
代表項目版本,隨著我們項目的開發和改進,版本號也會不斷更新,就像LOL一樣,每次賽季更新都會有一個大版本更新,我們的Maven項目也是這樣,我們可以手動指定當前項目的版本號,其他人使用我們的項目作為依賴時,也可以根本版本號進行選擇(這裡的SNAPSHOT代表快照,一般表示這是一個處於開發中的項目,正式發布項目一般只帶版本號)
properties
中一般都是一些變數和選項的配置,我們這裡指定了JDK的源程式碼和編譯版本為1.8,無需進行修改。
Maven依賴導入
現在我們嘗試使用Maven來幫助我們快速導入依賴,我們需要導入之前的JDBC驅動依賴、JUnit依賴、Mybatis依賴、Lombok依賴,那麼如何使用Maven來管理依賴呢?
我們可以創建一個dependencies
節點:
<dependencies>
//裡面填寫的就是所有的依賴
</dependencies>
那麼現在就可以向節點中填寫依賴了,那麼我們如何知道每個依賴的坐標呢?我們可以在://mvnrepository.com/ 進行查詢(可能打不開,建議用流量,或是直接百度某個項目的Maven依賴),我們直接搜索lombok即可,打開後可以看到已經給我們寫出了依賴的坐標:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>provided</scope>
</dependency>
我們直接將其添加到dependencies
節點中即可,現在我們來編寫一個測試用例看看依賴導入成功了沒有:
public class Main {
public static void main(String[] args) {
Student student = new Student("小明", 18);
System.out.println(student);
}
}
@Data
@AllArgsConstructor
public class Student {
String name;
int age;
}
項目運行成功,表示成功導入了依賴。那麼,Maven是如何進行依賴管理呢,以致於如此便捷的導入依賴,我們來看看Maven項目的依賴管理流程:
通過流程圖我們得知,一個項目依賴一般是存儲在中央倉庫中,也有可能存儲在一些其他的遠程倉庫(私服),幾乎所有的依賴都被放到了中央倉庫中,因此,Maven可以直接從中央倉庫中下載大部分的依賴(Maven第一次導入依賴是需要聯網的),遠程倉庫中下載之後 ,會暫時存儲在本地倉庫,我們會發現我們本地存在一個.m2
文件夾,這就是Maven本地倉庫文件夾,默認建立在C盤,如果你C盤空間不足,會出現問題!
在下次導入依賴時,如果Maven發現本地倉庫中就已經存在某個依賴,那麼就不會再去遠程倉庫下載了。
可能在導入依賴時,小小夥伴們會出現卡頓的問題,我們建議配置一下IDEA自帶的Maven插件遠程倉庫地址,我們打開IDEA的安裝目錄,找到安裝根目錄/plugins/maven/lib/maven3/conf
文件夾,找到settings.xml
文件,打開編輯:
找到mirros標籤,添加以下內容:
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>*</mirrorOf>
<name>Nexus aliyun</name>
<url>//maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
這樣,我們就將默認的遠程倉庫地址(國外),配置為中國的阿里雲倉庫地址了(依賴的下載速度就會快起來了)
Maven依賴作用域
除了三個基本的屬性用於定位坐標外,依賴還可以添加以下屬性:
- type:依賴的類型,對於項目坐標定義的packaging。大部分情況下,該元素不必聲明,其默認值為jar
- scope:依賴的範圍(作用域,著重講解)
- optional:標記依賴是否可選
- exclusions:用來排除傳遞性依賴(一個項目有可能依賴於其他項目,就像我們的項目,如果別人要用我們的項目作為依賴,那麼就需要一起下載我們項目的依賴,如Lombok)
我們著重來講解一下scope
屬性,它決定了依賴的作用域範圍:
- compile :為默認的依賴有效範圍。如果在定義依賴關係的時候,沒有明確指定依賴有效範圍的話,則默認採用該依賴有效範圍。此種依賴,在編譯、運行、測試時均有效。
- provided :在編譯、測試時有效,但是在運行時無效,也就是說,項目在運行時,不需要此依賴,比如我們上面的Lombok,我們只需要在編譯階段使用它,編譯完成後,實際上已經轉換為對應的程式碼了,因此Lombok不需要在項目運行時也存在。
- runtime :在運行、測試時有效,但是在編譯程式碼時無效。比如我們如果需要自己寫一個JDBC實現,那麼肯定要用到JDK為我們指定的介面,但是實際上在運行時是不用自帶JDK的依賴,因此只保留我們自己寫的內容即可。
- test :只在測試時有效,例如:JUnit,我們一般只會在測試階段使用JUnit,而實際項目運行時,我們就用不到測試了,那麼我們來看看,導入JUnit的依賴:
同樣的,我們可以在網站上搜索Junit的依賴,我們這裡導入最新的JUnit5作為依賴:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
我們所有的測試用例全部編寫到Maven項目給我們劃分的test目錄下,位於此目錄下的內容不會在最後被打包到項目中,只用作開發階段測試使用:
public class MainTest {
@Test
public void test(){
System.out.println("測試");
//Assert在JUnit5時名稱發生了變化Assertions
Assertions.assertArrayEquals(new int[]{1, 2, 3}, new int[]{1, 2});
}
}
因此,一般僅用作測試的依賴如JUnit只保留在測試中即可,那麼現在我們再來添加JDBC和Mybatis的依賴:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
我們發現,Maven還給我們提供了一個resource
文件夾,我們可以將一些靜態資源,比如配置文件,放入到這個文件夾中,項目在打包時會將資源文件夾中文件一起打包的Jar中,比如我們在這裡編寫一個Mybatis的配置文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"//mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="cacheEnabled" value="true"/>
<setting name="logImpl" value="JDK_LOGGING" />
</settings>
<!-- 需要在environments的上方 -->
<typeAliases>
<package name="com.test.entity"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/study"/>
<property name="username" value="test"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper class="com.test.mapper.TestMapper"/>
</mappers>
</configuration>
現在我們創建一下測試用例,順便帶大家了解一下Junit5的一些比較方便的地方:
public class MainTest {
//因為配置文件位於內部,我們需要使用Resources類的getResourceAsStream來獲取內部的資源文件
private static SqlSessionFactory factory;
//在JUnit5中@Before被廢棄,它被細分了:
@BeforeAll // 一次性開啟所有測試案例只會執行一次 (方法必須是static)
// @BeforeEach 一次性開啟所有測試案例每個案例開始之前都會執行一次
@SneakyThrows
public static void before(){
factory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream("mybatis.xml"));
}
@DisplayName("Mybatis資料庫測試") //自定義測試名稱
@RepeatedTest(3) //自動執行多次測試
public void test(){
try (SqlSession sqlSession = factory.openSession(true)){
TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
System.out.println(testMapper.getStudentBySid(1));
}
}
}
那麼就有人提問了,如果我需要的依賴沒有上傳的遠程倉庫,而是只有一個Jar怎麼辦呢?我們可以使用第四種作用域:
- system:作用域和provided是一樣的,但是它不是從遠程倉庫獲取,而是直接導入本地Jar包:
<dependency>
<groupId>javax.jntm</groupId>
<artifactId>lbwnb</artifactId>
<version>2.0</version>
<scope>system</scope>
<systemPath>C://學習資料/4K高清無碼/test.jar</systemPath>
</dependency>
比如上面的例子,如果scope為system,那麼我們需要添加一個systemPath來指定jar文件的位置,這裡就不再演示了。
Maven可選依賴
當項目中的某些依賴不希望被使用此項目作為依賴的項目使用時,我們可以給依賴添加optional
標籤表示此依賴是可選的,默認在導入依賴時,不會導入可選的依賴:
<optional>true</optional>
比如Mybatis的POM文件中,就存在大量的可選依賴:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.30</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
<optional>true</optional>
</dependency>
...
由於Mybatis要支援多種類型的日誌,需要用到很多種不同的日誌框架,因此需要導入這些依賴來做兼容,但是我們項目中並不一定會使用這些日誌框架作為Mybatis的日誌列印器,因此這些日誌框架僅Mybatis內部做兼容需要導入使用,而我們可以選擇不使用這些框架或是選擇其中一個即可,也就是說我們導入Mybatis之後想用什麼日誌框架再自己加就可以了。
Maven排除依賴
我們了解了可選依賴,現在我們可以讓使用此項目作為依賴的項目默認不使用可選依賴,但是如果存在那種不是可選依賴,但是我們導入此項目有不希望使用此依賴該怎麼辦呢,這個時候我們就可以通過排除依賴來防止添加不必要的依賴:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.1</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
我們這裡演示了排除JUnit的一些依賴,我們可以在外部庫中觀察排除依賴之後和之前的效果。
Maven繼承關係
一個Maven項目可以繼承自另一個Maven項目,比如多個子項目都需要父項目的依賴,我們就可以使用繼承關係來快速配置。
我們右鍵左側欄,新建一個模組,來創建一個子項目:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="//maven.apache.org/POM/4.0.0"
xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="//maven.apache.org/POM/4.0.0 //maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>MavenTest</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ChildModel</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
我們可以看到,IDEA默認給我們添加了一個parent節點,表示此Maven項目是父Maven項目的子項目,子項目直接繼承父項目的groupId
,子項目會直接繼承父項目的所有依賴,除非依賴添加了optional標籤,我們來編寫一個測試用例嘗試一下:
import lombok.extern.java.Log;
@Log
public class Main {
public static void main(String[] args) {
log.info("我是日誌資訊");
}
}
可以看到,子項目也成功繼承了Lombok依賴。
我們還可以讓父Maven項目統一管理所有的依賴,包括版本號等,子項目可以選取需要的作為依賴,而版本全由父項目管理,我們可以將dependencies
全部放入dependencyManagement
節點,這樣父項目就完全作為依賴統一管理。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
</dependencies>
</dependencyManagement>
我們發現,子項目的依賴失效了,因為現在父項目沒有依賴,而是將所有的依賴進行集中管理,子項目需要什麼再拿什麼即可,同時子項目無需指定版本,所有的版本全部由父項目決定,子項目只需要使用即可:
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
當然,父項目如果還存在dependencies節點的話,裡面的內依賴依然是直接繼承:
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
...
Maven常用命令
我們可以看到在IDEA右上角Maven板塊中,每個Maven項目都有一個生命周期,實際上這些是Maven的一些插件,每個插件都有各自的功能,比如:
clean
命令,執行後會清理整個target
文件夾,在之後編寫Springboot項目時可以解決一些快取沒更新的問題。validate
命令可以驗證項目的可用性。compile
命令可以將項目編譯為.class文件。install
命令可以將當前項目安裝到本地倉庫,以供其他項目導入作為依賴使用verify
命令可以按順序執行每個默認生命周期階段(validate
,compile
,package
等)
Maven測試項目
通過使用test
命令,可以一鍵測試所有位於test目錄下的測試案例,請注意有以下要求:
- 測試類的名稱必須是以
Test
結尾,比如MainTest
- 測試方法上必須標註
@Test
註解,實測@RepeatedTest
無效
這是由於JUnit5比較新,我們需要重新配置插件升級到高版本,才能完美的兼容Junit5:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<!-- JUnit 5 requires Surefire version 2.22.0 or higher -->
<version>2.22.0</version>
</plugin>
</plugins>
</build>
現在@RepeatedTest
、@BeforeAll
也能使用了。
Maven打包項目
我們的項目在編寫完成之後,要麼作為Jar依賴,供其他模型使用,要麼就作為一個可以執行的程式,在控制台運行,我們只需要直接執行package
命令就可以直接對項目的程式碼進行打包,生成jar文件。
當然,以上方式僅適用於作為Jar依賴的情況,如果我們需要打包一個可執行文件,那麼我不僅需要將自己編寫的類打包到Jar中,同時還需要將依賴也一併打包到Jar中,因為我們使用了別人為我們通過的框架,自然也需要運行別人的程式碼,我們需要使用另一個插件來實現一起打包:
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>com.test.Main</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
在打包之前也會執行一次test命令,來保證項目能夠正常運行,當測試出現問題時,打包將無法完成,我們也可以手動跳過,選擇執行Maven目標
來手動執行Maven命令,輸入mvn package -Dmaven.test.skip=true
來以跳過測試的方式進行打包。
最後得到我們的Jar文件,在同級目錄下輸入java -jar xxxx.jar
來運行我們打包好的Jar可執行程式(xxx代表文件名稱)
deploy
命令用於發布項目到本地倉庫和遠程倉庫,一般情況下用不到,這裡就不做講解了。site
命令用於生成當前項目的發布站點,暫時不需要了解。
我們之前還講解了多模組項目,那麼多模組下父項目存在一個packing
打包類型標籤,所有的父級項目的packing都為pom,packing默認是jar類型,如果不作配置,maven會將該項目打成jar包。作為父級項目,還有一個重要的屬性,那就是modules,通過modules標籤將項目的所有子項目引用進來,在build父級項目時,會根據子模組的相互依賴關係整理一個build順序,然後依次build。