Skip to content

Commit 9127ea4

Browse files
Christopher-Chianellijanwillemkeizertriceo
authored
fix: use context class loader, then parent in GIZMO (#2284)
Co-authored-by: Jan Willem Keizer <janwillemkeizer@gmail.com> Co-authored-by: Lukáš Petrovický <lukas@timefold.ai>
1 parent 06081ca commit 9127ea4

5 files changed

Lines changed: 527 additions & 0 deletions

File tree

core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ public final class SolverConfig extends AbstractConfig<SolverConfig> {
203203
@XmlTransient
204204
private Clock clock = null;
205205
@XmlTransient
206+
@Deprecated(since = "2.1.0", forRemoval = true)
206207
private ClassLoader classLoader = null;
207208

208209
// Warning: all fields are null (and not defaulted) because they can be inherited
@@ -263,6 +264,10 @@ public SolverConfig(@NonNull Clock clock) {
263264
this.clock = Objects.requireNonNull(clock);
264265
}
265266

267+
/**
268+
* @deprecated does not have any effect.
269+
*/
270+
@Deprecated(since = "2.1.0", forRemoval = true)
266271
public SolverConfig(@Nullable ClassLoader classLoader) {
267272
this.classLoader = classLoader;
268273
}
@@ -294,10 +299,18 @@ public void setClock(@Nullable Clock clock) {
294299
this.clock = clock;
295300
}
296301

302+
/**
303+
* @deprecated does not have any effect.
304+
*/
305+
@Deprecated(since = "2.1.0", forRemoval = true)
297306
public @Nullable ClassLoader getClassLoader() {
298307
return classLoader;
299308
}
300309

310+
/**
311+
* @deprecated does not have any effect.
312+
*/
313+
@Deprecated(since = "2.1.0", forRemoval = true)
301314
public void setClassLoader(@Nullable ClassLoader classLoader) {
302315
this.classLoader = classLoader;
303316
}
@@ -501,6 +514,10 @@ public void setMonitoringConfig(@Nullable MonitoringConfig monitoringConfig) {
501514
return this;
502515
}
503516

517+
/**
518+
* @deprecated does not have any effect.
519+
*/
520+
@Deprecated(since = "2.1.0", forRemoval = true)
504521
public @NonNull SolverConfig withClassLoader(@NonNull ClassLoader classLoader) {
505522
this.setClassLoader(classLoader);
506523
return this;

core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/gizmo/GizmoClassLoader.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,42 @@ public String getName() {
4444
return "Timefold Solver Gizmo ClassLoader";
4545
}
4646

47+
@Nullable
48+
private Class<?> loadClassFrom(@Nullable ClassLoader otherClassLoader, String name) {
49+
if (otherClassLoader == null || otherClassLoader == this) {
50+
return null;
51+
}
52+
try {
53+
return otherClassLoader.loadClass(name);
54+
} catch (ClassNotFoundException ignored) {
55+
return null;
56+
}
57+
}
58+
59+
@Override
60+
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
61+
synchronized (getClassLoadingLock(name)) {
62+
var loadedClass = findLoadedClass(name);
63+
if (loadedClass == null) {
64+
if (hasBytecodeFor(name)) {
65+
loadedClass = findClass(name);
66+
} else {
67+
var contextClassLoader = Thread.currentThread().getContextClassLoader();
68+
69+
// First context, then parent
70+
loadedClass = loadClassFrom(contextClassLoader, name);
71+
if (loadedClass == null) {
72+
loadedClass = super.loadClass(name, false);
73+
}
74+
}
75+
}
76+
if (resolve) {
77+
resolveClass(loadedClass);
78+
}
79+
return loadedClass;
80+
}
81+
}
82+
4783
@Override
4884
public Class<?> findClass(String name) throws ClassNotFoundException {
4985
var byteCode = getBytecodeFor(name);

core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
import ai.timefold.solver.core.testdomain.TestdataEntity;
7979
import ai.timefold.solver.core.testdomain.TestdataSolution;
8080
import ai.timefold.solver.core.testdomain.TestdataValue;
81+
import ai.timefold.solver.core.testdomain.classloader.TestdataSeparateClassLoaderDomain;
8182
import ai.timefold.solver.core.testdomain.list.TestdataListEntity;
8283
import ai.timefold.solver.core.testdomain.list.TestdataListSolution;
8384
import ai.timefold.solver.core.testdomain.list.TestdataListValue;
@@ -1775,6 +1776,26 @@ private void solveListVarEntityRangeModel(MoveSelectorConfig<?> moveSelectionCon
17751776
assertThat(bestEntity3.getValueList()).doesNotContain(value1, value2, value3);
17761777
}
17771778

1779+
@Test
1780+
void solveCustomClassLoader() {
1781+
// Spring DevTools redefine existing classes, with their updated definitions replacing
1782+
// existing instances, and their updated definition are only available from the Thread's Context ClassLoader.
1783+
// This test verify the Context ClassLoader definition is used instead of the one from the parent ClassLoader.
1784+
var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSeparateClassLoaderDomain.getTestdataSolutionClass(),
1785+
TestdataSeparateClassLoaderDomain.getTestdataEntityClass());
1786+
var originalClassLoader = Thread.currentThread().getContextClassLoader();
1787+
try {
1788+
Thread.currentThread().setContextClassLoader(TestdataSeparateClassLoaderDomain.getClassLoader());
1789+
var solution = TestdataSeparateClassLoaderDomain.generateSolution();
1790+
1791+
solution = PlannerTestUtils.solveAssertingEvents(solverConfig, solution,
1792+
BestScoreChangedEvent.constructionHeuristic(SimpleScore.ZERO, 0));
1793+
assertThat(solution).isNotNull();
1794+
} finally {
1795+
Thread.currentThread().setContextClassLoader(originalClassLoader);
1796+
}
1797+
}
1798+
17781799
@Test
17791800
void solveListVarEntityRangeModelWithTreeSet() {
17801801
var solverConfig = new SolverConfig()

0 commit comments

Comments
 (0)