Skip to content

Instantly share code, notes, and snippets.

@ucpwang
Created October 14, 2015 08:46
Show Gist options
  • Save ucpwang/949145408a12bb40a671 to your computer and use it in GitHub Desktop.
Save ucpwang/949145408a12bb40a671 to your computer and use it in GitHub Desktop.
Spring RestTemplate / multipart request korean parameter value encoding error

Spring RestTemplate / multipart request korean parameter value encoding error

spring:spring-web 3.2.2 에서 FormHttpMessageConverter에서 사용하는 partConverters 중 StringHttpMessageConverter 는 기본 charset이 ISO-8859-1 이다. 이로 인해서 Spring RestTemplate 을 통해서 multipart/form-data 형식으로 요청할 경우 파라미터의 인코딩이 깨지게 된다.

분석을 위해 코드를 열어보자.

RestTemplate 에서 별도 설정 없이 multipart/form-data 형식의 요청을 날리면, FormHttpMessageConverter 을 확장한 AllEncompassingFormHttpMessageConverter 를 통해서 컨버팅되어진다. 이때 AllEncompassingFormHttpMessageConverter 에서는 xml, json 등 타입에 따라 FormHttpMessageConverter.partConverters 외에도 더 필요한 컨버터가 있다면 add해주는 역할만 할뿐 별다른 행위를 하지는 않는다. 문제는 FormHttpMessageConverter 기본 생성자에 있다.

아래는 org.springframework.http.converter.FormHttpMessageConverter 의 생성자 부분 코드이다.

...
public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {

    private static final byte[] BOUNDARY_CHARS =
            new byte[]{'-', '_', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
                    'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A',
                    'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
                    'V', 'W', 'X', 'Y', 'Z'};

    private final Random rnd = new Random();

    private Charset charset = Charset.forName("UTF-8");

    private List<MediaType> supportedMediaTypes = new ArrayList<MediaType>();

    private List<HttpMessageConverter<?>> partConverters = new ArrayList<HttpMessageConverter<?>>();


    public FormHttpMessageConverter() {
        this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
        this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);

        this.partConverters.add(new ByteArrayHttpMessageConverter());
        StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(); <------ StringHttpMessageConverter 를 기본 생성자를 이용하여 인스턴스화 함
        stringHttpMessageConverter.setWriteAcceptCharset(false);
        this.partConverters.add(stringHttpMessageConverter);
        this.partConverters.add(new ResourceHttpMessageConverter());
    }


    /**
     * Set the message body converters to use. These converters are used to convert objects to MIME parts.
     */
    public final void setPartConverters(List<HttpMessageConverter<?>> partConverters) {
        Assert.notEmpty(partConverters, "'partConverters' must not be empty");
        this.partConverters = partConverters;
    }

    /**
     * Add a message body converter. Such a converters is used to convert objects to MIME parts.
     */
    public final void addPartConverter(HttpMessageConverter<?> partConverter) {
        Assert.notNull(partConverter, "'partConverter' must not be NULL");
        this.partConverters.add(partConverter);
    }
...

화살표로 표시한것 처럼 StringHttpMessageConverter 를 기본 생성자를 이용하여 인스턴스화 하여 partConverters 에 add 하고 있다.

아래는 org.springframework.http.converter.StringHttpMessageConverter 의 생성자 부분 코드이다.

...
public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {

    public static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1"); <------ 헉 찾았다..

    private final Charset defaultCharset;

    private final List<Charset> availableCharsets;

    private boolean writeAcceptCharset = true;


    /**
     * A default constructor that uses {@code "ISO-8859-1"} as the default charset. <------ 미국에서 태어났더라면...삽질을 안했을텐데.
     * @see #StringHttpMessageConverter(Charset)
     */
    public StringHttpMessageConverter() {
        this(DEFAULT_CHARSET);
    }

    /**
     * A constructor accepting a default charset to use if the requested content
     * type does not specify one.
     */
    public StringHttpMessageConverter(Charset defaultCharset) {
        super(new MediaType("text", "plain", defaultCharset), MediaType.ALL);
        this.defaultCharset = defaultCharset;
        this.availableCharsets = new ArrayList<Charset>(Charset.availableCharsets().values());
    }
...

자, 그럼 어떻게 이 난관을 극복했는지 살펴보자.

...
this.restTemplate = new RestTemplate();

/* FormHttpMessageConverter > partConverters > StringHttpMessageConverter 의 기본 인코딩타입이 ISO-8859-1 여서 multipart/form-data 로 파라미터를 넘길 경우 한글인코딩이 깨짐 */
for (HttpMessageConverter<?> hmc : restTemplate.getMessageConverters()) {
    if (hmc instanceof AllEncompassingFormHttpMessageConverter) {

        /** AllEncompassingFormHttpMessageConverter 생성자 내용 일부 가져와서 수정 **/
        List<HttpMessageConverter<?>> partConverterList = new ArrayList<HttpMessageConverter<?>>();
        partConverterList.add(new ByteArrayHttpMessageConverter());
        StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(Charset.forName("UTF-8")); <------ StringHttpMessageConverter Charset 오버로딩 생성자를 통해서 해결하였다.
        stringHttpMessageConverter.setWriteAcceptCharset(false);
        partConverterList.add(stringHttpMessageConverter);
        partConverterList.add(new ResourceHttpMessageConverter());
        partConverterList.add(new SourceHttpMessageConverter());
        if (ClassUtils.isPresent("javax.xml.bind.Binder", AllEncompassingFormHttpMessageConverter.class.getClassLoader())) {
            partConverterList.add(new Jaxb2RootElementHttpMessageConverter());
        }
        if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", AllEncompassingFormHttpMessageConverter.class.getClassLoader())
                && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", AllEncompassingFormHttpMessageConverter.class.getClassLoader())) {
            partConverterList.add(new MappingJackson2HttpMessageConverter());
        }
        else if (ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", AllEncompassingFormHttpMessageConverter.class.getClassLoader())
                && ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator", AllEncompassingFormHttpMessageConverter.class.getClassLoader())) {
            partConverterList.add(new MappingJacksonHttpMessageConverter());
        }

        ((AllEncompassingFormHttpMessageConverter) hmc).setPartConverters(partConverterList);
    }
}
...

그닥 깊은 설명은 필요없을듯하다. 사실 http client 를 직접 구현하기 싫어서 spring restclient 를 사용한건데 인코딩 땜에 너무 진을 뺏다. 게다가... 버전이 3.2.2 라니... 이휴... 언젠가는 잊혀질 공룡코드로 남겠지만 잠시 머릿속에 각인을 위해 남겨본다.

@ucpwang
Copy link
Author

ucpwang commented Oct 14, 2015

@seotory
Copy link

seotory commented Nov 17, 2015

RestTemplate rest = new RestTemplate();
rest.getMessageConverters()
.add(0, new StringHttpMessageConverter(Charset.forName("UTF-8")));
이건 어떤가요.

@ucpwang
Copy link
Author

ucpwang commented Feb 1, 2016

@seotory 말씀주신 부분으로도 해봤던거 같은데요~ 해결되지 않았기에 어쩔수 없이 코드를 까보게된것 같습니다.

@yks8890
Copy link

yks8890 commented May 10, 2017

덕분에 한글 문제 해결되었어요
그런데 파일인코딩이 여전히 깨집니다. multipartfile.getorinalfilename은 인코딩이 적용이안되네여
???_image.jpg 이런식으로 파일이름이 한글이 깨져서 나옵니다

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment