Different JSON (de)serialization configs on different endpoints using Spring WebFlux - jackson

My micro service needs to communicate with 2 different services over HTTP. 1 has an API contract with snake_case JSON, while the other uses camelCase. How can I configure WebFlux to deserialize and serialize JSON with a certain Jackson ObjectMapper on a set of functional endpoints, while use another one on different endpoints?
The WebFlux documentation shows how to wire in another ObjectMapper, but this applies to all the endpoints of my API. So right now either all my JSON in snake_case or in camelCase. Cant find any resource to solve this issue, but it must be doable right?
Update: to make it clear I want to configure the web server which receives the requests from other services, not the webclient for sending http requests myself. I know how to do the latter.

you can use the #JsonNaming annotation on the classes you want to serialize/deserialize and specify what type of naming strategy you want.
jackson-advanced-annotations

Okay, so this is not the cleaned up solution, I will use this solution from our library, but the basic gist of my work around looks like this:
#Controller
public class Handler {
private ObjectMapper mapper;
public Handler(#Qualifier("snakeCaseWrapper") ObjectMapper mapper) {
this.mapper = mapper;
}
Mono<ServerResponse> returnUser(final ServerRequest request) {
//REQUEST DESERIALIZATION
var messageReader = new DecoderHttpMessageReader<>(new Jackson2JsonDecoder(mapper));
var configuredRequest = ServerRequest.create(request.exchange(), List.of(messageReader));
//RESPONSE SERIALIZATION
return configuredRequest.bodyToMono(UserDto.class)
.map(userDto -> {
try {
return mapper.writeValueAsString(userDto);
} catch (JsonProcessingException e) {
e.printStackTrace();
//properly handle the error here
return "";
}
})
.flatMap(json -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromObject(json))
);
}
}
This is the only way I could find to programatically choose which kind of ObjectMapper I want to use for a specific endpoint/handler method for request deserialization. For response serialization, the trick was to first use the ObjectMapper to serialize the response body to a String, and put that String into the response with BodyInserters.fromObject(json) .
It works, so I'm happy with it.

Related

Spring Cloud Gateway Custom Filter : WebClient.create().post() causes hanging when testing

So I've created a custom filter that, when accessed, will create a webflux client and post to a predetermined url. This seems to work fine when running, but when testing this code the test is hanging (until I cancel the test). So I feel there is a possible memory leak on top of not being able to complete the test to make sure this route is working properly. If I switch the WebClient method to get() then a resulting test of the filter works fine. Something with a post() I am not sure what is missing.
#Component
class ProxyGatewayFilterFactory: AbstractGatewayFilterFactory<ProxyGatewayFilterFactory.Params>(Params::class.java) {
override fun apply(params: Params): GatewayFilter {
return OrderedGatewayFilter(
GatewayFilter { exchange, chain ->
exchange.request.mutate().header("test","test1").build()
WebClient.create().post()
.uri(params.proxyBasePath)
.body(BodyInserters.fromDataBuffers(exchange.request.body))
.headers { it.addAll(exchange.request.headers) }
.exchange()
.flatMap {
println("the POST statusCode is "+it.statusCode())
Mono.just(it.statusCode().is2xxSuccessful)
}
.map {
exchange.request.mutate().header("test", "test2").build()
println("exchange request uri is " + exchange.request.uri)
println("exchange response statusCode is "+ exchange.response.statusCode)
exchange
}
.flatMap(chain::filter)
}, params.order)
}
Taken from the documentation, if using exchange you have an obligation to consume the body.
Unlike retrieve(), when using exchange(), it is the responsibility of the application to consume any response content regardless of the scenario (success, error, unexpected data, etc). Not doing so can cause a memory leak. The Javadoc for ClientResponse lists all the available options for consuming the body. Generally prefer using retrieve() unless you have a good reason for using exchange() which does allow to check the response status and headers before deciding how to or if to consume the response.
Spring framework 5.2.9 Webclient
This api has been changed in the latest version of the spring framework 5.3.0 now spring will force you to consume the body, because developers didn't actually read the docs.

how can webflux handle global error, like 404 page not found

i use #restcontrolleradvice and #ExceptionHandler , but i can handle controller exception. server error like 404, 500 can't handle.
#RestControllerAdvice
public class HttpExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(HttpExceptionHandler.class);
#ExceptionHandler(value = Exception.class)
public String exceptions(Exception e) {
String code = Global.ERR_UNKNOWN;
if (e instanceof MethodNotAllowedException) {
code = Global.ERR_HTTP_METHOD;
}
return code;
}
}
If you're using Spring Boot, this is already done for you and you can customize this support as well quite easily (see Spring Boot reference docs).
If you're using plain Spring Framework, then you need to register a custom WebExceptionHandler bean to handle that (see Spring Framework reference docs). Because those errors can happen at any point during request handling (i.e. not only during the controller handling phase, but also during response encoding, within a WebFilter...), the API there is quite low level and you need to deal with raw DataBuffer instances. If you're looking for inspiration on how to achieve higher level error handling support, you can also take a look at what's done in Spring Boot.

JAX-RS Apache Oltu server request with JSON body

I am trying to implement OAuth2 in my JAX-RS application, using Apache Oltu. I have found this:
https://github.com/apache/oltu/tree/trunk/oauth-2.0/integration-tests/src/test/java/org/apache/oltu/oauth2/integration/endpoints
#POST
#Consumes("application/x-www-form-urlencoded")
#Produces("application/json")
public Response authorize(#Context HttpServletRequest request) throws OAuthSystemException
{
OAuthTokenRequest oauthRequest = null;
OAuthIssuer oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());
try {
oauthRequest = new OAuthTokenRequest(request);
} catch (OAuthProblemException e) {
OAuthResponse res = OAuthASResponse.errorResponse(HttpServletResponse.SC_BAD_REQUEST).error(e)
.buildJSONMessage();
return Response.status(res.getResponseStatus()).entity(res.getBody()).build();
}
This works fine with application/x-www-form-urlencoded. However I want to have support for application/json too. I can't find any documentation how I extend or implement this. Is anyone familiar with this problem?
Preferably I want to reuse OAuthTokenRequest
The #Consumes and #Produces annotations from the JAX-RS api have support for an Array of Strings by default. You can declare your REST endpoint like this to support multiple formats: #Consumes({"application/x-www-form-urlencoded", "application/json"})

Spring Data Rest Interceptor not working for CustomController

I have a spring data REST application in which I have added a interceptor for authentication & authorization.
private static final boolean IS_JPA_AVAILABLE = ClassUtils.isPresent("javax.persistence.EntityManager",
RepositoryRestMvcConfiguration.class.getClassLoader());
#Override
public JpaHelper jpaHelper() {
if (IS_JPA_AVAILABLE) {
JpaHelper helper = new JpaHelper();
helper.getInterceptors().add(new MyInterceptor());
return helper;
} else {
return null;
}
}
In this application, I have few controllers as well. Some of them are #RepositoryRestController & other are #BasePathAwareController. I want to call the interceptor when a request comes to these controllers. For #RepositoryRestController the interceptor get called, but for #BasePathAwareController it is bypassed.
How can I make this interceptor get called for both controller classes?
This issue is resolved by adding mapped interceptor (thanks llya for your inputs). In the configuration class, I have added following mapped interceptor. In this way it will be called for all requests coming to any controller.
#Bean
public MappedInterceptor myMappedInterceptor() {
return new MappedInterceptor(new String[]{"/**"}, getSecurityInterceptor());
}
Reference
How to add Custom Interceptor in Spring data rest (spring-data-rest-webmvc 2.3.0)

How to add Custom Interceptor in Spring data rest (spring-data-rest-webmvc 2.3.0)

I am working on the spring data rest services & facing some issue in the custom interceptors. Earlier I used spring-data-rest-webmvc 2.2.0 & added interceptor in following way.
public RequestMappingHandlerMapping repositoryExporterHandlerMapping() {
RequestMappingHandlerMapping mapping = super
.repositoryExporterHandlerMapping();
mapping.setInterceptors(new Object[] { new MyInterceptor() });
return mapping;
}
It worked perfectly fine for me. But when i upgraded to spring-data-rest-webmvc 2.3.0 version, I noticed that handlerMapping is hidden behind DelegatingHandlerMapping. Hence I tried to add interceptor in following way.
In one of my config class I have extended RepositoryRestMvcConfiguration class & override its method.
public class AppConfig extends RepositoryRestMvcConfiguration {
#Autowired ApplicationContext applicationContext;
#Override
public DelegatingHandlerMapping restHandlerMapping()
{
RepositoryRestHandlerMapping repositoryMapping = new RepositoryRestHandlerMapping(super.resourceMappings(), super.config());
repositoryMapping.setInterceptors(new Object[] { new MyInterceptor()});
repositoryMapping.setJpaHelper(super.jpaHelper());
repositoryMapping.setApplicationContext(applicationContext);
repositoryMapping.afterPropertiesSet();
BasePathAwareHandlerMapping basePathMapping = new BasePathAwareHandlerMapping(super.config());
basePathMapping.setApplicationContext(applicationContext);
basePathMapping.afterPropertiesSet();
List<HandlerMapping> mappings = new ArrayList<HandlerMapping>();
mappings.add(basePathMapping);
mappings.add(repositoryMapping);
return new DelegatingHandlerMapping(mappings);
}
}
But after adding this some of my repository operations (findAll() operation on repository) start failing. If I removed this interceptors those operations worked fine. (In this interceptor I am just authenticate the user.)
Hence I am unable to understand problem here. Am I adding the interceptor in wrong way? Is there any other way to add the interceptor?
You should not use repositoryMapping.setInterceptors() - it destoys the internal interceptors Spring placed there, and that's probably the reason some methods stopped working.
I suggest you override jpaHelper() method and put your interceptors into the JpaHelper object in RepositoryRestMvcConfiguration. Spring will should them to the global interceptor list.
But, again, if all you need is authentication, why not use a Spring Security filter?
EDIT: the solution above works only for RepositoryRestHandlerMapping, not for BasePathAwareHandlerMapping.
I suggest you declare a custom MappedInterceptor bean somewhere:
#Bean
public MappedInterceptor myMappedInterceptor() {
return new MappedInterceptor(new String[]{"/**"}, new MyInterceptor());
}
From my understanding of the source code Spring should automatically add this interceptor to all request handlers.