第一章 Spring之旅

第一章 Spring之旅

[TOC]

前言

在誕生之初,創建spring的主要目的就是用來替代更加基重量級的企業級Java技術,尤其是EJB,相對于EJB,Spring提供了更加輕量級和簡單的編程模型。

為降低java開發的復雜性,spring采取的四種策略

  • 基于POJO輕量級最小侵入性編程
  • 通過依賴注入和面向接口實現松耦合
  • 基于切面和管理進行聲明式編程
  • 通過切面和模板減少樣板式代碼

什么是最小侵入性編程?

一些框架要求繼承他們提供的類或現他們的接口導致應用和框架綁死,這是侵入性編程(PS : Spring Data好像就是這么干的 XD)。而具有最小侵入性編程的框架不會強迫這些,最壞的情況是需要使用注解。

public class HelloWorld{
    public void sayHello(){
        System.out.println("Hello World!");
    }
}

這是一個非常簡單的Java類(POJO),Spring賦予POJO魔力的方式之一就是通過DI(依賴注入)來裝配他們,幫助應用對象之間保持低耦合

一個簡單的勇者騎士類

BraveKnight.class

public class BraveKnight implements Knight {

    //拯救少女請求
    private RescueDamselQuest quest;

    public BraveKnight(){
        this.quest = new RescueDamselQuest();
    }

    @Override
    public void embarkOnQuest() {
        quest.embark();
    }
}

BraveKnightRescueDamselQuest緊密的耦合在一起。

耦合具有兩面性,一方面,高耦合度代碼難以復用、難以測試、難以理解,當修復一個bug時,將會出現一個或更多bug。另一方面,一定程度的耦合度是必須的 —— 完全沒有耦合的代碼什么也做不了。

對象的依賴關系將由系統中負責協調對象的第三方組件在創建對象的時候進行設定,對象無需自行創建或管理他們的依賴關系。

第三方組件
第三方組件

"靈活"的勇敢騎士:構造器注入

BraveKnight.class

public class BraveKnight implements Knight {

    private Quest quest;
    // 構造器注入
    public BraveKnight(Quest quest){
        this.quest = quest;
    }

    @Override
    public void embarkOnQuest() {
        quest.embark();
    }
}

BraveKnight 沒有自行創建探險任務,而是在構造的時候把Quest作為構造器參數傳入,這是依賴注入的方式之一:構造器注入

下面寫一個新的Quest實現:
SlayDragonQuest.class

public class SlayDragonQuest implements Quest {
    private PrintStream printStream;
    public SlayDragonQuest(PrintStream printStream){
        this.printStream = printStream;
    }
    @Override
    public void embark() {
        printStream.println("勇士.請和惡龍爭斗吧!");
    }
}

構建應用組件之間的協作的行為通常稱為裝配。Spring有多種裝配bean的方式,采用XML是很常見的一種裝配方式。

構造器注入:基于XML進行裝配

knights.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="knight" class="com.springinaction.knights.BraveKnight">
        <constructor-arg ref="slayDragonQuest"/>
    </bean>
    <bean id="slayDragonQuest" class="com.springinaction.quest.SlayDragonQuest">
        <constructor-arg value="#{T(System).out}"/>
    </bean>
    <bean id="rescueDamselQuest" class="com.springinaction.quest.RescueDamselQuest">
        <constructor-arg value="#{T(System).out}"/>
    </bean>
</beans>

其中,BraveKnight . SlayDragonQuest . RescueDamselQuest 被聲明為Spring中的bean。就BraveKnight來講,它在構造時傳入了對SlayDragonQuest的引用,將其作為構造器參數。同時,SlayDragonQuest bean 的聲明使用了spring表達式語言,將System.out(這是一個PrintStream)傳入到了SlayDragonQuest的構造器中。

加載XML文件:

public class KnightMain {
    public static void main(String []args){
        // 通過xml的方式 加載spring上下文
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("com/springinaction/knights.xml");
        // 獲取 knight bean
        Knight knight = context.getBean(Knight.class);
        knight.embarkOnQuest();
        context.close();
    }
}

Spring通過應用上下文(Application Context)裝配bean的定義并把它們組裝起來,Spring應用上下文全權負責對象的創建和組裝,Spring自帶了多種應用上下文的實現,它們之間主要的區別僅僅在于如何加載配置

KnightMain.class過程:

  1. 基于knights.xml創建Spring應用上下文
  2. 調用應用上下文獲取一個ID為knight的bean
  3. 簡單調用embarkOnQuest()方法執行探險任務

構造器注入:基于Java文件

Spring還支持基于Java的配置:

KnightConfig.class

@Configuration
public class KnightConfig {
    @Bean
    public Knight braveKnight(){
        return new BraveKnight(rescueDamselQuest());
    }
    @Bean
    public Quest rescueDamselQuest(){
        return new RescueDamselQuest(System.out);
    }
    @Bean
    public Quest slayDragonQuest(){
        return new SlayDragonQuest(System.out);
    }
}

KnightMain.class

public class KnightMain {
    public static void main(String []args){
        // 通過java配置的方式 加載spring上下文
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(KnightConfig.class);
        // 第二種寫法
        // AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.springinaction.config");
        // 獲取 knight bean
        Knight knight = context.getBean(Knight.class);
        knight.embarkOnQuest();
        context.close();
    }
}

基于切面進行聲明式編程

DI(依賴注入)可以讓互相協作的軟件保持松散耦合,而面向切面編程(AOP)允許你把遍布應用各處的功能分離出來形成可重用的組件。

橫切關注點:除了實現自身核心功能之外,一些組件還經常承擔著額外的職責,諸如日志、事務管理和安全這樣的系統服務經常融入到自身具有核心業務邏輯的組件中去,這些系統服務通常被稱為橫向關注點。

如果將這些關注點分散到多個組件中,代碼將會帶來雙重復雜性:

  • 實現系統關注點功能的代碼將會分散到多個組件中去;即使把關注點抽象成獨立的模塊,其他模塊仍然需要調用其方法。
  • 組件會因為那些與自身核心業務無關的代碼而變得混亂。例如一個向地址簿增加地址條目的方法不應該關注它是否是安全的或者是否支持事務。

AOP能夠使服務模塊化(比如說課程服務、學生服務...),并以聲明的方式將橫切關注點(例如日志模塊、安全模塊、事務管理模塊...)應用到它們需要影響的組件中去。所造成的結果就是這些組件會具有更高的內聚性并且會更加關注自身的業務,完全不需要了解涉及系統服務

關注點的調用經常會散布到各個模塊中
關注點的調用經常會散布到各個模塊中

AOP應用

吟游詩人:Minstrel.class

public class Minstrel {
    private PrintStream printStream;
    public Minstrel(PrintStream printStream){
        this.printStream = printStream;
    }
    /**
     *  探險之前調用
     */
    public void singBeforeQuest(){
        printStream.println("探險之前調用.");
    }
    /**
     *  探險之后調用
     */
    public void singAfterQuest(){
        printStream.println("探險之后調用.");
    }
}

如果在BraveKnight.class中調用Minstrel的方法的話:

public void embarkOnQuest() {
        minstrel.singBeforeQuest();
        quest.embark();
        minstrel.singAfterQuest();
    }

顯然有點不正確,Minstrel 吟游詩人應該做他分內的事情,而不需要minstrel的“提醒”。

我們需要做的是將Minstrel 聲明為一個切面:

knights.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" xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <bean id="rescueDamselQuest" class="com.springinaction.quest.RescueDamselQuest">
        <constructor-arg value="#{T(System).out}"/>
    </bean>
    <bean id="SlayDragonQuest" class="com.springinaction.quest.SlayDragonQuest">
        <constructor-arg value="#{T(System).out}"/>
    </bean>
    <bean id="braveKnight" class="com.springinaction.knights.BraveKnight">
        <constructor-arg ref="rescueDamselQuest"/>
    </bean>
    <!-- 吟游詩人 -->
    <bean id="minstrel" class="com.springinaction.minstrel.Minstrel">
        <constructor-arg value="#{T(System).out}"/>
    </bean>
    <aop:config>

        <!-- 把吟游詩人聲明為一個切面 -->
        <aop:aspect ref="minstrel">

            <!-- 定義切點 -->
            <aop:pointcut id="embark" expression="execution(* *.embarkOnQuest(..))"/>

            <aop:before method="singBeforeQuest" pointcut-ref="embark"/>

            <aop:after method="singAfterQuest" pointcut-ref="embark"/>

        </aop:aspect>

    </aop:config>
