2021-1-02--------ssm之第4章 Spring核心

我卡在門縫裡了>_<發表於2021-01-02


技能目標

❖ 理解Spring IoC的原理

❖ 掌握Spring IoC的配置

❖ 理解Spring AOP的原理

❖ 掌握Spring AOP的配置

任務1:認識Spring

任務2:Spring IoC的簡單運用

任務3:Spring AOP的簡單運用

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-V3NnPwcf-1609551766329)(C:\Users\pcy\AppData\Roaming\Typora\typora-user-images\image-20201217220043163.png)]

任務1 認識Spring

➢ 瞭解Spring的優點。

➢ 瞭解Spring的整體架構。

4.1.1 傳統Java EE開發

企業級應用是指為商業組織、大型企業建立並部署的解決方案及應用。這些大型企業級應用的結構複雜,涉及的外部資源眾多,事務密集、資料規模大、使用者數量多,有較強的安全性考慮和較高的效能要求。

企業級應用絕不可能是一個個的獨立系統。在企業中,一般都會部署多個互動的應用,同時這些應用又有可能與其他企業的相關應用連線,從而構成一個結構複雜的、跨越Internet的分散式企業應用叢集。此外,作為企業級應用,不但要有強大的功能,還要能夠滿足未來業務需求的發展變化,易於擴充套件和維護。

傳統Java EE在解決企業級應用問題時的“重量級”架構體系,使它的開發效率、開發難度和實際效能都令人失望。當人們苦苦尋找解決辦法的時候,Spring以一個“救世主”的形象出現在廣大Java程式設計師面前。說到Spring,就要提到Rod Johnson,2002年他編寫了《Expert One-on-One Java EE Design and Development》一書。在書中,他對傳統Java EE技術的日益臃腫和低效提出了質疑,他覺得應該有更便捷的做法,於是提出了Interface 21,也就是Spring框架的雛形。他提出了技術應以實用為準的主張,引發了人們對“正統”Java EE的反思。2003年2月,Spring框架正式成為一個開源專案,併釋出於SourceForge中。

Spring致力於Java EE應用的各種解決方案,而不僅僅專注於某一層的方案。可以說,Spring是企業應用開發的“一站式”選擇,貫穿表現層、業務層和持久層。並且Spring並不想取代那些已有的框架,而是以高度的開放性與它們無縫整合。

4.1.2 Spring整體架構

Spring確實給人一種格外清新的感覺,彷彿微雨後的綠草叢,蘊藏著勃勃生機。Spring是一個輕量級框架,它大大簡化了Java企業級開發,提供強大、穩定功能的同時並沒有帶來額外的負擔。Spring有兩個主要目標:一是讓現有技術更易於使用,二是養成良好的程式設計習慣(或者稱為最佳實踐)。

作為一個全面的解決方案,Spring堅持一個原則:不重新發明輪子。已經有較好解決方案的領域,Spring絕不做重複性的實現。例如,物件持久化和ORM, Spring只是對現有的JDBC、MyBatis、Hibernate等技術提供支援,使之更易用,而不是重新實現。

Spring框架由大約20個功能模組組成。這些模組被分成六個部分,分別是Core Container、Data Access/Integration、Web、AOP(Aspect Oriented Programming)、Instrumentation及Test

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-YGvJB7xT-1609551766339)(C:\Users\pcy\AppData\Roaming\Typora\typora-user-images\image-20201217220742029.png)]

Spring Core是框架的最基礎部分,提供了IoC特性。SpringContext為企業級開發提供了便利的整合工具。Spring AOP是基於Spring Core的符合規範的面向切面程式設計的實現。Spring JDBC提供了JDBC的抽象層,簡化了JDBC編碼,同時使程式碼更健壯。SpringORM對市面上流行的ORM框架提供了支援。Spring Web為Spring在Web應用程式中的使用提供了支援。關於Spring的其他功能模組在開發中的作用,可以查閱Spring的文件進行了解

任務2 Spring IoC的簡單運用

關鍵步驟如下。

➢ 掌握IoC的原理。

➢ 使用IoC的設值注入方式輸出“Hello, Spring! ”。

➢ 使用IoC的設值注入方式實現動態組裝的印表機。

4.2.1 IoC/DI

控制反轉(Inversion of Control, IoC)也稱為依賴注入(Dependency Injection,DI),是物件導向程式設計中的一種設計理念,用來降低程式程式碼之間的耦合度。

依賴一般指通過區域性變數、方法引數、返回值等建立的對於其他物件的呼叫關係。例如,在A類的方法中,例項化了B類的物件並呼叫其方法來完成特定的功能,我們就說A類依賴於B類。

幾乎所有的應用都由兩個或更多的類通過合作來實現完整的功能。類與類之間的依賴關係增加了程式開發的複雜程度,我們在開發一個類的時候,還要考慮對正在使用該類的其他類的影響。例如,常見的業務層呼叫資料訪問層以實現持久化操作

/**
*使用者Dao介面,定義了所需的持久化方法
**/
public interface UserDao{
	public void save(User user);
}
/**
*使用者dao實現類,實現對user類的持久化操作
**/
public clsaa UserDaoImpl implements UserDao{
	public void save(User user){
		System.out.print("儲存資料庫資訊到資料庫");
	}
}
/**
*使用者業務類,實現對user功能的業務管理
**/
public class UserServiceImpl implements UserServicxe{
	private UserDao dao=new UserDaoImpl();
	public void addNewUser(User user){
		dao.save(user);
	}
}

工廠模式:根據我們提供的所需要的物件例項的描述,為我們返回所需的產品

1.產品的規範

2.產品

3.工廠

4.客戶端/呼叫

如以上程式碼所示,UserServiceImpl對UserDaoImpl存在依賴關係。這樣的程式碼很常見,但是存在一個嚴重的問題,即UserServiceImpl和UserDaoImpl高度耦合,如果因為需求變化需要替換UserDao的實現類,將導致UserServiceImpl中的程式碼隨之發生修改。如此,程式將不具備優良的可擴充套件性和可維護性,甚至在開發中難以測試。

我們可以利用簡單工廠和工廠方法模式的思路解決此類問題

public class UserDaoFactory{
	public static UserDao getInstance(){
		
	}
}
public class UserServiceImpl implements UserService{
	private UserDao dao=UserDaoFactory.getInstance();
	public void addNewUser(User user){
		dao.save(user);
	}
}

示例2中的使用者DAO工廠類UserDaoFactory體現了“控制反轉”的思想:User ServiceImpl不再依靠自身的程式碼去獲得所依賴的具體DAO物件,而是把這一工作轉交給了“第三方”UserDaoFactory,從而避免了和具體UserDao實現類之間的耦合。由此可見,在如何獲取所依賴的物件上,“控制權”發生了“反轉”,即從UserServiceImpl轉移到了UserDaoFactory,這就是“控制反轉”。

問題雖然得到了解決,但是大量的工廠類被引入開發過程中,明顯增加了開發的工作量。而Spring能夠分擔這些額外的工作,其提供了完整的IoC實現,讓我們得以專注於業務類和DAO類的設計。

4.2.2 Spring實現輸出

問題我們已經瞭解了“控制反轉”,

那麼在專案中如何使用Spring實現“控制反轉”呢?開發第一個Spring專案,輸出“Hello, Spring! ”。

具體要求如下。

➢ 編寫HelloSpring類輸出“Hello, Spring ! ”。

➢ 其中的字串內容“Spring”是通過Spring框架賦值到HelloSpring類中的。

實現思路及關鍵程式碼

(1)下載Spring並新增到專案中。

(2)編寫Spring配置檔案。

(3)編寫程式碼通過Spring獲取HelloSpring例項。

首先通過Spring官網http://repo.spring.io/release/org/springframework/spring/下載所需版本的Spring資源,這裡以Spring Framework 3.2.13版本為例。下載的壓縮包spring-framework-3.2.13.RELEASE-dist.zip解壓後的資料夾目錄結構如圖4.3所示

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-SrxeJ20k-1609551766344)(C:\Users\pcy\AppData\Roaming\Typora\typora-user-images\image-20201217224934486.png)]

➢ docs:該資料夾下包含Spring的相關文件,包括API參考文件、開發手冊。

➢ libs:該資料夾下存放Spring各個模組的jar檔案,每個模組均提供三項內容:開發所需的jar檔案、以“-javadoc”字尾表示的API和以“-sources”字尾表示的原始檔。

➢ schema:配置Spring的某些功能時需要用到的schema檔案,對於已經整合了Spring的IDE環境(如MyEclipse),這些檔案不需要專門匯入。

在MyEclipse中新建一個專案HelloSpring,將所需的Spring的jar檔案新增到該專案中。需要注意的是,Spring的執行依賴於commons-logging元件,需要將相關jar檔案一併匯入。為了方便觀察Bean例項化過程,我們採用log4j作為日誌輸出,所以也應該將log4j的jar檔案新增到專案中。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-uE3RL7qC-1609551766350)(C:\Users\pcy\AppData\Roaming\Typora\typora-user-images\image-20201217225250720.png)]

為專案新增log4j.properties檔案,用來控制日誌輸出。log4j.properties檔案

# rootLogger是所有日誌的根日誌,修改該日誌屬性將對所有日誌起作用
# 下面的屬性配置中,所有日誌的輸出級別是info,輸出源是con
log4j.rootLogger=info,con
# 定義輸出源的輸出位置是控制檯
log4j.appender.con=org.apache.log4j.ConsoleAppender
# 定義輸出日誌的佈局採用的類
log4j.appender.con.layout=org.apache.log4j.PatternLayout
# 定義日誌輸出佈局
log4j.appender.con.layout.ConversionPattern=%d{MM-dd HH:mm:ss}[%p]%c%n -%m%n

編寫HelloSpring類

public class HelloSpring {
   // 定義who屬性,該屬性的值將通過Spring框架進行設定
   private String who = null;

   /**
    * 定義列印方法,輸出一句完整的問候。
    */
   public void print() {
      System.out.println("Hello," + this.getWho() + "!");
   }

   /**
    * 獲得 who。
    * 
    * @return who
    */
   public String getWho() {
      return who;
   }

   /**
    * 設定 who。
    * 
    * @param who
    */
   public void setWho(String who) {
      this.who = who;
   }

}

