<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          我們到底為什么要用 IoC 和 AOP

          共 8575字,需瀏覽 18分鐘

           ·

          2021-04-27 23:49

          來源 | RudeCrab

          作為一名 Java 開發(fā),對 Spring 框架是再熟悉不過的了。Spring 支持的控制反轉(zhuǎn)(Inversion of Control,縮寫為IoC)和面向切面編程(Aspect-oriented programming,縮寫為AOP)早已成為我們的開發(fā)習慣,仿佛 Java 開發(fā)天生就該如此。

          人總是會忽略習以為常的事物,所有人都熟練使用 IoC 和 AOP,卻鮮有人說得清楚到底為什么要用 IoC 和 AOP。

          技術肯定是為了解決某個問題而誕生,要弄清楚為什么使用 IoC 和 AOP,就得先弄清楚不用它們會碰到什么問題。

          IoC

          我們現(xiàn)在假設回到了沒有 IoC 的時代,用傳統(tǒng)的 Servlet 進行開發(fā)。

          傳統(tǒng)開發(fā)模式的弊端

          三層架構是經(jīng)典的開發(fā)模式,我們一般將視圖控制、業(yè)務邏輯和數(shù)據(jù)庫操作分別抽離出來單獨形成一個類,這樣各個職責就非常清晰且易于復用和維護。大致代碼如下:

          @WebServlet("/user")
          public class UserServlet extends HttpServlet {
              // 用于執(zhí)行業(yè)務邏輯的對象
              private UserService userService = new UserServiceImpl();
              
              @Override
              protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                  // ...省略其他代碼
                      
                  // 執(zhí)行業(yè)務邏輯
                  userService.doService();
                  
                  // ...返回頁面視圖
              }
          }
          public class UserServiceImpl implements UserService{
              // 用于操作數(shù)據(jù)庫的對象
              private UserDao userDao = new UserDaoImpl();
              
              @Override
              public void doService() {
                  // ...省略業(yè)務邏輯代碼
                      
                  // 執(zhí)行數(shù)據(jù)庫操作
                  userDao.doUpdate();
                  
                  // ...省略業(yè)務邏輯代碼
              }
          }
          public class UserDaoImpl implements UserDao{
              @Override
              public void doUpdate() {
                  // ...省略JDBC代碼
              }
          }

          上層依賴下層的抽象,代碼就分為了三層:

          業(yè)界普遍按這種分層方式組織代碼,其核心思想是職責分離。層次越低復用程度越高,比如一個 DAO 對象往往會被多個 Service 對象使用,一個 Service 對象往往也會被多個 Controller 對象使用:

          條理分明,井然有序。這些被復用的對象就像一個個的組件,供多方使用。

          雖然這個倒三角看上去非常漂亮,然而我們目前的代碼有一個比較大的問題,那就是我們只做到了邏輯復用,并沒有做到資源復用

          上層調(diào)用下一層時,必然會持有下一層的對象引用,即成員變量。目前我們每一個成員變量都會實例化一個對象,如下圖所示:

          每一個鏈路都創(chuàng)建了同樣的對象,造成了極大的資源浪費。本應多個 Controller 復用同一個 Service,多個 Service 復用同一個 DAO。現(xiàn)在變成了一個 Controller創(chuàng)建多個重復的 Service,多個 Service 又創(chuàng)建了多個重復的 DAO,從倒三角變成了正三角。

          許多組件只需要實例化一個對象就夠了,創(chuàng)建多個沒有任何意義。針對對象重復創(chuàng)建的問題,我們自然而然想到了單例模式。只要編寫類時都將其寫為單例,這樣就避免了資源浪費。但是,引入設計模式必然會帶來復雜性,況且還是每一個類都為單例,每一個類都會有相似的代碼,其弊端不言自明。

          有人可能會說,那我不在意“這點”資源浪費了,我服務器內(nèi)存大無所謂,我只求開發(fā)便捷痛快不想寫額外的代碼。

          確實,三層架構達到邏輯復用已經(jīng)非常方便了,還奢求其他的干什么呢。但就算不管資源問題,目前代碼還有一個致命缺陷,那就是變化的代價太大

          假設有 10 個 Controller 依賴了 UserService,最開始實例化的是 UserServiceImpl,后面需要換一個實現(xiàn)類 OtherUserServiceImpl,我就得逐個修改那 10 個 Controller,非常麻煩。更換實現(xiàn)類的需求可能不會太多,沒多大說服力。那咱們看另一個情況。

          之前咱們演示的組件創(chuàng)建過程非常簡單,new 一下就完了,可很多時候創(chuàng)建一個組件沒那么容易。比如 DAO 對象要依賴一個這樣的數(shù)據(jù)源組件:

          public class UserDaoImpl implements UserDao{
              private MyDataSource dataSource;

              public UserDaoImpl() {
                  // 構造數(shù)據(jù)源
                  dataSource = new MyDataSource("jdbc:mysql://localhost:3306/test""root""password");
                  // 進行一些其他配置
                  dataSource.setInitiaSize(10);
                  dataSource.setMaxActive(100);
                  // ...省略更多配置項
              }
          }

          該數(shù)據(jù)源組件要想真正生效需要對其進行許多配置,這個創(chuàng)建和配置過程是非常麻煩的。而且配置可能會隨著業(yè)務需求的變化經(jīng)常更改,這時候你就需要修改每一個依賴該組件的地方,牽一發(fā)而動全身。這還只是演示了一個數(shù)據(jù)源的創(chuàng)建配置過程,真實開發(fā)中可有太多組件和太多配置需要編碼了,其麻煩程度堪稱恐怖。

          當然,這些問題都可以引入設計模式來解決,不過這樣一來又繞回去了:設計模式本身也會帶來復雜性。這就像一種死循環(huán):傳統(tǒng)開發(fā)模式編碼復雜,要想解決這種復雜卻得陷入另一種復雜。難道沒有辦法解決了嗎?當然不是的,在講優(yōu)秀解決方案前,我們先來梳理一下目前出現(xiàn)的問題:

          • 創(chuàng)建了許多重復對象,造成大量資源浪費;

          • 更換實現(xiàn)類需要改動多個地方;

          • 創(chuàng)建和配置組件工作繁雜,給組件調(diào)用方帶來極大不便。

          透過現(xiàn)象看本質(zhì),這些問題的出現(xiàn)都是同一個原因:組件的調(diào)用方參與了組件的創(chuàng)建和配置工作

          其實調(diào)用方只需關注組件如何調(diào)用,至于這個組件如何創(chuàng)建和配置又與調(diào)用方有什么關系呢?就好比我去餐館只需點菜,飯菜并不需要我親自去做,餐館自然會做好給我送過來。如果我們編碼時,有一個「東西」能幫助我們創(chuàng)建和配置好那些組件,我們只負責調(diào)用該多好。這個「東西」就是容器。

          容器這一概念我們已接觸過,Tomcat 就是 Servlet 的容器,它幫我們創(chuàng)建并配置好 Servlet,我們只需編寫業(yè)務邏輯即可。試想一下,如果 Servlet 要我們自己創(chuàng)建,HttpRequest、HttpResponse 對象也需要我們自己配置,那代碼量得有多恐怖。

          Tomcat 是 Servlet 容器,只負責管理 Servlet。我們平常使用的組件則需要另一種容器來管理,這種容器我們稱之為 IoC 容器

          控制反轉(zhuǎn)和依賴注入

          控制反轉(zhuǎn),是指對象的創(chuàng)建和配置的控制權從調(diào)用方轉(zhuǎn)移給容器。好比在家自己做菜,菜的味道全部由自己控制;去餐館吃飯,菜的味道則是交由餐館控制。IoC 容器就擔任了餐館的角色。

          有了 IoC 容器,我們可以將對象交由容器管理,交由容器管理后的對象稱之為 Bean。調(diào)用方不再負責組件的創(chuàng)建,要使用組件時直接獲取 Bean 即可:

          @Component
          public class UserServiceImpl implements UserService{
              @Autowired // 獲取 Bean
              private UserDao userDao;
          }

          調(diào)用方只需按照約定聲明依賴項,所需要的 Bean 就自動配置完畢了,就好像在調(diào)用方外部注入了一個依賴項給其使用,所以這種方式稱之為 依賴注入(Dependency Injection,縮寫為 DI)。控制反轉(zhuǎn)和依賴注入是一體兩面,都是同一種開發(fā)模式的表現(xiàn)形式

          IoC 輕而易舉地解決了我們剛剛總結的問題:

          對象交由容器管理后,默認是單例的,這就解決了資源浪費問題。

          若要更換實現(xiàn)類,只需更改 Bean 的聲明配置,即可達到無感知更換:

          public class UserServiceImpl implements UserService{
              ...
          }

          // 將該實現(xiàn)類聲明為 Bean
          @Component
          public class OtherUserServiceImpl implements UserService{
              ...
          }

          現(xiàn)在組件的使用和組件的創(chuàng)建與配置完全分離開來。調(diào)用方只需調(diào)用組件而無需關心其他工作,這極大提高了我們的開發(fā)效率,也讓整個應用充滿了靈活性、擴展性。

          這樣看來,我們?nèi)绱酥幸?IoC 不是沒有道理的。

          AOP

          我們再來假設沒有 AOP 會怎樣。

          面向?qū)ο蟮木窒扌?/span>

          面向?qū)ο缶幊蹋∣bject-oriented programming,縮寫:OOP)三大特性:封裝、繼承、多態(tài),我們早已用得爐火純青。OOP 的好處已無需贅言,相信大家都有體會,這里咱們來看一下 OOP 的局限性。

          當有重復代碼出現(xiàn)時,可以就將其封裝出來然后復用。我們通過分層、分包、分類來規(guī)劃不同的邏輯和職責,就像之前講解的三層架構。但這里的復用的都是核心業(yè)務邏輯,并不能復用一些輔助邏輯,比如:日志記錄、性能統(tǒng)計、安全校驗、事務管理,等等。這些邊緣邏輯往往貫穿你整個核心業(yè)務,傳統(tǒng) OOP 很難將其封裝:

          public class UserServiceImpl implements UserService {
              @Override
              public void doService() {
                  System.out.println("---安全校驗---");
                  System.out.println("---性能統(tǒng)計 Start---");
                  System.out.println("---日志打印 Start---");
                  System.out.println("---事務管理 Start---");

                  System.out.println("業(yè)務邏輯");

                  System.out.println("---事務管理 End---");
                  System.out.println("---日志打印 End---");
                  System.out.println("---性能統(tǒng)計 End---");
              }
          }

          為了方便演示,這里只用了打印語句,就算如此這代碼看著也很難受,而且這些邏輯是所有業(yè)務方法都要加上,想想都恐怖。

          OOP 是至上而下的編程方式,猶如一個樹狀圖,A調(diào)用B、B調(diào)用C,或者A繼承B、B繼承C。這種方式對于業(yè)務邏輯來說是合適的,通過調(diào)用或繼承以復用。而輔助邏輯就像一把閘刀橫向貫穿所有方法,如圖2-4所示:

          這一條條橫線仿佛切開了 OOP 的樹狀結構,猶如一個大蛋糕被切開多層,每一層都會執(zhí)行相同的輔助邏輯,所以大家將這些輔助邏輯稱為層面或者切面。

          代理模式用來增加或增強原有功能再適合不過了,但切面邏輯的難點不是不修改原有業(yè)務,而是對所有業(yè)務生效。對一個業(yè)務類增強就得新建一個代理類,對所有業(yè)務增強,每個類都要新建代理類,這無疑是一場災難。而且這里只是演示了一個日志打印的切面邏輯,如果我再加一個性能統(tǒng)計切面,就得新建一個切面代理類來代理日志打印的代理類,一旦切面多起來這個代理類嵌套就會非常深。

          面向切面編程(Aspect-oriented programming,縮寫為 AOP)正是為了解決這一問題而誕生的技術。

          面向切面編程

          AOP 不是 OOP 的對立面,它是對 OOP 的一種補充。OOP 是縱向的,AOP 是橫向的,兩者相結合方能構建出良好的程序結構。AOP 技術,讓我們能夠不修改原有代碼,便能讓切面邏輯在所有業(yè)務邏輯中生效

          我們只需聲明一個切面,寫上切面邏輯:

          @Aspect // 聲明一個切面
          @Component
          public class MyAspect {
              // 原業(yè)務方法執(zhí)行前
              @Before("execution(public void com.rudecrab.test.service.*.doService())")
              public void methodBefore() {
                  System.out.println("===AspectJ 方法執(zhí)行前===");
              }

              // 原業(yè)務方法執(zhí)行后
              @AfterReturning("execution(* com.rudecrab.test.service..doService(..))")
              public void methodAddAfterReturning() {
                  System.out.println("===AspectJ 方法執(zhí)行后===");
              }
          }

          無論你有一個業(yè)務方法,還是一萬個業(yè)務方法,對我們開發(fā)者來說只需編寫一次切面邏輯,就能讓所有業(yè)務方法生效,極大提高了我們的開發(fā)效率。

          總結

          IoC 解決了以下問題:

          • 創(chuàng)建了許多重復對象,造成大量資源浪費;

          • 更換實現(xiàn)類需要改動多個地方;

          • 創(chuàng)建和配置組件工作繁雜,給組件調(diào)用方帶來極大不便。

          AOP 解決了以下問題:

          • 切面邏輯編寫繁瑣,有多少個業(yè)務方法就需要編寫多少次。


          1、全網(wǎng)最全 Java 日志框架適配方案!還有誰不會?
          2、Chrome瀏覽器最新高危漏洞曝光!升級最新版也沒用~
          3、Spring中毒太深,離開Spring我居然連最基本的接口都不會寫了
          4、黑客用GitHub服務器挖礦,三天跑了3萬個任務,代碼驚現(xiàn)中文
          5、瀏覽器輸入「xxxxhub」的背后.....
          6、Gradle真能干掉Maven?今天體驗了一把,賊爽!
          7、如何重構千行“又臭又長”的類?IntelliJ IDEA 幾分鐘就搞定!

          點分享

          點收藏

          點點贊

          點在看

          瀏覽 50
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  91福利网址 | 涩婷婷| 国产大黄片久久久久久 | 欧美人妻中文字幕久久久苍井空 | 美女性日日爱 |