最近做的一个项目,其一需要用到Spring 的oauth认证功能, 其二需要对spring 的ContextRefreshedEvent 这个事件进行监听,实现一部分自定义注解的功能(具体功能不作赘述),本来以为毫不相关的两个功能,却出现了一些意料之外的异常。下面是一些具体的异常排查过程以及最终的解决方案,若有部分理解错误或描述错误,欢迎指正(自创文章,如需转载请说明出处)。
/**
* @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 就能满足需求了
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 lazy-init singletons and
* objects created by FactoryBeans (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);
至此,问题得以解决。