From f4b9d28c7602dbb52a0023922b894127a7ea8549 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 27 Jan 2023 10:50:51 +0100 Subject: [PATCH] Add native image hints and AOT pre-processor. Closes gh-747 --- spring-vault-core/pom.xml | 5 + .../PropertySourceAotProcessor.java | 128 ++++++++++++++++++ .../VaultPropertySourceRegistrar.java | 1 + .../vault/aot/VaultRuntimeHints.java | 99 ++++++++++++++ .../vault/aot/package-info.java | 6 + .../client/ClientHttpConnectorFactory.java | 26 +--- .../ClientHttpRequestFactoryFactory.java | 25 +--- .../vault/core/util/PropertyTransformers.java | 8 +- .../resources/META-INF/spring/aot.factories | 3 + 9 files changed, 259 insertions(+), 42 deletions(-) create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/annotation/PropertySourceAotProcessor.java create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/aot/VaultRuntimeHints.java create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/aot/package-info.java create mode 100644 spring-vault-core/src/main/resources/META-INF/spring/aot.factories diff --git a/spring-vault-core/pom.xml b/spring-vault-core/pom.xml index 2d859efb..f9e8d403 100644 --- a/spring-vault-core/pom.xml +++ b/spring-vault-core/pom.xml @@ -23,6 +23,11 @@ ../src/main/resources META-INF + + + src/main/resources/META-INF + META-INF + diff --git a/spring-vault-core/src/main/java/org/springframework/vault/annotation/PropertySourceAotProcessor.java b/spring-vault-core/src/main/java/org/springframework/vault/annotation/PropertySourceAotProcessor.java new file mode 100644 index 00000000..deca2412 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/annotation/PropertySourceAotProcessor.java @@ -0,0 +1,128 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.annotation; + +import java.util.Map.Entry; +import java.util.function.Predicate; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments; +import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator; +import org.springframework.beans.factory.config.BeanReference; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.javapoet.CodeBlock; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.vault.core.lease.domain.RequestedSecret; +import org.springframework.vault.core.lease.domain.RequestedSecret.Mode; +import org.springframework.vault.core.util.PropertyTransformers; +import org.springframework.vault.core.util.PropertyTransformers.KeyPrefixPropertyTransformer; +import org.springframework.vault.core.util.PropertyTransformers.NoOpPropertyTransformer; + +/** + * AOT processor to serialize + * + * @author Mark Paluch + * @since 3.0.1 + */ +class PropertySourceAotProcessor implements BeanRegistrationAotProcessor { + + @Override + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + + if (registeredBean.getBeanClass() == org.springframework.vault.core.env.LeaseAwareVaultPropertySource.class) { + return BeanRegistrationAotContribution.withCustomCodeFragments(AotContribution::new); + } + + if (registeredBean.getBeanClass() == org.springframework.vault.core.env.VaultPropertySource.class) { + return BeanRegistrationAotContribution.withCustomCodeFragments(AotContribution::new); + } + + return null; + } + + static class AotContribution extends BeanRegistrationCodeFragmentsDecorator { + + protected AotContribution(BeanRegistrationCodeFragments delegate) { + super(delegate); + } + + @Override + public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition, + Predicate attributeFilter) { + + CodeBlock.Builder code = CodeBlock.builder(); + + ConstructorArgumentValues values = beanDefinition.getConstructorArgumentValues(); + + for (Entry entry : values.getIndexedArgumentValues().entrySet()) { + + CodeBlock renderedValue = render(entry.getValue().getValue()); + code.addStatement("$N.getConstructorArgumentValues().addIndexedArgumentValue($L, $L)", + BeanRegistrationCodeFragments.BEAN_DEFINITION_VARIABLE, entry.getKey(), renderedValue); + } + + return code.build(); + } + + private static CodeBlock render(@Nullable Object value) { + + if (value instanceof RuntimeBeanReference runtimeBeanReference + && runtimeBeanReference.getBeanType() != null) { + return CodeBlock.of("new $T($T.class)", RuntimeBeanReference.class, runtimeBeanReference.getBeanType()); + } + + if (value instanceof BeanReference beanReference) { + return CodeBlock.of("new $T($S)", RuntimeBeanReference.class, beanReference.getBeanName()); + } + + if (value instanceof String) { + return CodeBlock.of("$S", value.toString()); + } + + if (value == null || ClassUtils.isPrimitiveOrWrapper(value.getClass())) { + return CodeBlock.of("$L", value != null ? value.toString() : "null"); + } + + if (value instanceof NoOpPropertyTransformer) { + return CodeBlock.of("$T.$N()", PropertyTransformers.class, "noop"); + } + + if (value instanceof KeyPrefixPropertyTransformer kpt) { + return CodeBlock.of("$T.$N($S)", PropertyTransformers.class, "propertyNamePrefix", + kpt.getPropertyNamePrefix()); + } + + if (value instanceof RequestedSecret rs) { + return CodeBlock.of("$T.$N($S)", RequestedSecret.class, + rs.getMode() == Mode.ROTATE ? "rotating" : "renewable", rs.getPath()); + + } + + throw new IllegalArgumentException("Unsupported value type: " + value.getClass()); + } + + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySourceRegistrar.java b/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySourceRegistrar.java index 824ef393..cf5cbf68 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySourceRegistrar.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/annotation/VaultPropertySourceRegistrar.java @@ -135,6 +135,7 @@ class VaultPropertySourceRegistrar AbstractBeanDefinition beanDefinition = createBeanDefinition(ref, renewal, propertyTransformer, ignoreSecretNotFound, potentiallyResolveRequiredPlaceholders(propertyPath)); + beanDefinition.setSource(annotationMetadata.getClassName()); do { String beanName = "vaultPropertySource#" + counter; diff --git a/spring-vault-core/src/main/java/org/springframework/vault/aot/VaultRuntimeHints.java b/spring-vault-core/src/main/java/org/springframework/vault/aot/VaultRuntimeHints.java new file mode 100644 index 00000000..082a6a5f --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/aot/VaultRuntimeHints.java @@ -0,0 +1,99 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.aot; + +import java.io.IOException; +import java.util.stream.Stream; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.util.ClassUtils; + +/** + * Runtime hints for Spring Vault. + * + * @author Mark Paluch + * @since 3.0.1 + */ +class VaultRuntimeHints implements RuntimeHintsRegistrar { + + private final CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory(); + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(classLoader); + + ReflectionHints reflection = hints.reflection(); + MemberCategory[] dataObjectCategories = new MemberCategory[] { MemberCategory.DECLARED_FIELDS, + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, + MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS, MemberCategory.INTROSPECT_DECLARED_METHODS }; + try { + Resource[] resources = resolver.getResources(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath("org.springframework.vault.support") + "/*.class"); + + for (Resource resource : resources) { + + MetadataReader metadataReader = factory.getMetadataReader(resource); + String className = metadataReader.getClassMetadata().getClassName(); + + if (className.contains("-")) { + continue; + } + + reflection.registerType(TypeReference.of(className), dataObjectCategories); + } + } + catch (IOException e) { + throw new RuntimeException(e); + } + + Stream.of("org.springframework.vault.core.VaultSysTemplate$GetMounts$VaultMountsResponse", + "org.springframework.vault.core.VaultVersionedKeyValueTemplate$VersionedResponse", + "org.springframework.vault.core.ReactiveVaultTemplate$VaultListResponse", + "org.springframework.vault.core.VaultListResponse", + + "org.springframework.vault.core.VaultTransitTemplate$RawTransitKeyImpl", + "org.springframework.vault.core.VaultTransitTemplate$VaultTransitKeyImpl", + + "org.springframework.vault.core.VaultSysTemplate$GetMounts", + "org.springframework.vault.core.VaultSysTemplate$GetUnsealStatus", + "org.springframework.vault.core.VaultSysTemplate$Health", + "org.springframework.vault.core.VaultSysTemplate$Seal", + "org.springframework.vault.core.VaultSysTemplate$VaultHealthImpl", + "org.springframework.vault.core.VaultSysTemplate$VaultInitializationResponseImpl", + "org.springframework.vault.core.VaultSysTemplate$VaultUnsealStatusImpl", + + "org.springframework.vault.core.VaultVersionedKeyValueTemplate$VersionedResponse") + .forEach(cls -> reflection.registerType(TypeReference.of(cls), dataObjectCategories)); + + reflection.registerTypeIfPresent(classLoader, "com.google.api.client.json.jackson2.JacksonFactory", + MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); + + reflection.registerTypeIfPresent(classLoader, "com.google.api.client.json.gson.GsonFactory", + MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); + + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/aot/package-info.java b/spring-vault-core/src/main/java/org/springframework/vault/aot/package-info.java new file mode 100644 index 00000000..319ab0f4 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/aot/package-info.java @@ -0,0 +1,6 @@ +/** + * Ahead-of-Time support. + */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields +package org.springframework.vault.aot; diff --git a/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpConnectorFactory.java b/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpConnectorFactory.java index a8eb7cb9..5dadad4f 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpConnectorFactory.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpConnectorFactory.java @@ -65,28 +65,14 @@ import static org.springframework.vault.client.ClientHttpRequestFactoryFactory.* */ public class ClientHttpConnectorFactory { - private static final boolean REACTOR_NETTY_PRESENT = isPresent("reactor.netty.http.client.HttpClient"); + private static final boolean REACTOR_NETTY_PRESENT = ClassUtils.isPresent("reactor.netty.http.client.HttpClient", + ClientHttpConnectorFactory.class.getClassLoader()); - private static final boolean HTTP_COMPONENTS_PRESENT = isPresent("org.apache.hc.client5.http.impl.async"); + private static final boolean HTTP_COMPONENTS_PRESENT = ClassUtils.isPresent("org.apache.hc.client5.http.impl.async", + ClientHttpConnectorFactory.class.getClassLoader()); - private static final boolean JETTY_PRESENT = isPresent("org.eclipse.jetty.client.HttpClient"); - - /** - * Checks for presence of all {@code classNames} using this class' classloader. - * @param classNames - * @return {@literal true} if all classes are present; {@literal false} if at least - * one class cannot be found. - */ - private static boolean isPresent(String... classNames) { - - for (String className : classNames) { - if (!ClassUtils.isPresent(className, ClientHttpConnectorFactory.class.getClassLoader())) { - return false; - } - } - - return true; - } + private static final boolean JETTY_PRESENT = ClassUtils.isPresent("org.eclipse.jetty.client.HttpClient", + ClientHttpConnectorFactory.class.getClassLoader()); /** * Create a {@link ClientHttpConnector} for the given {@link ClientOptions} and diff --git a/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpRequestFactoryFactory.java b/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpRequestFactoryFactory.java index 86c7e026..2e48e1cb 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpRequestFactoryFactory.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpRequestFactoryFactory.java @@ -89,27 +89,12 @@ public class ClientHttpRequestFactoryFactory { @SuppressWarnings("FieldMayBeFinal") // allow setting via reflection. private static Log logger = LogFactory.getLog(ClientHttpRequestFactoryFactory.class); - private static final boolean HTTP_COMPONENTS_PRESENT = isPresent( - "org.apache.hc.client5.http.impl.classic.HttpClientBuilder"); + private static final boolean HTTP_COMPONENTS_PRESENT = ClassUtils.isPresent( + "org.apache.hc.client5.http.impl.classic.HttpClientBuilder", + ClientHttpRequestFactoryFactory.class.getClassLoader()); - private static final boolean OKHTTP3_PRESENT = isPresent("okhttp3.OkHttpClient"); - - /** - * Checks for presence of all {@code classNames} using this class' classloader. - * @param classNames - * @return {@literal true} if all classes are present; {@literal false} if at least - * one class cannot be found. - */ - private static boolean isPresent(String... classNames) { - - for (String className : classNames) { - if (!ClassUtils.isPresent(className, ClientHttpRequestFactoryFactory.class.getClassLoader())) { - return false; - } - } - - return true; - } + private static final boolean OKHTTP3_PRESENT = ClassUtils.isPresent("okhttp3.OkHttpClient", + ClientHttpRequestFactoryFactory.class.getClassLoader()); /** * Create a {@link ClientHttpRequestFactory} for the given {@link ClientOptions} and diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/util/PropertyTransformers.java b/spring-vault-core/src/main/java/org/springframework/vault/core/util/PropertyTransformers.java index 2724e89a..5316aef7 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/util/PropertyTransformers.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/util/PropertyTransformers.java @@ -57,7 +57,7 @@ public abstract class PropertyTransformers { * {@link PropertyTransformer} that passes the given properties through without * returning changed properties. */ - static class NoOpPropertyTransformer implements PropertyTransformer { + public static class NoOpPropertyTransformer implements PropertyTransformer { static NoOpPropertyTransformer INSTANCE = new NoOpPropertyTransformer(); @@ -118,7 +118,7 @@ public abstract class PropertyTransformers { /** * {@link PropertyTransformer} that adds a prefix to each key name. */ - static class KeyPrefixPropertyTransformer implements PropertyTransformer { + public static class KeyPrefixPropertyTransformer implements PropertyTransformer { private final String propertyNamePrefix; @@ -129,6 +129,10 @@ public abstract class PropertyTransformers { this.propertyNamePrefix = propertyNamePrefix; } + public String getPropertyNamePrefix() { + return propertyNamePrefix; + } + /** * Create a new {@link KeyPrefixPropertyTransformer} that adds a prefix to each * key name. diff --git a/spring-vault-core/src/main/resources/META-INF/spring/aot.factories b/spring-vault-core/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 00000000..a7c09072 --- /dev/null +++ b/spring-vault-core/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1,3 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar=org.springframework.vault.aot.VaultRuntimeHints + +org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=org.springframework.vault.annotation.PropertySourceAotProcessor