概述
问题描述
Java输出至前端的整数长度超过16位时,前端js在解析整数时,超过16位的整数时,后面的数字会损失精度。
问题原因
JS内置的number类型是基于32位整数,Number类型的最大安全整数为9007199254740991,当Java Long型的值大小超过JS Number的最大安全整数时,超出此范围的整数值可能会被破坏,丢失精度。
解决办法
在后台将整数转换成字符串,围绕这个目标,共探索出5种解决方案,能满足大部分的个性化需求。
解决方案
- 过滤器统一拦截
- 自定义Serializer序列化类
- POJO属性增加注解
- 修改Serializers源码
- 代码转化
项目环境:
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类型
参考资料
- https://stackoverflow.com/questions/7854030/configuring-objectmapper-in-spring
- https://www.jianshu.com/p/92ce8c52d468