2014-06-08

如何讓 spring-mvc 關閉 chunked 回應

最近使用 spring-mvc 回傳 json 格式資料,發現它總是以 chunked 的方式回應資料給客戶端,對此感到好奇,想嘗試關閉這個選項,才發現這並不容易,因為它並不是個選項。

基於挑戰心態,還是實作了某種解法:
  1. 首先建立一個自己的 JacksonHttpMessageConverter 實作。
  2. 在載入階段更換預載的 MappingJacksonHttpMessageConverter 實例。
  3. 完成。


以下這段 xml 是在描述 spring 載入 mvc 時的設置:
  <mvc:annotation-driven/>
  <bean class="funweb.spring.JsonHackProcessor"/>

JsonHackProcessor 實作

package funweb.spring;

import java.util.ArrayList;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter;

public class JsonHackProcessor implements BeanPostProcessor {

    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof AnnotationMethodHandlerAdapter) {
            AnnotationMethodHandlerAdapter adapter = (AnnotationMethodHandlerAdapter) bean;
            HttpMessageConverter<?>[] converters = adapter.getMessageConverters();

            ArrayList<HttpMessageConverter<?>> list = new ArrayList<>();
            for (HttpMessageConverter<?> converter : converters) {
                if (converter instanceof MappingJacksonHttpMessageConverter) {
                    list.add(new JacksonHttpMessageConverter());
                    continue;
                }
                list.add(converter);
            }

            converters = list.toArray(converters);
            adapter.setMessageConverters(converters);
        }
        return bean;
    }

}
大致上的做法是透過 AnnotationMethodHandlerAdapter 類別取出預載的 MessageConverter 實例陣列,其中一個會是 MappingJacksonHttpMessageConverter 實例,將它替換成我們的實例即可。


JacksonHttpMessageConverter 實作

package funweb.spring;

import java.io.*;
import java.nio.charset.Charset;
import java.util.*;
import org.apache.commons.io.IOUtils;
import org.codehaus.jackson.*;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.type.TypeFactory;
import org.codehaus.jackson.type.JavaType;
import org.springframework.http.*;
import org.springframework.http.converter.*;

public class JacksonHttpMessageConverter implements HttpMessageConverter<Object> {

    private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
    private final MediaType supportedMediaType = new MediaType("application", "json", DEFAULT_CHARSET);
    private final List<MediaType> supportedMediaTypes = Collections.unmodifiableList(Collections.singletonList(supportedMediaType));
    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        JavaType javaType = getJavaType(clazz);
        return this.objectMapper.canDeserialize(javaType) && canRead(mediaType);
    }

    protected boolean canRead(MediaType mediaType) {
        if (mediaType == null) {
            return true;
        }
        return supportedMediaType.includes(mediaType);
    }

    @SuppressWarnings("deprecation")
    protected JavaType getJavaType(Class<?> clazz) {
        return TypeFactory.type(clazz);
    }

    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        return this.objectMapper.canSerialize(clazz) && canWrite(mediaType);
    }

    protected boolean canWrite(MediaType mediaType) {
        if (mediaType == null || MediaType.ALL.equals(mediaType)) {
            return true;
        }
        return supportedMediaType.isCompatibleWith(mediaType);
    }

    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return this.supportedMediaTypes;
    }

    @Override
    public Object read(Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        JavaType javaType = getJavaType(clazz);
        try {
            return this.objectMapper.readValue(inputMessage.getBody(), javaType);
        }
        catch (JsonParseException ex) {
            throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex);
        }
    }

    @Override
    public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {

        HttpHeaders headers = outputMessage.getHeaders();
        if (headers.getContentType() == null) {
            if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
                contentType = supportedMediaType;
            }
            if (contentType != null) {
                headers.setContentType(contentType);
            }
        }

        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        JsonEncoding encoding = getEncoding(outputMessage.getHeaders().getContentType());
        JsonGenerator jsonGenerator = this.objectMapper.getJsonFactory().createJsonGenerator(outStream, encoding);
        try {
            this.objectMapper.writeValue(jsonGenerator, o);
            byte[] buff = outStream.toByteArray();
            headers.setContentLength(outStream.size());
            IOUtils.copy(new ByteArrayInputStream(buff), outputMessage.getBody());
        }
        catch (JsonGenerationException ex) {
            throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
        }

        outputMessage.getBody().flush();
    }

    private JsonEncoding getEncoding(MediaType contentType) {
        if (contentType != null && contentType.getCharSet() != null) {
            Charset charset = contentType.getCharSet();
            for (JsonEncoding encoding : JsonEncoding.values()) {
                if (charset.name().equals(encoding.getJavaName())) {
                    return encoding;
                }
            }
        }
        return JsonEncoding.UTF8;
    }

}
可別被上面這段程式碼給嚇著了,其實也不過是將原始的 MappingJacksonHttpMessageConverter.java 拿來塗改罷了,有興趣的人可以拿來比較看看,其實上面這個版本比較簡單;並突破了上層類別的限制。

沒有留言 :

張貼留言