Skip to content

Commit c7bcbee

Browse files
Figure out the Meta annotation problem, and introduce 3 new annotations for spring users: @RestateService, @RestateVirtualObject and @RestateWorkflow
1 parent b881b04 commit c7bcbee

File tree

11 files changed

+206
-67
lines changed

11 files changed

+206
-67
lines changed

sdk-api-gen/src/main/java/dev/restate/sdk/gen/ElementConverter.java

Lines changed: 22 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
import javax.tools.Diagnostic;
3232
import org.jspecify.annotations.Nullable;
3333

34-
public class ElementConverter {
34+
class ElementConverter {
3535

3636
private static final PayloadType EMPTY_PAYLOAD =
3737
new PayloadType(true, "", "Void", "dev.restate.sdk.common.Serde.VOID");
@@ -47,49 +47,29 @@ public ElementConverter(Messager messager, Elements elements, Types types) {
4747
this.types = types;
4848
}
4949

50-
public Service fromTypeElement(TypeElement element) {
50+
Service fromTypeElement(MetaRestateAnnotation metaAnnotation, TypeElement element) {
5151
validateType(element);
5252

53-
dev.restate.sdk.annotation.Service serviceAnnotation =
54-
element.getAnnotation(dev.restate.sdk.annotation.Service.class);
55-
dev.restate.sdk.annotation.VirtualObject virtualObjectAnnotation =
56-
element.getAnnotation(dev.restate.sdk.annotation.VirtualObject.class);
57-
dev.restate.sdk.annotation.Workflow workflowAnnotation =
58-
element.getAnnotation(dev.restate.sdk.annotation.Workflow.class);
59-
boolean isAnnotatedWithService = serviceAnnotation != null;
60-
boolean isAnnotatedWithVirtualObject = virtualObjectAnnotation != null;
61-
boolean isAnnotatedWithWorkflow = workflowAnnotation != null;
62-
63-
// Should be guaranteed by the caller
64-
assert isAnnotatedWithWorkflow || isAnnotatedWithVirtualObject || isAnnotatedWithService;
65-
66-
// Check there's no more than one annotation
67-
if (!Boolean.logicalXor(
68-
isAnnotatedWithService,
69-
Boolean.logicalXor(isAnnotatedWithWorkflow, isAnnotatedWithVirtualObject))) {
70-
messager.printMessage(
71-
Diagnostic.Kind.ERROR,
72-
"The type can be annotated only with one annotation between @VirtualObject, @Workflow and @Service",
73-
element);
74-
}
75-
76-
ServiceType type =
77-
isAnnotatedWithWorkflow
78-
? ServiceType.WORKFLOW
79-
: isAnnotatedWithService ? ServiceType.SERVICE : ServiceType.VIRTUAL_OBJECT;
53+
// Find annotation mirror
54+
AnnotationMirror metaAnnotationMirror =
55+
element.getAnnotationMirrors().stream()
56+
.filter(
57+
a ->
58+
a.getAnnotationType()
59+
.asElement()
60+
.equals(metaAnnotation.getAnnotationTypeElement()))
61+
.findFirst()
62+
.orElseThrow(
63+
() ->
64+
new IllegalStateException(
65+
"Cannot find the annotation mirror for meta annotation "
66+
+ metaAnnotation.getAnnotationTypeElement().getQualifiedName()));
8067

8168
// Infer names
82-
8369
CharSequence targetPkg = elements.getPackageOf(element).getQualifiedName();
8470
CharSequence targetFqcn = element.getQualifiedName();
85-
86-
String serviceName =
87-
isAnnotatedWithService
88-
? serviceAnnotation.name()
89-
: isAnnotatedWithVirtualObject
90-
? virtualObjectAnnotation.name()
91-
: workflowAnnotation.name();
92-
if (serviceName.isEmpty()) {
71+
String serviceName = metaAnnotation.resolveName(metaAnnotationMirror);
72+
if (serviceName == null || serviceName.isEmpty()) {
9373
// Use simple class name, flattening subclasses names
9474
serviceName =
9575
targetFqcn.toString().substring(targetPkg.length()).replaceAll(Pattern.quote("."), "");
@@ -105,7 +85,9 @@ public Service fromTypeElement(TypeElement element) {
10585
|| e.getAnnotation(Workflow.class) != null
10686
|| e.getAnnotation(Exclusive.class) != null
10787
|| e.getAnnotation(Shared.class) != null)
108-
.map(e -> fromExecutableElement(type, ((ExecutableElement) e)))
88+
.map(
89+
e ->
90+
fromExecutableElement(metaAnnotation.getServiceType(), ((ExecutableElement) e)))
10991
.collect(Collectors.toList());
11092

11193
if (handlers.isEmpty()) {
@@ -118,7 +100,7 @@ public Service fromTypeElement(TypeElement element) {
118100
.withTargetPkg(targetPkg)
119101
.withTargetFqcn(targetFqcn)
120102
.withServiceName(serviceName)
121-
.withServiceType(type)
103+
.withServiceType(metaAnnotation.getServiceType())
122104
.withHandlers(handlers)
123105
.validateAndBuild();
124106
} catch (Exception e) {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
2+
//
3+
// This file is part of the Restate Java SDK,
4+
// which is released under the MIT license.
5+
//
6+
// You can find a copy of the license in file LICENSE in the root
7+
// directory of this repository or package, or at
8+
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
9+
package dev.restate.sdk.gen;
10+
11+
import dev.restate.sdk.common.ServiceType;
12+
import java.util.Map;
13+
import javax.lang.model.element.*;
14+
import org.jspecify.annotations.Nullable;
15+
16+
class MetaRestateAnnotation {
17+
18+
private final TypeElement annotation;
19+
private final ServiceType serviceType;
20+
21+
private MetaRestateAnnotation(TypeElement annotation, ServiceType serviceType) {
22+
this.annotation = annotation;
23+
this.serviceType = serviceType;
24+
}
25+
26+
TypeElement getAnnotationTypeElement() {
27+
return annotation;
28+
}
29+
30+
ServiceType getServiceType() {
31+
return serviceType;
32+
}
33+
34+
@Nullable String resolveName(AnnotationMirror mirror) {
35+
for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry :
36+
mirror.getElementValues().entrySet()) {
37+
if (entry.getKey().getSimpleName().contentEquals("name")) {
38+
// Got a match, this is the name of the service
39+
return (String) entry.getValue().getValue();
40+
}
41+
}
42+
// No name parameter found!
43+
return null;
44+
}
45+
46+
static @Nullable MetaRestateAnnotation metaRestateAnnotationOrNull(TypeElement elem) {
47+
if (elem.getQualifiedName().toString().equals("dev.restate.sdk.annotation.Service")
48+
|| elem.getAnnotation(dev.restate.sdk.annotation.Service.class) != null) {
49+
return new MetaRestateAnnotation(elem, ServiceType.SERVICE);
50+
}
51+
if (elem.getQualifiedName().toString().equals("dev.restate.sdk.annotation.VirtualObject")
52+
|| elem.getAnnotation(dev.restate.sdk.annotation.VirtualObject.class) != null) {
53+
return new MetaRestateAnnotation(elem, ServiceType.VIRTUAL_OBJECT);
54+
}
55+
if (elem.getQualifiedName().toString().equals("dev.restate.sdk.annotation.Workflow")
56+
|| elem.getAnnotation(dev.restate.sdk.annotation.Workflow.class) != null) {
57+
return new MetaRestateAnnotation(elem, ServiceType.WORKFLOW);
58+
}
59+
return null;
60+
}
61+
}

sdk-api-gen/src/main/java/dev/restate/sdk/gen/ServiceProcessor.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,7 @@
2727
import javax.tools.FileObject;
2828
import javax.tools.StandardLocation;
2929

30-
@SupportedAnnotationTypes({
31-
"dev.restate.sdk.annotation.Service",
32-
"dev.restate.sdk.annotation.Workflow",
33-
"dev.restate.sdk.annotation.VirtualObject"
34-
})
30+
@SupportedAnnotationTypes("*")
3531
@SupportedSourceVersion(SourceVersion.RELEASE_11)
3632
public class ServiceProcessor extends AbstractProcessor {
3733

@@ -97,9 +93,19 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
9793
// Parsing phase
9894
List<Map.Entry<Element, Service>> parsedServices =
9995
annotations.stream()
100-
.flatMap(annotation -> roundEnv.getElementsAnnotatedWith(annotation).stream())
101-
.filter(e -> e.getKind().isClass() || e.getKind().isInterface())
102-
.map(e -> Map.entry((Element) e, converter.fromTypeElement((TypeElement) e)))
96+
.map(MetaRestateAnnotation::metaRestateAnnotationOrNull)
97+
.filter(Objects::nonNull)
98+
.flatMap(
99+
metaAnnotation ->
100+
roundEnv
101+
.getElementsAnnotatedWith(metaAnnotation.getAnnotationTypeElement())
102+
.stream()
103+
.filter(e -> e.getKind().isClass() || e.getKind().isInterface())
104+
.map(
105+
e ->
106+
Map.entry(
107+
(Element) e,
108+
converter.fromTypeElement(metaAnnotation, (TypeElement) e))))
103109
.collect(Collectors.toList());
104110

105111
Filer filer = processingEnv.getFiler();

sdk-common/src/main/java/dev/restate/sdk/annotation/Service.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,16 @@
88
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
99
package dev.restate.sdk.annotation;
1010

11-
import java.lang.annotation.ElementType;
12-
import java.lang.annotation.Retention;
13-
import java.lang.annotation.RetentionPolicy;
14-
import java.lang.annotation.Target;
11+
import java.lang.annotation.*;
1512

1613
/**
1714
* Annotation to define a class/interface as Restate Service. This triggers the code generation of
1815
* the related Client class and the {@link
1916
* dev.restate.sdk.common.syscalls.ServiceDefinitionFactory}.
2017
*/
2118
@Target(ElementType.TYPE)
22-
@Retention(RetentionPolicy.SOURCE)
19+
@Retention(RetentionPolicy.RUNTIME)
20+
@Documented
2321
public @interface Service {
2422
/**
2523
* Name of the Service for Restate. If not provided, it will be the simple class name of the

sdk-common/src/main/java/dev/restate/sdk/annotation/VirtualObject.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,16 @@
88
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
99
package dev.restate.sdk.annotation;
1010

11-
import java.lang.annotation.ElementType;
12-
import java.lang.annotation.Retention;
13-
import java.lang.annotation.RetentionPolicy;
14-
import java.lang.annotation.Target;
11+
import java.lang.annotation.*;
1512

1613
/**
1714
* Annotation to define a class/interface as Restate VirtualObject. This triggers the code
1815
* generation of the related Client class and the {@link
1916
* dev.restate.sdk.common.syscalls.ServiceDefinitionFactory}.
2017
*/
2118
@Target(ElementType.TYPE)
22-
@Retention(RetentionPolicy.SOURCE)
19+
@Retention(RetentionPolicy.RUNTIME)
20+
@Documented
2321
public @interface VirtualObject {
2422
/**
2523
* Name of the VirtualObject for Restate. If not provided, it will be the simple class name of the

sdk-common/src/main/java/dev/restate/sdk/annotation/Workflow.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@
88
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
99
package dev.restate.sdk.annotation;
1010

11-
import java.lang.annotation.ElementType;
12-
import java.lang.annotation.Retention;
13-
import java.lang.annotation.RetentionPolicy;
14-
import java.lang.annotation.Target;
11+
import java.lang.annotation.*;
1512

1613
/**
1714
* Annotation to define a class/interface as Restate Workflow. This triggers the code generation of
@@ -20,7 +17,8 @@
2017
* workflow, you must annotate one of its methods too as {@link Workflow}.
2118
*/
2219
@Target({ElementType.METHOD, ElementType.TYPE})
23-
@Retention(RetentionPolicy.SOURCE)
20+
@Retention(RetentionPolicy.RUNTIME)
21+
@Documented
2422
public @interface Workflow {
2523
/**
2624
* Name of the Workflow for Restate. If not provided, it will be the simple class name of the
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
2+
//
3+
// This file is part of the Restate Java SDK,
4+
// which is released under the MIT license.
5+
//
6+
// You can find a copy of the license in file LICENSE in the root
7+
// directory of this repository or package, or at
8+
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
9+
package dev.restate.sdk.springboot;
10+
11+
import dev.restate.sdk.annotation.Service;
12+
import java.lang.annotation.ElementType;
13+
import java.lang.annotation.Retention;
14+
import java.lang.annotation.RetentionPolicy;
15+
import java.lang.annotation.Target;
16+
17+
/**
18+
* Proxy annotation for {@link Service}, to avoid naming clashes with Spring's built in annotations
19+
*
20+
* @see Service
21+
*/
22+
@Target(ElementType.TYPE)
23+
@Retention(RetentionPolicy.RUNTIME)
24+
@Service
25+
@RestateComponent
26+
public @interface RestateService {
27+
/**
28+
* Name of the Service for Restate. If not provided, it will be the simple class name of the
29+
* annotated element.
30+
*/
31+
String name() default "";
32+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
2+
//
3+
// This file is part of the Restate Java SDK,
4+
// which is released under the MIT license.
5+
//
6+
// You can find a copy of the license in file LICENSE in the root
7+
// directory of this repository or package, or at
8+
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
9+
package dev.restate.sdk.springboot;
10+
11+
import dev.restate.sdk.annotation.Service;
12+
import dev.restate.sdk.annotation.VirtualObject;
13+
import java.lang.annotation.ElementType;
14+
import java.lang.annotation.Retention;
15+
import java.lang.annotation.RetentionPolicy;
16+
import java.lang.annotation.Target;
17+
18+
/**
19+
* Proxy annotation for {@link VirtualObject}.
20+
*
21+
* @see Service
22+
*/
23+
@Target(ElementType.TYPE)
24+
@Retention(RetentionPolicy.RUNTIME)
25+
@VirtualObject
26+
@RestateComponent
27+
public @interface RestateVirtualObject {
28+
/**
29+
* Name of the VirtualObject for Restate. If not provided, it will be the simple class name of the
30+
* annotated element.
31+
*/
32+
String name() default "";
33+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
2+
//
3+
// This file is part of the Restate Java SDK,
4+
// which is released under the MIT license.
5+
//
6+
// You can find a copy of the license in file LICENSE in the root
7+
// directory of this repository or package, or at
8+
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
9+
package dev.restate.sdk.springboot;
10+
11+
import dev.restate.sdk.annotation.Service;
12+
import dev.restate.sdk.annotation.Workflow;
13+
import java.lang.annotation.ElementType;
14+
import java.lang.annotation.Retention;
15+
import java.lang.annotation.RetentionPolicy;
16+
import java.lang.annotation.Target;
17+
18+
/**
19+
* Proxy annotation for {@link Workflow}.
20+
*
21+
* @see Service
22+
*/
23+
@Target(ElementType.TYPE)
24+
@Retention(RetentionPolicy.RUNTIME)
25+
@Workflow
26+
@RestateComponent
27+
public @interface RestateWorkflow {
28+
/**
29+
* Name of the Workflow for Restate. If not provided, it will be the simple class name of the
30+
* annotated element.
31+
*/
32+
String name() default "";
33+
}

sdk-spring-boot-starter/src/test/java/dev/restate/sdk/springboot/Greeter.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,9 @@
1010

1111
import dev.restate.sdk.Context;
1212
import dev.restate.sdk.annotation.Handler;
13-
import dev.restate.sdk.annotation.Service;
1413
import org.springframework.beans.factory.annotation.Value;
1514

16-
@RestateComponent
17-
@Service
15+
@RestateService(name = "greeter")
1816
public class Greeter {
1917

2018
@Value("${greetingPrefix}")

0 commit comments

Comments
 (0)