Ohhnews

分类导航

$ cd ..
foojay原文

OpenAPI、ORM、SVG与Lottie

#openapi#orm#svg#lottie#构建时代码生成

目录

[LOADING...]

本文是周五发布公告后的第三篇后续文章。周六的内容涉及迭代方法;昨日介绍的是核心中的新平台 API;今日则聚焦一系列改变应用结构性部分编写方式的组件。

这些组件包括:OpenAPI 客户端生成器、SQLite ORM、JSON 和 XML 映射器、带验证的组件绑定器、构建时 SVG 和 Lottie 转码器,以及带深度链接的声明式路由器。它们全部运行在同一个构建时代码生成管道上:一个 Maven 插件阶段,在构建时读取注解或声明式源文件,并生成编译到二进制文件中的类型化 Java 代码。无需反射、服务加载器或 Class.forName。本文末尾的“工作原理”部分将详细介绍代码生成的机制,帮助理解各组件背后的技术。

OpenAPI 客户端生成

本版本的头条功能,为任何需要对接后端的团队量身打造。

新增的 cn1:generate-openapi-client Mojo 可读取 OpenAPI 3.x JSON 规范(支持 URL 或本地文件),并生成类型化的 Codename One 客户端代码,直接编译到应用中:

  • 每个 components.schemas 条目对应一个 @Mapped POJO。
  • 每个 OpenAPI 标签对应一个 Api.java 类,其中每个操作对应一个流畅风格的方法。
  • 所有方法均通过 Rest. + Mappers.toJson + fetchAsMapped / fetchAsMappedList 完成,因此生成的表层与框架其余部分无缝集成,无需引入额外的 HTTP 栈。

在项目的 pom.xml 中配置如下:

$ xml
    ...
    com.codenameone
    codenameone-maven-plugin
    ...
            
                petstore-client
                generate-openapi-client
                
                    https://petstore3.swagger.io/api/v3/openapi.json
                    com.example.petstore
                
            
        

执行 mvn generate-sources 即可获取规范、下载并在 target/generated-sources/ 下为每个 schema 和每个标签生成一个文件。Petstore 参考规范端到端测试生成了六个模型类(PetOrderCustomerTagCategoryUser)和三个 API 类(PetApiStoreApiUserApi),生成的九个 .class 文件能够干净地编译到 codenameone-core 中。详细文档请参阅 OpenAPI 代码生成 Maven 目标

在应用代码中,调用生成的 Api 类与调用任何其他 Java 方法无异:

$ java
PetApi pets = new PetApi();

// 返回 AsyncResource;解析后得到反序列化的对象。
pets.getPetById(42).onResult((pet, err) -> {
    if (err == null) Log.p("Got " + pet.getName());
});

// 返回 AsyncResource<List>。
pets.findPetsByStatus("available").onResult((list, err) -> {
    if (err == null) {
        for (Pet p : list) Log.p(p.getName());
    }
});

// POST 请求带请求体。addPet 接收 Pet 并返回 Pet。
Pet candidate = new Pet();
candidate.setName("Mittens");
candidate.setStatus("available");
pets.addPet(candidate).onResult((created, err) -> { /* ... */ });

无需手写 ConnectionRequest 设置、手动 JSON 解析或字符串类型的请求体。生成的客户端接收类型化的 Pet,通过 Mappers.toJson(...) 序列化,发送正确的 HTTP 动词,通过 Mappers.fromJson(...) 反序列化响应,并通过框架的 AsyncResource 将结果呈现在回调中(在 EDT 上触发)。

对于已经将 OpenAPI 规范作为后端一部分发布的团队(大多数现代后端框架自动提供此功能:FastAPI、Spring 的 springdoc-openapi、NestJS、ASP.NET Core、Go 的 gnostic),实际效果是移动客户端的绑定与后端保持同步,无需任何人手写任何网络调用。更新规范,重新运行 mvn generate-sources,新增或更改的端点就会以类型化 Java 代码形式出现在应用中,IDE 立即可用。

这种改动在你不知道自己拥有它时最为有用:拉取一份新规范,重新构建,IDE 便会高亮所有调用了已重命名端点或传递了错误类型参数的代码位置。

SQLite ORM

使用 @Entity 标记类;@Id@Column 定义 schema;@DbTransient 排除某个字段:

$ java
@Entity
public class TodoItem {
    @Id @Column                  long id;
    @Column                      String title;
    @Column(name = "completed_at")
                                 Date completedAt;
    @DbTransient                 Object cachedView;
}

Dao dao = EntityManager.open("todos.db").dao(TodoItem.class);
dao.createTable();
dao.insert(new TodoItem(0, "Read the post", null));

List open = dao.find("completed_at IS NULL", new Object[] {});
TodoItem byId = dao.findById(42);
dao.delete(byId);

生成的 DAO 在底层完成类型化的工作。insert 中无需反射;生成的代码直接对 SQLite 的 PreparedStatement 调用 setString(1, e.title)setLong(2, e.id)。构建时验证会捕获缺少 @Id、看起来像关系但尚未支持的字段以及抽象实体类;构建失败并显示类名和原因。

对于 JPA / Hibernate 开发者而言, 该 API 刻意保持熟悉感。@Entity@Id@Column@Transient(此处重命名为 @DbTransient 以避免与 java.beans.Transient 冲突)的含义与 javax.persistence / jakarta.persistence 中的相同。EntityManager 名称也相同。Dao#findByIdDao#findAllDao#find(where, params)Dao#insertDao#updateDao#delete 与基本的 JPA 仓库契约一致。查询语言为纯 SQL(无 JPQL 或 Criteria DSL),但注解表面、生命周期以及运行时方法会让任何具有服务端 Java 持久化经验的开发者感到熟悉。

JSON / XML 映射

@Mapped 标记一个类为可传输的 POJO。@JsonProperty@XmlElement(以及 @XmlRoot@XmlAttribute@JsonIgnore@XmlTransient)定义传输格式。运行时入口点为 Mappers.toJson(...)Mappers.fromJson(...)Mappers.toXml(...)Mappers.fromXml(...)

$ java
@Mapped
public class User {
    @JsonProperty("user_id") long   id;
    @JsonProperty            String name;
    @JsonProperty("created_at")
                             Date   createdAt;
    @JsonIgnore              String passwordHash;
}

String json = Mappers.toJson(user);
User   back = Mappers.fromJson(json, User.class);

同一个 @Mapped POJO 也是类型化 Rest 辅助方法接受的类型:

$ java
Rest.get("https://api.example.com/users/42")
    .fetchAsMapped(User.class)
    .onResult((user, err) -> { /* ... */ });

Rest.get("https://api.example.com/users")
    .fetchAsMappedList(User.class)
    .onResult((users, err) -> { /* ... */ });

Rest.fetchAsJsonList(顶级 JSON 数组,无需 {"root":[...]} 包裹技巧)、JSONWriterJSONParser 的补充,提供流畅构建器以及面向 WriterOutputStream 的流式变体)以及 URLImage.setDefaultBearerToken(图像获取时的认证头)均一同发布。

对于 JAXB 开发者来说, XML 表面(@XmlRoot@XmlElement@XmlAttribute@XmlTransient)是长期存在的 javax.xml.bind.annotation 表面的直接移植。同一个模型类可以同时装饰 @XmlRoot@JsonProperty,从而为两种传输格式提供单一事实来源。JSON 表面采用了 Jackson 约定(@JsonProperty@JsonIgnore),几乎所有现代 JVM JSON 绑定(Jackson、Moshi、kotlinx-serialization)都继承自它。

组件绑定与验证

同一管道上的第四个注解处理器是组件绑定器。@Bindable 标记一个模型类;@Bind(name = "userField") 通过组件的 name 将字段绑定到表单上的组件。字段级验证注解可与同一字段上的 @Bind 组合使用:

$ java
@Bindable
public class SignupModel {
    @Bind(name = "userField")  @Required @Length(min = 3)
    private String user;

    @Bind(name = "emailField") @Required @Email
    private String email;

    @Bind(name = "ageField")   @Numeric(min = 13, max = 120)
    private String age;

    @Bind(name = "roleField")  @ExistIn({ "admin", "editor", "viewer" })
    private String role;
}

匹配的表单需要为每个组件设置 name,以便绑定器找到它们:

$ java
TextField user = new TextField();    user.setName("userField");
TextField email = new TextField();   email.setName("emailField");
TextField age = new TextField();     age.setName("ageField");
ComboBox role = new ComboBox("admin", "editor", "viewer");
role.setName("roleField");

Button submit = new Button("Sign up");

Form form = new Form("Sign Up", BoxLayout.y());
form.add(user).add(email).add(age).add(role).add(submit);
form.show();

SignupModel model = new SignupModel();
Binding binding = Binders.bind(model, form);
binding.getValidator().addSubmitButtons(submit);

Binding 是操作句柄:refresh() 将模型数据重新读入组件,commit() 将组件内容写回模型,disconnect() 拆解监听器。单个字段上的多个验证注解通过 Validator.addConstraint(Component, Constraint...)GroupConstraint(先失败者优先)组合。@Validate(MyClass.class) 是编写自定义 Constraint 实现的逃生口。验证集包括:@Required@Length@Regex@Email@Url@Numeric@ExistIn@Validate

新的 BindAttr 枚举允许 @Bind 针对组件的特定属性(TEXTUIIDSELECTED……),当默认行为(“将 String 字段写入组件文本”)不符合需求时使用。

构建时 SVG

将 SVG 放入 src/main/css/ 目录,与 theme.css 并列:

src/main/css/
    theme.css
    star.svg
    gradient_circle.svg
    path_arrow.svg
    rounded_button.svg
    wave.svg
    pro_badge.svg
    clipped_badge.svg

下次构建后,每个 SVG 都成为常规的 Codename One Image由转码器处理的 SVG 是矢量图像,但它仍然是一个 Image 任何可以使用栅格 Image 的地方(Label.setIconButton.setIconBorderLayout.NORTH、工具栏、MultiButton 的前导图标、CSS 的 background: url(...) 规则),SVG 也同样适用。区别在于它在任何尺寸下都保持清晰:同一源文件在 16 点列表行图标、64 点页眉标题和 256 点启动屏上都能保持锐利,适用于每个 DPI 桶。

来自 hellocodenameone 测试夹具的静态 SVG 网格,通过新管道渲染:

[LOADING...]

以毫米为单位设置尺寸

SVG 转码器最有用的功能也是最容易被忽略的:从 CSS 中以毫米为单位设置每个 SVG 的尺寸。日常使用的 SVG 经常带有奇怪的 width / height 属性(例如 1024×1024 的 24×24 图标导出、完全没有尺寸、来自某个特定框架的设计像素值)。将渲染尺寸固定为毫米可以绕过所有这些问题。

$ style
HomeIcon {
    background: url(home.svg);
    cn1-svg-width:  6mm;
    cn1-svg-height: 6mm;
    bg-type:        image_scaled_fit;
}

LogoBanner {
    background: url(logo.svg);
    cn1-svg-width:  32mm;
    cn1-svg-height: 12mm;
}

一个 6mm 的图标在 1× 桌面上是 6mm,在高密度手机上也是 6mm,在 4K 平板上同样是 6mm。转码器在安装时将这两个值通过 Display.convertToPixels() 转换,其方式与 Codename One CSS 中已有的 font-size: 3mm 完全相同。无需猜测设计像素,无需选择 DPI 桶,无需担心设计师以不同分辨率重新导出源 SVG 时出现缩放问题。

如果项目不使用 CSS 进行主题设置,生成类的两个 float 构造函数可直接接受毫米值:new com.codename1.generated.svg.Home(6f, 6f)

覆盖范围与所需反馈

转码器是一个 maven/svg-transcoder/ 模块,使用 javax.xml StAX 解析 SVG。不依赖 Batik、Flamingo 或任何外部库。覆盖范围针对真实世界图标 SVG 常用元素:rect(含圆角)、circleellipselinepolylinepolygon、完整 path 语法(M/L/H/V/C/S/Q/T/A/Z 及相对坐标和平滑曲线反射)、带仿射变换的组(translatescalerotateskewmatrix)、通过 LinearGradientPaint 实现的线性渐变、fill、stroke、stroke-width、linecap、linejoin、opacity。

同一管道还支持 SMIL 动画:<animate><animateTransform>translatescalerotate)和 <set>。时间值在每次绘制时根据挂钟时间进行插值,支持 from/to/values/begin/dur/repeatCount/fill="freeze"

文本和剪裁路径已在静态 SVG 测试夹具的后续 PR 中实现,两者均可在上方截图中看到(圆角按钮中的“Codename One / build-time SVG”字样、PRO 徽章文本以及下面的剪裁路径形状圆角徽章)。<text><tspan> 支持单一样式填充和变换;通过 clip-path="url(#id)" 引用的 <clipPath> 适用于 rectcirclepath 剪裁形状(嵌套剪裁引用被忽略)。

尚不支持的内容:SVG filter 元素、<feImage>(视为剪裁,因此 alpha 遮罩回退为不透明)、<radialGradient>(回退为第一个停靠点的颜色)以及 SVG 内嵌 CSS(SVG 文档中的样式规则;转码器读取呈现属性和内联 style="..." 属性,但带有选择器的 <style> 元素不会被解析)。

如果你遇到无法按预期转码的 SVG,请通过 github.com/codenameone/CodenameOne/issues 提交问题,并附上源文件。扩展覆盖范围的最快方法是让我们运行失败的案例并通过测试夹具观察输出。我们提供的每个 SVG 测试基准最初都是某个用户报告“渲染不正确”的问题。

iOS 注意事项: 转码后的 SVG 使用框架的形状 API(fillShapedrawShapeLinearGradientPaint)。完整功能在 Metal 渲染器上实现。已弃用的 GL ES 2 管道无法在每个操作上达到同等效果,因此在 ios.metal=false 下绘制的 SVG 通常会出现明显的渲染瑕疵(缺少渐变、剪裁填充、路径变形),而不是预期的占位符。由于自上周五起 Metal 已成为 iOS 新构建的默认渲染器,这对大多数应用来说不是问题;如果你明确设置了 ios.metal=false,预计 SVG 内容会出现一些视觉退化,并请告知我们。

覆盖范围矩阵和故障排除信息请参阅开发者指南中的 SVG 转码器

构建时 Lottie

构建时 Lottie 转码器沿用了 SVG 转码器的形式。将 .json Lottie 文件放入 src/main/css/ 目录:

src/main/css/
    theme.css
    loading.json
    celebration.json
    transition_spin.json

Lottie 文件通过 LottieAnimation 或 CSS 的 url(...) 作为常规 Codename One Image 访问。构建时阶段会解析 JSON,提取形状和动画,并生成一个轻量级表示,可以直接在 Metal 渲染器上绘制,无需运行时解析器,也不消耗 JSON 解析开销。它支持矢量形状的完整层次结构,并正确解释 Lottie 关键帧。它仍是一个 Image,因此可以像栅格图像一样用于图标、背景等,同时保持矢量清晰度。

$ java
LottieAnimation anim = LottieAnimation.create("loading.json");
anim.setSpeed(1.5f);
anim.setLoop(true);
anim.play();

生成的类针对简单的集成进行了优化:create("loading.json") 返回可控制的动画实例。速度控制、循环和播放状态均可直接设置。由于所有重绘都在 EDT 上处理,并利用 Metal 的形状缓存,动画播放流畅,帧损失极小。

Lottie 支持级别的详细说明以及已知限制请参阅开发者指南中的 Lottie 转码器章节。

深度链接与路由

本版本包含一个声明式路由层,基于已建立的 UI 样式,可降低现代应用中客户端导航的复杂度。

深度链接

深度链接配置现在是一个简单的映射,将路径前缀连接到 Java 类。在应用启动时调用一次 DeepLinks.register 即可建立路由表:

$ java
DeepLinks.register("/products", ProductForm.class);
DeepLinks.register("/profile", ProfileForm.class);
DeepLinks.register("/settings", SettingsForm.class);

当应用收到一个 URL 时,框架会匹配第一个注册的前缀,启动 Display.execute() 行为(通常是显示表单或触发导航事件)。类通过 @DeepLink 可以访问原始 URI 及其参数:

$ java
@DeepLink
public class ProfileForm extends Form {
    public ProfileForm(String uri) {
        super("Profile");
        // 从 URI 中提取查询参数
        String userId = Util.getUrlParam(uri, "id");
        // 使用 userId 加载数据
    }
}

无需在多个位置重复造轮子处理 URI 匹配、参数提取或表单切换。只需声明一次映射,框架负责其余工作。

路由

新的 @Routed 注解提供了一种声明式的方式来声明导航目标:

$ java
@Routed(url = "/products/:id")
public class ProductDetailForm extends Form {
    public ProductDetailForm(String id) {
        // 按 id 加载产品
    }
}

@Routed 支持路径参数(例如 :id)、通配符段(/files/*)以及查询字符串提取。所有路由在构建时注册,无需运行时扫描。与深度链接结合使用时,你可以实现完整的前端路由系统,而无需依赖第三方路由库或复杂的导航栈。

工作原理:构建时代码生成管道

本版本中所有组件都基于同一个架构:构建时代码生成。一个 Maven 插件(Mojo 目标)在 process-sources 阶段运行,读取注解(@Mapped@Bindable@Entity@Routed)或外部 JSON 配置文件(OpenAPI 规范、Lottie .json、SVG 文件),并生成类型化的 Java 源文件。

管道由以下步骤组成:

  1. 扫描阶段: Mojo 扫描项目的类路径和源目录,收集所有带有相关注解的类,或读取配置中指定的 JSON/SVG 文件。
  2. 解析阶段: 对每个注解或文件,调用专用的解析器。例如,对 @Entity 类,解析器读取字段、注解和关系;对 SVG 文件,解析器使用 StAX 读取 XML 树,提取路径、渐变和变换。
  3. 生成阶段: 解析器的输出传递到代码生成器。生成器使用模板(或直接字符串拼接)发出 Java 源文件,放在 target/generated-sources/ 下。生成的代码导入 Codename One 核心 API,并针对目标应用进行硬编码优化。
  4. 编译阶段: Maven 将生成的源文件编译为 .class 文件,与手写的应用代码一起打包到二进制文件中。由于是在构建时生成,最终二进制文件中不存在反射、服务加载器或运行时注解处理。生成的类直接编译到应用中,提供类型安全和编译时验证。

这种方法的优势在于:

  • 类型安全:所有 API 调用都是类型化的,IDE 补全和编译时检查正常工作。
  • 无反射:没有 Class.forNameMethod.invoke 或运行时代理。所有方法都是直接调用。
  • 性能:生成代码针对特定用例高度优化(例如,DAO 插入直接对 SQLite 参数绑定,而非泛型循环)。
  • 轻量级:生成的类仅包含严格必要的代码,无冗余包装或适配器。
  • 同步性:模型、API 客户端和 UI 绑定在构建时保持同步。更改一个注解或源文件即触发重新生成。

管道本身是 Codename One Maven 插件的一部分,因此无需额外配置或依赖。只需确保 pom.xml 中声明了该插件,并按需运行 mvn generate-sources(或 mvn compile,它会自动触发生成阶段)。

总结

本次版本引入了一组连贯的构建时组件,它们共同简化了 Codename One 应用中结构部分的编写。从 OpenAPI 客户端生成到 SQLite ORM,从 JSON/XML 映射到组件绑定与验证,再到 SVG/Lottie 转码和声明式路由,所有这些都基于同一套强调类型安全、编译时验证和无反射运行时的代码生成管道。其结果是:代码更少、错误更少、性能更优,且能与现代后端工作流保持同步。

我们期待社区试用这些特性并给予反馈。遇到问题时,请在官方仓库提交 issue,并附上复现步骤和源文件(如相关)。我们的目标是在未来版本中继续扩展覆盖范围并完善这些管道。## 构建时的 Lottie
同样的流水线也处理 Lottie。将 Bodymovin 导出文件放入同一个 src/main/css/ 目录:

src/main/css/
        theme.css
        pulse.json
        spinner.json

下次构建后,两者都会成为所有支持形状 API 的平台上的真实 Image 实例。与 SVG 相同的“矢量无处不在”的故事:Lottie 动画在任何尺寸下都清晰渲染,并可放入框架中的任何 Image 插槽。

$ java
Image pulse   = Resources.getGlobalResources().getImage("pulse");
Image spinner = Resources.getGlobalResources().getImage("spinner");
form.add(pulse).add(spinner);

动画根据墙上时钟时间在每个绘制周期运行,没有 Timer,热路径中也没有内存分配。下面是 hellocodenameone 中 Lottie 动画的捕捉:

[LOADING...]

Lottie 转码器位于 maven/lottie-transcoder/。它使用 no 外部依赖(框架内置的 JSON 解析器承担工作量)解析 Bodymovin JSON,并将每个文件降级为 SVG 路径使用的相同 SVGDocument 模型。相同的 JavaCodeGenerator 生成相同的 GeneratedSVGImage 子类,相同的 SVGRegistry 在源文件名下注册它。不需要新的 Image 基类、新注册表或每个端口的手动连接,因为 SVG 路径的 JavaSE 反射加载和 iOS/Android Stub 编织已经覆盖了新的格式。

v1 中覆盖的内容:形状层(rc / el / sh)与纯色填充和描边;层变换(锚点、位置、缩放、旋转、不透明度);动画化的旋转、位置和缩放(折叠为两个关键帧循环);作为填充矩形的纯色层。大多数图标级别的 Bodymovin 导出都能干净地降级。来自 After Effects 的复杂角色动画(包含图像引用、遮罩和效果)则无法处理,转码器会记录它丢弃了哪些图层,以便任何空白输出的原因一目了然。

与 SVG 相同的请求:如果 Lottie / Bodymovin 文件未按预期转码,请于 github.com/codenameone/CodenameOne/issues 提交问题,并附上源 .json 文件。转码器会根据社区报告的案例一次增加一个形状族。

相同的 iOS 警告也适用:渲染器依赖形状 API,因此已弃用的 GL ES 2 管道会在更复杂的 Lottie 动画上显示伪影。请使用 Metal 默认值(新 iOS 构建默认开启)。

深度链接和路由

两个用于处理来自应用外部 URL(通知点击、营销链接、分享目标、来自 Safari 的 Universal Links 以及来自 Chrome for Android 的等效 App Links)的组件。

深度链接

Codename One 长期以来通过 Display.setProperty("AppArg", url) 提供深度链接支持。平台基础设施已在冷启动时将传入 URL 写入该属性,并在热启动时再次设置(通过应用恢复);从 start() 中读取它对于少量模式来说效果很好。AppArg 方法变得脆弱的地方在于一致性。冷启动和热启动路径执行不同的生命周期代码,值是一个没有解析的扁平字符串,最棘手的情况是用户通过链接进入应用中间,然后继续交互:他们的下一次导航需要与入口点组合,后退栈需要像他们通过常规流程到达一样合理,而“按返回键退出应用”是一个常见的 bug。使用手动编写的 AppArg 读取器很容易忽略其中一点,从而导致半工作的流程。

此版本引入了一个类型化的 DeepLink 和一个单一的处理器,该处理器在冷启动和热启动时都会触发:

$ java
Display.getInstance().setDeepLinkHandler(link -> {
    // link 是一个标准化的 DeepLink:scheme、host、path、
    // segments、query map、fragment。冷启动和热启动形状相同。
    if ("/users".equals(link.path()) && link.segments().size() == 2) {
        showUserDetailForm(link.segments().get(1));
        return true;
    }
    return false;
});

AppArg 对于依赖它的项目仍然有效,但我们推荐使用新的处理器。处理器在冷启动和热启动时都运行在一致的生命周期路径上,并且解析后的 DeepLink 值包含 scheme、host、路径段、查询映射和 fragment,因此应用代码无需自己解析 URL。

路由

对于处理多个 URL 模式的项目,第二个组件是 com.codename1.router 中的声明式路由器。我们将其构建在与 ORM 和映射器相同的构建时代码生成管道上(路由器实际上是新预处理器第一个具体的消费者),因此两者表面可以组合:一个委托给路由器的深度链接处理器变成了一行代码。

每个表单通过 @Route 注解声明其自身路径:

$ java
@Route("/")
public class HomeForm extends Form { /* ... */ }

@Route("/users/:id")
public class UserDetailForm extends Form {
    public UserDetailForm(RouteMatch match) {
        String userId = match.param("id");
        // 为用户 `userId` 构建 UI
    }
}

@Route("/about")
public class AboutForm extends Form { /* ... */ }

Router.navigate("/users/42") 解析路径,实例化 UserDetailForm,并显示它。深度链接处理器现在简化为:

$ java
Display.getInstance().setDeepLinkHandler(link -> Router.navigate(link.toString()));

每个表单拥有自己的路由规则。添加或移动一个屏幕只需修改一个类。回答“这个应用有哪些屏幕,路径是什么?”只需在 IDE 中搜索 @Route,而无需阅读项目中的每个表单构造函数。

对于 Spring 开发者来说,这种形状是熟悉的,这是有意为之。@Route 扮演的角色与 Spring MVC 的 @RequestMapping 相同:一个类级别的声明,说明“此控制器处理此形状的 URL”。:id 参数语法镜像了 Spring 的 {id} 路径变量语法;RouteMatch.param("id") 与 Spring 的 @PathVariable 是同类访问器。这种心智模型几乎无需任何摩擦即可从服务端 Java 迁移过来。任何具有 React Router、Vue Router 或 Angular Router 经验的人也能识别出这种表现方式;:param 约定是跨框架的默认方式。

