All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.quarkus.smallrye.reactivemessaging.deployment.SmallRyeReactiveMessagingProcessor Maven / Gradle / Ivy

There is a newer version: 3.15.2
Show newest version
package io.quarkus.smallrye.reactivemessaging.deployment;

import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;
import static io.quarkus.smallrye.reactivemessaging.deployment.ReactiveMessagingDotNames.BLOCKING;
import static io.quarkus.smallrye.reactivemessaging.deployment.ReactiveMessagingDotNames.SMALLRYE_BLOCKING;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Vetoed;
import javax.enterprise.inject.spi.DeploymentException;

import org.eclipse.microprofile.reactive.messaging.Channel;
import org.eclipse.microprofile.reactive.messaging.spi.Connector;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;
import org.jboss.logging.Logger;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem;
import io.quarkus.arc.deployment.CustomScopeAnnotationsBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem.BeanClassAnnotationExclusion;
import io.quarkus.arc.processor.AnnotationsTransformer;
import io.quarkus.arc.processor.BeanInfo;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.builder.item.SimpleBuildItem;
import io.quarkus.deployment.Feature;
import io.quarkus.deployment.GeneratedClassGizmoAdaptor;
import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.GeneratedClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem;
import io.quarkus.deployment.recording.RecorderContext;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.ClassOutput;
import io.quarkus.gizmo.FieldDescriptor;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.runtime.metrics.MetricsFactory;
import io.quarkus.runtime.util.HashUtil;
import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem;
import io.quarkus.smallrye.reactivemessaging.deployment.items.InjectedChannelBuildItem;
import io.quarkus.smallrye.reactivemessaging.deployment.items.InjectedEmitterBuildItem;
import io.quarkus.smallrye.reactivemessaging.deployment.items.MediatorBuildItem;
import io.quarkus.smallrye.reactivemessaging.runtime.QuarkusMediatorConfiguration;
import io.quarkus.smallrye.reactivemessaging.runtime.QuarkusWorkerPoolRegistry;
import io.quarkus.smallrye.reactivemessaging.runtime.ReactiveMessagingConfiguration;
import io.quarkus.smallrye.reactivemessaging.runtime.SmallRyeReactiveMessagingLifecycle;
import io.quarkus.smallrye.reactivemessaging.runtime.SmallRyeReactiveMessagingRecorder;
import io.quarkus.smallrye.reactivemessaging.runtime.SmallRyeReactiveMessagingRecorder.SmallRyeReactiveMessagingContext;
import io.quarkus.smallrye.reactivemessaging.runtime.WorkerConfiguration;
import io.quarkus.smallrye.reactivemessaging.runtime.devmode.DevModeSupportConnectorFactory;
import io.quarkus.smallrye.reactivemessaging.runtime.devmode.DevModeSupportConnectorFactoryInterceptor;
import io.smallrye.reactive.messaging.Invoker;
import io.smallrye.reactive.messaging.annotations.Blocking;
import io.smallrye.reactive.messaging.extension.ChannelConfiguration;
import io.smallrye.reactive.messaging.extension.EmitterConfiguration;
import io.smallrye.reactive.messaging.health.SmallRyeReactiveMessagingLivenessCheck;
import io.smallrye.reactive.messaging.health.SmallRyeReactiveMessagingReadinessCheck;
import io.smallrye.reactive.messaging.health.SmallRyeReactiveMessagingStartupCheck;

public class SmallRyeReactiveMessagingProcessor {

    private static final Logger LOGGER = Logger
            .getLogger("io.quarkus.smallrye-reactive-messaging.deployment.processor");

    static final String INVOKER_SUFFIX = "_SmallRyeMessagingInvoker";

    @BuildStep
    FeatureBuildItem feature() {
        return new FeatureBuildItem(Feature.SMALLRYE_REACTIVE_MESSAGING);
    }

    @BuildStep
    AdditionalBeanBuildItem beans() {
        // We add the connector and channel qualifiers to make them part of the index.
        return new AdditionalBeanBuildItem(SmallRyeReactiveMessagingLifecycle.class, Connector.class,
                Channel.class, io.smallrye.reactive.messaging.annotations.Channel.class,
                QuarkusWorkerPoolRegistry.class);
    }

    @BuildStep
    AnnotationsTransformerBuildItem transformBeanScope(BeanArchiveIndexBuildItem index,
            CustomScopeAnnotationsBuildItem scopes) {
        return new AnnotationsTransformerBuildItem(new AnnotationsTransformer() {
            @Override
            public boolean appliesTo(AnnotationTarget.Kind kind) {
                return kind == org.jboss.jandex.AnnotationTarget.Kind.CLASS;
            }

            @Override
            public void transform(AnnotationsTransformer.TransformationContext ctx) {
                if (ctx.isClass()) {
                    ClassInfo clazz = ctx.getTarget().asClass();
                    Map> annotations = clazz.annotations();
                    if (scopes.isScopeDeclaredOn(clazz)
                            || annotations.containsKey(ReactiveMessagingDotNames.JAXRS_PATH)
                            || annotations.containsKey(ReactiveMessagingDotNames.REST_CONTROLLER)
                            || annotations.containsKey(ReactiveMessagingDotNames.JAXRS_PROVIDER)) {
                        // Skip - has a built-in scope annotation or is a JAX-RS endpoint/provider
                        return;
                    }
                    if (annotations.containsKey(ReactiveMessagingDotNames.INCOMING)
                            || annotations.containsKey(ReactiveMessagingDotNames.OUTGOING)
                            || annotations.containsKey(ReactiveMessagingDotNames.CHANNEL)) {
                        LOGGER.debugf(
                                "Found reactive messaging annotations on a class %s with no scope defined - adding @Dependent",
                                ctx.getTarget());
                        ctx.transform().add(Dependent.class).done();
                    }
                }
            }
        });
    }

    @BuildStep
    public List removalExclusions() {
        return Arrays.asList(
                new UnremovableBeanBuildItem(
                        new BeanClassAnnotationExclusion(
                                ReactiveMessagingDotNames.INCOMING)),
                new UnremovableBeanBuildItem(
                        new BeanClassAnnotationExclusion(
                                ReactiveMessagingDotNames.OUTGOING)));
    }

