使用MybatisPlus默認的主鍵生成策略是雪花算法生成的19位數字,數據庫使用bigint19字節,實體類Long類型,vo為了方便復制id屬性也是Long類型,結果導致一個問題:前端js number類型接收時導致精度丟失。
js的number類型有個最大值(安全值)。即2的53次方,為9007199254740992。如果超過這個值,那么js會出現不精確的問題。這個值為16位。
下面提幾個解決辦法:
1、這個方法比較麻煩就是設置一個額外的idStr字符串類型的id值返回給前端使用,不推薦。
2、注解方式:屬性上增加注解
/** * 主鍵 */ //@JSONField(serializeUsing= ToStringSerializer.class) //@JsonFormat(shape = JsonFormat.Shape.STRING) @JsonSerialize(using = ToStringSerializer.class) private Long id; /** * 創建時間 */ @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8") private Date createTime;
3、自定義ObjectMapper :啟動類中增加配置
@SpringBootApplication @EnableTransactionManagement public class Application { /** * 解決Jackson導致Long型數據精度丟失問題 * @return */ @Bean public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { ObjectMapper objectMapper = builder.createXmlMapper(false).build(); objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); SimpleModule module = new SimpleModule(); module.addSerializer(Long.class, ToStringSerializer.instance); module.addSerializer(Long.TYPE, ToStringSerializer.instance); objectMapper.registerModule(module); return objectMapper; } public static void main(String[] args){ SpringApplication.run(Application.class,args); } }
或者通過方式:
@JsonComponent public class JsonSerializerManage { @Bean public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { ObjectMapper objectMapper = builder.createXmlMapper(false).build(); //忽略value為null 時 key的輸出 objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); /** * 序列換成json時,將所有的long變成string * 因為js中得數字類型不能包含所有的java long值 */ SimpleModule module = new SimpleModule(); module.addSerializer(Long.class, ToStringSerializer.instance); module.addSerializer(Long.TYPE, ToStringSerializer.instance); objectMapper.registerModule(module); return objectMapper; } }
4、配置參數:在yml或properties中增加配置,不推薦。
jackson: generator: write_numbers_as_strings: true
該方式會強制將所有數字全部轉成字符串輸出,這種方式的優點是使用方便,不需要調整代碼;缺點是顆粒度太大,所有的數字都被轉成字符串輸出了,包括按照timestamp格式輸出的時間也是如此。不推薦。
5、自定義全局轉換器
springboot2以下的版本寫個配置類實現WebMvcConfigurerAdapter重寫configureMessageConverters方法。
springboot2及其以上的版本寫個配置類實現WebMvcConfigurer重寫configureMessageConverters方法(2以上版本WebMvcConfigurerAdapter已經廢棄了,不推薦使用)。
代碼如下:
package com.thecityos.city.indicator.admin.common.config; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import com.alibaba.fastjson.serializer.SerializeConfig; import com.alibaba.fastjson.serializer.ToStringSerializer; import com.alibaba.fastjson.support.config.FastJsonConfig; import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; /** * @title: WebMvcConfig * @description: 自定義轉換 * @author: * @date 2019-08-03 16:23 */ @Configuration public class WebMvcConfig implements WebMvcConfigurer { /** * 解決主鍵Long類型返回給頁面時,頁面精度丟失的問題,時間格式化返回 * @param converters */ @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); //格式化json數據格式 FastJsonConfig fastJsonConfig = new FastJsonConfig(); //序列化時避免精度丟失,轉換為字符串 SerializeConfig serializeConfig = SerializeConfig.globalInstance; serializeConfig.put(BigInteger.class, ToStringSerializer.instance); serializeConfig.put(Long.class, ToStringSerializer.instance); serializeConfig.put(Long.TYPE, ToStringSerializer.instance); fastJsonConfig.setSerializeConfig(serializeConfig); fastJsonConfig.setDateFormat("yyyy-HH-dd HH:mm:ss"); fastConverter.setFastJsonConfig(fastJsonConfig); List<MediaType> fastMediaTypes = new ArrayList<>(); fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8); fastMediaTypes.add(MediaType.APPLICATION_JSON); fastConverter.setSupportedMediaTypes(fastMediaTypes); converters.add(0,fastConverter); } }
PS:一些常見的問題,寫了后沒生效,大概率是雖然添加了轉換器進去,但是沒在首位或者被后續一些配置擠在了后面,因為springmvc處理時,converters里包含很多轉換器,但是它匹配到第一個轉換器后就直接使用了,后續轉換器無效。
我就遇到了項目里因為引用了
<dependency>
<groupId>com.github.rkonovalov</groupId>
<artifactId>json-ignore</artifactId>
<version>1.0.14</version>
</dependency>
這個是解決controller層想屏蔽或者只返回某些字段的一個注解依賴。
但它有個類FilterRegister實現了WebMvcConfigurer並且重寫了extendMessageConverters
如下:
package com.jfilter.components; import com.jfilter.EnableJsonFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.format.FormatterRegistry; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.config.annotation.*; import java.util.List; /** * This class used for register FilterConverter in Spring converter list * * <p>Class depends from {@link EnableJsonFilter} annotation */ @Configuration public class FilterRegister implements WebMvcConfigurer { private FilterConfiguration filterConfiguration; @Autowired public FilterRegister(FilterConfiguration filterConfiguration) { this.filterConfiguration = filterConfiguration; } @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { // Do nothing } @Override public void configurePathMatch(PathMatchConfigurer configurer) { // Do nothing } @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { // Do nothing } @Override public void configureAsyncSupport(AsyncSupportConfigurer configurer) { // Do nothing } @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { // Do nothing } @Override public void addFormatters(FormatterRegistry registry) { // Do nothing } @Override public void addInterceptors(InterceptorRegistry registry) { // Do nothing } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // Do nothing } @Override public void addCorsMappings(CorsRegistry registry) { // Do nothing } @Override public void addViewControllers(ViewControllerRegistry registry) { // Do nothing } @Override public void configureViewResolvers(ViewResolverRegistry registry) { // Do nothing } @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { // Do nothing } @Override public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) { // Do nothing } /** * Add converter if filtration is enabled * * @param converters list of {@link HttpMessageConverter} */ @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { if (filterConfiguration.isEnabled()) converters.add(0, new FilterConverter(filterConfiguration)); } @Override public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) { // Do nothing } @Override public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) { // Do nothing } @Override public Validator getValidator() { return null; } @Override public MessageCodesResolver getMessageCodesResolver() { return null; } }
這就導致了,我自定義的那個配置類解決Long類型丟失的轉換器被擠在了后面,所以沒有生效。
5.1 Jackjson配置轉換方式:
@EnableWebMvc @Configuration public class WebConfig extends WebMvcConfigurerAdapter { public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); ObjectMapper objectMapper = new ObjectMapper(); /** * 序列換成json時,將所有的long變成string * 因為js中得數字類型不能包含所有的java long值 */ SimpleModule simpleModule = new SimpleModule(); simpleModule.addSerializer(Long.class, ToStringSerializer.instance); simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance); objectMapper.registerModule(simpleModule); jackson2HttpMessageConverter.setObjectMapper(objectMapper); converters.add(jackson2HttpMessageConverter); } }
Spring HttpMessageConverter的作用及替換解析
相信使用過Spring的開發人員都用過@RequestBody、@ResponseBody注解,可以直接將輸入解析成Json、將輸出解析成Json,但HTTP 請求和響應是基於文本的,意味着瀏覽器和服務器通過交換原始文本進行通信,而這里其實就是HttpMessageConverter發揮着作用。
HttpMessageConverter
Http請求響應報文其實都是字符串,當請求報文到java程序會被封裝為一個ServletInputStream流,開發人員再讀取報文,響應報文則通過ServletOutputStream流,來輸出響應報文。
從流中只能讀取到原始的字符串報文,同樣輸出流也是。那么在報文到達SpringMVC / SpringBoot和從SpringMVC / SpringBoot出去,都存在一個字符串到java對象的轉化問題。這一過程,在SpringMVC / SpringBoot中,是通過HttpMessageConverter來解決的。HttpMessageConverter接口源碼:
public interface HttpMessageConverter<T> { boolean canRead(Class<?> clazz, MediaType mediaType); boolean canWrite(Class<?> clazz, MediaType mediaType); List<MediaType> getSupportedMediaTypes(); T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException; void write(T t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException; }
下面以一例子來說明:
@RequestMapping("/test")
@ResponseBody
public String test(@RequestBody String param) {
return "param '" + param + "'";
}
在請求進入test方法前,會根據@RequestBody注解選擇對應的HttpMessageConverter實現類來將請求參數解析到param變量中,因為這里的參數是String類型的,所以這里是使用了StringHttpMessageConverter類,它的canRead()方法返回true,然后read()方法會從請求中讀出請求參數,綁定到test()方法的param變量中。
同理當執行test方法后,由於返回值標識了@ResponseBody,SpringMVC / SpringBoot將使用StringHttpMessageConverter的write()方法,將結果作為String值寫入響應報文,當然,此時canWrite()方法返回true。
借用下圖簡單描述整個過程:

在Spring的處理過程中,一次請求報文和一次響應報文,分別被抽象為一個請求消息HttpInputMessage和一個響應消息HttpOutputMessage。
處理請求時,由合適的消息轉換器將請求報文綁定為方法中的形參對象,在這里同一個對象就有可能出現多種不同的消息形式,如json、xml。同樣響應請求也是同樣道理。
在Spring中,針對不同的消息形式,有不同的HttpMessageConverter實現類來處理各種消息形式,至於各種消息解析實現的不同,則在不同的HttpMessageConverter實現類中。
替換@ResponseBody默認的HttpMessageConverter
這里使用SpringBoot演示例子,在SpringMVC / SpringBoot中@RequestBody這類注解默認使用的是jackson來解析json,看下面例子:
@Controller @RequestMapping("/user") public class UserController { @RequestMapping("/testt") @ResponseBody public User testt() { User user = new User("name", 18); return user; } }
public class User { private String username; private Integer age; private Integer phone; private String email; public User(String username, Integer age) { super(); this.username = username; this.age = age; } }
瀏覽器訪問/user/testt返回如下:

這就是使用jackson解析的結果,現在來改成使用fastjson解析對象,這里就是替換默認的HttpMessageConverter,就是將其改成使用FastJsonHttpMessageConverter來處理Java對象與HttpInputMessage/HttpOutputMessage間的轉化。
首先新建一配置類來添加配置FastJsonHttpMessageConverter,Spring4.x開始推薦使用Java配置加注解的方式,也就是無xml文件,SpringBoot就更是了。
import com.alibaba.fastjson.serializer.SerializerFeature; import com.alibaba.fastjson.support.config.FastJsonConfig; import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; import org.springframework.boot.autoconfigure.web.HttpMessageConverters; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import java.nio.charset.Charset; @Configuration public class HttpMessageConverterConfig { //引入Fastjson解析json,不使用默認的jackson //必須在pom.xml引入fastjson的jar包,並且版必須大於1.2.10 @Bean public HttpMessageConverters fastJsonHttpMessageConverters() { //1、定義一個convert轉換消息的對象 FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); //2、添加fastjson的配置信息 FastJsonConfig fastJsonConfig = new FastJsonConfig(); SerializerFeature[] serializerFeatures = new SerializerFeature[]{ // 輸出key是包含雙引號 // SerializerFeature.QuoteFieldNames, // 是否輸出為null的字段,若為null 則顯示該字段 // SerializerFeature.WriteMapNullValue, // 數值字段如果為null,則輸出為0 SerializerFeature.WriteNullNumberAsZero, // List字段如果為null,輸出為[],而非null SerializerFeature.WriteNullListAsEmpty, // 字符類型字段如果為null,輸出為"",而非null SerializerFeature.WriteNullStringAsEmpty, // Boolean字段如果為null,輸出為false,而非null SerializerFeature.WriteNullBooleanAsFalse, // Date的日期轉換器 SerializerFeature.WriteDateUseDateFormat, // 循環引用 SerializerFeature.DisableCircularReferenceDetect, }; fastJsonConfig.setSerializerFeatures(serializerFeatures); fastJsonConfig.setCharset(Charset.forName("UTF-8")); //3、在convert中添加配置信息 fastConverter.setFastJsonConfig(fastJsonConfig); //4、將convert添加到converters中 HttpMessageConverter<?> converter = fastConverter; return new HttpMessageConverters(converter); } }
這里將字符串類型的值如果是null就返回“”,數值類型的如果是null就返回0,重啟應用,再次訪問/user/testt接口,返回如下:

可以看到此時null都轉化成“”或0了。
參考文章:
https://blog.csdn.net/SkyFire1121/article/details/91383772
https://blog.csdn.net/tsh18523266651/article/details/98588235
