Skip to main content

converting-recorders-to-services

Step-by-step guide for converting Quarkus extensions from the legacy @Record/@Recorder pattern to the ServiceRegistrar service system.

Informations de source

Dépôt
quarkusio/quarkus
Dernière activité de la source
9 octobre 2026 à 15:54
Langue détectée de SKILL.md
anglais
Étoiles
15 942
Forks
3 337

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
converting-recorders-to-services
description
Step-by-step guide for converting Quarkus extensions from the legacy @Record/@Recorder pattern to the ServiceRegistrar service system.
# Converting Recorders to Services ## Architecture The `ServiceRegistrar` service system replaces `@Recorder` with typed service. Services declare dependencies explicitly and execute in dependency order, not build step order. The system is designed for incremental migration — converted and unconverted extensions may coexist. ### Key principles 1. **Explicit dependencies only.** Services get NO implicit ordering from the build step graph. All ordering comes from `require()`, `after()`, `before()`, or `afterBuildItem()`. 2. **Services are independent from recorders in the same step.** Even if a build step method contains both a `@Record` call and an `ServiceRegistrar` call, the service and recorder are separate nodes with no implicit ordering between them. 3. **Values flow through the dependency graph.** Service values are passed from producer to consumer via indexed dependency access on `ServiceNode`, not through the `StartupContext` maps (except for recorder proxy bridges). ### Node types in the service graph | Kind | Description | Gets step-graph deps? | |------|-------------|-----------------------| | `LEGACY_RECORDER` | Recorder chunk from a `@Record` step | Yes | | `ALIAS` | Bridges a recorder value to a service key | Yes | | `SERVICE` | `ServiceRegistrar` service with a lambda body | No | | `RV_WRAPPER` | Wraps a service value in `RuntimeValue` for recorders | No | | `CROSS_PHASE_PROXY` | Bridges a static-init service to the runtime graph | No | Legacy recorders and aliases use step-graph ordering for compatibility. Service nodes use only explicit declarations. ## Prerequisites Before converting, verify that all runtime dependencies are available as services: | Dependency Type | Availability | |-----------------|-------------| | `ArcContainer` | Service in `ArcProcessor#initializeContainer` | | `BeanContainer` | Service in `ArcProcessor#createBeanContainer` | | `ScheduledExecutorService` | Aliased by `ThreadPoolSetup` | | `@ConfigMapping` (`RUN_TIME`) | Auto-registered by `ConfigServiceRegistrationStep` | | `@ConfigMapping` (`BUILD_AND_RUN_TIME_FIXED`) | Auto-registered as static-init | | `@ConfigMapping` (`BUILD_TIME`) | **Cannot be a service** — extract values into locals | | Other runtime objects | Search for `aliasRecorderValue` or `forService` | ## Conversion steps ### 1. Analyze the recorder For each `@Recorder` method, determine: - **Return type** — becomes the service type. `RuntimeValue<X>` unwraps to `X`. `void` becomes `Void.class` with a descriptive name. - **Parameters** — `RuntimeValue<X>` params become `require(X.class)`. Simple values (String, int, boolean, enum) can be captured. Immutable collections (`List.of()`, `Set.of()`, etc.) can be captured. - **Instance fields** — config `RuntimeValue`s injected via constructor become `require()` dependencies. - **Trivial methods** — inline directly into the lambda. ### 2. Convert the recorder class - **Trivial methods** — delete; inline into the lambda. - **Complex methods** — remove `@Recorder`, make methods `static`, unwrap `RuntimeValue` parameters and return types. - If all methods are converted, delete the class. Otherwise keep `@Recorder` for remaining methods. ### 3. Convert the build step **Before:** ```java @BuildStep @Record(ExecutionTime.RUNTIME_INIT) ServiceStartBuildItem setup(MyRecorder recorder, SomeBuildItem item) { recorder.initialize(item.getValue()); return new ServiceStartBuildItem("my-feature"); } ``` **After:** ```java @BuildStep ServiceStartBuildItem setup(ServiceRegistrar serviceRegistrar, SomeBuildItem item) { serviceRegistrar .forService("io.quarkus.my-feature.setup") .onStart(ctx -> MyRecorder.initialize()); return new ServiceStartBuildItem("my-feature"); } ``` Key changes: - Remove `@Record(ExecutionTime.*)` annotation. - Replace recorder parameter with `ServiceRegistrar serviceRegistrar`. - Use `.atPhase(Phase.STATIC_INIT)` for static-init services (default is runtime-init / `Phase.APPLICATION`). - For void services, use hierarchical dot-separated names: `"io.quarkus.<extension>.<purpose>"`. ### 4. Declare dependencies **Every dependency must be explicit.** There is no implicit ordering from the build step graph. | Old Pattern | New Pattern | |------------|-------------| | `recorder.method(beanContainer.getValue())` | `.require(BeanContainer.class)` | | `recorder.method(someRuntimeValue)` | `.require(SomeType.class)` | | `recorder.method(config.maxSize())` | Capture `int maxSize = config.maxSize()` before lambda | | `@Consume(SyntheticBeansRuntimeInitBuildItem.class)` on step | `.afterBuildItem(SyntheticBeansRuntimeInitBuildItem.class)` on service | #### `afterBuildItem()` — bridging to build step ordering When a service depends on state produced by a legacy recorder (e.g., synthetic beans must be initialized), declare the dependency via `afterBuildItem()`: ```java serviceRegistrar .forService("io.quarkus.arc.lifecycle") .afterBuildItem(SyntheticBeansRuntimeInitBuildItem.class) .onStart(ctx -> ArcRecorder.fireLifecycleEvent(new StartupEvent())); ``` This resolves the producing step's nodes (with passthrough resolution for steps that have no service graph nodes) and creates ordering edges. **`afterBuildItem()` is deprecated** — it exists only for recorder coexistence. Once the producing recorder is converted to a service, replace with `require()` or `after()`, or drop it, as appropriate. #### Cross-phase dependencies Runtime services can `require()` static-init services. The framework automatically creates a `CROSS_PHASE_PROXY` node that reads the value from the `serviceValues` map (populated during static-init and retained via `retainServiceValues()`). ```java // static-init service serviceRegistrar.forService(ArcContainer.class) .atPhase(Phase.STATIC_INIT) .afterBuildItem(ResourcesGeneratedPhaseBuildItem.class) .onStart(ctx -> Arc.initialize()); // runtime service that depends on it serviceRegistrar.forService("io.quarkus.my-ext.setup") .require(ArcContainer.class) // cross-phase: resolved via proxy .onStart((ctx, container) -> { ... }); ``` #### Config mappings - `BUILD_TIME` — extract values into locals before the lambda. - `BUILD_AND_RUN_TIME_FIXED` — use `.require(ConfigType.class)` (resolved directly from SmallRye Config, no graph edge). - `RUN_TIME` — use `.require(ConfigType.class)` (same mechanism). ### 5. Stop ordering with `before()` `before(X)` declares "X depends on me" — this service starts before X and stops after X. Use it for cleanup services that must outlive their dependents: ```java serviceRegistrar .forService("io.quarkus.vertx.netty-thread-local-cleanup") .atPhase(Phase.STATIC_INIT) .before(IOThreadDetector.class) .before(ArcContainer.class) .onStart(ctx -> { ctx.onStop(() -> InternalThreadLocalMap.remove()); }); ``` ### 6. Bridge to legacy build items Build items consumed by unconverted extensions need bridge proxies: | Build Item Stores | Bridge Method | |-------------------|---------------| | `RuntimeValue<T>` | `serviceRegistrar.staticInitServiceAsRuntimeValue(T.class)` or `serviceRegistrar.serviceAsRuntimeValue(T.class)` | | Bare `T` (interface) | `serviceRegistrar.staticInitServiceAsRecorderValue(T.class)` or `serviceRegistrar.serviceAsRecorderValue(T.class)` | | Bare `T` (concrete) | Use `RuntimeValue` variant — concrete classes may not be proxyable | Recorder proxies resolve via `startupContext.getServiceValue()` (for `__service$$value()` proxies) or `startupContext.getValue()` (for standard recorder proxies). The service deploy body stores values in the `serviceValues` map for this resolution. ### 7. Synthetic beans **Before:** ```java Supplier<MyBean> supplier = recorder.createBeanSupplier(args); SyntheticBeanBuildItem.configure(MyBean.class).supplier(supplier).done(); ``` **After:** ```java serviceRegistrar.forService(MyBean.class, "my-bean-name") .onStart(ctx -> new MyBean(args)); SyntheticBeanBuildItem.configure(MyBean.class) .serviceValue(MyBean.class, "my-bean-name") .done(); ``` ### 8. Shutdown handlers Register cleanup via `ctx.onStop(Runnable)` or `ctx.onStopAsync(Consumer<AsyncStopContext>)`. Stop handlers run in reverse dependency order. `addLastShutdownTask()` is **deprecated** — its per-node "last" semantics don't provide global ordering. Use `before()` to ensure your service stops after the services it cleans up for. ### 9. Runtime-optional services A value-producing service must return a non-`null` value — the generated code throws `"...returned null; use a void service if no value is produced"` otherwise. For a service whose *identity* is known at build time but which may be **absent at runtime** (e.g. a handler gated by `quarkus.log.file.enabled`), declare `optional()` before `onStart()`. This flips the service's return type from `T` to `Optional<T>`; returning `Optional.empty()` marks the service absent (nothing constructed, no side effects) instead of failing. ```java serviceRegistrar.forService(Handler.class, "file") .atPhase(Phase.LOGGING) .require(LogRuntimeConfig.class) .optional() .onStart((ctx, config) -> config.file().enabled() ? Optional.of(buildFileHandler(config.file())) : Optional.empty()); ``` Consuming an optional service: - `consumeAll(T.class)` skips absent producers (the map omits them). - `request(T.class)` yields `Optional.empty()` when absent. - `require(T.class)` on an optional service is a **build error** — a mandatory dependency cannot depend on a maybe-absent value. Use `request()` or `consumeAll()` instead. Do not return `null` from a normal (non-`optional`) service, and do not use a sentinel value — `optional()` is the supported mechanism. (The `null` tolerance in `consumeAll` applies only to aliased recorder values that resolve to `null`, not to direct services.) ### 10. Lambda capture rules Supported captures: - Primitives, `String`, `Class`, enum values - Immutable collections: `List.of()`, `Set.of()`, `Map.of()`, `Collections.singletonList/Set/Map()`, `Collections.empty*()`, `Collections.unmodifiable*()`, `Set.copyOf()`, `List.copyOf()`, `Map.copyOf()` - `BUILD_AND_RUN_TIME_FIXED` config mappings (via `ConfigCaptureInterceptor`) - `Constable` values — any type implementing `java.lang.constant.Constable` is captured via its `describeConstable()`. To make a custom value type capturable, implement `Constable` with a `describeConstable()` that reconstructs it (e.g. a `DynamicConstantDesc` invoking a static factory), rather than registering a recorder `ObjectSubstitution`. Immutable collections of `Constable` values (via `Map.copyOf`/`List.copyOf`) are themselves capturable. - `record` values — captured by reconstructing via the canonical constructor, as long as every component is itself capturable (recursively). Note this is the **capture** path; it is unrelated to the *proxy* path — a `record` still cannot be used as an aliased recorder return type (see "Aliased recorder return types must be proxyable" below). **Not capturable:** - Mutable collections — use `Set.copyOf()` / `List.copyOf()` first - `BUILD_TIME` config objects — extract values into locals - `RUN_TIME` config objects — use `require()` instead - Arbitrary runtime objects — use `require()` instead ## Common pitfalls ### No implicit ordering from build steps Services get NO ordering from `@Consume`/`@Produce` annotations or build item parameters on their build step method. Every ordering dependency must be declared on the service itself. If a service needs to run after a legacy recorder step, use `afterBuildItem()` to depend on a build item that step produces. ### Concrete class proxying `staticInitServiceAsRecorderValue(ConcreteClass.class)` fails if the class has a non-trivial constructor. Use `staticInitServiceAsRuntimeValue()` instead — `RuntimeValue` is always proxyable. ### Aliased recorder return types must be proxyable A value published via `aliasRecorderValue(T.class, recorderValue)` — or any `@Recorder` method whose return value is used as a recorder proxy — must have a **proxyable** type: an interface or a non-`final` class. A `record` is `final`, so it fails with *"the return type cannot be proxied; use RuntimeValue to wrap the return value instead"*. When bridging a bundle of recorder values into a single service value, make the holder an **interface** (with a nested `record` impl for the runtime value), or wrap it in `RuntimeValue`. Abstract JDK types such as `java.util.logging.Formatter` and `java.util.logging.Handler` are already proxyable. ### Package-private visibility Lambda bytecode runs at runtime but references runtime classes directly. Package-private classes become inaccessible from the generated consolidated class. Solutions: 1. Add a public static factory method on a public runtime class. 2. Move the class to a `.impl` subpackage and make it public. ### Never reference deployment classes from within service lambdas Action lambda bytecode (defined inside `.onStart(ctx -> { ... })`) is executed at **runtime**. Therefore, the lambda body must **never** reference, instantiate, or invoke methods on any deployment classes (such as your `@BuildStep` processor classes). Doing so will compile successfully but will throw a `ClassNotFoundException` / `NoClassDefFoundError` at runtime because deployment modules are absent from the runtime classpath. * **The Anonymous Inner Class Trap**: Writing an anonymous inner class (e.g. `new MyInterface() {}`) inside an instance-method lambda forces `javac` to generate a constructor that captures `EnclosingProcessor.this` (via a synthetic `this$0` parameter) to bind the inner class to its enclosing processor scope. This turns your lambda into an instance-capturing lambda (throwing a `TransliterationException` at build time).
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub