从 CompletableFuture 线程中的主线程访问 SessionScoped bean 不起作用

Accessing SessionScoped bean from main thread within the CompletableFuture thread is not working

我是 Spring 引导开发的新手。我需要 运行 使用 CompletableFuture 并行执行一些任务,还需要从 CompletableFuture 线程中的主线程访问 SessionScoped bean。根据 blow 代码,当它尝试从 HelloService.completableFuture1() 调用 helloBean.getHelloMessage() 时,它会停止进一步处理。任何帮助将不胜感激。

SessionScopeWithCfApplication.java

@EnableAsync
@SpringBootApplication
public class SessionScopeWithCfApplication {

    public static void main(String[] args) {
        SpringApplication.run(SessionScopeWithCfApplication.class, args);
    }

}

=====

HelloBean.java

public class HelloBean {

    private String helloMessage;

    public String getHelloMessage() {
        return helloMessage;
    }

    public void setHelloMessage(String helloMessage) {
        this.helloMessage = helloMessage;
    }


}

=====

HelloBeanScopeConfig.java

@Configuration
public class HelloBeanScopeConfig {

    @Bean
    //@SessionScope
    //@Scope(value = "session",  proxyMode = ScopedProxyMode.TARGET_CLASS)
    @Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
    public HelloBean helloBean() {
        return new HelloBean();
    }

}

=====

HelloController.java

@Controller
public class HelloController {

    @Resource(name = "helloBean")
    HelloBean helloBean;
    
    @RequestMapping(value = {"/"}, method = RequestMethod.GET)
    public String home(Model model, HttpServletRequest request) {
        System.out.println("HelloController.home() - helloBean.getHelloMessage() = " + helloBean.getHelloMessage());
        helloBean.setHelloMessage("Welcome");
        System.out.println("HelloController.home() - helloBean.getHelloMessage() = " + helloBean.getHelloMessage());
    return "index";
    }
    
}

=====

index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Login</title>
    <link rel="stylesheet" type="text/css" th:href="@{/webjars/bootstrap/3.3.6/css/bootstrap.min.css}" />
        <script type='text/javascript' th:src="@{/webjars/jquery/1.9.1/jquery.min.js}"></script>
    <script type="text/javascript" th:src="@{/webjars/bootstrap/3.3.6/js/bootstrap.min.js}"></script>
    <script type='text/javascript'>
        function getHelloMessage() {
            return $.ajax({
                url: '/gethellomessage',
                method: 'get',
                contentType: "application/json; charset=utf-8",
            });
        };
        $(document).ready(function() {
            $('#btn').on('click', function () {
                getHelloMessage();
            }); 
        });
    </script>
</head>
<body>
    <div>
        <button id="btn" type="submit" class="btn btn-primary">Click Me</button>
    </div>
</body>
</html>

=====

HelloRestController.java

@RestController
public class HelloRestController {

    @Autowired
    HelloService helloService;
    
    @Resource(name = "helloBean")
    HelloBean helloBean;
    
    @RequestMapping(value = "/gethellomessage", method = RequestMethod.GET)
    public ResponseEntity getHelloMessage() {
    try {
            System.out.println("HelloRestController.getHelloMessage() - helloBean.getHelloMessage() = " + helloBean.getHelloMessage());
            helloService.completableFuture1();
            //CompletableFuture.allOf(helloService.completableFuture1()).get();
            return new ResponseEntity(HttpStatus.OK);
    } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
    }        
    }

}

=====

HelloService.java

@Service
public class HelloService {

    @Resource(name = "helloBean")
    HelloBean helloBean;

    @Async
    @Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
    public CompletableFuture<Void> completableFuture1() {
        System.out.println("executing completableFuture1 by - "+Thread.currentThread().getName());
    try {
            System.out.println("completableFuture1 - helloBean.getHelloMessage() = " + helloBean.getHelloMessage());
        Thread.sleep(5000);
            System.out.println("Done completableFuture1");
    } catch (Exception e) {
            throw new RuntimeException((new Exception().getStackTrace()[0].getMethodName()) + ": " + e.getClass().getSimpleName() + ": " + e.getMessage());
    }
        return CompletableFuture.completedFuture(null);
    }
    
}


