ClientBundle & Images
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against GWT 2.13.1 (2026-06-19), gwt-maven-plugin 1.3.0 (net.ltgt.gwt.maven), JDK 21 running the compiler with -sourceLevel 17, the Jakarta Servlet 6 server of the Bookshelf scenario and the org.gwtproject:gwt BOM, checked on 2026-10-09.
ClientBundle is client code with a large build-time half and a small server half: you declare an interface in Java, a generator that runs inside the GWT compiler reads the files it names and decides how each one reaches the browser, and the web server that delivers the compiled application must send the right HTTP caching headers for the strong-named files the generator writes.
The module that provides it, com.google.gwt.resources.Resources, is inherited by com.google.gwt.user.UI and therefore by every module that inherits com.google.gwt.user.User, so the Bookshelf client needs no extra dependency.
Every listing on this page was compiled by the 2.13.1 compiler in a private copy of the Bookshelf scenario; the compiler output, the generated JavaScript, the files in the war directory, the DOM seen by headless Chrome 155 and the HTTP headers served by Jetty 12.1.10 are quoted as they were printed (Super Dev Mode used code server port 9912 and the web server port 8912).
A web application is mostly made of files that are not code: logos, icons, stylesheets, help texts, templates for download.
Delivering them naively costs one HTTP request each, freshness checks for every cached copy, and file names that can silently drift away from the Java code that mentions them.
ClientBundle turns each of those files into a typed Java method that the compiler checks, and then packages the file in the best way the browser and the file size allow: inside the JavaScript as a string or a data: URL, composited with other images, or as a content-addressed file that can be cached for a year.
Why ClientBundle exists
The Developer Guide divides the files of a deployed GWT application into three groups by how they should be cached:
| Class | Files | Right caching policy |
|---|---|---|
Never cache |
|
Revalidate on every visit; it is the small file that decides which permutation to load. |
Cache forever |
the compiled permutations ( |
The name is a hash of the content, so a changed file has a new name; a far-future expiry is safe. |
Everything else |
a hand-written |
No safe policy: if you cache it, users may keep an old copy after a release; if you do not, you pay a request and a freshness check every time. |
ClientBundle moves the "everything else" files into the "cache forever" class (or into the JavaScript itself, which is cached forever anyway).
The Developer Guide lists the goals: no uncertainty about whether the application gets the right contents, less non-determinism caused by intermediate proxy servers, more aggressive caching headers, consistency checks between file names and Java code at compile time, data: URLs and other embedding where the browser and size allow it, and an extensible design for new resource types.
The "non-goals" entry says it is not a file-system abstraction: a bundle holds files known at build time, not user uploads.
Book covers uploaded by librarians to the Bookshelf catalogue are fetched by URL at run time; the placeholder shown while a book has no cover is a bundle resource.
Source: GWT — ClientBundle: Goals
The model: a bundle interface, @Source and resource types
A bundle is an interface that extends com.google.gwt.resources.client.ClientBundle and declares no-argument accessor methods.
Each method returns a subtype of ResourcePrototype, and an optional @Source annotation names the file (or files) behind it:
The smallest useful bundle of the Bookshelf scenario holds the stylesheet and the logo.
GWT.create on the interface is ordinary deferred binding (see Deferred Binding & Generators): the module rule in Resources.gwt.xml sends every type assignable to ClientBundle to a generator, and the generator writes the implementation class.
package com.example.bookshelf.client.resources;
import com.google.gwt.core.client.GWT;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.ImageResource;
public interface AppResources extends ClientBundle {
AppResources INSTANCE = GWT.create(AppResources.class);
@Source("app.gss")
AppCss css();
@Source("logo.png")
ImageResource logo();
}
Source: GWT — ClientBundle: Examples
How the compiler builds a bundle
Three details of this pipeline matter in practice:
-
The generator runs once per combination of the properties the bundle depends on. Each
localevalue of the module gets its own generated class; in a first probe the compiler producedAppResources_default_InlineClientBundleGenerator,AppResources_en_InlineClientBundleGeneratorandAppResources_es_InlineClientBundleGenerator. Once a stylesheet of the bundle testeduser.agent(see below), the names becameAppResources_safari_en_InlineClientBundleGeneratorand so on, because the bundle now depends on that property too. This is what makes locale-specific files possible (see below). -
Unused accessors are pruned from the JavaScript. When a probe stopped calling
banner(), the compiled permutations contained zero references to the banner’s file name. The file itself was still copied into the output directory (see the pitfalls table), so pruning saves script size, not disk space. -
Generator results are cached between compiles. Since GWT 2.5 the
ClientBundlegenerators support result caching (the 2.5.0 release notes list a significant speedup of development refresh).
@Source: where files are found
@Source takes one or more classpath paths.
The generator tries each path first relative to the package of the bundle interface, and, if nothing is found there, as an absolute classpath path (the way ClassLoader.getResource expects it).
In the Bookshelf the bundle lives in com.example.bookshelf.client.resources, so @Source("app.gss") resolves to src/main/resources/com/example/bookshelf/client/resources/app.gss, and a file elsewhere can be named @Source("com/example/shared/logo.png").
When @Source is omitted, the accessor name plus the default extensions of the resource type is tried in the bundle’s package:
| Resource type | Extensions tried, in order | Example |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
none |
|
Only the resource types that read several files accept more than one path (a CssResource concatenates its stylesheets); ImageResource and DataResource must name exactly one, and the generator says so (see the pitfalls table).
Resources are located when the compiler runs, so a mistyped name is a compile error rather than a 404 in production.
Accessor rules, nested bundles and lookup by name
-
An accessor has no parameters and returns a type that extends
ResourcePrototypeorClientBundle(a nested bundle, which is how a big application groups icons by feature). The generator rejects anything else, see the pitfalls table. -
ResourcePrototype.getName()returns the name of the accessor method; it is also howClientBundleWithLookupfinds a resource. -
ClientBundleWithLookupaddsgetResource(String)andgetResources(). Its Javadoc warns that calling these methods stops the compiler from pruning any resource of the bundle, so use it only when you really need name-based access (for example a tool-bar built from configuration).
public interface IconSet extends ClientBundleWithLookup {
IconSet INSTANCE = GWT.create(IconSet.class);
@Source("icon-star.png") ImageResource star();
@Source("icon-warning.png") ImageResource warning();
@Source("icon-next.png") ImageResource next();
Nested nested(); // a nested bundle: only a bundle type, no @Source
interface Nested extends ClientBundle {
@Source("badge.png") ImageResource badge();
}
}
In the probe IconSet.INSTANCE.getResources().length was 3 (the three images; the nested bundle is not a resource of the outer one), getResource("star").getName() was star, getResource("nope") was null, and nested().badge().getWidth() was 16.
The resource types at a glance
| Type | You get | Delivery in 2.13.1 |
|---|---|---|
|
|
Each image under about 48 KiB is a |
|
|
A |
|
|
The text is a string constant inside the JavaScript (read as UTF-8 at compile time); synchronous. |
|
|
All external texts of the bundle go into one JSON file, |
|
|
A string of obfuscated CSS inside the JavaScript, injected when you call |
|
|
No file: a factory around |
a nested |
the nested bundle |
Its own generated class. |
The Developer Guide names GssResource in its GSS pages, but there is no such type in 2.13.1: GSS is a mode of CssResource (a .gss file in a CssResource accessor, with CssResource.enableGss on).
ImageResource
An ImageResource is the bundle’s image type.
It is deliberately not tied to a widget: it is a description of an image (a URL, a size and a position inside that URL) that widgets, cells, renderers and CSS rules can all use.
AppResources res = AppResources.INSTANCE;
Image logo = new Image(res.logo()); // widget
logo.setResource(res.logo()); // swap later
SafeHtml html = new ImageResourceRenderer().render(res.star()); // for cells and SafeHtml builders
AbstractImagePrototype.create(res.star()).applyTo(existingImage); // bridge for old APIs
Image(ImageResource) and Image.setResource (the Image widget is described on Widgets) set the width, height and (for strips) the clipping for you.
ImageResourceRenderer renders to SafeHtml, ImageResourceCell renders in the cell widgets (see Cell Widgets), and UiBinder declares one with <ui:image field="logo" src="logo.png"/> (see UiBinder).
getURL() is deprecated; use getSafeUri(), which returns a SafeUri that can be passed to Anchor.setHref(SafeUri) and similar methods without an unchecked string.
@ImageOptions
@ImageOptions on an accessor tunes the compile-time processing of one image:
| Attribute | Default | Effect |
|---|---|---|
|
|
The image is mirrored about the vertical axis when |
|
|
Scale the image at compile time to this size. If only one is set, the aspect ratio is kept. |
|
|
|
|
|
Always serve the image as a separate file instead of a |
@Source("icon-next.png")
@ImageOptions(flipRtl = true)
ImageResource next();
@Source("cover-default.png") // a 64 by 96 pixel file
@ImageOptions(width = 32) // becomes 32 by 48 at compile time
ImageResource thumbnail();
@Source("stripe.png")
@ImageOptions(repeatStyle = RepeatStyle.Horizontal)
ImageResource stripe();
@Source("icon-star.png")
@ImageOptions(preventInlining = true)
ImageResource starFile();
In the probe, thumbnail().getWidth() and getHeight() returned 32 and 48, starFile().getSafeUri() returned http://127.0.0.1:8912/bookshelf/88D0F514D62A5124E8D7C3C870FE10B9.cache.png while star() was a data URL, and in a second build with inlining off and the extra locale Arabic (ar, an RTL locale), LocaleInfo.getCurrentLocale().isRTL() was true and the strip served for next() was a different file (the RTL strip) from the one served for en.
Mirroring only matters for locales that are RTL; if the module has none, flipRtl costs a second image that is never used.
Source: GWT — ClientBundle: ImageOptions
Sprites or inlining: what the compiler really does
The Developer Guide and its Image Bundles page present ImageResource as the type through which all the images of an interface are combined into one optimized image.
In 2.13.1 that is only half of the story, because the default configuration prefers inlining.
The module Resources.gwt.xml defines the deferred-binding property ClientBundle.enableInlining (values true,false) and sets it to true; the inlining generator then treats every image as a stand-alone file and, when the file is small enough, embeds it as a base64 data: URL instead of building a strip.
Setting the property to false brings back the classic image strips.
The probe measured both configurations with the same bundle:
enableInlining = true (default), locale en
logo: 160x48 left=0 top=0 url=data:image/png;base64,iVBORw0K... (len 1266)
star: 16x16 left=0 top=0 url=data:image/png;base64,iVBORw0K... (len 222)
thumbnail: 32x48 left=0 top=0 url=data:image/png;base64,iVBORw0K... (len 810)
banner: 220x220 left=0 top=0 url=http://127.0.0.1:8912/bookshelf/C8E1F467097791503C560E209E8A... (145,604 bytes)
starFile: 16x16 left=0 top=0 url=http://127.0.0.1:8912/bookshelf/88D0F514D62A5124E8D7C3C870FE...
enableInlining = false, locale en
logo: 160x48 left=220 top=0 url=http://127.0.0.1:8913/bookshelf/88F667C0B902BBFDBC9DFB2FE92E...
thumbnail: 32x48 left=220 top=48 url=http://127.0.0.1:8913/bookshelf/88F667C0B902BBFDBC9DFB2FE92E...
warning: 16x16 left=220 top=96 url=http://127.0.0.1:8913/bookshelf/88F667C0B902BBFDBC9DFB2FE92E...
star: 16x16 left=220 top=112 url=http://127.0.0.1:8913/bookshelf/88F667C0B902BBFDBC9DFB2FE92E...
next: 16x16 left=220 top=128 url=http://127.0.0.1:8913/bookshelf/88F667C0B902BBFDBC9DFB2FE92E...
banner: 220x220 left=0 top=0 url=http://127.0.0.1:8913/bookshelf/88F667C0B902BBFDBC9DFB2FE92E...
stripe: 8x24 left=0 top=0 url=http://127.0.0.1:8913/bookshelf/BB9E1A9C36A001C52218A276672C...
With inlining on, getLeft() and getTop() are always 0: nothing is composited.
With inlining off, the images share one strip (88F667...cache.png, 169,698 bytes in the probe, mostly the noisy banner) and each ImageResource knows its rectangle (left=220 top=112 for the star); the horizontally repeating stripe is arranged in a strip of its own, and the preventInlining star is a separate file.
The DOM shows what the widget does with each result.
new Image(res.star()) in the default configuration is a plain <img> with the data URL as its source:
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9h..." width="16" height="16" class="gwt-Image">
With inlining off the same widget is a transparent one-pixel GIF (clear.cache.gif, shipped by GWT) whose CSS background selects the rectangle of the strip:
<img src="http://127.0.0.1:8913/bookshelf/clear.cache.gif"
style="width:16px;height:16px;background:url(http://127.0.0.1:8913/bookshelf/88F667C0B902BBFDBC9DFB2FE92ED2DC.cache.png) no-repeat -220px -112px;"
border="0" class="gwt-Image">
Widgets that build images through AbstractImagePrototype use the second form even with inlining on. The stock Tree with a bundle of its own (shown in the migration section) rendered each icon as an img with clear.cache.gif as its source and the data URL in the CSS background.
Which setting is better? Inlining removes the request entirely, but the bytes are base64 (about a third bigger) and are repeated in the JavaScript of every permutation (a browser downloads only its own permutation, but that file grows); strips need one extra request but keep the script small and let the image be cached on its own. The Developer Guide’s argument for strips (browsers allow only two connections per host) describes HTTP/1.1 browsers of its time; modern browsers allow more and HTTP/2 multiplexes requests, so the gain from spriting is smaller today than the guide suggests. Measure with your own bundle before changing the default.
Sprites in CSS
A CSS rule can use an ImageResource of the same bundle as its background, with the width and height filled in for you.
In the legacy CSS syntax this is the @sprite rule (@sprite .rating { gwt-image: "star"; }); in GSS it is the gwt-sprite property, which is replaced by the properties needed to show the image:
.rating {
gwt-sprite: "star";
display: inline-block;
}
.shelfBar {
gwt-sprite: "stripe"; /* stripe has RepeatStyle.Horizontal */
}
The compiler turned the first rule into this CSS (the data URL is shortened here):
.CFEWP3D-a-d{height:16px;width:16px;overflow:hidden;background:url(data:image/png;base64,iVBORw0K...) -0px -0px no-repeat;display:inline-block}
.CFEWP3D-a-e{height:24px;overflow:hidden;background:url(data:image/png;base64,iVBORw0K...) -0px -0px repeat-x}
Note that the horizontally repeating sprite gets a height but no width, and repeat-x; a vertically repeating one would get a width and repeat-y.
With inlining off the same rules reference the strip’s URL and the negative offsets of the image inside it.
The accessor String rating(); on the CssResource interface returned the obfuscated name CFEWP3D-a-d in that build (obfuscated names change from build to build); in Super Dev Mode the same accessor returned the readable com-example-bookshelf-client-resources-AppCss-rating.
The data-resource twin of sprites is @url in CSS and resourceUrl("accessorName") in GSS: both produce a url(...) from a DataResource or ImageResource accessor of the same bundle.
The full syntax of these directives is on Styling: CSS & GSS.
Supported image formats
The generator reads images with Java’s ImageIO framework at compile time, and the default extensions are PNG, JPEG, GIF and BMP.
Animated GIFs are accepted but are never put in a strip (isAnimated() is true); they are still served efficiently by the bundle machinery.
SVG is not an image format for ImageResource: pointing an ImageResource at an .svg file stops the compile with Unrecognized image file format.
Declare SVG files as DataResource with @MimeType("image/svg+xml") (they then get a data: or strong-named URL you can use in <img src> or CSS), or inline the markup through a TextResource and SafeHtml when the file is trusted build-time content.
The compiler also decodes every image it bundles, so only commit images you trust (the legacy ImageBundle Javadoc warns about a JVM vulnerability in the ImageIO classes; keep the JDK current).
DataResource
A DataResource is the simplest resource: a URL by which the contents of a file can be fetched.
Use it for anything that is not an image or text the compiler should understand: a spreadsheet template, a PDF, a font, a custom cursor, an SVG or an audio clip.
@Source("catalogue-template.csv")
@MimeType("text/csv")
DataResource template();
@Source("catalogue-template.csv")
@DoNotEmbed
DataResource templateFile();
@Source("export-sample.csv")
DataResource sample();
Anchor template = new Anchor("CSV template");
template.setHref(res.template().getSafeUri()); // data: URL, the file has 65 bytes
template.getElement().setAttribute("download", "catalogue-template.csv");
The three accessors were delivered like this:
template.url = data:text/csv;base64,dGl0bGUsYXV0aG9yLHllYXIsY29waWVzCiJFeGFtcGxlIFRpdGxlIiwiRXhhbXBsZSBBdXRob3IiLDIwMDEsMwo=
templateFile.url = http://127.0.0.1:8912/bookshelf/8D8BD1DF43E414CEF7E20BF9BA7F33E7.cache.csv
sample.url = http://127.0.0.1:8912/bookshelf/F5C30E10877F8DB1884AB44A9EB454DC.cache.csv
-
@MimeTypesets the type used in thedata:URL. Without it the generator asks the file connection for its content type (the JDK guesses from the extension); set it explicitly for types the JDK does not know. -
@DoNotEmbedforces a file even for a tiny resource. Use it for browsers or plug-ins that cannot read RFC 2397data:URLs, or when the resource is fetched by code that cannot use them.templateFile()above did exactly that, and the URL it returned is the strong-named copy. -
The size limit. The inlining generator embeds a resource only when its bytes are fewer than 65,536 (
MAX_INLINE_SIZE = 2 << 15) and the encoded string is shorter than 65,535 characters (MAX_ENCODED_SIZE). Base64 inflates data by a third, so the real limit is about 48 KiB of raw file. The 141,934-byteexport-sample.csvwas therefore a file. -
Identical content is stored once. The strong name is a hash of the bytes (Murmur3, upper-case hex), so two accessors that point at the same content share one output file. With inlining off,
template()andtemplateFile()returned the same8D8BD1DF...cache.csvURL. -
getUrl()is deprecated; usegetSafeUri().asString()for a string and pass theSafeUriitself where an API accepts it.
Source: GWT — ClientBundle: DataResource
TextResource and ExternalTextResource
Both types give access to static text, such as a licence, a help page, an e-mail template or a JSON schema. The difference is when the text arrives.
TextResource: part of the script
TextResource.getText() returns a string that was written into the compiled JavaScript as a constant, so it is available synchronously and costs no request:
@Source("about.txt")
TextResource about();
Button about = new Button("About");
about.addClickHandler(e -> Window.alert(res.about().getText()));
The text is read as UTF-8 at compile time. The compiled permutation shows the literal in place of the file:
alert_0(($clinit_AppResources_safari_en_InlineClientBundleGenerator$aboutInitializer() ,
$clinit_AppResources_safari_en_InlineClientBundleGenerator() ,
'Bookshelf 1.0\nA small catalogue of books, authors, reviews and loans.\n'));
Because the text becomes part of every permutation, a large help document in a TextResource makes every download bigger; that is what the external variant is for.
The text is data: show it with setText, setInnerText or Label, not setHTML.
If the file really contains HTML that you wrote and review, convert it once with SafeHtmlUtils.fromTrustedString at one reviewed place, and never mix user-supplied values into it.
ExternalTextResource: one file, fetched on demand
An ExternalTextResource is identical in meaning but not in delivery: the text is not in the script.
All external texts of one bundle are collected into a single JSON file with a strong name, fetched the first time any of them is requested, and cached for the rest of the page’s life:
@Source("changelog.txt")
ExternalTextResource changelog();
try {
res.changelog().getText(new ResourceCallback<TextResource>() { // one request, then cached
@Override public void onError(ResourceException e) { Window.alert("Changelog unavailable: " + e.getMessage()); }
@Override public void onSuccess(TextResource text) { Window.alert(text.getText()); }
});
} catch (ResourceException e) { // the request could not even be started
Window.alert("Changelog request failed: " + e.getMessage());
}
The bundle file in the output directory was 80A51388D2B8B1A22EB4D1964373A575.cache.txt, 49 bytes, and contained a JSON array holding the (only) text:
[
"1.0 first release\n1.1 loans and reviews\n"]
Points to know:
-
Two ways to fetch. By default the file is fetched with an
XMLHttpRequest(so it must be same-origin). The configuration propertyExternalTextResource.useJsonp(defaultfalse) switches to JSONP for a bundle served from another host.getTextthrowsResourceExceptionwhen the request cannot be initiated, andonErrorreceives aResourceExceptionif the payload cannot be evaluated. -
It evaluates the response with
eval. The prototype checks the payload against the JSON-safety regular expression from RFC 4627 and then callseval. Under a Content-Security-Policy without'unsafe-eval'the call does not work. The probe ran the application underdefault-src 'self'; script-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline': the application itself, the CSS injection and everydata:image worked, but thechangelogcallback was never invoked, neitheronSuccessnoronError. Adding'unsafe-eval'toscript-srcmade it work. In a strict-CSP application use aTextResource, aDataResourcefetched withRequestBuilderand parsed by your own code, or a normal RPC or REST call. -
The data URL rule does not apply: external text is always a file.
CssResource and GSS
A CssResource accessor turns a stylesheet into a typed interface whose methods return the (obfuscated) class names and constants of the file.
The bundle holds the stylesheet text, and ensureInjected() puts it in a <style> element once:
public interface AppCss extends CssResource {
String header();
String nav();
String lowStock();
String rating();
}
AppResources.INSTANCE.css().ensureInjected(); // safe to call many times; usually first thing in onModuleLoad
Everything about the language belongs to Styling: CSS & GSS; this page only needs the packaging facts:
-
The stylesheet is a string inside the JavaScript, not a file, so it is never a separate request and it is always as fresh as the script.
-
The same accessor takes a
.cssfile (the legacy syntax) or a.gssfile (Google Closure Stylesheets). GSS needs<set-configuration-property name="CssResource.enableGss" value="true"/>in the module; with it off the compiler stops withGSS is not enabled. Add the following line to your gwt.xml file to enable it. The related propertiesCssResource.conversionMode(strict,lenient,off, defaultoff) andCssResource.gssDefaultInUiBinderare described on Styling: CSS & GSS. -
Class names in the stylesheet are strict by default: every class selector needs a
Stringaccessor on the interface, or the compile fails (see the pitfalls table). -
The sprite and URL functions above (
gwt-sprite,resourceUrl,@url,@sprite) are how a stylesheet reaches the bundle’s images and data files. Conditional rules such as@if (is("safari")) { ... }are evaluated per permutation (a probe stylesheet with aletter-spacingof 3px for Safari and 5px otherwise produced the 3px text only in thesafaripermutations and the 5px text only ingecko1_8).
Source: GWT — ClientBundle: CssResource
GwtCreateResource
GwtCreateResource<T> is a bridge between a bundle and any type that GWT can instantiate: it is a factory that calls GWT.create for you.
It is useful when a class should not know which implementation it gets, or when an interface wants to hand out several things that already exist in the module, such as a Messages interface.
GwtCreateResource<AppMessages> messages(); // GWT.create(AppMessages.class)
@ClassType(ShortCitation.class) // create the subtype, expose the interface
GwtCreateResource<BookFormatter> formatter();
public interface BookFormatter { String format(Book book); }
public class ShortCitation implements BookFormatter { // default-instantiable
@Override public String format(Book book) {
return book.getAuthor() + " (" + book.getYear() + "): " + book.getTitle();
}
}
// elsewhere:
BookFormatter formatter = AppResources.INSTANCE.formatter().create();
String line = formatter.format(book); // "Frank Herbert (1965): Dune"
Each create() call returns a new instance.
No deferred-binding rule is needed for ShortCitation, as long as it has a public no-argument constructor.
@ClassType names the class passed to GWT.create; without it the type argument is used.
The Javadoc of @ClassType suggests using it for RPC, as @ClassType(Service.class) GwtCreateResource<ServiceAsync>.
That pattern does not compile in 2.13.1: the generator writes a create() method whose return type is the @ClassType class (CatalogService), which is not assignable to CatalogServiceAsync, and the compile stops with The return type is incompatible with GwtCreateResource<CatalogServiceAsync>.create().
Use a subtype of the type argument in @ClassType, as above, and create RPC proxies with GWT.create(CatalogService.class) directly.
Locale-specific resources
ClientBundle is integrated with GWT internationalization.
For each value of the deferred-binding property locale, the generator looks for variations of the file name in the manner of java.util.ResourceBundle, best match first.
For @Source("default.txt") and the locale fr_FR it tries default_fr_FR.txt, default_fr.txt and default.txt; the locale’s segments are removed from the right one by one.
This works for every resource type, so you can ship ownersManual_en.pdf and ownersManual_fr.pdf behind one accessor.
The Bookshelf module declares the locales en and es (plus the implicit default).
Next to logo.png and about.txt it has logo_es.png and about_es.txt:
src/main/resources/com/example/bookshelf/client/resources/
logo.png logo_es.png
about.txt about_es.txt
Loading the page with ?locale=es in the probe returned a different logo (the data URL was 1,274 characters instead of 1,266) and the Spanish text:
locale=en about=Bookshelf 1.0 | A small catalogue of books, authors, reviews and loans.
locale=es about=Libro de estanteria 1.0 | Un pequeno catalogo de libros, autores, resenas y prestamos.
locale=default (no locale parameter, no meta tag) falls back to the unsuffixed files
Things to remember:
-
The lookup is done at compile time, per locale. Each locale permutation contains only its own variant, so English users never download the Spanish logo.
-
The file name suffix must match the locale value of the module, not the browser’s language: if the module declares only
es, a filelogo_es_MX.pngis never used. -
Locale permutations multiply the work and the output: three locales and two user agents produced six permutations of about 288 KB each in the probe. That is the same price you already pay for
Messages. -
A missing variant is silently replaced by the base file; there is no warning when
logo_es.pngis forgotten.
See Internationalization for the locale property, locale.queryparam and <extend-property>.
Source: GWT — ClientBundle: I18N
User-agent-specific resources
There is no _safari suffix for file names: the file lookup knows only the locale property.
Two mechanisms cover browser differences.
Inside a stylesheet.
CSS and GSS conditionals can test the user.agent property, and the choice is made at compile time (no runtime cost).
In GSS it is @if (is("safari")) { ... } @else { ... }; in the legacy CSS syntax @if user.agent safari { ... }.
The probe confirmed that each permutation keeps only the matching branch.
A different file for a different engine.
Choose between two bundles with ordinary deferred binding.
A <replace-with> rule cannot name a bundle interface as its replacement: the compile stops with Rebind result 'com.example.bookshelf.client.resources.BadgeResourcesWebkit' must be a class.
Put the rule on a small factory class that deferred binding replaces, and let each factory create its own bundle:
public interface BadgeResources extends ClientBundle {
BadgeResources INSTANCE = GWT.<Factory>create(Factory.class).create();
@Source("badge.png")
ImageResource badge();
class Factory { // default behaviour
BadgeResources create() { return GWT.create(BadgeResources.class); }
}
}
public interface BadgeResourcesWebkit extends BadgeResources {
@Override @Source("badge_webkit.png")
ImageResource badge();
class Factory extends BadgeResources.Factory { // chosen for user.agent safari
@Override BadgeResources create() { return GWT.create(BadgeResourcesWebkit.class); }
}
}
<replace-with class="com.example.bookshelf.client.resources.BadgeResourcesWebkit.Factory">
<when-type-is class="com.example.bookshelf.client.resources.BadgeResources.Factory" />
<when-property-is name="user.agent" value="safari" />
</replace-with>
In headless Chrome, which selected the safari permutation, BadgeResources.INSTANCE.badge() returned the 214-character data URL of badge_webkit.png. The compiled safari permutations contain BadgeResourcesWebkit_en_InlineClientBundleGenerator, the gecko1_8 ones contain BadgeResources_en_InlineClientBundleGenerator and no Webkit class at all.
GWT 2.13.1 defines only two values for the property, gecko1_8 and safari (UserAgent.gwt.xml), so a user-agent split gives each locale exactly two permutations; use it only for a real rendering difference.
Many "browser-specific image" needs are better served by CSS media queries and image-set().
The knobs: deferred-binding and configuration properties
Resources.gwt.xml defines these properties that affect bundles (all verified in the 2.13.1 file):
| Property | Default | Effect |
|---|---|---|
|
|
|
|
|
|
|
|
Fetch the external text bundle with JSONP instead of XHR. |
|
CSS and GSS handling. |
Set them in the module file: <set-property name="ClientBundle.enableInlining" value="false"/> and <set-configuration-property name="ClientBundle.enableRenaming" value="false"/>.
Turning renaming off removes the cache-forever property: the probe’s war directory then held banner.png, export-sample.csv and catalogue-template.csv under their original names, the external-text bundle as com_example_bookshelf_client_resources_AppResources_jsonbundle.txt, and three temporary-named images like ImageResourceGenerator17549398938070882451.png.
Two bundles with a same-named file would then collide, and the files can no longer be given a far-future expiry; leave renaming on.
The source tree still contains an MHTML-based generator from the Internet Explorer era, but 2.13.1’s Resources.gwt.xml binds only the static and the inline generators.
Caching: what is written and how to serve it
The files in the war directory
After mvn package the module output directory of the six-permutation probe held (sizes in bytes; ls -l columns trimmed):
288399 4A0539B63BAAA942B515B6077F5B15AE.cache.js locale es, user.agent safari
288738 59C0932D131152DAEA4B11E6D5FFFA93.cache.js locale es, user.agent gecko1_8
288299 8CAFC011635EE41CB5E435F33C3AC410.cache.js locale en, user.agent safari
288658 C1674B6986147ACB5C987C1047311F24.cache.js locale default, user.agent gecko1_8
288638 C24C938499B73830E2AFCD752F530B14.cache.js locale en, user.agent gecko1_8
288319 CACD996127D36DF0EC42B9A8B65E791C.cache.js locale default, user.agent safari
49 80A51388D2B8B1A22EB4D1964373A575.cache.txt ExternalTextResource bundle
150 88D0F514D62A5124E8D7C3C870FE10B9.cache.png starFile (preventInlining)
65 8D8BD1DF43E414CEF7E20BF9BA7F33E7.cache.csv templateFile (DoNotEmbed)
145604 C8E1F467097791503C560E209E8A3251.cache.png banner (over the limit)
141934 F5C30E10877F8DB1884AB44A9EB454DC.cache.csv sample (over the limit)
1415 85B004BBB10BADC9964AD6DE9572205E.gwt.rpc RPC serialization policy
933 CC3ED00F7EAF7D45F411FC9A39408C4A.gwt.rpc RPC serialization policy
43 clear.cache.gif shipped by GWT, used for image strips
7847 bookshelf.nocache.js bootstrap, revalidate
14281 bookshelf.devmode.js
460 compilation-mappings.txt permutation to locale and user.agent
Every file whose name contains .cache. is content-addressed: its name is a hash of its bytes, so it can never change without changing its URL.
The compiled permutations follow the same rule, and bookshelf.nocache.js is the only file the browser must always ask for.
The generated JavaScript refers to the resource files through the module base URL.
Compiled with -style PRETTY (add -Dgwt.style=PRETTY to the Maven command) the bundle’s static fields read:
externalImage = $moduleBase + 'C8E1F467097791503C560E209E8A3251.cache.png';
externalImage3 = $moduleBase + '88D0F514D62A5124E8D7C3C870FE10B9.cache.png';
externalTextUrl = $moduleBase + '80A51388D2B8B1A22EB4D1964373A575.cache.txt';
...
sample = new DataResourcePrototype(($clinit_UriUtils() , new SafeUriString($moduleBase + 'F5C30E10877F8DB1884AB44A9EB454DC.cache.csv')));
templateFile = new DataResourcePrototype(($clinit_UriUtils() , new SafeUriString($moduleBase + '8D8BD1DF43E414CEF7E20BF9BA7F33E7.cache.csv')));
template = new DataResourcePrototype(($clinit_UriUtils() , new SafeUriString('data:text/csv;base64,dGl0bGUsYXV0aG9yLHllYXIs...')));
Source: GWT — ClientBundle: DataResource
Telling the web server
GWT writes the files but does not set HTTP headers; that is the job of whatever serves the war.
The Developer Guide asks for the *.cache.* glob to be served with publicly cacheable headers and a far-future Expires.
In the Bookshelf the servlet container serves the compiled application next to the RPC endpoint, so a small Jakarta filter does it (the pattern .cache. does not match .nocache., which has no dot before cache):
public class CacheHeadersFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
String path = ((HttpServletRequest) req).getRequestURI();
HttpServletResponse response = (HttpServletResponse) resp;
if (path.contains(".nocache.")) {
response.setHeader("Cache-Control", "no-cache"); // always revalidate the bootstrap
} else if (path.contains(".cache.")) {
response.setHeader("Cache-Control", "public, max-age=31536000, immutable");
}
chain.doFilter(req, resp);
}
}
<filter>
<filter-name>cacheHeaders</filter-name>
<filter-class>com.example.bookshelf.server.CacheHeadersFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>cacheHeaders</filter-name>
<url-pattern>/bookshelf/*</url-pattern>
</filter-mapping>
Jetty 12.1.10 served the war with these headers (trimmed):
$ curl -sI http://127.0.0.1:8912/bookshelf/88D0F514D62A5124E8D7C3C870FE10B9.cache.png
HTTP/1.1 200 OK
Cache-Control: public, max-age=31536000, immutable
Content-Type: image/png
Content-Length: 150
$ curl -sI http://127.0.0.1:8912/bookshelf/bookshelf.nocache.js
HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Type: text/javascript
immutable tells browsers that support it not to revalidate even on reload, which is correct for hash-named files.
If you put a CDN or reverse proxy in front, give it the same two rules by path pattern.
In Super Dev Mode the code server on port 9912 serves the same strong names without cache headers, which is fine for development.
Cookies on static files break shared caching
The first version of the Bookshelf filter chain mapped the session filter (the one that makes sure an XSRF token can be derived from the session) to /*.
Every static file then answered with a Set-Cookie: JSESSIONID=... header, and Jetty added Expires: Thu, 01 Jan 1970 00:00:00 GMT next to the filter’s Cache-Control.
Shared caches generally do not store responses that set cookies, and the contradictory Expires is noise at best.
Mapping the session filter only to the RPC and REST paths (/bookshelf/catalog, /bookshelf/xsrf, /api/*) removed both headers from the static responses; keep the paths that deliver *.cache.* files free of sessions, cookies and authentication.
Worked example: the Bookshelf header strip
The Bookshelf shell needs a header with the logo (different for Spanish), a star rating icon in a CSS rule, a link to download a CSV template, an "About" text and a "Changelog" that is rarely opened. One bundle serves all of it.
public interface AppResources extends ClientBundle {
AppResources INSTANCE = GWT.create(AppResources.class);
@Source("app.gss") AppCss css(); // stylesheet, inside the script
@Source("logo.png") ImageResource logo(); // logo_es.png in the es permutation
@Source("icon-star.png") ImageResource star(); // small: data URL
@Source("icon-next.png") @ImageOptions(flipRtl = true) ImageResource next();
@Source("cover-default.png") @ImageOptions(width = 32) ImageResource thumbnail();
@Source("banner.png") ImageResource banner(); // 145,604 bytes: a cache file
@Source("catalogue-template.csv") @MimeType("text/csv") DataResource template();
@Source("export-sample.csv") DataResource sample(); // 141,934 bytes: a cache file
@Source("about.txt") TextResource about(); // about_es.txt for es
@Source("changelog.txt") ExternalTextResource changelog();
GwtCreateResource<AppMessages> messages();
@ClassType(ShortCitation.class) GwtCreateResource<BookFormatter> formatter();
}
/* app.gss, the rules that use the bundle */
.rating {
gwt-sprite: "star";
display: inline-block;
}
public class BookToolbar extends Composite {
public BookToolbar() {
AppResources res = AppResources.INSTANCE;
res.css().ensureInjected();
FlowPanel panel = new FlowPanel();
panel.add(new Image(res.logo()));
InlineLabel stars = new InlineLabel();
stars.setStyleName(res.css().rating());
panel.add(stars);
Anchor template = new Anchor("CSV template");
template.setHref(res.template().getSafeUri());
template.getElement().setAttribute("download", "catalogue-template.csv");
panel.add(template);
Button about = new Button("About");
about.addClickHandler(e -> Window.alert(res.about().getText()));
panel.add(about);
Button changes = new Button("Changelog");
changes.addClickHandler(e -> showChangelog(res)); // ExternalTextResource, see above
panel.add(changes);
initWidget(panel);
}
}
The DOM that headless Chrome produced for ?locale=en and ?locale=es (data URLs shortened):
<img src="data:image/png;base64,iVBORw0KGgoA..." width="160" height="48" class="gwt-Image">
<span class="CFEWP3D-a-d"></span> <!-- obfuscated name, differs per build -->
<a class="gwt-Anchor" href="data:text/csv;base64,dGl0bGUsYXV0..." download="catalogue-template.csv">CSV template</a>
<button type="button" class="gwt-Button">About</button>
<button type="button" class="gwt-Button">Changelog</button>
Where each byte went:
| Accessor | Size | Delivery |
|---|---|---|
|
1 KB or less |
|
|
16 by 16 |
The same star as a CSS background, data URL inside the injected stylesheet |
|
65 bytes |
|
|
145,604 and 141,934 bytes |
|
|
70 bytes |
A string in the script |
|
42 bytes |
|
The covers of real books are not in the bundle: the catalogue returns a URL per book, and a view builds new Image(SafeUri) from it, showing res.thumbnail() only while a book has no cover.
That keeps the bundle limited to assets the build knows.
Source: GWT — ClientBundle: Examples
Migrating from ImageBundle
ImageBundle is the 2007 mechanism (GWT 1.4) that the book teaches in its widgets chapter: an interface extending ImageBundle, methods returning AbstractImagePrototype, and images named by the method name or by a resource annotation.
The book writes the file name in a doc-comment tag; the ImageBundle Javadoc says that form was superseded by the @Resource annotation, and neither is used by current code.
ClientBundle arrived in GWT 2.0.0 as a generalisation of the idea, and in 2.13.1 the old interface is still in the jar but marked @Deprecated (its Javadoc says it is replaced by ClientBundle and ImageResource).
It still works. To check, the probe compiled a legacy bundle with the 2.13.1 compiler on JDK 21:
@SuppressWarnings("deprecation")
public interface ToolBarImageBundle extends ImageBundle {
@Resource("com/example/bookshelf/client/resources/icon-star.png")
AbstractImagePrototype star();
}
It compiled without a GWT warning (javac reports the deprecation only), a generated class ToolBarImageBundle_generatedBundle appeared, and createImage() produced an image whose URL was http://127.0.0.1:8912/bookshelf/88D0F514D62A5124E8D7C3C870FE...cache.png: for this bundle the legacy generator wrote a strong-named file and did not inline (the file has the same hash as the preventInlining star, so both share it).
Plan to migrate, because nothing improves the legacy path any more.
| ImageBundle | ClientBundle | Note |
|---|---|---|
|
|
Add |
|
|
|
|
|
Paths are relative to the interface’s package, then classpath-absolute, as before. |
the method name selects |
the method name selects |
The order of |
|
|
Same widget; sets size and clipping. |
|
|
|
|
|
Both return |
The widgets of the library followed the same road: Tree.Resources replaces TreeImages, MenuBar.Resources replaces MenuBarImages, and HorizontalSplitPanel.Resources replaces HorizontalSplitPanelImages; the old ImageBundle-based interfaces and constructors are @Deprecated.
Restyling a stock widget is therefore a bundle that extends the widget’s:
public interface ShelfTreeResources extends Tree.Resources {
@Override @Source("icon-next.png") ImageResource treeClosed();
@Override @Source("icon-star.png") ImageResource treeLeaf();
@Override @Source("icon-warning.png") ImageResource treeOpen();
}
Tree tree = new Tree(GWT.<ShelfTreeResources>create(ShelfTreeResources.class));
The accessor names must match the widget’s own (treeClosed, treeLeaf, treeOpen); the tree then draws its icons from your files.
Security notes
-
Treat text resources as data.
getText()output goes tosetText,setInnerTextorLabel.setHTMLwith a bundle text is only acceptable for markup that you wrote and review; wrap it once withSafeHtmlUtils.fromTrustedStringand never concatenate user input into it. -
SafeUriis trusted by construction.getSafeUri()returns aSafeUrithat the generator created from compile-time content, so it may be assigned tohrefandsrcthrough theSafeUrioverloads. Do not convert user-controlled strings toSafeUrito get the same effect. -
A Content-Security-Policy must allow
data:for images (img-src data:) if inlining stays on. Use@DoNotEmbedorpreventInliningfor resources a policy cannot allow asdata:.ExternalTextResourceover XHR needs'unsafe-eval', as measured above. -
Compile-time input is trusted input. The compiler decodes images and reads texts from the repository; do not bundle files from untrusted sources.
-
No secrets in bundles. Everything in a bundle is delivered to every browser, in clear, and a strong-named file is public to anyone who knows the name.
-
J2CL and the future.
ClientBundleis a feature of the GWT compiler and its generators. Theorg.gwtproject.*modules published for J2CL (all still at 1.0.0 release-candidate versions on Maven Central) contain no replacement for it (see J2CL & the Future for their status); a J2CL migration needs another asset pipeline, for example a bundler.
Pitfalls
The messages below were printed by the 2.13.1 compiler for a probe module with one deliberately broken bundle per row, or taken from the 2.13.1 generator source where the row says so.
Package names are shortened with ... where it helps.
Most generator errors are followed by a generic second line that is the failure you will search for in the log: Failed to resolve 'com.example...BadMissing' via deferred binding, because the GWT.create of the bundle could not complete.
| Symptom (message as printed) | Cause | Fix |
|---|---|---|
|
The |
Put the file next to the interface under |
|
An accessor has no |
Add |
|
|
Only |
|
An accessor returns |
Return |
|
An accessor declares a parameter. The messages come from the generated class, in each locale. |
Accessors take no arguments. Pass a name through |
|
An |
Use a |
|
|
Use a subtype of the type argument in |
|
A |
Replace a factory class and let it call |
|
The stylesheet has a class selector with no matching accessor on the |
Add |
|
A |
Set the property, or convert the file to CSS. |
|
The page runs under a Content-Security-Policy without |
Allow |
An unused resource still appears in the war directory ( |
The generator writes files while it builds the bundle; pruning removes only the JavaScript that refers to them. |
Remove the accessor from the bundle (or the file from the classpath) to stop shipping the file. |
Inlined images do not show under a Content-Security-Policy |
The policy has no |
Add |
Every static file carries |
A session-creating filter is mapped to |
Map the session filter to the dynamic paths only. |
The logo is the same in every locale although |
The module does not declare the locale ( |
Check |
|
Inlining is on (the default), and inlined images are not composited. |
Expected. Set |
Recent changes and the outdated book
| Version | Change |
|---|---|
1.4 |
|
2.0.0 (2009-12-09) |
|
2.5.0 |
Generator result caching for the |
2.6.0 |
|
2.8.0 |
GSS leaves experimental status and the legacy CSS syntax is deprecated; |
2.8.1 to 2.13.1 |
The release notes mention no |
-
The book. Chapter 4 of the Manning book (GWT 1.3 and 1.4) shows
ImageBundleas the way to reduce image requests. The idea is sound and survives inClientBundle, but the type is@Deprecated, the doc-comment form of the resource tag it uses has been superseded, and the book predates data-URL inlining, strong-named files,CssResource,DataResourceand the text resources. -
The official page. The Developer Guide Image Bundles page now describes
ImageResource, but its argument about two connections per host and its statement that images are combined into a single image do not match the 2.13.1 defaults (inlining on). TheClientBundlepage still mentions Internet Explorer 6 in the sprite notes, and calls the GSS modeGssResource, a name that is not a type.
See also
-
UiBinder for
ui:image,ui:dataandui:style, which wrap bundle accessors in templates. -
Compiler & Compilation for permutations,
-style PRETTYand the files in the output directory. -
Development Mode & Debugging for Super Dev Mode, which serves the strong-named files from the code server.
-
Project Structure & Modules for where the resource files live (
src/main/resourcesnext to the package). -
Runtime Loading Performance for image formats, sizes and lazy loading in plain HTML and CSS.
-
SVG Styling & Animation for SVG files, which belong in a
DataResourceand not in anImageResource.