Ho un controller che mi piacerebbe essere unico per sessione. Secondo la documentazione di spring ci sono due dettagli per l’implementazione:
1. Configurazione web iniziale
Per supportare l’ambito dei bean a richiesta, sessione e livelli di sessione globali (fagioli con ambito Web), è richiesta una configurazione iniziale minore prima di definire i bean.
Ho aggiunto quanto segue al mio web.xml
come mostrato nella documentazione:
org.springframework.web.context.request.RequestContextListener
2. Fagioli scoperti come dipendenze
Se si desidera iniettare (ad esempio) un bean con scope della richiesta HTTP in un altro bean, è necessario iniettare un proxy AOP al posto del bean con scope.
Ho annotato il bean con @Scope
fornendo il proxyMode
come mostrato di seguito:
@Controller @Scope(value="session", proxyMode=ScopedProxyMode.TARGET_CLASS) public class ReportBuilder implements Serializable { ... ... }
Problema
Nonostante la configurazione di cui sopra, ottengo la seguente eccezione:
org.springframework.beans.factory.BeanCreationException: errore nella creazione di bean con nome ‘scopedTarget.reportBuilder’: Scope ‘session’ non è attivo per il thread corrente; considerare la definizione di un proxy con scope per questo bean se si intende fare riferimento ad esso da un singleton; l’eccezione annidata è java.lang.IllegalStateException: Nessuna richiesta legata al thread trovata: ti stai riferendo alla richiesta di attributi al di fuori di una richiesta web effettiva o all’elaborazione di una richiesta al di fuori del thread di ricezione originale? Se si sta effettivamente operando all’interno di una richiesta Web e si continua a ricevere questo messaggio, il codice probabilmente è in esecuzione all’esterno di DispatcherServlet / DispatcherPortlet: In questo caso, utilizzare RequestContextListener o RequestContextFilter per esporre la richiesta corrente.
Aggiornamento 1
Di seguito è la mia scansione dei componenti. Ho il seguente in web.xml
:
contextClass org.springframework.web.context.support.AnnotationConfigWebApplicationContext contextConfigLocation org.example.AppConfig org.springframework.web.context.ContextLoaderListener
E il seguente in AppConfig.java
:
@Configuration @EnableAsync @EnableCaching @ComponentScan("org.example") @ImportResource("classpath:applicationContext.xml") public class AppConfig implements AsyncConfigurer { ... ... }
Aggiornamento 2
Ho creato un caso di test riproducibile. Questo è un progetto molto più piccolo, quindi ci sono differenze, ma succede lo stesso errore. Ci sono parecchi file, quindi l’ho caricato come tar.gz
su megafileupload .
Il problema non è nelle annotazioni Spring, ma nel modello di progettazione. Mescoli insieme diversi ambiti e thread:
Il singleton è disponibile ovunque, è ok. Tuttavia, l’ambito sessione / richiesta non è disponibile al di fuori di un thread allegato a una richiesta.
Il lavoro asincrono può essere eseguito anche se la richiesta o la sessione non esiste più, quindi non è ansible utilizzare un bean dipendente da richiesta / sessione. Inoltre non c’è modo di sapere, se stai facendo un lavoro in un thread separato, quale thread è la richiesta di originator (significa aop: proxy non è utile in questo caso).
Penso che il tuo codice abbia l’aspetto di un contratto tra ReportController, ReportBuilder, UselessTask e ReportPage. C’è un modo per usare solo una semplice class (POJO) per memorizzare i dati da UselessTask e leggerli in ReportController o ReportPage e non utilizzare più ReportBuilder ?
Se qualcun altro si è bloccato sullo stesso punto, in seguito ha risolto il mio problema.
Nel web.xml
org.springframework.web.context.request.RequestContextListener
Nel componente Session
@Component @Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
In pom.xml
cglib cglib 3.1
Sto rispondendo alla mia domanda perché fornisce una migliore panoramica della causa e delle possibili soluzioni. Ho assegnato il bonus a @Martin perché ha indicato la causa.
Causa
Come suggerito da @ Martin, la causa è l’uso di più thread. L’object richiesta non è disponibile in questi thread, come indicato nella Guida di spring :
DispatcherServlet
,RequestContextListener
eRequestContextFilter
fanno esattamente la stessa cosa, ovvero legano l’object di richiesta HTTP al thread che sta servendo quella richiesta. Ciò rende i bean con scope di richiesta e di sessione disponibili più in basso nella catena di chiamate.
Soluzione 1
È ansible rendere l’object di richiesta disponibile per altri thread, ma pone un paio di limitazioni sul sistema, che potrebbero non essere utilizzabili in tutti i progetti. Ho ottenuto questa soluzione dall’accesso ai bean con scope della richiesta in un’applicazione web multi-thread :
Sono riuscito a risolvere questo problema. Ho iniziato a utilizzare
SimpleAsyncTaskExecutor
invece diWorkManagerTaskExecutor
/ThreadPoolExecutorFactoryBean
. Il vantaggio è cheSimpleAsyncTaskExecutor
non utilizzerà mai i thread. Questa è solo la metà della soluzione. L’altra metà della soluzione consiste nell’utilizzare unRequestContextFilter
anzichéRequestContextListener
.RequestContextFilter
(così comeDispatcherServlet
) ha una proprietàthreadContextInheritable
che consente fondamentalmente ai thread figlio di ereditare il contesto padre.
Soluzione 2
L’unica altra opzione è usare il bean con scope di sessione all’interno del thread di richiesta. Nel mio caso questo non è stato ansible perché:
@Async
; Come da documentazione :
Se si accede ai bean con scope in Spring Web MVC, ovvero all’interno di una richiesta elaborata da Spring DispatcherServlet o DispatcherPortlet, non è necessaria alcuna installazione speciale: DispatcherServlet e DispatcherPortlet espongono già tutti gli stati pertinenti.
Se si esegue all’esterno di Spring MVC (Non elaborato da DispatchServlet), è necessario utilizzare RequestContextListener
non solo ContextLoaderListener
.
Aggiungi quanto segue nel tuo web.xml
org.springframework.web.context.request.RequestContextListener
Ciò fornirà la sessione a Spring per mantenere i bean in tale ambito
Aggiornamento: Come per altre risposte, @Controller
sensibile solo quando ci si trova in Spring MVC Context, quindi @Controller non sta servendo lo scopo reale nel codice. Tuttavia è ansible iniettare i bean in qualsiasi posizione con ambito di sessione / ambito di richiesta (non è necessario Spring MVC / Controller per iniettare i bean in un ambito particolare).
Aggiornamento: RequestContextListener espone la richiesta solo al thread corrente.
Hai creato ReportBuilder in due punti
1. ReportPage
: è ansible vedere Spring ha iniettato correttamente il generatore di report in quanto siamo ancora nella stessa web thread. Ho cambiato l’ordine del tuo codice per assicurarmi che il ReportBuilder sia stato iniettato in ReportPage come questo.
log.info("ReportBuilder name: {}", reportBuilder.getName()); reportController.getReportData();
Sapevo che il registro dovrebbe andare dopo secondo la tua logica, solo per scopo di debug che ho aggiunto.
2. UselessTasklet
– Abbiamo ottenuto un’eccezione, qui perché questo è diverso thread creato da Spring Batch, in cui la richiesta non è esposta da RequestContextListener
.
Dovresti avere una logica diversa per creare e iniettare ReportBuilder
istanza di ReportBuilder
su Spring Batch (May Spring Batch Parameters e using Future
puoi tornare per futuro riferimento)
https://stackoverflow.com/a/30640097/2569475
Per questo problema controlla la mia risposta nell’url sopra indicato
Utilizzo di un bean con ambito richiesta all’esterno di una richiesta Web effettiva
La mia risposta si riferisce a un caso particolare del problema generale descritto dall’OP, ma lo aggiungerò nel caso in cui aiuti qualcuno a uscire.
Quando si utilizza @EnableOAuth2Sso
, Spring inserisce un OAuth2RestTemplate
nel contesto dell’applicazione e questo componente assume OAuth2RestTemplate
legate al thread associate a thread.
Il mio codice ha un metodo async pianificato che utilizza un RestTemplate RestTemplate
. Questo non è in esecuzione all’interno di DispatcherServlet
, ma Spring stava iniettando OAuth2RestTemplate
, che ha prodotto l’errore che l’OP descrive.
La soluzione era fare un’iniezione basata sul nome. Nella configurazione di Java:
@Bean public RestTemplate pingRestTemplate() { return new RestTemplate(); }
e nella class che lo usa:
@Autowired @Qualifier("pingRestTemplate") private RestTemplate restTemplate;
Ora Spring inietta il RestTemplate
, progettato e privo di RestTemplate
.
Devi solo definire nel bean in cui hai bisogno di un ambito diverso rispetto all’ambito Singleton predefinito eccetto il prototipo. Per esempio: