代码生成

Apache Freemarker

qianmoQqianmoQ· 更新于 2026-10-01· 阅读 13 分钟· 0 次阅读

登录后可跨设备保存划线和私人笔记登录

对于 Freemarker 语言输出,我们使用未经修改的 Apache Freemarker 版本来生成输出。

提供 PLC4X 语言模块的样板代码位于 org.apache.plc4x.plugins:plc4x-code-generation-language-base-freemarker maven 模块中,具体在 FreemarkerLanguageOutput 类中。

该类配置了 Freemarker 上下文,并在其中提供标准化属性:

  • packageName:Java 风格的包名,可用于创建某种形式的目录结构。
  • typeName:简单的字符串类型名。
  • type:ComplexTypeDefinition 实例,包含应为其生成代码的类型的全部信息。
  • helper:由于有时在 Freemarker 中生成所有输出非常复杂,helper 允许提供模板所使用的代码,以辅助生成输出。

一个基于 Freemarker 的输出模块,必须提供一组 Template 实例以及一个 FreemarkerLanguageTemplateHelper 实例。

总体上,我们将模板分为以下几类:

  • Spec Templates(整个驱动程序总共生成一次的全局输出)
  • Complex Type Templates(为复杂类型生成输出)
  • Enum Templates(为枚举类型生成输出)
  • DataIO Templates(为读取和写入 PlcValue 生成输出,PlcValue 是我们 PLC4X 用来向用户呈现输入和输出数据的形式)

对于每一类,开发者可以提供一个模板列表,从而为每个类型生成多个文件(这对于 C 这类语言很重要,因为每个类型我们都必须生成一个 Header file (.h) 和一个 Implementation (.c))。

FreemarkerLanguageOutput 所做的,是遍历协议模块提供的所有类型,然后遍历当前语言定义的所有模板。

此工具中使用的唯一约定是:模板生成的第一行输出将被视为相对于基础输出目录的路径。

它会自动创建所需的所有中间目录,并将输入的其余部分生成到第一行所指定的文件中。

如果该行为空,则跳过该类型的输出。

示例 Java 输出

package org.apache.plc4x.language.java;

import com.google.googlejavaformat.java.Formatter;
import com.google.googlejavaformat.java.FormatterException;
import freemarker.template.Configuration;
import freemarker.template.Template;
import org.apache.commons.io.FileUtils;
import org.apache.plc4x.plugins.codegenerator.protocol.freemarker.FreemarkerLanguageOutput;
import org.apache.plc4x.plugins.codegenerator.protocol.freemarker.FreemarkerLanguageTemplateHelper;
import org.apache.plc4x.plugins.codegenerator.types.definitions.TypeDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;

public class JavaLanguageOutput extends FreemarkerLanguageOutput {

    private static final Logger LOGGER = LoggerFactory.getLogger(JavaLanguageOutput.class);

    private final Formatter formatter = new Formatter();

    @Override
    public String getName() {
        return "Java";
    }

    @Override
    public Set<String> supportedOptions() {
        return Collections.singleton("package");
    }

    @Override
    public List<String> supportedOutputFlavors() {
        return Arrays.asList("read-write", "read-only", "passive");
    }

    @Override
    protected List<Template> getSpecTemplates(Configuration freemarkerConfiguration) {
        return Collections.emptyList();
    }

    @Override
    protected List<Template> getComplexTypeTemplates(Configuration freemarkerConfiguration) throws IOException {
        return Arrays.asList(
            freemarkerConfiguration.getTemplate("templates/java/pojo-template.java.ftlh"),
            freemarkerConfiguration.getTemplate("templates/java/io-template.java.ftlh"));
    }

    @Override
    protected List<Template> getEnumTypeTemplates(Configuration freemarkerConfiguration) throws IOException {
        return Collections.singletonList(
            freemarkerConfiguration.getTemplate("templates/java/enum-template.java.ftlh"));
    }

    @Override
    protected List<Template> getDataIoTemplates(Configuration freemarkerConfiguration) throws IOException {
        return Collections.singletonList(
            freemarkerConfiguration.getTemplate("templates/java/data-io-template.java.ftlh"));
    }

    @Override
    protected FreemarkerLanguageTemplateHelper getHelper(TypeDefinition thisType, String protocolName, String flavorName, Map<String, TypeDefinition> types,
                                                         Map<String, String> options) {
        return new JavaLanguageTemplateHelper(thisType, protocolName, flavorName, types, options);
    }

    @Override
    protected void postProcessTemplateOutput(File outputFile) {
        try {
            FileUtils.writeStringToFile(
                outputFile,
                formatter.formatSourceAndFixImports(
                    FileUtils.readFileToString(outputFile, StandardCharsets.UTF_8)
                ),
                StandardCharsets.UTF_8
            );
        } catch (IOException | FormatterException e) {
            LOGGER.error("Error formatting {}", outputFile, e);
        }
    }
}

getName 方法返回 Java,这正是需要在 plc4x-maven-plugin 配置的 language 选项中定义的内容,用于选择该输出格式。

supportedOptions 告诉插件此代码生成输出支持哪些 option 标签。对于 Java 输出,只有 package 选项,它定义了生成输出的包名。

通过 supportedOutputFlavors,我们告诉用户,总体上我们支持三个选项:read-write、read-only 和 passive,它们是代码生成插件 outputFlavor 配置选项的有效输入。

在这种情况下,Java 不需要为 java 生成任何全局文件,因此我们只需返回一个空集合。

对于复杂类型,我们目前使用两个模板(不过这很快会减少为一个)。因此,对于协议定义中的每个复杂类型,都会执行模板 templates/java/pojo-template.java.ftlh 和 templates/java/io-template.java.ftlh。

对于枚举类型,只使用一个模板。

与 data-io 的处理方式相同。

下一个重要的方法是 getHelper 方法,它返回一个对象,该对象以 helper 为名传递给模板。如前所述,许多操作用纯 Freemarker 代码实现会过于复杂,因此借助这些辅助工具,每种语言都可以提供一个辅助工具类来处理这些复杂操作。

下面是一个用于生成 Java POJO 的模板片段示例:

${helper.packageName(protocolName, languageName, outputFlavor)?replace(".", "/")}/${type.name}.java
/*
 * 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.
 */
package ${helper.packageName(protocolName, languageName, outputFlavor)};

... imports ...

// Code generated by code-generation. DO NOT EDIT.

public<#if type.isDiscriminatedParentTypeDefinition()> abstract</#if> class ${type.name}<#if type.parentType??> extends ${type.parentType.name}</#if> implements Message {

    ... SNIP ...

}

如你所见,第一行将生成待生成输出文件的文件路径。

随着我们为不同语言生成越来越多的输出,我们发现 Helper 工具中需要的大量代码存在重复,因此我们引入了一个所谓的 BaseFreemarkerLanguageTemplateHelper,其中包含了在生成新的语言输出时非常重要的一大批内容。

评论

登录后参与评论

正在加载评论…