接下來編寫Spring配置檔案,在專案的classpath根路徑下建立applicationContext. xml檔案(為便於管理框架的配置檔案,可在專案中建立專門的原始檔夾,如resources目錄,並將Spring配置檔案建立在其根路徑下)。在Spring配置檔案中建立HelloSpring類的例項併為who屬性注入屬性值。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-LjI54ZW8-1609551766353)(C:\Users\pcy\AppData\Roaming\Typora\typora-user-images\image-20201218171241396.png)]

<?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-3.2.xsd">
   <!-- 通過bean元素宣告需要Spring建立的例項。該例項的型別通過class屬性指定,並通過id屬性為該例項指定一個名稱,以便在程式中使用 -->
   <bean id="helloSpring" class="cn.springdemo.HelloSpring">
      <!-- property元素用來為例項的屬性賦值,此處實際是呼叫setWho()方法實現賦值操作 -->
      <property name="who">
         <!-- 此處將字串"Spring"賦值給who屬性 -->
         <value>Spring</value>
      </property>
   </bean>
</beans>

在Spring配置檔案中,使用元素來定義Bean(也可稱為元件)的例項。元素有兩個常用屬性:一個是id,表示定義的Bean例項的名稱;另一個是class,表示定義的Bean例項的型別。

經驗

(1)使用元素定義一個元件時,通常需要使用id屬性為其指定一個用來訪問的唯一名稱。如果想為Bean指定更多的別名,可以通過name屬性指定,名稱之間使用逗號、分號或空格進行分隔。(2)在本例中,Spring為Bean的屬性賦值是通過呼叫屬性的setter方法實現的,這種做法稱為“設值注入”,而非直接為屬性賦值。若屬性名為who, setter方法名為setSomebody( ), Spring配置檔案中應寫成name="somebody"而非name=“who”。所以在為屬性和setter訪問器命名時,一定要遵循JavaBean的命名規範。

在專案中新增測試方法

@Test
public void helloSpring() {
    // 通過ClassPathXmlApplicationContext例項化Spring的上下文
    ApplicationContext context = new ClassPathXmlApplicationContext(
            "applicationContext.xml");
    // 通過ApplicationContext的getBean()方法,根據id來獲取bean的例項
    HelloSpring helloSpring = (HelloSpring) context.getBean("helloSpring");
    // 執行print()方法
    helloSpring.print();
}

ApplicationContext是一個介面,負責讀取Spring配置檔案,管理物件的載入、生成,維護Bean物件之間的依賴關係,負責Bean的生命週期等。ClassPat hXmlApplicationContext是ApplicationContext介面的實現類,用於從classpath路徑中讀取Spring配置檔案。

知識擴充套件

(1)除了ClassPathXmlApplicationContext,ApplicationContext介面還有其他實現類。例如,FileSystemXmlApplicationContext也可以用於載入Spring配置檔案。(2)除了ApplicationContext及其實現類,還可以通過BeanFactory介面及其實現類對Bean元件實施管理。事實上,ApplicationContext就是建立在BeanFactory的基礎之上。BeanFactory介面是Spring IoC容器的核心,負責管理元件和它們之間的依賴關係,應用程式通過BeanFactory介面與Spring IoC容器互動。ApplicationContext是BeanFactory的子介面,可以對企業級開發提供更全面的支援。

通過“Hello, Spring! ”的例子,我們發現Spring會自動接管配置檔案中Bean的建立和為屬性賦值的工作。Spring在建立Bean的例項後,會呼叫相應的setter方法為例項設定屬性值。例項的屬性值將不再由程式中的程式碼來主動建立和管理,改為被動接受Spring的注入,使得元件之間以配置檔案而不是硬編碼的方式組織在一起。

提示

相對於“控制反轉”, “依賴注入”的說法也許更容易理解一些,即由容器(如Spring)負責把元件所“依賴”的具體物件“注入”(賦值)給元件,從而避免元件之間以硬編碼的方式耦合在一起。

4.2.3 深入理解IoC/DI

如何開發一個印表機模擬程式,使其符合以下條件。

➢ 可以靈活地配置使用彩色墨盒或灰色墨盒。

➢ 可以靈活地配置列印頁面的大小。

分析程式中包括印表機(Printer)、墨盒(Ink)和紙張(Paper)三類元件,如圖4.5所示。印表機依賴墨盒和紙張。採取如下的步驟開發這個程式。

(1)定義Ink和Paper介面。

(2)使用Ink介面和Paper介面開發Printer程式。在開發Printer程式時並不依賴Ink和Paper的具體實現類。

(3)開發Ink介面和Paper介面的實現類:ColorInk、GreyInk和TextPaper。

(4)組裝印表機,執行除錯。

1.定義Ink和Paper介面

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-mkMSDPP9-1609551766356)(C:\Users\pcy\AppData\Roaming\Typora\typora-user-images\image-20201219212945359.png)]

public interface Ink {
    /**
     * 定義列印採用的顏色的方法。
     * 
     * @param r
     *            紅色值
     * @param g
     *            綠色值
     * @param b
     *            藍色值
     * @return 返回列印採用的顏色
     */
    public String getColor(int r, int g, int b);
}
public interface Paper {
    public static final String newline = "\r\n";

    /**
     * 輸出一個字元到紙張。
     */
    public void putInChar(char c);

    /**
     * 得到輸出到紙張上的內容。
     */
    public String getContent();
}

2.使用Ink介面和Paper介面開發Printer程式

