天天看點

我一直在用,但是之前一直不知道的spring核心原了解析簡介Spring入門使用Spring如何去建立一個對象Bean的建立過程Spring的推斷構造方法Aware回調AOP的大緻流程Spring的事務

目錄

簡介

Spring入門使用

Spring如何去建立一個對象

Bean的建立過程

Spring的推斷構造方法

Aware回調

AOP的大緻流程

Spring的事務

spring對事務的處理邏輯

事務的傳播行為:

spring事務生效和失效的簡單判定

簡介

Spring是現在在我們日常開發中用到的非常多的一個東西,最新的調研也表明,Spring全家桶占用了開發場景中超過50%的覆寫

spring全家桶包括很多内容,spring framework,springboot,springcloud,springdata等

本篇文章分享的主要是spring framework,Spring framework是一個很重要很基礎的東西,就像springboot,也是基于spring framework做的

當然,如同标題寫的,本次是大概介紹核心原理,至于底層的真正源碼,本次不會涉及,後期也會慢慢自己學習整理處理分享給大家

Spring入門使用

首先是Spring的入門使用

在我們剛接觸Spring的時候,那個時候還不知道Spring boot這種的,我們對Spring的第一次使用,還是用的xml的配置形式,當然,直到現在,很多程式員還是在用這種方式,很多老項目也是這種方式

我們先來寫一個Spring的入門使用代碼

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = (UserService) context.getBean("userService");
userService.test();
           

這就是spring的一個入門使用,可能很多人對這個代碼有着莫名的熟悉感哈哈哈哈~

雖然這是Spring的入門級使用,但是,其實這個使用方式已經漸漸開始被淘汰了,在新的Springmvc和Spring boot中,已經使用了 AnnotationConfigApplicationContext,熟悉的人應該一看名字就知道,這是一個基于注解的使用方式

我們來寫一個基于這個 AnnotationConfigApplicationContext 的入門使用

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = (UserService) context.getBean("userService");
userService.test();
           

其實大家可以看出來,AnnotationConfigApplicationContext 的使用方法和 ClassPathXmlApplicationContext 的使用方法是非常非常相似的,他們的差別就是,ClassPathXmlApplicationContext 傳入的是一個xml檔案,而 AnnotationConfigApplicationContext 傳入的是一個bean

其實無論是哪個方式,都是指明了spring的配置,也可以指定Spring掃描的路徑,可以直接去定義bean

我們來看看差別:

Spring.xml的寫法

<context:component-scan base-package="com.study"/>
<bean id="userService" class="com.study.service.UserService"/>
           

我們再看看注解的寫法

@ComponentScan("com.study")
public class AppConfig {

	@Bean
	public UserService userService(){
		return new UserService();
	}

}
           

是以說,其實不管是哪種方法,本質上都是一樣的

從現在的角度看,我們都很少使用這兩種方法,是直接使用的springmvc和springboot,但是它們其實都是基于上面的方式的,都是需要去在内部建立一個 ApplicationContext

關于Springmvc和Spring boot的差別,其實也是上面兩種方式的差別,Springmvc建立的是 XmlWebApplicationContext ,而Spring boot建立的是 AnnotationConfigApplicationContext

以下是關于Spring的一些接口的簡單介紹

- 接口:BeanFactory:spring容器的最頂層接口。功能相對不是那麼強大。
- 接口:ApplicationContext:是BeanFactory的子接口,他繼承了BeanFactory接口,功能進行了擴充。開發中應該盡量使用該接口。
- 類:FileSystemXmlApplicationContext 配置檔案在磁盤或網絡某個位址,這個類用來讀取這樣的檔案
- 類:ClassPathXmlApplicationContext 讀取位于classpath目錄下的xml檔案
- 類:AnnotationConfigApplicationContext 讀取使用注解的spring配置
           

Spring如何去建立一個對象

不管是上面的 AnnotationConfigApplicationContext 也好,ClassPathXmlApplicationContext 也罷,我們都可以暫時将它們簡單了解為是去建立java對象的

但是Spring是如何去建立一個對象的呢?

我們都知道,Java建立對象,肯定是基于一個類的,我們再來回顧一下上面的Spring入門代碼

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = (UserService) context.getBean("userService");
userService.test();
           

可以看出來,當我們調用了getBean() 方法的時候,就會建立一個userService對象,但是在getBean 的内部,它一定是做了什麼,不然怎麼會知道我們傳入的要擷取的 "userService"對應的是UserService類呢?

是以,我們就可以推斷出來,在調用 AnnotationConfigApplicationContext 的構造方法的時候,會去做這麼一些事情

1、解析傳進去的 Application.class ,得到掃描路徑

2、周遊掃描路徑下的所有Java類,如果發現某個類存在@Component、@Service等注解,Spring就會記錄這個類,存放在一個内部map中

3、Spring根據某種自己的規則生成目前類的bean name,把bean name當作key,目前類當作value,存入map中,

這樣,當我們調用 getBean() 方法的時候,就可以根據名字去擷取到對應的類了

Bean的建立過程

我們知道了是怎麼擷取到一個bean的,但是,Spring是怎麼建立一個bean的呢?

這就是bean建立的生命周期

bean建立的生命周期大概是這樣的:

1、利用要建立的類的構造方法執行個體化得到一個對象(注意,此處這個類的構造方法可能有多個,Spring會采用叫做 ‘推斷構造方法’ 的規則)

2、得到一個對象後,Spring會判斷這個對象中是否存在被 @Autowired 注解修飾的屬性,如果存在,Spring會把這些屬性找出來,并進行指派,這個找出來指派的操作,其實就是依賴注入

3、依賴注入後,Spring會判斷這個對象是不是實作了BeanNameAware接口、BeanClassLoaderAware接口、BeanFactoryAware接口,如果實作了,就表示目前對象必須實作該接口中所定義的setBeanName()、setBeanClassLoader()、setBeanFactory()方法,那Spring就會調用這些方法并傳入相應的參數,這就是所謂的Aware回調

4、Aware回調之後,Spring會判斷該對象中,是否存在某個方法被@PostConstruct注解修飾了,如果存在,Spring會調用目前對象的此方法,這個過程是在初始化前

5、然後,Spring會判斷目前對象是否實作了InitializingBean接口,如果實作了,就表示目前對象必須實作該接口中的afterPropertiesSet()方法,那Spring就會調用目前對象中的afterPropertiesSet()方法,這個就是初始化

6、最後,Spring會判斷這個對象需不需要進行AOP,如果不需要,那麼這個bean就建立完成了,如果需要進行AOP處理,那就會進行動态代理生成一個代理對象作為bean,注意⚠️,經過AOP的,不再是純淨的對象,而是一個被代理過的對象

類→構造方法→對象→依賴注入→→→bean

在上面第二步的時候,我們說到了依賴注入,那這個依賴注入的屬性該怎麼做呢?

首先是肯定需要找到這個類哪些屬性使用了 @Autowired 注解,我們可以擷取class檔案,擷取所有屬性,判斷是否有這個注解

Field[] fields = orderService.getClass().getFields();
for (Field field : fields) {
  boolean hasAnnotation = field.isAnnotationPresent(Autowired.class);
}
           

如果有這個注解,我們就可以給這個類的這個字段指派,無非就是給這個屬性指派什麼,這個我們可以之後再找

Field[] fields = orderService.getClass().getFields();
for (Field field : fields) {
    boolean hasAnnotation = field.isAnnotationPresent(Autowired.class);
    if (hasAnnotation) {
        field.set(orderService, ?);
    }
}
           

Spring的推斷構造方法

Spring在基于某一個類生成bean的過程中,需要使用該類的構造方法來進行執行個體化,但是如果一個類存在多個構造方法,Spring會使用哪個呢?

先說答案:

1、如果一個類隻存在一個構造方法,不管這個構造方法是有參還是無參,都會用這個構造方法,因為沒得選

2、如果一個類存在多個構造方法,Spring會找有沒有預設的無參構造方法,如果有無參構造方法,就選擇無參構造方法,畢竟無參構造方法本身代表了一種預設的含義

3、如果一個類存在多個構造方法,并且沒有無參的構造方法,Spring就會報錯

4、如果一個類存在多個構造方法,也沒有無參的構造方法,但是,有一個構造方法使用了@Autowired注解,那麼Spring就會使用這個構造方法

有一個地方需要注意,如果Spring選擇了一個有參的構造方法,Spring在調用這個有參的構造方法的時候,需要傳入參數,so,這個參數從哪兒來呢?

Spring會根據入參的類型和入參的名字,從Spring容器中找bean對象,如果根據類型找到了多個,那就再根據入參的名字确定唯一的一個,如果最後沒有辦法确定,那就會報錯,無法建立目前bean對象

Spring這個确定用哪個構造方法,确定入參的bean對象,這種行為過程,就叫做推斷構造方法

在java中,無參的構造方法就是認為是預設的

如果提供一個有參的構造方法,傳入參數,這個參數是有值的,這個值會從spring容器裡去找,也就是說,這個參數必須是一個bean,不是bean的話,不在spring容器裡,就無法找到,就不會有值,會抛異常,因為初始化要求你必須有一個值

spring的bean的預設命名是類名首字母小寫

如果在spring容器中,先根據名字去找,找到的bean類型,可能并不是正确的類型,如果根據類型去找,找到一個,那就直接用,但是可能會找到多個,找到多個的話,那就根據名字再找,如果找不到,那就報錯,名字相同的bean,會發生bean覆寫

同名bean,@Bean注解會覆寫@Compont注解

Aware回調

假設這麼一個需求,我想要在注入userService這個bean的時候,它裡面有一個goods屬性,我希望這個goods屬性,我取出來的時候就是我資料庫裡存的指定的值,這個該怎麼辦呢?注意,是指定值

@Component("userService")
public class UserService {

    private Goods goods;

}
           

怎麼辦呢,可不可以這樣,在生成這個 userService 這個bean之前,我們進行一次指派如何?說幹就幹

@Component("userService")
public class UserService {

    private Goods goods;
    
    private void updateGoods( ) {
        // todo ......
        // todo ......
        // todo ......
        // todo ......
        this.goods = ;
    }

}
           

我們提供一個方法來指派,如果spring在成為bean前調用了這個方法,那不就OK了麼

是以我們就用到了 @PostConstruct 注解,加了這個注解,spring 就知道它要調用這個方法,要注意,這個方法會影響這個bean的生成速度及結果,之前我也出過這個錯,具體的錯誤請看我這篇總結:

https://blog.csdn.net/weixin_46097842/article/details/116357109

那,spring怎麼知道哪些方法需要執行呢,說白了,就是spring需要知道哪些方法加了注解,跟上面給注入的屬性指派一樣,我們寫一個僞代碼

for (Method method : userService.getClass().getMethods()) {
    boolean hasAnnotation = method.isAnnotationPresent(PostConstruct.class);
}
           

如果判斷出來有注解,就可以進行指派

for (Method method : userService.getClass().getMethods()) {
    boolean hasAnnotation = method.isAnnotationPresent(PostConstruct.class);
    if (hasAnnotation) {
        method.invoke(userService, ?);
    }
}
           

當然,除了 @Postconstruct 之外,還可以實作 InitializingBean,重寫afterPropertiesSet 方法

@Component("userService")
public class UserService implements InitializingBean {

    private Goods goods;

    @PostConstruct
    private void updateGoods( ) {
        // todo ......
        // todo ......
        // todo ......
        // todo ......
        this.goods = null;
    }

    @Override
    public void afterPropertiesSet() {
        
    }
}
           

當然,相應的spring也需要判斷是否實作了 InitializingBean

boolean b = userService instanceof InitializingBean;
           

在spring的底層,spring判斷,如果實作了InitializingBean 接口,那就強轉為 InitializingBean,調用 .afterPropertiesSet()

我一直在用,但是之前一直不知道的spring核心原了解析簡介Spring入門使用Spring如何去建立一個對象Bean的建立過程Spring的推斷構造方法Aware回調AOP的大緻流程Spring的事務

AOP的大緻流程

先說一點,AOP是在初始化後做的,但是是在生成bean之前,進行AOP後,我們就會真的得到一個bean,而得到的bean,是一個被代理的對象,不再是純淨的對象

這兒有一個地方需要注意,在進行AOP代理時,産生的代理的bean,裡面注入的屬性是null,我們來寫一下代碼試試

package own.study.domain;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import own.study.domain.oneex.Application;
import own.study.domain.oneex.spring.UserService;
import own.study.vo.User;


public class TestMain {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(Application.class);
        UserService userService = (UserService) annotationConfigApplicationContext.getBean("myservice");
        userService.printMessage();
    }
}
           
package own.study.domain.oneex.spring;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component("myservice")
public class UserService {

    @Autowired
    private GoodsService goodsService;

    public void printMessage () {
        System.out.println("this is userservice's method output");
    }

}
           
package own.study.domain.oneex.spring;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;

@Aspect
@Component
@EnableAspectJAutoProxy
public class SpringAop {

    @Before(value = "execution (* own.study.domain.oneex.spring.UserService.*(..))")
    public void doAop( ) {
        System.out.println("before aop");
    }

}
           
package own.study.domain.oneex.spring;

import org.springframework.stereotype.Component;

@Component
public class GoodsService {
}
           
我一直在用,但是之前一直不知道的spring核心原了解析簡介Spring入門使用Spring如何去建立一個對象Bean的建立過程Spring的推斷構造方法Aware回調AOP的大緻流程Spring的事務

我們可以看到,userService已經被代理了,而且内部的goodsService是null,原因是因為,AOP的作用是在初始化後,初始化後并沒有進行一個指派,并沒有進行一個依賴注入

雖然AOP後的代理對象注入的屬性沒有值,但是在執行方法時,注入的屬性就有值了

