From 4beb05ddb327bb533ed410df767e8f787488dfd4 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Mon, 23 Jan 2023 18:29:25 +0100 Subject: [PATCH] Add native support for RSocketExchange. This commit introduces new AOT processors that look for `@RSocketExchange` annotated methods on interfaces implemented by beans and registers reachability metadata accordingly: * JDK proxies for the beans themselves * invocation reflection for annotated methods * binding reflection for arguments and return types This allows to compile such clients to Native Images. Closes gh-29877 --- spring-messaging/spring-messaging.gradle | 1 + .../rsocket/service/RSocketExchange.java | 3 + ...tExchangeBeanRegistrationAotProcessor.java | 84 ++++++++++ .../RSocketExchangeReflectiveProcessor.java | 68 ++++++++ .../resources/META-INF/spring/aot.factories | 2 + ...angeBeanRegistrationAotProcessorTests.java | 95 +++++++++++ ...ocketExchangeReflectiveProcessorTests.java | 151 ++++++++++++++++++ 7 files changed, 404 insertions(+) create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketExchangeBeanRegistrationAotProcessor.java create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketExchangeReflectiveProcessor.java create mode 100644 spring-messaging/src/main/resources/META-INF/spring/aot.factories create mode 100644 spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketExchangeBeanRegistrationAotProcessorTests.java create mode 100644 spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketExchangeReflectiveProcessorTests.java diff --git a/spring-messaging/spring-messaging.gradle b/spring-messaging/spring-messaging.gradle index 258b58e0f82..ea6016ef146 100644 --- a/spring-messaging/spring-messaging.gradle +++ b/spring-messaging/spring-messaging.gradle @@ -34,6 +34,7 @@ dependencies { testImplementation("org.skyscreamer:jsonassert") testImplementation("org.xmlunit:xmlunit-assertj") testImplementation("org.xmlunit:xmlunit-matchers") + testImplementation(project(":spring-core-test")) testRuntimeOnly("com.sun.activation:jakarta.activation") testRuntimeOnly("com.sun.xml.bind:jaxb-core") testRuntimeOnly("com.sun.xml.bind:jaxb-impl") diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketExchange.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketExchange.java index 4f86c51775e..59d1dd4e8c5 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketExchange.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketExchange.java @@ -22,6 +22,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.aot.hint.annotation.Reflective; + /** * Annotation to declare a method on an RSocket service interface as an RSocket * endpoint. The endpoint route is determined through the annotation attribute, @@ -65,6 +67,7 @@ import java.lang.annotation.Target; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented +@Reflective(RSocketExchangeReflectiveProcessor.class) public @interface RSocketExchange { /** diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketExchangeBeanRegistrationAotProcessor.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketExchangeBeanRegistrationAotProcessor.java new file mode 100644 index 00000000000..5427e1c413a --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketExchangeBeanRegistrationAotProcessor.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-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 + * + * 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.messaging.rsocket.service; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.ProxyHints; +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.MergedAnnotations; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * An AOT {@link BeanRegistrationAotProcessor} that detects the presence of + * {@link RSocketExchange @RSocketExchange} on methods and creates + * the required proxy hints. + * Based on {@code HttpExchangeBeanRegistrationAotProcessor} + * + * @author Sebastien Deleuze + * @author Olga Maciaszek-Sharma + * @since 6.0 + * @see org.springframework.web.service.annotation.HttpExchangeBeanRegistrationAotProcessor + */ +class RSocketExchangeBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor { + + @Override + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + Class beanClass = registeredBean.getBeanClass(); + Set> exchangeInterfaces = new HashSet<>(); + for (Class interfaceClass : ClassUtils.getAllInterfacesForClass(beanClass)) { + ReflectionUtils.doWithMethods(interfaceClass, method -> { + if (!exchangeInterfaces.contains(interfaceClass) && + MergedAnnotations.from(method, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY) + .get(RSocketExchange.class).isPresent()) { + exchangeInterfaces.add(interfaceClass); + } + }); + } + if (!exchangeInterfaces.isEmpty()) { + return new RSocketExchangeBeanRegistrationContribution(exchangeInterfaces); + } + return null; + } + + private static class RSocketExchangeBeanRegistrationContribution implements BeanRegistrationAotContribution { + + private final Set> rSocketExchangeInterfaces; + + public RSocketExchangeBeanRegistrationContribution(Set> rSocketExchangeInterfaces) { + this.rSocketExchangeInterfaces = rSocketExchangeInterfaces; + } + + @Override + public void applyTo(GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode) { + ProxyHints proxyHints = generationContext.getRuntimeHints().proxies(); + for (Class httpExchangeInterface : this.rSocketExchangeInterfaces) { + proxyHints.registerJdkProxy(AopProxyUtils + .completeJdkProxyInterfaces(httpExchangeInterface)); + } + } + + } +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketExchangeReflectiveProcessor.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketExchangeReflectiveProcessor.java new file mode 100644 index 00000000000..adba9f045ad --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketExchangeReflectiveProcessor.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-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 + * + * 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.messaging.rsocket.service; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +import org.springframework.aot.hint.BindingReflectionHintsRegistrar; +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.annotation.ReflectiveProcessor; +import org.springframework.core.MethodParameter; + +/** + * A {@link ReflectiveProcessor} implementation for {@link RSocketExchange @RSocketExchange} + * annotated methods. In addition to registering reflection hints for invoking + * the annotated method, this implementation handles reflection-based + * binding for return types and parameters. + * Based on {@code HttpExchangeReflectiveProcessor}. + * + * @author Sebastien Deleuze + * @author Olga Maciaszek-Sharma + * @since 6.0 + * @see org.springframework.web.service.annotation.HttpExchangeReflectiveProcessor + */ +public class RSocketExchangeReflectiveProcessor implements ReflectiveProcessor { + + private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar(); + + @Override + public void registerReflectionHints(ReflectionHints hints, AnnotatedElement element) { + if (element instanceof Method method) { + this.registerMethodHints(hints, method); + } + } + + protected void registerMethodHints(ReflectionHints hints, Method method) { + hints.registerMethod(method, ExecutableMode.INVOKE); + for (Parameter parameter : method.getParameters()) { + // Also register non-annotated parameters to handle metadata + this.bindingRegistrar.registerReflectionHints(hints, + MethodParameter.forParameter(parameter).getGenericParameterType()); + } + registerReturnTypeHints(hints, MethodParameter.forExecutable(method, -1)); + } + + protected void registerReturnTypeHints(ReflectionHints hints, MethodParameter returnTypeParameter) { + if (!void.class.equals(returnTypeParameter.getParameterType())) { + this.bindingRegistrar.registerReflectionHints(hints, returnTypeParameter + .getGenericParameterType()); + } + } +} diff --git a/spring-messaging/src/main/resources/META-INF/spring/aot.factories b/spring-messaging/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 00000000000..b7d10725eeb --- /dev/null +++ b/spring-messaging/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1,2 @@ +org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ +org.springframework.messaging.rsocket.service.RSocketExchangeBeanRegistrationAotProcessor \ No newline at end of file diff --git a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketExchangeBeanRegistrationAotProcessorTests.java b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketExchangeBeanRegistrationAotProcessorTests.java new file mode 100644 index 00000000000..3ad81066591 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketExchangeBeanRegistrationAotProcessorTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-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 + * + * 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.messaging.rsocket.service; + + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.SpringProxy; +import org.springframework.aop.framework.Advised; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.DecoratingProxy; +import org.springframework.lang.Nullable; +import org.springframework.messaging.handler.annotation.Payload; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RSocketExchangeBeanRegistrationAotProcessor}. + * + * @author Sebastien Deleuze + * @author Olga Maciaszek-Sharma + */ +class RSocketExchangeBeanRegistrationAotProcessorTests { + + private final RSocketExchangeBeanRegistrationAotProcessor processor = + new RSocketExchangeBeanRegistrationAotProcessor(); + + private final GenerationContext generationContext = new TestGenerationContext(); + + @Test + void shouldProcessesAnnotatedInterface() { + process(AnnotatedInterface.class); + assertThat(RuntimeHintsPredicates.proxies().forInterfaces(AnnotatedInterface.class, + SpringProxy.class, Advised.class, DecoratingProxy.class)) + .accepts(this.generationContext.getRuntimeHints()); + } + + @Test + void shouldSkipNonAnnotatedInterface() { + process(NonAnnotatedInterface.class); + assertThat(this.generationContext.getRuntimeHints().proxies().jdkProxyHints()).isEmpty(); + } + + + void process(Class beanClass) { + BeanRegistrationAotContribution contribution = createContribution(beanClass); + if (contribution != null) { + contribution.applyTo(this.generationContext, mock(BeanRegistrationCode.class)); + } + } + + @Nullable + private BeanRegistrationAotContribution createContribution(Class beanClass) { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition(beanClass.getName(), new RootBeanDefinition(beanClass)); + return this.processor.processAheadOfTime(RegisteredBean.of(beanFactory, beanClass.getName())); + + } + + interface NonAnnotatedInterface { + + void notExchange(); + } + + interface AnnotatedInterface { + + @RSocketExchange + void exchange(@Payload String testPayload); + } + +} + + diff --git a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketExchangeReflectiveProcessorTests.java b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketExchangeReflectiveProcessorTests.java new file mode 100644 index 00000000000..6c96836f8f7 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketExchangeReflectiveProcessorTests.java @@ -0,0 +1,151 @@ +/* + * Copyright 2002-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 + * + * 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.messaging.rsocket.service; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.util.MimeType; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link RSocketExchangeReflectiveProcessor}. + * + * @author Sebastien Deleuze + * @author Olga Maciaszek-Sharma + */ +class RSocketExchangeReflectiveProcessorTests { + + private final RSocketExchangeReflectiveProcessor processor = new RSocketExchangeReflectiveProcessor(); + + private final RuntimeHints hints = new RuntimeHints(); + + @Test + void shouldRegisterReflectionHintsForMethod() throws NoSuchMethodException { + Method method = SampleService.class.getDeclaredMethod("get", Request.class, Variable.class, + Metadata.class, MimeType.class); + + processor.registerReflectionHints(hints.reflection(), method); + + assertThat(RuntimeHintsPredicates.reflection().onType(SampleService.class)) + .accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleService.class, "get")) + .accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onType(Response.class)) + .accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onMethod(Response.class, "getMessage")) + .accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onMethod(Response.class, "setMessage")) + .accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onType(Request.class)) + .accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onMethod(Request.class, "getMessage")) + .accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onMethod(Request.class, "setMessage")) + .accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onType(Variable.class)) + .accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onMethod(Variable.class, "getValue")) + .accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onMethod(Variable.class, "setValue")) + .accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onType(Metadata.class)) + .accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onMethod(Metadata.class, "getValue")) + .accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onMethod(Metadata.class, "setValue")) + .accepts(hints); + } + + interface SampleService { + + @RSocketExchange + Response get(@Payload Request request, @DestinationVariable Variable variable, + Metadata metadata, MimeType mimeType); + + } + + static class Request { + + private String message; + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + } + + static class Response { + + private String message; + + public Response(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + } + + static class Variable { + + private String value; + + public Variable(String value) { + this.value = value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + static class Metadata { + + private String value; + + public Metadata(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +}