具有许多可选步骤的 JEE 批处理作业规范

JEE Batch Job Specification with many optional Steps

有没有办法用 JSR 352 Batch API 实现以下逻辑? 我有一系列步骤,每个步骤都需要根据开始工作时已知的不同条件执行。 ConditionsEntity 由外部系统提供。

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;
}

我的第一次尝试失败了,因为:com.ibm.jbatch.container.exception.BatchContainerRuntimeException:一个决定不能先于另一个决定。我在此处添加失败代码

<?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";
        }
    }
}

更新 我已经使用 passThroughStep 实现了以下建议的解决方案。它工作正常,但我仍然希望能够避免所有这些代码重复。

<?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>

决策者

@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());

    }
}

我的测试

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"));
    }
}

看来失败的原因是一个决定在运行时将另一个决定作为其下一个执行点。根据 JSR 352 规范第 8.5 节,它应该是受支持的用例:

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.

作为解决方法,您可以尝试使用包含相同条件和逻辑的直通批处理步骤。例如,

<step id="pass-through-step">
   <batchlet ref="PassThroughBatchlet"/>
   <next on="EXECUTE" to="step2"/>
   <next on="SKIP" to="decider2"/>
</step>

或者,如果您的某些条件逻辑可以通过包含过渡元素的批处理步骤实现,则您可以取消这些决定。

@cheng 有一个很好的答案,这与您正在做的事情相比只是一个很小的变化(您只需要基本上将 Decider 更改为 Batchlet)。

至少对我来说,考虑规范在这里为您提供的其他选项是一个有趣的问题。另一种方法是使用注入了所有 "isExecuteStepNN" 道具的 Decider 进行单一决策,您可以在每一步之后调用它。该决策程序通过 StepExecution 传递,因此您知道上一步是什么,并且您可以将其与 "isExecute..." 道具结合使用,使决策程序 return 成为要执行的下一步的 ID。

虽然这可能很聪明,但我认为 cheng 的回答是一个更简单的解决方法。我还认为规范应该考虑允许这样做。可能不支持这个的原因是为了避免回答这个问题:"what StepExecution(s) should be passed to the decide method?" 这似乎是可以解决的。

对于这个问题,我有另一种可能的解决方案,它与其他提议的解决方案有不同的缺点。

可以让 Step 自行决定是否需要执行任何操作。

xml 看起来更整洁:

<?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>

Batchlet 如下所示:

@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());

    }
}

这个测试还没有真正测试预期的行为。我实际上想跳过第 2 步,但是对于当前的解决方案,第 2 步确实得到了执行,但什么也没做。我还没有添加功能来对此进行测试。

@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());
}

我刚刚找到了另一种可能的方法来解决生成更容易理解的 xml 文件的问题。它避免了重复 xml,不依赖于虚拟步骤并且避免了必须将 if/else 逻辑移动到 Batchlet。 基本方法是创建一个决策,并在执行每个步骤后将控制权交还给该决策。 (显然同一个决定可以执行多次。)

<?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 
        <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>

决策者(请注意我只是快速破解了 POC 的逻辑,不要直接使用此代码):

@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';
    }
}

测试:

@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"));
}