Spring5框架入門學習之IOC依賴注入(二)

1. 注入方式

IOC的核心就是DI,Spring提供簡單的機制來管理組件的依賴項,並在組件整個生命期都能進行管理,Spring主要提供了兩種注入方式。

1.1 基於構造函數注入

當在組件的構造函數中提供依賴項時,就發生了構造函數注入。這與調用具有特有參數的靜態工廠方法創建Bean的實例一致(可參考上一篇文章內容)。代碼如下所示:
在這裏插入圖片描述

1.2 setter方法注入

IOC 容器可通過JavaBean的setter方法注入組件的依賴,下面演示使用setter方法注入依賴。
在這裏插入圖片描述

1.3 案例實戰

下面演示一個典型的構造器注入案例。

  1. 定義消息接口
/**
 * 消息服務接口
 */
public interface MessageService {

    String getMessage();
}
  1. 定義消息實現類
/**
 * 消息服務實現
 */
public class MessageServiceImpl implements MessageService {

    private String name;

    private int age;

    public MessageServiceImpl(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String getMessage() {
        return "this is a message comes from a "+age+" year's boy whose name is "+name;
    }
}
  1. 定義消息發送器,用來打印消息
public class MessageSender {

    private MessageService messageService;

    /**
     * 構造器注入
     * @param messageService messageService
     */
    public MessageSender(MessageService messageService) {
        this.messageService = messageService;
    }

    public void printMessage() {
        System.out.println(messageService.getMessage());
    }
}
  1. 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 http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="messageService" class="com.codegeek.day2.MessageServiceImpl">
        <constructor-arg name="name" value="張三"/>
        <constructor-arg name="age" value="23"/>
    </bean>

    <!--構造器注入-->
    <bean id="messageSender" class="com.codegeek.day2.MessageSender">
        <constructor-arg name="messageService" ref="messageService"/>
    </bean>
</beans>
  1. 應用測試類
public class SpringTest {
    
    public static void main(String[] args) {
        ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("day2/applicationContext.xml");
        MessageSender messageSender = classPathXmlApplicationContext.getBean("messageSender", MessageSender.class);
        messageSender.printMessage();
    }
}

運行結果如下:
在這裏插入圖片描述

2. 依賴注入配置詳解

2.1 直接配置

下面以開發中常常遇見的數據庫數據源配置爲例,如下圖所示一個常見的數據源配置
在這裏插入圖片描述
其中property屬性就是類的屬性名稱,而value就是爲其進行賦值,可以看到PropertyPlaceholderConfigurer 其繼承樹圖父類 PropertiesLoaderSupport
在這裏插入圖片描述
在這個類中有PropertiesLoaderSupport 中 setLocation方法。
在這裏插入圖片描述
如果Bean之間有協作方式則可以引用Bean,ref 元素可以是constructor-arg標籤也可以是Property。
在這裏插入圖片描述

2.2 集合配置

Java集合類型中如List、Set、Map、Properties 可以使用提供的<list>、<set>、<map>、<props> 進行屬性參數配置。下面在MessageSender類添加如下四種類型數據進行配置如下:
在這裏插入圖片描述

  • list

XML中元數據配置信息如下所示:

    <bean id="messageSender1" class="com.codegeek.day2.MessageSender">
      <property name="list">
         <list>
            <value>我是messageSender的list數據</value>
         </list>
      </property>
    </bean>

測試類代碼如下:

    @Test
    public void test() {
        MessageSender messageSender = applicationContext.getBean("messageSender1", MessageSender.class);
        System.out.println(messageSender.getList());
    }

運行結果:
在這裏插入圖片描述

  • set

XML中元數據配置信息如下所示:

    <bean id="messageSender2" class="com.codegeek.day2.MessageSender">
        <property name="set">
            <set>
                <value>我是messageSender的set數據1</value>
                <value>我是messageSender的set數據2</value>
            </set>
        </property>
    </bean>

測試類代碼如下:

    @Test
    public void testSet() {
        MessageSender messageSender = applicationContext.getBean("messageSender2", MessageSender.class);
        System.out.println(messageSender.getSet());
    }

運行結果:
在這裏插入圖片描述

  • map

XML中元數據配置信息如下所示:

    <bean id="student" class="com.codegeek.day1.Student">
        <property name="name" value="張三"/>
        <property name="age" value="23"/>
        <property name="gender" value=""/>
        <property name="address" value="中國"/>
    </bean>

    <!--靜態工廠方法參數賦值-->
    <bean id="iphone11" class="com.codegeek.day1.Phone" factory-method="instance">
        <constructor-arg name="brandName" value="iphone 11"/>
        <constructor-arg name="price" value="5999"/>
        <constructor-arg name="producePlace" value="china"/>
    </bean>

    <bean id="messageSender3" class="com.codegeek.day2.MessageSender">
        <property name="map">
            <map>
                <entry key="小學生" value="小明"/>
                <entry key="china">
                    <map>
                        <entry key="hubei" value="wuhan~加油"/>
                    </map>
                </entry>
                <entry key="student" value-ref="iphone11"/>
            </map>
        </property>
    </bean>

測試類代碼如下:

    @Test
    public void testMap() {
        MessageSender messageSender = applicationContext.getBean("messageSender3", MessageSender.class);
        System.out.println(messageSender.getMap());
    }

運行結果:
在這裏插入圖片描述

  • properties

XML中元數據配置信息如下所示:

    <bean id="messageSender4" class="com.codegeek.day2.MessageSender">
       <property name="properties">
           <props>
               <prop key="driver">com.mysql.jdbc.Driver</prop>
           </props>
       </property>
    </bean>

測試類代碼如下:

    @Test
    public void testProps() {
        MessageSender messageSender = applicationContext.getBean("messageSender4", MessageSender.class);
        System.out.println(messageSender.getProperties());
    }

運行結果:
在這裏插入圖片描述
還有一種特色的集合類型設置即util名稱創建空間集合,如使用util:list 如下所示:

    <bean id="messageSender1" class="com.codegeek.day2.MessageSender">
        <property name="list" ref="list1">
        </property>
    </bean>

    <util:list id="list1">
        <value>123</value>
        <value>456</value>
    </util:list>

再次運行testList方法運行結果如下:
在這裏插入圖片描述
其他類型集合與上面類似,這裏就不在一一演示,其常見簡單配置如下所示:
在這裏插入圖片描述

3. XML命名空間

  • p命名空間

開發中可以使用P命名空間簡化類property標籤賦值,XML配置如下所示:
在這裏插入圖片描述
需要注意的是需要導入P命名空間,利用Idea智能提示可以自動添加P命名空間。
在這裏插入圖片描述

  • c命名空間

與P命名空間類似,開發可以使用C標籤替換constructor-arg,xml配置信息如下所示:
在這裏插入圖片描述

4. 繼承Bean

開發中常常會複用父類的屬性或方法,此時如果希望子類繼承父類的一些配置數據就可以在子Bean中定義parent屬性,該屬性指向父Bean,Xml配置如下所示:
在這裏插入圖片描述
聲明一個抽象父類Person,然後User繼承自Person

@Data
@AllArgsConstructor
@NoArgsConstructor
public abstract class Person {

    private String userName;

    private String email;

    private Integer age;
    
}


@NoArgsConstructor
@Getter
@Setter
public class User extends Person {

    public User(String userName, String email, Integer age) {
       this.setUserName(userName);
       this.setEmail(email);
       this.setAge(age);
    }


    @Override
    public String toString() {
        return new StringJoiner(", ", User.class.getSimpleName() + "[", "]")
                .add("userName='" + this.getUserName() + "'")
                .add("email='" + this.getEmail() + "'")
                .add("age=" + this.getAge())
                .toString();
    }
}

測試類如下:
在這裏插入圖片描述
運行結果可以看出,User2對象繼承了來自Person的屬性。
在這裏插入圖片描述

5. Bean的scope

默認情況下位於IOC容器的Bean都是單例的,我們可以簡單測試證明一下。
在這裏插入圖片描述
Bean的scope屬性可用值如下所示:

範圍 說明
singleton 默認值,IOC容器只會有一個實例
prototype IOC容器會重複創建該類型Bean實例
request web環境一次請求對應一個實例
session web環境下一次會話對應一個實例
application 定義IOC容器一個實例
websocket web環境下一次webSocket對應一個實例

下面在演示一下prototype的實例,其他屬性值設置不常見就不在演示了。
將user2的scope設置爲prototype如下所示:
在這裏插入圖片描述
再次運行測試類代碼發現結果爲false。
在這裏插入圖片描述

6. 延遲加載Bean

一般情況下當IOC容器啓動時就會自動實例化所有的單例對象,如果想取消IOC容器這個行爲可以設置懶加載解決:即當要使用這個類在進行初始化。
在這裏插入圖片描述
如果上面的Bean設置了懶加載那麼啓動容器將不會創建該Bean,新增LazyBean對象:

@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class LazyBean {

    private Boolean flag;

    public void setFlag(Boolean flag) {
        System.out.println("設置了flag:"+flag);
        this.flag = flag;
    }
}

啓動IOC容器如下所示:
在這裏插入圖片描述
需要注意的是當延遲加載Bean作爲非延遲加載對象的引用時,延遲加載的對象會跟隨IOC容器啓動而創建,不再進行懶加載。
在這裏插入圖片描述
新增非延遲加載對象ReferenceBean
在這裏插入圖片描述
測試代碼運行如下:
在這裏插入圖片描述

6. Bean的生命週期

開發過程中可以使用三種方法來完成自定義Bean生命週期,可以使用 Spring提供的接口 InitializingBeanDisposableBean 或者使用JSR-250提供的兩個註解 @PostConstruct@preDestroy ,或者還可以使用Spring的 init-methoddestroy-method

6.1 使用 Spring接口InitializingBeanDisposableBean

Spring的兩個接口 InitializingBeanDisposableBean 實現了容器Bean的創建與銷燬。開發中若實現了這兩個接口方法,那麼容器將會自動調用 afterPropertiesSet()destroy()以管理我們Bean的生命週期,實現如下所示:

@Data
public class Account {

    private String userId;
    private String accountId;
    private BigDecimal accountBalance;

    private static final BigDecimal BIG_DECIMAL = new BigDecimal(10);

    public void addBalance(BigDecimal balance) {
        if (this.accountBalance.compareTo(new BigDecimal(10000)) < -1) {
            this.accountBalance = this.accountBalance.add(balance);
            System.out.println("用戶:" + userId + " 賬號:" + accountId + "入賬" + balance + "元");
        }
    }

    public void reduceBalance(BigDecimal balance) {
        if (this.accountBalance.compareTo(new BigDecimal(10000)) > -1) {
            this.accountBalance = this.accountBalance.subtract(balance);
            System.out.println("用戶:" + userId + " 賬號:" + accountId + "扣款" + balance + "元");
        }
    }
}


public class BeanLife implements InitializingBean, DisposableBean {

    @Autowired
    private Account account;

    /**
     * bean銷燬的時候可執行的操作
     *
     * @throws Exception
     */
    @Override
    public void destroy() throws Exception {
        System.out.println("銷燬的操作 .. 執行扣錢的操作");
        this.execute(account, "reduce");
    }

    /***
     * 初始化對象可執行的操作
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("初始化對象的操作.....執行賬號加錢操作");
        this.execute(account, "add");
    }

    private void execute(Account account, String method) {
        System.out.println("-----執行excute--------"+method);
        Assert.state(account != null, "account must not empty!");
        if ("add".equals(method)) {
            account.addBalance(new BigDecimal(50001));
        } else if ("reduce".equals(method)) {
            account.reduceBalance(new BigDecimal(49999));
        }
    }
}
  • xml配置如下:
    在這裏插入圖片描述
  • 測試類代碼如下:
    在這裏插入圖片描述

6.2 使用 JSR-250 @PostConstruct@preDestroy 註解

使用註解的方式要比直接實現Bean接口方式要好,在一個類中標註 @PostConstruct@preDestroy兩個註解,下面是這種方式的演示:

  • 新增PostConstructDemo
public class PostConstructDemo {

    @PostConstruct
    public void init() {
        System.out.println("---我是postConstruct註解的init方法");
    }

    @PreDestroy
    public void preDestroy() {
        System.out.println("---我是preDestroy註解的destroy方法");
    }
}
  • xml 配置如下:
    在這裏插入圖片描述
  • 運行測試類如下:
    在這裏插入圖片描述

6.3 使用Spring提供的 init-methoddestroy-method 方法

可以在XML元數據配置文件中使用Spring提供的 init-methoddestroy-method 方法完成Bean創建與銷燬的一些工作。

  • 新增 SpringInitDemo
public class SpringInitDemo {

    public void SpringInit() {
        System.out.println("---我是spring調用的init方法");
    }

    public void SpringDestroy() {
        System.out.println("---我是spring調用的destroy方法");
    }
}
  • xml元數據配置:
    在這裏插入圖片描述
    運行結果如下:
    在這裏插入圖片描述

6.4 三種方法執行優先級

將上面三種控制Bean生命週期方法一起如下所示:

  • 改造 BeanLife
public class BeanLife implements InitializingBean, DisposableBean {

    @Autowired
    private Account account;

    /**
     * bean銷燬的時候可執行的操作
     *
     * @throws Exception
     */
    @Override
    public void destroy() throws Exception {
        System.out.println("DisposableBean.destroy() 銷燬的操作 .. 執行扣錢的操作");
        this.execute(account, "reduce");
    }

    /***
     * 初始化對象可執行的操作
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("InitializingBean.afterPropertiesSet() 初始化對象的操作.....執行賬號加錢操作");
        this.execute(account, "add");
    }

    @PostConstruct
    public void init() {
        System.out.println("---我是postConstruct註解的init方法");
    }

    @PreDestroy
    public void preDestroy() {
        System.out.println("---我是preDestroy註解的destroy方法");
    }


    public void SpringInit() {
        System.out.println("---我是spring調用的init方法");
    }


    public void SpringDestroy() {
        System.out.println("---我是spring調用的destroy方法");
    }


    private void execute(Account account, String method) {
       // System.out.println("-----執行excute--------"+method);
        Assert.state(account != null, "account must not empty!");
        if ("add".equals(method)) {
            account.addBalance(new BigDecimal(50001));
        } else if ("reduce".equals(method)) {
            account.reduceBalance(new BigDecimal(49999));
        }
    }
}

運行測試結果如下:
BeanLife
由上面運行結果可知:一個Bean如果配置了多個生命週期的方法,其執行順序如下:

  1. 包含由@PostConstruct註解方法。
  2. 實現了InitializingBean接口的afterPropertiesSet()方法。
  3. 指定Spring XML的init-method屬性的自定義方法。
  4. 包含由 @PreDestroy註解方法。
  5. 實現了DisposableBean接口的destroy()方法。
  6. 指定Spring XML的destroy-method屬性的自定義方法。

7. 獲取 ApplicationContext

開發中有時候需要在未被Spring管理Bean對象去獲取Spring管理的JavaBean對象,這個功能很有用,比如調用Spring管理JavaBean對象邏輯層進行數據庫的讀寫。而這隻需我們定義一個類去實現 ApplicationContextAware 接口然後設置Spring的上下文對象 ApplicationContext 即可,下面演示是項目常用的工具類如下:

public class SpringUtils implements ApplicationContextAware {

    public static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if(SpringUtils.applicationContext == null) {
            SpringUtils.applicationContext = applicationContext;
        }
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }


    //通過name獲取 Bean.
    public static Object getBean(String name){
        return getApplicationContext().getBean(name);
    }

    //通過class獲取Bean.
    public static <T> T getBean(Class<T> clazz){
        return getApplicationContext().getBean(clazz);
    }

    //通過name,以及Clazz返回指定的Bean
    public static <T> T getBean(String name,Class<T> clazz){
        return getApplicationContext().getBean(name, clazz);
    }
}

8. 使用 BeanPostProcessor 處理器

最新版本的Spring中的 BeanPostProcessor 提供的兩個接口都爲默認方法:
在這裏插入圖片描述
BeanPostProcessor 方法定義了回調方法,可以結合項目實現自己對Bean初始化前或銷燬前所做的一些邏輯。

  • 新增JavaBean對象
@Getter
@Setter
@AllArgsConstructor
public class Order {

    private String orderName;

    private Double price;
    
    public Order() {
        System.out.println("order被創建了");
    }
    
    public void  init() {
        System.out.println("order的init方法");
    }

    public void destroy() {
        System.out.println("order的destroy方法");
    }
}
  • 自定義實現接口
public class BeanPostProcessorDemo implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("postProcessBeforeInitialization1: "+beanName+"開始執行");
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("postProcessAfterInitialization1: "+beanName+"開始執行");
        return bean;
    }
}
  • xml元數據配置如下:
    在這裏插入圖片描述
  • 測試運行
    在這裏插入圖片描述
    由此我們可知後置處理器在容器初始化方法之前和容器銷燬方法之前進行業務邏輯的設定。

9. 使用 PropertyPlaceholderConfigurer

最新版本該類標記的過時,不過這個類還是比較實用的。
在這裏插入圖片描述
在這裏插入圖片描述
開發中可使用該類具體化值,也就是我們將常見數據庫連接的用戶名和密碼進行加密這樣別人看到我們的數據庫連接信息是密文狀態的,然後當IOC容器啓動時再將其進行解密成明文然後連接數據庫。

  • 定義加密類
public class DESUtil {
    private static Key key;
    private static String KEY_STR = "myKey";
    private static String CHAR_SET_NAME = "UTF-8";
    private static String ALGORITHM = "DES";

    static {
        try {
            KeyGenerator generator = KeyGenerator.getInstance(ALGORITHM);
            SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
            secureRandom.setSeed(KEY_STR.getBytes());
            generator.init(secureRandom);
            key = generator.generateKey();
            generator = null;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static String getEncryptString(String str) {
        Base64.Encoder encoder = Base64.getEncoder();
        try {
            byte[] bytes = str.getBytes(CHAR_SET_NAME);
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, key);
            byte[] doFinal = cipher.doFinal(bytes);
            return encoder.encodeToString(doFinal);
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }

    public static String getDecryptString(String str) {
        Base64.Decoder decoder = Base64.getDecoder();
        try {
            byte[] bytes = decoder.decode(str);
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, key);
            byte[] doFinal = cipher.doFinal(bytes);
            return new String(doFinal, CHAR_SET_NAME);
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }

    public static void main(String[] args) {
        System.out.println(getEncryptString("123456")); //QAHlVoUc49w=
        System.out.println(getEncryptString("root")); // WnplV/ietfQ=
    }
}
  • 新增數據庫配置文件在這裏插入圖片描述
  • xml元數據配置
    在這裏插入圖片描述
  • PropertyPlaceholderConfigurer 繼承類
public class EncryptPropertySourcesPlaceholderConfigurer extends
        PropertyPlaceholderConfigurer {

    private String[] encryptPropNames = {"jdbc.username", "jdbc.password"};

    @Override
    protected String convertProperty(String propertyName, String propertyValue) {
        if (isEncryptProp(propertyName)) {
            String decryptValue = DESUtil.getDecryptString(propertyValue);
            System.out.println("解密後的值:"+decryptValue);
            return decryptValue;
        } else {
            return propertyValue;
        }
    }

    private boolean isEncryptProp(String propertyName) {
        for (String encryptPropertyName : encryptPropNames) {
            if (encryptPropertyName.equals(propertyName)) {
                return true;
            }
        }
        return false;
    }
}
  • 測試類運行如下:
    在這裏插入圖片描述
    可以看到我們的properties配置的 jdbc.usernamejdbc.password兩個屬性的值設置的是加密後的,當重寫 PropertyPlaceholderConfigurerconvertProperty方法然後在對其進行解密後即可。

源碼

以上代碼均可在 codegeekgao.git 下載查看,下一章將溫習Spring基於註解配置相關知識的學習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章