2

Currently, I need to send a large json object to an ajax request. For the purpose I am using the following controller method which works fine.

   @RequestMapping(method = RequestMethod.POST,params = {"dynamicScenario"})
   @ResponseBody
    public String getDynamicScenarioData(@RequestParam Map<String, String> map) throws JsonParseException, JsonMappingException, IOException
 {

  ObjectMapper mapper = new ObjectMapper();

    @SuppressWarnings("unchecked")
    Map<String,Object> queryParameters = mapper.readValue(map.get("parameters") , Map.class);

    Map<String, Object> getData = service.runDynamicScenario(queryParameters, map.get("queryString"));

    return writer.writeValueAsString(getData); //here java throws java.lang.OutOfMemoryError: Java heap space memory
} 

Update: my ajax is:

          $.ajax({
          type: "POST",
          url: "dynamicScenario.htm",
          data : tags,
          dataType: "json",
          success: function(data){});

My DispatcherServlet settings:

           public class ApplicationInitializer implements      WebApplicationInitializer
      {
       public void onStartup(ServletContext servletContext) throws      
     ServletException 
     {
    AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
    context.register(ApplicationConfig.class);

    servletContext.addListener(new ContextLoaderListener(context));

    ServletRegistration.Dynamic servletRegistration = servletContext.addServlet("dispatcher", new DispatcherServlet(context));
    servletRegistration.setLoadOnStartup(1);
    servletRegistration.addMapping("*.htmlx");
  }
}

I am using jackson to serialize a map of different objects and then send it back to the ajax. However, if the size of the json is large then java throws out of memory. I know the Jackson method writer.writeValueAsString is inefficient because its writing to a string but is there any other alternative? I can't use plain java POJO because I don't know what objects the map which I will have to serialize will contain so I can't simply map it to some java object. Any ideas? Thanks

Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724
george
  • 3,102
  • 5
  • 34
  • 51
  • Why aren't you making your method return type `Map` and returning `getData`? Jackson should be able to stream while serializing it. – Sotirios Delimanolis Jul 17 '15 at 14:37
  • I've tried but I am getting Status Code:406 Not Acceptable in the response.. – george Jul 17 '15 at 14:39
  • What `Accept` header are you sending in your request? – Sotirios Delimanolis Jul 17 '15 at 14:40
  • I've pasted my ajax..my dataType is "json" so it should work.. – george Jul 17 '15 at 14:41
  • Show us your MVC configuration. Also, don't send JSON as form parameters. Send it directly in the request body and use `@RequestBody` to have Spring deserialize it to a `Map` directly. – Sotirios Delimanolis Jul 17 '15 at 14:44
  • I know but I'have a few parameters in my request map so it's not too bad..I am more interested in the @reponseBody why it's not working..what config you want me to show you? – george Jul 17 '15 at 14:46
  • Anything related to MVC, ie. the context configuration passed to the `DispatcherServlet`. – Sotirios Delimanolis Jul 17 '15 at 14:47
  • I've added some of the settings..there's nothing special..so I don't think that's the issue. do you know a way to write to a stream in the response? – george Jul 17 '15 at 14:52
  • You're double loading the `context` right now, passing it both to the `ContextLoaderListener` and to the `DispatcherServlet`. Pass it only to the loader. Then show us the config, `ApplicationConfig`. – Sotirios Delimanolis Jul 17 '15 at 14:54
  • sry but I don't understand how's that related to my problem? There's nothing special in my appConfig related to the mvc.it's only freemarker stuff. The issue should be related to the headers.. – george Jul 17 '15 at 14:57
  • I want to see if you have any custom HttpMessageConverter instances registered. If you think it's a question of headers, open your browser's network console and check what is actually sent. – Sotirios Delimanolis Jul 17 '15 at 15:02
  • I don't have anything like HttpMessageConverter just freemarker stuff. The Status Code:406 Not Acceptable is exactly in the browser and it is weird since it works if I return jackson.writer serialized object but not the map directly – george Jul 17 '15 at 15:07
  • This is probably content negotiation issue then. Get rid of the `.htm` extension in your request URL. – Sotirios Delimanolis Jul 17 '15 at 15:08

1 Answers1

8

The issue you want to solve is

return writer.writeValueAsString(getData); 

creating too big of a String and causing an OutOfMemoryError. Jackson supports a Streaming API, which Spring makes use of in its MappingJackson2HttpMessageConverter, which handles serializing POJOs to JSON in the response body (and request body).

There are a few ways to handle this. The easiest is to change your return type to Map<String, Object and return the corresponding object directly.

@RequestMapping(method = RequestMethod.POST,params = {"dynamicScenario"})
@ResponseBody
public Map<String, Object> getDynamicScenarioData(@RequestParam Map<String, String> map) throws JsonParseException, JsonMappingException, IOException
 {
     ObjectMapper mapper = new ObjectMapper();

     @SuppressWarnings("unchecked")
     Map<String,Object> queryParameters = mapper.readValue(map.get("parameters") , Map.class);

     Map<String, Object> getData = service.runDynamicScenario(queryParameters, map.get("queryString"));

     return getData;
} 

Spring will take care of serializing getData by streaming the results directly to the response OutputStream.

This won't work on its own. For one, the url you are using to access your service

/dynamicScenario.htm

is causing Spring's content negotiation to kick in. It sees .htm and thinks you are expecting text/html content. Either turn off content negotiation or use a URL without an extension

/dynamicScenario

Spring will then, instead, look at the Accept headers for figuring out what your client is expecting. Since that is

dataType: "json"

it's going to write application/json with the MappingJackson2HttpMessageConverter.


Here are some more things to fix

AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(ApplicationConfig.class);

servletContext.addListener(new ContextLoaderListener(context));

ServletRegistration.Dynamic servletRegistration = servletContext.addServlet("dispatcher", new DispatcherServlet(context));
servletRegistration.setLoadOnStartup(1);
servletRegistration.addMapping("*.htmlx");

You're currently making both the ContextLoaderListener and DispatcherServlet load the same ApplicationContext. This is unnecessary and potentially harmful. The DispatcherServlet already uses the ContextLoaderListener context as a parent context. You can read more about it here:

If your ApplicationConfig only contains MVC configuration elements, only have the DispatcherServlet load it. You won't need a ContextLoaderListener.

If it has other types of config elements unrelated to the MVC stack, split it into two @Configuration classes. Pass the MVC one to the DispatcherServlet and the other to the ContextLoaderListener.

Don't pass JSON as form parameters. Pass it directly into the request body and use @RequestBody to deserialize it directly into your expected type.

@RequestMapping(method = RequestMethod.POST)
@ResponseBody
public Map<String, Object> getDynamicScenarioData(@RequestBody Map<String,Object> queryParameters) throws JsonParseException, JsonMappingException, IOException
 {
     Map<String, Object> getData = service.runDynamicScenario(queryParameters, /* find a better way to pass this map.get("queryString") */);

     return getData;
}

You save yourself an ObjectMapper object on each request (it's a heavy object) and you let Spring take care of all the deserializing (again streaming). You'll have to find another way to pass the queryString (you can still pass it as a single query parameter and get it with `@RequestParam).

Community
  • 1
  • 1
Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724