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.
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.
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;
}
}
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.
You can use the #RestController instead of #Controller annotation.
There are 2 annotations in Spring: #RequestBody and #ResponseBody. These annotations consumes, respectively produces JSONs. Some more info here.
Related
I have the following:
#Value("${apiVersion")
private String apiVersion;
#RequestMapping(value = "/{apiVersion}/service/call", method = RequestMethod.POST)
And I expected the URL to be:
/apiVersion/service/call
But it turns out {foo} accept any value, it doesn't actually use the String.
Is there a way for me to use the String value as part of the URL?
EDIT
The issue is that I have multiple calls that us that value.
#RequestMapping(value = apiVersion + "/call1", method = RequestMethod.POST)
#RequestMapping(value = apiVersion + "/call2", method = RequestMethod.POST)
#RequestMapping(value = apiVersion + "/call3", method = RequestMethod.POST)
etc.
Technically I can declare constants for each one like you suggested, but it doesn't sound optimal. If there is no way to do it then it is fine, I was just wondering if there is.
SOLUTION
Adding general mapping to the controller.
#RequestMapping("${apiVersion}")
If you want to apply it for all methods in a controller declare it on the controller class level:
#RestController
#RequestMapping("/test")
public class MyController { ...
and you do not need to prepend it before method path.
Otherwise it should be constant so like:
private static final String FOO = "test";
and prepend it before method path like:
FOO + "/service/call"
If you just want to predefine the path in Java just do
#RequestMapping(value = foo + "/service/call", method = RequestMethod.POST)
PathVariables in SpringMvc are meant to be a placeholder for endpoints like in the following
#GetMapping(value = "/books/{id}")
public String displayBook(#PathVariable id) { ... }
I am using Swagger version 2.4.0 and Spring Boot 2.0.4.RELEASE and have an application with several API endpoints and with default Swagger configuration having default produces header value set to application/json.
SwaggerConfig.java
#Configuration
#EnableSwagger2
public class SwaggerConfig {
private static final Set<String> DEFAULT_PRODUCES_AND_CONSUMES = ImmutableSet.of(
"application/json"
);
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.produces(DEFAULT_PRODUCES_AND_CONSUMES)
.consumes(DEFAULT_PRODUCES_AND_CONSUMES);
}
}
And with API endpoint set up being.
ApiEndpoint.java
#Consumes(MediaType.APPLICATION_JSON)
#Api(value = "/transform", protocols = "http, https")
#RequestMapping(value = "/transform")
public interface ApiEndpoint {
#POST
#RequestMapping(value = "/text", method = RequestMethod.POST)
#ApiOperation(value = "Transform to plain text", produces = MediaType.TEXT_PLAIN)
#CrossOrigin
String transformToText(#RequestBody TransformRequest transformRequest) throws Exception;
}
Now I want this endpoint to produce response with content type being only plain text, but SwaggerConfig adds application/json option as default one. So in order to correctly use this endpoint I would need to change Response Content Type from application/json to text/plain every time, which would get annoying pretty quick considering that this endpoint is used for testing. Is there a way to override SwaggerConfig or to add a parameter so text/plain is the only option or at least to set the text/plain as the default option only for this one endpoint?
you just have to define the response content type in your requestMapping annotation.
That is,
#RequestMapping(value = "/text", method = RequestMethod.POST)
will be replaced by,
#RequestMapping(value = "/text", method = RequestMethod.POST, produces="text/plain")
Means, you have to define in requestMapping that what type of content this mapping will going to return.
Note : Will be good practice if you use
#PostMapping(value = "/text", produces="text/plain")
Hope, the solution will work fine.
just specifying type in order in the endpoint annotation, example:
atribute produces receive an array, so you can put more than one type
#PostMapping(value = "/text", produces = { MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_VALUE })
I advise removing produces in #Bean, as its API contains endpoint that did not always follow the standard idea of "application/json"
Suppose I have the following method:
/** Account Types */
#ResponseBody
#Secured("ROLE_AccountTypes")
#RequestMapping(value = "/accountTypes", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public List<T> getAccountTypes() {
return accountSrv.getAccountTypes();
}
Now I want to put the comment of the method in a custom annotation programmatically. How is that possible?
I have the following combined custom annotation with Springs #PreAuthorize annotation,
#RequestMapping(
produces = MimeTypeUtils.APPLICATION_JSON_VALUE + ";charset=UTF-8",
method = RequestMethod.GET)
#ResponseBody
#PreAuthorize(value = "hasRole('permitAll()')")
#ResponseStatus(HttpStatus.OK)
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface Get {
#AliasFor(annotation = RequestMapping.class, attribute = "value")
String[] value() default {};
#AliasFor(annotation = PreAuthorize.class, attribute = "value")
String authorize() default "permitAll()";
}
And I've got the following client using it
#Get(value = "/users", authorize = "hasRole('ROLE_GET_USERS')")
public List<User> retrieveUsers() {
// body
}
As you can see the purpose is to allow clients of #Get annotation to override the #PreAuthorize so that they can provide the role they require for.
I din't have so far any problem using #AliasFor, even in this example #RequestMapping is working, but unfortunately it does not override the value of #PreAuthorize and everyone can still access the resources as the default value is permitAll().
I wonder first of all why this does not work, and second if it is possible to make it work?
How come this code just works? I didn't specify any custom converter or annotation (like #RequestBody or #ModelAttribute) before argument ? Request is filled correctly from this GET call:
http://localhost:8080/WS/foo?token=C124EBD7-D9A5-4E21-9C0F-3402A1EE5E9B&lastSync=2001-01-01T00:00:00&pageNo=1
Code:
#RestController
#RequestMapping(value = "/foo")
public class FooController {
#RequestMapping(method = RequestMethod.GET)
public Result<Foo> excursions(Request request) {
// ...
}
}
Request is just POJO with getters and setters. I use it to shorten argument code because plenty methods uses those same arguments ...
public class Request {
private String token;
#DateTimeFormat(pattern = IsoDateTime.DATETIME)
private Date lastSync;
private Integer pageNo;
// getters and setters
}
This was my original method before introducing Request.
#RestController
#RequestMapping(value = "/foo")
public class FooController {
#RequestMapping(method = RequestMethod.GET)
public Result<Foo> excursions(#RequestParam String token, #RequestParam #DateTimeFormat(pattern = IsoDateTime.DATETIME) Date lastSync, #RequestParam Integer pageNo) {
// ...
}
}
Request parameters will be mapped to POJOs, as it is happening in your case, by default. Additionally, if you use #ModelAttribute, an attribute in the Model will be created. That attribute can be then used in views, e.g. JSPs, to access the object.
#RequestBody annotation tells that the body of the request is NOT a set of form parameters like
token=C124EBD7-D9A5-4E21-9C0F-3402A1EE5E9B&lastSync=2001-01-01T00:00:00&pageNo=1
but is in some other format, such as JSON.
This is a feature provided by Spring MVC:
Customizable binding and validation. Type mismatches as application-level validation errors that keep the offending value, localized date and number binding, and so on instead of String-only form objects with manual parsing and conversion to business objects.
You can see it in the doc: http://docs.spring.io/spring/docs/4.2.x/spring-framework-reference/htmlsingle/