查看原文
其他

技术分享|Java SDK 动态类型

高同学 明源云天际PaaS平台 2022-09-10

源宝导读:在 ERP 产品中,根据客户的需求扩展某个数据列表的字段进行展示是基本能力之一,传统的扩展字段实现要么需要进行重启生效,要么性能存在问题,建模平台 Java SDK 在实现扩展字段上提出了自己的思路并进行了一定的实践,本文将分享建模平台 Java SDK 在扩展字段上的实现思路和实现实践。

一、背景

随着天际平台的诞生,承接了整个公司的技术底座。建模也同样需要从只支持ERP的产品线,扩充到支撑公司SaaS类业务。为了更多产品,客户能用上建模,享受低代码能力带来的便捷,我们引入Java SDK作为低代码的后端开发语言。配合上原有的.Net Core的改造,完成整个建模跨平台,跨语言开发,和容器化部署。

在 ERP 场景下,产品为了满足不同客户在同一产品上能有不同的功能表现,需要具备灵活的个性化能力。建模平台作为各 Erp 产品的底层支撑,使用元数据驱动的方式实现灵活的个性化能力,产品构建一套标准元数据,个性化再通过增量的方式构建一套自己的扩展元数据,结合两者,最终决定产品的对外表现。
在众多个性化功能中,数据库表个性化是核心之一。关系元数据(包含描述实体的元数据、描述实体之间的关联关系的元数据等)便是建模平台用以实现数据库表个性化的元数据,其对使用建模平台构建的应用所涉及的数据库表做出了明确的定义。在其发生变更时(如新增了一个字段),能调整数据库表结构(表结构变更)和代码中的对表的增删查改 SQL 语句(SQL 变更)是建模平台后端支撑个性化能力的重要一环,也是 Java SDK 个性化能力支持的首要任务。
本篇着眼于 Java SDK 的相关经验,在表结构变更和 SQL 变更上做出一些分享和探讨。

二、常用方案

表结构变更

在调整表结构上,SQL 语言已经有 DDL 语法来专门进行处理,直接使用 DDL 语法对数据库发送命令,即可即时对数据库表结构进行调整。

SQL 变更

实现 SQL 变更,有很多的解决方案,如:重新生成代码,预建字段,使用 EAV 模型等,在此对这些方案作出简要的分析和比较。
分析
重新生成代码:重新生成代码实现一般为通过代码生成器对增删查改的代码进行调整,然后通过重新启动服务的方式保证其生效。
预建字段:预建字段的方式一般在代码实体对象上预先就写死一批字段,如:field1,field2,...,field100,这种方式相对于重新生成代码而言在字段数量一定的情况下可以实现不需要重启,但如果超出预设字段数量,那就需要考虑扩充字段,然后重新启动服务使扩充字段生效。
EAV 模型:Entity-Attribute-Value 模型,通俗上讲就是列转行,也就是将数据库表的列信息用数据行来进行存储,这种方式的实现可以通过一套代码来进行关系元数据变更前后的操作,但由于数据库表设计接近了第 6 范式的内容,所以在增删查改上(特别是大数据量数据查询上)性能会有所下降。
方案对比

重新生成代码

预建字段

使用 EAV 模型

动态性静态静态动态
性能
说明:
  • 动态性:描述 SQL 变更是否对服务进行重启,静态即为需要,动态即为不需要。
  • 性能:描述在表数据增删查改时相对于通常写法(数据库第 3 范式上的增删查改)的效率差别,好即为相同,差即为效率有下降,不可能出现性能提升情况。
  • 总体上重新生成代码和预建字段都无法实现动态特性,使用 EAV 模型使数据库的表结构复杂化,在进行查询时会性能下降。


三、解决方案

表结构变更

表的结构调整,Java SDK 沿用常用方案,在关系元数据发生变更时,使用 DDL 即时对数据库表结构进行调整。

SQL 变更

增删查改 SQL 构建方式

增删查改 SQL 其实是 SQL 语言的 DML 语法。根据构建 DML 语句的方式不同,可粗糙的分为直接构建 DML 语句(SQL 式)和通过 ORM 转换为 DML 语句(ORM 式)。出于跨数据库的考量,Java SDK 使用 ORM 式作为命令生产的方式。

动态性支持

由于低代码平台的特性,需要能够支撑用户在平台上进行功能设计后马上就能预览体验到设计的功能,所以需要 SQL 变更的方案具备动态性(在服务运行期间能够实现功能变化)。

实现分析

需要使用 ORM 进行增删查改 SQL 的构建,又需要进行动态性的支持,所以 Java SDK SQL 变更实现的核心在于让 ORM 具备有动态特性(ORM 动态化)。ORM 的核心在于映射关系(内存中数据对象如何和数据库中的关系表进行对应)以及实体对象(对象形式的数据库表),所以具备动态性的 ORM 其核心的映射关系和实体对象要能够在不重启服务的情况下进行触发更新。根据 ORM 实现语言的不同、ORM 特性的不同,其动态化往往不同,在此,我们对在 Java 语言中使用 Hibernate(使用 Jpa 作为封装) 作为 ORM 的情况做进一步的分享和探讨。

四、ORM 动态化及 Java SDK 实现

(一)实现分析

映射关系定义的形式

在 Hibernate 中映射关系的定义表现为两种形式,一种为 xml 定义的形式,其映射关系的解析完全不依赖于其他,一种为注解定义的形式,其注解标注在实体对象上,需要结合实体对象进行映射关系的解析,两种形式的定义样例可见如下:
/xml形式<?xml version="1.0" encoding="UTF-8" ?><entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/ORM" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/ORM_2_1.xsd" version="2.1">...<entity access="FIELD" class="..."> <attributes> <basic access="FIELD" name="..."> <column name="..."/> </basic> ... </attributes></entity>...</entity-mappings>

//注解形式@Entity@Table(name = "...")public class Entity {
@Id @Column(name = "id") private String id;
@Column(name = "name") protected String name; ...}

分析

Hibernate(在 Jpa 的封装下)存储映射关系并向外提供服务的核心对象为 EntityManagerFactory(SessionFactory),由于没有寻找到 EntityManagerFactory 有合适的 API 直接进行映射关系的调整,所以需要使用变通方案运行时替换 EntityManagerFactory 以具备在服务运行期间替换映射关系的能力(动态替换映射关系)。另外替换的 EntityManagerFactory 仍然是通过 xml 或注解进行映射关系解析,所以根据需要还需要具备生成 xml 的能力(从关系元数据进行解析生成)或调整实体对象结构的能力以实现调整映射关系的内容。
ORM 的操作均基于实体对象,所以还需使用调整实体对象结构的能力对实体对象进行字段扩展以使其操作数据完整。
能具备有以上的三个能力(运行时替换 EntityManagerFactory、生成 xml、调整实体对象结构),即可实现 Java SDK 的 ORM 动态化,进而实现 SQL 变更,最终实现关系元数据的个性化支持。

运行时替换 EntityManagerFactory

