Spring MVC - HttpMessageConverter

목차

Spring MVC - HttpMessageConverter

HttpMessageConverter 는 HTTP 요청, 응답 둘다 사용된다.

  • canRead, canWrite : 메시지 컨버터가 해당 클래스, 미디어 타입을 지원하는지 확인한다.
  • read, write : 메시지 컨버터를 통해 메시지르 읽고 쓰는 기능
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);

boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

List<MediaType> getSupportedMediaTypes();

default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
return (canRead(clazz, null) || canWrite(clazz, null) ? getSupportedMediaTypes() : Collections.emptyList());
}

T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;

void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}

스프링 부트 기본 메시지 컨버터

  1. ByteArrayHttpMessageConverter
  2. StringHttpMessageConverter
  3. MappingJackson2HttpMessageConverter

스프링 부트는 다양한 메시지 컨버터를 제공하는데, 대상 클래스 타입과 미디어 타입 둘을 체크해 사용여부를 결정한다. 만약 해당되지 않으면 다음 메시지 컨버터로 넘어간다.

ByteArrayHttpMessageConverter

byte[] 데이터를 처리하기 위한 Message Converter

  • 클래스 타입 : byte[]
  • 미디어 타입 : /
  • 요청 예시 : @ReqeustBody byte[] data
  • 응답 예시 : @ResponseBody return byte[]
    • 쓰기 미디어 타입 : application/octet-stream

StringHttpMessageConverter

문자로 데이터를 처리하기 위한 Message Converter

  • 클래스 타입 : String
  • 미디어 타입 : /
  • 요청 예시 : @RequestBody String data
  • 응답 예시 : @ResponseBody return String
    • 쓰기 미디어 타입 : text/plain

MappingJackson2HttpMessageConverter

Json 데이터를 처리하기 위한 Message Converter

  • 클래스 타입 : 객체 또는 HashMap
  • 미디어 타입 : application/json
  • 요청 예시 : @RequestBody Hello data
  • 응답 예시 : @ResponseBody return helloData
    • 쓰기 미디어 타입 : application/json

HTTP 요청 데이터 읽기

  • HTTP 요청이 오면 컨트롤러에서 @RequestBody, HttpEntity 파라미터를 호출한다.
  • 메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 canRead 메소드를 호출한다.
    • 대상 클래스를 지원하는지 확인
    • HTTP 요청의 Content-Type을 지원하는지 확인
  • canRead 조건에 만족하면 read 메소드를 호출한다.

HTTP 응답 데이터 보내기

  • 컨트롤러에서 @ResponseBody, HttpEntity 로 값이 반환된다.
  • 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite 메소드를 호출한다.
    • 대상 클래스를 지원하는지 확인
    • HTTP 요청의 Accept 미디어 타입을 지원하는지 확인
  • canWrite 조건에 만족하면 write 메소드를 호출해 HTTP 응답 메시지 바디에 데이터를 생성한다.
public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConverter<T> {

/** Logger available to subclasses. */
protected final Log logger = HttpLogging.forLogName(getClass());

private List<MediaType> supportedMediaTypes = Collections.emptyList();

@Nullable
private Charset defaultCharset;

protected AbstractHttpMessageConverter() {
}

protected AbstractHttpMessageConverter(MediaType supportedMediaType) {
setSupportedMediaTypes(Collections.singletonList(supportedMediaType));
}

protected AbstractHttpMessageConverter(MediaType... supportedMediaTypes) {
setSupportedMediaTypes(Arrays.asList(supportedMediaTypes));
}

protected AbstractHttpMessageConverter(Charset defaultCharset, MediaType... supportedMediaTypes) {
this.defaultCharset = defaultCharset;
setSupportedMediaTypes(Arrays.asList(supportedMediaTypes));
}

public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) {
Assert.notEmpty(supportedMediaTypes, "MediaType List must not be empty");
this.supportedMediaTypes = new ArrayList<>(supportedMediaTypes);
}

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

public void setDefaultCharset(@Nullable Charset defaultCharset) {
this.defaultCharset = defaultCharset;
}

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

@Override
public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
return supports(clazz) && canRead(mediaType);
}

protected boolean canRead(@Nullable MediaType mediaType) {
if (mediaType == null) {
return true;
}
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
if (supportedMediaType.includes(mediaType)) {
return true;
}
}
return false;
}

@Override
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
return supports(clazz) && canWrite(mediaType);
}

protected boolean canWrite(@Nullable MediaType mediaType) {
if (mediaType == null || MediaType.ALL.equalsTypeAndSubtype(mediaType)) {
return true;
}
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
if (supportedMediaType.isCompatibleWith(mediaType)) {
return true;
}
}
return false;
}

@Override
public final T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {

return readInternal(clazz, inputMessage);
}

@Override
public final void write(final T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {

final HttpHeaders headers = outputMessage.getHeaders();
addDefaultHeaders(headers, t, contentType);

if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(outputStream -> writeInternal(t, new HttpOutputMessage() {
@Override
public OutputStream getBody() {
return outputStream;
}

@Override
public HttpHeaders getHeaders() {
return headers;
}
}));
} else {
writeInternal(t, outputMessage);
outputMessage.getBody().flush();
}
}

protected void addDefaultHeaders(HttpHeaders headers, T t, @Nullable MediaType contentType) throws IOException {
if (headers.getContentType() == null) {
MediaType contentTypeToUse = contentType;
if (contentType == null || !contentType.isConcrete()) {
contentTypeToUse = getDefaultContentType(t);
} else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
MediaType mediaType = getDefaultContentType(t);
contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
}
if (contentTypeToUse != null) {
if (contentTypeToUse.getCharset() == null) {
Charset defaultCharset = getDefaultCharset();
if (defaultCharset != null) {
contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
}
}
headers.setContentType(contentTypeToUse);
}
}
if (headers.getContentLength() < 0 && !headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) {
Long contentLength = getContentLength(t, headers.getContentType());
if (contentLength != null) {
headers.setContentLength(contentLength);
}
}
}

@Nullable
protected MediaType getDefaultContentType(T t) throws IOException {
List<MediaType> mediaTypes = getSupportedMediaTypes();
return (!mediaTypes.isEmpty() ? mediaTypes.get(0) : null);
}

@Nullable
protected Long getContentLength(T t, @Nullable MediaType contentType) throws IOException {
return null;
}

protected abstract boolean supports(Class<?> clazz);

protected abstract T readInternal(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;

protected abstract void writeInternal(T t, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter<byte[]> {

/**
* Create a new instance of the {@code ByteArrayHttpMessageConverter}.
*/
public ByteArrayHttpMessageConverter() {
super(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL);
}

@Override
public boolean supports(Class<?> clazz) {
return byte[].class == clazz;
}

@Override
public byte[] readInternal(Class<? extends byte[]> clazz, HttpInputMessage inputMessage) throws IOException {
long contentLength = inputMessage.getHeaders().getContentLength();
ByteArrayOutputStream bos = new ByteArrayOutputStream(
contentLength >= 0 ? (int) contentLength : StreamUtils.BUFFER_SIZE);
StreamUtils.copy(inputMessage.getBody(), bos);
return bos.toByteArray();
}

@Override
protected Long getContentLength(byte[] bytes, @Nullable MediaType contentType) {
return (long) bytes.length;
}

@Override
protected void writeInternal(byte[] bytes, HttpOutputMessage outputMessage) throws IOException {
StreamUtils.copy(bytes, outputMessage.getBody());
}

}
public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {

private static final MediaType APPLICATION_PLUS_JSON = new MediaType("application", "*+json");

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

@Nullable
private volatile List<Charset> availableCharsets;

private boolean writeAcceptCharset = false;

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

/**
* Whether the {@code Accept-Charset} header should be written to any outgoing
* request sourced from the value of {@link Charset#availableCharsets()}. The
* behavior is suppressed if the header has already been set.
* <p>
* As of 5.2, by default is set to {@code false}.
*/
public void setWriteAcceptCharset(boolean writeAcceptCharset) {
this.writeAcceptCharset = writeAcceptCharset;
}

@Override
public boolean supports(Class<?> clazz) {
return String.class == clazz;
}

@Override
protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {
Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
return StreamUtils.copyToString(inputMessage.getBody(), charset);
}

@Override
protected Long getContentLength(String str, @Nullable MediaType contentType) {
Charset charset = getContentTypeCharset(contentType);
return (long) str.getBytes(charset).length;
}

@Override
protected void addDefaultHeaders(HttpHeaders headers, String s, @Nullable MediaType type) throws IOException {
if (headers.getContentType() == null) {
if (type != null && type.isConcrete() && (type.isCompatibleWith(MediaType.APPLICATION_JSON)
|| type.isCompatibleWith(APPLICATION_PLUS_JSON))) {
// Prevent charset parameter for JSON..
headers.setContentType(type);
}
}
super.addDefaultHeaders(headers, s, type);
}

@Override
protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {
HttpHeaders headers = outputMessage.getHeaders();
if (this.writeAcceptCharset && headers.get(HttpHeaders.ACCEPT_CHARSET) == null) {
headers.setAcceptCharset(getAcceptedCharsets());
}
Charset charset = getContentTypeCharset(headers.getContentType());
StreamUtils.copy(str, charset, outputMessage.getBody());
}

/**
* Return the list of supported {@link Charset Charsets}.
* <p>
* By default, returns {@link Charset#availableCharsets()}. Can be overridden in
* subclasses.
*
* @return the list of accepted charsets
*/
protected List<Charset> getAcceptedCharsets() {
List<Charset> charsets = this.availableCharsets;
if (charsets == null) {
charsets = new ArrayList<>(Charset.availableCharsets().values());
this.availableCharsets = charsets;
}
return charsets;
}

private Charset getContentTypeCharset(@Nullable MediaType contentType) {
if (contentType != null) {
Charset charset = contentType.getCharset();
if (charset != null) {
return charset;
} else if (contentType.isCompatibleWith(MediaType.APPLICATION_JSON)
|| contentType.isCompatibleWith(APPLICATION_PLUS_JSON)) {
// Matching to AbstractJackson2HttpMessageConverter#DEFAULT_CHARSET
return StandardCharsets.UTF_8;
}
}
Charset charset = getDefaultCharset();
Assert.state(charset != null, "No default charset");
return charset;
}
}
public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {

@Nullable
private String jsonPrefix;

/**
* Construct a new {@link MappingJackson2HttpMessageConverter} using default
* configuration provided by {@link Jackson2ObjectMapperBuilder}.
*/
public MappingJackson2HttpMessageConverter() {
this(Jackson2ObjectMapperBuilder.json().build());
}

/**
* Construct a new {@link MappingJackson2HttpMessageConverter} with a custom
* {@link ObjectMapper}. You can use {@link Jackson2ObjectMapperBuilder} to
* build it easily.
*
* @see Jackson2ObjectMapperBuilder#json()
*/
public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
}

/**
* Specify a custom prefix to use for this view's JSON output. Default is none.
*
* @see #setPrefixJson
*/
public void setJsonPrefix(String jsonPrefix) {
this.jsonPrefix = jsonPrefix;
}

/**
* Indicate whether the JSON output by this view should be prefixed with ")]}',
* ". Default is false.
* <p>
* Prefixing the JSON string in this manner is used to help prevent JSON
* Hijacking. The prefix renders the string syntactically invalid as a script so
* that it cannot be hijacked. This prefix should be stripped before parsing the
* string as JSON.
*
* @see #setJsonPrefix
*/
public void setPrefixJson(boolean prefixJson) {
this.jsonPrefix = (prefixJson ? ")]}', " : null);
}

@Override
protected void writePrefix(JsonGenerator generator, Object object) throws IOException {
if (this.jsonPrefix != null) {
generator.writeRaw(this.jsonPrefix);
}
}
}
Share