最近为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位浮点数)。其结构如图:

image.png

各位的含义如下:

1位(s) 用来表示符号位,0表示正数,1表示负数

11位(e) 用来表示指数部分

52位(f) 表示小数部分(即有效数字)

双精度浮点数(double)并不是能够精确表示范围内的所有数, 虽然双精度浮点型的范围看上去很大:

。 可以表示的最大整数可以很大,但能够精确表示、使用算数运算的并没有这么大。因为小数部分最大是 52 位,因此 JavaScript 中能精准表示的最大整数是

,十进制为 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表示的整数,也就是说在(

,

)范围内,双精度数表示和整数是一对一的,在这个范围以内,所有的整数都有唯一的浮点数表示,这叫做安全整数。

而超过这个范围,会有两个或更多整数的双精度表示是相同的;即超过这个范围,有的整数是无法精确表示的,只能大约(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位(扣除一位符号位),其最大值为

,十进制为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的一些开源库 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() {

@Override

public 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-urlencoded

application/x-www-form-urlencoded

SourceHttpMessageConverter

数据与 javax.xml.transform.Source 的相互转换

text/xml 和 application/xml

text/xml 和 application/xml

MarshallingHttpMessageConverter

使用 Spring 的 Marshaller/Unmarshaller 转换 XML 数据

text/xml 和 application/xml

text/xml 和 application/xml

MappingJackson2HttpMessageConverter

使用 Jackson 的 ObjectMapper 转换 Json 数据

application/json

application/json

MappingJackson2XmlHttpMessageConverter

使用 Jackson 的 XmlMapper 转换 XML 数据

application/xml

application/xml

BufferedImageHttpMessageConverter

数据与 java.awt.image.BufferedImage 的相互转换

Java I/O API 支持的所有类型

Java I/O API 支持的所有类型

image.png

注意到AbstractMessageConverterMethodProcessor类的getProducibleMediaTypes、writeWithMessageConverters等方法在每次消息解析转换都要作GenericHttpMessageConverter分支判断,为什么呢?

package org.springframework.web.servlet.mvc.method.annotation;

public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver

implements HandlerMethodReturnValueHandler {

protected List getProducibleMediaTypes(HttpServletRequest request, Class> valueClass, Type declaredType) {

Set mediaTypes = (Set) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

if (!CollectionUtils.isEmpty(mediaTypes)) {

return new ArrayList(mediaTypes);

}

else if (!this.allSupportedMediaTypes.isEmpty()) {

List result = new ArrayList();

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

@ConditionalOnMissingBean

public 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 对象

*/

@Override

public boolean canRead(Class> clazz, MediaType mediaType) {

// 不需要反序列化

return false;

}

/**

* 判断该转换器是否能将请求内容转换成 Java 对象

*/

@Override

public boolean canRead(Type type, Class> contextClass, MediaType mediaType) {

// 不需要反序列化

return false;

}

/**

* 判断该转换器是否可以将 Java 对象转换成返回内容.

* 匹配web api(形如/web/xxxx)中的接口方法的返回参数

*/

@Override

public 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 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> 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> converters;

public HttpMessageConvertersAutoConfiguration(

ObjectProvider>> 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> {

public HttpMessageConverters(boolean addDefaultConverters,

Collection> converters) {

// 将自定义的转换器和默认的转换器进行合并

List> combined = getCombinedConverters(converters,

addDefaultConverters ? getDefaultConverters()

: Collections.>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 {

@Override

public 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; // 总记录数

前端参数无法转为后端实体内部类_Spring Boot返回前端Long型丢失精度相关推荐

  1. 前端参数无法转为后端实体内部类_javaweb实现自动将前端的属性值(HttpServletRequest)转化为后台对应的实体类...

    通常我们在后台需要获取前端的属性值的时候用到的是request.getParameter("xxx"); 下面的方法将实现自动将HttpServletRequest中的值封装到实体 ...

  2. 前端参数无法转为后端实体内部类_Java学到什么程度才能叫精通?

    怎样才算是把Java学到精通的地步? 我是第226个回答这个问题的答主,我花了大半天时间仔细浏览了排在前面的100个答主的回答以及文章,视频以及各种资料介绍.很多答主都是一些行业里面的精英,知乎平台的 ...

  3. java前后端分离框架_Spring Boot 入门及前后端分离项目实践

    本课程是一个 Spring Boot 技术栈的实战类课程,课程共分为 3 个部分,前面两个部分为基础环境准备和相关概念介绍,第三个部分是 Spring Boot 项目实践开发.Spring Boot ...

  4. java程序员需要会前端吗_一个后端程序员,需要掌握前端技术吗?

    一个后端程序员,需要掌握前端技术吗? JSP时代 8年前,刚刚进入编程这个行业,当时的Web开发使用古老的SSH框架+JSP.那个时候,几乎所有的Java程序员都要懂得如何写JavaScript.如何 ...

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

    最近为Prong开发了一个基于snowflake算法的Java分布式ID组件,将实体主键从原来的String类型的UUID修改成了Long型的分布式ID.修改后发现前端显示的ID和数据库中的ID不一致 ...

  6. 后端的日期类型赋值前端表单_Spring Boot实践--前端字符串日期自动转换成后台date类型。...

    简单介绍 在前后台开发的时候:日期格式会转来转去,很麻烦.大致总结如下: 1:后端返回对象:可以使用spring提供的:HttpMessageConverter来自动转换,有很多实现. 比如:Abst ...

  7. 后端实体类接收数组_前端代码+后端API,值得一学的Vue高仿音乐播放器实战项目...

    项目名称:vue-fds_music 项目作者:符道胜 开源许可协议:Apache-2.0 项目地址:https://gitee.com/fudaosheng/vue-fds_music 项目简介 V ...

  8. swagger内部类_spring boot 1.5.4 集成spring-Data-JPA(七)

    1Spring Boot使用Spring-Data-JPA访问数据库 spring boot整合jdbcTemplate项目源码: spring-boot相关项目源码, 1.1Sping data J ...

  9. 写给刚入门的前端工程师的前后端交互指南

    转自原文 写给刚入门的前端工程师的前后端交互指南 作为刚接触前端的不久的童鞋,大家都会兴奋于CSS和JS所带来漂亮界面,然而,前端工程师除了UI重构外,还有非常重要的职责在正确的区域渲染出服务端的数据 ...

最新文章

  1. 从tomcat下载文件的配置方法(很全呢)
  2. SAP 修改物料价格那些事
  3. 同一公司代码下工厂间的库存转储 (轉載)
  4. JMS 在 SpringBoot 中的使用
  5. 三菱模拟量输入与输出程序_初学PLC是学习西门子还是三菱?
  6. 后台代码和前台显示一样a href=' + URL + ' 使用转义字符
  7. 检验例题_高一化学微课之81氨气的制备和铵根离子的检验
  8. 物联网形势大好,传感器前景可观
  9. C 语言指针与汇编地址(一)
  10. Java简单实现贪吃蛇经典小游戏(附源代码)
  11. 21年美赛F题-DEA模型和逻辑回归模型
  12. 【Simulink】电力系统仿真常用模块位置
  13. Exadata使用EXAchk进行健康检查
  14. 怎么把ide改成ahci_怎么IDE改成AHCI
  15. Github上最热门的Java开源项目
  16. 消除“Permission is only granted to system apps”错误
  17. 深度学习模块介绍 —— Hourglass Module
  18. 题解 UVA12304 【2D Geometry 110 in 1!】
  19. org.apache.flink.shaded.guava18.com.google.common.util.concurrent.ThreadFactoryBuilder 真实解决方案
  20. 图片风格迁移:基于实例缓解细节丢失、人脸风格化失败问题

热门文章

  1. 301转向和网址规范化
  2. matlab编译器和程序发布
  3. WPF ControlTemplate TemplateBinding
  4. sicily 1762. 排座椅
  5. DirectSound学习笔记(3):协作级别
  6. MySQL 同步(三)不同版本问题
  7. Updater Application Block v1.0 翻译文档目录
  8. java actionsupport_struts2中的Action接口和Actionsupport接口各有什么作用
  9. oracle 1g apex030200,APEX_030200
  10. flink 6-检查点和水位线