1. 程式人生 > >Spring boot 整合Spring Security過程中的出現的關於Session scope的異常排查及解決方案

Spring boot 整合Spring Security過程中的出現的關於Session scope的異常排查及解決方案

背景介紹

最近做的一個專案,其一需要用到Spring 的oauth認證功能, 其二需要對spring 的ContextRefreshedEvent 這個事件進行監聽,實現一部分自定義註解的功能(具體功能不作贅述),本來以為毫不相關的兩個功能,卻出現了一些意料之外的異常。下面是一些具體的異常排查過程以及最終的解決方案,若有部分理解錯誤或描述錯誤,歡迎指正(自創文章,如需轉載請說明出處)。

場景

  1. 引入Spring Security Oauth2,通過 @EnableOAuthClient 註解啟用所需oauth認證功能
  2. 監聽ContextRefreshedEvent事件,從ApplicationContext中獲取所有的bean names並根據相應的bean name獲取到bean,具體程式碼如下:
/**
 * @author Lanny Yao
 * @date 8/30/2018 9:58 AM
 */
@Component
public class Listener implements ApplicationListener<ContextRefreshedEvent> {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        ApplicationContext context = event.getApplicationContext();
        String[] beanNames = context.getBeanNamesForType(Object.class);

        for
(String beanName : beanNames){ Object bean = context.getBean(beanName); ... } } }

啟動程式,丟擲異常

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.oauth2ClientContext': Scope 'session' is not active for the current thread; consider defining a scoped proxy for
this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request. at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:362) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE] at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1089) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE] at com.lanny.blog.demo.seesionexception.Listener.onApplicationEvent(Listener.java:21) ~[classes/:na] at com.lanny.blog.demo.seesionexception.Listener.onApplicationEvent(Listener.java:12) ~[classes/:na] at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE] at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE] at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE] at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:400) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE] at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:354) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE] at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:888) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE] at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.finishRefresh(ServletWebServerApplicationContext.java:161) ~[spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:553) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE] at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:140) ~[spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE] at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:762) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE] at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:398) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:330) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1258) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1246) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE] at com.lanny.blog.demo.seesionexception.SeesionexceptionApplication.main(SeesionexceptionApplication.java:14) [classes/:na] Caused by: java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request. at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131) ~[spring-web-5.0.8.RELEASE.jar:5.0.8.RELEASE] at org.springframework.web.context.request.SessionScope.get(SessionScope.java:55) ~[spring-web-5.0.8.RELEASE.jar:5.0.8.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:350) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE] ... 19 common frames omitted

排查過程

起初一看,完全不知所云啊, 不能建立bean的異常倒是經常看到,但是明明是在getBean(),怎麼還影響到了oauth2ClientContext 這個bean的建立了呢?看下面spring 的原始碼(位於AbstractBeanFactory 中的doGetBean()方法中),發現在根據scope和beanName獲取相應bean的時候會有一個create Bean的操作,所以也就印證了上面說的問題。

try {
    Object scopedInstance = scope.get(beanName, () -> {
        beforePrototypeCreation(beanName);
        try {
            return createBean(beanName, mbd, args);
        }
        finally {
            afterPrototypeCreation(beanName);
        }
    });
    bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
    }
    catch (IllegalStateException ex) {
        throw new BeanCreationException(beanName,
                "Scope '" + scopeName + "' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton", ex);
    }
}

既然是oauth相關的bean,罪魁禍首肯定是@EnableOAuth2Client註解了,果不其然,註釋掉該註解之後程式可以正常啟動,而且試了幾種其他的監聽方式,發現只要用到了ApplicationContext 和這個註解,就會報錯。你倆到底誰的鍋,我來找找。
先看看這個oauth2ClientContext bean是在哪裡定義的,全域性搜尋一下,發現一個程式碼片段,喲嗬,確實是被標記為“session” scope的,那麼問題來了,是不是擁有“session”這個scope的bean都會出現這個異常呢

@Bean
@Scope(value = "session", proxyMode = ScopedProxyMode.INTERFACES)
public OAuth2ClientContext oauth2ClientContext() {
    return new DefaultOAuth2ClientContext(accessTokenRequest);
}

