日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > HTML >内容正文

HTML

Spring Boot返回前端Long型丢失精度

發布時間:2024/4/11 HTML 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Spring Boot返回前端Long型丢失精度 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

最近為Prong開發了一個基于snowflake算法的Java分布式ID組件,將實體主鍵從原來的String類型的UUID修改成了Long型的分布式ID。修改后發現前端顯示的ID和數據庫中的ID不一致。例如數據庫中存儲的是:812782555915911412,顯示出來卻成了812782555915911400,后面2位變成了0,精度丟失了:

console.log(812782555915911412); 812782555915911400

這是什么原因呢?

原來,JavaScript中數字的精度是有限的,Java的Long類型的數字超出了JavaScript的處理范圍。JavaScript內部只有一種數字類型Number,所有數字都是采用IEEE 754 標準定義的雙精度64位格式存儲,即使整數也是如此。這就是說,JavaScript 語言的底層根本沒有整數,所有數字都是小數(64位浮點數)。其結構如圖:

各位的含義如下:

  • 1位(s) 用來表示符號位,0表示正數,1表示負數
  • 11位(e) 用來表示指數部分
  • 52位(f) 表示小數部分(即有效數字)

雙精度浮點數(double)并不是能夠精確表示范圍內的所有數, 雖然雙精度浮點型的范圍看上去很大: 2.23?10?3082.23*10^{-308}2.23?10?308~ 1.79?103081.79*10^{308}1.79?10308。 可以表示的最大整數可以很大,但能夠精確表示、使用算數運算的并沒有這么大。因為小數部分最大是 52 位,因此 JavaScript 中能精準表示的最大整數是 253?12^{53}-1253?1,十進制為 9007199254740991。

console.log(Math.pow(2, 53) - 1); console.log(1L<<53); 9007199254740991

JavaScript 有所謂的最大和最小安全值:

console.log(Number.MAX_SAFE_INTEGER); console.log(Number.MIN_SAFE_INTEGER); 9007199254740991 -9007199254740991

安全意思是說能夠one-by-one表示的整數,也就是說在(?253,253)(-2^{53}, 2^{53})(?253,253)范圍內,雙精度數表示和整數是一對一的,在這個范圍以內,所有的整數都有唯一的浮點數表示,這叫做安全整數。

而超過這個范圍,會有兩個或更多整數的雙精度表示是相同的;即超過這個范圍,有的整數是無法精確表示的,只能大約(round)到與它相近的浮點數(說到底就是科學計數法)表示,這種情況下叫做不安全整數,例如:

console.log(Number.MAX_SAFE_INTEGER + 1); // 結果:9007199254740992,精度未丟失 console.log(Number.MAX_SAFE_INTEGER + 2); // 結果:9007199254740992,精度丟失 console.log(Number.MAX_SAFE_INTEGER + 3); // 結果:9007199254740994,精度未丟失 console.log(Number.MAX_SAFE_INTEGER + 4); // 結果:9007199254740996,精度丟失 console.log(Number.MAX_SAFE_INTEGER + 5); // 結果:9007199254740996,精度未丟失

而Java的Long類型的有效位數是63位(扣除一位符號位),其最大值為263?12^{63}-1263?1,十進制為9223372036854775807。

public static void main(String[] args) {System.out.println(Long.MAX_VALUE);System.out.println((1L<<63) -1); } 9223372036854775807 9223372036854775807

所以只要java傳給JavaScript的Long類型的值超過9007199254740991,就有可能產生精度丟失,從而導致數據和邏輯出錯。

和其他編程語言(如 C 和 Java)不同,JavaScript 不區分整數值和浮點數值,所有數字在 JavaScript 中均用浮點數值表示,所以在進行數字運算的時候要特別注意精度缺失問題。容易造成混淆的是,某些運算只有整數才能完成,此時 JavaScript
會自動把64位浮點數,轉成32位整數,然后再進行運算,由于浮點數不是精確的值,所以涉及小數的比較和運算要特別小心。

