Spring進階篇(8)-BeanPostProcessor的注冊時機

JAVA && Spring && SpringBoot2.x — 學習目錄

在項目中允趟,我們可以將BeanPostProcessor注冊到IOC容器中涣楷,有什么注意事項呢?

1. 問題起源:

在shiro中,若配置LifecycleBeanPostProcessor后弧蝇,可能會導致一些類不能支持事務等代理功能去枷。

@Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

注:LifecycleBeanPostProcessor是Bean的后置處理器,shiro去控制Bean的生命周期碾阁。但不推薦在配置中加入上述配置罪帖,因為Bean的生命周期一般由Spring去控制。不推薦交由shiro進行控制邮屁!

事務失效場景復現(xiàn):

@Configuration
public class ShiroConfig {
    //配置過濾器鏈
    @Bean("shiroFilterBean")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,JwtAuthcFilter jwtAuthcFilter,JwtPermissionFilter jwtPermissionFilter,Ini.Section section) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        ...
        return shiroFilterFactoryBean;
    }
    //配置shiro生命周期后處理器
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
}

之后整袁,我們發(fā)現(xiàn)ShiroFilterFactoryBean依賴的Bean,事務等功能全部失效(即依賴的Bean不是代理對象)佑吝。

2. 問題分析

BeanPostProcessor本質(zhì)上也是一個Bean對象坐昙,但是它的初始化時機會早于普通Bean對象。實際上Bean的生命周期大體可分為:

  1. new 對象(反射)芋忿。
  2. 屬性依賴注入炸客。
  3. 調(diào)用生命周期回調(diào)方法疾棵。
  4. 完成AOP代理。

而實際上無論是屬性的依賴注入還是完成AOP代理實際上均是在后置處理器BeanPostProcessor中進行統(tǒng)一處理的痹仙。

若普通Bean初始化時是尔,BeanPostProcessor沒有全部初始化,那么可能會造成普通Bean對象功能上的缺失蝶溶。在Spring啟動過程中,會拋出如下警告:

//沒有得到所有后置處理器的處理宣渗,例如缺失了自動代理后置處理器處理抖所。
Bean 'accountImpl' of type [com.tellme.Impl.AccountImpl] is not eligible for getting 
processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

但是有些同學會問,在自定義BeanPostProcessor時依賴普通Bean痕囱,可能會造成自動代理失效田轧。但"問題起源"中僅僅引入了shiro定義的LifecycleBeanPostProcessor,為什么還會出現(xiàn)上述問題鞍恢?

3. 問題原因

在Spring源碼中傻粘,注冊BeanPostProcessor時,會調(diào)用getBean()方法獲取Bean對象帮掉。

//源碼位置:org.springframework.context.support.PostProcessorRegistrationDelegate#registerBeanPostProcessors(org.springframework.beans.factory.config.ConfigurableListableBeanFactory, org.springframework.context.support.AbstractApplicationContext)
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);

在獲取Bean對象時弦悉,遍歷所有的BeanDefinition,來獲取對應的Bean蟆炊。若容器中存在FactoryBean時稽莉,會檢驗FactoryBean創(chuàng)建的Bean類型,而不是FactoryBean的類型涩搓。故檢測FactoryBean類型時污秆,可能會將普通Bean實例化。

//核心源碼:
1. org.springframework.beans.factory.support.DefaultListableBeanFactory#doGetBeanNamesForType
2. org.springframework.beans.factory.support.AbstractBeanFactory#isTypeMatch(java.lang.String, org.springframework.core.ResolvableType)

LifecycleBeanPostProcessor是在@Configuration中進行注冊的昧甘。并且注冊時@Configuration容器中也包含了一個FactoryBean對象(ShiroFilterFactoryBean)良拼。

這樣會導致注冊Ordered級別的BeanPostProcessor調(diào)用getBean()方法時,會將ShiroFilterFactoryBean以及其依賴的屬性進行初始化充边,失去同級別的后置處理器的處理庸推。

總結(jié):BeanPostProcessor@Configuration容器過早的初始化。于是在注冊后置處理器浇冰,會調(diào)用getBean()導致FactoryBean過早被初始化予弧。

4. 問題復現(xiàn)

自定義一個PriorityOrdered級別的后置處理器,啟動項目湖饱,查看Bean的初始化時機掖蛤。

  1. UserServiceBeanFactoryBean并使用@Service注冊到容器;
@Service
public class UserServiceBean implements FactoryBean {
    @Autowired
    private AcService acService;  //包含一個普通Bean井厌,查看給普通Bean的初始化時機
    public UserServiceBean() {
        System.out.println("UserServiceBean 的構造方法..");
    }
    @Override
    public Object getObject() throws Exception {
        return "";
    }
    @Override
    public Class<?> getObjectType() {
        return Object.class;
    }
}
  1. aConfig是一個@Configuration類蚓庭,其中包含了
    2.1 PersonServiceBean一個FactoryBean致讥;
    2.2 MyBeanPostProcessor是一個PriorityOrdered級別的后置處理器;
    2.3 AcService一個普通的Service類器赞;
@Configuration
public class aConfig {
    public aConfig() {
        System.out.println("aConfig 構造函數(shù)...");
    }
    @Bean
    public PersonServiceBean personService(IAccount account) {
        PersonServiceBean personServiceBean = new PersonServiceBean();
        personServiceBean.setAccount(account);
        return personServiceBean;
    }
    @Bean
    public MyBeanPostProcessor myBeanPostProcessor() {
        return new MyBeanPostProcessor();
    }
}
//后置處理器
public class MyBeanPostProcessor implements BeanPostProcessor, PriorityOrdered {
    public MyBeanPostProcessor() {
        System.out.println("MyBeanPostProcessor 初始化...");
    }
    //低優(yōu)先級
    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
}
//FactoryBean類
public class PersonServiceBean implements FactoryBean {
    private IAccount account;
    public IAccount getAccount() {
        return account;
    }
    public PersonServiceBean() {
        System.out.println("PersonServiceBean 構造方法...");
    }
    public void setAccount(IAccount account) {
        this.account = account;
    }
    @Override
    public Object getObject() throws Exception {
        return "";
    }
    @Override
    public Class<?> getObjectType() {
        return Object.class;
    }
}
  1. bConfig是一個@Configuration類垢袱,包含了一個SomeServiceBean
//FactoryBean類
public class SomeServiceBean implements FactoryBean {
    public SomeServiceBean() {
        System.out.println("SomeServiceBean 的構造方法");
    }
    @Override
    public Object getObject() throws Exception {
        return "";
    }
    @Override
    public Class<?> getObjectType() {
        return Object.class;
    }
}

執(zhí)行結(jié)果

UserServiceBean 的構造方法..   (使用@Service注解FactoryBean)
aConfig 構造函數(shù)...            (帶有后置處理器的@Configuration容器港柜,容器先初始化请契,才會初始化后置處理器。)
MyBeanPostProcessor 初始化...  (PriorityOrdered級別的后置處理器)
AccountImpl 的構造方法...      (FactoryBean所依賴的普通Bean夏醉,accountImpl是方法參數(shù)爽锥,先初始化它,才能進行FactoryBean的構造)
PersonServiceBean 構造方法...  (FactoryBean的構造方法畔柔。)

bConfig 構造方法...            (普通的@Configuration容器)
SomeServiceBean 的構造方法     (普通容器中的FactoryBean方法)
AcService 的構造方法...        (帶有@Service的普通Bean)

可以看到氯夷,MyBeanPostProcessor導致了aConfig過早的初始化。在注冊Ordered級別的BeanPostProcessor時靶擦,遍歷所有的BeanDefinition對象腮考,導致aConfigPersonServiceBean被初始化。而PersonServiceBean在初始化之前玄捕,要先初始化方法參數(shù)踩蔚,即將AccountImpl提前初始化。

而實際上枚粘,若是@Service注冊的FactoryBean寂纪,其依賴的屬性AcService也不會過早的被初始化。

