diff --git a/docs/modules/ROOT/examples/calculator-client/application.properties b/docs/modules/ROOT/examples/calculator-client/application.properties index 7ea2b1aff..b7d52e1a3 100644 --- a/docs/modules/ROOT/examples/calculator-client/application.properties +++ b/docs/modules/ROOT/examples/calculator-client/application.properties @@ -20,3 +20,10 @@ quarkus.cxf.client.codeFirstClient.client-endpoint-url=${cxf.it.calculator.baseU quarkus.cxf.client.codeFirstClient.service-interface=io.quarkiverse.cxf.client.it.CodeFirstClient quarkus.cxf.client.codeFirstClient.endpoint-namespace=http://www.jboss.org/eap/quickstarts/wscalculator/Calculatorrr quarkus.cxf.client.codeFirstClient.endpoint-name=CalculatorService + +quarkus.cxf.client.clientWithRuntimeInitializedPayload.client-endpoint-url=${cxf.it.calculator.baseUri}/calculator-ws/CalculatorService +quarkus.cxf.client.clientWithRuntimeInitializedPayload.service-interface=io.quarkiverse.cxf.client.it.rtinit.ClientWithRuntimeInitializedPayload +quarkus.cxf.client.clientWithRuntimeInitializedPayload.endpoint-namespace=http://www.jboss.org/eap/quickstarts/wscalculator/Calculator +quarkus.cxf.client.clientWithRuntimeInitializedPayload.endpoint-name=CalculatorService +quarkus.cxf.client.clientWithRuntimeInitializedPayload.native.runtime-initialized = true +quarkus.native.additional-build-args=--initialize-at-run-time=io.quarkiverse.cxf.client.it.rtinit.Operands\\,io.quarkiverse.cxf.client.it.rtinit.Result diff --git a/docs/modules/ROOT/pages/includes/quarkus-cxf.adoc b/docs/modules/ROOT/pages/includes/quarkus-cxf.adoc index 6fcf2ba90..70150d174 100644 --- a/docs/modules/ROOT/pages/includes/quarkus-cxf.adoc +++ b/docs/modules/ROOT/pages/includes/quarkus-cxf.adoc @@ -154,6 +154,56 @@ endif::add-copy-button-to-env-var[] | +a|icon:lock[title=Fixed at build time] [[quarkus-cxf_quarkus.cxf.client.-clients-.service-interface]]`link:#quarkus-cxf_quarkus.cxf.client.-clients-.service-interface[quarkus.cxf.client."clients".service-interface]` + +[.description] +-- +The client service interface class name + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_CXF_CLIENT__CLIENTS__SERVICE_INTERFACE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_CXF_CLIENT__CLIENTS__SERVICE_INTERFACE+++` +endif::add-copy-button-to-env-var[] +--|string +| + + +a|icon:lock[title=Fixed at build time] [[quarkus-cxf_quarkus.cxf.client.-clients-.alternative]]`link:#quarkus-cxf_quarkus.cxf.client.-clients-.alternative[quarkus.cxf.client."clients".alternative]` + +[.description] +-- +Indicates whether this is an alternative proxy client configuration. If true, then this configuration is ignored when configuring a client without annotation `@CXFClient`. + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_CXF_CLIENT__CLIENTS__ALTERNATIVE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_CXF_CLIENT__CLIENTS__ALTERNATIVE+++` +endif::add-copy-button-to-env-var[] +--|boolean +|`false` + + +a|icon:lock[title=Fixed at build time] [[quarkus-cxf_quarkus.cxf.client.-clients-.native.runtime-initialized]]`link:#quarkus-cxf_quarkus.cxf.client.-clients-.native.runtime-initialized[quarkus.cxf.client."clients".native.runtime-initialized]` + +[.description] +-- +If `true`, the client dynamic proxy class generated by native compiler will be initialized at runtime; otherwise the proxy class will be initialized at build time. +Setting this to `true` makes sense if your service endpoint interface references some class initialized at runtime in its method signatures. E.g. Say, your service interface has method @code++{++int add(Operands o)++}++ and the `Operands` class was requested to be initialized at runtime. Then, without setting this configuration parameter to `true`, the native compiler will throw an exception saying something like `Classes that should be initialized at run time got initialized during image building: org.acme.Operands ... jdk.proxy.$Proxy caused initialization of this class`. `jdk.proxy.$Proxy` is the proxy class generated by the native compiler. +While `quarkus-cxf` can auto-detect the proper setting in some cases, the auto-detection is not perfect. This is because runtime initialization of classes can be requested in many ways out of which only the ones done via Quarkus `RuntimeInitializedClassBuildItem` and `RuntimeInitializedPackageBuildItem` can safely be observed by `quarkus-cxf`. In other cases, you'll have to set this manually. + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_CXF_CLIENT__CLIENTS__NATIVE_RUNTIME_INITIALIZED+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_CXF_CLIENT__CLIENTS__NATIVE_RUNTIME_INITIALIZED+++` +endif::add-copy-button-to-env-var[] +--|boolean +|`false` + + a| [[quarkus-cxf_quarkus.cxf.endpoint.-endpoints-.implementor]]`link:#quarkus-cxf_quarkus.cxf.endpoint.-endpoints-.implementor[quarkus.cxf.endpoint."endpoints".implementor]` [.description] diff --git a/extensions/core/deployment/src/main/java/io/quarkiverse/cxf/deployment/CxfClientBuildItem.java b/extensions/core/deployment/src/main/java/io/quarkiverse/cxf/deployment/CxfClientBuildItem.java index caebae96b..8df55c866 100644 --- a/extensions/core/deployment/src/main/java/io/quarkiverse/cxf/deployment/CxfClientBuildItem.java +++ b/extensions/core/deployment/src/main/java/io/quarkiverse/cxf/deployment/CxfClientBuildItem.java @@ -4,15 +4,22 @@ * Holds a client endpoint metadata. */ public final class CxfClientBuildItem extends AbstractEndpointBuildItem { + private final String sei; + private final boolean proxyClassRuntimeInitialized; + public CxfClientBuildItem(String sei, String soapBinding, String wsNamespace, - String wsName) { + String wsName, boolean runtimeInitialized) { super(soapBinding, wsNamespace, wsName); this.sei = sei; + this.proxyClassRuntimeInitialized = runtimeInitialized; } - private final String sei; - public String getSei() { return sei; } + + public boolean isProxyClassRuntimeInitialized() { + return proxyClassRuntimeInitialized; + } + } diff --git a/extensions/core/deployment/src/main/java/io/quarkiverse/cxf/deployment/CxfClientProcessor.java b/extensions/core/deployment/src/main/java/io/quarkiverse/cxf/deployment/CxfClientProcessor.java index 35f6af036..3ceb96cc0 100644 --- a/extensions/core/deployment/src/main/java/io/quarkiverse/cxf/deployment/CxfClientProcessor.java +++ b/extensions/core/deployment/src/main/java/io/quarkiverse/cxf/deployment/CxfClientProcessor.java @@ -1,35 +1,46 @@ package io.quarkiverse.cxf.deployment; +import java.io.IOException; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; import java.util.stream.Collectors; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.inject.Produces; import javax.enterprise.inject.spi.InjectionPoint; import javax.inject.Inject; +import javax.xml.ws.BindingProvider; import javax.xml.ws.soap.SOAPBinding; +import org.apache.cxf.endpoint.Client; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.MethodParameterInfo; +import org.jboss.jandex.Type; import org.jboss.logging.Logger; import io.quarkiverse.cxf.CXFClientData; import io.quarkiverse.cxf.CXFClientInfo; import io.quarkiverse.cxf.CXFRecorder; import io.quarkiverse.cxf.CxfClientProducer; +import io.quarkiverse.cxf.CxfFixedConfig; +import io.quarkiverse.cxf.CxfFixedConfig.ClientFixedConfig; import io.quarkiverse.cxf.annotation.CXFClient; +import io.quarkiverse.cxf.graal.QuarkusCxfFeature; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; @@ -40,7 +51,12 @@ import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.NativeImageFeatureBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedPackageBuildItem; +import io.quarkus.deployment.util.IoUtil; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.FieldCreator; @@ -57,16 +73,29 @@ public class CxfClientProcessor { @BuildStep void collectClients( + CxfFixedConfig config, CombinedIndexBuildItem combinedIndexBuildItem, + List runtimeInitializedClasses, + List runtimeInitializedPackages, + BuildProducer nativeImageFeatures, BuildProducer proxies, BuildProducer clients) { IndexView index = combinedIndexBuildItem.getIndex(); - final Set clientSEIsInUse = findClientSEIsInUse(index); + final Set rtInitClasses = runtimeInitializedClasses.stream() + .map(RuntimeInitializedClassBuildItem::getClassName) + .collect(Collectors.toSet()); + final Set rtInitPackages = runtimeInitializedPackages.stream() + .map(RuntimeInitializedPackageBuildItem::getPackageName) + .collect(Collectors.toSet()); + + final AtomicBoolean hasRuntimeInitializedProxy = new AtomicBoolean(false); + final Map clientSEIsInUse = findClientSEIsInUse(index, config); CxfDeploymentUtils.webServiceAnnotations(index) .forEach(annotation -> { final ClassInfo wsClassInfo = annotation.target().asClass(); - if (clientSEIsInUse.contains(wsClassInfo.name().toString())) { + ClientFixedConfig clientConfig = clientSEIsInUse.get(wsClassInfo.name().toString()); + if (clientConfig != null) { final String sei = wsClassInfo.name().toString(); AnnotationInstance webserviceClient = findWebServiceClientAnnotation(index, wsClassInfo.name()); final String wsName; @@ -87,14 +116,27 @@ void collectClients( .map(bindingType -> bindingType.value().asString()) .orElse(SOAPBinding.SOAP11HTTP_BINDING); + final ProxyInfo proxyInfo = ProxyInfo.of( + Optional.ofNullable(clientConfig.native_).map(native_ -> native_.runtimeInitialized) + .orElse(false), + wsClassInfo, + rtInitClasses, + rtInitPackages, + index); + proxies.produce(new NativeImageProxyDefinitionBuildItem(proxyInfo.interfaces)); + clients.produce( - new CxfClientBuildItem(sei, soapBinding, wsNamespace, wsName)); - proxies.produce(new NativeImageProxyDefinitionBuildItem(wsClassInfo.name().toString(), - "javax.xml.ws.BindingProvider", "java.io.Closeable", "org.apache.cxf.endpoint.Client")); + new CxfClientBuildItem(sei, soapBinding, wsNamespace, wsName, proxyInfo.isRuntimeInitialized)); + + hasRuntimeInitializedProxy.set(hasRuntimeInitializedProxy.get() || proxyInfo.isRuntimeInitialized); } }); + if (hasRuntimeInitializedProxy.get()) { + nativeImageFeatures.produce(new NativeImageFeatureBuildItem(QuarkusCxfFeature.class)); + } + } @BuildStep @@ -118,7 +160,8 @@ void startClient( client.getSei(), client.getWsName(), client.getWsNamespace(), - wrapperClassNames.get(client.getSei()))) + wrapperClassNames.get(client.getSei()), + client.isProxyClassRuntimeInitialized())) .map(cxf -> { LOGGER.debugf("producing dedicated CXFClientInfo bean named '%s' for SEI %s", cxf.getSei(), cxf.getSei()); return SyntheticBeanBuildItem @@ -146,25 +189,87 @@ private static AnnotationInstance findWebServiceClientAnnotation(IndexView index return null; } - private static Set findClientSEIsInUse(IndexView index) { - return index.getAnnotations(CxfDotNames.CXFCLIENT_ANNOTATION).stream() - .map(AnnotationInstance::target) - .map(target -> { - switch (target.kind()) { - case FIELD: - return target.asField().type(); - case METHOD_PARAMETER: - MethodParameterInfo paramInfo = target.asMethodParameter(); - return paramInfo.method().parameterTypes().get(paramInfo.position()); - default: - return null; - } - }) - .filter(Objects::nonNull) - .map(type -> type.name().equals(CxfDotNames.INJECT_INSTANCE) ? type.asParameterizedType().arguments().get(0) - : type) - .map(type -> type.name().toString()) - .collect(Collectors.toSet()); + private static Map findClientSEIsInUse(IndexView index, CxfFixedConfig config) { + final Map seiToClientConfig = new TreeMap<>(); + index.getAnnotations(CxfDotNames.CXFCLIENT_ANNOTATION).forEach(annotationInstance -> { + final AnnotationTarget target = annotationInstance.target(); + Type type; + switch (target.kind()) { + case FIELD: + type = target.asField().type(); + break; + case METHOD_PARAMETER: + MethodParameterInfo paramInfo = target.asMethodParameter(); + MethodInfo method = paramInfo.method(); + type = method.parameterTypes().get(paramInfo.position()); + break; + default: + type = null; + break; + } + if (type != null) { + type = type.name().equals(CxfDotNames.INJECT_INSTANCE) ? type.asParameterizedType().arguments().get(0) + : type; + final String typeName = type.name().toString(); + final ClientFixedConfig clientConfig = findClientConfig( + config, + Optional.ofNullable(annotationInstance.value()).map(AnnotationValue::asString).orElse(null), + typeName); + seiToClientConfig.put(typeName, clientConfig); + } + }); + return seiToClientConfig; + } + + /** + * Find a {@link ClientFixedConfig} by the given client configuration {@code key} or by the given + * {@code serviceInterfaceName}. + * Note that there is a similar algorithm implemented in + * {@code io.quarkiverse.cxf.CxfClientProducer.selectorCXFClientInfo(CxfConfig, CxfFixedConfig, InjectionPoint, CXFClientInfo)} + * + * @param config the {@link CxfFixedConfig} to search in + * @param key the key to lookup in the {@link CxfFixedConfig#clients} map + * @param serviceInterfaceName {@link ClientFixedConfig#serviceInterface} to look for + * @return a matching {@link ClientFixedConfig}, possibly a default one produced by + * {@link ClientFixedConfig#createDefault()} + * + * @throws IllegalStateException if there are too many {@link ClientFixedConfig}s available for the given + * {@code serviceInterfaceName} + */ + static ClientFixedConfig findClientConfig(CxfFixedConfig config, String key, String serviceInterfaceName) { + if (key != null && !key.isEmpty()) { + ClientFixedConfig result = config.clients.get(key); + if (result == null) { + /* + * We cannot tell at build time, whether this is illegal, because there can be some runtime config + * for the given key that we do not see here. So we just return a default ClientFixedConfig + */ + return ClientFixedConfig.createDefault(); + } + return result; + } + + final List> configsBySei = config.clients.entrySet().stream() + .filter(cl -> serviceInterfaceName.equals(cl.getValue().serviceInterface.orElse(null))) + .filter(cl -> !cl.getValue().alternative) + .collect(Collectors.toList()); + + switch (configsBySei.size()) { + case 0: + /* + * We cannot tell at build time, whether this is illegal, because there can be some runtime config + * for the given key that we do not see here. So we just return a default ClientFixedConfig + */ + return ClientFixedConfig.createDefault(); + case 1: + return configsBySei.get(0).getValue(); + default: + throw new IllegalStateException("quarkus.cxf.*.service-interface = " + serviceInterfaceName + + " with alternative = false expected once, but found " + configsBySei.size() + " times in " + + configsBySei.stream().map(k -> "quarkus.cxf.\"" + k + "\".service-interface") + .collect(Collectors.joining(", "))); + } + } /** @@ -174,13 +279,46 @@ private static Set findClientSEIsInUse(IndexView index) { void generateClientProducers( List clients, BuildProducer generatedBeans, - BuildProducer unremovableBeans) { + BuildProducer unremovableBeans, + BuildProducer reflectiveClasses) { clients .stream() .map(CxfClientBuildItem::getSei) .forEach(sei -> { generateCxfClientProducer(sei, generatedBeans, unremovableBeans); }); + + if (clients.stream().anyMatch(CxfClientBuildItem::isProxyClassRuntimeInitialized)) { + reflectiveClasses + .produce(ReflectiveClassBuildItem.builder(CxfClientProducer.RUNTIME_INITIALIZED_PROXY_MARKER_INTERFACE_NAME) + .build()); + copyMarkerInterfaceToApplication(generatedBeans); + } + } + + /** + * Copies the {@value CxfClientProducer#RUNTIME_INITIALIZED_PROXY_MARKER_INTERFACE_NAME} from the current + * classloader + * to the user application. Why we have do that: First, the interface is package-visible so that adding it to + * the client proxy definition forces GraalVM to generate the proxy class in + * {@value CxfClientProducer#RUNTIME_INITIALIZED_PROXY_MARKER_INTERFACE_PACKAGE} package rather than under a random + * package/class name. Thanks to that we can request the postponed initialization of the generated proxy class by package + * name. + * More details in #580. + * + * @param generatedBeans + */ + private void copyMarkerInterfaceToApplication(BuildProducer generatedBeans) { + byte[] bytes; + try { + bytes = IoUtil.readClassAsBytes(getClass().getClassLoader(), + CxfClientProducer.RUNTIME_INITIALIZED_PROXY_MARKER_INTERFACE_NAME); + } catch (IOException e) { + throw new RuntimeException("Could not read " + CxfClientProducer.RUNTIME_INITIALIZED_PROXY_MARKER_INTERFACE_NAME + + ".class from quarkus-cxf-deployment jar"); + } + String className = CxfClientProducer.RUNTIME_INITIALIZED_PROXY_MARKER_INTERFACE_NAME.replace('.', '/'); + generatedBeans.produce(new GeneratedBeanBuildItem(className, bytes)); } private void generateCxfClientProducer( @@ -328,4 +466,84 @@ private void produceUnremovableBean( .forEach(unremovables::produce); } -} \ No newline at end of file + private static class ProxyInfo { + + public static ProxyInfo of( + boolean refersToRuntimeInitializedClasses, + ClassInfo wsClassInfo, + Set rtInitClasses, + Set rtInitPackages, + IndexView index) { + final List result = new ArrayList<>(); + result.add(wsClassInfo.name().toString()); + result.add(BindingProvider.class.getName()); + result.add("java.io.Closeable"); + result.add(Client.class.getName()); + + if (!refersToRuntimeInitializedClasses) { + /* Try to auto-detect unless the user decided himself */ + Predicate isRuntimeInitializedClass = className -> rtInitClasses.contains(className) + || rtInitPackages.contains(getPackage(className)); + refersToRuntimeInitializedClasses = refersToRuntimeInitializedClasses( + wsClassInfo, + isRuntimeInitializedClass, + index); + } + + if (refersToRuntimeInitializedClasses) { + result.add(io.quarkiverse.cxf.CxfClientProducer.RUNTIME_INITIALIZED_PROXY_MARKER_INTERFACE_NAME); + } + return new ProxyInfo(result, refersToRuntimeInitializedClasses); + } + + static String getPackage(String className) { + int lastDot = className.lastIndexOf('.'); + if (lastDot < 0) { + return ""; + } + return className.substring(0, lastDot); + } + + private static boolean refersToRuntimeInitializedClasses(ClassInfo wsClassInfo, + Predicate isRuntimeInitializedClass, IndexView index) { + if (isRuntimeInitializedClass.test(wsClassInfo.name().toString())) { + return true; + } + boolean ownMethods = wsClassInfo.methods().stream() + .filter(m -> (m.flags() & java.lang.reflect.Modifier.STATIC) == 0) // only non-static methods + .anyMatch(m -> isRuntimeInitializedClass.test(m.returnType().name().toString()) + || m.parameterTypes().stream() + .map(Type::name) + .map(DotName::toString) + .anyMatch(isRuntimeInitializedClass)); + if (ownMethods) { + return true; + } + + /* Do the same recursively for all interfaces */ + return wsClassInfo.interfaceNames().stream() + .map(intf -> { + final ClassInfo cl = index.getClassByName(intf); + if (cl == null) { + LOGGER.warnf( + "Could not check whether %s refers to runtime initialized classes because it was not found in Jandex", + intf); + } + return cl; + }) + .filter(cl -> cl != null) + .anyMatch(cl -> refersToRuntimeInitializedClasses(cl, isRuntimeInitializedClass, index)); + } + + private ProxyInfo(List interfaces, boolean isRuntimeInitialized) { + super(); + this.interfaces = interfaces; + this.isRuntimeInitialized = isRuntimeInitialized; + } + + private final List interfaces; + private final boolean isRuntimeInitialized; + + } + +} diff --git a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CXFClientData.java b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CXFClientData.java index 8a7673937..4e0846f4f 100644 --- a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CXFClientData.java +++ b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CXFClientData.java @@ -18,6 +18,7 @@ public class CXFClientData implements Serializable { private String sei; private String wsName; private String wsNamespace; + private boolean proxyClassRuntimeInitialized; public CXFClientData() { } @@ -27,12 +28,14 @@ public CXFClientData( String sei, String wsName, String wsNamespace, - List classNames) { + List classNames, + boolean proxyClassRuntimeInitialized) { this.soapBinding = soapBinding; this.sei = sei; this.wsName = wsName; this.wsNamespace = wsNamespace; this.classNames = Collections.unmodifiableList(classNames); + this.proxyClassRuntimeInitialized = proxyClassRuntimeInitialized; } public List getClassNames() { @@ -55,6 +58,14 @@ public String getWsNamespace() { return wsNamespace; } + public boolean isProxyClassRuntimeInitialized() { + return proxyClassRuntimeInitialized; + } + + public void setProxyClassRuntimeInitialized(boolean proxyClassRuntimeInitialized) { + this.proxyClassRuntimeInitialized = proxyClassRuntimeInitialized; + } + public void setClassNames(List classNames) { this.classNames = Collections.unmodifiableList(classNames); } diff --git a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CXFClientInfo.java b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CXFClientInfo.java index abd77f054..3c2577d3a 100644 --- a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CXFClientInfo.java +++ b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CXFClientInfo.java @@ -19,6 +19,7 @@ public class CXFClientInfo { private String epName; private String username; private String password; + private boolean proxyClassRuntimeInitialized; private final List inInterceptors = new ArrayList<>(); private final List outInterceptors = new ArrayList<>(); private final List outFaultInterceptors = new ArrayList<>(); @@ -36,7 +37,8 @@ public CXFClientInfo( String soapBinding, String wsNamespace, String wsName, - List classNames) { + List classNames, + boolean proxyClassRuntimeInitialized) { this.classNames.addAll(classNames); this.endpointAddress = endpointAddress; this.epName = null; @@ -48,10 +50,12 @@ public CXFClientInfo( this.wsName = wsName; this.wsNamespace = wsNamespace; this.wsdlUrl = null; + this.proxyClassRuntimeInitialized = proxyClassRuntimeInitialized; } public CXFClientInfo(CXFClientInfo other) { - this(other.sei, other.endpointAddress, other.soapBinding, other.wsNamespace, other.wsName, other.classNames); + this(other.sei, other.endpointAddress, other.soapBinding, other.wsNamespace, other.wsName, other.classNames, + other.proxyClassRuntimeInitialized); this.wsdlUrl = other.wsdlUrl; this.epNamespace = other.epNamespace; this.epName = other.epName; @@ -128,6 +132,10 @@ public List getClassNames() { return classNames; } + public boolean isProxyClassRuntimeInitialized() { + return proxyClassRuntimeInitialized; + } + public List getFeatures() { return features; } diff --git a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CXFRecorder.java b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CXFRecorder.java index 4b45796f1..66cff7293 100644 --- a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CXFRecorder.java +++ b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CXFRecorder.java @@ -38,7 +38,8 @@ public RuntimeValue cxfClientInfoSupplier(CXFClientData cxfClient cxfClientData.getSoapBinding(), cxfClientData.getWsNamespace(), cxfClientData.getWsName(), - cxfClientData.getClassNames())); + cxfClientData.getClassNames(), + cxfClientData.isProxyClassRuntimeInitialized())); } private static class ServletConfig { diff --git a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CxfClientProducer.java b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CxfClientProducer.java index 839563b72..287730b2d 100644 --- a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CxfClientProducer.java +++ b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CxfClientProducer.java @@ -4,6 +4,7 @@ import static java.lang.String.join; import static java.util.stream.Collectors.toList; +import java.io.Closeable; import java.util.List; import java.util.Map; @@ -12,11 +13,13 @@ import javax.enterprise.inject.spi.InjectionPoint; import javax.inject.Inject; import javax.xml.namespace.QName; +import javax.xml.ws.BindingProvider; import javax.xml.ws.handler.Handler; import org.apache.cxf.Bus; import org.apache.cxf.common.spi.GeneratedNamespaceClassLoader; import org.apache.cxf.common.spi.NamespaceClassCreator; +import org.apache.cxf.endpoint.Client; import org.apache.cxf.endpoint.dynamic.ExceptionClassCreator; import org.apache.cxf.endpoint.dynamic.ExceptionClassLoader; import org.apache.cxf.feature.Feature; @@ -26,7 +29,6 @@ import org.apache.cxf.jaxb.FactoryClassLoader; import org.apache.cxf.jaxb.WrapperHelperClassLoader; import org.apache.cxf.jaxb.WrapperHelperCreator; -import org.apache.cxf.jaxws.JaxWsProxyFactoryBean; import org.apache.cxf.jaxws.spi.WrapperClassCreator; import org.apache.cxf.jaxws.spi.WrapperClassLoader; import org.apache.cxf.message.Message; @@ -49,6 +51,9 @@ public abstract class CxfClientProducer { private static final Logger LOGGER = Logger.getLogger(CxfClientProducer.class); + public static final String RUNTIME_INITIALIZED_PROXY_MARKER_INTERFACE_PACKAGE = "io.quarkiverse.cxf.runtime.proxy"; + public static final String RUNTIME_INITIALIZED_PROXY_MARKER_INTERFACE_NAME = "io.quarkiverse.cxf.runtime.proxy.RuntimeInitializedProxyMarker"; + @Inject CxfConfig config; @@ -73,8 +78,26 @@ private Object produceCxfClient(CXFClientInfo cxfClientInfo) { LOGGER.errorf("WebService interface (client) class %s not found", cxfClientInfo.getSei()); return null; } + Class[] interfaces; + try { + interfaces = cxfClientInfo.isProxyClassRuntimeInitialized() + ? new Class[] { + BindingProvider.class, + Closeable.class, + Client.class, + Class.forName(RUNTIME_INITIALIZED_PROXY_MARKER_INTERFACE_NAME, true, + Thread.currentThread().getContextClassLoader()) + } + : new Class[] { + BindingProvider.class, + Closeable.class, + Client.class + }; + } catch (ClassNotFoundException e) { + throw new RuntimeException("Could not load " + RUNTIME_INITIALIZED_PROXY_MARKER_INTERFACE_NAME, e); + } QuarkusClientFactoryBean quarkusClientFactoryBean = new QuarkusClientFactoryBean(cxfClientInfo.getClassNames()); - JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean(quarkusClientFactoryBean); + QuarkusJaxWsProxyFactoryBean factory = new QuarkusJaxWsProxyFactoryBean(quarkusClientFactoryBean, interfaces); Bus bus = quarkusClientFactoryBean.getBus(true); bus.setExtension(new WrapperHelperClassLoader(bus), WrapperHelperCreator.class); bus.setExtension(new ExtensionClassLoader(bus), ExtensionClassCreator.class); diff --git a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CxfFixedConfig.java b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CxfFixedConfig.java new file mode 100644 index 000000000..b68b9dcf9 --- /dev/null +++ b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/CxfFixedConfig.java @@ -0,0 +1,75 @@ +package io.quarkiverse.cxf; + +import java.util.Map; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * Quarkus CXF build time configuration options that are also available at runtime but only in read-only mode. + */ +@ConfigRoot(name = "cxf", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +public class CxfFixedConfig { + + /** + * The build time part of the client configuration. + */ + @ConfigItem(name = "client") + public Map clients; + + @ConfigGroup + public static class ClientFixedConfig { + + /** + * The client service interface class name + */ + @ConfigItem + public Optional serviceInterface; + + /** + * Indicates whether this is an alternative proxy client configuration. If + * true, then this configuration is ignored when configuring a client without + * annotation `@CXFClient`. + */ + @ConfigItem(defaultValue = "false") + public boolean alternative; + + /** Configuration options related to native mode */ + @ConfigItem(name = "native") + public NativeClientFixedConfig native_; + + public static ClientFixedConfig createDefault() { + ClientFixedConfig result = new ClientFixedConfig(); + result.serviceInterface = Optional.empty(); + return result; + } + } + + @ConfigGroup + public static class NativeClientFixedConfig { + + /** + * If {@code true}, the client dynamic proxy class generated by native compiler will be initialized at runtime; + * otherwise the proxy class will be initialized at build time. + *

+ * Setting this to {@code true} makes sense if your service endpoint interface references some class initialized + * at runtime in its method signatures. E.g. Say, your service interface has method @code{int add(Operands o)} + * and the {@code Operands} class was requested to be initialized at runtime. Then, without setting this + * configuration parameter to {@code true}, the native compiler will throw an exception saying something like + * {@code Classes that should be initialized at run time got initialized during image building: org.acme.Operands ... jdk.proxy.$Proxy caused initialization of this class}. + * {@code jdk.proxy.$Proxy} is the proxy class generated by the native compiler. + *

+ * While {@code quarkus-cxf} can auto-detect the proper setting in some cases, the auto-detection is not perfect. + * This is because runtime initialization of classes can be requested in many ways out of which only the ones + * done via Quarkus {@code RuntimeInitializedClassBuildItem} and {@code RuntimeInitializedPackageBuildItem} + * can safely be observed by {@code quarkus-cxf}. In other cases, you'll have to set this manually. + */ + @ConfigItem(defaultValue = "false") + public boolean runtimeInitialized; + + } + +} diff --git a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/QuarkusJaxWsProxyFactoryBean.java b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/QuarkusJaxWsProxyFactoryBean.java new file mode 100644 index 000000000..cb61c9cb4 --- /dev/null +++ b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/QuarkusJaxWsProxyFactoryBean.java @@ -0,0 +1,24 @@ +package io.quarkiverse.cxf; + +import org.apache.cxf.frontend.ClientFactoryBean; +import org.apache.cxf.jaxws.JaxWsProxyFactoryBean; + +public class QuarkusJaxWsProxyFactoryBean extends JaxWsProxyFactoryBean { + + private final Class[] additionalImplementingClasses; + + public QuarkusJaxWsProxyFactoryBean(ClientFactoryBean fact, Class... additionalImplementingClasses) { + super(fact); + this.additionalImplementingClasses = additionalImplementingClasses; + } + + @Override + protected Class[] getImplementingClasses() { + Class cls = getClientFactoryBean().getServiceClass(); + Class[] result = new Class[additionalImplementingClasses.length + 1]; + result[0] = cls; + System.arraycopy(additionalImplementingClasses, 0, result, 1, additionalImplementingClasses.length); + return result; + } + +} diff --git a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/graal/QuarkusCxfFeature.java b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/graal/QuarkusCxfFeature.java new file mode 100644 index 000000000..b589774db --- /dev/null +++ b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/graal/QuarkusCxfFeature.java @@ -0,0 +1,20 @@ +package io.quarkiverse.cxf.graal; + +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; + +import io.quarkiverse.cxf.CxfClientProducer; + +/** + * + */ +public class QuarkusCxfFeature implements Feature { + @Override + public void afterRegistration(AfterRegistrationAccess access) { + /* + * We cannot do this using a RuntimeInitializedPackageBuildItem because it would cause a dependency cycle in + * io.quarkiverse.cxf.deployment.CxfClientProcessor.collectClients() + */ + RuntimeClassInitialization.initializeAtRunTime(CxfClientProducer.RUNTIME_INITIALIZED_PROXY_MARKER_INTERFACE_PACKAGE); + } +} diff --git a/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/runtime/proxy/RuntimeInitializedProxyMarker.java b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/runtime/proxy/RuntimeInitializedProxyMarker.java new file mode 100644 index 000000000..6f1addd8f --- /dev/null +++ b/extensions/core/runtime/src/main/java/io/quarkiverse/cxf/runtime/proxy/RuntimeInitializedProxyMarker.java @@ -0,0 +1,4 @@ +package io.quarkiverse.cxf.runtime.proxy; + +interface RuntimeInitializedProxyMarker { +} \ No newline at end of file diff --git a/integration-tests/client/src/main/java/io/quarkiverse/cxf/client/it/rtinit/Operands.java b/integration-tests/client/src/main/java/io/quarkiverse/cxf/client/it/rtinit/Operands.java new file mode 100644 index 000000000..668ea4923 --- /dev/null +++ b/integration-tests/client/src/main/java/io/quarkiverse/cxf/client/it/rtinit/Operands.java @@ -0,0 +1,60 @@ + +package io.quarkiverse.cxf.client.it.rtinit; + +import java.util.Objects; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlType; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "operands", propOrder = { + "a", + "b" +}) +public class Operands { + private int a; + private int b; + + public Operands() { + } + + public Operands(int a, int b) { + super(); + this.a = a; + this.b = b; + } + + public int getA() { + return a; + } + + @Override + public int hashCode() { + return Objects.hash(a, b); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Operands other = (Operands) obj; + return a == other.a && b == other.b; + } + + public void setA(int a) { + this.a = a; + } + + public int getB() { + return b; + } + + public void setB(int b) { + this.b = b; + } +} \ No newline at end of file diff --git a/integration-tests/client/src/main/java/io/quarkiverse/cxf/client/it/rtinit/Result.java b/integration-tests/client/src/main/java/io/quarkiverse/cxf/client/it/rtinit/Result.java new file mode 100644 index 000000000..bfc18bdc6 --- /dev/null +++ b/integration-tests/client/src/main/java/io/quarkiverse/cxf/client/it/rtinit/Result.java @@ -0,0 +1,62 @@ + +package io.quarkiverse.cxf.client.it.rtinit; + +import java.util.Objects; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlType; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "result", propOrder = { + "operands", + "result" +}) +public class Result { + private int result; + + private Operands operands; + + public Result() { + } + + public Result(int result, Operands operands) { + super(); + this.result = result; + this.operands = operands; + } + + public int getResult() { + return result; + } + + public void setResult(int result) { + this.result = result; + } + + public Operands getOperands() { + return operands; + } + + public void setOperands(Operands operands) { + this.operands = operands; + } + + @Override + public int hashCode() { + return Objects.hash(operands, result); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Result other = (Result) obj; + return Objects.equals(operands, other.operands) && result == other.result; + } + +} \ No newline at end of file diff --git a/integration-tests/client/src/main/resources/application.properties b/integration-tests/client/src/main/resources/application.properties index 7ea2b1aff..b7d52e1a3 100644 --- a/integration-tests/client/src/main/resources/application.properties +++ b/integration-tests/client/src/main/resources/application.properties @@ -20,3 +20,10 @@ quarkus.cxf.client.codeFirstClient.client-endpoint-url=${cxf.it.calculator.baseU quarkus.cxf.client.codeFirstClient.service-interface=io.quarkiverse.cxf.client.it.CodeFirstClient quarkus.cxf.client.codeFirstClient.endpoint-namespace=http://www.jboss.org/eap/quickstarts/wscalculator/Calculatorrr quarkus.cxf.client.codeFirstClient.endpoint-name=CalculatorService + +quarkus.cxf.client.clientWithRuntimeInitializedPayload.client-endpoint-url=${cxf.it.calculator.baseUri}/calculator-ws/CalculatorService +quarkus.cxf.client.clientWithRuntimeInitializedPayload.service-interface=io.quarkiverse.cxf.client.it.rtinit.ClientWithRuntimeInitializedPayload +quarkus.cxf.client.clientWithRuntimeInitializedPayload.endpoint-namespace=http://www.jboss.org/eap/quickstarts/wscalculator/Calculator +quarkus.cxf.client.clientWithRuntimeInitializedPayload.endpoint-name=CalculatorService +quarkus.cxf.client.clientWithRuntimeInitializedPayload.native.runtime-initialized = true +quarkus.native.additional-build-args=--initialize-at-run-time=io.quarkiverse.cxf.client.it.rtinit.Operands\\,io.quarkiverse.cxf.client.it.rtinit.Result diff --git a/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageData.java b/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageData.java index af24328ad..09062084d 100644 --- a/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageData.java +++ b/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageData.java @@ -2,6 +2,8 @@ import java.awt.Image; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlType; @XmlAccessorType(XmlAccessType.FIELD) diff --git a/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageResponse.java b/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageResponse.java index e101ebc21..e81fac77c 100644 --- a/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageResponse.java +++ b/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageResponse.java @@ -2,11 +2,11 @@ import java.awt.Image; -import jakarta.xml.bind.annotation.XmlAccessType; -import jakarta.xml.bind.annotation.XmlAccessorType; -import jakarta.xml.bind.annotation.XmlElement; -import jakarta.xml.bind.annotation.XmlMimeType; -import jakarta.xml.bind.annotation.XmlType; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlMimeType; +import javax.xml.bind.annotation.XmlType; @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "ImageResponse", propOrder = { diff --git a/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageService.java b/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageService.java index fa1aef4c1..f8a918d5f 100644 --- a/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageService.java +++ b/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageService.java @@ -1,14 +1,18 @@ package io.quarkiverse.cxf.it.ws.mtom.awt.server; +import java.awt.Image; + import javax.jws.WebMethod; +import javax.jws.WebParam; import javax.jws.WebService; -import javax.jws.soap.SOAPBinding; import javax.xml.ws.soap.MTOM; @WebService(name = "ImageService", targetNamespace = ImageService.NS) @MTOM public interface ImageService { + public static final String NS = "https://quarkiverse.github.io/quarkiverse-docs/quarkus-cxf/test/mtom-awt"; + @WebMethod Image downloadImage( @WebParam(name = "name", targetNamespace = NS) String name); diff --git a/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageServiceImpl.java b/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageServiceImpl.java index 1e5166a6f..50c6f236b 100644 --- a/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageServiceImpl.java +++ b/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageServiceImpl.java @@ -1,12 +1,10 @@ package io.quarkiverse.cxf.it.ws.mtom.awt.server; +import java.awt.Image; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.jws.WebService; -import javax.jws.soap.SOAPBinding; - -import org.jboss.logging.Logger; @WebService(name = "ImageService", serviceName = "ImageService", endpointInterface = "io.quarkiverse.cxf.it.ws.mtom.awt.server.ImageService", targetNamespace = ImageService.NS) public class ImageServiceImpl implements ImageService { diff --git a/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageServiceWithWrappers.java b/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageServiceWithWrappers.java index f9ce5b89e..0e19c42dc 100644 --- a/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageServiceWithWrappers.java +++ b/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageServiceWithWrappers.java @@ -2,12 +2,12 @@ import java.awt.Image; -import jakarta.jws.WebMethod; -import jakarta.jws.WebParam; -import jakarta.jws.WebService; -import jakarta.xml.ws.RequestWrapper; -import jakarta.xml.ws.ResponseWrapper; -import jakarta.xml.ws.soap.MTOM; +import javax.jws.WebMethod; +import javax.jws.WebParam; +import javax.jws.WebService; +import javax.xml.ws.RequestWrapper; +import javax.xml.ws.ResponseWrapper; +import javax.xml.ws.soap.MTOM; @WebService(name = "ImageServiceWithWrappers", targetNamespace = ImageServiceWithWrappers.NS) @MTOM diff --git a/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageServiceWithWrappersImpl.java b/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageServiceWithWrappersImpl.java index c517150be..ef123ca4a 100644 --- a/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageServiceWithWrappersImpl.java +++ b/integration-tests/mtom-awt/src/main/java/io/quarkiverse/cxf/it/ws/mtom/awt/server/ImageServiceWithWrappersImpl.java @@ -4,7 +4,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import jakarta.jws.WebService; +import javax.jws.WebService; @WebService(name = "ImageServiceWithWrappers", serviceName = "ImageServiceWithWrappers", endpointInterface = "io.quarkiverse.cxf.it.ws.mtom.awt.server.ImageServiceWithWrappers", targetNamespace = ImageServiceWithWrappers.NS) public class ImageServiceWithWrappersImpl implements ImageServiceWithWrappers {