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

3

10

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)

Safford answered 15/7, 2016 at 11:52 Comment(2)
I don't really know how the batch api works but can't you just put your configuration in the properties you pass to the operator? Properties can contain Strings (getProperty/setProperty) or Objects (get/put).Markusmarl
Thanks for 'or Objects (get/put)'. It's not what I would like to have but it seems that have no choice.Safford
S
12

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.

Simpleminded answered 18/7, 2016 at 12:52 Comment(7)
Thanks for response. I wonder if I could adapt it to current custom scope which is JobScoped as I mentioned in question. Then I would start from point 5 and hopefully avoid adding third party libraries which is not only dependent on me ;) and could be rejected...Safford
You can also copy the ScopeContext class, it's another dependency, but you only need 1 or 2 classes. The JobScope is implemented with the JobScopeContext. Perhaps that Context already gives you the tools to make this work. See: github.com/jberet/jsr352/blob/… . And please point possible rejecting collegues to the discussion, love to see the final outcome :)Simpleminded
I marked your answer as solution because it was 'most helpful in finding my solution'Safford
Thanks! What solution did you find?Simpleminded
I described it below: #38395962Safford
You decided to use ScopeContext<>. Would it be possible to implement the same with org.jboss.weld.contexts.AbstractSharedContext?Depth
Pretty sure, at the time I was very unfamiliar with implementing custom scopes, and never really needed it anymore.Simpleminded
E
1

The batch spec (JSR 352) defines a standard way to pass user object within a job, by calling:

javax.batch.runtime.context.JobContext#setTransientUserData(myObject);

For simple cases, this should suffice. You can define a job listener, inject JobContext into your job listener class, and inside its startJob() method, set transient user data. It will then be available to the entire job execution, and can be rest to other values. For more complex use cases, @JobScoped is a better choice.

Entelechy answered 26/9, 2016 at 20:20 Comment(2)
Your idea is very similar to my final solution but I don't see posibility to setTransientUserData during start of job. Of course it could be sent as property in java.util.Properties as I did. Getting configuration also it's not so convient becasue you need to inject JobContext and then get Object from that and cast it. Additionaly If you would like to send more objects you would have to wrap it to some container because the transientUserData is dafaultly one object.Safford
You could store many values if you set the transientUserData value as a collection, e.g. a map of objects.Impanel
S
0

Firstly I would like to thank you again Jan-Willem Gmelig Meyling because your answer was very helpful. Anyway, I wanted to use given scope by JBeret which is JobScoped, today it could be only used on TYPE level. I did similary workaround as Jan-Willem Gmelig Meyling suggested but:

  • can use JobScoped
  • don't have to import extra libraries, everything works within CDI

Solution:

1) Configuration class:

@JobScoped
public class Configuration
{...}

2) At JobListener magic happens. Additional comments are redundant.

Let's my code speak for itself ;)

import javax.batch.api.listener.AbstractJobListener;

public class MyJobListener extends AbstractJobListener{

    @Inject
    private Configuration jobScopedConfiguration;


    @Override
    public void beforeJob() throws Exception {
        enrichJobScopedConfigurationWithRequestConfiguration();
        ...
        super.beforeJob();
    }

    private void enrichJobScopedConfigurationWithRequestConfiguration(){
    Configuration requestConfiguration =
                (Configuration) BatchRuntime.getJobOperator().getJobExecution(currentExecutionId).getJobParameters()
                        .get("configuration");
        jobScopedConfiguration.updateWith(requestConfiguration);
    }

Now I can Inject my Configuration in any jberet/java batch artifact in context of job, e.g.:

public class MyReader implements ItemReader {

@Inject
private Configuration configFile;
}
Safford answered 30/9, 2016 at 11:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.