Skip to content

Commit f0d276a

Browse files
Spring workflow versioning (#1646)
* feat: Spring workflow version support Signed-off-by: Javier Aliaga <javier@diagrid.io> * chore: Add workflow and activity annoations Signed-off-by: Javier Aliaga <javier@diagrid.io> * chore: Rename annotation Signed-off-by: Javier Aliaga <javier@diagrid.io> * chore: Fix spring boot examples Signed-off-by: Javier Aliaga <javier@diagrid.io> --------- Signed-off-by: Javier Aliaga <javier@diagrid.io>
1 parent bb84ad1 commit f0d276a

File tree

51 files changed

+2743
-44
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+2743
-44
lines changed

dapr-spring/dapr-spring-workflows/src/main/java/io/dapr/spring/workflows/config/DaprWorkflowsConfiguration.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
package io.dapr.spring.workflows.config;
1515

16+
import io.dapr.spring.workflows.config.annotations.ActivityMetadata;
17+
import io.dapr.spring.workflows.config.annotations.WorkflowMetadata;
1618
import io.dapr.workflows.Workflow;
1719
import io.dapr.workflows.WorkflowActivity;
1820
import io.dapr.workflows.runtime.WorkflowRuntime;
@@ -46,17 +48,38 @@ private void registerWorkflowsAndActivities(ApplicationContext applicationContex
4648
Map<String, Workflow> workflowBeans = applicationContext.getBeansOfType(Workflow.class);
4749

4850
for (Workflow workflow : workflowBeans.values()) {
49-
LOGGER.info("Dapr Workflow: '{}' registered", workflow.getClass().getName());
5051

51-
workflowRuntimeBuilder.registerWorkflow(workflow);
52+
// Get the workflowDefinition annotation from the workflow class and validate it
53+
// If the annotation is not present, register the instance
54+
// If preset register with the workflowDefinition annotation values
55+
WorkflowMetadata workflowDefinition = workflow.getClass().getAnnotation(WorkflowMetadata.class);
56+
57+
if (workflowDefinition == null) {
58+
// No annotation present, register the instance with default behavior
59+
LOGGER.info("Dapr Workflow: '{}' registered", workflow.getClass().getName());
60+
workflowRuntimeBuilder.registerWorkflow(workflow);
61+
continue;
62+
}
63+
64+
// Register with annotation values
65+
String workflowName = workflowDefinition.name();
66+
String workflowVersion = workflowDefinition.version();
67+
boolean isLatest = workflowDefinition.isLatest();
68+
69+
workflowRuntimeBuilder.registerWorkflow(workflowName, workflow, workflowVersion, isLatest);
5270
}
5371

5472
Map<String, WorkflowActivity> workflowActivitiesBeans = applicationContext.getBeansOfType(WorkflowActivity.class);
5573

5674
for (WorkflowActivity activity : workflowActivitiesBeans.values()) {
5775
LOGGER.info("Dapr Workflow Activity: '{}' registered", activity.getClass().getName());
76+
ActivityMetadata activityDefinition = activity.getClass().getAnnotation(ActivityMetadata.class);
77+
if (activityDefinition == null) {
78+
workflowRuntimeBuilder.registerActivity(activity);
79+
continue;
80+
}
5881

59-
workflowRuntimeBuilder.registerActivity(activity);
82+
workflowRuntimeBuilder.registerActivity(activityDefinition.name(), activity);
6083
}
6184

6285
WorkflowRuntime runtime = workflowRuntimeBuilder.build();
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2026 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.spring.workflows.config.annotations;
15+
16+
import org.springframework.stereotype.Component;
17+
18+
import java.lang.annotation.ElementType;
19+
import java.lang.annotation.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
import java.lang.annotation.Target;
22+
23+
@Target(ElementType.TYPE)
24+
@Retention(RetentionPolicy.RUNTIME)
25+
@Component
26+
public @interface ActivityMetadata {
27+
/**
28+
* Name of the activity.
29+
*
30+
* @return the name of the activity.
31+
*/
32+
String name() default "";
33+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2026 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.spring.workflows.config.annotations;
15+
16+
import org.springframework.stereotype.Component;
17+
18+
import java.lang.annotation.ElementType;
19+
import java.lang.annotation.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
import java.lang.annotation.Target;
22+
23+
@Target(ElementType.TYPE)
24+
@Retention(RetentionPolicy.RUNTIME)
25+
@Component
26+
public @interface WorkflowMetadata {
27+
/**
28+
* Name of the workflow.
29+
* Required when version is specified.
30+
*
31+
* @return the name of the workflow.
32+
*/
33+
String name() default "";
34+
35+
36+
/**
37+
* Version of the workflow.
38+
* When specified, name must also be provided.
39+
*
40+
* @return the version of the workflow.
41+
*/
42+
43+
String version() default "";
44+
45+
/**
46+
* Specifies if the version is the latest or not.
47+
* When true, the version and name must be provided.
48+
*
49+
* @return true if the version is the latest
50+
*/
51+
boolean isLatest() default false;
52+
}

sdk-workflows/src/main/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilder.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ public <T extends Workflow> WorkflowRuntimeBuilder registerWorkflow(String name,
145145
Class<T> clazz,
146146
String versionName,
147147
Boolean isLatestVersion) {
148+
149+
if (StringUtils.isEmpty(name)) {
150+
throw new IllegalArgumentException("Workflow name cannot be empty");
151+
}
152+
148153
this.builder.addOrchestration(new WorkflowClassWrapper<>(name, clazz, versionName, isLatestVersion));
149154
this.workflowSet.add(name);
150155
this.workflows.add(name);
@@ -167,8 +172,8 @@ public <T extends Workflow> WorkflowRuntimeBuilder registerWorkflow(String name,
167172
* @return the WorkflowRuntimeBuilder
168173
*/
169174
public <T extends Workflow> WorkflowRuntimeBuilder registerWorkflow(T instance) {
170-
Class<T> clazz = (Class<T>) instance.getClass();
171-
this.registerWorkflow(clazz.getCanonicalName(), instance, null, null);
175+
var name = instance.getClass().getCanonicalName();
176+
this.registerWorkflow(name, instance, null, null);
172177
return this;
173178
}
174179

@@ -186,6 +191,10 @@ public <T extends Workflow> WorkflowRuntimeBuilder registerWorkflow(String name,
186191
T instance,
187192
String versionName,
188193
Boolean isLatestVersion) {
194+
if (StringUtils.isEmpty(name)) {
195+
throw new IllegalArgumentException("Workflow name cannot be empty");
196+
}
197+
189198
this.builder.addOrchestration(new WorkflowInstanceWrapper<>(name, instance, versionName, isLatestVersion));
190199
this.workflowSet.add(name);
191200
this.workflows.add(name);
@@ -220,6 +229,10 @@ public <T extends WorkflowActivity> WorkflowRuntimeBuilder registerActivity(Clas
220229
* @return the WorkflowRuntimeBuilder
221230
*/
222231
public <T extends WorkflowActivity> WorkflowRuntimeBuilder registerActivity(String name, Class<T> clazz) {
232+
if (StringUtils.isEmpty(name)) {
233+
throw new IllegalArgumentException("Activity name cannot be empty");
234+
}
235+
223236
this.builder.addActivity(new WorkflowActivityClassWrapper<>(name, clazz));
224237
this.activitySet.add(name);
225238
this.activities.add(name);
@@ -305,5 +318,4 @@ public WorkflowRuntimeBuilder registerTaskOrchestrationFactory(
305318

306319
return this;
307320
}
308-
309321
}

sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowRuntimeBuilderTest.java

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import io.dapr.workflows.WorkflowActivity;
2121
import io.dapr.workflows.WorkflowActivityContext;
2222
import io.dapr.workflows.WorkflowStub;
23+
import org.junit.Assert;
2324
import org.junit.jupiter.api.Test;
2425
import org.slf4j.Logger;
2526

@@ -28,7 +29,9 @@
2829

2930
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
3031
import static org.mockito.ArgumentMatchers.eq;
31-
import static org.mockito.Mockito.*;
32+
import static org.mockito.Mockito.mock;
33+
import static org.mockito.Mockito.times;
34+
import static org.mockito.Mockito.verify;
3235

3336
public class WorkflowRuntimeBuilderTest {
3437
public static class TestWorkflow implements Workflow {
@@ -39,51 +42,47 @@ public WorkflowStub create() {
3942
}
4043
}
4144

45+
@Test
46+
public void registerValidWorkflowInstances() {
47+
var b = new WorkflowRuntimeBuilder();
48+
49+
assertDoesNotThrow(() -> b.registerWorkflow("TestWorkflow", new TestWorkflow(), null, null));
50+
assertDoesNotThrow(() -> b.registerWorkflow("NameWithClass", TestWorkflow.class));
51+
// assertDoesNotThrow(() -> b.registerWorkflow(new TestWorkflowWithNameAndVersionIsLatest()));
52+
53+
Assert.assertThrows(IllegalArgumentException.class, () -> b.registerWorkflow("", new TestWorkflow(), null, null));
54+
Assert.assertThrows(IllegalArgumentException.class, () -> b.registerWorkflow("", TestWorkflow.class, null, null));
55+
Assert.assertThrows(IllegalArgumentException.class, () -> b.registerActivity("", new TestActivity()));
56+
Assert.assertThrows(IllegalArgumentException.class, () -> b.registerActivity("", TestActivity.class));
57+
}
58+
4259
public static class TestActivity implements WorkflowActivity {
4360
@Override
4461
public Object run(WorkflowActivityContext ctx) {
4562
return null;
4663
}
4764
}
4865

49-
@Test
50-
public void registerValidWorkflowClass() {
51-
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerWorkflow(TestWorkflow.class));
52-
}
53-
5466
@Test
5567
public void registerValidVersionWorkflowClass() {
56-
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerWorkflow("TestWorkflow", TestWorkflow.class,"testWorkflowV1", false));
57-
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerWorkflow("TestWorkflow", TestWorkflow.class,"testWorkflowV2", true));
58-
}
59-
60-
@Test
61-
public void registerValidWorkflowInstance() {
62-
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerWorkflow(new TestWorkflow()));
68+
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerWorkflow("TestWorkflow", TestWorkflow.class, "testWorkflowV1", false));
69+
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerWorkflow("TestWorkflow", TestWorkflow.class, "testWorkflowV2", true));
6370
}
6471

6572
@Test
6673
public void registerValidVersionWorkflowInstance() {
67-
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerWorkflow("testWorkflowV1", new TestWorkflow(),"testWorkflowV1", false));
68-
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerWorkflow("testWorkflowV2",new TestWorkflow(),"testWorkflowV2", true));
69-
}
70-
71-
72-
@Test
73-
public void registerValidWorkflowActivityClass() {
74-
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerActivity(TestActivity.class));
74+
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerWorkflow("testWorkflowV1", new TestWorkflow(), "testWorkflowV1", false));
75+
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerWorkflow("testWorkflowV2", new TestWorkflow(), "testWorkflowV2", true));
7576
}
7677

7778
@Test
78-
public void registerValidWorkflowActivityInstance() {
79-
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerActivity(new TestActivity()));
79+
public void registerValidWorkflowClass() {
80+
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerWorkflow(TestWorkflow.class));
8081
}
8182

82-
83-
8483
@Test
8584
public void registerValidTaskActivityFactory() {
86-
class A implements WorkflowActivity{
85+
class A implements WorkflowActivity {
8786

8887
@Override
8988
public Object run(WorkflowActivityContext ctx) {
@@ -106,9 +105,14 @@ public TaskActivity create() {
106105
}));
107106
}
108107

108+
@Test
109+
public void registerValidWorkflowInstance() {
110+
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerWorkflow(new TestWorkflow()));
111+
}
112+
109113
@Test
110114
public void registerValidWorkflowOrchestrator() {
111-
class W implements Workflow{
115+
class W implements Workflow {
112116

113117
@Override
114118
public WorkflowStub create() {
@@ -145,17 +149,15 @@ public Boolean isLatestVersion() {
145149

146150
}
147151

152+
148153
@Test
149-
public void buildTest() {
150-
assertDoesNotThrow(() -> {
151-
try {
152-
WorkflowRuntime runtime = new WorkflowRuntimeBuilder().build();
153-
System.out.println("WorkflowRuntime created");
154-
runtime.close();
155-
} catch (Exception e) {
156-
throw new RuntimeException(e);
157-
}
158-
});
154+
public void registerValidWorkflowActivityClass() {
155+
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerActivity(TestActivity.class));
156+
}
157+
158+
@Test
159+
public void registerValidWorkflowActivityInstance() {
160+
assertDoesNotThrow(() -> new WorkflowRuntimeBuilder().registerActivity(new TestActivity()));
159161
}
160162

161163
@Test
@@ -166,7 +168,7 @@ public void loggingOutputTest() {
166168

167169
Logger testLogger = mock(Logger.class);
168170

169-
var runtimeBuilder = new WorkflowRuntimeBuilder(testLogger);
171+
var runtimeBuilder = new WorkflowRuntimeBuilder(testLogger);
170172
assertDoesNotThrow(() -> runtimeBuilder.registerWorkflow(TestWorkflow.class));
171173
assertDoesNotThrow(() -> runtimeBuilder.registerActivity(TestActivity.class));
172174

@@ -180,4 +182,17 @@ public void loggingOutputTest() {
180182

181183
runtime.close();
182184
}
185+
186+
@Test
187+
public void buildTest() {
188+
assertDoesNotThrow(() -> {
189+
try {
190+
WorkflowRuntime runtime = new WorkflowRuntimeBuilder().build();
191+
System.out.println("WorkflowRuntime created");
192+
runtime.close();
193+
} catch (Exception e) {
194+
throw new RuntimeException(e);
195+
}
196+
});
197+
}
183198
}

spotbugs-exclude.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@
1010
<Bug pattern="EI_EXPOSE_REP"/>
1111
</Match>
1212

13+
<!--Temporarily ignoring checking after upgrade to new spotbugs version-->
14+
<Match>
15+
<Package name="~io\.dapr.*"/>
16+
<Bug pattern="MS_EXPOSE_REP"/>
17+
</Match>
18+
19+
<!--Temporarily ignoring checking after upgrade to new spotbugs version-->
20+
<Match>
21+
<Package name="~io\.dapr.*"/>
22+
<Bug pattern="SE_BAD_FIELD"/>
23+
</Match>
24+
1325
<!--Temporarily ignoring checking after upgrade to new spotbugs version-->
1426
<Match>
1527
<Package name="~io\.dapr.*"/>

spring-boot-examples/workflows/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<modules>
2121
<module>patterns</module>
2222
<module>multi-app</module>
23+
<module>versioning</module>
2324
</modules>
2425

2526
<build>

0 commit comments

Comments
 (0)