1. 程式人生 > 實用技巧 >【Spring】Spring 入門

【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 延遲載入,什麼時候使用什麼時候建立物件,適用於多例物件