面渣逆襲:Spring三十五問,四萬字+五十圖詳解

三分惡發表於2022-04-19

大家好,我是老三啊,面渣逆襲 繼續,這節我們來搞定另一個面試必問知識點——Spring。

有人說,“Java程式設計師都是Spring程式設計師”,老三不太贊成這個觀點,但是這也可以看出Spring在Java世界裡舉足輕重的作用。

基礎

1.Spring是什麼?特性?有哪些模組?

Spring Logo

一句話概括:Spring 是一個輕量級、非入侵式的控制反轉 (IoC) 和麵向切面 (AOP) 的框架。

2003年,一個音樂家Rod Johnson決定發展一個輕量級的Java開發框架,Spring作為Java戰場的龍騎兵漸漸崛起,並淘汰了EJB這個傳統的重灌騎兵。

Spring重要版本

到了現在,企業級開發的標配基本就是 Spring5 + Spring Boot 2 + JDK 8

Spring有哪些特性呢?

Spring有很多優點:

Spring特性

  1. IOCDI 的支援

Spring 的核心就是一個大的工廠容器,可以維護所有物件的建立和依賴關係,Spring 工廠用於生成 Bean,並且管理 Bean 的生命週期,實現高內聚低耦合的設計理念。

  1. AOP 程式設計的支援

Spring 提供了面向切面程式設計,可以方便的實現對程式進行許可權攔截、執行監控等切面功能。

  1. 宣告式事務的支援

支援通過配置就來完成對事務的管理,而不需要通過硬編碼的方式,以前重複的一些事務提交、回滾的JDBC程式碼,都可以不用自己寫了。

  1. 快捷測試的支援

Spring 對 Junit 提供支援,可以通過註解快捷地測試 Spring 程式。

  1. 快速整合功能

方便整合各種優秀框架,Spring 不排斥各種優秀的開源框架,其內部提供了對各種優秀框架(如:Struts、Hibernate、MyBatis、Quartz 等)的直接支援。

  1. 複雜API模板封裝

Spring 對 JavaEE 開發中非常難用的一些 API(JDBC、JavaMail、遠端呼叫等)都提供了模板化的封裝,這些封裝 API 的提供使得應用難度大大降低。

2.Spring有哪些模組呢?

Spring 框架是分模組存在,除了最核心的Spring Core Container是必要模組之外,其他模組都是可選,大約有 20 多個模組。

Spring模組劃分

最主要的七大模組:

  1. Spring Core:Spring 核心,它是框架最基礎的部分,提供 IOC 和依賴注入 DI 特性。
  2. Spring Context:Spring 上下文容器,它是 BeanFactory 功能加強的一個子介面。
  3. Spring Web:它提供 Web 應用開發的支援。
  4. Spring MVC:它針對 Web 應用中 MVC 思想的實現。
  5. Spring DAO:提供對 JDBC 抽象層,簡化了 JDBC 編碼,同時,編碼更具有健壯性。
  6. Spring ORM:它支援用於流行的 ORM 框架的整合,比如:Spring + Hibernate、Spring + iBatis、Spring + JDO 的整合等。
  7. Spring AOP:即面向切面程式設計,它提供了與 AOP 聯盟相容的程式設計實現。

3.Spring有哪些常用註解呢?

Spring有很多模組,甚至廣義的SpringBoot、SpringCloud也算是Spring的一部分,我們來分模組,按功能來看一下一些常用的註解:

Spring常用註解

Web:

  • @Controller:組合註解(組合了@Component註解),應用在MVC層(控制層)。
  • @RestController:該註解為一個組合註解,相當於@Controller和@ResponseBody的組合,註解在類上,意味著,該Controller的所有方法都預設加上了@ResponseBody。
  • @RequestMapping:用於對映Web請求,包括訪問路徑和引數。如果是Restful風格介面,還可以根據請求型別使用不同的註解:
    • @GetMapping
    • @PostMapping
    • @PutMapping
    • @DeleteMapping
  • @ResponseBody:支援將返回值放在response內,而不是一個頁面,通常使用者返回json資料。
  • @RequestBody:允許request的引數在request體中,而不是在直接連線在地址後面。
  • @PathVariable:用於接收路徑引數,比如@RequestMapping(“/hello/{name}”)申明的路徑,將註解放在引數中前,即可獲取該值,通常作為Restful的介面實現方法。
  • @RestController:該註解為一個組合註解,相當於@Controller和@ResponseBody的組合,註解在類上,意味著,該Controller的所有方法都預設加上了@ResponseBody。

容器:

  • @Component:表示一個帶註釋的類是一個“元件”,成為Spring管理的Bean。當使用基於註解的配置和類路徑掃描時,這些類被視為自動檢測的候選物件。同時@Component還是一個元註解。
  • @Service:組合註解(組合了@Component註解),應用在service層(業務邏輯層)。
  • @Repository:組合註解(組合了@Component註解),應用在dao層(資料訪問層)。
  • @Autowired:Spring提供的工具(由Spring的依賴注入工具(BeanPostProcessor、BeanFactoryPostProcessor)自動注入)。
  • @Qualifier:該註解通常跟 @Autowired 一起使用,當想對注入的過程做更多的控制,@Qualifier 可幫助配置,比如兩個以上相同型別的 Bean 時 Spring 無法抉擇,用到此註解
  • @Configuration:宣告當前類是一個配置類(相當於一個Spring配置的xml檔案)
  • @Value:可用在欄位,構造器引數跟方法引數,指定一個預設值,支援 #{} 跟 ${} 兩個方式。一般將 SpringbBoot 中的 application.properties 配置的屬性值賦值給變數。
  • @Bean:註解在方法上,宣告當前方法的返回值為一個Bean。返回的Bean對應的類中可以定義init()方法和destroy()方法,然後在@Bean(initMethod=”init”,destroyMethod=”destroy”)定義,在構造之後執行init,在銷燬之前執行destroy。
  • @Scope:定義我們採用什麼模式去建立Bean(方法上,得有@Bean) 其設定型別包括:Singleton 、Prototype、Request 、 Session、GlobalSession。

AOP:

  • @Aspect:宣告一個切面(類上) 使用@After、@Before、@Around定義建言(advice),可直接將攔截規則(切點)作為引數。
    • @After :在方法執行之後執行(方法上)。
    • @Before: 在方法執行之前執行(方法上)。
    • @Around: 在方法執行之前與之後執行(方法上)。
    • @PointCut: 宣告切點 在java配置類中使用@EnableAspectJAutoProxy註解開啟Spring對AspectJ代理的支援(類上)。

事務:

  • @Transactional:在要開啟事務的方法上使用@Transactional註解,即可宣告式開啟事務。

4.Spring 中應用了哪些設計模式呢?

Spring 框架中廣泛使用了不同型別的設計模式,下面我們來看看到底有哪些設計模式?

Spring中用到的設計模式

  1. 工廠模式 : Spring 容器本質是一個大工廠,使用工廠模式通過 BeanFactory、ApplicationContext 建立 bean 物件。
  2. 代理模式 : Spring AOP 功能功能就是通過代理模式來實現的,分為動態代理和靜態代理。
  3. 單例模式 : Spring 中的 Bean 預設都是單例的,這樣有利於容器對Bean的管理。
  4. 模板模式 : Spring 中 JdbcTemplate、RestTemplate 等以 Template結尾的對資料庫、網路等等進行操作的模板類,就使用到了模板模式。
  5. 觀察者模式: Spring 事件驅動模型就是觀察者模式很經典的一個應用。
  6. 介面卡模式 :Spring AOP 的增強或通知 (Advice) 使用到了介面卡模式、Spring MVC 中也是用到了介面卡模式適配 Controller。
  7. 策略模式:Spring中有一個Resource介面,它的不同實現類,會根據不同的策略去訪問資源。

IOC

5.說一說什麼是IOC?什麼是DI?

Java 是物件導向的程式語言,一個個例項物件相互合作組成了業務邏輯,原來,我們都是在程式碼裡建立物件和物件的依賴。

所謂的IOC(控制反轉):就是由容器來負責控制物件的生命週期和物件間的關係。以前是我們想要什麼,就自己建立什麼,現在是我們需要什麼,容器就給我們送來什麼。

引入IOC之前和引入IOC之後

也就是說,控制物件生命週期的不再是引用它的物件,而是容器。對具體物件,以前是它控制其它物件,現在所有物件都被容器控制,所以這就叫控制反轉

控制反轉示意圖

DI(依賴注入):指的是容器在例項化物件的時候把它依賴的類注入給它。有的說法IOC和DI是一回事,有的說法是IOC是思想,DI是IOC的實現。

為什麼要使用IOC呢?

最主要的是兩個字解耦,硬編碼會造成物件間的過度耦合,使用IOC之後,我們可以不用關心物件間的依賴,專心開發應用就行。

6.能簡單說一下Spring IOC的實現機制嗎?

PS:這道題老三在面試中被問到過,問法是“你有自己實現過簡單的Spring嗎?

Spring的IOC本質就是一個大工廠,我們想想一個工廠是怎麼執行的呢?

工廠執行

  • 生產產品:一個工廠最核心的功能就是生產產品。在Spring裡,不用Bean自己來例項化,而是交給Spring,應該怎麼實現呢?——答案毫無疑問,反射

    那麼這個廠子的生產管理是怎麼做的?你應該也知道——工廠模式

  • 庫存產品:工廠一般都是有庫房的,用來庫存產品,畢竟生產的產品不能立馬就拉走。Spring我們都知道是一個容器,這個容器裡存的就是物件,不能每次來取物件,都得現場來反射建立物件,得把建立出的物件存起來。

  • 訂單處理:還有最重要的一點,工廠根據什麼來提供產品呢?訂單。這些訂單可能五花八門,有線上籤籤的、有到工廠籤的、還有工廠銷售上門籤的……最後經過處理,指導工廠的出貨。

    在Spring裡,也有這樣的訂單,它就是我們bean的定義和依賴關係,可以是xml形式,也可以是我們最熟悉的註解形式。

我們簡單地實現一個mini版的Spring IOC:

mini版本Spring IOC

Bean定義:

Bean通過一個配置檔案定義,把它解析成一個型別。

  • beans.properties

    偷懶,這裡直接用了最方便解析的properties,這裡直接用一個<key,value>型別的配置來代表Bean的定義,其中key是beanName,value是class

    userDao:cn.fighter3.bean.UserDao
    
  • BeanDefinition.java

    bean定義類,配置檔案中bean定義對應的實體

    public class BeanDefinition {
    
        private String beanName;
    
        private Class beanClass;
         //省略getter、setter  
     }   
    
  • ResourceLoader.java

    資源載入器,用來完成配置檔案中配置的載入

    public class ResourceLoader {
    
        public static Map<String, BeanDefinition> getResource() {
            Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>(16);
            Properties properties = new Properties();
            try {
                InputStream inputStream = ResourceLoader.class.getResourceAsStream("/beans.properties");
                properties.load(inputStream);
                Iterator<String> it = properties.stringPropertyNames().iterator();
                while (it.hasNext()) {
                    String key = it.next();
                    String className = properties.getProperty(key);
                    BeanDefinition beanDefinition = new BeanDefinition();
                    beanDefinition.setBeanName(key);
                    Class clazz = Class.forName(className);
                    beanDefinition.setBeanClass(clazz);
                    beanDefinitionMap.put(key, beanDefinition);
                }
                inputStream.close();
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
            return beanDefinitionMap;
        }
    
    }
    
  • BeanRegister.java

    物件註冊器,這裡用於單例bean的快取,我們大幅簡化,預設所有bean都是單例的。可以看到所謂單例註冊,也很簡單,不過是往HashMap裡存物件。

    public class BeanRegister {
    
        //單例Bean快取
        private Map<String, Object> singletonMap = new HashMap<>(32);
    
        /**
         * 獲取單例Bean
         *
         * @param beanName bean名稱
         * @return
         */
        public Object getSingletonBean(String beanName) {
            return singletonMap.get(beanName);
        }
    
        /**
         * 註冊單例bean
         *
         * @param beanName
         * @param bean
         */
        public void registerSingletonBean(String beanName, Object bean) {
            if (singletonMap.containsKey(beanName)) {
                return;
            }
            singletonMap.put(beanName, bean);
        }
    
    }
    
  • BeanFactory.java

    BeanFactory

    • 物件工廠,我們最核心的一個類,在它初始化的時候,建立了bean註冊器,完成了資源的載入。

    • 獲取bean的時候,先從單例快取中取,如果沒有取到,就建立並註冊一個bean

      public class BeanFactory {
      
          private Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>();
      
          private BeanRegister beanRegister;
      
          public BeanFactory() {
              //建立bean註冊器
              beanRegister = new BeanRegister();
              //載入資源
              this.beanDefinitionMap = new ResourceLoader().getResource();
          }
      
          /**
           * 獲取bean
           *
           * @param beanName bean名稱
           * @return
           */
          public Object getBean(String beanName) {
              //從bean快取中取
              Object bean = beanRegister.getSingletonBean(beanName);
              if (bean != null) {
                  return bean;
              }
              //根據bean定義,建立bean
              return createBean(beanDefinitionMap.get(beanName));
          }
      
          /**
           * 建立Bean
           *
           * @param beanDefinition bean定義
           * @return
           */
          private Object createBean(BeanDefinition beanDefinition) {
              try {
                  Object bean = beanDefinition.getBeanClass().newInstance();
                  //快取bean
                  beanRegister.registerSingletonBean(beanDefinition.getBeanName(), bean);
                  return bean;
              } catch (InstantiationException | IllegalAccessException e) {
                  e.printStackTrace();
              }
              return null;
          }
      }
      
  • 測試

    • UserDao.java

      我們的Bean類,很簡單

      public class UserDao {
      
          public void queryUserInfo(){
              System.out.println("A good man.");
          }
      }
      
    • 單元測試

      public class ApiTest {
          @Test
          public void test_BeanFactory() {
              //1.建立bean工廠(同時完成了載入資源、建立註冊單例bean註冊器的操作)
              BeanFactory beanFactory = new BeanFactory();
      
              //2.第一次獲取bean(通過反射建立bean,快取bean)
              UserDao userDao1 = (UserDao) beanFactory.getBean("userDao");
              userDao1.queryUserInfo();
      
              //3.第二次獲取bean(從快取中獲取bean)
              UserDao userDao2 = (UserDao) beanFactory.getBean("userDao");
              userDao2.queryUserInfo();
          }
      }
      
    • 執行結果

      A good man.
      A good man.
      

至此,我們一個乞丐+破船版的Spring就完成了,程式碼也比較完整,有條件的可以跑一下。

PS:因為時間+篇幅的限制,這個demo比較簡陋,沒有面向介面、沒有解耦、邊界檢查、異常處理……健壯性、擴充套件性都有很大的不足,感興趣可以學習參考[15]。

7.說說BeanFactory和ApplicantContext?

可以這麼形容,BeanFactory是Spring的“心臟”,ApplicantContext是完整的“身軀”。

BeanFactory和ApplicantContext的比喻

  • BeanFactory(Bean工廠)是Spring框架的基礎設施,面向Spring本身。
  • ApplicantContext(應用上下文)建立在BeanFactoty基礎上,面向使用Spring框架的開發者。
BeanFactory 介面

BeanFactory是類的通用工廠,可以建立並管理各種類的物件。

Spring為BeanFactory提供了很多種實現,最常用的是XmlBeanFactory,但在Spring 3.2中已被廢棄,建議使用XmlBeanDefinitionReader、DefaultListableBeanFactory。

Spring5 BeanFactory繼承體系

BeanFactory介面位於類結構樹的頂端,它最主要的方法就是getBean(String var1),這個方法從容器中返回特定名稱的Bean。

BeanFactory的功能通過其它的介面得到了不斷的擴充套件,比如AbstractAutowireCapableBeanFactory定義了將容器中的Bean按照某種規則(比如按名字匹配、按型別匹配等)進行自動裝配的方法。

這裡看一個 XMLBeanFactory(已過期) 獲取bean 的例子:

public class HelloWorldApp{ 
   public static void main(String[] args) { 
      BeanFactory factory = new XmlBeanFactory (new ClassPathResource("beans.xml")); 
      HelloWorld obj = (HelloWorld) factory.getBean("helloWorld");    
      obj.getMessage();    
   }
}
ApplicationContext 介面

ApplicationContext由BeanFactory派生而來,提供了更多面向實際應用的功能。可以這麼說,使用BeanFactory就是手動檔,使用ApplicationContext就是自動檔。

Spring5 ApplicationContext部分體系類圖

ApplicationContext 繼承了HierachicalBeanFactory和ListableBeanFactory介面,在此基礎上,還通過其他的介面擴充套件了BeanFactory的功能,包括:

  • Bean instantiation/wiring

  • Bean 的例項化/串聯

  • 自動的 BeanPostProcessor 註冊

  • 自動的 BeanFactoryPostProcessor 註冊

  • 方便的 MessageSource 訪問(i18n)

  • ApplicationEvent 的釋出與 BeanFactory 懶載入的方式不同,它是預載入,所以,每一個 bean 都在 ApplicationContext 啟動之後例項化

這是 ApplicationContext 的使用例子:

public class HelloWorldApp{ 
   public static void main(String[] args) { 
      ApplicationContext context=new ClassPathXmlApplicationContext("beans.xml"); 
      HelloWorld obj = (HelloWorld) context.getBean("helloWorld");    
      obj.getMessage();    
   }
}

ApplicationContext 包含 BeanFactory 的所有特性,通常推薦使用前者。

8.你知道Spring容器啟動階段會幹什麼嗎?

PS:這道題老三面試被問到過

Spring的IOC容器工作的過程,其實可以劃分為兩個階段:容器啟動階段Bean例項化階段

其中容器啟動階段主要做的工作是載入和解析配置檔案,儲存到對應的Bean定義中。

容器啟動和Bean例項化階段

容器啟動開始,首先會通過某種途徑載入Congiguration MetaData,在大部分情況下,容器需要依賴某些工具類(BeanDefinitionReader)對載入的Congiguration MetaData進行解析和分析,並將分析後的資訊組為相應的BeanDefinition。

xml配置資訊對映註冊過程

最後把這些儲存了Bean定義必要資訊的BeanDefinition,註冊到相應的BeanDefinitionRegistry,這樣容器啟動就完成了。

9.能說一下Spring Bean生命週期嗎?

可以看看:Spring Bean生命週期,好像人的一生。。

在Spring中,基本容器BeanFactory和擴充套件容器ApplicationContext的例項化時機不太一樣,BeanFactory採用的是延遲初始化的方式,也就是隻有在第一次getBean()的時候,才會例項化Bean;ApplicationContext啟動之後會例項化所有的Bean定義。

Spring IOC 中Bean的生命週期大致分為四個階段:例項化(Instantiation)、屬性賦值(Populate)、初始化(Initialization)、銷燬(Destruction)。

Bean生命週期四個階段

我們再來看一個稍微詳細一些的過程:

  • 例項化:第 1 步,例項化一個 Bean 物件
  • 屬性賦值:第 2 步,為 Bean 設定相關屬性和依賴
  • 初始化:初始化的階段的步驟比較多,5、6步是真正的初始化,第 3、4 步為在初始化前執行,第 7 步在初始化後執行,初始化完成之後,Bean就可以被使用了
  • 銷燬:第 8~10步,第8步其實也可以算到銷燬階段,但不是真正意義上的銷燬,而是先在使用前註冊了銷燬的相關呼叫介面,為了後面第9、10步真正銷燬 Bean 時再執行相應的方法
    SpringBean生命週期

簡單總結一下,Bean生命週期裡初始化的過程相對步驟會多一些,比如前置、後置的處理。

最後通過一個例項來看一下具體的細節:
Bean一生例項

  • 定義一個PersonBean類,實現DisposableBean, InitializingBean, BeanFactoryAware, BeanNameAware這4個介面,同時還有自定義的init-methoddestroy-method
public class PersonBean implements InitializingBean, BeanFactoryAware, BeanNameAware, DisposableBean {

    /**
     * 身份證號
     */
    private Integer no;

    /**
     * 姓名
     */
    private String name;

    public PersonBean() {
        System.out.println("1.呼叫構造方法:我出生了!");
    }

    public Integer getNo() {
        return no;
    }

    public void setNo(Integer no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        System.out.println("2.設定屬性:我的名字叫"+name);
    }

    @Override
    public void setBeanName(String s) {
        System.out.println("3.呼叫BeanNameAware#setBeanName方法:我要上學了,起了個學名");
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println("4.呼叫BeanFactoryAware#setBeanFactory方法:選好學校了");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("6.InitializingBean#afterPropertiesSet方法:入學登記");
    }

    public void init() {
        System.out.println("7.自定義init方法:努力上學ing");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("9.DisposableBean#destroy方法:平淡的一生落幕了");
    }

    public void destroyMethod() {
        System.out.println("10.自定義destroy方法:睡了,別想叫醒我");
    }

    public void work(){
        System.out.println("Bean使用中:工作,只有對社會沒有用的人才放假。。");
    }

}
  • 定義一個MyBeanPostProcessor實現BeanPostProcessor介面。
public class MyBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("5.BeanPostProcessor.postProcessBeforeInitialization方法:到學校報名啦");
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("8.BeanPostProcessor#postProcessAfterInitialization方法:終於畢業,拿到畢業證啦!");
        return bean;
    }
}

  • 配置檔案,指定init-methoddestroy-method屬性
