Ohhnews

分类导航

$ cd ..
foojay原文

一分钟搭建Uber克隆:全栈Java网约车应用

#uber克隆#java#网约车#全栈应用#实时追踪

[LOADING...]

自 2018 年以来,无论是编程领域还是 Codename One,都发生了很多变化。当时我写了《7 天创建一个 Uber 克隆:用 Java 构建真实世界的全栈移动应用》。今天,你可以从当年那七天本来要教你构建的应用开始,然后把时间花在把它变成你自己的应用上。

Codename One 是什么? Codename One 是一个开源框架,可用单一 Java 或 Kotlin 代码库构建原生 iOS、Android、桌面和 Web 应用。了解更多请访问 codenameone.com。

认识一下 Wayline:一个用 Java 编写的全栈、白标网约车应用。乘客端和司机端应用、管理控制台、支付、实时追踪和推送通知都已接入 Java 后端。你会得到应用及其源代码,可直接按你的业务进行适配。打开 Initializr,选择 Wayline,为它取一个你自己的名称和包名,然后生成项目。这就是那个一分钟任务。下载和首次构建取决于你的机器;真正启动一项运输服务仍然需要实打实的工作。

源代码100% 免费,采用 MIT 无署名许可,也称为 MIT-0。无版税、无收入分成,使用 Wayline 无需强制订阅。你可以修改它、出售你的应用,并让改动保持私密。托管和外部服务有其各自的费用和条款。你的应用属于你。

本周即将发布

今天是完整应用导览,包括代码以及你会为自己的业务进行适配的部分。接下来六篇文章将介绍帮助你构建并交付这些工作的工具:

[LOADING...] 选择一个目的地。

[LOADING...] 作为司机接单。

[LOADING...] 管理服务。

一个可以变成你自己的全栈 Java 应用

Wayline 涵盖服务的两端。前端在移动端、桌面端和浏览器构建中实现乘客、司机和管理员工作流。后端负责身份验证、行程匹配、定价、支付和数据库。这是一个白标应用,你可以为其打造品牌并进行扩展,运营细节已经体现在代码中。

项目有三个模块:common 用于前端,backend 用于服务器,shared 用于两者之间的类型化契约。两端都使用 Java。你可以沿着一次按钮按下,追踪到 API 调用、服务和仓库,而无需切换语言或维护另一套请求模型。

$ mermaid
flowchart TD
    UI[Java 前端:乘客、司机和管理员] --> Contract[共享 API 接口与记录]
    Contract --> Endpoint[生成的 REST 客户端与服务器契约]
    Endpoint --> Services[Java 服务与授权]
    Services --> DB[生成的仓库与数据库]
    Services --> Providers[支付、地图和 Push v3]
    Services --> Live[已认证的 WebSocket 更新]
    Live --> UI

下面是共享 RiderApi 接口中的预约方法:

$ java
@POST("/api/rides")
void request(@Body RideRequestDto request,
        OnComplete<Response<RideDto>> callback);

该接口带有 @RestClient 注解。构建过程会生成其客户端实现和 RiderApiServer 契约。Wayline 的 RiderEndpoint 实现该契约,注入 Rides 服务并委托请求:

$ java
@Override
public RideDto request(RideRequestDto request) throws Exception {
    return rides.request(Caller.name(), request);
}

乘客身份来自已认证的调用者。客户端提供它想要的行程,服务器决定已登录用户是否可以请求该行程。Spring 风格的依赖注入和授权把这个决定留在后端;生成的 DTO 序列化和 API 绑定让前端与后端保持一致。

开发时你可以在 JVM 上运行服务器,然后使用 ParparVM 将其打包为原生可执行文件。前端在各目标平台之间共享屏幕和行为。用户指南会带你游览应用,而实现者指南会跟踪每个功能进入其对应类。下面所有 Wayline 截图都来自这些指南,点击后可以全尺寸打开。

从完整行程开始

应用会以适合已登录用户的模式打开。乘客进入预约屏幕,司机进入自己的班次,管理员进入控制台。一个账户可以拥有多个角色并在它们之间切换。这些是同一个共享应用中的不同工作流,因此你不需要维护三个互不相关的客户端。

新用户会注册、验证手机号,并选择是否共享使用统计数据。开发配置会显示验证码而不是发送短信,因此你可以在接入服务提供商之前先试用该流程。

[LOADING...] 登录、创建账户或选择服务器。

[LOADING...] 注册成为乘客或申请成为司机。

[LOADING...] 验证手机号;开发环境会显示验证码。

[LOADING...] 选择此设备是否共享使用统计数据。

乘客搜索目的地,查看路线,并比较三种车辆类别及各自的票价。“家”和“公司”快捷方式可省去重复输入。行程偏好涵盖安静行程、无障碍需求和宠物,并可根据司机资料将匹配限制为女司机。

请求之后,乘客会实时跟随正在接近的司机,查看车辆和车牌,并可以打电话或发消息联系对方。行程结束时,流程会进入评分、可选小费和分项收据。

[LOADING...] 比较车辆类别和报价。

[LOADING...] 跟随司机前往上车点。

[LOADING...] 评价已完成的行程并选择小费。

屏幕会跟随服务器的状态。按钮请求一项变更;服务器决定该变更是否有效。仓库使用条件更新,因此相互竞争的操作可以由数据库来裁决。当取消和接单同时到达时,这一点很重要。

请求流程可以在 Rides 中看到:

$ java
public RideDto request(String rider, RideRequestDto request) throws IOException {
    validate(request);
    String id = attempt(() -> open(rider, request));
    live.rideChanged(id, RideState.REQUESTED, rider, "", "");
    search(id);
    return read(id, rider);
}

open 是事务性的。它会在创建行程之前,将同一乘客的竞争请求串行化;attempt 处理重试路径。该工作返回后,服务会发布变更并开始匹配。防止出现两个活动行程的细节就放在这里,让每个客户端都获得相同的规则。

$ mermaid
flowchart LR
    Request[乘客请求行程] --> Match[服务器向司机派单]
    Match --> Accept[司机接单]
    Accept --> Pickup[司机到达]
    Pickup --> Trip[行程进行中]
    Trip --> Complete[提交已完成行程]
    Complete --> Payment[结算支付]
    Payment --> Receipt[收据与评分]

这里展示的是正常行程流程。应用还会处理被拒绝的派单、超时、取消和支付失败。

也给司机一个应用

一个只做到乘客端界面的网约车演示,会把大部分服务留给你自己构建。Wayline 包含司机注册、文件提交与审核,然后进入实际工作日。

[LOADING...] 提交申请所需的文件。

[LOADING...] 查看申请所处状态。

[LOADING...] 准备好接单时上线。

派单会显示上车点、目的地、车费和倒计时。行程中,司机会看到路线和下一步操作,包括现金行程需要收取的金额。收入视图会显示每日总计和服务记录的余额。

[LOADING...] 跟随行程并完成它。

[LOADING...] 查看收入和记录的结算余额。

当司机切换到导航应用或锁屏时,移动端后台定位服务会在班次期间持续上报位置。上报会按时间和距离间隔进行,而不是转发每一次位置变化。iOS 使用后台定位;Android 使用定位前台服务。浏览器和桌面预览不会复现这种移动端后台行为。

Shift.begin() 开始追踪,并在 Android 上使用方法引用为其后台任务创建前台服务:

$ java
service = ForegroundService.start(CHANNEL, "You're online",
        "Wayline shares your location with riders while you drive.", null,
        Shift::keepAlive);

Shift.end() 会移除定位监听器,并在司机下线时停止该服务。同一个类会按距离和经过时间限制上报频率,包括为停泊车辆发送心跳。

