spring 源碼分析(一)Core

spring 源碼分析 -- 1 -- core 核心簡介

sschrodinger

2019/03/04


Spring 框架簡介


spring 框架是 Java 開發(fā)中最著名的輕量級框架,使用各種方式簡化 Java 程序的開發(fā)。spring 采取了如下4個關(guān)鍵策略來簡化程序的開發(fā)。

  1. 基于POJO的輕量級和最小侵入性編程
  2. 通過依賴注入和面向接口實現(xiàn)松耦合
  3. 基于切面和慣例進(jìn)行聲明式編程
  4. 通過切面和模板減少樣板式代碼

基于POJO的輕量級和最小侵入性編程

想象在沒有 spring 框架時,我們構(gòu)建 web 程序,需要顯式的繼承 HttpServlet 并且改寫 doGet 等函數(shù)。樣例代碼如下:

class MyServlet extends HttpServlet {

    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
                    throws ServletException, IOException {
        PrintWriter writer = resp.getWriter();
        writer.writer("Hello,world");
        writer.flush();
    }
    
     protected void doPost(HttpServletRequest req, HttpServletResponse resp)
                    throws ServletException, IOException {
        doGet(req, resp);
    }
    
}

這樣的程序和 servlet 協(xié)議緊耦合,必須要實現(xiàn) HttpServlet 接口才能夠編寫程序。基于POJO的輕量級和最小侵入性編程的基本思想就是使用簡單的 Java 語言( Plain Old Java Object),實現(xiàn)功能的實現(xiàn)。比如說,我們使用 spring 框架搭建 servlet,就可以寫成如下形式:

@Controller
@RequestMapping("/")
public class MyController {
    @RequestMapping(method = RequestMethod.GET)
    public String hello() {
        return "hello";
    }
}

note

  • POJO stands for Plain Old Java Object. It is an ordinary Java object, not bound by any special restriction other than those forced by the Java Language Specification and not requiring any class path.
  1. Extend prespecified classes, Ex: public class GFG extends javax.servlet.http.HttpServlet { … } is not a POJO class.
  2. Implement prespecified interfaces, Ex: public class Bar implements javax.ejb.EntityBean { … } is not a POJO class.
  3. Contain prespecified annotations, Ex: @javax.persistence.Entity public class Baz { … } is not a POJO class.

通過依賴注入和面向接口實現(xiàn)松耦合

IoC控制反轉(zhuǎn)的實現(xiàn)方式是依賴注入。正常的 Java 程序中,我們在代碼中使用新建對象的方式獲得對象實例,但是在一些情況下,我們不知道使用具體的哪個類新建獲得實例對象,比如說對數(shù)據(jù)庫的訪問。這個時候我們就需要定義接口,并在運(yùn)行時自動選擇具體的實現(xiàn)。IoC 將類的新建托付給第三方,并讓第三方在運(yùn)行時自動選擇需要創(chuàng)建的具體類實現(xiàn)(反射機(jī)制)。

基于切面和慣例進(jìn)行聲明式編程

面向切面編程,即 AOP,是通過預(yù)編譯方式和運(yùn)行期動態(tài)代理實現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。

想象在一個大型教育系統(tǒng)中,我們提供講師服務(wù),學(xué)生服務(wù)等服務(wù),為了方便管理,我們需要為每一個服務(wù)添加日志接口,但是日志接口并不是各種服務(wù)的主要功能,為了松耦合,我們需要將日志服務(wù)單獨出來。

在緊耦合的代碼中,可能代碼的結(jié)構(gòu)如下:

public class StudentService {

    public Logger logger;

    service() {
        try {
            logger.logBefore();
            //do some service
            logger.logAfter();
        } catch(Exception e) {
            logError();
        }
    }
}

public class Logger {

    public void logError() {
        //log error
    }
    public void logBefore() {
        //log before service
    }
    public void logAfter() {
        //log after
    }
    
}

在如上所示的代碼中,StudentService 的 service 實現(xiàn),將 日志記錄代碼也寫了進(jìn)去。但是日志記錄并不應(yīng)該出現(xiàn)在 service 的實現(xiàn)中,而是應(yīng)該將 service 作為一個切點,將日志記錄的功能添加在這個切點中,通常的做法是使用 xml 等配置文件進(jìn)行配置,如下所示:

public class StudentService {

    service() {
        //do some service
    }
}

public class Logger {

    public void logError() {
        //log error
    }
    public void logBefore() {
        //log before service
    }
    public void logAfter() {
        //log after
    }
    
}
<配置>
<h>在 service 之前執(zhí)行Logger.logBefore()</h>
<h>在 service 之后執(zhí)行Logger.logAfter()</h>
<h>在 service 錯誤之后執(zhí)行Logger.logError()</h>
</配置>

相當(dāng)于框架對 service 函數(shù)進(jìn)行了包裝。

通過切面和模板減少樣板式代碼

一些程序的代碼會多次使用,比如說數(shù)據(jù)庫的登陸等代碼,spring 提供這些代碼的模板,可以讓人更加專注于實現(xiàn)自己的邏輯代碼。


Spring 核心技術(shù)


Spring IOC

在我們的日常開發(fā)中,創(chuàng)建對象的操作隨處可見以至于對其十分熟悉的同時又感覺十分繁瑣,每次需要對象都需要親手將其new出來,甚至某些情況下由于壞編程習(xí)慣還會造成對象無法被回收,這是相當(dāng)糟糕的。但更為嚴(yán)重的是,我們一直倡導(dǎo)的松耦合,少入侵原則,這種情況下變得一無是處。于是前輩們開始謀求改變這種編程陋習(xí),考慮如何使用編碼更加解耦合,由此而來的解決方案是面向接口的編程,于是便有了如下寫法:

public class BookServiceImpl {

 //class
    private  BookDaoImpl bookDaoImpl;
    public void oldCode(){
     //原來的做法
        bookDaoImpl=new bookDaoImpl();
        bookDaoImpl.getAllCategories();
    }

 }
 //=================new====================

public class BookServiceImpl {

 //interface
    private BookDao bookDao;

    public void newCode(){
     //變?yōu)槊嫦蚪涌诰幊?        bookDao=new bookDaoImpl();
        bookDao.getAllCategories();
    }
}

BookServiceImpl 類中由原來直接與 BookDaoImpl 打交互變?yōu)?BookDao,即使 BookDao 最終實現(xiàn)依然是 BookDaoImp,這樣的做的好處是顯而易見的,所有調(diào)用都通過接口bookDao 來完成,而接口的真正的實現(xiàn)者和最終的執(zhí)行者就是 BookDaoImpl,當(dāng)替換 bookDaoImpl 類,也只需修 改bookDao 指向新的實現(xiàn)類。

image

雖然上述的代碼在很大程度上降低了代碼的耦合度,但是代碼依舊存在入侵性和一定程度的耦合性,比如在修改 bookDao 的實現(xiàn)類時,仍然需求修改 BookServiceImpl 的內(nèi)部代碼,當(dāng)依賴的類多起來時,查找和修改的過程也會顯得相當(dāng)糟糕,因此我們?nèi)孕枰獙ふ乙环N方式,它可以令開發(fā)者在無需觸及 BookServiceImpl 內(nèi)容代碼的情況下實現(xiàn)修改 bookDao 的實現(xiàn)類,以便達(dá)到最低的耦合度和最少入侵的目的。實際上存在一種稱為反射的編程技術(shù)可以協(xié)助解決上述問題,反射是一種根據(jù)給出的完整類名(字符串方式)來動態(tài)地生成對象,這種編程方式可以讓對象在生成時才決定到底是哪一種對象,因此可以這樣假設(shè),在某個配置文件,該文件已寫好 bookDaoImpl 類的完全限定名稱,通過讀取該文件而獲取到 bookDao 的真正實現(xiàn)類完全限定名稱,然后通過反射技術(shù)在運(yùn)行時動態(tài)生成該類,最終賦值給 bookDao 接口,也就解決了剛才的存在問題,這里為簡單演示,使用 properties 文件作為配置文件,className.properties 如下:

bookDao.name=com.zejian.spring.dao.BookDaoImpl

獲取該配置文件信息動態(tài)為bookDao生成實現(xiàn)類:

public class BookServiceImpl implements BookService
{
    //讀取配置文件的工具類
    PropertiesUtil propertiesUtil = new PropertiesUtil("conf/className.properties");

    private BookDao bookDao;

    public void DaymicObject() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        //獲取完全限定名稱
        String className=propertiesUtil.get("bookDao.name");
        //通過反射
        Class c=Class.forName(className);
        //動態(tài)生成實例對象
        bookDao= (BookDao) c.newInstance();
    }
}

的確如我們所愿生成了 bookDao 的實例,這樣做的好處是在替換 bookDao 實現(xiàn)類的情況只需修改配置文件的內(nèi)容而無需觸及 BookServiceImpl 的內(nèi)部代碼,從而把代碼修改的過程轉(zhuǎn)到配置文件中,相當(dāng)于 BookServiceImpl 及其內(nèi)部的 bookDao 通過配置文件與 bookDao 的實現(xiàn)類進(jìn)行關(guān)聯(lián),這樣 BookServiceImpl 與 bookDao 的實現(xiàn)類間也就實現(xiàn)了解耦合,當(dāng)然 BookServiceImpl 類中存在著 BookDao 對象是無法避免的,畢竟這是協(xié)同工作的基礎(chǔ),我們只能最大程度去解耦合。

image

了解了上述的問題再來理解IOC就顯得簡單多了。Spring IOC 也是一個 Java 對象,在某些特定的時間被創(chuàng)建后,可以進(jìn)行對其他對象的控制,包括初始化、創(chuàng)建、銷毀等。簡單地理解,在上述過程中,我們通過配置文件配置了 BookDaoImpl 實現(xiàn)類的完全限定名稱,然后利用反射在運(yùn)行時為 BookDao 創(chuàng)建實際實現(xiàn)類,包括 BookServiceImpl 的創(chuàng)建,Spring 的 IOC 容器都會幫我們完成,而我們唯一要做的就是把需要創(chuàng)建的類和其他類依賴的類以配置文件的方式告訴IOC容器需要創(chuàng)建那些類和注入哪些類即可。Spring 通過這種控制反轉(zhuǎn)(IoC)的設(shè)計模式促進(jìn)了松耦合,這種方式使一個對象依賴其它對象時會通過被動的方式傳送進(jìn)來(如 BookServiceImpl 被創(chuàng)建時,其依賴的 BookDao 的實現(xiàn)類也會同時被注入 BookServiceImpl 中),而不是通過手動創(chuàng)建這些類。我們可以把IoC模式看做是工廠模式的升華,可以把IoC看作是一個大工廠,只不過這個大工廠里要生成的對象都是在配置文件(XML)中給出定義的,然后利用 Java 的反射技術(shù),根據(jù) XML 中給出的類名生成相應(yīng)的對象。從某種程度上來說,IoC 相當(dāng)于把在工廠方法里通過硬編碼創(chuàng)建對象的代碼,改變?yōu)橛?XML 文件來定義,也就是把工廠和對象生成這兩者獨立分隔開來,目的就是提高靈活性和可維護(hù)性,更是達(dá)到最低的耦合度,因此我們要明白所謂為的 IOC 就將對象的創(chuàng)建權(quán),交由 Spring 完成,從此解放手動創(chuàng)建對象的過程,同時讓類與類間的關(guān)系到達(dá)最低耦合度。

快速入門案例

理解了 Spring IOC 模式(容器)后,我們來看一個簡單入門實例。使用 Spring 的 IOC 功能,必須先引入 Spring 的核心依賴包(使用 Maven 作為構(gòu)建工具):

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-core</artifactId>
  <version>${spring.version}</version>
</dependency>

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-beans</artifactId>
  <version>${spring.version}</version>
</dependency>

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>${spring.version}</version>
</dependency>

然后創(chuàng)建 Dao 層(AccountDao):

public interface AccountDao {
    void addAccount();
}

實現(xiàn)類(AccountDaoImpl):

public class AccountDaoImpl implements AccountDao{
    @Override
    public void addAccount() {
        System.out.println("addAccount....");
    }
}

再創(chuàng)建 Service,AccountService

public interface AccountService {
    void doSomething();
}

實現(xiàn)類:

public class AccountServiceImpl implements AccountService {

    /**
     * 需要注入的對象
     */
    private AccountDao accountDao;

    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }

    @Override
    public void doSomething() {
        System.out.println("AccountServiceImpl#doSomething......");
        accountDao.addAccount();
    }
}

上面我們創(chuàng)建了 Dao 層和 Service 層的接口類及其實現(xiàn)類,其中 Service 層的操作依賴于 Dao 層,下面通過 Spring 的 IOC 容器幫助我們創(chuàng)建并注入這些類。IOC 使用的是 XML 配置文件,代碼如下:

<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
        ">
    <!-- 聲明accountDao對象,交給spring創(chuàng)建 -->
    <bean name="accountDao" class="com.zejian.spring.springIoc.dao.impl.AccountDaoImpl"/>
    <!-- 聲明accountService對象,交給spring創(chuàng)建 -->
    <bean name="accountService" class="com.zejian.spring.springIoc.service.impl.AccountServiceImpl">
          <!-- 注入accountDao對象,需要set方法-->
          <property name="accountDao" ref="accountDao"/>
    </bean>

</beans>

從 xml 文件中,我們需要聲明一個beans的頂級標(biāo)簽,同時需要引入核心命名空間,Spring 的功能在使用時都需要聲明相對應(yīng)的命名空間,上述的命名空間是最基本的。然后通過 bean 子標(biāo)簽聲明那些需要IOC容器幫助我們創(chuàng)建的類,其中 name 是指明 IOC 創(chuàng)建后該對象的名稱(當(dāng)然也可以使用 id 替換 name,這個后面會講到),class 則是告訴 IOC 這個類的完全限定名稱,IOC 就會通過這組信息利用反射技術(shù)幫助我們創(chuàng)建對應(yīng)的類對象,如下:

<!-- 聲明accountDao對象,交給spring創(chuàng)建 -->
<bean name="accountDao" class="com.zejian.spring.springIoc.dao.impl.AccountDaoImpl"/>

接著我們還看到如下聲明,accountService 聲明中多出了一個 property 的標(biāo)簽,這個標(biāo)簽指向了我們剛才創(chuàng)建的 accountDao 對象,它的作用是把 accountDao 對象傳遞給 accountService 實現(xiàn)類中的 accountDao 屬性,該屬性必須擁有 set 方法才能注入成功,我們把這種往類 accountService 對象中注入其他對象(accountDao)的操作稱為依賴注入,這個后面會分析到,其中的 name 必須與 AccountService 實現(xiàn)類中變量名稱相同,到此我們就完成對需要創(chuàng)建的對象聲明。接著看看如何使用它們。

<!-- 聲明accountService對象,交給spring創(chuàng)建 -->
<bean name="accountService" class="com.zejian.spring.springIoc.service.impl.AccountServiceImpl">
  <!-- 注入accountDao對象,需要set方法-->
  <property name="accountDao" ref="accountDao"/>
</bean>

使用這些類需要利用 Spring 提供的核心類,ApplicationContext,通過該類去加載已聲明好的配置文件,然后便可以獲取到我們需要的類了。

public void testByXml() throws Exception {
    //加載配置文件
    ApplicationContext applicationContext=new ClassPathXmlApplicationContext("spring/spring-ioc.xml");

//        AccountService accountService=applicationContext.getBean("accountService",AccountService.class);
    //多次獲取并不會創(chuàng)建多個accountService對象,因為spring默認(rèn)創(chuàng)建是單實例的作用域
    AccountService accountService= (AccountService) applicationContext.getBean("accountService");
    accountService.doSomething();
}
循環(huán)依賴

如下有兩個 bean,A 和 B,這兩個 bean 通過構(gòu)造函數(shù)互為依賴,這種情況下 Spring 容器將無法實例化這兩個 bean。

public class A{
    private B b;
    public A(B b){
     this.b=b;
    }
}

public class B{
    private A a;
    public B(A a){
     this.a=a;
    }
}
<bean id="a" class="com.zejian.springioc.pojo.A">
    <constructor-arg ref="b" />
</bean>

<bean id="b" class="com.zejian.springioc.pojo.B">
    <constructor-arg ref="a" />
</bean>

這是由于 A 被創(chuàng)建時,希望 B 被注入到自身,然而,此時 B 還有沒有被創(chuàng)建,而且 B 也依賴于 A,這樣將導(dǎo)致 Spring 容器左右為難,無法滿足兩方需求,最后腦袋奔潰,拋出異常。解決這種困境的方式是使用 Setter 依賴,但還是會造成一些不必要的困擾,因此,強(qiáng)烈不建議在配置文件中使用循環(huán)依賴

AOP 編程

面向切面編程,使得代碼能夠更加專注于該業(yè)務(wù)的邏輯。

比如說日志系統(tǒng),許多系統(tǒng)模塊都會需要用到日志系統(tǒng),但是每個模塊如果都增加對日志的寫入會顯得邏輯代碼不清晰,因為日志代碼并不是該模塊的主要功能,如下偽代碼:

class Model {
    
    public void addUser(User user) {
        log.info("start...");
        try {
            add(user);
            log.info("add user success");
        } catch (IOException e) {
            log.err("add user error");
        }
    }
    
    Logger log = LoggerFactory.getInstance();
    
}

如上代碼展示了一個典型的日志系統(tǒng)的寫入功能,可以發(fā)現(xiàn)日志系統(tǒng)占了邏輯的一大塊,使得本身的邏輯不明顯。面向切面編程更加關(guān)注橫切面,即真正的業(yè)務(wù)邏輯,將日志記錄通過 class 文件修改或者動態(tài)代理等方式,實現(xiàn)日志記錄的功能。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容