Files
Donald Pinckney 0c8586b4c2 Add Java SDK support (#42)
* 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>
2026-04-02 17:07:33 -04:00

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