How to use Spring Converter for some controllers only? - java

I have c converter which works:
public class StringToLongConverter implements Converter<String, Long> {
#Override
public Long convert(String source) {
Long myDecodedValue = ...
return myDecodedValue;
}
}
In web configuration I have:
#Override
public void addFormatters (FormatterRegistry registry) {
registry.addConverter(new StringToLongConverter());
}
Everything is good but it works for all controllers and I need it to be executed only for some controllers.
//I need this controller to get myvalue from converter
#RequestMapping(value = "{myvalue}", method = RequestMethod.POST)
public ResponseEntity myvalue1(#PathVariable Long myvalue) {
return new ResponseEntity<>(HttpStatus.OK);
}
//I need this controller to get myvalue without converter
#RequestMapping(value = "{myvalue}", method = RequestMethod.POST)
public ResponseEntity myvalue2(#PathVariable Long myvalue) {
return new ResponseEntity<>(HttpStatus.OK);
}
Can we specify which converters or parameters should be used with custom converter and which should not?

Normally speaking, a registered Converter is bound to an input source and an output destination. In your case <String, Long>. The default Spring converter you used will apply the conversion on each matching source-destination pair.
To gain more control over when to conditionally apply the conversion, a ConditionalGenericConverter can be used. The interface contains 3 methods:
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType), to determine whether the conversion should be applied
Set<ConvertiblePair> getConvertibleTypes() to return a set of source-destination pairs the conversion can be applied to
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) the method in which the actual conversion takes places.
I've set up a small Spring project to play around with the use of a ConditionalGenericConverter:
RequiresConversion.java:
// RequiresConversion is a custom annotation solely used in this example
// to annotate an attribute as "convertable"
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
public #interface RequiresConversion {
}
SomeConverter.java:
#Component
public class SomeConverter implements ConditionalGenericConverter {
#Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
// Verify whether the annotation is present
return targetType.getAnnotation(RequiresConversion.class) != null;
}
#Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(String.class, Long.class));
}
#Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
// Conversion logic here
// In this example it strips "value" from the source string
String sourceValue = ((String) source).replace("value", "");
return Long.valueOf(sourceValue);
}
}
SomeController.java:
#RestController
public class SomeController {
// The path variable used will be converted, resulting in the "value"-prefix
// being stripped in SomeConverter
// Notice the custom '#RequiresConversion' annotation
#GetMapping(value = "/test/{myvalue}")
public ResponseEntity myvalue(#RequiresConversion #PathVariable Long myvalue) {
return new ResponseEntity<>(HttpStatus.OK);
}
// As the #RequiresConversion annotation is not present,
// the conversion is not applied to the #PathVariable
#GetMapping(value = "/test2/{myvalue}")
public ResponseEntity myvalue2(#PathVariable Long myvalue) {
return new ResponseEntity<>(HttpStatus.OK);
}
}
The conversion will occur on http://localhost:8080/test/value123 , resulting in a 123 Long value. However, as the custom annotation #RequiresConversion is not present on the second mapping, the conversion on http://localhost:8080/test2/value123 will be skipped.
You could also inverse the annotation by renaming it to SkipConversion and verifying whether the annotation is absent in the matches() method.
Hope this helps!

Related

Convert #RequestParam to custom Object

Have a problem with optimizing search request.
I have search method that accepts parameters in url query like:
http://localhost:8080/api?code.<type>=<value>&name=Test
Example: http://localhost:8080/api?code.phone=9999999999&name=Test
Defined SearchDto:
public class SearchDto {
String name;
List<Code> code;
}
Defined Code class:
public class Code {
String type;
String value;
}
Currently I'm using Map<String,String> as incoming parameter for the method:
#GetMapping("/search")
public ResponseEntity<?> search(final #RequestParam Map<String, String> searchParams) {
return service.search(searchParams);
}
Then manually converting map values for SearchDto class. Is it possible to get rid of Map<String,String> and pass SearchDto directly as argument in controller method?
Passing a json in querystring is actually a bad practice, since it decrease the security and sets limits on the number of parameters you can send to your endpoint.
Technically speaking, you could make everything work by using your DTO as a controller's parameter, then URL encoding the json before you send it to the backend.
The best option, in your case, is to serve an endpoint that listen to a POST request: it is not an error, neither a bad practise, to use POST when performing a search.
you can customize a HandlerMethodArgumentResolver to implement it.
but , if you want a object receive incoming parameter. why not use POST
#Target({ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface Example {
}
public class ExampleArgumentResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter parameter) {
Example requestParam = parameter.getParameterAnnotation(Example.class);
return requestParam != null;
}
#Override
public Object resolveArgument(MethodParameter parameter, #Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, #Nullable WebDataBinderFactory binderFactory) throws Exception {
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
Map<String, String[]> parameterMap = webRequest.getParameterMap();
Map<String, String> result = CollectionUtils.newLinkedHashMap(parameterMap.size());
parameterMap.forEach((key, values) -> {
if (values.length > 0) {
result.put(key, values[0]);
}
});
//here will return a map object. then you convert map to your object, I don't know how to convert , but you have achieve it.
return o;
}
}
add to container
#Configuration
#EnableWebMvc
public class ExampleMvcConfiguration implements WebMvcConfigurer {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new ExampleArgumentResolver());
}
}
usage
#RestController
public class TestCtrl {
#GetMapping("api")
public Object gg(#Example SearchDto searchDto) {
System.out.println(searchDto);
return "1";
}
#Data
public static class SearchDto {
String name;
List<Code> code;
}
#Data
public static class Code {
String type;
String value;
}
}
Here is a demo.

Spring Boot: Optional mapping of request parameters to POJO

I'm trying to map request parameters of a controller method into a POJO object, but only if any of its fields are present. However, I can't seem to find a way to achieve this. I have the following POJO:
public class TimeWindowModel {
#NotNull
public Date from;
#NotNull
public Date to;
}
If none of the fields are specified, I'd like to get an empty Optional, otherwise I'd get an Optional with a validated instance of the POJO. Spring supports mapping request parameter into POJO objects by leaving them unannotated in the handler:
#GetMapping("/shop/{shopId}/slot")
public Slice<Slot> getSlots(#RequestAttribute("staff") Staff staff,
#PathVariable("shopId") Long shopId, #Valid TimeWindowModel timeWindow) {
// controller code
}
With this, Spring will map request parameters "from" and "to" to an instance of TimeWindowModel. However, I want to make this mapping optional. For POST requests you can use #RequestBody #Valid Optional<T>, which will give you an Optional<T> containing an instance of T, but only if a request body was provided, otherwise it will be empty. This makes #Valid work as expected.
When not annotated, Optional<T> doesn't appear to do anything. You always get an Optional<T> with an instance of the POJO. This is problematic when combined with #Valid because it will complain that "from" and "to" are not set.
The goal is to get either (a) an instance of the POJO where both "from" and "to" are not null or (b) nothing at all. If only one of them is specified, then #Valid should fail and report that the other is missing.
I came up with a solution with a custom HandlerMethodArgumentResolver, Jackson and Jackson Databind.
The annotation:
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
public #interface RequestParamBind {
}
The resolver:
public class RequestParamBindResolver implements HandlerMethodArgumentResolver {
private final ObjectMapper mapper;
public RequestParamBindResolver(ObjectMapper mapper) {
this.mapper = mapper.copy();
this.mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(RequestParamBind.class) != null;
}
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mav, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// take the first instance of each request parameter
Map<String, String> requestParameters = webRequest.getParameterMap()
.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue()[0]));
// perform the actual resolution
Object resolved = doResolveArgument(parameter, requestParameters);
// *sigh*
// see: https://stackoverflow.com/questions/18091936/spring-mvc-valid-validation-with-custom-handlermethodargumentresolver
if (parameter.hasParameterAnnotation(Valid.class)) {
String parameterName = Conventions.getVariableNameForParameter(parameter);
WebDataBinder binder = binderFactory.createBinder(webRequest, resolved, parameterName);
// DataBinder constructor unwraps Optional, so the target could be null
if (binder.getTarget() != null) {
binder.validate();
BindingResult bindingResult = binder.getBindingResult();
if (bindingResult.getErrorCount() > 0)
throw new MethodArgumentNotValidException(parameter, bindingResult);
}
}
return resolved;
}
private Object doResolveArgument(MethodParameter parameter, Map<String, String> requestParameters) {
Class<?> clazz = parameter.getParameterType();
if (clazz != Optional.class)
return mapper.convertValue(requestParameters, clazz);
// special case for Optional<T>
Type type = parameter.getGenericParameterType();
Class<?> optionalType = (Class<?>)((ParameterizedType)type).getActualTypeArguments()[0];
Object obj = mapper.convertValue(requestParameters, optionalType);
// convert back to a map to find if any fields were set
// TODO: how can we tell null from not set?
if (mapper.convertValue(obj, new TypeReference<Map<String, String>>() {})
.values().stream().anyMatch(Objects::nonNull))
return Optional.of(obj);
return Optional.empty();
}
}
Then, we register it:
#Configuration
public class WebConfig implements WebMvcConfigurer {
//...
#Override
public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new RequestParamBindResolver(new ObjectMapper()));
}
}
Finally, we can use it like so:
#GetMapping("/shop/{shopId}/slot")
public Slice<Slot> getSlots(#RequestAttribute("staff") Staff staff,
#PathVariable("shopId") Long shopId,
#RequestParamBind #Valid Optional<TimeWindowModel> timeWindow) {
// controller code
}
Which works exactly as you'd expect.
I'm sure it's possible to accomplish this by using Spring's own DataBind classes in the resolver. However, Jackson Databind seemed like the most straight-forward solution. That said, it's not able to distinguish between fields that are set to null and fields that just not set. This is not really an issue for my use-case, but it's something that should be noted.
To implement logic (a) both not null or (b) both are nulls you need to implement custom validation.
Examples are here:
https://blog.clairvoyantsoft.com/spring-boot-creating-a-custom-annotation-for-validation-edafbf9a97a4
https://www.baeldung.com/spring-mvc-custom-validator
Generally, you create a new annotation, it's just a stub, and then you create a validator which implements ConstraintValidator where you provide your logic and then you put your new annotation to your POJO.

Custom argument resolver for #PathVariable

Scenario : My controller accepts a Long value for the id which is a Path Variable.
I need to pass a String which is an external reference to the id. So I need to resolve the string reference to its Long value.
Attempt: When the annotation #PathVariable is present, my custom argument resolver is not called as PathVariableMethodArgumentResolver is above than my custom resolver in the resolver list and it just supports all arguments with #PathVariable annotation
It works fine if I remove #PathVariable and add my own annotation. But then Swagger gets the id as a Request body parameter and produces this error:
TypeError: Failed to execute 'fetch' on 'Window': Request with GET/HEAD method cannot have body.
My custom resolver:
#Override
public boolean supportsParameter( MethodParameter methodParameter )
{
return methodParameter.hasParameterAnnotation( ExternalRefParam.class );
}
#Override public Object resolveArgument( MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory ) throws Exception
{
Map nameValueMap = (Map) nativeWebRequest.getAttribute( HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, 0 );
switch( methodParameter.getParameterName() )
{
case CART_ID:
return resolveCartId( nameValueMap );
case PRODUCT_KEY:
return resolveProductKey( nameValueMap );
}
return -1L;
}
Controller Signature:
public ResponseEntity<Cart> readCart(
#ApiParam(value = "Cart ID", required = true) #ExternalRefParam Long cartId, HttpServletRequest request )
I had a similar issue where I wanted to add a custom argument resolver that would convert a path variable string value to uppercase. I solved it by creating a GenericConverter that converted a string => string for path variables annotated with a certain annotation.
The path variable annotation type was just a tagging annotation like this:
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface Uppercase {
String value() default "";
}
which was used on a rest controller mapping like this
#PostMapping(value = "/clients/{clientId}/postalAddress")
#ResponseStatus(HttpStatus.CREATED)
public IdResponse create(
#PathVariable("clientId") #Uppercase final String clientId,
#RequestBody #NotNull #Valid final AddressRequest request) {...}
And then the Generic Converter was triggered to run on any String argument and the convert method checked that the argument was tagged with the Uppercase annotation to know if it should be uppercased. This also meant that Swagger API still reports the Path Variable as being from the path and properly extracts the path variable value, then runs the converter if it's annotated with Uppercase.
public class CarPolicyIdAttributeConverter implements GenericConverter {
#Override
public Set<ConvertiblePair> getConvertibleTypes() {
final ConvertiblePair[] pairs = new ConvertiblePair[] {
new ConvertiblePair(String.class, String.class)
};
return ImmutableSet.copyOf(pairs);
}
#Override
public Object convert(final Object source, final TypeDescriptor sourceType, final TypeDescriptor targetType) {
if (targetType.getAnnotation(Uppercase.class) != null) {
return ((String)source).toUppercase();
}
return source;
}
}

Spring Boot using Json as request parameters instead of an entity/model

Our company is planning to switch our microservice technology to Spring Boot. As an initiative I did some advanced reading and noting down its potential impact and syntax equivalents. I also started porting the smallest service we had as a side project.
One issue that blocked my progress was trying to convert our Json request/response exchange to Spring Boot.
Here's an example of the code: (This is Nutz framework for those who don't recognize this)
#POST
#At // These two lines are equivalent to #PostMapping("/create")
#AdaptBy(type=JsonAdapter.class)
public Object create(#Param("param_1") String param1, #Param("param_2) int param2) {
MyModel1 myModel1 = new MyModel1(param1);
MyModel2 myModel2 = new MyModel2(param2);
myRepository1.create(myMode12);
myRepository2.create(myModel2);
return new MyJsonResponse();
}
On PostMan or any other REST client I simply pass POST:
{
"param_1" : "test",
"param_2" : 1
}
I got as far as doing this in Spring Boot:
#PostMapping("/create")
public Object create(#RequestParam("param_1") String param1, #RequestParam("param_2) int param2) {
MyModel1 myModel1 = new MyModel1(param1);
MyModel2 myModel2 = new MyModel2(param2);
myRepository1.create(myMode12);
myRepository2.create(myModel2);
return new MyJsonResponse();
}
I am not sure how to do something similar as JsonAdapter here. Spring doesn't recognize the data I passed.
I tried this but based on the examples it expects the Json paramters to be of an Entity's form.
#RequestMapping(path="/wallet", consumes="application/json", produces="application/json")
But I only got it to work if I do something like this:
public Object (#RequestBody MyModel1 model1) {}
My issue with this is that MyModel1 may not necessarily contain the fields/parameters that my json data has.
The very useful thing about Nutz is that if I removed JsonAdapter it behaves like a regular form request endpoint in spring.
I couldn't find an answer here in Stack or if possible I'm calling it differently than what existing spring devs call it.
Our bosses expect us (unrealistically) to implement these changes without forcing front-end developers to adjust to these changes. (Autonomy and all that jazz). If this is unavoidable what would be the sensible explanation for this?
In that case you can use Map class to read input json, like
#PostMapping("/create")
public Object create(#RequestBody Map<String, ?> input) {
sout(input.get("param1")) // cast to String, int, ..
}
I actually figured out a more straightforward solution.
Apparently this works:
#PostMapping("/endpoint")
public Object endpoint(#RequestBody MyWebRequestObject request) {
String value1 = request.getValue_1();
String value2 = request.getValue_2();
}
The json payload is this:
{
"value_1" : "hello",
"value_2" : "world"
}
This works if MyRequestObject is mapped like the json request object like so. Example:
public class MyWebRequestObject {
String value_1;
String value_2
}
Unmapped values are ignored. Spring is smart like that.
I know this is right back where I started but since we introduced a service layer for the rest control to interact with, it made sense to create our own request model object (DTOs) that is separate from the persistence model.
You can use #RequestBody Map as a parameter for #PostMapping, #PutMapping and #PatchMapping. For #GetMapping and #DeleteMapping, you can write a class which implements Converter to convert from json-formed request parameters to Map. And you would register that class as a bean with #Component annotation. Then you can bind your parameters to #RequestParameter Map.
Here is an example of Converter below.
#Component
public class StringToMapConverter implements Converter<String, Map<String, Object>> {
private final ObjectMapper objectMapper;
#Autowired
public StringToMapConverter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
#Override
public Map<String, Object> convert(String source) {
try {
return objectMapper.readValue(source, new TypeReference<Map<String, Object>>(){});
} catch (IOException e) {
return new HashMap<>();
}
}
}
If you want to exclude specific field of your MyModel1 class, use #JsonIgnore annotation onto the field like below.
class MyModel1 {
private field1;
#JsonIgnore field2;
}
Then, I guess you can just use what you have done.(I'm not sure.)
public Object (#RequestBody MyModel1 model1) {}
i think that you can use a strategy that involve dto
https://auth0.com/blog/automatically-mapping-dto-to-entity-on-spring-boot-apis/
you send a json to your rest api that is map like a dto object, after you can map like an entity or use it for your needs
try this:
Add new annotation JsonParam and implement HandlerMethodArgumentResolver of this, Parse json to map and get data in HandlerMethodArgumentResolver
{
"aaabbcc": "aaa"
}
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
public #interface JsonParam {
String value();
}
#Component
public class JsonParamMethodResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(JsonParam.class);
}
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
RepeatedlyRequestWrapper nativeRequest = webRequest.getNativeRequest(RepeatedlyRequestWrapper.class);
if (nativeRequest == null) {
return null;
}
Gson gson = new Gson();
Map<String, Object> response = gson.fromJson(nativeRequest.getReader(), new TypeToken<Map<String, Object>>() {
}.getType());
if (response == null) {
return null;
}
JsonParam parameterAnnotation = parameter.getParameterAnnotation(JsonParam.class);
String value = parameterAnnotation.value();
Class<?> parameterType = parameter.getParameterType();
return response.get(value);
}
}
#Configuration
public class JsonParamConfig extends WebMvcConfigurerAdapter {
#Autowired
JsonParamMethodResolver jsonParamMethodResolver;
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(jsonParamMethodResolver);
}
}
#PostMapping("/methodName")
public void methodName(#JsonParam("aaabbcc") String ddeeff) {
System.out.println(username);
}

Can you configure Spring controller specific Jackson deserialization?

I need to add a custom Jackson deserializer for java.lang.String to my Spring 4.1.x MVC application. However all answers (such as this) refer to configuring the ObjectMapper for the complete web application and the changes will apply to all Strings across all #RequestBody in all controllers.
I only want to apply the custom deserialization to #RequestBody arguments used within particular controllers. Note that I don't have the option of using #JsonDeserialize annotations for the specific String fields.
Can you configure custom deserialization for specific controllers only?
To have different deserialization configurations you must have different ObjectMapper instances but out of the box Spring uses MappingJackson2HttpMessageConverter which is designed to use only one instance.
I see at least two options here:
Move away from MessageConverter to an ArgumentResolver
Create a #CustomRequestBody annotation, and an argument resolver:
public class CustomRequestBodyArgumentResolver implements HandlerMethodArgumentResolver {
private final ObjectMapperResolver objectMapperResolver;
public CustomRequestBodyArgumentResolver(ObjectMapperResolver objectMapperResolver) {
this.objectMapperResolver = objectMapperResolver;
}
#Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterAnnotation(CustomRequestBody.class) != null;
}
#Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
if (this.supportsParameter(methodParameter)) {
ObjectMapper objectMapper = objectMapperResolver.getObjectMapper();
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
return objectMapper.readValue(request.getInputStream(), methodParameter.getParameterType());
} else {
return WebArgumentResolver.UNRESOLVED;
}
}
}
#CustomRequestBody annotation:
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface CustomRequestBody {
boolean required() default true;
}
ObjectMapperResolver is an interface we will be using to resolve actual ObjectMapper instance to use, I will discuss it below. Of course if you have only one use case where you need custom mapping you can simply initialize your mapper here.
You can add custom argument resolver with this configuration:
#Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {
#Bean
public CustomRequestBodyArgumentResolver customBodyArgumentResolver(ObjectMapperResolver objectMapperResolver) {
return new CustomRequestBodyArgumentResolver(objectMapperResolver)
}
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(customBodyArgumentResolver(objectMapperResolver()));
}
}
Note: Do not combine #CustomRequestBody with #RequestBody, it will be ignored.
Wrap ObjectMapper in a proxy that hides multiple instances
MappingJackson2HttpMessageConverter is designed to work with only one instance of ObjectMapper. We can make that instance a proxy delegate. This will make working with multiple mappers transparent.
First of all we need an interceptor that will translate all method invocations to an underlying object.
public abstract class ObjectMapperInterceptor implements MethodInterceptor {
#Override
public Object invoke(MethodInvocation invocation) throws Throwable {
return ReflectionUtils.invokeMethod(invocation.getMethod(), getObject(), invocation.getArguments());
}
protected abstract ObjectMapper getObject();
}
Now our ObjectMapper proxy bean will look like this:
#Bean
public ObjectMapper objectMapper(ObjectMapperResolver objectMapperResolver) {
ProxyFactory factory = new ProxyFactory();
factory.setTargetClass(ObjectMapper.class);
factory.addAdvice(new ObjectMapperInterceptor() {
#Override
protected ObjectMapper getObject() {
return objectMapperResolver.getObjectMapper();
}
});
return (ObjectMapper) factory.getProxy();
}
Note: I had class loading issues with this proxy on Wildfly, due to its modular class loading, so I had to extend ObjectMapper (without changing anything) just so I can use class from my module.
It all tied up together using this configuration:
#Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {
#Bean
public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() {
return new MappingJackson2HttpMessageConverter(objectMapper(objectMapperResolver()));
}
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(jackson2HttpMessageConverter());
}
}
ObjectMapperResolver implementations
Final piece is the logic that determines which mapper should be used, it will be contained in ObjectMapperResolver interface. It contains only one look up method:
public interface ObjectMapperResolver {
ObjectMapper getObjectMapper();
}
If you do not have a lot of use cases with custom mappers you can simply make a map of preconfigured instances with ReqeustMatchers as keys. Something like this:
public class RequestMatcherObjectMapperResolver implements ObjectMapperResolver {
private final ObjectMapper defaultMapper;
private final Map<RequestMatcher, ObjectMapper> mapping = new HashMap<>();
public RequestMatcherObjectMapperResolver(ObjectMapper defaultMapper, Map<RequestMatcher, ObjectMapper> mapping) {
this.defaultMapper = defaultMapper;
this.mapping.putAll(mapping);
}
public RequestMatcherObjectMapperResolver(ObjectMapper defaultMapper) {
this.defaultMapper = defaultMapper;
}
#Override
public ObjectMapper getObjectMapper() {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = sra.getRequest();
for (Map.Entry<RequestMatcher, ObjectMapper> entry : mapping.entrySet()) {
if (entry.getKey().matches(request)) {
return entry.getValue();
}
}
return defaultMapper;
}
}
You can also use a request scoped ObjectMapper and then configure it on a per-request basis. Use this configuration:
#Bean
public ObjectMapperResolver objectMapperResolver() {
return new ObjectMapperResolver() {
#Override
public ObjectMapper getObjectMapper() {
return requestScopedObjectMapper();
}
};
}
#Bean
#Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public ObjectMapper requestScopedObjectMapper() {
return new ObjectMapper();
}
This is best suited for custom response serialization, since you can configure it right in the controller method. For custom deserialization you must also use Filter/HandlerInterceptor/ControllerAdvice to configure active mapper for current request before the controller method is triggered.
You can create interface, similar to ObjectMapperResolver:
public interface ObjectMapperConfigurer {
void configureObjectMapper(ObjectMapper objectMapper);
}
Then make a map of this instances with RequstMatchers as keys and put it in a Filter/HandlerInterceptor/ControllerAdvice similar to RequestMatcherObjectMapperResolver.
P.S. If you want to explore dynamic ObjectMapper configuration a bit further I can suggest my old answer here. It describes how you can make dynamic #JsonFilters at run time. It also contains my older approach with extended MappingJackson2HttpMessageConverter that I suggested in comments.
Probably this would help, but it ain't pretty. It would require AOP. Also I did not validate it.
Create a #CustomAnnotation.
Update your controller:
void someEndpoint(#RequestBody #CustomAnnotation SomeEntity someEntity);
Then implemment the AOP part:
#Around("execution(* *(#CustomAnnotation (*)))")
public void advice(ProceedingJoinPoint proceedingJoinPoint) {
// Here you would add custom ObjectMapper, I don't know another way around it
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String body = request .getReader().lines().collect(Collectors.joining(System.lineSeparator()));
SomeEntity someEntity = /* deserialize */;
// This could be cleaner, cause the method can accept multiple parameters
proceedingJoinPoint.proceed(new Object[] {someEntity});
}
You can create custom deserializer for your String data.
Custom Deserializer
public class CustomStringDeserializer extends JsonDeserializer<String> {
#Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String str = p.getText();
//return processed String
}
}
Now suppose the String is present inside a POJO use #JsonDeserialize annotation above the variable:
public class SamplePOJO{
#JsonDeserialize(using=CustomStringDeserializer.class)
private String str;
//getter and setter
}
Now when you return it as a response it will be Deserialized in the way you have done it in CustomDeserializer.
Hope it helps.
You could try Message Converters.
They have a context about http input request (for example, docs see here, JSON). How to customize you could see here.
Idea that you could check HttpInputMessage with special URIs, which used in your controllers and convert string as you want.
You could create special annotation for this, scan packages and do it automatically.
Note
Likely, you don't need implementation of ObjectMappers. You can use simple default ObjectMapper to parse String and then convert string as you wish.
In that case you would create RequestBody once.
You can define a POJO for each different type of request parameter that you would like to deserialize. Then, the following code will pull in the values from the JSON into the object that you define, assuming that the names of the fields in your POJO match with the names of the field in the JSON request.
ObjectMapper mapper = new ObjectMapper();
YourPojo requestParams = null;
try {
requestParams = mapper.readValue(JsonBody, YourPOJO.class);
} catch (IOException e) {
throw new IOException(e);
}

Categories

Resources