Spring RestTemplate with Jackson throws "Can not resolve BeanPropertyFilter" when using #JsonFilter - spring

Can I specify the Jackson ObjectMapper that Spring's RestTemplate uses?
I'm not 100% that's what I need to do but see below for details.
Background:
With help from this StackOverflow post I added #JsonFilter to my domain class and edited my jax-rs web service (implemented in CXF). I'm now successfully able to dynamically select which domain class fields to return in my RESTful API. So far so good.
I'm using Spring's RestTemplate in my JUnit tests to test my RESTful API. This was working fine until I added #JasonFilter to my domain class. Now I'm getting the following exception:
org.springframework.web.client.ResourceAccessException: I/O error: Can not resolve BeanPropertyFilter with id 'apiFilter'; no FilterProvider configured; nested exception is org.codehaus.jackson.map.JsonMappingException: Can not resolve BeanPropertyFilter with id 'apiFilter'; no FilterProvider configured
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:453)
rest of stack trace omitted for brevity
Caused by: org.codehaus.jackson.map.JsonMappingException: Can not resolve BeanPropertyFilter with id 'apiFilter'; no FilterProvider configured
at org.codehaus.jackson.map.ser.BeanSerializer.findFilter(BeanSerializer.java:252)
I was getting a similar problem on the server side and was able to resolve it (with help from this post) by giving a FilterProvider to the Jackson ObjectMapper as follows:
ObjectMapper mapper = new ObjectMapper();
FilterProvider filters = new SimpleFilterProvider().addFilter("apiFilter", SimpleBeanPropertyFilter.filterOutAllExcept(filterProperties));
Can I do something similar on the RestTemplate side? Any ideas of how to solve this issue are appreciated.
Just to be clear, on the client RestTemplate side I do not want to filter the domain object properties at all.

Can I specify the Jackson ObjectMapper that Spring's RestTemplate uses?
I was able to force RestTemplate to use a customized ObjectMapper by doing the following:
ObjectMapper mapper = new ObjectMapper();
// set a custom filter
Set<String> filterProperties = new HashSet<String>();
FilterProvider filters = new SimpleFilterProvider().addFilter("apiFilter", SimpleBeanPropertyFilter.serializeAllExcept(filterProperties));
mapper.setFilters(filters);
MappingJacksonHttpMessageConverter messageConverter = new MappingJacksonHttpMessageConverter();
messageConverter.setObjectMapper(mapper);
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
messageConverters.add(messageConverter);
restTemplate.setMessageConverters(messageConverters);
This website provided example for part of the above code.

Just adding to the answer. If you are using TestRestTemplate then you can actually get the underlying RestTemplate class and then modify its MappingJackson2HttpMessageConverter to include your filter:
var jackson2HttpMessageConverter = testRestTemplate.getRestTemplate().getMessageConverters().stream()
.filter(mc -> mc instanceof MappingJackson2HttpMessageConverter)
.map(mc -> (MappingJackson2HttpMessageConverter) mc)
.findFirst()
.orElseThrow();
jackson2HttpMessageConverter.getObjectMapper().setFilterProvider(
new SimpleFilterProvider().addFilter("MyFilterName", SimpleBeanPropertyFilter.serializeAll())
);

Related

Get Spring Default Feign Options on programmatically created feign client

