#1858 - Improved AOT metadata.
We now register proxy types for all controller beans and the return types of their methods if suitable for proxying. This allows the link creation by pointing to controller methods. We now also register all types (de)serialized by Jackson for reflection.
This commit is contained in:
@@ -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<? extends Annotation> 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<? extends Annotation> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String> mediaTypePackage;
|
||||
private final Set<String> packagesSeen;
|
||||
|
||||
/**
|
||||
* Creates a new {@link MediaTypeReflectionAotContribution} for the given packages.
|
||||
*
|
||||
* @param mediaTypePackage must not be {@literal null}.
|
||||
*/
|
||||
public MediaTypeReflectionAotContribution(List<String> 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 <em>outside</em> 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<String> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Class<?>> 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));
|
||||
}
|
||||
}
|
||||
@@ -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<MediaType> 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<MediaType> getMediaTypes() {
|
||||
public MediaType getMediaType() {
|
||||
return this.mediaTypes;
|
||||
}
|
||||
|
||||
public String getLocalPackageName() {
|
||||
return localPackageName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ class HypermediaConfigurationImportSelector implements ImportSelector, ResourceL
|
||||
List<MediaType> 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")) {
|
||||
|
||||
@@ -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<MediaType> getMediaTypes() {
|
||||
return HypermediaType.COLLECTION_JSON.getMediaTypes();
|
||||
return Collections.singletonList(HypermediaType.COLLECTION_JSON.getMediaType());
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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<MediaType> getMediaTypes() {
|
||||
return HypermediaType.UBER.getMediaTypes();
|
||||
return Collections.singletonList(HypermediaType.UBER.getMediaType());
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user