Output:

HelloController.home() - helloBean.getHelloMessage() = null
HelloController.home() - helloBean.getHelloMessage() = Welcome
HelloRestController.getHelloMessage() - helloBean.getHelloMessage() = Welcome
executing completableFuture1 by - task-1

It is not printing value from HelloService.completableFuture1() for the below command and stops processing at this stage:
    
System.out.println("completableFuture1 - helloBean.getHelloMessage() = " + helloBean.getHelloMessage());

问题出在不正确的 bean 存储范围内,因为我收到以下异常消息:

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: In this case, 
use RequestContextListener or RequestContextFilter to expose the current request.

为什么会这样,所有带有请求、会话范围的 bean 都存储在 ThreadLocal classes 中。 bean存储很少,位于LocaleContextHolderRequestContextHolder,这些上下文与DispatcherServlet相关,servlet将执行initContextHolders和[=21]的每个请求=].

当请求到达服务器时,服务器从池中提供线程并且DispatcherServlet开始在该线程中执行请求,该线程从http会话接收会话范围的bean并创建请求范围的bean的新实例,以及所有这些bean如果这些组件在与 DispatcherServlet.

相同的线程中执行,则在任何 spring 组件(控制器、服务)中都可用

在我们的例子中,我们从正在执行 DispatcherServlet 的线程(主)启动新线程,这意味着,bean 存储已经为新线程更改,它是从 @Async 代理启动的用于执行我们的方法,因为线程直接与 ThreadLocal class 连接,因此在新线程中没有会话范围的 bean。

我们可以通过将 属性 setThreadContextInheritable 设置为 true 来设置 bean 存储的可继承性,方法如下:

@Configuration
public class WebConfig {

    @Bean
    @SessionScope
    public HelloBean helloBean() {
        return new HelloBean();
    }

    @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
    public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
        DispatcherServlet dispatcherServlet = new DispatcherServlet();
        dispatcherServlet.setThreadContextInheritable(true);
        dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
        dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
        dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
        dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
        dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
        return dispatcherServlet;
    }

}

然后稍微改变一下HelloService:

@Service
public class HelloService {

    @Autowired
    private HelloBean helloBean;

    @Async
    public CompletableFuture<Void> completableFuture1() {
        System.out.println("executing completableFuture1 by - " + Thread.currentThread().getName());
        try {
            System.out.println("completableFuture1 - helloBean.getHelloMessage() = " + helloBean.getHelloMessage());
            Thread.sleep(5000);
            System.out.println("Done completableFuture1");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return CompletableFuture.completedFuture(null);
    }

}

设置后 属性 所有新线程将从父线程继承 bean 存储,因此您的会话作用域 bean 将可用。 警告: 属性 您不能与线程池一起使用,请参阅 java 文档:

    /**
     * Set whether to expose the LocaleContext and RequestAttributes as inheritable
     * for child threads (using an {@link java.lang.InheritableThreadLocal}).
     * <p>Default is "false", to avoid side effects on spawned background threads.
     * Switch this to "true" to enable inheritance for custom child threads which
     * are spawned during request processing and only used for this request
     * (that is, ending after their initial task, without reuse of the thread).
     * <p><b>WARNING:</b> Do not use inheritance for child threads if you are
     * accessing a thread pool which is configured to potentially add new threads
     * on demand (e.g. a JDK {@link java.util.concurrent.ThreadPoolExecutor}),
     * since this will expose the inherited context to such a pooled thread.
     */
    public void setThreadContextInheritable(boolean threadContextInheritable) {
        this.threadContextInheritable = threadContextInheritable;
    }

P.S. 您可以更改代码段:

    @Resource(name = "helloBean")
    private HelloBean helloBean;

    @Autowired
    private HelloBean helloBean;

Spring 支持两种注入 bean 的方式,从我的角度来看,第二个代码片段更 spring 风格。