-
Notifications
You must be signed in to change notification settings - Fork 38.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rejection of @RequestMapping and @HttpExchange declarations on the same element prevents elegant designs. #32328
Comments
@NemesisMate thanks for the report. We put the assertion in place because I'm wondering if you have considered to use only Another option is to remove mapping annotations from the common contracts and re-declare methods, with the appropriate mapping annotations, in controller and client contracts. There is repetition in that, but the IDE and compiler should help help to keep them in sync, and it enforces common method signatures. |
Hi @rstoyanchev , thanks for your reply.
Can it not be done as per-design and therefore guaranteeing it instead of having it "just happen"?
At first, when I encountered the error after the update this is the first thing I tried to do but it became very messy. The issue comes when there is a second level of inheritance. Since you can't add both annotations in the first and second level, every new method that is introduced needs to be overridden by the final implementation.
This ends up being quite similar to the previous workaround since it requires the same duplication of interfaces/classes. In both cases I'd end up with the second image posted above which, can be achieved but it feels quite more cumbersome than what is possible by not adding this limitation. Basically, instead of having a single contract that can be implemented by server/client we need to have mirror contracts (IMO, quite less elegant as a design and also, even with the help of the IDE/compiler, prone to missing parts and harder to keep coherence). So, coming back to:
The fact of it being supported on server side suggests that it is to help having a single contract that can be implemented by both server & client but at the same time its limitations on server-side makes it unusable in many cases, cases in which now, instead of being able to use an annotation for each of the parts, we need to create separate contracts. Therefore, this "help" might end up being more of a burden than a help (in my case certainly is).
Ideally the |
I'd like to second the issue with my use case which is similar to the author's. In my setup, this change is a regression and I'd need to stuck with the previous Spring release for now. My typical project structure is:
In this configuration, interface AppApi {
@GetExchange("/resources/{resource_id}", accept = ["application/json"])
@GetMapping("/resources/{resource_id}", produces = ["application/json"])
fun getResource(
@PathVariable("resource_id", required = true)
resourceId: UUID,
): ResponseEntity<Resource>
} which is then implemented by @RestController
class AppApiController: AppsApi {
fun getResource(
resourceId: UUID,
): ResponseEntity<Resource> {
return ResponseEntity.ok(service.getResource(id = resourceId))
}
} and This way, API contract is defined neatly in a central place, neither duplication of code that can vary independently nor repetition of annotations occurs. Any other service in the monorepo that needs to communicate with
I guess this will work but certainly not as clean as pre-change look. It is also confusing for devs who come to see a controller and only see
I remember there were some GH issues requesting Actually, given it is supported by the server, I tried using |
@wingsofovnia, I was wondering at first whether you had noticed that we added support for
|
@NemesisMate, thank you for the feedback. You raise a good point that if we are to support Generally, we have not supported multiple mapping annotations on the same method. The question came up recently for In the mean time, you've only provided a very rough sketch of the actual scenario, and it's not enough to understand how mappings can vary between client and server while method signature remain the same. Seeing more concrete examples of controller methods and mappings would be really helpful. Ideally, if you can work out a small but realistic sample that would be great. |
Hi @rstoyanchev , a more concrete example could be as follows: Having a common API for all micro-services so they all follow certain structure: public interface CommonApi<DTO, ID> {
@GetMapping
@GetExchange
List<DTO> getAll();
@GetMapping("/{id}")
@GetExchange(value = "/{id}")
DTO get(@PathVariable("id") ID id);
} Creating an API definition for the @RequestMapping({"/book", "/books"})
@HttpExchange(url = "/book", contentType = MediaType.APPLICATION_PROTOBUF_VALUE, accept = MediaType.APPLICATION_PROTOBUF_VALUE)
public interface BookApi extends CommonApi<BookApi.BookDto, Integer> {
@GetMapping(params = "category")
@GetExchange
List<BookApi.BookDto> getAllByCategory(@RequestParam(value = "category") String category);
record BookDto(int id) { }
} A server module: @RestController
public class BookController implements BookApi {
@Override
public List<BookDto> getAllByCategory(String category) {return null;}
@Override
public List<BookDto> getAll() {return null;}
@Override
public BookDto get(Integer integer) {return null;}
} And the client (which would be an auto-configuration library) one so other applications can use it if proceeds: public interface BookClient extends BookApi {
} Now, the "store" micro-service would use the api module to code its functionality (gradle's With this setup, creating a "centralized" project to de-distribute the applications and run them all as a monolith would be as simple as importing both projects store and book without their client modules and all would glue together just as if the client/server technology was not there in the first place (which is part of the point of these annotations IMO, to not pollute the code logic itself). As it stands in 6.1.3, there are still some caveats to workaround (like duplicating values here and there) but it is by far easier to work with than 6.1.4 (the one introducing the breaking change that doesn't allow this kind of intuitive designs anymore). |
I think there is room to simplify, but it might be just me not fully seeing all the details yet. Rather than responding in snippets, could you turn this into a small Github project that can actually run and makes it possible to try out changes. That would help to further clarify the scenario and limitations. |
@rstoyanchev , I've created a small project dedicated to this issue: https://github.com/NemesisMate/spring-framework-issue-32328. Although it is a much simpler design than the one I'm working with, I believe it is complex enough to test the problems commented in this issue, which you can trigger by upping |
@rstoyanchev , since it looks like this will take some time to reach a conclusion, what do you think about "unbreaking" it meanwhile so at least existing code using both can benefit of general upgrades? (I believe it would be enough by just replacing the recently added exception by a warning). |
@NemesisMate, we need to make progress under #32043 first and the broader topic of what it would look like to support multiple mapping annotations on the same controller method. We don't intend to remove the checks in the mean time, and although it was possible to do before, it's not something that was intentional. |
Thanks for providing the sample. Am I correct in assuming that running I experimented with leaving only So the sample is a great start, but it does not yet fully demonstrate the challenge. Could you comment or better yet continue to update the app to provide more detail. |
Thanks to you for the support, @rstoyanchev
Wouldn't a breaking change like this (even if it wasn't intended, it might have been broadly used and it breaks in a way that it has no easy fixing when a project reaches a certain size) be left for a minor/major (as in semver's) release?
Both
The challenge appears when you upgrade from I believe that If you try and upgrade the version, you'll realize the workarounds that are needed at code-level to ensure the tests still work (which can be huge changes for big sized projects). And what is worse is, you might have a big project, simple, with the new versions without using both annotations in the same classes as long as you don't need advanced variances (e.g., different paths in the controller) but if all of a sudden you need it, you have to redesign everything. |
I realize I forgot to share my branch: It is upgraded to 3.2.5. Both tests pass.
The timing wasn't great, but it caught us by surprise. We did not expect this type of usage, and we don't want to have to break even more applications later. As I mentioned, making use case more clear is the best way to advance this. |
It's a quite common usage I believe, which was already promoted by Open Feign.
'Relatively' being the key word here ;). But as relatively new is my current project (which, in the light of
I guess that I have to settle with being one of the few then :'(
From your changes, I can say that it is already not ideal since now the request mappings in the controller implementation (which come from the common API) need to be defined in every service implementing the common api (a change in the common, requires tens of APIs having to tweak their annotations when they could be in a single place). The fact that some annotations (like RequestBody and everything else but the "mappings/exchanges" ones are supported doesn't feel totally right. Looking at it now, I'm thinking that maybe I was trying too hard to keep the annotations centralized (because of the reason mentioned above) and that's why I came up with the "can only be made by changing the code" solution, that was my bad. Still, it would be great to avoid all the redundancy required and the quirky integration between these annotations and get them working in a seamless way.
Let me find some time and add more to it so it gets clearer, but the changes you did are the main of it I believe, it just doesn't look that bad in a small project, but it adds up on bigger ones (think tens of micro-services, with more complex annotation configurations, params, paths, etc). |
In common-lib I did nothing more than drop In |
I've added more "items" (implementations of the common APIs) to better see the cumbersomeness of the current approach. For my surprise, it is not as bad as I thought it was. It is true the project I'm working for is much bigger and with much more customizations but still, I've been thinking wrongly about something or I can't recall the real pain points, which I guess I'll see again when I retry to upgrade everything to the "correct" approach. Still, this is not to say that it is ideal. There are still a bit of annoyances since An outstanding issue (and that is really annoying) is the fact that the |
Caused by #32065
Upgrading Spring version I Just stumbled upon an issue caused from removing the support to have both type of annotations in a single element.
Allowing both annotations provides more flexibility on simpler designs since the API can be defined in a single interface and ensure both controller and clients work by implementing the same root API. It also simplifies quite much when there is inheritance and the need to use
@RequestMapping
unique features (like multiple URLs or theparams
condition).As of this change, where I have a generic interface defining a common API, which is inherited by a domain-based API and finally implemented by a controller and its respective client, I would need to separate from the root API (most generic one) since there are methods that have unique requirements on the
@RequestMapping
declarations (includingGet
,Post
and the other method variants) and that can't be set with@HttpExchange
(and therefore can't opt to use that one also for Controllers).This workaround is less natural and becomes messy very fast.
Would it be possible to review this decision and, maybe, revert it?
Design allowing
@RequestMapping
and@HttpExchange
based declarations on the same elementDesign to achieve the same by rejecting
@RequestMapping
and@HttpExchange
based declarations on the same elementThe text was updated successfully, but these errors were encountered: