OpenAPI plugin: 'non-static' and/or 'wrapped Handler' fields annotation HowTo
See original GitHub issueHi, I am familiarizing myself with the OpenAPI plugin API, notably using annotations and may need some nudge/guidance on how to use the annotation API for non-static and wrapped Handler
endpoints.
Inspired by the tutorial and documentation snippets, I crafted a MVP using a Handler
that is defined via a final static
(case 1), final
-only (case 2) and one with a custom/wrapped Handler
implementation that internally branches depending on the context’s ‘Accept’ header:
// [..] standard imports [..]
public class Test {
private static final String ENDPOINT_TEST1 = "/test1";
private static final String ENDPOINT_TEST2 = "/test2";
private static final String ENDPOINT_TEST3 = "/test3";
public Test() {
Javalin instance = RestServer.getInstance();
instance.routes(() -> {
get(ENDPOINT_TEST1, serveTestPage1);
get(ENDPOINT_TEST2, serveTestPage2);
get(ENDPOINT_TEST3, serveTestPage3);
});
}
@OpenApi(description = "GET endpoint test V1", operationId = "serveTestPage", summary = "to serve",
tags = { "Test" }, path = ENDPOINT_TEST1, method = HttpMethod.GET,
responses = { @OpenApiResponse(status = "200", content = @OpenApiContent(type = "text/html")) })
private static final Handler serveTestPage1 = ctx -> { // works OK
ctx.result("serveTestPage1 path = " + ctx.path());
// [serving request based on ctx]
};
@OpenApi(description = "GET endpoint test V2", operationId = "serveTestPage2", summary = "to serve",
tags = { "Test" }, path = ENDPOINT_TEST2, method = HttpMethod.GET,
responses = { @OpenApiResponse(status = "200", content = @OpenApiContent(type = "text/html")) })
private final Handler serveTestPage2 = ctx -> { // note: w/o 'static' modifier
ctx.result("serveTestPage2 path = " + ctx.path());
// [serving request based on ctx]
};
@OpenApi(description = "GET endpoint test V3", operationId = "serveTestPage3", summary = "to serve",
tags = { "Test" }, path = ENDPOINT_TEST3, method = HttpMethod.GET,
responses = { @OpenApiResponse(status = "200", content = @OpenApiContent(type = "text/html")) })
private final Handler serveTestPage3 = new CombinedHandler(ctx -> { // note: wrapped Handler implementation
ctx.result("serveTestPage3 path = " + ctx.path());
// [serving request based on ctx] serving BINARY/JSON/YML based on user 'Accept' header request
});
/**
* little helper class to allow regular GET and SSE initialization via the same end-point
*/
public class CombinedHandler implements Handler {
private final Handler getHandler;
public CombinedHandler(Handler getHandler) {
this.getHandler = getHandler; // end-user/service specific Handler code
}
@Override
public void handle(Context ctx) throws Exception {
// if (MimeType.EVENT_STREAM.toString().equals(ctx.header(Header.ACCEPT))) {
if (MimeType.EVENT_STREAM.equals(RestServer.getRequestedMimeProtocol(ctx)) || ctx.queryParam("sse") != null) {
// [..] handle SSE initialisation requests
ctx.result("handle SSE init process path = " + ctx.path());
return;
}
getHandler.handle(ctx);
}
}
}
All endpoints serve the correct responses. However, while the endpoint swagger documentation seems to work for the standard case 1 (doc shows up in ‘Test’ category’, it fails and triggers the default
“A default response was added to the documentation of […]”
action (and ending up in the ‘default’ category) for endpoints ENDPOINT_TEST2
(not static) and ENDPOINT_TEST3
(wrapped/branching custom Handler
implementation).
What am I missing? Have I moved outside the boundary of what the annotation API allows?
Any hint would be much appreciated!
Many thanks in advance.
Issue Analytics
- State:
- Created 3 years ago
- Comments:16 (16 by maintainers)
That’s essentially what I was getting at. Since your annotation isn’t on the thing that’s being passed, and the thing that’s being passed can’t directly reference the annotation, you’re now just looking for any open api annotation that matches your path. At that point, why bother annotating the variable that stores the lambda? You could put it anywhere. This also doesn’t work if the lambda being bassed doesn’t originate in the class the annotation is stored in since the variable you’re annotating could store a lambda generated anywhere (though I don’t know why you’d do that).
This actually begins to a look a lot like annotation based routing. You’re functionally declaring the route handler with javalin’s get, but then expecting the documentation to be found for that same function being passed by looking for an annotation that specifies the path and method. At that point it would make a lot more sense to me to bind the routes by passing the entire Controller and inspecting all annotations.
I would argue that the
methodReferenceOfNonStaticJavaHandler
is highly counter intuitive and unexpected as well. It’s unfortunate that Java doesn’t keep function annotations (or even function name) when passing by reference, but it doesn’t and I wouldn’t expect this library to attempt to bypass that.It makes more sense to me to expect people to use the DocumentedHandler implementation to work around the limitations of Java.
That said, I normally don’t do much Javalin development in Java anymore, so perhaps others don’t consider this as counterintuitive.
Hi @tipsy, I reviewed this issue and dived into source code. After debug I faced with a strange situation which I don’t know how to explain, because I am not an expert in Java reflection. As I noticed the reason why non-static lambda field is ignored is inside
ReflectionUtil.lambdaField
whereReflectionUtil.javaFieldName
is called: https://github.com/tipsy/javalin/blob/1872e25a266432e0156aceb4b32d4c12c197b187/javalin/src/main/java/io/javalin/core/util/ReflectionUtil.kt#L55-L60I have two fields in my test class:
Here inside
javaFieldName
-Field.get(it)
works for static lambda field, but non-static throwsIllegalArgumentException
: https://github.com/tipsy/javalin/blob/1872e25a266432e0156aceb4b32d4c12c197b187/javalin/src/main/java/io/javalin/core/util/ReflectionUtil.kt#L11I think the key for solving this problem - is the reason why javaFieldName for non-static lambda fields cannot be retrieved from declaredFields. Need advice from someone with more expertise in Java reflection.