Spring學習筆記(1) IOC
Spring 工廠
什麼是Spring
Spring是一個輕量級的JavaEE解決方案,它整合了眾多優秀的設計模式。
什麼是輕量級?
-
對於執行環境沒有額外要求的;
開源: tomcat、resion、jetty
收費:weblogic、websphere
-
程式碼移植性高:不需要實現額外介面。
工廠設計模式
- 好處: 解決耦合
- 耦合: 指定是程式碼間的強關聯關係,⼀方的改變會影響到另⼀方;
- 問題: 把介面的實現類硬編碼在了程式中,不利於程式碼維護;
UserService userService = new UserServiceImpl();
簡單工廠的設計
import java.io.IOException; import java.io.InputStream; import java.util.Properties; public class BeanFactory { private static Properties env = new Properties(); static{ try { //第一步 獲得IO輸入流 InputStream inputStream = BeanFactory.class.getResourceAsStream("/applicationContext.properties"); //第二步 檔案內容 封裝 Properties集合中 key = userService value = com.baizhixx.UserServiceImpl env.load(inputStream); inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } /* 物件的建立方式: 1. 直接呼叫構造方法 建立物件 UserService userService = new UserServiceImpl(); 2. 通過反射的形式 建立物件 解耦合 Class clazz = Class.forName("com.2hu0.basic.UserServiceImpl"); UserService userService = (UserService)clazz.newInstance(); */ public static UserService getUserService() { UserService userService = null; try { Class clazz = Class.forName(env.getProperty("userService")); userService = (UserService) clazz.newInstance(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return userService; } public static UserDAO getUserDAO(){ UserDAO userDAO = null; try { Class clazz = Class.forName(env.getProperty("userDAO")); userDAO = (UserDAO) clazz.newInstance(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return userDAO; } }
配置檔案
# Properties 集合 儲存 Properties檔案的內容
# 特殊Map key=String value=String
# Properties [userService = com.2hu0.xxx.UserServiceImpl]
# Properties.getProperty("userService")
userService = com.2hu0.basic.UserServiceImpl
userDAO = com.2hu0.basic.UserDAOImpl
通用工廠的設計
簡單工廠存在程式碼冗餘,需要進一步簡化
通用工廠程式碼
import java.io.IOException; import java.io.InputStream; import java.util.Properties; public class BeanFactory { private static Properties env = new Properties(); static{ try { //第一步 獲得IO輸入流 InputStream inputStream = BeanFactory.class.getResourceAsStream("/applicationContext.properties"); //第二步 檔案內容 封裝 Properties集合中 key = userService value = com.baizhixx.UserServiceImpl env.load(inputStream); inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } /* key 小配置檔案中的key [userDAO,userService] */ public static Object getBean(String key){ Object ret = null; try { Class clazz = Class.forName(env.getProperty(key)); ret = clazz.newInstance(); } catch (Exception e) { e.printStackTrace(); } return ret; } }
同時我們需要更新配置檔案
# Properties 集合 儲存 Properties檔案的內容
# 特殊Map key=String value=String
# Properties [userService = com.baizhiedu.xxx.UserServiceImpl]
# Properties.getProperty("userService")
userService = com.baizhiedu.basic.UserServiceImpl
userDAO = com.baizhiedu.basic.UserDAOImpl
通用工廠的使用方式
1. 定義型別(類) 2. 通過配置檔案的配置告知工廠 `applicationContext.properties` 中 `key = value`; 3. 通過工廠獲得類的物件 `Object ret = BeanFactory.getBean("key");`
總結:
Spring本質:工廠ApplicationContext(applicationContext.xml)
第一個Spring程式
環境搭建
配置Spring的jar包:
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.4.RELEASE</version>
</dependency>
Spring的配置檔案:
- 配置檔案的放置位置:任意位置,沒有硬性要求
- 命名建議為:applicationContext.xml
Spring的核心API
ApplicationContext
-
作用:Spring 提供的
ApplicationContext
這個工廠,用於物件的建立;
好處:解耦合 -
ApplicationContext是介面型別
介面:遮蔽實現的差異
非Web環境:
ClassPathXmlApplicationContext
Web環境:
XmlWebApplicationContext
-
重量級資源:
ApplicationContext 工廠的物件佔用大量的記憶體
一個應用只會建立一個工廠物件。
ApplicationContext 工廠:一定是執行緒安全的
樣例
1.建立型別:Person.java
public class Person{}
2.配置檔案的配置
<?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 http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="person" class="com.yusael.basic.Person"/>
</beans>
3.通過工廠類獲得物件
/**
* 用於測試Spring的第一個程式
*/
@Test
public void test() {
// 1、獲取spring的工廠
ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");
// 2、通過工廠類獲得物件
Person person = (Person)ctx.getBean("person");
System.out.println(person);
}
細節:
Spring工廠建立的物件,叫做bean或者元件(componet)
Spring工廠常見的方法
getbean
:傳入id值 和 類名 獲取物件,不需要強制型別轉換。
// 通過這種方式獲得物件,就不需要強制型別轉換
Person person = ctx.getBean("person", Person.class);
System.out.println("person = " + person);
getbean
只指定類名,Spring的配置檔案中只能有一個bean是這個型別
// 使用這種方式的話, 當前Spring的配置檔案中 只能有一個bean class是Person型別
Person person = ctx.getBean(Person.class);
System.out.println("person = " + person);
getBeanDefinitionNames
:獲取Spring配置檔案中所有的bean標籤的id值
// 獲取的是Spring工廠配置檔案中所有bean標籤的id值 person person1
String[] beanDefinitionNames = ctx.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
System.out.println("beanDefinitionName = " + beanDefinitionName);
}
getBeanNamesForType:根據型別獲得Spring配置檔案中對應的id值
// 根據型別獲得Spring配置檔案中對應的id值
String[] beanNamesForType = ctx.getBeanNamesForType(Person.class);
for (String id : beanNamesForType) {
System.out.println("id = " + id);
}
containsBeanDefinition
:用於判斷是否存在指定id值的bean,不能判斷name值
// 用於判斷是否存在指定id值的bean,不能判斷name值
if (ctx.containsBeanDefinition("person")) {
System.out.println(true);
} else {
System.out.println(false);
}
配置檔案中的細節
如果bean只配置class屬性:
<bean class="com.yusael.basic.Person"></bean>
-
會自動生成一個id
-
應用場景:
如果這個bean只需要使用一次,那麼就可以省略id值
如果這個bean會使用多次,或者被其他bean引用則需要設定id值
name屬性:
- 作用:用於在 Spring 的配置檔案中,為 bean 物件定義別名(小名)
- name 與 id 的相同點:
-
ctx.getBean("id")
或ctx.getBean("name")
都可以建立物件; -
<bean id="person" class="Person"/>
與<bean name="person" class="Person"/>
等效;
-
- name 和 id 的區別
- 別名可以定義多個,但是 id 屬性只能有⼀個值;
- XML 的 id 屬性的值,命名要求:必須以字母開頭,可以包含 字母、數字、下劃線、連字元;不能以特殊字元開頭
/person
; - XML 的 name 屬性的值,命名沒有要求,
/person
可以。
但其實 XML 發展到了今天:ID屬性的限制已經不存在,/person
也可以。
思考:
問題:未來在開發過程中,是不是所有的物件,都會交給 Spring 工廠來建立呢?
回答:理論上是的,但是有特例 :實體物件(entity) 是不會交給Spring建立,它由持久層框架進行建立。
Spring整合日誌框架
Spring 與日誌框架進行整合,日誌框架就可以在控制檯中,輸出Spring框架執行過程中的⼀
些重要的資訊。
好處:便於瞭解Spring框架的執行過程,利於程式的除錯。
預設日誌框架
Spring 1.x、2.x、3.x 早期都是基於commonslogging.jar
Spring 5.x 預設整合的日誌框架 logback、log4j2
匯入log4j依賴
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
建立log4j.properties配置檔案
# resources資料夾根目錄下
### 配置根
log4j.rootLogger = debug,console
### 日誌輸出到控制檯顯示
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.Target=System.out
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
注入(Injection)
什麼是注入
注入:通過 Spring 工廠及配置檔案,為所建立物件的成員變數賦值。
為什麼要注入?
- 通過編碼的方式,為成員變數進行賦值,存在耦合。
- 注入的好處:解耦合。
public void test4() {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");
Person person = (Person) ctx.getBean("person");
// 通過程式碼為變數賦值, 存在耦合, 如果我們以後想修改變數的值, 需要修改程式碼, 重新編譯
person.setId(1);
person.setName("zhenyu");
System.out.println(person);
}
具體步驟
- 類的成員變數提供 set get 方法
- 配置 spring 的配置檔案
<bean id="person" name="p" class="com.yusael.basic.Person">
<property name="id">
<value>10</value>
</property>
<property name="name">
<value>yusael</value>
</property>
</bean>
Spring 底層通過呼叫物件屬性對應的 set 方法,完成成員變數的賦值,這種方式也稱為 Set注入。
Set注入詳解
Set注入的變數型別:
-
JDK內建型別
8種基本型別 + String、陣列型別、set集合、list集合、Map集合、Properties集合。 - 使用者自定義型別
針對於不同型別的成員變數,在<property
標籤中,需要巢狀其他標籤:
JDK內建型別
String+8種基本型別
<property name="id">
<value>10</value>
</property>
<property name="name">
<value>yusael</value>
</property>
陣列
<property name="emails">
<list>
<value>[email protected]</value>
<value>[email protected]</value>
<value>[email protected]</value>
</list>
</property>
Set集合
<property name="tels">
<set>
<value>138xxxxxxxxxx</value>
<value>139xxxxxxxxxx</value>
<value>138xxxxxxxxxx</value><!--set會自動去重-->
</set>
</property>
List集合
<property name="addresses">
<list>
<value>China</value>
<value>Earth</value>
<value>hell</value>
</list>
</property>
Map集合
<property name="qqs">
<map>
<entry>
<key><value>hello</value></key>
<value>12312312312</value>
</entry>
<entry>
<key><value>world</value></key>
<value>21314214214</value>
</entry>
</map>
</property>
Properties
<property name="p">
<props>
<prop key="key1">value1</prop>
<prop key="key2">value2</prop>
<prop key="key3">value3</prop>
</props>
</property>
複雜JDK型別(Date、)
需要程式設計師自定義型別轉換器
使用者自定義型別
第一種方式
- 為成員變數提供 set get 方法
- 配置檔案中進行注入(賦值)
<bean id="userService" class="com.yusael.service.UserServiceImpl">
<property name="userDAO">
<bean class="com.yusael.dao.UserDAOImpl"/>
</property>
</bean>
第二種方式
第⼀種賦值方式存在的問題:
- 配置檔案程式碼冗餘;
- 被注入的物件 (UserDAO)多次建立,浪費(JVM)記憶體資源。
[開發步驟]:
- 為成員變數提供 set get 方法;
- 配置檔案中進行配置;
<bean id="userDAO" class="com.yusael.dao.UserDAOImpl"></bean>
<bean id="userService" class="com.yusael.service.UserServiceImpl">
<property name="userDAO">
<ref bean="userDAO"/>
</property>
</bean>
Set注入簡化
基於屬性的簡化
JDK型別注入
<property name="id">
<value>10</value>
</property>
JDK型別注入簡化:value
屬性只能簡化 8種基本型別 + String 注入標籤;
<property name="id" value="10"/>
使用者自定義型別注入:
<bean id="userDAO" class="com.yusael.dao.UserDAOImpl"></bean>
<bean id="userService" class="com.yusael.service.UserServiceImpl">
<property name="userDAO">
<ref bean="userDAO"/>
</property>
</bean>
使用者自定義型別注入簡化:
<bean id="userDAO" class="com.yusael.dao.UserDAOImpl"></bean>
<bean id="userService" class="com.yusael.service.UserServiceImpl">
<property name="userDAO" ref="userDAO"/>
</bean>
基於p名稱空間的簡化
JDK型別注入
<bean id="person" name="p" class="com.yusael.basic.Person">
<property name="id">
<value>10</value>
</property>
<property name="name">
<value>yusael</value>
</property>
</bean>
JDK型別注入-基於p名稱空間的簡化
<bean id="person" name="p" class="com.yusael.basic.Person"
p:name="yusael" p:id="10"/>
使用者自定義型別注入
<bean id="userDAO" class="com.yusael.dao.UserDAOImpl"></bean>
<bean id="userService" class="com.yusael.service.UserServiceImpl">
<property name="userDAO">
<ref bean="userDAO"/>
</property>
</bean>
使用者自定義型別注入 - 基於p名稱空間的簡化
<bean id="userDAO" class="com.yusael.dao.UserDAOImpl"></bean>
<bean id="userService" class="com.yusael.service.UserServiceImpl"
p:userDAO-ref="userDAO"/>
構造注入
- 注入:通過 Spring 的配置檔案,為成員變數賦值;
- Set注入:Spring 呼叫 Set 方法 通過 配置檔案 為成員變數賦值;
- 構造注入:Spring 呼叫 構造方法 通過 配置檔案 為成員變數賦值
具體實現
-
提供有參構造方法
public class Customer { private String name; private int age; public Customer(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Customer{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
spring配置檔案
<bean id="customer" class="com.yusael.constructor.Customer">
<constructor-arg>
<value>zhenyu</value>
</constructor-arg>
<constructor-arg>
<value>21</value>
</constructor-arg>
</bean>
構造方法過載
引數個數不同
引數個數不同時,通過控制 <constructor-arg>
標籤的數量進行區分;
如果只有一個引數的話,只需要一對 <constructor-arg>
標籤:
<bean id="customer" class="com.yusael.constructor.Customer">
<constructor-arg>
<value>zhenyu</value>
</constructor-arg>
</bean>
引數相同
<bean id="customer" class="com.yusael.constructor.Customer">
<constructor-arg type="int">
<value>20</value>
</constructor-arg>
</bean>
總結
未來的實戰中,應用 set注入 還是 構造注入?
答:set 注入更多。
- 構造注入麻煩(過載)
- Spring 框架底層大量應用了 set注入。
反轉控制與依賴注入
反轉控制(IOC Inverse of Control)
- 控制:對於成員變數賦值的控制權;
- 反轉控制:把對於成員變數賦值的控制權,從程式碼中轉移(反轉)到 Spring 工廠和配置檔案中完成。
- 好處:解耦合;
- 底層實現:工廠設計模式;
依賴注入
- 注入:通過 Spring 的工廠及配置檔案,為物件(bean,元件)的成員變數賦值;
- 依賴注入:當⼀個類需要另⼀個類時,就意味著依賴,⼀旦出現依賴,就可以把另⼀個類作為本類的成員變數,最終通過 Spring 配置檔案進行注入(賦值)。
- 好處:解耦合;
建立複雜物件
複雜物件就是不能通過new構造方法建立的物件
例如Connection
SqlSessionFactory
FactoryBean介面
- 實現 FactoryBean 介面:實現 getObject,getObjectType,isSingleton 方法;
-
getObject()
:用於書寫建立複雜物件時的程式碼。 -
getObjectType()
:返回建立的複雜物件的型別。 -
isSingleton
:用於決定是否單例。
public class ConnectionFactoryBean implements FactoryBean<Connection> {
// 用於書寫建立複雜物件時的程式碼
@Override
public Connection getObject() throws Exception {
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/spring", "root", "1234");
return conn;
}
// 返回建立的複雜物件的型別
@Override
public Class<Connection> getObjectType() {
return Connection.class;
}
// 是否單例
@Override
public boolean isSingleton() {
return false; // 每一次都建立新的複雜物件
// return true; // 只建立一次這種型別的複雜物件
}
}
Spring配置檔案的配置
如果class中指定的型別是FactoryBean介面的實現類,那麼通過id值獲取的就是這個類所建立的複雜物件
比如下面 class 指定的是 ConnectionFactoryBean
,獲得的是 Connection
物件。
<!--class 指定了 ConnectionFactoryBean, 獲得的是該類建立的複雜物件 Connection -->
<bean id="conn" class="com.2hu0.factorybean.ConnectionFactoryBean"/>
FactoryBean細節
如果你就想要獲得FactoryBean型別的物件,加個 &
,ctx.getBean("&conn")
ConnectionFactoryBean cfb = (ConnectionFactoryBean) ctx.getBean("&conn");
isSingleton
方法返回 true 只會建立⼀個複雜物件,返回 false 每⼀次都會建立新的物件;
需要根據這個物件的特點 ,決定是返回 true(SqlSessionFactory
) 還是 false(Connection
);
依賴注入(DI):把 ConnectionFactoryBean
中依賴的 4 個字串資訊 ,通過配置檔案進行注入
@Getter@Setter // 提供 get set 方法
public class ConnectionFactoryBean implements FactoryBean<Connection> {
// 將依賴的字串資訊變為成員變數, 利用配置檔案進行注入。
private String driverClassName;
private String url;
private String username;
private String password;
@Override
public Connection getObject() throws Exception {
Class.forName(driverClassName);
Connection conn = DriverManager.getConnection(url, username, password);
return conn;
}
@Override
public Class<Connection> getObjectType() {
return Connection.class;
}
@Override
public boolean isSingleton() {
return false;
}
}
!--體會依賴注入, 好處: 解耦合, 今後要修改連線資料庫的資訊只需要修改配置檔案, 無需改動程式碼-->
<bean id="conn" class="com.yusael.factorybean.ConnectionFactoryBean">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring?useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="1234"/>
</bean>
Factory實現原理簡易版
原理:介面回撥。
問題:
-
為什麼 Spring 規定 FactoryBean 介面實現 getObject()?
2 .為什麼 ctx.getBean("conn") 獲得的是複雜物件 Connection ⽽非 ConnectionFactoryBean?
Spring 內部執行流程: -
配置檔案中通過 id conn 獲得 ConnectionFactoryBean 類的物件 ,進而通過 instanceof 判斷出是 FactoryBean 介面的實現類;
-
Spring 按照規定 getObject() —> Connection;
返回 Connection;
Factory總結
Spring中用於建立複雜物件的一種方式,也是Spring原生提供的,後續Spring整合其他框架的時候會大量的應用FactoryBean
例項工廠
- 避免Spring框架的侵入
- 整合遺留系統
- ConnectionFactory
public class ConnectionFactory {
public Connection getConnection() {
Connection conn = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/spring?useSSL=false", "root", "1234");
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
return conn;
}
}
- 配置檔案
<!--例項工廠-->
<!-- 先創建出工廠例項 -->
<bean id="connFactory" class="com.yusael.factorybean.ConnectionFactory"/>
<!-- 通過工廠例項裡的方法建立複雜物件 -->
<bean id="conn" factory-bean="connFactory" factory-method="getConnection"/>
靜態工廠
- StaticConnectionFactory類
public class StaticFactoryBean {
// 靜態方法
public static Connection getConnection() {
Connection conn = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/spring?useSSL=false", "root", "1234");
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
return conn;
}
}
- 配置檔案
<!--靜態工廠-->
<bean id="conn" class="com.yusael.factorybean.StaticFactoryBean" factory-method="getConnection"/>
控制 Spring 工廠建立物件的次數
控制簡單物件的建立次數 - scope
配置檔案中進行配置:singleton
:每一個 IoC 容器只會建立⼀次簡單物件,預設值;prototype
:每⼀次都會建立新的物件;
<!--控制簡單物件建立次數-->
<bean id="scope" scope="singleton" class="com.yusael.scope.Scope"/>
控制複雜物件的建立次數
如果是 FactoryBean
方式建立的複雜物件:
public class xxxFactoryBean implements FactoryBean {
public boolean isSingleton() {
return true; // 只會建立⼀次
// return false; // 每⼀次都會建立新的
}
// 省略其餘實現方法......
}
如果是例項工廠或者靜態工廠,沒有 isSingleton
⽅法,與簡單物件一樣通過 scope
控制。
為什麼要控制物件的建立次數?
好處:節省不必要的記憶體浪費。
什麼樣的物件只建立⼀次?(service DAO)
- 重量級的、可以被共用的、執行緒安全的
什麼樣的物件每⼀次都要建立新的?
- 不能被共用的,執行緒不安全的…(session Connecton)