說明
本文非原創,我只是進行了整理以及做了一些改動,僅供學習,若需進行商業使用,請聯絡原作者
原作者:蘇三
原文連結:蘇三說技術:Spring系列
Spring IOC
本章節解讀的流程為Spring容器初始化的前期準備工作
- Spring容器初始化的入口
- refresh方法的主要流程
- 解析xml配置檔案
- 生成BeanDefinition
- 註冊BeanDefinition
- 修改BeanDefinition
- 註冊BeanPostProcessor
真正的好戲是後面的流程:例項化Bean
、依賴注入
、初始化Bean
、BeanPostProcessor呼叫
等。
入口
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容器初始化的真正入口。
呼叫refresh
方法的類並非只有這兩個,用一張圖整體認識一下:
雖說呼叫refresh
方法的類有這麼多,但我決定用ClassPathXmlApplicationContext
類作為列子,因為它足夠經典,而且難度相對來說要小一些。
refresh方法
refresh
方法是Spring IOC
的真正入口,它負責初始化Spring容器。refresh
表示重新構建的意思。
既然這個方法的作用是初始化Spring容器,那方法名為啥不叫init
?因為它不只被呼叫一次。
在Spring Boot
的SpringAppication
類中的run
方法會呼叫refreshContext
方法,該方法會呼叫一次refresh
方法。
在spring Cloud
的BootstrapApplicationListener
類中的onApplicationEvent
方法會呼叫SpringAppication
類中的run
方法。也會呼叫一次refresh
方法。
這是Spring Boot專案中如果引入了Spring Cloud,則
refresh
方法會被呼叫兩次的原因。
在Spring MVC
的FrameworkServlet
類中的initWebApplicationContext
方法會呼叫configureAndRefreshWebApplicationContext
方法,該方法會呼叫一次refresh
方法,不過會提前判斷容器是否啟用。
所以這裡的refresh
表示重新構建的意思。
refresh
的關鍵步驟:
一眼看過去好像有很多方法,但是真正的核心的方法不多,我主要講其中最重要的:
- obtainFreshBeanFactory
- invokeBeanFactoryPostProcessors
- registerBeanPostProcessors
- 【finishBeanFactoryInitialization】
obtainFreshBeanFactory:解析xml配置檔案,生成BeanDefinition物件,註冊到Spring容器中
obtainFreshBeanFactory
方法會解析xml的bean配置,生成BeanDefinition
物件,並且註冊到Spring容器中(說白了就是很多map集合中)。
經過幾層呼叫之後,會調到AbstractBeanDefinitionReader
類的loadBeanDefinitions
方法:
該方法會迴圈locations
(applicationContext.xml檔案路徑),呼叫另外一個loadBeanDefinitions
方法,一個檔案一個檔案解析。
經過一些列的騷操作,會將location轉換成inputSource和resource,然後再轉換成Document物件,方便解析。
在解析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了一個物件:
真正複雜的地方是在前面的各種屬性的解析和賦值上。
註冊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
方法:
流程看起來很長,其實邏輯比較簡單,主要是在處理BeanDefinitionRegistryPostProcessor
和BeanFactoryPostProcessor
。
而BeanDefinitionRegistryPostProcessor
本身是一種特殊的BeanFactoryPostProcessor
,它也會執行BeanFactoryPostProcessor
的邏輯,只是加了一個額外的方法
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生成代理物件?你不好奇?
一張圖概括:
入口1:自定義TargetSource的場景
AbstractAutowireCapableBeanFactory類的createBean方法中,有這樣一段程式碼:
它透過BeanPostProcessor提供了一個生成代理物件的機會。具體邏輯在AbstractAutoProxyCreator類的postProcessBeforeInstantiation方法中:
說白了,需要實現
TargetSource
才有可能會生成代理物件。該介面是對Target
目標物件的封裝,透過該介面可以獲取到目標物件的例項。
不出意外,這時又會冒出一個吊毛。
刺頭青F說:這裡生成代理物件有什麼用呢?
有時我們想自己控制bean的建立和初始化,而不需要透過spring容器,這時就可以透過實現TargetSource
滿足要求。只是建立單純的例項還好,如果我們想使用代理該怎麼辦呢?這時候,入口1的作用就體現出來了。
入口2:解決代理物件迴圈依賴問題的場景
AbstractAutowireCapableBeanFactory類的doCreateBean方法中,有這樣一段程式碼:
它主要作用是為了解決物件的迴圈依賴問題,核心思路是提前暴露singletonFactory到快取中。
透過getEarlyBeanReference方法生成代理物件:
它又會呼叫wrapIfNecessary方法:
這裡有你想看到的生成代理的邏輯。
這時。。。。,你猜錯了,吊毛為報養育之恩,帶父嫖娼去了。。。
入口3:普通Bean生成代理物件的場景
AbstractAutowireCapableBeanFactory類的initializeBean方法中,有這樣一段程式碼:
它會呼叫到AbstractAutoProxyCreator類postProcessAfterInitialization方法:
該方法中能看到我們熟悉的面孔: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方法中,有這樣一段程式碼:
它裡面包含:
- JdkDynamicAopProxy JDK動態代理生成類
- ObjenesisCglibAopProxy cglib代理生成類
JdkDynamicAopProxy類的invoke方法生成的代理物件。而ObjenesisCglibAopProxy類的父類:CglibAopProxy,它的getProxy方法生成的代理物件。
哪個更好?
不出意外,又會來個吊毛,但這吊毛不是別人,是你!
啊,蒼天啊,大地呀!勒個墳哇,我熱你溫啦:JDK動態代理和cglib哪個更好啊?
嘻嘻~其實這個問題沒有標準答案,要看具體的業務場景:
- 沒有定義介面,只能使用cglib,不說它好不行。
- 定義了介面,需要建立單例或少量物件,呼叫多次時,可以使用jdk動態代理,因為它建立時更耗時,但呼叫時速度更快。
- 定義了介面,需要建立多個物件時,可以使用cglib,因為它建立速度更快。
隨著jdk版本不斷迭代更新,jdk動態代理建立耗時不斷被最佳化,8以上的版本中,跟cglib已經差不多。所以Spring官方預設推薦使用jdk動態代理,因為它呼叫速度更快。
如果要強制使用cglib,可以透過以下兩種方式:
spring.aop.proxy-target-class=true
@EnableAspectJAutoProxy(proxyTargetClass = true)
五種通知 / 增強
Spring AOP給這五種通知,分別分配了一個xxxAdvice類。在ReflectiveAspectJAdvisorFactory類的getAdvice方法中可以看得到:
用一張圖總結一下對應關係:
這五種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方法中,有這樣一段程式碼:
用一張圖捋一捋上面的邏輯:
包含了一個遞迴的鏈式呼叫,為什麼要這樣設計?
假如不這樣設計,我們程式碼中是不是需要寫很多if...else,根據不同的切面和通知單獨處理?
而Spring巧妙的使用責任鏈模式消除了原本需要大量的if...else判斷,讓程式碼的擴充套件性更好,很好的體現了開閉原則:對擴充套件開放,對修改關閉。
快取中存的是原始物件還是代理物件?
都知道Spring中為了效能考慮是有快取的,通常說包含了三級快取:
只聽“咻兒”地一聲,刺頭青D的兄弟,刺頭青F忍不住趕過來問了句:快取中存的是原始物件還是代理物件?
前面那位帶父搬磚的仁兄下意識地來了一句:應該不是物件,是馬子
嘻嘻~這個問題要從三個方面回答
singletonFactories(三級快取)
AbstractAutowireCapableBeanFactory類的doCreateBean方法中,有這樣一段程式碼:
其實之前已經說過,它是為了解決迴圈依賴問題。這次要說的是addSingletonFactory方法:
它裡面儲存的是singletonFactory物件,所以是原始物件
earlySingletonObjects(二級快取)
AbstractBeanFactory類的doGetBean方法中,有這樣一段程式碼:
在呼叫getBean方法獲取bean例項時,會呼叫getSingleton嘗試先從快取中看能否獲取到,如果能獲取到則直接返回。
這段程式碼會先從一級快取中獲取bean,如果沒有再從二級快取中獲取,如果還是沒有則從三級快取中獲取singletonFactory,透過getObject方法獲取例項,將該例項放入到二級快取中。
答案的謎底就聚焦在getObject方法中,而這個方法又是在哪來定義的呢?
其實就是上面的getEarlyBeanReference方法,我們知道這個方法生成的是代理物件,所以二級快取中存的是代理物件。
singletonObjects(一級快取)
提示
走好,看好,眼睛不要打跳(t iao~ 三聲),這裡是DefaultSingletonBeanRegistry類的getSingleton方法,跟上面二級快取中說的AbstractBeanFactory類getSingleton方法不一樣
DefaultSingletonBeanRegistry類的getSingleton方法中,有這樣一段程式碼:
此時的bean建立、注入和初始化完成了,判斷如果是新的單例物件,則會加入到一級快取中,具體程式碼如下:
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無法生成代理物件,事務會失效。這個問題的解決辦法有很多:
- 使用TransactionTemplate手動開啟事務
- 將事務方法save放到新加的類UserSaveService中,透過userSaveService.save呼叫事務方法。
- UserService類中
@Autowired
注入自己的例項userService,透過userService.save呼叫事務方法。 - 透過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介面是很常用的功能,目前包含如下功能:
如何初始化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-method
、PostConstruct
和 InitializingBean
的執行順序是什麼樣的?
決定他們呼叫順序的關鍵程式碼在AbstractAutowireCapableBeanFactory
類的initializeBean
方法中。
這段程式碼中會先呼叫BeanPostProcessor
的postProcessBeforeInitialization
方法,而PostConstruct
是透過InitDestroyAnnotationBeanPostProcessor
實現的,它就是一個BeanPostProcessor
,所以PostConstruct
先執行。
而invokeInitMethods
方法中的程式碼:
決定了先呼叫InitializingBean
,再呼叫init-method
。
所以得出結論,他們的呼叫順序是:
自定義自己的Scope
我們都知道Spring
預設支援的Scope
只有兩種:
- singleton 單例,每次從Spring容器中獲取到的bean都是同一個物件。
- prototype 多例,每次從Spring容器中獲取到的bean都是不同的物件。
Spring web
又對Scope
進行了擴充套件,增加了:
- RequestScope 同一次請求從Spring容器中獲取到的bean都是同一個物件。
- SessionScope 同一個會話從Spring容器中獲取到的bean都是同一個物件。
即便如此,有些場景還是無法滿足我們的要求。
比如,我們想在同一個執行緒中從Spring容器獲取到的bean都是同一個物件,該怎麼辦?
這就需要自定義Scope
了。
- 實現
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;
}
}
- 將新定義的
Scope
注入到Spring容器中
@Component
public class ThreadLocalBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
beanFactory.registerScope("threadLocalScope", new ThreadLocalScope());
}
}
- 使用新定義的
Scope
@Scope("threadLocalScope")
@Service
public class CService {
public void add() {
}
}
FactoryBean
說起FactoryBean
就不得不提BeanFactory
,因為面試官老喜歡問它們的區別。
- BeanFactory:Spring容器的頂級介面,管理bean的工廠。
- FactoryBean:並非普通的工廠bean,它隱藏了例項化一些複雜Bean的細節,給上層應用帶來了便利。
Spring原始碼中有70多個地方在用FactoryBean介面。
上面這張圖足以說明該介面的重要性
提一句:
mybatis
的SqlSessionFactory
物件就是透過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,要如何處理呢?
- 定義一個實體
User
@Data
public class User {
private Long id;
private String name;
private Date registerDate;
}
- 實現
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;
}
}
- 將新定義的型別轉換器注入到Spring容器中
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new DateConverter());
}
}
- 呼叫介面
RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/save")
public String save(@RequestBody User user) {
return "success";
}
}
請求介面時User
物件中registerDate
欄位會被自動轉換成Date
型別。
Spring MVC攔截器
Spring MVC攔截器跟Spring攔截器相比,它裡面能夠獲取HttpServletRequest
和HttpServletResponse
等web物件例項。
Spring MVC攔截器的頂層介面是:HandlerInterceptor
,包含三個方法:
- preHandle 目標方法執行前執行
- postHandle 目標方法執行後執行
- afterCompletion 請求完成時執行
為了方便我們一般情況會用HandlerInterceptor介面的實現類HandlerInterceptorAdapter
類。
假如有許可權認證、日誌、統計的場景,可以使用該攔截器。
- 繼承
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;
}
}
- 將該攔截器註冊到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
方法中看到呼叫過程:
RestTemplate攔截器
我們使用RestTemplate
呼叫遠端介面時,有時需要在header
中傳遞資訊,比如:traceId,source等,便於在查詢日誌時能夠串聯一次完整的請求鏈路,快速定位問題。
這種業務場景就能透過ClientHttpRequestInterceptor
介面實現,具體做法如下:
- 實現
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);
}
}
- 定義配置類
@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
介面,它都搞定了。使用方式如下:
- Spring Boot專案啟動類上加
@EnableAsync
註解
@EnableAsync
@SpringBootApplication
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
}
}
- 在需要使用非同步的方法上加上
@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非同步的核心方法:
根據返回值不同,處理情況也不太一樣,具體分為如下情況:
Spring cache
Spring cache架構圖:
它目前支援多種快取:
這裡以caffeine
為例,它是Spring
官方推薦的。
- 引入
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>
- 配置
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;
}
}
- 使用
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
註解的屬性:
value
、cacheNames
- 這兩個引數其實是等同的( 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屬性是互斥的。
- 作用:用於指定key生成器。 若需要繫結一個自定義的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
有沒有遇到過這些問題:
- 某個功能需要根據專案中有沒有某個jar判斷是否開啟該功能。
- 某個bean的例項化需要先判斷另一個bean有沒有例項化,再判斷是否例項化自己。
- 某個功能是否開啟,在配置檔案中有個引數可以對它進行控制。
@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註解還有:ConditionalOnMissingClass
、ConditionalOnMissingBean
、ConditionalOnWebApplication
等。
整體認識一下@Conditional
家族:
自定義Conditional
Spring Boot自帶的Conditional系列已經可以滿足我們絕大多數的需求了。但如果你有比較特殊的場景,也可以自定義自定義Conditional。
- 自定義註解
@Conditional(MyCondition.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface MyConditionOnProperty {
String name() default "";
String havingValue() default "";
}
- 實現Condition介面
public class MyCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
System.out.println("實現自定義邏輯");
return false;
}
}
- 使用
@MyConditionOnProperty
註解
Conditional的奧秘就藏在ConfigurationClassParser
類的processConfigurationClass
方法中:
- 先判斷有沒有使用Conditional註解,如果沒有直接返回false
- 收集condition到集合中
- 按
order
排序該集合 - 遍歷該集合,迴圈呼叫
condition
的matchs
方法。
@Import
有時我們需要在某個配置類中引入另外一些類,被引入的類也加到Spring容器中。這時可以使用
@Import
註解完成這個功能。
引入的類支援三種不同型別:最好將普通類和@Configuration
註解的配置類分開講解,所以列了四種不同型別
這四種引入類的方式各有千秋,總結如下:
- 普通類,用於建立沒有特殊要求的bean例項。
@Configuration
註解的配置類,用於層層巢狀引入的場景。- 實現ImportSelector介面的類,用於一次性引入多個類的場景,或者可以根據不同的配置決定引入不同類的場景。
- 實現ImportBeanDefinitionRegistrar介面的類,主要用於可以手動控制BeanDefinition的建立和註冊的場景,它的方法中可以獲取BeanDefinitionRegistry註冊容器物件。
在ConfigurationClassParser
類的processImports
方法中可以看到這三種方式的處理邏輯:
最後的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
註解。
而@SpringBootConfiguration
註解,上面又加了@Configuration
註解,所以,Spring Boot啟動類本身帶有@Configuration
註解的功能。
@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介面的好處主要有以下兩點:
- 把某個功能的相關類,可以放到一起,方面管理和維護。
- 重寫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
。
AutoConfigurationImportSelector類實現了ImportSelector
介面。
並且重寫了selectImports(AnnotationMetadata importingClassMetadata)
方法,該方法會根據某些註解去找所有需要建立bean的類名,然後返回這些類名。其中在查詢這些類名之前,先呼叫isEnabled方法,判斷是否需要繼續查詢。
該方法會根據ENABLED_OVERRIDE_PROPERTY
的值來作為判斷條件。
而這個值就是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介面實現的:
@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
註解
- 定義ThreadPoolProperties類
@Data
@Component
@ConfigurationProperties("thread.pool")
public class ThreadPoolProperties {
private int corePoolSize;
private int maxPoolSize;
private int queueCapacity;
private int keepAliveSeconds;
private String threadNamePrefix;
}
- 使用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
方法完成的:
以上這段程式碼會遞迴繫結資料,主要考慮了三種情況:
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
,就能輕鬆搞定事務。而且一直用一直爽。
但如果使用不當,它也會坑人於無形。
事務不生效
訪問許可權問題
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方法不會生成事務。
如果有些場景,確實想在同一個類的某個方法中,呼叫它自己的另外一個方法,該怎麼辦?
- 第一種方式:新加一個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();
}
}
- 第二種方式:在該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種場景
- 第三種方式:透過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方法非常多,呼叫層級很深,而且有部分查詢方法比較耗時的話,會造成整個事務非常耗時,從而造成大事務問題。
程式設計式事務
上面這些內容都是基於@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
的程式設計式事務。主要原因如下:
- 避免由於Spring AOP問題,導致事務失效的問題。
- 能夠更小粒度的控制事務的範圍,更直觀。
提示
建議在專案中少使用
@Transactional
註解開啟事務。但並不是說一定不能用它,如果專案中有些業務邏輯比較簡單,而且不經常變動,使用@Transactional
註解開啟事務開啟事務也無妨,因為它更簡單,開發效率更高,但是千萬要小心事務失效的問題。
跨域問題
關於跨域問題,前後端的解決方案還是挺多的,這裡我重點說說Spring的解決方案,目前有三種:
使用@CrossOrigin
註解 和 實現WebMvcConfigurer
介面的方案,Spring在底層最終都會呼叫到DefaultCorsProcessor
類的handleInternal
方法
最終三種方案殊途同歸,都會往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.xm
l檔案中配置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
提示
透過這種
@Component
掃描註解的方式定義bean的前提是:需要先配置掃描路徑。
目前常用的配置掃描路徑的方式如下:
- 在
applicationContext.xml
檔案中使用<context:component-scan>
標籤。例如:
<context:component-scan base-package="com.zixq.cache" />
- 在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);
}
}
- 直接在
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。
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)
上有這樣一段描述:
翻譯:修改應用程式上下文的內部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 {
}
}
翻譯:在應用程式上下文的標準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
註解的定義:
從圖中可以看出該註解能夠使用在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物件收集到集合中。
@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
檔案中系統屬性的讀取。如果系統屬性包含中文字元,就會出現亂碼
如何解決亂碼問題?
目前主要有如下三種方案:
- 手動將ISO-8859-1格式的屬性值,轉換成UTF-8格式。
- 設定encoding引數,不過這個只對
@PropertySource
註解有用。 - 將中文字元用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
編碼。
提示
亂碼問題一般出現在本地環境,因為本地直接讀取的.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;
結果卻跟想象中不太一樣:
Set集合怎麼不是空的,而是包含了一個空字串的集合?
嗯。。。。。那我在:
號後加null
,總可以了吧?
@Value("#{'${zixq.test.set:null}'.split(',')}")
private Set<String> set;
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 + 括號
。
- 可以注入系統的路徑分隔符到path中
@Value("#{T(java.io.File).separator}")
private String path;
- 可以注入一個隨機數到randomValue中
@Value("#{T(java.lang.Math).random()}")
private double randomValue;
邏輯運算
透過上面介紹的內容,我們可以獲取到絕大多數類的變數和方法的值了。但有了這些值,還不夠,我們能不能在EL表示式中加點邏輯?
- 拼接字串
@Value("#{roleService.roleName + '' + roleService.DEFAULT_AGE}")
private String value;
- 邏輯判斷
@Value("#{roleService.DEFAULT_AGE > 16 and roleService.roleName.equals('蘇三')}")
private String operation;
- 三目運算
@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 中不得不瞭解的姿勢