How not to make a framework for default request values in Spring
Artem Ptushkin
Posted on April 7, 2020
I used to look for common solutions for the problems I face. And I fail sometimes within my researches of the best solution like many developers. But it's still important to describe your path for the future researcher.
Problem
There are a lot of written Spring REST API services with a lot of common code lines like converters, formatters, custom marshallers, etc. But it is a question still:
How to make dynamic Spring default values for composite objects input into a REST controller?
There are a lot of questions and discussions about it [1]
What do we have?
-
Default values for request parameters and request headers
@RequestParam(defaultValue = "19") Integer age, @RequestHeader(defaultValue = "John Snow") String name
-
Dynamic values for request parameters and request headers
@RequestParam(defaultValue = "${some.property.default.age}") Integer size, @RequestHeader(defaultValue = "${some.property.default.name}") String name
-
Query parameters collection into a custom object. It doesnt require
RequestParam
annotation and doesnt work with headers by default
@GetMapping("/characters") public Character getCharacter(Character character)
DTO class:
@Data public class Character { private String name; private Integer age; }
Example call:
curl --location --request GET 'http://localhost:8080/characters? age=19&name=John%20Snow'
-
Request body collection into a custom object
@PostMapping("/characters") public Character getCharacter(@RequestBody Character character)
In this way, we can customize default values or dynamic one by Spring properties in cases of:
Default values | Dynamic default values | |
---|---|---|
Query | TRUE | TRUE |
Header | TRUE | TRUE |
Composite query | TRUE | FALSE |
Composite header | TRUE | FALSE |
Body | TRUE | FALSE |
What do we need?
The most rational way is to have a mechanism to put default values by annotation/annotation's parameters as we do with default values in annotations: @Value
, @RequestParam
, @RequestHeader
Requirements:
- The Jackson library must have an opportunity for default values. There is an open GitHub ticket and a note at the @JsonProperty documentation
Documentation: It is possible that in future this annotation could be used for value defaulting, and especially for default values of Creator properties, since they support required() in 2.6 and above.
- Spring must provide custom properties to Jackson module. For example:
spring.servlet:
defaults:
unit-request.value:
name: Artem
Workaround with a decorator class
There is a workaround for single cases to have an object of default values.
The hard part that we have to verify nullable values at every field and then use a field from another one.
-
Have a common interface because we'll write a wrapper:
public interface Unit { String getName(); Long getId(); }
-
Request body implementation class:
@Data public class UnitRequest implements Unit { @NotNull private String name; @Nullable private Long id; }
-
Declare default properties:
spring.servlet: defaults: unit-request.value: name: Artem
-
Declare a class for default properties:
@Data public class DefaultProperties<T> { @Nullable private T value; }
-
Declare a configuration properties bean. Please notice, that I suggest to reuse the same class
UnitRequest
for properties because it is the most rational way to reuse existed class behavior. The other way - create another implementation:
@Configuration public class DefaultPropertiesConfiguration { @Bean @ConfigurationProperties("spring.servlet.defaults.unit-request") public DefaultProperties<UnitRequest> unitRequestDefaultProperties() { return new DefaultProperties<>(); } }
-
Create a wrapper class to use values from another object on null cases:
public class UnitRequestWrapper implements Unit { private final Unit decorated; private final Unit defaultUnit; public UnitRequestWrapper(Unit decorated, Unit defaultUnit) { this.decorated = decorated; this.defaultUnit = defaultUnit; } @Override public String getName() { String actual = decorated.getName(); return actual == null ? defaultUnit.getName() : actual; } @Override public Long getId() { Long actual = decorated.getId(); return actual == null ? defaultUnit.getId() : actual; } }
-
Change your
RestController
code
private final DefaultProperties<UnitRequest> unitRequestDefaultProperties; //custom @PostMapping("/units") public UnitResponse post(@RequestBody UnitRequest unitRequest) { Unit unit = new UnitRequestWrapper(unitRequest, unitRequestDefaultProperties.getValue()); //custom return UnitResponse .builder() .id(1L) .name(unit.getName()) .subUnit(unit.getSubUnit()) .build(); }
Workaround with a proxy class
This solution is very close to a reusable one and could be a part of another solution.
-
Proxy methods of web request DTO to call the methods from another object on null values:
public class NullableProxyFactory { public <T> T getProxy(Class<? super T> type, T first, T second) { return (T) Proxy.newProxyInstance( type.getClassLoader(), new Class[]{type}, new NullableInvocationHandler<>(first, second) ); }}
-
Invocation Handler code:
public class NullableInvocationHandler<T> implements InvocationHandler { private final T first; private final T second; @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Method m = findMethod(second.getClass(), method); if (m != null) { Object invocationResult = m.invoke(first, args); if (invocationResult == null) { return m.invoke(second, args); } return invocationResult; } return null; } private Method findMethod(Class<?> clazz, Method method) { try { return clazz.getDeclaredMethod(method.getName(), method.getParameterTypes()); } catch (NoSuchMethodException e) { return null; } } }
-
Usage:
@PostMapping("/units") public UnitResponse post(@RequestBody UnitRequest unitRequest) { Unit unit = proxyFactory.getProxy(Unit.class, unitRequest, unitRequestDefaultProperties.getValue()); ... }
Pros
- We have only one additional line inside the controller
- The Proxy solution looks interesting for reusable cases. We could not write boilerplate code for field values verification.
- We can take inside a library three classes from the proxy example.
Cons
- It will take many more lines of code to proxy the value of the internal composite fields. Like when
UnitRequest
has a fieldSubUnit
. - It requires modifying the controller code and this is redundant cause we have a lot of Spring and Jackson code from above.
- We have to write boilerplate code inside our wrapper.
- The Proxy example is somehow applicable to dynamic query parameters and dynamic headers but the Wrapper example not so much.
What I have tried also?
- AOP. I've written an aspect to interact with all the calls for getter methods with my custom annotation. The annotation is needed to reduce method quantity.
But there is a complicated moment with Spring bean injection into aspect. You have to put
@Configurable
on every DTO class plus AOP code seemed too risky, I've got a lot of problems on this way. -
@ControllerAdvice
+@InitBinder
. It works for single cases also. You either override all the fields or have to verify (as in my workaround above) all the fields values - Proxy +
BeanDeserializerModifier
. It is possible to wrap the result of Jackson deserialization method. I have wrapped the result by proxy object but got thejava.lang.IllegalArgumentException: argument type mismatch
exception. TheProxy
type object came to the controller it is obviously unresolvable.
Conclusions
I wanted to share the unsuccessful example of software solution research in Java backend world. It helps me to understand better the Jackson library and Proxy mechanisms.
Keep researching the best solution. It broadens the mind on the way.
Hope this article describes the question fully.
Problem links
- https://github.com/FasterXML/jackson-databind/issues/1420
- https://stackoverflow.com/questions/18805455/setting-default-values-to-null-fields-when-mapping-with-jackson
- https://stackoverflow.com/questions/32587551/how-to-make-requestparam-configurable-through-properties-file
- https://stackoverflow.com/questions/42329235/how-to-provide-default-values-for-array-parameters-in-spring-mvc-url-mapping
- https://stackoverflow.com/questions/15213752/spring-requestbody-and-default-values
- https://stackoverflow.com/questions/20469938/spring-boot-and-mvc-how-to-set-default-value-for-requestbody-object-fields-fro
- https://stackoverflow.com/questions/38882639/handle-null-and-default-values-in-requestbody-with-jackson
- https://stackoverflow.com/questions/58053179/spring-jacksonrest-modify-request-body-before-reaching-to-controller-add
- https://stackoverflow.com/questions/18088955/markdown-continue-numbered-list
Posted on April 7, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.