Spring boot 整合Spring Security過程中的出現的關於Session scope的異常排查及解決方案
背景介紹
最近做的一個專案,其一需要用到Spring 的oauth認證功能, 其二需要對spring 的ContextRefreshedEvent 這個事件進行監聽,實現一部分自定義註解的功能(具體功能不作贅述),本來以為毫不相關的兩個功能,卻出現了一些意料之外的異常。下面是一些具體的異常排查過程以及最終的解決方案,若有部分理解錯誤或描述錯誤,歡迎指正(自創文章,如需轉載請說明出處)。
場景
- 引入Spring Security Oauth2,通過 @EnableOAuthClient 註解啟用所需oauth認證功能
- 監聽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 就能滿足需求了
解決方案
- context.isSingleton(beanName) 直接通過這個方法做判斷找出所有的singleton的bean,但是如上所述這個方法存在風險
- 可以看到程式碼裡是通過 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);
至此,問題得以解決。