Skip to main content


Eclipse Community Forums
Forum Search:

Search      Help    Register    Login    Home
Home » Eclipse Projects » Eclipse Scout » Backend client bean and dependency injection in services
Backend client bean and dependency injection in services [message #1856823] Wed, 04 January 2023 14:04 Go to next message
Nils Israel is currently offline Nils IsraelFriend
Messages: 72
Registered: May 2010
Member
Hi,
we are using a generated OpenAPI client (ApiClient) as a backend for our Scout 22 application. Since we want to transmit the
scout user id and the scout correlation id to the backend as additional http headers, we created a factory for our ApiClient class.
The idea is to let scout inject the ApiClient bean into the scout server services.

Since the ApiClient class is generated we can't annotate it with scout annotations. According to the documentation it is also possible to
define the bean metadata programmatically. So that is, what we've tried:
public class RegisterBeansListener implements IPlatformListener {
  @Override
  public void stateChanged(PlatformEvent event) {
    if (event.getState() == IPlatform.State.BeanManagerPrepared) {
      register(ApiClient.class, ApiClientProducer.class, false);
    }
  }

  private void register(Class<?> clazz, Class<?> producerClazz, boolean isSingleton) {
    IBeanInstanceProducer<?> producer = (IBeanInstanceProducer<?>) BEANS.get(producerClazz);
    BeanMetaData beanData = new BeanMetaData(clazz)
      .withProducer(producer)
      .withApplicationScoped(isSingleton);
    BEANS.getBeanManager().registerBean(beanData);
  }
}


@ApplicationScoped
public class ApiClientProducer extends NonSingeltonBeanInstanceProducer<ApiClient> {

  private static final Logger log = LoggerFactory.getLogger(ApiClientProducer.class);

  private final ApiClientConfig apiClientConfig;

  @InjectBean
  public ApiClientProducer(ApiClientConfig apiClientConfig) {
    this.apiClientConfig = apiClientConfig;
  }

  @Override
  public ApiClient produce(IBean<ApiClient> bean) {
    ApiClient apiClient = super.produce(bean);
    apiClient.setBasePath(apiClientConfig.getBasePath());
    apiClient.setUsername(apiClientConfig.getUsername());
    apiClient.setPassword(apiClientConfig.getPassword());

    var currentUserId = Sessions.getCurrentUserId();
    apiClient.addDefaultHeader("x-frontend-username", currentUserId);

    var cid = CorrelationId.CURRENT.get();
    apiClient.addDefaultHeader("X-Request-ID", cid);

    log.info("created API client for user: {} -> {}", currentUserId, apiClient);

    return apiClient;
  }
}


@ApplicationScoped
public class PriceGroupRestControllerApiAdapter implements RestControllerApiAdapter<PriceGroup> {
  private final PriceGroupRestControllerApi api;

  @InjectBean
  public PriceGroupRestControllerApiAdapter(ApiClient apiClient) {
    this.api = new PriceGroupRestControllerApi(apiClient);
  }

  @Override
  public PriceGroup create(PriceGroup entity) throws ApiException {
    return api.createPriceGroup(entity);
  }
}


Now, we should get a new ApiClient for every "injection". Is this the recommended way to handle such a bean?
We had some trouble with incorrect user ids transmitted to the backend and couldn't figure out the cause of it.
I would be glad if someone can give me a hint or a best practice for this scenario.

Thanks and best regards
Nils
Re: Backend client bean and dependency injection in services [message #1856837 is a reply to message #1856823] Thu, 05 January 2023 08:54 Go to previous messageGo to next message
Matthias Villiger is currently offline Matthias VilligerFriend
Messages: 232
Registered: September 2011
Senior Member
Hi Nils

I am not sure what an RestControllerApiAdapter is. But as far as I can see the ApiClient is a non-application-scoped bean which makes sense because for each request the current user must be used.
But the RestControllerApiAdapter seems to be application scoped. And therefore the ApiClient is only initialized once for this singleton controller and all calls using PriceGroupRestControllerApi use the one initial ApiClient. Maybe the controller should not be application scoped as well, as each controller needs the ApiClient for the current user?

Kind regards
Mat
Re: Backend client bean and dependency injection in services [message #1856851 is a reply to message #1856837] Thu, 05 January 2023 14:21 Go to previous messageGo to next message
Nils Israel is currently offline Nils IsraelFriend
Messages: 72
Registered: May 2010
Member
Hi Mat,
I tested this and you are absolutely right. Now that you pointed it out it seems so clear, that this is likely the or at least one of the reasons for the incorrect user.
Thank you!

To improve the situation I now have some questions:
- Is my understanding correct, that there is only @ApplicationScoped or no scope at all and no @SessionScoped?
- Is it possible to simulate a SessionScope by using something like the NonSingeltonBeanInstanceProducer but instead of creating the bean on every call, I only create and save it in the session if there isn't one already stored there and return the one stored there otherwise?
- Can I use the @InjectBean annotation on any constructor, method and field or only on those of other registered beans (via @Bean or BeanManager)?

Thanks again.
Nils
Re: Backend client bean and dependency injection in services [message #1856854 is a reply to message #1856851] Thu, 05 January 2023 16:05 Go to previous messageGo to next message
Nils Israel is currently offline Nils IsraelFriend
Messages: 72
Registered: May 2010
Member
Hi Mat,
we discussed this internally and now came up with another solution. Maybe it can help someone with a similiar problem:

We liked the idea of using the EntityRestControllerApiAdapters as Singleton. The PriceGroupRestControllerApiAdapter is an example of such a EntityRestControllerApiAdapter, which implement
the usual Scout CRUD service operations, like prepareCreate, create, update, delete by adapting it to the generated OpenAPI client.
So, instead of changing all the adapters to "NotSingleton", we made the ApiClient a Singleton as well and added the possibility to add headers via callback functions, in this case method references of type Supplier<String> in the context of the request.

Here is the result:
public class RegisterBeansListener implements IPlatformListener {
  @Override
  public void stateChanged(PlatformEvent event) {
    if (event.getState() == IPlatform.State.BeanManagerPrepared) {
	  // changed isSingleton => true
      register(ApiClient.class, ApiClientProducer.class, true);
    }
  }

  private void register(Class<?> clazz, Class<?> producerClazz, boolean isSingleton) {
    IBeanInstanceProducer<?> producer = (IBeanInstanceProducer<?>) BEANS.get(producerClazz);
    BeanMetaData beanData = new BeanMetaData(clazz)
      .withProducer(producer)
      .withApplicationScoped(isSingleton);
    BEANS.getBeanManager().registerBean(beanData);
  }
}


@ApplicationScoped
// implement necessary Interface instead of extending the provided ProducerClasses
public class ApiClientProducer implements IBeanInstanceProducer<ApiClient> {

  private static final Logger log = LoggerFactory.getLogger(ApiClientProducer.class);

  private final ApiClientConfig apiClientConfig;
  private final String userAgent;


  @InjectBean
  public ApiClientProducer(
    ApiClientConfig apiClientConfig,
    UserAgentFactory userAgentFactory
  ) {
    this.apiClientConfig = apiClientConfig;
    this.userAgent = userAgentFactory.create();
  }

  @Override
  public ApiClient produce(IBean<ApiClient> bean) {
    // created a new anonymous subclass of ApiClient with the additional methods
    var apiClient = new ApiClient() {
      // HashMap of the functions, which create the header values
      private final Map<String, Supplier<String>> lazyHeaderSuppliers = new HashMap<>();

      public void addLazyDefaultHeader(String header, Supplier<String> lazyHeaderValueSupplier) {
        lazyHeaderSuppliers.put(header, lazyHeaderValueSupplier);
      }

      @Override
      public <T> ApiResponse<T> invokeAPI(String operation,
                                          String path,
                                          String method,
                                          List<Pair> queryParams,
                                          Object body,
                                          Map<String, String> headerParams,
                                          Map<String, String> cookieParams,
                                          Map<String, Object> formParams,
                                          String accept,
                                          String contentType,
                                          String[] authNames,
                                          GenericType<T> returnType,
                                          boolean isBodyNullable) throws ApiException {

        // call the functions to create the header values and put it into the provided hashmap
        lazyHeaderSuppliers.keySet().forEach(header -> {
          var headerValueSupplier = lazyHeaderSuppliers.get(header);
          headerParams.put(header, headerValueSupplier.get());
        });
		// call super with the enriched HashMap for the header information
        return super.invokeAPI(operation, path, method, queryParams, body, headerParams, cookieParams,
          formParams, accept, contentType, authNames, returnType, isBodyNullable);
      }
    };
    apiClient.setBasePath(apiClientConfig.getBasePath());
    apiClient.setUsername(apiClientConfig.getUsername());
    apiClient.setPassword(apiClientConfig.getPassword());

	// use the method reference Sessions::getCurrentUserId as a Supplier<String> for the header
	// note that the method is executed in the context of the call to ApiClient.invokeApi and therefore returns the correct userId
    apiClient.addLazyDefaultHeader("x-frontend-username", Sessions::getCurrentUserId);
    apiClient.addLazyDefaultHeader("X-Request-ID", CorrelationId.CURRENT::get);

    apiClient.setUserAgent(userAgent);
    return apiClient;
  }
}


// unchanged
@ApplicationScoped
public class PriceGroupRestControllerApiAdapter implements RestControllerApiAdapter<PriceGroup> {
  private final PriceGroupRestControllerApi api;

  @InjectBean
  public PriceGroupRestControllerApiAdapter(ApiClient apiClient) {
    this.api = new PriceGroupRestControllerApi(apiClient);
  }

  @Override
  public PriceGroup create(PriceGroup entity) throws ApiException {
    return api.createPriceGroup(entity);
  }
}


Best regards
Nils
Re: Backend client bean and dependency injection in services [message #1856857 is a reply to message #1856851] Thu, 05 January 2023 17:27 Go to previous message
Matthias Villiger is currently offline Matthias VilligerFriend
Messages: 232
Registered: September 2011
Senior Member
Quote:

Is my understanding correct, that there is only @ApplicationScoped or no scope at all and no @SessionScoped?

Correct. Out of the box there is ApplicationScoped (more or less like a singleton) and just Bean (new instance on every lookup).

Quote:

Is it possible to simulate a SessionScope by using something like the NonSingeltonBeanInstanceProducer but instead of creating the bean on every call, I only create and save it in the session if there isn't one already stored there and return the one stored there otherwise?

This would be possible, yes. Something like this existed in older Scout versions but has been dropped in favor of ApplicationScoped beans. Bean lookups for such beans would then of course only work, if a Session is on the run context.

Quote:

Can I use the @InjectBean annotation on any constructor, method and field or only on those of other registered beans (via @Bean or BeanManager)?

Basically you can use it on every instance for which BeanInstanceUtil#initializeBeanInstance is called after the instance creation. By default this is done automatically for all beans. But you are free to call it in your own factories or other classes as well.

Glad you found a solution that works for you.

Kind regards
Mat
Previous Topic:Migrating Scout from 11 to 22 - Error when importing custom fonts from WEB
Next Topic:Cannot build without clean
Goto Forum:
  


Current Time: Thu May 09 01:33:14 GMT 2024

Powered by FUDForum. Page generated in 0.03541 seconds
.:: Contact :: Home ::.

Powered by: FUDforum 3.0.2.
Copyright ©2001-2010 FUDforum Bulletin Board Software

Back to the top