【Spring】Spring 入門
Spring 入門
Spring 概述
Spring
Spring 是分層的 Java SE/EE 應用全棧式輕量級開源框架,以 IOC(Inverse Of Control,反轉控制)和 AOP(Aspect Oriented Programming,面向切面程式設計)為核心,提供了 表現層 Spring MVC 和 持久層 Spring JDBC 以及 業務層事務管理等眾多技術。而且可以方便的整合其他開源框架和類庫。
Spring 優勢
- 方便解耦,簡化開發:通過 IOC 容器可以將物件間的依賴關係交由 Spring 進行控制,可以避免過度的程式耦合。而且也不需要再為單例模式、屬性解析等底層需求編寫程式碼。
- 面向切面:通過 AOP,可以方便進行面向切面的程式設計,彌補面向物件的一切缺陷。
- 宣告式事務:通過 宣告式方式靈活的進行事務管理,可以提高開發效率和質量。
- 原始碼學習典範:Spring 的原始碼設計精妙、結構清晰、匠心獨用,處處體現著對 Java 設計模式靈活運用以及對 Java 技術的高深造詣。它的原始碼是 Java 技術的最佳實踐的範例。
Spring 體系結構
程式的耦合和解耦
耦合性是對模組間關聯程度的度量。耦合的強弱取決於模組間介面的複雜性、呼叫模組的方式以及通過介面傳送資料的多少。
耦合的分類:
- 內容耦合:當一個模組直接修改或操作另一個模組的資料時,或一個模組不通過正常入口而轉入另一個模組時。內容耦合是最高程度
- 公共耦合:兩個或兩個以上的模組共同引用一個全域性資料項,這種耦合被稱為公共耦合。在具有大量公共耦合的結構中,確定究竟是哪個模組給全域性變數賦了一個特定的值是十分困難的。
- 外部耦合:一組模組都訪問同一全域性簡單變數而不是同一全域性資料結構,而且不是通過引數表傳遞該全域性變數的資訊,則稱之為外部耦合。
- 控制耦合:一個模組通過介面向另一個模組傳遞一個控制訊號,接受訊號的模組根據訊號值而進行適當的動作,這種耦合被稱為控制耦合。
- 標記耦合:若一個模組 A 通過介面向兩個模組 B 和 C 傳遞一個公共引數,那麼稱模組 B 和 C 之間存在一個標記耦合。
- 資料耦合:模組之間通過引數來傳遞資料,那麼被稱為資料耦合。資料耦合是最低
- 非直接耦合:兩個模組之間沒有直接關係,它們之間的聯絡完全是通過主模組的控制和呼叫來實現的。
總結起來,就是如果模組間必須存在耦合,就儘量使用資料耦合,少用控制耦合,限制公共耦合的範圍,儘量避免使用內容耦合。
程式耦合舉例
-
AccountDAOImpl.java
package cn.parzulpan.dao; /** * @Author : parzulpan * @Time : 2020-12 * @Desc : 賬戶持久層介面的實現類 */ public class AccountDAOImpl implements AccountDAO{ /** * 模擬儲存賬戶 */ public void saveAccount() { System.out.println("儲存了賬戶..."); } }
-
AccountServiceImpl.java
package cn.parzulpan.service; import cn.parzulpan.dao.AccountDAO; import cn.parzulpan.dao.AccountDAOImpl; /** * @Author : parzulpan * @Time : 2020-12 * @Desc : 賬戶業務層介面的實現類 */ public class AccountServiceImpl implements AccountService{ private AccountDAO accountDAO = new AccountDAOImpl(); // 這裡發生了耦合 /** * 模擬儲存賬戶 */ public void saveAccount() { accountDAO.saveAccount(); } }
-
Client.java
package cn.parzulpan.ui; import cn.parzulpan.service.AccountServiceImpl; /** * @Author : parzulpan * @Time : 2020-12 * @Desc : 模擬一個表現層,用於呼叫業務層,實際開發中應該是一個 Servlet 等 */ public class Client { public static void main(String[] args) { AccountServiceImpl accountService = new AccountServiceImpl(); // 這裡發生了耦合 accountService.saveAccount(); } }
Factory 解耦
在實際開發中可以把三層的物件都使用配置檔案配置起來,當啟動伺服器應用載入的時候,讓一個類中的方法通過讀取配置檔案,把這些物件創建出來並且存起來(用容器儲存)。在接下來的使用的時候,直接拿過來用就行。
那麼,這個讀取配置檔案,建立和獲取三層物件的類就是工廠類。即兩個步驟:
- 通過讀取配置檔案來獲取建立物件的全限定類名。
- 使用反射來建立物件,避免使用 new 關鍵字。
工廠就是負責給從容器中獲取指定物件的類,這時候獲取物件的方式發生了改變。之前,在獲取物件時,採用 new 的方式,是主動的。現在,在獲取物件時,採用跟工廠要的方式,工廠會查詢或者建立物件,是被動的。
之前:
現在:
-
bean.properties
accountService=cn.parzulpan.service.AccountServiceImpl accountDAO = cn.parzulpan.dao.AccountDAOImpl
-
BeanFactory.java
package cn.parzulpan.factory; import java.io.InputStream; import java.util.*; /** * @Author : parzulpan * @Time : 2020-12 * @Desc : 工廠類,負責給從容器中獲取指定物件的類 */ public class BeanFactory { private static Properties properties; private static Map<String, Object> beans; // Factory 解耦的優化,存放建立的物件,稱為容器 static { try { // 例項化物件 properties = new Properties(); // 獲取檔案流物件,使用類載入器 InputStream is = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties"); properties.load(is); beans = new HashMap<>(); Enumeration<Object> keys = properties.keys(); while (keys.hasMoreElements()) { String key = keys.nextElement().toString(); String beanPath = properties.getProperty(key); Object instance = Class.forName(beanPath).newInstance(); beans.put(key, instance); } } catch (Exception e) { e.printStackTrace(); throw new ExceptionInInitializerError("初始化 Properties 失敗!"); } } /** * 獲取指定物件的類 * @param beanName * @return */ public static Object getBean(String beanName){ try { // return Class.forName(properties.getProperty(beanName)).newInstance(); // 兩個步驟 System.out.println(beanName + " " + beans.get(beanName)); return beans.get(beanName); // 兩個步驟 } catch (Exception e) { e.printStackTrace(); } return null; } }
-
AccountServiceImpl.java
package cn.parzulpan.service; import cn.parzulpan.dao.AccountDAO; import cn.parzulpan.dao.AccountDAOImpl; import cn.parzulpan.factory.BeanFactory; /** * @Author : parzulpan * @Time : 2020-12 * @Desc : 賬戶業務層介面的實現類 */ public class AccountServiceImpl implements AccountService{ // private AccountDAO accountDAO = new AccountDAOImpl(); // 這裡發生了耦合 /** * 模擬儲存賬戶 */ public void saveAccount() { AccountDAO accountDAO = (AccountDAO) BeanFactory.getBean("accountDAO"); // 通過 Factory 解耦 if (accountDAO != null) { accountDAO.saveAccount(); } } }
-
Client.java
package cn.parzulpan.ui; import cn.parzulpan.factory.BeanFactory; import cn.parzulpan.service.AccountService; import cn.parzulpan.service.AccountServiceImpl; /** * @Author : parzulpan * @Time : 2020-12 * @Desc : 模擬一個表現層,用於呼叫業務層,實際開發中應該是一個 Servlet 等 */ public class Client { public static void main(String[] args) { // AccountServiceImpl accountService = new AccountServiceImpl(); // 這裡發生了耦合 AccountService accountService = (AccountService) BeanFactory.getBean("accountService"); // 通過 Factory 解耦 if (accountService != null) { accountService.saveAccount(); } // 通過 Factory 解耦存在的問題 for (int i = 0; i < 5; ++i) { System.out.println(BeanFactory.getBean("accountService")); // 物件被建立多次 } } }
IOC 解耦
-
bean.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 把物件的建立交給 Spring 來管理--> <bean id="accountService" class="cn.parzulpan.service.AccountServiceImpl"/> <bean id="accountDAO" class="cn.parzulpan.dao.AccountDAOImpl"/> </beans>
-
ClientIOC.java
package cn.parzulpan.ui; import cn.parzulpan.dao.AccountDAO; import cn.parzulpan.service.AccountService; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * @Author : parzulpan * @Time : 2020-12 * @Desc : 使用 IOC */ public class ClientIOC { public static void main(String[] args) { // 使用 ApplicationContext 介面,獲取 Spring 核心容器 ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml"); // 根據 id 獲取 Bean 物件 AccountService as = ac.getBean("accountService", AccountService.class); System.out.println(as); AccountDAO ad = ac.getBean("accountDAO", AccountDAO.class); System.out.println(ad); } }
IOC
IOC(Inverse Of Control,反轉控制),把建立物件的權利交給 Spring 框架,它包括 DI(Dependency Injection,依賴注入)和 DL(Dependency Lookup,依賴查詢)。
簡單的說,IOC 是一種以被動接收的方式獲取物件的思想,它主要是為了降低程式的耦合。
bean 標籤
- 作用:用於配置物件讓 Spring 來建立。預設情況下它呼叫的是類中的無參建構函式,如果沒有無參建構函式則不能建立成功。
- 屬性:
id
:給物件在容器中提供一個唯一標識,用於獲取物件。class
:指定類的全限定類名,用於反射建立物件,預設情況下呼叫無參建構函式。scope
:指定物件的作用範圍singleton
預設值,單例的prototype
多例的request
WEB 專案中,Spring 建立一個 Bean 的物件,將物件存入到 request 域中session
WEB 專案中,Spring 建立一個 Bean 的物件,將物件存入到 session 域中global session
WEB 專案中,應用在叢集環境,如果沒有叢集環境那麼 globalSession 相當於 session
init-method
:指定類中的初始化方法名稱。destroy-method
:指定類中銷燬方法名稱。
bean 的三種建立方式
第一種方式:使用預設無參建構函式。它會根據預設無參建構函式來建立類物件。如果 bean 中沒有預設無參建構函式,將會建立失敗。
<bean id="accountServiceIOC" class="cn.parzulpan.service.AccountServiceImplIOC"/>
<bean id="accountDAOIOC" class="cn.parzulpan.dao.AccountDAOImplIOC"/>
第二種方式:使用例項工廠的方法建立物件。先把工廠的建立交給 Spring 來管理,然後在使用工廠的 bean 來呼叫裡面的方法。
- factory-bean 屬性:用於指定例項工廠 bean 的 id
- factory-method 屬性:用於指定例項工廠中建立物件的方法
package cn.parzulpan.factory;
import cn.parzulpan.service.AccountService;
import cn.parzulpan.service.AccountServiceImplIOC;
/**
* @Author : parzulpan
* @Time : 2020-12
* @Desc : Spring 管理例項工廠。模擬一個工廠類,該類可能存在於 jar 包中,無法通過修改原始碼來提供預設建構函式
*/
public class InstanceFactory {
public AccountService getAccountService() {
return new AccountServiceImplIOC();
}
}
<bean id="instanceFactory" class="cn.parzulpan.factory.InstanceFactory"/>
<bean id="accountServiceIOC" factory-bean="instanceFactory" factory-method="getAccountService"/>
<bean id="accountDAOIOC" class="cn.parzulpan.dao.AccountDAOImplIOC"/>
第三種方式:使用靜態工廠的方法建立物件。使用某個類中的靜態方法建立物件,並存入 Spring 核心容器。
- id 屬性:指定 bean 的 id,用於從容器中獲取
- class 屬性:指定靜態工廠的全限定類名
- factory-method 屬性:指定生產物件的靜態方法
package cn.parzulpan.factory;
import cn.parzulpan.service.AccountService;
import cn.parzulpan.service.AccountServiceImplIOC;
/**
* @Author : parzulpan
* @Time : 2020-12
* @Desc : Spring 管理靜態工廠。模擬一個工廠類,該類可能存在於 jar 包中,無法通過修改原始碼來提供預設建構函式
*/
public class StaticFactory {
public static AccountService getAccountService() {
return new AccountServiceImplIOC();
}
}
<bean id="accountServiceIOC" class="cn.parzulpan.factory.StaticFactory" factory-method="getAccountService"/>
<bean id="accountDAOIOC" class="cn.parzulpan.dao.AccountDAOImplIOC"/>
測試 ClientIOC.java:
public class ClientIOC {
public static void main(String[] args) {
// 使用 ApplicationContext 介面,獲取 Spring 核心容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
System.out.println("------");
//
AccountService asi = ac.getBean("accountServiceIOC", AccountService.class);
System.out.println(asi);
AccountDAO adi = ac.getBean("accountDAOIOC", AccountDAO.class);
System.out.println(adi);
adi.saveAccount();
}
}
bean 的作用範圍和生命週期
對於單例物件:scope="singleton"
一個應用只有一個物件的例項,它的作用範圍就是整個引用。
生命週期:
- 物件出生:當應用載入,建立容器時,物件就被建立了。
- 物件活著:只要容器在,物件一直活著。
- 物件死亡:當應用解除安裝,銷燬容器時,物件就被銷燬了。
<!-- bean 的作用範圍和生命週期 -->
<bean id="accountServiceIOC" class="cn.parzulpan.service.AccountServiceImplIOC" scope="singleton"
init-method="init" destroy-method="destroy"/>
<bean id="accountDAOIOC" class="cn.parzulpan.dao.AccountDAOImplIOC" scope="singleton"
init-method="init" destroy-method="destroy"/>
對於多例物件:scope="prototype"
每次訪問物件時,都會重新建立物件例項。
生命週期:
- 物件出生:當使用物件時,建立新的物件例項。
- 物件活著:只要物件在使用中,就一直活著。
- 物件死亡:當物件長時間不用時,被 java 的垃圾回收器回收了。
<bean id="accountServiceIOC" class="cn.parzulpan.service.AccountServiceImplIOC" scope="prototype"
init-method="init" destroy-method="destroy"/>
<bean id="accountDAOIOC" class="cn.parzulpan.dao.AccountDAOImplIOC" scope="prototype"
init-method="init" destroy-method="destroy"/>
測試:
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
AccountService asi = ac.getBean("accountServiceIOC", AccountService.class);
System.out.println(asi);
AccountDAO adi = ac.getBean("accountDAOIOC", AccountDAO.class);
System.out.println(adi);
adi.saveAccount();
ac.close(); // 手動關閉容器
DI
DI(Dependency Injection,依賴注入),它是 Spring IOC 的具體實現。因為 IOC 作用是降低耦合,那麼依賴關係的維護都交給了 Spring,依賴關係的維護就稱之為依賴注入。
能依賴注入的資料,有三類:
- 基本資料型別和 String
- 其他 Bean 型別,在配置檔案中或者其他註解配置過的 Bean
- 集合型別
依賴注入的方法,有三種:
- 使用建構函式注入
- 使用 set 方法注入
- 使用註解注入
使用建構函式注入
類中需要提供一個對應引數列表的建構函式。
屬性:
- index 指定引數在建構函式引數列表的索引位置
- type 指定引數在建構函式中的資料型別
- name 指定引數在建構函式中的名稱
- value 它能賦的值是基本資料型別和 String 型別
- ref 它能賦的值是其他 bean 型別,也就是說,必須得是在配置檔案中配置過的 bean
- 前三個都是找給誰賦值,後兩個指的是賦什麼值的
<!-- 建構函式注入
類中需要提供一個對應引數列表的建構函式
屬性:
index 指定引數在建構函式引數列表的索引位置
type 指定引數在建構函式中的資料型別
name 指定引數在建構函式中的名稱
value 它能賦的值是基本資料型別和 String 型別
ref 它能賦的值是其他 bean 型別,也就是說,必須得是在配置檔案中配置過的 bean
前三個都是找給誰賦值,後兩個指的是賦什麼值的
-->
<bean id="accountServiceDI" class="cn.parzulpan.service.AccountServiceImplDI">
<constructor-arg name="name" value="parzulpan"/>
<constructor-arg name="age" value="100"/>
<constructor-arg name="birthday" ref="now"/>
</bean>
<bean id="now" class="java.util.Date"/>
ClientDI.java
package cn.parzulpan.ui;
import cn.parzulpan.service.AccountService;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @Author : parzulpan
* @Time : 2020-12
* @Desc :
*/
public class ClientDI {
public static void main(String[] args) {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
AccountService asi = ac.getBean("accountServiceDI", AccountService.class);
System.out.println(asi);
asi.saveAccount(); // call saveAccount() parzulpan 100 Sun Dec 20 19:27:49 CST 2020
}
}
這種注入方式的優點:在獲取 bean 物件時,注入資料是必須的操作,否則無法建立成功。
缺點:改變了 bean 物件的例項化方式,在建立物件時,如果用不到這些屬性,也必須提供。
使用 set 方法注入
類中需要提供屬性的 set 方法。
屬性:
- name:找的是類中 set 方法後面的部分
- ref:給屬性賦值是其他 bean 型別的
- value:給屬性賦值是基本資料型別和 string 型別的
<!-- set 方法 注入
類中需要提供屬性的 set 方法
屬性:
name:找的是類中 set 方法後面的部分
ref:給屬性賦值是其他 bean 型別的
value:給屬性賦值是基本資料型別和 string 型別的
-->
<bean id="accountServiceDI2" class="cn.parzulpan.service.AccountServiceImplDI2">
<property name="name" value="庫裡"/>
<property name="age" value="30"/>
<property name="birthday" ref="nowSet"/>
</bean>
<bean id="nowSet" class="java.util.Date"/>
ClientDI.java
AccountService asi2 = ac.getBean("accountServiceDI2", AccountService.class);
System.out.println(asi2);
asi2.saveAccount(); // call saveAccount() 庫裡 30 Sun Dec 20 20:11:01 CST 2020
這種注入方式的優點:建立物件時沒有明確的限制,可以直接使用預設建構函式。
缺點:如果某個成員必須有值,則 set 方法無法保證一定執行。
但是,set 方式是更常用的方式。
注入集合屬性
注入集合屬性,在注入集合資料時,只要結構相同,標籤可以互換。
List 結構的:array, list, set
Map 結構的:map, entry, props, prop
<!-- 注入集合屬性
在注入集合資料時,只要結構相同,標籤可以互換
List 結構的:array, list, set
Map 結構的:map, entry, props, prop
-->
<bean id="accountServiceDI3" class="cn.parzulpan.service.AccountServiceImplDI3">
<property name="myStr">
<set>
<value>AAA</value>
<value>BBB</value>
<value>CCC</value>
</set>
</property>
<property name="myList">
<list>
<value>AAA</value>
<value>BBB</value>
<value>CCC</value>
</list>
</property>
<property name="mySet">
<set>
<value>AAA</value>
<value>BBB</value>
<value>CCC</value>
</set>
</property>
<property name="myMap">
<map>
<entry key="testA" value="aaa"/>
<entry key="testB" value="bbb"/>
</map>
</property>
<property name="myProps">
<props>
<prop key="testA">aaa</prop>
<prop key="testB">bbb</prop>
</props>
</property>
</bean>
練習和總結
ApplicationContext 介面的三個實現類?
ClassPathXmlApplicationContext
它是從類的根路徑下載入配置檔案,推薦使用這種FileSystemXmlApplicationContext
它是從磁碟路徑上載入配置檔案,配置檔案可以在磁碟的任意位置,不推薦使用這種AnnotationConfigApplicationContext
使用註解配置容器物件時,需要使用此類來建立 Spring 核心容器,它用來讀取註解
BeanFactory 和 ApplicationContext 的區別?
BeanFactory
是 Spring 核心容器中的頂層介面ApplicationContext
是 BeanFactory 的子介面- 它們兩者建立物件的時間點不一樣:
ApplicationContext
立即載入,只要讀取了配置檔案,預設情況下就會建立物件,適用於單例物件,推薦使用這種BeanFactory
延遲載入,什麼時候使用什麼時候建立物件,適用於多例物件