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

大家好,我是老三啊,面渣逆襲 繼續,這節我們來搞定另一個面試必問知識點——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原理解析


⭐面渣逆襲系列:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章