mirror of
https://github.com/temporalio/skill-temporal-developer.git
synced 2026-09-14 13:52:58 +08:00
0c8586b4c2
* Add Java SDK reference files (11 files) Create complete Java reference documentation covering: - java.md: Entry point with quick start tutorial, key concepts - patterns.md: 17 patterns (signals, queries, updates, child workflows, saga, cancellation scopes, heartbeating, etc.) - determinism.md: Safe alternatives table, forbidden operations - determinism-protection.md: Convention-based enforcement (no sandbox) - error-handling.md: ApplicationFailure, retry/timeout config - gotchas.md: Non-deterministic operations, cancellation, heartbeating - testing.md: TestWorkflowEnvironment, Mockito mocking, replay testing - versioning.md: Workflow.getVersion(), worker versioning - data-handling.md: Jackson, PayloadConverter, encryption, search attributes - observability.md: SLF4J logging, Micrometer metrics - advanced-features.md: Schedules, async completion, worker tuning Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix Java alignment issues from self-review - Reduce gotchas.md Non-Deterministic Operations from ~94 lines to ~12 (reference determinism.md instead of duplicating) - Remove Workflow Failure Exception Types duplication from error-handling.md (keep only in advanced-features.md) - Expand versioning.md Worker Versioning with Key Concepts, PINNED vs AUTO_UPGRADE, Deployment Strategies subsections - Fix section names to match Python reference style: Activity Heartbeat Details, Handling Activity Errors, Retry Policy Configuration, Workflow Test Environment, Mocking Activities, Workflow Replay Testing - Reduce data-handling.md Payload Encryption verbosity - Reduce observability.md Logger Customization verbosity - Reduce testing.md to single approach per section - Rename determinism.md "Convention-Based Enforcement" to "SDK Protection" - Fix handler guidance in patterns.md to match Python Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix correctness issues in Java reference files - patterns.md: Fix Queries section — ActivityStub → typed interface (Workflow.newActivityStub returns the typed interface, not ActivityStub) - data-handling.md: Add missing ProtobufPayloadConverter to default converter chain (4th of 5 converters) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add Java to SKILL.md and core/determinism.md - SKILL.md: Add "Temporal Java" trigger phrase, update Overview to list Java, add Java entry to Getting Started references - core/determinism.md: Add Java entry to SDK Protection Mechanisms (no sandbox, convention-based, NonDeterministicException at replay) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Apply manual editorial fixes to Java references - java.md: Remove "Understanding Replay" section (covered by Overview), simplify File Organization note (no sandbox rationale) - gotchas.md: Move Heartbeating before Cancellation, make Wrong Retry Classification brief with reference (not inline examples) - error-handling.md: Remove editorializing from Workflow Failure note - determinism-protection.md: Remove cross-language comparison paragraph (state Java's approach on its own terms) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add temporal-workflowcheck static analysis to Java determinism docs - determinism-protection.md: Add "Static Analysis with temporal-workflowcheck" section with Gradle/Maven setup, manual run, and suppression instructions. Beta warning included. - determinism.md: Update overview and SDK Protection to reference workflowcheck - core/determinism.md: Update Java entry in SDK Protection Mechanisms Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Integrate feedback from Go PR into Java patterns - Updates: Add validator note — validators must not mutate state or block (matches note added to Python, TypeScript, Go, and core) - Saga Pattern: Use Workflow.newDetachedCancellationScope() for compensations so they execute even if the workflow is cancelled (mirrors Go's workflow.NewDisconnectedContext pattern) Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * docs: add @WorkflowInit description to java.md Key Concepts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * mark java as supported * Apply suggestions from code review Co-authored-by: Brian Strauch <brian@brianstrauch.com> * strongly recommend java 21+ * Softened stance on static checker and replay testing. * address python/typescript sandboxing comment --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Brian Strauch <brian.strauch@temporal.io> Co-authored-by: Brian Strauch <brian@brianstrauch.com>
289 lines
8.8 KiB
Markdown
289 lines
8.8 KiB
Markdown
# Java SDK Data Handling
|
|
|
|
## Overview
|
|
|
|
The Java SDK uses data converters to serialize/deserialize workflow inputs, outputs, and activity parameters. The `DataConverter` interface controls how values are converted to and from Temporal `Payload` protobufs.
|
|
|
|
## Default Data Converter
|
|
|
|
`DefaultDataConverter` applies converters in order, using the first that accepts the value:
|
|
|
|
1. `NullPayloadConverter` — `null` values
|
|
2. `ByteArrayPayloadConverter` — `byte[]` as raw binary
|
|
3. `ProtobufJsonPayloadConverter` — Protobuf `Message` instances as JSON
|
|
4. `ProtobufPayloadConverter` — Protobuf `Message` instances as binary
|
|
5. `JacksonJsonPayloadConverter` — Everything else via Jackson `ObjectMapper`
|
|
|
|
## Jackson Integration
|
|
|
|
Use `JacksonJsonPayloadConverter` with a custom `ObjectMapper` for advanced serialization (e.g., Java 8 time module, custom serializers):
|
|
|
|
```java
|
|
ObjectMapper mapper = new ObjectMapper()
|
|
.registerModule(new JavaTimeModule())
|
|
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
|
|
|
DefaultDataConverter converter = DefaultDataConverter.newDefaultInstance()
|
|
.withPayloadConverterOverrides(
|
|
new JacksonJsonPayloadConverter(mapper)
|
|
);
|
|
|
|
WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
|
|
WorkflowClient client = WorkflowClient.newInstance(
|
|
service,
|
|
WorkflowClientOptions.newBuilder()
|
|
.setDataConverter(converter)
|
|
.build()
|
|
);
|
|
```
|
|
|
|
## Custom Data Converter
|
|
|
|
Implement `PayloadConverter` for custom serialization:
|
|
|
|
```java
|
|
public class MyCustomPayloadConverter implements PayloadConverter {
|
|
@Override
|
|
public String getEncodingType() {
|
|
return "json/my-custom";
|
|
}
|
|
|
|
@Override
|
|
public Optional<Payload> toData(Object value) throws DataConverterException {
|
|
// Return Optional.empty() if this converter doesn't handle the type
|
|
if (!(value instanceof MyCustomType)) {
|
|
return Optional.empty();
|
|
}
|
|
// Serialize to Payload
|
|
byte[] data = serialize(value);
|
|
return Optional.of(
|
|
Payload.newBuilder()
|
|
.putMetadata("encoding", ByteString.copyFromUtf8(getEncodingType()))
|
|
.setData(ByteString.copyFrom(data))
|
|
.build()
|
|
);
|
|
}
|
|
|
|
@Override
|
|
public <T> T fromData(Payload content, Class<T> valueClass, Type valueType)
|
|
throws DataConverterException {
|
|
// Deserialize from Payload
|
|
return deserialize(content.getData().toByteArray(), valueClass);
|
|
}
|
|
}
|
|
```
|
|
|
|
Override specific converters in the default chain:
|
|
|
|
```java
|
|
DefaultDataConverter converter = DefaultDataConverter.newDefaultInstance()
|
|
.withPayloadConverterOverrides(new MyCustomPayloadConverter());
|
|
```
|
|
|
|
## Composition of Payload Converters
|
|
|
|
`DefaultDataConverter` holds a list of `PayloadConverter` instances tried in order. The first converter whose `toData()` returns a non-empty `Optional` wins. When using `withPayloadConverterOverrides()`, converters with matching encoding types replace existing ones.
|
|
|
|
```java
|
|
DefaultDataConverter converter = DefaultDataConverter.newDefaultInstance()
|
|
.withPayloadConverterOverrides(
|
|
new MyCustomPayloadConverter(), // encoding: "json/my-custom"
|
|
new JacksonJsonPayloadConverter(mapper) // replaces default Jackson converter
|
|
);
|
|
```
|
|
|
|
## Protobuf Support
|
|
|
|
Protobuf messages are handled by `ProtobufJsonPayloadConverter` (enabled by default). It serializes `com.google.protobuf.Message` instances as JSON for human readability in the Temporal UI.
|
|
|
|
```java
|
|
// Protobuf messages work out of the box as workflow/activity params
|
|
@WorkflowInterface
|
|
public interface MyWorkflow {
|
|
@WorkflowMethod
|
|
MyProtoResult run(MyProtoInput input);
|
|
}
|
|
```
|
|
|
|
For binary protobuf encoding instead of JSON, use `ProtobufPayloadConverter`:
|
|
|
|
```java
|
|
DefaultDataConverter converter = DefaultDataConverter.newDefaultInstance()
|
|
.withPayloadConverterOverrides(new ProtobufPayloadConverter());
|
|
```
|
|
|
|
## Payload Encryption
|
|
|
|
Use `PayloadCodec` with `CodecDataConverter` to encrypt/compress payloads:
|
|
|
|
```java
|
|
public class EncryptionCodec implements PayloadCodec {
|
|
private final SecretKey key;
|
|
|
|
public EncryptionCodec(SecretKey key) {
|
|
this.key = key;
|
|
}
|
|
|
|
@Override
|
|
public List<Payload> encode(List<Payload> payloads) {
|
|
return payloads.stream().map(payload -> {
|
|
// Encrypt payload.toByteArray() using your chosen algorithm (e.g., AES/GCM)
|
|
byte[] encrypted = encryptBytes(payload.toByteArray(), key);
|
|
return Payload.newBuilder()
|
|
.putMetadata("encoding", ByteString.copyFromUtf8("binary/encrypted"))
|
|
.setData(ByteString.copyFrom(encrypted))
|
|
.build();
|
|
}).collect(Collectors.toList());
|
|
}
|
|
|
|
@Override
|
|
public List<Payload> decode(List<Payload> payloads) {
|
|
return payloads.stream().map(payload -> {
|
|
String encoding = payload.getMetadataOrDefault(
|
|
"encoding", ByteString.EMPTY).toStringUtf8();
|
|
if (!"binary/encrypted".equals(encoding)) return payload;
|
|
// Decrypt and reconstruct the original Payload
|
|
byte[] decrypted = decryptBytes(payload.getData().toByteArray(), key);
|
|
return Payload.parseFrom(decrypted);
|
|
}).collect(Collectors.toList());
|
|
}
|
|
}
|
|
```
|
|
|
|
Apply the codec to the client:
|
|
|
|
```java
|
|
CodecDataConverter codecDataConverter = new CodecDataConverter(
|
|
DefaultDataConverter.newDefaultInstance(),
|
|
Collections.singletonList(new EncryptionCodec(secretKey))
|
|
);
|
|
|
|
WorkflowClient client = WorkflowClient.newInstance(
|
|
service,
|
|
WorkflowClientOptions.newBuilder()
|
|
.setDataConverter(codecDataConverter)
|
|
.build()
|
|
);
|
|
```
|
|
|
|
## Search Attributes
|
|
|
|
Custom searchable fields for workflow visibility.
|
|
|
|
```java
|
|
import io.temporal.common.SearchAttributeKey;
|
|
import io.temporal.common.SearchAttributes;
|
|
|
|
// Define typed search attribute keys
|
|
static final SearchAttributeKey<String> ORDER_ID =
|
|
SearchAttributeKey.forKeyword("OrderId");
|
|
static final SearchAttributeKey<String> ORDER_STATUS =
|
|
SearchAttributeKey.forKeyword("OrderStatus");
|
|
static final SearchAttributeKey<Double> ORDER_TOTAL =
|
|
SearchAttributeKey.forDouble("OrderTotal");
|
|
static final SearchAttributeKey<OffsetDateTime> CREATED_AT =
|
|
SearchAttributeKey.forOffsetDateTime("CreatedAt");
|
|
|
|
// Set at workflow start
|
|
WorkflowOptions options = WorkflowOptions.newBuilder()
|
|
.setWorkflowId("order-" + orderId)
|
|
.setTaskQueue("orders")
|
|
.setTypedSearchAttributes(
|
|
SearchAttributes.newBuilder()
|
|
.set(ORDER_ID, orderId)
|
|
.set(ORDER_STATUS, "pending")
|
|
.set(ORDER_TOTAL, 99.99)
|
|
.set(CREATED_AT, OffsetDateTime.now())
|
|
.build()
|
|
)
|
|
.build();
|
|
```
|
|
|
|
Upsert during workflow execution:
|
|
|
|
```java
|
|
@WorkflowInterface
|
|
public interface OrderWorkflow {
|
|
@WorkflowMethod
|
|
String run(Order order);
|
|
}
|
|
|
|
public class OrderWorkflowImpl implements OrderWorkflow {
|
|
static final SearchAttributeKey<String> ORDER_STATUS =
|
|
SearchAttributeKey.forKeyword("OrderStatus");
|
|
|
|
@Override
|
|
public String run(Order order) {
|
|
// ... process order ...
|
|
|
|
Workflow.upsertTypedSearchAttributes(
|
|
ORDER_STATUS.valueSet("completed")
|
|
);
|
|
return "done";
|
|
}
|
|
}
|
|
```
|
|
|
|
### Querying Workflows by Search Attributes
|
|
|
|
```java
|
|
ListWorkflowExecutionsRequest request = ListWorkflowExecutionsRequest.newBuilder()
|
|
.setNamespace("default")
|
|
.setQuery("OrderStatus = 'processing' OR OrderStatus = 'pending'")
|
|
.build();
|
|
```
|
|
|
|
## Workflow Memo
|
|
|
|
Store arbitrary metadata with workflows (not searchable).
|
|
|
|
```java
|
|
// Set memo at workflow start
|
|
WorkflowOptions options = WorkflowOptions.newBuilder()
|
|
.setWorkflowId("order-" + orderId)
|
|
.setTaskQueue("orders")
|
|
.setMemo(Map.of(
|
|
"customer_name", order.getCustomerName(),
|
|
"notes", "Priority customer"
|
|
))
|
|
.build();
|
|
```
|
|
|
|
```java
|
|
// Read memo from workflow
|
|
@Override
|
|
public String run(Order order) {
|
|
String notes = Workflow.getMemo("notes", String.class);
|
|
// ...
|
|
}
|
|
```
|
|
|
|
## Deterministic APIs for Values
|
|
|
|
Use these APIs within workflows for deterministic values:
|
|
|
|
```java
|
|
@Override
|
|
public String run() {
|
|
// Deterministic UUID (same on replay)
|
|
String uniqueId = Workflow.randomUUID().toString();
|
|
|
|
// Deterministic random (same on replay)
|
|
Random rng = Workflow.newRandom();
|
|
int value = rng.nextInt(100);
|
|
|
|
// Deterministic current time (same on replay)
|
|
long now = Workflow.currentTimeMillis();
|
|
|
|
return uniqueId;
|
|
}
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. Use Jackson `ObjectMapper` customization for complex serialization needs
|
|
2. Keep payloads small — see `references/core/gotchas.md` for limits
|
|
3. Encrypt sensitive data with `PayloadCodec` and `CodecDataConverter`
|
|
4. Use POJOs or Protobuf messages for workflow/activity parameters
|
|
5. Use `Workflow.randomUUID()`, `Workflow.newRandom()`, and `Workflow.currentTimeMillis()` for deterministic values
|