1. 程式人生 > 其它 >Spring學習筆記(1) IOC

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>
第二種方式

第⼀種賦值方式存在的問題:

  1. 配置檔案程式碼冗餘;
  2. 被注入的物件 (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實現原理簡易版

原理:介面回撥。

問題:

  1. 為什麼 Spring 規定 FactoryBean 介面實現 getObject()?
    2 .為什麼 ctx.getBean("conn") 獲得的是複雜物件 Connection ⽽非 ConnectionFactoryBean?
    Spring 內部執行流程:

  2. 配置檔案中通過 id conn 獲得 ConnectionFactoryBean 類的物件 ,進而通過 instanceof 判斷出是 FactoryBean 介面的實現類;

  3. Spring 按照規定 getObject() —> Connection;
    返回 Connection;

Factory總結

Spring中用於建立複雜物件的一種方式,也是Spring原生提供的,後續Spring整合其他框架的時候會大量的應用FactoryBean

例項工廠

  1. 避免Spring框架的侵入
  2. 整合遺留系統
  • 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)