try catch 一下:

Error bean -> [scopedTarget.oauth2ClientContext], caused by:Error creating bean with name 'scopedTarget.oauth2ClientContext': Scope 'session' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
Error bean -> [scopedTarget.accessTokenRequest], caused by:Error creating bean with name 'scopedTarget.accessTokenRequest': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.

發現還有另外一個bean accessTokenRequest也出現問題了,這麼看來不只是session scope, request scope的bean也出了問題,如此一來弄清楚scope的意義就變成首要任務了:

spring中bean的scope屬性,有如下5種類型:

singleton 表示在spring容器中的單例,通過spring容器獲得該bean時總是返回唯一的例項
prototype表示每次獲得bean都會生成一個新的物件
request表示在一次http請求內有效(只適用於web應用)
session表示在一個使用者會話內有效(只適用於web應用)
globalSession表示在全域性會話內有效(只適用於web應用)
在多數情況,我們只會使用singleton和prototype兩種scope,如果在spring配置檔案內未指定scope屬性,預設為singleton。

(摘自https://www.cnblogs.com/wgbs25673578/p/5617700.html)

可以清楚的看到session 和request 的scope只適用於web應用,生命週期取決於http請求和session過期時間,所以通過Spring 上下文也就是ApplicationContext獲取bean時,當beanName對應的bean的scope是“session”或者“request”之類時,其實是不允許直接建立的.所以到這裡,異常出現的根本原因已經找到,所以程式碼裡面需要做的就是: 過濾scope !

很慶幸,ApplicationContext本身就提供了方法判斷scope,但是隻能判斷“singleton” 和“prototype”型別的:

if (context.isPrototype(beanName) || context.isSingleton(beanName))

ok,過濾完成,執行一下,WTF!加個判斷條件,又給我出新的異常,讓不讓人活了!!!

2018-08-30 13:54:04.396  INFO 18784 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2018-08-30 13:54:04.437 ERROR 18784 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

A component required a bean named 'autoConfigurationReport' that could not be found.


Action:

Consider defining a bean named 'autoConfigurationReport' in your configuration.

這個更懵逼,感覺更加的毫不相關,研究原始碼之下發現scope 是singleton的bean是不能去調isPrototype()方法的,呼叫後會出現這個異常,直接導致JVM掛掉,比之前的異常更加暴力。關於這一點沒有去深究,不知道是出於什麼策略會有這樣的設計,還是說是因為其他的一些原因。其實沒有特殊需求的情況下,工程專案下的自定義的所有的bean都預設scope是singleton的,所以,只需要找出singleton的bean 就能滿足需求了

解決方案

  1. context.isSingleton(beanName) 直接通過這個方法做判斷找出所有的singleton的bean,但是如上所述這個方法存在風險
  2. 可以看到程式碼裡是通過 String[] beanNames = context.getBeanNamesForType(Object.class);獲取到的所有的bean name,對於這個方法,其實有兩個引數可以使用,這是解決這個問題的關鍵,把第二個引數設定成false,就可以只取scope為singleton的bean了,第三個引數根據實際情況設定,我這裡直接設為true。
String[] beanNames = context.getBeanNamesForType(Object.class,false,true);
* @param type the class or interface to match, or {@code null} for all bean names
     * @param includeNonSingletons whether to include prototype or scoped beans too
     * or just singletons (also applies to FactoryBeans)
     * @param allowEagerInit whether to initialize <i>lazy-init singletons</i> and
     * <i>objects created by FactoryBeans</i> (or by factory methods with a
     * "factory-bean" reference) for the type check. Note that FactoryBeans need to be
     * eagerly initialized to determine their type: So be aware that passing in "true"
     * for this flag will initialize FactoryBeans and "factory-bean" references.
     * @return the names of beans (or objects created by FactoryBeans) matching
     * the given object type (including subclasses), or an empty array if none
     * @see FactoryBean#getObjectType
     * @see BeanFactoryUtils#beanNamesForTypeIncludingAncestors(ListableBeanFactory, Class, boolean, boolean)
     */
    String[] getBeanNamesForType(@Nullable Class<?> type, boolean includeNonSingletons, boolean allowEagerInit);

至此,問題得以解決。