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);

至此,问题得以解决。

展开阅读全文

没有更多推荐了,返回首页