构建时处理器验证每个带注解的类是否扩展了 Form、路径是否以 / 开头、构造函数是否可访问,以及没有重复的模式。任何规则违反都会在构建时以类名和原因的方式失败,而不是在运行时抛出堆栈跟踪。

路由器的其余表面涵盖了现代客户端路由中已经成为标配的功能:

  • 路由守卫 在导航完成前运行,可以取消或重定向。
  • 每个标签页的导航栈 通过 TabsForm 实现,每个标签页维护自己的后退栈。
  • 位置监听器,使应用中任何内容都可以订阅“路由已更改”事件。
  • Form.setPopGuard(PopGuard) 拦截硬件返回键、工具栏返回键或 Router.pop(),并有机会询问“你确定吗?”。
  • Sheet.showForResult() 返回一个 AsyncResource,如果用户关闭 Sheet,它会自动使用 null 取消。

该 API 是可选加入的。偏爱现有 Form.show() / Form.showBack() 流程的应用可以继续使用;一切不变。

对于链接发布端,AasaBuilder 输出 iOS 的 apple-app-site-association JSON,AssetLinksBuilder 输出 Android 的 assetlinks.json。完整的设置指南(entitlements、Android 的 intent-filter、需要在你源服务器的 .well-known/ 目录上传的内容)可在开发者指南中的 Routing and Deep Links 找到。

JavaScript 端口将路由器桥接到 window.history 中,使得在应用内路由导航时会在浏览器会话历史中压入一个真实条目。浏览器中的前进和后退驱动路由器;重新加载页面会定位到深度链接 URL;从地址栏分享 URL 会将同事带到相同的应用内位置。

工作原理:构建时代码生成管道

以上所有内容都建立在一个单一的 Maven 插件阶段上。

该插件有一个 AnnotationProcessor SPI 和两个新的 Mojo:cn1:generate-annotation-stubs(在 generate-sources 阶段)和 cn1:process-annotations(在 process-classes 阶段)。编排器使用 ASM 扫描 target/classes,分派到每个已注册的处理器,验证带注解的类,并在每个类旁边发出一个类型化的运行时工件,以及一个注册所有内容的微型 Index 类到公共运行时注册表中。之后添加一个新处理器只需将其放入 META-INF/services 中,无需更改编排器。

之所以针对字节码而非源码文本运行,是因为源码正则表达式原型在早期就被放弃了。字节码阶段看到的是 JVM 视角下的项目(extends Form 是 JVM 实际知道的东西,而不是我们希望用户以特定方式编写的模式),规则违规会返回类名和原因,构建在任何生成的 .class 文件落盘之前快速失败。该基础设施共享了 BytecodeComplianceMojo 现有字符串重写已经使用的 ASM 遍历。

generate-sources 阶段期间,一个小型存根源码被生成到 target/generated-sources/cn1-annotations/ 下,以便引用生成注册表的应用代码在编译时可以解析。真正的 .class 文件稍后在 process-classes 阶段覆盖存根。标准的“针对存根编译,链接真实现实”模式;在单个 Maven 构建内即可工作,无需多模块拆分。

cn1-core 为每个生成的索引(RoutesIndexMappersIndexBindersIndexDaosIndex)提供了无操作存根,因此即使项目没有带注解的类,应用代码也能编译。构建时处理器在打包前用真实实现覆盖每个存根。

SVG 和 Lottie 转码器位于并行管道上(声明式图形文件替代注解),但它们生成相同形状的代码并遵守相同约束。实际效果是,历史上需要在运行时通过反射(伴随所有混淆风险和意外内存分配)完成的代码,现在只会在构建时发生一次,并产生直接的、可被死代码消除的、重命名安全的符号引用。

总结

这结束了本次发布系列文章。我们已经为本周五的发布文章准备了一些相当重要的功能;头条部分将是几个月来最实质性的内容,值得回来查看。

返回每周索引

原文《OpenAPI, ORM, SVG and Lottie》首次发表于 foojay