Skip to content

Commit 90d8959

Browse files
committed
Make domain model entities immutable
This commit introduces several major changes to make the domain model entities immutable: - It removes the setter of the Entity id - It creates all entities with an assigned ID as well as their dependencies which cannot be changed after creation - The type of the identity field has been changed to a primitive long in order to avoid nullability issues - Shallow entity creation has been removed from all DAOs - All entity creation is now centralized in the job repository (ie removal of JobExecution#creatStepExecution). Note that StepExecution#createStepContribution is an exception since StepContribution is not an Entity - Superfluous methods "add" and "save" have been removed and replaced with clearly defined CRUD operations - Move restartability logic from JobRepository to JobOperator These changes are important enablers for cleaner and safer APIs. Resolves #1870
1 parent 1cd023e commit 90d8959

File tree

210 files changed

+2506
-5528
lines changed

Some content is hidden

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

210 files changed

+2506
-5528
lines changed
Lines changed: 10 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2006-2023 the original author or authors.
2+
* Copyright 2006-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.springframework.batch.core;
1818

1919
import java.io.Serializable;
20+
import java.util.Objects;
2021

2122
import org.springframework.util.ClassUtils;
2223

@@ -32,46 +33,25 @@
3233
*/
3334
public class Entity implements Serializable {
3435

35-
private Long id;
36+
private final long id;
3637

37-
private volatile Integer version;
38-
39-
/**
40-
* Default constructor for {@link Entity}.
41-
* <p>
42-
* The ID defaults to zero.
43-
*/
44-
public Entity() {
45-
super();
46-
}
38+
private Integer version;
4739

4840
/**
4941
* The constructor for the {@link Entity} where the ID is established.
5042
* @param id The ID for the entity.
5143
*/
52-
public Entity(Long id) {
53-
super();
54-
55-
// Commented out because StepExecutions are still created in a disconnected
56-
// manner. The Repository should create them, then this can be uncommented.
57-
// Assert.notNull(id, "Entity id must not be null.");
44+
public Entity(long id) {
5845
this.id = id;
5946
}
6047

6148
/**
6249
* @return The ID associated with the {@link Entity}.
6350
*/
64-
public Long getId() {
51+
public long getId() {
6552
return id;
6653
}
6754

68-
/**
69-
* @param id The ID for the {@link Entity}.
70-
*/
71-
public void setId(Long id) {
72-
this.id = id;
73-
}
74-
7555
/**
7656
* @return the version.
7757
*/
@@ -108,47 +88,16 @@ public String toString() {
10888
return String.format("%s: id=%d, version=%d", ClassUtils.getShortName(getClass()), id, version);
10989
}
11090

111-
/**
112-
* Attempt to establish identity based on {@code id} if both exist. If either
113-
* {@code id} does not exist, use {@code Object.equals()}.
114-
*
115-
* @see java.lang.Object#equals(java.lang.Object)
116-
*/
11791
@Override
118-
public boolean equals(Object other) {
119-
if (other == this) {
120-
return true;
121-
}
122-
if (other == null) {
123-
return false;
124-
}
125-
if (!(other instanceof Entity entity)) {
126-
return false;
127-
}
128-
if (id == null || entity.getId() == null) {
92+
public boolean equals(Object o) {
93+
if (!(o instanceof Entity entity))
12994
return false;
130-
}
131-
return id.equals(entity.getId());
95+
return id == entity.id;
13296
}
13397

134-
/**
135-
* Use {@code id}, if it exists, to establish a hash code. Otherwise fall back to
136-
* {@code Object.hashCode()}. It is based on the same information as {@code equals},
137-
* so, if that changes, this will. Note that this follows the contract of
138-
* {@code Object.hashCode()} but will cause problems for anyone adding an unsaved
139-
* {@link Entity} to a {@code Set} because {@code Set.contains()} almost certainly
140-
* returns false for the {@link Entity} after it is saved. Spring Batch does not store
141-
* any of its entities in sets as a matter of course, so this is internally
142-
* consistent. Clients should not be exposed to unsaved entities.
143-
*
144-
* @see java.lang.Object#hashCode()
145-
*/
14698
@Override
14799
public int hashCode() {
148-
if (id == null) {
149-
return System.identityHashCode(this);
150-
}
151-
return 39 + 87 * id.hashCode();
100+
return Objects.hashCode(id);
152101
}
153102

154103
}

spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,8 @@ public final void execute(JobExecution execution) {
277277
Observation observation = MicrometerMetrics
278278
.createObservation(BatchMetrics.METRICS_PREFIX + "job", this.observationRegistry)
279279
.highCardinalityKeyValue(BatchMetrics.METRICS_PREFIX + "job.instanceId",
280-
execution.getJobInstance().getId().toString())
281-
.highCardinalityKeyValue(BatchMetrics.METRICS_PREFIX + "job.executionId", execution.getId().toString())
280+
String.valueOf(execution.getJobInstance().getId()))
281+
.highCardinalityKeyValue(BatchMetrics.METRICS_PREFIX + "job.executionId", String.valueOf(execution.getId()))
282282
.lowCardinalityKeyValue(BatchMetrics.METRICS_PREFIX + "job.name", execution.getJobInstance().getJobName())
283283
.start();
284284
try (Observation.Scope scope = observation.openScope()) {

spring-batch-core/src/main/java/org/springframework/batch/core/job/JobExecution.java

Lines changed: 39 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2006-2023 the original author or authors.
2+
* Copyright 2006-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,14 +16,12 @@
1616

1717
package org.springframework.batch.core.job;
1818

19-
import java.io.IOException;
20-
import java.io.ObjectInputStream;
2119
import java.time.LocalDateTime;
2220
import java.util.ArrayList;
2321
import java.util.Collection;
2422
import java.util.Collections;
2523
import java.util.HashSet;
26-
import java.util.LinkedHashSet;
24+
import java.util.LinkedList;
2725
import java.util.List;
2826
import java.util.Set;
2927
import java.util.concurrent.CopyOnWriteArrayList;
@@ -50,85 +48,38 @@ public class JobExecution extends Entity {
5048

5149
private JobInstance jobInstance;
5250

53-
private volatile Collection<StepExecution> stepExecutions = Collections.synchronizedSet(new LinkedHashSet<>());
51+
private final List<StepExecution> stepExecutions = Collections.synchronizedList(new LinkedList<>());
5452

55-
private volatile BatchStatus status = BatchStatus.STARTING;
53+
private BatchStatus status = BatchStatus.STARTING;
5654

57-
private volatile LocalDateTime startTime = null;
55+
private LocalDateTime createTime = LocalDateTime.now();
5856

59-
private volatile LocalDateTime createTime = LocalDateTime.now();
57+
private LocalDateTime startTime = null;
6058

61-
private volatile LocalDateTime endTime = null;
59+
private LocalDateTime endTime = null;
6260

63-
private volatile LocalDateTime lastUpdated = null;
61+
private LocalDateTime lastUpdated = null;
6462

65-
private volatile ExitStatus exitStatus = ExitStatus.UNKNOWN;
63+
private ExitStatus exitStatus = ExitStatus.UNKNOWN;
6664

67-
private volatile ExecutionContext executionContext = new ExecutionContext();
65+
private ExecutionContext executionContext = new ExecutionContext();
6866

69-
private transient volatile List<Throwable> failureExceptions = new CopyOnWriteArrayList<>();
67+
private final List<Throwable> failureExceptions = new CopyOnWriteArrayList<>();
7068

7169
/**
72-
* Constructor that sets the state of the instance to the {@link JobExecution}
73-
* parameter.
74-
* @param original The {@link JobExecution} to be copied.
75-
*/
76-
public JobExecution(JobExecution original) {
77-
this.jobParameters = original.getJobParameters();
78-
this.jobInstance = original.getJobInstance();
79-
this.stepExecutions = original.getStepExecutions();
80-
this.status = original.getStatus();
81-
this.startTime = original.getStartTime();
82-
this.createTime = original.getCreateTime();
83-
this.endTime = original.getEndTime();
84-
this.lastUpdated = original.getLastUpdated();
85-
this.exitStatus = original.getExitStatus();
86-
this.executionContext = original.getExecutionContext();
87-
this.failureExceptions = original.getFailureExceptions();
88-
this.setId(original.getId());
89-
this.setVersion(original.getVersion());
90-
}
91-
92-
/**
93-
* Because a JobExecution is not valid unless the job is set, this constructor is the
94-
* only valid one from a modeling point of view.
95-
* @param job The job of which this execution is a part.
96-
* @param id A {@link Long} that represents the {@code id} for the
97-
* {@code JobExecution}.
70+
* Create a new {@link JobExecution} instance. Because a JobExecution is not valid
71+
* unless the job instance is set, this constructor is the only valid one from a
72+
* modeling point of view.
73+
* @param jobInstance The job instance of which this execution is a part.
74+
* @param id of the {@code JobExecution}.
9875
* @param jobParameters A {@link JobParameters} instance for this
9976
* {@code JobExecution}.
10077
*/
101-
public JobExecution(JobInstance job, Long id, @Nullable JobParameters jobParameters) {
78+
// TODO add execution context parameter
79+
public JobExecution(long id, JobInstance jobInstance, JobParameters jobParameters) {
10280
super(id);
103-
this.jobInstance = job;
104-
this.jobParameters = jobParameters == null ? new JobParameters() : jobParameters;
105-
}
106-
107-
/**
108-
* Constructor for transient (unsaved) instances.
109-
* @param job The enclosing {@link JobInstance}.
110-
* @param jobParameters The {@link JobParameters} instance for this
111-
* {@code JobExecution}.
112-
*/
113-
public JobExecution(JobInstance job, JobParameters jobParameters) {
114-
this(job, null, jobParameters);
115-
}
116-
117-
/**
118-
* Constructor that accepts the job execution {@code id} and {@link JobParameters}.
119-
* @param id The job execution {@code id}.
120-
* @param jobParameters The {@link JobParameters} for the {@link JobExecution}.
121-
*/
122-
public JobExecution(Long id, JobParameters jobParameters) {
123-
this(null, id, jobParameters);
124-
}
125-
126-
/**
127-
* Constructor that accepts the job execution {@code id}.
128-
* @param id The job execution {@code id}.
129-
*/
130-
public JobExecution(Long id) {
131-
this(null, id, null);
81+
this.jobInstance = jobInstance;
82+
this.jobParameters = jobParameters;
13283
}
13384

13485
/**
@@ -204,15 +155,14 @@ public void upgradeStatus(BatchStatus status) {
204155
}
205156

206157
/**
207-
* Convenience getter for the {@code id} of the enclosing job. Useful for DAO
158+
* Convenience getter for the {@code id} of the enclosing job instance. Useful for DAO
208159
* implementations.
209-
* @return the {@code id} of the enclosing job.
160+
* @return the {@code id} of the enclosing job instance.
210161
*/
211-
public Long getJobId() {
212-
if (jobInstance != null) {
213-
return jobInstance.getId();
214-
}
215-
return null;
162+
// TODO why is that needed for DAO implementations? should not be needed with the new
163+
// model
164+
public long getJobInstanceId() {
165+
return this.jobInstance.getId();
216166
}
217167

218168
/**
@@ -230,29 +180,18 @@ public ExitStatus getExitStatus() {
230180
}
231181

232182
/**
233-
* @return the Job that is executing.
183+
* @return the Job instance that is executing.
234184
*/
235185
public JobInstance getJobInstance() {
236-
return jobInstance;
186+
return this.jobInstance;
237187
}
238188

239189
/**
240190
* Accessor for the step executions.
241191
* @return the step executions that were registered.
242192
*/
243193
public Collection<StepExecution> getStepExecutions() {
244-
return List.copyOf(stepExecutions);
245-
}
246-
247-
/**
248-
* Register a step execution with the current job execution.
249-
* @param stepName the name of the step the new execution is associated with.
250-
* @return an empty {@link StepExecution} associated with this {@code JobExecution}.
251-
*/
252-
public StepExecution createStepExecution(String stepName) {
253-
StepExecution stepExecution = new StepExecution(stepName, this);
254-
this.stepExecutions.add(stepExecution);
255-
return stepExecution;
194+
return List.copyOf(this.stepExecutions);
256195
}
257196

258197
/**
@@ -305,11 +244,19 @@ public void setCreateTime(LocalDateTime createTime) {
305244
}
306245

307246
/**
308-
* Add a step execution from an existing instance.
247+
* Add a step execution.
309248
* @param stepExecution The {@code stepExecution} execution to be added.
310249
*/
311250
public void addStepExecution(StepExecution stepExecution) {
312-
stepExecutions.add(stepExecution);
251+
this.stepExecutions.add(stepExecution);
252+
}
253+
254+
/**
255+
* Add some step executions.
256+
* @param stepExecutions The step executions to add to the current list.
257+
*/
258+
public void addStepExecutions(List<StepExecution> stepExecutions) {
259+
this.stepExecutions.addAll(stepExecutions);
313260
}
314261

315262
/**
@@ -364,33 +311,11 @@ public synchronized List<Throwable> getAllFailureExceptions() {
364311
return new ArrayList<>(allExceptions);
365312
}
366313

367-
/**
368-
* Deserialize and ensure transient fields are re-instantiated when read back.
369-
* @param stream instance of {@link ObjectInputStream}.
370-
* @throws IOException if an error occurs during read.
371-
* @throws ClassNotFoundException thrown if the class is not found.
372-
*/
373-
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
374-
stream.defaultReadObject();
375-
failureExceptions = new ArrayList<>();
376-
}
377-
378314
@Override
379315
public String toString() {
380316
return super.toString() + String.format(
381317
", startTime=%s, endTime=%s, lastUpdated=%s, status=%s, exitStatus=%s, job=[%s], jobParameters=[%s]",
382318
startTime, endTime, lastUpdated, status, exitStatus, jobInstance, jobParameters);
383319
}
384320

385-
/**
386-
* Add some step executions. For internal use only.
387-
* @param stepExecutions The step executions to add to the current list.
388-
*/
389-
public void addStepExecutions(List<StepExecution> stepExecutions) {
390-
if (stepExecutions != null) {
391-
this.stepExecutions.removeAll(stepExecutions);
392-
this.stepExecutions.addAll(stepExecutions);
393-
}
394-
}
395-
396321
}

0 commit comments

Comments
 (0)