<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://docs.webforj.com/zh/blog</id>
    <title>webforJ Blog</title>
    <updated>2026-11-11T12:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://docs.webforj.com/zh/blog"/>
    <subtitle>webforJ Blog</subtitle>
    <icon>https://docs.webforj.com/zh/https://docs.webforj.com/icons/icon.png</icon>
    <entry>
        <title type="html"><![CDATA[The Business Case for a Java UI Framework: What Your Boss Will Ask]]></title>
        <id>https://docs.webforj.com/zh/blog/business-case-java-ui-framework</id>
        <link href="https://docs.webforj.com/zh/blog/business-case-java-ui-framework"/>
        <updated>2026-11-11T12:00:00.000Z</updated>
        <summary type="html"><![CDATA[The business case for a Java UI framework, in the questions a manager or architecture board asks: who is behind it, upgrades, fit, lock-in, testing and a pilot.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-11-11-business-case-java-ui-framework/cover.jpg" alt="cover image" class="img_ev3q"></p>
<p>You have looked at webforJ and you like what you see. Now somebody with a budget or a veto wants to know why the team should adopt it, and "the developers prefer it" isn't an answer that survives a meeting.</p>
<p>This post is the business case for a Java UI framework, written as the questions a decision-maker asks, with a plain answer to each. webforJ is the full-stack Java framework for business web apps: you build the whole app, screens included, in Java. Each answer below links the page you can hand to your manager.</p>
<div class="cta_UErl" role="complementary" aria-label="Moving a desktop Java app to the web?"><div class="copy_XH8h"><p class="title_HfaR">Moving a desktop Java app to the web?</p><p class="text_x4Wj">Send us a screenshot of one screen and we'll reply with how it moves to webforJ.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="mailto:sales@basis.cloud?subject=My%20app%20screen%3A%20how%20would%20it%20move%20to%20webforJ%3F&amp;body=Hi%20webforJ%20team%2C%0A%0AAttached%20is%20a%20screenshot%20of%20one%20screen%20from%20our%20app.%20It%20is%20built%20with%3A%20(Swing%2C%20JavaFX%2C%20other)%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->发送截图</a><a class="secondary_GDTR" href="https://webforj.com/schedule-discovery/" target="_blank" rel="noopener noreferrer">预约通话<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="who-is-behind">Who is behind the framework?<a href="https://docs.webforj.com/zh/blog/business-case-java-ui-framework#who-is-behind" class="hash-link" aria-label="直接链接到 Who is behind the framework?" title="直接链接到 Who is behind the framework?" translate="no">​</a></h2>
<p>webforJ is built by BASIS International. The company is privately owned and operated, and BASIS software and software tools have been used to develop business solutions for both large international corporations and SMEs for almost 40 years, with 20+ years of delivering technology built on Java.</p>
<p>BASIS operates its own sites in North America and Europe and works with partners in more than 30 countries. For the maturity question in full, read <a class="" href="https://docs.webforj.com/zh/blog/is-webforj-production-ready">Is webforJ production ready</a>, and for the customers, <a class="" href="https://docs.webforj.com/zh/blog/who-uses-webforj">Who uses webforJ</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="convince-my-boss">How do I convince my boss to adopt a Java framework?<a href="https://docs.webforj.com/zh/blog/business-case-java-ui-framework#convince-my-boss" class="hash-link" aria-label="直接链接到 How do I convince my boss to adopt a Java framework?" title="直接链接到 How do I convince my boss to adopt a Java framework?" translate="no">​</a></h2>
<p>Show that the decision keeps what the company already has. The team stays a Java team, the code stays Java, and the screens stop needing a second language and a second set of specialists.</p>
<p>That's the whole argument, and it's qualitative on purpose. With webforJ, components, routing, forms with data binding, tables and route security come from one framework, so standard screens don't need a JavaScript codebase beside the Java one. Fewer languages and fewer hand-offs mean fewer places for a project to stall. Our <a class="" href="https://docs.webforj.com/zh/blog/why-webforj">why webforJ</a> post makes the wider case.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="justify-a-framework-choice">How do I justify a framework choice?<a href="https://docs.webforj.com/zh/blog/business-case-java-ui-framework#justify-a-framework-choice" class="hash-link" aria-label="直接链接到 How do I justify a framework choice?" title="直接链接到 How do I justify a framework choice?" translate="no">​</a></h2>
<p>Judge it against the risks the board will name, not against a feature list. Most boards ask about the same few: vendor, upgrades, fit, existing code, lock-in, quality and the cost of being wrong. The sections below take them in turn, and the last one turns the rest into a small experiment.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-risks">What risks will the architecture board ask about?<a href="https://docs.webforj.com/zh/blog/business-case-java-ui-framework#what-risks" class="hash-link" aria-label="直接链接到 What risks will the architecture board ask about?" title="直接链接到 What risks will the architecture board ask about?" translate="no">​</a></h2>
<p>The vendor, the upgrade path and the fit with your app come first, followed by existing code, lock-in and testing. The sections below answer each one.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="releases-and-upgrades">How often does it ship, and how do upgrades work?<a href="https://docs.webforj.com/zh/blog/business-case-java-ui-framework#releases-and-upgrades" class="hash-link" aria-label="直接链接到 How often does it ship, and how do upgrades work?" title="直接链接到 How often does it ship, and how do upgrades work?" translate="no">​</a></h2>
<p>webforJ follows a published release model. Major releases occur annually, minor releases roughly every six to eight weeks, and patches ship when critical bugs or security fixes need them. The <a class="" href="https://docs.webforj.com/zh/docs/upgrading/overview">upgrading guides</a> describe each type and what to expect from it.</p>
<p>Major releases may require adjustments to an app, so the project gives you tools for that. Snapshot builds are available before most releases so you can test early, and OpenRewrite recipes handle <a class="" href="https://docs.webforj.com/zh/docs/upgrading/automated-upgrades">automated upgrades</a> by renaming APIs, updating dependencies and flagging the fixes left for a person.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="does-it-fit">Does it fit our kind of app?<a href="https://docs.webforj.com/zh/blog/business-case-java-ui-framework#does-it-fit" class="hash-link" aria-label="直接链接到 Does it fit our kind of app?" title="直接链接到 Does it fit our kind of app?" translate="no">​</a></h2>
<p>If the app is a business app, yes. webforJ is made for forms, tables, navigation, role-based access and the other screens that business software consists of, and it covers them in Java. The <a class="" href="https://docs.webforj.com/zh/blog/when-to-use-webforj">when to use webforJ</a> guide lists the projects it fits and the ones it doesn't.</p>
<p>Your data layer is yours. The <a class="" href="https://docs.webforj.com/zh/docs/integrations/spring/overview">Spring Boot integration</a> lets components receive your Spring beans, and the <a class="" href="https://docs.webforj.com/zh/docs/advanced/repository/overview">repository pattern</a> connects tables to Spring Data and JPA entities, or to data outside your app such as a REST API. If your team uses Spring already, it keeps using it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="existing-code-and-team">What happens to our existing code and team?<a href="https://docs.webforj.com/zh/blog/business-case-java-ui-framework#existing-code-and-team" class="hash-link" aria-label="直接链接到 What happens to our existing code and team?" title="直接链接到 What happens to our existing code and team?" translate="no">​</a></h2>
<p>Both stay. A webforJ developer is a Java developer, so you hire for Java and onboard to the framework. Our post on the <a class="" href="https://docs.webforj.com/zh/blog/webforj-learning-curve">webforJ learning curve</a> walks through what a Swing or Spring developer already knows, and why candidates don't need webforJ on their CV.</p>
<p>The same holds for code. If a Swing, JavaFX or SWT desktop app holds years of business rules, the <a class="" href="https://docs.webforj.com/zh/docs/integrations/webswing/overview">Webswing integration</a> runs it in the browser without changes to its source and embeds it inside a webforJ app. You then replace screens one at a time, and the old and new screens stay connected during the move. The full path is in <a class="" href="https://docs.webforj.com/zh/blog/swing-to-web-migration">Swing to web migration</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="avoid-lock-in">How do we avoid lock-in?<a href="https://docs.webforj.com/zh/blog/business-case-java-ui-framework#avoid-lock-in" class="hash-link" aria-label="直接链接到 How do we avoid lock-in?" title="直接链接到 How do we avoid lock-in?" translate="no">​</a></h2>
<p>Keep the framework in the views and nowhere else. Every UI framework shapes the screens written against it, so what you control is how much of the app knows about the framework.</p>
<p>With webforJ, business rules can live in plain Java classes or Spring services, forms can bind to plain beans validated by Jakarta Validation, and data can come through repositories. In that layout only the views use webforJ components, so the part you would redo if you ever left is the thin part. The post on how to <a class="" href="https://docs.webforj.com/zh/blog/avoid-ui-framework-lock-in">avoid UI framework lock-in</a> shows the split layer by layer.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-do-we-test">How do we test it?<a href="https://docs.webforj.com/zh/blog/business-case-java-ui-framework#how-do-we-test" class="hash-link" aria-label="直接链接到 How do we test it?" title="直接链接到 How do we test it?" translate="no">​</a></h2>
<p>With the tools your team already uses. Unit tests run in JUnit, and end-to-end tests run with Selenium or Playwright. The <a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/overview">TestBed</a> runs views in plain JUnit tests without a browser or a server. Our <a class="" href="https://docs.webforj.com/zh/blog/java-web-app-testing-strategy">testing strategy</a> post shows which layer to use for what.</p>
<p>That matters to a manager for a plain reason: the app goes through the same build and the same CI checks as the rest of your Java code. Nothing about the UI needs a separate quality process.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="run-a-pilot">How do I run a pilot?<a href="https://docs.webforj.com/zh/blog/business-case-java-ui-framework#run-a-pilot" class="hash-link" aria-label="直接链接到 How do I run a pilot?" title="直接链接到 How do I run a pilot?" translate="no">​</a></h2>
<p>Pick one real screen, not a demo. Choose a screen from your own app that people use and that a manager recognizes, such as a customer search, an order form, or a status dashboard. A screen from the app, rather than a todo list, makes the result easy to judge.</p>
<p>Then choose a time box that fits your team, tell your manager the date up front, and build that one screen in webforJ against your existing services. The <a class="" href="https://docs.webforj.com/zh/docs/introduction/getting-started">getting started guide</a> covers project creation, and startforJ generates a Maven project you can open in your usual IDE. If the screen belongs to a desktop app, run the old app under Webswing and replace that single screen next to it.</p>
<p>At the end, show three things:</p>
<ul>
<li class="">The screen running in the browser, next to the one it replaces or complements.</li>
<li class="">The Java source, so your manager can see one language and one project.</li>
<li class="">The test and the build, running in the same pipeline as everything else.</li>
</ul>
<p>Then take the open questions from this post back to the board with an actual result attached. A small pilot doesn't commit the company to anything, and it gives the architecture board something concrete to approve or reject.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">Frequently asked questions<a href="https://docs.webforj.com/zh/blog/business-case-java-ui-framework#faq" class="hash-link" aria-label="直接链接到 Frequently asked questions" title="直接链接到 Frequently asked questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-convince-boss">How do I convince my boss to adopt a Java framework?<a href="https://docs.webforj.com/zh/blog/business-case-java-ui-framework#faq-convince-boss" class="hash-link" aria-label="直接链接到 How do I convince my boss to adopt a Java framework?" title="直接链接到 How do I convince my boss to adopt a Java framework?" translate="no">​</a></h3>
<p>Show that it keeps the team, the language and the existing code, and run a pilot on one real screen so the result is visible.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-justify">How do I justify a framework choice?<a href="https://docs.webforj.com/zh/blog/business-case-java-ui-framework#faq-justify" class="hash-link" aria-label="直接链接到 How do I justify a framework choice?" title="直接链接到 How do I justify a framework choice?" translate="no">​</a></h3>
<p>Answer the risks the board will name: vendor, release cadence, fit, existing code, lock-in and testing. Each has a documented answer for webforJ in the preceding sections.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-risks">What risks will the architecture board ask about?<a href="https://docs.webforj.com/zh/blog/business-case-java-ui-framework#faq-risks" class="hash-link" aria-label="直接链接到 What risks will the architecture board ask about?" title="直接链接到 What risks will the architecture board ask about?" translate="no">​</a></h3>
<p>Usually the vendor, upgrades, lock-in, hiring, and quality. BASIS has almost 40 years behind it, releases follow a published cadence, and the framework fits into a normal Java build.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-pilot">How do I run a pilot?<a href="https://docs.webforj.com/zh/blog/business-case-java-ui-framework#faq-pilot" class="hash-link" aria-label="直接链接到 How do I run a pilot?" title="直接链接到 How do I run a pilot?" translate="no">​</a></h3>
<p>Choose one real screen, set a time box, build it against your existing services and show the screen, the source, and the passing build.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pick-the-pilot">Pick the pilot screen this week<a href="https://docs.webforj.com/zh/blog/business-case-java-ui-framework#pick-the-pilot" class="hash-link" aria-label="直接链接到 Pick the pilot screen this week" title="直接链接到 Pick the pilot screen this week" translate="no">​</a></h2>
<p>Write down one screen from your own app that a manager would recognize, agree a time box, and ask for approval for that screen alone. A single working screen answers more questions in a meeting than any slide.</p><div></div>]]></content>
        <author>
            <name>webforJ</name>
        </author>
        <category label="Insights" term="Insights"/>
        <category label="Modernization" term="Modernization"/>
        <category label="Spring" term="Spring"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[webforJ JavaScript Integration: Use the Libraries You Already Have]]></title>
        <id>https://docs.webforj.com/zh/blog/webforj-javascript-integration</id>
        <link href="https://docs.webforj.com/zh/blog/webforj-javascript-integration"/>
        <updated>2026-11-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[webforJ JavaScript integration explained for evaluators: wrap web components in Java, run client-side JavaScript, load scripts, and bring npm packages and React, Svelte or Lit components.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-11-11-webforj-javascript-integration/cover.jpg" alt="cover image" class="img_ev3q"></p>
<p>A Java framework that builds the whole screen in Java raises a fair worry for anyone choosing it: what happens the day you need a charting library, a date picker or a widget your team already owns, and it's written in JavaScript? Do you wait for the framework to ship its own version?</p>
<p>You don't. This post covers webforJ JavaScript integration: the documented ways to bring the JavaScript ecosystem into a webforJ app, and which one fits which job. webforJ is the full-stack Java framework for business web apps: you build the whole app, screens included, in Java. The standard app needs no JavaScript at all, and when you want some, there are four clear routes in.</p>
<div class="cta_UErl" role="complementary" aria-label="Connecting webforJ to your stack?"><div class="copy_XH8h"><p class="title_HfaR">Connecting webforJ to your stack?</p><p class="text_x4Wj">Ask the team about your libraries, services, and build.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="https://docs.webforj.com/startforj/" target="_blank" rel="noopener noreferrer">启动应用<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a><a class="secondary_GDTR" href="mailto:sales@basis.cloud?subject=Integrating%20webforJ&amp;body=Hi%20webforJ%20team%2C%0A%0A%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->邮件咨询</a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-libraries">Can I use JavaScript libraries with webforJ?<a href="https://docs.webforj.com/zh/blog/webforj-javascript-integration#faq-libraries" class="hash-link" aria-label="直接链接到 Can I use JavaScript libraries with webforJ?" title="直接链接到 Can I use JavaScript libraries with webforJ?" translate="no">​</a></h2>
<p>Yes. Every route below keeps your screens in Java and lets a script, a stylesheet, a web component, or an npm package join them. You choose the route by what you are bringing:</p>
<ul>
<li class="">A web component becomes a typed Java class.</li>
<li class="">A script or stylesheet you already have gets attached to a component or to the app.</li>
<li class="">A single browser call runs straight from Java.</li>
<li class="">An npm package or a React, Svelte or Lit component goes through the frontend bundler.</li>
</ul>
<p>You can mix them in one app, and each can be adopted on its own.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="element-composite">Wrap a web component in Java<a href="https://docs.webforj.com/zh/blog/webforj-javascript-integration#element-composite" class="hash-link" aria-label="直接链接到 Wrap a web component in Java" title="直接链接到 Wrap a web component in Java" translate="no">​</a></h2>
<p><a class="" href="https://docs.webforj.com/zh/docs/building-ui/element-composite">ElementComposite</a> wraps a custom HTML element or third-party web component in a Java class. Its properties, attributes and events become part of the Java API, so the rest of your team calls <code>setX()</code> and <code>getX()</code> on a normal class and never touches the underlying element.</p>
<p>A wrapper declares the tag it wraps and the script and stylesheet the component depends on, with the <code>@NodeName</code>, <code>@JavaScript</code> and <code>@StyleSheet</code> annotations. webforJ deduplicates script loads, so several components can depend on the same script without loading it twice. Events work the same way: you register listeners from Java, with options such as debouncing, throttling, and filtering.</p>
<p>This is the route for a component library you want to use across many screens. You write the wrapper once, and from then on the component behaves like any built-in one. Our post on <a class="" href="https://docs.webforj.com/zh/blog/web-components-in-java">web components in Java</a> walks through the idea in more depth.</p>
<p>If a built-in component already covers the need, such as a text field or a button, use that instead. For a one-off piece of DOM work that nobody will reuse, the <code>Element</code> class can be used directly without a wrapper.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="execute-javascript">Run JavaScript from Java<a href="https://docs.webforj.com/zh/blog/webforj-javascript-integration#execute-javascript" class="hash-link" aria-label="直接链接到 Run JavaScript from Java" title="直接链接到 Run JavaScript from Java" translate="no">​</a></h2>
<p>Sometimes you don't need a component, just one call in the browser: scroll the window, focus a field, read a browser value or call a method on a web component. <a class="" href="https://docs.webforj.com/zh/docs/building-ui/execute-javascript">Execute JavaScript</a> covers that with <code>executeJs</code>, <code>executeJsAsync</code> and <code>executeJsVoidAsync</code>. They work at two levels. <code>Page</code> runs a script in the context of the whole page, and an <code>Element</code> runs a script scoped to a single client element.</p>
<p>The three methods differ in whether the calling thread waits and whether a result comes back. Fire-and-forget calls return nothing. The asynchronous form returns a <code>PendingResult</code>, so Java can react when the browser answers, and returned values arrive as matching Java types such as <code>String</code>, <code>Boolean</code> and numbers.</p>
<p>This is the lightest route. It suits glue code, not whole widgets.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="managing-resources">Load scripts and styles<a href="https://docs.webforj.com/zh/blog/webforj-javascript-integration#managing-resources" class="hash-link" aria-label="直接链接到 Load scripts and styles" title="直接链接到 Load scripts and styles" translate="no">​</a></h2>
<p>If you already have a script or a stylesheet, <a class="" href="https://docs.webforj.com/zh/docs/managing-resources/importing-assets">Importing Assets</a> attaches it to a component or the app with annotations. There is no build step, no npm, and no compile. The same page covers inline scripts and styles and how they load, and the <a class="" href="https://docs.webforj.com/zh/docs/managing-resources/overview">Managing Resources</a> section also covers runtime injection of resources, custom URL protocols and file downloads.</p>
<p>Choose this route when the asset is already built: a vendor script served from a URL, a company stylesheet or a bundle another team publishes.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="bundler">Bring npm packages and components<a href="https://docs.webforj.com/zh/blog/webforj-javascript-integration#bundler" class="hash-link" aria-label="直接链接到 Bring npm packages and components" title="直接链接到 Bring npm packages and components" translate="no">​</a></h2>
<p>The <a class="" href="https://docs.webforj.com/zh/docs/managing-resources/bundler/overview">frontend bundler</a>, available since 26.01, lets a class draw on the <a href="https://www.npmjs.com/" target="_blank" rel="noopener noreferrer" class="">npm</a> ecosystem and write components in React, Svelte or Lit, all from a Java project with no Node toolchain to install or run. A class names the frontend it needs with <code>@BundleEntry</code> and <code>@BundlePackage</code>. The build installs the packages, compiles the sources and loads the result when a component of that class is created.</p>
<p>The bundler runs as part of the <a class="" href="https://docs.webforj.com/zh/docs/configuration/build-plugin">webforJ build plugin</a> for Maven or Gradle. It also handles SCSS, Less and Tailwind through <a class="" href="https://docs.webforj.com/zh/docs/managing-resources/bundler/extensions/overview">extensions</a>, and it can run frontend tests as part of the build. Because the annotations sit on the class, the frontend a view needs travels with the view.</p>
<p>It's opt in. A project that already uses the asset annotations keeps them unchanged and adds the bundler only where a package, a compiled source, or a test calls for it. A step-by-step example of <a class="" href="https://docs.webforj.com/zh/blog/npm-packages-java-web-app">installing an npm package in a Java app</a> shows it end to end. The <a class="" href="https://docs.webforj.com/zh/docs/managing-resources/bundler/npm-and-assets">packages and assets page</a> explains how packages and entries work.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ready-made">Ready-made integrations<a href="https://docs.webforj.com/zh/blog/webforj-javascript-integration#ready-made" class="hash-link" aria-label="直接链接到 Ready-made integrations" title="直接链接到 Ready-made integrations" translate="no">​</a></h2>
<p>Some wrapped libraries ship as webforJ components. The <a class="" href="https://docs.webforj.com/zh/docs/components/google-charts">GoogleChart</a> component integrates the Google Charts library, so you configure bar, line, pie and geo charts in Java with a type, a data set, and an options map. No script tag or wrapper is involved.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="which-route">Which route fits?<a href="https://docs.webforj.com/zh/blog/webforj-javascript-integration#which-route" class="hash-link" aria-label="直接链接到 Which route fits?" title="直接链接到 Which route fits?" translate="no">​</a></h2>
<div class="table-wrapper"><div class="table-container" style="overflow-x:auto"><table><thead><tr><th>You have</th><th>Use</th><th>Build step</th></tr></thead><tbody><tr><td>A third-party web component</td><td>ElementComposite</td><td>None</td></tr><tr><td>One browser action to trigger</td><td>Execute JavaScript</td><td>None</td></tr><tr><td>A built script or stylesheet</td><td>Asset annotations</td><td>None</td></tr><tr><td>An npm package</td><td>Frontend bundler</td><td>Yes, by the build plugin</td></tr><tr><td>A React, Svelte or Lit component</td><td>Frontend bundler</td><td>Yes, by the build plugin</td></tr></tbody></table></div><style data-emotion="css 1k371a6">@media print{.css-1k371a6{position:absolute!important;}}</style></div>
<p>The routes stack. A common shape is a bundler entry that registers a custom element, Java that consumes it, and an occasional script call for the odd browser action.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-npm">Can I use npm packages in a webforJ app?<a href="https://docs.webforj.com/zh/blog/webforj-javascript-integration#faq-npm" class="hash-link" aria-label="直接链接到 Can I use npm packages in a webforJ app?" title="直接链接到 Can I use npm packages in a webforJ app?" translate="no">​</a></h2>
<p>Yes. Declare the package and version with <code>@BundlePackage</code>, point <code>@BundleEntry</code> at the file under <code>src/main/frontend</code> that imports it, and the build installs and compiles it. The build plugin manages Bun for you, so you install nothing extra.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-react">Can I use a React component in webforJ?<a href="https://docs.webforj.com/zh/blog/webforj-javascript-integration#faq-react" class="hash-link" aria-label="直接链接到 Can I use a React component in webforJ?" title="直接链接到 Can I use a React component in webforJ?" translate="no">​</a></h2>
<p>Yes. The bundler lets you write components in React, Svelte or Lit and consume them from Java, with the compiled output loaded when a component of that class is created. The entry registers a custom element, and the Java side consumes it with <code>Element</code> and listens to its events. A worked example shows how to <a class="" href="https://docs.webforj.com/zh/blog/wrap-react-component-java">wrap a React component in Java</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-no-js">Do I have to write JavaScript to use webforJ?<a href="https://docs.webforj.com/zh/blog/webforj-javascript-integration#faq-no-js" class="hash-link" aria-label="直接链接到 Do I have to write JavaScript to use webforJ?" title="直接链接到 Do I have to write JavaScript to use webforJ?" translate="no">​</a></h2>
<p>No. Components, routing, forms, tables and security are written in Java. JavaScript enters only when you choose to bring a library or a script, and then through the routes in this post.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next-step">Pick your first integration<a href="https://docs.webforj.com/zh/blog/webforj-javascript-integration#next-step" class="hash-link" aria-label="直接链接到 Pick your first integration" title="直接链接到 Pick your first integration" translate="no">​</a></h2>
<p>Start with the library you already know you need. If it ships as a web component, read the <a class="" href="https://docs.webforj.com/zh/docs/building-ui/element-composite">ElementComposite guide</a>. If it's an npm package or a React, Svelte or Lit component, read the <a class="" href="https://docs.webforj.com/zh/docs/managing-resources/bundler/overview">frontend bundler overview</a>. If it's a script you've already built, the <a class="" href="https://docs.webforj.com/zh/docs/managing-resources/importing-assets">asset annotations</a> attach it in one line.</p><div></div>]]></content>
        <author>
            <name>webforJ</name>
        </author>
        <category label="Integrations" term="Integrations"/>
        <category label="JavaScript" term="JavaScript"/>
        <category label="Web Development" term="Web Development"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Who Uses webforJ? Customer Stories From Real Projects]]></title>
        <id>https://docs.webforj.com/zh/blog/who-uses-webforj</id>
        <link href="https://docs.webforj.com/zh/blog/who-uses-webforj"/>
        <updated>2026-11-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Who uses webforJ in production? The published webforJ customer stories, Prodin and b+p, with their team size, timeline and user counts, plus who builds webforJ.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-11-10-who-uses-webforj/cover.jpg" alt="cover image" class="img_ev3q"></p>
<p>A team that is about to pick a framework wants to know who already runs it. Logos on a landing page don't help much. What helps is knowing which companies built what, how big the team was, and how long it took.</p>
<p>So, who uses webforJ? Two full customer stories are published, one about Prodin and one about b+p, and both name webforJ as the framework behind the project. Prodin is Prodin Business Solutions, a Dutch software provider for enterprise resource planning (ERP). b+p is an HR consulting firm in Southwest Germany. Here is what each team built, how many people it took, and how long it took.</p>
<div class="cta_UErl" role="complementary" aria-label="Working on something similar?"><div class="copy_XH8h"><p class="title_HfaR">Working on something similar?</p><p class="text_x4Wj">Ask the webforJ team. Questions are welcome, big or small.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="https://webforj.com/schedule-discovery/" target="_blank" rel="noopener noreferrer">预约通话<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a><a class="secondary_GDTR" href="mailto:sales@basis.cloud?subject=Question%20about%20webforJ&amp;body=Hi%20webforJ%20team%2C%0A%0A%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->邮件咨询</a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="which-companies">Which companies use webforJ?<a href="https://docs.webforj.com/zh/blog/who-uses-webforj#which-companies" class="hash-link" aria-label="直接链接到 Which companies use webforJ?" title="直接链接到 Which companies use webforJ?" translate="no">​</a></h2>
<p>Two customer stories are on our <a href="https://webforj.com/success-stories/" target="_blank" rel="noopener noreferrer" class="">success stories page</a>:</p>
<ol>
<li class=""><strong>Prodin</strong> moved a Swing-based Java ERP app to the browser.</li>
<li class=""><strong>b+p</strong> delivered an employee survey for the University of Saarland.</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="prodin">Prodin: a Swing ERP on the web<a href="https://docs.webforj.com/zh/blog/who-uses-webforj#prodin" class="hash-link" aria-label="直接链接到 Prodin: a Swing ERP on the web" title="直接链接到 Prodin: a Swing ERP on the web" translate="no">​</a></h2>
<p>Prodin Business Solutions is a Dutch ERP software provider with over 45 years of experience. Its ERP system was built on a Java Swing architecture, and the task was to modernize it for a web-first world.</p>
<p>Prodin looked at low-code platforms, and at a Java backend combined with front-end frameworks like Angular and React. It chose webforJ. The reasons include reuse of existing Java-capable code, some of which had been written over two decades ago, and using Java across the whole stack instead of splitting work across technologies and teams.</p>
<p>How the project went:</p>
<ul>
<li class="">Prodin assembled a six-member Java development team, including junior and mid-level developers, to assist in the migration alongside its existing staff.</li>
<li class="">Within four months, Prodin delivered its first web-based modules to customers.</li>
<li class="">The first web modules shipped while reusing the existing Java codebase, which avoided a complete rewrite.</li>
<li class="">Zero deployment was one of the reasons for the choice: updates roll out across the web without requiring end-user intervention.</li>
<li class="">Prodin can now offer both on-premise and SaaS solutions.</li>
</ul>
<p>In Prodin's words: "webforJ enabled us to deliver a fully web-based version of our ERP in just four months, all while reusing our existing Java codebase."</p>
<p>The full story is <a href="https://webforj.com/wp-content/uploads/2024/10/BASIS_SuccessStory_Prodin.pdf" target="_blank" rel="noopener noreferrer" class="">Prodin's Journey to Web Modernization with webforJ</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="bp">The b+p survey project<a href="https://docs.webforj.com/zh/blog/who-uses-webforj#bp" class="hash-link" aria-label="直接链接到 The b+p survey project" title="直接链接到 The b+p survey project" translate="no">​</a></h2>
<p>b+p Beratung und Personal is an established HR consulting firm in the southwest of Germany, founded in 1998. In fall 2023 it partnered with BASIS International to deliver an employee survey at the University of Saarland. The goal was to gather insights from over 500 employees across mobile and desktop devices within just two weeks.</p>
<p>b+p managed the overall project. BASIS International provided consulting and development capacity, and the custom solution was built with webforJ.</p>
<p>The requirements were:</p>
<ul>
<li class="">Support a large number of respondents across desktop and mobile.</li>
<li class="">Comply with GDPR, with anonymized responses that are accessible only in aggregated form.</li>
<li class="">Scale to handle thousands of concurrent users.</li>
</ul>
<p>Over 500 employees were invited to take part, and the app handled the load with no noticeable performance degradation. Responses were anonymized before delivery to the university.</p>
<p>The full story is <a href="https://webforj.com/wp-content/uploads/2024/10/BASIS_SuccessStory_bp.pdf" target="_blank" rel="noopener noreferrer" class="">Empowering Project Implementation Partners with webforJ for Employee Surveys</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="in-common">What these projects have in common<a href="https://docs.webforj.com/zh/blog/who-uses-webforj#in-common" class="hash-link" aria-label="直接链接到 What these projects have in common" title="直接链接到 What these projects have in common" translate="no">​</a></h2>
<p>Both are business apps, and both ran under real constraints. One had an existing codebase to keep, the other a two-week window and a privacy rule.</p>
<p>Prodin is the desktop modernization case. A Swing ERP system with Java logic written over a long period moved to the web without a rewrite, by a Java team. Our guide to <a class="" href="https://docs.webforj.com/zh/blog/swing-to-web-migration">Swing to web migration</a> covers moving screens one at a time.</p>
<p>b+p is the custom app case. A standard survey tool wasn't the fit, so the team built a tailored app in Java that was developed and deployed in one environment.</p>
<p>Both projects show what we built the framework for. webforJ is the full-stack Java framework for business web apps, and you build the whole app, screens included, in Java. For the capabilities behind that, read our guide to the <a class="" href="https://docs.webforj.com/zh/blog/java-full-stack-framework">full-stack Java framework</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="who-is-behind">Who builds and supports webforJ?<a href="https://docs.webforj.com/zh/blog/who-uses-webforj#who-is-behind" class="hash-link" aria-label="直接链接到 Who builds and supports webforJ?" title="直接链接到 Who builds and supports webforJ?" translate="no">​</a></h2>
<p>webforJ is built by BASIS International. <a href="https://webforj.com/company" target="_blank" rel="noopener noreferrer" class="">BASIS</a> software and software tools have been used to develop business solutions for both large international corporations and SMEs for almost 40 years, with 20+ years of delivering technology built on Java.</p>
<p>Operating its own sites in North America and Europe and working with partners in more than 30 countries, BASIS is well positioned on the international stage.</p>
<p>Support is part of it too. Prodin took part in BASIS International's growth accelerator support program, with regular Q&amp;A sessions and personal access to the core webforJ development team.</p>
<p>For the wider case on releases and maturity, read <a class="" href="https://docs.webforj.com/zh/blog/is-webforj-production-ready">Is webforJ production ready</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">Frequently asked questions<a href="https://docs.webforj.com/zh/blog/who-uses-webforj#faq" class="hash-link" aria-label="直接链接到 Frequently asked questions" title="直接链接到 Frequently asked questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-companies">Which companies use webforJ in production?<a href="https://docs.webforj.com/zh/blog/who-uses-webforj#faq-companies" class="hash-link" aria-label="直接链接到 Which companies use webforJ in production?" title="直接链接到 Which companies use webforJ in production?" translate="no">​</a></h3>
<p>Prodin Business Solutions, a Dutch software provider for enterprise resource planning, and b+p, an HR consulting firm in Southwest Germany, each have a published webforJ customer story. Prodin delivered its first web-based modules within four months, with a six-member Java development team assisting its existing staff. b+p delivered an employee survey for over 500 employees at the University of Saarland.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-stories">Where are the webforJ customer stories?<a href="https://docs.webforj.com/zh/blog/who-uses-webforj#faq-stories" class="hash-link" aria-label="直接链接到 Where are the webforJ customer stories?" title="直接链接到 Where are the webforJ customer stories?" translate="no">​</a></h3>
<p>On the <a href="https://webforj.com/success-stories/" target="_blank" rel="noopener noreferrer" class="">success stories page</a> on our website, under the "Our webforJ Success Stories" heading. Each story is a PDF you can read in full.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-apps">What kinds of apps are built with webforJ?<a href="https://docs.webforj.com/zh/blog/who-uses-webforj#faq-apps" class="hash-link" aria-label="直接链接到 What kinds of apps are built with webforJ?" title="直接链接到 What kinds of apps are built with webforJ?" translate="no">​</a></h3>
<p>The published stories are business applications: an ERP system moved from Swing to the web, and a custom employee survey with anonymized responses. Both were built in Java.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-behind">Who is behind webforJ?<a href="https://docs.webforj.com/zh/blog/who-uses-webforj#faq-behind" class="hash-link" aria-label="直接链接到 Who is behind webforJ?" title="直接链接到 Who is behind webforJ?" translate="no">​</a></h3>
<p>BASIS International. BASIS software and software tools have been used to develop business solutions for both large international corporations and SMEs for almost 40 years, with 20+ years of delivering technology built on Java.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="start">Read the stories, then start a project<a href="https://docs.webforj.com/zh/blog/who-uses-webforj#start" class="hash-link" aria-label="直接链接到 Read the stories, then start a project" title="直接链接到 Read the stories, then start a project" translate="no">​</a></h2>
<p>Open the <a href="https://webforj.com/success-stories/" target="_blank" rel="noopener noreferrer" class="">success stories page</a> and read the Prodin and b+p stories in full. If your own situation looks like one of them, generate a project with <a href="https://docs.webforj.com/startforj" target="_blank" rel="noopener noreferrer" class="">startforJ</a>, follow the <a class="" href="https://docs.webforj.com/zh/docs/introduction/getting-started">getting started guide</a>, and build one real screen of your app in Java.</p><div></div>]]></content>
        <author>
            <name>webforJ</name>
        </author>
        <category label="Insights" term="Insights"/>
        <category label="Web Development" term="Web Development"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Why webforJ? Straight Answers to Questions Java Teams Ask]]></title>
        <id>https://docs.webforj.com/zh/blog/why-webforj</id>
        <link href="https://docs.webforj.com/zh/blog/why-webforj"/>
        <updated>2026-11-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Why webforJ for your next business web app? What sets it apart, why we built it, who stands behind it, and how it fits your backend and your own components.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-11-10-why-webforj/cover-v2.jpg" alt="cover image" class="img_ev3q"></p>
<p>Your Java team has a new business web app to build, and the shortlist of frameworks keeps growing. Every option promises productivity, so the useful question is the one developers keep asking in public: why webforJ, and what do you get that you would not get elsewhere?</p>
<p>Here are straight answers. webforJ is the full-stack Java framework for business web apps: you build the whole app, screens included, in Java. The rest of this post shows what that means in practice, why we built it, and how it fits the backend, the tools, and the components you already have.</p>
<div class="cta_UErl" role="complementary" aria-label="Already running Spring Boot?"><div class="copy_XH8h"><p class="title_HfaR">Already running Spring Boot?</p><p class="text_x4Wj">Ask the team how webforJ fits into your existing Spring app.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="https://webforj.com/schedule-discovery/" target="_blank" rel="noopener noreferrer">预约通话<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a><a class="secondary_GDTR" href="mailto:sales@basis.cloud?subject=webforJ%20with%20Spring%20Boot&amp;body=Hi%20webforJ%20team%2C%0A%0A%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->邮件咨询</a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="different">What makes webforJ different?<a href="https://docs.webforj.com/zh/blog/why-webforj#different" class="hash-link" aria-label="直接链接到 What makes webforJ different?" title="直接链接到 What makes webforJ different?" translate="no">​</a></h2>
<p>Writing a web UI in Java is the starting point, not the answer. What sets webforJ apart is everything it puts around that code.</p>
<ol>
<li class=""><strong>Your AI assistant's work gets checked.</strong> An AI plugin teaches coding tools the real API, browserless tests give them a pass or a fail, and an in-app assistant compiles every edit before you see it.</li>
<li class=""><strong>Your desktop apps come along.</strong> Swing, JavaFX and SWT apps run in the browser today and move to web screens one at a time.</li>
<li class=""><strong>You edit the app while it runs.</strong> Live editing changes the running app and writes the result back into your Java source.</li>
<li class=""><strong>Your backend stays yours.</strong> webforJ runs standalone or with Spring Boot, and data comes from Spring Data or from any source you choose.</li>
<li class=""><strong>The whole stack is Java.</strong> Components, routing, forms, tables, security, translation and tests come from one framework, built by one team.</li>
</ol>
<p>The sections below go through each one, with the documentation behind it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="full-stack">Everything in one Java framework<a href="https://docs.webforj.com/zh/blog/why-webforj#full-stack" class="hash-link" aria-label="直接链接到 Everything in one Java framework" title="直接链接到 Everything in one Java framework" translate="no">​</a></h2>
<p>A business app needs far more than buttons. webforJ covers the list in Java:</p>
<ul>
<li class=""><strong>Components and routing.</strong> You compose screens from <a class="" href="https://docs.webforj.com/zh/docs/building-ui/overview">components</a> and define pages with <a class="" href="https://docs.webforj.com/zh/docs/routing/overview">routes</a>.</li>
<li class=""><strong>Forms.</strong> <a class="" href="https://docs.webforj.com/zh/docs/data-binding/overview">Data binding</a> connects components to your Java models with two-way synchronization, validation and transformation.</li>
<li class=""><strong>Tables.</strong> The <a class="" href="https://docs.webforj.com/zh/docs/components/table/overview">Table</a> component binds an entity class to a repository to populate columns and rows.</li>
<li class=""><strong>Security.</strong> <a class="" href="https://docs.webforj.com/zh/docs/security/overview">Route security</a> protects views with declarative annotations and central authentication and authorization.</li>
<li class=""><strong>Translation.</strong> The built-in <a class="" href="https://docs.webforj.com/zh/docs/advanced/i18n-localization">translation system</a> looks up localized strings by key and detects the browser locale.</li>
<li class=""><strong>Testing.</strong> The <a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/overview">TestBed</a> runs views in plain JUnit tests, with no browser and no server.</li>
</ul>
<p>Because it's one framework, these pieces share the same conventions. For a deeper walk through the stack, read our guide to the <a class="" href="https://docs.webforj.com/zh/blog/java-full-stack-framework">full-stack Java framework</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-built">Why we built webforJ<a href="https://docs.webforj.com/zh/blog/why-webforj#why-built" class="hash-link" aria-label="直接链接到 Why we built webforJ" title="直接链接到 Why we built webforJ" translate="no">​</a></h2>
<p>Java teams run business-critical software. They know the domain, the data and the build tools, and the browser shouldn't force them to staff a second codebase in another language. We built webforJ so that the same Java skills carry the app from the database to the screen.</p>
<p>That goal shaped the framework. webforJ handles the user interface in Java while your services, repositories, and configuration stay yours. With Spring on the classpath, Spring handles backend concerns and webforJ handles the screens. For standard components there is no second codebase in JavaScript to staff and maintain, so fewer moving parts need to stay current.</p>
<p>The same goal explains the desktop story. Many organizations have years of business logic in Java desktop apps that can't be rewritten overnight. We wanted those teams to reach the web without throwing that work away.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="who-is-behind">Who is behind webforJ?<a href="https://docs.webforj.com/zh/blog/why-webforj#who-is-behind" class="hash-link" aria-label="直接链接到 Who is behind webforJ?" title="直接链接到 Who is behind webforJ?" translate="no">​</a></h2>
<p>webforJ is built by BASIS International. <a href="https://webforj.com/company" target="_blank" rel="noopener noreferrer" class="">BASIS</a> software and software tools have been used to develop business solutions for both large international corporations and SMEs for almost 40 years, with 20+ years of delivering technology built on Java.</p>
<p>Operating its own sites in North America and Europe and working with partners in more than 30 countries, BASIS is well positioned on the international stage.</p>
<p>Releases are regular and public. Version 24.00 went live on April 8, 2024, and 26.02 followed on August 31, 2026, with artifacts on Maven Central. The full case, with the release record and customer stories, is in <a class="" href="https://docs.webforj.com/zh/blog/is-webforj-production-ready">Is webforJ production ready</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="any-backend">Does it work with any backend?<a href="https://docs.webforj.com/zh/blog/why-webforj#any-backend" class="hash-link" aria-label="直接链接到 Does it work with any backend?" title="直接链接到 Does it work with any backend?" translate="no">​</a></h2>
<p>Yes. You choose the backend, and webforJ adapts to it.</p>
<p>You can run webforJ standalone, or add <a class="" href="https://docs.webforj.com/zh/docs/integrations/spring/spring-boot">Spring Boot</a> through the same startforJ generator. With Spring, your components can inject any Spring bean through the constructor, and your routes keep working exactly as in a plain app.</p>
<p>Data follows the same freedom. <a class="" href="https://docs.webforj.com/zh/docs/integrations/spring/spring-data-jpa">Spring Data JPA</a> repositories connect to tables through <code>SpringDataRepository</code>. Data that lives elsewhere, such as a REST API or an external service, connects through a <a class="" href="https://docs.webforj.com/zh/docs/advanced/repository/delegating-repository">custom data source</a> with <code>DelegatingRepository</code>, where you supply the find, count and key-lookup functions.</p>
<p>Nothing here ties you to one persistence layer or one service style. The wider picture, from built-in components to npm packages and the Java ecosystem, is covered in <a class="" href="https://docs.webforj.com/zh/blog/webforj-ecosystem">how big the webforJ ecosystem is</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="own-components">Can I use my own components?<a href="https://docs.webforj.com/zh/blog/why-webforj#own-components" class="hash-link" aria-label="直接链接到 Can I use my own components?" title="直接链接到 Can I use my own components?" translate="no">​</a></h2>
<p>Yes. webforJ's own components are built on web components, so wrapping a third-party custom element uses the same mechanism in the open. With <a class="" href="https://docs.webforj.com/zh/docs/building-ui/element-composite"><code>ElementComposite</code></a> you expose the element's properties, attributes and events through a typed Java class and use it like any other component.</p>
<p>JavaScript isn't off limits either. You can <a class="" href="https://docs.webforj.com/zh/docs/building-ui/execute-javascript">run client-side JavaScript from Java</a>, and the <a class="" href="https://docs.webforj.com/zh/docs/managing-resources/bundler/overview">frontend bundler</a> lets a class draw on npm packages and write components in React, Svelte or Lit from a Java project.</p>
<p>To see the full wrapping pattern from script loading to typed events, read <a class="" href="https://docs.webforj.com/zh/blog/web-components-in-java">Web components in Java</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ai-ready">Ready for AI-assisted development<a href="https://docs.webforj.com/zh/blog/why-webforj#ai-ready" class="hash-link" aria-label="直接链接到 Ready for AI-assisted development" title="直接链接到 Ready for AI-assisted development" translate="no">​</a></h2>
<p>AI coding tools are fast, and their output still needs checking. webforJ gives them three ways to check.</p>
<ul>
<li class=""><strong>The AI plugin.</strong> The <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling">webforJ AI plugin</a> installs the MCP server and Agent Skills in one step for Claude Code, Copilot, Cursor, Gemini and Codex. The MCP server gives the assistant live access to the knowledge base, and the skills teach it which workflows to follow.</li>
<li class=""><strong>Browserless tests.</strong> The TestBed lets an assistant run a view in a JUnit test and get a clear pass or fail.</li>
<li class=""><strong>An assistant in the running app.</strong> The <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/craftforj-assistant">craftforJ Assistant</a> writes Java, compiles each edit before you see it, and applies it to the running app.</li>
</ul>
<p>Together they form a loop of write, check, and review. We cover it in <a class="" href="https://docs.webforj.com/zh/blog/ai-verification-loop-java">the AI verification loop for Java</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="for-developers">Built for developers<a href="https://docs.webforj.com/zh/blog/why-webforj#for-developers" class="hash-link" aria-label="直接链接到 Built for developers" title="直接链接到 Built for developers" translate="no">​</a></h2>
<p>A framework should make the team faster on day one and keep it fast. That's why webforJ comes with tools around the code.</p>
<ul>
<li class=""><strong>One language, one team.</strong> The same Java developers who write the services write the screens.</li>
<li class=""><strong>A project generator.</strong> <a class="" href="https://docs.webforj.com/zh/docs/introduction/getting-started">startforJ</a> creates a Maven project from an archetype with the dependencies and a pre-made layout.</li>
<li class=""><strong>Live editing.</strong> <a class="" href="https://docs.webforj.com/zh/docs/craftforj">craftforJ</a> shows the component tree of your running app, lets you change properties live, and writes the changes you keep back into your Java source.</li>
<li class=""><strong>One design system.</strong> The DWC <a class="" href="https://docs.webforj.com/zh/docs/styling/overview">design system</a> ships built-in <a class="" href="https://docs.webforj.com/zh/docs/styling/themes">themes</a> and an official Figma kit, so designers and developers work from the same tokens and components.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="swing-to-web">From Swing to the web<a href="https://docs.webforj.com/zh/blog/why-webforj#swing-to-web" class="hash-link" aria-label="直接链接到 From Swing to the web" title="直接链接到 From Swing to the web" translate="no">​</a></h2>
<p>If your business runs on a Swing, JavaFX or SWT desktop app, you don't have to rewrite it to reach the browser. The <a class="" href="https://docs.webforj.com/zh/docs/integrations/webswing/overview">Webswing integration</a> runs those apps in a web browser without changes to the source, and the <code>WebswingConnector</code> component embeds them inside a webforJ app.</p>
<p>From there the move is gradual. You build new screens in webforJ around the embedded app, replace old screens one at a time, and phase out the legacy app when you are ready. During the move the old and new screens stay connected, because the connector supports <a class="" href="https://docs.webforj.com/zh/docs/integrations/webswing/communication">two-way communication</a>: your webforJ app sends actions to the Swing app and receives its events.</p>
<p>The full migration path is in <a class="" href="https://docs.webforj.com/zh/blog/swing-to-web-migration">Swing to web migration</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">Frequently asked questions<a href="https://docs.webforj.com/zh/blog/why-webforj#faq" class="hash-link" aria-label="直接链接到 Frequently asked questions" title="直接链接到 Frequently asked questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-different">What makes webforJ different?<a href="https://docs.webforj.com/zh/blog/why-webforj#faq-different" class="hash-link" aria-label="直接链接到 What makes webforJ different?" title="直接链接到 What makes webforJ different?" translate="no">​</a></h3>
<p>webforJ is the full-stack Java framework for business web apps, and it's built for how teams work now. AI assistants get their work checked against the real API and by browserless tests, existing Swing apps move to the web one screen at a time, and the whole app, screens included, stays in Java.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-who-behind">Who is behind webforJ?<a href="https://docs.webforj.com/zh/blog/why-webforj#faq-who-behind" class="hash-link" aria-label="直接链接到 Who is behind webforJ?" title="直接链接到 Who is behind webforJ?" translate="no">​</a></h3>
<p>BASIS International. BASIS software and software tools have been used to develop business solutions for both large international corporations and SMEs for almost 40 years, with 20+ years of delivering technology built on Java.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-backend">Does webforJ lock me into one backend?<a href="https://docs.webforj.com/zh/blog/why-webforj#faq-backend" class="hash-link" aria-label="直接链接到 Does webforJ lock me into one backend?" title="直接链接到 Does webforJ lock me into one backend?" translate="no">​</a></h3>
<p>No. webforJ runs standalone or with Spring Boot, works with Spring Data repositories, and connects to any other data source through a custom repository.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-components">Can I use my own web components?<a href="https://docs.webforj.com/zh/blog/why-webforj#faq-components" class="hash-link" aria-label="直接链接到 Can I use my own web components?" title="直接链接到 Can I use my own web components?" translate="no">​</a></h3>
<p>Yes. <code>ElementComposite</code> wraps any custom element as a typed Java component, and you can also run JavaScript from Java or bundle npm packages.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-ai">Can AI assistants build with webforJ?<a href="https://docs.webforj.com/zh/blog/why-webforj#faq-ai" class="hash-link" aria-label="直接链接到 Can AI assistants build with webforJ?" title="直接链接到 Can AI assistants build with webforJ?" translate="no">​</a></h3>
<p>Yes. The webforJ AI plugin gives assistants the MCP server and Agent Skills, and the TestBed and the craftforJ Assistant check the result.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="start">Start a project with startforJ<a href="https://docs.webforj.com/zh/blog/why-webforj#start" class="hash-link" aria-label="直接链接到 Start a project with startforJ" title="直接链接到 Start a project with startforJ" translate="no">​</a></h2>
<p>The quickest way to judge these answers is to build something. Generate a project with <a href="https://docs.webforj.com/startforj" target="_blank" rel="noopener noreferrer" class="">startforJ</a>, follow the <a class="" href="https://docs.webforj.com/zh/docs/introduction/getting-started">getting started guide</a>, and build one real screen of your own app in Java.</p>
<p>If you are still comparing options, <a class="" href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications">Java framework for web applications</a> lists what to require from any of them. The decisions a team settles first are laid out in <a class="" href="https://docs.webforj.com/zh/blog/starting-new-java-project">starting a new Java project</a>.</p><div></div>]]></content>
        <author>
            <name>webforJ</name>
        </author>
        <category label="Insights" term="Insights"/>
        <category label="Web Development" term="Web Development"/>
        <category label="Spring" term="Spring"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Cursor Spring Boot UI: Let the Assistant Write Your Screens in Java]]></title>
        <id>https://docs.webforj.com/zh/blog/cursor-spring-boot-ui</id>
        <link href="https://docs.webforj.com/zh/blog/cursor-spring-boot-ui"/>
        <updated>2026-11-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Build a Spring Boot UI with Cursor and webforJ. Install the AI plugin, prompt for a view that injects a Spring bean, and check it with a browserless test.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-11-09-cursor-spring-boot-ui/cover.jpg" alt="cover image" class="img_ev3q"></p>
<p>You have a Spring Boot backend with services that work, and now it needs screens. Asking an AI assistant to write them is tempting, but a plain prompt in Cursor leaves the assistant guessing at your front end. For a Cursor Spring Boot UI that holds up, give the assistant the real webforJ API and let it write the views in Java. The webforJ AI plugin does that, and a short test confirms the result.</p>
<div class="cta_UErl" role="complementary" aria-label="Using AI to build Java apps?"><div class="copy_XH8h"><p class="title_HfaR">Using AI to build Java apps?</p><p class="text_x4Wj">Ask the team about webforJ AI tooling: the MCP server, Agent Skills, and how they keep generated code correct.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="https://webforj.com/schedule-discovery/" target="_blank" rel="noopener noreferrer">预约通话<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a><a class="secondary_GDTR" href="mailto:sales@basis.cloud?subject=AI%20and%20webforJ&amp;body=Hi%20webforJ%20team%2C%0A%0A%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->邮件咨询</a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-cursor-needs">What Cursor needs for a Spring Boot UI<a href="https://docs.webforj.com/zh/blog/cursor-spring-boot-ui#what-cursor-needs" class="hash-link" aria-label="直接链接到 What Cursor needs for a Spring Boot UI" title="直接链接到 What Cursor needs for a Spring Boot UI" translate="no">​</a></h2>
<p>An assistant that guesses at a framework invents method names. Two pieces from the <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling">webforJ AI plugin</a> remove the guessing:</p>
<ul>
<li class="">The <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/mcp">MCP server</a> lets the assistant look things up in the webforJ docs, scaffold projects, and validate design tokens instead of recalling them from memory.</li>
<li class="">The <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/agent-skills">Agent Skills</a> tell the assistant when to use those tools and in what order. There are skills for building forms, adding servlets, styling apps, and creating components.</li>
</ul>
<p>Your screens are plain Java classes in the same project as your services. There is no second codebase to hand the assistant. If you use Claude Code instead, the same plugin lets you <a class="" href="https://docs.webforj.com/zh/blog/claude-code-java-web-app">build a Java web app with Claude Code</a> end to end.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cursor-ai-spring-boot">Cursor AI and Spring Boot, step by step<a href="https://docs.webforj.com/zh/blog/cursor-spring-boot-ui#cursor-ai-spring-boot" class="hash-link" aria-label="直接链接到 Cursor AI and Spring Boot, step by step" title="直接链接到 Cursor AI and Spring Boot, step by step" translate="no">​</a></h2>
<p>Follow these steps.</p>
<ol>
<li class="">
<p><strong>Create a Spring Boot project.</strong> Use <a href="https://docs.webforj.com/startforj" target="_blank" rel="noopener noreferrer" class="">startforJ</a> and choose <strong>Spring Boot</strong> under <strong>Runs on</strong>. Or use the command line with the <code>webforj-spring</code> flavor:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">mvn </span><span class="token parameter variable" style="color:var(--dwc-code-variable)">-B</span><span class="token plain"> archetype:generate </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token parameter variable" style="color:var(--dwc-code-variable)">-DarchetypeGroupId</span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain">com.webforj </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token parameter variable" style="color:var(--dwc-code-variable)">-DarchetypeArtifactId</span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain">webforj-archetype-hello-world </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token parameter variable" style="color:var(--dwc-code-variable)">-DarchetypeVersion</span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain">LATEST </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token parameter variable" style="color:var(--dwc-code-variable)">-DgroupId</span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain">org.example </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token parameter variable" style="color:var(--dwc-code-variable)">-DartifactId</span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain">my-app </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token parameter variable" style="color:var(--dwc-code-variable)">-Dversion</span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token number" style="color:var(--dwc-code-number)">1.0</span><span class="token plain">-SNAPSHOT </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token parameter variable" style="color:var(--dwc-code-variable)">-Dflavor</span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain">webforj-spring</span><br></div></code></pre></div></div>
</li>
<li class="">
<p><strong>Connect Cursor to webforJ.</strong> Cursor uses manual configuration rather than a marketplace command. The docs point to the <a href="https://github.com/webforj/webforj-ai#clients" target="_blank" rel="noopener noreferrer" class="">per-client install guide</a> for the exact steps, so follow that guide. Cursor is an MCP-over-HTTP client, so the server address is <code>https://mcp.webforj.com/mcp</code>.</p>
</li>
<li class="">
<p><strong>Name webforJ in the prompt.</strong> That's the cue the assistant uses to reach for the plugin instead of general Java knowledge. Ask for verification too:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">Add a webforJ view at /orders. It takes the existing OrderService</span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">through the constructor and shows its summary for the customer name</span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">typed into a text field. Verify the API against the webforJ docs.</span><br></div></code></pre></div></div>
</li>
<li class="">
<p><strong>Review the view it writes.</strong> Look for a <code>@Route</code> class that receives the bean through its constructor. The next section shows the shape.</p>
</li>
<li class="">
<p><strong>Run the app.</strong> The archetype sets a default Maven goal, so this compiles the app and starts it on port 8080:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">mvn</span><br></div></code></pre></div></div>
</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="build-a-spring-boot-ui">Build a Spring Boot UI with Cursor<a href="https://docs.webforj.com/zh/blog/cursor-spring-boot-ui#build-a-spring-boot-ui" class="hash-link" aria-label="直接链接到 Build a Spring Boot UI with Cursor" title="直接链接到 Build a Spring Boot UI with Cursor" translate="no">​</a></h2>
<p>This is the kind of view the assistant should hand back. A Spring service holds the logic:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockTitle_OeMC">OrderService.java</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Service</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">OrderService</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">summary</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> customer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"Open orders for "</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> customer </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">": 3"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>The view is a <code>@Route</code> class. When Spring is on the classpath, webforJ creates the route through Spring, so the constructor receives the bean:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockTitle_OeMC">OrdersView.java</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Route</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"/orders"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">OrdersView</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">extends</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Composite</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Div</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">OrdersView</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">OrderService</span><span class="token plain"> orders</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token plain"> customer </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Customer"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">Paragraph</span><span class="token plain"> summary </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Paragraph</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    customer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">onModify</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">e </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> summary</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">orders</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">summary</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">customer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getValue</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token function" style="color:var(--dwc-code-function)">getBoundComponent</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">customer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> summary</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>The route class needs no Spring annotation. The <code>@Route</code> annotation alone enables both route discovery and constructor injection. See <a class="" href="https://docs.webforj.com/zh/docs/integrations/spring/routing">Spring routing</a> for how webforJ creates a fresh instance on each navigation.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="check-with-a-test">Check the result with a test<a href="https://docs.webforj.com/zh/blog/cursor-spring-boot-ui#check-with-a-test" class="hash-link" aria-label="直接链接到 Check the result with a test" title="直接链接到 Check the result with a test" translate="no">​</a></h2>
<p>A generated view that compiles can still do the wrong thing. TestBed runs views in a plain JUnit test with no browser. In a Spring Boot app, <code>@SpringBootTest</code> starts the context and <code>@WebforjTest</code> registers the view. TestBed needs webforJ 26.03, and the Spring pieces are in the <a class="" href="https://docs.webforj.com/zh/docs/integrations/spring/testing">Spring testing guide</a>.</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockTitle_OeMC">OrdersViewTest.java</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@SpringBootTest</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@WebforjTest</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">routeClasses </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">OrdersView</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">OrdersViewTest</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Autowired</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">OrderService</span><span class="token plain"> orders</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Test</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">shouldShowTheSummaryFromTheService</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">WebforjTestbed</span><span class="token plain"> context</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    context</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">navigate</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">OrdersView</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">UserInteraction</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">type</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token class-name" style="color:var(--dwc-code-class)">ComponentLocator</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">of</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">byLabel</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Customer"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">locate</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"Ada"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token function" style="color:var(--dwc-code-function)">verifyThat</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">ComponentLocator</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">of</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Paragraph</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">locate</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">hasText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">orders</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">summary</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Ada"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>Ask the assistant to write this test with the view, then run <code>mvn test</code>. If it fails, paste the failure back into the chat. This write, test, fix loop is the one described in <a class="" href="https://docs.webforj.com/zh/blog/ai-verification-loop-java">the AI verification loop</a>. For more on tests without a browser, read <a class="" href="https://docs.webforj.com/zh/blog/java-ui-testing-without-browser">Java UI testing without a browser</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="prompts-that-work">Prompts that work in Cursor<a href="https://docs.webforj.com/zh/blog/cursor-spring-boot-ui#prompts-that-work" class="hash-link" aria-label="直接链接到 Prompts that work in Cursor" title="直接链接到 Prompts that work in Cursor" translate="no">​</a></h2>
<p>The <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/mcp">MCP server docs</a> give a few habits worth keeping:</p>
<ul>
<li class="">Mention <strong>webforJ</strong> in every prompt.</li>
<li class="">Be specific. A named project and archetype beat a vague request like make an app.</li>
<li class="">Ask for verification, such as verify against webforJ docs.</li>
<li class="">If the assistant answers without consulting the server, say use the webforJ MCP server to answer.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">Frequently asked questions<a href="https://docs.webforj.com/zh/blog/cursor-spring-boot-ui#faq" class="hash-link" aria-label="直接链接到 Frequently asked questions" title="直接链接到 Frequently asked questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-cursor">Does the webforJ AI plugin work with Cursor?<a href="https://docs.webforj.com/zh/blog/cursor-spring-boot-ui#faq-cursor" class="hash-link" aria-label="直接链接到 Does the webforJ AI plugin work with Cursor?" title="直接链接到 Does the webforJ AI plugin work with Cursor?" translate="no">​</a></h3>
<p>Yes. Cursor is one of the clients that support the plugin and the Agent Skills. Cursor uses manual configuration, and the exact steps are in the <a href="https://github.com/webforj/webforj-ai#clients" target="_blank" rel="noopener noreferrer" class="">per-client install guide</a>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-no-javascript">Can Cursor build a Java web app without JavaScript?<a href="https://docs.webforj.com/zh/blog/cursor-spring-boot-ui#faq-no-javascript" class="hash-link" aria-label="直接链接到 Can Cursor build a Java web app without JavaScript?" title="直接链接到 Can Cursor build a Java web app without JavaScript?" translate="no">​</a></h3>
<p>Yes. The views are Java classes, so the assistant writes them in the same language as your services and you review them the same way.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-spring-services">Can I use my existing Spring services?<a href="https://docs.webforj.com/zh/blog/cursor-spring-boot-ui#faq-spring-services" class="hash-link" aria-label="直接链接到 Can I use my existing Spring services?" title="直接链接到 Can I use my existing Spring services?" translate="no">​</a></h3>
<p>Yes. Any bean in the Spring context can be a constructor parameter of a <code>@Route</code> class, including services and Spring Data repositories.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ship-one-view">Ship one view with a passing test<a href="https://docs.webforj.com/zh/blog/cursor-spring-boot-ui#ship-one-view" class="hash-link" aria-label="直接链接到 Ship one view with a passing test" title="直接链接到 Ship one view with a passing test" translate="no">​</a></h2>
<ul>
<li class="">Project created with the <code>webforj-spring</code> flavor or startforJ with <strong>Spring Boot</strong>.</li>
<li class="">Cursor configured with the steps from the per-client install guide.</li>
<li class="">"webforJ" named in the prompt, with a request to verify against the docs.</li>
<li class="">View is a <code>@Route</code> class and takes Spring beans in its constructor.</li>
<li class="">Test added with <code>@SpringBootTest</code> and <code>@WebforjTest</code>, and <code>mvn test</code> passes.</li>
<li class="">Generated code read by a person before it ships.</li>
</ul><div></div>]]></content>
        <author>
            <name>Eric Handtke</name>
        </author>
        <category label="AI" term="AI"/>
        <category label="Spring" term="Spring"/>
        <category label="MCP" term="MCP"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The webforJ ecosystem: what a Java framework needs in the AI era]]></title>
        <id>https://docs.webforj.com/zh/blog/webforj-ecosystem</id>
        <link href="https://docs.webforj.com/zh/blog/webforj-ecosystem"/>
        <updated>2026-11-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How big is the webforJ ecosystem? Built-in components from one team, any web component or npm package, the Java ecosystem, and AI assistants that write current webforJ code.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-11-09-webforj-ecosystem/cover-v2.jpg" alt="cover image" class="img_ev3q"></p>
<p>When a Java team compares frameworks, one question comes up early: how big is the ecosystem? It used to be a fair proxy for safety. More add-ons, more examples and more forum answers meant less code to write yourself and fewer dead ends.</p>
<p>The webforJ ecosystem answers that question differently, because the work has changed. Your team now writes much of its code with an AI assistant, so what counts is whether the parts you need are maintained for the version you run, and whether your assistant writes current, correct code for it.</p>
<div class="cta_UErl" role="complementary" aria-label="Already running Spring Boot?"><div class="copy_XH8h"><p class="title_HfaR">Already running Spring Boot?</p><p class="text_x4Wj">Ask the team how webforJ fits into your existing Spring app.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="https://webforj.com/schedule-discovery/" target="_blank" rel="noopener noreferrer">预约通话<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a><a class="secondary_GDTR" href="mailto:sales@basis.cloud?subject=webforJ%20with%20Spring%20Boot&amp;body=Hi%20webforJ%20team%2C%0A%0A%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->邮件咨询</a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="maintained">Count what's maintained<a href="https://docs.webforj.com/zh/blog/webforj-ecosystem#maintained" class="hash-link" aria-label="直接链接到 Count what's maintained" title="直接链接到 Count what's maintained" translate="no">​</a></h2>
<p>A long list of third-party add-ons looks good on a comparison chart. In practice, a list like that ages. Add-ons are built by volunteers for one major version, the author moves on, and the entry stays. The list keeps growing while the share you can safely put in production shrinks, and every add-on you adopt becomes an upgrade you own.</p>
<p>webforJ takes the other route. The components a business app needs ship in the framework itself, built and released by one team: <a class="" href="https://docs.webforj.com/zh/docs/components/table/overview">tables</a>, <a class="" href="https://docs.webforj.com/zh/docs/components/fields/textfield">forms and fields</a>, <a class="" href="https://docs.webforj.com/zh/docs/components/dialog">dialogs</a>, <a class="" href="https://docs.webforj.com/zh/docs/components/app-layout">layouts</a>, <a class="" href="https://docs.webforj.com/zh/docs/components/tree">trees</a>, <a class="" href="https://docs.webforj.com/zh/docs/components/upload">uploads</a>, <a class="" href="https://docs.webforj.com/zh/docs/components/google-charts">charts</a>, and more. They follow the same release as the framework and come out together in each version on Maven Central. Upgrading the framework upgrades all of them.</p>
<p>Upgrades themselves are automated. The <a class="" href="https://docs.webforj.com/zh/docs/upgrading/automated-upgrades">webforJ OpenRewrite recipes</a> rename APIs, update dependencies and flag the spots that need a person, so moving to a new version is a command, not a project.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="web-components">Every web component is a webforJ component<a href="https://docs.webforj.com/zh/blog/webforj-ecosystem#web-components" class="hash-link" aria-label="直接链接到 Every web component is a webforJ component" title="直接链接到 Every web component is a webforJ component" translate="no">​</a></h2>
<p>The built-in set is where the ecosystem starts, not where it ends. webforJ's own components are built on web components, so any custom element from the wider web works the same way. With <a class="" href="https://docs.webforj.com/zh/docs/building-ui/element-composite"><code>ElementComposite</code></a> you expose its properties, attributes and events through a typed Java class and use it like any other component.</p>
<p>The <a class="" href="https://docs.webforj.com/zh/docs/managing-resources/bundler/overview">frontend bundler</a> goes further. A class names the npm package it needs, by name and version, and the build installs and compiles it. You can also write a component in React, Svelte or Lit and use it from Java, all without a Node toolchain to install or run.</p>
<p>That puts the whole npm and web component world behind a Java API. Our guide to <a class="" href="https://docs.webforj.com/zh/blog/web-components-in-java">web components in Java</a> walks through the full pattern.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="java-ecosystem">The Java ecosystem is yours<a href="https://docs.webforj.com/zh/blog/webforj-ecosystem#java-ecosystem" class="hash-link" aria-label="直接链接到 The Java ecosystem is yours" title="直接链接到 The Java ecosystem is yours" translate="no">​</a></h2>
<p>webforJ is a Java framework, so every library on Maven Central is part of its ecosystem. With <a class="" href="https://docs.webforj.com/zh/docs/integrations/spring/spring-boot">Spring Boot</a>, your views inject any Spring bean, tables read from <a class="" href="https://docs.webforj.com/zh/docs/integrations/spring/spring-data-jpa">Spring Data JPA</a> repositories, and routes are protected through <a class="" href="https://docs.webforj.com/zh/docs/security/overview">route security</a> with Spring Security. Long-running work runs as <a class="" href="https://docs.webforj.com/zh/docs/integrations/spring/background-jobs">background jobs</a> that update the UI when they finish.</p>
<p>Your team keeps the libraries, the build, and the tooling it already trusts. webforJ adds the screens.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ai-fluency">Can your AI assistant speak webforJ?<a href="https://docs.webforj.com/zh/blog/webforj-ecosystem#ai-fluency" class="hash-link" aria-label="直接链接到 Can your AI assistant speak webforJ?" title="直接链接到 Can your AI assistant speak webforJ?" translate="no">​</a></h2>
<p>In the AI era, the size of an ecosystem is measured less in add-ons and more in how well an assistant writes code for it. The amount of old example code on the internet doesn't settle that. An assistant trained on years of posts mixes versions, and the more outdated code there is, the more often it suggests an API that's gone.</p>
<p>What makes an assistant fluent is current, version-aware knowledge and a way to check its work. webforJ ships both, in one install of the <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling">webforJ AI plugin</a> for Claude Code, Copilot, Cursor, Gemini, and Codex.</p>
<ul>
<li class=""><strong>The MCP server</strong> gives the assistant live access to the webforJ knowledge base. It reads your <code>pom.xml</code>, resolves the webforJ version you're on and scopes every answer to it. It also validates design tokens before they land in your CSS. See <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/mcp">the MCP server</a>.</li>
<li class=""><strong>Agent Skills</strong> teach the assistant the webforJ way to do a task, from forms with binding and validation to route security, translation, timers and async work, styling, servlets, custom components and version upgrades. See <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/agent-skills">Agent Skills</a>.</li>
<li class=""><strong>Browserless tests</strong> let the assistant run a view in a plain JUnit test with the <a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/overview">TestBed</a> and get a clear pass or fail.</li>
<li class=""><strong>The craftforJ Assistant</strong> works inside your running app. It writes Java, compiles each edit before you see it and applies it. See <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/craftforj-assistant">the craftforJ Assistant</a>.</li>
</ul>
<p>So the assistant doesn't rely on what it happened to read during training. It looks up the API for your version, follows the workflow, and checks the result. We cover that loop in <a class="" href="https://docs.webforj.com/zh/blog/ai-verification-loop-java">the AI verification loop for Java</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="checklist">What to ask of any ecosystem<a href="https://docs.webforj.com/zh/blog/webforj-ecosystem#checklist" class="hash-link" aria-label="直接链接到 What to ask of any ecosystem" title="直接链接到 What to ask of any ecosystem" translate="no">​</a></h2>
<p>When you weigh a framework for your next business app, these questions say more than the length of an add-on list:</p>
<ol>
<li class="">Are the components you need maintained by the same team that ships the framework?</li>
<li class="">Can you use any web component or npm package without waiting for someone to wrap it?</li>
<li class="">Does it work with the Java libraries and Spring stack you already run?</li>
<li class="">Are upgrades automated?</li>
<li class="">Does your AI assistant get version-correct answers and a way to test its own code?</li>
</ol>
<p>webforJ answers yes to all five. For the full picture of what the framework covers, read our guide to the <a class="" href="https://docs.webforj.com/zh/blog/java-full-stack-framework">full-stack Java framework</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">Frequently asked questions<a href="https://docs.webforj.com/zh/blog/webforj-ecosystem#faq" class="hash-link" aria-label="直接链接到 Frequently asked questions" title="直接链接到 Frequently asked questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-big-enough">Is the webforJ ecosystem big enough for business apps?<a href="https://docs.webforj.com/zh/blog/webforj-ecosystem#faq-big-enough" class="hash-link" aria-label="直接链接到 Is the webforJ ecosystem big enough for business apps?" title="直接链接到 Is the webforJ ecosystem big enough for business apps?" translate="no">​</a></h3>
<p>Yes. The components business apps need ship in the framework, and one team maintains and releases them. Any web component or npm package connects through a typed Java class, and the whole Java ecosystem, Spring included, is available.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-ai-know">Do AI assistants know webforJ?<a href="https://docs.webforj.com/zh/blog/webforj-ecosystem#faq-ai-know" class="hash-link" aria-label="直接链接到 Do AI assistants know webforJ?" title="直接链接到 Do AI assistants know webforJ?" translate="no">​</a></h3>
<p>With the webforJ AI plugin, yes. The MCP server gives the assistant live, version-scoped access to the webforJ knowledge base, Agent Skills teach it the workflows, and the TestBed lets it test what it wrote.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-missing-component">What if I need a component webforJ doesn't have?<a href="https://docs.webforj.com/zh/blog/webforj-ecosystem#faq-missing-component" class="hash-link" aria-label="直接链接到 What if I need a component webforJ doesn't have?" title="直接链接到 What if I need a component webforJ doesn't have?" translate="no">​</a></h3>
<p>Wrap an existing web component with <code>ElementComposite</code>, or pull in an npm package or a React, Svelte or Lit component through the frontend bundler. The webforJ AI plugin includes a skill for exactly this task.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="start">Build your first screen with AI<a href="https://docs.webforj.com/zh/blog/webforj-ecosystem#start" class="hash-link" aria-label="直接链接到 Build your first screen with AI" title="直接链接到 Build your first screen with AI" translate="no">​</a></h2>
<p>Install the <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling">webforJ AI plugin</a>, generate a project with <a href="https://docs.webforj.com/startforj" target="_blank" rel="noopener noreferrer" class="">startforJ</a>, and ask your assistant to build one real screen of your app. Then run its test.</p><div></div>]]></content>
        <author>
            <name>webforJ</name>
        </author>
        <category label="Insights" term="Insights"/>
        <category label="Spring" term="Spring"/>
        <category label="JavaScript" term="JavaScript"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Modernize a Java Swing app without JavaScript: JavaOne 2026]]></title>
        <id>https://docs.webforj.com/zh/blog/modernize-java-ui-without-javascript-talk</id>
        <link href="https://docs.webforj.com/zh/blog/modernize-java-ui-without-javascript-talk"/>
        <updated>2026-11-08T12:00:00.000Z</updated>
        <summary type="html"><![CDATA[Watch how to modernize a Java Swing app without JavaScript: run it in the browser with Webswing, then build new screens in Java with webforJ, step by step.]]></summary>
        <content type="html"><![CDATA[
<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-11-08-modernize-java-ui-without-javascript-talk/cover.jpg" alt="cover image" class="img_ev3q"></p>
<p>Your Swing app works, your users know it, and the request on the table is to make it run in a browser. The usual answer is a rewrite with a JavaScript framework, a new API layer, and a second team. At JavaOne 2026, Stephan Wald, our Head of Development at BASIS International, showed how to modernize a Java Swing app without JavaScript instead: put the existing app in the browser first, then build everything that comes next in Java with webforJ, the full-stack Java framework.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;margin-bottom:1.5rem"><iframe src="https://www.youtube.com/embed/P2W54sxjqgY" title="Modernizing Java UIs Without JavaScript: Escape the Multi-Stack Trap" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0" allowfullscreen=""></iframe></div>
<div class="cta_UErl" role="complementary" aria-label="Moving a desktop Java app to the web?"><div class="copy_XH8h"><p class="title_HfaR">Moving a desktop Java app to the web?</p><p class="text_x4Wj">Send us a screenshot of one screen and we'll reply with how it moves to webforJ.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="mailto:sales@basis.cloud?subject=My%20app%20screen%3A%20how%20would%20it%20move%20to%20webforJ%3F&amp;body=Hi%20webforJ%20team%2C%0A%0AAttached%20is%20a%20screenshot%20of%20one%20screen%20from%20our%20app.%20It%20is%20built%20with%3A%20(Swing%2C%20JavaFX%2C%20other)%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->发送截图</a><a class="secondary_GDTR" href="https://webforj.com/schedule-discovery/" target="_blank" rel="noopener noreferrer">预约通话<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-the-talk-covers">What the talk covers<a href="https://docs.webforj.com/zh/blog/modernize-java-ui-without-javascript-talk#what-the-talk-covers" class="hash-link" aria-label="直接链接到 What the talk covers" title="直接链接到 What the talk covers" translate="no">​</a></h2>
<ul>
<li class=""><strong><a href="https://www.youtube.com/watch?v=P2W54sxjqgY&amp;t=0s" target="_blank" rel="noopener noreferrer" class="">00:00</a></strong> Why business apps move to the browser</li>
<li class=""><strong><a href="https://www.youtube.com/watch?v=P2W54sxjqgY&amp;t=113s" target="_blank" rel="noopener noreferrer" class="">01:53</a></strong> The multi-stack trap: an API layer plus a JavaScript rewrite</li>
<li class=""><strong><a href="https://www.youtube.com/watch?v=P2W54sxjqgY&amp;t=259s" target="_blank" rel="noopener noreferrer" class="">04:19</a></strong> Why big rewrites stall</li>
<li class=""><strong><a href="https://www.youtube.com/watch?v=P2W54sxjqgY&amp;t=486s" target="_blank" rel="noopener noreferrer" class="">08:06</a></strong> Keep both sides in Java</li>
<li class=""><strong><a href="https://www.youtube.com/watch?v=P2W54sxjqgY&amp;t=630s" target="_blank" rel="noopener noreferrer" class="">10:30</a></strong> Step one: run the Swing app in the browser with Webswing</li>
<li class=""><strong><a href="https://www.youtube.com/watch?v=P2W54sxjqgY&amp;t=1031s" target="_blank" rel="noopener noreferrer" class="">17:11</a></strong> Step two: the Webswing connector in webforJ</li>
<li class=""><strong><a href="https://www.youtube.com/watch?v=P2W54sxjqgY&amp;t=1240s" target="_blank" rel="noopener noreferrer" class="">20:40</a></strong> What webforJ is, and how it maps to Swing</li>
<li class=""><strong><a href="https://www.youtube.com/watch?v=P2W54sxjqgY&amp;t=1699s" target="_blank" rel="noopener noreferrer" class="">28:19</a></strong> A customer example</li>
<li class=""><strong><a href="https://www.youtube.com/watch?v=P2W54sxjqgY&amp;t=2046s" target="_blank" rel="noopener noreferrer" class="">34:06</a></strong> Start a webforJ app from scratch, live</li>
<li class=""><strong><a href="https://www.youtube.com/watch?v=P2W54sxjqgY&amp;t=2233s" target="_blank" rel="noopener noreferrer" class="">37:13</a></strong> Questions from the audience</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-the-browser">Why the browser, and why now<a href="https://docs.webforj.com/zh/blog/modernize-java-ui-without-javascript-talk#why-the-browser" class="hash-link" aria-label="直接链接到 Why the browser, and why now" title="直接链接到 Why the browser, and why now" translate="no">​</a></h2>
<p>The talk opens with a show of hands. Hands go up for Swing, someone in the room runs Jakarta Faces, and every one of those teams hears the same request. The app should run in a browser.</p>
<p>Part of that is expectation, because the browser is where users do everything else. Part of it is cost. A desktop client needs a Java runtime on every machine, and many companies add remote desktop servers or terminal services on top so people can reach the app from anywhere. Each of those layers adds cost, infrastructure, and administration. A browser app removes the client install entirely.</p>
<p>The point Stephan makes early is worth keeping in mind for the whole talk: users do not dislike the app because it is Java. They dislike reaching it through a desktop client in a world that runs in a browser. The problem is the delivery, not the code.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-multi-stack-trap">The multi-stack trap<a href="https://docs.webforj.com/zh/blog/modernize-java-ui-without-javascript-talk#the-multi-stack-trap" class="hash-link" aria-label="直接链接到 The multi-stack trap" title="直接链接到 The multi-stack trap" translate="no">​</a></h2>
<p>The reflex is to treat the browser as a reason to switch languages. The business logic gets wrapped in REST endpoints it was never designed for, the screens get rewritten in JavaScript, and the company now hires and coordinates two teams.</p>
<p>A well-built Swing app often has a clean split between model and view already. That split still does not map onto a REST contract, because an API has to draw its lines in different places. So a team with battle-tested logic ends up rewriting large parts of it just to make it reachable from a JavaScript front end. In Stephan's words, rewriting is the wrong default.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-rewrites-stall">Why big rewrites stall<a href="https://docs.webforj.com/zh/blog/modernize-java-ui-without-javascript-talk#why-rewrites-stall" class="hash-link" aria-label="直接链接到 Why big rewrites stall" title="直接链接到 Why big rewrites stall" translate="no">​</a></h2>
<p>The talk names the pattern most teams recognize:</p>
<ul>
<li class=""><strong>The second-version problem.</strong> A rewrite becomes the chance to add everything that was never built, and the scope keeps growing until the project never ends.</li>
<li class=""><strong>Knowledge walks out the door.</strong> The people who wrote the original logic leave over time, and the rules buried in that code get harder to recover with every month the rewrite runs.</li>
<li class=""><strong>Two stacks to staff.</strong> Front-end and back-end specialists each own half of every feature, and every change crosses a contract between them.</li>
</ul>
<p>The alternative is simple. Keep both sides in Java, and let the framework handle the browser.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-one-webswing">Step one: the app in the browser<a href="https://docs.webforj.com/zh/blog/modernize-java-ui-without-javascript-talk#step-one-webswing" class="hash-link" aria-label="直接链接到 Step one: the app in the browser" title="直接链接到 Step one: the app in the browser" translate="no">​</a></h2>
<p><a class="" href="https://docs.webforj.com/zh/docs/integrations/webswing/overview">Webswing</a> runs a Java desktop app (Swing, JavaFX, or SWT) in a web browser without changes to its source code. It renders the app on the server and streams the interface to the browser, so users reach the same screens through a URL instead of an installed client or a remote desktop session.</p>
<p>The live demo makes the point quickly. Stephan configures a stock Swing app in the Webswing admin console and opens it in the browser. It runs with its familiar look and responds quickly. He then opens an Eclipse RCP demo the same way, with configuration only.</p>
<p>That is the fastest answer to the browser request. Configure the app, deploy it, and your users open it in a browser. It also buys the time to modernize properly instead of under pressure.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-two-webforj">Step two: new screens in webforJ<a href="https://docs.webforj.com/zh/blog/modernize-java-ui-without-javascript-talk#step-two-webforj" class="hash-link" aria-label="直接链接到 Step two: new screens in webforJ" title="直接链接到 Step two: new screens in webforJ" translate="no">​</a></h2>
<p>Webswing keeps the existing UI as it is, including its fixed desktop layouts. The real modernization starts when new work moves to webforJ, and it can start right away. The <a class="" href="https://docs.webforj.com/zh/docs/integrations/webswing/setup">Webswing connector</a> embeds the Webswing-hosted app inside a webforJ view, so old and new screens live in one modern UI:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Route</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"/"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">CustomerTableView</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">extends</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Composite</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">FlexLayout</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">FlexLayout</span><span class="token plain"> self </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">getBoundComponent</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">CustomerTableView</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Value</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"${webswing.connector.url}"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> webswingUrl</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">WebswingConnector</span><span class="token plain"> connector </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">WebswingConnector</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">webswingUrl</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    connector</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setSize</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"100vw"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"100vh"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    self</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">connector</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>The connector also opens a channel between the two sides, so the Swing app and the webforJ view can exchange events. The <a class="" href="https://docs.webforj.com/zh/docs/integrations/webswing/tutorial">Webswing tutorial</a> builds a full example, and the <a class="" href="https://docs.webforj.com/zh/docs/integrations/webswing/communication">communication guide</a> covers events in both directions. From there you replace modules one at a time, starting wherever the business needs change first, while every new initiative is built in webforJ from day one.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-it-feels-familiar">Why webforJ feels familiar<a href="https://docs.webforj.com/zh/blog/modernize-java-ui-without-javascript-talk#why-it-feels-familiar" class="hash-link" aria-label="直接链接到 Why webforJ feels familiar" title="直接链接到 Why webforJ feels familiar" translate="no">​</a></h2>
<p>This is where webforJ makes the case for itself. For a Swing developer, the move is short. A <code>JButton</code> with an action listener becomes a webforJ <code>Button</code> with a click listener:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token plain"> save </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Save"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">save</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">onClick</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">event </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token comment" style="color:var(--dwc-code-comment);font-style:italic">// Handle the click</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><br></div></code></pre></div></div>
<p>The code runs on the server, where your business logic already lives. The browser renders the components through a thin client and sends events back to your Java listeners. That listener can call a Spring service or any plain Java class, because a webforJ app is a Java program.</p>
<p>Two things follow that are much harder to get with a separate JavaScript front end. First, debugging stays in one place: set a breakpoint in the listener, click the button in the browser, and your IDE stops on the line, with every variable in reach (<a class="" href="https://docs.webforj.com/zh/docs/configuration/debugging">debugging</a>). Second, the screen needs no CSS or JavaScript to work at all. The Java code is the whole screen.</p>
<p>What changes is the layout, and it changes for the better. New screens can use responsive layouts, drawers, dialogs, and accordions that a fixed Swing window never had. And because webforJ integrates with Spring Boot and ships its own AI tooling, the new code joins the modern Java ecosystem instead of a parallel one.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="customer-example">A customer example<a href="https://docs.webforj.com/zh/blog/modernize-java-ui-without-javascript-talk#customer-example" class="hash-link" aria-label="直接链接到 A customer example" title="直接链接到 A customer example" translate="no">​</a></h2>
<p>The talk backs the approach with Prodin, one of our customers, and its published <a href="https://webforj.com/wp-content/uploads/2024/10/BASIS_SuccessStory_Prodin.pdf" target="_blank" rel="noopener noreferrer" class="">success story</a>. Prodin assembled a six-member Java development team, including junior and mid-level developers, to assist in the migration. Within four months, Prodin delivered their first web-based modules to customers.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="one-stack">One stack for the long run<a href="https://docs.webforj.com/zh/blog/modernize-java-ui-without-javascript-talk#one-stack" class="hash-link" aria-label="直接链接到 One stack for the long run" title="直接链接到 One stack for the long run" translate="no">​</a></h2>
<p>The closing argument is about the next ten years, not the next release. Every framework a team adds is one more thing to maintain long after the people who chose it have moved on.</p>
<p>A single Java stack is the answer webforJ gives. The language, the debugger, the build, the tests, and the people stay the same from the database to the browser. That is what helps a business app stay maintainable at scale. Another talk builds <a class="" href="https://docs.webforj.com/zh/blog/java-installable-web-app-talk">an installable Java web app in 15 minutes</a>, all in Java.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="monday-morning-plan">A plan for next week<a href="https://docs.webforj.com/zh/blog/modernize-java-ui-without-javascript-talk#monday-morning-plan" class="hash-link" aria-label="直接链接到 A plan for next week" title="直接链接到 A plan for next week" translate="no">​</a></h2>
<p>The talk ends with a plan you can start next week:</p>
<ol>
<li class="">Pick your most urgent legacy app and run it in the browser with Webswing on your machine.</li>
<li class="">Create a webforJ project. It takes about a minute with <a href="https://docs.webforj.com/startforj" target="_blank" rel="noopener noreferrer" class="">startforJ</a> or a <a class="" href="https://docs.webforj.com/zh/docs/building-ui/archetypes/overview">webforJ archetype</a>.</li>
<li class="">Follow the <a class="" href="https://docs.webforj.com/zh/docs/integrations/webswing/tutorial">Webswing tutorial</a> to connect the old app and a webforJ view, and try it out.</li>
</ol>
<p>From there, build the next new screen in webforJ instead of in Swing.</p>
<p>The live coding at the end does exactly step two. Stephan creates a project from the SideMenu archetype in his IDE, runs it on Jetty, and opens a working multi-view app in the browser, all in plain Java. A webinar covers how to <a class="" href="https://docs.webforj.com/zh/blog/move-java-app-to-web-and-mobile">move a Java app to the web and mobile</a> with the same Webswing-first path.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="written-roadmap">Read the written roadmap<a href="https://docs.webforj.com/zh/blog/modernize-java-ui-without-javascript-talk#written-roadmap" class="hash-link" aria-label="直接链接到 Read the written roadmap" title="直接链接到 Read the written roadmap" translate="no">​</a></h2>
<p>If you prefer reading, <a class="" href="https://docs.webforj.com/zh/blog/webswing-and-webforj">Webswing and webforJ: a modernization roadmap</a> covers the same path in writing, with the setup details for each step.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">Frequently asked questions<a href="https://docs.webforj.com/zh/blog/modernize-java-ui-without-javascript-talk#faq" class="hash-link" aria-label="直接链接到 Frequently asked questions" title="直接链接到 Frequently asked questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-without-javascript">Can I modernize a Java Swing app without JavaScript?<a href="https://docs.webforj.com/zh/blog/modernize-java-ui-without-javascript-talk#faq-without-javascript" class="hash-link" aria-label="直接链接到 Can I modernize a Java Swing app without JavaScript?" title="直接链接到 Can I modernize a Java Swing app without JavaScript?" translate="no">​</a></h3>
<p>Yes. Webswing puts the existing Swing app in the browser without source changes, and webforJ lets you build every new screen in Java. Your team keeps working in one language from the database to the browser.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-rewrite-first">Do I have to rewrite the Swing app first?<a href="https://docs.webforj.com/zh/blog/modernize-java-ui-without-javascript-talk#faq-rewrite-first" class="hash-link" aria-label="直接链接到 Do I have to rewrite the Swing app first?" title="直接链接到 Do I have to rewrite the Swing app first?" translate="no">​</a></h3>
<p>No. The app runs in the browser through Webswing as it is. You then replace modules with webforJ screens at your own pace, while the Webswing connector keeps old and new screens in one UI.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-rcp-javafx">Does it work for other desktop toolkits?<a href="https://docs.webforj.com/zh/blog/modernize-java-ui-without-javascript-talk#faq-rcp-javafx" class="hash-link" aria-label="直接链接到 Does it work for other desktop toolkits?" title="直接链接到 Does it work for other desktop toolkits?" translate="no">​</a></h3>
<p>Webswing supports Swing, JavaFX, and SWT apps, and the talk shows an Eclipse RCP demo running in the browser. RCP apps that rely heavily on OSGi class loading can need extra configuration, so test yours early.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-where-talk">Where can I see the talk?<a href="https://docs.webforj.com/zh/blog/modernize-java-ui-without-javascript-talk#faq-where-talk" class="hash-link" aria-label="直接链接到 Where can I see the talk?" title="直接链接到 Where can I see the talk?" translate="no">​</a></h3>
<p>The full talk is embedded at the top of this page, and each timestamp in the list of topics opens the video on YouTube at that moment.</p><div></div>]]></content>
        <author>
            <name>webforJ</name>
        </author>
        <category label="Community" term="Community"/>
        <category label="Modernization" term="Modernization"/>
        <category label="Webswing" term="Webswing"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Java frontend frameworks: five ways to build the UI]]></title>
        <id>https://docs.webforj.com/zh/blog/java-frontend-frameworks</id>
        <link href="https://docs.webforj.com/zh/blog/java-frontend-frameworks"/>
        <updated>2026-11-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Compare Java frontend frameworks: Thymeleaf, Jakarta Faces, React or Angular, GWT-style compilers and webforJ, the full-stack Java framework in pure Java.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-11-08-java-frontend-frameworks/cover-v2.jpg" alt="cover image" class="img_ev3q"></p>
<p>You have a Java backend, a business app to ship, and a browser to put it in. Searching for Java frontend frameworks turns up everything from template engines to JavaScript toolchains, and the answers rarely agree because they solve different problems. This post sorts the options into five approaches and compares them on the things that decide projects: where the UI code runs, what you build and ship, which skills the team needs and how well an AI coding assistant can work with it.</p>
<p>For most Java teams building business apps, the best frontend framework for a Java backend is webforJ, the full-stack Java framework. The whole app, screens included, is Java. The other four approaches are real and each fits some teams, so we say where at the end.</p>
<div class="cta_UErl" role="complementary" aria-label="Working on something similar?"><div class="copy_XH8h"><p class="title_HfaR">Working on something similar?</p><p class="text_x4Wj">Ask the webforJ team. Questions are welcome, big or small.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="https://webforj.com/schedule-discovery/" target="_blank" rel="noopener noreferrer">预约通话<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a><a class="secondary_GDTR" href="mailto:sales@basis.cloud?subject=Question%20about%20webforJ&amp;body=Hi%20webforJ%20team%2C%0A%0A%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->邮件咨询</a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-five-approaches">The five approaches<a href="https://docs.webforj.com/zh/blog/java-frontend-frameworks#the-five-approaches" class="hash-link" aria-label="直接链接到 The five approaches" title="直接链接到 The five approaches" translate="no">​</a></h2>
<p>Every Java frontend choice falls into one of these shapes.</p>
<p><strong>Server-rendered templates.</strong> <a href="https://www.thymeleaf.org/" target="_blank" rel="noopener noreferrer" class="">Thymeleaf</a> is a server-side template engine. A Spring MVC controller fills a model and the browser receives a finished page, so interactivity beyond forms and links means adding JavaScript yourself.</p>
<p><strong>Component frameworks from the Java EE line.</strong> <a href="https://jakarta.ee/specifications/faces/" target="_blank" rel="noopener noreferrer" class="">Jakarta Faces</a>, formerly JSF, is the Jakarta EE specification for server-side component UIs, with XHTML views bound to managed beans.</p>
<p><strong>A JavaScript SPA on a Java backend.</strong> You build the UI with React, Angular or Vue as its own project and the Java side serves REST or GraphQL endpoints. It is powerful and popular, and it is a second discipline.</p>
<p><strong>Java compiled to JavaScript.</strong> <a href="https://www.gwtproject.org/" target="_blank" rel="noopener noreferrer" class="">GWT</a> compiles Java source to JavaScript, and tools such as J2CL and <a href="https://teavm.org/" target="_blank" rel="noopener noreferrer" class="">TeaVM</a> take the same idea further for Java code or bytecode. Your UI code is Java in the editor and JavaScript in the browser.</p>
<p><strong>A full-stack Java framework.</strong> The screens are Java classes that run on the server next to your services. The browser renders them as web components and sends events back to your Java handlers. That is webforJ. What sets webforJ apart is what comes after the screen: routing, forms, tables, security, translation, background updates and tests, all in one model.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-they-compare">How they compare<a href="https://docs.webforj.com/zh/blog/java-frontend-frameworks#how-they-compare" class="hash-link" aria-label="直接链接到 How they compare" title="直接链接到 How they compare" translate="no">​</a></h2>
<div class="table-wrapper"><div class="table-container" style="overflow-x:auto"><table><thead><tr><th></th><th>Templates</th><th>Faces</th><th>JS SPA</th><th>Java to JS</th><th>webforJ</th></tr></thead><tbody><tr><td>UI language</td><td>HTML plus expressions</td><td>XHTML plus beans</td><td>JavaScript or TypeScript</td><td>Java</td><td>Java</td></tr><tr><td>Where UI code runs</td><td>Server renders, browser shows</td><td>Server</td><td>Browser</td><td>Browser, after compiling</td><td>Server</td></tr><tr><td>Build tooling</td><td>Maven or Gradle</td><td>Maven or Gradle</td><td>Maven plus a Node toolchain</td><td>Maven plus a compiler step</td><td>Maven or Gradle</td></tr><tr><td>Backend contract</td><td>Controllers</td><td>Beans</td><td>REST or GraphQL</td><td>Remote calls</td><td>Direct service calls</td></tr><tr><td>Team skills</td><td>Java and HTML</td><td>Java and XHTML</td><td>Java and a frontend stack</td><td>Java, with compiler limits</td><td>Java</td></tr><tr><td>Rich components</td><td>Bring your own</td><td>Component libraries</td><td>Large ecosystem</td><td>Widget toolkit</td><td>Table, dialogs, layouts, forms ready, plus your own components and web components</td></tr><tr><td>AI-agent friendliness</td><td>Good for pages, two languages</td><td>Mixed</td><td>Two codebases to keep in sync</td><td>Java subset supported by the compiler</td><td>Skills, MCP and a compiler loop</td></tr></tbody></table></div><style data-emotion="css 1k371a6">@media print{.css-1k371a6{position:absolute!important;}}</style></div>
<p>The rows that decide projects are build tooling, team skills and the backend contract, so those get their own sections.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="second-language">What changes with a second language<a href="https://docs.webforj.com/zh/blog/java-frontend-frameworks#second-language" class="hash-link" aria-label="直接链接到 What changes with a second language" title="直接链接到 What changes with a second language" translate="no">​</a></h2>
<p>A JavaScript SPA is the most capable option and the most expensive for a Java team. It adds a package manager, a bundler, a test runner, a framework upgrade cycle and an API contract that both sides must keep compatible. Someone has to own all of it. If you already employ frontend specialists, that cost is already paid. If you do not, you are hiring or retraining before the first screen ships.</p>
<p>Template engines keep you in one build but limit you to what server-rendered pages do well. Compilers keep the language but put a translation step between your code and what runs, which means some Java library features and some debugging habits do not carry over.</p>
<p>With webforJ the same developers who write services write screens, in the same project, with the same debugger, and the same tests. You can still reach JavaScript when a specific library calls for it. <a class="" href="https://docs.webforj.com/zh/docs/building-ui/element-composite"><code>ElementComposite</code></a> wraps a web component in a typed Java class and <a class="" href="https://docs.webforj.com/zh/docs/building-ui/execute-javascript"><code>executeJs</code></a> runs script from Java, so JavaScript is a tool you choose rather than a layer you must staff.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-webforj">Why webforJ is the strongest pick<a href="https://docs.webforj.com/zh/blog/java-frontend-frameworks#why-webforj" class="hash-link" aria-label="直接链接到 Why webforJ is the strongest pick" title="直接链接到 Why webforJ is the strongest pick" translate="no">​</a></h2>
<p>This is the long answer to "which Java frontend framework", and it comes down to three things.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="screen-is-java">The screen is a Java class<a href="https://docs.webforj.com/zh/blog/java-frontend-frameworks#screen-is-java" class="hash-link" aria-label="直接链接到 The screen is a Java class" title="直接链接到 The screen is a Java class" translate="no">​</a></h3>
<p>A view is a component class bound to a URL. A view looks like this, as covered in <a class="" href="https://docs.webforj.com/zh/docs/routing/defining-routes">defining routes</a>:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Route</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">value </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"dashboard"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">DashboardView</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">extends</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Composite</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Div</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token comment" style="color:var(--dwc-code-comment);font-style:italic">// Component logic here</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>Routing, layouts and navigation are part of the framework. You compose the interface from Java components such as <code>Table</code>, <code>FlexLayout</code> and dialogs, and the whole screen is checked by the Java compiler, refactored by your IDE and covered by the same tools as the rest of your code.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="everything-included">Everything after the screen is included<a href="https://docs.webforj.com/zh/blog/java-frontend-frameworks#everything-included" class="hash-link" aria-label="直接链接到 Everything after the screen is included" title="直接链接到 Everything after the screen is included" translate="no">​</a></h3>
<p>Most Java frontend choices stop at rendering and leave the rest to a stack of libraries. webforJ covers the rest of the app too. <a class="" href="https://docs.webforj.com/zh/docs/data-binding/overview">Data binding</a> connects form fields to a Java model with Jakarta validation. The <a class="" href="https://docs.webforj.com/zh/docs/components/table/overview">Table</a> works with repositories and Spring Data. Route security plugs into Spring Security through annotations. The framework also provides translation, installable apps and <code>Environment.runLater</code> for pushing background updates to the browser. You read these in one set of docs instead of assembling them from separate projects. Our <a class="" href="https://docs.webforj.com/zh/blog/java-full-stack-framework">full-stack framework overview</a> goes through the whole chain.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="no-contract">No contract between screen and data<a href="https://docs.webforj.com/zh/blog/java-frontend-frameworks#no-contract" class="hash-link" aria-label="直接链接到 No contract between screen and data" title="直接链接到 No contract between screen and data" translate="no">​</a></h3>
<p>In an SPA every piece of data crosses a serialization boundary twice. In webforJ a screen calls your service method and gets a Java object back, because both live in the same JVM. With Spring Boot, services arrive by constructor injection, and the <a class="" href="https://docs.webforj.com/zh/docs/integrations/spring/overview">Spring integration</a> is a starter dependency. The framework also runs standalone when you have no Spring app. Business rules such as pricing and permission checks stay on the server and never reach the browser.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ai-assistants">AI coding assistants and your frontend<a href="https://docs.webforj.com/zh/blog/java-frontend-frameworks#ai-assistants" class="hash-link" aria-label="直接链接到 AI coding assistants and your frontend" title="直接链接到 AI coding assistants and your frontend" translate="no">​</a></h2>
<p>Assistants write all five approaches. The difference is how much stays in your hands to check afterwards. A SPA split means an assistant changes two codebases and a contract between them, and a mistake in either shows up at runtime. A compiler-based setup works within the Java subset its compiler supports.</p>
<p>webforJ gives the assistant its own tooling. The <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling">AI plugin</a> bundles the <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/mcp">MCP server</a>, which looks up real classes and methods instead of guessing them, with <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/agent-skills">Agent Skills</a> that carry webforJ workflows for forms, styling, security, and more. The <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/craftforj-assistant">craftforJ Assistant</a> sends each edit to a real Java compiler and corrects it against the diagnostics, so the code you review already compiles. Because the screen is plain Java, <a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/overview">TestBed</a> can test it in JUnit with no browser. We cover the full loop in <a class="" href="https://docs.webforj.com/zh/blog/java-framework-for-ai-coding">Java framework for AI coding</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="when-another-fits">When another option fits better<a href="https://docs.webforj.com/zh/blog/java-frontend-frameworks#when-another-fits" class="hash-link" aria-label="直接链接到 When another option fits better" title="直接链接到 When another option fits better" translate="no">​</a></h2>
<p>We want webforJ to win on merit, so here is where it does not.</p>
<ul>
<li class=""><strong>Public, content-heavy sites with strict SEO needs.</strong> Server-rendered templates produce plain HTML pages, which suits marketing sites and documentation. webforJ targets business apps where the user logs in.</li>
<li class=""><strong>A team that already runs React or Angular well.</strong> If frontend specialists are on staff and the product lives or dies by a bespoke browser experience, a SPA is a fair choice. You can still keep the Java services. And it is not all or nothing: the webforJ <a class="" href="https://docs.webforj.com/zh/docs/managing-resources/bundler/overview">frontend bundler</a> lets you write components in React and use them from Java views, and pull UI libraries from npm, with no Node toolchain to install.</li>
<li class=""><strong>An existing Faces app that works.</strong> Rewriting a stable app for its own sake rarely pays. Move new screens when the business asks for them.</li>
<li class=""><strong>Offline-first or heavily client-side apps.</strong> When most logic must run in the browser without a server connection, a JavaScript framework or a compiler approach is the natural tool.</li>
</ul>
<p>For the rest, which is internal tools, admin consoles, customer portals, line-of-business forms and data-heavy screens built by Java developers, one language and one project is hard to beat. A <a class="" href="https://docs.webforj.com/zh/blog/java-front-end-development">decision guide to Java for front end development</a> goes further.</p>
<p>Our neighboring comparisons cover these angles in more depth. The <a class="" href="https://docs.webforj.com/zh/blog/best-java-ui-frameworks">best Java UI framework for business apps</a> post goes through Spring MVC, Faces and SPAs with their own trade-offs, and <a class="" href="https://docs.webforj.com/zh/blog/java-web-frameworks-2026">the best Java web framework by job</a> separates the backend choice from the screen choice. If a compiler approach is what you are weighing, see the <a class="" href="https://docs.webforj.com/zh/blog/java-to-javascript-compiler-alternative">Java to JavaScript compiler alternative</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="build-one-screen">Build one screen in Java<a href="https://docs.webforj.com/zh/blog/java-frontend-frameworks#build-one-screen" class="hash-link" aria-label="直接链接到 Build one screen in Java" title="直接链接到 Build one screen in Java" translate="no">​</a></h2>
<p>The fastest way to compare is to build one real screen. Generate a project with <a href="https://docs.webforj.com/startforj" target="_blank" rel="noopener noreferrer" class="">startforJ</a>, follow the <a class="" href="https://docs.webforj.com/zh/docs/introduction/getting-started">getting started guide</a>, and rebuild the form or table that is hardest to maintain today. If it feels natural to your team, the next screen will too.</p>
<p>Once you have picked an approach, <a class="" href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications">Java framework for web applications</a> lists what to require from it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">Frequently asked questions<a href="https://docs.webforj.com/zh/blog/java-frontend-frameworks#faq" class="hash-link" aria-label="直接链接到 Frequently asked questions" title="直接链接到 Frequently asked questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-best">What is the best frontend framework for a Java backend?<a href="https://docs.webforj.com/zh/blog/java-frontend-frameworks#faq-best" class="hash-link" aria-label="直接链接到 What is the best frontend framework for a Java backend?" title="直接链接到 What is the best frontend framework for a Java backend?" translate="no">​</a></h3>
<p>For business apps built by Java developers, webforJ is the best fit because the screens, routing, forms, tables and security are all Java in one project that integrates with Spring Boot. A React or Angular SPA fits teams that already have frontend specialists, and Thymeleaf fits server-rendered content pages.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-no-javascript">Are there Java front end tools that need no JavaScript?<a href="https://docs.webforj.com/zh/blog/java-frontend-frameworks#faq-no-javascript" class="hash-link" aria-label="直接链接到 Are there Java front end tools that need no JavaScript?" title="直接链接到 Are there Java front end tools that need no JavaScript?" translate="no">​</a></h3>
<p>Yes. Thymeleaf and Jakarta Faces keep the pages in Java and XHTML on the server, and webforJ builds the whole interface from Java components. In webforJ you can still call JavaScript through <code>executeJs</code> or wrap a web component with <code>ElementComposite</code> when a project needs it.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-compiler">Is Java to JavaScript compilation still a good way to build web UIs?<a href="https://docs.webforj.com/zh/blog/java-frontend-frameworks#faq-compiler" class="hash-link" aria-label="直接链接到 Is Java to JavaScript compilation still a good way to build web UIs?" title="直接链接到 Is Java to JavaScript compilation still a good way to build web UIs?" translate="no">​</a></h3>
<p>It works when most of the logic must run in the browser. The cost is a build step between the code you write and the code that runs, plus limits on which Java libraries compile. Teams building server-backed business apps usually avoid that step by keeping the UI code on the server.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-ai">Which Java web UI frameworks work well with AI coding assistants?<a href="https://docs.webforj.com/zh/blog/java-frontend-frameworks#faq-ai" class="hash-link" aria-label="直接链接到 Which Java web UI frameworks work well with AI coding assistants?" title="直接链接到 Which Java web UI frameworks work well with AI coding assistants?" translate="no">​</a></h3>
<p>Assistants can work with all of them, so the question is how much they can check. webforJ ships an MCP server, Agent Skills and a compiler feedback loop through the craftforJ Assistant, which lets an assistant look up real APIs and see compile errors before you review. Setups split across two languages leave more to verify by hand.</p><div></div>]]></content>
        <author>
            <name>webforJ</name>
        </author>
        <category label="Insights" term="Insights"/>
        <category label="Full-Stack" term="Full-Stack"/>
        <category label="Front End" term="Front End"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Claude Code Java Web App: Build the Whole Thing, Screens Included]]></title>
        <id>https://docs.webforj.com/zh/blog/claude-code-java-web-app</id>
        <link href="https://docs.webforj.com/zh/blog/claude-code-java-web-app"/>
        <updated>2026-11-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Build a Claude Code Java web app end to end. One plugin gives Claude Code the real webforJ API, a scaffolder, and TestBed tests that pass or fail without a browser.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-11-07-claude-code-java-web-app/cover.jpg" alt="cover image" class="img_ev3q"></p>
<p>You ask your coding agent for a Java screen and get back something that looks right. Then you build it. A method does not exist, a constructor takes other arguments, and the button you asked for has never been clicked by anything. The agent is fast. What you lack is a way to know the result is correct before you read every line.</p>
<p>A Claude Code Java web app does not have to work that way. In webforJ, the full-stack Java framework for business web apps, Claude Code builds the whole app, screens included, and the work gets checked on three fronts: the real API, a scaffolded project, and a test that returns pass or fail without a browser.</p>
<div class="cta_UErl" role="complementary" aria-label="Using AI to build Java apps?"><div class="copy_XH8h"><p class="title_HfaR">Using AI to build Java apps?</p><p class="text_x4Wj">Ask the team about webforJ AI tooling: the MCP server, Agent Skills, and how they keep generated code correct.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="https://webforj.com/schedule-discovery/" target="_blank" rel="noopener noreferrer">预约通话<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a><a class="secondary_GDTR" href="mailto:sales@basis.cloud?subject=AI%20and%20webforJ&amp;body=Hi%20webforJ%20team%2C%0A%0A%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->邮件咨询</a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-it-breaks">Where agent-built Java apps break<a href="https://docs.webforj.com/zh/blog/claude-code-java-web-app#where-it-breaks" class="hash-link" aria-label="直接链接到 Where agent-built Java apps break" title="直接链接到 Where agent-built Java apps break" translate="no">​</a></h2>
<p>When a coding agent has nothing to look things up in, it writes from memory. Memory is a poor source for a framework with releases, and the failures are predictable:</p>
<ul>
<li class="">It calls an API that does not exist, or used to.</li>
<li class="">It follows generic Java habits instead of the conventions of the framework.</li>
<li class="">It produces code that compiles and does nothing useful when a user clicks.</li>
</ul>
<p>Each failure needs its own check. Telling the agent to be careful fixes none of them. webforJ closes each gap with a piece of tooling, and Claude Code can use all of it. The <a class="" href="https://docs.webforj.com/zh/blog/ai-verification-loop-java">verification loop</a> post covers the idea across tools. This post is the Claude Code walkthrough.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="for-java-developers">Claude Code for Java developers<a href="https://docs.webforj.com/zh/blog/claude-code-java-web-app#for-java-developers" class="hash-link" aria-label="直接链接到 Claude Code for Java developers" title="直接链接到 Claude Code for Java developers" translate="no">​</a></h2>
<p>Claude Code is Anthropic's coding agent that runs in your terminal and works inside your project. For a Java team that matters, because the agent edits the same <code>pom.xml</code> and the same source tree you do.</p>
<p>webforJ gives it three things:</p>
<ol>
<li class=""><strong>The real API.</strong> The <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/mcp">MCP server</a> searches the webforJ knowledge base, so the agent reads signatures instead of recalling them.</li>
<li class=""><strong>The right workflow.</strong> <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/agent-skills">Agent Skills</a> tell the agent how to approach forms, security, styling, timers, and more.</li>
<li class=""><strong>A pass or fail.</strong> The <a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/overview">TestBed</a> runs a view in a plain JUnit test, so the agent can prove its own work.</li>
</ol>
<p>The first two arrive in one install.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="install">Install the plugin<a href="https://docs.webforj.com/zh/blog/claude-code-java-web-app#install" class="hash-link" aria-label="直接链接到 Install the plugin" title="直接链接到 Install the plugin" translate="no">​</a></h2>
<p>The webforJ AI plugin bundles the MCP server and the Agent Skills. For Claude Code, run:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">claude plugin marketplace </span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token plain"> webforj/webforj-ai</span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">claude plugin </span><span class="token function" style="color:var(--dwc-code-function)">install</span><span class="token plain"> webforj@webforj-ai</span><br></div></code></pre></div></div>
<p>Then verify inside Claude Code:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">/plugin</span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">/mcp</span><br></div></code></pre></div></div>
<p>The <code>webforj</code> plugin appears under <strong>Installed</strong>, and the server shows up as <code>plugin:webforj:webforj-mcp</code> among the connected servers. The <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling">plugin page</a> lists the steps for other clients.</p>
<p>One habit helps: mention <strong>webforJ</strong> in your prompts. That is the cue that sends the agent to the plugin instead of general Java knowledge.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="build">Build a Java web app with Claude Code<a href="https://docs.webforj.com/zh/blog/claude-code-java-web-app#build" class="hash-link" aria-label="直接链接到 Build a Java web app with Claude Code" title="直接链接到 Build a Java web app with Claude Code" translate="no">​</a></h2>
<p>Start with the project. The MCP server can scaffold one from the official Maven archetypes, so you ask for it in plain words:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">Scaffold a new webforJ sidemenu project called CustomerPortal.</span><br></div></code></pre></div></div>
<p>The server returns the Maven archetype command, and the agent runs it. The archetypes are <code>hello-world</code>, <code>blank</code>, <code>tabs</code>, and <code>sidemenu</code>, and they come in two flavors: <code>webforj</code> and <code>webforj-spring</code>. You can create the same project yourself with <a href="https://docs.webforj.com/startforj" target="_blank" rel="noopener noreferrer" class="">startforJ</a> or the <a class="" href="https://docs.webforj.com/zh/docs/introduction/getting-started">command line</a>. What changes is that the project exists before the agent writes its first view. It already has the correct dependencies and a ready layout.</p>
<p>From there, you describe screens the way you would to a colleague:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">Add a webforJ greeting view at /greeting. A name field, a Greet button,</span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">and a paragraph that says Hello with the name.</span><br></div></code></pre></div></div>
<p>A view in webforJ is a Java class with a route. Nothing else is needed: no template, no JavaScript, and no REST layer between the screen and your code.</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockTitle_OeMC">GreetingView.java</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv codeBlockLinesWithNumbering_o6Pm" style="counter-reset:line-count 0"><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Route</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"greeting"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">GreetingView</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">extends</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Composite</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Div</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token plain"> name </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Name"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Paragraph</span><span class="token plain"> message </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Paragraph</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token plain"> greet </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">      </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Greet"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">ButtonTheme</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">PRIMARY</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> e </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">greet</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain" style="display:inline-block"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">GreetingView</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">    </span><span class="token function" style="color:var(--dwc-code-function)">getBoundComponent</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">name</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> greet</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> message</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain" style="display:inline-block"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">greet</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">    message</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Hello, "</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> name</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getValue</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"!"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span></span><br></div></code></pre></div></div>
<p>That is a complete screen. It is also the kind of code an agent gets right more often when it has looked up <code>Composite</code>, <code>Button</code>, and <code>@Route</code> first.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="check">Give the agent a way to check<a href="https://docs.webforj.com/zh/blog/claude-code-java-web-app#check" class="hash-link" aria-label="直接链接到 Give the agent a way to check" title="直接链接到 Give the agent a way to check" translate="no">​</a></h2>
<p>Reading the view tells you it looks right. A test tells you it works. The TestBed runs the view inside a JUnit test with no browser and no server, so the agent can run <code>mvn test</code> and read the result. Add the module with test scope:</p>
<div class="language-xml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockTitle_OeMC">pom.xml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-xml codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">dependency</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">groupId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">com.webforj</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">groupId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">artifactId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">webforj-testbed-junit</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">artifactId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">scope</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">test</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">scope</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">dependency</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><br></div></code></pre></div></div>
<p>The version comes from the webforJ BOM. Then ask for the test:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">Write a TestBed test for GreetingView that types a name, clicks Greet,</span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">and checks the paragraph.</span><br></div></code></pre></div></div>
<p>The test follows the steps of a user:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockTitle_OeMC">GreetingViewTest.java</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv codeBlockLinesWithNumbering_o6Pm" style="counter-reset:line-count 0"><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@WebforjTest</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">GreetingViewTest</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain" style="display:inline-block"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Test</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">shouldGreetTheNameTheUserTyped</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">WebforjTestbed</span><span class="token plain"> context</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">    context</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">GreetingView</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain" style="display:inline-block"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">var</span><span class="token plain"> name </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">ComponentLocator</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">of</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">byLabel</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Name"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">locate</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">var</span><span class="token plain"> greet </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">ComponentLocator</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">of</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">byText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Greet"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">locate</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain" style="display:inline-block"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">UserInteraction</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">type</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">name</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"Ada"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">UserInteraction</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">click</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">greet</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain" style="display:inline-block"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">    </span><span class="token function" style="color:var(--dwc-code-function)">verifyThat</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">ComponentLocator</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">of</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Paragraph</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">locate</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">hasText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Hello, Ada!"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span></span><br></div><div class="token-line codeLine_lJS_" style="color:var(--dwc-code-text)"><span class="codeLineNumber_Tfdd"></span><span class="codeLineContent_feaV"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span></span><br></div></code></pre></div></div>
<p><code>ComponentLocator</code> finds the field by its label and the button by its text. <code>UserInteraction</code> types and clicks, so the listener of the button really runs. <code>verifyThat</code> checks the text a user would read, and a failure names the component and both values. That last detail matters for an agent. A failure it can read is a failure it can fix.</p>
<p>Now the loop closes. The agent writes the view, writes the test, runs it, and reads a pass or a fail. If it fails, the message says what differed, and the agent edits and runs again. You review a result that already passed instead of a guess. The guides on <a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/finding-components">finding components</a> and <a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/assertions">assertions</a> show how far this goes, and the <a class="" href="https://docs.webforj.com/zh/blog/java-ui-testing-without-browser">TestBed guide</a> covers forms, dialogs, and routes.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="spring-boot">Claude Code Spring Boot UI<a href="https://docs.webforj.com/zh/blog/claude-code-java-web-app#spring-boot" class="hash-link" aria-label="直接链接到 Claude Code Spring Boot UI" title="直接链接到 Claude Code Spring Boot UI" translate="no">​</a></h2>
<p>If your backend is Spring Boot, nothing about the loop changes. Ask for the <code>webforj-spring</code> flavor when you scaffold, and the screens stay Java classes next to your beans and services. For tests, Spring Boot apps use the <code>webforj-spring-testbed</code> module, which works with <code>@SpringBootTest</code> and <code>@WebforjTest</code>, with injected beans and Spring Security on routes. The details are on the <a class="" href="https://docs.webforj.com/zh/docs/integrations/spring/testing">Spring testing</a> page.</p>
<p>So a Claude Code Spring Boot UI gets the same three checks as a standalone one. Standalone and Spring Boot are both supported, and your data can come from Spring Data or any other source. Using Cursor instead of Claude Code, <a class="" href="https://docs.webforj.com/zh/blog/cursor-spring-boot-ui">building a Spring Boot UI with Cursor</a> follows the same path.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="limits">What the loop still misses<a href="https://docs.webforj.com/zh/blog/claude-code-java-web-app#limits" class="hash-link" aria-label="直接�链接到 What the loop still misses" title="直接链接到 What the loop still misses" translate="no">​</a></h2>
<p>Be clear about the limits before you trust it.</p>
<ul>
<li class=""><strong>AI still makes mistakes.</strong> Even with the plugin, an agent can produce wrong code in complex cases. You review and test what it writes, which is why the test is part of the loop.</li>
<li class=""><strong>A browserless test is not a browser test.</strong> The TestBed checks what the view shows and how it reacts. If you need to see how a page renders in an actual browser, you still want an end-to-end test for that.</li>
<li class=""><strong>A test checks what you ask it to check.</strong> If the prompt does not describe a behavior, nothing asserts it. Describe the behavior you care about.</li>
</ul>
<p>Within those limits, the loop does what it should. The agent works from the real API, follows the framework's conventions, and cannot hand back a screen that fails its own test. The same limits shape <a class="" href="https://docs.webforj.com/zh/blog/vibe-coding-java-web-app">vibe coding without losing control</a> in a Java web app.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="one-screen">Ask for one screen with a test<a href="https://docs.webforj.com/zh/blog/claude-code-java-web-app#one-screen" class="hash-link" aria-label="直接链接到 Ask for one screen with a test" title="直接链接到 Ask for one screen with a test" translate="no">​</a></h2>
<p>Pick a screen you would otherwise build by hand this week. Install the plugin with the two commands from the install section, scaffold or open a webforJ project, and ask Claude Code for the view and its TestBed test in one prompt. Run <code>mvn test</code> and read the result. The <a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/getting-started">Getting started</a> page covers the dependency and the first test in more detail.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">FAQ<a href="https://docs.webforj.com/zh/blog/claude-code-java-web-app#faq" class="hash-link" aria-label="直接链接到 FAQ" title="直接链接到 FAQ" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-build">Can Claude Code build a Java web app?<a href="https://docs.webforj.com/zh/blog/claude-code-java-web-app#faq-build" class="hash-link" aria-label="直接链接到 Can Claude Code build a Java web app?" title="直接链接到 Can Claude Code build a Java web app?" translate="no">​</a></h3>
<p>Yes. With webforJ you describe the app, and Claude Code writes the project, the routes, the views, and the tests in Java. The webforJ AI plugin gives it the MCP server and the Agent Skills so it uses real API signatures.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-install">How do you install webforJ for Claude Code?<a href="https://docs.webforj.com/zh/blog/claude-code-java-web-app#faq-install" class="hash-link" aria-label="直接链接到 How do you install webforJ for Claude Code?" title="直接链接到 How do you install webforJ for Claude Code?" translate="no">​</a></h3>
<p>Run <code>claude plugin marketplace add webforj/webforj-ai</code>, then <code>claude plugin install webforj@webforj-ai</code>. Check it with <code>/plugin</code> and <code>/mcp</code> inside Claude Code.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-spring">Does it work with Spring Boot?<a href="https://docs.webforj.com/zh/blog/claude-code-java-web-app#faq-spring" class="hash-link" aria-label="直接链接到 Does it work with Spring Boot?" title="直接链接到 Does it work with Spring Boot?" translate="no">​</a></h3>
<p>Yes. Scaffold the <code>webforj-spring</code> flavor and use the <code>webforj-spring-testbed</code> module for tests. Standalone webforJ works the same way without Spring.</p><div></div>]]></content>
        <author>
            <name>Eric Handtke</name>
        </author>
        <category label="AI" term="AI"/>
        <category label="MCP" term="MCP"/>
        <category label="Full-Stack" term="Full-Stack"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Java framework for web applications: what to require]]></title>
        <id>https://docs.webforj.com/zh/blog/java-framework-for-web-applications</id>
        <link href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications"/>
        <updated>2026-11-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A Java framework for web applications needs routing, forms, security, data, testing and upgrades. See how webforJ, the full-stack Java framework, covers each.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-11-07-java-framework-for-web-applications/cover-v2.jpg" alt="cover image" class="img_ev3q"></p>
<p>Most lists of Java web frameworks compare names. The question that decides the project is different: what does the app need on the day it goes to production, and how many separate tools will you stitch together to get it? A Java framework for web applications has to answer that question, and the answer starts with a choice between two architectures.</p>
<p>For a business app, our answer is webforJ, the full-stack Java framework. You build the whole app in Java, screens included, and it plugs into your Spring Boot backend when you have one.</p>
<p>This guide is the pillar guide for the search phrase java framework for web applications. It lists what such a framework must provide, explains the two architectures you can choose from, shows what webforJ ships for each item, and says plainly when a backend framework with a JavaScript frontend is the better fit.</p>
<div class="cta_UErl" role="complementary" aria-label="Already running Spring Boot?"><div class="copy_XH8h"><p class="title_HfaR">Already running Spring Boot?</p><p class="text_x4Wj">Ask the team how webforJ fits into your existing Spring app.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="https://webforj.com/schedule-discovery/" target="_blank" rel="noopener noreferrer">预约通话<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a><a class="secondary_GDTR" href="mailto:sales@basis.cloud?subject=webforJ%20with%20Spring%20Boot&amp;body=Hi%20webforJ%20team%2C%0A%0A%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->邮件咨询</a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="two-architectures">The two architectures<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#two-architectures" class="hash-link" aria-label="直接链接到 The two architectures" title="直接链接到 The two architectures" translate="no">​</a></h2>
<p>Every Java web framework falls into one of two shapes.</p>
<p><strong>A backend framework plus a separate frontend.</strong> Spring Boot, Quarkus, Micronaut, Jakarta EE and Javalin handle HTTP, services and data in Java. The browser UI is a second app, usually React or Angular, that talks to the backend over REST or GraphQL. Two languages, two build chains, one API contract to keep in sync.</p>
<p><strong>One full-stack Java framework.</strong> Routing, components, forms, security and data access live in a single Java codebase. The same developer who writes the service writes the screen that uses it. There is no REST layer between them that exists only to feed your own UI.</p>
<p>Our <a class="" href="https://docs.webforj.com/zh/blog/java-web-frameworks-2026">comparison of Java web frameworks by job</a> covers the backend options one by one, and <a class="" href="https://docs.webforj.com/zh/blog/best-java-ui-frameworks">the best Java UI framework for business apps</a> ranks the UI side. This post is about the requirements that cut across both.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="requirements">What the framework must provide<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#requirements" class="hash-link" aria-label="直接链接到 What the framework must provide" title="直接链接到 What the framework must provide" translate="no">​</a></h2>
<p>Nine capabilities decide whether a framework carries a business app or leaves you assembling one. Here is what each means and how webforJ answers it. The decisions a team settles first on a new app are covered in <a class="" href="https://docs.webforj.com/zh/blog/starting-new-java-project">starting a new Java project</a>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="req-routing">Routing and navigation<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#req-routing" class="hash-link" aria-label="直接链接到 Routing and navigation" title="直接链接到 Routing and navigation" translate="no">​</a></h3>
<p>Require URLs that map to screens, deep links that survive a reload and nested layouts. In webforJ a view is a Java class mapped to a path with an annotation. See <a class="" href="https://docs.webforj.com/zh/docs/routing/overview">routing</a> and <a class="" href="https://docs.webforj.com/zh/docs/routing/defining-routes">defining routes</a>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="req-components">UI components<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#req-components" class="hash-link" aria-label="直接链接到 UI components" title="直接链接到 UI components" translate="no">​</a></h3>
<p>Require a table, a date picker and an app layout out of the box, or you are shopping for libraries on day two. webforJ ships a <a class="" href="https://docs.webforj.com/zh/docs/components/overview">component library</a> that includes <a class="" href="https://docs.webforj.com/zh/docs/components/app-layout">AppLayout</a>, all composed in Java.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="req-forms">Forms and validation<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#req-forms" class="hash-link" aria-label="直接链接到 Forms and validation" title="直接链接到 Forms and validation" translate="no">​</a></h3>
<p>Require field binding to a model and validation with a standard. webforJ uses <a class="" href="https://docs.webforj.com/zh/docs/data-binding/overview">data binding</a> with <a class="" href="https://docs.webforj.com/zh/docs/data-binding/validation/jakarta-validation">Jakarta Validation</a>, the same annotations your backend entities already carry.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="req-data">Data access<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#req-data" class="hash-link" aria-label="直接链接到 Data access" title="直接链接到 Data access" translate="no">​</a></h3>
<p>Require rows to come from a repository, not a hand-written endpoint. webforJ's <a class="" href="https://docs.webforj.com/zh/docs/advanced/repository/overview">repository pattern</a> feeds tables, and the <a class="" href="https://docs.webforj.com/zh/docs/integrations/spring/spring-data-jpa">Spring Data JPA integration</a> connects the repositories you already have.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="req-security">Security<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#req-security" class="hash-link" aria-label="直接链接到 Security" title="直接链接到 Security" translate="no">​</a></h3>
<p>Require access rules on routes, not only on buttons. webforJ attaches annotations to routes, so a view that a user may not open is never rendered, and it works with Spring Security. Route security is available since 25.10. Start at <a class="" href="https://docs.webforj.com/zh/docs/security/overview">security</a>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="req-testing">Testing<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#req-testing" class="hash-link" aria-label="直接链接到 Testing" title="直接链接到 Testing" translate="no">​</a></h3>
<p>Require fast tests that do not need a browser. webforJ's <a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/overview">TestBed</a>, available since 26.03, runs browserless JUnit tests against views, and end-to-end tests with Selenium or Playwright remain an option. See <a class="" href="https://docs.webforj.com/zh/docs/testing/overview">testing</a> and <a class="" href="https://docs.webforj.com/zh/blog/java-ui-testing-without-browser">Java UI testing without a browser</a>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="req-deployment">Deployment<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#req-deployment" class="hash-link" aria-label="直接链接到 Deployment" title="直接链接到 Deployment" translate="no">​</a></h3>
<p>Require one artifact to ship. A webforJ app is an executable JAR with Spring Boot or a WAR for a servlet container, with no frontend bundle on a second host. The <a class="" href="https://docs.webforj.com/zh/docs/integrations/spring/spring-boot">Spring Boot guide</a> and the <a class="" href="https://docs.webforj.com/zh/docs/configuration/deploy-reload/spring-devtools#production-builds">production builds</a> section cover it.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="req-upgrades">Upgrades<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#req-upgrades" class="hash-link" aria-label="直接链接到 Upgrades" title="直接链接到 Upgrades" translate="no">​</a></h3>
<p>Require a path for existing code when a major release lands. webforJ publishes <a class="" href="https://docs.webforj.com/zh/docs/upgrading/automated-upgrades">OpenRewrite recipes</a> that rename APIs, change types and update dependencies, and leave a <code>TODO</code> comment where no one-to-one replacement exists.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="req-ai">AI tooling<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#req-ai" class="hash-link" aria-label="直接链接到 AI tooling" title="直接链接到 AI tooling" translate="no">​</a></h3>
<p>Require an assistant that checks real APIs instead of guessing method names. The webforJ <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling">AI plugin</a> bundles an <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/mcp">MCP server</a> and <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/agent-skills">Agent Skills</a>, and the craftforJ Assistant checks every edit against a Java compiler. The full loop is in <a class="" href="https://docs.webforj.com/zh/blog/ai-verification-loop-java">the AI verification loop</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="checklist">The requirements at a glance<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#checklist" class="hash-link" aria-label="直接链接到 The requirements at a glance" title="直接链接到 The requirements at a glance" translate="no">​</a></h2>
<p>Use this table when you shortlist a framework. The right column is what webforJ provides.</p>
<div class="table-wrapper"><div class="table-container" style="overflow-x:auto"><table><thead><tr><th>Requirement</th><th>What to ask</th><th>webforJ</th></tr></thead><tbody><tr><td>Routing</td><td>URL-to-view mapping, nesting, deep links</td><td>Annotated routes, layouts</td></tr><tr><td>Components</td><td>Table, inputs, dialogs, app layout</td><td>Component library composed in Java</td></tr><tr><td>Forms</td><td>Binding and standard validation</td><td>Data binding, Jakarta Validation</td></tr><tr><td>Data access</td><td>Rows from repositories, not hand-built endpoints</td><td>Repository pattern, Spring Data JPA</td></tr><tr><td>Security</td><td>Rules on routes, Spring Security fit</td><td>Route annotations, Spring Security integration</td></tr><tr><td>Testing</td><td>Fast tests without a browser</td><td>TestBed, Bun frontend tests, Selenium or Playwright</td></tr><tr><td>Deployment</td><td>One artifact to ship</td><td>Executable JAR or WAR</td></tr><tr><td>Upgrades</td><td>A tool for major versions</td><td>OpenRewrite recipes</td></tr><tr><td>AI tooling</td><td>Assistant that checks real APIs</td><td>MCP server, Agent Skills, AI plugin</td></tr></tbody></table></div><style data-emotion="css 1k371a6">@media print{.css-1k371a6{position:absolute!important;}}</style></div>
<p>A backend-only framework answers the middle rows well and leaves the first three and the last to the second app you build next to it. That gap is the cost of the split architecture, and <a class="" href="https://docs.webforj.com/zh/blog/cost-separate-frontend-backend-teams">the cost of separate frontend and backend teams</a> is worth reading before you commit to it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="spring-boot">How it fits with Spring Boot<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#spring-boot" class="hash-link" aria-label="直接链接到 How it fits with Spring Boot" title="直接链接到 How it fits with Spring Boot" translate="no">​</a></h2>
<p>You do not choose between webforJ and Spring Boot. webforJ integrates with your Spring Boot backend: you generate a Spring Boot project, inject services into views, use Spring Data repositories for tables and Spring Security for routes. It also runs standalone when there is no Spring in the picture. If your team already knows Spring, <a class="" href="https://docs.webforj.com/zh/blog/java-full-stack-framework">our Java full stack framework guide</a> walks through a slice from entity to screen.</p>
<p>The unusual part is what you stop writing. A UI that talks to your own services over REST needs controllers, DTOs and a client, all of which exist only to carry data between two halves of the same app. <a class="" href="https://docs.webforj.com/zh/blog/java-ui-without-rest-api">Your Spring Boot app doesn't need a REST API for the UI</a> explains the reasoning, and you can still expose a REST API for other clients when you need one.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="when-split-wins">When a split stack wins<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#when-split-wins" class="hash-link" aria-label="直接链接到 When a split stack wins" title="直接链接到 When a split stack wins" translate="no">​</a></h2>
<p>A separate JavaScript frontend is the better choice in a few situations, and we would rather say so than pretend otherwise.</p>
<ul>
<li class="">Several independent clients, such as a public website, a mobile app and partner integrations, all need the same API from the first day.</li>
<li class="">You have a frontend team that already ships a JavaScript app and wants to keep its tools.</li>
<li class="">The product is a consumer-facing site whose design is the product, with heavy custom animation and marketing pages.</li>
</ul>
<p>For internal tools, back-office systems, admin consoles, ERP screens and the long tail of business apps, none of that applies, and the split costs more than it returns. These are the apps webforJ is built for. The <a class="" href="https://docs.webforj.com/zh/blog/java-enterprise-web-framework">enterprise checklist</a> and the <a class="" href="https://docs.webforj.com/zh/blog/complete-java-web-platform">complete platform walk-through</a> go deeper on the preceding items, and <a class="" href="https://docs.webforj.com/zh/blog/how-to-build-web-app-java">how to build a web app in Java</a> is the hands-on version.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-webforj">Why this is the stronger choice<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#why-webforj" class="hash-link" aria-label="直接链接到 Why this is the stronger choice" title="直接链接到 Why this is the stronger choice" translate="no">​</a></h2>
<p>Pick a framework by counting the seams. Every seam between languages, build chains, teams or API contracts is a place where work is duplicated and bugs hide. With a backend framework plus a frontend, the seams sit at the API, the build, the type definitions and the validation rules, which you write once in Java and again in TypeScript. <a class="" href="https://docs.webforj.com/zh/blog/avoid-ui-framework-lock-in">Keeping business logic outside the views</a> also keeps the cost of a later framework change small.</p>
<p>With webforJ, the full-stack Java framework, validation lives on the model, security lives on the route, tests run in the same JUnit build and the deployable is one artifact. A developer who reads Java can read the whole app, and an AI assistant works against one codebase and one set of checked APIs instead of two. That is the case for it, and it holds for the class of apps that most Java teams build. <a class="" href="https://docs.webforj.com/zh/blog/simple-java-web-development">Simple Java web development</a> describes what changes day to day.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="start">Start with one view<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#start" class="hash-link" aria-label="直接链接到 Start with one view" title="直接链接到 Start with one view" translate="no">​</a></h2>
<p>Generate a Spring Boot project with <a href="https://docs.webforj.com/startforj" target="_blank" rel="noopener noreferrer" class="">startforJ</a>, add one route and one form bound to a model, and run it. The <a class="" href="https://docs.webforj.com/zh/docs/introduction/getting-started">getting started guide</a> covers prerequisites and the first run.</p>
<p>For the approaches themselves, see the comparison of <a class="" href="https://docs.webforj.com/zh/blog/java-frontend-frameworks">Java frontend frameworks</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">Frequently asked questions<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#faq" class="hash-link" aria-label="直接链接到 Frequently asked questions" title="直接链接到 Frequently asked questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-best">What is the best Java framework for web applications?<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#faq-best" class="hash-link" aria-label="直�接链接到 What is the best Java framework for web applications?" title="直接链接到 What is the best Java framework for web applications?" translate="no">​</a></h3>
<p>It depends on the app. For business apps where the team writes Java, webforJ, the full-stack Java framework, covers routing, components, forms, security, testing, and deployment in one codebase. For a backend that serves several independent clients, a backend framework such as Spring Boot fits.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-difference">How is a Java web app framework different from a backend framework?<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#faq-difference" class="hash-link" aria-label="直接链接到 How is a Java web app framework different from a backend framework?" title="直接链接到 How is a Java web app framework different from a backend framework?" translate="no">​</a></h3>
<p>A backend framework handles HTTP, services and data, and leaves the browser UI to a separate JavaScript app. A full-stack framework such as webforJ also covers the screens, so the whole app is Java. They combine well, because webforJ integrates with Spring Boot.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-javascript">Do I need JavaScript for Java web development?<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#faq-javascript" class="hash-link" aria-label="直接链接到 Do I need JavaScript for Java web development?" title="直接链接到 Do I need JavaScript for Java web development?" translate="no">​</a></h3>
<p>Not for a business app built with webforJ. Views, forms, tables and navigation are written in Java. You can still add custom web components or frontend sources when a project needs them.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-time">How long does Java web app development take with a full-stack framework?<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#faq-time" class="hash-link" aria-label="直接链接到 How long does Java web app development take with a full-stack framework?" title="直接链接到 How long does Java web app development take with a full-stack framework?" translate="no">​</a></h3>
<p>It depends on the app, so we do not quote a number. What shortens the path is fewer moving parts: one language, one build, no REST layer for your own UI, and generated project structure from startforJ.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-existing-spring">Can I use webforJ with an existing Spring Boot app?<a href="https://docs.webforj.com/zh/blog/java-framework-for-web-applications#faq-existing-spring" class="hash-link" aria-label="直接链接到 Can I use webforJ with an existing Spring Boot app?" title="直接链接到 Can I use webforJ with an existing Spring Boot app?" translate="no">​</a></h3>
<p>Yes. webforJ integrates with Spring Boot, so views can use your existing services, Spring Data repositories and Spring Security configuration. You add it to the project and build screens in Java next to the code you have.</p><div></div>]]></content>
        <author>
            <name>webforJ</name>
        </author>
        <category label="Insights" term="Insights"/>
        <category label="Full-Stack" term="Full-Stack"/>
        <category label="Spring" term="Spring"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Selenium vs Playwright for Java: testing a webforJ UI]]></title>
        <id>https://docs.webforj.com/zh/blog/selenium-vs-playwright-java</id>
        <link href="https://docs.webforj.com/zh/blog/selenium-vs-playwright-java"/>
        <updated>2026-11-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Selenium vs Playwright for Java UIs: both drive a webforJ app from plain JUnit. Here are the myths, the facts and which one to pick for your suite.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-11-07-selenium-vs-playwright-java/cover.jpg" alt="cover image" class="img_ev3q"></p>
<p>If you're about to write the first browser tests for a Java web app, the tool question comes up fast. One teammate swears by Selenium, another has heard Playwright is easier, and nobody wants to pick wrong and rewrite the suite later.</p>
<p>Selenium vs Playwright for Java turns out to be a gentler choice than it looks with webforJ. A webforJ app is a normal web page in the browser, so both tools drive it from plain JUnit tests, and the webforJ docs ship a working example for each. There is one real difference worth understanding, and it shapes which one suits a new suite.</p>
<p>In this post you'll go through the common myths one at a time, with the fact next to each. The headings are the facts, so you can skim them first and come back for the detail.</p>
<div class="cta_UErl" role="complementary" aria-label="Trying this in your own app?"><div class="copy_XH8h"><p class="title_HfaR">Trying this in your own app?</p><p class="text_x4Wj">Ask the webforJ team when something doesn't fit your case.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="https://docs.webforj.com/startforj/" target="_blank" rel="noopener noreferrer">启动应用<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a><a class="secondary_GDTR" href="mailto:sales@basis.cloud?subject=Question%20about%20a%20webforJ%20tutorial&amp;body=Hi%20webforJ%20team%2C%0A%0A%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->邮件咨询</a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="myth-special-driver">No special driver is needed<a href="https://docs.webforj.com/zh/blog/selenium-vs-playwright-java#myth-special-driver" class="hash-link" aria-label="直接链接到 No special driver is needed" title="直接链接到 No special driver is needed" translate="no">​</a></h2>
<p><strong>Fact:</strong> webforJ renders standard web components in a standard browser. There is no webforJ-specific driver, plugin or agent to install, and nothing to add to the app itself. The <a class="" href="https://docs.webforj.com/zh/docs/testing/e2e/playwright">Playwright guide</a> and the <a class="" href="https://docs.webforj.com/zh/docs/testing/e2e/selenium">Selenium guide</a> both start from the same place: run the app, point the test at <code>http://localhost:&lt;port&gt;/</code>, and use the tool's regular Java bindings.</p>
<p>The only project-side setup is a test-scoped dependency in your <code>pom.xml</code>. The docs use <code>com.microsoft.playwright:playwright</code> for Playwright, and <code>org.seleniumhq.selenium:selenium-java</code> with WebDriverManager for Selenium.</p>
<p><strong>Recap:</strong> same app, same URL, a single dependency either way.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="myth-selenium-spa">A single-page UI does not break Selenium<a href="https://docs.webforj.com/zh/blog/selenium-vs-playwright-java#myth-selenium-spa" class="hash-link" aria-label="直接链接到 A single-page UI does not break Selenium" title="直接链接到 A single-page UI does not break Selenium" translate="no">​</a></h2>
<p><strong>Fact:</strong> it needs one habit. Here's the concept first. A webforJ app produces a single-page UI, so the DOM is built after the first load. A test that looks up an element the instant the page opens can find nothing. The Selenium guide says so directly and points to <code>WebDriverWait</code>.</p>
<p>The docs example waits for the page title before it touches anything:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">WebDriverWait</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">driver</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">ofSeconds</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token number" style="color:var(--dwc-code-number)">30</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">until</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token function" style="color:var(--dwc-code-function)">titleIs</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"webforJ Hello World"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token class-name" style="color:var(--dwc-code-class)">WebElement</span><span class="token plain"> button </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> driver</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">findElement</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">By</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">tagName</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"dwc-button"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><br></div></code></pre></div></div>
<p>After that, <code>By.tagName</code>, <code>By.id</code> and the other locators work as usual.</p>
<p><strong>Recap:</strong> this is still a fine fit for Selenium. You just write the waits yourself.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="myth-playwright-javascript">Playwright works in Java<a href="https://docs.webforj.com/zh/blog/selenium-vs-playwright-java#myth-playwright-javascript" class="hash-link" aria-label="直接链接到 Playwright works in Java" title="直接链接到 Playwright works in Java" translate="no">​</a></h2>
<p><strong>Fact:</strong> the Playwright team ships first-class <a href="https://playwright.dev/java/docs/intro" target="_blank" rel="noopener noreferrer" class="">Java bindings</a>, and the webforJ docs test with them from JUnit. A full test for the hello world view looks like this:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">GreetingFlowTest</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">static</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Playwright</span><span class="token plain"> playwright</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">static</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Browser</span><span class="token plain"> browser</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token class-name" style="color:var(--dwc-code-class)">Page</span><span class="token plain"> page</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@BeforeAll</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">static</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">launch</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    playwright </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Playwright</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">create</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    browser </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> playwright</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">chromium</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">launch</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@AfterAll</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">static</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">close</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    playwright</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">close</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@BeforeEach</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">open</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    page </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> browser</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">newPage</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    page</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">navigate</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"http://localhost:"</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">System</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getProperty</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"server.port"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"8080"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"/"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Test</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">greetsTheUser</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    page</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">locator</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"input"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">fill</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"webforJ"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    page</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getByText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Say Hello"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">click</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token function" style="color:var(--dwc-code-function)">assertThat</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">page</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">locator</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"dwc-toast"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">first</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">containsText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Welcome to webforJ Starter webforJ!"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>Notice there's no explicit wait anywhere. Playwright <a href="https://playwright.dev/java/docs/actionability" target="_blank" rel="noopener noreferrer" class="">checks that an element is ready</a> before it acts on it, and the <a href="https://playwright.dev/java/docs/test-assertions" target="_blank" rel="noopener noreferrer" class="">web-first assertion</a> <code>assertThat(...).containsText(...)</code> keeps retrying until the toast shows up or the timeout ends.</p>
<p><strong>Recap:</strong> for a UI where the server decides what appears next, Playwright's waiting is the behavior you want out of the box.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="myth-pick-one">Picking one is not a lock-in<a href="https://docs.webforj.com/zh/blog/selenium-vs-playwright-java#myth-pick-one" class="hash-link" aria-label="直接链接到 Picking one is not a lock-in" title="直接链接到 Picking one is not a lock-in" translate="no">​</a></h2>
<p><strong>Fact:</strong> both approaches test the same running app through the same URL. The test classes are JUnit, they run with <code>mvn test</code>, and the app starts the same way for either one: <code>mvn jetty:run</code>. Switching later means rewriting test code, not the app. Since the tests are plain JUnit classes, a suite can move from one tool to the other one class at a time.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="myth-e2e-replaces-unit">E2E tests do not replace unit tests<a href="https://docs.webforj.com/zh/blog/selenium-vs-playwright-java#myth-e2e-replaces-unit" class="hash-link" aria-label="直接链接到 E2E tests do not replace unit tests" title="直接链接到 E2E tests do not replace unit tests" translate="no">​</a></h2>
<p><strong>Fact:</strong> browser tests are the slowest and most expensive layer, so they should cover whole user journeys, not every branch of your logic. The <a class="" href="https://docs.webforj.com/zh/docs/testing/overview">testing overview</a> recommends combining layers, and since 26.03 there's one more layer between unit tests and the browser.</p>
<p>The <a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/overview">TestBed</a> runs a webforJ view inside a plain JUnit test without a browser or a server. It finds components the way a user picks them out, types and clicks, and checks the result. So a check like "this button updates the total" no longer needs Selenium or Playwright at all.</p>
<p><strong>Recap:</strong> rules go in unit tests, view behavior goes in the TestBed, and the browser keeps what only a browser can show.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-to-test">What to test in the browser<a href="https://docs.webforj.com/zh/blog/selenium-vs-playwright-java#what-to-test" class="hash-link" aria-label="直接链接到 What to test in the browser" title="直接链接到 What to test in the browser" translate="no">​</a></h2>
<p>Pick journeys, not screens. A good browser test in a webforJ app does one thing a user would do and checks one visible result:</p>
<ol>
<li class="">Open the route and wait for the first component to render.</li>
<li class="">Fill a field, click a button, or choose a row.</li>
<li class="">Assert on what the user sees, such as a <code>dwc-toast</code> message or the text of a <code>dwc-button</code>.</li>
</ol>
<p>Both guides locate webforJ components by their rendered tags and visible text. That keeps the tests close to what a person would describe, and independent of how the Java view is structured.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="running-suite">Running the suite<a href="https://docs.webforj.com/zh/blog/selenium-vs-playwright-java#running-suite" class="hash-link" aria-label="直接链接到 Running the suite" title="直接链接到 Running the suite" translate="no">​</a></h2>
<p>The workflow is the same for both tools, because both guides use the same two commands. Start the app in one terminal with <code>mvn jetty:run</code>, then run <code>mvn test</code> in another. The port comes from the <code>server.port</code> system property and falls back to <code>8080</code>, so a test class can point at whatever port you started the app on.</p>
<p>That makes the tests easy to reason about. The browser opens a URL, the page renders, and the test asserts on what's on screen. No part of the app has to know a test exists.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="which-one">Which one should you pick?<a href="https://docs.webforj.com/zh/blog/selenium-vs-playwright-java#which-one" class="hash-link" aria-label="直接链接到 Which one should you pick?" title="直接链接到 Which one should you pick?" translate="no">​</a></h2>
<p>Here's how the two compare on a webforJ app:</p>
<div class="table-wrapper"><div class="table-container" style="overflow-x:auto"><table><thead><tr><th></th><th>Selenium</th><th>Playwright</th></tr></thead><tbody><tr><td>Java bindings and JUnit</td><td>Yes</td><td>Yes</td></tr><tr><td>Works with webforJ unchanged</td><td>Yes</td><td>Yes</td></tr><tr><td>Waiting for the dynamic DOM</td><td>You add <code>WebDriverWait</code></td><td>Built into actions and assertions</td></tr><tr><td>Browser setup</td><td>WebDriverManager in the docs example</td><td>Playwright can install browsers during setup</td></tr><tr><td>Locator style</td><td><code>By.tagName</code>, <code>By.id</code></td><td><code>locator</code>, <code>getByText</code></td></tr><tr><td>Best fit</td><td>Teams with an existing Selenium suite</td><td>New suites</td></tr></tbody></table></div><style data-emotion="css 1k371a6">@media print{.css-1k371a6{position:absolute!important;}}</style></div>
<p>The row that matters most for webforJ is the third one. A toast, a dialog or a table row is rendered into the page after the click, not before it. A tool that waits for the element by default means fewer hand-written waits, and your test code stays focused on what the user does.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="best-e2e-framework-java">Best e2e framework for Java: a simple rule<a href="https://docs.webforj.com/zh/blog/selenium-vs-playwright-java#best-e2e-framework-java" class="hash-link" aria-label="直接链接到 Best e2e framework for Java: a simple rule" title="直接链接到 Best e2e framework for Java: a simple rule" translate="no">​</a></h2>
<p>Start new webforJ projects with Playwright. Keep Selenium where it already runs. Whichever you use, put most of your confidence in <a class="" href="https://docs.webforj.com/zh/blog/java-web-app-testing-strategy">fast JUnit and TestBed tests</a>, and save the browser for the handful of journeys that must never break: sign in, create a record, submit a form.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="first-browser-test">Write your first browser test<a href="https://docs.webforj.com/zh/blog/selenium-vs-playwright-java#first-browser-test" class="hash-link" aria-label="直接链接到 Write your first browser test" title="直接链接到 Write your first browser test" translate="no">​</a></h2>
<p>Generate a project from <a href="https://docs.webforj.com/startforj" target="_blank" rel="noopener noreferrer" class="">startforJ</a>, run it with <code>mvn jetty:run</code>, and paste the Playwright test from earlier next to it. The hello world view is enough to get a first browser test passing. The <a class="" href="https://docs.webforj.com/zh/docs/introduction/getting-started">getting started guide</a> covers the project setup.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">Frequently asked questions<a href="https://docs.webforj.com/zh/blog/selenium-vs-playwright-java#faq" class="hash-link" aria-label="直接链接到 Frequently asked questions" title="直接链接到 Frequently asked questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-best-e2e">What is the best e2e framework for Java?<a href="https://docs.webforj.com/zh/blog/selenium-vs-playwright-java#faq-best-e2e" class="hash-link" aria-label="直接链接到 What is the best e2e framework for Java?" title="直接链接到 What is the best e2e framework for Java?" translate="no">​</a></h3>
<p>For a webforJ app, Playwright is the easier start for new suites, since it waits for the dynamic DOM by default. Selenium works equally well when you add explicit waits, and the webforJ docs cover both.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-playwright-or-selenium">Playwright or Selenium for Java, which one?<a href="https://docs.webforj.com/zh/blog/selenium-vs-playwright-java#faq-playwright-or-selenium" class="hash-link" aria-label="直接链接到 Playwright or Selenium for Java, which one?" title="直接链接到 Playwright or Selenium for Java, which one?" translate="no">​</a></h3>
<p>Use the one your team can maintain. If you have no suite yet, Playwright is the shorter path. If you already have Selenium tests, a webforJ app needs no changes to keep using them.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-change-app">Do I need to change my webforJ app to test it in a browser?<a href="https://docs.webforj.com/zh/blog/selenium-vs-playwright-java#faq-change-app" class="hash-link" aria-label="直接链接到 Do I need to change my webforJ app to test it in a browser?" title="直接链接到 Do I need to change my webforJ app to test it in a browser?" translate="no">​</a></h3>
<p>No. Both tools open the running app at its URL and interact with the rendered components, so the app code stays as it is. Start the server with <code>mvn jetty:run</code>, then run <code>mvn test</code>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-browser-testing-speed">How do I do browser testing in Java without slowing down the build?<a href="https://docs.webforj.com/zh/blog/selenium-vs-playwright-java#faq-browser-testing-speed" class="hash-link" aria-label="直接链接到 How do I do browser testing in Java without slowing down the build?" title="直接链接到 How do I do browser testing in Java without slowing down the build?" translate="no">​</a></h3>
<p>Keep browser tests to full user journeys. Test logic with plain JUnit and view behavior with the <a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/overview">TestBed</a>, which needs no browser. This split keeps the browser layer small.</p><div></div>]]></content>
        <author>
            <name>Matthew Hawkins</name>
        </author>
        <category label="Tutorial" term="Tutorial"/>
        <category label="Performance" term="Performance"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The AI coding agent verification loop for Java]]></title>
        <id>https://docs.webforj.com/zh/blog/ai-verification-loop-java</id>
        <link href="https://docs.webforj.com/zh/blog/ai-verification-loop-java"/>
        <updated>2026-11-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How webforJ closes the AI coding agent verification loop in Java: skills, MCP docs checks, compiled edits and browserless tests, installed with one plugin.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-11-06-ai-verification-loop-java/cover-v2.jpg" alt="cover image" class="img_ev3q"></p>
<p>You ask a coding agent for a new screen and it comes back fast, confident, and half wrong. A method that does not exist, a CSS token it made up, a build that fails on the second line, and a button that nobody ever clicked. The model is rarely the problem. The missing piece is verification.</p>
<p>The AI coding agent verification loop is the set of checks that turns a guess into code you can trust: write with known workflows, check every API against the real docs, compile every edit, and run a test on every change. webforJ, the <a class="" href="https://docs.webforj.com/zh/blog/java-full-stack-framework">full-stack Java framework</a>, closes that whole loop with Agent Skills, an MCP server, the craftforJ Assistant and the TestBed, and one plugin installs the first two for Claude Code, Copilot, Gemini, and Codex. Cursor and other clients use manual setup. Because of that loop, webforJ is the most AI-ready framework in the Java ecosystem.</p>
<div class="cta_UErl" role="complementary" aria-label="Using AI to build Java apps?"><div class="copy_XH8h"><p class="title_HfaR">Using AI to build Java apps?</p><p class="text_x4Wj">Ask the team about webforJ AI tooling: the MCP server, Agent Skills, and how they keep generated code correct.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="https://webforj.com/schedule-discovery/" target="_blank" rel="noopener noreferrer">预约通话<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a><a class="secondary_GDTR" href="mailto:sales@basis.cloud?subject=AI%20and%20webforJ&amp;body=Hi%20webforJ%20team%2C%0A%0A%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->邮件咨询</a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-problem">The problem with unverified agents<a href="https://docs.webforj.com/zh/blog/ai-verification-loop-java#the-problem" class="hash-link" aria-label="直接链接到 The problem with unverified agents" title="直接链接到 The problem with unverified agents" translate="no">​</a></h2>
<p>An agent that writes code without checking it is doing autocomplete with extra steps. It recalls what a class probably looks like from training data, and for a framework that changes between versions that memory is often stale. Which framework you pair with an agent matters too, and the <a class="" href="https://docs.webforj.com/zh/blog/best-java-ui-frameworks">best Java UI framework for business apps</a> is a place to start.</p>
<p>The failures fall into four groups, and each one needs a different check:</p>
<ul>
<li class="">The agent invents or misremembers an API.</li>
<li class="">The agent follows generic habits instead of the conventions of your framework.</li>
<li class="">The code does not compile, and you find out when you build.</li>
<li class="">The code compiles and still does the wrong thing, and you find out in production.</li>
</ul>
<p>Reviewing all four by hand is the cost people complain about when they say AI code is not worth the trouble. A single check cannot remove it. A loop of checks can, because each step catches what the step before it let through. The rest of this post walks the loop in order. A separate post covers <a class="" href="https://docs.webforj.com/zh/blog/ai-generated-java-code-guardrails">quality control for AI-generated Java code</a> with the same pieces.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="skills">Step 1: skills write the code<a href="https://docs.webforj.com/zh/blog/ai-verification-loop-java#skills" class="hash-link" aria-label="直接链接到 Step 1: skills write the code" title="直接链接到 Step 1: skills write the code" translate="no">​</a></h2>
<p>The first check happens before any code exists. <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/agent-skills">Agent Skills</a> are structured webforJ workflows that the assistant loads when your prompt matches a task. They tell it how to classify the work, which patterns fit, which tools to consult and how to validate the result, so the code compiles and follows best practices on the first attempt.</p>
<p>The skills cover adding servlets, building forms, creating components, handling timers and async work, localizing apps, securing apps, styling apps, and upgrading versions. This is the difference between code that is technically valid and code that looks like it belongs in your project.</p>
<p>What it catches: convention drift. The agent reaches for webforJ's own approach instead of a generic one.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="mcp">Step 2: the MCP server checks every API<a href="https://docs.webforj.com/zh/blog/ai-verification-loop-java#mcp" class="hash-link" aria-label="直接链接到 Step 2: the MCP server checks every API" title="直接链接到 Step 2: the MCP server checks every API" translate="no">​</a></h2>
<p>Skills say when to look something up. The <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/mcp">MCP server</a> is what answers. It plugs the assistant into the webforJ documentation, APIs and design tokens, so it asks the server instead of guessing at method names.</p>
<p>With our MCP server, the assistant can target the webforJ version you use, search the knowledge base, scaffold a project from the official Maven archetypes, generate an accessible DWC theme from a brand color, read the styling surface of a component and validate any <code>--dwc-*</code> token before it lands in your CSS.</p>
<p>That last one is a real example of the pattern. A token the agent invented fails validation against the catalog, so it never reaches your stylesheet.</p>
<p>What it catches: invented classes, wrong signatures and made-up design tokens.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="craftforj">Step 3: a real compiler checks each edit<a href="https://docs.webforj.com/zh/blog/ai-verification-loop-java#craftforj" class="hash-link" aria-label="直接链接到 Step 3: a real compiler checks each edit" title="直接链接到 Step 3: a real compiler checks each edit" translate="no">​</a></h2>
<p>Some mistakes survive a docs lookup, so the next check has to be a compiler. A type is wrong, an import is missing, a call does not fit the object it is called on. Only a compiler sees those.</p>
<p>The <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/craftforj-assistant">craftforJ Assistant</a> is a coding agent that works inside your running app. Each edit is staged rather than written to disk, sent to a real Java compiler, and corrected by the agent against the diagnostics that come back. What reaches your review already compiles against your running app. Applying the edit restarts the app, and the agent picks its plan up again once it is back. This is its own agent, so it needs a model of your choice configured, and craftforJ runs only in development.</p>
<p>It also reads the live component tree, real property values, the routing table and screenshots of a component, so it works from what the app does and not from a description of it.</p>
<p>What it catches: compile errors, before you ever see them.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="testbed">Step 4: TestBed proves the behavior<a href="https://docs.webforj.com/zh/blog/ai-verification-loop-java#testbed" class="hash-link" aria-label="直接链接到 Step 4: TestBed proves the behavior" title="直接链接到 Step 4: TestBed proves the behavior" translate="no">​</a></h2>
<p>Compiling is not the same as working. The last gap is behavior, and the usual answer, a browser test, is slow enough that agents and humans both skip it.</p>
<p>The <a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/overview">TestBed</a> runs a view inside a plain JUnit test. No browser is opened and no server is started. Every interaction finishes before the next line of the test runs, so nothing waits and nothing flakes. That is what makes it practical to run tests on every change an agent makes. This is the example from the getting started page:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockTitle_OeMC">GreetingFormTest.java</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@WebforjTest</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">GreetingFormTest</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Test</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">shouldGreetTheNameTheUserTyped</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">WebforjTestbed</span><span class="token plain"> context</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    context</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">GreetingForm</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">var</span><span class="token plain"> name </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">ComponentLocator</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">of</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">byLabel</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Name"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">locate</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">var</span><span class="token plain"> greet </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">ComponentLocator</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">of</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">byText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Greet"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">locate</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">UserInteraction</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">type</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">name</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"Ada"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">UserInteraction</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">click</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">greet</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token function" style="color:var(--dwc-code-function)">verifyThat</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">ComponentLocator</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">of</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Paragraph</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">locate</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">hasText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Hello, Ada!"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>The test reads like the steps of a user, and <code>UserInteraction</code> only makes gestures a user could make. Clicking a disabled button fails the test instead of running its listener, which is exactly the kind of bug an unverified agent ships. Snapshots compare a whole view against a stored description of it, so an agent that changes more than you asked for shows up as a diff. The mechanics are covered in <a class="" href="https://docs.webforj.com/zh/blog/java-ui-testing-without-browser">Java UI testing without a browser</a>.</p>
<p>Browser tests still have their place for styling, layout, and client behavior. The TestBed covers the rest: what your views do when a user works with them.</p>
<p>What it catches: behavior that compiles and is still wrong.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="plugin">Step 5: one plugin installs the loop<a href="https://docs.webforj.com/zh/blog/ai-verification-loop-java#plugin" class="hash-link" aria-label="直接链接到 Step 5: one plugin installs the loop" title="直接链接到 Step 5: one plugin installs the loop" translate="no">​</a></h2>
<p>You do not wire these pieces together yourself. The <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling">webforJ AI plugin</a> installs the MCP server and the Agent Skills in one step for Claude Code, GitHub Copilot, Gemini CLI and OpenAI Codex CLI. Cursor and other clients that support Agent Skills use manual configuration. The craftforJ Assistant ships with webforJ itself, so there is nothing extra to install beyond turning it on in development, and the TestBed is a test-scope dependency.</p>
<p>For Claude Code it is two commands:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">claude plugin marketplace </span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token plain"> webforj/webforj-ai</span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">claude plugin </span><span class="token function" style="color:var(--dwc-code-function)">install</span><span class="token plain"> webforj@webforj-ai</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="loop-table">The loop at a glance<a href="https://docs.webforj.com/zh/blog/ai-verification-loop-java#loop-table" class="hash-link" aria-label="直接链接到 The loop at a glance" title="直接链接到 The loop at a glance" translate="no">​</a></h2>
<div class="table-wrapper"><div class="table-container" style="overflow-x:auto"><table><thead><tr><th>Step</th><th>What it checks</th><th>Docs page</th></tr></thead><tbody><tr><td>Agent Skills</td><td>The agent follows webforJ workflows and conventions</td><td><a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/agent-skills">Agent Skills</a></td></tr><tr><td>MCP server</td><td>APIs, docs answers and <code>--dwc-*</code> tokens against the real sources</td><td><a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/mcp">MCP server</a></td></tr><tr><td>craftforJ Assistant</td><td>Each edit compiles with a real Java compiler</td><td><a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/craftforj-assistant">craftforJ Assistant</a></td></tr><tr><td>TestBed</td><td>Behavior, through JUnit tests with no browser and no server</td><td><a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/overview">TestBed</a></td></tr><tr><td>AI plugin</td><td>Skills and MCP installed together in one step</td><td><a class="" href="https://docs.webforj.com/zh/docs/ai-tooling">AI plugin</a></td></tr></tbody></table></div><style data-emotion="css 1k371a6">@media print{.css-1k371a6{position:absolute!important;}}</style></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-a-loop">Why this is a loop, not a toolbox<a href="https://docs.webforj.com/zh/blog/ai-verification-loop-java#why-a-loop" class="hash-link" aria-label="直接链接到 Why this is a loop, not a toolbox" title="直接链接到 Why this is a loop, not a toolbox" translate="no">​</a></h2>
<p>Each tool alone is useful. Together they cover the failure modes in order. Skills reduce wrong approaches, the MCP server reduces wrong facts, the compiler removes wrong code, and the tests remove wrong behavior. By the time something reaches your review, the mistakes each layer targets are already filtered out. A wider look at <a class="" href="https://docs.webforj.com/zh/blog/ai-java-web-framework">how webforJ is built for AI work</a> adds an agent in your running app and agents that use your UI.</p>
<p>This works because the whole app is Java. There is no template language or separate front end for the agent to get out of sync with, so the compiler and a JUnit test can see everything the agent changed. For the framework-level argument, see <a class="" href="https://docs.webforj.com/zh/blog/java-framework-for-ai-coding">why webforJ fits AI coding</a>.</p>
<p>To be clear about the limit: even with the plugin, AI assistants can produce incorrect code in complex scenarios. The loop lowers what you have to catch yourself. It does not replace your review.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next-step">Set it up in your project<a href="https://docs.webforj.com/zh/blog/ai-verification-loop-java#next-step" class="hash-link" aria-label="直接链接到 Set it up in your project" title="直接链接到 Set it up in your project" translate="no">​</a></h2>
<p>Install the plugin from the <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling">AI tooling page</a>, mention webforJ in your prompts so the assistant reaches for it, and add the <code>webforj-testbed-junit</code> dependency with test scope. Then ask for one small screen and ask for a test with it. Spring Boot apps use the <code>webforj-spring-testbed</code> module instead.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">Frequently asked questions<a href="https://docs.webforj.com/zh/blog/ai-verification-loop-java#faq" class="hash-link" aria-label="直接链接到 Frequently asked questions" title="直接链接到 Frequently asked questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-what-is-it">What is an AI coding agent verification loop?<a href="https://docs.webforj.com/zh/blog/ai-verification-loop-java#faq-what-is-it" class="hash-link" aria-label="直接链接到 What is an AI coding agent verification loop?" title="直接链接到 What is an AI coding agent verification loop?" translate="no">​</a></h3>
<p>It is the chain of checks between a prompt and merged code: the agent writes with known workflows, checks APIs against real documentation, compiles its edits, and runs tests. In webforJ those steps are Agent Skills, the MCP server, the craftforJ Assistant and the TestBed.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-reliable">How do I make AI-generated Java code reliable?<a href="https://docs.webforj.com/zh/blog/ai-verification-loop-java#faq-reliable" class="hash-link" aria-label="直接链接到 How do I make AI-generated Java code reliable?" title="直接链接到 How do I make AI-generated Java code reliable?" translate="no">​</a></h3>
<p>Do not rely on one check. Ground the agent in the real docs, make it compile each edit and require a test for each change. With webforJ the AI plugin covers the first part, the craftforJ Assistant covers compiling and the TestBed covers tests.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-no-browser">Can I test AI-generated code without a browser?<a href="https://docs.webforj.com/zh/blog/ai-verification-loop-java#faq-no-browser" class="hash-link" aria-label="直接链接到 Can I test AI-generated code without a browser?" title="直接链接到 Can I test AI-generated code without a browser?" translate="no">​</a></h3>
<p>Yes. The webforJ TestBed runs views and components in plain JUnit tests without a browser or a server. The test adds the view, acts on it the way a user does and checks the result from Java.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-assistants">Which AI assistants work with the webforJ plugin?<a href="https://docs.webforj.com/zh/blog/ai-verification-loop-java#faq-assistants" class="hash-link" aria-label="直接链接到 Which AI assistants work with the webforJ plugin?" title="直接链接到 Which AI assistants work with the webforJ plugin?" translate="no">​</a></h3>
<p>The plugin installs for Claude Code, GitHub Copilot, Gemini CLI and OpenAI Codex CLI. Cursor and other clients that support Agent Skills, such as Kiro, Goose, Junie and Antigravity, use manual configuration.</p><div></div>]]></content>
        <author>
            <name>webforJ</name>
        </author>
        <category label="AI" term="AI"/>
        <category label="MCP" term="MCP"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Java web app testing strategy: what to test, and where]]></title>
        <id>https://docs.webforj.com/zh/blog/java-web-app-testing-strategy</id>
        <link href="https://docs.webforj.com/zh/blog/java-web-app-testing-strategy"/>
        <updated>2026-11-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A Java web app testing strategy for webforJ: JUnit for logic, TestBed for views without a browser, Bun for frontend code, and a few browser tests for journeys.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-11-06-java-web-app-testing-strategy/cover-v2.jpg" alt="cover image" class="img_ev3q"></p>
<p>Your app works on your machine. Then a refactor lands, a button stops doing its job, and nobody notices until a user does. Most teams know they need tests. The hard part is deciding which kind goes where, so the suite stays fast and people still trust a red build.</p>
<p>A Java web app testing strategy answers that question layer by layer. In webforJ every layer runs from JUnit in one Maven build, and the newest one tests your views without opening a browser at all.</p>
<p>This guide walks through the four layers, what each one catches, and how to keep the slow layer small.</p>
<div class="cta_UErl" role="complementary" aria-label="Trying this in your own app?"><div class="copy_XH8h"><p class="title_HfaR">Trying this in your own app?</p><p class="text_x4Wj">Ask the webforJ team when something doesn't fit your case.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="https://docs.webforj.com/startforj/" target="_blank" rel="noopener noreferrer">启动应用<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a><a class="secondary_GDTR" href="mailto:sales@basis.cloud?subject=Question%20about%20a%20webforJ%20tutorial&amp;body=Hi%20webforJ%20team%2C%0A%0A%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->邮件咨询</a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="testing-pyramid">The testing pyramid for a Java UI<a href="https://docs.webforj.com/zh/blog/java-web-app-testing-strategy#testing-pyramid" class="hash-link" aria-label="直接链接到 The testing pyramid for a Java UI" title="直接链接到 The testing pyramid for a Java UI" translate="no">​</a></h2>
<p>The classic testing pyramid puts many fast tests at the bottom and a few slow ones at the top. The idea holds for a Java UI. In webforJ the UI is written in Java, so the line between "logic" and "view" is a design choice, not a language boundary.</p>
<p>webforJ <a class="" href="https://docs.webforj.com/zh/docs/testing/overview">testing</a> gives you four layers:</p>
<ol>
<li class=""><strong>Unit tests</strong> check business logic with JUnit.</li>
<li class=""><strong>TestBed tests</strong> run views and components in plain JUnit, with no browser and no server.</li>
<li class=""><strong>Frontend tests</strong> cover TypeScript or JavaScript sources you author with the frontend bundler.</li>
<li class=""><strong>End-to-end tests</strong> drive a real browser against the running app with Selenium or Playwright.</li>
</ol>
<p>Each layer answers one question. Is this rule correct? Does this view behave when a user works with it? Does this client code behave? Does the page look and work right in a real browser?</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="unit-tests">Unit tests: keep logic out of views<a href="https://docs.webforj.com/zh/blog/java-web-app-testing-strategy#unit-tests" class="hash-link" aria-label="直接链接到 Unit tests: keep logic out of views" title="直接链接到 Unit tests: keep logic out of views" translate="no">​</a></h2>
<p>Most bugs in a business app live in rules: pricing, validation, eligibility, state transitions. None of them need a view. Put them in a plain Java class and a JUnit test checks them in milliseconds.</p>
<p>Keep views thin. A view builds components and wires events. A service decides the answer. The <a class="" href="https://docs.webforj.com/zh/blog/webforj-mvc">MVC post</a> shows this split in a real webforJ app.</p>
<p>Here is a small service with a rule worth testing:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockTitle_OeMC">DiscountService.java</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">DiscountService</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">BigDecimal</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">apply</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">BigDecimal</span><span class="token plain"> total</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> code</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"SAVE10"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">equals</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">code</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token plain"> total</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">multiply</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">BigDecimal</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"0.90"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setScale</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token number" style="color:var(--dwc-code-number)">2</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">RoundingMode</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">HALF_UP</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token plain"> total</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>And the JUnit test that pins its behavior:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockTitle_OeMC">DiscountServiceTest.java</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">DiscountServiceTest</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">DiscountService</span><span class="token plain"> service </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">DiscountService</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Test</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">knownCodeTakesTenPercentOff</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token function" style="color:var(--dwc-code-function)">assertEquals</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">BigDecimal</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"90.00"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> service</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">apply</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">BigDecimal</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"100.00"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"SAVE10"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Test</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">unknownCodeLeavesTotalUnchanged</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token function" style="color:var(--dwc-code-function)">assertEquals</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">BigDecimal</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"100.00"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> service</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">apply</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">BigDecimal</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"100.00"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"NOPE"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>No server, no browser, no setup. Hundreds of tests like this finish in seconds. JUnit 5 runs them, Mockito stands in for repositories, and Spring's test support helps when the service is a bean.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="testbed-tests">Test views without a browser<a href="https://docs.webforj.com/zh/blog/java-web-app-testing-strategy#testbed-tests" class="hash-link" aria-label="直接链接到 Test views without a browser" title="直接链接到 Test views without a browser" translate="no">​</a></h2>
<p>A unit test can't tell you that the <strong>Apply code</strong> button is wired to the right handler. That used to mean a browser test. Since 26.03 the <a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/overview">TestBed</a> covers it in plain JUnit. The view runs inside the test, components fire their events as they do in the running app, and every interaction finishes before the next line runs. Nothing waits and nothing flakes.</p>
<p>Here is a checkout view that uses the service:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockTitle_OeMC">CheckoutView.java</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">CheckoutView</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">extends</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Composite</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Div</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">DiscountService</span><span class="token plain"> discounts </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">DiscountService</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">CheckoutView</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token plain"> code </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Discount code"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">Paragraph</span><span class="token plain"> total </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Paragraph</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Total: 100.00"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token plain"> apply </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Apply code"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        e </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> total</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Total: "</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> discounts</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">apply</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">BigDecimal</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"100.00"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> code</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getValue</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token function" style="color:var(--dwc-code-function)">getBoundComponent</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">code</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> apply</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> total</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>The test does what a user does. It finds the field by its label, types a code, clicks the button, and checks the total:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockTitle_OeMC">CheckoutViewTest.java</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@WebforjTest</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">CheckoutViewTest</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Test</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">discountCodeUpdatesTheTotal</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">WebforjTestbed</span><span class="token plain"> context</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    context</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">CheckoutView</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">var</span><span class="token plain"> code </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">ComponentLocator</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">of</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">byLabel</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Discount code"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">locate</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">var</span><span class="token plain"> apply </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">ComponentLocator</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">of</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">byText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Apply code"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">locate</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">UserInteraction</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">type</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">code</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"SAVE10"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">UserInteraction</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">click</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">apply</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token function" style="color:var(--dwc-code-function)">verifyThat</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">ComponentLocator</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">of</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Paragraph</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">locate</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">hasText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Total: 90.00"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>The app isn't running and no browser opens. The test only needs the <code>webforj-testbed-junit</code> dependency with test scope, and the <a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/getting-started">getting started page</a> shows the setup. Beyond single views, the TestBed navigates routes, tests secured views, answers dialogs and runs timers, as <a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/testing-apps">testing apps</a> shows. Spring Boot apps use the <code>webforj-spring-testbed</code> module instead.</p>
<p>This layer moves most UI checks out of the browser. The full walkthrough is in <a class="" href="https://docs.webforj.com/zh/blog/java-ui-testing-without-browser">Java UI testing without a browser</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="frontend-tests">Frontend tests: cover your client code<a href="https://docs.webforj.com/zh/blog/java-web-app-testing-strategy#frontend-tests" class="hash-link" aria-label="直接链接到 Frontend tests: cover your client code" title="直接链接到 Frontend tests: cover your client code" translate="no">​</a></h2>
<p>Some apps ship their own client code, such as a custom element written in TypeScript or a helper that formats values in the browser. When a project authors those sources with the <a class="" href="https://docs.webforj.com/zh/docs/managing-resources/bundler/overview">frontend bundler</a>, webforJ tests them with the <a href="https://bun.sh/" target="_blank" rel="noopener noreferrer" class="">Bun</a> test runner.</p>
<p>Tests live under <code>src/main/frontend</code> next to the code they cover. A file named <code>*.test.ts</code> or <code>*.spec.ts</code> is picked up as a test, and it imports <code>expect</code>, <code>test</code> and <code>describe</code> from <code>bun:test</code>. These tests run in the same build as the Java tests, and a failing one fails the build. The <a class="" href="https://docs.webforj.com/zh/docs/testing/frontend-testing">frontend testing page</a> has the layout and a runnable example, and the <a class="" href="https://docs.webforj.com/zh/docs/managing-resources/bundler/build-and-tests#frontend-tests">build and tests page</a> explains the test step.</p>
<p>No custom frontend sources? Skip this layer. Plenty of webforJ apps are pure Java.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="end-to-end-tests">Browser tests for key journeys<a href="https://docs.webforj.com/zh/blog/java-web-app-testing-strategy#end-to-end-tests" class="hash-link" aria-label="直接链接到 Browser tests for key journeys" title="直接链接到 Browser tests for key journeys" translate="no">​</a></h2>
<p>Some questions only a real browser can answer: styling, layout, and how the page behaves on the client. That's the end-to-end layer. It confirms the server, the components, and the browser work together.</p>
<p>Java teams usually pick <a class="" href="https://docs.webforj.com/zh/docs/testing/e2e/playwright">Playwright</a> or <a class="" href="https://docs.webforj.com/zh/docs/testing/e2e/selenium">Selenium</a>. Both run from JUnit, so they fit the same <code>mvn test</code> flow. Here is a Playwright test in the shape the docs use, filling a field, clicking a button and checking the toast webforJ renders:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockTitle_OeMC">CheckoutJourneyTest.java</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">CheckoutJourneyTest</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">static</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Playwright</span><span class="token plain"> playwright</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">static</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Browser</span><span class="token plain"> browser</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@BeforeAll</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">static</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">launch</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    playwright </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Playwright</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">create</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    browser </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> playwright</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">chromium</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">launch</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@AfterAll</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">static</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">close</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    playwright</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">close</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Test</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">discountCodeShowsConfirmation</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">Page</span><span class="token plain"> page </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> browser</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">newPage</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    page</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">navigate</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"http://localhost:8080/checkout"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    page</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">locator</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"input"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">fill</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"SAVE10"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    page</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getByText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Apply code"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">click</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token function" style="color:var(--dwc-code-function)">assertThat</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">page</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">locator</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"dwc-toast"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">first</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">containsText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Discount applied"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>The app has to be running for this test to pass, so start it with <code>mvn jetty:run</code>, then run <code>mvn test</code>. That setup cost is why this layer stays small. The post on <a class="" href="https://docs.webforj.com/zh/blog/selenium-vs-playwright-java">choosing between Selenium and Playwright</a> for a webforJ suite covers the trade-offs.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="which-test-goes-where">Which test goes where<a href="https://docs.webforj.com/zh/blog/java-web-app-testing-strategy#which-test-goes-where" class="hash-link" aria-label="直接链接到 Which test goes where" title="直接链接到 Which test goes where" translate="no">​</a></h2>
<p>Use this table when you're unsure where a new test belongs.</p>
<div class="table-wrapper"><div class="table-container" style="overflow-x:auto"><table><thead><tr><th>What you want to check</th><th>Layer</th><th>Why</th></tr></thead><tbody><tr><td>A pricing, validation or eligibility rule</td><td>JUnit unit test</td><td>Fast, exact failure</td></tr><tr><td>A service with mocked collaborators</td><td>JUnit unit test</td><td>Plain Java, standard tooling</td></tr><tr><td>A button runs the right handler and the view updates</td><td>TestBed test</td><td>No browser, no server</td></tr><tr><td>A route loads, a secured view blocks, a dialog answers</td><td>TestBed test</td><td>Runs the real components in JUnit</td></tr><tr><td>A TypeScript helper or custom element logic</td><td>Bun frontend test</td><td>Runs in the same Maven build</td></tr><tr><td>Layout and styling in different browsers or devices</td><td>End-to-end test</td><td>Only a real browser can answer it</td></tr><tr><td>One full journey through the deployed stack</td><td>End-to-end test</td><td>Checks server and browser together</td></tr></tbody></table></div><style data-emotion="css 1k371a6">@media print{.css-1k371a6{position:absolute!important;}}</style></div>
<p>Rule of thumb: push every check as far down as it can go. If the TestBed can answer it, don't open a browser for it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="keep-e2e-small">Keep the end-to-end layer small<a href="https://docs.webforj.com/zh/blog/java-web-app-testing-strategy#keep-e2e-small" class="hash-link" aria-label="直接链接到 Keep the end-to-end layer small" title="直接链接到 Keep the end-to-end layer small" translate="no">​</a></h2>
<p>A common mistake is inverting the pyramid: a few unit tests and a long list of slow browser tests. The suite takes ten minutes, flakes on timing, and people stop trusting red builds.</p>
<p>Four habits keep the top layer healthy:</p>
<ol>
<li class=""><strong>One test per journey, not per field.</strong> Cover sign in, create an order, and the other paths that would hurt if they broke.</li>
<li class=""><strong>Move view behavior to the TestBed.</strong> Clicks, field values and navigation don't need a browser anymore.</li>
<li class=""><strong>Use stable locators.</strong> Prefer visible text and roles over deep CSS paths that break when markup changes.</li>
<li class=""><strong>Run it last.</strong> Let the fast layers fail first, so a broken rule never costs you a browser launch.</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-webforj">Why this works well in webforJ<a href="https://docs.webforj.com/zh/blog/java-web-app-testing-strategy#why-webforj" class="hash-link" aria-label="直接链接到 Why this works well in webforJ" title="直接链接到 Why this works well in webforJ" translate="no">​</a></h2>
<p>One language and one build make the strategy cheap to follow. The UI, the services and the tests are all Java. The developer who writes a view also writes its unit test, its TestBed test and its Playwright test, without switching context. Frontend tests join the same <code>mvn test</code> run when the project has client code. One command, one report, one place for CI to look.</p>
<p>There's no REST contract between a client and a server to test separately, and no second test stack that drifts apart from the first.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="add-a-testbed-test">Add a TestBed test today<a href="https://docs.webforj.com/zh/blog/java-web-app-testing-strategy#add-a-testbed-test" class="hash-link" aria-label="直接链接到 Add a TestBed test today" title="直接链接到 Add a TestBed test today" translate="no">​</a></h2>
<p>A short checklist to start:</p>
<ul>
<li class="">Move one rule out of a view into a service and pin it with a JUnit test.</li>
<li class="">Add <code>webforj-testbed-junit</code> with test scope and write one TestBed test for the view that uses it.</li>
<li class="">Keep a single Playwright test for the journey end to end.</li>
<li class="">Run <code>mvn test</code> and watch all three pass in one build.</li>
</ul>
<p>The <a class="" href="https://docs.webforj.com/zh/docs/testing/overview">testing overview</a> lists every testing page, and <a class="" href="https://docs.webforj.com/zh/docs/introduction/getting-started">getting started</a> creates a project you can try this in.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">Frequently asked questions<a href="https://docs.webforj.com/zh/blog/java-web-app-testing-strategy#faq" class="hash-link" aria-label="直接链接到 Frequently asked questions" title="直接链接到 Frequently asked questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="best-way-to-test-java-web-ui">How do you test a Java web UI?<a href="https://docs.webforj.com/zh/blog/java-web-app-testing-strategy#best-way-to-test-java-web-ui" class="hash-link" aria-label="直接链接到 How do you test a Java web UI?" title="直接链接到 How do you test a Java web UI?" translate="no">​</a></h3>
<p>Split the work by what each test can answer. Test rules with JUnit, test views with the TestBed, test client code with the Bun runner, and keep a short list of Playwright or Selenium tests for what only a real browser shows. Most checks stay fast, and the slow ones cover what only a browser can answer.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="unit-vs-e2e">Should I rely on unit tests or end-to-end tests?<a href="https://docs.webforj.com/zh/blog/java-web-app-testing-strategy#unit-vs-e2e" class="hash-link" aria-label="直接链接到 Should I rely on unit tests or end-to-end tests?" title="直接链接到 Should I rely on unit tests or end-to-end tests?" translate="no">​</a></h3>
<p>Rely on both, in proportion, with the TestBed in between. Unit and TestBed tests should be the large majority because they're fast and point at the exact failure. End-to-end tests should be few, and each should protect a journey a user would notice breaking.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="browser-for-views">Do I need a browser to test a webforJ view?<a href="https://docs.webforj.com/zh/blog/java-web-app-testing-strategy#browser-for-views" class="hash-link" aria-label="直接链接到 Do I need a browser to test a webforJ view?" title="直接链接到 Do I need a browser to test a webforJ view?" translate="no">​</a></h3>
<p>No. The TestBed runs a view inside a JUnit test, finds components the way a user picks them out, clicks and types, and checks the result. You need a browser only for styling, layout, and client behavior.</p><div></div>]]></content>
        <author>
            <name>Eric Handtke</name>
        </author>
        <category label="Tutorial" term="Tutorial"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Best Java framework for AI coding: why webforJ fits]]></title>
        <id>https://docs.webforj.com/zh/blog/java-framework-for-ai-coding</id>
        <link href="https://docs.webforj.com/zh/blog/java-framework-for-ai-coding"/>
        <updated>2026-11-05T15:00:00.000Z</updated>
        <summary type="html"><![CDATA[webforJ is the best Java framework for AI coding. One language, typed APIs and an official MCP server and skills keep generated UI code correct and compiling.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-11-05-java-framework-for-ai-coding/cover-v2.jpg" alt="cover image" class="img_ev3q"></p>
<p>Hand an AI assistant a Java web project and ask for a new screen. Whether you get working code or an afternoon of fixes depends less on the model than on the framework: how many languages the screen spans, how many ways there are to do the same thing, and whether the assistant can check the real APIs or has to remember them.</p>
<p>The best Java framework for AI coding is the one that leaves the assistant the least room to be wrong. In webforJ the whole UI is plain Java with typed components, so mistakes fail at compile time, and an official <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling">AI plugin</a> connects Claude Code, Copilot, Cursor, Gemini and Codex to the real webforJ docs and APIs.</p>
<div class="cta_UErl" role="complementary" aria-label="Using AI to build Java apps?"><div class="copy_XH8h"><p class="title_HfaR">Using AI to build Java apps?</p><p class="text_x4Wj">Ask the team about webforJ AI tooling: the MCP server, Agent Skills, and how they keep generated code correct.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="https://webforj.com/schedule-discovery/" target="_blank" rel="noopener noreferrer">预约通话<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a><a class="secondary_GDTR" href="mailto:sales@basis.cloud?subject=AI%20and%20webforJ&amp;body=Hi%20webforJ%20team%2C%0A%0A%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->邮件咨询</a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-ai-tools-struggle">Why AI tools struggle with web UIs<a href="https://docs.webforj.com/zh/blog/java-framework-for-ai-coding#why-ai-tools-struggle" class="hash-link" aria-label="直接链接到 Why AI tools struggle with web UIs" title="直接链接到 Why AI tools struggle with web UIs" translate="no">​</a></h2>
<p>Ask an assistant for a web screen and the answer usually spans several languages and layers. A template for the markup, a stylesheet, some JavaScript for behavior, a REST controller to feed it, and a client-side data layer to glue it together. Each layer has its own conventions and its own ways to fail quietly.</p>
<p>That is where generated code goes wrong. A model can produce a template that looks right and binds to a field that no longer exists. It can call an endpoint with a slightly wrong shape. None of this fails until someone runs the page, and the failure often shows up as a blank area instead of an error.</p>
<p>The more the framework relies on strings, conventions and runtime wiring, the more an assistant has to guess. Guessing is what language models do worst when the guess has to be exact. If you are still comparing options, see the <a class="" href="https://docs.webforj.com/zh/blog/best-java-ui-frameworks">best Java UI framework for business apps</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ai-friendly-java-framework">What an AI-friendly framework needs<a href="https://docs.webforj.com/zh/blog/java-framework-for-ai-coding#ai-friendly-java-framework" class="hash-link" aria-label="直接链接到 What an AI-friendly framework needs" title="直接链接到 What an AI-friendly framework needs" translate="no">​</a></h2>
<p>If you are choosing a Java framework for AI-generated code, four properties matter more than anything else:</p>
<ol>
<li class=""><strong>One language for the whole screen.</strong> No template dialect, no second syntax to keep in sync.</li>
<li class=""><strong>Typed APIs.</strong> A wrong method name or argument is a compiler error the assistant can read and fix, not a silent runtime surprise.</li>
<li class=""><strong>A small, consistent set of patterns.</strong> Fewer ways to do the same thing means fewer divergent answers.</li>
<li class=""><strong>Grounding.</strong> A way for the assistant to look up real signatures instead of recalling them.</li>
</ol>
<p>webforJ is built around the first three, and ships the fourth.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="java-all-the-way-down">Java all the way down<a href="https://docs.webforj.com/zh/blog/java-framework-for-ai-coding#java-all-the-way-down" class="hash-link" aria-label="直接链接到 Java all the way down" title="直接链接到 Java all the way down" translate="no">​</a></h2>
<p>In webforJ you compose the UI imperatively in Java. There is no JSX and no HTML template to keep in step with your code. A view is a class, components are objects, and events are lambdas. This is the hello world view from the <a class="" href="https://docs.webforj.com/zh/docs/introduction/basics">webforJ basics</a> page:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Route</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"/"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">HelloWorldView</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">extends</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Composite</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">FlexLayout</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">FlexLayout</span><span class="token plain"> self </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">getBoundComponent</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token plain"> hello </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"What is your name?"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token plain"> btn </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Say Hello"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">HelloWorldView</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    self</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setDirection</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">FlexDirection</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">COLUMN</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    self</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setMaxWidth</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token number" style="color:var(--dwc-code-number)">300</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    self</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setStyle</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"margin"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"1em auto"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    btn</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setTheme</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">ButtonTheme</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">PRIMARY</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">addClickListener</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">e </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">          </span><span class="token class-name" style="color:var(--dwc-code-class)">Toast</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">show</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Welcome to webforJ Starter "</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> hello</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getValue</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"!"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Theme</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">GRAY</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    self</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">hello</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> btn</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>For an assistant this is the easy case. The layout, the state and the behavior sit in one file, in plain Java. If it invents <code>btn.onPress(...)</code>, <code>javac</code> says so immediately, and the assistant fixes it in the next turn without you reading a stack trace.</p>
<p>The server-side model helps too. The app runs on the server and webforJ handles the client traffic, so the code the assistant writes is ordinary Java calling ordinary Java methods. It does not have to design a REST contract between a front end and a back end just to show a table. If that idea is new to you, our post on the <a class="" href="https://docs.webforj.com/zh/blog/java-full-stack-framework">Java full stack framework</a> walks through it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="few-patterns">Few patterns, so fewer wrong answers<a href="https://docs.webforj.com/zh/blog/java-framework-for-ai-coding#few-patterns" class="hash-link" aria-label="直接链接到 Few patterns, so fewer wrong answers" title="直接链接到 Few patterns, so fewer wrong answers" translate="no">​</a></h2>
<p>Consistency matters as much as typing. Most webforJ component classes are <code>final</code>, and the framework steers you toward <a class="" href="https://docs.webforj.com/zh/docs/architecture/controls-components">composites</a> instead of subclassing. Every custom piece of UI is built the same way: a class that wraps other components.</p>
<p>That sounds like a restriction for humans, but it is a gift to a model. There is one answer to "how do I make a reusable address card" and not four inheritance variants that behave differently. The code an assistant generates for your app looks like the code it generated last week, and like the code your team writes by hand. Code review gets easier for the same reason, because every reviewer already knows the shape of a webforJ view.</p>
<p>It also keeps prompts short. You do not need to explain a house style for components, because the framework already enforces one.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="mcp-and-skills">Grounding with the MCP server and skills<a href="https://docs.webforj.com/zh/blog/java-framework-for-ai-coding#mcp-and-skills" class="hash-link" aria-label="直接链接到 Grounding with the MCP server and skills" title="直接链接到 Grounding with the MCP server and skills" translate="no">​</a></h2>
<p>Types catch wrong code. Grounding prevents it. The <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/mcp">webforJ MCP server</a> lets an assistant:</p>
<ul>
<li class="">search the webforJ docs and API instead of hallucinating method names</li>
<li class="">scaffold new projects from the official Maven archetypes</li>
<li class="">generate DWC themes from a brand color</li>
<li class="">read the styling surface of a component and validate any <code>--dwc-*</code> token before it reaches your CSS</li>
</ul>
<p>On top of that, <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/agent-skills">Agent Skills</a> give the assistant a playbook for the task in front of it. The server answers questions, and the skills say when to ask them, which pattern fits and how to check the result. Skills ship for building forms, creating components, styling, securing routes, localization, timers and async work, servlets, and version upgrades.</p>
<p>The <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling">webforJ AI plugin</a> installs both in one step. For Claude Code it is two commands:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">claude plugin marketplace </span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token plain"> webforj/webforj-ai</span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">claude plugin </span><span class="token function" style="color:var(--dwc-code-function)">install</span><span class="token plain"> webforj@webforj-ai</span><br></div></code></pre></div></div>
<p>Copilot CLI, VS Code with Copilot, Gemini CLI and Codex CLI have their own install steps on the same page, and Cursor and other Agent Skills clients can be configured manually. We first covered the MCP server in <a class="" href="https://docs.webforj.com/zh/blog/mcp">webforJ, meet Claude</a>.</p>
<p>One habit helps: mention <strong>webforJ</strong> in your prompt. That is the cue the assistant uses to reach for the plugin instead of general Java knowledge.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-it-compares">Best Java framework for AI coding<a href="https://docs.webforj.com/zh/blog/java-framework-for-ai-coding#how-it-compares" class="hash-link" aria-label="直接链接到 Best Java framework for AI coding" title="直接链接到 Best Java framework for AI coding" translate="no">​</a></h2>
<div class="table-wrapper"><div class="table-container" style="overflow-x:auto"><table><thead><tr><th>What the assistant needs</th><th>Typical multi-layer web stack</th><th>webforJ</th></tr></thead><tbody><tr><td>Languages per screen</td><td>Several (markup, style, script, Java)</td><td>Java</td></tr><tr><td>Wrong API call</td><td>Often found at runtime</td><td>Compiler error</td></tr><tr><td>Docs access</td><td>Training data or generic web search</td><td>Official MCP server</td></tr><tr><td>Task playbooks</td><td>None, or written by you</td><td>Agent Skills for common tasks</td></tr><tr><td>Ways to build a reusable component</td><td>Many</td><td>Composites</td></tr></tbody></table></div><style data-emotion="css 1k371a6">@media print{.css-1k371a6{position:absolute!important;}}</style></div>
<p>The pattern behind the table is simple. Every row is a place where a model would otherwise guess, and webforJ replaces the guess with a compiler check, a lookup, or a single agreed pattern. That is the case for webforJ as the Java framework to hand to an assistant.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next-step">Try it on a small screen<a href="https://docs.webforj.com/zh/blog/java-framework-for-ai-coding#next-step" class="hash-link" aria-label="直接链接到 Try it on a small screen" title="直接链接到 Try it on a small screen" translate="no">​</a></h2>
<p>Scaffold a project at <a href="https://docs.webforj.com/startforj" target="_blank" rel="noopener noreferrer" class="">startforJ</a>, install the plugin, and ask your assistant for a form with a few fields and a button, mentioning webforJ in the prompt. The <a class="" href="https://docs.webforj.com/zh/docs/introduction/getting-started">getting started guide</a> covers running the result.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">Frequently asked questions<a href="https://docs.webforj.com/zh/blog/java-framework-for-ai-coding#faq" class="hash-link" aria-label="直接链接到 Frequently asked questions" title="直接链接到 Frequently asked questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="which-java-ui-framework-works-with-an-llm">Which Java UI framework works with an LLM?<a href="https://docs.webforj.com/zh/blog/java-framework-for-ai-coding#which-java-ui-framework-works-with-an-llm" class="hash-link" aria-label="直接链接到 Which Java UI framework works with an LLM?" title="直接链接到 Which Java UI framework works with an LLM?" translate="no">​</a></h3>
<p>Any of them can be driven by an assistant, but webforJ is the one with first-party tooling for it. The plugin works with Claude Code, GitHub Copilot, Cursor, Gemini CLI and Codex, and it connects them to the webforJ docs, scaffolding, and styling tools.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="is-webforj-ai-friendly-without-the-plugin">Is webforJ AI-friendly without the plugin?<a href="https://docs.webforj.com/zh/blog/java-framework-for-ai-coding#is-webforj-ai-friendly-without-the-plugin" class="hash-link" aria-label="直接链接到 Is webforJ AI-friendly without the plugin?" title="直接链接到 Is webforJ AI-friendly without the plugin?" translate="no">​</a></h3>
<p>Yes, in the sense that matters most. The UI is Java, the APIs are typed and the patterns are consistent, so generated code is checked by the compiler. The plugin adds lookup and workflows on top, which improves accuracy further.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="can-ai-generated-code-be-trusted">Can AI-generated webforJ code be trusted in production?<a href="https://docs.webforj.com/zh/blog/java-framework-for-ai-coding#can-ai-generated-code-be-trusted" class="hash-link" aria-label="直接链接到 Can AI-generated webforJ code be trusted in production?" title="直接链接到 Can AI-generated webforJ code be trusted in production?" translate="no">​</a></h3>
<p>Treat it like any code from a new teammate. Compile it, run it, and review it. The compiler catches wrong calls, and the MCP server reduces them in the first place, but you should still test generated code before shipping.</p><div></div>]]></content>
        <author>
            <name>webforJ</name>
        </author>
        <category label="AI" term="AI"/>
        <category label="MCP" term="MCP"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Advanced RAG with Spring AI: test each step before you add it]]></title>
        <id>https://docs.webforj.com/zh/blog/advanced-rag-java</id>
        <link href="https://docs.webforj.com/zh/blog/advanced-rag-java"/>
        <updated>2026-11-05T13:00:00.000Z</updated>
        <summary type="html"><![CDATA[Advanced RAG in Java with Spring AI and webforJ: measure retrieval first, then add query rewriting, hybrid search and reranking only where they move the numbers.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-11-05-advanced-rag-java/cover-v2.jpg" alt="cover image" class="img_ev3q"></p>
<p>Advanced RAG in Java means fixing retrieval before touching the prompt. Spring AI gives you every stage you need: query rewriting, multi-query expansion, a custom retriever for hybrid search, and a post-processor for reranking, all inside one <code>RetrievalAugmentationAdvisor</code>. Each stage also costs model calls and seconds of waiting, so the real skill is knowing which ones your data needs. This post builds the full pipeline, measures it, and puts it behind a webforJ view that shows what every stage did.</p>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>Run the finished app</div><div class="admonitionContent_BuS1"><p>The complete project, <a href="https://github.com/webforj/built-with-webforj/tree/main/webforj-advancedrag" target="_blank" rel="noopener noreferrer" class="">Advanced RAG</a>, is published on GitHub, so you can view the code and run it with your own Gemini API key.</p></div></div>
<div style="text-align:center"><p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-11-05-advanced-rag-java/advancedrag.png" alt="Advanced RAG app showing each pipeline stage for an error code question: rewrite, expand, hybrid search, rerank and answer" class="img_ev3q"></p></div>
<div class="cta_UErl" role="complementary" aria-label="Already running Spring Boot?"><div class="copy_XH8h"><p class="title_HfaR">Already running Spring Boot?</p><p class="text_x4Wj">Ask the team how webforJ fits into your existing Spring app.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="https://webforj.com/schedule-discovery/" target="_blank" rel="noopener noreferrer">预约通话<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a><a class="secondary_GDTR" href="mailto:sales@basis.cloud?subject=webforJ%20with%20Spring%20Boot&amp;body=Hi%20webforJ%20team%2C%0A%0A%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->邮件咨询</a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-basic-rag-fails">Why basic RAG returns the wrong chunks<a href="https://docs.webforj.com/zh/blog/advanced-rag-java#why-basic-rag-fails" class="hash-link" aria-label="直接链接到 Why basic RAG returns the wrong chunks" title="直接链接到 Why basic RAG returns the wrong chunks" translate="no">​</a></h2>
<p><a class="" href="https://docs.webforj.com/zh/blog/spring-ai-rag">A basic RAG setup</a> embeds the user's question, asks the vector store for the top few chunks, and pastes them into the prompt. That works in a demo where the question resembles the document text. Real users break it in predictable ways.</p>
<ul>
<li class=""><strong>Chatty questions.</strong> <em>And what about the second one?</em> has no searchable content on its own.</li>
<li class=""><strong>Vocabulary mismatch.</strong> The user writes <em>cancel</em> and the manual says <em>revoke</em>. Embeddings often bridge that gap, but not every time.</li>
<li class=""><strong>Exact terms.</strong> Error codes, product SKUs and class names are where smaller or older embedding models slip. A keyword index matches them exactly.</li>
<li class=""><strong>Weak ordering.</strong> The right chunk is in the top 20 but not in the top 3, so the model never sees it.</li>
</ul>
<p>Each failure has a matching stage. Whether your data has the failure is a separate question, and the answer is often no. So the first thing to build is a way to tell.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="measure-first">Measure retrieval before tuning<a href="https://docs.webforj.com/zh/blog/advanced-rag-java#measure-first" class="hash-link" aria-label="直接链接到 Measure retrieval before tuning" title="直接链接到 Measure retrieval before tuning" translate="no">​</a></h2>
<p>Write down 20 to 30 real questions, each paired with the id of the chunk that should answer it. Then score retrieval, not the final answer. For every question, record where the right chunk lands in the results. A good answer can hide a bad search when the model fills gaps from its own knowledge, so the rank is the honest number.</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token keyword" style="color:var(--dwc-code-keyword)">record</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Case</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> question</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> expectedId</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">static</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">int</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">rank</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Document</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> results</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> expectedId</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">for</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">int</span><span class="token plain"> i </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token number" style="color:var(--dwc-code-number)">0</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"> i </span><span class="token operator" style="color:var(--dwc-code-operator)">&lt;</span><span class="token plain"> results</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">size</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"> i</span><span class="token operator" style="color:var(--dwc-code-operator)">++</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">results</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">get</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">i</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getId</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">equals</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">expectedId</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token plain"> i </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> </span><span class="token number" style="color:var(--dwc-code-number)">1</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token plain"> </span><span class="token number" style="color:var(--dwc-code-number)">0</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">static</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">compare</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Case</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> cases</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">VectorStore</span><span class="token plain"> vectorStore</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">DocumentRetriever</span><span class="token plain"> hybrid</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">for</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Case</span><span class="token plain"> test </span><span class="token operator" style="color:var(--dwc-code-operator)">:</span><span class="token plain"> cases</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">int</span><span class="token plain"> vector </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">rank</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">vectorStore</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">similaritySearch</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token class-name" style="color:var(--dwc-code-class)">SearchRequest</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">query</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">test</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">question</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">topK</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token number" style="color:var(--dwc-code-number)">20</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">build</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> test</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">expectedId</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">int</span><span class="token plain"> fused </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">rank</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">hybrid</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">retrieve</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Query</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">test</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">question</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> test</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">expectedId</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">System</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token plain">out</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">printf</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"%s vector=#%d hybrid=#%d%n"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> test</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">question</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> vector</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> fused</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>Scoring only the search keeps the run cheap. It needs embedding calls for the questions and no chat calls at all.</p>
<p>I ran this against a manual of 200 generated entries built to be hard for embeddings. Every entry is an error code from the same narrow range, E-4000 to E-4999, with near-identical wording such as <em>Error E-4517: the file upload timed out. Restart the job.</em> Twenty questions asked for a random code, for example <em>What should I do about error E-4517?</em> With <code>gemini-embedding-001</code>, plain vector search ranked the right entry first for 20 of 20 questions. Hybrid search also scored 20 of 20, so on this data it added nothing.</p>
<p>That doesn't make hybrid search useless. Your codes may be longer, your chunks noisier, or your embedding model smaller. It does mean the measurement comes before the stage, not after.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="rewrite-and-expand">Rewrite and expand the query<a href="https://docs.webforj.com/zh/blog/advanced-rag-java#rewrite-and-expand" class="hash-link" aria-label="直接链接到 Rewrite and expand the query" title="直接链接到 Rewrite and expand the query" translate="no">​</a></h2>
<p>The cheapest improvement happens before the search. Spring AI ships two pieces for it in <code>spring-ai-rag</code>. <code>RewriteQueryTransformer</code> turns the raw question into a better search query, and <code>MultiQueryExpander</code> produces several differently worded versions of it so one unlucky phrasing doesn't decide the result. The <a href="https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html" target="_blank" rel="noopener noreferrer" class="">Spring AI RAG reference</a> describes both.</p>
<p>Each one takes a <code>ChatClient.Builder</code>, so each costs one extra model call per question. I'd use a small, fast model for these two stages. They only rephrase text and don't need your best model.</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token class-name" style="color:var(--dwc-code-class)">RewriteQueryTransformer</span><span class="token plain"> rewrite </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">RewriteQueryTransformer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">chatClientBuilder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">clone</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">build</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token class-name" style="color:var(--dwc-code-class)">MultiQueryExpander</span><span class="token plain"> expander </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">MultiQueryExpander</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">chatClientBuilder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">clone</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">numberOfQueries</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token number" style="color:var(--dwc-code-number)">3</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">includeOriginal</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">true</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">build</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><br></div></code></pre></div></div>
<p><code>includeOriginal(true)</code> keeps the user's own wording in the mix, which I recommend. Rewrites occasionally drift, and the original query is your safety net.</p>
<p>The trade-off is latency. In one run with <code>gemini-3.5-flash-lite</code>, the rewrite took about a second and the expansion about 0.6 seconds, all before retrieval starts. Add these stages when your test questions are worded differently from your documents and the right chunk ranks low, and check the rank again afterwards.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="hybrid-search">Hybrid search for advanced RAG in Java<a href="https://docs.webforj.com/zh/blog/advanced-rag-java#hybrid-search" class="hash-link" aria-label="直接链接到 Hybrid search for advanced RAG in Java" title="直接链接到 Hybrid search for advanced RAG in Java" translate="no">​</a></h2>
<p>Vector search finds meaning. Keyword search finds exact strings. Hybrid search runs both and merges the two ranked lists. It is worth testing when your corpus is full of identifiers, codes or product names and your measurement shows the embedding model missing them.</p>
<p>Spring AI's <code>VectorStoreDocumentRetriever</code> covers the vector half. For the hybrid half you implement <code>DocumentRetriever</code>, a one-method interface, and fuse the results yourself with reciprocal rank fusion (RRF). RRF scores each document as the sum of <code>1 / (k + rank)</code> across the lists it appears in, so a document ranked well by both searches beats one ranked first by only one. The method comes from <a href="https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf" target="_blank" rel="noopener noreferrer" class="">Cormack, Clarke and Buettcher's 2009 paper</a>, and <code>k = 60</code> is the value that paper used.</p>
<p><code>KeywordSearch</code> below is a small interface of your own. Back it with whatever your data already lives in, such as PostgreSQL full-text search or a Lucene index.</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">HybridRetriever</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">implements</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">DocumentRetriever</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">static</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">int</span><span class="token plain"> </span><span class="token constant" style="color:var(--dwc-code-number)">RRF_K</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token number" style="color:var(--dwc-code-number)">60</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">static</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">int</span><span class="token plain"> </span><span class="token constant" style="color:var(--dwc-code-number)">CANDIDATES</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token number" style="color:var(--dwc-code-number)">20</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">static</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">int</span><span class="token plain"> </span><span class="token constant" style="color:var(--dwc-code-number)">TOP</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token number" style="color:var(--dwc-code-number)">10</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">VectorStore</span><span class="token plain"> vectorStore</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">KeywordSearch</span><span class="token plain"> keywordSearch</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">HybridRetriever</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">VectorStore</span><span class="token plain"> vectorStore</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">KeywordSearch</span><span class="token plain"> keywordSearch</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">this</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token plain">vectorStore </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> vectorStore</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">this</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token plain">keywordSearch </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> keywordSearch</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Override</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Document</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">retrieve</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Query</span><span class="token plain"> query</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Document</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> semantic </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> vectorStore</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">similaritySearch</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token class-name" style="color:var(--dwc-code-class)">SearchRequest</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">query</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">query</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">text</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">topK</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token constant" style="color:var(--dwc-code-number)">CANDIDATES</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">build</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Document</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> keyword </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> keywordSearch</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">search</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">query</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">text</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token constant" style="color:var(--dwc-code-number)">CANDIDATES</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">Map</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">String</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token generics"> </span><span class="token generics class-name" style="color:var(--dwc-code-class)">Double</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> scores </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">LinkedHashMap</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">Map</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">String</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token generics"> </span><span class="token generics class-name" style="color:var(--dwc-code-class)">Document</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> byId </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">LinkedHashMap</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">for</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Document</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> ranking </span><span class="token operator" style="color:var(--dwc-code-operator)">:</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">of</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">semantic</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> keyword</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token keyword" style="color:var(--dwc-code-keyword)">for</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">int</span><span class="token plain"> rank </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token number" style="color:var(--dwc-code-number)">0</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"> rank </span><span class="token operator" style="color:var(--dwc-code-operator)">&lt;</span><span class="token plain"> ranking</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">size</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"> rank</span><span class="token operator" style="color:var(--dwc-code-operator)">++</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token class-name" style="color:var(--dwc-code-class)">Document</span><span class="token plain"> doc </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> ranking</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">get</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">rank</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        byId</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">putIfAbsent</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">doc</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getId</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> doc</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        scores</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">merge</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">doc</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getId</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token number" style="color:var(--dwc-code-number)">1.0</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">/</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token constant" style="color:var(--dwc-code-number)">RRF_K</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> rank </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> </span><span class="token number" style="color:var(--dwc-code-number)">1</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Double</span><span class="token operator" style="color:var(--dwc-code-operator)">::</span><span class="token function" style="color:var(--dwc-code-function)">sum</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Document</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> fused </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">ArrayList</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">for</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Document</span><span class="token plain"> doc </span><span class="token operator" style="color:var(--dwc-code-operator)">:</span><span class="token plain"> byId</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">values</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      fused</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">doc</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">mutate</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">score</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">scores</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">get</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">doc</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getId</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">build</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    fused</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">sort</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Comparator</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">comparingDouble</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Document</span><span class="token operator" style="color:var(--dwc-code-operator)">::</span><span class="token function" style="color:var(--dwc-code-function)">getScore</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">reversed</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token plain"> fused</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">size</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">&gt;</span><span class="token plain"> </span><span class="token constant" style="color:var(--dwc-code-number)">TOP</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">?</span><span class="token plain"> fused</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">subList</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token number" style="color:var(--dwc-code-number)">0</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token constant" style="color:var(--dwc-code-number)">TOP</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">:</span><span class="token plain"> fused</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>Three details matter here. Both searches must give the same chunk the same document id, otherwise RRF counts one chunk twice as two different documents. RRF only uses ranks, never raw scores, which is exactly why it works across systems whose scores can't be compared. A cosine similarity and a BM25 score live on different scales.</p>
<p>The third detail is easy to miss. After retrieval, the advisor merges the results of all queries with Spring AI's default <code>ConcatenationDocumentJoiner</code>, which sorts documents by <code>Document.getScore()</code>. If the retriever only reorders the list, the joiner sorts it back by vector similarity and keyword-only hits sink to the bottom. Writing the fused score onto each document with <code>mutate().score(...)</code> keeps the fusion alive through the join.</p>
<p>A weak keyword index can also add noise. A naive word match finds every chunk that contains <em>error</em>, and those chunks then take up places in the fused list. The retriever keeps the top ten per query, and cutting that down further is the next stage's job.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="rerank">Rerank before the model sees anything<a href="https://docs.webforj.com/zh/blog/advanced-rag-java#rerank" class="hash-link" aria-label="直接链接到 Rerank before the model sees anything" title="直接链接到 Rerank before the model sees anything" translate="no">​</a></h2>
<p>Retrieval is built for recall. It casts a wide net quickly. Reranking is built for precision, and it looks at the question and each candidate together, which a vector comparison can't do.</p>
<p>Spring AI models this stage as a <code>DocumentPostProcessor</code>, which receives the <code>Query</code> and the retrieved documents and returns the documents to keep. The advisor runs post-processors after retrieval and joining. You can plug in a dedicated reranking model through any HTTP client. For a self-contained version, ask your chat model to order the candidates:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">LlmReranker</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">implements</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">DocumentPostProcessor</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">ChatClient</span><span class="token plain"> chat</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">int</span><span class="token plain"> keep</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">LlmReranker</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">ChatClient</span><span class="token class-name punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token class-name" style="color:var(--dwc-code-class)">Builder</span><span class="token plain"> builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">int</span><span class="token plain"> keep</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">this</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token plain">chat </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">build</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">this</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token plain">keep </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> keep</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Override</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Document</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">process</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Query</span><span class="token plain"> query</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Document</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> documents</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">documents</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">size</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">&lt;=</span><span class="token plain"> keep</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token plain"> documents</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">StringBuilder</span><span class="token plain"> passages </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">StringBuilder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">for</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">int</span><span class="token plain"> i </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token number" style="color:var(--dwc-code-number)">0</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"> i </span><span class="token operator" style="color:var(--dwc-code-operator)">&lt;</span><span class="token plain"> documents</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">size</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"> i</span><span class="token operator" style="color:var(--dwc-code-operator)">++</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      passages</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">append</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"["</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">append</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">i</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">append</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"] "</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">          </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">append</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">documents</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">get</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">i</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">append</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"\n\n"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> reply </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> chat</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">prompt</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">system</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token triple-quoted-string string" style="color:var(--dwc-code-string)">"""</span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token triple-quoted-string string" style="color:var(--dwc-code-string)">            Rank the passages by how directly they answer the question.</span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token triple-quoted-string string" style="color:var(--dwc-code-string)">            Reply with the passage numbers only, best first, separated by commas.</span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token triple-quoted-string string" style="color:var(--dwc-code-string)">            """</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">user</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Question: "</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> query</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">text</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"\n\nPassages:\n"</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> passages</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">call</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">content</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Document</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> ranked </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">ArrayList</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">for</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> token </span><span class="token operator" style="color:var(--dwc-code-operator)">:</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">valueOf</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">reply</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">split</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"[^0-9]+"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token keyword" style="color:var(--dwc-code-keyword)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">token</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">isEmpty</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">||</span><span class="token plain"> token</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">length</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">&gt;</span><span class="token plain"> </span><span class="token number" style="color:var(--dwc-code-number)">4</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token keyword" style="color:var(--dwc-code-keyword)">continue</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token keyword" style="color:var(--dwc-code-keyword)">int</span><span class="token plain"> index </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Integer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">parseInt</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">token</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token keyword" style="color:var(--dwc-code-keyword)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">index </span><span class="token operator" style="color:var(--dwc-code-operator)">&lt;</span><span class="token plain"> documents</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">size</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">&amp;&amp;</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">!</span><span class="token plain">ranked</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">contains</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">documents</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">get</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">index</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        ranked</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">documents</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">get</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">index</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">ranked</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">isEmpty</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token plain"> documents</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">subList</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token number" style="color:var(--dwc-code-number)">0</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> keep</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token plain"> ranked</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">size</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">&gt;</span><span class="token plain"> keep </span><span class="token operator" style="color:var(--dwc-code-operator)">?</span><span class="token plain"> ranked</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">subList</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token number" style="color:var(--dwc-code-number)">0</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> keep</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">:</span><span class="token plain"> ranked</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>The parsing is deliberately forgiving. Models add stray words, repeat numbers or invent an index, so the loop ignores anything that isn't a valid, unseen passage number, and skips digit runs too long to be an index before <code>Integer.parseInt</code> can overflow. <code>keep</code> is a maximum, not a promise. In one run the model listed a single passage out of 15 candidates, so the answer was built from one chunk. When the reply holds no usable number at all, or no reply comes back, the method keeps the first <code>keep</code> candidates in retrieval order instead of sending the model an empty context.</p>
<p>Sending 20 passages to a model for ranking is not free. Keep the candidate count modest and the passages short. A purpose-built reranker model is cheaper and faster per call than a general chat model, so treat this version as a starting point you can swap out behind the same interface.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wire-the-pipeline">Wire the pipeline together<a href="https://docs.webforj.com/zh/blog/advanced-rag-java#wire-the-pipeline" class="hash-link" aria-label="直接链接到 Wire the pipeline together" title="直接链接到 Wire the pipeline together" translate="no">​</a></h2>
<p><code>RetrievalAugmentationAdvisor</code> takes every stage through its builder. The order of the calls doesn't matter, because the advisor always runs the stages in the same sequence: transform, expand, retrieve, join, post-process, augment.</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Configuration</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">RagConfig</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Bean</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token class-name" style="color:var(--dwc-code-class)">RetrievalAugmentationAdvisor</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">ragAdvisor</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">ChatClient</span><span class="token class-name punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token class-name" style="color:var(--dwc-code-class)">Builder</span><span class="token plain"> builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">VectorStore</span><span class="token plain"> vectorStore</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token class-name" style="color:var(--dwc-code-class)">KeywordSearch</span><span class="token plain"> keywordSearch</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">RetrievalAugmentationAdvisor</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">queryTransformers</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">RewriteQueryTransformer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">            </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">chatClientBuilder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">clone</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">            </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">build</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">queryExpander</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">MultiQueryExpander</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">            </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">chatClientBuilder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">clone</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">            </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">numberOfQueries</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token number" style="color:var(--dwc-code-number)">3</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">            </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">includeOriginal</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">true</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">            </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">build</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">documentRetriever</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">HybridRetriever</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">vectorStore</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> keywordSearch</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">documentPostProcessors</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">LlmReranker</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">clone</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token number" style="color:var(--dwc-code-number)">5</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">build</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>Because the advisor is a bean, every <code>ChatClient</code> that registers it gets the full pipeline. Count what one question now costs. There are four model calls: the rewrite, the expansion, the rerank, and the answer. Retrieval runs once for each of the four queries, so the pipeline makes eight searches, four by vector and four by keyword, and each vector search embeds its query first.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="see-each-stage">See what each stage did<a href="https://docs.webforj.com/zh/blog/advanced-rag-java#see-each-stage" class="hash-link" aria-label="直接链接到 See what each stage did" title="直接链接到 See what each stage did" translate="no">​</a></h2>
<p>A pipeline this long is hard to trust when all you see is the final answer. Each stage is a small interface, so you can wrap it, record what it received and returned, and pass the call on unchanged:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token keyword" style="color:var(--dwc-code-keyword)">static</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">QueryTransformer</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">recorded</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">QueryTransformer</span><span class="token plain"> stage</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">String</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> log</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token plain"> query </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">Query</span><span class="token plain"> result </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> stage</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">transform</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">query</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    log</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Rewrite: "</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> query</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">text</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">" -&gt; "</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> result</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">text</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token plain"> result</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">static</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">DocumentPostProcessor</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">recorded</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">DocumentPostProcessor</span><span class="token plain"> stage</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">String</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> log</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">query</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> documents</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Document</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> kept </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> stage</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">process</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">query</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> documents</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    log</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Rerank: kept "</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> kept</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">size</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">" of "</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> documents</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">size</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token plain"> kept</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>Wrap the expander, the retriever, and the vector store and keyword index inside <code>HybridRetriever</code> the same way, and build the advisor per question instead of once as a bean, so each run gets its own record. Retrieval can run on several threads, one per expanded query, so use a thread-safe list.</p>
<p>In webforJ the record goes straight into the page next to the answer. Each step becomes a row with its time and the chunk ids it returned, and the ids the answer used get highlighted in every step. You can follow one chunk from the vector results through the fusion and the rerank into the answer. When a test question fails, the view shows the stage that lost the right chunk.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="background-pipeline">Keep the slow pipeline off the UI thread<a href="https://docs.webforj.com/zh/blog/advanced-rag-java#background-pipeline" class="hash-link" aria-label="直接链接到 Keep the slow pipeline off the UI thread" title="直接链接到 Keep the slow pipeline off the UI thread" translate="no">​</a></h2>
<p>Four model calls and eight searches per question take seconds. If a click handler in a webforJ view does that work directly, the view is blocked until it finishes. The fix is the pattern from the <a class="" href="https://docs.webforj.com/zh/docs/integrations/spring/background-jobs">background jobs guide</a>: annotate the service method with <code>@Async</code>, return a <code>CompletableFuture</code>, read the <code>Environment</code> before the call and hand the result back to the UI thread with <code>Environment.runLater()</code>.</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Service</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">DocsAssistant</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">ChatClient</span><span class="token plain"> chat</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">DocsAssistant</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">ChatClient</span><span class="token class-name punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token class-name" style="color:var(--dwc-code-class)">Builder</span><span class="token plain"> builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">RetrievalAugmentationAdvisor</span><span class="token plain"> advisor</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">this</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token plain">chat </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">defaultAdvisors</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">advisor</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">build</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Async</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">CompletableFuture</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">String</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">ask</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> question</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">CompletableFuture</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">completedFuture</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        chat</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">prompt</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">user</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">question</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">call</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">content</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p><code>@Async</code> only takes effect once you add <code>@EnableAsync</code> to your Spring Boot app class, as the guide describes. The view calls the service, and the completion callback updates the page:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Route</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"/ask"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">AskView</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">extends</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Composite</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">FlexLayout</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">DocsAssistant</span><span class="token plain"> assistant</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">MarkdownViewer</span><span class="token plain"> answer </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">MarkdownViewer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token plain"> question </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token plain"> ask </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Ask"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">ButtonTheme</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">PRIMARY</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">CompletableFuture</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">String</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> current</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">boolean</span><span class="token plain"> destroyed</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">AskView</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">DocsAssistant</span><span class="token plain"> assistant</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">this</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token plain">assistant </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> assistant</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token function" style="color:var(--dwc-code-function)">getBoundComponent</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setDirection</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">FlexDirection</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">COLUMN</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">question</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> ask</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> answer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    ask</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">onClick</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">e </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">run</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">run</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    ask</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setEnabled</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">false</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    answer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setContent</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Searching the documentation..."</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">Environment</span><span class="token plain"> env </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Environment</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getCurrent</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    current </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> assistant</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">ask</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">question</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getValue</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    current</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">whenComplete</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">text</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> error</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Environment</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">runLater</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">env</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token keyword" style="color:var(--dwc-code-keyword)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">destroyed</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      answer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setContent</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">error </span><span class="token operator" style="color:var(--dwc-code-operator)">==</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">null</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">?</span><span class="token plain"> text </span><span class="token operator" style="color:var(--dwc-code-operator)">:</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"The search failed. Try again."</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      ask</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setEnabled</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">true</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Override</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">protected</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">onDestroy</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    destroyed </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token boolean" style="color:var(--dwc-code-number)">true</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">current </span><span class="token operator" style="color:var(--dwc-code-operator)">!=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">null</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">&amp;&amp;</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">!</span><span class="token plain">current</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">isDone</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      current</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">cancel</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">true</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">super</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">onDestroy</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>The <code>whenComplete</code> callback runs on Spring's thread pool, which serves every user, so every component call inside it is wrapped in <code>Environment.runLater()</code> with the <code>Environment</code> that <code>run()</code> read on the UI thread. That rule is explained in <a class="" href="https://docs.webforj.com/zh/docs/advanced/asynchronous-updates">Asynchronous Updates</a>. The <code>onDestroy()</code> override cancels the future when the user leaves the view. Cancelling a <code>CompletableFuture</code> doesn't interrupt the thread behind it, so the model calls already in flight still finish. What the cancel does is stop the view from waiting. It also runs the <code>whenComplete</code> callback at once, which is why the <code>destroyed</code> flag is set first, so the callback never touches components that are going away.</p>
<p>The <a class="" href="https://docs.webforj.com/zh/docs/components/markdownviewer"><code>MarkdownViewer</code></a> renders the model's markdown answer, including lists and code blocks, with one <code>setContent()</code> call. If you'd rather stream the final generation token by token, the same component has <code>append()</code>, auto-scroll and progressive rendering. The retrieval stages still finish before the first token arrives, which is why the placeholder text in the previous snippet matters. A user who sees a progress message waits more patiently than one staring at a frozen page.</p>
<p>All of this is one Spring Boot app. The retrieval pipeline, the service and the view live in the same Java project, and there is no endpoint to design between them. The <a class="" href="https://docs.webforj.com/zh/docs/integrations/spring/overview">Spring integration overview</a> covers how webforJ and Spring Boot fit together.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="when-to-skip">When to skip a stage<a href="https://docs.webforj.com/zh/blog/advanced-rag-java#when-to-skip" class="hash-link" aria-label="直接链接到 When to skip a stage" title="直接链接到 When to skip a stage" translate="no">​</a></h2>
<p>Every stage adds latency and model cost. Add one only when your test questions show the failure it fixes, and keep it only if the right chunk ranks higher afterwards.</p>
<div class="table-wrapper"><div class="table-container" style="overflow-x:auto"><table><thead><tr><th>Symptom in your test questions</th><th>Try</th></tr></thead><tbody><tr><td>Codes, SKUs or class names rank low with vector search</td><td>Hybrid search</td></tr><tr><td>Follow-up questions retrieve nothing useful</td><td><code>CompressionQueryTransformer</code> with the chat history</td></tr><tr><td>Questions use other words than the documents</td><td>Query rewriting, then multi-query expansion</td></tr><tr><td>Right chunk is retrieved but ranked below the cut</td><td>Reranking</td></tr></tbody></table></div><style data-emotion="css 1k371a6">@media print{.css-1k371a6{position:absolute!important;}}</style></div>
<p>Spring AI's <code>CompressionQueryTransformer</code> folds the conversation history into a single standalone query. That fixes the "what about the second one?" problem better than a plain rewrite.</p>
<p>Every row starts from a symptom, not from the kind of data. Having identifiers in your documents doesn't call for keyword search on its own. In the error-code test earlier in this post, vector search already found every code. Your data decides, and the measurement takes an afternoon.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next-step">Next step<a href="https://docs.webforj.com/zh/blog/advanced-rag-java#next-step" class="hash-link" aria-label="直接链接到 Next step" title="直接链接到 Next step" translate="no">​</a></h2>
<p>Start a project from <a href="https://docs.webforj.com/startforj" target="_blank" rel="noopener noreferrer" class="">startforJ</a> or follow <a class="" href="https://docs.webforj.com/zh/docs/introduction/getting-started">Getting started</a>, add the Spring AI RAG dependency and your model starter, and write the test questions before any stage. Run them against plain vector search first. The ranks tell you which section of this post to build next, if any.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">Frequently asked questions<a href="https://docs.webforj.com/zh/blog/advanced-rag-java#faq" class="hash-link" aria-label="直接链接到 Frequently asked questions" title="直接链接到 Frequently asked questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-reranking">What is reranking in RAG and does Spring AI support it?<a href="https://docs.webforj.com/zh/blog/advanced-rag-java#faq-reranking" class="hash-link" aria-label="直接链接到 What is reranking in RAG and does Spring AI support it?" title="直接链接到 What is reranking in RAG and does Spring AI support it?" translate="no">​</a></h3>
<p>Reranking reorders the retrieved chunks by how well each one answers the question, then keeps only the best few. Spring AI supports it through the <code>DocumentPostProcessor</code> interface, which the <code>RetrievalAugmentationAdvisor</code> runs after retrieval. You implement the scoring yourself with a chat model or a dedicated reranking model.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-hybrid-search">How do I do hybrid search for RAG in Java?<a href="https://docs.webforj.com/zh/blog/advanced-rag-java#faq-hybrid-search" class="hash-link" aria-label="直接链接到 How do I do hybrid search for RAG in Java?" title="直接链接到 How do I do hybrid search for RAG in Java?" translate="no">​</a></h3>
<p>Implement Spring AI's <code>DocumentRetriever</code> so it queries both your vector store and a keyword index, then merge the two rankings with reciprocal rank fusion. Make sure both sources use the same document ids. Measure plain vector search first, because a current embedding model may already find your exact terms.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-accuracy">How can I improve RAG accuracy in Java without changing the model?<a href="https://docs.webforj.com/zh/blog/advanced-rag-java#faq-accuracy" class="hash-link" aria-label="直接链接到 How can I improve RAG accuracy in Java without changing the model?" title="直接链接到 How can I improve RAG accuracy in Java without changing the model?" translate="no">​</a></h3>
<p>Work on retrieval first, and measure it. Pair real questions with the chunk that should answer them, record the rank of that chunk, and add rewriting, hybrid search or reranking only where the rank is poor. Keep a stage only if the rank improves.</p><div></div>]]></content>
        <author>
            <name>Hyyan Abo Fakher</name>
        </author>
        <category label="Integrations" term="Integrations"/>
        <category label="Spring" term="Spring"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[AI tools for Java migration: pick by the job, not the brand]]></title>
        <id>https://docs.webforj.com/zh/blog/ai-tools-java-migration</id>
        <link href="https://docs.webforj.com/zh/blog/ai-tools-java-migration"/>
        <updated>2026-11-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The best AI tools for Java migration depend on the job: reading legacy code, upgrades, UI ports and tests. See what grounds each one and why the target matters.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-11-05-ai-tools-java-migration/cover-v2.jpg" alt="cover image" class="img_ev3q"></p>
<p>You have a Java app that has been in production for years, a deadline that keeps moving, and a long list of AI tools that all claim they can help. Comparing them by brand does not get you far, because a migration is not one task. It is five or six different jobs, and each one needs a different kind of help.</p>
<p>The best AI tools for Java migration are coding assistants such as Claude Code, GitHub Copilot, Cursor, Gemini CLI and Codex, matched to the job at hand and grounded in the real documentation of the framework you are moving to. That last part decides most of the outcome. With webforJ, the <a class="" href="https://docs.webforj.com/zh/blog/java-full-stack-framework">full-stack Java framework</a>, one AI plugin gives those assistants the real docs, project scaffolding and ready-made workflows, so the code they write for your new screens targets APIs that exist.</p>
<div class="cta_UErl" role="complementary" aria-label="Moving a desktop Java app to the web?"><div class="copy_XH8h"><p class="title_HfaR">Moving a desktop Java app to the web?</p><p class="text_x4Wj">Send us a screenshot of one screen and we'll reply with how it moves to webforJ.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="mailto:sales@basis.cloud?subject=My%20app%20screen%3A%20how%20would%20it%20move%20to%20webforJ%3F&amp;body=Hi%20webforJ%20team%2C%0A%0AAttached%20is%20a%20screenshot%20of%20one%20screen%20from%20our%20app.%20It%20is%20built%20with%3A%20(Swing%2C%20JavaFX%2C%20other)%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->发送截图</a><a class="secondary_GDTR" href="https://webforj.com/schedule-discovery/" target="_blank" rel="noopener noreferrer">预约通话<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="target-matters">Why the target beats the tool<a href="https://docs.webforj.com/zh/blog/ai-tools-java-migration#target-matters" class="hash-link" aria-label="直接链接到 Why the target beats the tool" title="直接链接到 Why the target beats the tool" translate="no">​</a></h2>
<p>Coding assistants work from models trained on a snapshot of public code. When you ask it to read your old code, it has all the evidence it needs, because the code is in front of it. When you ask it to write code for a framework, it has only its memory, and memory is where invented methods, wrong constructors, and renamed annotations come from.</p>
<p>That is why we think about AI for legacy Java in two halves. Reading the old system is a job the tool can do well with nothing but your repository. Writing the new system is a job whose quality depends on what the tool can look up while it works. A target framework that gives the assistant a source of truth turns a guessing game into a checked one.</p>
<p>None of this removes the reviewer. A person reads every change, runs the tests, and decides what ships.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="understanding-legacy-code">Job 1: understanding the legacy code<a href="https://docs.webforj.com/zh/blog/ai-tools-java-migration#understanding-legacy-code" class="hash-link" aria-label="直接链接到 Job 1: understanding the legacy code" title="直接链接到 Job 1: understanding the legacy code" translate="no">​</a></h2>
<p>Before anything moves, someone has to know what the old app does. This is the job where AI helps most with the least setup.</p>
<p>A coding assistant that can read a repository and run commands can list the screens, trace which classes a screen calls and show which database tables a feature reads. Ask for a screen inventory with the data each screen needs and the business rules it applies. That inventory becomes your migration backlog.</p>
<p>What grounds it here is your own code and your database schema. There is no framework gap to worry about yet. The weak spot is confidence. A summary can be fluent and still wrong, so spot-check it against the code for the screens that matter most, and keep the inventory in the repository where the team can correct it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="upgrading-versions">Job 2: upgrading versions<a href="https://docs.webforj.com/zh/blog/ai-tools-java-migration#upgrading-versions" class="hash-link" aria-label="直接链接到 Job 2: upgrading versions" title="直接链接到 Job 2: upgrading versions" translate="no">​</a></h2>
<p>Version upgrades are mostly mechanical, and mechanical work is better done by a recipe than by a model improvising. <a href="https://docs.openrewrite.org/" target="_blank" rel="noopener noreferrer" class="">OpenRewrite</a> is an automated refactoring project for Java. Its recipes rename types, change dependencies and rewrite deprecated calls the same way every time, and a dry run shows you the diff before anything changes.</p>
<p>The assistant's role is to drive the recipe and handle what is left. For projects already on webforJ, we publish <a class="" href="https://docs.webforj.com/zh/blog/openrewrite-java-framework-upgrade">upgrade recipes between webforJ versions</a>. You add the OpenRewrite Maven plugin with the <code>webforj-rewrite</code> dependency, then run <code>mvn rewrite:dryRun</code> to preview the patch and <code>mvn rewrite:run</code> to apply it:</p>
<div class="language-xml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-xml codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">plugin</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">groupId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">org.openrewrite.maven</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">groupId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">artifactId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">rewrite-maven-plugin</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">artifactId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">version</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">6.36.0</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">version</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">configuration</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">activeRecipes</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">recipe</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">RECIPE_NAME</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">recipe</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">activeRecipes</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">configuration</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">dependencies</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">dependency</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">groupId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">com.webforj</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">groupId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">artifactId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">webforj-rewrite</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">artifactId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">version</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">TARGET_VERSION</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">version</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">dependency</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">dependencies</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">plugin</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><br></div></code></pre></div></div>
<p>Where a one-to-one replacement does not exist, the recipe leaves a <code>TODO</code> comment with instructions, and those are the lines a person or an assistant resolves. Recipe names for each webforJ version are listed in <a class="" href="https://docs.webforj.com/zh/docs/upgrading/automated-upgrades">automated upgrades</a>, and the <code>webforj-upgrading-versions</code> Agent Skill in the <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/agent-skills">AI plugin</a> walks an assistant through the same flow, including resolving the <code>TODO</code> comments.</p>
<p>What grounds this job is the recipe, not the model. Use the assistant to run it, read the diff, and fix the leftovers. Do not ask it to rewrite deprecated calls from memory when a recipe exists.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="porting-screens">Job 3: porting screens to a web UI<a href="https://docs.webforj.com/zh/blog/ai-tools-java-migration#porting-screens" class="hash-link" aria-label="直接链接到 Job 3: porting screens to a web UI" title="直接链接到 Job 3: porting screens to a web UI" translate="no">​</a></h2>
<p>This is the job people mean when they ask about AI Java code migration, and it is the one with the biggest gap between a good and a bad result. The source is a Swing frame, a JSP page, a JSF view, or a Struts action. The target is a screen in a web UI built in Java.</p>
<p>An assistant can read the old screen, identify its fields, tables, buttons and event handlers, and write the equivalent in the new framework. Whether that code is right depends on whether the assistant knows the target. We cover the screen-by-screen method for desktop apps in <a class="" href="https://docs.webforj.com/zh/blog/swing-to-web-migration">Swing to web migration</a> and the decision between rebuilding and embedding in <a class="" href="https://docs.webforj.com/zh/blog/ai-modernize-java-desktop-app">how to migrate a Java desktop app with AI</a>. The short version is to keep the domain layer and move one screen at a time. For Swing, <a class="" href="https://docs.webforj.com/zh/docs/integrations/webswing/overview">Webswing integration</a> runs the existing app in the browser so users are not waiting for the last screen to be ported.</p>
<p>What grounds this job is the target framework's documentation, and that is where webforJ is built to help, which the next section covers.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="writing-tests">Job 4: writing tests for the ported code<a href="https://docs.webforj.com/zh/blog/ai-tools-java-migration#writing-tests" class="hash-link" aria-label="直接链接到 Job 4: writing tests for the ported code" title="直接链接到 Job 4: writing tests for the ported code" translate="no">​</a></h2>
<p>A port without tests is a port you cannot trust. Assistants are good at drafting test cases from a described behavior, such as "the Save button stays disabled until the name field has a value".</p>
<p>The practical limit is how tests run. Browser-based tests can be slow and flaky, which is hard on an agent that wants to run them after every change. webforJ's <a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/overview">TestBed</a>, available since 26.03, runs a view inside a plain JUnit test with no browser and no server. A test adds the view, acts on it the way a user does and checks the result from Java, and every interaction finishes before the next line runs. That makes it practical to ask an assistant to write a test, run <code>mvn test</code> and read the result in one loop. <a class="" href="https://docs.webforj.com/zh/blog/java-ui-testing-without-browser">Java UI testing without a browser</a> shows the structure of a test.</p>
<p>What grounds this job is the test run itself. A generated test that has never failed proves little, so make the assistant show you a red run before the green one.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="maintainable-result">Job 5: keeping the result maintainable<a href="https://docs.webforj.com/zh/blog/ai-tools-java-migration#maintainable-result" class="hash-link" aria-label="直接链接到 Job 5: keeping the result maintainable" title="直接链接到 Job 5: keeping the result maintainable" translate="no">​</a></h2>
<p>The last job is the one that gets skipped. A migration that ends in thousands of lines of unfamiliar generated code has traded one legacy system for another. AI helps with renaming for consistency, extracting repeated layout into reusable components and checking new code against conventions you write down once. Put the conventions in the repository, point the assistant at them, and review its output against them.</p>
<p>A target where a view is one Java class with the layout, events and state together gives a reviewer one place to read.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="job-by-tool">Which AI tool for which migration job<a href="https://docs.webforj.com/zh/blog/ai-tools-java-migration#job-by-tool" class="hash-link" aria-label="直接链接到 Which AI tool for which migration job" title="直接链接到 Which AI tool for which migration job" translate="no">​</a></h2>
<div class="table-wrapper"><div class="table-container" style="overflow-x:auto"><table><thead><tr><th>Job</th><th>Kind of tool</th><th>What grounds it</th><th>What a person checks</th></tr></thead><tbody><tr><td>Understand legacy code</td><td>Coding assistant with repository access</td><td>Your code and schema</td><td>Summaries against the source</td></tr><tr><td>Upgrade versions</td><td>Refactoring recipe, driven by an assistant</td><td>OpenRewrite and <code>webforj-rewrite</code> recipes</td><td>The dry-run diff and <code>TODO</code> comments</td></tr><tr><td>Port screens to the web</td><td>Coding assistant with docs access</td><td>webforJ AI plugin: MCP server and Agent Skills</td><td>Behavior of each ported screen</td></tr><tr><td>Write tests</td><td>Coding assistant that can run Maven</td><td>TestBed runs without a browser</td><td>That tests fail when they should</td></tr><tr><td>Keep it maintainable</td><td>Coding assistant with written conventions</td><td>Your conventions and the framework docs</td><td>Naming, structure and reuse</td></tr></tbody></table></div><style data-emotion="css 1k371a6">@media print{.css-1k371a6{position:absolute!important;}}</style></div>
<p>Claude Code, GitHub Copilot, Cursor, Gemini CLI and Codex differ in where they run and how you drive them. For the preceding migration jobs, the difference between them is smaller than the difference between an assistant with grounding and one without.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="webforj-grounding">How webforJ grounds your AI tools<a href="https://docs.webforj.com/zh/blog/ai-tools-java-migration#webforj-grounding" class="hash-link" aria-label="直接链接到 How webforJ grounds your AI tools" title="直接链接到 How webforJ grounds your AI tools" translate="no">​</a></h2>
<p>The webforJ <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling">AI plugin</a> installs into Claude Code, GitHub Copilot, Cursor, Gemini CLI and Codex, so you keep the tool your team already uses. It bundles two things.</p>
<p>The <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/mcp">MCP server</a> lets the assistant look things up in the webforJ knowledge base instead of recalling them. It can also scaffold a Maven project from the official archetypes. When the assistant ports a JTable to a webforJ <code>Table</code>, it can ask what the <code>Table</code> API actually offers.</p>
<p>The <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/agent-skills">Agent Skills</a> tell the assistant when to use those tools and in what order. There are skills for building forms with data binding and validation, securing routes, localizing apps, handling timers and background work, styling, wrapping components, and upgrading versions.</p>
<p>Then there is the compiler. A webforJ view is Java, so a wrong method name fails the build and the error tells the assistant what to fix. Add TestBed and the assistant can prove behavior without a browser. The pieces fit into a loop: look up the API, write the code, compile it, run the test, and hand a person a diff that has already passed the compiler and the tests.</p>
<p>We are careful about what that loop promises. It raises the odds that the code you review is correct. It does not replace the review, and even with the plugin installed an assistant can still produce wrong code in complex cases. Read the diff, run the tests and keep the change small enough to understand.</p>
<p>Lookup, compile, and test together make up <a class="" href="https://docs.webforj.com/zh/blog/ai-verification-loop-java">the AI verification loop for Java</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next-step">Run the upgrade recipe on a branch<a href="https://docs.webforj.com/zh/blog/ai-tools-java-migration#next-step" class="hash-link" aria-label="直接链接到 Run the upgrade recipe on a branch" title="直接链接到 Run the upgrade recipe on a branch" translate="no">​</a></h2>
<p>If you are not sure where to begin, begin with Job 1 and Job 2, because they are contained and teach your team how the assistant behaves on your code. Ask it for a screen inventory of your app. Then, once you are on webforJ, pick one project, add the OpenRewrite plugin on a branch, run <code>mvn rewrite:dryRun</code> and read the patch. When you are ready to port a screen, generate a project with <a href="https://docs.webforj.com/startforj" target="_blank" rel="noopener noreferrer" class="">startforJ</a>, install the plugin and follow the <a class="" href="https://docs.webforj.com/zh/docs/introduction/getting-started">getting started guide</a> for the first view.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">Frequently asked questions<a href="https://docs.webforj.com/zh/blog/ai-tools-java-migration#faq" class="hash-link" aria-label="直接链接到 Frequently asked questions" title="直接链接到 Frequently asked questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-best-tools">What are the best AI tools for Java migration?<a href="https://docs.webforj.com/zh/blog/ai-tools-java-migration#faq-best-tools" class="hash-link" aria-label="直接链接到 What are the best AI tools for Java migration?" title="直接链接到 What are the best AI tools for Java migration?" translate="no">​</a></h3>
<p>Coding assistants such as Claude Code, GitHub Copilot, Cursor, Gemini CLI and Codex handle the reading, writing and testing jobs, and OpenRewrite recipes handle mechanical version upgrades. Choose by job, and connect the assistant to your target framework's documentation so it does not guess.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-migrate-alone">Can AI migrate legacy Java code by itself?<a href="https://docs.webforj.com/zh/blog/ai-tools-java-migration#faq-migrate-alone" class="hash-link" aria-label="直接链接到 Can AI migrate legacy Java code by itself?" title="直接链接到 Can AI migrate legacy Java code by itself?" translate="no">​</a></h3>
<p>AI can help with the reading, porting and test writing, but a person should review every change. Work screen by screen, keep the business logic, and run tests after each step so mistakes show up early.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-java-programming">What is the best AI tool for Java programming?<a href="https://docs.webforj.com/zh/blog/ai-tools-java-migration#faq-java-programming" class="hash-link" aria-label="直接链接到 What is the best AI tool for Java programming?" title="直接链接到 What is the best AI tool for Java programming?" translate="no">​</a></h3>
<p>The tool matters less than what it can reach while it works. A coding assistant connected to your framework's real docs through MCP and loaded with matching Agent Skills writes more accurate Java than the same assistant working from memory. The webforJ AI plugin sets that up in one install.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-target-framework">How does the target framework change AI results?<a href="https://docs.webforj.com/zh/blog/ai-tools-java-migration#faq-target-framework" class="hash-link" aria-label="直接链接到 How does the target framework change AI results?" title="直接链接到 How does the target framework change AI results?" translate="no">​</a></h3>
<p>An assistant writing for a framework it has seen little of tends to invent methods and use outdated annotations. A framework that offers a docs lookup, workflows and a compiler-checked single-language codebase gives the assistant something to verify against, which helps the code you review compile and behave as intended.</p><div></div>]]></content>
        <author>
            <name>webforJ</name>
        </author>
        <category label="AI" term="AI"/>
        <category label="Modernization" term="Modernization"/>
        <category label="MCP" term="MCP"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[From Java to an installable web app in 15 minutes: the talk]]></title>
        <id>https://docs.webforj.com/zh/blog/java-installable-web-app-talk</id>
        <link href="https://docs.webforj.com/zh/blog/java-installable-web-app-talk"/>
        <updated>2026-11-04T12:00:00.000Z</updated>
        <summary type="html"><![CDATA[Watch a Java installable web app built live in 15 minutes with webforJ: views, shared state, live updates, and a phone-ready app, all written in Java.]]></summary>
        <content type="html"><![CDATA[
<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-11-04-java-installable-web-app-talk/cover.jpg" alt="cover image" class="img_ev3q"></p>
<p>How far can you get with a Java installable web app in 15 minutes? At Devoxx UK, Stephan Wald, our Head of Development at BASIS International, took that challenge live on stage, while the audience scanned a QR code to open the finished app on their phones. Everything in it is Java, written with webforJ, the full-stack Java framework.</p>
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;margin-bottom:1.5rem"><iframe src="https://www.youtube.com/embed/5MYgvCfbWRg" title="From Java to Installable Web App in 15 Minutes by Stephan Wald" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0" allowfullscreen=""></iframe></div>
<div class="cta_UErl" role="complementary" aria-label="Want to see webforJ on your project?"><div class="copy_XH8h"><p class="title_HfaR">Want to see webforJ on your project?</p><p class="text_x4Wj">Book a call and talk it through with the team.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="https://webforj.com/schedule-discovery/" target="_blank" rel="noopener noreferrer">预约通话<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a><a class="secondary_GDTR" href="mailto:sales@basis.cloud?subject=Talking%20to%20the%20webforJ%20team&amp;body=Hi%20webforJ%20team%2C%0A%0A%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->邮件咨询</a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-the-talk-covers">What the talk covers<a href="https://docs.webforj.com/zh/blog/java-installable-web-app-talk#what-the-talk-covers" class="hash-link" aria-label="直接链接到 What the talk covers" title="直接链接到 What the talk covers" translate="no">​</a></h2>
<ul>
<li class=""><strong><a href="https://www.youtube.com/watch?v=5MYgvCfbWRg&amp;t=0s" target="_blank" rel="noopener noreferrer" class="">00:00</a></strong> The challenge: a working app in 15 minutes</li>
<li class=""><strong><a href="https://www.youtube.com/watch?v=5MYgvCfbWRg&amp;t=60s" target="_blank" rel="noopener noreferrer" class="">01:00</a></strong> Starting from a webforJ archetype</li>
<li class=""><strong><a href="https://www.youtube.com/watch?v=5MYgvCfbWRg&amp;t=180s" target="_blank" rel="noopener noreferrer" class="">03:00</a></strong> Shaping the tabs and routes</li>
<li class=""><strong><a href="https://www.youtube.com/watch?v=5MYgvCfbWRg&amp;t=420s" target="_blank" rel="noopener noreferrer" class="">07:00</a></strong> Building the first view in Java</li>
<li class=""><strong><a href="https://www.youtube.com/watch?v=5MYgvCfbWRg&amp;t=480s" target="_blank" rel="noopener noreferrer" class="">08:00</a></strong> Shared state with namespaces</li>
<li class=""><strong><a href="https://www.youtube.com/watch?v=5MYgvCfbWRg&amp;t=660s" target="_blank" rel="noopener noreferrer" class="">11:00</a></strong> Responding to a click and showing a toast</li>
<li class=""><strong><a href="https://www.youtube.com/watch?v=5MYgvCfbWRg&amp;t=780s" target="_blank" rel="noopener noreferrer" class="">13:00</a></strong> Filling the list and validating input</li>
<li class=""><strong><a href="https://www.youtube.com/watch?v=5MYgvCfbWRg&amp;t=1020s" target="_blank" rel="noopener noreferrer" class="">17:00</a></strong> Live updates in a second view</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-challenge">The challenge<a href="https://docs.webforj.com/zh/blog/java-installable-web-app-talk#the-challenge" class="hash-link" aria-label="直接链接到 The challenge" title="直接链接到 The challenge" translate="no">​</a></h2>
<p>The setup is simple. The audience gets a QR code that opens a finished version of the app on their phones. On stage, Stephan builds it again, live: a screen to enter a nickname and pick a mood, and a second screen that lists everyone's entries as they arrive. Fifteen minutes, one language, no front-end project.</p>
<p>It is a good test for any framework, because it touches what most business apps need: navigation, a form, input handling, validation, shared data, and a screen that updates when other users act.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="start-from-an-archetype">Start from an archetype<a href="https://docs.webforj.com/zh/blog/java-installable-web-app-talk#start-from-an-archetype" class="hash-link" aria-label="直接链接到 Start from an archetype" title="直接链接到 Start from an archetype" translate="no">​</a></h2>
<p>Stephan doesn't start from a blank file. Before the session, following the getting started steps in our docs, he generated a project from the <a class="" href="https://docs.webforj.com/zh/docs/building-ui/archetypes/tabs">tabs archetype</a>, which gives a working app with a tabbed layout and a few sample views. To fit the time limit, he also prepared some of the code he pastes in along the way. You can generate the same kind of project with <a href="https://docs.webforj.com/startforj" target="_blank" rel="noopener noreferrer" class="">startforJ</a> or the <a class="" href="https://docs.webforj.com/zh/docs/building-ui/archetypes/overview">archetypes</a>.</p>
<p>From there, the first change is the navigation. In the main layout he renames and reorders the tabs, swaps in an icon from the bundled icon set, and points each tab at its route. A view is a Java class with a route, so the menu and the pages stay in one language.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="first-view">The first view in Java<a href="https://docs.webforj.com/zh/blog/java-installable-web-app-talk#first-view" class="hash-link" aria-label="直接链接到 The first view in Java" title="直接链接到 The first view in Java" translate="no">​</a></h2>
<p>The "you" view is a class that extends <code>Composite</code> with a <code>FlexLayout</code> inside it. It holds three components as fields: a <code>TextField</code> for the nickname, a <code>ListBox</code> for the mood, and a <code>Button</code> to submit. Adding them to the layout puts them on the screen.</p>
<p>Styling stays light on purpose: a maximum width so the form is easier on the eye, and a margin taken from the theme's spacing scale instead of a hard-coded pixel value. That keeps the view consistent with the rest of the app's design.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="shared-state">Shared state across users<a href="https://docs.webforj.com/zh/blog/java-installable-web-app-talk#shared-state" class="hash-link" aria-label="直接链接到 Shared state across users" title="直接链接到 Shared state across users" translate="no">​</a></h2>
<p>The interesting part of the demo is the model. Instead of a database, Stephan stores the entries in a <a class="" href="https://docs.webforj.com/zh/docs/advanced/namespaces">namespace</a>, a webforJ feature for thread-safe, key-value state that can be shared across sessions, a bit like a store that lives on the server. A private namespace is identified by a prefix and a name, and every part of the app that asks for the same name gets the same instance:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token class-name" style="color:var(--dwc-code-class)">PrivateNamespace</span><span class="token plain"> ns </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">PrivateNamespace</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"myApp"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"sharedState"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><br></div></code></pre></div></div>
<p>That one choice is what makes the second screen work. When one user submits, everyone else can see it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="clicks-toasts-validation">Clicks, toasts, and validation<a href="https://docs.webforj.com/zh/blog/java-installable-web-app-talk#clicks-toasts-validation" class="hash-link" aria-label="直接链接到 Clicks, toasts, and validation" title="直接链接到 Clicks, toasts, and validation" translate="no">​</a></h2>
<p>To respond to the button, Stephan registers a click listener and lets the IDE generate the handler method. The first version just confirms the event with a <a class="" href="https://docs.webforj.com/zh/docs/components/toast">toast</a>:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token class-name" style="color:var(--dwc-code-class)">Toast</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">show</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Operation completed successfully!"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Theme</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">SUCCESS</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><br></div></code></pre></div></div>
<p>The real handler then checks that the nickname isn't blank, stores the nickname and the selected mood in the namespace, and shows an error when the input is missing. The mood list is filled by iterating over the available options and selecting the first one by default.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="live-updates">Live updates in a second view<a href="https://docs.webforj.com/zh/blog/java-installable-web-app-talk#live-updates" class="hash-link" aria-label="直接链接到 Live updates in a second view" title="直接链接到 Live updates in a second view" translate="no">​</a></h2>
<p>The users view closes the loop. It registers a change listener on the same namespace, and whenever a value changes, it reloads the list into its table. Namespaces support listeners for any change or for a specific key, so a view can react as soon as another user acts:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">ns</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">onChange</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">event </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token class-name" style="color:var(--dwc-code-class)">System</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token plain">out</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">println</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Key changed: "</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> event</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getVariableName</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><br></div></code></pre></div></div>
<p>This is what turns a form into a shared, live app: anyone with the app open sees new entries appear without reloading.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="installable">Making it installable<a href="https://docs.webforj.com/zh/blog/java-installable-web-app-talk#installable" class="hash-link" aria-label="直接链接到 Making it installable" title="直接链接到 Making it installable" translate="no">​</a></h2>
<p>A webforJ app already runs in any modern browser, including on phones. To let users install it like a native app, webforJ provides the <a class="" href="https://docs.webforj.com/zh/docs/configuration/installable-apps"><code>@AppProfile</code> annotation</a> on the app class. Once installed, the app appears on the home screen or app menu, similar to native apps. There is no separate mobile codebase, and no JavaScript build to maintain. If you start from an existing desktop app instead, a webinar on <a class="" href="https://docs.webforj.com/zh/blog/move-java-app-to-web-and-mobile">moving a Java app to the web and mobile</a> shows the staged path.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="split-stack">The same app on a split stack<a href="https://docs.webforj.com/zh/blog/java-installable-web-app-talk#split-stack" class="hash-link" aria-label="直接链接到 The same app on a split stack" title="直接链接到 The same app on a split stack" translate="no">​</a></h2>
<p>It helps to picture the same app built the usual way, with a Java back end and a JavaScript front end. The form needs an API endpoint to submit to and another to read the list. The live table needs a push channel, such as WebSockets or server-sent events, plus client code to receive messages and update its state. The mood list, the validation rules, and the data types exist twice, once in Java and once in TypeScript. There are two builds, two dependency trees, and a deployment that has to keep both sides in step.</p>
<p>None of that is hard on its own. Together it is a lot of plumbing for a nickname and a mood, and it would not fit in 15 minutes on stage. In webforJ, the click handler, the shared model, and the table update are plain Java method calls in one project, so the whole app stays small enough to build in front of an audience and to read top to bottom afterwards.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-it-shows">What 15 minutes shows<a href="https://docs.webforj.com/zh/blog/java-installable-web-app-talk#what-it-shows" class="hash-link" aria-label="直接链接到 What 15 minutes shows" title="直接链接到 What 15 minutes shows" translate="no">​</a></h2>
<p>The demo is short, and that is the point. Navigation, a form, validation, shared state across users, live updates, and a phone-ready app, all in plain Java and all in one project. Stephan's development loop is the familiar one too: change the code, rebuild when the structure changes, and reload. With <a class="" href="https://docs.webforj.com/zh/docs/configuration/deploy-reload/overview">live reload</a> configured, small changes show up without a restart.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="build-your-own">Build your own in 15 minutes<a href="https://docs.webforj.com/zh/blog/java-installable-web-app-talk#build-your-own" class="hash-link" aria-label="直接链接到 Build your own in 15 minutes" title="直接链接到 Build your own in 15 minutes" translate="no">​</a></h2>
<p>Generate a project with <a href="https://docs.webforj.com/startforj" target="_blank" rel="noopener noreferrer" class="">startforJ</a>, add a view with a form, keep its data in a <a class="" href="https://docs.webforj.com/zh/docs/advanced/namespaces">namespace</a>, and add <code>@AppProfile</code> to make it installable. Then open it on your phone.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">Frequently asked questions<a href="https://docs.webforj.com/zh/blog/java-installable-web-app-talk#faq" class="hash-link" aria-label="直接链接到 Frequently asked questions" title="直接链接到 Frequently asked questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-installable-java">Can I build an installable web app in Java?<a href="https://docs.webforj.com/zh/blog/java-installable-web-app-talk#faq-installable-java" class="hash-link" aria-label="直接链接到 Can I build an installable web app in Java?" title="直接链接到 Can I build an installable web app in Java?" translate="no">​</a></h3>
<p>Yes. With webforJ you write the whole app in Java, and the <code>@AppProfile</code> annotation makes it installable, so users can add it to the home screen or app menu like a native app.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-no-javascript">Do I need JavaScript or a mobile framework?<a href="https://docs.webforj.com/zh/blog/java-installable-web-app-talk#faq-no-javascript" class="hash-link" aria-label="直接链接到 Do I need JavaScript or a mobile framework?" title="直接链接到 Do I need JavaScript or a mobile framework?" translate="no">​</a></h3>
<p>No. The app in the talk is plain Java with webforJ components. It runs in mobile browsers and can be installed without a separate mobile codebase.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-shared-data">How do views share data in real time?<a href="https://docs.webforj.com/zh/blog/java-installable-web-app-talk#faq-shared-data" class="hash-link" aria-label="直接链接到 How do views share data in real time?" title="直接链接到 How do views share data in real time?" translate="no">​</a></h3>
<p>Through namespaces. A view stores values in a namespace, and other views register change listeners on the same namespace and update when a value changes.</p><div></div>]]></content>
        <author>
            <name>webforJ</name>
        </author>
        <category label="Community" term="Community"/>
        <category label="Events" term="Events"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[AI form filling in Java: Spring AI structured output]]></title>
        <id>https://docs.webforj.com/zh/blog/ai-form-filling-java</id>
        <link href="https://docs.webforj.com/zh/blog/ai-form-filling-java"/>
        <updated>2026-11-04T09:00:00.000Z</updated>
        <summary type="html"><![CDATA[Build an AI form filler in Java with Spring AI and webforJ. Turn pasted text or an uploaded file into a typed bean, load it with BindingContext, and validate before saving.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-10-21-ai-form-filling-java/cover-v2.jpg" alt="cover image" class="img_ev3q"></p>
<p>To fill a form with AI in Java, let the user paste free text or upload a file, ask Spring AI for a typed bean with <code>ChatClient</code> and <code>.entity(...)</code>, and load that bean into a webforJ form with <code>BindingContext.read</code>. The same Jakarta validation rules that guard manual entry then run on what the model produced, and the user reviews and edits the fields before anything is saved.</p>
<p>This post builds that flow end to end in a webforJ view. The model proposes values, webforJ validates them, and the person stays in control. The complete app, <a href="https://github.com/webforj/built-with-webforj/tree/main/webforj-smartform" target="_blank" rel="noopener noreferrer" class="">SmartForm</a>, is on GitHub and runs with your own Gemini API key.</p>
<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-10-21-ai-form-filling-java/smartform.png" alt="SmartForm turning a pasted email into a support ticket with name, email, priority and summary filled in" class="img_ev3q"></p>
<div class="cta_UErl" role="complementary" aria-label="Using AI to build Java apps?"><div class="copy_XH8h"><p class="title_HfaR">Using AI to build Java apps?</p><p class="text_x4Wj">Ask the team about webforJ AI tooling: the MCP server, Agent Skills, and how they keep generated code correct.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="https://webforj.com/schedule-discovery/" target="_blank" rel="noopener noreferrer">预约通话<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a><a class="secondary_GDTR" href="mailto:sales@basis.cloud?subject=AI%20and%20webforJ&amp;body=Hi%20webforJ%20team%2C%0A%0A%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->邮件咨询</a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-it-works">How does an AI form filler work?<a href="https://docs.webforj.com/zh/blog/ai-form-filling-java#how-it-works" class="hash-link" aria-label="直接链接到 How does an AI form filler work?" title="直接链接到 How does an AI form filler work?" translate="no">​</a></h2>
<p>The flow has five steps:</p>
<ol>
<li class="">The user pastes an email into a text area, or uploads a file whose text lands there.</li>
<li class="">A Spring service sends the text to the model and asks for a Java object back.</li>
<li class="">Spring AI parses the model's JSON reply into that object.</li>
<li class="">The view loads the object into the form through a <code>BindingContext</code> and runs validation.</li>
<li class="">The user fixes anything wrong and saves.</li>
</ol>
<p>The model never writes to your database and never skips validation. It only drafts the form, and the form's normal save path decides what is accepted.</p>
<p>webforJ fits this well because the form is already bound to a bean. Data binding gives you the load step, the validation step and the error display without any AI-specific code. Spring AI gives you the structured output. If you are new to wiring a model into a webforJ app, <a class="" href="https://docs.webforj.com/zh/blog/add-ai-to-java-app">add AI to a Java app</a> covers the basics.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="target-bean">What should the target bean look like?<a href="https://docs.webforj.com/zh/blog/ai-form-filling-java#target-bean" class="hash-link" aria-label="直接链接到 What should the target bean look like?" title="直接链接到 What should the target bean look like?" translate="no">​</a></h2>
<p>Use the same bean the form already binds to. Put your Jakarta constraints on it, because they apply both to manual typing and to AI-filled values.</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">SupportTicket</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@NotBlank</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">message </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"Name is required"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> name</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Email</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">message </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"Enter a valid email address"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@NotBlank</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">message </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"Email is required"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> email</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Pattern</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">regexp </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"LOW|NORMAL|HIGH"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> message </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"Priority must be LOW, NORMAL or HIGH"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> priority</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Size</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">max </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token number" style="color:var(--dwc-code-number)">200</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> message </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"Keep the summary under 200 characters"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> summary</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token comment" style="color:var(--dwc-code-comment);font-style:italic">// getters and setters</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>Spring AI derives a JSON schema from this class and adds it to the prompt, so field names and types matter. The <a href="https://docs.spring.io/spring-ai/reference/api/structured-output/converters.html" target="_blank" rel="noopener noreferrer" class="">Spring AI documentation</a> shows the same pattern with a plain record. The webforJ binding examples use classes with getters and setters, so that shape is the natural target for a form.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="structured-output">How do you get structured output?<a href="https://docs.webforj.com/zh/blog/ai-form-filling-java#structured-output" class="hash-link" aria-label="直接链接到 How do you get structured output?" title="直接链接到 How do you get structured output?" translate="no">​</a></h2>
<p>Inject <code>ChatClient.Builder</code>, set a system prompt once, and call <code>.entity(SupportTicket.class)</code> at the end of the request chain.</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Service</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TicketExtractor</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">ChatClient</span><span class="token plain"> chatClient</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TicketExtractor</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">ChatClient</span><span class="token class-name punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token class-name" style="color:var(--dwc-code-class)">Builder</span><span class="token plain"> builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">this</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token plain">chatClient </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> builder</span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">defaultSystem</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token triple-quoted-string string" style="color:var(--dwc-code-string)">"""</span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token triple-quoted-string string" style="color:var(--dwc-code-string)">            Extract support ticket fields from the user's text.</span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token triple-quoted-string string" style="color:var(--dwc-code-string)">            Leave a field empty if the text does not state it.</span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token triple-quoted-string string" style="color:var(--dwc-code-string)">            Priority is LOW, NORMAL or HIGH."""</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">build</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Async</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">CompletableFuture</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">SupportTicket</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">extract</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> text</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">CompletableFuture</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">completedFuture</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        chatClient</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">prompt</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">user</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">text</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">call</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">entity</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">SupportTicket</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p><code>.entity(...)</code> appends format instructions to the prompt, then converts the reply into the target class. The system prompt tells the model what to do when information is missing, which keeps it from inventing a priority or an email address. The same structured output can turn a typed sentence into a filter for a <code>Table</code>, as in <a class="" href="https://docs.webforj.com/zh/blog/ai-table-natural-language-search">natural language search for a Java table</a>.</p>
<p>The call is blocking and can take seconds. <code>@Async</code> moves it off the request thread, so enable it once with <code>@EnableAsync</code> on your Spring Boot app class. The <a class="" href="https://docs.webforj.com/zh/docs/integrations/spring/background-jobs">background jobs guide</a> shows the same pattern for any long-running service.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="binding">How do you fill the form?<a href="https://docs.webforj.com/zh/blog/ai-form-filling-java#binding" class="hash-link" aria-label="直接链接到 How do you fill the form?" title="直接链接到 How do you fill the form?" translate="no">​</a></h2>
<p>Create the <code>BindingContext</code> with <code>BindingContext.of(this, SupportTicket.class, true)</code>. It matches the view's fields to bean properties by name, and the <code>true</code> turns on Jakarta validation. Buttons are skipped, but every other input field must match a property. Mark the paste box and the upload with <code>@BindingExclude</code> because they are not part of the ticket. The post on <a class="" href="https://docs.webforj.com/zh/blog/bind-java-bean-to-form">binding a Java bean to form fields automatically</a> walks through <code>UseProperty</code> and <code>BindingExclude</code>.</p>
<p>After the model replies, call <code>context.read(ticket)</code> to put the values into the fields, then <code>context.validate()</code> to show any violations right away.</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Route</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"/"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TicketView</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">extends</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Composite</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">FlexLayout</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@BindingExclude</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TextArea</span><span class="token plain"> pasted </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TextArea</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Paste an email or note"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@BindingExclude</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Upload</span><span class="token plain"> upload </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Upload</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token plain"> fill </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Fill form"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">ButtonTheme</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">PRIMARY</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Div</span><span class="token plain"> thinking </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Div</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token plain"> name </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Name"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token plain"> email </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Email"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token plain"> priority </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Priority"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token plain"> summary </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Summary"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token plain"> save </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Save ticket"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">ButtonTheme</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">PRIMARY</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">BindingContext</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">SupportTicket</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> context </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token class-name" style="color:var(--dwc-code-class)">BindingContext</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">of</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">this</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">SupportTicket</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token boolean" style="color:var(--dwc-code-number)">true</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TicketExtractor</span><span class="token plain"> extractor</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TicketView</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">TicketExtractor</span><span class="token plain"> extractor</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">this</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token plain">extractor </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> extractor</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    thinking</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Spinner</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">SpinnerExpanse</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">SMALL</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Span</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Reading the text..."</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    thinking</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setVisible</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">false</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token function" style="color:var(--dwc-code-function)">getBoundComponent</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setDirection</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">FlexDirection</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">COLUMN</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token function" style="color:var(--dwc-code-function)">getBoundComponent</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">upload</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> pasted</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> fill</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> thinking</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> name</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> email</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> priority</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> summary</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> save</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    context</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">read</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">SupportTicket</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    fill</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">onClick</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">e </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">fillFromText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    save</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">onClick</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">e </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token class-name" style="color:var(--dwc-code-class)">SupportTicket</span><span class="token plain"> ticket </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">SupportTicket</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token keyword" style="color:var(--dwc-code-keyword)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">context</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">write</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">ticket</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">isValid</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token comment" style="color:var(--dwc-code-comment);font-style:italic">// A real app would persist the ticket here.</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token class-name" style="color:var(--dwc-code-class)">Toast</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">show</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Ticket from "</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> ticket</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getName</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">" saved"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Theme</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">SUCCESS</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        context</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">read</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">SupportTicket</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        pasted</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setValue</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">""</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>Spring injects <code>TicketExtractor</code> into the route through its constructor, as the <a class="" href="https://docs.webforj.com/zh/docs/integrations/spring/routing">Spring routing guide</a> describes. The save button uses <code>context.write(ticket)</code>, which validates and copies the field values into a new bean. This is the same call you would use for a form with no AI at all.</p>
<p>Validation runs on AI output the same way it runs on typed input. If the model returns <code>priority</code> as <code>URGENT</code>, the <code>@Pattern</code> rule fails and the field shows your message. See <a class="" href="https://docs.webforj.com/zh/docs/data-binding/validation/jakarta-validation">Jakarta validation</a> for the supported constraints and message handling.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="upload">How do you read an uploaded file?<a href="https://docs.webforj.com/zh/blog/ai-form-filling-java#upload" class="hash-link" aria-label="直接链接到 How do you read an uploaded file?" title="直接链接到 How do you read an uploaded file?" translate="no">​</a></h2>
<p>Users often have the text in a file rather than on the clipboard. The webforJ <code>Upload</code> component takes the file, and Spring AI's <code>TikaDocumentReader</code> from the <code>spring-ai-tika-document-reader</code> artifact reads the text out of PDF, Word, email and plain text files. Configure the upload in the constructor:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">upload</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setAutoUpload</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Upload</span><span class="token class-name punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token class-name" style="color:var(--dwc-code-class)">AutoUpload</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">ALWAYS</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">addFilter</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Emails and documents"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"*.txt;*.eml;*.md;*.pdf;*.docx"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">upload</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setVisible</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">false</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Upload</span><span class="token class-name punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token class-name" style="color:var(--dwc-code-class)">Part</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">UPLOAD_BUTTON</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">upload</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">onUpload</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">e </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> e</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getFiles</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">forEach</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">this</span><span class="token operator" style="color:var(--dwc-code-operator)">::</span><span class="token function" style="color:var(--dwc-code-function)">readFile</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><br></div></code></pre></div></div>
<p><code>AutoUpload.ALWAYS</code> sends a file as soon as it is picked or dropped, so the upload button is hidden. Each file then goes to <code>readFile</code>, which puts its text into the paste box:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">readFile</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">UploadedFile</span><span class="token plain"> uploaded</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token class-name" style="color:var(--dwc-code-class)">Path</span><span class="token plain"> folder </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">null</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">try</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    folder </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Files</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">createTempDirectory</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"smartform"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">File</span><span class="token plain"> saved </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> uploaded</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">move</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">folder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">resolve</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">uploaded</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getSanitizedClientName</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">toString</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> text </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TikaDocumentReader</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">FileSystemResource</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">saved</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">get</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">stream</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">map</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Document</span><span class="token operator" style="color:var(--dwc-code-operator)">::</span><span class="token function" style="color:var(--dwc-code-function)">getText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">collect</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Collectors</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">joining</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"\n"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    pasted</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setInvalid</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">false</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    pasted</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setValue</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">text</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">strip</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">catch</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Exception</span><span class="token plain"> ex</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">Toast</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">show</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Could not read "</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> uploaded</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getClientName</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Theme</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">DANGER</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">finally</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token function" style="color:var(--dwc-code-function)">delete</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">folder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>The file's text goes into the paste box instead of straight to the model. The user sees exactly what will be sent, can remove parts of it, and the model is only called when they click <strong>Fill form</strong>. <code>delete</code> removes the temporary folder once the text is read. The <a class="" href="https://docs.webforj.com/zh/blog/java-file-upload">Java file upload guide</a> covers filters, limits and progress in more depth.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="off-the-ui-thread">How do you keep the UI responsive?<a href="https://docs.webforj.com/zh/blog/ai-form-filling-java#off-the-ui-thread" class="hash-link" aria-label="直接链接到 How do you keep the UI responsive?" title="直接链接到 How do you keep the UI responsive?" translate="no">​</a></h2>
<p>webforJ components can only be changed on the UI thread. The model call returns on a different thread, so read the <code>Environment</code> before the call and hand the result back with <code>Environment.runLater</code>.</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">fillFromText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> text </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> pasted</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getValue</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">text </span><span class="token operator" style="color:var(--dwc-code-operator)">==</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">null</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">||</span><span class="token plain"> text</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">isBlank</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token function" style="color:var(--dwc-code-function)">showProblem</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Paste or upload some text first."</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  pasted</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setInvalid</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">false</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  fill</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setEnabled</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">false</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  thinking</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setVisible</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">true</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token class-name" style="color:var(--dwc-code-class)">Environment</span><span class="token plain"> env </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Environment</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getCurrent</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  extractor</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">extract</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">text</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">whenComplete</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">ticket</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> error</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Environment</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">runLater</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">env</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        fill</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setEnabled</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">true</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        thinking</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setVisible</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">false</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token keyword" style="color:var(--dwc-code-keyword)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">error </span><span class="token operator" style="color:var(--dwc-code-operator)">!=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">null</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">||</span><span class="token plain"> ticket </span><span class="token operator" style="color:var(--dwc-code-operator)">==</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">null</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">          </span><span class="token function" style="color:var(--dwc-code-function)">showProblem</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Could not read that text. Fill the form by hand."</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">          </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        context</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">read</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">ticket</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        context</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">validate</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">showProblem</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> message</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  pasted</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setInvalid</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">true</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  pasted</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setInvalidMessage</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">message</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p><code>Environment.runLater</code> takes the <code>Environment</code> of the user who clicked, read with <code>Environment.getCurrent()</code> on the UI thread, and returns a <code>PendingResult</code>. Called from a background thread it queues the task for that user's UI thread, and called from the UI thread it runs immediately. The <a class="" href="https://docs.webforj.com/zh/docs/advanced/asynchronous-updates">asynchronous updates guide</a> explains the thread model, ordering, and cancellation. Disabling the button while the call runs prevents a second request on the same text, and the spinner shows the user that the model is working.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="error-handling">How do you handle bad output?<a href="https://docs.webforj.com/zh/blog/ai-form-filling-java#error-handling" class="hash-link" aria-label="直接链接到 How do you handle bad output?" title="直接链接到 How do you handle bad output?" translate="no">​</a></h2>
<p>Structured output is best effort. Spring AI's documentation states that the model is not guaranteed to return the format you asked for. Plan for three failures.</p>
<div class="table-wrapper"><div class="table-container" style="overflow-x:auto"><table><thead><tr><th>Failure</th><th>What happens</th><th>What to do</th></tr></thead><tbody><tr><td>The call fails or times out</td><td>The future completes with an exception</td><td>Show a message and leave the form for manual entry</td></tr><tr><td>The reply can't be parsed</td><td><code>.entity(...)</code> throws</td><td>Treat it like a failed call</td></tr><tr><td>The reply parses but breaks a rule</td><td><code>context.validate()</code> marks the fields</td><td>The user corrects them before saving</td></tr></tbody></table></div><style data-emotion="css 1k371a6">@media print{.css-1k371a6{position:absolute!important;}}</style></div>
<p>The earlier code covers the first two with the <code>error</code> branch, which also catches a reply that converts to no ticket at all. The third needs no extra code because the binding reports violations on the fields themselves. Keep the failure message short and give the user a way forward, which here is the ordinary form.</p>
<p>Spring AI also documents that some models wrap JSON in Markdown fences, which breaks parsing, and describes a converter that strips them. If your provider does this, wrap <code>BeanOutputConverter</code> as that page shows and pass it to <code>.entity(...)</code>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="review-before-saving">Why review before saving?<a href="https://docs.webforj.com/zh/blog/ai-form-filling-java#review-before-saving" class="hash-link" aria-label="直接链接到 Why review before saving?" title="直接链接到 Why review before saving?" translate="no">​</a></h2>
<p>A model can misread a name, pick the wrong priority, or fill a field from the wrong part of a long email. Loading the draft into editable fields makes every value visible and correctable. Nothing is saved until the user clicks save and <code>context.write</code> passes validation.</p>
<p>Two small choices keep the review honest. Leave fields empty when the text doesn't state them, so a blank is not mistaken for a model answer. And call <code>context.validate()</code> after <code>read</code>, so the user sees required-field and format problems before they reach the save button.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="sensitive-text">What about sensitive text?<a href="https://docs.webforj.com/zh/blog/ai-form-filling-java#sensitive-text" class="hash-link" aria-label="直接链接到 What about sensitive text?" title="直接链接到 What about sensitive text?" translate="no">​</a></h2>
<p>Pasted text goes to whichever model provider you configure. Decide which data you are willing to send, and redact or exclude fields such as account numbers before the call. Keep the API key in Spring configuration or the environment and not in source.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next-step">Next step<a href="https://docs.webforj.com/zh/blog/ai-form-filling-java#next-step" class="hash-link" aria-label="直接链接到 Next step" title="直接链接到 Next step" translate="no">​</a></h2>
<p>Clone <a href="https://github.com/webforj/built-with-webforj/tree/main/webforj-smartform" target="_blank" rel="noopener noreferrer" class="">SmartForm</a> to run the finished app, or start a project with <a href="https://docs.webforj.com/startforj" target="_blank" rel="noopener noreferrer" class="">startforJ</a>, add a Spring AI model starter, and bind one existing form to a bean. Then add the extractor and the paste box. The <a class="" href="https://docs.webforj.com/zh/docs/data-binding/overview">data binding overview</a> shows the binding side in more detail, and <a class="" href="https://docs.webforj.com/zh/blog/java-form-validation">form validation in Java</a> covers validation patterns for the fields you bind.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">Frequently asked questions<a href="https://docs.webforj.com/zh/blog/ai-form-filling-java#faq" class="hash-link" aria-label="直接链接到 Frequently asked questions" title="直接链接到 Frequently asked questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-fill-form">How do I fill a form with AI in Java?<a href="https://docs.webforj.com/zh/blog/ai-form-filling-java#faq-fill-form" class="hash-link" aria-label="直接链接到 How do I fill a form with AI in Java?" title="直接链接到 How do I fill a form with AI in Java?" translate="no">​</a></h3>
<p>Send the user's text to Spring AI's <code>ChatClient</code> and call <code>.entity(YourBean.class)</code> to get a typed object. Load that object into the form with <code>BindingContext.read</code>. In webforJ the form's validation then runs on the AI-filled values.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-valid-data">Is the output always valid?<a href="https://docs.webforj.com/zh/blog/ai-form-filling-java#faq-valid-data" class="hash-link" aria-label="直接链接到 Is the output always valid?" title="直接链接到 Is the output always valid?" translate="no">​</a></h3>
<p>No. Spring AI documents that the model isn't guaranteed to return the requested structure. Handle exceptions from the call, and rely on Jakarta validation to catch values that parse but break your rules.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-runlater">Why use Environment.runLater?<a href="https://docs.webforj.com/zh/blog/ai-form-filling-java#faq-runlater" class="hash-link" aria-label="直接链接到 Why use Environment.runLater?" title="直接链接到 Why use Environment.runLater?" translate="no">​</a></h3>
<p>The model call finishes on a background thread, and webforJ components can only be updated on the UI thread. <code>Environment.runLater</code> queues the update so it runs safely there.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-auto-save">Should the AI save the form?<a href="https://docs.webforj.com/zh/blog/ai-form-filling-java#faq-auto-save" class="hash-link" aria-label="直接链接到 Should the AI save the form?" title="直接链接到 Should the AI save the form?" translate="no">​</a></h3>
<p>It is safer not to. Load the draft into the form, let the user review it, and save through <code>context.write</code>, which validates first.</p><div></div>]]></content>
        <author>
            <name>Hyyan Abo Fakher</name>
        </author>
        <category label="AI" term="AI"/>
        <category label="Spring AI" term="Spring AI"/>
        <category label="Spring" term="Spring"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Spring AI RAG web UI: chat with your documents in Java]]></title>
        <id>https://docs.webforj.com/zh/blog/spring-ai-rag</id>
        <link href="https://docs.webforj.com/zh/blog/spring-ai-rag"/>
        <updated>2026-11-04T08:00:00.000Z</updated>
        <summary type="html"><![CDATA[Build a Spring AI RAG web UI in pure Java with webforJ: upload documents, index them in a vector store, and stream answers with their sources into the page.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-10-26-spring-ai-rag/cover-v2.jpg" alt="cover image" class="img_ev3q"></p>
<p>The fastest way to build a Spring AI RAG web UI is to write the whole thing in Java with webforJ. The <code>Upload</code> component takes the files, Spring AI reads, splits and embeds them into a <code>VectorStore</code>, a <code>RetrievalAugmentationAdvisor</code> on a <code>ChatClient</code> grounds every answer in the retrieved chunks, and a <code>MarkdownViewer</code> renders the streamed answer. There is no REST layer, no JavaScript and no separate frontend project.</p>
<p>This guide builds a chat-with-your-documents app that way. It is two classes: a Spring service that owns the knowledge base and a webforJ view that talks to it. The complete app, <a href="https://github.com/webforj/built-with-webforj/tree/main/webforj-askdocs" target="_blank" rel="noopener noreferrer" class="">AskDocs</a>, is on GitHub and runs with your own Gemini API key.</p>
<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-10-26-spring-ai-rag/askdocs.png" alt="AskDocs answering a question from an uploaded travel handbook, with the source file listed under the answer" class="img_ev3q"></p>
<div class="cta_UErl" role="complementary" aria-label="Using AI to build Java apps?"><div class="copy_XH8h"><p class="title_HfaR">Using AI to build Java apps?</p><p class="text_x4Wj">Ask the team about webforJ AI tooling: the MCP server, Agent Skills, and how they keep generated code correct.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="https://webforj.com/schedule-discovery/" target="_blank" rel="noopener noreferrer">预约通话<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a><a class="secondary_GDTR" href="mailto:sales@basis.cloud?subject=AI%20and%20webforJ&amp;body=Hi%20webforJ%20team%2C%0A%0A%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->邮件咨询</a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="architecture">Spring AI RAG web UI architecture<a href="https://docs.webforj.com/zh/blog/spring-ai-rag#architecture" class="hash-link" aria-label="直接链�接到 Spring AI RAG web UI architecture" title="直接链接到 Spring AI RAG web UI architecture" translate="no">​</a></h2>
<p>A document Q&amp;A app has two flows. Ingestion turns files into searchable chunks. Querying finds the chunks closest to a question and hands them to the model as context.</p>
<div class="table-wrapper"><div class="table-container" style="overflow-x:auto"><table><thead><tr><th>Step</th><th>Where it happens</th><th>Class</th></tr></thead><tbody><tr><td>Pick and transfer files</td><td>Browser to server</td><td>webforJ <code>Upload</code></td></tr><tr><td>Read text out of PDF, DOCX or TXT</td><td>Server</td><td>Spring AI <code>TikaDocumentReader</code></td></tr><tr><td>Split into chunks</td><td>Server</td><td>Spring AI <code>TokenTextSplitter</code></td></tr><tr><td>Embed and store</td><td>Server</td><td>Spring AI <code>VectorStore</code></td></tr><tr><td>Retrieve and augment the prompt</td><td>Server</td><td><code>RetrievalAugmentationAdvisor</code></td></tr><tr><td>Stream and render the answer</td><td>Server to browser</td><td><code>MarkdownViewer</code></td></tr></tbody></table></div><style data-emotion="css 1k371a6">@media print{.css-1k371a6{position:absolute!important;}}</style></div>
<p>Because webforJ runs your UI code on the server, the view can call a Spring bean directly. The uploaded file lands on the server, the service reads it, and the answer streams back into a component. Nothing in that path needs an HTTP endpoint you write yourself.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dependencies">Dependencies<a href="https://docs.webforj.com/zh/blog/spring-ai-rag#dependencies" class="hash-link" aria-label="直接链接到 Dependencies" title="直接链接到 Dependencies" translate="no">​</a></h2>
<p>Start from a webforJ Spring Boot project, then add the Spring AI BOM, a model provider, and the three RAG artifacts. This example uses Gemini for both the answers and the embeddings:</p>
<div class="language-xml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-xml codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">dependency</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">groupId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">org.springframework.ai</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">groupId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">artifactId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">spring-ai-starter-model-google-genai</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">artifactId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">dependency</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">dependency</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">groupId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">org.springframework.ai</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">groupId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">artifactId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">spring-ai-google-genai-embedding</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">artifactId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">dependency</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">dependency</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">groupId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">org.springframework.ai</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">groupId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">artifactId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">spring-ai-rag</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">artifactId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">dependency</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">dependency</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">groupId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">org.springframework.ai</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">groupId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">artifactId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">spring-ai-vector-store</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">artifactId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">dependency</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">dependency</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">groupId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">org.springframework.ai</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">groupId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token tag" style="color:var(--dwc-code-number)">artifactId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain">spring-ai-tika-document-reader</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">artifactId</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&lt;/</span><span class="token tag" style="color:var(--dwc-code-number)">dependency</span><span class="token tag punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><br></div></code></pre></div></div>
<p>Choose the models in <code>application.properties</code>, and pass the API key through the environment variables <code>SPRING_AI_GOOGLE_GENAI_API_KEY</code> and <code>SPRING_AI_GOOGLE_GENAI_EMBEDDING_API_KEY</code> so it never lands in a file:</p>
<div class="language-properties codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-properties codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">spring.ai.google.genai.chat.options.model=gemini-3.5-flash-lite</span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">spring.ai.google.genai.embedding.text.options.model=gemini-embedding-001</span><br></div></code></pre></div></div>
<p>The <a href="https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html" target="_blank" rel="noopener noreferrer" class="">Spring AI reference</a> covers the RAG modules in detail. On the webforJ side the app uses the <a class="" href="https://docs.webforj.com/zh/docs/integrations/spring/overview">Spring integration</a>, so views receive Spring beans through their constructors.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="index-documents">Index documents in a vector store<a href="https://docs.webforj.com/zh/blog/spring-ai-rag#index-documents" class="hash-link" aria-label="直接链接到 Index documents in a vector store" title="直接链接到 Index documents in a vector store" translate="no">​</a></h2>
<p>The whole RAG pipeline is one Spring service. It builds a <code>SimpleVectorStore</code> from the <code>EmbeddingModel</code> that the starter configures, and a <code>ChatClient</code> that has the RAG advisor attached.</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Service</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">KnowledgeBase</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">VectorStore</span><span class="token plain"> store</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">final</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">ChatClient</span><span class="token plain"> chat</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">KnowledgeBase</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">EmbeddingModel</span><span class="token plain"> embeddings</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">ChatClient</span><span class="token class-name punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token class-name" style="color:var(--dwc-code-class)">Builder</span><span class="token plain"> builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    store </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">SimpleVectorStore</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">embeddings</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">build</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    chat </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> builder</span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">defaultSystem</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Answer in short Markdown. Use only the context you are given."</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">defaultAdvisors</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">RetrievalAugmentationAdvisor</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">            </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">documentRetriever</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">VectorStoreDocumentRetriever</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">                </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">vectorStore</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">store</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">                </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">similarityThreshold</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token number" style="color:var(--dwc-code-number)">0.5</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">                </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">topK</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token number" style="color:var(--dwc-code-number)">4</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">                </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">build</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">            </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">build</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">build</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">int</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Resource</span><span class="token plain"> document</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> name</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Document</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> pages </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TikaDocumentReader</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">document</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">get</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    pages</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">forEach</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">page </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> page</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getMetadata</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">put</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"source"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> name</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Document</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> chunks </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TokenTextSplitter</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">builder</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">build</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">apply</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">pages</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    store</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">chunks</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token plain"> chunks</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">size</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Flux</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">ChatClientResponse</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">ask</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> question</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token plain"> chat</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">prompt</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">user</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">question</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">stream</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">chatClientResponse</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p><code>add</code> is the whole indexing pipeline. <code>TikaDocumentReader</code> extracts text from a PDF, Word, Markdown, or text file. The <code>source</code> metadata records the name to show next to an answer. <code>TokenTextSplitter</code> cuts the text into chunks that fit an embedding request, and <code>store.add</code> embeds each chunk and keeps the vector next to its text and metadata.</p>
<p><code>add</code> takes a Spring <code>Resource</code>, so an uploaded file (<code>FileSystemResource</code>) and a document bundled with the app (<code>ClassPathResource</code>) go through the same code. If you only need a summary of one file, <a class="" href="https://docs.webforj.com/zh/blog/summarize-documents-spring-ai">summarizing PDFs with Spring AI</a> uses the same upload and text extraction.</p>
<p><code>ask</code> asks for <code>chatClientResponse()</code> instead of plain <code>content()</code>. That keeps the advisor context attached to every streamed item, which is how the view finds out which documents were retrieved.</p>
<p><code>SimpleVectorStore</code> keeps vectors in memory. It suits a demo or a small internal tool. Swapping it for a database-backed store changes one line in the constructor, because the rest of the code only sees the <code>VectorStore</code> interface.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="upload">Add documents with the webforJ Upload component<a href="https://docs.webforj.com/zh/blog/spring-ai-rag#upload" class="hash-link" aria-label="直接链接到 Add documents with the webforJ Upload component" title="直接链接到 Add documents with the webforJ Upload component" translate="no">​</a></h2>
<p>The <a class="" href="https://docs.webforj.com/zh/docs/components/upload"><code>Upload</code></a> component (since 26.01) handles the picker, the drop zone, the file list, and the progress display. Configure it once and listen for one event:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">upload</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setSelectionMode</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Upload</span><span class="token class-name punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token class-name" style="color:var(--dwc-code-class)">SelectionMode</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">MULTIPLE</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setAutoUpload</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Upload</span><span class="token class-name punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token class-name" style="color:var(--dwc-code-class)">AutoUpload</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">ALWAYS</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">addFilter</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Documents"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"*.pdf;*.docx;*.txt;*.md"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">upload</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setVisible</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">false</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Upload</span><span class="token class-name punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token class-name" style="color:var(--dwc-code-class)">Part</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">UPLOAD_BUTTON</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">upload</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">onUpload</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">e </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> e</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getFiles</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">forEach</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">this</span><span class="token operator" style="color:var(--dwc-code-operator)">::</span><span class="token function" style="color:var(--dwc-code-function)">index</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><br></div></code></pre></div></div>
<p><code>AutoUpload.ALWAYS</code> starts the transfer as soon as files are picked or dropped, so the Upload button can be hidden. <code>onUpload</code> fires when the files have reached the server. Each <code>UploadedFile</code> is then moved out of the upload cache into a folder the view owns:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">index</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">UploadedFile</span><span class="token plain"> uploaded</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">try</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> target </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> uploads</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">resolve</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token constant" style="color:var(--dwc-code-number)">UUID</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">randomUUID</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"-"</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> uploaded</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getSanitizedClientName</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">toString</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">File</span><span class="token plain"> saved </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> uploaded</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">move</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">target</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">FileSystemResource</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">saved</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> uploaded</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getClientName</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">catch</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Exception</span><span class="token plain"> ex</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">Toast</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">show</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Could not read "</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> uploaded</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getClientName</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Theme</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">DANGER</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p><code>uploads</code> is a temporary folder the view creates with <code>Files.createTempDirectory</code>. Give every file a unique target. Calling <code>move()</code> without an argument places the file in the app's working directory under its client name, so adding the same file twice fails. Build paths from <code>getSanitizedClientName()</code>, never from the raw client name.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="stream-answer">Stream the answer into a MarkdownViewer<a href="https://docs.webforj.com/zh/blog/spring-ai-rag#stream-answer" class="hash-link" aria-label="直接链接到 Stream the answer into a MarkdownViewer" title="直接链接到 Stream the answer into a MarkdownViewer" translate="no">​</a></h2>
<p>Models answer in Markdown, so the <a class="" href="https://docs.webforj.com/zh/docs/components/markdownviewer"><code>MarkdownViewer</code></a> is the natural target. It renders appended text incrementally, and <code>setAutoScroll(true)</code> keeps the newest text in view while the answer grows.</p>
<p>The stream arrives on a Reactor thread, not on the webforJ UI thread, so every UI change goes back to the UI thread with <code>Environment.runLater</code>, as described in <a class="" href="https://docs.webforj.com/zh/docs/advanced/asynchronous-updates">Asynchronous Updates</a>. Capture the <code>Environment</code> while you are still on the UI thread and pass it in:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">ask</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> text </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> question</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getValue</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">text </span><span class="token operator" style="color:var(--dwc-code-operator)">==</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">null</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">||</span><span class="token plain"> text</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">isBlank</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">||</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">!</span><span class="token plain">ask</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">isEnabled</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token class-name" style="color:var(--dwc-code-class)">Environment</span><span class="token plain"> env </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Environment</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getCurrent</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token class-name" style="color:var(--dwc-code-class)">Set</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">String</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> used </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">LinkedHashSet</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token function" style="color:var(--dwc-code-function)">startAnswer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  kb</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">ask</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">text</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">subscribe</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      response </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        used</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">addAll</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">KnowledgeBase</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">sourcesOf</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">response</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> chunk </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> response</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">chatResponse</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getResult</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getOutput</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token class-name" style="color:var(--dwc-code-class)">Environment</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">runLater</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">env</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">showChunk</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">chunk</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      error </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Environment</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">runLater</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">env</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token function" style="color:var(--dwc-code-function)">showChunk</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Sorry, the answer failed: "</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> error</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getMessage</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token function" style="color:var(--dwc-code-function)">finishAnswer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Set</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">of</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Environment</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">runLater</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">env</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">finishAnswer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">used</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>Tasks queued with <code>runLater</code> run in the order they were submitted, so chunks appear in the right sequence.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="spinner">Show a spinner while searching<a href="https://docs.webforj.com/zh/blog/spring-ai-rag#spinner" class="hash-link" aria-label="直接链接到 Show a spinner while searching" title="直接链接到 Show a spinner while searching" translate="no">​</a></h2>
<p>Retrieval and the first tokens take a few seconds, so the user needs to see that something is happening. A <code>Spinner</code> with a short label shows in place of the answer until the first chunk arrives:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">thinking</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Spinner</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">SpinnerExpanse</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">SMALL</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Span</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Searching your documents..."</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><br></div></code></pre></div></div>
<p>The three state methods switch between waiting, streaming and done:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">startAnswer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  ask</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setEnabled</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">false</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  question</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setEnabled</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">false</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  answer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">clear</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  answer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setVisible</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">false</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  sources</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">removeAll</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  sources</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setVisible</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">false</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  thinking</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setVisible</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">true</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">showChunk</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token plain"> chunk</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">chunk </span><span class="token operator" style="color:var(--dwc-code-operator)">==</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">null</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">||</span><span class="token plain"> chunk</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">isEmpty</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  thinking</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setVisible</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">false</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  answer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setVisible</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">true</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  answer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">append</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">chunk</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">private</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">finishAnswer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Set</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">String</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> used</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  thinking</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setVisible</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">false</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  answer</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setVisible</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">true</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token operator" style="color:var(--dwc-code-operator)">!</span><span class="token plain">used</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">isEmpty</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    sources</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Span</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Sources"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    used</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">forEach</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">name </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> sources</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Span</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">name</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    sources</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setVisible</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">true</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  ask</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setEnabled</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">true</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  question</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setEnabled</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">true</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  question</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">focus</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>The question field and the Ask button stay disabled until the stream ends or fails, which stops a second question from interleaving with the first answer.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="show-sources">Show which documents the answer used<a href="https://docs.webforj.com/zh/blog/spring-ai-rag#show-sources" class="hash-link" aria-label="直接链接到 Show which documents the answer used" title="直接链接到 Show which documents the answer used" translate="no">​</a></h2>
<p>The RAG advisor puts the retrieved chunks into the response context under <code>RetrievalAugmentationAdvisor.DOCUMENT_CONTEXT</code>. Each chunk is a Spring AI <code>Document</code> that carries the <code>source</code> name set in <code>add</code>, so a short helper on the service turns the context into a list of names:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@SuppressWarnings</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"unchecked"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">static</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">String</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">sourcesOf</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">ChatClientResponse</span><span class="token plain"> response</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Document</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> docs </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Document</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> response</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">context</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getOrDefault</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">RetrievalAugmentationAdvisor</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token constant" style="color:var(--dwc-code-number)">DOCUMENT_CONTEXT</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">List</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">of</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">return</span><span class="token plain"> docs</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">stream</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">map</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">doc </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">String</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">valueOf</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">doc</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getMetadata</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">get</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"source"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">toList</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>The <code>Set</code> in <code>ask()</code> removes duplicates, since several chunks often come from one file. These are the documents the retriever handed to the model, and they let a reader open the original file to check the answer.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="empty-results">Tune what counts as relevant<a href="https://docs.webforj.com/zh/blog/spring-ai-rag#empty-results" class="hash-link" aria-label="直接链接到 Tune what counts as relevant" title="直接链接到 Tune what counts as relevant" translate="no">​</a></h2>
<p><code>similarityThreshold(0.5)</code> drops chunks that are not close enough to the question, and <code>topK(4)</code> caps how many reach the model. Tune both against your own documents. A higher threshold gives fewer but more relevant chunks, and the system prompt keeps the model from answering beyond the context it is given. To go further, the post on <a class="" href="https://docs.webforj.com/zh/blog/advanced-rag-java">advanced RAG with Spring AI</a> measures retrieval before adding query rewriting, hybrid search, and reranking.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="production">Production concerns<a href="https://docs.webforj.com/zh/blog/spring-ai-rag#production" class="hash-link" aria-label="直接链接到 Production concerns" title="直接链接到 Production concerns" translate="no">​</a></h2>
<p>A few things deserve attention before this leaves a prototype:</p>
<ul>
<li class=""><strong>Tenancy.</strong> The vector store in this service is shared by every user of the app. Tag chunks with an owner in their metadata and pass a filter expression per request with <code>.advisors(a -&gt; a.param(VectorStoreDocumentRetriever.FILTER_EXPRESSION, "owner == 'alice'"))</code>.</li>
<li class=""><strong>Upload limits.</strong> Set <code>webforj.fileUpload.maxSize</code> and <code>webforj.fileUpload.accept</code> in <code>webforj.conf</code> to enforce limits on the server, and <code>setMaxFileSize</code> and <code>setMaxFiles</code> on the component to give the user instant feedback. See <a class="" href="https://docs.webforj.com/zh/docs/configuration/properties">configuration properties</a>.</li>
<li class=""><strong>Long indexing.</strong> Embedding a large PDF takes a while. Run <code>add</code> from an <code>@Async</code> method and report progress with <code>Environment.runLater</code>, as shown in <a class="" href="https://docs.webforj.com/zh/docs/integrations/spring/background-jobs">background jobs</a>.</li>
<li class=""><strong>Persistence.</strong> Replace <code>SimpleVectorStore</code> with a database-backed store when documents must survive a restart.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-webforj">Why webforJ fits AI apps<a href="https://docs.webforj.com/zh/blog/spring-ai-rag#why-webforj" class="hash-link" aria-label="直接链接到 Why webforJ fits AI apps" title="直接链接到 Why webforJ fits AI apps" translate="no">​</a></h2>
<p>A RAG app is a server-side workflow with a UI on top. Files arrive at the server, embeddings and model calls happen on the server, and secrets such as API keys stay there. webforJ keeps the UI on the server too, so the view calls <code>KnowledgeBase</code> as a plain Java method and receives results as Java objects. The streaming handoff is one call to <code>Environment.runLater</code>, and the components you need for this app (<code>Upload</code>, <code>MarkdownViewer</code>, <code>TextField</code>, <code>Button</code>) are part of the framework. The result is a document Q&amp;A app that one Java developer can read top to bottom.</p>
<p>If you want the broader pattern around Spring AI and a chat surface, the earlier post <a class="" href="https://docs.webforj.com/zh/blog/add-ai-to-java-app">Adding an AI chat feature to a Java app</a> walks through a streaming chat with tools.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next-step">Next step<a href="https://docs.webforj.com/zh/blog/spring-ai-rag#next-step" class="hash-link" aria-label="直接链接到 Next step" title="直接链接到 Next step" translate="no">​</a></h2>
<p>Clone <a href="https://github.com/webforj/built-with-webforj/tree/main/webforj-askdocs" target="_blank" rel="noopener noreferrer" class="">AskDocs</a> to run the finished app, or generate a project with <a href="https://docs.webforj.com/startforj" target="_blank" rel="noopener noreferrer" class="">startforJ</a>, add the Spring AI starter for your model provider, and add the service and the view from this guide. Then read the <a class="" href="https://docs.webforj.com/zh/docs/components/upload"><code>Upload</code></a> docs to shape the picker for your users.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">Frequently asked questions<a href="https://docs.webforj.com/zh/blog/spring-ai-rag#faq" class="hash-link" aria-label="直接链接到 Frequently asked questions" title="直接链接到 Frequently asked questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-requirements">What do I need for a Spring AI RAG web UI in Java?<a href="https://docs.webforj.com/zh/blog/spring-ai-rag#faq-requirements" class="hash-link" aria-label="直接链接到 What do I need for a Spring AI RAG web UI in Java?" title="直接链接到 What do I need for a Spring AI RAG web UI in Java?" translate="no">​</a></h3>
<p>You need a Spring Boot app with the webforJ Spring integration, a Spring AI model starter that provides chat and embedding models, and the <code>spring-ai-rag</code>, <code>spring-ai-vector-store</code> and <code>spring-ai-tika-document-reader</code> artifacts. The webforJ <code>Upload</code> and <code>MarkdownViewer</code> components cover the UI, so no frontend code is required.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-advisor">Which Spring AI class adds retrieval to a ChatClient?<a href="https://docs.webforj.com/zh/blog/spring-ai-rag#faq-advisor" class="hash-link" aria-label="直接链接到 Which Spring AI class adds retrieval to a ChatClient?" title="直接链接到 Which Spring AI class adds retrieval to a ChatClient?" translate="no">​</a></h3>
<p><code>RetrievalAugmentationAdvisor</code> from the <code>spring-ai-rag</code> module. You pass it a <code>VectorStoreDocumentRetriever</code> and register it with <code>defaultAdvisors</code> on the <code>ChatClient.Builder</code>. Every prompt then runs a similarity search first and sends the matching chunks to the model as context.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-sources">How do I show which documents an answer came from?<a href="https://docs.webforj.com/zh/blog/spring-ai-rag#faq-sources" class="hash-link" aria-label="直接链接到 How do I show which documents an answer came from?" title="直接链接到 How do I show which documents an answer came from?" translate="no">​</a></h3>
<p>Request <code>chatClientResponse()</code> from the stream and read <code>RetrievalAugmentationAdvisor.DOCUMENT_CONTEXT</code> from the response context. It holds the retrieved <code>Document</code> objects, and their metadata carries the source name you set while indexing. Show the distinct names under the answer when the stream completes.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-runlater">Why do I need Environment.runLater when streaming?<a href="https://docs.webforj.com/zh/blog/spring-ai-rag#faq-runlater" class="hash-link" aria-label="直接链接到 Why do I need Environment.runLater when streaming?" title="直接链接到 Why do I need Environment.runLater when streaming?" translate="no">​</a></h3>
<p>The reactive stream delivers chunks on a thread that is not the webforJ UI thread, and components may only change on the UI thread. <code>Environment.runLater(env, task)</code> queues the update for the UI thread and keeps submission order, so the answer text appears in sequence.</p><div></div>]]></content>
        <author>
            <name>Hyyan Abo Fakher</name>
        </author>
        <category label="AI" term="AI"/>
        <category label="Spring" term="Spring"/>
        <category label="Chat" term="Chat"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[AI coding in Java: quality control for generated code]]></title>
        <id>https://docs.webforj.com/zh/blog/ai-generated-java-code-guardrails</id>
        <link href="https://docs.webforj.com/zh/blog/ai-generated-java-code-guardrails"/>
        <updated>2026-11-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[AI coding in Java needs quality control. Use the webforJ MCP server, Agent Skills and browserless TestBed tests to review AI-generated UI code with confidence.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://cdn.webforj.com/webforj-documentation/blogs/2026-11-04-ai-generated-java-code-guardrails/cover.jpg" alt="cover image" class="img_ev3q"></p>
<p>An AI assistant can write a Java view in seconds. The slow part comes after: reading the diff, wondering whether that method really exists, and clicking through the screen to see if it behaves. If every change needs that, the speed is gone.</p>
<p>AI coding in Java stays fast when the checks are automatic. webforJ gives you three: a source of truth the assistant reads before it writes, a workflow it has to follow, and tests that fail when it guesses wrong. The tests run in plain JUnit, with no browser, so quality control takes seconds.</p>
<div class="cta_UErl" role="complementary" aria-label="Using AI to build Java apps?"><div class="copy_XH8h"><p class="title_HfaR">Using AI to build Java apps?</p><p class="text_x4Wj">Ask the team about webforJ AI tooling: the MCP server, Agent Skills, and how they keep generated code correct.</p></div><div class="actions_9PxU"><a class="primary_idFC" href="https://webforj.com/schedule-discovery/" target="_blank" rel="noopener noreferrer">预约通话<!-- --> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 448 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"></path></svg></a><a class="secondary_GDTR" href="mailto:sales@basis.cloud?subject=AI%20and%20webforJ&amp;body=Hi%20webforJ%20team%2C%0A%0A%0A%0A(I%20was%20reading%3A%20)"><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48L48 64zM0 176L0 384c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-208L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"></path></svg> <!-- -->邮件咨询</a></div></div><h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-guardrails">Why does AI code need guardrails?<a href="https://docs.webforj.com/zh/blog/ai-generated-java-code-guardrails#why-guardrails" class="hash-link" aria-label="直接链接到 Why does AI code need guardrails?" title="直接链接到 Why does AI code need guardrails?" translate="no">​</a></h2>
<p>An assistant writes code that looks right. That is the problem. A method name that doesn't exist, a component configured the wrong way, a view that compiles but shows the wrong label: none of it announces itself, and a reviewer reading a diff will miss some of it.</p>
<p>Guardrails move the checking from the reviewer's eyes to the toolchain. There are three places to put them:</p>
<div class="table-wrapper"><div class="table-container" style="overflow-x:auto"><table><thead><tr><th>Stage</th><th>Question it answers</th><th>webforJ piece</th></tr></thead><tbody><tr><td>Before the code is written</td><td>Does the assistant know the real API?</td><td><a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/mcp">MCP server</a></td></tr><tr><td>While it is written</td><td>Does it follow the right workflow?</td><td><a class="" href="https://docs.webforj.com/zh/docs/ai-tooling/agent-skills">Agent Skills</a></td></tr><tr><td>After it is written</td><td>Does the view do what a user expects?</td><td><a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/overview">TestBed</a></td></tr></tbody></table></div><style data-emotion="css 1k371a6">@media print{.css-1k371a6{position:absolute!important;}}</style></div>
<p>The first two shape what the assistant produces. The third judges it, and that is the one that never depends on trusting the model. The wider picture, from an assistant in your running app to agents that use your UI, is covered in <a class="" href="https://docs.webforj.com/zh/blog/ai-java-web-framework">how webforJ is built for AI work</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="invented-apis">How do you stop invented APIs?<a href="https://docs.webforj.com/zh/blog/ai-generated-java-code-guardrails#invented-apis" class="hash-link" aria-label="直接链接到 How do you stop invented APIs?" title="直接链接到 How do you stop invented APIs?" translate="no">​</a></h2>
<p>Models fill gaps with plausible names. The webforJ MCP server closes the gap by letting the assistant ask instead of guess. It can search the webforJ knowledge base for the class or guide you need, read the styling surface of a DWC component, list the <code>--dwc-*</code> design tokens for your version, and validate any CSS, Java or Markdown source against that catalog, flagging unknown names with suggested corrections.</p>
<p>The setup for Claude Code is one command:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">claude mcp </span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token plain"> webforj-mcp https://mcp.webforj.com/mcp </span><span class="token parameter variable" style="color:var(--dwc-code-variable)">-t</span><span class="token plain"> http </span><span class="token parameter variable" style="color:var(--dwc-code-variable)">-s</span><span class="token plain"> user</span><br></div></code></pre></div></div>
<p>Cursor, Gemini CLI, Codex and other MCP clients connect to the same endpoint, <code>https://mcp.webforj.com/mcp</code>. Most teams should skip the manual setup and install the <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling">webforJ AI plugin</a>, which registers the server and the skills together. The <a class="" href="https://docs.webforj.com/zh/blog/ai-coding-assistant-java">AI coding assistant post</a> walks through that install.</p>
<p>A habit helps: name the framework in the prompt. "Add a filter to this webforJ table" sends the assistant to the server. "Add a filter to this table" sends it to its general memory of Java. Which framework to name is a separate choice, and our guide to the <a class="" href="https://docs.webforj.com/zh/blog/best-java-ui-frameworks">best Java UI framework for business apps</a> can help.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="skills">Can skills keep AI on your conventions?<a href="https://docs.webforj.com/zh/blog/ai-generated-java-code-guardrails#skills" class="hash-link" aria-label="直接链接到 Can skills keep AI on your conventions?" title="直接链接到 Can skills keep AI on your conventions?" translate="no">​</a></h2>
<p>The server answers questions. It doesn't tell the assistant when to ask or in what order to work. Agent Skills do that. Each skill is a task playbook that the assistant loads when your prompt matches, and the webforJ plugin ships eight of them:</p>
<ul>
<li class=""><code>webforj-adding-servlets</code> for REST endpoints and webhooks</li>
<li class=""><code>webforj-building-forms</code> for binding, validation and input masks</li>
<li class=""><code>webforj-creating-components</code> for wrapping web components and JavaScript libraries</li>
<li class=""><code>webforj-handling-timers-and-async</code> for timers, debouncers and background work</li>
<li class=""><code>webforj-localizing-apps</code> for translation and locale switching</li>
<li class=""><code>webforj-securing-apps</code> for login and role-based route access</li>
<li class=""><code>webforj-styling-apps</code> for theming with DWC tokens</li>
<li class=""><code>webforj-upgrading-versions</code> for major upgrades with OpenRewrite</li>
</ul>
<p>Two of these show what a guardrail looks like in practice. The async skill steers the assistant away from raw <code>java.util.Timer</code> and plain threads, which throw <code>IllegalStateException</code> the moment they reach a UI component. The styling skill validates every <code>--dwc-*</code> reference against the real catalog before anything lands in your CSS, so a theme doesn't silently break dark mode. Installing and using these skills is covered in <a class="" href="https://docs.webforj.com/zh/blog/ai-agent-skills-java">agent skills for Claude Code</a>.</p>
<p>Skills follow the open <a href="https://agentskills.io/specification" target="_blank" rel="noopener noreferrer" class="">Agent Skills</a> standard, so the same folders work across assistants.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="review-ui-code">How do you review AI-written UI?<a href="https://docs.webforj.com/zh/blog/ai-generated-java-code-guardrails#review-ui-code" class="hash-link" aria-label="直接链接到 How do you review AI-written UI?" title="直接链接到 How do you review AI-written UI?" translate="no">​</a></h2>
<p>Reading a diff is the weakest form of review for UI code. The better review is a test that uses the view the way a user does. In webforJ that test doesn't need a browser. The TestBed, available since 26.03, runs a view inside a plain JUnit test with no browser and no server. Each interaction finishes before the next line runs, so nothing waits and nothing flakes.</p>
<p>Say the assistant generated a newsletter form, the one the TestBed docs use as their example. The Subscribe button should stay disabled until the field holds an address:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockTitle_OeMC">NewsletterForm.java</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">NewsletterForm</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">extends</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Composite</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&lt;</span><span class="token generics class-name" style="color:var(--dwc-code-class)">Div</span><span class="token generics punctuation" style="color:var(--dwc-code-punctuation)">&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">public</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">NewsletterForm</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token plain"> email </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Email"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">Paragraph</span><span class="token plain"> status </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Paragraph</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token plain"> subscribe </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">        </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">            </span><span class="token string" style="color:var(--dwc-code-string)">"Subscribe"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> e </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> status</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Subscribed "</span><span class="token plain"> </span><span class="token operator" style="color:var(--dwc-code-operator)">+</span><span class="token plain"> email</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getValue</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    subscribe</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setEnabled</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token boolean" style="color:var(--dwc-code-number)">false</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    email</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">onModify</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">e </span><span class="token operator" style="color:var(--dwc-code-operator)">-&gt;</span><span class="token plain"> subscribe</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">setEnabled</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token operator" style="color:var(--dwc-code-operator)">!</span><span class="token plain">email</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">getValue</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">isBlank</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token function" style="color:var(--dwc-code-function)">getBoundComponent</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">email</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> subscribe</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> status</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>You write the test, or ask the assistant to write it, and then you own a contract the generated code has to satisfy. This is the test from the <a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/overview">TestBed overview</a>:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Test</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">shouldSubscribeTheEmailTheUserTyped</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">WebforjTestbed</span><span class="token plain"> context</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  context</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">NewsletterForm</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">var</span><span class="token plain"> email </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">ComponentLocator</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">of</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">TextField</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">byLabel</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Email"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">locate</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">var</span><span class="token plain"> subscribe </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">ComponentLocator</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">of</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Button</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">byText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Subscribe"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">locate</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token class-name" style="color:var(--dwc-code-class)">UserInteraction</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">type</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">email</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">,</span><span class="token plain"> </span><span class="token string" style="color:var(--dwc-code-string)">"ada@example.com"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token class-name" style="color:var(--dwc-code-class)">UserInteraction</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">click</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">subscribe</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token function" style="color:var(--dwc-code-function)">assertTrue</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">      </span><span class="token class-name" style="color:var(--dwc-code-class)">ComponentLocator</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">of</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">Paragraph</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">          </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">byText</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token string" style="color:var(--dwc-code-string)">"Subscribed ada@example.com"</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">          </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">isPresent</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>A snapshot test is just as short. This one, also from the docs, records a whole view:</p>
<div class="language-java codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:var(--dwc-code-text);--prism-background-color:var(--dwc-code-bg)"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-java codeBlock_bY9V thin-scrollbar" style="color:var(--dwc-code-text);background-color:var(--dwc-code-bg)"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:var(--dwc-code-text)"><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@WebforjTest</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token keyword" style="color:var(--dwc-code-keyword)">class</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">OrderSummaryTest</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token annotation punctuation" style="color:var(--dwc-code-punctuation)">@Test</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token keyword" style="color:var(--dwc-code-keyword)">void</span><span class="token plain"> </span><span class="token function" style="color:var(--dwc-code-function)">shouldMatchTheSnapshot</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token class-name" style="color:var(--dwc-code-class)">WebforjTestbed</span><span class="token plain"> context</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token keyword" style="color:var(--dwc-code-keyword)">var</span><span class="token plain"> summary </span><span class="token operator" style="color:var(--dwc-code-operator)">=</span><span class="token plain"> </span><span class="token keyword" style="color:var(--dwc-code-keyword)">new</span><span class="token plain"> </span><span class="token class-name" style="color:var(--dwc-code-class)">OrderSummary</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    context</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">add</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">summary</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">    </span><span class="token function" style="color:var(--dwc-code-function)">verifyThat</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token plain">summary</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">.</span><span class="token function" style="color:var(--dwc-code-function)">matchesSnapshot</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">(</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">)</span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain">  </span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:var(--dwc-code-text)"><span class="token plain"></span><span class="token punctuation" style="color:var(--dwc-code-punctuation)">}</span><br></div></code></pre></div></div>
<p>The test finds components by what a user sees, a field by its label and a button by its text, so it keeps passing when the assistant restructures the view and fails when the user would see something different. <code>UserInteraction</code> only makes gestures a user could make. Clicking a disabled button fails the test instead of running its listener, which catches the common generated-code mistake of wiring logic that a real user can never reach.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="snapshot">What does the snapshot add?<a href="https://docs.webforj.com/zh/blog/ai-generated-java-code-guardrails#snapshot" class="hash-link" aria-label="直接链接到 What does the snapshot add?" title="直接链接到 What does the snapshot add?" translate="no">​</a></h2>
<p>The snapshot is the part that fits AI work best. A snapshot writes down everything a user would see in the view, one line per component, and stores it next to the test. The first run writes the file. Every later run describes the view again and compares.</p>
<p>If the assistant adds a field, changes a label or hides a button while fixing something unrelated, the test fails and the change shows up as a plain text diff in code review. You review a short description of the screen instead of hunting through generated Java. See <a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/snapshots">Snapshots</a> for how to read the format.</p>
<p>Single assertions with <code>verifyThat</code> cover the specific facts, such as the text of a button after a click. Snapshots cover the rest of the view.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="trust">Can you trust AI-written UI code?<a href="https://docs.webforj.com/zh/blog/ai-generated-java-code-guardrails#trust" class="hash-link" aria-label="直接链接到 Can you trust AI-written UI code?" title="直接链接到 Can you trust AI-written UI code?" translate="no">​</a></h2>
<p>Trust the checks, not the model. A workable loop looks like this:</p>
<ol>
<li class="">The assistant reads the webforJ docs through the MCP server before it writes.</li>
<li class="">A skill sets the workflow for the task.</li>
<li class="">You commit the test first, or ask the assistant to write it and read it carefully.</li>
<li class="">The generated view has to pass it. A failure goes back to the assistant with the message, which names the component and both values.</li>
</ol>
<p>Browser tests still matter for styling, layout and client behavior, which only a browser shows. The TestBed covers what your views do when a user works with them, and it covers it fast enough to run on every change the assistant makes. The <a class="" href="https://docs.webforj.com/zh/blog/java-ui-testing-without-browser">UI testing without a browser post</a> goes deeper.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next-step">Next step<a href="https://docs.webforj.com/zh/blog/ai-generated-java-code-guardrails#next-step" class="hash-link" aria-label="直接链接到 Next step" title="直接链接到 Next step" translate="no">​</a></h2>
<p>Install the <a class="" href="https://docs.webforj.com/zh/docs/ai-tooling">webforJ AI plugin</a>, then follow <a class="" href="https://docs.webforj.com/zh/docs/testing/testbed/getting-started">TestBed getting started</a> to add the <code>webforj-testbed-junit</code> test dependency and write your first test. Spring Boot apps use the <code>webforj-spring-testbed</code> module instead.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq">Frequently asked questions<a href="https://docs.webforj.com/zh/blog/ai-generated-java-code-guardrails#faq" class="hash-link" aria-label="直接链接到 Frequently asked questions" title="直接链接到 Frequently asked questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-review">How do I review AI generated Java code faster?<a href="https://docs.webforj.com/zh/blog/ai-generated-java-code-guardrails#faq-review" class="hash-link" aria-label="直接链接到 How do I review AI generated Java code faster?" title="直接链接到 How do I review AI generated Java code faster?" translate="no">​</a></h3>
<p>Replace line-by-line reading with tests that act on the view the way a user does. A TestBed test locates components, types and clicks, then checks what the page shows, and a snapshot flags any visible change as a text diff.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="faq-spring">Do AI code guardrails work with Spring?<a href="https://docs.webforj.com/zh/blog/ai-generated-java-code-guardrails#faq-spring" class="hash-link" aria-label="直接链接到 Do AI code guardrails work with Spring?" title="直接链接到 Do AI code guardrails work with Spring?" translate="no">​</a></h3>
<p>Yes. Spring Boot apps use the <code>webforj-spring-testbed</code> module for tests with their beans and security, and the MCP server and Agent Skills are independent of how the app is built. The <code>webforj-securing-apps</code> skill prefers Spring Security when Spring Boot is on the classpath.</p><div></div>]]></content>
        <author>
            <name>webforJ</name>
        </author>
        <category label="AI" term="AI"/>
        <category label="MCP" term="MCP"/>
    </entry>
</feed>