5. 歸納總結(jié)

我們可以先看下SpringBoot2.x如何去注冊后置處理器的赌结。

  1. 注冊@Validated后置處理器

源碼:org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration

public class ValidationAutoConfiguration {
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    @ConditionalOnMissingBean(Validator.class)
    public static LocalValidatorFactoryBean defaultValidator() {
        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
        factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
        return factoryBean;
    }
    @Bean
    @ConditionalOnMissingBean
    public static MethodValidationPostProcessor methodValidationPostProcessor(
            Environment environment, @Lazy Validator validator) {
        MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
        boolean proxyTargetClass = environment
                .getProperty("spring.aop.proxy-target-class", Boolean.class, true);
        processor.setProxyTargetClass(proxyTargetClass);
        processor.setValidator(validator);
        return processor;
    }
}

MethodValidationPostProcessor和其他Bean在一個容器中捞蛋,那么注冊時使用static方法。

  1. 注冊@Async 后置處理器:

源碼:org.springframework.scheduling.annotation.ProxyAsyncConfiguration

public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration {

    @Bean(name = TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME)
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public AsyncAnnotationBeanPostProcessor asyncAdvisor() {
        Assert.notNull(this.enableAsync, "@EnableAsync annotation metadata was not injected");
        AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor();
        bpp.configure(this.executor, this.exceptionHandler);
        Class<? extends Annotation> customAsyncAnnotation = this.enableAsync.getClass("annotation");
        if (customAsyncAnnotation != AnnotationUtils.getDefaultValue(EnableAsync.class, "annotation")) {
            bpp.setAsyncAnnotationType(customAsyncAnnotation);
        }
        bpp.setProxyTargetClass(this.enableAsync.getBoolean("proxyTargetClass"));
        bpp.setOrder(this.enableAsync.<Integer>getNumber("order"));
        return bpp;
    }
}

將后置處理器單獨放在了一個容器中柬姚。

總結(jié):

BeanPostProcessor的注冊需要注意時機拟杉,防止將@Configuration容器中過早的初始化,造成一些Bug量承。根據(jù)源碼來講搬设,一般推薦兩種方式:

  1. BeanPostProcessor單獨的放在一個@Configuration容器中;
  2. 若與其他Bean共用一個@Configuration容器撕捍,推薦使用static方法注冊后置處理器拿穴;
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市忧风,隨后出現(xiàn)的幾起案子默色,更是在濱河造成了極大的恐慌,老刑警劉巖狮腿,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腿宰,死亡現(xiàn)場離奇詭異呕诉,居然都是意外死亡,警方通過查閱死者的電腦和手機吃度,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門甩挫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人椿每,你說我怎么就攤上這事伊者。” “怎么了间护?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵亦渗,是天一觀的道長。 經(jīng)常有香客問我兑牡,道長央碟,這世上最難降的妖魔是什么税灌? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任均函,我火速辦了婚禮,結(jié)果婚禮上菱涤,老公的妹妹穿的比我還像新娘苞也。我一直安慰自己,他們只是感情好粘秆,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布如迟。 她就那樣靜靜地躺著,像睡著了一般攻走。 火紅的嫁衣襯著肌膚如雪殷勘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天昔搂,我揣著相機與錄音玲销,去河邊找鬼。 笑死摘符,一個胖子當著我的面吹牛贤斜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播逛裤,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼瘩绒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了带族?” 一聲冷哼從身側(cè)響起锁荔,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蝙砌,沒想到半個月后堕战,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體坤溃,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年嘱丢,在試婚紗的時候發(fā)現(xiàn)自己被綠了薪介。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡越驻,死狀恐怖汁政,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情缀旁,我是刑警寧澤记劈,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站并巍,受9級特大地震影響目木,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜懊渡,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一刽射、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧剃执,春花似錦誓禁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至怒见,卻和暖如春俗慈,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背遣耍。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工闺阱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人配阵。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓馏颂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親棋傍。 傳聞我的和親對象是個殘疾皇子救拉,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355