Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deferred Result in Spring MVC returning incorrect response

I am using spring mvc 3.2.4 and jquery 1.9.0 for long polling. My application is deployed on Tomcat 7.0.42. My spring configuration files are as below:

Application Web.xml

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee"   
  version="3.0">

    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>

    <servlet>
        <servlet-name>app</servlet-name>

        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>

        <load-on-startup>1</load-on-startup>
        <async-supported>true</async-supported>
    </servlet>

    <servlet-mapping>
        <servlet-name>app</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            /WEB-INF/app-servlet.xml
        </param-value>
    </context-param>

</web-app>

Spring Configration xml as:-

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
    http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-3.2.xsd">
        <context:annotation-config/>
        <mvc:annotation-driven/>  
        <mvc:default-servlet-handler/>
        <context:component-scan base-package="com.webchat"/>
        <bean id="defferedResult" class="com.exp.DeferredResultContainer"></bean>
</beans>

Controller for Posting data looks as

@RequestMapping(value = "/postComment", method = RequestMethod.POST)
public @ResponseBody String postComment(HttpServletRequest request) {
    deferredResultContainer.updateAllResults(request.getParameter("comment"));
    return "success";
}

Deferred Result container class

public class DeferredResultContainer {
    private final Set<DeferredResult<String>> deferredResults =   Collections.synchronizedSet(new HashSet<DeferredResult<String>>() ); 

    public void put(DeferredResult<String> deferredResult){ 
        deferredResults.add(deferredResult); 
    } 

    public void updateAllResults(String value){
        for (DeferredResult<String> deferredResult : deferredResults){ 
            deferredResult.setResult(value); 
        }
    }

    public void remove(DeferredResult<String> deferredResult){ 
        deferredResults.remove(deferredResult); 
    } 

    public int determineSize(){
        return deferredResults.size();
    }
}

Controller for Deferred Result looks as

 @RequestMapping(value = "/getComments", method = RequestMethod.GET)
 @ResponseBody
 public DeferredResult<String> getComments() throws Exception{
     final DeferredResult<String> deferredResult= new DeferredResult<String>(); 
     deferredResultContainer.put(deferredResult);
     deferredResult.onTimeout(new Runnable() {

         @Override public void run() {
             deferredResultContainer.remove(deferredResult);
         }
     });

     deferredResult.onCompletion(new Runnable() { 
         @Override public void run() { 
             deferredResultContainer.remove(deferredResult); 
         } 
     });
     return deferredResult;
 }

When I am trying to long poll it through ajax i am getting following response :-

{"setOrExpired":false}

And onCompletion method is also not getting executed.

To Simply things below controller gives perfect response as {"1":"2"}

@RequestMapping(value = "/test1", method = RequestMethod.GET)
@ResponseBody
public Map test1() throws Exception{
     Map m1 = new HashMap<String, Object>();
     m1.put("1", "2");
     return m1;
}

Once i change it to below and add Deferred result i get response as {"setOrExpired":true}

@RequestMapping(value = "/test", method = RequestMethod.GET)
@ResponseBody
public DeferredResult<Map> test() throws Exception{
    DeferredResult<Map> result = new DeferredResult<Map>();
     Map m1 = new HashMap<String, Object>();
     m1.put("1", "2");
     result.setResult(m1);
     return result;
}

Polling code

$(document).ready(function() {
    longPoll();

    function longPoll(){
         $.support.cors = true;
        var path = "http://localhost:8080/WebChatExp/rest";
         $.ajax({
            url: path + "/getComments",
            cache:false,
            success: function(data){
                //To Do
                            alert("Data" + JSON.stringify(data));
            },
            error: function(err, status, errorThrown ) {

            },
            type: "GET",
            dataType: "json",
            complete: longPoll,
            timeout: 60000 // timeout every one minute
        }); 
    }

I have searched various examples but cannot figure out if any extra configuration is required for deferred result. Please advise.

like image 682
Khalsa_it Avatar asked Nov 29 '13 05:11

Khalsa_it


2 Answers

The response body you are getting

{"setOrExpired":true}

indicates that Spring serialized your DeferredResult (which has various properties including setOrExpired) to JSON instead of handling it with a DeferredResultMethodReturnValueHandler. In other words, it used another HandlerMethodReturnValueHandler, most likely RequestResponseBodyMethodProcessor (which handles @ResponseBody), to handle the value returned from your @RequestMapping annotated handler method. (The simplest way to test this is to see what happens when you remove the @ResponseBody annotation.)

Looking at the 3.2.x source code of RequestMappingHandlerAdapter, which registers the default HandlerMethodReturnValueHandler instances, the DeferredResultMethodReturnValueHandler is registered before RequestResponseBodyMethodProcessor and therefore will handle the DeferredResult return value first.

Since you're seeing different behavior, we must assume your configuration is not what you've shown here. (Note that <mvc:annotation-driven/> registers a RequestMappingHandlerAdapter.)


Also note that you are currently loading the configuration in /WEB-INF/app-servlet.xml twice, once by the ContextLoaderListener and once by the DispatcherServlet.

Get rid of your ContextLoaderListener and the corresponding context-param entirely. They are not needed in your example.

like image 127
Sotirios Delimanolis Avatar answered Oct 21 '22 14:10

Sotirios Delimanolis


I know is impossible to be the problem in this case (spring mvc 3.2.4) but just for future references: I get the same issue with spring boot 2.2.0.BUILD-SNAPSHOT (spring 5.1.5). This error happens when you try to return DeferredResult using webflux instead of the traditional spring mvc app. Remember, in the new webflux api you should use mono/flux, not DeferredResult.

like image 32
domgom Avatar answered Oct 21 '22 16:10

domgom