概述

问题描述
Java输出至前端的整数长度超过16位时,前端js在解析整数时,超过16位的整数时,后面的数字会损失精度。

问题原因
JS内置的number类型是基于32位整数,Number类型的最大安全整数为9007199254740991,当Java Long型的值大小超过JS Number的最大安全整数时,超出此范围的整数值可能会被破坏,丢失精度。

解决办法
在后台将整数转换成字符串,围绕这个目标,共探索出5种解决方案,能满足大部分的个性化需求。

解决方案

  1. 过滤器统一拦截
  2. 自定义Serializer序列化类
  3. POJO属性增加注解
  4. 修改Serializers源码
  5. 代码转化

项目环境:

Spring 3.X and Jackson 2.X

1)过滤器统一拦截

配置Spring消息转换器的ObjectMapper类,引用创建的自定义类型转换器类。

  • 创建CustomObjectMapperForJackson类
package com.xiaodajia.framework.util.web.databind;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.SimpleDateFormat;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;

public class CustomObjectMapperForJackson extends ObjectMapper {

	public CustomObjectMapperForJackson() {
		super();
		// 设置日期转换yyyy-MM-dd HH:mm:ss
		setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
		SimpleModule simpleModule = new SimpleModule();
		simpleModule.addSerializer(BigDecimal.class,
				ToStringSerializer.instance);
		simpleModule.addSerializer(BigInteger.class,
				ToStringSerializer.instance);
		simpleModule.addSerializer(Long.class,
				ToStringSerializer.instance);
		simpleModule.addSerializer(Long.TYPE,
				ToStringSerializer.instance);
		registerModule(simpleModule);
	}
}
  • spring-mvc.xml配置objectMapper
<mvc:annotation-driven validator="validator" conversion-service="conversion-service">
    	<mvc:message-converters register-defaults="true">
			<!-- 将StringHttpMessageConverter的默认编码设为UTF-8 -->
			<bean class="org.springframework.http.converter.StringHttpMessageConverter">
		    	<constructor-arg value="UTF-8" />
			</bean>
			<!-- 将Jackson2HttpMessageConverter的默认格式化输出设为true -->
			<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
                <property name="prettyPrint" value="true"/>
                <property name="objectMapper">
                    <bean class="com.xiaodajia.framework.util.web.databind.CustomObjectMapperForJackson"/>
                </property>
            </bean>			
  		</mvc:message-converters>
	</mvc:annotation-driven>

关键代码
当项目中使用Mybatis持久层框架,如果结果类型设置为resultType="map",将集合数据输出至前端时,Jackson会默认将数组、对象中的数值以BigDecimal类型序列化,如果我们只配置了Long的序列化类,对map中数据是不会生效,还需要在自定义ObjectMapper中配置BigDecimal、BigInteger类的序列化

simpleModule.addSerializer(BigDecimal.class,ToStringSerializerForJackson.instance);
simpleModule.addSerializer(BigInteger.class,ToStringSerializerForJackson.instance);

2)自定义Serializer序列化类

在“过滤器统一拦截”方案中,会将所有数值全部转化为String,如果我们只需要将数值长度超过16位的整数才转化为String,那该如何处理呢?

面对这类需求,我们可以参考ToStringSerializer类,自定义序列化类ToStringSerializerForJackson,重写serialize方法中BigDecimal、BigInteger、Long类型数值,将长度超过16位数值转化为String。

package com.xiaodajia.framework.util.web.databind;

import java.io.IOException;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;

import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.databind.util.TokenBuffer;

/**
 * Simple general purpose serializer, useful for any type for which
 * {@link Object#toString} returns the desired JSON value.
 */
@JacksonStdImpl
public class ToStringSerializerForJackson extends StdSerializer<Object> {
	/**
	 * Singleton instance to use.
	 */
	public final static ToStringSerializerForJackson instance = new ToStringSerializerForJackson();

	/**
	 * <p>
	 * Note: usually you should NOT create new instances, but instead use
	 * {@link #instance} which is stateless and fully thread-safe. However,
	 * there are cases where constructor is needed; for example, when using
	 * explicit serializer annotations like
	 * {@link com.fasterxml.jackson.databind.annotation.JsonSerialize#using}.
	 */
	public ToStringSerializerForJackson() {
		super(Object.class);
	}

	@Override
	public boolean isEmpty(Object value) {
		if (value == null) {
			return true;
		}
		String str = value.toString();
		// would use String.isEmpty(), but that's JDK 1.6
		return (str == null) || (str.length() == 0);
	}

	@Override
	public void serialize(Object value, JsonGenerator jgen,
			SerializerProvider provider) throws IOException,
			JsonGenerationException {
//		System.out.println("ToStringSerializerForJackson序列化------" + value);
		if (null != value) {
			if (value instanceof BigDecimal) {
				if (provider
						.isEnabled(SerializationFeature.WRITE_BIGDECIMAL_AS_PLAIN)) {
					// [Issue#232]: Ok, rather clumsy, but let's try to work
					// around the problem with:
					if (!(jgen instanceof TokenBuffer)) {
						jgen.writeNumber(((BigDecimal) value).toPlainString());
						return;
					}
				}
				if (((BigDecimal) value).compareTo(new BigDecimal(
						1000000000000000L)) == 1) {
//					System.out.println("ToStringSerializerForJackson BigDecimal序列化------"+ value);
					jgen.writeString(value + "");
				} else {
//					System.out.println("ToStringSerializerForJackson BigDecimal  To String序列化------"+ value);
					jgen.writeNumber((BigDecimal) value);
				}
			} else if (value instanceof BigInteger) {
				if (((BigInteger) value).compareTo(new BigInteger(
						"1000000000000000")) > 0) {
//					System.out.println("ToStringSerializerForJackson BigInteger序列化------"+ value);
					jgen.writeString(value + "");
				} else {
//					System.out.println("ToStringSerializerForJackson BigInteger  To String序列化------"+ value);
					jgen.writeNumber((BigInteger) value);
				}
			} else if (value instanceof Long) {
				if (((Long) value).longValue() >= 1000000000000000L) {
//					System.out.println("ToStringSerializerForJackson Long序列化------"
//							+ value);
					jgen.writeString(value + "");
				} else {
//					System.out.println("ToStringSerializerForJackson Long To String序列化------"+ value);
					jgen.writeNumber(((Long) value).longValue());
				}
			} else {
//				System.out.println("StringSerializer else序列化------" + value);
				jgen.writeString(value.toString());
			}
		} else {
			jgen.writeString("");
		}
	}

	/*
	 * 01-Mar-2011, tatu: We were serializing as "raw" String; but generally
	 * that is not what we want, since lack of type information would imply real
	 * String type.
	 */
	/**
	 * Default implementation will write type prefix, call regular serialization
	 * method (since assumption is that value itself does not need JSON Array or
	 * Object start/end markers), and then write type suffix. This should work
	 * for most cases; some sub-classes may want to change this behavior.
	 */
	@Override
	public void serializeWithType(Object value, JsonGenerator jgen,
			SerializerProvider provider, TypeSerializer typeSer)
			throws IOException, JsonGenerationException {
		System.out.println("ToStringSerializerForJackson -serializeWithType序列化------"
				+ value);
		typeSer.writeTypePrefixForScalar(value, jgen);
		serialize(value, jgen, provider);
		typeSer.writeTypeSuffixForScalar(value, jgen);
	}

	@Override
	public JsonNode getSchema(SerializerProvider provider, Type typeHint)
			throws JsonMappingException {
		return createSchemaNode("string", true);
	}

	@Override
	public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor,
			JavaType typeHint) throws JsonMappingException {
		if (visitor != null) {
			visitor.expectStringFormat(typeHint);
		}
	}
}

3)POJO属性增加注解

在POJO类需要转换的属性上增加@JsonSerialize注解,如果需要转换的字段多,则需要逐一增加注解,管理很麻烦

public class YNotice implements Serializable {
	private static final long serialVersionUID = 1L;

	@JsonSerialize(using=ToStringSerializer.class)
	private long noticeId;

	private String content;

	private Date createTime;

4)修改Serializers源码

修改Jackson源码,在NumberSerializers类的子类NumberSerializer中,将Long转换为String

@JacksonStdImpl
    public final static class NumberSerializer
        extends StdScalarSerializer<Number>
    {
        public final static NumberSerializer instance = new NumberSerializer();
    
        public NumberSerializer() { super(Number.class); }
    
        @Override
        public void serialize(Number value, JsonGenerator jgen, SerializerProvider provider)
            throws IOException, JsonGenerationException
        {
        	/**
        	 * 强制转化类,判断数字长度超过16位时,将数字转化为字符串
        	 */
            if (value instanceof BigDecimal) {
                if (provider.isEnabled(SerializationFeature.WRITE_BIGDECIMAL_AS_PLAIN)) {
                    // [Issue#232]: Ok, rather clumsy, but let's try to work around the problem with:
                    if (!(jgen instanceof TokenBuffer)) {
                        jgen.writeNumber(((BigDecimal) value).toPlainString());
                        return;
                    }
                }
                if(((BigDecimal) value).compareTo(new BigDecimal(10000000000000000L)) == 1){
                	System.out.println("NumberSerializer BigDecimal 转化为String------" + value);
                	jgen.writeString(value+"");
                }else{
                	System.out.println("NumberSerializer BigDecimal ------" + value);
                	jgen.writeNumber((BigDecimal) value);
                }
            } else if (value instanceof BigInteger) {
                jgen.writeNumber((BigInteger) value);
            } else if (value instanceof Integer) {
            	jgen.writeNumber(value.intValue());
            } else if (value instanceof Long) {
            	if(value.longValue()>=10000000000000000L) {
            		System.out.println("NumberSerializer Long 转化为String------" + value);
            		jgen.writeString(value+"");
            	}else{
            		System.out.println("NumberSerializer Long ------" + value);
            		jgen.writeNumber(value.longValue());
            	}
            } else if (value instanceof Double) {
            	jgen.writeNumber(value.doubleValue());
            } else if (value instanceof Float) {
            	jgen.writeNumber(value.floatValue());
            } else if ((value instanceof Byte) || (value instanceof Short)) {
                jgen.writeNumber(value.intValue()); // doesn't need to be cast to smaller numbers
            } else {
                jgen.writeNumber(value.toString());
            }
        }
    
        @Override
        public JsonNode getSchema(SerializerProvider provider, Type typeHint)
        {
            return createSchemaNode("number", true);
        }
        
        @Override
        public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
            throws JsonMappingException
        {
            // Hmmh. What should it be? Ideally should probably indicate BIG_DECIMAL
            // to ensure no information is lost? But probably won't work that well...
            JsonNumberFormatVisitor v2 = visitor.expectNumberFormat(typeHint);
            if (v2 != null) {
                v2.numberType(JsonParser.NumberType.BIG_DECIMAL);
            }
        }
    }

关键代码是NumberSerializer类的serialize方法,在方法中判断数值是否BigDecimal、Long类型,如果数字长度超过16位时,则将数字转化为字符串。

代码转化

直接在java代码中,将Map对象和集合的Long数值强转化为String类型

参考资料

  1. https://stackoverflow.com/questions/7854030/configuring-objectmapper-in-spring
  2. https://www.jianshu.com/p/92ce8c52d468