/**
 * 印表機程式。
 * 
 * @author
 */
public class Printer {
   // 面向介面程式設計,而不是具體的實現類
   private Ink ink = null;
   // 面向介面程式設計,而不是具體的實現類
   private Paper paper = null;

   /**
    * 設值注入所需的setter方法。
    * 
    * @param ink
    *            傳入墨盒引數
    */
   public void setInk(Ink ink) {
      this.ink = ink;
   }

   /**
    * 設值注入所需的setter方法。
    * 
    * @param paper
    *            傳入紙張引數
    */
   public void setPaper(Paper paper) {
      this.paper = paper;
   }

   /**
    * 印表機列印方法。
    * 
    * @param str
    *            傳入列印內容
    */
   public void print(String str) {
      // 輸出顏色標記
      System.out.println("使用" + ink.getColor(255, 200, 0) + "顏色列印:\n");
      // 逐字元輸出到紙張
      for (int i = 0; i < str.length(); ++i) {
         paper.putInChar(str.charAt(i));
      }
      // 將紙張的內容輸出
      System.out.print(paper.getContent());
   }
}

Printer類中只有一個print()方法,輸入引數是一個即將被列印的字串,印表機將這個字串逐個字元輸出到紙張,然後將紙張中的內容輸出。

在開發Printer程式的時候,只需要瞭解Ink介面和Paper介面即可,完全不需要依賴這些介面的某個具體實現類,這是符合實際情況的。在設計真實的印表機時也是這樣,設計師只是針對紙張和墨盒的介面規範進行設計。在使用時,只要符合相應的規範,印表機就可以根據需要更換不同的墨盒和紙張。

軟體設計與此類似,由於明確地定義了介面,在編寫程式碼的時候,完全不用考慮和某個具體實現類的依賴關係,從而可以構建更復雜的系統。元件間的依賴關係和介面的重要性在將各個元件組裝在一起的時候得以體現。通過這種開發模式,還可以根據需要方便地更換介面的實現,就像為印表機更換不同的墨盒和紙張一樣。Spring提倡面向介面程式設計也是基於這樣的考慮。

print()方法執行的時候是從哪裡獲得Ink和Paper的例項呢?

這時就需要提供“插槽”,以便組裝的時候可以將Ink和Paper的例項“注入”進來,對Java程式碼來說就是定義setter方法。至此,Printer類的開發工作就完成了。

3.開發Ink介面和Paper介面的實現類:ColorInk、GreyInk和TextPaper

/**
 * 彩色墨盒。ColorInk實現Ink介面。
 * 
 * @author
 */
public class ColorInk implements Ink {
   // 列印採用彩色
   public String getColor(int r, int g, int b) {
      Color color = new Color(r, g, b);
      return "#" + Integer.toHexString(color.getRGB()).substring(2);
   }
}
/**
 * 灰色墨盒。GreyInk實現Ink介面。
 * 
 * @author 
 */
public class GreyInk implements Ink {
   // 列印採用灰色
   public String getColor(int r, int g, int b) {
      int c = (r + g + b) / 3;
      Color color = new Color(c, c, c);
      return "#" + Integer.toHexString(color.getRGB()).substring(2);
   }
}

彩色墨盒的getColor()方法對傳入的顏色引數做了簡單的格式轉換;灰色墨盒則對傳入的顏色值進行計算,先轉換成灰度顏色,再進行格式轉換。這不是需要關注的重點,瞭解其功能即可。

/**
 * 文字列印紙張實現。TextPaper實現Paper介面。
 * 
 * @author 
 */
public class TextPaper implements Paper {
    // 每行字元數
    private int charPerLine = 16;
    // 每頁行數
    private int linePerPage = 5;
    // 紙張中內容
    private String content = "";
    // 當前橫向位置,從0到charPerLine-1
    private int posX = 0;
    // 當前行數,從0到linePerPage-1
    private int posY = 0;
    // 當前頁數
    private int posP = 1;

    public String getContent() {
        String ret = this.content;
        // 補齊本頁空行,並顯示頁碼
        if (!(posX == 0 && posY == 0)) {
            int count = linePerPage - posY;
            for (int i = 0; i < count; ++i) {
                ret += Paper.newline;
            }
            ret += "== 第" + posP + "頁 ==";
        }
        return ret;
    }

    public void putInChar(char c) {
        content += c;
        ++posX;
        // 判斷是否換行
        if (posX == charPerLine) {
            content += Paper.newline;
            posX = 0;
            ++posY;
        }
        // 判斷是否翻頁
        if (posY == linePerPage) {
            content += "== 第" + posP + "頁 ==";
            content += Paper.newline + Paper.newline;
            posY = 0;
            ++posP;
        }
    }

    // setter方法,用於屬性注入
    public void setCharPerLine(int charPerLine) {
        this.charPerLine = charPerLine;
    }

    // setter方法,用於屬性注入
    public void setLinePerPage(int linePerPage) {
        this.linePerPage = linePerPage;
    }
}

在TextPaper實現類的程式碼中,我們不用關心具體的邏輯實現,只需理解其功能即可。其中content用於儲存當前紙張的內容。charPerLine和linePerPage用於限定每行可以列印多少個字元和每頁可以列印多少行。需要注意的是,setCharPerLine()和setLinePerPage()這兩個setter方法,與示例9中的setter方法類似,也是為了組裝時“注入”資料留下的“插槽”。我們不僅可以注入某個類的例項,還可以注入基本資料型別、字串等型別的資料。

