Skip to content

Commit 88b96cb

Browse files
chore: use Gizmo MemberAccessors when possible, remove domainAccessType from SolverConfig (#2123)
SolutionCloner will still use REFLECTION by default, since GIZMO cannot copy final fields and requires all fields to be accessible (i.e. either public or have public getters and setters). --------- Co-authored-by: Lukáš Petrovický <lukas@petrovicky.net>
1 parent 65f6bbb commit 88b96cb

69 files changed

Lines changed: 946 additions & 911 deletions

File tree

Some content is hidden

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

core/pom.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
<dependency>
3333
<groupId>io.quarkus.gizmo</groupId>
3434
<artifactId>gizmo2</artifactId>
35-
<!-- <optional>true</optional>-->
3635
</dependency>
3736

3837
<!-- External dependencies -->

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

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import jakarta.xml.bind.annotation.XmlTransient;
2727
import jakarta.xml.bind.annotation.XmlType;
2828

29-
import ai.timefold.solver.core.api.domain.common.DomainAccessType;
3029
import ai.timefold.solver.core.api.domain.solution.cloner.SolutionCloner;
3130
import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator;
3231
import ai.timefold.solver.core.api.score.stream.ConstraintProvider;
@@ -73,7 +72,6 @@
7372
"monitoringConfig",
7473
"solutionClass",
7574
"entityClassList",
76-
"domainAccessType",
7775
"scoreDirectorFactoryConfig",
7876
"terminationConfig",
7977
"nearbyDistanceMeterClass",
@@ -228,7 +226,6 @@ public class SolverConfig extends AbstractConfig<SolverConfig> {
228226

229227
@XmlElement(name = "entityClass")
230228
protected List<Class<?>> entityClassList = null;
231-
protected DomainAccessType domainAccessType = null;
232229
@XmlTransient
233230
protected Map<String, MemberAccessor> gizmoMemberAccessorMap = null;
234231
@XmlTransient
@@ -399,14 +396,6 @@ public void setEntityClassList(@Nullable List<Class<?>> entityClassList) {
399396
this.entityClassList = entityClassList;
400397
}
401398

402-
public @Nullable DomainAccessType getDomainAccessType() {
403-
return domainAccessType;
404-
}
405-
406-
public void setDomainAccessType(@Nullable DomainAccessType domainAccessType) {
407-
this.domainAccessType = domainAccessType;
408-
}
409-
410399
public @Nullable Map<@NonNull String, @NonNull MemberAccessor> getGizmoMemberAccessorMap() {
411400
return gizmoMemberAccessorMap;
412401
}
@@ -527,11 +516,6 @@ public void setMonitoringConfig(@Nullable MonitoringConfig monitoringConfig) {
527516
return this;
528517
}
529518

530-
public @NonNull SolverConfig withDomainAccessType(@NonNull DomainAccessType domainAccessType) {
531-
this.domainAccessType = domainAccessType;
532-
return this;
533-
}
534-
535519
public @NonNull SolverConfig
536520
withGizmoMemberAccessorMap(@NonNull Map<@NonNull String, @NonNull MemberAccessor> memberAccessorMap) {
537521
this.gizmoMemberAccessorMap = memberAccessorMap;
@@ -651,10 +635,6 @@ public boolean canTerminate() {
651635
return Objects.requireNonNullElse(environmentMode, EnvironmentMode.PHASE_ASSERT);
652636
}
653637

654-
public @NonNull DomainAccessType determineDomainAccessType() {
655-
return Objects.requireNonNullElse(domainAccessType, DomainAccessType.REFLECTION);
656-
}
657-
658638
public @NonNull MonitoringConfig determineMetricConfig() {
659639
return Objects.requireNonNullElse(monitoringConfig,
660640
new MonitoringConfig().withSolverMetricList(Arrays.asList(SolverMetric.SOLVE_DURATION, SolverMetric.ERROR_COUNT,
@@ -698,7 +678,6 @@ public void offerRandomSeedFromSubSingleIndex(long subSingleIndex) {
698678
solutionClass = ConfigUtils.inheritOverwritableProperty(solutionClass, inheritedConfig.getSolutionClass());
699679
entityClassList = ConfigUtils.inheritMergeableListProperty(entityClassList,
700680
inheritedConfig.getEntityClassList());
701-
domainAccessType = ConfigUtils.inheritOverwritableProperty(domainAccessType, inheritedConfig.getDomainAccessType());
702681
gizmoMemberAccessorMap = ConfigUtils.inheritMergeableMapProperty(
703682
gizmoMemberAccessorMap, inheritedConfig.getGizmoMemberAccessorMap());
704683
gizmoSolutionClonerMap = ConfigUtils.inheritMergeableMapProperty(

core/src/main/java/ai/timefold/solver/core/config/util/ConfigUtils.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package ai.timefold.solver.core.config.util;
22

3-
import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD;
3+
import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorType.FIELD_OR_READ_METHOD;
44

55
import java.lang.annotation.Annotation;
66
import java.lang.reflect.AnnotatedElement;
@@ -30,10 +30,10 @@
3030
import java.util.stream.Collectors;
3131
import java.util.stream.Stream;
3232

33-
import ai.timefold.solver.core.api.domain.common.DomainAccessType;
3433
import ai.timefold.solver.core.api.domain.common.PlanningId;
3534
import ai.timefold.solver.core.config.AbstractConfig;
3635
import ai.timefold.solver.core.impl.domain.common.AlphabeticMemberComparator;
36+
import ai.timefold.solver.core.impl.domain.common.DomainAccessType;
3737
import ai.timefold.solver.core.impl.domain.common.ReflectionHelper;
3838
import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor;
3939
import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory;

core/src/main/java/ai/timefold/solver/core/api/domain/common/DomainAccessType.java renamed to core/src/main/java/ai/timefold/solver/core/impl/domain/common/DomainAccessType.java

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package ai.timefold.solver.core.api.domain.common;
1+
package ai.timefold.solver.core.impl.domain.common;
22

33
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
44

@@ -7,25 +7,26 @@
77
* are accessed.
88
*/
99
public enum DomainAccessType {
10+
/**
11+
* Determine what domain access type to use automatically.
12+
* <p>
13+
* This is the default.
14+
*/
15+
AUTO,
16+
1017
/**
1118
* Use reflection to read/write members (fields and methods) of the domain.
1219
* <p>
1320
* When used in a modulepath, the module must be open.
1421
* When used in GraalVM, the domain must be open for reflection.
15-
* <p>
16-
* This is the default, except with timefold-solver-quarkus.
1722
*/
18-
REFLECTION,
23+
FORCE_REFLECTION,
24+
1925
/**
2026
* Use Gizmo generated bytecode to access members (fields and methods) to avoid reflection
2127
* for additional performance.
2228
* <p>
23-
* With timefold-solver-quarkus, this bytecode is generated at build time
24-
* and it supports planning annotations on non-public members too.
25-
* <p>
26-
* Without timefold-solver-quarkus, this bytecode is generated at bootstrap runtime
27-
* and you must add Gizmo in your classpath or modulepath
28-
* and use planning annotations on public members only.
29+
* This is the default when the application is run inside a JVM and not a native image.
2930
*/
30-
GIZMO
31+
FORCE_GIZMO
3132
}

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

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,22 @@
1010
import java.util.Objects;
1111
import java.util.concurrent.ConcurrentHashMap;
1212

13-
import ai.timefold.solver.core.api.domain.common.DomainAccessType;
1413
import ai.timefold.solver.core.api.solver.SolverFactory;
14+
import ai.timefold.solver.core.impl.domain.common.DomainAccessType;
1515
import ai.timefold.solver.core.impl.domain.common.ReflectionHelper;
1616
import ai.timefold.solver.core.impl.domain.common.accessor.gizmo.AccessorInfo;
1717
import ai.timefold.solver.core.impl.domain.common.accessor.gizmo.GizmoClassLoader;
1818
import ai.timefold.solver.core.impl.domain.common.accessor.gizmo.GizmoMemberAccessorFactory;
1919

20+
import org.jspecify.annotations.NullMarked;
21+
import org.jspecify.annotations.Nullable;
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
25+
@NullMarked
2026
public final class MemberAccessorFactory {
2127

28+
static final Logger LOGGER = LoggerFactory.getLogger(MemberAccessorFactory.class);
2229
// exists only so that the various member accessors can share the same text in their exception messages
2330
static final String CLASSLOADER_NUDGE_MESSAGE =
2431
"Maybe add getClass().getClassLoader() as a parameter to the %s.create...() method call."
@@ -30,10 +37,10 @@ public final class MemberAccessorFactory {
3037
* @param member never null, method or field to access
3138
* @param memberAccessorType never null
3239
* @param domainAccessType never null
33-
* @param classLoader null or {@link GizmoClassLoader} if domainAccessType is {@link DomainAccessType#GIZMO}.
40+
* @param classLoader null or {@link GizmoClassLoader} if domainAccessType is {@link DomainAccessType#FORCE_GIZMO}.
3441
* @return never null, new instance of the member accessor
3542
*/
36-
public static MemberAccessor buildMemberAccessor(Member member, MemberAccessorType memberAccessorType,
43+
private static MemberAccessor buildMemberAccessor(Member member, MemberAccessorType memberAccessorType,
3744
DomainAccessType domainAccessType, ClassLoader classLoader) {
3845
return buildMemberAccessor(member, memberAccessorType, null, domainAccessType, classLoader);
3946
}
@@ -45,28 +52,31 @@ public static MemberAccessor buildMemberAccessor(Member member, MemberAccessorTy
4552
* @param memberAccessorType never null
4653
* @param annotationClass the annotation the member was annotated with (used for error reporting)
4754
* @param domainAccessType never null
48-
* @param classLoader null or {@link GizmoClassLoader} if domainAccessType is {@link DomainAccessType#GIZMO}.
55+
* @param classLoader null or {@link GizmoClassLoader} if domainAccessType is {@link DomainAccessType#FORCE_GIZMO}.
4956
* @return never null, new instance of the member accessor
5057
*/
51-
public static MemberAccessor buildMemberAccessor(Member member, MemberAccessorType memberAccessorType,
52-
Class<? extends Annotation> annotationClass, DomainAccessType domainAccessType, ClassLoader classLoader) {
58+
static MemberAccessor buildMemberAccessor(Member member, MemberAccessorType memberAccessorType,
59+
@Nullable Class<? extends Annotation> annotationClass, DomainAccessType domainAccessType, ClassLoader classLoader) {
60+
MemberAccessorValidator.verifyIsValidMember(annotationClass, member, memberAccessorType);
5361
return switch (domainAccessType) {
54-
case GIZMO -> GizmoMemberAccessorFactory.buildGizmoMemberAccessor(member, annotationClass,
55-
AccessorInfo.of(memberAccessorType != MemberAccessorType.VOID_METHOD,
56-
memberAccessorType == MemberAccessorType.FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER),
62+
case AUTO -> throw new IllegalStateException(
63+
"Impossible state: called with %s (AUTO) instead of a resolved domain access type"
64+
.formatted(DomainAccessType.class.getSimpleName()));
65+
case FORCE_GIZMO -> GizmoMemberAccessorFactory.buildGizmoMemberAccessor(member, annotationClass,
66+
AccessorInfo.of(memberAccessorType),
5767
(GizmoClassLoader) Objects.requireNonNull(classLoader));
58-
case REFLECTION -> buildReflectiveMemberAccessor(member, memberAccessorType, annotationClass);
68+
case FORCE_REFLECTION -> buildReflectiveMemberAccessor(member, memberAccessorType, annotationClass);
5969
};
6070
}
6171

6272
private static MemberAccessor buildReflectiveMemberAccessor(Member member, MemberAccessorType memberAccessorType,
63-
Class<? extends Annotation> annotationClass) {
73+
@Nullable Class<? extends Annotation> annotationClass) {
6474
return buildReflectiveMemberAccessor(member, memberAccessorType, annotationClass,
6575
(AnnotatedElement) member);
6676
}
6777

6878
private static MemberAccessor buildReflectiveMemberAccessor(Member member, MemberAccessorType memberAccessorType,
69-
Class<? extends Annotation> annotationClass, AnnotatedElement annotatedElement) {
79+
@Nullable Class<? extends Annotation> annotationClass, AnnotatedElement annotatedElement) {
7080
var messagePrefix = (annotationClass == null) ? "The" : "The @%s annotated".formatted(annotationClass.getSimpleName());
7181
if (member instanceof Field field) {
7282
var getter = ReflectionHelper.getGetterMethod(field.getDeclaringClass(), field.getName());
@@ -126,7 +136,7 @@ private static MemberAccessor buildReflectiveMemberAccessor(Member member, Membe
126136
memberAccessor = new ReflectionBeanPropertyMemberAccessor(method, annotatedElement, getterOnly);
127137
break;
128138
case VOID_METHOD:
129-
memberAccessor = new ReflectionMethodMemberAccessor(method, false, false);
139+
memberAccessor = new ReflectionMethodMemberAccessor(method);
130140
break;
131141
default:
132142
throw new IllegalStateException("The memberAccessorType (%s) is not implemented."
@@ -154,6 +164,7 @@ private static MemberAccessor buildReflectiveMemberAccessor(Member member, Membe
154164

155165
private final Map<String, MemberAccessor> memberAccessorCache;
156166
private final GizmoClassLoader gizmoClassLoader = new GizmoClassLoader();
167+
private final boolean isGizmoSupported;
157168

158169
public MemberAccessorFactory() {
159170
this(null);
@@ -162,12 +173,19 @@ public MemberAccessorFactory() {
162173
/**
163174
* Prefills the member accessor cache.
164175
*
165-
* @param memberAccessorMap key is the fully qualified member name
176+
* @param memberAccessorMap key is the fully qualified member name, value is a pregenerated {@link MemberAccessor}.
177+
* Used by Quarkus since the {@link MemberAccessor} are generated at build time.
178+
* If null, it is treated as an empty map.
166179
*/
167-
public MemberAccessorFactory(Map<String, MemberAccessor> memberAccessorMap) {
180+
public MemberAccessorFactory(@Nullable Map<String, MemberAccessor> memberAccessorMap) {
168181
// The MemberAccessorFactory may be accessed, and this cache both read and updated, by multiple threads.
169182
this.memberAccessorCache =
170183
memberAccessorMap == null ? new ConcurrentHashMap<>() : new ConcurrentHashMap<>(memberAccessorMap);
184+
// If the memberAccessorMap is not empty, we are in Quarkus using pregenerated member accessors
185+
this.isGizmoSupported =
186+
(memberAccessorMap != null && !memberAccessorMap.isEmpty()) || gizmoClassLoader.isGizmoSupported();
187+
LOGGER.trace("Using domain access type {} for member accessors.",
188+
isGizmoSupported ? DomainAccessType.FORCE_GIZMO : DomainAccessType.FORCE_REFLECTION);
171189
}
172190

173191
/**
@@ -180,10 +198,16 @@ public MemberAccessorFactory(Map<String, MemberAccessor> memberAccessorMap) {
180198
* @return never null, new {@link MemberAccessor} instance unless already found in memberAccessorMap
181199
*/
182200
public MemberAccessor buildAndCacheMemberAccessor(Member member, MemberAccessorType memberAccessorType,
183-
Class<? extends Annotation> annotationClass, DomainAccessType domainAccessType) {
201+
@Nullable Class<? extends Annotation> annotationClass, DomainAccessType domainAccessType) {
184202
String generatedClassName = GizmoMemberAccessorFactory.getGeneratedClassName(member);
203+
if (domainAccessType == DomainAccessType.AUTO) {
204+
domainAccessType = isGizmoSupported ? DomainAccessType.FORCE_GIZMO : DomainAccessType.FORCE_REFLECTION;
205+
}
206+
207+
var finalDomainAccessType = domainAccessType;
185208
return memberAccessorCache.computeIfAbsent(generatedClassName,
186-
k -> MemberAccessorFactory.buildMemberAccessor(member, memberAccessorType, annotationClass, domainAccessType,
209+
k -> MemberAccessorFactory.buildMemberAccessor(member, memberAccessorType, annotationClass,
210+
finalDomainAccessType,
187211
gizmoClassLoader));
188212
}
189213

@@ -198,33 +222,18 @@ public MemberAccessor buildAndCacheMemberAccessor(Member member, MemberAccessorT
198222
public MemberAccessor buildAndCacheMemberAccessor(Member member, MemberAccessorType memberAccessorType,
199223
DomainAccessType domainAccessType) {
200224
String generatedClassName = GizmoMemberAccessorFactory.getGeneratedClassName(member);
225+
if (domainAccessType == DomainAccessType.AUTO) {
226+
domainAccessType = isGizmoSupported ? DomainAccessType.FORCE_GIZMO : DomainAccessType.FORCE_REFLECTION;
227+
}
228+
229+
var finalDomainAccessType = domainAccessType;
201230
return memberAccessorCache.computeIfAbsent(generatedClassName,
202-
k -> MemberAccessorFactory.buildMemberAccessor(member, memberAccessorType, domainAccessType, gizmoClassLoader));
231+
k -> MemberAccessorFactory.buildMemberAccessor(member, memberAccessorType, finalDomainAccessType,
232+
gizmoClassLoader));
203233
}
204234

205235
public GizmoClassLoader getGizmoClassLoader() {
206236
return gizmoClassLoader;
207237
}
208238

209-
public enum MemberAccessorType {
210-
FIELD_OR_READ_METHOD,
211-
FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER,
212-
FIELD_OR_GETTER_METHOD,
213-
FIELD_OR_GETTER_METHOD_WITH_SETTER(true),
214-
VOID_METHOD;
215-
216-
private final boolean setterRequired;
217-
218-
MemberAccessorType() {
219-
setterRequired = false;
220-
}
221-
222-
MemberAccessorType(boolean setterRequired) {
223-
this.setterRequired = setterRequired;
224-
}
225-
226-
public boolean isSetterRequired() {
227-
return setterRequired;
228-
}
229-
}
230239
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package ai.timefold.solver.core.impl.domain.common.accessor;
2+
3+
public enum MemberAccessorType {
4+
FIELD_OR_READ_METHOD,
5+
FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER,
6+
FIELD_OR_GETTER_METHOD,
7+
FIELD_OR_GETTER_METHOD_WITH_SETTER(true),
8+
VOID_METHOD;
9+
10+
private final boolean setterRequired;
11+
12+
MemberAccessorType() {
13+
setterRequired = false;
14+
}
15+
16+
MemberAccessorType(boolean setterRequired) {
17+
this.setterRequired = setterRequired;
18+
}
19+
20+
public boolean isSetterRequired() {
21+
return setterRequired;
22+
}
23+
}

0 commit comments

Comments
 (0)