-
Notifications
You must be signed in to change notification settings - Fork 474
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
8 changed files
with
508 additions
and
24 deletions.
There are no files selected for viewing
202 changes: 202 additions & 0 deletions
202
src/main/java/org/springframework/hateoas/aot/ControllerMethodReturnTypeAotProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.