Search code examples
jakarta-eejax-rscdijava-batchjberet

How to put in custom scope/context (JobScoped - custom CDI scope) particular instance from request to make it injectable?


Saying in a nutshell I would like to put in custom scope particular instance of Configuration class from rest request. Main problem is that custom scope (JobScoped from JBeret https://jberet.gitbooks.io/jberet-user-guide/content/custom_cdi_scopes/index.html) is eligable after job starts. I know that there is possibility to add properties when starting job but my Configuration class agregates a lot of configurations and it's quite complicated so it would by very uncomfortable to convert this files to Properties class.

Details below:

This is rest request pseudocode:

@Path("/job")
public class RunJob {

@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Path("/start")
public String startJob(@FormDataParam("file") InputStream uploadedInputStream) {
    JobOperatorImpl jobOperator = (JobOperatorImpl) BatchRuntime.getJobOperator();

    Configuration config = new Configuration(uploadedInputStream);
    Properties properties = new Properties();
    jobOperator.start(job, properties);
}

What I wanted to achieve is to Inject some configuration files in context of Job like below:

public class MyReader implements ItemReader {

@Inject
private Configuration configFile;
}

Configuration class presents like below:

@JobScoped
public class Configuration {
 // some flags, methods etc
}

I've read about Instance, Provider but don't know how to use them in my case. In fact I think it's impossible to use them because the jobs are identified by their name which is dynamic and known at runtime.


Meanwhile I found similar situation to mine: Can I create a request-scoped object and access it from anywhere, and avoid passing it around as a parameter in JAX-RS?

But then occurs problem with missing context. When Job starts there is JobScoped context. According to above solution I had annotated Configuration as RequestScoped, then i received:

org.jboss.weld.context.ContextNotActiveException: WELD-001303: No active contexts for scope type javax.enterprise.context.RequestScoped at org.jboss.weld.manager.BeanManagerImpl.getContext(BeanManagerImpl.java:689) at org.jboss.weld.bean.ContextualInstanceStrategy$DefaultContextualInstanceStrategy.getIfExists(ContextualInstanceStrategy.java:90) at org.jboss.weld.bean.ContextualInstanceStrategy$CachingContextualInstanceStrategy.getIfExists(ContextualInstanceStrategy.java:165) at org.jboss.weld.bean.ContextualInstance.getIfExists(ContextualInstance.java:63) at org.jboss.weld.bean.proxy.ContextBeanInstance.getInstance(ContextBeanInstance.java:83) at org.jboss.weld.bean.proxy.ProxyMethodHandler.getInstance(ProxyMethodHandler.java:125) Configuration$Proxy$_$$_WeldClientProxy.toString(Unknown Source)


Solution

  • I think this question consists of several parts:

    1. How to inject values into batch jobs?
    2. How to seed context based values to batch jobs?
    3. How to enter the RequestScope in a batch job?
    4. How to create a custom scope?
    5. How to enter a custom scope?
    6. How to seed a value in a custom scope?

    I will try to answer all individual questions, but keep in mind that I've only very recently started using CDI/Weld, and have no experience with JBeret.

    1. How to inject values into batch jobs?

    The reason I am adding this question, is because I think Configuration may not need to be a scoped entity. If Configuration has nothing specific to the scope, it could be @Singleton or @Stateless as well. Think for example from configuration files, resources, or environment variables, that will not change on runtime. Non-scoped (or Singleton-scoped) dependencies can be injected into batchlets just fine, using regular @Inject fields, without any need for a @JobScoped annotation.

    2. How to seed context based values to batch jobs?

    So what if the actual value depends on the context and cannot be injected in a @Singleton fashion? Based from the JBeret documentation, it is preferred to pass all configuration by Properties. These can then be read from the JobContext, or injected using the @BatchProperty annotation. This only works for a predefined list of types that are serialisable from a String.

    @Named
    public class MyBatchlet extends AbstractBatchlet {
    
        @Inject
        @BatchProperty(name = "number")
        int number;
    
    }
    

    3. How to enter the @RequestScope in a batch job?

    I think you shouldn't. The @RequestScope is for requests solely. If you have dependencies dependent on @RequestScope that should be accessible outside of a request, consider to introduce a custom scope.

    If you really need to enter the @RequestScope programatically, you can define your own context for it and enter that context (see part 4 below) or enter the context by default, as addressed in this blogpost by Dan Haywood, in his attempt to get into the @RequestScope in Java SE.

    4. How to create a custom scope?

    It is fairly easy to create a custom scope. A custom scope however requires an implementation for the scope context. I found this to be a little unclear in the documentation. Luckily there is the library microscoped library. For this example, you only need the microscoped-core dependency, which provides a ScopeContext implementation that is used in their custom scopes. We will use that ScopeContext for our simple scope as well.

    First we have to create the Scope annotation:

    @Documented
    @Scope
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
    public @interface CustomScoped {}
    

    Secondly, we have to create an extension:

    public class CustomScopedExtension implements Extension, Serializable {
    
        public void addScope(@Observes final BeforeBeanDiscovery event) {
            event.addScope(CustomScoped, true, false);
        }
    
        public void registerContext(@Observes final AfterBeanDiscovery event) {
            event.addContext(new ScopeContext<>(CustomScoped.class));
        }
    
    }
    

    Note that we're using the ScopeContext from microscoped here. Furthermore, you should register your extension by adding the full classname toMETA-INF/services/javax.enterprise.inject.spi.Extension`.

    5. How to enter a custom scope?

    Now we need to enter our scope. We can do this with a little bit of code, that you can place for example in a web Filter or method interceptor. The code uses an BeanManager instance, which can be obtained with @Inject:

    ScopeContext<?> context = (ScopeContext<?>) beanManager.getContext(CustomScoped.class);
    context.enter(key);
    try {
         // continue computation
    } finally {
        context.destroy(key);
    }
    

    6. How to seed a value in a custom scope?

    I have been asking myself the very same question, and this is the solution I came up with. See also my question on how to properly seed from custom Weld CDI scopes: Seed value in Weld CDI custom scope . I do have a workaround for your issue though:

    @Singleton
    public class ConfigurationProducer {
    
        private final InheritableThreadLocal<Configuration>  threadLocalConfiguration =
        new InheritableThreadLocal<>();
    
        @Produces
        @ActiveDataSet
        public ConfigurationConfiguration() {
           return threadLocalConfiguration.get()
        }
    
        public void setConfiguration(Configuration configuration) {
             threadLocalConfiguration.set(configuration);
        }    
    
    }
    

    Now from your the interceptor written above, you can inject ConfigurationProducer and use ConfigurationProducer #setConfiguration(Configuration) to set the Configuration for the current thread. I am still looking for better options here.