Java SDK 使用代理的方式对 EntityManagerFactory 对象进行了代理,操作表数据的请求经过代理转发至真正处理请求的 EntityManagerFactory 实现上,当关系元数据发生变更时,将实现进行清空,然后再发生操作表数据请求时重新构建 EntityManagerFactory 实现进行请求处理,最终实现动态替换映射关系。
运行时 EntityManagerFactory 变化的过程可表现如下:
在构建 EntityManagerFactory 时,是使用 EntityManagerFactoryBuilder(构建器)进行构建。向构建器传递映射关系定义即可实现通过实体对象和 xml 构建映射关系。
构建器接入映射关系定义的点可简要展现如下:
/** * 扩展 EMF 构建器实现 */public class ExtensionEntityManagerFactoryBuilderImpl implements EntityManagerFactoryBuilder { ... //实体对象接入 final List<Class> loadedAnnotatedClasses = (List<Class>) configurationValues.remove(AvailableSettings.LOADED_CLASSES); if (loadedAnnotatedClasses != null) { for (Class cls : loadedAnnotatedClasses) { if (AttributeConverter.class.isAssignableFrom(cls)) { if (attributeConverterDefinitions == null) { attributeConverterDefinitions = new ArrayList<>(); } attributeConverterDefinitions.add(AttributeConverterDefinition.from((Class<? extends AttributeConverter>) cls)); } else { metadataSources.addAnnotatedClass(cls); } } } ... //xml 接入 final List<String> explicitORMXmlInputStream = (List<String>) configurationValues.remove(XML_FILE_CONTENT); if (explicitORMXmlInputStream != null) { explicitORMXmlInputStream.forEach(content -> { try (InputStream inputStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) { metadataSources.addInputStream(inputStream); } catch (IOException e) { throw new RuntimeException(e); } }); } ...}
生成 xml
xml 生成可以通过先构建对象再使用对象转 xml 的工具进行转换处理,异或是直接进行字符串拼接也可。
其转换代码简要展现如下:
//对象结构/** * OneToMany 节点 */@Getter@Setter@JacksonXmlRootElement(localName = "one-to-many")public class OneToMany extends RelationAttribute { @JacksonXmlProperty(localName = "mapped-by", isAttribute = true) private String mappedBy; ...}

//对象转 xmlimport com.fasterxml.jackson.datafORMat.xml.XmlMapper;...
/** * Xml工具类 */public class XmlUtil { ... /** * 序列化 */ public static String write(Object o) throws JsonProcessingException { return xmlMapper.writeValueAsString(o); } ...

public static void main(String[] args) { XmlUtil.write(xmlObject); }}
调整实体对象结构

运行时实体对象结构

在运行时实体对象结构由一对被继承关系维系的类型进行表现,分别是在编译期书写声明的实体类型(声明类型,一般包含产品对实体的定义)以及在运行时通过字节码技术生成的声明类型的子类型(动态类型,一般包含个性化对实体的扩展)。
其结构样例可见如图:
运行时映射关系定义
由于声明类型在运行时结构已不进行调整,所以其映射关系的定义形式为xml,动态类型由于本身就是使用字节码技术进行生成,所以采用注解作为其映射关系的定义形式。
创建动态类型
动态的生成一个类型需要使用字节码操作技术来进行实现,Java SDK 选用 Javassist 进行类型创建。
常见的字节码操作技术及其简要对比可见如下:

asm
javassist
cglib
instrumentation+javaagent

代码复杂度
复杂
不复杂
不复杂
不复杂
部署复杂度
不复杂
不复杂
不复杂
复杂
功能偏向性
构建类
构建类
代理类
修改类
使用 Javassist 创建类型流程和概括代码可见如下:
/** * 创建 Javassist 类型 */public CtClass createClass(String className, String superClassName) { ... //构建类型文件 ClassFile classFile = new ClassFile(false, className, superClassName); ... //设置访问性 classFile.setAccessFlags(AccessFlag.PUBLIC); ... //添加构造函数 ctClass.addConstructor(CtNewConstructor.defaultConstructor(ctClass)); ... //返回 return classFile;}

/** * 补充字段信息 */public void createField(CtClass targetCc, CtClass fieldType, String fieldName) { ... //创建字段 CtField field = new CtField(fieldType, fieldName, targetCc); ... //设置访问性 field.setModifiers(Modifier.PRIVATE); ... //绑定getter和setter方法 addGetterAndSetterMethodOfField(field); ... //添加字段 targetCc.addField(field);}

/** * 编译到 Jvm */public Class<?> toClass(CtClass targetCc) { ... return targetCc.toClass();}
调整
在关系元数据变更时,通过重新创建动态类型来实现实体对象结构进行调整,其主要调整实体对象和动态类型涉及到的映射关系(个性化的映射关系)(声明类型的通过重新生成 xml 进行调整)。

五、例子

这里使用供应商表作为例子对 Java SDK 动态类型做进一步的实现说明。
产品定义
在供应商表中,产品对其的定义为如下(核心为去除审计部分的红框内容):
其声明类型定义可见如下:
public class Supplier { private UUID supplierGUID; private String name; private UUID number; private String phone;}
个性化定义
在产品的基础上,个性化对供应商表新增了邮箱字段,其定义变更为如下:
数据新增
现需要对供应商表进行数据新增,其传输数据的 DTO 的 JSON 形式可见如下:
{ "name":"zonas", "number":"00000000-0000-0000-0000-000000000000", "phone":"135***2601", "email":"gao**@mingyuanyun.com"}
在运行时插入的实体对象为:
最终落入数据库的数据为如下图(省略部分审计字段):
分析
产品在代码中定义了供应商类型没有邮箱字段(声明类型),个性化扩展在界面上扩展了元数据结构新增了邮箱字段,数据库即时新增了 email 字段进行扩展,Java SDK 构建的应用上供应商类型表现的实际类型(动态类型)即时包含了 email 属性,然后及由 ORM 最终将由前端传入的数据(携带有扩展数据 email)成功落到数据库。

六、局限及其优化思考

(一)局限

现今 Java SDK 的实现方式存在有一定的局限性,其主要如下:
产品在编码时使用的类型仍为声明类型,在数据映射、数据操作类(Repository)等方1面都需要将操作的声明类型转为动态类型,给上层代码框架造成了兼容负担。
代理替换 EntityManagerFactory 的方式即使发生微小改动也会造成整个 EntityManagerFactory 的替换,而频繁的 EntityManagerFactory 构建,会给系统带来一定的性能开销。

(二)优化思考

框架兼容负担

框架的兼容负担,很大一部分来源于声明类型和动态类型的继承关系导致。去除继承结构,使声明类型和动态类型同一在一定层度上可以减少框架的兼容负担。Java 类型的唯一性由类加载器和全类名进行确定,扩展使用自定义类加载器进行对象类的加载可以作为实现类型同一的手段。

EntityManagerFactory 频繁构建性能开销

频繁构建的原因在于无法实现 EntityManagerFactory 内部维护的对象和映射内容的局部刷新,最直接的思考便是持续的研究、改造 ORM 的源码使其具备有局部刷新的能力。另外的一个思路在于既然整个 EntityManagerFactory 的刷新不可避免,那可以将业务对象以某种维度进行划分,然后由多个 EntityManagerFactory 进行维护,从业务上减少 EntityManagerFactory 维护的内容,这样也可以减少一部分由 EntityManagerFactory 频繁刷新导致的性能开销。

------ END ------
作者简介
高同学: 研发工程师,目前负责建模平台相关工作。

也许您还想看:
技术分享|给混合应用装上涡轮增压
技术分析|在 Nginx 服务器启用 GZip 压缩的实践
更多明源云·天际开放平台场景案例与开发小知识,可以关注明源云天际开发者社区公众号:
【集成】API 参数映射之 JavaScript引擎介绍
【建模】用户自定义搜索条件,检索效率快人一步
移动新玩法,0代码实现已有业务的实时在线

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存