I have a spring REST controller which returns the following JSON payload:
[
{
"id": 5920,
"title": "a title"
},
{
"id": 5926,
"title": "another title",
}
]
The REST controller with its corresponding get request method:
#RequestMapping(value = "example")
public Iterable<Souvenir> souvenirs(#PathVariable("user") String user) {
return new souvenirRepository.findByUserUsernameOrderById(user);
}
Now the Souvenir class is a pojo:
#Entity
#Data
public class Souvenir {
#Id
#GeneratedValue
private long id;
private String title;
private Date date;
}
Regarding https://www.owasp.org/index.php/OWASP_AJAX_Security_Guidelines#Always_return_JSON_with_an_Object_on_the_outside and http://haacked.com/archive/2009/06/25/json-hijacking.aspx/ I would like to wrap the response within an object so that it is not vulnerable to attacks. Of course I could do something like this:
#RequestMapping(value = "example")
public SouvenirWrapper souvenirs(#PathVariable("user") String user) {
return new SouvenirWrapper(souvenirRepository.findByUserUsernameOrderById(user));
}
#Data
class SouvenirWrapper {
private final List<Souvenir> souvenirs;
public SouvenirWrapper(List<Souvenir> souvenirs) {
this.souvenirs = souvenirs;
}
}
This results in the following JSON payload:
{
"souvenirs": [
{
"id": 5920,
"title": "a title"
},
{
"id": 5926,
"title": "another title",
}
]
}
This helps in preventing some JSON/Javascript attacks but I don't like the verbosity of the Wrapper class. I could of course generalize the above approach with generics. Is there another way to achieve the same result in the Spring ecosystem (with an annotation or something similar)? An idea would be that the behaviour is done by Spring automatically, so whenever there is a REST controller that returns a list of objects, it could wrap those objects within an object wrapper so that no direct list of objects get serialized?
I ended up with the following solution (thanks to #vadim-kirilchuk):
My controller still looks exactly as before:
#RequestMapping(value = "example")
public Iterable<Souvenir> souvenirs(#PathVariable("user") String user) {
return new souvenirRepository.findByUserUsernameOrderById(user);
}
I added the following implementation of ResponseBodyAdvice which basically gets executed when a controller in the referenced package tries to respond to a client call (to my understanding):
#ControllerAdvice(basePackages = "package.where.all.my.controllers.are")
public class JSONResponseWrapper implements ResponseBodyAdvice {
#Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
#Override
#SuppressWarnings("unchecked")
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof List) {
return new Wrapper<>((List<Object>) body);
}
return body;
}
#Data // just the lombok annotation which provides getter and setter
private class Wrapper<T> {
private final List<T> list;
public Wrapper(List<T> list) {
this.list = list;
}
}
}
So with this approach I can keep my existing method signature in my controller (public Iterable<Souvenir> souvenirs(#PathVariable("user") String user)) and future controllers don't have to worry about wrapping its Iterables within such a wrapper because the framework does this part of the work.
Based in your solution I ended up with a more flexible option. First I created an annotation to activate the behaviour whenever I want and with a customizable wrapper attribute name:
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Target({ElementType.TYPE, ElementType.METHOD})
public #interface JsonListWrapper {
String name() default "list";
}
This annotation can be used on the entity class so it's applied to all controllers responses of List<MyEntity> or can be used for specifics controller methods.
The ControllerAdvice will look like this (note that I return a Map<Object> to dynamically set the wrapper name as a map key).
public class WebResponseModifierAdvice implements ResponseBodyAdvice<Object> {
#Override
public boolean supports(final MethodParameter returnType, final Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public Object beforeBodyWrite(final Object body,
final MethodParameter returnType,
final MediaType selectedContentType,
final Class<? extends HttpMessageConverter<?>> selectedConverterType,
final ServerHttpRequest request,
final ServerHttpResponse response) {
if (body instanceof List && selectedContentType.isCompatibleWith(MediaType.APPLICATION_JSON)) {
return checkListWrapper(body, returnType);
} else {
return body;
}
}
/**
* Detects use of {#link JsonListWrapper} in a response like <tt>List<T></tt>
* in case it's necesary to wrap the answer.
*
* #param body body to be written in the response
* #param returnType controller method return type
* #return
*/
private Object checkListWrapper(final Object body,
final MethodParameter returnType) {
String wrapperName = null;
try {
// Checks class level annotation (List<C>).
String typeName = "";
String where = "";
String whereName = "";
// Gets generic type List<T>
Type[] actualTypeArgs = ((ParameterizedType) returnType.getGenericParameterType()).getActualTypeArguments();
if (actualTypeArgs.length > 0) {
Type listType = ((ParameterizedType) returnType.getGenericParameterType()).getActualTypeArguments()[0];
if (listType instanceof ParameterizedType) {
Type elementType = ((ParameterizedType) listType).getActualTypeArguments()[0];
elementType.getClass();
try {
typeName = elementType.getTypeName();
Class<?> clz = Class.forName(typeName);
JsonListWrapper classListWrapper = AnnotationUtils.findAnnotation(clz, JsonListWrapper.class);
if (classListWrapper != null) {
where = "clase";
whereName = typeName;
wrapperName = classListWrapper.name();
}
} catch (ClassNotFoundException e) {
log.error("Class not found" + elementType.getTypeName(), e);
}
}
}
// Checks method level annotations (prevails over class level)
JsonListWrapper methodListWrapper = AnnotationUtils.findAnnotation(returnType.getMethod(), JsonListWrapper.class);
if (methodListWrapper != null) {
where = "método";
whereName = returnType.getMethod().getDeclaringClass() + "." + returnType.getMethod().getName() + "()";
wrapperName = methodListWrapper.name();
}
if (wrapperName != null) {
if (log.isTraceEnabled()) {
log.trace("#JsonListWrapper detected {} {}. Wrapping List<{}> in \"{}\"", where, whereName, typeName, wrapperName);
}
final Map<String, Object> map = new HashMap<>(1);
map.put(wrapperName, body);
return map;
}
} catch(Exception ex) {
log.error("Error getting type of List in the response", ex);
}
return body;
}
}
This way you can use either:
#JsonListWrapper(name = "souvenirs")
public class Souvenir {
//...class members
}
...or
#JsonListWrapper(name = "souvenirs")
#RequestMapping(value = "example")
public ResponseEntity<List<Souvenir>> souvenirs(#PathVariable("user") String user) {
return new souvenirRepository.findByUserUsernameOrderById(user);
}
Related
Consider the below Controller method,
#GetMapping("/getdata")
public Data getDetails() {
try {
Data obj = template.getForObject("http://localhost:8090/details/get", Data.class);
return obj;
} catch (Exception e) {
.....
}
}
Let's say the response object for the above endpoint looks like,
data : {
name : "ABCD",
age : "20"
}
Now what I am trying to achieve is to Wrap the entire response object under another object, so the final response should look like,
{
status : "SOMETHING",
response : {
name : "ABCD",
age : "20"
},
extra : null
}
So the issue is, I don't want to create a function under each controller method to sent values to this Wrapper Object. My question is, Is there any possibility in Spring Framework that allows me to create a Global Wrapper function somewhere, and it will automatically pick it and wrap the response from the controller?
You will need to implement a ResponseBodyAdvice as a #ControllerAdvice. First, you need to have a model for your generic response.
public class GenericResponse<T> {
private String status;
private String extra;
private T response;
}
Then you need to implement the ResponseBodyAdvice itself:
#ControllerAdvice
public class CustomResponseBodyAdvice implements ResponseBodyAdvice<Object> {
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public Object beforeBodyWrite(Object response, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
GenericResponse<Object> genericResponse = new GenericResponse<>();
output.setStatus("SOMETHING"); // I guess that you need some logic here
output.setResponse(response);
return genericResponse;
}
}
In my spring rest application I need to read the headers information for PUT and POST calls and set those information in the bean passed as #RequestBody. currently what am doing is like follows.
#RequestMapping(value = "/create", method = RequestMethod.POST)
#ResponseStatus(value = HttpStatus.OK)
#ResponseBody
public ReportRepresentation createDailyReport(#RequestBody ReportEntity reportEntity,
#RequestHeader(value= "FIRST_HEAD1", required = false) boolean isHeaderSet,
#RequestHeader(value= "SECOND_HEAD2", required = false) Long scondHead) {
// Setting the header values into bean properties .
}
So am extracting the headers in all methods(POST and PUT) and setting values in different entities.
My question is is there any way to parse/ override the #RequestBody in method param in global level and set those headers?
You can use RequestBodyAdvice:
For example:
Bean:
#Data
public class MyBean {
private String property;
}
Controller:
#RestController
public class MyController {
#RequestMapping("/")
public MyBean get(#RequestBody MyBean myBean) {
return myBean;
}
}
Advisor:
#ControllerAdvice(annotations = RestController.class)
public class MyRequestBodyAdvisor extends RequestBodyAdviceAdapter {
#Override
public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return methodParameter.getParameterType() == MyBean.class;
}
#Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
MyBean myBean = (MyBean)body;
List<String> strings = inputMessage.getHeaders().get("X-Property");
myBean.setProperty(strings.get(0));
return myBean;
}
}
Testing:
$ curl localhost:8080 -d '{}' -X POST -H 'X-Property: myProp' -H 'Content-Type: application/json' -s
Output:
{"property":"myProp"}
I have implemented the same as #caco3 mentioned here is my implementation with set the values to bean.
#ControllerAdvice
public class RequestBodyAdviceChain implements RequestBodyAdvice {
.. Other methods
#Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
HttpHeaders headers = inputMessage.getHeaders();
List<String> emulated = headers.get("FIRST_HEAD1");
Boolean isEmulated = false;
Long emulatedUserId = null;
if (!CollectionUtils.isEmpty(emulated)) {
isEmulated = Boolean.valueOf(emulated.get(0));
}
if (isEmulated) {
List<String> users = headers.get("SECOND_HEAD2");
if (!CollectionUtils.isEmpty(users)) {
emulatedUserId = Long.valueOf(users.get(0));
}
}
if (isEmulated) {
if (setField(body, Is_Emulated_Field, isEmulated)) {
setField(body, EmulatedUserId_FIELD, emulatedUserId);
}
}
return body;
}
/**
* <p>
* Method to set the field value for the emulated user and it's id wven
* though if the fields are defined in the super class.
*/
private static boolean setField(Object targetObject, String fieldName, Object fieldValue) {
Field field;
try {
field = targetObject.getClass().getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
field = null;
}
Class superClass = targetObject.getClass().getSuperclass();
while (field == null && superClass != null) {
try {
field = superClass.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
superClass = superClass.getSuperclass();
}
}
if (field == null) {
return false;
}
field.setAccessible(true);
try {
field.set(targetObject, fieldValue);
return true;
} catch (IllegalAccessException e) {
return false;
}
}
}
I have the following #RestController
#RequestMapping(...)
public ResponseEntity(#RequestBody #Valid SomeDTO, BindingResult errors) {
//do something with errors if validation error occur
}
public class SomeDTO {
public SomeEnum someEnum;
}
If the JSON request is { "someEnum": "valid value" }, everything works fine. However, if the request is { "someEnum": "invalid value" }, it only return error code 400.
How can I trap this error so I can provide a custom error message, such as "someEnum must be of value A/B/C".
The answer provided by #Amit is good and works. You can go ahead with that if you want to deserialize an enum in a specific way. But that solution is not scalable. Because every enum which needs validation must be annotated with #JsonCreator.
Other answers won't help you beautify the error message.
So here's my solution generic to all the enums in spring web environment.
#RestControllerAdvice
public class ControllerErrorHandler extends ResponseEntityExceptionHandler {
public static final String BAD_REQUEST = "BAD_REQUEST";
#Override
public ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException exception,
HttpHeaders headers, HttpStatus status, WebRequest request) {
String genericMessage = "Unacceptable JSON " + exception.getMessage();
String errorDetails = genericMessage;
if (exception.getCause() instanceof InvalidFormatException) {
InvalidFormatException ifx = (InvalidFormatException) exception.getCause();
if (ifx.getTargetType()!=null && ifx.getTargetType().isEnum()) {
errorDetails = String.format("Invalid enum value: '%s' for the field: '%s'. The value must be one of: %s.",
ifx.getValue(), ifx.getPath().get(ifx.getPath().size()-1).getFieldName(), Arrays.toString(ifx.getTargetType().getEnumConstants()));
}
}
ErrorResponse errorResponse = new ErrorResponse();
errorResponse.setTitle(BAD_REQUEST);
errorResponse.setDetail(errorDetails);
return handleExceptionInternal(exception, errorResponse, headers, HttpStatus.BAD_REQUEST, request);
}
}
This will handle all the invalid enum values of all types and provides a better error message for the end user.
Sample output:
{
"title": "BAD_REQUEST",
"detail": "Invalid enum value: 'INTERNET_BANKING' for the field: 'paymentType'. The value must be one of: [DEBIT, CREDIT]."
}
#ControllerAdvice
public static class GenericExceptionHandlers extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException e, HttpHeaders headers, HttpStatus status, WebRequest request) {
return new ResponseEntity<>(new ErrorDTO().setError(e.getMessage()), HttpStatus.BAD_REQUEST);
}
}
I created a fully functional Spring boot Application with a Test on Bitbucket
You do not need #Valid for enum validation, you can achieve the required response using below code:
Controller Code, StackDTO has an enum PaymentType in it:
#RequestMapping(value = "/reviews", method = RequestMethod.POST)
#ResponseBody
public ResponseEntity<String> add(#RequestBody StackDTO review) {
return new ResponseEntity<String>(HttpStatus.ACCEPTED);
}
Create an exception class, as EnumValidationException
public class EnumValidationException extends Exception {
private String enumValue = null;
private String enumName = null;
public String getEnumValue() {
return enumValue;
}
public void setEnumValue(String enumValue) {
this.enumValue = enumValue;
}
public String getEnumName() {
return enumName;
}
public void setEnumName(String enumName) {
this.enumName = enumName;
}
public EnumValidationException(String enumValue, String enumName) {
super(enumValue);
this.enumValue = enumValue;
this.enumName = enumName;
}
public EnumValidationException(String enumValue, String enumName, Throwable cause) {
super(enumValue, cause);
this.enumValue = enumValue;
this.enumName = enumName;
}
}
I have enum as below, with a special annotation #JsonCreator on a method create
public enum PaymentType {
CREDIT("Credit"), DEBIT("Debit");
private final String type;
PaymentType(String type) {
this.type = type;
}
String getType() {
return type;
}
#Override
public String toString() {
return type;
}
#JsonCreator
public static PaymentType create (String value) throws EnumValidationException {
if(value == null) {
throw new EnumValidationException(value, "PaymentType");
}
for(PaymentType v : values()) {
if(value.equals(v.getType())) {
return v;
}
}
throw new EnumValidationException(value, "PaymentType");
}
}
Finally RestErrorHandler class,
#ControllerAdvice
public class RestErrorHandler {
#ExceptionHandler(HttpMessageNotReadableException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
public ResponseEntity<ValidationErrorDTO> processValidationIllegalError(HttpMessageNotReadableException ex,
HandlerMethod handlerMethod, WebRequest webRequest) {
EnumValidationException exception = (EnumValidationException) ex.getMostSpecificCause();
ValidationErrorDTO errorDTO = new ValidationErrorDTO();
errorDTO.setEnumName(exception.getEnumName());
errorDTO.setEnumValue(exception.getEnumValue());
errorDTO.setErrorMessage(exception.getEnumValue() + " is an invalid " + exception.getEnumName());
return new ResponseEntity<ValidationErrorDTO>(errorDTO, HttpStatus.BAD_REQUEST);
}
}
ValidationErrorDTO is the dto with setters/getters of enumValue, enumName and errorMessage. Now when you send POST call to controller endpoint /reviews with below request
{"paymentType":"Credit2"}
Then code returns response as 400 with below response body -
{
"enumValue": "Credit2",
"enumName": "PaymentType",
"errorMessage": "Credit2 is an invalid PaymentType"
}
Let me know if it resolves your issue.
Yon can achieve this using #ControllerAdvice as follows
#org.springframework.web.bind.annotation.ExceptionHandler(value = {InvalidFormatException.class})
public ResponseEntity handleIllegalArgumentException(InvalidFormatException exception) {
return ResponseEntity.badRequest().body(exception.getMessage());
}
Basically , the idea is to catch com.fasterxml.jackson.databind.exc.InvalidFormatException and handle it as per your requirement.
#Valid has to do with Hibernate bean validation. Currently enum type is not supported out of the box. I found this answer to be the closet, https://funofprograming.wordpress.com/2016/09/29/java-enum-validator/, the drawback however is that you have to make the enum field of type String instead.
I want to serialize nulls for a specific field or class.
In GSON, the option serializeNulls() applies to the whole JSON.
Example:
class MainClass {
public String id;
public String name;
public Test test;
}
class Test {
public String name;
public String value;
}
MainClass mainClass = new MainClass();
mainClass.id = "101"
// mainClass has no name.
Test test = new Test();
test.name = "testName";
test.value = null;
mainClass.test = test;
Creating JSON using GSON:
GsonBuilder builder = new GsonBuilder().serializeNulls();
Gson gson = builder.create();
System.out.println(gson.toJson(mainClass));
Current ouput:
{
"id": "101",
"name": null,
"test": {
"name": "testName",
"value": null
}
}
Desired output:
{
"id": "101",
"test": {
"name": "testName",
"value": null
}
}
How to achieve the desired output?
Preferred solution would have the following properties:
Do NOT serialize nulls by default,
Serialize nulls for fields with a specific annotation.
I have a solution similar to the one of Aleksey but that can be applied to one or more fields in any class (example in Kotlin):
Create a new annotation for fields that should be serialized as null:
#Retention(AnnotationRetention.RUNTIME)
#Target(AnnotationTarget.FIELD)
annotation class SerializeNull
Create a TypeAdapterFactory that checks if a class has fields annotated with this annotation and removes the fields that are null and not annotated with the annotation from the JsonTree when writing the object:
class SerializableAsNullConverter : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
fun Field.serializedName() = declaredAnnotations
.filterIsInstance<SerializedName>()
.firstOrNull()?.value ?: name
val declaredFields = type.rawType.declaredFields
val nullableFieldNames = declaredFields
.filter { it.declaredAnnotations.filterIsInstance<SerializeNull>().isNotEmpty() }
.map { it.serializedName() }
val nonNullableFields = declaredFields.map { it.serializedName() } - nullableFieldNames
return if (nullableFieldNames.isEmpty()) {
null
} else object : TypeAdapter<T>() {
private val delegateAdapter = gson.getDelegateAdapter(this#SerializableAsNullConverter, type)
private val elementAdapter = gson.getAdapter(JsonElement::class.java)
override fun write(writer: JsonWriter, value: T?) {
val jsonObject = delegateAdapter.toJsonTree(value).asJsonObject
nonNullableFields
.filter { jsonObject.get(it) is JsonNull }
.forEach { jsonObject.remove(it) }
val originalSerializeNulls = writer.serializeNulls
writer.serializeNulls = true
elementAdapter.write(writer, jsonObject)
writer.serializeNulls = originalSerializeNulls
}
override fun read(reader: JsonReader): T {
return delegateAdapter.read(reader)
}
}
}
}
Register the adapter with your Gson instance:
val builder = GsonBuilder().registerTypeAdapterFactory(SerializableAsNullConverter())
And annotate the fields you would like to be nullable:
class MyClass(val id: String?, #SerializeNull val name: String?)
Serialization result:
val myClass = MyClass(null, null)
val gson = builder.create()
val json = gson.toJson(myClass)
json:
{
"name": null
}
I have interface to check when object should be serialized as null:
public interface JsonNullable {
boolean isJsonNull();
}
And the corresponding TypeAdapter (supports write only)
public class JsonNullableAdapter extends TypeAdapter<JsonNullable> {
final TypeAdapter<JsonElement> elementAdapter = new Gson().getAdapter(JsonElement.class);
final TypeAdapter<Object> objectAdapter = new Gson().getAdapter(Object.class);
#Override
public void write(JsonWriter out, JsonNullable value) throws IOException {
if (value == null || value.isJsonNull()) {
//if the writer was not allowed to write null values
//do it only for this field
if (!out.getSerializeNulls()) {
out.setSerializeNulls(true);
out.nullValue();
out.setSerializeNulls(false);
} else {
out.nullValue();
}
} else {
JsonElement tree = objectAdapter.toJsonTree(value);
elementAdapter.write(out, tree);
}
}
#Override
public JsonNullable read(JsonReader in) throws IOException {
return null;
}
}
Use it as follows:
public class Foo implements JsonNullable {
#Override
public boolean isJsonNull() {
// You decide
}
}
In the class where Foo value should be serialized as null. Note that foo value itself must be not null, otherwise custom adapter annotation will be ignored.
public class Bar {
#JsonAdapter(JsonNullableAdapter.class)
public Foo foo = new Foo();
}
For those looking for a Java version of #Joris's excellent answer, the below code should do the trick. It's largely just a translation of the Kotlin, with a minor improvement to how the serialized name of the attribute is fetched to ensure it always works when the serialized name is different than the attribute name (see the comments on the original answer).
This is the TypeAdapterFactory implementation:
public class NullableAdapterFactory implements TypeAdapterFactory {
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Field[] declaredFields = type.getRawType().getDeclaredFields();
List<String> nullableFieldNames = new ArrayList<>();
List<String> nonNullableFieldNames = new ArrayList<>();
for (Field declaredField : declaredFields) {
if (declaredField.isAnnotationPresent(JsonNullable.class)) {
if (declaredField.getAnnotation(SerializedName.class) != null) {
nullableFieldNames.add(declaredField.getAnnotation(SerializedName.class).value());
} else {
nullableFieldNames.add(declaredField.getName());
}
} else {
if (declaredField.getAnnotation(SerializedName.class) != null) {
nonNullableFieldNames.add(declaredField.getAnnotation(SerializedName.class).value());
} else {
nonNullableFieldNames.add(declaredField.getName());
}
}
}
if (nullableFieldNames.size() == 0) {
return null;
}
TypeAdapter<T> delegateAdapter = gson.getDelegateAdapter(NullableAdapterFactory.this, type);
TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);
return new TypeAdapter<T>() {
#Override
public void write(JsonWriter out, T value) throws IOException {
JsonObject jsonObject = delegateAdapter.toJsonTree(value).getAsJsonObject();
for (String name: nonNullableFieldNames) {
if (jsonObject.has(name) && jsonObject.get(name) instanceof JsonNull) {
jsonObject.remove(name);
}
}
boolean originalSerializeNulls = out.getSerializeNulls();
out.setSerializeNulls(true);
elementAdapter.write(out, jsonObject);
out.setSerializeNulls(originalSerializeNulls);
}
#Override
public T read(JsonReader in) throws IOException {
return delegateAdapter.read(in);
}
};
}
}
And this is the #JsonNullable annotation to mark the target attributes:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
public #interface JsonNullable {
}
I implemented it as an #JsonAdapter(NullableAdapterFactory.class) annotation on the object class, rather registering it as a TypeAdapterFactory on the GsonBuilder instance, so my object classes looked a bit like this:
#JsonAdapter(NullableAdapterFactory.class)
public class Person {
public String firstName;
public String lastName;
#JsonNullable
public String someNullableInfo;
}
However, the other approach should work just as well with this code if preferred.
Create subclass of com.google.gson.TypeAdapter and register it for required field using annotation com.google.gson.annotations.JsonAdapter. Or register it using GsonBuilder.registerTypeAdapter. In that adapter write (and read) should be implemented. For example:
public class JsonTestNullableAdapter extends TypeAdapter<Test> {
#Override
public void write(JsonWriter out, Test value) throws IOException {
out.beginObject();
out.name("name");
out.value(value.name);
out.name("value");
if (value.value == null) {
out.setSerializeNulls(true);
out.nullValue();
out.setSerializeNulls(false);
} else {
out.value(value.value);
}
out.endObject();
}
#Override
public Test read(JsonReader in) throws IOException {
in.beginObject();
Test result = new Test();
in.nextName();
if (in.peek() != NULL) {
result.name = in.nextString();
} else {
in.nextNull();
}
in.nextName();
if (in.peek() != NULL) {
result.value = in.nextString();
} else {
in.nextNull();
}
in.endObject();
return result;
}
}
in MainClass add JsonAdapter annotation with the adapter to Test class field:
public static class MClass {
public String id;
public String name;
#JsonAdapter(JsonTestNullableAdapter.class)
public Test test;
}
the output of System.out.println(new Gson.toJson(mainClass)) is:
{
"id": "101",
"test": {
"name": "testName",
"value": null
}
}
I took a few ideas from various answers here.
this implementation:
lets you choose at runtime, whether the JSON is
null
happens when JsonNullable.isJsonNull() == true
not null
happens when JsonNullable.isJsonNull() == false
omitted from the JSON (useful for HTTP PATCH requests)
happens field in Parent containing JsonNullable is null
no annotations needed
properly delegates unhandled work to a delegateAdapter by using a TypeAdapterFactory
objects that may need to be serialized to null implement this interface
/**
* [JsonNullableTypeAdapterFactory] needs to be registered with the [com.google.gson.Gson]
* serializing implementations of [JsonNullable] for [JsonNullable] to work.
*
* [JsonNullable] allows objects to choose at runtime whether they should be serialized as "null"
* serialized normally, or be omitted from the JSON output from [com.google.gson.Gson].
*
* when [isJsonNull] returns true, the subclass will be serialized to a [com.google.gson.JsonNull].
*
* when [isJsonNull] returns false, the subclass will be serialized normally.
*/
interface JsonNullable {
/**
* return true to have the entire object serialized as `null` during JSON serialization.
* return false to have this object serialized normally.
*/
fun isJsonNull(): Boolean
}
type adapter factory that serializes values to null
class JsonNullableTypeAdapterFactory : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
return object : TypeAdapter<T>() {
private val delegateAdapter = gson.getDelegateAdapter(this#JsonNullableTypeAdapterFactory, type)
override fun read(reader: JsonReader): T = delegateAdapter.read(reader)
override fun write(writer: JsonWriter, value: T?) {
if (value is JsonNullable && value.isJsonNull()) {
val originalSerializeNulls = writer.serializeNulls
writer.serializeNulls = true
writer.nullValue()
writer.serializeNulls = originalSerializeNulls
} else {
delegateAdapter.write(writer, value)
}
}
}
}
}
register thr type adapter factroy with GSON
new GsonBuilder()
// ....
.registerTypeAdapterFactory(new JsonNullableTypeAdapterFactory())
// ....
.create();
example object that gets serialized to JSON
data class Parent(
val hello: Child?,
val world: Child?
)
data class Child(
val name: String?
) : JsonNullable {
override fun isJsonNull(): Boolean = name == null
}
Adding to the answer given by #Arvoreniad
The two additions are resetting the null serialization state in the JsonWriter after setting to true for the output and to use the field naming policy from Gson for getting the field name.
public class SerializeNullTypeAdapterFactory implements TypeAdapterFactory {
/**
* {#inheritDoc}
*/
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Field[] declaredFields = type.getRawType().getDeclaredFields();
List<String> nullableFields = new ArrayList<>();
List<String> nonNullableFields = new ArrayList<>();
FieldNamingStrategy fieldNamingStrategy = gson.fieldNamingStrategy();
for (Field declaredField : declaredFields) {
// The Gson FieldNamingStrategy will handle the #SerializedName annotation + casing conversions
final String fieldName = fieldNamingStrategy.translateName(declaredField);
if (declaredField.isAnnotationPresent(JsonNullable.class)) {
nullableFields.add(fieldName);
} else {
nonNullableFields.add(fieldName);
}
}
if (nullableFields.isEmpty()) {
return null;
}
TypeAdapter<T> delegateAdapter = gson.getDelegateAdapter(this, type);
TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);
return new TypeAdapter<T>() {
#Override
public void write(JsonWriter out, T value) throws IOException {
JsonObject jsonObject = delegateAdapter.toJsonTree(value).getAsJsonObject();
nonNullableFields.forEach((var name) -> {
if (jsonObject.has(name) && (jsonObject.get(name) instanceof JsonNull)) {
jsonObject.remove(name);
}
});
boolean serializeNulls = out.getSerializeNulls();
out.setSerializeNulls(true);
elementAdapter.write(out, jsonObject);
// Reset default (in case JsonWriter is reused)
out.setSerializeNulls(serializeNulls);
}
#Override
public T read(JsonReader in) throws IOException {
return delegateAdapter.read(in);
}
};
}
}
I have a command object:
public class Job {
private String jobType;
private String location;
}
Which is bound by spring-mvc:
#RequestMapping("/foo")
public String doSomethingWithJob(Job job) {
...
}
Which works fine for http://example.com/foo?jobType=permanent&location=Stockholm. But now I need to make it work for the following url instead:
http://example.com/foo?jt=permanent&loc=Stockholm
Obviously, I don't want to change my command object, because the field names have to remain long (as they are used in the code). How can I customize that? Is there an option to do something like this:
public class Job {
#RequestParam("jt")
private String jobType;
#RequestParam("loc")
private String location;
}
This doesn't work (#RequestParam can't be applied to fields).
The thing I'm thinking about is a custom message converter similar to FormHttpMessageConverter and read a custom annotation on the target object
This solution more concise but requires using RequestMappingHandlerAdapter, which Spring use when <mvc:annotation-driven /> enabled.
Hope it will help somebody.
The idea is to extend ServletRequestDataBinder like this:
/**
* ServletRequestDataBinder which supports fields renaming using {#link ParamName}
*
* #author jkee
*/
public class ParamNameDataBinder extends ExtendedServletRequestDataBinder {
private final Map<String, String> renameMapping;
public ParamNameDataBinder(Object target, String objectName, Map<String, String> renameMapping) {
super(target, objectName);
this.renameMapping = renameMapping;
}
#Override
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
super.addBindValues(mpvs, request);
for (Map.Entry<String, String> entry : renameMapping.entrySet()) {
String from = entry.getKey();
String to = entry.getValue();
if (mpvs.contains(from)) {
mpvs.add(to, mpvs.getPropertyValue(from).getValue());
}
}
}
}
Appropriate processor:
/**
* Method processor supports {#link ParamName} parameters renaming
*
* #author jkee
*/
public class RenamingProcessor extends ServletModelAttributeMethodProcessor {
#Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
//Rename cache
private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<Class<?>, Map<String, String>>();
public RenamingProcessor(boolean annotationNotRequired) {
super(annotationNotRequired);
}
#Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
Object target = binder.getTarget();
Class<?> targetClass = target.getClass();
if (!replaceMap.containsKey(targetClass)) {
Map<String, String> mapping = analyzeClass(targetClass);
replaceMap.put(targetClass, mapping);
}
Map<String, String> mapping = replaceMap.get(targetClass);
ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), mapping);
requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(paramNameDataBinder, nativeWebRequest);
super.bindRequestParameters(paramNameDataBinder, nativeWebRequest);
}
private static Map<String, String> analyzeClass(Class<?> targetClass) {
Field[] fields = targetClass.getDeclaredFields();
Map<String, String> renameMap = new HashMap<String, String>();
for (Field field : fields) {
ParamName paramNameAnnotation = field.getAnnotation(ParamName.class);
if (paramNameAnnotation != null && !paramNameAnnotation.value().isEmpty()) {
renameMap.put(paramNameAnnotation.value(), field.getName());
}
}
if (renameMap.isEmpty()) return Collections.emptyMap();
return renameMap;
}
}
Annotation:
/**
* Overrides parameter name
* #author jkee
*/
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface ParamName {
/**
* The name of the request parameter to bind to.
*/
String value();
}
Spring config:
<mvc:annotation-driven>
<mvc:argument-resolvers>
<bean class="ru.yandex.metrika.util.params.RenamingProcessor">
<constructor-arg name="annotationNotRequired" value="true"/>
</bean>
</mvc:argument-resolvers>
</mvc:annotation-driven>
And finally, usage (like Bozho solution):
public class Job {
#ParamName("job-type")
private String jobType;
#ParamName("loc")
private String location;
}
Here's what I got working:
First, a parameter resolver:
/**
* This resolver handles command objects annotated with #SupportsAnnotationParameterResolution
* that are passed as parameters to controller methods.
*
* It parses #CommandPerameter annotations on command objects to
* populate the Binder with the appropriate values (that is, the filed names
* corresponding to the GET parameters)
*
* In order to achieve this, small pieces of code are copied from spring-mvc
* classes (indicated in-place). The alternative to the copied lines would be to
* have a decorator around the Binder, but that would be more tedious, and still
* some methods would need to be copied.
*
* #author bozho
*
*/
public class AnnotationServletModelAttributeResolver extends ServletModelAttributeMethodProcessor {
/**
* A map caching annotation definitions of command objects (#CommandParameter-to-fieldname mappings)
*/
private ConcurrentMap<Class<?>, Map<String, String>> definitionsCache = Maps.newConcurrentMap();
public AnnotationServletModelAttributeResolver(boolean annotationNotRequired) {
super(annotationNotRequired);
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
if (parameter.getParameterType().isAnnotationPresent(SupportsAnnotationParameterResolution.class)) {
return true;
}
return false;
}
#Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
bind(servletRequest, servletBinder);
}
#SuppressWarnings("unchecked")
public void bind(ServletRequest request, ServletRequestDataBinder binder) {
Map<String, ?> propertyValues = parsePropertyValues(request, binder);
MutablePropertyValues mpvs = new MutablePropertyValues(propertyValues);
MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class);
if (multipartRequest != null) {
bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
}
// two lines copied from ExtendedServletRequestDataBinder
String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
mpvs.addPropertyValues((Map<String, String>) request.getAttribute(attr));
binder.bind(mpvs);
}
private Map<String, ?> parsePropertyValues(ServletRequest request, ServletRequestDataBinder binder) {
// similar to WebUtils.getParametersStartingWith(..) (prefixes not supported)
Map<String, Object> params = Maps.newTreeMap();
Assert.notNull(request, "Request must not be null");
Enumeration<?> paramNames = request.getParameterNames();
Map<String, String> parameterMappings = getParameterMappings(binder);
while (paramNames != null && paramNames.hasMoreElements()) {
String paramName = (String) paramNames.nextElement();
String[] values = request.getParameterValues(paramName);
String fieldName = parameterMappings.get(paramName);
// no annotation exists, use the default - the param name=field name
if (fieldName == null) {
fieldName = paramName;
}
if (values == null || values.length == 0) {
// Do nothing, no values found at all.
} else if (values.length > 1) {
params.put(fieldName, values);
} else {
params.put(fieldName, values[0]);
}
}
return params;
}
/**
* Gets a mapping between request parameter names and field names.
* If no annotation is specified, no entry is added
* #return
*/
private Map<String, String> getParameterMappings(ServletRequestDataBinder binder) {
Class<?> targetClass = binder.getTarget().getClass();
Map<String, String> map = definitionsCache.get(targetClass);
if (map == null) {
Field[] fields = targetClass.getDeclaredFields();
map = Maps.newHashMapWithExpectedSize(fields.length);
for (Field field : fields) {
CommandParameter annotation = field.getAnnotation(CommandParameter.class);
if (annotation != null && !annotation.value().isEmpty()) {
map.put(annotation.value(), field.getName());
}
}
definitionsCache.putIfAbsent(targetClass, map);
return map;
} else {
return map;
}
}
/**
* Copied from WebDataBinder.
*
* #param multipartFiles
* #param mpvs
*/
protected void bindMultipart(Map<String, List<MultipartFile>> multipartFiles, MutablePropertyValues mpvs) {
for (Map.Entry<String, List<MultipartFile>> entry : multipartFiles.entrySet()) {
String key = entry.getKey();
List<MultipartFile> values = entry.getValue();
if (values.size() == 1) {
MultipartFile value = values.get(0);
if (!value.isEmpty()) {
mpvs.add(key, value);
}
} else {
mpvs.add(key, values);
}
}
}
}
And then registering the parameter resolver using a post-processor. It should be registered as a <bean>:
/**
* Post-processor to be used if any modifications to the handler adapter need to be made
*
* #author bozho
*
*/
public class AnnotationHandlerMappingPostProcessor implements BeanPostProcessor {
#Override
public Object postProcessAfterInitialization(Object bean, String arg1)
throws BeansException {
return bean;
}
#Override
public Object postProcessBeforeInitialization(Object bean, String arg1)
throws BeansException {
if (bean instanceof RequestMappingHandlerAdapter) {
RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
List<HandlerMethodArgumentResolver> resolvers = adapter.getCustomArgumentResolvers();
if (resolvers == null) {
resolvers = Lists.newArrayList();
}
resolvers.add(new AnnotationServletModelAttributeResolver(false));
adapter.setCustomArgumentResolvers(resolvers);
}
return bean;
}
}
In Spring 3.1, ServletRequestDataBinder provides a hook for additional bind values:
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
}
The ExtendedServletRequestDataBinder subclass uses it to add URI template variables as binding values. You could extend it further to make it possible to add command-specific field aliases.
You can override RequestMappingHandlerAdapter.createDataBinderFactory(..) to provide a custom WebDataBinder instance. From a controller's perspective it could look like this:
#InitBinder
public void initBinder(MyWebDataBinder binder) {
binder.addFieldAlias("jobType", "jt");
// ...
}
Thanks the answer of #jkee .
Here is my solution.
First, a custom annotation:
#Inherited
#Documented
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
public #interface ParamName {
/**
* The name of the request parameter to bind to.
*/
String value();
}
A customer DataBinder:
public class ParamNameDataBinder extends ExtendedServletRequestDataBinder {
private final Map<String, String> paramMappings;
public ParamNameDataBinder(Object target, String objectName, Map<String, String> paramMappings) {
super(target, objectName);
this.paramMappings = paramMappings;
}
#Override
protected void addBindValues(MutablePropertyValues mutablePropertyValues, ServletRequest request) {
super.addBindValues(mutablePropertyValues, request);
for (Map.Entry<String, String> entry : paramMappings.entrySet()) {
String paramName = entry.getKey();
String fieldName = entry.getValue();
if (mutablePropertyValues.contains(paramName)) {
mutablePropertyValues.add(fieldName, mutablePropertyValues.getPropertyValue(paramName).getValue());
}
}
}
}
A parameter resolver:
public class ParamNameProcessor extends ServletModelAttributeMethodProcessor {
#Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
private static final Map<Class<?>, Map<String, String>> PARAM_MAPPINGS_CACHE = new ConcurrentHashMap<>(256);
public ParamNameProcessor() {
super(false);
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestParam.class)
&& !BeanUtils.isSimpleProperty(parameter.getParameterType())
&& Arrays.stream(parameter.getParameterType().getDeclaredFields())
.anyMatch(field -> field.getAnnotation(ParamName.class) != null);
}
#Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
Object target = binder.getTarget();
Map<String, String> paramMappings = this.getParamMappings(target.getClass());
ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), paramMappings);
requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(paramNameDataBinder, nativeWebRequest);
super.bindRequestParameters(paramNameDataBinder, nativeWebRequest);
}
/**
* Get param mappings.
* Cache param mappings in memory.
*
* #param targetClass
* #return {#link Map<String, String>}
*/
private Map<String, String> getParamMappings(Class<?> targetClass) {
if (PARAM_MAPPINGS_CACHE.containsKey(targetClass)) {
return PARAM_MAPPINGS_CACHE.get(targetClass);
}
Field[] fields = targetClass.getDeclaredFields();
Map<String, String> paramMappings = new HashMap<>(32);
for (Field field : fields) {
ParamName paramName = field.getAnnotation(ParamName.class);
if (paramName != null && !paramName.value().isEmpty()) {
paramMappings.put(paramName.value(), field.getName());
}
}
PARAM_MAPPINGS_CACHE.put(targetClass, paramMappings);
return paramMappings;
}
}
Finally, a bean configuration for adding ParamNameProcessor into the first of argument resolvers:
#Configuration
public class WebConfig {
/**
* Processor for annotation {#link ParamName}.
*
* #return ParamNameProcessor
*/
#Bean
protected ParamNameProcessor paramNameProcessor() {
return new ParamNameProcessor();
}
/**
* Custom {#link BeanPostProcessor} for adding {#link ParamNameProcessor} into the first of
* {#link RequestMappingHandlerAdapter#argumentResolvers}.
*
* #return BeanPostProcessor
*/
#Bean
public BeanPostProcessor beanPostProcessor() {
return new BeanPostProcessor() {
#Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof RequestMappingHandlerAdapter) {
RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>(adapter.getArgumentResolvers());
argumentResolvers.add(0, paramNameProcessor());
adapter.setArgumentResolvers(argumentResolvers);
}
return bean;
}
};
}
}
Param pojo:
#Data
public class Foo {
private Integer id;
#ParamName("first_name")
private String firstName;
#ParamName("last_name")
private String lastName;
#ParamName("created_at")
#DateTimeFormat(pattern = "yyyy-MM-dd")
private Date createdAt;
}
Controller method:
#GetMapping("/foos")
public ResponseEntity<List<Foo>> listFoos(#RequestParam Foo foo, #PageableDefault(sort = "id") Pageable pageable) {
List<Foo> foos = fooService.listFoos(foo, pageable);
return ResponseEntity.ok(foos);
}
That's all.
There is a simple way, you can just add one more setter method, like "setLoc,setJt".
there is no nice built in way to do it, you can only choose which workaround you apply. The difference between handling
#RequestMapping("/foo")
public String doSomethingWithJob(Job job)
and
#RequestMapping("/foo")
public String doSomethingWithJob(String stringjob)
is that job is a bean and stringjob isn't (no surprise so far). The real difference is that beans are resolved with the standard Spring bean resolver mechanism, while string params are resolved by spring MVC that knows the concept of the #RequestParam annotation. To make the long story short there is no way in the standard spring bean resolution (that is using classes like PropertyValues, PropertyValue, GenericTypeAwarePropertyDescriptor) to resolve "jt" to a property called "jobType" or at least I dont know about it.
The workarounds coud be as others suggested to add a custom PropertyEditor or a filter, but I think it just messes up the code. In my opinion the cleanest solution would be to declare a class like this :
public class JobParam extends Job {
public String getJt() {
return super.job;
}
public void setJt(String jt) {
super.job = jt;
}
}
then use that in your controller
#RequestMapping("/foo")
public String doSomethingWithJob(JobParam job) {
...
}
UPDATE :
A slightly simpler option is to not to extend, just add the extra getters, setters to the original class
public class Job {
private String jobType;
private String location;
public String getJt() {
return jobType;
}
public void setJt(String jt) {
jobType = jt;
}
}
You can use Jackson com.fasterxml.jackson.databind.ObjectMapper to convert any map to your DTO/POJO class with nested props. You need annotate your POJOs with #JsonUnwrapped on nested object. Like this:
public class MyRequest {
#JsonUnwrapped
private NestedObject nested;
public NestedObject getNested() {
return nested;
}
}
And than use it like this:
#RequestMapping(method = RequestMethod.GET, value = "/myMethod")
#ResponseBody
public Object myMethod(#RequestParam Map<String, Object> allRequestParams) {
MyRequest request = new ObjectMapper().convertValue(allRequestParams, MyRequest.class);
...
}
That's all. A little coding. Also, you can give any names to your props usign #JsonProperty.
I would like to point you to another direction. But I do not know if it works.
I would try to manipulate the binding itself.
It is done by WebDataBinder and will be invoked from HandlerMethodInvoker method Object[] resolveHandlerArguments(Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception
I have no deep look in Spring 3.1, but what I have seen, is that this part of Spring has been changed a lot. So it is may possible to exchange the WebDataBinder. In Spring 3.0 it seams not possible without overriding the HandlerMethodInvoker.
Try intercepting request using InterceptorAdaptor, and then using simple checking mechanism decide whether to foward the request to the controller handler. Also wrap HttpServletRequestWrapper around the request, to enable you override the requests getParameter().
This way you can repass the actual parameter name and its value back to the request to be seen by the controller.
Example option:
public class JobInterceptor extends HandlerInterceptorAdapter {
private static final String requestLocations[]={"rt", "jobType"};
private boolean isEmpty(String arg)
{
return (arg !=null && arg.length() > 0);
}
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
//Maybe something like this
if(!isEmpty(request.getParameter(requestLocations[0]))|| !isEmpty(request.getParameter(requestLocations[1]))
{
final String value =
!isEmpty(request.getParameter(requestLocations[0])) ? request.getParameter(requestLocations[0]) : !isEmpty(request
.getParameter(requestLocations[1])) ? request.getParameter(requestLocations[1]) : null;
HttpServletRequest wrapper = new HttpServletRequestWrapper(request)
{
public String getParameter(String name)
{
super.getParameterMap().put("JobType", value);
return super.getParameter(name);
}
};
//Accepted request - Handler should carry on.
return super.preHandle(request, response, handler);
}
//Ignore request if above condition was false
return false;
}
}
Finally wrap the HandlerInterceptorAdaptor around your controller handler as shown below. The SelectedAnnotationHandlerMapping allows you to specify which handler will be interecepted.
<bean id="jobInterceptor" class="mypackage.JobInterceptor"/>
<bean id="publicMapper" class="org.springplugins.web.SelectedAnnotationHandlerMapping">
<property name="urls">
<list>
<value>/foo</value>
</list>
</property>
<property name="interceptors">
<list>
<ref bean="jobInterceptor"/>
</list>
</property>
</bean>
EDITED.
There's a little improvement to jkee's answer.
In order to support inheritance you should also analyze parent classes.
/**
* ServletRequestDataBinder which supports fields renaming using {#link ParamName}
*
* #author jkee
* #author Yauhen Parmon
*/
public class ParamRenamingProcessor extends ServletModelAttributeMethodProcessor {
#Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
//Rename cache
private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<>();
public ParamRenamingProcessor(boolean annotationNotRequired) {
super(annotationNotRequired);
}
#Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
Object target = binder.getTarget();
Class<?> targetClass = Objects.requireNonNull(target).getClass();
if (!replaceMap.containsKey(targetClass)) {
replaceMap.put(targetClass, analyzeClass(targetClass));
}
Map<String, String> mapping = replaceMap.get(targetClass);
ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), mapping);
Objects.requireNonNull(requestMappingHandlerAdapter.getWebBindingInitializer())
.initBinder(paramNameDataBinder);
super.bindRequestParameters(paramNameDataBinder, nativeWebRequest);
}
private Map<String, String> analyzeClass(Class<?> targetClass) {
Map<String, String> renameMap = new HashMap<>();
for (Field field : targetClass.getDeclaredFields()) {
ParamName paramNameAnnotation = field.getAnnotation(ParamName.class);
if (paramNameAnnotation != null && !paramNameAnnotation.value().isEmpty()) {
renameMap.put(paramNameAnnotation.value(), field.getName());
}
}
if (targetClass.getSuperclass() != Object.class) {
renameMap.putAll(analyzeClass(targetClass.getSuperclass()));
}
return renameMap;
}
}
This processor will analyze fields of superclasses annotated with #ParamName. It also doesn't use initBinder method with 2 parameters which is deprecated as of Spring 5.0. All the rest in jkee's answer is OK.