代码生成
由于用多种语言为大量驱动手写代码无异于一场噩梦,我们投入了大量时间去寻找一种自动化的方式。
因此,最终我们需要三个部分:
- 协议定义
- 语言模板
- 一个生成代码的 Maven 插件
该 Maven 插件使用给定的协议定义和语言模板,生成使用该语言读写此协议数据的代码。
Types Base 模块提供了 Protocol 模块输出的所有结构,这些结构随后被用于 Language 模板中生成代码。
Protocol Base 和 Language Base 在此仅提供引用这些类型的接口,并为 plc4x-maven-plugin 提供可使用的 API。
这些模块同样维护在一个仓库中,该仓库与 PLC4X 其余代码相互独立。
这通常只是因为 Maven 构建系统的一些限制。如果你有兴趣了解其中的原因——请阅读本页末尾关于 Problems with Maven 的章节。
具体的协议规范解析器、代码生成器以及实际生成代码的模板,均在主项目仓库中 code-generation 部分下的派生模块中实现。
我们不想把自己局限在只有一种指定协议和生成代码的方式上。通常,用于指定驱动的格式有多种,同样,生成代码的方式也有多种。不过目前我们只有一个解析器:MSpec 和一个生成器:Freemarker。
它们为层次结构增加了更多层级。
因此,例如为 Java 生成一个 Siemens S7 驱动时,结构会是这样:
深蓝色部分是对外发布的部分,青绿色部分是 PLC4X 主仓库的一部分。
简介
该 Maven 插件的构建非常模块化。
因此,总体上可以添加新的提供协议定义的方式以及新的语言模板。
在指定协议的格式方面,我们试过许多工具和框架,但结果始终不尽如人意。
使用它们通常需要大量变通手段,使解决方案变得相当复杂。这主要是因为 Thrift、Avro、gRPC…… 等工具都是为将对象结构从 A 传输到 B 而设计的。它们关注的是保持对象结构不变,而不提供控制传输格式的方式。
现有的行业标准,例如 ASN.1,遗憾的是大多依赖大段文本来描述部分解析或序列化逻辑,这使得它们对于全自动代码生成几乎毫无用处。
最终,只有 DFDL 及其对应的 Apache 项目 Apache Daffodil 似乎提供了我们所寻找的能力。
借助它,我们得以提供完全以 XML 规范描述的首个驱动版本。
不好的一面是,PLC4X 社区认为这种 XML 格式相当复杂,而且在实现一个试验性的代码生成器时,我们很快发现由于无法在 DFDL 模式中对类型的继承关系建模,因此不可能生成出一个良好的对象模型。
最终我们设计出了自己的格式,称之为 MSpec,并在 MSpec 格式说明 中加以描述。
配置
plc4x-maven-plugin 的配置选项非常有限。
通常情况下,你需要指定的只有 protocolName 和 languageName。
额外的选项 outputFlavor 可以针对同一种语言生成某个驱动的多个版本。如果我们希望生成 read-only 或 passive mode 的驱动变体,这个选项就派上用场了。
为了能够在重构和改进协议规范时不必更新该协议的所有驱动,我们最近添加了 protocolVersion 属性,它允许我们提供并使用同一协议的多个版本。因此,如果我们更新了虚构的 wombat-protocol,就可以为此添加一个 version 2 mspec,然后在 java 驱动中使用版本 2,而在所有其他语言中继续使用版本 1。等所有驱动都更新完毕后,我们就可以再次去掉该版本。
最后但同样重要的是,我们还有一个非常通用的 options 配置选项,其类型为 Map。
通过这些选项,可以向代码生成过程传递通用参数。因此,如果某个驱动或语言需要进一步的定制,就可以使用这些选项。关于某一种语言模板所支持的全部选项列表,请参阅相应的语言页面。
目前,Java 模块就使用了这样一个选项来指定生成代码所使用的 Java package。如果未提供 package 选项,则使用默认包 org.apache.plc4x.{language-name}.{protocol-name}.{output-flavor};不过,尤其是在生成不属于 Apache PLC4X 项目的自定义驱动时,使用不同的包名更为合适。因此在这种情况下,用户只需覆盖默认的包名即可。
还有一个额外的参数:outputDir,其默认值为 ${project.build.directory}/generated-sources/plc4x/,对于 Java 项目来说通常无需更改,但在为其他语言生成代码时通常需要进行调整。
下面是一个用于为 java 构建 S7 驱动的驱动 pom 示例:
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>
<parent>
<groupId>org.apache.plc4x.plugins</groupId>
<artifactId>plc4x-code-generation</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>test-java-s7-driver</artifactId>
<build>
<plugins>
<plugin>
<groupId>org.apache.plc4x.plugins</groupId>
<artifactId>plc4x-maven-plugin</artifactId>
<executions>
<execution>
<id>test</id>
<phase>generate-sources</phase>
<goals>
<goal>generate-driver</goal>
</goals>
<configuration>
<protocolName>s7</protocolName>
<languageName>java</languageName>
<outputFlavor>read-write</outputFlavor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.apache.plc4x.plugins</groupId>
<artifactId>plc4x-code-generation-driver-base-java</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.plc4x.plugins</groupId>
<artifactId>plc4x-code-generation-language-java</artifactId>
<version>1.0.0</version>
<!-- Scope is 'provided' as this way it's not shipped with the driver -->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.plc4x.plugins</groupId>
<artifactId>plc4x-code-generation-protocol-s7</artifactId>
<version>1.0.0</version>
<!-- Scope is 'provided' as this way it's not shipped with the driver -->
<scope>provided</scope>
</dependency>
</dependencies>
</project>所以插件的配置非常简单,只需要指定 protocolName、languageName 和 output-flavor 即可。
依赖项:
<dependency>
<groupId>org.apache.plc4x.plugins</groupId>
<artifactId>plc4x-code-generation-driver-base-java</artifactId>
<version>1.0.0</version>
</dependency>例如,这里包含了生成代码所依赖的所有类。
s7 协议和 java 语言的定义由以下两个依赖提供:
<dependency>
<groupId>org.apache.plc4x.plugins</groupId>
<artifactId>plc4x-code-generation-language-java</artifactId>
<version>1.0.0</version>
<!-- Scope is 'provided' as this way it's not shipped with the driver -->
<scope>provided</scope>
</dependency>以及:
<dependency>
<groupId>org.apache.plc4x.plugins</groupId>
<artifactId>plc4x-code-generation-protocol-s7</artifactId>
<version>1.0.0</version>
<!-- Scope is 'provided' as this way it's not shipped with the driver -->
<scope>provided</scope>
</dependency>依赖项之所以以 code-dependencies 的方式添加,以及作用域如此设置的原因,见为什么协议和语言依赖项的添加方式如此奇怪?]一节。
自定义模块
该插件使用 Java Serviceloader] 机制来查找模块。
协议模块
要提供一个新的协议模块,只需创建一个模块,其中包含一个 META-INF/services/org.apache.plc4x.plugins.codegenerator.protocol.Protocol 文件,该文件引用了 org.apache.plc4x.plugins.codegenerator.protocol.Protocol 接口的一个实现。
该接口位于 org.apache.plc4x.plugins:plc4x-code-generation-protocol-base 模块中,通常只定义三个方法:
package org.apache.plc4x.plugins.codegenerator.protocol;
import org.apache.plc4x.plugins.codegenerator.types.exceptions.GenerationException;
import java.util.Optional;
public interface Protocol {
/**
* The name of the protocol what the plugin will use to select the correct protocol module.
*
* @return the name of the protocol.
*/
String getName();
/**
* Returns a map of type definitions for which code has to be generated.
*
* @return the Map of types that need to be generated.
* @throws GenerationException if anything goes wrong parsing.
*/
TypeContext getTypeContext() throws GenerationException;
/**
* @return the protocolVersion is applicable
*/
default Optional<String> getVersion() {
return Optional.empty();
}
}name 被模块用来找到正确的语言模块,因此 getName() 的返回值必须与 Maven 配置选项 protocolName 中提供的值相匹配。
如前所述,我们支持协议的多个版本,因此如果 getVersions() 返回非空版本,就会使用该版本来进行选择。
不过,对于实际的代码生成而言,最重要的方法是 getTypeContext(),它返回一个 TypeContext 类型,该类型通常包含给定协议的所有已解析类型列表。
语言模块
与 协议模块 类似,语言模块的构造方式也非常相似。
LanguageOutput 接口同样非常简单,位于 org.apache.plc4x.plugins:plc4x-code-generation-language-base 模块中,通常只定义了四个方法:
package org.apache.plc4x.plugins.codegenerator.language;
import org.apache.plc4x.plugins.codegenerator.types.definitions.ComplexTypeDefinition;
import org.apache.plc4x.plugins.codegenerator.types.exceptions.GenerationException;
import java.io.File;
import java.util.Map;
public interface LanguageOutput {
/**
* The name of the template is what the plugin will use to select the correct language module.
*
* @return the name of the template.
*/
String getName();
List<String> supportedOutputFlavors();
/**
* An additional method which allows generator to have a hint which options are supported by it.
* This method might be used to improve user experience and warn, if set options are ones generator does not support.
*
* @return Set containing names of options this language output can accept.
*/
Set<String> supportedOptions();
void generate(File outputDir, String version, String languageName, String protocolName, String outputFlavor,
Map<String, TypeDefinition> types, Map<String, String> options) throws GenerationException;
}用于注册语言模块的文件位于:META-INF/services/org.apache.plc4x.plugins.codegenerator.language.LanguageOutput
插件使用 name 来查找由 Maven 配置选项 languageName 定义的语言输出模块。
supportedOutputFlavors 提供了可供 Maven 配置选项 outputFlavor 引用的候选 flavor 列表。
supportedOptions 提供了一个 options 列表,列出了当前语言模块能够使用的内容,并且可以通过 options 设置项传递给 Maven 配置。
Maven 相关的问题
为什么前 4 个模块要单独发布?
我们在引言中提到过,前 4 个模块是在主 PLC4X 仓库之外进行维护和发布的。
这是由于 Maven 的一些限制所致,而这些限制源于 Maven 的整体工作方式。
主要问题在于,开始构建时,在 validate 阶段,Maven 会遍历配置、下载插件并对它们进行配置。这意味着 Maven 同样会尝试下载这些插件的依赖项。
如果在一个项目中既使用了某个 Maven 插件,又在该项目中构建该 Maven 插件本身,那么这必然会导致失败——尤其是在发布期间。而在日常开发过程中,Maven 大概只会从我们的 Maven 仓库下载最新的 SNAPSHOT,并对此感到满意,即使这个版本稍后会在构建过程中被覆盖也不会报错。等到确实需要时,它会直接使用新版本即可。
然而在发布期间,release 插件会把版本号改为发布版本,然后启动一次构建。在这种情况下,构建将会失败,因为任何地方都没有该版本的插件可供下载。此时唯一的选择就是手动以发布版本构建并部署该插件,然后重新启动发布流程(这对发布经理来说可不是一件愉快的事)。
因此,我们把该插件及其依赖精简到了绝对最小的程度,并将其与其余部分分开发布,希望凭借极少量的依赖,我们不必频繁地重复这一过程。
一旦该工具发布,PLC4X 构建中的版本就会随之更新,之后便可以毫无阻碍地使用发布版本。
为什么协议和语言依赖的处理方式如此别扭?
如果我们将协议模块和语言模块的依赖作为插件依赖提供,显然会干净得多。
然而,正如我们在上一小节中提到的,Maven 会在运行构建之前尝试下载并配置插件。因此在发布期间,新版本的模块还不存在,这会导致构建失败。
我们也可以把协议模块和语言模块分开发布,但我们希望语言模块和协议模块能够成为本项目的一部分,以免把事情弄得过于复杂——尤其是在发布期间。
为了让构建和发布尽可能简单,我们以这样的方式构建了该 Maven 插件:它使用各模块的依赖,并在运行时创建自己的 Classloader 来容纳所有这些模块。
这样做的好处是,可以利用 Maven 确定构建顺序以及动态创建各模块构建类路径的能力。
然而,如果添加普通的依赖,Maven 就会随其他模块一起部署这些构件。
我们不希望如此,因为协议模块和语言模块一旦被用于生成代码,就毫无用处了。
所以,我们采用了一个通常用于 Web 应用的技巧,例如:在这里,Servlet 引擎的供应商被期望提供Servlet API的实现。应用程序不允许自带该实现,但构建应用程序时又必须有它。
为此,我们使用 Maven 的 provided 作用域,它告诉 Maven 在构建期间提供该依赖,但将其排除在它所构建的任何应用程序之外,因为运行该应用程序的系统会提供它。
这并不完全准确,但确实管用。
评论
登录后参与评论
KnowForge