diff --git a/src/main/java/org/springframework/hateoas/aot/ControllerMethodReturnTypeAotProcessor.java b/src/main/java/org/springframework/hateoas/aot/ControllerMethodReturnTypeAotProcessor.java new file mode 100644 index 00000000..e577284f --- /dev/null +++ b/src/main/java/org/springframework/hateoas/aot/ControllerMethodReturnTypeAotProcessor.java @@ -0,0 +1,202 @@ +/* + * Copyright 2022 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 + * + * https://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.hateoas.aot; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.function.Predicate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.framework.AopConfigException; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.target.EmptyTargetSource; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.MemberCategory; +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.support.RegisteredBean; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.hateoas.server.core.DummyInvocationUtils; +import org.springframework.hateoas.server.core.LastInvocationAware; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Controller; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * A {@link BeanRegistrationAotProcessor} that contributes proxy types for return types of controller methods so that + * can be pointed to by {@link DummyInvocationUtils}, i.e. creating links via fake method invocations. + * + * @author Christoph Strobl + * @author Oliver Drotbohm + * @since 2.0 + */ +public class ControllerMethodReturnTypeAotProcessor implements BeanRegistrationAotProcessor { + + private final Class controllerAnnotationType; + + /** + * Creates a new {@link ControllerMethodReturnTypeAotProcessor} looking for classes annotated with + * {@link Controller}. + */ + public ControllerMethodReturnTypeAotProcessor() { + this(Controller.class); + } + + /** + * Creates a new {@link ControllerMethodReturnTypeAotProcessor} looking for classes equipped with the given + * annotation. + * + * @param controllerAnnotationType must not be {@literal null}. + */ + protected ControllerMethodReturnTypeAotProcessor( + Class controllerAnnotationType) { + + Assert.notNull(controllerAnnotationType, "Controller anntotation type must not be null!"); + + this.controllerAnnotationType = controllerAnnotationType; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.aot.BeanRegistrationAotProcessor#processAheadOfTime(org.springframework.beans.factory.support.RegisteredBean) + */ + @Override + @Nullable + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + + var beanClass = registeredBean.getBeanClass(); + + return AnnotatedElementUtils.isAnnotated(beanClass, controllerAnnotationType) + ? new ProxyRegisteringAotContribution(beanClass) + : null; + } + + /** + * AOT contribution that registers proxy types for return types of controller methods. + * + * @author Oliver Drotbohm + * @since 2.0 + */ + private static class ProxyRegisteringAotContribution implements BeanRegistrationAotContribution { + + private static final Logger LOGGER = LoggerFactory.getLogger(ProxyRegisteringAotContribution.class); + + private final Class beanClass; + + ProxyRegisteringAotContribution(Class beanClass) { + + Assert.notNull(beanClass, "Bean class must not be null!"); + + this.beanClass = beanClass; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.aot.BeanRegistrationAotContribution#applyTo(org.springframework.aot.generate.GenerationContext, org.springframework.beans.factory.aot.BeanRegistrationCode) + */ + @Override + public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { + + Class proxyType = registerCglibProxy(beanClass, beanClass, generationContext); + + if (proxyType != null) { + LOGGER.info("Created proxy type {} for {}", proxyType, beanClass); + } + + ReflectionUtils.doWithMethods(beanClass, (method) -> { + + Class returnType = method.getReturnType(); + + if (ReflectionUtils.isObjectMethod(method) + || method.isSynthetic() + || method.isBridge() + || Modifier.isPrivate(method.getModifiers()) + || ClassUtils.isAssignable(returnType, void.class)) { + return; + } + + if (returnType.isInterface()) { + generationContext.getRuntimeHints().proxies().registerJdkProxy(returnType); + return; + } + + registerCglibProxy(returnType, beanClass, generationContext); + }); + } + + @Nullable + private Class registerCglibProxy(Class type, Class beanClass, GenerationContext context) { + + if (Modifier.isFinal(type.getModifiers())) { + return null; + } + + // Wee need to find at least one non-private constructor to be able to create a CGLib proxy in the first + // place + var anyNonPrivateConstructor = Arrays.stream(type.getDeclaredConstructors()) + .map(Constructor::getModifiers) + .anyMatch(Predicate.not(Modifier::isPrivate)); + + if (!anyNonPrivateConstructor) { + return null; + } + + var result = createProxyClass(type, beanClass); + + if (result != null) { + + // Required for EnhancerFactoryData(Class, Class[], boolean) + var reflection = context.getRuntimeHints().reflection(); + + reflection.registerType(result, MemberCategory.INVOKE_DECLARED_METHODS); + + reflection.registerField(ReflectionUtils.findField(result, "CGLIB$FACTORY_DATA")); + reflection.registerField(ReflectionUtils.findField(result, "CGLIB$CALLBACK_FILTER")); + } + + return result; + } + + @Nullable + private Class createProxyClass(Class type, Class beanClass) { + + try { + + var factory = new ProxyFactory(); + + factory.addInterface(LastInvocationAware.class); + factory.setProxyTargetClass(true); + factory.setTargetSource(EmptyTargetSource.forClass(type)); + + return factory.getProxyClass(type.getClassLoader()); + + } catch (AopConfigException o_O) { + + LOGGER.info("Could not create proxy class for {} (via {}). Reason {}", type, beanClass, + o_O.getMessage()); + } + + return null; + } + } + +} diff --git a/src/main/java/org/springframework/hateoas/aot/HypermediaTypeAotProcessor.java b/src/main/java/org/springframework/hateoas/aot/HypermediaTypeAotProcessor.java new file mode 100644 index 00000000..b8788f91 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/aot/HypermediaTypeAotProcessor.java @@ -0,0 +1,260 @@ +/* + * Copyright 2022 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 + * + * https://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.hateoas.aot; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.TypeReference; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +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.config.BeanDefinition; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * A {@link BeanRegistrationAotProcessor} to register types that will be rendered by Jackson for reflection. The + * registration will consider the media types activated via {@link EnableHypermediaSupport} but always register the core + * HATEOAS package as well as the ones for ALPS and HTTP Error details. + * + * @author Oliver Drotbohm + * @since 2.0 + */ +class HypermediaTypeAotProcessor implements BeanRegistrationAotProcessor { + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.aot.BeanRegistrationAotProcessor#processAheadOfTime(org.springframework.beans.factory.support.RegisteredBean) + */ + @Override + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + + EnableHypermediaSupport annotation = AnnotatedElementUtils.findMergedAnnotation(registeredBean.getBeanClass(), + EnableHypermediaSupport.class); + + if (annotation == null) { + return null; + } + + var fromConfig = Arrays.stream(annotation.type()) + .map(HypermediaType::getLocalPackageName); + + var mediaTypePackages = Stream.concat(fromConfig, Stream.of("alps", "problem")) + .map("org.springframework.hateoas.mediatype."::concat); + + var packagesToScan = Stream.concat(Stream.of("org.springframework.hateoas"), mediaTypePackages).toList(); + + return packagesToScan.isEmpty() ? null : new MediaTypeReflectionAotContribution(packagesToScan); + } + + static class MediaTypeReflectionAotContribution implements BeanRegistrationAotContribution { + + private static final Logger LOGGER = LoggerFactory.getLogger(MediaTypeReflectionAotContribution.class); + + private final List mediaTypePackage; + private final Set packagesSeen; + + /** + * Creates a new {@link MediaTypeReflectionAotContribution} for the given packages. + * + * @param mediaTypePackage must not be {@literal null}. + */ + public MediaTypeReflectionAotContribution(List mediaTypePackage) { + + Assert.notNull(mediaTypePackage, "Media type packages must not be null!"); + + this.mediaTypePackage = mediaTypePackage; + this.packagesSeen = new HashSet<>(); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.aot.BeanRegistrationAotContribution#applyTo(org.springframework.aot.generate.GenerationContext, org.springframework.beans.factory.aot.BeanRegistrationCode) + */ + @Override + public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { + + var reflection = generationContext.getRuntimeHints().reflection(); + + mediaTypePackage.forEach(it -> { + + if (packagesSeen.contains(it)) { + return; + } + + packagesSeen.add(it); + + // Register RepresentationModel types for full reflection + FullTypeScanner provider = new FullTypeScanner(); + provider.addIncludeFilter(new JacksonAnnotationPresentFilter()); + provider.addIncludeFilter(new JacksonSuperTypeFilter()); + + // Add filter to limit scan to sole package, not nested ones + provider.addExcludeFilter(new EnforcedPackageFilter(it)); + + LOGGER.info("Registering Spring HATEOAS types in {} for reflection.", it); + + provider.findCandidateComponents(it).stream() + .map(BeanDefinition::getBeanClassName) + .sorted() + .peek(type -> LOGGER.debug("> {}", type)) + .map(TypeReference::of) + .forEach(reference -> reflection.registerType(reference, // + MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS)); + }); + } + } + + static class FullTypeScanner extends ClassPathScanningCandidateComponentProvider { + + public FullTypeScanner() { + super(false); + } + + /* + * (non-Javadoc) + * @see org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#isCandidateComponent(org.springframework.beans.factory.annotation.AnnotatedBeanDefinition) + */ + @Override + protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { + return true; + } + } + + /** + * A {@link TypeFilter} to only match types outside the configured package. Usually used as exclude filter + * to limit scans to not find nested packages. + * + * @author Oliver Drotbohm + */ + static class EnforcedPackageFilter implements TypeFilter { + + private final String referencePackage; + + public EnforcedPackageFilter(String referencePackage) { + this.referencePackage = referencePackage; + } + + /* + * (non-Javadoc) + * @see org.springframework.core.type.filter.TypeFilter#match(org.springframework.core.type.classreading.MetadataReader, org.springframework.core.type.classreading.MetadataReaderFactory) + */ + @Override + public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) + throws IOException { + return !referencePackage + .equals(ClassUtils.getPackageName(metadataReader.getClassMetadata().getClassName())); + } + } + + static abstract class TraversingTypeFilter implements TypeFilter { + + /* + * (non-Javadoc) + * @see org.springframework.core.type.filter.TypeFilter#match(org.springframework.core.type.classreading.MetadataReader, org.springframework.core.type.classreading.MetadataReaderFactory) + */ + @Override + public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) + throws IOException { + + if (doMatch(metadataReader, metadataReaderFactory)) { + return true; + } + + var classMetadata = metadataReader.getClassMetadata(); + + String superClassName = classMetadata.getSuperClassName(); + + if (superClassName != null && !superClassName.startsWith("java") + && match(metadataReaderFactory.getMetadataReader(superClassName), metadataReaderFactory)) { + return true; + } + + for (String names : classMetadata.getInterfaceNames()) { + + MetadataReader reader = metadataReaderFactory.getMetadataReader(names); + + if (match(reader, metadataReaderFactory)) { + return true; + } + } + + return false; + } + + protected abstract boolean doMatch(MetadataReader reader, MetadataReaderFactory factory); + } + + static class JacksonAnnotationPresentFilter extends TraversingTypeFilter { + + private static final Predicate IS_JACKSON_ANNOTATION = it -> it.startsWith("com.fasterxml.jackson"); + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.aot.HateoasRuntimeHints.TraversingTypeFilter#doMatch(org.springframework.core.type.classreading.MetadataReader, org.springframework.core.type.classreading.MetadataReaderFactory) + */ + @Override + protected boolean doMatch(MetadataReader reader, MetadataReaderFactory factory) { + + var annotationMetadata = reader.getAnnotationMetadata(); + + // Type annotations + return annotationMetadata + .getAnnotationTypes() + .stream() + .anyMatch(IS_JACKSON_ANNOTATION) + + // Method annotations + || annotationMetadata.getDeclaredMethods().stream() + .flatMap(it -> it.getAnnotations().stream()) + .map(MergedAnnotation::getType) + .map(Class::getName) + .anyMatch(IS_JACKSON_ANNOTATION); + } + } + + static class JacksonSuperTypeFilter extends TraversingTypeFilter { + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.aot.HateoasRuntimeHints.TraversingTypeFilter#doMatch(org.springframework.core.type.classreading.MetadataReader, org.springframework.core.type.classreading.MetadataReaderFactory) + */ + @Override + protected boolean doMatch(MetadataReader reader, MetadataReaderFactory factory) { + return reader.getClassMetadata().getClassName().startsWith("com.fasterxml.jackson"); + } + } +} diff --git a/src/main/java/org/springframework/hateoas/aot/HateoasRuntimeHints.java b/src/main/java/org/springframework/hateoas/aot/RepresentationModelRuntimeHints.java similarity index 56% rename from src/main/java/org/springframework/hateoas/aot/HateoasRuntimeHints.java rename to src/main/java/org/springframework/hateoas/aot/RepresentationModelRuntimeHints.java index cf7dd6e8..5e4b59d1 100644 --- a/src/main/java/org/springframework/hateoas/aot/HateoasRuntimeHints.java +++ b/src/main/java/org/springframework/hateoas/aot/RepresentationModelRuntimeHints.java @@ -15,15 +15,29 @@ */ package org.springframework.hateoas.aot; +import java.util.List; + 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.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.PagedModel; +import org.springframework.hateoas.RepresentationModel; /** + * Registers reflection metadata for {@link RepresentationModel} types. + * * @author Oliver Drotbohm */ -class HateoasRuntimeHints implements RuntimeHintsRegistrar { +class RepresentationModelRuntimeHints implements RuntimeHintsRegistrar { + + private static final List> REPRESENTATION_MODELS = List.of(RepresentationModel.class, // + EntityModel.class, // + CollectionModel.class, // + PagedModel.class, + PagedModel.PageMetadata.class); /* * (non-Javadoc) @@ -32,11 +46,9 @@ class HateoasRuntimeHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, ClassLoader classLoader) { - var serializeTypeReference = TypeReference - .of("org.springframework.hateoas.EntityModel$MapSuppressingUnwrappingSerializer"); + ReflectionHints reflection = hints.reflection(); - hints.reflection().registerType(serializeTypeReference, builder -> { - builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); - }); + REPRESENTATION_MODELS.forEach(it -> reflection.registerType(it, // + MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS)); } } diff --git a/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java b/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java index 5c17073d..5ff86baf 100644 --- a/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java +++ b/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java @@ -20,8 +20,6 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.Arrays; -import java.util.List; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Import; @@ -52,8 +50,8 @@ public @interface EnableHypermediaSupport { /** * Configures which {@link WebStack}s we're supposed to enable support for. By default we're activating it for all - * available ones if they happen to be in use. Configure this explicitly in case you're using WebFlux components like - * {@link WebClient} but don't want to use hypermedia operations with it. + * available ones if they happen to be in use. Configure this explicitly in case you're using WebFlux components + * like {@link WebClient} but don't want to use hypermedia operations with it. * * @return */ @@ -73,39 +71,45 @@ public @interface EnableHypermediaSupport { * @see http://stateless.co/hal_specification.html * @see https://tools.ietf.org/html/draft-kelly-json-hal-05 */ - HAL(MediaTypes.HAL_JSON), + HAL(MediaTypes.HAL_JSON, "hal"), /** * HAL-FORMS - Independent, backward-compatible extension of the HAL designed to add runtime FORM support * * @see https://rwcbook.github.io/hal-forms/ */ - HAL_FORMS(MediaTypes.HAL_FORMS_JSON), + HAL_FORMS(MediaTypes.HAL_FORMS_JSON, "hal.forms"), - HTTP_PROBLEM_DETAILS(MediaTypes.HTTP_PROBLEM_DETAILS_JSON), + HTTP_PROBLEM_DETAILS(MediaTypes.HTTP_PROBLEM_DETAILS_JSON, "problem"), /** * Collection+JSON * * @see http://amundsen.com/media-types/collection/format/ */ - COLLECTION_JSON(MediaTypes.COLLECTION_JSON), + COLLECTION_JSON(MediaTypes.COLLECTION_JSON, "collectionjson"), /** * UBER Hypermedia * * @see https://rawgit.com/uber-hypermedia/specification/master/uber-hypermedia.html */ - UBER(MediaTypes.UBER_JSON); + UBER(MediaTypes.UBER_JSON, "uber"); - private final List mediaTypes; + private final MediaType mediaTypes; + private final String localPackageName; - HypermediaType(MediaType... mediaTypes) { - this.mediaTypes = Arrays.asList(mediaTypes); + HypermediaType(MediaType mediaType, String localPackageName) { + this.mediaTypes = mediaType; + this.localPackageName = localPackageName; } - public List getMediaTypes() { + public MediaType getMediaType() { return this.mediaTypes; } + + public String getLocalPackageName() { + return localPackageName; + } } } diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java b/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java index 896c8045..4624788a 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java @@ -79,7 +79,7 @@ class HypermediaConfigurationImportSelector implements ImportSelector, ResourceL List types = attributes == null // ? Collections.emptyList() // : Arrays.stream((HypermediaType[]) attributes.get("type")) // - .flatMap(it -> it.getMediaTypes().stream()) // + .map(it -> it.getMediaType()) // .collect(Collectors.toList()); if (!beanFactory.containsBean("hateoasMediaTypeConfigurer")) { diff --git a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonMediaTypeConfiguration.java b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonMediaTypeConfiguration.java index 26677654..0f5b2ae7 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonMediaTypeConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonMediaTypeConfiguration.java @@ -15,6 +15,7 @@ */ package org.springframework.hateoas.mediatype.collectionjson; +import java.util.Collections; import java.util.List; import org.springframework.context.annotation.Bean; @@ -47,7 +48,7 @@ class CollectionJsonMediaTypeConfiguration implements HypermediaMappingInformati */ @Override public List getMediaTypes() { - return HypermediaType.COLLECTION_JSON.getMediaTypes(); + return Collections.singletonList(HypermediaType.COLLECTION_JSON.getMediaType()); } /* diff --git a/src/main/java/org/springframework/hateoas/mediatype/uber/UberMediaTypeConfiguration.java b/src/main/java/org/springframework/hateoas/mediatype/uber/UberMediaTypeConfiguration.java index 277ef853..8eb22d6b 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/uber/UberMediaTypeConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/uber/UberMediaTypeConfiguration.java @@ -15,6 +15,7 @@ */ package org.springframework.hateoas.mediatype.uber; +import java.util.Collections; import java.util.List; import org.springframework.context.annotation.Bean; @@ -47,7 +48,7 @@ class UberMediaTypeConfiguration implements HypermediaMappingInformation { */ @Override public List getMediaTypes() { - return HypermediaType.UBER.getMediaTypes(); + return Collections.singletonList(HypermediaType.UBER.getMediaType()); } /* diff --git a/src/main/resources/META-INF/spring/aot.factories b/src/main/resources/META-INF/spring/aot.factories index dae19dc1..89100538 100644 --- a/src/main/resources/META-INF/spring/aot.factories +++ b/src/main/resources/META-INF/spring/aot.factories @@ -1,2 +1,6 @@ +org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ + org.springframework.hateoas.aot.ControllerMethodReturnTypeAotProcessor,\ + org.springframework.hateoas.aot.HypermediaTypeAotProcessor + org.springframework.aot.hint.RuntimeHintsRegistrar=\ - org.springframework.hateoas.aot.HateoasRuntimeHints + org.springframework.hateoas.aot.RepresentationModelRuntimeHints