<导航

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

posted @ 2020-01-11 13:56  字节悦动  阅读(8476)  评论(2编辑  收藏  举报