進一步閱讀:JavaScript 教程 - 數據類型 - 數值

那有什么解決方法呢?

解決辦法之一就是讓Javascript把數字當成字符串進行處理,對Javascript來說如果不進行運算,數字和字符串處理起來沒有什么區別。但如果需要進行運算,只能采用其他方法,例如JavaScript的一些開源庫 bignum、bigint等支持長整型的處理。在我們這個場景里不需要進行運算,且Java進行JSON處理的時候是能夠正確處理long型的,所以只需要將數字轉化成字符串就可以了。

大家都知道,用Spring cloud構建微服務架構時,API(controller)通常用@RestController進行注解,而 @Restcontroller是@Controller和@ResponseBody的結合體,而@ResponseBody用于將后臺返回的Java對象轉換為Json字符串傳遞給前臺。

@Controller 注解用于配合視圖解析器InternalResourceViewResolver來完成頁面跳轉。如果要返回JSON數據到頁面上,則需要使用@RestController注解。

當數據庫字段為date類型時,@ResponseBody注解在轉換日期類型時會默認把日期轉換為時間戳(例如: date:2017-10-25 轉換為 時間戳:15003323990)。

在Spring boot中處理方法基本上有以下幾種:

一、配置參數

Jackson有個配置參數WRITE_NUMBERS_AS_STRINGS,可以強制將所有數字全部轉成字符串輸出。其功能介紹為:Feature that forces all Java numbers to be written as JSON strings.。使用方法很簡單,只需要配置參數即可:

spring:jackson:generator:write_numbers_as_strings: true

這種方式的優點是使用方便,不需要調整代碼;缺點是顆粒度太大,所有的數字都被轉成字符串輸出了,包括按照timestamp格式輸出的時間也是如此。

二、注解

另一個方式是使用注解JsonSerialize。

使用官方提供的Serializer

@JsonSerialize(using=ToStringSerializer.class) private Long bankcardHash;

指定了ToStringSerializer進行序列化,將數字編碼成字符串格式。這種方式的優點是顆粒度可以很精細;缺點同樣是太精細,如果需要調整的字段比較多會比較麻煩。

三、自定義ObjectMapper

可以單獨根據類型進行設置,只對Long型數據進行處理,轉換成字符串,而對其他類型的數字不做處理。Jackson提供了這種支持,即對ObjectMapper進行定制。根據SpringBoot的官方幫助,找到一種相對簡單的方法,只對ObjectMapper進行定制,而不是完全從頭定制,方法如下:

@Bean("jackson2ObjectMapperBuilderCustomizer") public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {Jackson2ObjectMapperBuilderCustomizer customizer = new Jackson2ObjectMapperBuilderCustomizer() {@Overridepublic void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {jacksonObjectMapperBuilder.serializerByType(Long.class, ToStringSerializer.instance).serializerByType(Long.TYPE, ToStringSerializer.instance);}};return customizer; }

通過定義Jackson2ObjectMapperBuilderCustomizer,對Jackson2ObjectMapperBuilder對象進行定制,對Long型數據進行了定制,使用ToStringSerializer來進行序列化。問題終于完美解決。

四、使用HttpMessageConverter(建議方案)

關于HttpMessageConverter

HttpMessageConverter接口提供了 5 個方法:

  • canRead:判斷該轉換器是否能將請求內容轉換成 Java 對象
  • canWrite:判斷該轉換器是否可以將 Java 對象轉換成返回內容
  • getSupportedMediaTypes:獲得該轉換器支持的 MediaType 類型
  • read:讀取請求內容并轉換成 Java 對象
  • write:將 Java 對象轉換后寫入返回內容

其中read和write方法的參數分別有有HttpInputMessage和HttpOutputMessage對象,這兩個對象分別代表著一次 Http 通訊中的請求和響應部分,可以通過getBody方法獲得對應的輸入流和輸出流。

當前 Spring 中已經默認提供了相當多的轉換器,分別有:

名稱作用讀支持 MediaType寫支持 MediaType
ByteArrayHttpMessageConverter數據與字節數組的相互轉換*/*application/octet-stream
StringHttpMessageConverter數據與 String 類型的相互轉換text/*text/plain
FormHttpMessageConverter表單與 MultiValueMap的相互轉換application/x-www-form-urlencodedapplication/x-www-form-urlencoded
SourceHttpMessageConverter數據與 javax.xml.transform.Source 的相互轉換text/xml 和 application/xmltext/xml 和 application/xml
MarshallingHttpMessageConverter使用 Spring 的 Marshaller/Unmarshaller 轉換 XML 數據text/xml 和 application/xmltext/xml 和 application/xml
MappingJackson2HttpMessageConverter使用 Jackson 的 ObjectMapper 轉換 Json 數據application/jsonapplication/json
MappingJackson2XmlHttpMessageConverter使用 Jackson 的 XmlMapper 轉換 XML 數據application/xmlapplication/xml
BufferedImageHttpMessageConverter數據與 java.awt.image.BufferedImage 的相互轉換Java I/O API 支持的所有類型Java I/O API 支持的所有類型


注意到AbstractMessageConverterMethodProcessor類的getProducibleMediaTypes、writeWithMessageConverters等方法在每次消息解析轉換都要作GenericHttpMessageConverter分支判斷,為什么呢?

package org.springframework.web.servlet.mvc.method.annotation;public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolverimplements HandlerMethodReturnValueHandler {protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, Type declaredType) {Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);if (!CollectionUtils.isEmpty(mediaTypes)) {return new ArrayList<MediaType>(mediaTypes);}else if (!this.allSupportedMediaTypes.isEmpty()) {List<MediaType> result = new ArrayList<MediaType>();for (HttpMessageConverter<?> converter : this.messageConverters) {// 分支判斷if (converter instanceof GenericHttpMessageConverter && declaredType != null) {if (((GenericHttpMessageConverter<?>) converter).canWrite(declaredType, valueClass, null)) {result.addAll(converter.getSupportedMediaTypes());}}else if (converter.canWrite(valueClass, null)) {result.addAll(converter.getSupportedMediaTypes());}}return result;}else {return Collections.singletonList(MediaType.ALL);}} }

}
GenericHttpMessageConverter接口繼承自HttpMessageConverter接口,用于提供支持泛型信息(java.lang.reflect.Type)參數的canRead/read/canWrite/write方法。它的實現類為 AbstractGenericHttpMessageConverter。

定制HttpMessageConverter
package io.prong.boot.framework;import java.math.BigInteger;import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;import io.prong.boot.framework.json.CustomMappingJackson2HttpMessageConverter;/*** prong boot 自動配置* * @author tangyz**/ @Configuration public class ProngBootAutoConfig {/*** 解決前端js處理大數字丟失精度問題,將Long和BigInteger轉換成string* * @return*/@Bean@ConditionalOnMissingBeanpublic MappingJackson2HttpMessageConverter getMappingJackson2HttpMessageConverter() {CustomMappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new CustomMappingJackson2HttpMessageConverter();ObjectMapper objectMapper = new ObjectMapper();SimpleModule simpleModule = new SimpleModule();// 序列換成json時,將所有的long變成string 因為js中得數字類型不能包含所有的java long值simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);simpleModule.addSerializer(Long.class, ToStringSerializer.instance);simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);objectMapper.registerModule(simpleModule);jackson2HttpMessageConverter.setObjectMapper(objectMapper);return jackson2HttpMessageConverter;}}

因為全局地對所有的long轉string的粒度太粗了,我們需要對不同的接口進行區分,比如限定只對web前端的接口需要轉換,但對于內部微服務之間的調用或者第三方接口等則不需要進行轉換。CustomMappingJackson2HttpMessageConverter的主要作用就是為了限定long轉string的范圍為web接口,即符合/web/xxxxx風格的url(當然這個你需要根據自己產品的規范進行自定義)。

package io.prong.boot.framework.json;import java.lang.reflect.Type;import javax.servlet.http.HttpServletRequest;import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes;/*** 自定義的json轉換器,匹配web api(以/web/開頭的controller)中的接口方法的返回參數* * @author tangyz**/ public class CustomMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {private final static Logger logger = LoggerFactory.getLogger(CustomMappingJackson2HttpMessageConverter.class);/*** 判斷該轉換器是否能將請求內容轉換成 Java 對象*/@Overridepublic boolean canRead(Class<?> clazz, MediaType mediaType) {// 不需要反序列化return false;}/*** 判斷該轉換器是否能將請求內容轉換成 Java 對象*/@Overridepublic boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {// 不需要反序列化return false;}/*** 判斷該轉換器是否可以將 Java 對象轉換成返回內容.* 匹配web api(形如/web/xxxx)中的接口方法的返回參數*/@Overridepublic boolean canWrite(Class<?> clazz, MediaType mediaType) {if (super.canWrite(clazz, mediaType)) {ServletRequestAttributes ra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (ra != null) { // web請求HttpServletRequest request = ra.getRequest();String uri = request.getRequestURI(); // 例如: "/web/frontApplicationPage"logger.debug("Current uri is: {}", uri);if (uri.startsWith("/web/")) {return true;}}}return false;}}

我們的疑問來了,spring boot默認到底有多少個轉換器?我們自定義的CustomMappingJackson2HttpMessageConverter是覆蓋了默認的MappingJackson2HttpMessageConverter,還是兩者并存?多個轉換器之間的順序是如何的?相互之間是否有影響?

下面我們來一一分析并回答。

查看spring的源碼,首先我們找到了DelegatingWebMvcConfiguration類,它的setConfigurers方法將Spring容器中所有的WebMvcConfigurer接口bean注入了方法的參數configurers中。

package org.springframework.web.servlet.config.annotation;@Configuration public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();/*** 將Spring容器中所有的WebMvcConfigurer接口bean注入了參數configurers*/@Autowired(required = false)public void setConfigurers(List<WebMvcConfigurer> configurers) {if (!CollectionUtils.isEmpty(configurers)) {this.configurers.addWebMvcConfigurers(configurers);}}

跟蹤org.springframework.web.servlet.config.annotation.WebMvcConfigurerComposite類的configureMessageConverters方法,有以下WebMvcConfigurer接口的9個代理(this.delegates):

[0]io.prong.cloud.platform.config.SwaggerConfig [1]org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration [2]org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration [3]org.springframework.cloud.netflix.metrics.MetricsInterceptorConfiguration$MetricsWebResourceConfiguration [4]org.springframework.boot.actuate.endpoint.mvc.HeapdumpMvcEndpoint [5]org.springframework.boot.actuate.endpoint.mvc.LogFileMvcEndpoint [6]org.springframework.boot.actuate.endpoint.mvc.AuditEventsMvcEndpoint [7]org.springframework.data.web.config.SpringDataWebConfiguration [8]org.springframework.cloud.netflix.rx.RxJavaAutoConfiguration$RxJavaReturnValueHandlerConfig

當然,這個代理的數量是不確定的,跟你的工程以及所依賴組件里面包含的WebMvcConfigurer接口實現類的數量有關系。

目前這里面只有WebMvcAutoConfiguration代理類覆蓋了configureMessageConverters方法并定義了spring boot默認的轉換器,所以其他代理類的我們可以無視了。跟蹤代碼可以找到spring boot在WebMvcConfigurationSupport類的addDefaultHttpMessageConverters方法中對默認的轉換器進行了定義。

跟蹤org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration的內部類WebMvcAutoConfigurationAdapter類的configureMessageConverters(List<HttpMessageConverter<?>> converters)方法,發現最終初始化的轉換器順序如下:

[0]org.springframework.http.converter.ByteArrayHttpMessageConverter [1]org.springframework.http.converter.StringHttpMessageConverter // spring boot自定義的轉換器 [2]org.springframework.http.converter.StringHttpMessageConverter [3]org.springframework.http.converter.ResourceHttpMessageConverter [4]org.springframework.http.converter.xml.SourceHttpMessageConverter [5]org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter [6]io.prong.boot.framework.json.CustomMappingJackson2HttpMessageConverter // prong boot自定義的轉換器 [7]org.springframework.http.converter.json.MappingJackson2HttpMessageConverter [8]org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter

那么我們定義的轉換器是怎么加入進來的呢?
HttpMessageConvertersAutoConfiguration類的構造函數,掃描spring容器并找到所有通過@bean方式定義的HttpMessageConverter轉換器:

package org.springframework.boot.autoconfigure.web;public class HttpMessageConvertersAutoConfiguration {static final String PREFERRED_MAPPER_PROPERTY = "spring.http.converters.preferred-json-mapper";private final List<HttpMessageConverter<?>> converters;public HttpMessageConvertersAutoConfiguration(ObjectProvider<List<HttpMessageConverter<?>>> convertersProvider) {// 找到容器里自定義的HttpMessageConverter實例this.converters = convertersProvider.getIfAvailable();}

這里面找到了2個:

[0]io.prong.boot.framework.json.CustomMappingJackson2HttpMessageConverter [1]org.springframework.http.converter.StringHttpMessageConverter

接下來spring boot將自定義的轉換器和默認的轉換器進行合并:

package org.springframework.boot.autoconfigure.web;public class HttpMessageConverters implements Iterable<HttpMessageConverter<?>> {public HttpMessageConverters(boolean addDefaultConverters,Collection<HttpMessageConverter<?>> converters) {// 將自定義的轉換器和默認的轉換器進行合并List<HttpMessageConverter<?>> combined = getCombinedConverters(converters,addDefaultConverters ? getDefaultConverters(): Collections.<HttpMessageConverter<?>>emptyList());combined = postProcessConverters(combined);this.converters = Collections.unmodifiableList(combined);} }

合并在方法getCombinedConverters中進行,具體的算法大家可以看看源代碼,我總結算法的主要核心如下:

1、比較自定義轉換器類型是否為可以替換默認轉換器的類型?例如 CustomMappingJackson2HttpMessageConverter 是可以替換默認的 MappingJackson2HttpMessageConverter。 2、如果是,將自定義轉換器放在默認轉換器的前面。

因此,我們可以最終看到如上所述的,CustomMappingJackson2HttpMessageConverter轉換器的順序排在了默認轉換器MappingJackson2HttpMessageConverter的前面。

注意,轉換器是采用read、write分離的2條職責鏈的設計模式,一旦某個轉換器的read/write可以處理請求,則退出職責鏈。

排除例外
定義自己的Serializer

上面的MappingJackson2HttpMessageConverter將所有的long都轉成了string,對于有些例外的情況,例如前端antd列表組件的總記錄數為number,java后端使用了pagehelper分頁組件,pagehelper的Page類返回的記錄總數total為long型,如果轉為string給前端就會有問題,因此,我們通過自定義的Serializer來排除這種例外。

import java.io.IOException;import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider;public class LongJsonSerializer extends JsonSerializer<Long> {@Overridepublic void serialize(Long value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)throws IOException {if (value != null) {jsonGenerator.writeNumber(value);}}}
如何使用?

使用自定義的PageBean類替換官方的PageInfo,并在PageBean類中使用:

@JsonSerialize(using = LongJsonSerializer.class) private long total; // 總記錄數

原文作者:大浪滔滔
原文鏈接:https://www.jianshu.com/p/f46699ea331a
來源:簡書

總結

以上是生活随笔為你收集整理的Spring Boot返回前端Long型丢失精度的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。