Spring 中不得不瞭解的姿勢

紫邪情發表於2024-03-13

說明

本文非原創,我只是進行了整理以及做了一些改動,僅供學習,若需進行商業使用,請聯絡原作者

原作者:蘇三

原文連結:蘇三說技術:Spring系列

Spring IOC

本章節解讀的流程為Spring容器初始化的前期準備工作

  1. Spring容器初始化的入口
  2. refresh方法的主要流程
  3. 解析xml配置檔案
  4. 生成BeanDefinition
  5. 註冊BeanDefinition
  6. 修改BeanDefinition
  7. 註冊BeanPostProcessor

真正的好戲是後面的流程:例項化Bean依賴注入初始化BeanBeanPostProcessor呼叫等。

入口

Spring容器的頂層介面是:BeanFactory,但我們使用更多的是它的子介面:ApplicationContext

通常情況下,如果我們想要手動初始化透過xml檔案配置的Spring容器時,程式碼是這樣的:

ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");

User user = (User)applicationContext.getBean("name");

如果想要手動初始化透過配置類配置的Spring容器時,程式碼是這樣的:

AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Config.class);

User user = (User)applicationContext.getBean("name");

這兩個類應該是最常見的入口了,它們卻殊途同歸,最終都會呼叫refresh方法,該方法才是Spring容器初始化的真正入口。

image-20240313125038104

image-20240313125132546

呼叫refresh方法的類並非只有這兩個,用一張圖整體認識一下:

圖片

雖說呼叫refresh方法的類有這麼多,但我決定用ClassPathXmlApplicationContext類作為列子,因為它足夠經典,而且難度相對來說要小一些。

refresh方法

refresh方法是Spring IOC的真正入口,它負責初始化Spring容器。refresh表示重新構建的意思。

既然這個方法的作用是初始化Spring容器,那方法名為啥不叫init?因為它不只被呼叫一次。

Spring BootSpringAppication類中的run方法會呼叫refreshContext方法,該方法會呼叫一次refresh方法。

spring CloudBootstrapApplicationListener類中的onApplicationEvent方法會呼叫SpringAppication類中的run方法。也會呼叫一次refresh方法。

這是Spring Boot專案中如果引入了Spring Cloud,則refresh方法會被呼叫兩次的原因。

Spring MVCFrameworkServlet類中的initWebApplicationContext方法會呼叫configureAndRefreshWebApplicationContext方法,該方法會呼叫一次refresh方法,不過會提前判斷容器是否啟用。

所以這裡的refresh表示重新構建的意思。

refresh的關鍵步驟:

圖片

一眼看過去好像有很多方法,但是真正的核心的方法不多,我主要講其中最重要的:

  • obtainFreshBeanFactory
  • invokeBeanFactoryPostProcessors
  • registerBeanPostProcessors
  • 【finishBeanFactoryInitialization】

obtainFreshBeanFactory:解析xml配置檔案,生成BeanDefinition物件,註冊到Spring容器中

obtainFreshBeanFactory方法會解析xml的bean配置,生成BeanDefinition物件,並且註冊到Spring容器中(說白了就是很多map集合中)。

經過幾層呼叫之後,會調到AbstractBeanDefinitionReader類的loadBeanDefinitions方法:

image-20240313131048977

該方法會迴圈locations(applicationContext.xml檔案路徑),呼叫另外一個loadBeanDefinitions方法,一個檔案一個檔案解析。

經過一些列的騷操作,會將location轉換成inputSource和resource,然後再轉換成Document物件,方便解析。

image-20240313131708708

在解析xml檔案時,需要判斷是預設標籤,還是自定義標籤,處理邏輯不一樣:

圖片

Spring的預設標籤只有4種:

  • <import/>
  • <alias/>
  • <bean/>
  • <beans/>

對應的處理方法是:

圖片

提示

常見的:<aop/><context/><mvc/>等都是自定義標籤。

從上圖中處理<bean/>標籤的processBeanDefinition方法開始,經過一系列呼叫,最終會調到DefaultBeanDefinitionDocumentReader類的processBeanDefinition方法。這個方法包含了關鍵步驟:解析元素生成BeanDefinition 和 註冊BeanDefinition。

圖片

生成BeanDefinition

上面的方法會呼叫BeanDefinitionParserDelegate類的parseBeanDefinitionElement方法:

圖片

一個<bean/>標籤會對應一個BeanDefinition物件。

該方法又會呼叫同名的過載方法:processBeanDefinition,真正建立BeanDefinition物件,並且解析一系列引數填充到物件中:

圖片

其實真正建立BeanDefinition的邏輯是非常簡單的,直接new了一個物件:

image-20240313132421796

真正複雜的地方是在前面的各種屬性的解析和賦值上。

註冊BeanDefinition

上面透過解析xml檔案生成了很多BeanDefinition物件,下面就需要把BeanDefinition物件註冊到Spring容器中,這樣Spring容器才能初始化bean。

BeanDefinitionReaderUtils類的registerBeanDefinition方法很簡單,只有兩個流程:

圖片

先看看DefaultListableBeanFactory類的registerBeanDefinition方法是如何註冊beanName的:

圖片

接下來看看SimpleAliasRegistry類的registerAlias方法是如何註冊alias別名的:

圖片

這樣就能透過多個不同的alias找到同一個name,再透過name就能找到BeanDefinition

invokeBeanFactoryPostProcessors:修改已經註冊的BeanDefinition物件

上面BeanDefinition物件已經註冊到Spring容器當中了,接下來,如果想要修改已經註冊的BeanDefinition物件該怎麼辦?

refresh方法中透過invokeBeanFactoryPostProcessors方法修改BeanDefinition物件。

經過一系列的呼叫,最終會到PostProcessorRegistrationDelegate類的invokeBeanFactoryPostProcessors方法:

圖片

流程看起來很長,其實邏輯比較簡單,主要是在處理BeanDefinitionRegistryPostProcessorBeanFactoryPostProcessor

BeanDefinitionRegistryPostProcessor本身是一種特殊的BeanFactoryPostProcessor,它也會執行BeanFactoryPostProcessor的邏輯,只是加了一個額外的方法

image-20240313132656276

ConfigurationClassPostProcessor可能是最重要的BeanDefinitionRegistryPostProcessor,它負責處理@Configuration註解。

registerBeanPostProcessors:註冊BeanPostProcessor

處理完前面的邏輯,refresh方法接著會呼叫registerBeanPostProcessors註冊BeanPostProcessor,它的功能非常強大。

經過一系列的呼叫,最終會到PostProcessorRegistrationDelegate類的registerBeanPostProcessors方法:

圖片

注意

這一步只是註冊BeanPostProcessor,真正的使用在後面。

Spring AOP

從實戰出發

在Spring AOP還沒出現之前,想要在目標方法之前先後加上日誌列印的功能,我們一般是這樣做的:

@Service
public class TestService {

    public void doSomething1() {
        beforeLog();
        System.out.println("==doSomething1==");
        afterLog();
    }

    public void doSomething2() {
        beforeLog();
        System.out.println("==doSomething1==");
        afterLog();
    }

    public void doSomething3() {
        beforeLog();
        System.out.println("==doSomething1==");
        afterLog();
    }

    public void beforeLog() {
        System.out.println("列印請求日誌");
    }

    public void afterLog() {
        System.out.println("列印響應日誌");
    }
}

如果加了新doSomethingXXX方法,就需要在新方法前後手動加beforeLog和afterLog方法。

原本相安無事的,但長此以往,總會出現幾個刺頭青。

刺頭青A說:每加一個新方法,都需要加兩行重複的程式碼,是不是很麻煩?

刺頭青B說:業務程式碼和公共程式碼是不是耦合在一起了?

刺頭青C說:如果有幾千個類中加了公共程式碼,而有一天我需要刪除,是不是要瘋了?

Spring大師們說:我們提供一套Spring的AOP機制,你們可以閉嘴了。

下面看看用Spring AOP(還用了aspectj)是如何列印日誌的:

@Service
public class TestService {

    public void doSomething1() {
        System.out.println("==doSomething1==");
    }

    public void doSomething2() {
        System.out.println("==doSomething1==");
    }

    public void doSomething3() {
        System.out.println("==doSomething1==");
    }
}




@Component
@Aspect
public class LogAspect {

    @Pointcut("execution(public * com.sue.cache.service.*.*(..))")
    public void pointcut() {
    }

    @Before("pointcut()")
    public void beforeLog() {
        System.out.println("列印請求日誌");
    }

    @After("pointcut()")
    public void afterLog() {
        System.out.println("列印響應日誌");
    }
}

改造後,業務方法在TestService類中,而公共方法在LogAspect類中,是分離的。如果要新加一個業務方法,直接加就好,LogAspect類不用改任何程式碼,新加的業務方法就自動擁有列印日誌的功能

圖片

Spring AOP其實是一種橫切的思想,透過動態代理技術將公共程式碼織入到業務方法中。

AOP不是spring獨有的,目前市面上比較出名的有:

  • aspectj
  • spring aop
  • jboss aop

我們現在主流的做法是將Spring AOP和aspectj結合使用,Spring借鑑了AspectJ的切面,以提供註解驅動的AOP。

此時,一個吊毛一閃而過。

刺頭青D問:你說的“橫切”,“動態代理”,“織入” 是什麼雞巴意思?

幾個重要的概念

根據上面Spring AOP的程式碼,用一張圖聊聊幾個重要的概念:

圖片

  • 連線點(Joinpoint):程式執行的某個特定位置,如某個方法呼叫前,呼叫後,方法丟擲異常後,這些程式碼中的特定點稱為連線點。
  • 切點(Pointcut):每個程式的連線點有多個,如何定位到某個感興趣的連線點,就需要透過切點來定位。
  • 通知 / 增強(Advice):增強是織入到目標類連線點上的一段程式程式碼。
  • 切面(Aspect):切面由切點和通知組成,它既包括了橫切邏輯的定義,也包括了連線點的定義,SpringAOP就是將切面所定義的橫切邏輯織入到切面所制定的連線點中。
  • 目標物件(Target):需要被增強的業務物件
  • 代理類(Proxy):一個類被AOP織入增強後,就產生了一個代理類。
  • 織入(Weaving):織入就是將增強新增到對目標類具體連線點上的過程。

還是刺頭青D那個吊毛說(旁邊:這位仁兄比較好學):Spring AOP概念弄明白了,@Pointcut註解的execution表示式剛剛看得我一臉懵逼,可以再說說不?貧道請你去洗腳城

execution:切入點表示式

@Pointcut註解的execution切入點表達,看似簡單,裡面還是有些內容的。為了更直觀一些,還是用張圖來總結一下:

圖片

該表示式的含義是:匹配訪問許可權是public,任意返回值,包名為:com.sue.cache.service,下面的所有類所有方法和所有引數型別(用*表示)。

如果具體匹配某個類,比如:TestService,則表示式可以換成:

@Pointcut("execution(public * com.sue.cache.service.TestService.*(..))")

其實Spring支援9種表示式,execution只是其中一種

圖片

有哪些入口?

Spring AOP有哪些入口?說人話就是在問:Spring中有哪些場景需要呼叫AOP生成代理物件?你不好奇?

img

一張圖概括:

圖片

入口1:自定義TargetSource的場景

AbstractAutowireCapableBeanFactory類的createBean方法中,有這樣一段程式碼:

image-20240313134102394

它透過BeanPostProcessor提供了一個生成代理物件的機會。具體邏輯在AbstractAutoProxyCreator類的postProcessBeforeInstantiation方法中:

image-20240313134229315

說白了,需要實現TargetSource才有可能會生成代理物件。該介面是對Target目標物件的封裝,透過該介面可以獲取到目標物件的例項。

不出意外,這時又會冒出一個吊毛。

刺頭青F說:這裡生成代理物件有什麼用呢?

有時我們想自己控制bean的建立和初始化,而不需要透過spring容器,這時就可以透過實現TargetSource滿足要求。只是建立單純的例項還好,如果我們想使用代理該怎麼辦呢?這時候,入口1的作用就體現出來了。

入口2:解決代理物件迴圈依賴問題的場景

AbstractAutowireCapableBeanFactory類的doCreateBean方法中,有這樣一段程式碼:

image-20240313134449968

它主要作用是為了解決物件的迴圈依賴問題,核心思路是提前暴露singletonFactory到快取中。

透過getEarlyBeanReference方法生成代理物件:

image-20240313134655579

它又會呼叫wrapIfNecessary方法:

image-20240313134931118

這裡有你想看到的生成代理的邏輯。

這時。。。。,你猜錯了,吊毛為報養育之恩,帶父嫖娼去了。。。

入口3:普通Bean生成代理物件的場景

AbstractAutowireCapableBeanFactory類的initializeBean方法中,有這樣一段程式碼:

image-20240313135115847

它會呼叫到AbstractAutoProxyCreator類postProcessAfterInitialization方法:

image-20240313135632127

該方法中能看到我們熟悉的面孔:wrapIfNecessary方法。從上面得知該方法裡面包含了真正生成代理物件的邏輯。

這個入口,是為了給普通bean能夠生成代理用的,是Spring最常見並且使用最多的入口。

JDK動態代理 vs cglib

JDK動態代理

jdk動態代理是透過反射技術實現的

jdk動態代理三個要素:

  • 定義一個介面
  • 實現InvocationHandler介面
  • 使用Proxy建立代理物件
public interface IUser {
    void add();
}



public class User implements IUser{
    @Override
    public void add() {
        System.out.println("===add===");
    }
}




public class JdkProxy implements InvocationHandler {

    private Object target;

    public Object getProxy(Object target) {
        this.target = target;
        // 建立一個代理物件
        return Proxy.newProxyInstance(this.getClass().getClassLoader(),
                                      target.getClass().getInterfaces(),
                                      this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target, args);
        after();
        return result;
    }

    private void before() {
        System.out.println("===before===");
    }

    private void after() {
        System.out.println("===after===");
    }
}



public class Test {
    public static void main(String[] args) {
        User user = new User();
        JdkProxy jdkProxy = new JdkProxy();
        IUser proxy = (IUser)jdkProxy.getProxy(user);
        proxy.add();
    }
}

cglib

cglib底層是透過asm位元組碼技術實現的

cglib兩個要素:

  • 實現MethodInterceptor介面
  • 使用Enhancer建立代理物件
public class User {
    public void add() {
        System.out.println("===add===");
    }
}



public class CglibProxy implements MethodInterceptor {

    private Object target;

    public Object getProxy(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass());
        enhancer.setCallback(this);
        // 透過Enhancer建立代理物件
        return enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
        Object result = method.invoke(target,objects);
        after();
        return result;
    }

    private void before() {
        System.out.println("===before===");
    }

    private void after() {
        System.out.println("===after===");
    }
}



public class Test {
    public static void main(String[] args) {
        User user = new User();
        CglibProxy cglibProxy = new CglibProxy();
        IUser proxy = (IUser)cglibProxy.getProxy(user);
        proxy.add();
    }
}

Spring中如何用的?

DefaultAopProxyFactory類的createAopProxy方法中,有這樣一段程式碼:

image-20240313135934771

它裡面包含:

  • JdkDynamicAopProxy JDK動態代理生成類
  • ObjenesisCglibAopProxy cglib代理生成類

JdkDynamicAopProxy類的invoke方法生成的代理物件。而ObjenesisCglibAopProxy類的父類:CglibAopProxy,它的getProxy方法生成的代理物件。

哪個更好?

不出意外,又會來個吊毛,但這吊毛不是別人,是你!

啊,蒼天啊,大地呀!勒個墳哇,我熱你溫啦:JDK動態代理和cglib哪個更好啊?

嘻嘻~其實這個問題沒有標準答案,要看具體的業務場景:

  1. 沒有定義介面,只能使用cglib,不說它好不行。
  2. 定義了介面,需要建立單例或少量物件,呼叫多次時,可以使用jdk動態代理,因為它建立時更耗時,但呼叫時速度更快。
  3. 定義了介面,需要建立多個物件時,可以使用cglib,因為它建立速度更快。

隨著jdk版本不斷迭代更新,jdk動態代理建立耗時不斷被最佳化,8以上的版本中,跟cglib已經差不多。所以Spring官方預設推薦使用jdk動態代理,因為它呼叫速度更快。

如果要強制使用cglib,可以透過以下兩種方式:

  • spring.aop.proxy-target-class=true
  • @EnableAspectJAutoProxy(proxyTargetClass = true)

五種通知 / 增強

圖片

Spring AOP給這五種通知,分別分配了一個xxxAdvice類。在ReflectiveAspectJAdvisorFactory類的getAdvice方法中可以看得到:

image-20240313140355903

用一張圖總結一下對應關係:

圖片

這五種xxxAdvice類都實現了Advice介面,但是有些差異。

下面三個xxxAdvice類實現了MethodInterceptor介面:

圖片

前置通知

該通知在方法執行之前執行,只需在公共方法上加@Before註解,就能定義前置通知

@Before("pointcut()")
public void beforeLog(JoinPoint joinPoint) {
    System.out.println("列印請求日誌");
}

後置通知

該通知在方法執行之後執行,只需在公共方法上加@After註解,就能定義後置通知

@After("pointcut()")
public void afterLog(JoinPoint joinPoint) {
    System.out.println("列印響應日誌");
}

環繞通知

該通知在方法執行前後執行,只需在公共方法上加@Round註解,就能定義環繞通知

@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("列印請求日誌");
    Object result = joinPoint.proceed();
    System.out.println("列印響應日誌");
    return result;
}

結果通知

該通知在方法結束後執行,能夠獲取方法返回結果,只需在公共方法上加@AfterReturning註解,就能定義結果通知

@AfterReturning(pointcut = "pointcut()",returning = "retVal")
public void afterReturning(JoinPoint joinPoint, Object retVal) {
    System.out.println("獲取結果:"+retVal);
}

異常通知

該通知在方法丟擲異常之後執行,只需在公共方法上加@AfterThrowing註解,就能定義異常通知

@AfterThrowing(pointcut = "pointcut()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Throwable e) {
    System.out.println("異常:"+e);
}

一個猝不及防,依然是刺頭青D那個吊毛,不知何時從洗腳城回來站你身後,你莫名感覺一緊問了句:這五種通知的執行順序是怎麼樣的?

五種通知的執行順序

單個切面正常情況

圖片

單個切面異常情況

圖片

多個切面正常情況

圖片

多個切面異常情況

圖片

提示

當有多切面時,按照可以透過@Order(n)指定執行順序,n值越小越先執行。

為什麼使用鏈式呼叫?

這個問題沒人問,是我自己想聊聊(旁白:因為我長得相當哇塞)

其實這個問題一看就知道答案了,即為什麼要使用責任鏈模式?

先看看Spring是如何使用鏈式呼叫的,在ReflectiveMethodInvocation的proceed方法中,有這樣一段程式碼:

image-20240313140528790

用一張圖捋一捋上面的邏輯:

圖片

包含了一個遞迴的鏈式呼叫,為什麼要這樣設計?

假如不這樣設計,我們程式碼中是不是需要寫很多if...else,根據不同的切面和通知單獨處理?

而Spring巧妙的使用責任鏈模式消除了原本需要大量的if...else判斷,讓程式碼的擴充套件性更好,很好的體現了開閉原則:對擴充套件開放,對修改關閉。

快取中存的是原始物件還是代理物件?

都知道Spring中為了效能考慮是有快取的,通常說包含了三級快取:

圖片

只聽“咻兒”地一聲,刺頭青D的兄弟,刺頭青F忍不住趕過來問了句:快取中存的是原始物件還是代理物件?

前面那位帶父搬磚的仁兄下意識地來了一句:應該不是物件,是馬子

嘻嘻~這個問題要從三個方面回答

singletonFactories(三級快取)

AbstractAutowireCapableBeanFactory類的doCreateBean方法中,有這樣一段程式碼:

image-20240313140634901

其實之前已經說過,它是為了解決迴圈依賴問題。這次要說的是addSingletonFactory方法:

image-20240313140730051

它裡面儲存的是singletonFactory物件,所以是原始物件

earlySingletonObjects(二級快取)

AbstractBeanFactory類的doGetBean方法中,有這樣一段程式碼:

image-20240313140908949

在呼叫getBean方法獲取bean例項時,會呼叫getSingleton嘗試先從快取中看能否獲取到,如果能獲取到則直接返回。

image-20240313141011485

這段程式碼會先從一級快取中獲取bean,如果沒有再從二級快取中獲取,如果還是沒有則從三級快取中獲取singletonFactory,透過getObject方法獲取例項,將該例項放入到二級快取中。

答案的謎底就聚焦在getObject方法中,而這個方法又是在哪來定義的呢?

其實就是上面的getEarlyBeanReference方法,我們知道這個方法生成的是代理物件,所以二級快取中存的是代理物件。

singletonObjects(一級快取)

提示

走好,看好,眼睛不要打跳(t iao~ 三聲),這裡是DefaultSingletonBeanRegistry類的getSingleton方法,跟上面二級快取中說的AbstractBeanFactory類getSingleton方法不一樣

DefaultSingletonBeanRegistry類的getSingleton方法中,有這樣一段程式碼:

image-20240313141135925

此時的bean建立、注入和初始化完成了,判斷如果是新的單例物件,則會加入到一級快取中,具體程式碼如下:

image-20240313141210906

Spring AOP幾個常見的坑

我們幾乎每天都在用Spring AOP。

“啥子?我怎麼不知道,你說兒豁誒?” 。

如果你每天在用Spring事務的話,就是每天在用Spring AOP,因為Spring事務的底層就用到了Spring AOP。

本節可跳過,可直接看後面的:Spring事務,這裡只選取了部分內容

坑1:方法內部呼叫

使用Spring事務時,直接方法呼叫

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Transactional
    public void add(UserModel userModel) {
        userMapper.queryUser(userModel);
        save(userModel);
    }

    @Transactional
    public void save(UserModel userModel) {
        System.out.println("儲存資料");
    }
}

這種情況直接方法呼叫Spring AOP無法生成代理物件,事務會失效。這個問題的解決辦法有很多:

  1. 使用TransactionTemplate手動開啟事務
  2. 將事務方法save放到新加的類UserSaveService中,透過userSaveService.save呼叫事務方法。
  3. UserService類中@Autowired注入自己的例項userService,透過userService.save呼叫事務方法。
  4. 透過AopContext類獲取代理物件:((UserService)AopContext.currentProxy()).save(user);

坑2:訪問許可權錯誤

@Service
public class UserService {
    @Autowired
    private UserService userService;
    @Autowired
    private UserMapper userMapper;

    public void add(UserModel userModel) {
        userMapper.queryUser(userModel);
        userService.save(userModel);
    }

    @Transactional
    private void save(UserModel userModel) {
        System.out.println("儲存資料");
    }
}

上面用 UserService類中@Autowired注入自己的例項userService的方式解決事務失效問題,如果不出意外的話,是可以的。

但是恰恰出現了意外,save方法被定義成了private的,這時也無法生成代理物件,事務同樣會失效。

因為Spring要求被代理方法必須是public

坑3:目標類用final修飾

@Service
public class UserService {
    @Autowired
    private UserService userService;
    @Autowired
    private UserMapper userMapper;

    public void add(UserModel userModel) {
        userMapper.queryUser(userModel);
        userService.save(userModel);
    }

    @Transactional
    public final void save(UserModel userModel) {
        System.out.println("儲存資料");
    }
}

這種情況Spring AOP生成代理物件,重寫save方法時,發現的final的,重寫不了,也會導致事務失效。

如果某個方法用final修飾了,那麼在它的代理類中,就無法重寫該方法,而新增事務功能

重要提示

如果某個方法是static的,同樣無法透過動態代理,變成事務方法。

坑4:迴圈依賴問題

在使用@Async註解開啟非同步功能的場景,它會透過AOP自動生成代理物件

@Service
public class TestService1 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {
    }
}



@Service
public class TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

啟動服務會報錯:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

獲取Spring容器物件的方式

實現BeanFactoryAware介面

實現BeanFactoryAware介面,然後重寫setBeanFactory方法,就能從該方法中獲取到Spring容器物件。

@Service
public class PersonService implements BeanFactoryAware {
    private BeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    public void add() {
        Person person = (Person) beanFactory.getBean("person");
    }
}

實現ApplicationContextAware介面

實現ApplicationContextAware介面,然後重寫setApplicationContext方法,也能從該方法中獲取到Spring容器物件。

@Service
public class PersonService2 implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public void add() {
        Person person = (Person) applicationContext.getBean("person");
    }

}

實現ApplicationListener介面

實現ApplicationListener介面,需要注意的是該介面接收的泛型是ContextRefreshedEvent類,然後重寫onApplicationEvent方法,也能從該方法中獲取到Spring容器物件。

@Service
public class PersonService3 implements ApplicationListener<ContextRefreshedEvent> {
    private ApplicationContext applicationContext;


    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        applicationContext = event.getApplicationContext();
    }

    public void add() {
        Person person = (Person) applicationContext.getBean("person");
    }

}

提一下Aware介面,它其實是一個空介面,裡面不包含任何方法。它表示已感知的意思,透過這類介面可以獲取指定物件,比如:

  • 透過BeanFactoryAware獲取BeanFactory
  • 透過ApplicationContextAware獲取ApplicationContext
  • 透過BeanNameAware獲取BeanName等

Aware介面是很常用的功能,目前包含如下功能:

a72eedc0fdd2b6fef9580f02a0394927

如何初始化bean

Spring中支援3種初始化bean的方法:

  • xml中指定init-method方法。此種方式很老了
  • 使用@PostConstruct註解
  • 實現InitializingBean介面

使用@PostConstruct註解

在需要初始化的方法上增加@PostConstruct註解,這樣就有初始化的能力。

@Service
public class AService {

    @PostConstruct
    public void init() {
        System.out.println("===初始化===");
    }
}

實現InitializingBean介面

實現InitializingBean介面,重寫afterPropertiesSet方法,該方法中可以完成初始化功能。

@Service
public class BService implements InitializingBean {

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("===初始化===");
    }
}

順便丟擲一個有趣的問題:init-methodPostConstructInitializingBean 的執行順序是什麼樣的?

決定他們呼叫順序的關鍵程式碼在AbstractAutowireCapableBeanFactory類的initializeBean方法中。

image-20240313141437902

這段程式碼中會先呼叫BeanPostProcessorpostProcessBeforeInitialization方法,而PostConstruct是透過InitDestroyAnnotationBeanPostProcessor實現的,它就是一個BeanPostProcessor,所以PostConstruct先執行。

invokeInitMethods方法中的程式碼:

image-20240313141607275

決定了先呼叫InitializingBean,再呼叫init-method

所以得出結論,他們的呼叫順序是:

圖片

自定義自己的Scope

我們都知道Spring預設支援的Scope只有兩種:

  • singleton 單例,每次從Spring容器中獲取到的bean都是同一個物件。
  • prototype 多例,每次從Spring容器中獲取到的bean都是不同的物件。

Spring web又對Scope進行了擴充套件,增加了:

  • RequestScope 同一次請求從Spring容器中獲取到的bean都是同一個物件。
  • SessionScope 同一個會話從Spring容器中獲取到的bean都是同一個物件。

即便如此,有些場景還是無法滿足我們的要求。

比如,我們想在同一個執行緒中從Spring容器獲取到的bean都是同一個物件,該怎麼辦?

這就需要自定義Scope了。

  1. 實現Scope介面
public class ThreadLocalScope implements Scope {

    private static final ThreadLocal THREAD_LOCAL_SCOPE = new ThreadLocal();

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Object value = THREAD_LOCAL_SCOPE.get();
        if (value != null) {
            return value;
        }

        Object object = objectFactory.getObject();
        THREAD_LOCAL_SCOPE.set(object);
        return object;
    }

    @Override
    public Object remove(String name) {
        THREAD_LOCAL_SCOPE.remove();
        return null;
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {

    }

    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    @Override
    public String getConversationId() {
        return null;
    }
}
  1. 將新定義的Scope注入到Spring容器中
@Component
public class ThreadLocalBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        beanFactory.registerScope("threadLocalScope", new ThreadLocalScope());
    }
}
  1. 使用新定義的Scope
@Scope("threadLocalScope")
@Service
public class CService {

    public void add() {
    }
}

FactoryBean

說起FactoryBean就不得不提BeanFactory,因為面試官老喜歡問它們的區別。

  • BeanFactory:Spring容器的頂級介面,管理bean的工廠。
  • FactoryBean:並非普通的工廠bean,它隱藏了例項化一些複雜Bean的細節,給上層應用帶來了便利。

Spring原始碼中有70多個地方在用FactoryBean介面。

image-20240313141721527

上面這張圖足以說明該介面的重要性

提一句:mybatisSqlSessionFactory物件就是透過SqlSessionFactoryBean類建立的。

定義自己的FactoryBean

@Component
public class MyFactoryBean implements FactoryBean {

    @Override
    public Object getObject() throws Exception {
        String data1 = buildData1();
        String data2 = buildData2();
        return buildData3(data1, data2);
    }

    private String buildData1() {
        return "data1";
    }

    private String buildData2() {
        return "data2";
    }

    private String buildData3(String data1, String data2) {
        return data1 + data2;
    }


    @Override
    public Class<?> getObjectType() {
        return null;
    }
}

獲取FactoryBean例項物件

@Service
public class MyFactoryBeanService implements BeanFactoryAware {
    private BeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    public void test() {
        Object myFactoryBean = beanFactory.getBean("myFactoryBean");
        System.out.println(myFactoryBean);
        Object myFactoryBean1 = beanFactory.getBean("&myFactoryBean");
        System.out.println(myFactoryBean1);
    }
}
  • getBean("myFactoryBean");獲取的是MyFactoryBeanService類中getObject方法返回的物件,
  • getBean("&myFactoryBean");獲取的才是MyFactoryBean物件。

自定義型別轉換

Spring目前支援3中型別轉換器:

  • Converter<S,T>:將 S 型別物件轉為 T 型別物件
  • ConverterFactory<S, R>:將 S 型別物件轉為 R 型別及子類物件
  • GenericConverter:它支援多個source和目標型別的轉化,同時還提供了source和目標型別的上下文,這個上下文能讓你實現基於屬性上的註解或資訊來進行型別轉換。

這3種型別轉換器使用的場景不一樣,我們以Converter<S,T>為例。假如:介面中接收引數的實體物件中,有個欄位的型別是Date,但是實際傳參的是字串型別:2021-01-03 10:20:15,要如何處理呢?

  1. 定義一個實體User
@Data
public class User {

    private Long id;
    private String name;
    private Date registerDate;
}
  1. 實現Converter介面
public class DateConverter implements Converter<String, Date> {

    private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Override
    public Date convert(String source) {
        if (source != null && !"".equals(source)) {
            try {
                simpleDateFormat.parse(source);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}
  1. 將新定義的型別轉換器注入到Spring容器中
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new DateConverter());
    }
}
  1. 呼叫介面
RequestMapping("/user")
@RestController
public class UserController {

    @RequestMapping("/save")
    public String save(@RequestBody User user) {
        return "success";
    }
}

請求介面時User物件中registerDate欄位會被自動轉換成Date型別。

Spring MVC攔截器

Spring MVC攔截器跟Spring攔截器相比,它裡面能夠獲取HttpServletRequestHttpServletResponse 等web物件例項。

Spring MVC攔截器的頂層介面是:HandlerInterceptor,包含三個方法:

  • preHandle 目標方法執行前執行
  • postHandle 目標方法執行後執行
  • afterCompletion 請求完成時執行

為了方便我們一般情況會用HandlerInterceptor介面的實現類HandlerInterceptorAdapter類。

假如有許可權認證、日誌、統計的場景,可以使用該攔截器。

  1. 繼承HandlerInterceptorAdapter類定義攔截器
public class AuthInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        String requestUrl = request.getRequestURI();
        if (checkAuth(requestUrl)) {
            return true;
        }

        return false;
    }

    private boolean checkAuth(String requestUrl) {
        System.out.println("===許可權校驗===");
        return true;
    }
}
  1. 將該攔截器註冊到Spring容器
@Configuration
public class WebAuthConfig extends WebMvcConfigurerAdapter {
 
    @Bean
    public AuthInterceptor getAuthInterceptor() {
        return new AuthInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor());
    }
}

在請求介面時Spring MVC透過該攔截器,能夠自動攔截該介面,並且校驗許可權。

可以在DispatcherServlet類的doDispatch方法中看到呼叫過程:

image-20240313142059807

RestTemplate攔截器

我們使用RestTemplate呼叫遠端介面時,有時需要在header中傳遞資訊,比如:traceId,source等,便於在查詢日誌時能夠串聯一次完整的請求鏈路,快速定位問題。

這種業務場景就能透過ClientHttpRequestInterceptor介面實現,具體做法如下:

  1. 實現ClientHttpRequestInterceptor介面
public class RestTemplateInterceptor implements ClientHttpRequestInterceptor {

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        request.getHeaders().set("traceId", MdcUtil.get());
        return execution.execute(request, body);
    }
}

MdcUtil其實是利用MDC工具在ThreadLocal中儲存和獲取traceId

public class MdcUtil {

    private static final String TRACE_ID = "TRACE_ID";

    public static String get() {
        return MDC.get(TRACE_ID);
    }

    public static void add(String value) {
        MDC.put(TRACE_ID, value);
    }
}
  1. 定義配置類
@Configuration
public class RestTemplateConfiguration {

    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setInterceptors(Collections.singletonList(restTemplateInterceptor()));
        return restTemplate;
    }

    @Bean
    public RestTemplateInterceptor restTemplateInterceptor() {
        return new RestTemplateInterceptor();
    }
}

這個例子中沒有演示MdcUtil類的add方法具體調的地方,我們可以在filter中執行介面方法之前,生成traceId,呼叫MdcUtil類的add方法新增到MDC中,然後在同一個請求的其他地方就能透過MdcUtil類的get方法獲取到該traceId。

統一異常處理

@RestControllerAdvice	// controller增強
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)	// 捕獲哪種異常會觸發本方法
    public String handleException(Exception e) {
        if (e instanceof ArithmeticException) {
            return "資料異常";
        }
        if (e instanceof Exception) {
            return "伺服器內部異常";
        }
        retur nnull;
    }
}

只需在handleException方法中處理異常情況,業務介面中可以放心使用,不再需要捕獲異常(有人統一處理了)。

非同步

以前我們在使用非同步功能時,通常情況下有三種方式:

  • 繼承Thread類
  • 實現Runable介面
  • 使用執行緒池

第一種:繼承Thread類

public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("===call MyThread===");
    }

    public static void main(String[] args) {
        new MyThread().start();
    }
}

第二種:實現Runable介面

public class MyWork implements Runnable {
    @Override
    public void run() {
        System.out.println("===call MyWork===");
    }

    public static void main(String[] args) {
        new Thread(new MyWork()).start();
    }
}

第三種:使用執行緒池

public class MyThreadPool {

    private static ExecutorService executorService = new ThreadPoolExecutor(1, 5, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(200));

    static class Work implements Runnable {

        @Override
        public void run() {
            System.out.println("===call work===");
        }
    }

    public static void main(String[] args) {
        try {
            executorService.submit(new MyThreadPool.Work());
        } finally {
            executorService.shutdown();
        }

    }
}

這三種實現非同步的方法不能說不好,但是Spring已經幫我們抽取了一些公共的地方,我們無需再繼承Thread類或實現Runable介面,它都搞定了。使用方式如下:

  1. Spring Boot專案啟動類上加@EnableAsync註解
@EnableAsync
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
    }
}
  1. 在需要使用非同步的方法上加上@Async註解
@Service
public class PersonService {

    @Async
    public String get() {
        System.out.println("===add==");
        return "data";
    }
}

然後在使用的地方呼叫一下:personService.get();就擁有了非同步功能。

預設情況下,Spring會為我們的非同步方法建立一個執行緒去執行,如果該方法被呼叫次數非常多的話,需要建立大量的執行緒,會導致資源浪費。

這時,我們可以定義一個執行緒池,非同步方法將會被自動提交到執行緒池中執行。

@Configuration
public class ThreadPoolConfig {

    @Value("${thread.pool.corePoolSize:5}")
    private int corePoolSize;

    @Value("${thread.pool.maxPoolSize:10}")
    private int maxPoolSize;

    @Value("${thread.pool.queueCapacity:200}")
    private int queueCapacity;

    @Value("${thread.pool.keepAliveSeconds:30}")
    private int keepAliveSeconds;

    @Value("${thread.pool.threadNamePrefix:ASYNC_}")
    private String threadNamePrefix;

    @Bean
    public Executor MessageExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveSeconds);
        executor.setThreadNamePrefix(threadNamePrefix);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

Spring非同步的核心方法:

image-20240313142424948

根據返回值不同,處理情況也不太一樣,具體分為如下情況:

圖片

Spring cache

Spring cache架構圖:

圖片

它目前支援多種快取:

圖片

這裡以caffeine為例,它是Spring官方推薦的。

  1. 引入caffeine的相關jar包
<dependency>
    <groupId>org.Springframework.boot</groupId>
    <artifactId>Spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.6.0</version>
</dependency>
  1. 配置CacheManager,開啟EnableCaching
@Configuration
@EnableCaching	// 此註解根據情況也可以放到啟動類上
public class CacheConfig {
    @Bean
    public CacheManager cacheManager(){
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        // Caffeine配置
        Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
                // 最後一次寫入後經過固定時間過期
                .expireAfterWrite(10, TimeUnit.SECONDS)
                // 快取的最大條數
                .maximumSize(1000);
        cacheManager.setCaffeine(caffeine);
        return cacheManager;
    }
}
  1. 使用Cacheable註解獲取資料
@Service
public class CategoryService {
   
   // category是快取名稱,#type是具體的key,可支援el表示式
   @Cacheable(value = "category", key = "#type")
   public CategoryModel getCategory(Integer type) {
       return getCategoryByType(type);
   }

   private CategoryModel getCategoryByType(Integer type) {
       System.out.println("根據不同的type:" + type + "獲取不同的分類資料");
       CategoryModel categoryModel = new CategoryModel();
       categoryModel.setId(1L);
       categoryModel.setParentId(0L);
       categoryModel.setName("電器");
       categoryModel.setLevel(3);
       return categoryModel;
   }
}

呼叫categoryService.getCategory()方法時,先從caffine快取中獲取資料,如果能夠獲取到資料則直接返回該資料,不會進入方法體。如果不能獲取到資料,則直接方法體中的程式碼獲取到資料,然後放到caffine快取中。

@CacheConfig註解

用於標註在類上,可以存放該類中所有快取的公有屬性(如:設定快取名字)。

@CacheConfig(cacheNames = "users")
public class UserService{

}

當然:這個註解其實可以使用@Cacheable來代替。

@Cacheable註解(讀資料時):用得最多

應用到讀取資料的方法上,如:查詢資料的方法,使用了之後可以做到先從本地快取中讀取資料,若是沒有,則再呼叫此註解下的方法去資料庫中讀取資料,當然:還可以將資料庫中讀取的資料放到用此註解配置的指定快取中。

@Cacheable(value = "user", key = "#userId")
User selectUserById( Integer userId );

@Cacheable 註解的屬性:

  • valuecacheNames
    • 這兩個引數其實是等同的( acheNames為Spring 4新增的,作為value的別名)。
    • 這兩個屬性的作用:用於指定快取儲存的集合名
  • key 作用:快取物件儲存在Map集合中的key值
  • condition 作用:快取物件的條件。 即:只有滿足這裡面配置的表示式條件的內容才會被快取,如:@Cache( key = "#userId",condition="#userId.length() < 3" 這個表示式表示只有當userId長度小於3的時候才會被快取。
  • unless 作用:另外一個快取條件。 它不同於condition引數的地方在於此屬性的判斷時機(此註解中編寫的條件是在函式被呼叫之後才做判斷,所以:這個屬性可以透過封裝的result進行判斷)。
  • keyGenerator
    • 作用:用於指定key生成器。 若需要繫結一個自定義的key生成器,我們需要去實現org.Springframewart.cahce.intercceptor.KeyGenerator介面,並使用該引數來繫結。
    • 注意點:該引數與上面的key屬性是互斥的
  • cacheManager 作用:指定使用哪個快取管理器。 也就是當有多個快取器時才需要使用。
  • cacheResolver
    • 作用:指定使用哪個快取解析器
    • 需要透過org.Springframewaork.cache.interceptor.CacheResolver介面來實現自己的快取解析器

@CachePut註解 (寫資料時)

用在寫資料的方法上,如:新增 / 修改方法,呼叫方法時會自動把對應的資料放入快取,@CachePut 的引數和 @Cacheable 差不多。

@CachePut(value="user", key = "#userId")
public User save(User user) {
	users.add(user);
	return user;
}

@CacheEvict註解 (刪除資料時)

用在刪除資料的方法上,呼叫方法時會從快取中移除相應的資料。

@CacheEvict(value = "user", key = "#userId")
void delete( Integer userId);

這個註解除了和 @Cacheable 一樣的引數之外,還有另外兩個引數:

  • allEntries: 預設為false,當為true時,會移除快取中該註解該屬性所在的方法的所有資料。
  • beforeInvocation:預設為false,在呼叫方法之後移除資料,當為true時,會在呼叫方法之前移除資料。

@Cacheing組合註解:推薦

// 將userId、username、userAge放到名為user的快取中存起來
@Caching(
	put = {
		@CachePut(value = "user", key = "#userId"),
		@CachePut(value = "user", key = "#username"),
		@CachePut(value = "user", key = "#userAge"),
	}
)

@Conditional

有沒有遇到過這些問題:

  1. 某個功能需要根據專案中有沒有某個jar判斷是否開啟該功能。
  2. 某個bean的例項化需要先判斷另一個bean有沒有例項化,再判斷是否例項化自己。
  3. 某個功能是否開啟,在配置檔案中有個引數可以對它進行控制。

@ConditionalOnClass

某個功能需要根據專案中有沒有某個jar判斷是否開啟該功能,可以用@ConditionalOnClass註解解決。

public class A {
}

public class B {
}



@ConditionalOnClass(B.class)
@Configuration
public class TestConfiguration {

    @Bean
    public A a() {
      return new A();
    }
}

如果專案中存在B類,則會例項化A類。如果不存在B類,則不會例項化A類。

可能會問:不是判斷有沒有某個jar嗎?怎麼現在判斷某個類了?

直接判斷有沒有該jar下的某個關鍵類更簡單。

這個註解有個升級版的應用場景:比如common工程中寫了一個發訊息的工具類mqTemplate,業務工程引用了common工程,只需再引入訊息中介軟體,比如rocketmq的jar包,就能開啟mqTemplate的功能。而如果有另一個業務工程,通用引用了common工程,如果不需要發訊息的功能,不引入rocketmq的jar包即可。

@ConditionalOnBean

某個bean的例項化需要先判斷另一個bean有沒有例項化,再判斷是否例項化自己。可以透過@ConditionalOnBean註解解決。

@Configuration
public class TestConfiguration {

    @Bean
    public B b() {
        return new B();
    }

    @ConditionalOnBean(name="b")
    @Bean
    public A a() {
      return new A();
    }
}

例項A只有在例項B存在時,才能例項化。

@ConditionalOnProperty

某個功能是否開啟,在配置檔案中有個引數可以對它進行控制。可以透過@ConditionalOnProperty註解解決

applicationContext.properties檔案中配置引數:

demo.enable=false
@ConditionalOnProperty(
    prefix = "demo",	// 表示引數名的字首
    name = "enable", 	// 表示引數名
    havingValue = "true",	// 表示指定的值,引數中配置的值需要跟指定的值比較是否相等,相等才滿足條件
    matchIfMissing = true	// 表示是否允許預設配置
)

@Configuration
public class TestConfiguration {

    @Bean
    public A a() {
      return new A();
    }
}

這個功能可以作為開關,相比EnableXXX註解的開關更優雅,因為它可以透過引數配置是否開啟,而EnableXXX註解的開關需要在程式碼中硬編碼開啟或關閉。

其他的Conditional註解

Spring用得比較多的Conditional註解還有:ConditionalOnMissingClassConditionalOnMissingBeanConditionalOnWebApplication等。

整體認識一下@Conditional家族:

圖片

自定義Conditional

Spring Boot自帶的Conditional系列已經可以滿足我們絕大多數的需求了。但如果你有比較特殊的場景,也可以自定義自定義Conditional。

  1. 自定義註解
@Conditional(MyCondition.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface MyConditionOnProperty {
    String name() default "";

    String havingValue() default "";
}
  1. 實現Condition介面
public class MyCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        System.out.println("實現自定義邏輯");
        return false;
    }
}
  1. 使用@MyConditionOnProperty註解

Conditional的奧秘就藏在ConfigurationClassParser類的processConfigurationClass方法中:

image-20240313142547961

image-20240313142651897

  1. 先判斷有沒有使用Conditional註解,如果沒有直接返回false
  2. 收集condition到集合中
  3. order排序該集合
  4. 遍歷該集合,迴圈呼叫conditionmatchs方法。

@Import

有時我們需要在某個配置類中引入另外一些類,被引入的類也加到Spring容器中。這時可以使用@Import註解完成這個功能。

引入的類支援三種不同型別:最好將普通類和@Configuration註解的配置類分開講解,所以列了四種不同型別

圖片

這四種引入類的方式各有千秋,總結如下:

  1. 普通類,用於建立沒有特殊要求的bean例項。
  2. @Configuration註解的配置類,用於層層巢狀引入的場景。
  3. 實現ImportSelector介面的類,用於一次性引入多個類的場景,或者可以根據不同的配置決定引入不同類的場景。
  4. 實現ImportBeanDefinitionRegistrar介面的類,主要用於可以手動控制BeanDefinition的建立和註冊的場景,它的方法中可以獲取BeanDefinitionRegistry註冊容器物件。

ConfigurationClassParser類的processImports方法中可以看到這三種方式的處理邏輯:

image-20240313142955119

最後的else方法其實包含了:普通類和@Configuration註解的配置類兩種不同的處理邏輯。

普通類

Spring4.2之後@Import註解可以例項化普通類的bean例項,即被引入的類會被例項化bean物件

public class A {
}



@Import(A.class)
@Configuration
public class TestConfiguration {
}

透過@Import註解引入A類,Spring就能自動例項化A物件,然後在需要使用的地方透過@Autowired註解注入即可:

@Autowired
private A a;

問題:@Import註解能定義單個類的bean,但如果有多個類需要定義bean該怎麼辦?

其實@Import註解也支援:

@Import({Role.class, User.class})
@Configuration
public class MyConfig {
}

甚至,如果想偷懶,不想寫這種MyConfig類,Spring Boot也歡迎:

@Import({Role.class, User.class})
@SpringBootApplication(
    exclude = {
        DataSourceAutoConfiguration.class,
        DataSourceTransactionManagerAutoConfiguration.class
    }
)
public class Application {

    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
    }
}

這樣也能生效?

Spring Boot的啟動類一般都會加@SpringBootApplication註解,該註解上加了@SpringBootConfiguration註解。

image-20240313143158949

@SpringBootConfiguration註解,上面又加了@Configuration註解,所以,Spring Boot啟動類本身帶有@Configuration註解的功能。

image-20240313143224210

@Configuration 註解的配置類

缺點:不太適合加複雜的判斷條件,根據某些條件定義這些bean,根據另外的條件定義那些bean

這種引入方式是最複雜的,因為@Configuration註解還支援多種組合註解,比如:

  • @Import
  • @ImportResource
  • @PropertySource
public class A {
}

public class B {
}



@Import(B.class)
@Configuration
public class AConfiguration {

    @Bean
    public A a() {
        return new A();
    }
}



@Import(AConfiguration.class)
@Configuration
public class TestConfiguration {
}

透過@Import註解引入@Configuration註解的配置類,會把該配置類相關@Import@ImportResource@PropertySource等註解引入的類進行遞迴,一次性全部引入。

這種方式,如果AConfiguration類已經在Spring指定的掃描目錄或者子目錄下,則AConfiguration類會顯得有點多餘。因為AConfiguration類本身就是一個配置類,它裡面就能定義bean。

但如果AConfiguration類不在指定的Spring掃描目錄或者子目錄下,則透過AConfiguration類的匯入功能,也能把AConfiguration類識別成配置類。

擴充:swagger2是如何匯入相關類的?

眾所周知,我們引入swagger相關jar包之後,只需要在Spring Boot的啟動類上加上@EnableSwagger2註解,就能開啟swagger的功能。

其中@EnableSwagger2註解中匯入了Swagger2DocumentationConfiguration類。

圖片

該類是一個Configuration類,它又匯入了另外兩個類:

  • SpringfoxWebMvcConfiguration
  • SwaggerCommonConfiguration

圖片

SpringfoxWebMvcConfiguration類又會匯入新的Configuration類,並且透過@ComponentScan註解掃描了一些其他的路徑。

圖片

SwaggerCommonConfiguration同樣也透過@ComponentScan註解掃描了一些額外的路徑。

圖片

如此一來,我們透過一個簡單的@EnableSwagger2註解,就能輕鬆的匯入swagger所需的一系列bean,並且擁有swagger的功能。

實現ImportSelector介面的類

上一節知道:@Configuration 註解配置的類不太適合加複雜的判斷條件,根據某些條件定義這些bean,根據另外的條件定義那些bean。

而本節的實現ImportSelector介面的類就可以做到了。

這種引入方式需要實現ImportSelector介面

這種方式的好處是selectImports方法返回的是陣列,意味著可以同時引入多個類

缺點:沒法自定義bean的名稱和作用域等屬性

實現ImportSelector介面的好處主要有以下兩點:

  1. 把某個功能的相關類,可以放到一起,方面管理和維護。
  2. 重寫selectImports方法時,能夠根據條件判斷某些類是否需要被例項化,或者某個條件例項化這些bean,其他的條件例項化那些bean等。我們能夠非常靈活的定製化bean的例項化。
public class AImportSelector implements ImportSelector {

    private static final String CLASS_NAME = "com.zixq.cache.service.test13.A";

    /**
     * 指定需要定義bean的類名,注意要包含完整路徑,而非相對路徑
     */
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
            return new String[]{CLASS_NAME};
	}
}



@Import(AImportSelector.class)
@Configuration
public class TestConfiguration {
}

ImportSelector介面相關:@EnableAutoConfiguration註解

@EnableAutoConfiguration註解中匯入了AutoConfigurationImportSelector類,並且裡面包含系統引數名稱:Spring.boot.enableautoconfiguration

image-20240313143327574

AutoConfigurationImportSelector類實現了ImportSelector介面。

圖片

並且重寫了selectImports(AnnotationMetadata importingClassMetadata)方法,該方法會根據某些註解去找所有需要建立bean的類名,然後返回這些類名。其中在查詢這些類名之前,先呼叫isEnabled方法,判斷是否需要繼續查詢。

image-20240313143516293

該方法會根據ENABLED_OVERRIDE_PROPERTY的值來作為判斷條件。

image-20240313143604632

而這個值就是Spring.boot.enableautoconfiguration

換句話說,這裡能根據系統引數控制bean是否需要被例項化

實現ImportBeanDefinitionRegistrar介面的類

由上一節知道:實現ImportSelector介面的方式沒法自定義bean的名稱和作用域等屬性。

有需求,就有解決方案,透過本節的內容即可解決

這種引入方式需要實現ImportBeanDefinitionRegistrar介面

這種方式是最靈活的,能在registerBeanDefinitions方法中獲取到BeanDefinitionRegistry容器註冊物件,可以手動控制BeanDefinition的建立和註冊

public class AImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, 
                                        BeanDefinitionRegistry registry) {
        
        RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(A.class);
        registry.registerBeanDefinition("a", rootBeanDefinition);
    }
}



@Import(AImportBeanDefinitionRegistrar.class)
@Configuration
public class TestConfiguration {
}

當然@import註解非常人性化,還支援同時引入多種不同型別的類。

@Import({B.class, AImportBeanDefinitionRegistrar.class})
@Configuration
public class TestConfiguration {
}

我們所熟悉的fegin功能,就是使用ImportBeanDefinitionRegistrar介面實現的:

image-20240313144659770

@ConfigurationProperties賦值

@ConfigurationProperties是Spring Boot中新加的註解

在專案中使用配置引數是非常常見的場景,比如,我們在配置執行緒池的時候,需要在applicationContext.propeties檔案中定義如下配置:

thread.pool.corePoolSize=5
thread.pool.maxPoolSize=10
thread.pool.queueCapacity=200
thread.pool.keepAliveSeconds=30

第一種方式:透過@Value註解讀取這些配置。適合引數少的情況

缺點:@Value註解定義的引數看起來有點分散,不容易辨別哪些引數是一組的

建議在使用時都加上:,因為:後面跟的是預設值,比如:@Value("${thread.pool.corePoolSize:5}"),定義的預設核心執行緒數是5

假如有這樣的場景:business工程下定義了這個ThreadPoolConfig類,api工程引用了business工程,同時job工程也引用了business工程,而ThreadPoolConfig類只想在api工程中使用。這時,如果不配置預設值,job工程啟動的時候可能會報錯

public class ThreadPoolConfig {

    @Value("${thread.pool.corePoolSize:5}")
    private int corePoolSize;

    @Value("${thread.pool.maxPoolSize:10}")
    private int maxPoolSize;

    @Value("${thread.pool.queueCapacity:200}")
    private int queueCapacity;

    @Value("${thread.pool.keepAliveSeconds:30}")
    private int keepAliveSeconds;

    @Value("${thread.pool.threadNamePrefix:ASYNC_}")
    private String threadNamePrefix;

    @Bean
    public Executor threadPoolExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveSeconds);
        executor.setThreadNamePrefix(threadNamePrefix);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

第二種方式@ConfigurationProperties註解

  1. 定義ThreadPoolProperties類
@Data
@Component
@ConfigurationProperties("thread.pool")
public class ThreadPoolProperties {

    private int corePoolSize;
    private int maxPoolSize;
    private int queueCapacity;
    private int keepAliveSeconds;
    private String threadNamePrefix;
}
  1. 使用ThreadPoolProperties類
@Configuration
public class ThreadPoolConfig {

    @Autowired
    private ThreadPoolProperties threadPoolProperties;

    @Bean
    public Executor threadPoolExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(threadPoolProperties.getCorePoolSize());
        executor.setMaxPoolSize(threadPoolProperties.getMaxPoolSize());
        executor.setQueueCapacity(threadPoolProperties.getQueueCapacity());
        executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
        executor.setThreadNamePrefix(threadPoolProperties.getThreadNamePrefix());
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

這種方式要方便很多,我們只需編寫xxxProperties類,Spring會自動裝配引數。此外,不同系列的引數可以定義不同的xxxProperties類,也便於管理,推薦優先使用這種方式。

底層是透過:ConfigurationPropertiesBindingPostProcessor類實現的,該類實現了BeanPostProcessor介面,在postProcessBeforeInitialization方法中解析@ConfigurationProperties註解,並且繫結資料到相應的物件上。

繫結是透過Binder類的bindObject方法完成的:

image-20240313144929932

以上這段程式碼會遞迴繫結資料,主要考慮了三種情況:

  • bindAggregate 繫結集合類
  • bindBean 繫結物件
  • bindProperty 繫結引數 前面兩種情況最終也會呼叫到bindProperty方法。

@ConfigurationProperties對應引數動態更新問題

使用@ConfigurationProperties註解有些場景有問題,比如:在apollo中修改了某個引數,正常情況可以動態更新到@ConfigurationProperties註解定義的xxxProperties類的物件中,但是如果出現比較複雜的物件,比如:

private Map<String, Map<String,String>>  urls;

可能動態更新不了。這時候該怎麼辦呢?

答案是使用ApolloConfigChangeListener監聽器自己處理:

@ConditionalOnClass(com.ctrip.framework.apollo.Spring.annotation.EnableApolloConfig.class)
public class ApolloConfigurationAutoRefresh implements ApplicationContextAware {
    
    private ApplicationContext applicationContext;
   
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
   
    @ApolloConfigChangeListener
    private void onChange(ConfigChangeEvent changeEvent) {
        refreshConfig(changeEvent.changedKeys());
    }
    
    private void refreshConfig(Set<String> changedKeys){
       System.out.println("將變更的引數更新到相應的物件中");
    }
}

Spring事務

需要同時寫入多張表的資料。為了保證操作的原子性(要麼同時成功,要麼同時失敗),避免資料不一致的情況,我們一般都會用到Spring事務(也會選擇其他事務框架)。

Spring事務用起來賊爽,就用一個簡單的註解:@Transactional,就能輕鬆搞定事務。而且一直用一直爽。

但如果使用不當,它也會坑人於無形。

img

事務不生效

訪問許可權問題

Java的訪問許可權主要有四種:private、default、protected、public,它們的許可權從左到右,依次變大。

在開發過程中,把某些事務方法,定義了錯誤的訪問許可權,就會導致事務功能出問題。

@Service
public class UserService {

    @Transactional
    private void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }
}

上述程式碼就會導致事務失效,因為Spring要求被代理方法必須是public

AbstractFallbackTransactionAttributeSource 類的 computeTransactionAttribute 方法中有個判斷,如果目標方法不是public,則TransactionAttribute返回null,即不支援事務。

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

    // The method may be on an interface, but we need attributes from the target class.
    // If the target class is null, the method will be unchanged.
    Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

    // First try is the method in the target class.
    TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
    if (txAttr != null) {
      return txAttr;
    }

    // Second try is the transaction attribute on the target class.
    txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
    if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
      return txAttr;
    }

    if (specificMethod != method) {
      // Fallback is to look at the original method.
      txAttr = findTransactionAttribute(method);
      if (txAttr != null) {
        return txAttr;
      }
      // Last fallback is the class of the original method.
      txAttr = findTransactionAttribute(method.getDeclaringClass());
      if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
        return txAttr;
      }
    }
    return null;
  }

方法用final修飾

有時候,某個方法不想被子類重新,這時可以將該方法定義成final的。普通方法這樣定義是沒問題的,但如果將事務方法定義成final就會導致事務失效。

@Service
public class UserService {

    @Transactional
    public final void add(UserModel userModel){
        saveData(userModel);
        updateData(userModel);
    }
}

因為Spring事務底層使用了AOP幫我們生成代理類,在代理類中實現的事務功能。如果某個方法用final修飾了,那麼在它的代理類中,就無法重寫該方法,而新增事務功能

重要提示

如果某個方法是static的,同樣無法透過動態代理,變成事務方法。

方法內部呼叫

有時需要在某個Service類的某個事務方法中呼叫另外一個事務方法。

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Transactional
    public void add(UserModel userModel) {
        userMapper.insertUser(userModel);
        updateStatus(userModel);
    }

    @Transactional
    public void updateStatus(UserModel userModel) {
        doSameThing();
    }
}

上述程式碼就會導致事務失效,因為updateStatus方法擁有事務的能力是Spring AOP生成代理物件,但是updateStatus這種方法直接呼叫了this物件的方法,所以updateStatus方法不會生成事務。

如果有些場景,確實想在同一個類的某個方法中,呼叫它自己的另外一個方法,該怎麼辦?

  1. 第一種方式:新加一個Service方法。把@Transactional註解加到新Service方法上,把需要事務執行的程式碼移到新方法中。
@Servcie
public class ServiceA {
   @Autowired
   prvate ServiceB serviceB;

   public void save(User user) {
         queryData1();
         queryData2();
         serviceB.doSave(user);
   }
 }




 @Servcie
 public class ServiceB {

    @Transactional(rollbackFor=Exception.class)
    public void doSave(User user) {
       addData1();
       updateData2();
    }
 }
  1. 第二種方式:在該Service類中注入自己。如果不想再新加一個Service類,在該Service類中注入自己也是一種選擇。
@Servcie
public class ServiceA {
   @Autowired
   prvate ServiceA serviceA;

   public void save(User user) {
         queryData1();
         queryData2();
         serviceA.doSave(user);
   }

   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
       addData1();
       updateData2();
    }
 }

第二種做法會不會出現迴圈依賴問題?

不會。Spring IOC內部的三級快取保證了它,不會出現迴圈依賴問題。但有些坑,解放方式去參考:Spring:如何解決迴圈依賴

迴圈依賴:就是一個或多個物件例項之間存在直接或間接的依賴關係,這種依賴關係構成了構成一個環形呼叫。

第一種情況:自己依賴自己的直接依賴。

圖片

第二種情況:兩個物件之間的直接依賴。

圖片

第三種情況:多個物件之間的間接依賴。

圖片

前面兩種情況的直接迴圈依賴比較直觀,非常好識別,但是第三種間接迴圈依賴的情況有時候因為業務程式碼呼叫層級很深,不容易識別出來。

迴圈依賴的N種場景

圖片

  1. 第三種方式:透過AopContent類。在該Service類中使用AopContext.currentProxy()獲取代理物件。

上面第二種方式確實可以解決問題,但是程式碼看起來並不直觀,還可以透過在該Service類中使用AOPProxy獲取代理物件,實現相同的功能。

@Servcie
public class ServiceA {

   public void save(User user) {
         queryData1();
         queryData2();
         ((ServiceA)AopContext.currentProxy()).doSave(user);
   }

   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
       addData1();
       updateData2();
    }
 }

未被Spring託管

使用Spring事務的前提是:物件要被Spring管理,需要建立bean例項

通常情況下,我們透過@Controller@Service@Component@Repository等註解,可以自動實現bean例項化和依賴注入的功能。

但要是噼裡啪啦敲完Service類,忘了加 @Service 註解呢?

那麼該類不會交給Spring管理,它的方法也不會生成事務。

多執行緒呼叫

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {

        userMapper.insertUser(userModel);

        new Thread(() -> {
            roleService.doOtherThing();
        }).start();
    }
}



@Service
public class RoleService {

    @Transactional
    public void doOtherThing() {
        System.out.println("儲存role表資料");
    }
}

上述程式碼事務方法add中是另外一個執行緒呼叫的事務方法doOtherThing。

這樣會導致兩個方法不在同一個執行緒中,獲取到的資料庫連線不一樣,從而是兩個不同的事務。如果想doOtherThing方法中拋了異常,add方法也回滾是不可能的。

Spring事務其實是透過資料庫連線來實現的。當前執行緒中儲存了一個map,key是資料來源,value是資料庫連線

private static final ThreadLocal<Map<Object, Object>> resources = 
    new NamedThreadLocal<>("Transactional resources");

我們說的同一個事務,其實是指同一個資料庫連線,只有擁有同一個資料庫連線才能同時提交和回滾。如果在不同的執行緒,拿到的資料庫連線肯定是不一樣的,所以是不同的事務。

表不支援事務

MySQL 5之前,預設的資料庫引擎是myisam。好處是:索引檔案和資料檔案是分開儲存的,對於查多寫少的單表操作,效能比innodb更好。

但有個很致命的問題是:不支援事務。如果需要跨多張表操作,由於其不支援事務,資料極有可能會出現不完整的情況。

提示

有時候我們在開發的過程中,發現某張表的事務一直都沒有生效,那不一定是Spring事務的鍋,最好確認一下你使用的那張表,是否支援事務。

未開啟事務

有時候,事務沒有生效的根本原因是沒有開啟事務。

看到這句話可能會覺得好笑。因為開啟事務不是一個專案中,最最最基本的功能嗎?為什麼還會沒有開啟事務?

如果使用的是Spring Boot專案,那很幸運。因為Spring Boot透過 DataSourceTransactionManagerAutoConfiguration 類,已經默默的幫忙開啟了事務。自己所要做的事情很簡單,只需要配置Spring.datasource相關引數即可。

但如果使用的還是傳統的Spring專案,則需要在applicationContext.xml檔案中,手動配置事務相關引數。如果忘了配置,事務肯定是不會生效的。

<!-- 配置事務管理器 --> 
<bean class="org.Springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager"> 
    <property name="dataSource" ref="dataSource"></property> 
</bean> 

<tx:advice id="advice" transaction-manager="transactionManager"> 
    <tx:attributes> 
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes> 
</tx:advice> 

<!-- 用切點把事務切進去 --> 
<aop:config> 
    <aop:pointcut expression="execution(* com.zixieqing.*.*(..))" id="pointcut"/> 
    <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/> 
</aop:config> 

注意

如果在pointcut標籤中的切入點匹配規則配錯了的話,有些類的事務也不會生效。

事務不回滾

錯誤的傳播特性

在使用@Transactional註解時,是可以指定propagation引數的。

該引數的作用是指定事務的傳播特性,Spring目前支援7種傳播特性:

  • REQUIRED 如果當前上下文中存在事務,那麼加入該事務,如果不存在事務,建立一個事務,這是預設的傳播屬性值。
  • REQUIRES_NEW 每次都會新建一個事務,並且同時將上下文中的事務掛起,執行當前新建事務完成以後,上下文事務恢復再執行。
  • NESTED 如果當前上下文中存在事務,則巢狀事務執行,如果不存在事務,則新建事務。
  • SUPPORTS 如果當前上下文存在事務,則支援事務加入事務,如果不存在事務,則使用非事務的方式執行。
  • MANDATORY 如果當前上下文中存在事務,否則丟擲異常。
  • NOT_SUPPORTED 如果當前上下文中存在事務,則掛起當前事務,然後新的方法在沒有事務的環境中執行。
  • NEVER 如果當前上下文中存在事務,則丟擲異常,否則在無事務環境上執行程式碼。

如果我們在手動設定propagation引數的時候,把傳播特性設定錯了就會出問題。

@Service
public class UserService {

    // Propagation.NEVER	這種型別的傳播特性不支援事務,如果有事務則會拋異常
    @Transactional(propagation = Propagation.NEVER)
    public void add(UserModel userModel) {
        saveData(userModel);
        updateData(userModel);
    }
}

目前只有這三種傳播特性才會建立新事務:REQUIRED,REQUIRES_NEW,NESTED。

自己吞了異常

事務不會回滾,最常見的問題是:開發者在程式碼中手動try...catch了異常。

@Slf4j
@Service
public class UserService {

    @Transactional
    public void add(UserModel userModel) {
        try {
            saveData(userModel);
            updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}

這種情況下Spring事務當然不會回滾,因為開發者自己捕獲了異常,又沒有手動丟擲,換句話說就是把異常吞掉了。

如果想要Spring事務能夠正常回滾,必須丟擲它能夠處理的異常。如果沒有拋異常,則Spring認為程式是正常的

手動拋了別的異常

即使開發者沒有手動捕獲異常,但如果拋的異常不正確,Spring事務也不會回滾。

@Slf4j
@Service
public class UserService {

    @Transactional
    public void add(UserModel userModel) throws Exception {
        try {
             saveData(userModel);
             updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new Exception(e);
        }
    }
}

手動丟擲了異常:Exception,事務同樣不會回滾。

因為Spring事務,預設情況下只會回滾RuntimeException(執行時異常)和Error(錯誤),對於普通的Exception(非執行時異常),它不會回滾

自定義了回滾異常

在使用@Transactional註解宣告事務時,有時我們想自定義回滾的異常,Spring也是支援的。可以透過設定rollbackFor引數,來完成這個功能。

但如果這個引數的值設定錯了,就會引出一些莫名其妙的問題,

@Service
public class UserService {

    @Transactional(rollbackFor = BusinessException.class)
    public void add(UserModel userModel) throws Exception {
       saveData(userModel);
       updateData(userModel);
    }
}

如果在執行上面這段程式碼,儲存和更新資料時,程式報錯了,拋了SqlException、DuplicateKeyException等異常。而BusinessException是我們自定義的異常,報錯的異常不屬於BusinessException,所以事務也不會回滾。

即使rollbackFor有預設值,但阿里巴巴開發者規範中,還是要求開發者重新指定該引數。why?

因為如果使用預設值,一旦程式丟擲了Exception,事務不會回滾,這會出現很大的bug。所以,建議一般情況下,將該引數設定成:Exception或Throwable

巢狀事務回滾多了

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        roleService.doOtherThing();
    }
}



@Service
public class RoleService {

    @Transactional(propagation = Propagation.NESTED)
    public void doOtherThing() {
        System.out.println("儲存role表資料");
    }
}

這種情況使用了巢狀的內部事務,原本是希望呼叫roleService.doOtherThing()方法時,如果出現了異常,只回滾doOtherThing方法裡的內容,不回滾 userMapper.insertUser裡的內容,即回滾儲存點。。但事實是,insertUser也回滾了。why?

因為doOtherThing方法出現了異常,沒有手動捕獲,會繼續往上拋,到外層add方法的代理方法中捕獲了異常。所以,這種情況是直接回滾了整個事務,不只回滾單個儲存點。

怎麼樣才能只回滾儲存點?

將內部巢狀事務放在try/catch中,並且不繼續往上拋異常。這樣就能保證,如果內部巢狀事務中出現異常,只回滾內部事務,而不影響外部事務。

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {

        userMapper.insertUser(userModel);
        try {
            roleService.doOtherThing();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}

大事務問題

在使用Spring事務時,有個讓人非常頭疼的問題,就是大事務問題。

通常情況下,我們會在方法上@Transactional註解,填加事務功能,

@Transactional註解,如果被加到方法上,有個缺點就是整個方法都包含在事務當中了。

@Service
public class UserService {

    @Autowired 
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
       query1();
       query2();
       query3();
       roleService.save(userModel);
       update(userModel);
    }
}


@Service
public class RoleService {

    @Autowired 
    private RoleService roleService;

    @Transactional
    public void save(UserModel userModel) throws Exception {
       query4();
       query5();
       query6();
       saveData(userModel);
    }
}

上述程式碼,在UserService類中,其實只有這兩行才需要事務:

roleService.save(userModel);
update(userModel);

在RoleService類中,只有這一行需要事務:

saveData(userModel);

而上面的寫法會導致所有的query方法也被包含在同一個事務當中。

如果query方法非常多,呼叫層級很深,而且有部分查詢方法比較耗時的話,會造成整個事務非常耗時,從而造成大事務問題。

img

程式設計式事務

上面這些內容都是基於@Transactional註解的,主要說的是它的事務問題,我們把這種事務叫做:宣告式事務

其實,Spring還提供了另外一種建立事務的方式,即透過手動編寫程式碼實現的事務,我們把這種事務叫做:程式設計式事務

在Spring中為了支援程式設計式事務,專門提供了一個類:TransactionTemplate,在它的execute()方法中,就實現了事務的功能。

   @Autowired
   private TransactionTemplate transactionTemplate;

   ...

   public void save(final User user) {

         queryData1();
         queryData2();

         transactionTemplate.execute((status) => {
            addData1();
            updateData2();
            return Boolean.TRUE;
         })
   }

相較於@Transactional註解宣告式事務,我更建議大家使用,基於TransactionTemplate的程式設計式事務。主要原因如下:

  1. 避免由於Spring AOP問題,導致事務失效的問題。
  2. 能夠更小粒度的控制事務的範圍,更直觀。

提示

建議在專案中少使用@Transactional註解開啟事務。但並不是說一定不能用它,如果專案中有些業務邏輯比較簡單,而且不經常變動,使用@Transactional註解開啟事務開啟事務也無妨,因為它更簡單,開發效率更高,但是千萬要小心事務失效的問題。

跨域問題

關於跨域問題,前後端的解決方案還是挺多的,這裡我重點說說Spring的解決方案,目前有三種:

圖片

使用@CrossOrigin註解 和 實現WebMvcConfigurer介面的方案,Spring在底層最終都會呼叫到DefaultCorsProcessor類的handleInternal方法

image-20240313145127887

最終三種方案殊途同歸,都會往header中新增跨域需要引數,只是實現形式不一樣而已。

使用@CrossOrigin註解

該方案需要在跨域訪問的介面上加@CrossOrigin註解,訪問規則可以透過註解中的引數控制,控制粒度更細。如果需要跨域訪問的介面數量較少,可以使用該方案。

@RequestMapping("/user")
@RestController
public class UserController {

    @CrossOrigin(origins = "http://localhost:8016")
    @RequestMapping("/getUser")
    public String getUser(@RequestParam("name") String name) {
        System.out.println("name:" + name);
        return "success";
    }
}

全域性配置

實現WebMvcConfigurer介面,重寫addCorsMappings方法,在該方法中定義跨域訪問的規則。這是一個全域性的配置,可以應用於所有介面。

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "POST")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");

    }
}

自定義過濾器

透過在請求的header中增加Access-Control-Allow-Origin等引數解決跨域問題。

@WebFilter("corsFilter")
@Configuration
public class CorsFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET");
        httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", "x-requested-with");
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {

    }
}

Spring中定義bean的方法

Spring是建立和管理bean的工廠,它提供了多種定義bean的方式,能夠滿足我們日常工作中的多種業務場景。

一般常見的是下圖三種:

圖片

xml檔案配置bean

這是Spring最早支援的方式。後來,隨著Spring Boot越來越受歡迎,該方法目前已經用得很少了,

構造器

如果之前有在bean.xml檔案中配置過bean的經歷,那麼對如下的配置肯定不會陌生:

<bean id="personService" class="com.zixq.cache.service.test7.PersonService">
</bean>

這種方式是以前使用最多的方式,它預設使用了無參構造器建立bean。

當然還可以使用有參的構造器,透過<constructor-arg>標籤來完成配置。

<bean id="personService" class="com.zixq.cache.service.test7.PersonService">
   <constructor-arg index="0" value="zixq"></constructor-arg>
   <constructor-arg index="1" ref="baseInfo"></constructor-arg>
</bean>

其中:

  • index表示下標,從0開始。
  • value表示常量值
  • ref表示引用另一個bean

setter方法

Spring還提供了另外一種思路:透過setter方法設定bean所需引數,這種方式耦合性相對較低,比有參構造器使用更為廣泛。

先定義Person實體:

@Data
public class Person {
    private String name;
    private int age;
}

它裡面包含:成員變數name和age,getter/setter方法。

然後在bean.xml檔案中配置bean時,加上<property>標籤設定bean所需引數。

<bean id="person" class="com.zixq.cache.service.test7.Person">
   <property name="name" value="zixq" />
   <property name="age" value="18" />
</bean>

靜態工廠

這種方式的關鍵是需要定義一個工廠類,它裡面包含一個建立bean的靜態方法

public class ZixqBeanFactory {
    public static Person createPerson(String name, int age) {
        return new Person(name, age);
    }
}

接下來定義Person類如下:

@AllArgsConstructor
@NoArgsConstructor
@Data
public class Person {
    private String name;
    private int age;
}

它裡面包含:成員變數name和age,getter/setter方法,無參構造器和全參構造器。

然後在bean.xml檔案中配置bean時,透過factory-method引數指定靜態工廠方法,同時透過<constructor-arg>設定相關引數。

<bean class="com.zixq.cache.service.test7.ZixqBeanFactory" factory-method="createPerson">
   <constructor-arg index="0" value="zixq"></constructor-arg>
   <constructor-arg index="1" value="18"></constructor-arg>
</bean>

例項工廠方法

這種方式也需要定義一個工廠類,但裡面包含非靜態的建立bean的方法

public class ZixqBeanFactory {
    public Person createPerson(String name, int age) {
        return new Person(name, age);
    }
}

Person類跟上面一樣

然後bean.xml檔案中配置bean時,需要先配置工廠bean。然後在配置例項bean時,透過factory-bean引數指定該工廠bean的引用。

<bean id="susanBeanFactory" class="com.zixq.cache.service.test7.SusanBeanFactory">
</bean>

<bean factory-bean="ZixqBeanFactory" factory-method="createPerson">
   <constructor-arg index="0" value="zixq"></constructor-arg>
   <constructor-arg index="1" value="18"></constructor-arg>
</bean>

FactoryBean

上面的例項工廠方法每次都需要建立一個工廠類,不方面統一管理。這時就可以使用FactoryBean介面。

public class UserFactoryBean implements FactoryBean<User> {
    
    /**
     * 實現我們自己的邏輯建立物件
     */
    @Override
    public User getObject() throws Exception {
        return new User();
    }

    /**
     * 定義物件的型別
     */
    @Override
    public Class<?> getObjectType() {
        return User.class;
    }
}

然後在bean.xml檔案中配置bean時,只需像普通的bean一樣配置即可。

<bean id="userFactoryBean" class="com.zixq.async.service.UserFactoryBean">
</bean>

注意

getBean("userFactoryBean");獲取的是getObject方法中返回的物件;

getBean("&userFactoryBean");獲取的才是真正的UserFactoryBean物件。

透過上面五種方式,在bean.xml檔案中把bean配置好之後,Spring就會自動掃描和解析相應的標籤,並且幫我們建立和例項化bean,然後放入Spring容器中。

但如果遇到比較複雜的專案,則需要配置大量的bean,而且bean之間的關係錯綜複雜,這樣久而久之會導致xml檔案迅速膨脹,非常不利於bean的管理。

@Component 註解

為了解決bean太多時,xml檔案過大,從而導致膨脹不好維護的問題。在Spring2.5中開始支援:@Component@Repository@Service@Controller等註解定義bean。

這四種註解在功能上沒有特別的區別,不過在業界有個不成文的約定:

  • @Controller 一般用在控制層
  • @Service 一般用在業務層
  • @Repository 一般用在資料層
  • @Component 一般用在公共元件上

其實@Repository@Service@Controller三種註解也是@Component

image-20240313145208896

image-20240313145310670

image-20240313145339588

提示

透過這種@Component掃描註解的方式定義bean的前提是:需要先配置掃描路徑

目前常用的配置掃描路徑的方式如下:

  1. applicationContext.xml檔案中使用<context:component-scan>標籤。例如:
<context:component-scan base-package="com.zixq.cache" />
  1. 在Spring Boot的啟動類上加上@ComponentScan註解,例如:
@ComponentScan(basePackages = "com.zixq.cache")
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
    }
}
  1. 直接在SpringBootApplication註解上加,它支援ComponentScan功能:
@SpringBootApplication(scanBasePackages = "com.zixq.cache")
public class Application {
    
    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
    }
}

當然,如果你需要掃描的類跟Spring Boot的入口類,在同一級或者子級的包下面,無需指定scanBasePackages引數,Spring預設會從入口類的同一級或者子級的包去找。

@SpringBootApplication
public class Application {
    
    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
    }
}

除了上述四種@Component註解之外,Springboot還增加了@RestController註解,它是一種特殊的@Controller註解,所以也是@Component註解。

@RestController還支援@ResponseBody註解的功能,即將介面響應資料的格式自動轉換成JSON。

image-20240313145419559

JavaConfig:@Configuration + @Bean

缺點:只能建立該類中定義的bean例項,不能建立其他類的bean例項

@Component系列註解雖說使用起來非常方便,但是bean的建立過程完全交給Spring容器來完成,我們沒辦法自己控制。

Spring從3.0以後,開始支援JavaConfig的方式定義bean。它可以看做Spring的配置檔案,但並非真正的配置檔案,我們需要透過編碼Java程式碼的方式建立bean。例如:

@Configuration
public class MyConfiguration {

    @Bean
    public Person person() {
        return new Person();
    }
}

在JavaConfig類上加@Configuration註解,相當於配置了<beans>標籤。而在方法上加@Bean註解,相當於配置了<bean>標籤。

此外,Spring Boot還引入了一些列的@Conditional註解,用來控制bean的建立,這個註解前面已經說明了。

@Configuration
public class MyConfiguration {

    @ConditionalOnClass(Country.class)
    @Bean
    public Person person() {
        return new Person();
    }
}

@Import 註解

這個內容前面已經講了

前面介紹的@Configuration@Bean相結合的方式,我們可以透過程式碼定義bean。但也知道它的缺點是:它只能建立該類中定義的bean例項,不能建立其他類的bean例項。如果我們想建立其他類的bean例項該怎麼辦?答案就是可以使用@Import註解匯入

PostProcessor

Spring還提供了專門註冊bean的介面:BeanDefinitionRegistryPostProcessor

該介面的方法postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)上有這樣一段描述:

image-20240313145635877

翻譯:修改應用程式上下文的內部bean定義登錄檔標準初始化。所有常規bean定義都將被載入,但是還沒有bean被例項化。這允許進一步新增在下一個後處理階段開始之前定義bean。

如果用這個介面來定義bean,我們要做的事情就變得非常簡單了。只需定義一個類實現BeanDefinitionRegistryPostProcessor介面。重寫postProcessBeanDefinitionRegistry方法,在該方法中能夠獲取BeanDefinitionRegistry物件,它負責bean的註冊工作。

@Component
public class MyRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
    
    /**
     * BeanDefinitionRegistry 物件負責bean的註冊工作
     */
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        
        RootBeanDefinition roleBeanDefinition = new RootBeanDefinition(Role.class);
        registry.registerBeanDefinition("role", roleBeanDefinition);

        RootBeanDefinition userBeanDefinition = new RootBeanDefinition(User.class);
        userBeanDefinition.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE);
        registry.registerBeanDefinition("user", userBeanDefinition);
    }

    /**
     * 這個方法是它的父介面:BeanFactoryPostProcessor裡的方法,所以可以啥都不做
     */
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    }
}

image-20240313145613247

翻譯:在應用程式上下文的標準bean工廠之後修改其內部bean工廠初始化。所有bean定義都已載入,但沒有bean將被例項化。這允許重寫或新增屬性甚至可以初始化bean

@Component
public class MyPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        
        DefaultListableBeanFactory registry = (DefaultListableBeanFactory)beanFactory;
        RootBeanDefinition roleBeanDefinition = new RootBeanDefinition(Role.class);
        registry.registerBeanDefinition("role", roleBeanDefinition);

        RootBeanDefinition userBeanDefinition = new RootBeanDefinition(User.class);
        userBeanDefinition.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE);
        registry.registerBeanDefinition("user", userBeanDefinition);
    }
}

問題:BeanDefinitionRegistryPostProcessor 介面 和 BeanFactoryPostProcessor 介面都能註冊bean,那它們有什麼區別?

  • BeanDefinitionRegistryPostProcessor 更側重於bean的註冊
  • BeanFactoryPostProcessor 雖然也可以註冊bean,但更側重於對已經註冊的bean的屬性進行修改。

問題:既然拿到BeanDefinitionRegistry物件就能註冊bean,那透過BeanFactoryAware的方式是不是也能註冊bean?

DefaultListableBeanFactory就實現了BeanDefinitionRegistry介面

圖片

這樣一來,我們如果能夠獲取DefaultListableBeanFactory物件的例項,然後呼叫它的註冊方法,不就可以註冊bean了?

那就試試:定義一個類實現BeanFactoryAware介面,重寫setBeanFactory方法,在該方法中能夠獲取BeanFactory物件,它能夠強制轉換成DefaultListableBeanFactory物件,然後透過該物件的例項註冊bean。

@Component
public class BeanFactoryRegistry implements BeanFactoryAware {
    
    /**
     * 獲取BeanFactory物件,它能夠強制轉換成DefaultListableBeanFactory物件,然後透過該物件的例項註冊bean
     */
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        
        DefaultListableBeanFactory registry = (DefaultListableBeanFactory) beanFactory;
        RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(User.class);
        registry.registerBeanDefinition("user", rootBeanDefinition);

        RootBeanDefinition userBeanDefinition = new RootBeanDefinition(User.class);
        userBeanDefinition.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE);
        registry.registerBeanDefinition("user", userBeanDefinition);
    }
}

激動的心,顫抖的手,啟動專案就一個錯誤懟在臉上

圖片

Why?這跟Spring中bean的建立過程順序有關,大致如下:

圖片

BeanFactoryAware介面是在bean建立成功,並且完成依賴注入之後,在真正初始化之前才被呼叫的。在這個時候去註冊bean意義不大,因為這個介面是給我們獲取bean的,並不建議去註冊bean,會引發很多問題。

提示

ApplicationContextRegistry 和 ApplicationListener介面也有類似的問題,我們可以用他們獲取bean,但不建議用它們註冊bean。

@Autowired 註解

@Autowired的預設裝配

主要針對相同型別的物件只有一個的情況,此時物件型別是唯一的,可以找到正確的物件。

在Spring中@Autowired註解,是用來自動裝配物件的。通常,我們在專案中是這樣用的:

import org.springframework.stereotype.Service;

@Service
public class TestService1 {
    public void test1() {
    }
}



@Service
public class TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

沒錯,這樣是能夠裝配成功的,因為預設情況下Spring是按照型別裝配的,也就是我們所說的byType方式。

此外,@Autowired註解的required引數預設是true,表示開啟自動裝配,有些時候我們不想使用自動裝配功能,可以將該引數設定成false。

相同型別的物件不只一個時

上面byType方式主要針對相同型別的物件只有一個的情況,此時物件型別是唯一的,可以找到正確的物件。

但如果相同型別的物件不只一個時,會發生什麼?

建個同名的類TestService1:

import org.springframework.stereotype.Service;

@Service
public class TestService1 {

    public void test1() {
    }
}

重新啟動專案時:

Caused by: org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'testService1' for bean class [com.sue.cache.service.test.TestService1] conflicts with existing, non-compatible bean definition of same name and class [com.sue.cache.service.TestService1]

結果報錯了,報類類名稱有衝突,直接導致專案啟動不來。

注意

這種情況不是相同型別的物件在Autowired時有兩個導致的,非常容易產生混淆。這種情況是因為Spring的@Service方法不允許出現相同的類名,因為Spring會將類名的第一個字母轉換成小寫,作為bean的名稱,比如:testService1,而預設情況下bean名稱必須是唯一的。

下面看看什麼情況會產生兩個相同的型別bean:

public class TestService1 {

    public void test1() {
    }
}
@Service
public class TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}
@Configuration
public class TestConfig {

    @Bean("test1")
    public TestService1 test1() {
        return new TestService1();
    }

    @Bean("test2")
    public TestService1 test2() {
        return new TestService1();
    }
}

在TestConfig類中手動建立TestService1例項,並且去掉TestService1類上原有的@Service註解。

重新啟動專案:

圖片

果然報錯了,提示testService1是單例的,卻找到兩個物件。

其實還有一個情況會產生兩個相同的型別bean:

public interface IUser {
    void say();
}
@Service
public class User1 implements IUser{
    @Override
    public void say() {
    }
}
@Service
public class User2 implements IUser{
    @Override
    public void say() {
    }
}
@Service
public class UserService {

    @Autowired
    private IUser user;
}

專案重新啟動時:

圖片

報錯了,提示跟上面一樣,testService1是單例的,卻找到兩個物件。

第二種情況在實際的專案中出現得更多一些,後面的例子,我們主要針對第二種情況。

@Qualifier 和 @Primary

在Spring中,按照Autowired預設的裝配方式:byType,是無法解決上面的問題的,這時可以改用按名稱裝配:byName。

在程式碼上加上@Qualifier註解即可:

@Service
public class UserService {

    @Autowired
    @Qualifier("user1")
    private IUser user;
}

只需這樣調整之後,專案就能正常啟動了。

Qualifier意思是合格者,一般跟Autowired配合使用,需要指定一個bean的名稱,透過bean名稱就能找到需要裝配的bean。

除了上面的@Qualifier註解之外,還能使用@Primary註解解決上面的問題。在User1上面加上@Primary註解:

@Primary
@Service
public class User1 implements IUser{
    @Override
    public void say() {
    }
}

去掉UserService上的@Qualifier註解:

@Service
public class UserService {

    @Autowired
    private IUser user;
}

重新啟動專案,一樣能正常執行。

當我們使用自動配置的方式裝配Bean時,如果這個Bean有多個候選者,假如其中一個候選者具有@Primary註解修飾,該候選者會被選中,作為自動配置的值。

@Autowired的使用範圍

上面的例項中@Autowired註解,都是使用在成員變數上,但@Autowired的強大之處,遠非如此。

先看看@Autowired註解的定義:

image-20240313145812722

從圖中可以看出該註解能夠使用在5種目標型別上,用一張圖總結一下:

圖片

該註解我們平常使用最多的地方可能是在成員變數上。接下來,看看在其他地方該怎麼用

成員變數上使用@Autowired

@Service
public class UserService {

    @Autowired
    private IUser user;
}

這種方式是平時用得最多的。

構造器上使用@Autowired

@Service
public class UserService {

    private IUser user;

    @Autowired
    public UserService(IUser user) {
        this.user = user;
        System.out.println("user:" + user);
    }
}

注意

在構造器上加@Autowired註解,實際上還是使用了Autowired裝配方式,並非構造器裝配。

方法上使用@Autowired

@Service
public class UserService {

    @Autowired
    public void test(IUser user) {
       user.say();
    }
}

Spring會在專案啟動的過程中,自動呼叫一次加了@Autowired註解的方法,我們可以在該方法做一些初始化的工作。

也可以在setter方法上@Autowired註解:

@Service
public class UserService {

    private IUser user;

    @Autowired
    public void setUser(IUser user) {
        this.user = user;
    }
}

引數上使用@Autowired

@Service
public class UserService {

    private IUser user;

    public UserService(@Autowired IUser user) {
        this.user = user;
        System.out.println("user:" + user);
    }
}

也可以在非靜態方法的入參上加@Autowired註解:

@Service
public class UserService {

    public void test(@Autowired IUser user) {
       user.say();
    }
}

註解上使用@Autowired

想啥呢,看一眼就夠了,你還想更進一步?

這種方式用得不多,不用瞭解。

@Autowired的高階玩法

面舉的例子都是透過@Autowired自動裝配單個例項,@Autowired也能自動裝配多個例項

將UserService方法調整一下,用一個List集合接收IUser型別的引數:

@Service
public class UserService {

    @Autowired
    private List<IUser> userList;

    @Autowired
    private Set<IUser> userSet;

    @Autowired
    private Map<String, IUser> userMap;

    public void test() {
        System.out.println("userList:" + userList);
        System.out.println("userSet:" + userSet);
        System.out.println("userMap:" + userMap);
    }
}

增加一個controller:

@RequestMapping("/u")
@RestController
public class UController {

    @Autowired
    private UserService userService;

    @RequestMapping("/test")
    public String test() {
        userService.test();
        return "success";
    }
}

呼叫該介面後:

圖片

從圖中看出:userList、userSet和userMap都列印出了兩個元素,說明@Autowired會自動把相同型別的IUser物件收集到集合中

img

@Autowired一定能裝配成功?

有些情況下,即使使用了@Autowired裝配的物件還是null,到底是什麼原因?

沒有加@Service註解

在類上面忘了加@Controller@Service@Component@Repository等註解,Spring就無法完成自動裝配的功能

public class UserService {

    @Autowired
    private IUser user;

    public void test() {
        user.say();
    }
}

這種情況應該是最常見的錯誤了,別以為你長得帥,就不會犯這種低階的錯誤

注入Filter 或 Listener

web應用啟動的順序是:listener->filter->servlet

public class UserFilter implements Filter {

    @Autowired
    private IUser user;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        user.say();
    }

    @Override
    public void doFilter(ServletRequest request, 
                         ServletResponse response, 
                         FilterChain chain) throws IOException, ServletException {

    }

    @Override
    public void destroy() {
    }
}
@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setFilter(new UserFilter());
        bean.addUrlPatterns("/*");
        return bean;
    }
}

程式啟動會報錯:

圖片

tomcat無法正常啟動。Why?

眾所周知,Spring MVC的啟動是在DisptachServlet裡面做的,而它是在listener和filter之後執行。如果我們想在listener和filter裡面@Autowired某個bean,肯定是不行的,因為filter初始化的時候,此時bean還沒有初始化,無法自動裝配。

如果工作當中真的需要這樣做,我們該如何解決這個問題?

答案是使用WebApplicationContextUtils.getWebApplicationContext獲取當前的ApplicationContext,再透過它獲取到bean例項

public class UserFilter  implements Filter {

    private IUser user;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 獲取當前的ApplicationContext
        ApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(filterConfig.getServletContext());
        
        this.user = ((IUser)(applicationContext.getBean("user1")));
        user.say();
    }

    @Override
    public void doFilter(ServletRequest request, 
                         ServletResponse response, 
                         FilterChain chain) throws IOException, ServletException {

    }

    @Override
    public void destroy() {

    }
}

註解未被@ComponentScan掃描

通常情況下,@Controller@Service@Component@Repository@Configuration等註解,是需要透過@ComponentScan註解掃描,收集後設資料的

但是,如果沒有加@ComponentScan註解,或者@ComponentScan註解掃描的路徑不對,或者路徑範圍太小,會導致有些註解無法收集,到後面無法使用@Autowired完成自動裝配的功能。

號外號外

在Spring Boot專案中,如果使用了@SpringBootApplication註解,它裡面內建了@ComponentScan註解的功能

迴圈依賴問題

迴圈依賴:就是一個或多個物件例項之間存在直接或間接的依賴關係,這種依賴關係構成了構成一個環形呼叫。

第一種情況:自己依賴自己的直接依賴。

圖片

第二種情況:兩個物件之間的直接依賴。

圖片

第三種情況:多個物件之間的間接依賴。

圖片

前面兩種情況的直接迴圈依賴比較直觀,非常好識別,但是第三種間接迴圈依賴的情況有時候因為業務程式碼呼叫層級很深,不容易識別出來。

圖片

Spring的bean預設是單例的,如果單例bean使用@Autowired自動裝配,大多數情況,能解決迴圈依賴問題。

但是如果bean是多例的,會出現迴圈依賴問題,導致bean自動裝配不了。

還有有些情況下,如果建立了代理物件,即使bean是單例的,依然會出現迴圈依賴問題。

@Autowired 和 @Resouce的區別

@Autowired功能雖說非常強大,但是也有些不足之處。比如:比如它跟Spring強耦合了,如果換成了JFinal等其他框架,功能就會失效。而@Resource是JSR-250提供的,它是Java標準,絕大部分框架都支援。

除此之外,有些場景使用@Autowired無法滿足的要求,改成@Resource卻能解決問題。接下來看看@Autowired@Resource的區別:

  • @Autowired預設按byType自動裝配,而@Resource預設byName自動裝配。
  • @Autowired只包含一個引數:required,表示是否開啟自動准入,預設是true。而@Resource包含七個引數,其中最重要的兩個引數是:name 和 type。
  • @Autowired如果要使用byName,需要使用@Qualifier一起配合。而@Resource如果指定了name,則用byName自動裝配,如果指定了type,則用byType自動裝配。
  • @Autowired能夠用在:構造器、方法、引數、成員變數和註解上,而@Resource能用在:類、成員變數和方法上。
  • @Autowired是Spring定義的註解,而@Resource是JSR-250定義的註解。

此外,它們的裝配順序不同。

@Autowired的裝配順序如下:

圖片

@Resource的裝配順序如下:

1.、如果同時指定了name和type:

圖片

2、如果指定了name:

圖片

3、如果指定了type:

圖片

4、如果既沒有指定name,也沒有指定type:

圖片

@Value 註解

由一個例子開始

假如在UserService類中,需要注入系統屬性到userName變數中。通常情況下,我們會寫出如下的程式碼:

@Service
public class UserService {

    @Value("${zixq.test.userName}")
    private String userName;

    public String test() {
        System.out.println(userName);
        return userName;
    }
}

不過,上面功能的重點是要在applicationContext.properties配置檔案中配置同名的系統屬性:

# 張三
zixq.test.userName=\u5f20\u4e09

那麼,名稱真的必須完全相同嗎?

關於屬性名

這時候,有個吊毛會說啦:在@ConfigurationProperties配置類中,定義的引數名可以跟配置檔案中的系統屬性名不同。

如:在配置類MyConfig類中定義的引數名是userName

@Configuration
@ConfigurationProperties(prefix = "zixq.test")
@Data
public class MyConfig {
    private String userName;
}

而配置檔案中配置的系統屬性名是:

zixq.test.user-name=\u5f20\u4e09

兩個引數名不一樣,測試之後,發現該功能能夠正常執行。

配置檔案中的系統屬性名用 駝峰標識小寫字母加中劃線的組合,Spring都能找到配置類中的屬性名進行賦值。

由此可見,配置檔案中的系統屬性名,可以跟配置類中的屬性名不一樣。

吊毛啊,你說的這些是有個前提的:字首(zixq.test)必須相同

那麼,@Value註解中定義的系統屬性名也可以不一樣?

答案:不能。如果不一樣,啟動專案時會直接報錯

Caused bt:java.lang.IllegatArgumentEcveption:Could not resolve placeholder“zixq.test.userName” in value “${zixq.test.UserName}”

此外,如果只在@Value註解中指定了系統屬性名,但實際在配置檔案中沒有配置它,也會報跟上面一樣的錯。

所以,@Value註解中指定的系統屬性名,必須跟配置檔案中的相同。

亂碼問題

前面我配置的屬性值:張三,其實是轉義過的

zixq.test.userName=\u5f20\u4e09

為什麼要做這個轉義?

假如在配置檔案中配置中文的張三:

zixq.test.userName=張三

最後獲取資料時,你會發現userName竟然出現了亂碼:

å¼ ä¸

王德發?為什麼?

答:在Spring Boot的CharacterReader類中,預設的編碼格式是ISO-8859-1,該類負責.properties檔案中系統屬性的讀取。如果系統屬性包含中文字元,就會出現亂碼

image-20240313150142431

如何解決亂碼問題?

目前主要有如下三種方案:

  1. 手動將ISO-8859-1格式的屬性值,轉換成UTF-8格式。
  2. 設定encoding引數,不過這個只對@PropertySource註解有用。
  3. 將中文字元用unicode編碼轉義。

顯然@Value不支援encoding引數,所以方案2不行。

假如使用方案1,具體實現程式碼如下:

@Service
public class UserService {

    @Value(value = "${zixq.test.userName}")
    private String userName;

    public String test() {
        String userName1 = new String(userName.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
        System.out.println();
        return userName1;
    }
}

確實可以解決亂碼問題。

但如果專案中包含大量中文系統屬性值,每次都需要加這樣一段特殊轉換程式碼。出現大量重複程式碼,有沒有覺得有點噁心?

反正我被噁心到了

那麼,如何解決程式碼重複問題?

答:將屬性值的中文內容轉換成unicode

zixq.test.userName=\u5f20\u4e09

這種方式同樣能解決亂碼問題,不會出現噁心的重複程式碼。但需要做一點額外的轉換工作,不過這個轉換非常容易,因為有現成的線上轉換工具。

推薦使用這個工具轉換:http://www.jsons.cn/unicode/

Duang Duang~去洗腳城的那個吊毛叼著歌過來了:太陽出來嘛爬山坡,爬到啦山頂嘛想唱guo(歌),真是給爺整笑了。你是真會吹牛掰啊,我使用.yml.yaml格式的配置檔案時,咋不會出現中文亂碼問題?

一邊涼快去,這玩意兒能一樣嗎。.yml.yaml格式的配置檔案,最終會使用UnicodeReader類進行解析,它的init方法中,首先讀取BOM檔案頭資訊,如果頭資訊中有UTF8、UTF16BE、UTF16LE,就採用對應的編碼,如果沒有,則採用預設UTF8編碼。

image-20240313150344807

提示

亂碼問題一般出現在本地環境,因為本地直接讀取的.properties配置檔案。在dev、test、生產等環境,如果從zookeeper、apollo、nacos等配置中心中獲取系統引數值,走的是另外的邏輯,並不會出現亂碼問題。

預設值

有時候,預設值是我們非常頭疼的問題

因為很多時候使用Java的預設值,並不能滿足我們的日常工作需求。

比如有這樣一個需求:如果配置了系統屬性,userName就用配置的屬性值;如果沒有配置,則userName用預設值zixq。

可能認為可以這樣做:

@Value(value = "${zixq.test.userName}")
private String userName = "zixq";

這招是行不通滴。因為設定userName預設值的時機,比@Value註解依賴注入屬性值要早,也就是說userName初始化好了預設值,後面還是會被覆蓋。

正確的姿勢是:使用:

@Value(value = "${zixq.test.userName:zixq}")
private String userName;

建議平時在使用@Value時,儘量都設定一個預設值。如果不需要預設值,寧可設定一個空

@Value(value = "${zixq.test.userName:}")
private String userName;

為什麼這麼說?

假如有這種場景:在business層中包含了UserService類,business層被api服務和job服務都引用了。但UserService類中@Value的userName只在api服務中有用,在job服務中根本用不到該屬性。

對於job服務來說,如果不在.properties檔案中配置同名的系統屬性,則服務啟動時就會報錯。

static變數

透過前面的內容已經知道:使用@Value註解,可以給類的成員變數注入系統屬性值

那麼靜態變數可以自動注入系統屬性值不?

@Value("${zixq.test.userName}")
private static String userName;

程式可以正常啟動,但是獲取到userName的值卻是null。

由此可見,static修飾的變數透過@Value會注入失敗

犄角旮旯傳出一個聲音:那如何才能給靜態變數注入系統屬性值?

嘿嘿嘿~那你要騷一點才行啊

@Service
public class UserService {

    private static String userName;

    @Value("${zixq.test.userName}")
    public void setUserName(String userName) {
        UserService.userName = userName;
    }

    public String test() {
        return userName;
    }
}

提供一個靜態引數的setter方法,在該方法上使用@Value注入屬性值,並且同時在該方法中給靜態變數賦值。

哎喲~我去,@Value註解在這裡竟然使用在setUserName方法上了,也就是對應的setter方法,而不是在變數上。嗯,騷,確實是騷!

不過,通常情況下,我們一般會在pojo實體類上,使用lombok的@Data@Setter@Getter等註解,在編譯時動態增加setter或getter方法,所以@Value用在方法上的場景其實不多。

變數型別

上面的內容,都是用的字串型別的變數進行舉例的。其實,@Value註解還支援其他多種型別的系統屬性值的注入。

基本型別

@Value註解對8種基本型別和相應的包裝類,有非常良好的支援

@Value("${zixq.test.a:1}")
private byte a;

@Value("${zixq.test.b:100}")
private short b;

@Value("${zixq.test.c:3000}")
private int c;

@Value("${zixq.test.d:4000000}")
private long d;

@Value("${zixq.test.e:5.2}")
private float e;

@Value("${zixq.test.f:6.1}")
private double f;

@Value("${zixq.test.g:false}")
private boolean g;

@Value("${zixq.test.h:h}")
private char h;

@Value("${zixq.test.a:1}")
private byte a1;




@Value("${zixq.test.b:100}")
private Short b1;

@Value("${zixq.test.c:3000}")
private Integer c1;

@Value("${zixq.test.d:4000000}")
private Long d1;

@Value("${zixq.test.e:5.2}")
private Float e1;

@Value("${zixq.test.f:6.1}")
private Double f1;

@Value("${zixq.test.g:false}")
private Boolean g1;

@Value("${zixq.test.h:h}")
private Character h1;

陣列

@Value("${zixq.test.array:1,2,3,4,5}")
private int[] array;

Spring預設使用逗號分隔引數值

如果用空格分隔,例如:

@Value("${zixq.test.array:1 2 3 4 5}")
private int[] array;

Spring會自動把空格去掉,導致資料中只有一個值:12345,所以注意千萬別搞錯了

多提一嘴

如果我們把陣列定義成:short、int、long、char、string型別,Spring是可以正常注入屬性值的。

但如果把陣列定義成:float、double型別,啟動專案時就會直接報錯

圖片

真是裂開了呀!按理說,1,2,3,4,5用float、double是能夠表示的呀,為什麼會報錯?

如果使用int的包裝類,比如:

@Value("${zixq.test.array:1,2,3,4,5}")
private Integer[] array;

啟動專案時同樣會報上面的異常。

此外,定義陣列時一定要注意屬性值的型別,必須完全一致才可以,如果出現下面這種情況:

@Value("${zixq.test.array:1.0,abc,3,4,5}")
private int[] array;

屬性值中包含了1.0和abc,顯然都無法將該字串轉換成int。

集合類

有了基本型別和陣列,的確讓我們更加方便了。但對資料的處理,只用陣列這一種資料結構是遠遠不夠的

List

List是陣列的變種,它的長度是可變的,而陣列的長度是固定的

@Value("${zixq.test.list}")
private List<String> list;

最關鍵的是看配置檔案:

zixq.test.list[0]=10
zixq.test.list[1]=11
zixq.test.list[2]=12
zixq.test.list[3]=13

當你滿懷希望的啟動專案,準備使用這個功能的時候,卻發現竟然報錯了。

Caused bt:java.lang.IllegatArgumentEcveption:Could not resolve placeholder“zixq.test.list” in value “${zixq.test.list}”

看來@Value不支援這種直接的List注入

那麼,如何解決這個問題?

嗯。。。。。。。你沒猜錯,曾經有個長得不咋滴的吊毛趴在我椅子上說:真是麻雀上插秧,搔首弄姿。用@ConfigurationProperties不就完了嗎

@Data
@Configuration
@ConfigurationProperties(prefix = "zixq.test")
public class MyConfig {
    private List<String> list;
}

然後在呼叫的地方這樣寫:

@Service
public class UserService {

    @Autowired
    private MyConfig myConfig;

    public String test() {
        System.out.println(myConfig.getList());
        return null;
    }
}

理所應當的,哪個欠懟的吊毛收到了一句話:啊哈。。。。。。。。還挺聰明啊,這種方法確實能夠完成List注入。簡直是豬鼻子上插大蔥,真可謂褲襠裡彈琴,扯卵彈(談),這隻能說明@ConfigurationProperties註解的強大,跟@Value有半毛錢的關係?

那麼問題來了,用@Value如何實現這個功能?

答:使用Spring的EL表示式(使用#號加大括號的EL表示式)

List的定義改成:

@Value("#{'${zixq.test.list}'.split(',')}")
private List<String> list;

然後配置檔案改成跟定義陣列時的配置檔案一樣:

zixq.test.list=10,11,12,13

Set

Set也是一種儲存資料的集合,它比較特殊,裡面儲存的資料不會重複

Set跟List的用法極為相似

@Value("#{'${zixq.test.set}'.split(',')}")
private Set<String> set;

配置檔案是這樣的:

zixq.test.set=10,11,12,13

但怎麼能少了騷操作呢

問題:如何給List 或 Set設定預設值空?

直接在@Value$表示式後面加個:號可行?

@Value("#{'${zixq.test.set:}'.split(',')}")
private Set<String> set;

結果卻跟想象中不太一樣:

image-20240313022902630

Set集合怎麼不是空的,而是包含了一個空字串的集合?

嗯。。。。。那我在:號後加null,總可以了吧?

@Value("#{'${zixq.test.set:null}'.split(',')}")
private Set<String> set;

image-20240313023042447

Set集合也不是空的,而是包含了一個"null"字串的集合

這也不行,那也不行,該如何是好?

答:使用EL表示式的empty方法

@Value("#{'${zixq.test.set:}'.empty ? null : '${zixq.test.set:}'.split(',')}")
private Set<String> set;

其實List也有類似的問題,也能使用該方法解決問題

提示

該判斷的表示式比較複雜,自己手寫非常容易寫錯,建議複製貼上之後根據實際需求改改

Map

還有一種比較常用的集合是map,它支援key/value鍵值對的形式儲存資料,並且不會出現相同key的資料。

@Value("#{${zixq.test.map}}")
private Map<String, String> map;

配置檔案是這樣的:

zixq.test.map={"name":"蘇三", "age":"18"}

設定預設值的程式碼如下:

@Value("#{'${zixq.test.map:}'.empty ? null : '${zixq.test.map:}'}")
private Map<String, String> map;

EL高階玩法

前面已經見識過spring EL表示式的用法了,在設定空的預設值時特別有用

其實,empty方法只是它很普通的用法,還有更高階的用法

注入bean

以前我們注入bean,一般都是用的@Autowired或者@Resource註解

@Value註解也可以注入bean,它是這麼做的:

@Value("#{roleService}")	// 注入id為roleService的bean
private RoleService roleService;

bean的變數和方法

透過EL表示式,@Value註解已經可以注入bean了。既然能夠拿到bean例項,接下來,可以再進一步。

在RoleService類中定義了:成員變數、常量、方法、靜態方法。

@Service
public class RoleService {
    public static final int DEFAULT_AGE = 18;
    public int id = 1000;

    public String getRoleName() {
        return "管理員";
    }

    public static int getParentId() {
        return 2000;
    }
}

在呼叫的地方這樣寫:

@Service
public class UserService {

    @Value("#{roleService.DEFAULT_AGE}")
    private int myAge;

    @Value("#{roleService.id}")
    private int id;

    @Value("#{roleService.getRoleName()}")
    private String myRoleName;

    @Value("#{roleService.getParentId()}")
    private String myParentId;

    public String test() {
        System.out.println(myAge);
        System.out.println(id);
        System.out.println(myRoleName);
        System.out.println(myParentId);
        return null;
    }
}

在UserService類中透過@Value可以注入:成員變數、常量、方法、靜態方法獲取到的值,到相應的成員變數中

靜態類

前面的內容都是基於bean的,但有時我們需要呼叫靜態類,比如:Math、xxxUtil等靜態工具類的方法,該怎麼辦?

答:用T + 括號

  1. 可以注入系統的路徑分隔符到path中
@Value("#{T(java.io.File).separator}")
private String path;
  1. 可以注入一個隨機數到randomValue中
@Value("#{T(java.lang.Math).random()}")
private double randomValue;

邏輯運算

透過上面介紹的內容,我們可以獲取到絕大多數類的變數和方法的值了。但有了這些值,還不夠,我們能不能在EL表示式中加點邏輯?

  1. 拼接字串
@Value("#{roleService.roleName + '' + roleService.DEFAULT_AGE}")
private String value;
  1. 邏輯判斷
@Value("#{roleService.DEFAULT_AGE > 16 and roleService.roleName.equals('蘇三')}")
private String operation;
  1. 三目運算
@Value("#{roleService.DEFAULT_AGE > 16 ? roleService.roleName: '蘇三' }")
private String realRoleName;

${} 和 #{}的區別

上面巴拉巴拉說了這麼多@Value的牛逼用法,歸根揭底就是${}#{}的用法

${}

主要用於獲取配置檔案中的系統屬性值

@Value(value = "${zixq.test.userName:susan}")
private String userName;

透過:可以設定預設值。如果在配置檔案中找不到zixq.test.userName的配置,則注入時用預設值。

如果在配置檔案中找不到zixq.test.userName的配置,也沒有設定預設值,則啟動專案時會報錯。

#{}

主要用於透過Spring的EL表示式,獲取bean的屬性,或者呼叫bean的某個方法。還有呼叫類的靜態常量和靜態方法

@Value("#{roleService.DEFAULT_AGE}")
private int myAge;

@Value("#{roleService.id}")
private int id;

@Value("#{roleService.getRoleName()}")
private String myRoleName;

@Value("#{T(java.lang.Math).random()}")
private double randomValue;

提示

如果是呼叫類的靜態方法,則需要加T(包名 + 方法名稱),如:T(java.lang.Math)

相關md文件

  • 連結:Spring 中不得不瞭解的姿勢

相關文章