问题
Is there a way to achieve the following logic with JSR 352 Batch API? I have a series of Steps that each need to be executed based on a different condition known when starting the job. ConditionsEntity is provided by an external system.
public List<Steps> createStepSequence(ConditionsEntity conditions) {
if (conditions.isStep1Enabled()) {
steps.add(step1)
}
if (conditions.isStep2Enabled()) {
steps.add(step2)
}
if (conditions.isStep3Enabled()) {
steps.add(step3)
}
//many more ifs
return steps;
}
My first attempt fails because of: com.ibm.jbatch.container.exception.BatchContainerRuntimeException: A decision cannot precede another decision. I'm adding the FAILING Code here
<?xml version="1.0" encoding="UTF-8"?>
<job id="myJob" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/jobXML_1_0.xsd" version="1.0">
<properties>
<property name="isExecuteStep2" value="false"/>
<property name="isExecuteStep3" value="false"/>
</properties>
<step id="step1" next="decider1">
<batchlet ref="myBatchlet1"/>
</step>
<decision id="decider1" ref="SkipNextStepDecider">
<properties>
<property name="isExecuteNextStep" value="#{jobProperties['isExecuteStep2']}"/>
</properties>
<next on="EXECUTE" to="step2"/>
<next on="SKIP" to="decider2"/>
</decision>
<step id="step2">
<batchlet ref="myBatchlet2"/>
</step>
<decision id="decider2" ref="SkipNextStepDecider">
<properties>
<property name="isExecuteNextStep" value="#{jobProperties['isExecuteStep3']}"/>
</properties>
<next on="EXECUTE" to="step3"/>
<end on="SKIP"/>
</decision>
<step id="step3">
<batchlet ref="myBatchlet3"/>
</step>
</job>
@Named
public class SkipNextStepDecider implements Decider {
@Inject
@BatchProperty
private String isExecuteNextStep;
@Override
public String decide(StepExecution[] ses) throws Exception {
if (isExecuteNextStep.equalsIgnoreCase("true")) {
return "EXECUTE";
} else {
return "SKIP";
}
}
}
UPDATE I have implemented the following suggested solution with a passThroughStep. It's working correctly, but I would still love to be able to avoid all this code duplication.
<?xml version="1.0" encoding="UTF-8"?>
<job id="decisionpoc" xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="1.0">
<step id="dummy0" next="decider1">
<batchlet ref="dummyBatchlet"/>
</step>
<decision id="decider1" ref="skipNextStepDecider">
<properties>
<property name="condition" value="isExecuteStep1"/>
</properties>
<next on="EXECUTE" to="step1"/>
<next on="SKIP" to="dummy1"/>
</decision>
<step id="step1" next="decider2">
<batchlet ref="myBatchlet1"/>
</step>
<step id="dummy1" next="decider2">
<batchlet ref="dummyBatchlet"/>
</step>
<decision id="decider2" ref="skipNextStepDecider">
<properties>
<property name="condition" value="isExecuteStep2"/>
</properties>
<next on="EXECUTE" to="step2"/>
<next on="SKIP" to="dummy2"/>
</decision>
<step id="step2">
<batchlet ref="myBatchlet2"/>
</step>
<step id="dummy2" next="decider3">
<batchlet ref="dummyBatchlet"/>
</step>
<decision id="decider3" ref="skipNextStepDecider">
<properties>
<property name="condition" value="isExecuteStep3"/>
</properties>
<next on="EXECUTE" to="step3"/>
<end on="SKIP"/>
</decision>
<step id="step3">
<batchlet ref="myBatchlet3"/>
</step>
</job>
The Decider
@Named
public class SkipNextStepDecider implements Decider {
@Inject
@BatchProperty
private String condition;
@Inject
private JobContext jobContext;
@Override
public String decide(StepExecution[] ses) throws Exception {
Properties parameters = getParameters();
String isExecuteNextStep = parameters.getProperty(condition);
if (isExecuteNextStep.equalsIgnoreCase("true")) {
return "EXECUTE";
} else {
return "SKIP";
}
}
private Properties getParameters() {
JobOperator operator = getJobOperator();
return operator.getParameters(jobContext.getExecutionId());
}
}
My Test
public class DecisionPOCTest extends AbstractBatchLOT {
@Test
public void testProcess() throws Exception {
JobOperator jobOperator = getJobOperator();
Properties properties = new Properties();
properties.setProperty("isExecuteStep1", "true");
properties.setProperty("isExecuteStep2", "false");
properties.setProperty("isExecuteStep3", "true");
Long executionId = jobOperator.start("poc/decisionPOC", properties);
JobExecution jobExecution = jobOperator.getJobExecution(executionId);
jobExecution = BatchTestHelper.keepTestAlive(jobExecution);
List<StepExecution> stepExecutions = jobOperator.getStepExecutions(executionId);
List<String> executedSteps = new ArrayList<>();
for (StepExecution stepExecution : stepExecutions) {
executedSteps.add(stepExecution.getStepName());
}
assertEquals(COMPLETED, jobExecution.getBatchStatus());
assertEquals(4, stepExecutions.size());
assertArrayEquals(new String[]{"dummy0", "step1", "dummy2", "step3"}, executedSteps.toArray());
assertFalse(executedSteps.contains("step2"));
}
}
回答1:
It looks like the failure was caused by the fact that one decision had another decison as its next execution point at runtime. As per the JSR 352 spec Section 8.5, it should be a supported use case:
A job may contain any number of decision elements. A decision element is the target of the "next" attribute from a job-level step, flow, split, or another decision.
As a workaround, you can try having a pass-through batchlet-step that contains the same condition and logic. For example,
<step id="pass-through-step">
<batchlet ref="PassThroughBatchlet"/>
<next on="EXECUTE" to="step2"/>
<next on="SKIP" to="decider2"/>
</step>
Or if some of your conditional logic can be achived with a batchlet-step containing transition elements, you can do away with those decisions.
回答2:
@cheng has a good answer which would be a very small change from what you're doing (you just need to change your Decider to a Batchlet basically).
To me, at least, it's an interesting question to consider what other options the spec gives you here. Another would be to have a single decision with a Decider with ALL "isExecuteStepNN" props injected into it, that you could call after each step. That Decider gets passed a StepExecution so you know what the previous step is, and you could combine that with the "isExecute..." props to have the Decider return the id of the next step to execute.
Though that may be clever, I think cheng's answer is an easier workaround. I also think the spec should consider allowing this. Probably the reason for not supporting this is to avoid answering the question: "what StepExecution(s) should be passed to the decide method?" which seems solvable.
回答3:
I have another possible solution to the problem that has different drawbacks than the other proposed solutions.
It's possible to let the Step decide for itself whether it needs to execute anything or not.
The xml looks much neater:
<?xml version="1.0" encoding="UTF-8"?>
<job id="decisionpoc" xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="1.0">
<step id="step1" next="step2">
<batchlet ref="myBatchletWithDecision1">
<properties>
<property name="condition" value="isExecuteStep1"/>
</properties>
</batchlet>
</step>
<step id="step2" next="step3">
<batchlet ref="myBatchletWithDecision2">
<properties>
<property name="condition" value="isExecuteStep2"/>
</properties>
</batchlet>
</step>
<step id="step3">
<batchlet ref="myBatchletWithDecision3">
<properties>
<property name="condition" value="isExecuteStep3"/>
</properties>
</batchlet>
</step>
</job>
The Batchlet then look as follows:
@Named
public class MyBatchletWithDecision1 extends AbstractBatchlet {
@Inject
@BatchProperty
private String condition;
@Inject
private JobContext jobContext;
@Override
public String process() {
Properties parameters = getParameters();
String isExecuteStep = parameters.getProperty(condition);
if (isExecuteStep.equalsIgnoreCase("true")) {
System.out.println("Running inside a batchlet 1");
} else {
//TODO somehow log that the step was skipped
}
return "COMPLETED";
}
private Properties getParameters() {
JobOperator operator = getJobOperator();
return operator.getParameters(jobContext.getExecutionId());
}
}
This Test somewhat doesn't really test the expected behaviour yet. I actually want to skip Step2, but with the current solution step2 does get executed, but doesn't do anything. I haven't added functionality to test for this yet.
@Test
public void testProcess() throws Exception {
JobOperator jobOperator = getJobOperator();
Properties properties = new Properties();
properties.setProperty("isExecuteStep1", "true");
properties.setProperty("isExecuteStep2", "false");
properties.setProperty("isExecuteStep3", "true");
Long executionId = jobOperator.start("poc/decisionWithoutDeciderPOC", properties);
JobExecution jobExecution = jobOperator.getJobExecution(executionId);
jobExecution = BatchTestHelper.keepTestAlive(jobExecution);
List<StepExecution> stepExecutions = jobOperator.getStepExecutions(executionId);
List<String> executedSteps = new ArrayList<>();
for (StepExecution stepExecution : stepExecutions) {
executedSteps.add(stepExecution.getStepName());
}
assertEquals(COMPLETED, jobExecution.getBatchStatus());
assertEquals(3, stepExecutions.size());
assertArrayEquals(new String[]{"step1", "step2", "step3"}, executedSteps.toArray());
}
回答4:
I have just found another possible way to solve the issue that generates a much easier to understand xml file. It avoids duplication xml, doesn't rely on dummy steps and avoids having to move if/else logic to the Batchlet. The basic approach is to create one decision and keep passing control back to this decision after each step that gets executed. (Apparently the same decision can get executed multiple times.)
<?xml version="1.0" encoding="UTF-8"?>
<job id="decisionpoc" xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="1.0">
<!-- This dummy step is needed because it's not possible to start with a decision-->
<step id="dummy0" next="decider">
<batchlet ref="dummyBatchlet"/>
</step>
<decision id="decider" ref="nextStepDecider">
<properties>
<property name="condition" value="isExecuteSteps"/>
</properties>
<!-- Need to list all steps, see https://stackoverflow.com/questions/59214372/jsr352-decide-next-step-based-on-return-parameter-from-decider-->
<next on="STEP1" to="step1"/>
<next on="STEP2" to="step2"/>
<next on="STEP3" to="step3"/>
<end on="SKIP"/>
</decision>
<step id="step1" next="decider">
<batchlet ref="myBatchlet1"/>
</step>
<step id="step2" next="decider">
<batchlet ref="myBatchlet2"/>
</step>
<step id="step3">
<batchlet ref="myBatchlet3"/>
</step>
</job>
The Decider (please note I just quickly hacked the logic for the POC, don't use this code directly):
@Named
public class NextStepDecider implements Decider {
@Inject
@BatchProperty
private String condition;
@Inject
private JobContext jobContext;
@Override
public String decide(StepExecution[] ses) throws Exception {
//FIXME: very hacky code in this method
if (ses.length != 1) {
// Decider not reached by transitioning from a step
return "ERROR";
}
Properties parameters = getParameters();
String executeSteps = parameters.getProperty(condition);
String[] steps = executeSteps.split(",");
int start = 0;
//advance start index to the next step based on the previous step that was executed
String previousStepName = ses[0].getStepName();
if (previousStepName.startsWith("step")) {
start = convertCharToInt(previousStepName);
}
//Loop through the remaining steps until we find a step that has its executeStep property set to true
for (int i = start; i < steps.length; i++) {
if (steps[i].equalsIgnoreCase("true")) {
return "STEP" + (i + 1);
}
}
return "SKIP";
}
private Properties getParameters() {
JobOperator operator = getJobOperator();
return operator.getParameters(jobContext.getExecutionId());
}
private int convertCharToInt(String previousStepName) {
return previousStepName.charAt(previousStepName.length()-1) - '0';
}
}
The Test:
@Test
public void testProcess() throws Exception {
JobOperator jobOperator = getJobOperator();
Properties properties = new Properties();
properties.setProperty("isExecuteSteps", "true,false,true");
Long executionId = jobOperator.start("poc/decisionWithDeciderPOC", properties);
JobExecution jobExecution = jobOperator.getJobExecution(executionId);
jobExecution = BatchTestHelper.keepTestAlive(jobExecution);
List<StepExecution> stepExecutions = jobOperator.getStepExecutions(executionId);
List<String> executedSteps = new ArrayList<>();
for (StepExecution stepExecution : stepExecutions) {
executedSteps.add(stepExecution.getStepName());
}
assertEquals(COMPLETED, jobExecution.getBatchStatus());
assertEquals(3, stepExecutions.size());
assertArrayEquals(new String[]{"dummy0", "step1", "step3"}, executedSteps.toArray());
assertFalse(executedSteps.contains("step2"));
}
来源:https://stackoverflow.com/questions/59054973/jee-batch-job-specification-with-many-optional-steps