Skip to content
Open
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
@@ -0,0 +1,52 @@
package io.nexusrpc.handler;

import io.nexusrpc.ServiceDefinition;
import java.lang.reflect.Method;
import java.util.Objects;
import org.jspecify.annotations.Nullable;

/**
* Extension point for {@link ServiceImplInstance#fromInstance(Object, java.util.List)} that lets a
* framework recognize its own operation-handler annotation alongside {@link OperationImpl}.
*
* <p>For each method discovered during scanning of a {@link ServiceImpl}-annotated class, the
* extension's {@link #extract} is called once. If the method belongs to the extension's annotation,
* the extension returns a {@link Result} describing the operation name and handler; otherwise it
* returns {@code null}. Duplicate operation registrations (across both {@link OperationImpl} and
* any extension) are caught by the builder.
*
* <p>Extensions are responsible for their own validation of method shape (parameter types, return
* type, modifiers) and for translating the method into an {@link OperationHandler}.
*/
public interface MethodExtension {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Let's mark this as experimental. We should do this for all of the non final APIs like the ServiceImpl.


/**
* Inspect {@code method} on {@code instance}. Return a {@link Result} to register an operation
* handler, or {@code null} if the method is not claimed by this extension.
*
* @param instance the service implementation instance
* @param method the method being scanned
* @param serviceDefinition the resolved service definition for the {@code @ServiceImpl}
* @return a result describing the operation, or {@code null} to skip
*/
@Nullable Result extract(Object instance, Method method, ServiceDefinition serviceDefinition);

/** Result returned by an extension that claims a method. */
final class Result {
private final String operationName;
private final OperationHandler<?, ?> handler;

public Result(String operationName, OperationHandler<?, ?> handler) {
this.operationName = Objects.requireNonNull(operationName, "operationName");
this.handler = Objects.requireNonNull(handler, "handler");
}

public String getOperationName() {
return operationName;
}

public OperationHandler<?, ?> getHandler() {
return handler;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ public class ServiceImplInstance {
* operations.
*/
public static ServiceImplInstance fromInstance(Object instance) {
return fromInstance(instance, Collections.emptyList());
}

/**
* Create a service instance from the given object instance, with additional {@link
* MethodExtension}s consulted for each scanned method. Extensions let frameworks recognize their
* own operation-handler annotations alongside {@link OperationImpl}.
*
* <p>For every method on the service class, {@link OperationImpl} is processed first; then each
* extension is invoked. Duplicate operation registrations across {@link OperationImpl} and any
* extension fail the build.
*/
public static ServiceImplInstance fromInstance(
Object instance, List<MethodExtension> extensions) {
// Expect the annotation on the class of the instance itself, do not expect it to be inherited
// and go searching through superclasses
ServiceImpl serviceImpl = instance.getClass().getDeclaredAnnotation(ServiceImpl.class);
Expand All @@ -39,14 +53,29 @@ public static ServiceImplInstance fromInstance(Object instance) {
Builder builder = newBuilder().setDefinition(serviceDefinition);
for (Method method : methods) {
OperationImpl operationImpl = method.getDeclaredAnnotation(OperationImpl.class);
if (operationImpl == null) {
continue;
if (operationImpl != null) {
try {
addOperationHandler(builder, serviceDefinition, instance, method);
} catch (Exception e) {
throw new RuntimeException(
"Failed obtaining operation handler from " + method.getName(), e);
}
}
try {
addOperationHandler(builder, serviceDefinition, instance, method);
} catch (Exception e) {
throw new RuntimeException(
"Failed obtaining operation handler from " + method.getName(), e);
for (MethodExtension extension : extensions) {
MethodExtension.Result result = extension.extract(instance, method, serviceDefinition);
if (result == null) {
continue;
}
if (builder.operationHandlers.containsKey(result.getOperationName())) {
throw new IllegalStateException(
"Operation name "
+ result.getOperationName()
+ " is registered by both "
+ builder.operationHandlers.get(result.getOperationName())
+ " and "
+ extension.getClass().getName());
}
builder.putOperationHandler(result.getOperationName(), result.getHandler());
}
}

Expand Down Expand Up @@ -119,7 +148,12 @@ private static void addOperationHandler(

// Add to builder
if (builder.operationHandlers.containsKey(operationDefinition.getName())) {
throw new RuntimeException("Multiple overloads with @OperationImpl");
throw new IllegalStateException(
"Operation name "
+ operationDefinition.getName()
+ " is registered by both "
+ method.getName()
+ " and an earlier handler");
}
builder.putOperationHandler(operationDefinition.getName(), (OperationHandler<?, ?>) handler);
}
Expand Down
Loading