Allow package-private planning domain classes and methods
メンテナーはふだん 2 日以内に返信
評価
調査の方向性
Start by reading MemberAccessorValidator and ReflectionHelper to trace where non-public classes and methods are rejected or missed. Then inspect ReflectionBeanPropertyMemberAccessor and GizmoMemberAccessorImplementor to understand the existing fallback constraints. Done means package-private planning classes, getters, and setters are discoverable and usable without changing behavior for public classes.
索引モデルが issue の本文から書いたものです。
説明
Problem
Timefold requires all planning domain classes, getters, and setters to be public. This is enforced by MemberAccessorValidator, but the underlying accessor layer already handles non-public access — making this restriction unnecessary.
The contradiction
The reflection-based accessor already calls setAccessible(true) and creates a MethodHandle that works fine with non-public methods:
// ReflectionBeanPropertyMemberAccessor.java
public ReflectionBeanPropertyMemberAccessor(Method getterMethod, AnnotatedElement annotatedElement, boolean getterOnly) {
this.getterMethod = getterMethod;
MethodHandles.Lookup lookup = MethodHandles.lookup();
this.getterMethod.setAccessible(true); // ← handles non-public
this.getherMethodHandle = lookup.unreflect(getterMethod) // ← creates fast MethodHandle
.asFixedArity();
// ...
}
At runtime, it invokes via MethodHandle, not raw Method.invoke():
public Object executeGetter(Object bean) {
return getherMethodHandle.invoke(bean); // MethodHandle — JIT-optimized to direct call
}
But MemberAccessorValidator rejects non-public members before the accessor ever gets a chance:
// MemberAccessorValidator.java — declaring class gate
private static void verifyDeclaringClassIsAccessible(Member member, String messagePrefix) {
var declaringClass = member.getDeclaringClass();
if (!Modifier.isPublic(declaringClass.getModifiers())) {
throw new IllegalArgumentException(
"%s is not accessible because its declaring class (%s) is not public."
.formatted(messagePrefix, declaringClass.getCanonicalName()));
}
}
// MemberAccessorValidator.java — repeated across verifyFieldOrGetter, verifyIsVoidMethod,
// verifyIsPublicFieldOrHasReadMethod, verifyGetterSetterProperties
if (!Modifier.isPublic(method.getModifiers())) {
throw new IllegalArgumentException(
"%s is a getter method, but it is not public."
.formatted(messagePrefix));
}
There's also a discovery-layer issue — ReflectionHelper uses Class.getMethod() which only returns public methods, so package-private getters are invisible before validation even runs.
What about Gizmo performance?
The Gizmo path generates bytecode with direct invokeVirtual instructions, which requires public access. However, the reflection path already uses MethodHandle — which the JVM JIT-compiles to the same direct call after warmup:
| Path | Runtime execution | After JIT |
|---|---|---|
| Gizmo | invokeVirtual bytecode |
direct call |
| Reflection (MethodHandle) | MethodHandle.invoke() |
direct call (inlined) |
For non-public members, falling back to the MethodHandle-based reflection path carries no meaningful performance penalty. The Gizmo implementor itself documents this constraint:
// GizmoMemberAccessorImplementor.java (line 353)
// The member MUST be public if not called in Quarkus
// (i.e. we don't delegate to the field getter/setter).
Why this matters
Package-private domain models are a standard Java encapsulation pattern. In modular codebases, it's common to keep domain classes package-private and expose only the API boundary publicly. Today you're forced to write:
// Everything must be public even though nothing outside this package needs access
public class Lesson {
public Timeslot getTimeslot() { ... }
public void setTimeslot(Timeslot timeslot) { ... }
}
This would enable:
class Lesson {
Timeslot getTimeslot() { ... }
void setTimeslot(Timeslot timeslot) { ... }
}
Scope
This is a small change — no behavior change for existing public classes, no performance impact, and the accessor infrastructure already supports it.
- 主要言語
- Java
- スター
- 1.8k
- フォーク
- 242
- 平均マージ
- 2日 14時間
- マージ済み PR(30日)
- 43
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
TimefoldAI/timefold-solver のほかの issue
-
component/docs
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
TimefoldAI/timefold-solver#2671 ·
メンテナーはふだん 2 日以内に返信
-
process/needs triage
難易度 5/5 1週間以上 初心者へのやさしさ 12/100
TimefoldAI/timefold-solver#2763 · コメント 3 件 ·
メンテナーはふだん 2 日以内に返信
-
Docs: Add keywords for search.対応中かも @TomCools が 17 日前に担当しました。 オープンcomponent/docs
TimefoldAI/timefold-solver#2762 · 担当者 1 名 ·
メンテナーはふだん 2 日以内に返信
-
SDK: Waypoints class has not OpenAPI Spec annotations.対応中かも @TomCools が 18 日前に担当しました。 オープンcomponent/service
TimefoldAI/timefold-solver#2759 · 担当者 1 名 ·
メンテナーはふだん 2 日以内に返信
-
process/needs triage
難易度 5/5 1週間以上 初心者へのやさしさ 30/100
TimefoldAI/timefold-solver#2755 ·
メンテナーはふだん 2 日以内に返信
TimefoldAI/timefold-solver の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
メンテナーはふだん 3 日以内に返信
-
A proxy that fails to start logs a RejectedExecutionException stack trace from its shutdown hookオープンfollow-up
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
Sanne/incus-spawn#1953 ·
メンテナーはふだん 1 日以内に返信
-
Bug
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
HexaHexes/ability-battlewills#37 ·
メンテナーはふだん 1 日以内に返信
-
enhancement
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
Shweit/EtherealPortals#30 ·