場景:
在web應用開發中,spring mvc憑借出現的性能和良好的可擴展性,導致使用日漸增多,成為事實標准,在日常的開發過程中,有一個很常見的場景:即前端通過ajax提交方式,提交參數為一個json對象的字符串,采用application/json的類型,在后端control中利用@RequestBody將json字符串直接轉換成對應的Java對象,如:
var dataStr = '[{"id":1476,"name":"test"}]'; $.ajax({ url : '${request.contextPath}/test/jsonParam.json', data : dataStr, type : "POST", async : false, contentType : "application/json;charset=utf-8", //設置請求頭信息 success : function(data) { console.log(data); //alert(data); } });
在control,我們想直接在處理的方法中獲取json字符串對應的對象,如:
@RequestMapping(value = "/jsonParam") public JSONObject handleJsonParam(WebRequest request, ModelMap model,@RequestBody User user) { System.out.println(user.getName()); JSONObject jsonObject = JSONObject.parseObject("{\"status\":\"ok\"}"); return jsonObject; }
在handleJsonParam中,我們希望一進入此方法,參數user對象就已經初始化,而且其對應的id,names屬性已經分別被賦值為1476和test,減少json字符串與對象間的反系列化工作,提升開發人員的效率。
場景分析:
我們知道,spring mvc在根據requestmapping找對對應的control方法處理前,會根據請求參數及請求類型做一些數據轉換,數據格式化及數據校驗等工作,因此我們的解決思路就是在數據轉換過程中,將前台請求傳過來的json字符串轉換成對應的對象,然后將此對象綁定到control方法的參數中。
解決方式:
方式一:最簡單的方式,spring mvc為我們提供了一個MappingJackson2HttpMessageConverter類,用於幫助從json字符串轉成java的對象,我們只需要在requestmappinghandleradpter中進行配置即可:
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"> <property name="messageConverters"> <list> <ref bean="mappingJacksonHttpMessageConverter" /> </list> </property> </bean> <bean id="mappingJacksonHttpMessageConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="supportedMediaTypes"> <list> <value>text/html;charset=UTF-8</value> <value>application/json;charset=utf-8</value> </list> </property> </bean>
此方式需要依賴以下三個包:jackson-core(2.4.0),jackson-databind(2.4.0),jackson-annotations(2.4.0)
方式二:
方式一對所有的json請求都會做如此轉換,有時候我們只需要對具體的json字符串做轉換或者我們希望控制轉換的細節,可以自己創建一個類繼承AbstractHttpMessageConverter並實現GenericHttpMessageConverter接口,通過重寫canRead,support方法來控制可發序列化的對象類型,並重寫read方法實現最終的轉換。先看配置文件:
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="com.web.converter.JsonRequestMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>application/json;charset=utf-8</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
此處我用的是mvc:annotation-driven配置方式,該標簽簡化了spring的配置,內部會注冊默認的DefaultAnnotationHandlerMapping及AnnotationMethodHandlerAdapter示例,與方式一的配置效果類似。其中JsonRequestMessageConverter類如下:
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
*
* @Description:json請求是對json格式的參數直接進行映射為對應的對象,control中可直接得到對應的對象
* @Create time: 2015年9月21日下午3:47:55
*
*/
public class JsonRequestMessageConverter extends AbstractHttpMessageConverter<Object> implements GenericHttpMessageConverter<Object> {
private final static Charset UTF8 = Charset.forName("UTF-8");
private final static String JSONP_FUNC_NAME = "callback";
private Charset charset = UTF8;
private String jsonpFuncName = JSONP_FUNC_NAME;
private ObjectMapper objectMapper;
public JsonRequestMessageConverter() {
super(new MediaType("application", "json", UTF8), new MediaType("application", "*+json", UTF8));
objectMapper = Jackson2ObjectMapperBuilder.json().build();
}
public void setJsonpFuncName(String jsonpFuncName) {
this.jsonpFuncName = jsonpFuncName;
}
public void setCharset(Charset charset) {
this.charset = charset;
}
private HttpServletRequest getRequest() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
private boolean requestJsonp(HttpServletRequest request) {
return request.getRequestURI().endsWith(".jsonp");
}
private String getJsonpFunc(HttpServletRequest request) {
String func = request.getParameter(jsonpFuncName);
return StringUtils.isEmpty(func) ? "null" : func;
}
/**
* 判斷前台請求提交的數據是否可以用此convert讀
* type為control中標記為RequestBody的參數類型
* contextClass為對應請求的control類
* mediaType為自持的請求類型,如json、text等
*
*/
@Override
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
JavaType javaType = getJavaType(type, contextClass);
AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
if (this.objectMapper.canDeserialize(javaType, causeRef) && canRead(mediaType)) {
return true;
}
Throwable cause = causeRef.get();
if (cause != null) {
String msg = "Failed to evaluate deserialization for type " + javaType;
if (logger.isDebugEnabled()) {
logger.warn(msg, cause);
}
else {
logger.warn(msg + ": " + cause);
}
}
return false;
}
private JavaType getJavaType(Type type, Class<?> contextClass) {
return this.objectMapper.getTypeFactory().constructType(type, contextClass);
}
/**
*
* @Description:泛型讀,將從前台傳過來的json請求串映射為具體的參數對象
* @param type
* @param contextClass
* @param inputMessage
* @return
* @throws IOException
* @throws HttpMessageNotReadableException
* @see org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(HttpInputMessage, MethodParameter, Type)
* @see org.springframework.http.converter.GenericHttpMessageConverter#read(java.lang.reflect.Type, java.lang.Class, org.springframework.http.HttpInputMessage)
* @update1:
*
*/
@Override
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
try {
return this.objectMapper.readValue(inputMessage.getBody(), this.getJavaType(type, contextClass));
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex);
}
}
/**
*
* @Description:如果泛型讀canRead方法返回false,則會調用AbstractHttpMessageConverter中的read方法,此方法會調用readInternal轉普通jsonObject
* @param clazz
* @param inputMessage
* @return
* @throws IOException
* @throws HttpMessageNotReadableException
* @see org.springframework.http.converter.AbstractHttpMessageConverter#readInternal(java.lang.Class, org.springframework.http.HttpInputMessage)
* @update1:
*
*/
@Override
protected Object readInternal(Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
try {
return this.objectMapper.readValue(inputMessage.getBody(), this.getJavaType(clazz, null));
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex);
}
}
/**
*
* @Description:定義此convert支持輸出的對象類型,即control端返回的類型,此處支持json格式的字符串及JSONObject/JsonArray對象
* @param clazz
* @return
* @see org.springframework.http.converter.AbstractHttpMessageConverter#supports(java.lang.Class)
* @update1:
*
*/
@Override
protected boolean supports(Class<?> clazz) {
// 只處理control返回的String/JSONObject/JsonArray對象
return String.class.isAssignableFrom(clazz) || JSON.class.isAssignableFrom(clazz);
}
/**
*
* @Description:定義此convert可以輸出的條件為json格式的字符串及JSONObject/JsonArray對象,且為json請求類型
* @param clazz
* @param mediaType
* @return
* @see org.springframework.http.converter.AbstractHttpMessageConverter#canWrite(java.lang.Class, org.springframework.http.MediaType)
* @update1:
*
*/
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return this.supports(clazz) && canWrite(mediaType);
}
/**
*
* @Description:
* @param t
* @param outputMessage
* @throws IOException
* @throws HttpMessageNotWritableException
* @see org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(T, MethodParameter, ServletServerHttpRequest, ServletServerHttpResponse)
* @see org.springframework.http.converter.AbstractHttpMessageConverter#writeInternal(java.lang.Object, org.springframework.http.HttpOutputMessage)
* @update1:
*
*/
@Override
protected void writeInternal(Object t, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
// 進來的t值只能是字符串或JSONObject/JsonArray格式
OutputStream out = outputMessage.getBody();
HttpServletRequest request = getRequest();
StringBuilder buffer = new StringBuilder();
boolean requestJsonp = requestJsonp(request);
if (requestJsonp) {
buffer.append(getJsonpFunc(request)).append('(');
}
buffer.append(resolveJsonString(t));
if (requestJsonp) {
buffer.append(");");
}
System.out.println("jsonvonvert:"+buffer);
byte[] bytes = buffer.toString().getBytes(charset);
out.write(bytes);
out.flush();
}
private String resolveJsonString(Object t) throws HttpMessageNotWritableException{
if(t instanceof JSON){
return ((JSON)t).toJSONString();
}else if(t instanceof String){
return (String)t;
}
throw new HttpMessageNotReadableException("Not Json Object");
}
}
方式三:
通過@InitBinder標簽指定json字符串到具體Java對象類型間轉換的處理類,這種方式需要了解類細節,不建議使用。
可能出現的問題:
有可能會出現如下兩個問題:
問題1:control端能得到JsonObject,但是無法泛型到具體的對象,如上面的配置中如果是得到一個List<User>,那么從control方法的參數看到的list內部不是User對象,而是JsonObject;
問題2:有時輸入或輸出的json數據時會出現亂碼。
問題原因:
從前端請求到后端處理,requestMappingHandlerAdapter需要通過requestMapping找到對應的方法,並在方法處理前對參數進行解析,如下圖:

在resolverArgument方法體里,會做如下處理:

通過readWithMessageConverters將傳過來的json串通過配置的convert類轉成具體的對象,如json字符串到jsonObject,在通過binder對象將得到的對象轉成泛型,如jsonObject到User對象,因此如果出現問題1,即沒有轉換成User對象,說明binder處理出現問題,如果出現問題2,說明得到解析得到arg對象時出現了問題,而解析此對象時根據預先配置的convert對象的,說明在轉換過程中從request讀取數據流出現了問題。
解決方式:
問題一解決:自己的轉換類一定要實現實現GenericHttpMessageConverter接口,並在read方法中處理將json字符串到User對象的轉換;
問題二解決:每一個converter都需要明確指定支持的MediaType,如:
public JsonRequestMessageConverter() { super(new MediaType("application", "json", UTF8), new MediaType("application", "*+json", UTF8)); objectMapper = Jackson2ObjectMapperBuilder.json().build(); }
convert默認的字符集的iso-8859,因此如果出現了亂碼,需要在配置文件中明確指定字符編碼,如:
<bean class="com.letv.shop.demoWeb.web.converter.JsonRequestMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>application/json;charset=utf-8</value>
</list>
</property>
</bean>
還有一點需要注意的是:對於上面的配置,如果是通過js發起ajax請求,需要加一行配置:
