quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.4k stars 2.57k forks source link

extension cookbook #13195

Open dufoli opened 3 years ago

dufoli commented 3 years ago

I propose a new documentation more dev oriented for extension instead of api oriented.

I have start to write it:

= Extension Cookbook

== Context & Objective

The purpose of this article is to be more dev oriented and less api oriented to explain how to produce extension for quarkus. So it will not detail how to generate BuildItem or use substitution, but it will explain the workflow to handle extension. First an extension is a module which will be run by quarkus during build time to generate code and inject it. It is a kind of pre-compilation.

== Organisation

There are 2 projects:

The processor is the heart of the extension all build step function is a hierarchy of dependant. Quarkus automatically detect build items input and output and run then in the right order. It mean that sometimes you add an uneeded input just ot be sure it is launched after another method.

The processor can generated bytecode, read config, and pass some data to runtime thanks to recorder (only simple structure).

== JVM : containerization

Quarkus run inside a container, and the container handle the CDI and all injected beans. So it means that processor must generate a producer of client, register servlet or route http request to backend. In order to do this stuff, there are 2 inputs:

=== Injectable client bean (config, producer)

BuildStep can generate a producer with Gizmo :

private void generateCxfClientProducer(BuildProducer<GeneratedBeanBuildItem> generatedBean) {
    ClassOutput classOutput = new GeneratedBeanGizmoAdaptor(generatedBean);

    try (ClassCreator classCreator = ClassCreator.builder().classOutput(classOutput)
            .className("MyProducer")
            .build()) {
            //here generate your client generation method with Produces annotation
    }
}

=== Register servlet

Build step can generate the ServletBuildItem which referring to the servlet class. If you can the used vert.x routes.

=== Route vertx

BuildStep must produce a RouteBuildItem and forward it to a class on Handler.

=== Removable Quarkus cleanup not used beans. So if you need to register a bean for injection but load without inject (CDI.current().select(MyClient.class).get()) There are 3 way to do it:

== Native

If you have followed previous steps, Quarkus container can manage client injection. Now it is time to work on native and that is the hard part because of all limitation you face in a native mode.

https://www.graalvm.org/reference-manual/native-image/Limitations/

=== Reflection Search full text on "Class.forName" and "loadClass" and "getMethod" on the source of module. Create a buildStep with the list of class ReflectiveClassBuildItem set methos flags to true if getMethod is used and false for other. Sometimes fields are used with reflection and constructor so paid attention to enable flags for this kind of usage.

=== ASM emit Search full text for "org.objectweb.asm" or "ClassWriter" and translate all generated class process to gizmo generation. ==> BuildStep with input BuildProducer generatedBeans and ClassOutput classOutput = new GeneratedBeanGizmoAdaptor(generatedBeans);

=== MethodHandle MethodHandle is not supported to use substitution to replace MethodHandle with Method.invoke();

=== Proxy Search full text for "newProxyInstance" each time try to find what is the interface used for proxy and register on a processor NativeImageProxyDefinitionBuildItem.

=== Resources Get list of resources present in the jar and produce a NativeImageResourceBuildItem for each one.

=== Runtime load For this part, you have to run and if you see an error from graalvm saying that a particular class must be loaded on runtime and not static init. It comme from a static init of a source class which load a class only available on runtime (thread for example). Goal is to find the source class with the static init and not the target. First, you must add a property to the project on native profile.

-H:+TraceClassInitialization true Start a new build with this parameter will give you the source class. Then you have to register in the extension processor a RuntimeInitializedClassBuildItem. == JVM : inject class emitted in classLoader and remove substitution Finally, the goal is to load class produce for native part on jvm too. In order to speed up boot time. On native, you can inject code but on jvm part, merge request must been provide to maintainer of source project to add the ability to inject class emitted by quarkus during augmentation to avoid class emitting on jvm. == Debugging class dump : mvn quarkus:dev -Dquarkus.debug.generated-classes-dir=dump-classes build jvm : mvn clean install build native : mvn clean install -Dnative fullstacktrace: -DtrimStackTrace=false
metacosm commented 3 years ago

What would be the best way to collaborate on such a document? Create a new doc/PR so that we can incrementally improve it? I'd be interested in helping with this effort based on my on-going extension development experience 😄 /cc @dufoli

dufoli commented 3 years ago

@metacosm ping