<?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 name="myBeanPostProcessor" class="cn.fighter3.spring.life.MyBeanPostProcessor" />
    <bean name="personBean" class="cn.fighter3.spring.life.PersonBean"
          init-method="init" destroy-method="destroyMethod">
        <property name="idNo" value= "80669865"/>
        <property name="name" value="張鐵鋼" />
    </bean>

</beans>
  • 測試
public class Main {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
        PersonBean personBean = (PersonBean) context.getBean("personBean");
        personBean.work();
        ((ClassPathXmlApplicationContext) context).destroy();
    }
}

  • 執行結果:
1.呼叫構造方法:我出生了!
2.設定屬性:我的名字叫張鐵鋼
3.呼叫BeanNameAware#setBeanName方法:我要上學了,起了個學名
4.呼叫BeanFactoryAware#setBeanFactory方法:選好學校了
5.BeanPostProcessor#postProcessBeforeInitialization方法:到學校報名啦
6.InitializingBean#afterPropertiesSet方法:入學登記
7.自定義init方法:努力上學ing
8.BeanPostProcessor#postProcessAfterInitialization方法:終於畢業,拿到畢業證啦!
Bean使用中:工作,只有對社會沒有用的人才放假。。
9.DisposableBean#destroy方法:平淡的一生落幕了
10.自定義destroy方法:睡了,別想叫醒我

關於原始碼,Bean建立過程可以檢視AbstractBeanFactory#doGetBean方法,在這個方法裡可以看到Bean的例項化,賦值、初始化的過程,至於最終的銷燬,可以看看ConfigurableApplicationContext#close()

Bean生命週期原始碼追蹤

10.Bean定義和依賴定義有哪些方式?

有三種方式:直接編碼方式配置檔案方式註解方式

Bean依賴配置方式

  • 直接編碼方式:我們一般接觸不到直接編碼的方式,但其實其它的方式最終都要通過直接編碼來實現。
  • 配置檔案方式:通過xml、propreties型別的配置檔案,配置相應的依賴關係,Spring讀取配置檔案,完成依賴關係的注入。
  • 註解方式:註解方式應該是我們用的最多的一種方式了,在相應的地方使用註解修飾,Spring會掃描註解,完成依賴關係的注入。

11.有哪些依賴注入的方法?

Spring支援構造方法注入屬性注入工廠方法注入,其中工廠方法注入,又可以分為靜態工廠方法注入非靜態工廠方法注入

Spring依賴注入方法

  • 構造方法注入

    通過呼叫類的構造方法,將介面實現類通過構造方法變數傳入

     public CatDaoImpl(String message){
       this. message = message;
     }
    
    <bean id="CatDaoImpl" class="com.CatDaoImpl"> 
      <constructor-arg value=" message "></constructor-arg>
    </bean>
    
  • 屬性注入

    通過Setter方法完成呼叫類所需依賴的注入

     public class Id {
        private int id;
    
        public int getId() { return id; }
     
        public void setId(int id) { this.id = id; }
    }
    
    <bean id="id" class="com.id "> 
      <property name="id" value="123"></property> 
    </bean>
    
  • 工廠方法注入

    • 靜態工廠注入

      靜態工廠顧名思義,就是通過呼叫靜態工廠的方法來獲取自己需要的物件,為了讓 Spring 管理所有物件,我們不能直接通過"工程類.靜態方法()"來獲取物件,而是依然通過 Spring 注入的形式獲取:

      public class DaoFactory { //靜態工廠
       
         public static final FactoryDao getStaticFactoryDaoImpl(){
            return new StaticFacotryDaoImpl();
         }
      }
       
      public class SpringAction {
       
       //注入物件
       private FactoryDao staticFactoryDao; 
       
       //注入物件的 set 方法
       public void setStaticFactoryDao(FactoryDao staticFactoryDao) {
           this.staticFactoryDao = staticFactoryDao;
       }
       
      }
      
      //factory-method="getStaticFactoryDaoImpl"指定呼叫哪個工廠方法
       <bean name="springAction" class=" SpringAction" >
         <!--使用靜態工廠的方法注入物件,對應下面的配置檔案-->
         <property name="staticFactoryDao" ref="staticFactoryDao"></property>
       </bean>
       
       <!--此處獲取物件的方式是從工廠類中獲取靜態方法-->
      <bean name="staticFactoryDao" class="DaoFactory"
        factory-method="getStaticFactoryDaoImpl"></bean>
      
    • 非靜態工廠注入

      非靜態工廠,也叫例項工廠,意思是工廠方法不是靜態的,所以我們需要首先 new 一個工廠例項,再呼叫普通的例項方法。

      //非靜態工廠 
      public class DaoFactory { 
         public FactoryDao getFactoryDaoImpl(){
           return new FactoryDaoImpl();
         }
       }
       
      public class SpringAction {
        //注入物件
        private FactoryDao factoryDao; 
        
        public void setFactoryDao(FactoryDao factoryDao) {
          this.factoryDao = factoryDao;
        }
      }
      
       <bean name="springAction" class="SpringAction">
         <!--使用非靜態工廠的方法注入物件,對應下面的配置檔案-->
         <property name="factoryDao" ref="factoryDao"></property>
       </bean>
       
       <!--此處獲取物件的方式是從工廠類中獲取例項方法-->
       <bean name="daoFactory" class="com.DaoFactory"></bean>
       
      <bean name="factoryDao" factory-bean="daoFactory" factory-method="getFactoryDaoImpl"></bean>
      

12.Spring有哪些自動裝配的方式?

什麼是自動裝配?

Spring IOC容器知道所有Bean的配置資訊,此外,通過Java反射機制還可以獲知實現類的結構資訊,如構造方法的結構、屬性等資訊。掌握所有Bean的這些資訊後,Spring IOC容器就可以按照某種規則對容器中的Bean進行自動裝配,而無須通過顯式的方式進行依賴配置。

Spring提供的這種方式,可以按照某些規則進行Bean的自動裝配,元素提供了一個指定自動裝配型別的屬性:autowire="<自動裝配型別>"

Spring提供了哪幾種自動裝配型別?

Spring提供了4種自動裝配型別:

Spring四種自動裝配型別

  • byName:根據名稱進行自動匹配,假設Boss又一個名為car的屬性,如果容器中剛好有一個名為car的bean,Spring就會自動將其裝配給Boss的car屬性
  • byType:根據型別進行自動匹配,假設Boss有一個Car型別的屬性,如果容器中剛好有一個Car型別的Bean,Spring就會自動將其裝配給Boss這個屬性
  • constructor:與 byType類似, 只不過它是針對建構函式注入而言的。如果Boss有一個建構函式,建構函式包含一個Car型別的入參,如果容器中有一個Car型別的Bean,則Spring將自動把這個Bean作為Boss建構函式的入參;如果容器中沒有找到和建構函式入參匹配型別的Bean,則Spring將丟擲異常。
  • autodetect:根據Bean的自省機制決定採用byType還是constructor進行自動裝配,如果Bean提供了預設的建構函式,則採用byType,否則採用constructor。

13.Spring 中的 Bean 的作用域有哪些?

Spring的Bean主要支援五種作用域:

Spring Bean支援作用域

  • singleton : 在Spring容器僅存在一個Bean例項,Bean以單例項的方式存在,是Bean預設的作用域。
  • prototype : 每次從容器重呼叫Bean時,都會返回一個新的例項。

以下三個作用域於只在Web應用中適用:

  • request : 每一次HTTP請求都會產生一個新的Bean,該Bean僅在當前HTTP Request內有效。
  • session : 同一個HTTP Session共享一個Bean,不同的HTTP Session使用不同的Bean。
  • globalSession:同一個全域性Session共享一個Bean,只用於基於Protlet的Web應用,Spring5中已經不存在了。

14.Spring 中的單例 Bean 會存線上程安全問題嗎?

首先結論在這:Spring中的單例Bean不是執行緒安全的

因為單例Bean,是全域性只有一個Bean,所有執行緒共享。如果說單例Bean,是一個無狀態的,也就是執行緒中的操作不會對Bean中的成員變數執行查詢以外的操作,那麼這個單例Bean是執行緒安全的。比如Spring mvc 的 Controller、Service、Dao等,這些Bean大多是無狀態的,只關注於方法本身。

假如這個Bean是有狀態的,也就是會對Bean中的成員變數進行寫操作,那麼可能就存線上程安全的問題。

Spring單例Bean執行緒安全問題

單例Bean執行緒安全問題怎麼解決呢?

常見的有這麼些解決辦法:

  1. 將Bean定義為多例

    這樣每一個執行緒請求過來都會建立一個新的Bean,但是這樣容器就不好管理Bean,不能這麼辦。

  2. 在Bean物件中儘量避免定義可變的成員變數

    削足適履了屬於是,也不能這麼幹。

  3. 將Bean中的成員變數儲存在ThreadLocal中⭐

    我們知道ThredLoca能保證多執行緒下變數的隔離,可以在類中定義一個ThreadLocal成員變數,將需要的可變成員變數儲存在ThreadLocal裡,這是推薦的一種方式。

15.說說迴圈依賴?

什麼是迴圈依賴?

Spring迴圈依賴

Spring 迴圈依賴:簡單說就是自己依賴自己,或者和別的Bean相互依賴。

雞和蛋

只有單例的Bean才存在迴圈依賴的情況,原型(Prototype)情況下,Spring會直接丟擲異常。原因很簡單,AB迴圈依賴,A例項化的時候,發現依賴B,建立B例項,建立B的時候發現需要A,建立A1例項……無限套娃,直接把系統幹垮。

Spring可以解決哪些情況的迴圈依賴?

Spring不支援基於構造器注入的迴圈依賴,但是假如AB迴圈依賴,如果一個是構造器注入,一個是setter注入呢?

看看幾種情形:

迴圈依賴的幾種情形

第四種可以而第五種不可以的原因是 Spring 在建立 Bean 時預設會根據自然排序進行建立,所以 A 會先於 B 進行建立。

所以簡單總結,當迴圈依賴的例項都採用setter方法注入的時候,Spring可以支援,都採用構造器注入的時候,不支援,構造器注入和setter注入同時存在的時候,看天。

16.那Spring怎麼解決迴圈依賴的呢?

PS:其實正確答案是開發人員做好設計,別讓Bean迴圈依賴,但是沒辦法,面試官不想聽這個。

我們都知道,單例Bean初始化完成,要經歷三步:

Bean初始化步驟

注入就發生在第二步,屬性賦值,結合這個過程,Spring 通過三級快取解決了迴圈依賴:

  1. 一級快取 : Map<String,Object> singletonObjects,單例池,用於儲存例項化、屬性賦值(注入)、初始化完成的 bean 例項
  2. 二級快取 : Map<String,Object> earlySingletonObjects,早期曝光物件,用於儲存例項化完成的 bean 例項
  3. 三級快取 : Map<String,ObjectFactory<?>> singletonFactories,早期曝光物件工廠,用於儲存 bean 建立工廠,以便於後面擴充套件有機會建立代理物件。

三級快取

我們來看一下三級快取解決迴圈依賴的過程:

當 A、B 兩個類發生迴圈依賴時:
迴圈依賴

A例項的初始化過程:

  1. 建立A例項,例項化的時候把A物件⼯⼚放⼊三級快取,表示A開始例項化了,雖然我這個物件還不完整,但是先曝光出來讓大家知道

    1

  2. A注⼊屬性時,發現依賴B,此時B還沒有被建立出來,所以去例項化B

  3. 同樣,B注⼊屬性時發現依賴A,它就會從快取裡找A物件。依次從⼀級到三級快取查詢A,從三級快取通過物件⼯⼚拿到A,發現A雖然不太完善,但是存在,把A放⼊⼆級快取,同時刪除三級快取中的A,此時,B已經例項化並且初始化完成,把B放入⼀級快取。

    2

  4. 接著A繼續屬性賦值,順利從⼀級快取拿到例項化且初始化完成的B物件,A物件建立也完成,刪除⼆級快取中的A,同時把A放⼊⼀級快取

  5. 最後,⼀級快取中儲存著例項化、初始化都完成的A、B物件

5

所以,我們就知道為什麼Spring能解決setter注入的迴圈依賴了,因為例項化和屬性賦值是分開的,所以裡面有操作的空間。如果都是構造器注入的化,那麼都得在例項化這一步完成注入,所以自然是無法支援了。

17.為什麼要三級快取?⼆級不⾏嗎?

不行,主要是為了⽣成代理物件。如果是沒有代理的情況下,使用二級快取解決迴圈依賴也是OK的。但是如果存在代理,三級沒有問題,二級就不行了。

因為三級快取中放的是⽣成具體物件的匿名內部類,獲取Object的時候,它可以⽣成代理物件,也可以返回普通物件。使⽤三級快取主要是為了保證不管什麼時候使⽤的都是⼀個物件。

假設只有⼆級快取的情況,往⼆級快取中放的顯示⼀個普通的Bean物件,Bean初始化過程中,通過 BeanPostProcessor 去⽣成代理物件之後,覆蓋掉⼆級快取中的普通Bean物件,那麼可能就導致取到的Bean物件不一致了。

二級快取不行的原因

18.@Autowired的實現原理?

實現@Autowired的關鍵是:AutowiredAnnotationBeanPostProcessor

在Bean的初始化階段,會通過Bean後置處理器來進行一些前置和後置的處理。

實現@Autowired的功能,也是通過後置處理器來完成的。這個後置處理器就是AutowiredAnnotationBeanPostProcessor。

  • Spring在建立bean的過程中,最終會呼叫到doCreateBean()方法,在doCreateBean()方法中會呼叫populateBean()方法,來為bean進行屬性填充,完成自動裝配等工作。

  • 在populateBean()方法中一共呼叫了兩次後置處理器,第一次是為了判斷是否需要屬性填充,如果不需要進行屬性填充,那麼就會直接進行return,如果需要進行屬性填充,那麼方法就會繼續向下執行,後面會進行第二次後置處理器的呼叫,這個時候,就會呼叫到AutowiredAnnotationBeanPostProcessor的postProcessPropertyValues()方法,在該方法中就會進行@Autowired註解的解析,然後實現自動裝配。

    /**
    * 屬性賦值
    **/
    protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
                //………… 
                if (hasInstAwareBpps) {
                    if (pvs == null) {
                        pvs = mbd.getPropertyValues();
                    }
    
                    PropertyValues pvsToUse;
                    for(Iterator var9 = this.getBeanPostProcessorCache().instantiationAware.iterator(); var9.hasNext(); pvs = pvsToUse) {
                        InstantiationAwareBeanPostProcessor bp = (InstantiationAwareBeanPostProcessor)var9.next();
                        pvsToUse = bp.postProcessProperties((PropertyValues)pvs, bw.getWrappedInstance(), beanName);
                        if (pvsToUse == null) {
                            if (filteredPds == null) {
                                filteredPds = this.filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
                            }
                            //執行後處理器,填充屬性,完成自動裝配
                            //呼叫InstantiationAwareBeanPostProcessor的postProcessPropertyValues()方法
                            pvsToUse = bp.postProcessPropertyValues((PropertyValues)pvs, filteredPds, bw.getWrappedInstance(), beanName);
                            if (pvsToUse == null) {
                                return;
                            }
                        }
                    }
                }
               //…………
        }
    
  • postProcessorPropertyValues()方法的原始碼如下,在該方法中,會先呼叫findAutowiringMetadata()方法解析出bean中帶有@Autowired註解、@Inject和@Value註解的屬性和方法。然後呼叫metadata.inject()方法,進行屬性填充。

        public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
            //@Autowired註解、@Inject和@Value註解的屬性和方法
            InjectionMetadata metadata = this.findAutowiringMetadata(beanName, bean.getClass(), pvs);
    
            try {
                //屬性填充
                metadata.inject(bean, beanName, pvs);
                return pvs;
            } catch (BeanCreationException var6) {
                throw var6;
            } catch (Throwable var7) {
                throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", var7);
            }
        }
    

AOP

19.說說什麼是AOP?

AOP:面向切面程式設計。簡單說,就是把一些業務邏輯中的相同的程式碼抽取到一個獨立的模組中,讓業務邏輯更加清爽。

橫向抽取

具體來說,假如我現在要crud寫一堆業務,可是如何業務程式碼前後前後進行列印日誌和引數的校驗呢?

我們可以把日誌記錄資料校驗可重用的功能模組分離出來,然後在程式的執行的合適的地方動態地植入這些程式碼並執行。這樣就簡化了程式碼的書寫。

AOP應用示例

業務邏輯程式碼中沒有參和通用邏輯的程式碼,業務模組更簡潔,只包含核心業務程式碼。實現了業務邏輯和通用邏輯的程式碼分離,便於維護和升級,降低了業務邏輯和通用邏輯的耦合性。

AOP 可以將遍佈應用各處的功能分離出來形成可重用的元件。在編譯期間、裝載期間或執行期間實現在不修改原始碼的情況下給程式動態新增功能。從而實現對業務邏輯的隔離,提高程式碼的模組化能力。

Java語言執行過程

AOP 的核心其實就是動態代理,如果是實現了介面的話就會使用 JDK 動態代理,否則使用 CGLIB 代理,主要應用於處理一些具有橫切性質的系統級服務,如日誌收集、事務管理、安全檢查、快取、物件池管理等。

AOP有哪些核心概念?

  • 切面(Aspect):類是對物體特徵的抽象,切面就是對橫切關注點的抽象

  • 連線點(Joinpoint):被攔截到的點,因為 Spring 只支援方法型別的連線點,所以在 Spring中連線點指的就是被攔截到的方法,實際上連線點還可以是欄位或者構造器

  • 切點(Pointcut):對連線點進行攔截的定位

  • 通知(Advice):所謂通知指的就是指攔截到連線點之後要執行的程式碼,也可以稱作增強

  • 目標物件 (Target):代理的目標物件

  • 織入(Weabing):織入是將增強新增到目標類的具體連線點上的過程。

    • 編譯期織入:切面在目標類編譯時被織入

    • 類載入期織入:切面在目標類載入到JVM時被織入。需要特殊的類載入器,它可以在目標類被引入應用之前增強該目標類的位元組碼。

    • 執行期織入:切面在應用執行的某個時刻被織入。一般情況下,在織入切面時,AOP容器會為目標物件動態地建立一個代理物件。SpringAOP就是以這種方式織入切面。

      Spring採用執行期織入,而AspectJ採用編譯期織入和類載入器織入。

  • 引介(introduction):引介是一種特殊的增強,可以動態地為類新增一些屬性和方法

AOP有哪些環繞方式?

AOP 一般有 5 種環繞方式:

  • 前置通知 (@Before)
  • 返回通知 (@AfterReturning)
  • 異常通知 (@AfterThrowing)
  • 後置通知 (@After)
  • 環繞通知 (@Around)

環繞方式

多個切面的情況下,可以通過 @Order 指定先後順序,數字越小,優先順序越高。

20.說說你平時有用到AOP嗎?

PS:這道題老三的同事面試候選人的時候問到了,候選人說了一堆AOP原理,同事就勢來一句,你能現場寫一下AOP的應用嗎?結果——場面一度很尷尬。雖然我對面試寫這種百度就能出來的東西持保留意見,但是還是加上了這一問,畢竟招人最後都是要擼程式碼的。

這裡給出一個小例子,SpringBoot專案中,利用AOP列印介面的入參和出參日誌,以及執行時間,還是比較快捷的。

  • 引入依賴:引入AOP依賴

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
    
  • 自定義註解:自定義一個註解作為切點

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD})
    @Documented
    public @interface WebLog {
    }
    
  • 配置AOP切面:

    • @Aspect:標識切面

    • @Pointcut:設定切點,這裡以自定義註解為切點,定義切點有很多其它種方式,自定義註解是比較常用的一種。

    • @Before:在切點之前織入,列印了一些入參資訊

    • @Around:環繞切點,列印返回引數和介面執行時間

    @Aspect
    @Component
    public class WebLogAspect {
    
        private final static Logger logger         = LoggerFactory.getLogger(WebLogAspect.class);
    
        /**
         * 以自定義 @WebLog 註解為切點
         **/
        @Pointcut("@annotation(cn.fighter3.spring.aop_demo.WebLog)")
        public void webLog() {}
    
        /**
         * 在切點之前織入
         */
        @Before("webLog()")
        public void doBefore(JoinPoint joinPoint) throws Throwable {
            // 開始列印請求日誌
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            // 列印請求相關引數
            logger.info("========================================== Start ==========================================");
            // 列印請求 url
            logger.info("URL            : {}", request.getRequestURL().toString());
            // 列印 Http method
            logger.info("HTTP Method    : {}", request.getMethod());
            // 列印呼叫 controller 的全路徑以及執行方法
            logger.info("Class Method   : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
            // 列印請求的 IP
            logger.info("IP             : {}", request.getRemoteAddr());
            // 列印請求入參
            logger.info("Request Args   : {}",new ObjectMapper().writeValueAsString(joinPoint.getArgs()));
        }
    
        /**
         * 在切點之後織入
         * @throws Throwable
         */
        @After("webLog()")
        public void doAfter() throws Throwable {
            // 結束後打個分隔線,方便檢視
            logger.info("=========================================== End ===========================================");
        }
    
        /**
         * 環繞
         */
        @Around("webLog()")
        public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            //開始時間
            long startTime = System.currentTimeMillis();
            Object result = proceedingJoinPoint.proceed();
            // 列印出參
            logger.info("Response Args  : {}", new ObjectMapper().writeValueAsString(result));
            // 執行耗時
            logger.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);
            return result;
        }
    
    }
    
  • 使用:只需要在介面上加上自定義註解

        @GetMapping("/hello")
        @WebLog(desc = "這是一個歡迎介面")
        public String hello(String name){
            return "Hello "+name;
        }
    
  • 執行結果:可以看到日誌列印了入參、出參和執行時間
    執行結果

21.說說JDK 動態代理和 CGLIB 代理 ?

Spring的AOP是通過動態代理來實現的,動態代理主要有兩種方式JDK動態代理和Cglib動態代理,這兩種動態代理的使用和原理有些不同。

