Quarkus: Explore notion of opinionated REST resource

Created on 11 Jan 2019  路  21Comments  路  Source: quarkusio/quarkus

Objective, most simple hello world.

Use the notion of opinionated @Controller

From

@Path("/hello")
public class GreetingResource {

    @Inject
    GreetingService service;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/greeting/{name}")
    public String greeting(@PathParam("name") String name) {
        return service.greeting(name);
    }

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "hello";
    }
}

To

@Controller
// /hello
public class Hello {

    // [...]/greetings/name
    public String greeting(String name) {
        return "Hello " + name;
    }

    // [...]/greetings/
    public String hello() {
        return "hello";
    }
}
exploration

All 21 comments

Maybe the (String name) could be a query parameter instead of a path parameter.

With the name of the class and the method there would already be two levels of path nesting.

We could do this by autogenerating @Path annotations at compile time if they're not present.

@emmanuelbernard @FroMage What was the problem with introspecting the names of method parameters that we ran into? (Even in recent versions of Java.) I don't recall the details but I do remember we investigated this and decided not to use it. Was it just that the JDK itself is not compiled with the necessary compiler switch?

Yes, it's a compiler switch, but the RESTEasy recommendation is to use that flag, we can even check it at compile-time too, to make sure the user is using it and warn if not.

Can't say I'm excited about this.

One of the issue I have is that you don't see the URL clearly in the code, which is IMHO useful.

In the example you took, the method is also a perfect candidate for a @Post as you have one parameter that could come from the request body and you return a string. Not obvious how you will decide?

You also decided "it's a string so let's do it plain text", but in the more common case where you have an object, you will still have to indicate if you want it XML or JSON.

What if you have a public method in your resource that is not supposed to be exposed? It will get exposed.

In the end, you end up with dark magic and you don't really know what's going on. I think it's a step too far. Adding a couple annotations is not that bad in this case, especially annotations people are used to.

We could do this by autogenerating @Path annotations at compile time if they're not present.

Does RESTEasy support this? RESTEasy is probably using reflection and to make this work you would need some abstraction layer or SPI (such as AnnotatedType in CDI "full") that would allow you to replace the original annotations.

One of the issue I have is that you don't see the URL clearly in the code, which is IMHO useful.

The same could be said of the REST prefix. In practice you know about the rule that it's class/method, and it poses no problem. It also allows you to start this way and get your app written fast and decide later on which URL you want to use.

In the example you took, the method is also a perfect candidate for a @Post as you have one parameter that could come from the request body and you return a string. Not obvious how you will decide?

So, it turns out that this only matters for generating docs, like openapi. In practice if your method answers to both GET and POST with the same action, taking the param from the query or the body, it doesn't matter one bit. Clients are still going to get served and your code is still going to work. It's a win-win.

You also decided "it's a string so let's do it plain text", but in the more common case where you have an object, you will still have to indicate if you want it XML or JSON.

If you have an object it's bad form to indicate in your method how you serialise it, it's much better to leave that to auto-negociation based on what the client accepts and what serialisers you have available, so that's actually better.

What if you have a public method in your resource that is not supposed to be exposed? It will get exposed.

Document the convention: public methods are endpoints, private methods are not. Use @Ignore or other documented annotation for the rare special-cases. In practice for JAX-RS resources there are never ever any public methods that are not endpoints.

In the end, you end up with dark magic and you don't really know what's going on. I think it's a step too far. Adding a couple annotations is not that bad in this case, especially annotations people are used to.

It's not dark magic if it's documented well and easy to understand with trivial rules. People love this in many frameworks, and that's the basis of Rails that was such a success.

Does RESTEasy support this? RESTEasy is probably using reflection and to make this work you would need some abstraction layer or SPI (such as AnnotatedType in CDI "full") that would allow you to replace the original annotations.

It does not need to support this if we can augment user resources as part of our build, which I'm working on.

By the way @Produces(MediaType.TEXT_PLAIN) can be applied to the resource class and assumed for all resource methods ;-)

It does not need to support this if we can augment user resources as part of our build, which I'm working on.

Like replacing the original class completely? That would be useful even for CDI/Arc.

Like replacing the original class completely? That would be useful even for CDI/Arc.

Yes it would be very useful for lots of things.

So, it turns out that this only matters for generating docs, like openapi.

Well, even if it was just for that... API auto documentation is very much appreciated nowadays.

In practice if your method answers to both GET and POST with the same action, taking the param from the query or the body, it doesn't matter one bit. Clients are still going to get served and your code is still going to work. It's a win-win.

Well, I don't agree with that. When I define an endpoint, I usually want it to be accessible one way and understand fully what's going on. And what if I have 2 parameters and someone sends a POST to my endpoint? Which will I take from the URL and which from the request body? It will send an error in this case?

If I want to test my REST end points fully, I will now have to test all the possible methods/inputs/outputs. At the moment, I test what I defined as the contract and I'm good to go.

Let's take a basic example: you have defined JSON annotations on your User class to avoid serializing the password, somebody uses XML, oh nice the password is there.

I think we open the Pandora box for a lot of corner cases. And it's going to be painful to document all of them. And painful for the users to understand all of them too.

I wrote REST end points in my previous life, and really, adding the annotations wasn't exactly what was an issue. It defines the contract, what is supposed to work, what I'm supposed to test. And it's important.

What you say about the behavior being well documented is IMHO flawed. Currently, I take a look at a JAX-RS example on the Internet and I have understood everything I need to know.

Having to read a specific documentation with weird corner cases and resolution algorithms won't be a step forward IMHO.

In the end, I think it's going to be more work for the users and make things very fragile.

@gsmet A couple of points.
It seems you are in the camp of people not liking RoR and the like. But bear in mind that there are people cabled like loose cannons :). For sure, regardless of this approach, we will keep supporting the strict JAX-RS model

The difficulty is to find a smooth transition path between this opinionated approach and vanilla JAX-RS. In particular, using JAX-RS annotation to enforce verbs, type conversion, path etc.

Yeah, I'm not at all saying we should get rid of regular JAX-RS. I'm saying we could relax it by adding sensible defaults, if you opt in to that kind of behaviour.

And opt-in could be explicit like a @o.j.resteasy.youareholdingitwrong.Controller on the resource

It seems you are in the camp of people not liking RoR and the like.

I'm all for simplifying things when we can but I just think there are too many corner cases to get this right. The @Post case is a good one. You just can't have any idea of what comes from the query parameters and what comes from the request body. You can still decide that your body will be in a body parameters but that's one more thing people will have to learn in the documentation.

Frankly, my testing argument is IMHO the most blocking one. You would have to test all the possible combinations to be sure your service is safe and that will be a nightmare if you combine the possible outputs + the possible GET/POST/whatever methods.

BTW, one other thing that comes to mind is that it will defeat the optimization we introduced to avoid registering all the RESTEasy providers. You need the @Consumes and @Produces annotations for that as you need to limit the scope to what you defined as possible output. That's 5 MB more in your native image as soon as XML is considered a possible output, which you will have to do if you want auto-negotiation.

All in all, I think you at least need to define the HTTP method and the input/output. It could be as simple as:

@GET(produces = JSON)

or:

@POST(consumes = JSON, produces = JSON)

And JSON could be the default value. So you would just have to define @GET if you want JSON-based services.

That solves most of the problems. There are still corner cases but at least your testing surface is limited. And this way, you would be able to also auto-document your services properly.

Would we want this to support XML? I think we should just have a very strict set of defaults, and if a user wants to move beyond that then they need to use annotations to make it explicit.

An example set of rules would be something like:

  • String or primitive return type = @Produces(text/plain)
  • Any other return type = @Produces(application/json)
  • String or primitive params = Request params from the URL, unless they are called 'requestEntity', in which case they are a representation of the request body.
  • InputStream or byte[] = request body
  • Any other param = JSON request body

If no param corresponds to the entity body then it is a GET request, otherwise it is a POST, unless the method name starts with get|put|post|delete.

These rules would need some tweaking, but the basic idea would be that you have a set of rules that directly map to JAX-RS annotations, that cover the 80% use case. If a user wants anything more they can just add annotations and gain more control.

In practice if you write a REST endpoint, all your methods take the same media types, so you can just put that @Consumes/@Produces annotation on the Application or package or wherever that works for all your methods because only madmen would put them on every single method.

But otherwise I agree with @stuartwdouglas that text and json are sensible defaults nowadays, and easy to override with annotations _somewhere_.

I'm +1 on Stuart's proposal as it defines the produce/consume somehow and doesn't rely on auto-negotiation.

I'm also +1 to only consider text/plain and application/json if there are no additional annotations.

The GET/POST thing looks mostly OK to me too.

Was this page helpful?
0 / 5 - 0 ratings