I'm working on a old Spring Application which contains lots of legacy code which needs to survive. Right now I'm working on a more modern approach of API usage and I've stumbled into a problem.
I've Added GSON for converting dates of different formats and front-ends (see below). But this causes a problem at runtime, the #ResponseBody objects become empty.
It all works fine in MockMVC which hooks up the config, but at runtime in Tomcat 8, it has problems. As I've been googling this for quite a while it seems that it could be a problem due to Jackson trying to parse the JSON as well.
Any idea's how I can ensure only GSON is used for JSON requests? (My requests contain only Simple Pojo's with Date, String and Long objects), but can have some nested objects.
Some code snippets:
WebConfig:
#EnableWebMvc
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RestLoggingInterceptor());
}
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new ExtendedGsonHttpMessageConverter());
}
}
ExtendedGsonHttpMessageConverter
public class ExtendedGsonHttpMessageConverter extends GsonHttpMessageConverter
{
private static final String[] DATE_FORMATS = new String[] {
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
"yyyy-MM-dd'T'HH:mm:ss.SSSZ",
"yyyy-MM-dd'T'HH:mmZ",
"yyyy-MM-dd'T'HH:mm:ssZ",
"yyyy-MM-dd'T'HH:mm:ss-'07:00'"
};
public ExtendedGsonHttpMessageConverter()
{
super();
super.setGson(buildGson());
}
protected static Gson buildGson() {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Date.class, new DateDeserializer());
return gsonBuilder.create();
}
private static class DateDeserializer implements JsonDeserializer<Date> {
#Override
public Date deserialize(JsonElement jsonElement, Type typeOF,
JsonDeserializationContext context) throws JsonParseException {
for (String format : DATE_FORMATS) {
try {
return new SimpleDateFormat(format, Locale.GERMANY).parse(jsonElement.getAsString());
} catch (ParseException e) {
}
}
throw new JsonParseException("Unparseable date: \"" + jsonElement.getAsString()
+ "\". Supported formats: " + Arrays.toString(DATE_FORMATS));
}
}
}
The problem was using an interceptor which was logging the request.
Logging the request causes the requestBody to be null, so now I'll be using Springs AbstractRequestLoggingFilter as in the comments.
Related
I'm facing an issue after adding Hibernate4Module to support lazy-objects serialization.
My configuration file:
#EnableWebMvc
#ServletComponentScan(basePackages = "my.backend")
#EnableAutoConfiguration(exclude = MultipartAutoConfiguration.class)
#Configuration
#EnableSwagger2
public class MyConfiguration extends WebMvcConfigurerAdapter{
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(false).
favorParameter(true).
parameterName("mediaType").
ignoreAcceptHeader(true).
useJaf(false).
defaultContentType(MediaType.APPLICATION_JSON).
mediaType("xml", MediaType.APPLICATION_XML).
mediaType("json", MediaType.APPLICATION_JSON);
}
/* Here we register the Hibernate4Module into an ObjectMapper, then set this custom-configured ObjectMapper
* to the MessageConverter and return it to be added to the HttpMessageConverters of our application*/
public MappingJackson2HttpMessageConverter jacksonMessageConverter(){
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper current = messageConverter.getObjectMapper();
//Registering Hibernate4Module to support lazy objects
current.registerModule(new Hibernate4Module());
messageConverter.setObjectMapper(current);
return messageConverter;
}
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//Here we add our custom-configured HttpMessageConverter
converters.add(jacksonMessageConverter());
super.configureMessageConverters(converters);
}
}
Hibernate entities get serializied ok.
The problem is with plain jsons, that where stored in DB or generated in any other way.
For example
String myJson = {"myField":"2123456"}
Is returned in mailformed format
"{\"myField\":"2123456"}
Looks like some default modules get broken. Can anyone give a piece of advice?
I have following Incoming json, which I deserialize to Model.java and then copy that java object to ModelView.java. I wan't to convert date from String to milliseconds and send the Outgoing json as response.
How do I go for it ?
I've specific reason to copy the value from Model.java to ModelView.java using object mapper. So please don't suggest to modify that part. I'm looking to do this via annotation. I'm pretty sure that it can be done, but don't know how.
The json provided here is a simplistic one. I have a large json in actual scenario.
Incoming json
{
"date":"2016-03-31"
}
Outgoing Json
{
"date":236484625196
}
My Controller Class
#Controller
public class SomeController extends BaseController {
#Autowired
private SomeService someService;
#RequestMapping(method = RequestMethod.GET, produces = "application/json")
public #ResponseBody
ResponseEntity<RestResponse> getDetails(HttpServletRequest request, HttpServletResponse response) {
Model model = someService.getData();
ModelView modelView = ModelView.valueOf(model);
return getSuccessResponse(modelView);
}
}
Model.java
#JsonInclude(Include.NON_NULL)
#JsonIgnoreProperties(ignoreUnknown = true)
public class Model implements Serializable {
private String date;
//normal getters and setters
}
ModelView.java
#JsonInclude(Include.NON_NULL)
#JsonIgnoreProperties(ignoreUnknown = true)
public class ModelView implements Serializable {
private Long date;
//normal getters and setters
public static ModelView valueOf(Model model){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd");
ObjectMapper mapper = new ObjectMapper();
ModelView modelView = mapper.convertValue(model, ModelView.class);
try {
modelView.setDate(sdf.parse(model.getDate()).getTime());
} catch (ParseException e) {
IntLogger.error("logging error here");
}
return modelView;
}
}
I'm open to change the variable name from "date" to something else in ModelView.java but the outgoing json should remain same.
Jackson has some build in date formatting, for example, you can set the DateFormatter on the object mapper, but i believe this only works if the serialization and deserialization format is the same.
A simpler approach to date serialization and deserialization, if you want serialization and deserialization to be different format, is to use #JsonSerialize and #JsonDeserialize annotations on your Model.class directly (this could obsolete the need for ModelView if your only purpose was to convert the date).
You can create two classes for serialization and deserialization:
public class JsonDateDeserializer extends JsonDeserializer<Date> {
#Override
public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String dateString = jsonParser.getText();
try {
return dateFormat.parse(dateString);
}
catch (ParseException e) {
throw new RuntimeException(e);
}
}
}
Then for the serialization to your Outgoing json:
public class JsonDateSerializer extends JsonSerializer<Date> {
#Override
public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
jsonGenerator.writeString(Long.toString(date.getTime()));
}
}
Now, you an just annotate your Model.java:
#JsonInclude(Include.NON_NULL)
#JsonIgnoreProperties(ignoreUnknown = true)
public class Model implements Serializable {
#JsonSerialize(using = JsonDateSerializer.class)
#JsonDeserialize(using = JsonDateDeserializer.class)
private String date;
//normal getters and setters
}
Is it possible to register the jackson-datatype-jdk8 module in the Restlet org.restlet.ext.jackson extension package? I need to take advantage of the new Optional feature. My guess is that it should be accessible through the converter services (getConverterService()) but I can't find anything in the documentation that suggests exactly how setting a module is possible.
I eventually pieced together from a variety of sources an answer that works with Restlet 2.3. My guess is that this will be refactored for Restlet 3 and it won't work for versions < 2.3 so be aware that this solution will most likely have a limited shelf life.
First step is to create a custom Jackson converter that implements any custom requirements you have:
public class CustomJacksonConverter extends JacksonConverter {
#Override
protected <T> JacksonRepresentation<T> create(MediaType mediaType, T source) {
ObjectMapper mapper = createMapper();
JacksonRepresentation<T> jr = new JacksonRepresentation<T>(mediaType, source);
jr.setObjectMapper(mapper);
return jr;
}
#Override
protected <T> JacksonRepresentation<T> create(Representation source, Class<T> objectClass) {
ObjectMapper mapper = createMapper();
JacksonRepresentation<T> jr = new JacksonRepresentation<T>(source, objectClass);
jr.setObjectMapper(mapper);
return jr;
}
private ObjectMapper createMapper() {
JsonFactory jsonFactory = new JsonFactory();
jsonFactory.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
ObjectMapper mapper = new ObjectMapper(jsonFactory);
mapper.registerModule(new Jdk8Module());
return mapper;
}
}
You then need to create a way of replacing the default Jackson converter with a copy of your new one:
static void replaceConverter(
Class<? extends ConverterHelper> converterClass,
ConverterHelper newConverter) {
ConverterHelper oldConverter = null;
List<ConverterHelper> converters = Engine.getInstance().getRegisteredConverters();
for (ConverterHelper converter : converters) {
if (converter.getClass().equals(converterClass)) {
converters.remove(converter);
oldConverter = converter;
break;
}
}
converters.add(newConverter);
}
You can now replace the converter in your inbound root:
replaceConverter(JacksonConverter.class, new CustomJacksonConverter());
Great start by #tarka, but gave me stream errors; i propose some [lower impact] refinements:
public class CustomJacksonConverter extends JacksonConverter {
#Override
protected <T> JacksonRepresentation<T> create(MediaType mediaType, T source) {
return new CustomJacksonRepresentation<>(mediaType, source);
}
#Override
protected <T> JacksonRepresentation<T> create(Representation source, Class<T> objectClass) {
return new CustomJacksonRepresentation<>(source, objectClass);
}
}
and then...
public class CustomJacksonRepresentation<T> extends JacksonRepresentation<T> {
public CustomJacksonRepresentation(MediaType mediaType, T object) {
super(mediaType, object);
}
public CustomJacksonRepresentation(Representation representation, Class<T> objectClass) {
super(representation, objectClass);
}
public CustomJacksonRepresentation(T object) {
super(object);
}
#Override
protected ObjectMapper createObjectMapper() {
ObjectMapper mapper = super.createObjectMapper();
mapper.registerModule(new Jdk8Module());
return mapper;
}
}
and replace the converter in the same way #tarka does
After extensive investigations, I wanted to share the problem and the resolution.
Problem
I have a RestController that works well, as long as I'm in charge of converting the JSON message. The moment I try to use an HttpMessageConverter to make the conversion more elegant, the client will start receiving HTTP 406.
So this works:
#RequestMapping(value = "/objects", method = RequestMethod.GET)
public Map<String, Object>[] getObjects(#RequestBody Object jsonQuery) {
MyQuery query = new MyConverter().convert(jsonQuery);
// do something with query
}
But, when I configure the converter, like this:
#Configuration
#EnableWebMvc
#ComponentScan
public class WebConfiguration extends WebMvcConfigurerAdapter {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> httpMessageConverters) {
httpMessageConverters.add(new QueryMessageConverter(new MediaType("application", "json")));
}
}
This causes HTTP 406:
#RequestMapping(value = "/objects", method = RequestMethod.GET)
public Map<String, Object>[] getObjects(#RequestBody Query Query) {
// do something with query
}
My pom.xml only refers spring-boot, and doesn't mention jackson at all.
Solution
See below
The solution is really very simple, and it is to register the jackson handler explicitly:
#Configuration
#EnableWebMvc
#ComponentScan
public class WebConfiguration extends WebMvcConfigurerAdapter {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> httpMessageConverters) {
httpMessageConverters.add(new QueryMessageConverter(new MediaType("application", "json")));
httpMessageConverters.add(new MappingJackson2HttpMessageConverter());
}
}
In a previous similar question, I asked about, how to serialise two different sets of fields using JacksonJson and Spring.
My use case is the typical Controller mapping with #ResponseBody annotation returning directly a particular object or collections of objects, that are then rendered with JacksonJson whenever the client adds application/json in the Accept header.
I had two answers, the first one suggests to return different interfaces with a different getter list, the second suggests to use Json Views.
I don't have problems to understand the first way, however, for the second, after reading the documentation on JacksonJsonViews, I don't know how to implement it with Spring.
To stay with the example, I would declare three stub classes, inside the class Views:
// View definitions:
public class Views {
public static class Public { }
public static class ExtendedPublic extends PublicView { }
public static class Internal extends ExtendedPublicView { }
}
Then I've to declare the classes mentioned:
public class PublicView { }
public class ExtendedPublicView { }
Why on earth they declare empty static classes and external empty classes, I don't know. I understand that they need a "label", but then the static members of Views would be enough. And it's not that ExtendedPublic extends Public, as it would be logical, but they are in fact totally unrelated.
And finally the bean will specify with annotation the view or list of views:
//changed other classes to String for simplicity and fixed typo
//in classname, the values are hardcoded, just for testing
public class Bean {
// Name is public
#JsonView(Views.Public.class)
String name = "just testing";
// Address semi-public
#JsonView(Views.ExtendedPublic.class)
String address = "address";
// SSN only for internal usage
#JsonView(Views.Internal.class)
String ssn = "32342342";
}
Finally in the Spring Controller, I've to think how to change the original mapping of my test bean:
#RequestMapping(value = "/bean")
#ResponseBody
public final Bean getBean() {
return new Bean();
}
It says to call:
//or, starting with 1.5, more convenient (ObjectWriter is reusable too)
objectMapper.viewWriter(ViewsPublic.class).writeValue(out, beanInstance);
So I have an ObjectMapper instance coming out of nowhere and an out which is not the servlet typical PrintWriter out = response.getWriter();, but is an instance of JsonGenerator and that can't be obtained with the new operator. So I don't know how to modify the method, here is an incomplete try:
#RequestMapping(value = "/bean")
#ResponseBody
public final Bean getBean() throws JsonGenerationException, JsonMappingException, IOException {
ObjectMapper objectMapper = new ObjectMapper();
JsonGenerator out; //how to create?
objectMapper.viewWriter(Views.Public.class).writeValue(out, new Bean());
return ??; //what should I return?
}
So I would like to know if anybody had success using JsonView with Spring and how he/she did. The whole concept seems interesting, but the documentation seems lacking, also the example code is missing.
If it's not possible I will just use interfaces extending each others. Sorry for the long question.
Based on the answers by #igbopie and #chrislovecnm, I've put together an annotation driven solution:
#Controller
public class BookService
{
#RequestMapping("/books")
#ResponseView(SummaryView.class)
public #ResponseBody List<Book> getBookSummaries() {}
#RequestMapping("/books/{bookId}")
public #ResponseBody Book getBook(#PathVariable("bookId") Long BookId) {}
}
Where SummaryView is annotated on the Book model like so:
#Data
class Book extends BaseEntity
{
#JsonView(SummaryView.class)
private String title;
#JsonView(SummaryView.class)
private String author;
private String review;
public static interface SummaryView extends BaseView {}
}
#Data
public class BaseEntity
{
#JsonView(BaseView.class)
private Long id;
}
public interface BaseView {}
A custom HandlerMethodReturnValueHandler is then wired into Spring MVC's context to detect the #ResponseView annotation, and apply the Jackson view accordingly.
I've supplied full code over on my blog.
You need to manually wire in the MappingJacksonHttpMessageConverter. In spring 3.1 you are able to use the mvc xml tags like the following:
<mvc:annotation-driven >
<mvc:message-converter>
<bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter" />
</mvc:message-converters>
</mvc:annotation-driven>
It is pretty ugly to not use spring 3.1, it will save you about 20 lines of xml. The mvc:annotation tag does ALOT.
You will need to wire in the object mapper with the correct view writer. I have noticed recently the using a #Configuration class can make complicated wiring like this a lot easier. Use a #Configuration class and create a #Bean with your MappingJacksonHttpMessageConverter, and wire the reference to that bean instead of the MappingJacksonHttpMessageConverter above.
I've manage to solve the problem this way:
Create custom abstract class to contain the json response object:
public abstract AbstractJson<E>{
#JsonView(Views.Public.class)
private E responseObject;
public E getResponseObject() {
return responseObject;
}
public void setResponseObject(E responseObject) {
this.responseObject = responseObject;
}
}
Create a class for each visibility (just to mark the response):
public class PublicJson<E> extends AbstractJson<E> {}
public class ExtendedPublicJson<E> extends AbstractJson<E> {}
public class InternalJson<E> extends AbstractJson<E> {}
Change your method declaration:
#RequestMapping(value = "/bean")
#ResponseBody
public final PublicJson<Bean> getBean() throws JsonGenerationException, JsonMappingException, IOException {
return new PublicJson(new Bean());
}
Create customs MessageConverter:
public class PublicJsonMessageConverter extends MappingJacksonHttpMessageConverter{
public PublicApiResponseMessageConverter(){
super();
org.codehaus.jackson.map.ObjectMapper objMapper=new org.codehaus.jackson.map.ObjectMapper();
objMapper.configure(SerializationConfig.Feature.DEFAULT_VIEW_INCLUSION, false);
objMapper.setSerializationConfig(objMapper.getSerializationConfig().withView(Views.Public.class));
this.setObjectMapper(objMapper);
}
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
if(clazz.equals(PublicJson.class)){
return true;
}
return false;
}
}
public class ExtendedPublicJsonMessageConverter extends MappingJacksonHttpMessageConverter{
public ExtendedPublicJsonMessageConverter(){
super();
org.codehaus.jackson.map.ObjectMapper objMapper=new org.codehaus.jackson.map.ObjectMapper();
objMapper.configure(SerializationConfig.Feature.DEFAULT_VIEW_INCLUSION, false);
objMapper.setSerializationConfig(objMapper.getSerializationConfig().withView(Views.ExtendedPublic.class));
this.setObjectMapper(objMapper);
}
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
if(clazz.equals(ExtendedPublicJson.class)){
return true;
}
return false;
}
}
public class InternalJsonMessageConverter extends MappingJacksonHttpMessageConverter{
public InternalJsonMessageConverter(){
super();
org.codehaus.jackson.map.ObjectMapper objMapper=new org.codehaus.jackson.map.ObjectMapper();
objMapper.configure(SerializationConfig.Feature.DEFAULT_VIEW_INCLUSION, false);
objMapper.setSerializationConfig(objMapper.getSerializationConfig().withView(Views.Internal.class));
this.setObjectMapper(objMapper);
}
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
if(clazz.equals(Internal.class)){
return true;
}
return false;
}
}
Add the following to your xml:
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="PublicJsonMessageConverter"></bean>
<bean class="ExtendedPublicJsonMessageConverter"></bean>
<bean class="InternalJsonMessageConverter"></bean>
</mvc:message-converters>
</mvc:annotation-driven>
That's it! I had to update to spring 3.1 but that's all. I use the responseObject to send more info about the json call but you can override more methods of the MessageConverter to be completely transparent. I hope someday spring include an annotation for this.
Hope this helps!