我一直在用,但是之前一直不知道的spring核心原了解析簡介Spring入門使用Spring如何去建立一個對象Bean的建立過程Spring的推斷構造方法Aware回調AOP的大緻流程Spring的事務

原因是因為,CJLIB會先生成一個代理類,就可以重寫被代理的類的方法,當我們執行方法,其實執行的是代理類的方法,然後會調用父類,也就是被代理類的方法,當然,會先執行切面方法(CJLIB是基于類的,基于繼承關系,而JDK的動态代理,是基于接口的)

可能你會覺得,代理的方法其實就是super.方法(),如果真的是這樣,是調用到了父類的方法,但是别忘了, 這中間隻是去執行了切面方法,并沒有做任何事,父類方法就算被調用了,也影響不到注入的屬性,注入的屬性依舊沒有值

要解決這個問題,其實也有辦法,在AOP的代理類最外層聲明一個被代理類的對象,假設屬性名叫tag,在生成AOP的代理類對象之前,先生成這個被代理類bean放入spring容器中,然後給AOP生成的代理類對象的tag指派已經生成好的被代理類的對象bean,執行這個tag的方法,不就ok了嗎?

要知道這個tag,是經過依賴注入的,是一個純淨的普通對象,這個普通對象可是進行過初始化的,是建立出來的,注入的屬性是有值的

我一直在用,但是之前一直不知道的spring核心原了解析簡介Spring入門使用Spring如何去建立一個對象Bean的建立過程Spring的推斷構造方法Aware回調AOP的大緻流程Spring的事務

 有可能有人有一個問題,為什麼不給代理對象也加一個依賴注入的流程呢?但其實并沒有什麼意義,因為且面是為了切一個方法,最重要的意義是,執行某個特定的方法的時候,會執行切面方法,你額外加了一步依賴注入,沒什麼必要了就,就算你真的要用,因為是切面方法,直接從入參去getTarget不也能拿到麼

我一直在用,但是之前一直不知道的spring核心原了解析簡介Spring入門使用Spring如何去建立一個對象Bean的建立過程Spring的推斷構造方法Aware回調AOP的大緻流程Spring的事務

那麼,在初始化bean的時候,怎麼判斷這個bean需不需要進行AOP?初始化一個bean的時候,spring怎麼知道這個類被切了呢,這個原因也很簡單,切面類就是切面bean,在spring裡面,先找出所有的切面bean,周遊所有的切面bean,拿到每一個切面bean裡面的方法,判斷是否有@Before啊啥的注解,如果有,判斷和正在建立的bean類型是否比對,如果比對,那就要進行AOP,并将切面方法緩存起來,當需要執行的時候,直接從緩存裡将全部的符合的方法拿出來執行,緩存map的結構,可以了解為,key是正在建立的bean的相關資訊,value是對應的方法集合

到了這裡,Bean的建立的生命周期已經是這樣:

類→構造方法(推斷構造方法)→普通對象→依賴注入(屬性指派)→初始化前(@PostConstruct)→初始化(InitializingBean)→初始化後(AOP)→代理對象→bean

Spring的事務

spring對事務的處理邏輯

1、判斷是否存在 @Transactional 注解

2、由事務管理器建立一個資料庫連接配接(事務内包含的操作,一定要是同一個DataSource)

3、将自動送出改為false

4、執行開發者的業務邏輯

5、如果執行完沒有異常,那就送出,有異常,那就復原

事務的傳播行為:

存在事務,那就加入,不存在,那就建立一個新事務(預設)
    存在事務,那就加入,不存在,那就用非事務方式運作
    存在事務,那就加入,不存在,那就抛異常
    建立一個新事務,如果目前有事務,那就把目前事務挂起
    以非事務運作,如果目前有事務,把目前事務挂起
    以非事務運作,如果目前有事務,就抛異常
    存在事務,那就建立一個嵌套事務,沒有事務,那就建立一個新事務
           

spring事務生效和失效的簡單判定

是否是代理類進行的調用,如果是被代理類調用,那是不會生效的

當然,事務失效的原因很多,這隻是代理方面的失效,解決代理方面的失效其實很簡單,可以建立一個類,交給spring管理,然後調用,就沒問題,但是這種比較麻煩,最簡單的,其實可以被代理類自己注入自己,然後調用方法,這樣也能解決事務失效的問題

總之,核心就一個,讓代理類去調用

這裡有一個地方一定要注意,jdbctemplate和事務管理器,一定要是同一個DataSource,事務才能生效,如果不是同一個DataSource,也會出現事務不生效的情況

好啦,本次的分享就到這兒,就像上面說的,本次知識大概分享一下spring比較重要的一些原理,後期會慢慢去研究源碼啊什麼的更深入的東西