Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring: Inject bean depended on context (session/web or local thread/background process)

Is it possible to create a factory or proxy that can decide if thread is running in (Web)Request or background-process (ie. scheduler) and then depending on that information, it creates a session bean or a prototype bean?

Example (pseudo Spring config :)

<bean id="userInfoSession" scope="session" />
<bean id="userInfoStatic" scope="prototype" />

<bean id="currentUserInfoFactory" />

<bean id="someService" class="...">
    <property name="userInfo" ref="currentUserInfoFactory.getCurrentUserInfo()" />
</bean>

I hope this makes my question easier to understand...


My Solution

It's never to late to update own questions ;). I solved it with two different instances of client session, one SessionScoped client session and one SingletonScoped session. Both are normal beans.

<bean id="sessionScopedClientSession" class="com.company.product.session.SessionScopedClientSession" scope="session">
    <aop:scoped-proxy />
</bean>

<bean id="singletonScopedClientSession" class="com.company.product.session.SingletonScopedClientSession" />

<bean id="clientSession" class="com.company.product.session.ClientSession">
    <property name="sessionScopedClientSessionBeanName" value="sessionScopedClientSession" />
    <property name="singletonScopedClientSessionBeanName" value="singletonScopedClientSession" />
</bean>

The ClientSession will then decide if singleton or session scope:

private IClientSession getSessionAwareClientData() {
    String beanName = (isInSessionContext() ? sessionScopedClientSessionBeanName : singletonScopedClientSessionBeanName);
    return (IClientSession) ApplicationContextProvider.getApplicationContext().getBean(beanName);
}

Where session type could be gathered through this:

private boolean isInSessionContext() {
    return RequestContextHolder.getRequestAttributes() != null;
}

All the classes implement a interface called IClientSession. Both singletonScoped and sessionScoped beans extends from a BaseClientSession where the implementation is found.

Every service then can use the client session ie:

@Resource
private ClientSession clientSession;

    ...

public void doSomething() {
    Long orgId = clientSession.getSomethingFromSession();
}

Now if we go one step further we can write something like a Emulator for the session. This could be done by initializing the clientSession (which is in no context of a request) the singleton session. Now all services can use the same clientSession and we still can "emulate" a user ie:

        clientSessionEmulator.startEmulateUser( testUser );
        try {
            service.doSomething();
        } finally {
            clientSessionEmulator.stopEmulation();
        }

One more advice: take care about threading in SingletonScoped clientSession instance! Wouw, I thought I could do it with less lines ;) If you like to know more about this approach feel free to contact me.

like image 738
Frank Szilinski Avatar asked Oct 15 '10 07:10

Frank Szilinski


2 Answers

I created small universal workaround to inject beans depends on context.

Guess we have two beans:

<bean class="xyz.UserInfo" id="userInfo" scope="session" />
<bean class="xyz.UserInfo" id="userInfoSessionLess" />

We want to use "userInfo" bean for web user actions and "userInfoSessionLess" bean for background services for example. Wa also want to write code and don't want to think about context, for example:

@Autowired
//You will get "java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request?" for session less services.
//We can fix it and autowire "userInfo" or "userInfoSessionLess" depends on context...
private UserInfo userInfo;

public save(Document superSecureDocument) {
    ...
    superSecureDocument.lastModifier = userInfo.getUser();
    ...
}

Now we need create custom session scope to make it worked:

public class MYSessionScope extends SessionScope implements ApplicationContextAware {
  private static final String SESSION_LESS_POSTFIX = "SessionLess";
  private ApplicationContext applicationContext;
  public Object get(String name, ObjectFactory objectFactory) {
    if (isInSessionContext()) {
      log.debug("Return session Bean... name = " + name);
      return super.get(name, objectFactory);
    } else {
      log.debug("Trying to access session Bean outside of Request Context... name = " + name + " return bean with name = " + name + SESSION_LESS_POSTFIX);
      return applicationContext.getBean(name.replace("scopedTarget.", "") + SESSION_LESS_POSTFIX);
    }
  }
  private boolean isInSessionContext() {
    return RequestContextHolder.getRequestAttributes() != null;
  }
  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    this.applicationContext = applicationContext;
  }
}

Register new scope:

<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
    <property name="scopes">
        <map>
            <entry key="mySession">
                <bean class="com.galantis.gbf.web.MYSessionScope" />
            </entry>
        </map>
    </property>
</bean>

Now we need modify beans definions like this:

<bean class="xyz.UserInfo" id="userInfo" scope="mySession" autowire-candidate="true"/>
<bean class="xyz.UserInfo" id="userInfoSessionLess" autowire-candidate="false"/>

That's all. Bean with name "SessionLess" will be used for all "mySession" scoped beans if we use bean outside of actual web request thread.

like image 156
LexeY4eg Avatar answered Oct 05 '22 08:10

LexeY4eg


Your rephrase is indeed considerably simpler :)

Your currentUserInfoFactory could make use of RequestContextHolder.getRequestAttributes(). If a session is present and associated with the calling thread, then this will return a non-null object, and you can then safely retrieve the session-scoped bean from the context. If it returns a null, then you should fetch the prototype-scoped bean instead.

It's not very neat, but it's simple, and should work.

like image 38
skaffman Avatar answered Oct 05 '22 06:10

skaffman