springboot解決Long類型數據傳入前端損失精度


  使用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;
    }
}
View Code

這就導致了,我自定義的那個配置類解決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

 https://blog.csdn.net/nicolas12/article/details/83342151


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM