zoukankan      html  css  js  c++  java
  • Spring MVC 后端获取前端提交的json格式字符串并直接转换成control方法对应的参数对象

    场景:

    在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请求,需要加一行配置:

     <value>application/javascript;charset=UTF-8</value> 
    特别是在IE下,因为IE下默认的js请求不会带charset,因此解析用的是默认的iso-8859,需要明确指定编码,如utf-8.
     
    spring mvc具有精巧的设计,如果出现了问题,可以通过源码不断的调试进来,细致耐心,则问题自然就可以搞定了。
     
  • 相关阅读:
    NHibernate之旅(2):第一个NHibernate程序
    Motion sensing game (Ping Pong Game)
    Java学习之道:Java操作Excel之导出下载
    安装和升级--基础--许可证信息--title and Copyright information
    spring 文件上传功能实现
    这些常见的网络故障,你都知道如何解决吗
    这些常见的网络故障,你都知道如何解决吗
    这些常见的网络故障,你都知道如何解决吗
    限流
    限流
  • 原文地址:https://www.cnblogs.com/dimmacro/p/4863420.html
Copyright © 2011-2022 走看看