4.組裝印表機,執行除錯

組裝印表機的工作在Spring的配置檔案(applicationContext.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-3.2.xsd">
   <!-- 定義彩色墨盒bean,該bean的id是colorInk,class指定該bean例項的實現類 -->
   <bean id="colorInk" class="cn.ink.ColorInk" />
   <!-- 定義灰色墨盒bean,該bean的id是greyInk,class指定該bean例項的實現類 -->
   <bean id="greyInk" class="cn.ink.GreyInk" />
   <!-- 定義A4紙張bean,該bean的id是a4Paper,class指定該bean例項的實現類 -->
   <bean id="a4Paper" class="cn.paper.TextPaper">
      <!-- property元素用來指定需要容器注入的屬性,charPerLine需要容器注入, TextPaper類必須擁有setCharPerLine()方法。 注入每行字元數 -->
      <property name="charPerLine" value="10" />
      <!-- property元素用來指定需要容器注入的屬性,linePerPage需要容器注入,TextPaper類必須擁有setLinePerPage()方法。 注入每頁行數 -->
      <property name="linePerPage" value="8" />
   </bean>
   <!-- 定義B5紙張bean,該bean的id是b5Paper,class指定該bean例項的實現類 -->
   <bean id="b5Paper" class="cn.paper.TextPaper">
      <!-- property元素用來指定需要容器注入的屬性,charPerLine需要容器注入, TextPaper類必須擁有setCharPerLine()方法。注入每行字元數 -->
      <property name="charPerLine" value="6" />
      <!-- property元素用來指定需要容器注入的屬性,linePerPage需要容器注入, TextPaper類必須擁有setLinePerPage()方法。注入每頁行數 -->
      <property name="linePerPage" value="5" />
   </bean>

各“零件”都定義好後,下面來完成印表機的組裝

<!-- 組裝印表機。定義印表機bean,該bean的id是printer, class指定該bean例項的實現類 -->
   <bean id="printer" class="cn.printer.Printer">
      <!-- 通過ref屬性注入已經定義好的bean -->
      <!-- 注入彩色墨盒 -->
      <property name="ink" ref="colorInk"></property>
      <!-- 注入A4列印紙張 -->
      <property name="paper" ref="b5Paper"></property>
   </bean>

</beans>

組裝了一臺彩色的、使用B5列印紙的印表機。需要注意的是,這裡沒有使用的value屬性,而是使用了ref屬性。value屬性用於注入基本資料型別以及字串型別的值。ref屬性用於注入已經定義好的Bean,如剛剛定義好的colorInk、greyInk、a4Paper和b5Paper。由於Printer的setInk(Ink ink)方法要求傳入的引數是Ink(介面)型別,所以任何Ink介面的實現類都可以注入。

完整的Spring配置檔案,applicationContext.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-3.2.xsd">
   <!-- 定義彩色墨盒bean,該bean的id是colorInk,class指定該bean例項的實現類 -->
   <bean id="colorInk" class="cn.ink.ColorInk" />
   <!-- 定義灰色墨盒bean,該bean的id是greyInk,class指定該bean例項的實現類 -->
   <bean id="greyInk" class="cn.ink.GreyInk" />
   <!-- 定義A4紙張bean,該bean的id是a4Paper,class指定該bean例項的實現類 -->
   <bean id="a4Paper" class="cn.paper.TextPaper">
      <!-- property元素用來指定需要容器注入的屬性,charPerLine需要容器注入, TextPaper類必須擁有setCharPerLine()方法。 注入每行字元數 -->
      <property name="charPerLine" value="10" />
      <!-- property元素用來指定需要容器注入的屬性,linePerPage需要容器注入,TextPaper類必須擁有setLinePerPage()方法。 注入每頁行數 -->
      <property name="linePerPage" value="8" />
   </bean>
   <!-- 定義B5紙張bean,該bean的id是b5Paper,class指定該bean例項的實現類 -->
   <bean id="b5Paper" class="cn.paper.TextPaper">
      <!-- property元素用來指定需要容器注入的屬性,charPerLine需要容器注入, TextPaper類必須擁有setCharPerLine()方法。注入每行字元數 -->
      <property name="charPerLine" value="6" />
      <!-- property元素用來指定需要容器注入的屬性,linePerPage需要容器注入, TextPaper類必須擁有setLinePerPage()方法。注入每頁行數 -->
      <property name="linePerPage" value="5" />
   </bean>
   <!-- 組裝印表機。定義印表機bean,該bean的id是printer, class指定該bean例項的實現類 -->
   <bean id="printer" class="cn.printer.Printer">
      <!-- 通過ref屬性注入已經定義好的bean -->
      <!-- 注入彩色墨盒 -->
      <property name="ink" ref="colorInk"></property>
      <!-- 注入A4列印紙張 -->
      <property name="paper" ref="b5Paper"></property>
   </bean>

</beans>

從配置檔案中可以看到Spring管理Bean的靈活性。Bean與Bean之間的依賴關係放在配置檔案裡組織,而不是寫在程式碼裡。通過對配置檔案的指定,Spring能夠精確地為每個Bean注入屬性。

每個Bean的id屬性是該Bean的唯一標識。程式通過id屬性訪問Bean,Bean與Bean的依賴關係也通過id屬性完成。

印表機組裝好之後如何工作呢?測試方法的關鍵程式碼

 */
public class PrinterTest {

    @Test
   public void printerTest() {
      ApplicationContext context = new ClassPathXmlApplicationContext(
            "applicationContext.xml");
      // 通過Printer bean的id來獲取Printer例項
      Printer printer = (Printer) context.getBean("printer");
      String content = "幾位輕量級容器的作者曾驕傲地對我說:這些容器非常有"
            + "用,因為它們實現了“控制反轉”。這樣的說辭讓我深感迷惑:控"
            + "制反轉是框架所共有的特徵,如果僅僅因為使用了控制反轉就認為"
            + "這些輕量級容器與眾不同,就好像在說“我的轎車是與眾不同的," + "因為它有4個輪子。”";
      printer.print(content);
   }

}

至此,印表機已經全部組裝完成並可以正常使用了。現在我們來總結一下:和Spring有關的只有組裝和執行兩部分程式碼。僅這兩部分程式碼就讓我們獲得了像更換印表機的墨盒和列印紙一樣更換程式元件的能力。這就是Spring依賴注入的魔力。

通過Spring的強大組裝能力,我們在開發每個程式元件的時候,只要明確關聯元件的介面定義,而不需要關心具體實現,這就是所謂的“面向介面程式設計”。

任務3 Spring AOP的簡單運用

關鍵步驟如下。

➢ 掌握Spring AOP的原理。

➢ 使用Spring AOP實現自動的系統日誌功能。

4.3.1 認識AOP

面向切面程式設計(Aspect Oriented Programming, AOP)是軟體程式設計思想發展到一定階段的產物,是對物件導向程式設計(ObjectOriented Programming, OOP)的有益補充。AOP一般適用於具有橫切邏輯的場合,如訪問控制、事務管理、效能監測等。

什麼是橫切邏輯呢?

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-IGjIDR2a-1609551766357)(C:\Users\pcy\AppData\Roaming\Typora\typora-user-images\image-20201220081129230.png)]

UserService的addNewUser()方法根據需求增加了日誌和事務功能。

這是一個再典型不過的業務處理方法。日誌、異常處理、事務控制等,都是一個健壯的業務系統所必需的。但為了保證系統健壯可用,就要在眾多的業務方法中反覆編寫類似的程式碼,使得原本就很複雜的業務處理程式碼變得更加複雜。業務功能的開發者還要關注這些“額外”的程式碼是否處理正確,是否有遺漏。如果需要修改日誌資訊的格式或者安全驗證的規則,或者再增加新的輔助功能,都會導致業務程式碼頻繁而大量的修改

在業務系統中,總有一些散落、滲透到系統各處且不得不處理的事情,這些穿插在既定業務中的操作就是所謂的“橫切邏輯”,也稱為切面。怎樣才能不受這些附加要求的干擾,專注於真正的業務邏輯呢?我們很容易想到的就是將這些重複性的程式碼抽取出來,放在專門的類和方法中處理,這樣就便於管理和維護了。但即便如此,依然無法實現既定業務和橫切邏輯的徹底解耦合,因為業務程式碼中還要保留對這些方法的呼叫程式碼,當需要增加或減少橫切邏輯的時候,還是要修改業務方法中的呼叫程式碼才能實現。我們希望的是無須編寫顯式的呼叫,在需要的時候,系統能夠“自動”呼叫所需的功能,這正是AOP要解決的主要問題。

面向切面程式設計,簡單地說就是在不改變原有程式的基礎上為程式碼段增加新的功能,對其進行增強處理。它的設計思想來源於代理設計模式,下面以圖示的方式進行簡單的說明。通常情況下呼叫物件的方法如圖所示

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-rDD4yEiD-1609551766359)(C:\Users\pcy\AppData\Roaming\Typora\typora-user-images\image-20201226222431028.png)]

在代理模式中可以為物件設定一個代理物件,代理物件為fun()提供一個代理方法,當通過代理物件的fun()方法呼叫原物件的fun()方法時,就可以在代理方法中新增新的功能,這就是所謂的增強處理。增強的功能既可以插到原物件的fun()方法前面,也可以插到其後面,如圖4.8所示。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-iU15YaNL-1609551766360)(C:\Users\pcy\AppData\Roaming\Typora\typora-user-images\image-20201226223212566.png)]

在這種模式下,給程式設計人員的感覺就是在原有程式碼乃至原業務流程都不改變的情況下,直接在業務流程中切入新程式碼,增加新功能,這就是所謂的面向切面程式設計。對面向切面程式設計有了感性認識以後,還需要了解它的一些基本概念

。➢ 切面(Aspect):一個模組化的橫切邏輯(或稱橫切關注點),可能會橫切多個物件。

➢ 連線點(Join Point):程式執行中的某個具體的執行點。圖4.8中原物件的fun()方法就是一個連線點。

➢ 增強處理(Advice):切面在某個特定連線點上執行的程式碼邏輯。

➢ 切入點(Pointcut):對連線點的特徵進行描述,可以使用正規表示式。增強處理和一個切入點表示式相關聯,並在與這個切入點匹配的某個連線點上執行。

➢ 目標物件(Target object):被一個或多個切面增強的物件。

➢ AOP代理(AOP proxy):由AOP框架所建立的物件,實現執行增強處理方法等功能。

➢ 織入(Weaving):將增強處理連線到應用程式中的型別或物件上的過程。

➢ 增強處理型別:如圖4.8所示,在原物件的fun()方法之前插入的增強處理為前置增強,在該方法正常執行完以後插入的增強處理為後置增強,此外還有環繞增強、異常丟擲增強、最終增強等型別。

說明

切面可以理解為由增強處理和切入點組成,既包含了橫切邏輯的定義,也包含了連線點的定義。面向切面程式設計主要關心兩個問題,即在什麼位置執行什麼功能。Spring AOP是負責實施切面的框架,即由Spring AOP完成織入工作。Advice直譯為“通知”,但這種叫法並不確切,在此處翻譯成“增強處理”,更便於理解。

4.3.2 Spring AOP初體驗

問題

日誌輸出的程式碼直接嵌入在業務流程的程式碼中,不利於系統的擴充套件和維護。如何使用Spring AOP來實現日誌輸出,以解決這個問題呢?

實現思路及關鍵程式碼

(1)在專案中新增Spring AOP相關的jar檔案。

(2)編寫前置增強和後置增強實現日誌功能。

(3)編寫Spring配置檔案,對業務方法進行增強處理。

(4)編寫程式碼,獲取帶有增強處理的業務物件

首先在專案中新增所需的jar檔案,jar檔案清單如圖4.10所示。spring-aop-3.2.13. RELEASE.jar提供了Spring AOP的實現。同時,Spring AOP還依賴AOP Alliance和AspectJ專案中的元件,相關版本的下載連結分別為https://sourceforge.net/projects/aopalliance/files/aopalliance/1.0/和http://mvnrepository.com/artifact/org.aspectj/aspectjweaver。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-WKui0Ktw-1609551766361)(C:\Users\pcy\AppData\Roaming\Typora\typora-user-images\image-20201226223938869.png)]

接下來,編寫業務類UserServiceImpl

package service.impl;

import service.UserService;
import dao.UserDao;
import entity.User;

/**
 * 使用者業務類,實現對User功能的業務管理
 */
public class UserServiceImpl implements UserService {

   // 宣告介面型別的引用,和具體實現類解耦合
   private UserDao dao;

   // dao 屬性的setter訪問器,會被Spring呼叫,實現設值注入
   public void setDao(UserDao dao) {
      this.dao = dao;
   }

   public void addNewUser(User user) {
      // 呼叫使用者DAO的方法儲存使用者資訊
      dao.save(user);
   }
}

UserServiceImpl業務類中有一個addNewUser()方法,實現使用者業務的新增。可以發現,在該方法中並沒有實現日誌輸出功能,接下來就以AOP的方式為該方法新增日誌功能。編寫增強類

package aop;

import java.util.Arrays;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;

public class UserServiceLogger {
   private static final Logger log = Logger.getLogger(UserServiceLogger.class);

   public void before(JoinPoint jp) {
       log.info("呼叫 " + jp.getTarget() + " 的 " + jp.getSignature().getName()
               + " 方法。方法入參:" + Arrays.toString(jp.getArgs()));
   }
   
   public void afterReturning(JoinPoint jp, Object result) {
       log.info("呼叫 " + jp.getTarget() + " 的 " + jp.getSignature().getName()
               + " 方法。方法返回值:" + result);
   }

}

UserServiceLogger類中定義了before()和afterReturning()兩個方法。我們希望把before()方法作為前置增強使用,即將該方法新增到目標方法之前執行;把afterReturning()方法作為後置增強使用,即將該方法新增到目標方法正常返回之後執行。這裡先以前置增強和後置增強為例

為了能夠在增強方法中獲得當前連線點的資訊,以便實施相關的判斷和處理,可以在增強方法中宣告一個JoinPoint型別的引數,Spring會自動注入例項。通過例項的getTarget()方法得到被代理的目標物件,通過getSignature()方法返回被代理的目標方法,通過getArgs()方法返回傳遞給目標方法的引數陣列。對於實現後置增強的afterReturning()方法,還可以定義一個引數用於接收目標方法的返回值。

在Spring配置檔案中對相關元件進行宣告。

<bean id="dao" class="dao.impl.UserDaoImpl"></bean>
<bean id="service" class="service.impl.UserServiceImpl">
    <property name="dao" ref="dao"></property>
</bean>
<!-- 宣告增強方法所在的Bean -->
<bean id="theLogger" class="aop.UserServiceLogger"></bean>

接下來在Spring配置檔案中進行AOP相關的配置,首先定義切入點

<?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-3.2.xsd
   http://www.springframework.org/schema/aop
   http://www.springframework.org/schema/aop/spring-aop-3.2.xsd">
    <bean id="dao" class="dao.impl.UserDaoImpl"></bean>
    <bean id="service" class="service.impl.UserServiceImpl">
        <property name="dao" ref="dao"></property>
    </bean>
    <!-- 宣告增強方法所在的Bean -->
    <bean id="theLogger" class="aop.UserServiceLogger"></bean>
    <!-- 配置切面 -->
    <aop:config>
        <!-- 定義切入點 -->
        <aop:pointcut id="pointcut"
            expression="execution(public void addNewUser(entity.User))" />
        <!-- 引用包含增強方法的Bean -->
        <aop:aspect ref="theLogger">
            <!-- 將before()方法定義為前置增強並引用pointcut切入點 -->
            <aop:before method="before" pointcut-ref="pointcut"></aop:before>
            <!-- 將afterReturning()方法定義為後置增強並引用pointcut切入點 -->
            <!-- 通過returning屬性指定為名為result的引數注入返回值 -->
            <aop:after-returning method="afterReturning"
                pointcut-ref="pointcut" returning="result" />
        </aop:aspect>
    </aop:config>
</beans>

注意

在元素中需要新增aop的名稱空間,以匯入與AOP相關的標籤。

與AOP相關的配置都放在aop:config標籤中,如配置切入點的標籤aop:pointcut。aop:pointcut的expression屬性可以配置切入點表示式,

execution(public void addNewUser(entity.User))

execution是切入點指示符,括號中是一個切入點表示式,用於配置需要切入增強處理的方法的特徵。切入點表示式支援模糊匹配,下面介紹幾種常用的模糊匹配。

➢ public * addNewUser(entity.User): “*”表示匹配所有型別的返回值。

➢ public void (entity.User): “”表示匹配所有方法名。

➢ public void addNewUser(…): “…”表示匹配所有引數個數和型別。

➢ * com.service..(…):這個表示式匹配com.service包下所有類的所有方法。

➢ * com.service….(…):這個表示式匹配com.service包及其子包下所有類的所有方法。

具體使用時可以根據自己的需求來設定切入點的匹配規則。當然,匹配的規則和關鍵字還有很多,可以參考Spring的開發手冊學習。

最後還需要在切入點處插入增強處理,這個過程的專業叫法是“織入”。實現織入的配置程式碼

<?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-3.2.xsd
   http://www.springframework.org/schema/aop
   http://www.springframework.org/schema/aop/spring-aop-3.2.xsd">
    <bean id="dao" class="dao.impl.UserDaoImpl"></bean>
    <bean id="service" class="service.impl.UserServiceImpl">
        <property name="dao" ref="dao"></property>
    </bean>
    <!-- 宣告增強方法所在的Bean -->
    <bean id="theLogger" class="aop.UserServiceLogger"></bean>
    <!-- 配置切面 -->
    <aop:config>
        <!-- 定義切入點 -->
        <aop:pointcut id="pointcut"
            expression="execution(public void addNewUser(entity.User))" />
        <!-- 引用包含增強方法的Bean -->
        <aop:aspect ref="theLogger">
            <!-- 將before()方法定義為前置增強並引用pointcut切入點 -->
            <aop:before method="before" pointcut-ref="pointcut"></aop:before>
            <!-- 將afterReturning()方法定義為後置增強並引用pointcut切入點 -->
            <!-- 通過returning屬性指定為名為result的引數注入返回值 -->
            <aop:after-returning method="afterReturning"
                pointcut-ref="pointcut" returning="result" />
        </aop:aspect>
    </aop:config>
</beans>

在aop:config中使用aop:aspect引用包含增強方法的Bean,然後分別通過aop:before和aop:after-returning將方法宣告為前置增強和後置增強,在aop:after-returning中通過returning屬性指定需要注入返回值的屬性名。方法的JoinPoint型別引數無須特殊處理,Spring會自動為其注入連線點例項。很明顯,UserService的addNewUser()方法可以和切入點pointcut相匹配,Spring會生成代理物件,在它執行前後分別呼叫before()和afterReturning()方法,這樣就完成了日誌輸出。

編寫測試程式碼

package test;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import service.UserService;
import service.impl.UserServiceImpl;

import entity.User;


public class AopTest {

    @Test
   public void aopTest() {
      ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
      UserService service = (UserService) ctx.getBean("service");
      
      User user = new User();
      user.setId(1);
      user.setUsername("test");
      user.setPassword("123456");
      user.setEmail("test@xxx.com");

      service.addNewUser(user);
   }

}

從以上示例可以看出,業務程式碼和日誌程式碼是完全分離的,經過AOP的配置以後,不做任何程式碼上的修改就在addNewUser()方法前後實現了日誌輸出。其實,只需稍稍修改切入點的指示符,不僅可以為UserService的addNewUser()方法增強日誌功能,也可以為所有業務方法進行增強;並且可以增強日誌功能,如實現訪問控制、事務管理、效能監測等實用功能。

本章總結

➢ Spring是一個輕量級的企業級框架,提供了IoC容器、AOP實現、DAO/ORM支援、Web整合等功能,目標是使現有的Java EE技術更易用,並形成良好的程式設計習慣。

➢ 依賴注入讓元件之間以配置檔案的形式組織在一起,而不是以硬編碼的方式耦合在一起。

➢ Spring配置檔案是完成組裝的主要場所,常用節點包括及其子節點。

➢ AOP的目的是從系統中分離出切面,將其獨立於業務邏輯實現,並在程式執行時織入程式中執行。

➢ 面向切面程式設計主要關心兩個問題:在什麼位置,執行什麼功能。

➢ 配置AOP主要使用aop名稱空間下的元素完成,可以完成定義切入點和織入增強等操作。

相關文章