SpringMVC返回字符串中文亂碼


一個例子
Spring版本為5.1.7

Controller中的方法如下:

@ResponseBody
@RequestMapping(value = "/call/{name}")
public String callSomeone(@PathVariable("name")String name) {
    return "call     "+name;
}

當這個方法被請求時會返回給瀏覽器一個字符串,現在遇到的問題是當name為中文時返回的字符串會亂碼。
發現亂碼的原因為response的Content-Type為text/html;charset=ISO_8859_1,charset應為UTF-8

設置了CharacterEncodingFilter之后還是有亂碼,暫不清楚原因。

方式一,指定RequestMapping的produces屬性:

@ResponseBody
@RequestMapping(value = "/call/{name}", produces = "text/html;charset=utf-8")
public String callSomeone(@PathVariable("name")String name) {
    return "call     "+name;
}

方式二,修改StringHttpMessageConverter的DefaultCharset屬性(先說做法,在說分析過程):
做法:新建一個工具類 AppUtil實現ApplicationContextAware接口,並監聽ContextRefreshedEvent事件

@Component
public class AppUtil implements ApplicationContextAware, ApplicationListener<ContextRefreshedEvent> {
    private ApplicationContext app;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.app = applicationContext;
    }

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        try {
            RequestMappingHandlerAdapter requestMappingHandlerAdapter = app.getBean(RequestMappingHandlerAdapter.class);
            if(requestMappingHandlerAdapter!=null) {
                List<HttpMessageConverter<?>> messageConverters = requestMappingHandlerAdapter.getMessageConverters();
                if(messageConverters!=null) {
                    // 獲取bean容器中的StringHttpMessageConverter,並修改DefaultCharset屬性
                    for(HttpMessageConverter item : messageConverters) {
                        if(item instanceof StringHttpMessageConverter) {
                            ((StringHttpMessageConverter) item).setDefaultCharset(StandardCharsets.UTF_8);
                        }
                    }
                }
            }
        }catch (BeansException e) {

        }
    }
}

-------------------到這里兩種解決方式已經說完了,下面說一下第二種解決方式的思路--------------------
經查資料知道SpringMVC返回字符串的編碼與StringHttpMessageConverter的DefaultCharset屬性有關。
查看StringHttpMessageConverter的源碼如下:

public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {

  /**
   * The default charset used by the converter.
   */
  public static final Charset DEFAULT_CHARSET = StandardCharsets.ISO_8859_1;

  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(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL);
  }
  ...
}

可以看出StringHttpMessageConverter的默認編碼方式是ISO_8859_1,而編碼實際存儲在AbstractHttpMessageConverter
的defaultCharset屬性中:

public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConverter<T> {
  ...
  protected AbstractHttpMessageConverter(Charset defaultCharset, MediaType... supportedMediaTypes) {
    this.defaultCharset = defaultCharset;
    setSupportedMediaTypes(Arrays.asList(supportedMediaTypes));
  }

  @Nullable
  public Charset getDefaultCharset() {
    return this.defaultCharset;
  }
  ...

getDefaultCharset()方法上打一個斷點,然后發起請求,查看方法的調用過程,
發現AbstractMessageConverterMethodProcessor類第275行對this.messageConverters進行遍歷,
StringHttpMessageConverter的實例對象存放在AbstractMessageConverterMethodArgumentResolver的
messageConverters屬性中。

那么StringHttpMessageConverter的實例對象是如何放到messageConverters屬性中的呢?
找到RequestMappingHandlerAdapter(我也搞不清是怎么找到的了,可能是運氣吧),RequestMappingHandlerAdapter部分源碼如下:

// 初始化方法
public RequestMappingHandlerAdapter() {
    StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
    stringHttpMessageConverter.setWriteAcceptCharset(false);  // see SPR-7316

    this.messageConverters = new ArrayList<>(4);
    this.messageConverters.add(new ByteArrayHttpMessageConverter());

    // 看這里,這里的messageConverters還是RequestMappingHandlerAdapter自己的屬性
    this.messageConverters.add(stringHttpMessageConverter); 
    try {
      this.messageConverters.add(new SourceHttpMessageConverter<>());
    }
    catch (Error err) {
      // Ignore when no TransformerFactory implementation is available
    }
    this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
}

// 實現了InitializingBean接口的afterPropertiesSet方法,該方法會在屬性注入之后,初始化方法執行之前執行
@Override
public void afterPropertiesSet() {
    // Do this first, it may add ResponseBody advice beans
    initControllerAdviceCache();

    if (this.argumentResolvers == null) {
      // 看這里的getDefaultArgumentResolvers()方法
      List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
      this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
    }
    ...
}

private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
    List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
    ...
    // 看這里,getMessageConverters()會獲取RequestMappingHandlerAdapter的messageConverters
    resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
    ...
  }

public List<HttpMessageConverter<?>> getMessageConverters() {
    return this.messageConverters;
}

看一下RequestResponseBodyMethodProcessor的構造方法,注意一下繼承關系

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
  public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
      @Nullable List<Object> requestResponseBodyAdvice) {
    // 調用父類的構造方法
    super(converters, null, requestResponseBodyAdvice);
  }
}

看一下AbstractMessageConverterMethodProcessor的這個構造方法

public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
    implements HandlerMethodReturnValueHandler {
  protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters,
      @Nullable ContentNegotiationManager manager, @Nullable List<Object> requestResponseBodyAdvice) {
    // 繼續調用父類的構造方法
    super(converters, requestResponseBodyAdvice);

    this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager());
    this.pathStrategy = initPathStrategy(this.contentNegotiationManager);
    this.safeExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions());
    this.safeExtensions.addAll(WHITELISTED_EXTENSIONS);
  }
}

再看AbstractMessageConverterMethodArgumentResolver的構造方法

  public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> converters,
      @Nullable List<Object> requestResponseBodyAdvice) {

    Assert.notEmpty(converters, "'messageConverters' must not be empty");

    // 到這里終於把RequestMappingHandlerAdapter里創建的StringHttpMessageConverter實例放到了
    // AbstractMessageConverterMethodArgumentResolver的messageConverters屬性里面
    this.messageConverters = converters;
    this.allSupportedMediaTypes = getAllSupportedMediaTypes(converters);
    this.advice = new RequestResponseBodyAdviceChain(requestResponseBodyAdvice);
  }

現在又出現一個問題,RequestMappingHandlerAdapter是什么時候創建的呢?
可以查看RequestMappingHandlerAdapter的構造方法在哪里被調用了,
發現只有WebMvcConfigurationSupport的createRequestMappingHandlerAdapter調用,
相關代碼如下:

public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
  // @Bean注解說明這個方法返回的類會被放到bean容器中,beanName就是方法名
  @Bean
  public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
    RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
    ...
  }

  protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
    return new RequestMappingHandlerAdapter();
  }
  ...
}

DelegatingWebMvcConfiguration繼承了WebMvcConfigurationSupport,
而DelegatingWebMvcConfiguration是Spring的一個配置類,所以會在Spring初始化容器時把該類和父類的
@Bean注解的方法創建的對象放入Spring的bean容器中

@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
  ...
}

總結一下:
bean容器初始化 =》
調用WebMvcConfigurationSupport的requestMappingHandlerAdapter()方法=》
RequestMappingHandlerAdapter進行初始化(初始化過程中創建StringHttpMessageConverter對象,並放入自己的messageConverters屬性中) =》
執行RequestMappingHandlerAdapter的afterPropertiesSet()方法,
會執行new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)
通過構造方法最終把StringHttpMessageConverter的對象放到了AbstractMessageConverterMethodArgumentResolver的messageConverters屬性中

我所做的就是在bean容器初始化完成后,修改bean容器StringHttpMessageConverter對象的defaultCharset屬性。
這樣當SpringMVC返回字符串的時候就會取出StringHttpMessageConverter對象的defaultCharset屬性的值作為
Content-Type的charset,解決亂碼問題。

如果文中有錯誤之處或各位大佬有其他解決方法歡迎留言交流。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM