Spring RequestMapping for controllers that produce and consume JSON
Asked Answered
C

5

55

With multiple Spring controllers that consume and produce application/json, my code is littered with long annotations like:

    @RequestMapping(value = "/foo", method = RequestMethod.POST,
            consumes = MediaType.APPLICATION_JSON_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE)

Is there a way to produce a "composite/inherited/aggregated" annotation with default values for consumes and produces, such that I could instead write something like:

    @JSONRequestMapping(value = "/foo", method = RequestMethod.POST)

How do we define something like @JSONRequestMapping above? Notice the value and method passed in just like in @RequestMapping, also good to be able to pass in consumes or produces if the default isn't suitable.

I need to control what I'm returning. I want the produces/consumes annotation-methods so that I get the appropriate Content-Type headers.

Chatoyant answered 1/2, 2016 at 5:56 Comment(0)
I
80

As of Spring 4.2.x, you can create custom mapping annotations, using @RequestMapping as a meta-annotation. So:

Is there a way to produce a "composite/inherited/aggregated" annotation with default values for consumes and produces, such that I could instead write something like:

@JSONRequestMapping(value = "/foo", method = RequestMethod.POST)

Yes, there is such a way. You can create a meta annotation like following:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@RequestMapping(consumes = "application/json", produces = "application/json")
public @interface JsonRequestMapping {
    @AliasFor(annotation = RequestMapping.class, attribute = "value")
    String[] value() default {};

    @AliasFor(annotation = RequestMapping.class, attribute = "method")
    RequestMethod[] method() default {};

    @AliasFor(annotation = RequestMapping.class, attribute = "params")
    String[] params() default {};

    @AliasFor(annotation = RequestMapping.class, attribute = "headers")
    String[] headers() default {};

    @AliasFor(annotation = RequestMapping.class, attribute = "consumes")
    String[] consumes() default {};

    @AliasFor(annotation = RequestMapping.class, attribute = "produces")
    String[] produces() default {};
}

Then you can use the default settings or even override them as you want:

@JsonRequestMapping(method = POST)
public String defaultSettings() {
    return "Default settings";
}

@JsonRequestMapping(value = "/override", method = PUT, produces = "text/plain")
public String overrideSome(@RequestBody String json) {
    return json;
}

You can read more about AliasFor in spring's javadoc and github wiki.

Introgression answered 4/2, 2016 at 7:31 Comment(3)
This is really interesting; didn't know about this new feature. The JavaDoc for @AliasFor is really good. And there is also a wiki page describing in more detail about Spring's annotation model.Grof
Thank you! This looks like the information I was looking for. In public @interface JsonRequestMapping , is it neccessary to redeclare name, value, path, method, params, headers? I'm only interested in having defaults for consumes, produces.Chatoyant
Yes, if you want to override their corresponding value in RequestMapping, you should re-declare them.Introgression
G
41

The simple answer to your question is that there is no Annotation-Inheritance in Java. However, there is a way to use the Spring annotations in a way that I think will help solve your problem.

@RequestMapping is supported at both the type level and at the method level.

When you put @RequestMapping at the type level, most of the attributes are 'inherited' for each method in that class. This is mentioned in the Spring reference documentation. Look at the api docs for details on how each attribute is handled when adding @RequestMapping to a type. I've summarized this for each attribute below:

  • name: Value at Type level is concatenated with value at method level using '#' as a separator.
  • value: Value at Type level is inherited by method.
  • path: Value at Type level is inherited by method.
  • method: Value at Type level is inherited by method.
  • params: Value at Type level is inherited by method.
  • headers: Value at Type level is inherited by method.
  • consumes: Value at Type level is overridden by method.
  • produces: Value at Type level is overridden by method.

Here is a brief example Controller that showcases how you could use this:

package com.example;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(path = "/", 
        consumes = MediaType.APPLICATION_JSON_VALUE, 
        produces = MediaType.APPLICATION_JSON_VALUE, 
        method = {RequestMethod.GET, RequestMethod.POST})
public class JsonProducingEndpoint {

    private FooService fooService;

    @RequestMapping(path = "/foo", method = RequestMethod.POST)
    public String postAFoo(@RequestBody ThisIsAFoo theFoo) {
        fooService.saveTheFoo(theFoo);
        return "http://myservice.com/foo/1";
    }

    @RequestMapping(path = "/foo/{id}", method = RequestMethod.GET)
    public ThisIsAFoo getAFoo(@PathVariable String id) {
        ThisIsAFoo foo = fooService.getAFoo(id);
        return foo;
    }

    @RequestMapping(path = "/foo/{id}", produces = MediaType.APPLICATION_XML_VALUE, method = RequestMethod.GET)
    public ThisIsAFooXML getAFooXml(@PathVariable String id) {
        ThisIsAFooXML foo = fooService.getAFoo(id);
        return foo;
    }
}
Grof answered 3/2, 2016 at 17:9 Comment(3)
Technically, Ali Dehghani's answer best answered the question I asked, so I've marked that as accepted. But I ended up using your suggestion, seemed cleaner in some sense, so I'm giving you a bounty too, (+100, since I can't do +50 twice). Thanks.Chatoyant
Question: I have exactly the same scenario as you, but a method returning a string cannot be accepted (415) because it is not JSON. You don't have this problem? The objects are deserialized correctly because we have Jackson and @Jsonproperty on the fields of the model.Quintie
@Quintie not sure I follow. The java method should return a serializable Object, not a String. In my example only the POST method is returning a String because it is returning the location of the created resource. Might be worth opening your own question.Grof
H
20

You shouldn't need to configure the consumes or produces attribute at all. Spring will automatically serve JSON based on the following factors.

  • The accepts header of the request is application/json
  • @ResponseBody annotated method
  • Jackson library on classpath

You should also follow Wim's suggestion and define your controller with the @RestController annotation. This will save you from annotating each request method with @ResponseBody

Another benefit of this approach would be if a client wants XML instead of JSON, they would get it. They would just need to specify xml in the accepts header.

Highmuckamuck answered 3/2, 2016 at 17:22 Comment(1)
Perfect and Absolute match for me. I even removed the consumes and producesRetread
P
10

You can use the @RestController instead of @Controller annotation.

Pollinosis answered 3/2, 2016 at 16:10 Comment(1)
Actually it is juct combination of @Controller and @ResponseBody, so it have nothing in common with Content TypeSuccedaneum
D
2

There are 2 annotations in Spring: @RequestBody and @ResponseBody. These annotations consumes, respectively produces JSONs. Some more info here.

Doralynn answered 3/2, 2016 at 16:2 Comment(1)
That is interesting information, but not exactly what I'm looking for. I need to control what I'm returning. I want the produces/consumes annotation-methods so that I get the appropriate Content-Type headers.Chatoyant

© 2022 - 2024 — McMap. All rights reserved.