    @BuildStep
    public AnnotationsTransformerBuildItem enableMetrics(Optional metricsCapability,
            ReactiveMessagingConfiguration configuration) {
        boolean isMetricEnabled = metricsCapability.isPresent() && configuration.metricsEnabled;
        boolean useMicrometer = isMetricEnabled && metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER);
        LOGGER.debug("Metrics Enabled: " + isMetricEnabled + "; Using Micrometer: " + useMicrometer);
        return new AnnotationsTransformerBuildItem(new AnnotationsTransformer() {
            @Override
            public boolean appliesTo(AnnotationTarget.Kind kind) {
                return kind == org.jboss.jandex.AnnotationTarget.Kind.CLASS;
            }

            @Override
            public void transform(AnnotationsTransformer.TransformationContext ctx) {
                // Remove the MicrometerDecorator that requires the Micrometer API
                if (ctx.getTarget().asClass().name().equals(ReactiveMessagingDotNames.MICROMETER_DECORATOR)) {
                    if (!isMetricEnabled || !useMicrometer) {
                        ctx.transform()
                                .removeAll()
                                .add(Vetoed.class).done();
                    }
                }
                // Remove the MetricDecorator that requires the MP Metrics API
                if (ctx.getTarget().asClass().name().equals(ReactiveMessagingDotNames.METRIC_DECORATOR)) {
                    if (!isMetricEnabled || useMicrometer) {
                        ctx.transform()
                                .removeAll()
                                .add(Vetoed.class).done();
                    }
                }
            }
        });
    }

    @BuildStep
    public void enableHealth(ReactiveMessagingBuildTimeConfig buildTimeConfig,
            BuildProducer producer) {
        producer.produce(
                new HealthBuildItem(SmallRyeReactiveMessagingLivenessCheck.class.getName(),
                        buildTimeConfig.healthEnabled));
        producer.produce(
                new HealthBuildItem(SmallRyeReactiveMessagingReadinessCheck.class.getName(),
                        buildTimeConfig.healthEnabled));
        producer.produce(
                new HealthBuildItem(SmallRyeReactiveMessagingStartupCheck.class.getName(),
                        buildTimeConfig.healthEnabled));
    }

    @BuildStep
    @Record(STATIC_INIT)
    public void build(SmallRyeReactiveMessagingRecorder recorder, RecorderContext recorderContext,
            BuildProducer syntheticBeans,
            List mediatorMethods,
            List emitterFields,
            List channelFields,
            BuildProducer generatedClass,
            BuildProducer reflectiveClass,
            ReactiveMessagingConfiguration conf) {

        ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClass, true);

        List mediatorConfigurations = new ArrayList<>(mediatorMethods.size());
        List workerConfigurations = new ArrayList<>();
        List emittersConfigurations = new ArrayList<>();
        List channelConfigurations = new ArrayList<>();

        /*
         * Go through the collected MediatorMethods and build up the corresponding MediaConfiguration
         * This includes generating an invoker for each method
         * The configuration will then be captured and used at static init time to push data into smallrye
         */
        for (MediatorBuildItem mediatorMethod : mediatorMethods) {
            MethodInfo methodInfo = mediatorMethod.getMethod();
            BeanInfo bean = mediatorMethod.getBean();

            if (methodInfo.hasAnnotation(BLOCKING) || methodInfo.hasAnnotation(SMALLRYE_BLOCKING)) {
                // Just in case both annotation are used, use @Blocking value.
                String poolName = Blocking.DEFAULT_WORKER_POOL;
                if (methodInfo.hasAnnotation(ReactiveMessagingDotNames.BLOCKING)) {
                    AnnotationInstance blocking = methodInfo.annotation(ReactiveMessagingDotNames.BLOCKING);
                    poolName = blocking.value() == null ? Blocking.DEFAULT_WORKER_POOL : blocking.value().asString();
                }
                workerConfigurations.add(new WorkerConfiguration(methodInfo.declaringClass().toString(),
                        methodInfo.name(), poolName));
            }

            try {
                boolean isSuspendMethod = isSuspendMethod(methodInfo);

                QuarkusMediatorConfiguration mediatorConfiguration = QuarkusMediatorConfigurationUtil
                        .create(methodInfo, isSuspendMethod, bean, recorderContext,
                                Thread.currentThread().getContextClassLoader(), conf.strict);
                mediatorConfigurations.add(mediatorConfiguration);

                String generatedInvokerName = generateInvoker(bean, methodInfo, isSuspendMethod, mediatorConfiguration,
                        classOutput);
                /*
                 * We need to register the invoker's constructor for reflection since it will be called inside smallrye.
                 * We could potentially lift this restriction with some extra CDI bean generation, but it's probably not worth
                 * it
                 */
                reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, generatedInvokerName));
                mediatorConfiguration
                        .setInvokerClass((Class) recorderContext.classProxy(generatedInvokerName));
            } catch (IllegalArgumentException e) {
                throw new DeploymentException(e); // needed to pass the TCK
            }
        }

        for (InjectedEmitterBuildItem it : emitterFields) {
            emittersConfigurations.add(it.getEmitterConfig());
        }
        for (InjectedChannelBuildItem it : channelFields) {
            channelConfigurations.add(it.getChannelConfig());
        }

        syntheticBeans.produce(SyntheticBeanBuildItem.configure(SmallRyeReactiveMessagingContext.class)
                .supplier(recorder.createContext(mediatorConfigurations, workerConfigurations, emittersConfigurations,
                        channelConfigurations))
                .done());
    }

    private boolean isSuspendMethod(MethodInfo methodInfo) {
        if (!methodInfo.parameters().isEmpty()) {
            return methodInfo.parameters().get(methodInfo.parameters().size() - 1).name()
                    .equals(ReactiveMessagingDotNames.CONTINUATION);
        }
        return false;
    }

    /**
     * Generates an invoker class that looks like the following:
     *
     * 
     * public class SomeName implements Invoker {
     *     private BeanType beanInstance;
     *
     *     public SomeName(Object var1) {
     *         this.beanInstance = var1;
     *     }
     *
     *     public Object invoke(Object[] args) {
     *         return this.beanInstance.doSomething(var1);
     *     }
     * }
     * 
*/ private String generateInvoker(BeanInfo bean, MethodInfo method, boolean isSuspendMethod, QuarkusMediatorConfiguration mediatorConfiguration, ClassOutput classOutput) { String baseName; if (bean.getImplClazz().enclosingClass() != null) { baseName = DotNames.simpleName(bean.getImplClazz().enclosingClass()) + "_" + DotNames.simpleName(bean.getImplClazz().name()); } else { baseName = DotNames.simpleName(bean.getImplClazz().name()); } StringBuilder sigBuilder = new StringBuilder(); sigBuilder.append(method.name()).append("_").append(method.returnType().name().toString()); for (Type i : method.parameters()) { sigBuilder.append(i.name().toString()); } String targetPackage = DotNames.internalPackageNameWithTrailingSlash(bean.getImplClazz().name()); String generatedName = targetPackage + baseName + INVOKER_SUFFIX + "_" + method.name() + "_" + HashUtil.sha1(sigBuilder.toString()); if (isSuspendMethod && ((mediatorConfiguration.getIncoming().isEmpty()) && (mediatorConfiguration.getOutgoing() != null))) { // TODO: this restriction needs to be lifted throw new IllegalStateException( "Currently suspend methods for Reactive Messaging are not supported on methods that are only annotated with @Outgoing"); } if (!isSuspendMethod) { generateStandardInvoker(method, classOutput, generatedName); } else if (!mediatorConfiguration.getIncoming().isEmpty()) { generateSubscribingCoroutineInvoker(method, classOutput, generatedName); } return generatedName.replace('/', '.'); } private void generateStandardInvoker(MethodInfo method, ClassOutput classOutput, String generatedName) { try (ClassCreator invoker = ClassCreator.builder().classOutput(classOutput).className(generatedName) .interfaces(Invoker.class) .build()) { String beanInstanceType = method.declaringClass().name().toString(); FieldDescriptor beanInstanceField = invoker.getFieldCreator("beanInstance", beanInstanceType) .getFieldDescriptor(); // generate a constructor that takes the bean instance as an argument // the method parameter needs to be of type Object because that is what is used as the call site in SmallRye // Reactive Messaging try (MethodCreator ctor = invoker.getMethodCreator("", void.class, Object.class)) { ctor.setModifiers(Modifier.PUBLIC); ctor.invokeSpecialMethod(MethodDescriptor.ofConstructor(Object.class), ctor.getThis()); ResultHandle self = ctor.getThis(); ResultHandle beanInstance = ctor.getMethodParam(0); ctor.writeInstanceField(beanInstanceField, self, beanInstance); ctor.returnValue(null); } try (MethodCreator invoke = invoker.getMethodCreator( MethodDescriptor.ofMethod(generatedName, "invoke", Object.class, Object[].class))) { int parametersCount = method.parameters().size(); String[] argTypes = new String[parametersCount]; ResultHandle[] args = new ResultHandle[parametersCount]; for (int i = 0; i < parametersCount; i++) { // the only method argument of io.smallrye.reactive.messaging.Invoker is an object array, so we need to pull out // each argument and put it in the target method arguments array args[i] = invoke.readArrayValue(invoke.getMethodParam(0), i); argTypes[i] = method.parameters().get(i).name().toString(); } ResultHandle result = invoke.invokeVirtualMethod( MethodDescriptor.ofMethod(beanInstanceType, method.name(), method.returnType().name().toString(), argTypes), invoke.readInstanceField(beanInstanceField, invoke.getThis()), args); if (ReactiveMessagingDotNames.VOID.equals(method.returnType().name())) { invoke.returnValue(invoke.loadNull()); } else { invoke.returnValue(result); } } } } private void generateSubscribingCoroutineInvoker(MethodInfo method, ClassOutput classOutput, String generatedName) { try (ClassCreator invoker = ClassCreator.builder().classOutput(classOutput).className(generatedName) .superClass(ReactiveMessagingDotNames.ABSTRACT_SUBSCRIBING_COROUTINE_INVOKER.toString()) .build()) { // generate a constructor that takes the bean instance as an argument // the method parameter type needs to be Object, because that is what is used as the call site in SmallRye // Reactive Messaging try (MethodCreator ctor = invoker.getMethodCreator("", void.class, Object.class)) { ctor.setModifiers(Modifier.PUBLIC); ctor.invokeSpecialMethod( MethodDescriptor.ofConstructor( ReactiveMessagingDotNames.ABSTRACT_SUBSCRIBING_COROUTINE_INVOKER.toString(), Object.class.getName()), ctor.getThis(), ctor.getMethodParam(0)); ctor.returnValue(null); } try (MethodCreator invoke = invoker.getMethodCreator("invokeBean", Object.class, Object.class, Object[].class, ReactiveMessagingDotNames.CONTINUATION.toString())) { ResultHandle[] args = new ResultHandle[method.parameters().size()]; ResultHandle array = invoke.getMethodParam(1); for (int i = 0; i < method.parameters().size() - 1; ++i) { args[i] = invoke.readArrayValue(array, i); } args[args.length - 1] = invoke.getMethodParam(2); ResultHandle result = invoke.invokeVirtualMethod(method, invoke.getMethodParam(0), args); invoke.returnValue(result); } } } @BuildStep(onlyIf = IsDevelopment.class) void devmodeSupport(CombinedIndexBuildItem index, BuildProducer beans, BuildProducer transformations) { beans.produce(new AdditionalBeanBuildItem(DevModeSupportConnectorFactory.class, DevModeSupportConnectorFactoryInterceptor.class)); transformations.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { @Override public boolean appliesTo(AnnotationTarget.Kind kind) { return kind == AnnotationTarget.Kind.CLASS; } @Override public void transform(TransformationContext ctx) { ClassInfo clazz = ctx.getTarget().asClass(); if (doesImplement(clazz, ReactiveMessagingDotNames.INCOMING_CONNECTOR_FACTORY, index.getIndex()) || doesImplement(clazz, ReactiveMessagingDotNames.OUTGOING_CONNECTOR_FACTORY, index.getIndex())) { ctx.transform().add(DevModeSupportConnectorFactory.class).done(); } } private boolean doesImplement(ClassInfo clazz, DotName iface, IndexView index) { while (clazz != null && !clazz.name().equals(ReactiveMessagingDotNames.OBJECT)) { if (clazz.interfaceNames().contains(iface)) { return true; } clazz = index.getClassByName(clazz.superName()); } return false; } })); } @BuildStep CoroutineConfigurationBuildItem producesCoroutineConfiguration() { try { Class.forName("kotlinx.coroutines.future.FutureKt", false, getClass().getClassLoader()); return new CoroutineConfigurationBuildItem(true); } catch (ClassNotFoundException e) { return new CoroutineConfigurationBuildItem(false); } } @BuildStep void produceCoroutineScope( CoroutineConfigurationBuildItem coroutineConfigurationBuildItem, BuildProducer buildItemBuildProducer) { if (coroutineConfigurationBuildItem.isEnabled()) { buildItemBuildProducer.produce(AdditionalBeanBuildItem.builder() .addBeanClasses( "io.quarkus.smallrye.reactivemessaging.runtime.kotlin.ApplicationCoroutineScope") .setUnremovable().build()); } } private void ensureKotlinCoroutinesEnabled(CoroutineConfigurationBuildItem coroutineConfigurationBuildItem, MethodInfo method) { if (!coroutineConfigurationBuildItem.isEnabled()) { String format = String.format( "Method %s.%s is suspendable but kotlinx-coroutines-jdk8 dependency not detected", method.declaringClass().name(), method.name()); throw new IllegalStateException(format); } } public static final class CoroutineConfigurationBuildItem extends SimpleBuildItem { private final boolean isEnabled; public CoroutineConfigurationBuildItem(boolean isEnabled) { this.isEnabled = isEnabled; } public boolean isEnabled() { return isEnabled; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy