真實案例測試你spring水平!

所有文章已遷移至csdn,csdn個人主頁https://blog.csdn.net/chaitoudaren

前言

近兩年網(wǎng)課上到處是《一天精通springboot》《2小時精通springboot》等課程,相信很多新手入門Java后端也都是從springboot開始的。2小時用搭建spring + mvc + mybatis后端常常讓人有一種架構(gòu)師的感覺,那么學完springboot到底還有必要學spring? 答案很負責任告訴你肯定是:有必要!簡單、開箱即用等特性讓springboot大受歡迎,但是請一定記住:一個框架越簡單易用,說明框架在背后為開發(fā)人員做了越多復(fù)雜的工作。在筆者眼里springboot只不過是spring的一個整合,整合了tomcat、spring mvc一些常用組件,而本質(zhì)依舊是spring

如果你還意識不到spring的重要性,以下有幾個很基礎(chǔ)的真實案例,如果發(fā)現(xiàn)不了案例問題的, 精通Spring 請再斟酌下要不要寫入你的簡歷中

在這里插入圖片描述

案例


難度★:單例(如: service, dao層)禁止使用非靜態(tài)成員變量

使用誤區(qū)

業(yè)務(wù)需求是需要導(dǎo)入10w張機票左右的Excel,PagService(單例)依賴ImportExcelListener(單例)進行Excel導(dǎo)入。使用到阿里的easyexcel,這里不討論該框架,只需要知道每讀取一行數(shù)據(jù)ImportExcelListener將調(diào)用invoke返回一行數(shù)據(jù),讀到末行將觸發(fā)doAfterAllAnalysed??紤]到數(shù)據(jù)量較大,每讀一行插入一次顯然不可行,10w行插入一次也太大,于是小A同學經(jīng)過評估后決定1000行插入一次

@Service
public class PagService extends BaseService<Pag> {
    @Autowired
    private ImportExcelListener importExcelListener;

    public void  improt(File file) {
        // 調(diào)用importExcelListener實現(xiàn)excel導(dǎo)入
        importExcelListener.read(file);
    }
}
@Component
public class ImportExcelListener extends AnalysisEventListener<Pag> {
    @Autowired
    private PagService pagService;
    
    // 用于保存1000張票
    List<Pag> list = new ArrayList<Pag>();

    @Override
    public void invoke(DemoData data, AnalysisContext context) {
        // 每獲取一張票將票加入list
        list.add(data);
        // 票數(shù)到達1000則批量保存
        if (list.size() >= 1000) {
            pagService.save(list);
            list.clear();
        }

    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 讀到最后一行則批量保存
        pagService.save(list);
        list.clear();
    }
}

造成BUG

此時的Web項目是正常使用的,但是將引發(fā)一個嚴重的BUG,線程不安全!1. 營業(yè)點A與營業(yè)點B同時導(dǎo)入數(shù)據(jù),A和B的數(shù)據(jù)將完全混在一起 2. 一旦程序出現(xiàn)異常,之后的Excel提交將永遠拋出唯一索引沖突異常

造成原因

ImportExcelListener@Component設(shè)置成了單例,同時使用到了非靜態(tài)成員變量List<Pag> list = new ArrayList<Pag>();這違反了單例的使用原則,造成線程不安全的問題。ImportExcelListener作為單例,也就是說在該Web項目中,將有且僅有一個實例。

  1. 當A、B營業(yè)處同時導(dǎo)入票證時,使用了同一個ImportExcelListener的同一個list,當A導(dǎo)入向list寫入數(shù)據(jù)時B也同時在同一個list寫入,因此導(dǎo)致A發(fā)現(xiàn)其導(dǎo)入的數(shù)據(jù)中雜糅著B的數(shù)據(jù)。
  2. 并且當導(dǎo)入數(shù)據(jù)發(fā)生異常拋出時,例如讀取到501行數(shù)據(jù)出現(xiàn)異常,但是list中將包含前500行數(shù)據(jù),且沒有被清空。而第二次導(dǎo)入因為ImportExcelListener為單例,緩存在Ioc容器中,因此ImportExcelListener還是原來那個ImportExcelListener,list也還是之前那個list,后續(xù)導(dǎo)入的數(shù)據(jù)又將1-500行加入list,導(dǎo)致永遠導(dǎo)入都是唯一索引沖突

