Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Register implicit converters for reflection #32162

Merged
merged 1 commit into from
Apr 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
import java.util.Optional;
import java.util.Set;

import org.eclipse.microprofile.config.spi.Converter;
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.Type;

import io.quarkus.deployment.annotations.BuildProducer;
Expand All @@ -26,19 +26,21 @@
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.util.ReflectUtil;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.ConfigMappingInterface;
import io.smallrye.config.ConfigMappingInterface.LeafProperty;
import io.smallrye.config.ConfigMappingInterface.MapProperty;
import io.smallrye.config.ConfigMappingInterface.Property;
import io.smallrye.config.ConfigMappingLoader;
import io.smallrye.config.ConfigMappingMetadata;

public class ConfigMappingUtils {

public static final DotName CONFIG_MAPPING_NAME = DotName.createSimple(ConfigMapping.class.getName());

private static final DotName OPTIONAL = DotName.createSimple(Optional.class.getName());

private ConfigMappingUtils() {
}

public static void generateConfigClasses(
public static void processConfigClasses(
CombinedIndexBuildItem combinedIndex,
BuildProducer<GeneratedClassBuildItem> generatedClasses,
BuildProducer<ReflectiveClassBuildItem> reflectiveClasses,
Expand Down Expand Up @@ -73,7 +75,7 @@ public static void processExtensionConfigMapping(
configClasses);
}

static void processConfigClass(
private static void processConfigClass(
Class<?> configClass,
Kind configClassKind,
String prefix,
Expand All @@ -85,45 +87,76 @@ static void processConfigClass(

List<ConfigMappingMetadata> configMappingsMetadata = ConfigMappingLoader.getConfigMappingsMetadata(configClass);
Set<String> generatedClassesNames = new HashSet<>();
Set<ClassInfo> mappingsInfo = new HashSet<>();
configMappingsMetadata.forEach(mappingMetadata -> {
generatedClasses.produce(
new GeneratedClassBuildItem(isApplicationClass, mappingMetadata.getClassName(),
mappingMetadata.getClassBytes()));
reflectiveClasses
.produce(ReflectiveClassBuildItem.builder(mappingMetadata.getInterfaceType()).methods().build());
reflectiveClasses
.produce(ReflectiveClassBuildItem.builder(mappingMetadata.getClassName())
.methods().build());

generatedClassesNames.add(mappingMetadata.getClassName());
// This is the generated implementation of the mapping by SmallRye Config.
generatedClasses.produce(new GeneratedClassBuildItem(isApplicationClass, mappingMetadata.getClassName(),
mappingMetadata.getClassBytes()));
// Register the interface and implementation methods for reflection. This is required for Bean Validation.
reflectiveClasses.produce(ReflectiveClassBuildItem.builder(mappingMetadata.getInterfaceType()).methods().build());
reflectiveClasses.produce(ReflectiveClassBuildItem.builder(mappingMetadata.getClassName()).methods().build());
// Register also the interface hierarchy
for (Class<?> parent : getHierarchy(mappingMetadata.getInterfaceType())) {
reflectiveClasses.produce(ReflectiveClassBuildItem.builder(parent).methods().build());
}

generatedClassesNames.add(mappingMetadata.getClassName());
processProperties(mappingMetadata.getInterfaceType(), reflectiveClasses);
});

ClassInfo mappingInfo = combinedIndex.getIndex()
.getClassByName(DotName.createSimple(mappingMetadata.getInterfaceType().getName()));
if (mappingInfo != null) {
mappingsInfo.add(mappingInfo);
configClasses.produce(new ConfigClassBuildItem(configClass, collectTypes(combinedIndex, configClass),
generatedClassesNames, prefix, configClassKind));
}

private static void processProperties(
Class<?> configClass,
BuildProducer<ReflectiveClassBuildItem> reflectiveClasses) {

ConfigMappingInterface mapping = ConfigMappingLoader.getConfigMapping(configClass);
for (Property property : mapping.getProperties()) {
Class<?> returnType = property.getMethod().getReturnType();
reflectiveClasses.produce(ReflectiveClassBuildItem.builder(returnType).methods().build());

if (property.hasConvertWith()) {
Class<? extends Converter<?>> convertWith;
if (property.isLeaf()) {
convertWith = property.asLeaf().getConvertWith();
} else {
convertWith = property.asPrimitive().getConvertWith();
}
reflectiveClasses.produce(ReflectiveClassBuildItem.builder(convertWith).build());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@radcortez how come you don't need methods here? What's the purpose of registering the converter if we can't call the convert method? What am I missing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Config system creates by reflection single instances of each Converter and calls convert as required directly into the instance.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Config system creates by reflection single instances of each Converter

OK, so that's why we need to register the classes, in order to get them to work with reflection and be able to instantiate them (through the default constructors that are registered by default when creating a ReflectiveClassBuildItem).

and calls convert as required directly into the instance.

Now that's the part I don't get how it works. Since we are registering the class without invoking methods() we are not asking GraalVM to register convert for reflection as well. So I would expect the call to fail :/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting.

Are you having an issue with this?

No, I am just trying to understand what's going on.

My curiosity comes from the fact that GraalVM doesn't know if a converter let's say MyConverter is reachable, thus treating it as unreachable (otherwise we would have an open world). Now when we register MyConverter for reflection we make it reachable, but not its methods (since we don't register the methods as well) which, as a result, I would expect to be left out of the native executable, but apparently they don't.

Thanks for the clarification @radcortez

}
});

// For implicit converters
for (ClassInfo classInfo : mappingsInfo) {
for (MethodInfo method : classInfo.methods()) {
Type type = method.returnType();
if (type.name().equals(OPTIONAL)) {
// E.g. Optional<Foo>
type = type.asParameterizedType().arguments().get(0);
registerImplicitConverter(property, reflectiveClasses);

if (property.isMap()) {
MapProperty mapProperty = property.asMap();
if (mapProperty.hasKeyConvertWith()) {
reflectiveClasses.produce(ReflectiveClassBuildItem.builder(mapProperty.getKeyConvertWith()).build());
} else {
reflectiveClasses.produce(ReflectiveClassBuildItem.builder(mapProperty.getKeyRawType()).build());
}
reflectiveClasses
.produce(ReflectiveClassBuildItem.builder(type.name().toString()).methods().build());

registerImplicitConverter(mapProperty.getValueProperty(), reflectiveClasses);
}
}
}

configClasses.produce(new ConfigClassBuildItem(configClass, collectTypes(combinedIndex, configClass),
generatedClassesNames, prefix, configClassKind));
private static void registerImplicitConverter(
Property property,
BuildProducer<ReflectiveClassBuildItem> reflectiveClasses) {

if (property.isLeaf() && !property.isOptional()) {
LeafProperty leafProperty = property.asLeaf();
if (leafProperty.hasConvertWith()) {
reflectiveClasses.produce(ReflectiveClassBuildItem.builder(leafProperty.getConvertWith()).build());
} else {
reflectiveClasses.produce(ReflectiveClassBuildItem.builder(leafProperty.getValueRawType()).methods().build());
}
} else if (property.isOptional()) {
registerImplicitConverter(property.asOptional().getNestedProperty(), reflectiveClasses);
} else if (property.isCollection()) {
registerImplicitConverter(property.asCollection().getElement(), reflectiveClasses);
}
}

public static Object newInstance(Class<?> configClass) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.deployment.steps;

import static io.quarkus.deployment.configuration.ConfigMappingUtils.CONFIG_MAPPING_NAME;
import static io.quarkus.deployment.configuration.ConfigMappingUtils.processConfigClasses;
import static io.quarkus.deployment.configuration.ConfigMappingUtils.processExtensionConfigMapping;
import static io.quarkus.deployment.steps.ConfigBuildSteps.SERVICES_PREFIX;
import static io.quarkus.deployment.util.ServiceUtil.classNamesNamedIn;
Expand Down Expand Up @@ -204,6 +206,16 @@ void runtimeDefaultsConfig(
runTimeConfigBuilder.produce(new RunTimeConfigBuilderBuildItem(builderClassName));
}

@BuildStep
void mappings(
CombinedIndexBuildItem combinedIndex,
BuildProducer<GeneratedClassBuildItem> generatedClasses,
BuildProducer<ReflectiveClassBuildItem> reflectiveClasses,
BuildProducer<ConfigClassBuildItem> configClasses) {

processConfigClasses(combinedIndex, generatedClasses, reflectiveClasses, configClasses, CONFIG_MAPPING_NAME);
}

@BuildStep
void extensionMappings(ConfigurationBuildItem configItem,
CombinedIndexBuildItem combinedIndex,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static io.quarkus.deployment.builditem.ConfigClassBuildItem.Kind.MAPPING;
import static io.quarkus.deployment.builditem.ConfigClassBuildItem.Kind.PROPERTIES;
import static io.quarkus.deployment.configuration.ConfigMappingUtils.CONFIG_MAPPING_NAME;
import static io.quarkus.deployment.configuration.ConfigMappingUtils.processConfigClasses;
import static io.smallrye.config.ConfigMappings.ConfigClassWithPrefix.configClassWithPrefix;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
Expand Down Expand Up @@ -67,13 +68,11 @@
import io.quarkus.deployment.builditem.GeneratedClassBuildItem;
import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.configuration.ConfigMappingUtils;
import io.quarkus.deployment.configuration.definition.RootDefinition;
import io.quarkus.deployment.recording.RecorderContext;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.smallrye.config.ConfigMappings.ConfigClassWithPrefix;
import io.smallrye.config.WithConverter;
import io.smallrye.config.inject.ConfigProducer;

/**
Expand All @@ -89,7 +88,6 @@ public class ConfigBuildStep {

private static final DotName SR_CONFIG = DotName.createSimple(io.smallrye.config.SmallRyeConfig.class.getName());
private static final DotName SR_CONFIG_VALUE_NAME = DotName.createSimple(io.smallrye.config.ConfigValue.class.getName());
private static final DotName SR_WITH_CONVERTER = DotName.createSimple(WithConverter.class.getName());

private static final DotName MAP_NAME = DotName.createSimple(Map.class.getName());
private static final DotName SET_NAME = DotName.createSimple(Set.class.getName());
Expand Down Expand Up @@ -277,17 +275,13 @@ public void transform(TransformationContext context) {
}

@BuildStep
void generateConfigClasses(
void generateConfigProperties(
CombinedIndexBuildItem combinedIndex,
BuildProducer<GeneratedClassBuildItem> generatedClasses,
BuildProducer<ReflectiveClassBuildItem> reflectiveClasses,
BuildProducer<ConfigClassBuildItem> configClasses) {

// TODO - Generation of Mapping interface classes can be done in core because they don't require CDI
ConfigMappingUtils.generateConfigClasses(combinedIndex, generatedClasses, reflectiveClasses, configClasses,
CONFIG_MAPPING_NAME);
ConfigMappingUtils.generateConfigClasses(combinedIndex, generatedClasses, reflectiveClasses, configClasses,
MP_CONFIG_PROPERTIES_NAME);
processConfigClasses(combinedIndex, generatedClasses, reflectiveClasses, configClasses, MP_CONFIG_PROPERTIES_NAME);
}

@BuildStep
Expand Down Expand Up @@ -378,19 +372,6 @@ void registerConfigPropertiesBean(
}
}

@BuildStep
void registerConfigMappingConverters(CombinedIndexBuildItem indexBuildItem,
BuildProducer<ReflectiveClassBuildItem> producer) {

String[] valueTypes = indexBuildItem.getIndex().getAnnotations(SR_WITH_CONVERTER).stream()
.map(i -> i.value().asClass().name().toString())
.toArray(String[]::new);
if (valueTypes.length > 0) {
producer.produce(
ReflectiveClassBuildItem.builder(valueTypes).build());
}
}

@BuildStep
void validateConfigMappingsInjectionPoints(
ArcConfig arcConfig,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package io.quarkus.it.smallrye.config;

import java.util.List;
import java.util.Map;
import java.util.Optional;

import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;

@ConfigMapping(prefix = "implicit.converters")
public interface ImplicitConverters {
@WithDefault("value")
Optional<ImplicitOptional> optional();

@WithDefault("value")
List<ImplicitElement> list();

Map<String, ImplicitValue> map();

class ImplicitOptional {
private final String value;

public ImplicitOptional(final String value) {
this.value = value;
}

public String getValue() {
return value;
}

public static ImplicitOptional of(String value) {
return new ImplicitOptional("converted");
}
}

class ImplicitElement {
private final String value;

public ImplicitElement(final String value) {
this.value = value;
}

public String getValue() {
return value;
}

public static ImplicitElement of(String value) {
return new ImplicitElement("converted");
}
}

class ImplicitValue {
private final String value;

public ImplicitValue(final String value) {
this.value = value;
}

public String getValue() {
return value;
}

public static ImplicitValue of(String value) {
return new ImplicitValue("converted");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.quarkus.it.smallrye.config;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;

@Path("/implicit/converters")
public class ImplicitConvertersResource {
@Inject
ImplicitConverters implicitConverters;

@GET
@Path("/optional")
public Response optional() {
return Response.ok(implicitConverters.optional().get().getValue()).build();
}

@GET
@Path("/list")
public Response list() {
return Response.ok(implicitConverters.list().get(0).getValue()).build();
}

@GET
@Path("/map")
public Response map() {
return Response.ok(implicitConverters.map().get("key").getValue()).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key=somearbitrarycra
smallrye.config.source.keystore.test.path=keystore
smallrye.config.source.keystore.test.password=secret
smallrye.config.source.keystore.test.handler=aes-gcm-nopadding

#implicit converters
implicit.converters.map."key"=value
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.quarkus.it.smallrye.config;

import io.quarkus.test.junit.QuarkusIntegrationTest;

@QuarkusIntegrationTest
public class ImplicitConvertersIT extends ImplicitConvertersTest {

}
Loading