主屏幕之外还有行程小组件。乘客的当前行程会以 iPhone 实时活动、Android 持续通知或小型桌面/模拟器窗口的形式出现。在 Wayline 中,浏览器没有对应的展示面。锁屏部分解释了其中的区别:iPhone 实现会在应用收到消息时更新,并不使用 ActivityKit 的直接服务器推送令牌。因此,被挂起的应用可能会让实时活动停留在最后收到的状态。

小组件的布局也使用 Java。下面这段来自 RideCard.descriptor() 的代码提供了 iPhone 紧凑呈现:

$ java
return new LiveActivityDescriptor(RideActivityKeys.KIND)
        .setContent(content())
        .setCompactLeading(new SurfaceText("${driver}").setFontSize(13)
                .setMaxLines(1))
        .setCompactTrailing(new SurfaceText("${eta}").setFontSize(13)
                .setColor(SurfaceColor.ACCENT));

完整描述符还会提供展开布局和最小布局。LiveActivity.start(descriptor(), now) 会启动它,current.update(now) 会发送新状态。Wayline 会把生成的实时活动 ID 注册到后端,以便后续推送消息可以定位到它。

通过 WebSocket 实现实时更新

打开的应用需要知道行程何时变化或司机何时移动。LiveChannel 会打开一个 WebSocket,并将其回调交回 UI 事件线程:

$ java
socket = WebSocket.build(url)
        .onConnect(ws -> CN.callSerially(() -> opened(mine)))
        .onTextMessage((ws, text) -> CN.callSerially(() -> frame(text, mine)))
        .onClose((ws, code, reason) -> CN.callSerially(() -> lost(mine)))
        .onError((ws, err) -> CN.callSerially(() -> lost(mine)))
        .connect(10000);

这是 LiveChannel.open() 的连接部分。它的辅助方法负责重连、心跳以及来自先前连接的过期回调。应用会在连接到 /ws/live 之前,通过其已认证 API 获取短期、一次性的票据。

后端端点实现 WebSocket,并通过 @WebSocketMapping("/ws/live") 注册。它会兑换票据、检查账户,并将连接加入 LiveHub。当行程发生变化时,LiveHub 会构造消息:

$ java
Map<String, Object> frame = new LinkedHashMap<String, Object>();
frame.put("type", "ride");
frame.put("id", id);
frame.put("state", state.name());
String text = Json.write(frame);

它通过 WebSocketSession.sendText 将该帧发送给乘客、司机和已连接的管理员。司机位置有自己的消息类型。移除管理员角色还会关闭该账户的旧连接,因此之前已授权的套接字无法继续接收车队位置。实时通道指南会追踪两端。

Push v3 回调与后端投递

实时 WebSocket 通道服务于已打开的应用。推送则覆盖手机放在口袋里的乘客,以及刚刚收到派单的司机。

前端使用 Push v3 的 PushClient API。下面是 Notifications.start() 中的注册代码:

$ java
client = PushClient.builder(AppConfig.PUSH_APP_KEY)
        .listener(new Listener())
        .build();
client.register();

它的 PushListener 通过 onRegistration 接收令牌,通过 onMessage 接收行程通知:

$ java
@Override
public void onRegistration(PushSubscription registered) {
    subscription = registered;
    signedIn();
}

消息回调会读取负载并打开相关行程屏幕:

$ java
@Override
public void onMessage(PushMessage message) {
    Object ride = message.getData().get("rideId");
    Object role = message.getData().get("role");
    String title = message.getTitle();
    if (ride != null && title != null && title.length() > 0
            && Session.user() != null) {
        Nav.showRide(role == null ? Nav.RIDER : String.valueOf(role));
    }
}

该屏幕会获取当前行程状态,而不是信任旧通知。静默消息可以在不打开屏幕的情况下更新实时活动。这些回调运行在 UI 事件线程上。

应用会为已登录账户注册设备令牌,并在退出登录时移除该关联。通知携带行程 ID 和角色,因此点击它会打开正确的屏幕,然后从服务器读取当前行程。即将过期的司机派单使用较短的生命周期,因此延迟的通知不会看起来像一个新的任务。

在后端,RidePushes 使用普通 Java 调用构造消息:

$ java
return PushNotification.builder()
        .title(title)
        .body(body)
        .collapseKey("ride-" + id + "-" + role)
        .data("rideId", id)
        .data("state", state.name())
        .data("role", role);

CloudPushSender 通过 CloudPush 发送构造好的通知和设备目标:

$ java
PushResult result = push.send(message, to);
accepted = result.isAccepted();

该调用在后台任务上运行,因此行程请求不会等待推送服务。这里的“已接受”意味着服务接受了通知;它并不证明有人读过。Wayline 还会在构造通知之前检查最新行程状态,防止延迟的接单事件宣布一个之后已被取消的行程。

服务器会在行程变更提交后发布这些变更。推送投递失败不会撤销已接受的行程。这些小小的决策,比在预约屏幕上再加一个动画带来的差别要大得多。

Wayline 包含这项集成,而开发环境会将通知记录到控制台。真正的移动推送需要应用和服务器密钥,以及推送设置中描述的平台凭据。Android 推送默认关闭,这样生成的项目可以在你尚未配置 Firebase 或华为之前完成构建。## 管理控制台也是产品的一部分

需要有人批准司机、回答支付问题,并查明某位乘客为什么被封禁。这些工作不应该要求打开数据库控制台。

在手机上,管理界面一次只呈现一个屏幕。仪表板先列出需要处理的工作,然后显示当前活动和趋势。申请审核会包含相关文件,审核记录则同时记录原因和负责的管理员。 [LOADING...] 查看每日行程和车费。 [LOADING...] 审核司机申请。 [LOADING...] 查看账户及其审核历史。

在宽屏上,同一个应用会变成带导航的控制台,并有足够空间在所选项目旁显示列表。这种布局可以让你整天在其中工作: [LOADING...] 宽屏仪表板将活动和趋势放在一起。 [LOADING...] 审核申请时不会丢失列表。

标记和封禁不是同一种操作。标记是提醒关注某个账户;封禁会将其登出,并在解除前阻止访问。人员视图包含角色变更、行程历史和财务汇总。行程视图允许管理员检查或取消行程,并在适用时发起银行卡退款。

后端也会防范棘手情况。在 Accounts.suspend() 中,管理员不能通过停用账户让某人滞留在行程途中:

$ java
if (suspended && signIn.isEnabled() && cleanup.inRideUnderWay(username)) {
    throw new ResponseStatusException(409,
            "That account is on a ride. Block it when the ride has ended");
}

该操作在事务中运行,并记录是谁更改了账户以及原因。管理员必须先结束行程。即使审核是通过其他客户端触发的,这条规则也会保护双方参与者。 [LOADING...] 搜索账户、更改角色并查看历史记录。 [LOADING...] 筛选并检查服务范围内的行程。

定价可编辑:起步价、距离、时间、最低车费、费用、佣金和倍率。支付列表会区分已完成的退款和仍在处理中的退款。实时地图会显示在线车辆,下面的指南截图展示的是空状态,而不是虚构一支繁忙的车队。 [LOADING...] 检查扣款及其支付状态。 [LOADING...] 在控制台中设置服务价格。 [LOADING...] 实时地图,此处没有司机在线。

账单处理也涵盖棘手情况

钱包存储支付方式,收据会拆解车费。开发环境使用模拟卡,并带有成功、拒付和结果不确定的测试行为。真实支付提供商位于 PaymentProvider 接口之后。 [LOADING...] 保留默认银行卡或选择现金。 [LOADING...] 查看车费明细和支付结果。

已完成行程与已结算支付是两个独立事实。Wayline 会提交行程,在不保持该数据库事务打开的情况下调用支付提供商,然后记录结果。扣款会携带一个由行程派生的键,这样重试结果不确定的请求时可以复用同一个支付标识。下面这段来自 Payments.settle() 的调用明确地体现了这个键:

$ java
reference = charge("ride-" + ride.getId(), ride.getRider(), ride.getMethodId(), amount,
        ride.getCurrency(), "Wayline ride");

该辅助方法会解析已存储的提供商引用,并委托给 PaymentProvider.charge。明确拒付会变为 FAILED;I/O 故障会将结果保留为 PENDING,以便恢复流程使用同一个键再次询问。

如果服务器在这些步骤之间停止,恢复扫描会重新检查待处理的支付。明确拒付与响应丢失会受到不同处理。退款也可能处于待处理状态;管理界面不会仅仅因为提供商接受了请求就认为款项已退回。

内置的 Stripe 适配器使用托管式银行卡设置,因此卡片详情不会经过应用的服务器。它有针对替代提供商响应的测试,但尚未针对 Stripe 账户进行验证。在接收真实支付之前,请与你的提供商一起演练完整流程,并完成账单章节中描述的缺失生产工作:webhooks、必需的持卡人认证、司机转账以及你的税务/发票规则。

尤其是,Cash out 目前只在账本中记录一笔打款;它并不会把钱转到司机的银行账户。这是一个具体的扩展点,而不是藏在看起来已完成的按钮背后的隐含承诺。

地图改变了起步阶段的经济性

2018 年,原生地图是这类应用的核心。我们仍然支持原生地图,包括指南中描述的平台集成。Wayline 侧重于由 Codename One 的 MapView 渲染的托管地图数据,这让小型运营者对提供商和成本模型拥有更多控制权。

默认设置会从 OpenFreeMap 绘制 OpenStreetMap 矢量瓦片,无需 API 密钥。路线使用 OSRM 演示服务,地点搜索则通过你的后端转到 Photon。这些是彼此独立的选择,而不是一个不可分割的地图账户。地图工厂会直接选择瓦片源和样式:

$ java
MapView map = new MapView(tiles == null ? MvtTileSource.openFreeMap() : tiles,
        Look.dark() ? MapStyle.dark() : MapStyle.light());
map.setWheelZoom(true);
map.moveCamera(center, zoom);

这段代码来自 Maps.create()。Maps.useTiles(TileSource source) 可提供替代源,这也让截图测试可以使用打包的瓦片,而不必依赖网络服务。

$ mermaid
flowchart LR
    App[Wayline app] --> Tiles[Hosted vector tiles]
    App --> Routes[Routing service]
    App --> Server[Your backend]
    Server --> Search[Place search service]

你可以让应用指向自己的瓦片和路线服务,也可以选择托管提供商。小型企业可以在没有地图 SDK 计费账户的情况下起步,之后再选择是接受提供商账单,还是承担托管自己的数据服务的工作。自托管仍然要花钱并耗费运维时间;公共路线和搜索服务器并不构成生产级服务保证。默认路线不包含实时交通信息。

这就把问题从“这个应用必须使用哪家地图供应商?”变成了“哪种服务适合这个地区和业务量?”。实现者指南的地图章节展示了替换点以及上线前需要做出的决定。 [LOADING...] 矢量地图会跟随深色主题。 [LOADING...] 宽屏桌面窗口中的乘客体验。

即使智能体做出更改,Java 代码仍然可读

你可以把更改车费规则、添加服务区域或为界面做品牌化的工作交给智能体。Java 的严格类型和直白结构有助于让这项工作保持可读。请求有声明的类型,服务有显式依赖,仓库会说明它读取什么。更改后的方法签名必须与其调用方一致。

Wayline 为智能体提供了我作为开发者同样需要的东西:有名称的职责、常规代码、测试和一个开发用 MCP 端点。你可以审查生成的差异,并沿着应用追踪行为。代码仍然由你理解和维护。

Spring Security 用户应该能认出这些授权规则。下面的片段是 SecurityConfig API 链的一部分:

$ java
.authorizeHttpRequests(auth -> auth
        .requestMatchers(AntPathRequestMatcher.antMatcher("POST",
                "/api/account/register")).permitAll()
        .requestMatchers("/api/driver/**").hasRole(Roles.DRIVER)
        .requestMatchers("/api/admin/**").hasRole(Roles.ADMIN)
        .anyRequest().authenticated())

完整链路会配置 JWT 认证和其他保护措施。服务器从其安全上下文中读取调用者,而不是接受客户端提供的乘客身份。这些 API 使用 Codename One 包和受支持的 Spring 风格约定;这并不是藏在项目里的 Spring 运行时。

数据层同样容易辨认。Wayline 的 RideRepository 开头如下:

$ java
@Repository(entity = Ride.class, key = String.class)
public interface RideRepository extends CrudRepository<Ride, String> {
    Ride findFirstByRiderAndStateIn(String rider, RideState[] states);
    Ride findFirstByDriverAndStateIn(String driver, RideState[] states);
}

构建过程会写出查询实现。服务决定哪些更改应属于同一个事务。如果你更喜欢直接使用数据访问对象,后端也会生成 DAO。下面这个来自数据库指南的独立示例展示了使用其 Reminder 实体的较低层风格:

$ java
Dao<Reminder> reminders = em.dao(Reminder.class);
Reminder reminder = new Reminder();
reminder.title = "renew the certificate";
reminder.due = new Date();
reminders.insert(reminder);

Reminder stored = reminders.findById(Long.valueOf(reminder.id));
stored.done = true;
reminders.update(stored);

在前端,一个辅助方法会创建一个普通组件,给它一个测试名称并附加动作。这是 Wayline 的按钮辅助方法:

$ java
private static Button button(String text, String name, String uiid,
        ActionListener<ActionEvent> onClick) {
    Button button = new Button(text, uiid);
    button.setName(name);
    if (onClick != null) {
        button.addActionListener(onClick);
    }
    return button;
}

UIID 将它连接到 CSS。主样式使用共享变量来处理品牌和形状:

$ style
WlPrimary {
    color: var(--brand-contrast);
    background-color: var(--brand);
    border-radius: var(--radius);
    font-family: "native:MainBold";
    font-size: 2.9mm;
    text-align: center;
    padding: 2.75mm 4mm 2.75mm 4mm;
    margin: 1.6mm 0 0.8mm 0;
}

这些变量及其深色变体位于 theme.css 顶部附近。智能体在更改你的品牌颜色时有明确的编辑位置,而审查更改的人也能看出它是否触碰了其他内容。

你可以在 Wayline 源码中查看这些片段。实现者指南将每项功能链接到其实现,并解释如何对其进行调整。

设置是让应用真正属于你的一部分

Wayline 包含浅色和深色主题、距离单位,以及英语、西班牙语、法语、德语和希伯来语翻译。希伯来语布局从右到左运行。宽屏设置界面会保持在可读的列中,而不是把每个字段都拉伸到整个显示器。 [LOADING...] 账户、行程和外观偏好。 [LOADING...] 深色模式下的相同设置。 [LOADING...] 希伯来语文本和从右到左布局。 [LOADING...] 使用统计仍是设备级的选择。 [LOADING...] 设置会适应桌面窗口。

有些设置是你产品策略的挂钩。促销开关会被存储,但 Wayline 不会发送任何促销信息。紧急联系人和自动行程分享偏好会被存储,但不会发送消息;行程的“分享”动作是独立的。指南特别指出这些内容,以便你决定它们对你的服务应意味着什么。

运行它,然后改动一件事

从 Initializr 生成 Wayline 模板。它会为你的项目重命名 Java 包、制品和应用配置。安装 JDK 17 或更高版本以及 Maven 后,从生成项目的根目录启动开发后端:

$ bash
CN1_PROFILE=dev backend/server.sh run

在另一个终端中,启动模拟器:

$ bash
mvn verify -Psimulator -DskipTests -Dcodename1.platform=javase

开发配置使用内存数据库、演示账户和模拟外部服务。启动说明列出了这些账户,并展示如何同时运行乘客端和司机端。开发时请保留该配置。

对于由同一后端托管的浏览器版本:

$ bash
backend/server.sh web
CN1_PROFILE=dev backend/server.sh run

然后打开 http://localhost:8080/。第一条命令会构建浏览器应用;更改客户端后请重新运行它。服务器打包也可以生成原生二进制文件。

在把更改交给智能体之前,给它一个有用的检查。后端测试命令是:

$ bash
mvn -pl shared,backend -Dcodename1.platform=backend test

该测试套件覆盖角色、行程生命周期和支付结果,以及其他服务行为。原生编译测试执行可在测试指南中作为仅限本地的选项使用。开发配置的 /mcp 端点让智能体有另一种检查后端的方式;在没有生产认证配置的情况下,不应将其公开暴露。

构建你的市场所需的服务

我认为这对那些以网约车为起点的初创公司来说是一个机会。机场接送、专业车队或面向特定社区的交通服务可以从此起步,然后构建真正让服务对那些人有用的一部分。做出更好的东西。

出租车公司可以运行自己品牌化、带集中管理的托管应用。出租车调度站适配讨论了固定车队、人工调度和电话预订这些需要进行的更改。这些新增功能并不都是 Wayline 中已完成的界面,但指南展示了它们应属于哪里。

我能看到围绕这项工作会形成一种小型产业:为本地企业适配和运营这些应用的开发者。小型运营者能在品牌和服务规则上获得更多自由,开发者则可以把时间花在本地需求上,而不是重新实现钱包和登录流程。这是否能成为一门生意,取决于能否找到你能解决其问题的运营者。起始代码现在已可供试用。

*** ** * ** ***## 本周更多内容

静态编译的 Spring Data 风格仓库

诸如 findFirstByDriverAndStateIn 这样的查找方法,将查询放在实体的其他数据访问代码旁边。新的仓库生成器会提供其实现,包括对声明式查询和 CASE 表达式的支持。

$ java
@Repository(entity = Ride.class, key = String.class)
public interface RideRepository extends CrudRepository<Ride, String> {
    Ride findFirstByDriverAndStateIn(String driver, RideState[] states);
}

你将获得 Spring Data 风格的查找方法、熟悉的 Hibernate 持久化约定,以及生成的 Java 实现。原生构建会提前编译这些实现。不会进行仓库类路径扫描或运行时代理生成,生成的调用可供编译器优化。

事务隔离与回调处理下一个问题:系统的另一部分何时可以基于该结果采取行动?完成回调在数据库确认提交后运行。如果回调失败,数据库更改仍已提交,因此重试整个操作并不是正确的默认做法。

周六的仓库与事务文章同时涵盖便利性与边界,包括批量更新、支持的隔离级别和持久化事件投递。

原生后端中的 Thymeleaf、Spring MVC 与 htmx

Thymeleaf 风格模板、Spring MVC 风格控制器和 htmx 构成了快速服务端渲染管理站点的实用组合。我们的实现将支持的模板语法编译为 Java 渲染器,然后编译进原生后端。控制器可以为 htmx 返回页面或片段:

$ java
return Htmx.isRequest(request) ? "products :: catalog" : "products";

类型化模型访问变为直接的 Java 访问。生产请求执行编译后的渲染代码,无需解析模板或解释属性表达式。这就是在构建期间完成模板工作的性能优势。

周日的 Thymeleaf 与 Spring MVC 文章通过控制器、表单错误和局部更新跟踪一个可运行的目录示例。它还解释了该子集遗漏了哪些内容。

样式表不应需要显示器