</beans>

聲明一個簡單切面的方法 :

  • 首先,把 Minstrel 聲明為一個bean
  • <aop:config></aop:config> 中編寫切面
  • <aop:aspect></aop:aspect> 中引用名為 Minstrel 的bean,使其聲明為一個切面
  • 使用 <aop:pointcut/> 定義切入點,并配置expression屬性來選擇所應用的通知
  • 使用 <aop:before/> 定義 前置通知
  • 使用 <aop:after/> 定義 后置通知

輸出如下:

探險之前調用.
勇士.請拯救你的少女!
11:17:15.056 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Returning cached instance of singleton bean 'minstrel'
探險之后調用.

使用模板消除樣板式代碼

為了實現簡單的功能或者任務,不得不重復寫一些重復冗長的代碼,比如查詢數據庫。這就是樣板式代碼 :

package com.springinaction;

import java.sql.*;

public class BookMain {
    public static void main(String []args){
        Connection con = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        Book book = new Book();
        try{
            Class.forName("com.mysql.cj.jdbc.Driver");
            con = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/graduate?serverTimezone=GMT&useSSL=true","root","root");
            statement = con.prepareStatement("SELECT * FROM BOOK WHERE author = ?");
            statement.setString(1,"施瓦辛格");
            resultSet = statement.executeQuery();
            while(resultSet.next()){
                book.setId(resultSet.getLong("id"));
                book.setDescription(resultSet.getString("description"));
                book.setTitle(resultSet.getString("title"));
                book.setIsbn(resultSet.getString("isbn"));
                book.setAuthor(resultSet.getString("author"));
                System.out.println(book);
            }
        }catch(Exception e){
            e.printStackTrace();
            if(resultSet!=null){
                try {
                    resultSet.close();
                } catch (SQLException e1) {
                    e1.printStackTrace();
                }
            }
            if (statement != null){
                try {
                    statement.close();
                } catch (SQLException e1) {
                    e1.printStackTrace();
                }
            }
            if (con != null){
                try {
                    con.close();
                } catch (SQLException e1) {
                    e1.printStackTrace();
                }
            }
        }
    }
}

如果說使用了Spring的JdbcTemplate的話,事情會變得比較簡單 :

首先需要進行數據庫的通用配置,假設文件名為applicationContext.xml


    <!--數據源的配置 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <constructor-arg name="url" value="jdbc:mysql://127.0.0.1/graduate?serverTimezone=GMT&amp;useSSL=true"/>
        <constructor-arg name="username" value="root"/>
        <constructor-arg name="password" value="root"/>
    </bean>

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

之后再創建BookJdbcTemplate類,操作底層數據庫(增、刪、改、查):

PS : 書上這一塊兒寫的比較簡略,我稍微改了一些內容,并不是那么與時俱進的加入了JDK8的Lamada表達式(畢竟JDK11都快呼之欲出了)

@Repository
public class BookJdbcTemplate {
    private final JdbcTemplate jdbcTemplate;
    @Autowired
    public BookJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    public Book getBookById(Long id){
        // sql查詢
        String sql = "SELECT * FROM BOOK WHERE ID = ?";
        Book book = new Book();
        RowMapper<Book> mapper = (rs , rowNum) -> {
            book.setAuthor(rs.getString("author"));
            book.setDescription(rs.getString("description"));
            book.setId(rs.getLong("id"));
            book.setIsbn(rs.getString("isbn"));
            book.setTitle(rs.getString("title"));
            return book;
        };
        return jdbcTemplate.queryForObject(sql , mapper , id);
    }
}

之后就是主程序的編寫了 :

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
JdbcTemplate jdbcTemplate =  context.getBean(JdbcTemplate.class);
BookJdbcTemplate template = new BookJdbcTemplate(jdbcTemplate);
try {
    Book book = template.getBookById(1L);
    System.out.println(book);
}catch (EmptyResultDataAccessException ex){
    System.out.println("未找到或找到多條數據.");
}

spring通過面向 POJO 編程、DI、切面和模板技術來簡化 Java 開發中的復雜性。

容納你的Bean:Spring容器

在基于Spring的應用中,你的應用對象生存與Spring容器中(container)中,Spring容器負責創建對象,裝配它們,配置它們,并管理它們的整個生命周期從生存到死亡。

容器是Spring框架的核心,Spring容器使用DI管理構成應用的組件,它會創建相互協作的組件之間的關聯。

Spring的容器并不是一個,Spring自帶了多個容器實現,可以歸為兩個不同的類型:bean工廠和應用上下文

bean工廠是最簡單的容器,提供基本的依賴注入支持。應用上下文基于BeanFactory構建,并提供應用框架級別的服務,例如從屬性文件解析文本以及發布應用事件給感興趣的事件監聽者。

BeanFactory和ApplicationContext
BeanFactory和ApplicationContext

應用上下文使用的要更多更頻繁一些。

使用應用上下文

  • AnnotationConfigApplicationContext : 從一個或多個基于Java的配置類中加載Spring應用上下文。
  • AnnotationConfigWebApplicationContext:從一個或多個基于Java配置類中加載Spring web應用上下文。
  • ClassPathXmlApplicationContext:從classpath路徑下的一個或多個XML配置文件中加載上下文定義,把應用上下文的定義文件作為類資源。
  • FileSystemXmlApplicationContext:從文件系統下的一個或多個XML配置文件中加載上下文定義。
  • XmlWebApplicationContext:從WEB應用下的一個或多個XML配置文件中加載上下文定義。

無論是從類中還是從文件系統中加載應用上下文都是類似的:
例如,下面是從類路徑中加載:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("com/springinaction/knights.xml");

下面是從文件系統中加載

FileSystemXmlApplicationContext context = new FileSystemXmlApplicationContext("c:/knights.xml");

下面是Java配置文件加載:

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(KnightConfig.class);

bean 的聲明周期

bean的聲明周期
bean的聲明周期

bean的聲明周期經常會被問到,但是卻又很難記憶 (建議把接下來的打印下來每天記憶):

作者:MOBIN-F
鏈接:https://www.zhihu.com/question/38597960/answer/77600561
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

  • Spring對Bean進行實例化(相當于程序中的newXx())

  • Spring將值和Bean的引用注入進Bean對應的屬性中

  • 如果Bean實現了BeanNameAware接口,Spring將Bean的ID傳遞給setBeanName()方法(實現BeanNameAware清主要是為了通過Bean的引用來獲得Bean的ID,一般業務中是很少有用到Bean的ID的)

  • 如果Bean實現了BeanFactoryAware接口,Spring將調用setBeanDactory(BeanFactory bf)方法并把BeanFactory容器實例作為參數傳入。(實現BeanFactoryAware主要目的是為了獲取Spring容器,如Bean通過Spring容器發布事件等)

  • 如果Bean實現了ApplicationContextAware接口,Spring容器將調用setApplicationContext(ApplicationContext ctx)方法,把應用上下文作為參數傳入。(作用與BeanFactory類似都是為了獲取Spring容器,不同的是Spring容器在調用setApplicationContext方法時會把它自己作為setApplicationContext的參數傳入,而Spring容器在調用setBeanDactory前需要程序員自己指定(注入)setBeanDactory里的參數BeanFactory )

  • 如果Bean實現了BeanPostProcess接口,Spring將調用它們的postProcessBeforeInitialization(預初始化)方法(作用是在Bean實例創建成功后對進行增強處理,如對Bean進行修改,增加某個功能)

  • 如果Bean實現了InitializingBean接口,Spring將調用它們的afterPropertiesSet方法,作用與在配置文件中對Bean使用init-method聲明初始化的作用一樣,都是在Bean的全部屬性設置成功后執行的初始化方法。

  • 如果Bean實現了BeanPostProcess接口,Spring將調用它們的postProcessAfterInitialization(后初始化)方法(作用與6的一樣,只不過6是在Bean初始化前執行的,而這個是在Bean初始化后執行的,時機不同 )

  • 經過以上的工作后,Bean將一直駐留在應用上下文中給應用使用,直到應用上下文被銷毀

  • 如果Bean實現了DispostbleBean接口,Spring將調用它的destory方法,作用與在配置文件中對Bean使用destory-method屬性的作用一樣,都是在Bean實例銷毀前執行的方法。

Spring模塊

Spring模塊
Spring模塊
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容