JDK 動態代理

  1. Interface:對於 JDK 動態代理,目標類需要實現一個Interface。
  2. InvocationHandler:InvocationHandler是一個介面,可以通過實現這個介面,定義橫切邏輯,再通過反射機制(invoke)呼叫目標類的程式碼,在次過程,可能包裝邏輯,對目標方法進行前置後置處理。
  3. Proxy:Proxy利用InvocationHandler動態建立一個符合目標類實現的介面的例項,生成目標類的代理物件。

CgLib 動態代理

  1. 使用JDK建立代理有一大限制,它只能為介面建立代理例項,而CgLib 動態代理就沒有這個限制。
  2. CgLib 動態代理是使用位元組碼處理框架 ASM,其原理是通過位元組碼技術為一個類建立子類,並在子類中採用方法攔截的技術攔截所有父類方法的呼叫,順勢織入橫切邏輯。
  3. CgLib 建立的動態代理物件效能比 JDK 建立的動態代理物件的效能高不少,但是 CGLib 在建立代理物件時所花費的時間卻比 JDK 多得多,所以對於單例的物件,因為無需頻繁建立物件,用 CGLib 合適,反之,使用 JDK 方式要更為合適一些。同時,由於 CGLib 由於是採用動態建立子類的方法,對於 final 方法,無法進行代理。

我們來看一個常見的小場景,客服中轉,解決使用者問題:

使用者向客服提問題

JDK動態代理實現:

JDK動態代理類圖

  • 介面

    public interface ISolver {
        void solve();
    }
    
  • 目標類:需要實現對應介面

    public class Solver implements ISolver {
        @Override
        public void solve() {
            System.out.println("瘋狂掉頭髮解決問題……");
        }
    }
    
  • 態代理工廠:ProxyFactory,直接用反射方式生成一個目標物件的代理物件,這裡用了一個匿名內部類方式重寫InvocationHandler方法,實現介面重寫也差不多

    public class ProxyFactory {
    
        // 維護一個目標物件
        private Object target;
    
        public ProxyFactory(Object target) {
            this.target = target;
        }
    
        // 為目標物件生成代理物件
        public Object getProxyInstance() {
            return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
                    new InvocationHandler() {
                        @Override
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                            System.out.println("請問有什麼可以幫到您?");
    
                            // 呼叫目標物件方法
                            Object returnValue = method.invoke(target, args);
    
                            System.out.println("問題已經解決啦!");
                            return null;
                        }
                    });
        }
    }
    
  • 客戶端:Client,生成一個代理物件例項,通過代理物件呼叫目標物件方法

    public class Client {
        public static void main(String[] args) {
            //目標物件:程式設計師
            ISolver developer = new Solver();
            //代理:客服小姐姐
            ISolver csProxy = (ISolver) new ProxyFactory(developer).getProxyInstance();
            //目標方法:解決問題
            csProxy.solve();
        }
    }
    

Cglib動態代理實現:

Cglib動態代理類圖

  • 目標類:Solver,這裡目標類不用再實現介面。

    public class Solver {
    
        public void solve() {
            System.out.println("瘋狂掉頭髮解決問題……");
        }
    }
    
  • 動態代理工廠:

    public class ProxyFactory implements MethodInterceptor {
    
       //維護一個目標物件
        private Object target;
    
        public ProxyFactory(Object target) {
            this.target = target;
        }
    
        //為目標物件生成代理物件
        public Object getProxyInstance() {
            //工具類
            Enhancer en = new Enhancer();
            //設定父類
            en.setSuperclass(target.getClass());
            //設定回撥函式
            en.setCallback(this);
            //建立子類物件代理
            return en.create();
        }
    
        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            System.out.println("請問有什麼可以幫到您?");
            // 執行目標物件的方法
            Object returnValue = method.invoke(target, args);
            System.out.println("問題已經解決啦!");
            return null;
        }
    
    }
    
  • 客戶端:Client

    public class Client {
        public static void main(String[] args) {
            //目標物件:程式設計師
            Solver developer = new Solver();
            //代理:客服小姐姐
            Solver csProxy = (Solver) new ProxyFactory(developer).getProxyInstance();
            //目標方法:解決問題
            csProxy.solve();
        }
    }
    

22.說說Spring AOP 和 AspectJ AOP 區別?

Spring AOP

Spring AOP 屬於執行時增強,主要具有如下特點:

  1. 基於動態代理來實現,預設如果使用介面的,用 JDK 提供的動態代理實現,如果是方法則使用 CGLIB 實現

  2. Spring AOP 需要依賴 IOC 容器來管理,並且只能作用於 Spring 容器,使用純 Java 程式碼實現

  3. 在效能上,由於 Spring AOP 是基於動態代理來實現的,在容器啟動時需要生成代理例項,在方法呼叫上也會增加棧的深度,使得 Spring AOP 的效能不如 AspectJ 的那麼好。

  4. Spring AOP 致力於解決企業級開發中最普遍的 AOP(方法織入)。

AspectJ

AspectJ 是一個易用的功能強大的 AOP 框架,屬於編譯時增強, 可以單獨使用,也可以整合到其它框架中,是 AOP 程式設計的完全解決方案。AspectJ 需要用到單獨的編譯器 ajc。

AspectJ 屬於靜態織入,通過修改程式碼來實現,在實際執行之前就完成了織入,所以說它生成的類是沒有額外執行時開銷的,一般有如下幾個織入的時機:

  1. 編譯期織入(Compile-time weaving):如類 A 使用 AspectJ 新增了一個屬性,類 B 引用了它,這個場景就需要編譯期的時候就進行織入,否則沒法編譯類 B。

  2. 編譯後織入(Post-compile weaving):也就是已經生成了 .class 檔案,或已經打成 jar 包了,這種情況我們需要增強處理的話,就要用到編譯後織入。

  3. 類載入後織入(Load-time weaving):指的是在載入類的時候進行織入,要實現這個時期的織入,有幾種常見的方法

整體對比如下:

Spring AOP和AspectJ對比

事務

Spring 事務的本質其實就是資料庫對事務的支援,沒有資料庫的事務支援,Spring 是無法提供事務功能的。Spring 只提供統一事務管理介面,具體實現都是由各資料庫自己實現,資料庫事務的提交和回滾是通過資料庫自己的事務機制實現。

23.Spring 事務的種類?

Spring 支援程式設計式事務管理和宣告式事務管理兩種方式:

Spring事務分類

  1. 程式設計式事務

程式設計式事務管理使用 TransactionTemplate,需要顯式執行事務。

  1. 宣告式事務

  2. 宣告式事務管理建立在 AOP 之上的。其本質是通過 AOP 功能,對方法前後進行攔截,將事務處理的功能編織到攔截的方法中,也就是在目標方法開始之前啟動一個事務,在執行完目標方法之後根據執行情況提交或者回滾事務

  3. 優點是不需要在業務邏輯程式碼中摻雜事務管理的程式碼,只需在配置檔案中做相關的事務規則宣告或通過 @Transactional 註解的方式,便可以將事務規則應用到業務邏輯中,減少業務程式碼的汙染。唯一不足地方是,最細粒度只能作用到方法級別,無法做到像程式設計式事務那樣可以作用到程式碼塊級別。

24.Spring 的事務隔離級別?

Spring的介面TransactionDefinition中定義了表示隔離級別的常量,當然其實主要還是對應資料庫的事務隔離級別:

  1. ISOLATION_DEFAULT:使用後端資料庫預設的隔離界別,MySQL 預設可重複讀,Oracle 預設讀已提交。
  2. ISOLATION_READ_UNCOMMITTED:讀未提交
  3. ISOLATION_READ_COMMITTED:讀已提交
  4. ISOLATION_REPEATABLE_READ:可重複讀
  5. ISOLATION_SERIALIZABLE:序列化

25.Spring 的事務傳播機制?

Spring 事務的傳播機制說的是,當多個事務同時存在的時候——一般指的是多個事務方法相互呼叫時,Spring 如何處理這些事務的行為。

事務傳播機制是使用簡單的 ThreadLocal 實現的,所以,如果呼叫的方法是在新執行緒呼叫的,事務傳播實際上是會失效的。

7種事務傳播機制

Spring預設的事務傳播行為是PROPAFATION_REQUIRED,它適合絕大多數情況,如果多個ServiceX#methodX()都工作在事務環境下(均被Spring事務增強),且程式中存在呼叫鏈Service1#method1()->Service2#method2()->Service3#method3(),那麼這3個服務類的三個方法通過Spring的事務傳播機制都工作在同一個事務中。

26.宣告式事務實現原理了解嗎?

就是通過AOP/動態代理。

  • 在Bean初始化階段建立代理物件:Spring容器在初始化每個單例bean的時候,會遍歷容器中的所有BeanPostProcessor實現類,並執行其postProcessAfterInitialization方法,在執行AbstractAutoProxyCreator類的postProcessAfterInitialization方法時會遍歷容器中所有的切面,查詢與當前例項化bean匹配的切面,這裡會獲取事務屬性切面,查詢@Transactional註解及其屬性值,然後根據得到的切面建立一個代理物件,預設是使用JDK動態代理建立代理,如果目標類是介面,則使用JDK動態代理,否則使用Cglib。

  • 在執行目標方法時進行事務增強操作:當通過代理物件呼叫Bean方法的時候,會觸發對應的AOP增強攔截器,宣告式事務是一種環繞增強,對應介面為MethodInterceptor,事務增強對該介面的實現為TransactionInterceptor,類圖如下:

    圖片來源網易技術專欄

    事務攔截器TransactionInterceptorinvoke方法中,通過呼叫父類TransactionAspectSupportinvokeWithinTransaction方法進行事務處理,包括開啟事務、事務提交、異常回滾。

27.宣告式事務在哪些情況下會失效?

宣告式事務的幾種失效的情況

1、@Transactional 應用在非 public 修飾的方法上

如果Transactional註解應用在非 public 修飾的方法上,Transactional將會失效。

是因為在Spring AOP 代理時,TransactionInterceptor (事務攔截器)在目標方法執行前後進行攔截,DynamicAdvisedInterceptor(CglibAopProxy 的內部類)的intercept方法 或 JdkDynamicAopProxy的invoke方法會間接呼叫AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute方法,獲取Transactional 註解的事務配置資訊。

protected TransactionAttribute computeTransactionAttribute(Method method,
    Class<?> targetClass) {
        // Don't allow no-public methods as required.
        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
        return null;
}

此方法會檢查目標方法的修飾符是否為 public,不是 public則不會獲取@Transactional 的屬性配置資訊。

2、@Transactional 註解屬性 propagation 設定錯誤

  • TransactionDefinition.PROPAGATION_SUPPORTS:如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續執行。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事務方式執行,如果當前存在事務,則把當前事務掛起。
  • TransactionDefinition.PROPAGATION_NEVER:以非事務方式執行,如果當前存在事務,則丟擲異常。

3、@Transactional 註解屬性 rollbackFor 設定錯誤

rollbackFor 可以指定能夠觸發事務回滾的異常型別。Spring預設丟擲了未檢查unchecked異常(繼承自 RuntimeException的異常)或者 Error才回滾事務,其他異常不會觸發回滾事務。

Spring預設支援的異常回滾

// 希望自定義的異常可以進行回滾
@Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class

若在目標方法中丟擲的異常是 rollbackFor 指定的異常的子類,事務同樣會回滾。

4、同一個類中方法呼叫,導致@Transactional失效

開發中避免不了會對同一個類裡面的方法呼叫,比如有一個類Test,它的一個方法A,A再呼叫本類的方法B(不論方法B是用public還是private修飾),但方法A沒有宣告註解事務,而B方法有。則外部呼叫方法A之後,方法B的事務是不會起作用的。這也是經常犯錯誤的一個地方。

那為啥會出現這種情況?其實這還是由於使用Spring AOP代理造成的,因為只有當事務方法被當前類以外的程式碼呼叫時,才會由Spring生成的代理物件來管理。

 //@Transactional
     @GetMapping("/test")
     private Integer A() throws Exception {
         CityInfoDict cityInfoDict = new CityInfoDict();
         cityInfoDict.setCityName("2");
         /**
          * B 插入欄位為 3的資料
          */
         this.insertB();
        /**
         * A 插入欄位為 2的資料
         */
        int insert = cityInfoDictMapper.insert(cityInfoDict);
        return insert;
    }

    @Transactional()
    public Integer insertB() throws Exception {
        CityInfoDict cityInfoDict = new CityInfoDict();
        cityInfoDict.setCityName("3");
        cityInfoDict.setParentCityId(3);

        return cityInfoDictMapper.insert(cityInfoDict);
    }

這種情況是最常見的一種@Transactional註解失效場景

@Transactional
private Integer A() throws Exception {
    int insert = 0;
    try {
        CityInfoDict cityInfoDict = new CityInfoDict();
        cityInfoDict.setCityName("2");
        cityInfoDict.setParentCityId(2);
        /**
         * A 插入欄位為 2的資料
         */
        insert = cityInfoDictMapper.insert(cityInfoDict);
        /**
         * B 插入欄位為 3的資料
        */
        b.insertB();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

如果B方法內部拋了異常,而A方法此時try catch了B方法的異常,那這個事務就不能正常回滾了,會丟擲異常:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

MVC

28.Spring MVC 的核心元件?

  1. DispatcherServlet:前置控制器,是整個流程控制的核心,控制其他元件的執行,進行統一排程,降低元件之間的耦合性,相當於總指揮。
  2. Handler:處理器,完成具體的業務邏輯,相當於 Servlet 或 Action。
  3. HandlerMapping:DispatcherServlet 接收到請求之後,通過 HandlerMapping 將不同的請求對映到不同的 Handler。
  4. HandlerInterceptor:處理器攔截器,是一個介面,如果需要完成一些攔截處理,可以實現該介面。
  5. HandlerExecutionChain:處理器執行鏈,包括兩部分內容:Handler 和 HandlerInterceptor(系統會有一個預設的 HandlerInterceptor,如果需要額外設定攔截,可以新增攔截器)。
  6. HandlerAdapter:處理器介面卡,Handler 執行業務方法之前,需要進行一系列的操作,包括表單資料的驗證、資料型別的轉換、將表單資料封裝到 JavaBean 等,這些操作都是由 HandlerApater 來完成,開發者只需將注意力集中業務邏輯的處理上,DispatcherServlet 通過 HandlerAdapter 執行不同的 Handler。
  7. ModelAndView:裝載了模型資料和檢視資訊,作為 Handler 的處理結果,返回給 DispatcherServlet。
  8. ViewResolver:檢視解析器,DispatcheServlet 通過它將邏輯檢視解析為物理檢視,最終將渲染結果響應給客戶端。

29.Spring MVC 的工作流程?

Spring MVC的工作流程

  1. 客戶端向服務端傳送一次請求,這個請求會先到前端控制器DispatcherServlet(也叫中央控制器)。
  2. DispatcherServlet接收到請求後會呼叫HandlerMapping處理器對映器。由此得知,該請求該由哪個Controller來處理(並未呼叫Controller,只是得知)
  3. DispatcherServlet呼叫HandlerAdapter處理器介面卡,告訴處理器介面卡應該要去執行哪個Controller
  4. HandlerAdapter處理器介面卡去執行Controller並得到ModelAndView(資料和檢視),並層層返回給DispatcherServlet
  5. DispatcherServlet將ModelAndView交給ViewReslover檢視解析器解析,然後返回真正的檢視。
  6. DispatcherServlet將模型資料填充到檢視中
  7. DispatcherServlet將結果響應給客戶端

Spring MVC 雖然整體流程複雜,但是實際開發中很簡單,大部分的元件不需要開發人員建立和管理,只需要通過配置檔案的方式完成配置即可,真正需要開發人員進行處理的只有 Handler(Controller)ViewModel

當然我們現在大部分的開發都是前後端分離,Restful風格介面,後端只需要返回Json資料就行了。

30.SpringMVC Restful風格的介面的流程是什麼樣的呢?

PS:這是一道全新的八股,畢竟ModelAndView這種方式應該沒人用了吧?現在都是前後端分離介面,八股也該更新換代了。

我們都知道Restful介面,響應格式是json,這就用到了一個常用註解:@ResponseBody

    @GetMapping("/user")
    @ResponseBody
    public User user(){
        return new User(1,"張三");
    }

加入了這個註解後,整體的流程上和使用ModelAndView大體上相同,但是細節上有一些不同:

Spring MVC Restful請求響應示意圖

  1. 客戶端向服務端傳送一次請求,這個請求會先到前端控制器DispatcherServlet

  2. DispatcherServlet接收到請求後會呼叫HandlerMapping處理器對映器。由此得知,該請求該由哪個Controller來處理

  3. DispatcherServlet呼叫HandlerAdapter處理器介面卡,告訴處理器介面卡應該要去執行哪個Controller

  4. Controller被封裝成了ServletInvocableHandlerMethod,HandlerAdapter處理器介面卡去執行invokeAndHandle方法,完成對Controller的請求處理

  5. HandlerAdapter執行完對Controller的請求,會呼叫HandlerMethodReturnValueHandler去處理返回值,主要的過程:

    5.1. 呼叫RequestResponseBodyMethodProcessor,建立ServletServerHttpResponse(Spring對原生ServerHttpResponse的封裝)例項

    5.2.使用HttpMessageConverter的write方法,將返回值寫入ServletServerHttpResponse的OutputStream輸出流中

    5.3.在寫入的過程中,會使用JsonGenerator(預設使用Jackson框架)對返回值進行Json序列化

  6. 執行完請求後,返回的ModealAndView為null,ServletServerHttpResponse裡也已經寫入了響應,所以不用關心View的處理

Spring Boot

31.介紹一下SpringBoot,有哪些優點?

Spring Boot 基於 Spring 開發,Spirng Boot 本身並不提供 Spring 框架的核心特性以及擴充套件功能,只是用於快速、敏捷地開發新一代基於 Spring 框架的應用程式。它並不是用來替代 Spring 的解決方案,而是和 Spring 框架緊密結合用於提升 Spring 開發者體驗的工具。

SpringBoot圖示

Spring Boot 以約定大於配置核心思想開展工作,相比Spring具有如下優勢:

  1. Spring Boot 可以快速建立獨立的Spring應用程式。
  2. Spring Boot 內嵌瞭如Tomcat,Jetty和Undertow這樣的容器,也就是說可以直接跑起來,用不著再做部署工作了。
  3. Spring Boot 無需再像Spring一樣使用一堆繁瑣的xml檔案配置。
  4. Spring Boot 可以自動配置(核心)Spring。SpringBoot將原有的XML配置改為Java配置,將bean注入改為使用註解注入的方式(@Autowire),並將多個xml、properties配置濃縮在一個appliaction.yml配置檔案中。
  5. Spring Boot 提供了一些現有的功能,如量度工具,表單資料驗證以及一些外部配置這樣的一些第三方功能。
  6. Spring Boot 可以快速整合常用依賴(開發庫,例如spring-webmvc、jackson-json、validation-api和tomcat等),提供的POM可以簡化Maven的配置。當我們引入核心依賴時,SpringBoot會自引入其他依賴。

32.SpringBoot自動配置原理了解嗎?

SpringBoot開啟自動配置的註解是@EnableAutoConfiguration ,啟動類上的註解@SpringBootApplication是一個複合註解,包含了@EnableAutoConfiguration:

SpringBoot自動配置原理

  • EnableAutoConfiguration 只是一個簡單的註解,自動裝配核心功能的實現實際是通過 AutoConfigurationImportSelector

    @AutoConfigurationPackage //將main同級的包下的所有元件註冊到容器中
    @Import({AutoConfigurationImportSelector.class}) //載入自動裝配類 xxxAutoconfiguration
    public @interface EnableAutoConfiguration {
        String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    
        Class<?>[] exclude() default {};
    
        String[] excludeName() default {};
    }
    
  • AutoConfigurationImportSelector實現了ImportSelector介面,這個介面的作用就是收集需要匯入的配置類,配合@Import()就可以將相應的類匯入到Spring容器中

  • 獲取注入類的方法是selectImports(),它實際呼叫的是getAutoConfigurationEntry,這個方法是獲取自動裝配類的關鍵,主要流程可以分為這麼幾步:

    1. 獲取註解的屬性,用於後面的排除
    2. 獲取所有需要自動裝配的配置類的路徑:這一步是最關鍵的,從META-INF/spring.factories獲取自動配置類的路徑
    3. 去掉重複的配置類和需要排除的重複類,把需要自動載入的配置類的路徑儲存起來
    protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return EMPTY_ENTRY;
        } else {
            //1.獲取到註解的屬性
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
            //2.獲取需要自動裝配的所有配置類,讀取META-INF/spring.factories,獲取自動配置類路徑
            List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
            //3.1.移除重複的配置
            configurations = this.removeDuplicates(configurations);
            //3.2.處理需要排除的配置
            Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
            this.checkExcludedClasses(configurations, exclusions);
            configurations.removeAll(exclusions);
            configurations = this.getConfigurationClassFilter().filter(configurations);
            this.fireAutoConfigurationImportEvents(configurations, exclusions);
            return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
        }
    }