CSS 编译器完全无头。同一个 mvn cn1:css 目标可以在没有显示服务器或帧缓冲配置的工作节点上运行。移除这套机制,也就从主题编译中去掉了启动工作和依赖显示环境的假设。

这也是对一台存在脆弱之处的旧编译器所做的重大更改。即使构建成功,字体、图片边框和物理单位转换也值得进行视觉检查。周一的无头 CSS 文章展示了该工作流程以及需要比较的内容。

使用桌面标题区域

[LOADING...]

我们的原生 macOS 编辑器原型的标题区域。文档操作与操作系统控件共享顶行,功能区和文档则位于下方。

将“保存”“搜索”或文档控件放入窗口标题区域是一项小众功能。如果你在构建编辑器,它可以节省一整行。Codename One 在原生 macOS、Windows 和 Linux 移植版中支持这一功能,同时保留真正的操作系统窗口控件。

$ java
editor.getToolbar().setTitleComponent(title);

选择自定义标题模式后,框架会为原生控件预留空间,并将交互组件与可拖动背景分开。周二的桌面工具文章解释了三套操作系统之间的差异。

从 Java 运行外部工具

新的共享 com.codename1.io.ProcessBuilder 始于后端需求,随后自然扩展到桌面应用。你可以启动已安装的命令,流式传输其输入和输出,带超时等待,并检查其退出状态。

$ java
boolean available = com.codename1.io.ProcessBuilder.isSupported();

移动端和浏览器目标不提供进程启动能力。周二的文章介绍了这一边界,以及避免管道写满导致工作进程挂起的输出排空模式。

无需发明归档格式即可导出 ZIP

ParparVM 运行时现在提供流式 java.util.zip API,包括 ZIP、GZIP 和校验和。应用可以通过熟悉的流接口导出数据;后端可以将归档直接写入响应。

$ java
zip.putNextEntry(new java.util.zip.ZipEntry("report.txt"));
zip.write("Completed rides\n".getBytes("UTF-8"));
zip.closeEntry();

此片段假定 ZipOutputStream 已打开。桌面工具文章还展示了用于保留可执行启动器 Unix 权限的辅助方法,以及流式子集的限制。

再次安装 Web 应用

在从 TeaVM 迁移到 ParparVM 的过程中,PWA 支持消失了。JavaScript 构建现在会再次生成清单、图标和 Service Worker,让支持的浏览器获得可安装应用和缓存的离线启动。

$ mermaid
flowchart LR
    Bundle[生成的应用包] --> Cache[Service Worker 缓存]
    Cache --> Launch[无网络启动]
    Launch --> Online[服务器功能仍需要连接]

周三的 PWA 文章涵盖部署、缓存更新,以及离线 UI 与仍需要服务器的叫车请求之间的区别。

不是云 IDE

实验性浏览器 IDE用编辑器和运行中的预览打开 Java 项目。编译和执行都在页面中进行,文件保存在此浏览器的存储中。当你显式请求平台构建时,云构建服务器会执行其常规工作。

[LOADING...]

这是一个非常早期的预览。它不会取代 IntelliJ,也不会在 JavaScript 中复现整个 Java。它可以让人在安装开发环境之前试用应用,然后在准备本地继续时下载项目。周四的 IDE 导览展示了这条路径,并解释了浏览器本地存储。

这将走向何方?

无处不在。Wayline 是 Initializr 中更广泛的全栈应用模板集合的第一步。我们计划为更多应用这样做,包括配送:选择模板,生成自己的项目,然后构建你的市场所需的服务。提高起点对整个市场都有好处。你的护城河可以在竞争对手无法从模板中获得的部分成长:你提供的服务、你建立的关系,以及你更理解的问题。从像 Wayline 这样体量可观的应用开始,让你有更多时间构建这种优势。我想看看你能用它做出什么。


作者:Shai Almog

作者、开发者关系、博主、开源黑客、Java 摇滚明星、会议演讲者、讲师和企业家。