難度★★:加上@Scope("Prototype")就是多例(原型模式)了嗎?

使用誤區(qū)

小A在發(fā)現(xiàn)問題后,也知道了問題的原因出在單例上,于是想當然的將上@Scope("Prototype"),把ImportExcelListener變成多例(也就是原型模式),不就解決問題了嗎?這樣的做法真的能解決問題嗎?

@Component
@Scope("Prototype")
public class ImportExcelListener extends AnalysisEventListener<Pag> {
    ...
}

造成BUG

小A改完代碼提交后,業(yè)務(wù)人員反應(yīng)BUG依然存在,并沒有解決

造成原因

BUG依舊存在,那么說明線程不安全的問題依舊存在,這說明多例同樣也是線程不安全的嗎?顯然不是,多例情況下每次請求使用的ImportExcelListener都是新的實例,不存在互相干擾的情況,也就沒有所謂的線程安全問題可言。那么問題出在哪里?主要問題出在,即使小A加了@Scope("Prototype")但是在本項目中ImportExcelListener依舊是單例。

原因是PagService依賴ImportExcelListener,PagService是單例ImportExcelListener是多例。當spring創(chuàng)建PagService時,發(fā)現(xiàn)其依賴ImportExcelListener,而ImportExcelListener是多例,因此新創(chuàng)建出importExcelListener@001對象并且把地址給PagService,創(chuàng)建完成后,PagService將被緩存到spring Ioc容器中,下次需要PagService時則直接從緩存中取。因此,在創(chuàng)建完成后,A售票處與B售票處實際上調(diào)用的PagService是同一個實例,而導(dǎo)致其引用的importExcelListener@001也是同一個,也就造成了即使加上@Scope("Prototype")卻還是同一個實例,因此線程不安全的BUG依舊存在。

這里貼一張單例setter的循環(huán)依賴流程圖,a為PagService,b為ImportExcelListener。當PagService第一次創(chuàng)建時,將走完1-17步驟完成PagService的創(chuàng)建。但是第二次再需要ImportExcelListener時,將在步驟1. 嘗試從各級緩存中獲取bean就會直接返回緩存中的PagService,而不會再去管ImportExcelListener是不是多例是不是需要重新創(chuàng)建。詳情請參考spring 循環(huán)依賴

單例setter循環(huán)依賴.jpg

難度★★★:那就讓PagService也變成多例!

使用誤區(qū)

小A同學發(fā)現(xiàn)錯誤后,最后發(fā)狠,那我就讓他們?nèi)慷甲兂啥嗬∵@總可以了吧?且先不論小A同學并不知道所有的spring mvc的controller都是單例(注:Struts框架的Action則是多例,這也是跟spring mvc最大的區(qū)別),并解決不了問題。但是出發(fā)點是好的,讓他們都是多例,這似乎解決了問題。代碼如下

@Component
@Scope("Prototype")
public class PagService extends BaseService<Pag> {
    @Autowired
    private ImportExcelListener importExcelListener;

    ...
}
@Component
@Scope("Prototype")
public class ImportExcelListener extends AnalysisEventListener<Pag> {
    @Autowired
    private PagService pagService;

    ...
}

造成BUG

如果你也覺得上面的代碼沒有問題,那么我們來看下結(jié)果,項目直接連跑都跑不起來了

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'pagService': Unsatisfied dependency expressed through field 'importExcelListener'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'importExcelListener': Unsatisfied dependency expressed through field 'pagService'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'pagService': Requested bean is currently in creation: Is there an unresolvable circular reference?

    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:667)
...

造成原因

其實從異常上面也看出來了,PagServiceImportExcelListener循環(huán)依賴了。小A表示很疑惑,我第一次第二次的代碼也是循環(huán)依賴啊,為什么第一次第二次就沒問題?這次就有問題了?這里先給出結(jié)論,spring僅可以解決單例setter方式注入的循環(huán)依賴問題,對于原型模式和單例構(gòu)造器注入模式都解決不了。

通俗一點來說,PagService為多例,也就是說每次獲取都需要是一個新的實例。而當PagService創(chuàng)建時,發(fā)現(xiàn)需要依賴ImportExcelListener也是多例,因此又新生產(chǎn)了ImportExcelListener實例,此時ImportExcelListener又發(fā)現(xiàn)需要依賴PagService,同時PagService是多例,便不會去緩存中取,而是又新建一個多例。造成的結(jié)果就是循環(huán)依賴死循環(huán),spring能做的就是幫你拋出異常...

spring循環(huán)依賴是面試最喜歡問的題目,如何檢測循環(huán)依賴?spring能解決哪些循環(huán)依賴?總的可以歸結(jié)成2點

  1. 必須提前曝光對象
  2. 曝光時機必須在實例化之后

缺一不可,關(guān)于循環(huán)依賴請參考spring 循環(huán)依賴


解決方案

就上述問題,歸納起來就是單例如何引用多例,使其在每次調(diào)用時都能保證使多例的問題。spring作為Java后端的元老,早就提供了解決方案,這里提供3種解決方案供參考:

  1. spring官方使用@Lookup或<lookup-method>標簽,解決單例引用多例的問題
  2. 阿里官方推薦,這種方法最暴力,ImportExcelListener直接不使用spring管理,也就是不加@Component標簽,讓用戶調(diào)用方法時,自己new一個。這種做法即完全脫離spring管理,用戶自己負責實例的生命周期
@Service
public class PagService extends BaseService<Pag> {
    public void  improt(File file) {
    ImportExcelListener importExcelListener = new ImportExcelListener(this);
    // 調(diào)用importExcelListener實現(xiàn)excel導(dǎo)入
        importExcelListener.read(file);
    }
}
  1. 實現(xiàn)BeanFactoryAware,從BeanFactory中獲取ImportExcelListener多例,以確保每次調(diào)用都新建一個。這種方案并不可取,他加大了代碼的耦合程度,只是提供給大家另外一種思路。對spring Aware感知器不熟悉的可以參考spring 生命周期
@Service
public class PagService extends BaseService<Pag> implements BeanFactoryAware{
    private BeanFactory beanFactory;
  
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }
  
    public void  improt(File file) {
    ImportExcelServic importExcelServic = (ImportExcelServic)beanFactory.getBean("importExcelServic", this);
    // 調(diào)用importExcelServic實現(xiàn)excel導(dǎo)入
        importExcelServic.read(file);
    }
}

總結(jié)

  1. spring單例絕對不能使用非靜態(tài)成員變量(靜態(tài)成員變量一個類只有一份,也就不存在線程安全問題)
  2. 單例依賴多例,想確保每次調(diào)用都使用全新的多例,需要使用@Lookup或lookup-method標簽
  3. spring只能解決單例下set注入但是的循環(huán)依賴問題

筆者也使用springboot,但是精通springboot并不是指2小時學幾個類似@Cacheable這樣的標簽就算精通了,如果是這樣相信我在面試中你會被錘的很慘。我的建議是好好學習spring,如果有想進大廠的同學最好能夠系統(tǒng)的學習一下spring 源碼,對代碼風格,設(shè)計思想都有很大幫助。畢竟大廠的面試官是不可能問出你會不會用@Cacheable、@Tranactional等標簽的,而是想讓你說出@Cacheable是如何通過AOP切面編程實現(xiàn)的、更甚是如何使用Jdk動態(tài)代理或者Cglib代理實現(xiàn)@Cacheable

最后編輯于
?著作權(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)容