33.如何自定義一個SpringBoot Srarter?

知道了自動配置原理,建立一個自定義SpringBoot Starter也很簡單。

  1. 建立一個專案,命名為demo-spring-boot-starter,引入SpringBoot相關依賴
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
  1. 編寫配置檔案

    這裡定義了屬性配置的字首

    @ConfigurationProperties(prefix = "hello")
    public class HelloProperties {
    
        private String name;
    
        //省略getter、setter
    }
    
  2. 自動裝配

    建立自動配置類HelloPropertiesConfigure

    @Configuration
    @EnableConfigurationProperties(HelloProperties.class)
    public class HelloPropertiesConfigure {
    }
    
  3. 配置自動類

    /resources/META-INF/spring.factories檔案中新增自動配置類路徑

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
      cn.fighter3.demo.starter.configure.HelloPropertiesConfigure
    
  4. 測試

    • 建立一個工程,引入自定義starter依賴

              <dependency>
                  <groupId>cn.fighter3</groupId>
                  <artifactId>demo-spring-boot-starter</artifactId>
                  <version>0.0.1-SNAPSHOT</version>
              </dependency>
      
    • 在配置檔案裡新增配置

      hello.name=張三
      
    • 測試類

      @RunWith(SpringRunner.class)
      @SpringBootTest
      public class HelloTest {
          @Autowired
          HelloProperties helloProperties;
      
          @Test
          public void hello(){
              System.out.println("你好,"+helloProperties.getName());
          }
      }
      
    • 執行結果

      執行結果

    至此,隨手寫的一個自定義SpringBoot-Starter就完成了,雖然比較簡單,但是完成了主要的自動裝配的能力。

34.Springboot 啟動原理?

SpringApplication 這個類主要做了以下四件事情:

  1. 推斷應用的型別是普通的專案還是 Web 專案
  2. 查詢並載入所有可用初始化器 , 設定到 initializers 屬性中
  3. 找出所有的應用程式監聽器,設定到 listeners 屬性中
  4. 推斷並設定 main 方法的定義類,找到執行的主類

SpringBoot 啟動大致流程如下 :

SpringBoot 啟動大致流程-圖片來源網路

Spring Cloud

35.對SpringCloud瞭解多少?

SpringCloud是Spring官方推出的微服務治理框架。

Spring Cloud Netfilx核心元件-來源參考[2]

什麼是微服務?

  1. 2014 年 Martin Fowler 提出的一種新的架構形式。微服務架構是一種架構模式,提倡將單一應用程式劃分成一組小的服務,服務之間相互協調,互相配合,為使用者提供最終價值。每個服務執行在其獨立的程式中,服務與服務之間採用輕量級的通訊機制(如HTTP或Dubbo)互相協作,每個服務都圍繞著具體的業務進行構建,並且能夠被獨立的部署到生產環境中,另外,應儘量避免統一的,集中式的服務管理機制,對具體的一個服務而言,應根據業務上下文,選擇合適的語言、工具(如Maven)對其進行構建。
  2. 微服務化的核心就是將傳統的一站式應用,根據業務拆分成一個一個的服務,徹底地去耦合,每一個微服務提供單個業務功能的服務,一個服務做一件事情,從技術角度看就是一種小而獨立的處理過程,類似程式的概念,能夠自行單獨啟動或銷燬,擁有自己獨立的資料庫。

微服務架構主要要解決哪些問題?

  1. 服務很多,客戶端怎麼訪問,如何提供對外閘道器?
  2. 這麼多服務,服務之間如何通訊? HTTP還是RPC?
  3. 這麼多服務,如何治理? 服務的註冊和發現。
  4. 服務掛了怎麼辦?熔斷機制。

有哪些主流微服務框架?

  1. Spring Cloud Netflix
  2. Spring Cloud Alibaba
  3. SpringBoot + Dubbo + ZooKeeper

SpringCloud有哪些核心元件?

SpringCloud

PS:微服務後面有機會再擴充套件,其實面試一般都是結合專案去問。



參考:

[1]. 《Spring揭祕》

[2]. 面試官:關於Spring就問這13個

[3]. 15個經典的Spring面試常見問題

[4].面試還不知道BeanFactory和ApplicationContext的區別?

[5]. Java面試中常問的Spring方面問題(涵蓋七大方向共55道題,含答案)

[6] .Spring Bean 生命週期 (例項結合原始碼徹底講透)

[7]. @Autowired註解的實現原理

[8].萬字長文,帶你從原始碼認識Spring事務原理,讓Spring事務不再是面試噩夢

[9].【技術乾貨】Spring事務原理一探

[10]. Spring的宣告式事務@Transactional註解的6種失效場景

[11].Spring官網

[12].Spring使用了哪些設計模式?

[13].《精通Spring4.X企業應用開發實戰》

[14].Spring 中的bean 是執行緒安全的嗎?

[15].小傅哥 《手擼Spring》

[16].手擼架構,Spring 面試63問

[17]. @Autowired註解的實現原理

[18].如何優雅地在 Spring Boot 中使用自定義註解

[19].Spring MVC原始碼(三) ----- @RequestBody和@ResponseBody原理解析


⭐面渣逆襲系列:

相關文章