类型安全地将DSL直接嵌入到Java中

2020-09-18 04:19:09

核心框架通过Javac插件API作为通用类型适配器直接插入到Java编译器中,以允许直接无缝地提供Java的类型系统无法访问的类型和功能。因此,流形核心框架提供了一个基础和插件SPI来动态解析类型名称并生成相应的Java源代码,并且更一般地增强Java的类型系统。这样的插件称为类型流形,实现了ITypeManifold SPI。

您可以将类型流形视为即时代码生成器。本质上,流形框架插入并覆盖编译器的类型解析器,以便在编译器遇到类型名称时,类型流形可以通过ITypeManifold SPI声明类型名称的所有权,并动态地提供与类型相对应的源代码。(=。作为一种onSequence,这个核心功能可以作为传统代码生成技术的高效替代方案,同时也是静态类型方面早该有的进步。

首先,因为框架直接插入编译器,所以代码生成器作为类型流形不再是单独的构建步骤。更重要的是,当编译器要求类型时,类型流形会按需生成代码。这不仅显著降低了代码生成器的复杂性,还使它们能够以增量方式运行。这意味着可以编辑生成的源代码所基于的资源,并且只会重新生成和重新编译与更改相对应的类型。因此,与传统的代码生成器相反,类型流形:

此外,类型流形可以通过不同的方式协作并为类型提供资源。最常见的情况是,类型歧义注册为提供类型主体的主要贡献者。例如,JSON类型流形是主要贡献者,因为它提供与JSON模式文件或示例文件相对应的完整类型定义。或者,类型流形可以是部分贡献者或补充贡献者。例如,扩展类型流形是一个补充因素,因为它用额外的方法、接口和其他功能扩充了现有类型。因此,JSON和扩展类型流形都可以促成同一类型,其中JSON流形提供主体,而扩展类型流形提供自定义方法和由扩展类提供的其它特征。因此,任何数量的类型流形可以协同操作以形成高效且强大的类型构建流水线,因此与传统的代码生成器不同,类型流形:

最后,可以从IDE和其他工具中使用流形核心框架,以提供对所有类型流形产生的类型和功能的一致、统一的访问。例如,IntelliJ IDEA的流形插件为流形框架提供了全面的支持。完全支持从类型歧管和其他歧管SPI产生的所有类型和功能。您可以编辑JSON和GraphQL文件等资源,无需编译步骤即可立即在Java中使用更改。代码完成、资源/代码导航、确定性使用搜索、重构/重命名、增量编译、热插拔调试等功能可以与过去、现在和将来的所有类型模型无缝协作。与传统的代码生成世界相比,这代表着生产力的巨大飞跃,在传统的代码生成世界中,代码生成器作者或第三方承担着投资于一次性IDE工具项目的负担,这通常会导致较差的IDE表示或没有IDE表示。因此,与传统代码生成器相比,类型流形具有的另一大优势是:

总而言之,与传统的代码生成技术相比,流形框架提供了明显的优势。Typemanifold不需要构建步骤,总是同步的,增量操作,并且很容易添加到任何项目中,它们还可以自然地协作形成一个强大的类型构建管道,该管道通过核心框架可以被诸如IntelliJ IDEA和Android Studio等IDE统一访问。综上所述,这些改进所产生的协同效应有可能显著提高Java开发人员的工作效率,并为新的可能性敞开心扉。

ITypeManifold此SPI是实现类型流形的基础。请参阅现有类型的歧管项目,如流形-GraphQL。

ICompilerComponent实现这个低级SPI,以使用新的或增强的行为(例如,多重字符串和多重异常)来补充Java。

IPreProcessor实现此SPI,以便在源代码进入Java的解析器(例如,流形预处理器)之前提供一个预处理器来过滤源代码。

IProxyFactory此SPI解决结构化接口性能问题;实现此SPI是为了为特定的结构化接口提供编译时代理。

任何数据资源都是潜在的类型流形。其中包括文件架构、查询语言、数据库定义、数据服务、模板、电子表格、编程语言等。因此,虽然流形团队提供了几种开箱即用的类型流形,但可能的类型流形的领域实际上是无限的。重要的是,它们与我们提供的那些没有什么特别之处--您可以使用与构建我们的API相同的公共API来构建类型流形。

API是全面的,旨在满足80/20规则--常见用例很容易实现,但是API足够灵活,几乎可以实现任何类型的流形。例如,因为大多数类型流形都是基于源文件的,所以API基础类处理文件管理、缓存和建模方面的大部分繁琐工作。此外,由于类型流形的主要职责是动态生成Java源代码,因此Manifold为构建和呈现Java类提供了一个简单的API。但是API很灵活,所以您可以根据自己的喜好使用其他工具。

公共类ImageTypeManifold扩展JavaTypeManifold<;Model>;{private static Final Set<;string>;file_tensions=new HashSet<;>;(数组.asList(";jpg";,";png";,";bmp";,";wbmp";,";gif";));@Override public void init(IModule Module){init(module,Model::new);}@Override public Boolean handlesFileExtension(String FileExtension){return file_Extensions.concludes(fileExtension.toLowerCase());}@Override受保护的字符串aliasFqn(string fqn,IFile file){return fqn+';_';+file.getExtension();}@Override受保护的BoolisInnerType(string topLevel,String relativeInternal){return false;}@Override受保护的字符串产生(string topLevelFqn,String Existing,Model,DiagnoticListener<;JavaFileObject>;ErrorHandler){SrcClass srcClass=new ImageCodeGen(model._url,topLevelFqn).make();StringBuilder sb=srcClass.ender(new StringBuilder(),0);return sb.toString();}}。

像大多数类型流形一样,图像流形是基于文件扩展名的,特别是它处理具有图像扩展名的文件域:jpg、png等。正如您将看到的,JavaTypeManifold基类是为处理此用例而构建的。首先,ImageTypeManifold重写init()方法为基类提供其Model。我们很快就会报道这一点。接下来,它覆盖handlesFileExtension()以告诉基类它处理哪些文件扩展名。接下来,由于图像流形生成的类的名称与基本文件名略有不同,因此它覆盖aliasFqn(),为表单<;package>;.<;image-name>;_<;ext>;.的限定名提供别名。该名称必须与图像流形生成的类名相匹配。此流形没有生成内部类,因此它覆盖返回false的isInnerType();基类必须要求子类解析内部类型。最后,图像流形覆盖Contribute(),这是您为指定类名贡献Java源代码的地方。

大多数情况下,您会希望创建一个单独的类来处理Java源代码的生成。使用ImageCodeGen,图像流形可以做到这一点:

公共类ImageCodeGen{私有最终字符串_fqn;私有最终字符串_url;ImageCodeGen(String url,String topLevelFqn){_url=url;_fqn=topLevelFqn;}public SrcClass make(){string simpleName=ManClassUtil.getShortClassName(_Fqn);返回新的SrcClass(_fqn,SrcClass)。善良。类).Imports(URL.class,SourcePosition.class).SuperClass(new SrcType(ImageIcon.class)).addField(new SrcField(";instance";,simpleName).修饰符(修饰符.STATIC)).addConstructor(new SrcConstructor().addParam(new SrcParameter(";url";).type(URL.class)).修饰符(修饰符.Private).body(new SrcStatementBlock().addStatement(new SrcRawStatement().rawText(";url";).type(URL.class)).修饰符(修饰符.Private).body(new SrcStatBlock().addStatement(new SrcRawStatement().rawText(";Super(Url);)。";)).addStatement(new SrcRawStatement().rawText(";instance=this;";).addMethod(new SrcMethod().修饰符(修饰符.STATIC).name(";get";).return(SimpleName).body(new SrcStatementBlock().addStatement(new SrcRawStatement().rawText(";try{";).rawText(";return instance!=null?实例:new";+simpleName+";(new url(";\\";+ManEscapeUtil.escapeForJavaStringLiteral(_url)+";));";).rawText(";}catch(Exception E){";).rawText(";抛出新运行异常(E);";).rawText(";}";)

这里,图像流形利用SrcClass构建图像类的Java源模型。SrcClass是Manifold API中的源代码生成实用程序。它很简单,可以处理基本的代码生成用例。如果SrcClass不适合您的用例,您可以随意使用其他Java源代码生成工具,因为最终您在这里唯一的工作就是为您的类生成由Java源代码组成的字符串。

类模型扩展AbstractSingleFileModel{string_url;Model(string fqn,set<;ifile>;files){Super(fqn,files);assignUrl();}private void assignUrl(){try{_url=getFile().toURI().toString();}catch(MalformedURLException E){抛出新的运行异常(E);}}公共字符串getUrl(){return_url;;}@Override public void updateFile(IFile File){super.updateFile(File);assignUrl();}}。

该类将ImageCodeGen生成源所需的图像数据建模为AbstractSingleFileModel子类,在本例中,模型数据只是图像的URL。此外,Model覆盖updateFile()以使URL在可更改的环境(如IDE)中保持最新。

要在项目中使用类型流形,必须将其注册为服务。通常,作为将歧管的用户从这一步中拯救出来的类型库提供程序,您可以直接在META-INF中自行注册您的歧管,如下所示:

遵循标准的Java ServiceLoader协议,在META-INF目录下的服务目录中创建一个名为symold.api.type.ITypeManifold的文本文件。该文件应包含类型多重类(实现ITypeManifold的类)的完全限定名称,后跟一个新的空行:

如您所见,构建类型流形可能相对简单。图像流形说明了大多数基于文件的流形的基本结构。当然,API还有更多的内容。检查其他流形的源代码,比如GraphQL流形(manifold-GraphQL)和JavaScript流形(manifold-js),它们可以作为包装解析器和绑定到现有语言的不错的参考实现。

注意:对于带有命名模块的Java 9+,您可以使用Providers关键字在module-info.java文件中注册服务提供者:

有三种方法可以在构建中配置Manifold:静态、动态和混合。每种配置类型由以下标准确定:

在项目使用的特定于编译的所有流形依赖项上使用只编译(或提供)作用域。

使用流形javac插件的动态参数,例如,-Xplugin:流形vs.-Xplugin:流形动态。

注意,静态是大多数项目的推荐配置。使用它可以实现更小、更快、更多功能的项目。

表演。由于资源类型提前编译为.class文件,因此资源类型的加载时间与Java类型相同。

零启动时间。动态编译子系统和其他专用于编译的服务在运行时不存在,因此Manifold对启动时间没有影响。

足迹。由于在此配置中只分发运行时模块,因此所产生的运行时占用空间比动态和混合配置小10倍以上,这意味着下载速度更快。

不支持动态编译。仍然可以使用结构类型等动态功能,但必须使用静态代理工厂进行配置。

动态:在运行时动态编译所有资源类型。没有资源类型在编译时编译为.class文件。您的发行版中包含编译时和运行时二进制文件。

充满活力。无需构建静态桥即可支持结构类型等功能。此外,纯动态特性(如Dark Java)支持资源的动态运行时编译。

启动和初始化较慢。多种动态服务会导致较慢的启动时间。此外,由于在初始加载时进行动态编译,资源类型的初始化时间较慢。

占用的空间更大。编译时使用的动态服务也随运行时一起分发,额外的文件会显著影响总体占用空间(超过10倍)。

有限的使用。仅支持Java SE环境。使用此配置不支持其他JVM环境,如Android和Kotlin。

混合:静态编译项目资源类型(如资源文件),动态编译动态资源类型(如Dark Java和动态结构接口桥)。您的发行版中包含编译时和运行时二进制文件。

充满活力。无需构建静态桥即可支持结构类型等功能。此外,纯动态特性(如Dark Java)支持资源的动态运行时编译。

表演。由于资源类型提前编译为.class文件,因此资源类型的加载时间与Java类型相同。

启动和初始化较慢。多种动态服务会导致较慢的启动时间。此外,由于在初始加载时进行动态编译,资源类型的初始化时间较慢。

占用的空间更大。编译时使用的动态服务也随运行时一起分发,额外的文件会显著影响总体占用空间(超过10倍)。

有限的使用。仅支持Java SE环境。使用此配置不支持其他JVM环境,如Android和Kotlin。

**Kotlin完全支持所有流形资源类型,如GraphQL和JSON,但是,@JailBreak、单元表达式和预处理器等流形JavaExtension功能是特定于Java编译器的。

歧管组件的结构支持静态和动态使用。例如,流形核心组件由两个模块组成:流形和流形RT,其中流形包含所有编译时功能,流形RT包含运行时API和内部运行时实现专用的代码。因此,要在项目中静态使用Manifoldcore模块,您需要在流形上添加一个仅编译依赖项,并在流形-RT上添加一个默认依赖项。

所有的流形模块都是这样设计的。因此,像Manifold core这样同时提供编译时和运行时功能的组件由两个模块Manifold-xxx和Manifold-xxx-rt组成,因此只编译作用域用在manifold-xxx上,默认作用域用在manifold-xxx-rt上。编译时独占的组件或不提供任何编译时独占功能的组件不会定义单独的";rt";模块。例如,流形预处理器是编译时专用的,因此您总是使用仅编译作用域添加它。相反,多种科学库没有定义编译时独有的任何特性,因此您总是将其与默认作用域一起使用,以便将其与您的可执行工件打包在一起。

如果使用kotlin或其他替代的JVM语言,请将流形资源放在单独的Java编译模块中,并添加-Amanifold.source.<;file-ext>;=<;type-name-regex>;javac参数来显式编译资源。请参阅下面的显式资源编译。

注意,静态是大多数项目的推荐配置。使用它可以实现更小、更快、更多功能的项目。

对流形依赖项使用默认作用域,不需要向";rt";模块添加依赖项。

对流形依赖项使用默认作用域,不需要向";rt";模块添加依赖项。

请注意,动态配置和混合配置之间的唯一区别是动态流形插件参数,它阻止流形将资源类型编译到磁盘。

如果项目使用任何多种运行时依赖项(";rt&34;依赖项),默认情况下,流形会在所有类中插入一个静态块,以自动初始化某些运行时服务:

DasBoot()调用调用所有注册的IBootstrap服务。实现的数量和性质取决于您的应用程序如何处理流形。例如,如果动态使用流形,则此调用将初始化动态编译服务以及其他任务。然而,大多数项目静态地使用流形,这意味着DasBoot()通常只执行以下操作:

禁用Java 9警告";发生了非法的反射访问操作,因为该消息有不必要地警告用户的历史记录。这是在Java8/Android上运行时的注意事项。

动态打开java.base模块到流形模块进行公共反射访问,这是在Java8/Android上运行的noop。

注DasBoot()执行这些任务一次,随后的调用只是返回,即没有性能损失。

如果您知道您的代码永远不会在Java 9+上运行,并且/或者您不介意Java 9+警告消息,您可以通过no-bootstrap插件参数消除DasBoot()静态初始值设定项:

如果您需要对哪些类具有静态块进行更细粒度的控制,您可以使用@NoBootstrap注释来过滤特定的类。

注意,诸如流形预处理器、流形异常和流形字符串等仅编译依赖项不涉及任何运行时依赖项,因此,如果您的项目暴露于流形项仅限于这些依赖项,则静态块永远不会插入到项目的任何类中。

默认情况下,当Java编译器在代码中遇到资源类型时,Manifold会将它们编译到磁盘上。因此,代码中从未作为类型使用的资源不会编译。例如,如果您有数百个JSON资源文件,但是您的项目只使用了少数类型-使用Manifold是安全的,只有少数类型会以.classfile的形式编译到磁盘上。

然而,该方案不适合于打算用作依赖项或库的模块,其中需要编译的资源集可能包括比模块内使用的资源更多的资源。例如,根据GraphQL文件定义querymodel的API可能不会直接使用任何资源文件,但使用该API的模块会。尽管Manifold仍然可以在这样的情况下工作(参见上面的模式),但它是通过在运行时动态编译类型来实现的,这会导致每次调用时都会出现性能提升。

.