I am trying to get the default feign options/config to use it in feign clients that I create programatically using Feign.Builder:
This is the config (application.yaml) that I want to get:
feign:
client:
config:
default:
connect-timeout: 5000
read-timeout: 5000
What I tried to do is to do is to add #EnableFeignClients and try to get the FactoryBean<Request.Options> or it's implementation OptionsFactoryBean but I don't see it being injected by Spring anywhere.
I've also tried searching in StackOverflow and other websites to see if there are other people that have tried what I am trying to do, but I wasn't able to find any information hence why I am creating this question.
public MyClientFactory(Client client,
ObjectMapper objectMapper,
FactoryBean<Request.Options> optionsFactory,
ErrorDecoder errorDecoder) throws Exception {
this.builder = Feign.builder()
.client(client)
.decoder(new JacksonDecoder(objectMapper))
.encoder(new JacksonEncoder(objectMapper))
.options(optionsFactory.getObject())
.errorDecoder(errorDecoder);
}
Can someone please let me know how I can get the default spring feign configs? Maybe my approach is incorrect?
Thanks!
Haven't tried it but I assume you'll need a bean from the FeignClientProperties class.
I ended up creating a Request.Options #Bean based on FeignClientProperties as #Arnold Galovics suggested.
#Bean
Request.Options options(FeignClientProperties feignClientProperties) {
FeignClientProperties.FeignClientConfiguration feignClientConfiguration = feignClientProperties.getConfig().get(feignClientProperties.getDefaultConfig());
Request.Options defaultOptions = new Request.Options();
return new Request.Options(
Optional.ofNullable(feignClientConfiguration.getConnectTimeout()).orElse(defaultOptions.connectTimeoutMillis()), TimeUnit.MILLISECONDS,
Optional.ofNullable(feignClientConfiguration.getReadTimeout()).orElse(defaultOptions.readTimeoutMillis()), TimeUnit.MILLISECONDS,
Optional.ofNullable(feignClientConfiguration.isFollowRedirects()).orElse(defaultOptions.isFollowRedirects())
);
}
And then I injected this bean in my Feign Client Factory:
public MyClientFactory(Client client,
ObjectMapper objectMapper,
Request.Options options,
ErrorDecoder errorDecoder) {
this.builder = Feign.builder()
.client(client)
.decoder(new JacksonDecoder(objectMapper))
.encoder(new JacksonEncoder(objectMapper))
.options(options)
.errorDecoder(errorDecoder);
}

How do I make spring boot RestTemplate client metrics not create a new tag for query parameters

I've got a spring boot application that is defining a RestTemplate bean as follows:
#Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
Also, pulling in spring-boot-starter-actuator and io.micrometer:micrometer-registry-prometheus.
When I use the injected RestTemplate as follows:
#Autowired
private RestTemplate restTemplate;
private String uriTemplate = "http://my.domain.com/bookstore-api/books";
public List<Book> getBooksByAuthor(String author) {
// create URI for "http://my.domain.com/bookstore-api/books?author={authorId}"
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder
.fromUriString(uriTemplate)
.queryParam("author", author);
// make the GET
ResponseEntity<Book[]> responseEntity = restTemplate.getForEntity(uriComponentsBuilder.toUriString(),Book[].class);
// rest ommitted for brevity
}
When getBooksByAuthor("Tolkien") is called, we can then hit /metrics/prometheus and see the following:
http_client_requests_seconds_count{clientName="my.domain.com",method="GET",status="200",uri="/bookstore-api/books?author=Tolkien",} 2.0
http_client_requests_seconds_sum{clientName="my.domain.com",method="GET",status="200",uri="/bookstore-api/books?author=Tolkien",} 0.253227898
This would be fine, except that there are lots of authors out there, and eventually I will get the "too many tags" exception.
I would prefer to have the following (similar to how path variables get templated):
http_client_requests_seconds_count{clientName="my.domain.com",method="GET",status="200",uri="/bookstore-api/books?author={author}",} 2.0
http_client_requests_seconds_sum{clientName="my.domain.com",method="GET",status="200",uri="/bookstore-api/books?author={author}",} 0.253227898
Is this possible to achieve by modifying the way I use UriComponentsBuilder? The closest thing I've found is to define my own RestTemplateExchangeTagsProvider, and override the default to do some crazy regex replacement.
Just fixed same issue in SpringBoot 2.4.5 using:
responseEntity = restTemplate.exchange(
config.getDataUrl(),
HttpMethod.GET,
httpEntity,
new ParameterizedTypeReference<String>() {},
rowId);
where getDataUrl resolves to:
https://data-service-dev.apps.cloud.net/api/hbase/getData?rowId={rowId}
metrics:
http_client_requests_seconds_count{clientName="data-service-dev.apps.cloud.net",method="GET",outcome="SUCCESS",status="200",uri="/api/hbase/getData?rowId={rowId}",} 1.0
...
I have had a same problem. Maybe this information will help you.
In my case restTemplate.setUriTemplateHandler(handler) had overwritten annonymous
MetricsClientHttpRequestInterceptor#createUriTemplateHandler.
And the original url templates had not been stored into memory for prometheus.
DefaultUriBuilderFactory builderFactory = new DefaultUriBuilderFactory();
builderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);
customizer.customize(restTemplate);
restTemplate.setUriTemplateHandler(handler);
So,
I changed order of the commands:
restTemplate.setUriTemplateHandler(handler);
customizer.customize(restTemplate);
Please check that there are no setting commands for restTemplate after MetricsClientHttpRequestInterceptor.customize(restTemplate).

How do i send a post request with Google guava Multi-map data using rest template Spring?

I have a multimap. When I try to send the data over to the backend-server from my frontend-server using restTemplate, it returns a "500 Internal Server Error" error code. It works for normal Map from the java.util.Map class. How do I configure restTemplate so that I send Google guava multimap data over?
This is my frontend-server code in spring boot.
import com.google.common.collect.Multimap;
...
try
{
RestTemplate rt = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
List<MediaType> acceptList = new ArrayList<>();
acceptList.add(MediaType.APPLICATION_JSON);
headers.setAccept(acceptList);
Multimap<String, byte[]> arrayMap = ArrayListMultimap.create();
arrayMap.put("test", "hello".getBytes());
arrayMap.put("title", "meow".getBytes());
arrayMap.put("body", "A powerful tool for building web apps.".getBytes());
HttpEntity<Multimap<String, byte[]>> uploadEntity = new HttpEntity<>(arrayMap, headers);
ResponseEntity<String> response = rt.postForEntity(uri, uploadEntity, String.class);
System.out.println(response);
}
catch (HttpClientErrorException e)
{
/**
*
* If we get a HTTP Exception display the error message
*/
logger.error("error http client: " + e.getResponseBodyAsString());
}
catch(Exception e)
{
logger.error("error: " + e.getMessage());
}
This is my Backend-server controller where it recevies the incoming data from my frontend-server.
import com.google.common.collect.Multimap;
...
#PostMapping(path="/rh2lev", consumes = "application/json", produces = "application/json")
public String setup(#RequestBody Multimap<String, byte[]> uploadIndex)
{
System.out.println(uploadIndex);
return "everything ok";
}
It seems that this error occurs on serializing Multimap objects into JSON string or deserializing JSON string into Multimap objects. Spring usually does JSON serialization/deserialization by using Jackson ObjectMapper. Jackson ObjectMapper can do JSON serialization/deserialization of java.util.Map objects by default but can't com.google.common.collect.Multimap objects.
So you need to configure a custom serializer in 'frontend-server' and also configure a custom deserializer in 'backend-server'. I think you can achieve this by using jackson-datatype-guava.
Example
Configurations for your 'frontend-server' and 'backend-server'
(pom.xml)
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-guava</artifactId>
</dependency>
(Spring Boot's configuration)
#Configuration
public class MyConfigurer {
#Bean
public Jackson2ObjectMapperBuilderCustomizer guavaModuleCustomizer() {
return builder -> {
builder.modules(new GuavaModule());
};
}
}
Related Question
Spring MVC mapping Guava Multimap
For More Information
“How-to” Guides 4.3. Customize the Jackson ObjectMapper - Spring Boot Reference Documentation
Intro to the Jackson ObjectMapper - Baeldung blog post
Jackson – Custom Serializer - Baeldung blog post
Getting Started with Custom Deserialization in Jackson - Baeldung blog post

RestTemplate RestClientException Could not extract response: no suitable HttpMessageConverter found

I am getting this error when calling the RestTemplate method
GetStatusRestfulResponse response = restTemplate.getForObject(restRequest.getUrl(), GetStatusRestfulResponse.class,restRequest.getParams());
>org.springframework.web.client.RestClientException: Could not extract response: no suitable HttpMessageConverter found for response type [class GetStatusRestfulResponse] and content type [application/json]
at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:108)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:550)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:511)
at org.springframework.web.client.RestTemplate.getForObject(RestTemplate.java:248)
RestTemplate restTemplate = new RestTemplate();
HttpClient httpClient = HttpClientBuilder.create().setDefaultCredentialsProvider(setupAuthentication(restRequest)).build();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
requestFactory.setReadTimeout(restRequest.getReqTimeOut());
restTemplate.setRequestFactory(requestFactory);
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
response = restTemplate.getForObject(restRequest.getUrl(), GetStatusRestfulResponse.class,restRequest.getParams());
I was able to solve the issue . the culprit was the java object I was using GetStatusRestfulResponse.
I took the following steps to debug the issue.
Got the source codes for spring-web and jackson-databind.
on debugging in the spring and jackson source code, realized that the issue was with the ObjectMapper not able to deserialize the java object.
The issue was that my Java object had inner classes .
in order to fix the problem with ObjectMapper not able to deserialize the java object , I had to
Add default no parameter constructors for the main java class and inner classes.
Made the inner classes static.
this solved the issue :)

Spring Boot RequestMapping with non-standard produces value returning 406 error when returning JAXB annotated object

I'm creating a Spring Boot app to replace a legacy api application, so all the routes/headers/etc are already set in stone. In that legacy app we used custom Accept headers to include both the version and the content type. So our Accept header is like:
catalog.v1.xml or catalog.v2.json etc.
Here is my request mapping for the method that is handling the request. I'm trying to handle the v1.xml one now. Spring is finding the correct method and the whole method is executed and it returns my JAXB annotated object:
#RequestMapping(value = "/catalog", method = RequestMethod.GET, produces="application/catalog.v1.xml")
How do I make sure Spring finds this matching handler method based on my Accept header, but knows that the output should be XML and marshall my JAXB object accordingly?
You need to provide Spring MVC with an HttpMessageConverter for your custom media type. To do so, I'd take advantage of Spring Boot automatically adding any HttpMessageConverter beans to Spring MVC's default configuration by configuring a bean that knows how to convert application/catalog.v1.xml:
#Bean
public Jaxb2RootElementHttpMessageConverter catalogXmlConverter() {
Jaxb2RootElementHttpMessageConverter xmlConverter = new Jaxb2RootElementHttpMessageConverter();
xmlConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("application", "catalog.v1.xml")));
return xmlConverter;
}
So once I realized I was changing the wrong configuration, and from deep debugging into Spring code, I realized I needed to replace or modify the message converter behavior. Here is my solution below. They don't make it super easy. If anyone has a simpler way of doing this, please let me know. This works.
public void extendMessageConverters(List<HttpMessageConverter<?>> converters)
{
for (HttpMessageConverter<?> converter : converters) {
if (converter.getClass() == MappingJackson2HttpMessageConverter.class){
MappingJackson2HttpMessageConverter jacksonConverter = (MappingJackson2HttpMessageConverter)converter;
MediaType jsonType = new MediaType("application", "catalog.v2.json");
MediaType jsonpType = new MediaType("application", "catalog.v2.jsonp");
List<MediaType> mediatTypes = new ArrayList<>(jacksonConverter.getSupportedMediaTypes());
mediatTypes.add(jsonpType);
mediatTypes.add(jsonType);
jacksonConverter.setSupportedMediaTypes(mediatTypes);
}
else if (converter.getClass() == Jaxb2RootElementHttpMessageConverter.class){
Jaxb2RootElementHttpMessageConverter xmlConverter = (Jaxb2RootElementHttpMessageConverter) converter;
MediaType xmlType = new MediaType("application", "catalog.v1.xml");
// Since the SupportMediaTypes list is unmodifiable, we have to create a new one based on it
// and replace it completely
List<MediaType> mediatTypes = new ArrayList<>(xmlConverter.getSupportedMediaTypes());
mediatTypes.add(xmlType);
xmlConverter.setSupportedMediaTypes(mediatTypes);
}
}
}

Resources