Skip to content

Commit a3d0f85

Browse files
authored
Support custom serialization (#21)
* added the ability to add custom serialization * added JacksonStateSerializer * improved doc structure * changed doc structure * better doc
1 parent f17bf65 commit a3d0f85

40 files changed

+610
-187
lines changed

core/src/main/java/org/sterl/spring/persistent_tasks/api/TaskId.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import java.io.Serializable;
44
import java.time.Duration;
55
import java.time.OffsetDateTime;
6+
import java.util.Map;
7+
import java.util.concurrent.ConcurrentHashMap;
68

79
import com.github.f4b6a3.uuid.UuidCreator;
810

@@ -13,6 +15,15 @@
1315
* Represents the ID of a persistentTask, which is currently not running.
1416
*/
1517
public record TaskId<T extends Serializable>(String name) implements Serializable {
18+
19+
@SuppressWarnings("rawtypes")
20+
private static final Map<String, TaskId> CACHE = new ConcurrentHashMap<>();
21+
22+
@SuppressWarnings("unchecked")
23+
public static <T extends Serializable> TaskId<T> of(String taskId) {
24+
if (taskId == null || taskId.isBlank()) return null;
25+
return CACHE.computeIfAbsent(taskId, s -> new TaskId<>(s));
26+
}
1627

1728
public TriggerBuilder<T> newTrigger() {
1829
return new TriggerBuilder<>(this);
@@ -36,11 +47,6 @@ public TriggerRequest<T> newUniqueTrigger(T state) {
3647
.build();
3748
}
3849

39-
public static TaskId<Serializable> of(String taskId) {
40-
if (taskId == null || taskId.isBlank()) return null;
41-
return new TaskId<>(taskId);
42-
}
43-
4450
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
4551
public static class TriggerBuilder<T extends Serializable> {
4652
private final TaskId<T> taskId;

core/src/main/java/org/sterl/spring/persistent_tasks/api/TriggerKey.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ public static TriggerKey of(@Nullable String id, TaskId<? extends Serializable>
3131
return new TriggerKey(id, taskId.name());
3232
}
3333

34-
public TaskId<Serializable> toTaskId() {
34+
public <T extends Serializable> TaskId<T> toTaskId() {
3535
if (taskName == null) return null;
36-
return new TaskId<>(taskName);
36+
return TaskId.of(taskName);
3737
}
38+
3839
/**
3940
* Builds a trigger for the given persistentTask name
4041
*/
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package org.sterl.spring.persistent_tasks.api.task;
2+
3+
import java.io.Serializable;
4+
5+
import org.springframework.lang.NonNull;
6+
import org.sterl.spring.persistent_tasks.api.TaskId;
7+
8+
import com.fasterxml.jackson.core.JsonProcessingException;
9+
import com.fasterxml.jackson.databind.ObjectMapper;
10+
11+
import lombok.RequiredArgsConstructor;
12+
13+
/**
14+
* Default implementation to use jackson instead java serialization for a task state.
15+
*
16+
* @param <T> the type of the state for the deserialization
17+
*/
18+
@RequiredArgsConstructor
19+
public class JacksonStateSerializer<T extends Serializable> implements StateSerializer<T> {
20+
21+
private final ObjectMapper mapper;
22+
private final Class<T> clazz;
23+
24+
@Override
25+
public byte[] serialize(@NonNull TaskId<T> id, @NonNull T obj)
26+
throws SerializationFailedException{
27+
28+
try {
29+
return mapper.writeValueAsBytes(obj);
30+
} catch (JsonProcessingException e) {
31+
throw new SerializationFailedException(obj, e);
32+
}
33+
}
34+
35+
@Override
36+
public T deserialize(@NonNull TaskId<T> id, @NonNull byte[] bytes)
37+
throws DeSerializationFailedException {
38+
try {
39+
return mapper.readValue(bytes, clazz);
40+
} catch (Exception e) {
41+
throw new DeSerializationFailedException(bytes, e);
42+
}
43+
}
44+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.sterl.spring.persistent_tasks.api.task;
2+
3+
import java.io.Serializable;
4+
5+
/**
6+
* /**
7+
* Any task may provide an own serialization class.
8+
*
9+
* @since 2.3.0
10+
* @param <T> the type of the task state
11+
*/
12+
public interface SerializationProvider<T extends Serializable> {
13+
StateSerializer<T> getSerializer();
14+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package org.sterl.spring.persistent_tasks.api.task;
2+
3+
import java.io.Serializable;
4+
5+
import org.springframework.lang.NonNull;
6+
import org.sterl.spring.persistent_tasks.api.TaskId;
7+
import org.sterl.spring.persistent_tasks.exception.SpringPersistentTaskException;
8+
9+
/**
10+
* Interface for a state serialization of task.
11+
*
12+
* @since 2.3.0
13+
* @param <T> the state type
14+
*/
15+
public interface StateSerializer<T extends Serializable> {
16+
17+
byte[] serialize(@NonNull TaskId<T> id, @NonNull T obj) throws SerializationFailedException;
18+
19+
T deserialize(@NonNull TaskId<T> id, @NonNull byte[] bytes) throws DeSerializationFailedException;
20+
21+
class DeSerializationFailedException extends SpringPersistentTaskException {
22+
private static final long serialVersionUID = 1L;
23+
24+
public DeSerializationFailedException(byte[] bytes, Exception e) {
25+
super("Failed to deserialize state of length " + bytes.length, bytes, e);
26+
}
27+
}
28+
29+
class SerializationFailedException extends SpringPersistentTaskException {
30+
private static final long serialVersionUID = 1L;
31+
32+
public SerializationFailedException(Serializable obj, Exception e) {
33+
super("Failed to serialize state " + obj.getClass(), obj, e);
34+
}
35+
}
36+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,36 @@
11
package org.sterl.spring.persistent_tasks.config;
22

3+
import java.io.Serializable;
4+
35
import org.springframework.beans.factory.config.BeanDefinition;
46
import org.springframework.boot.autoconfigure.AutoConfigurationPackage;
7+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
8+
import org.springframework.context.annotation.Bean;
59
import org.springframework.context.annotation.ComponentScan;
10+
import org.springframework.context.annotation.Configuration;
611
import org.springframework.context.annotation.Role;
712
import org.springframework.scheduling.annotation.EnableAsync;
813
import org.springframework.scheduling.annotation.EnableScheduling;
914
import org.sterl.spring.persistent_tasks.EnableSpringPersistentTasks;
15+
import org.sterl.spring.persistent_tasks.api.task.StateSerializer;
16+
import org.sterl.spring.persistent_tasks.trigger.component.DefaultStateSerializer;
17+
18+
import lombok.extern.slf4j.Slf4j;
1019

1120
@EnableScheduling
1221
@EnableAsync
1322
@AutoConfigurationPackage(basePackageClasses = EnableSpringPersistentTasks.class)
1423
@ComponentScan(basePackageClasses = EnableSpringPersistentTasks.class)
1524
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
25+
@Configuration
26+
@Slf4j
1627
public class SpringPersistentTasksConfig {
1728

29+
@Bean
30+
@ConditionalOnMissingBean(DefaultStateSerializer.class)
31+
StateSerializer<Serializable> defaultStateSerializer() {
32+
log.info("Using java serialization for task states.",
33+
DefaultStateSerializer.class.getSimpleName());
34+
return new DefaultStateSerializer();
35+
}
1836
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.sterl.spring.persistent_tasks.history.api;
2+
3+
import org.springframework.lang.NonNull;
4+
import org.springframework.stereotype.Component;
5+
import org.sterl.spring.persistent_tasks.api.Trigger;
6+
import org.sterl.spring.persistent_tasks.history.model.CompletedTriggerEntity;
7+
import org.sterl.spring.persistent_tasks.shared.ExtendetConvert;
8+
import org.sterl.spring.persistent_tasks.shared.converter.ToTrigger;
9+
10+
import lombok.RequiredArgsConstructor;
11+
12+
@Component
13+
@RequiredArgsConstructor
14+
public class FromCompletedTriggerEntityConverter implements ExtendetConvert<CompletedTriggerEntity, Trigger> {
15+
16+
private final ToTrigger toTrigger;
17+
18+
@NonNull
19+
@Override
20+
public Trigger convert(@NonNull CompletedTriggerEntity source) {
21+
var result = toTrigger.convert(source);
22+
result.setId(source.getId());
23+
result.setInstanceId(source.getId());
24+
return result;
25+
}
26+
}

core/src/main/java/org/sterl/spring/persistent_tasks/history/api/HistoryConverter.java

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,12 @@
22

33
import org.springframework.lang.NonNull;
44
import org.springframework.lang.Nullable;
5-
import org.sterl.spring.persistent_tasks.api.Trigger;
65
import org.sterl.spring.persistent_tasks.api.HistoryTrigger;
7-
import org.sterl.spring.persistent_tasks.history.model.CompletedTriggerEntity;
86
import org.sterl.spring.persistent_tasks.history.model.HistoryTriggerEntity;
97
import org.sterl.spring.persistent_tasks.shared.ExtendetConvert;
10-
import org.sterl.spring.persistent_tasks.shared.converter.ToTrigger;
118

129
interface HistoryConverter {
1310

14-
enum FromLastTriggerStateEntity implements ExtendetConvert<CompletedTriggerEntity, Trigger> {
15-
INSTANCE;
16-
17-
@NonNull
18-
@Override
19-
public Trigger convert(@NonNull CompletedTriggerEntity source) {
20-
var result = ToTrigger.INSTANCE.convert(source);
21-
result.setId(source.getId());
22-
result.setInstanceId(source.getId());
23-
return result;
24-
}
25-
}
26-
2711
enum ToHistoryTrigger implements ExtendetConvert<HistoryTriggerEntity, HistoryTrigger> {
2812
INSTANCE;
2913

@@ -41,6 +25,5 @@ public HistoryTrigger convert(@NonNull HistoryTriggerEntity source) {
4125
result.setStatus(source.getStatus());
4226
return result;
4327
}
44-
4528
}
4629
}

core/src/main/java/org/sterl/spring/persistent_tasks/history/api/TriggerHistoryResource.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@
1212
import org.springframework.web.bind.annotation.PostMapping;
1313
import org.springframework.web.bind.annotation.RequestMapping;
1414
import org.springframework.web.bind.annotation.RestController;
15+
import org.sterl.spring.persistent_tasks.api.HistoryTrigger;
1516
import org.sterl.spring.persistent_tasks.api.TaskStatusHistoryOverview;
1617
import org.sterl.spring.persistent_tasks.api.Trigger;
1718
import org.sterl.spring.persistent_tasks.api.TriggerGroup;
18-
import org.sterl.spring.persistent_tasks.api.HistoryTrigger;
1919
import org.sterl.spring.persistent_tasks.api.TriggerKey;
2020
import org.sterl.spring.persistent_tasks.api.TriggerSearch;
2121
import org.sterl.spring.persistent_tasks.history.HistoryService;
22-
import org.sterl.spring.persistent_tasks.history.api.HistoryConverter.FromLastTriggerStateEntity;
2322
import org.sterl.spring.persistent_tasks.history.api.HistoryConverter.ToHistoryTrigger;
2423

2524
import lombok.RequiredArgsConstructor;
@@ -31,6 +30,7 @@ public class TriggerHistoryResource {
3130

3231
public static final String PATH_GROUP = "history-grouped";
3332
private final HistoryService historyService;
33+
private final FromCompletedTriggerEntityConverter converter;
3434

3535
@GetMapping("history/instance/{instanceId}")
3636
public PagedModel<HistoryTrigger> listInstances(
@@ -56,7 +56,7 @@ public PagedModel<Trigger> list(
5656
TriggerSearch search,
5757
@PageableDefault(size = 100) Pageable page) {
5858

59-
return FromLastTriggerStateEntity.INSTANCE.toPage( //
59+
return converter.toPage( //
6060
historyService.searchTriggers(search, page));
6161
}
6262

core/src/main/java/org/sterl/spring/persistent_tasks/shared/converter/ToTrigger.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
package org.sterl.spring.persistent_tasks.shared.converter;
22

33
import org.springframework.lang.NonNull;
4+
import org.springframework.stereotype.Component;
45
import org.sterl.spring.persistent_tasks.api.Trigger;
56
import org.sterl.spring.persistent_tasks.shared.ExtendetConvert;
67
import org.sterl.spring.persistent_tasks.shared.model.HasTrigger;
78
import org.sterl.spring.persistent_tasks.shared.model.TriggerEntity;
8-
import org.sterl.spring.persistent_tasks.trigger.component.StateSerializer;
9+
import org.sterl.spring.persistent_tasks.trigger.component.StateSerializationComponent;
910

11+
import lombok.RequiredArgsConstructor;
1012
import lombok.extern.slf4j.Slf4j;
1113

1214
@Slf4j
13-
public enum ToTrigger implements ExtendetConvert<HasTrigger, Trigger> {
14-
INSTANCE;
15-
16-
private final static StateSerializer SERIALIZER = new StateSerializer();
15+
@Component
16+
@RequiredArgsConstructor
17+
public class ToTrigger implements ExtendetConvert<HasTrigger, Trigger> {
18+
19+
private final StateSerializationComponent stateSerialization;
1720

1821
@NonNull
1922
@Override
@@ -33,7 +36,7 @@ public Trigger convert(@NonNull HasTrigger hasData) {
3336
result.setRunningDurationInMs(source.getRunningDurationInMs());
3437
result.setStart(source.getStart());
3538
try {
36-
result.setState(SERIALIZER.deserialize(source.getState()));
39+
result.setState(stateSerialization.deserialize(source));
3740
} catch (Exception e) {
3841
var info = """
3942
Failed to deserialize state

0 commit comments

Comments
 (0)