Spring Rest and Jsonp

2019-03-27 01:59发布

问题:

I am trying to get my Spring rest controller to return jsonp but I am having no joy

The exact same code works ok if I want to return json but I have a requirement to return jsonp I have added in a converter I found source code for online for performing the jsonp conversion

I am using Spring version 4.1.1.RELEASE and Java 7

Any help is greatly appreciated

Here is the code in question

mvc-dispatcher-servlet.xml

    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:mvc="http://www.springframework.org/schema/mvc" 
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans     
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context.xsd
         http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">


  <bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
       <property name="favorPathExtension" value="false" />
       <property name="favorParameter" value="true" />
       <property name="parameterName" value="mediaType" />
       <property name="ignoreAcceptHeader" value="false"/>
       <property name="useJaf" value="false"/>
       <property name="defaultContentType" value="application/json" />

       <property name="mediaTypes">
            <map>
                <entry key="atom"  value="application/atom+xml" />
                <entry key="html"  value="text/html" />
                <entry key="jsonp" value="application/javascript" />
                <entry key="json"  value="application/json" />
                <entry key="xml"   value="application/xml"/>
            </map>
        </property>
  </bean>

    <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
        <property name="contentNegotiationManager" ref="contentNegotiationManager" />
        <property name="viewResolvers">
            <list>
                <bean class="org.springframework.web.servlet.view.BeanNameViewResolver" />
                <bean
                    class="org.springframework.web.servlet.view.InternalResourceViewResolver">
                    <property name="prefix" value="/WEB-INF/templates/slim/${views.template.directory}/" />
                    <property name="suffix" value=".jsp" />
                </bean>
            </list>
        </property>
        <property name="defaultViews">
            <list>
                <bean class="com.webapp.handler.MappingJacksonJsonpView" />
            </list>
        </property>
    </bean>

</beans>

com.webapp.handler.MappingJacksonJsonpView

package com.webapp.handler;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;

public class MappingJacksonJsonpView extends MappingJackson2JsonView {
    /** Local log variable. **/
    private static final Logger LOG = LoggerFactory.getLogger(MappingJacksonJsonpView.class);

    /**
     * Default content type. Overridable as bean property.
     */
    public static final String DEFAULT_CONTENT_TYPE = "application/javascript";

    @Override
    public String getContentType() {
        return DEFAULT_CONTENT_TYPE;
    }

    /**
     * Prepares the view given the specified model, merging it with static
     * attributes and a RequestContext attribute, if necessary.
     * Delegates to renderMergedOutputModel for the actual rendering.
     * @see #renderMergedOutputModel
     */
    @Override
    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        LOG.info("Entered render Method :{}", request.getMethod());

        if("GET".equals(request.getMethod().toUpperCase())) {
            LOG.info("Request Method is a GET call");

            Map<String, String[]> params = request.getParameterMap();

            if(params.containsKey("callback")) {
                String callbackParam = params.get("callback")[0];
                LOG.info("callbackParam:{}", callbackParam);
                response.getOutputStream().write(new String(callbackParam + "(").getBytes());
                super.render(model, request, response);
                response.getOutputStream().write(new String(");").getBytes());
                response.setContentType(DEFAULT_CONTENT_TYPE);
            }
            else {
                LOG.info("Callback Param not contained in request");
                super.render(model, request, response);
            }
        }

        else {
            LOG.info("Request Method is NOT a GET call");
            super.render(model, request, response);
        }
    }
}

Controller Method In Question

 @RequestMapping(value = { "/sources"}, method = RequestMethod.GET, 
        produces={MediaType.ALL_VALUE,
        "text/javascript",
        "application/javascript",
        "application/ecmascript",
        "application/x-ecmascript",
        "application/x-javascript", 
        MediaType.APPLICATION_JSON_VALUE})
@ResponseBody
public Object getSources(@PathVariable(value = API_KEY) String apiKey, 
        @RequestParam(value = "searchTerm", required = true) String searchTerm,
        @RequestParam(value = "callBack", required = false) String callBack) {
    LOG.info("Entered getSources - searchTerm:{}, callBack:{} ", searchTerm, callBack);

    List<SearchVO> searchVOList = myServices.findSources(searchTerm);

    if (CollectionUtils.isEmpty(searchVOList)) {
        LOG.error("No results exist for the searchterm of {}", searchTerm);
        return searchVOList;
    }         

    LOG.debug("{} result(s) exist for the searchterm of {}", searchVOList.size(), searchTerm);        

    LOG.info("Exiting getSources");
    return searchVOList;
}

**Jquery Ajax Code **

$.ajax({
                type: "GET",
                url: "http://localhost:8080/my-web/rest/sources,
                data: {
                    "searchTerm": request.term
                },
                //contentType: "application/json; charset=utf-8",
                //dataType: "json",
                contentType: "application/javascript",
                dataType: "jsonp",
                 success: function(data) {
                    alert("success");
                },
                error: function(XMLHttpRequest, textStatus, errorThrown) {
                    alert("Failure");
                }
            });

A snippet of the error stacktrace that I get is as follows

org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:168) ~[spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:101) ~[spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:198) ~[spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:71) ~[spring-web-4.1.1.RELEASE.jar:4.1.1.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:122) ~[spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:781) ~[spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:721) ~[spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:83) ~[spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:943) ~[spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:877) ~[spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:966) [spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:857) [spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:620) [servlet-api.jar:na]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:842) [spring-webmvc-4.1.1.RELEASE.jar:4.1.1.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:727) [servlet-api.jar:na]

回答1:

As stated on the spring.io blog regarding the Spring 4.1 release:

JSONP is now supported with Jackson. For response body methods declare an @ControllerAdvice as shown below. For View-based rendering simply configure the JSONP query parameter name(s) on MappingJackson2JsonView.

@ControllerAdvice
private static class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {

    public JsonpAdvice() {
        super("callback");
    }

}

[...] In 4.1 an @ControllerAdvice can also implement ResponseBodyAdvice in which case it will be called after the controller method returns but before the response is written and therefore committed. This has a number of useful applications with @JsonView the JSONP already serving as two examples built on it.

Javadoc taken from MappingJackson2JsonView:

Set JSONP request parameter names. Each time a request has one of those parameters, the resulting JSON will be wrapped into a function named as specified by the JSONP request parameter value. The parameter names configured by default are "jsonp" and "callback".

You don't need to implement this stuff by yourself. Just reuse the bits from the Spring Framework.

Spring Boot example

Following simple Spring Boot application demonstrates use of build in JSONP support in Spring MVC 4.1. Example requires at least Spring Boot 1.2.0.RC1.

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdvice;

import java.util.Collections;

import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

@RestController
@SpringBootApplication
class Application {

    @JsonAutoDetect(fieldVisibility = ANY)
    static class MyBean {
        String attr = "demo";
    }

    @ControllerAdvice
    static class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {
        public JsonpAdvice() {
            super("callback");
        }
    }

    @Bean
    public HttpMessageConverters customConverters() {
        return new HttpMessageConverters(false, Collections.<HttpMessageConverter<?> >singleton(new MappingJackson2HttpMessageConverter()));
    }

    @RequestMapping
    MyBean demo() {
        return new MyBean();
    }

    @RequestMapping(produces = APPLICATION_JSON_VALUE)
    String demo2() {
        return "demo2";
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

URL http://localhost:8080/demo?callback=test converts a POJO into a JSONP response:

test({"attr":"demo"});

URL http://localhost:8080/demo2?callback=test converts a String into a JSONP response:

test("demo2");