HtmlUnit is a headless browser implemented in Java. It can load HTML, maintain cookies, follow links, submit forms, execute JavaScript and expose the resulting document through a DOM API.
That makes HtmlUnit useful for browser-oriented testing, automation and web scraping where a plain HTTP client and HTML parser are not enough.
This tutorial modernizes the original example for Java 25 and HtmlUnit 5.3.0. The scraper will:
- load a web page;
- print the page title;
- extract an author with a CSS selector;
- iterate through every anchor element; and
- print output with Java 25's
IO.println().
HtmlUnit 5 Maven dependency
Older HtmlUnit applications used the Maven group ID net.sourceforge.htmlunit and Java packages beginning with com.gargoylesoftware.htmlunit. Modern HtmlUnit uses org.htmlunit for both the Maven coordinates and Java packages.
The current HtmlUnit dependency is:
<dependency>
<groupId>org.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>5.3.0</version>
</dependency>Java 25 Maven POM
For Java 25, use Maven's release setting instead of the old Java 8 source and target configuration.
A complete POM can look like this:
<project xmlns="https://maven.apache.org/POM/4.0.0"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
https://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mcnz.scraper</groupId>
<artifactId>htmlunit-java25-scraper</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>
UTF-8
</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>5.3.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
</plugin>
</plugins>
</build>
</project>The old tutorial targeted Java 8 and HtmlUnit 2.34.1. Neither configuration belongs in a new Java 25 project.
Modern HtmlUnit imports
The package names changed along with modern HtmlUnit releases. The scraper uses these imports:
import org.htmlunit.WebClient;
import org.htmlunit.html.DomNode;
import org.htmlunit.html.HtmlPage;Notice that the old com.gargoylesoftware.htmlunit package is gone.
A minimal Java 25 HtmlUnit scraper
Java 25 supports compact source files and instance main methods, so a small scraper does not need an explicit class declaration.
import org.htmlunit.WebClient;
import org.htmlunit.html.HtmlPage;
void main() throws Exception {
var url = "https://www.theserverside.com/";
try (var webClient = new WebClient()) {
HtmlPage page = webClient.getPage(url);
IO.println(page.getTitleText());
}
}There are several modern Java idioms in this example:
varremoves unnecessary local type repetition.IO.println()replacesSystem.out.println()for simple Java 25 console output.- A compact
void main()removes boilerplate from a small program. - Try-with-resources closes the
WebClientautomatically.
WebClient implements AutoCloseable, so it belongs in a try-with-resources statement rather than being left open.
Do not disable SSL validation
The old scraper included this configuration:
webClient.getOptions().setUseInsecureSSL(true);Do not use that as a normal scraper setting. It weakens HTTPS certificate validation and can hide real TLS configuration problems.
A modern scraper should use normal HTTPS validation unless it is deliberately interacting with a controlled development system that uses a certificate you explicitly understand and trust.
Should CSS and JavaScript be disabled?
HtmlUnit is more than an HTML parser. It simulates browser behavior and includes JavaScript support. Turning JavaScript off can therefore change the DOM you receive.
If the page is server-rendered and you only need its initial HTML, disabling CSS and JavaScript can reduce unnecessary processing:
try (var webClient = new WebClient()) {
webClient.getOptions().setCssEnabled(false);
webClient.getOptions().setJavaScriptEnabled(false);
HtmlPage page = webClient.getPage(url);
}Do this only when the content you need does not depend on JavaScript. For a client-rendered application, leave JavaScript enabled so HtmlUnit can execute the page's scripts.
Extract the HTML page title
Once WebClient.getPage() returns an HtmlPage, the title is available through getTitleText():
HtmlPage page = webClient.getPage(url);
IO.println(page.getTitleText());The original article contained a malformed method name, getTitleText with HTML formatting inserted inside the identifier. The corrected method call is simply getTitleText().
Query the DOM with a CSS selector
HtmlUnit's DOM nodes support CSS selector queries. Suppose the author link on a page matches this selector:
#author > div > aQuery it like this:
DomNode author = page.querySelector(
"#author > div > a"
);
if (author != null) {
IO.println(
"Author: " + author.asNormalizedText()
);
}querySelector() returns the first matching node, or null when nothing matches. A robust scraper should therefore perform a null check rather than assume the page always has the expected structure.
Extract every anchor link
HtmlPage.getAnchors() returns the page's anchor elements. Each HtmlAnchor provides getHrefAttribute() for the value of its href attribute.
for (var anchor : page.getAnchors()) {
IO.println(anchor.getHrefAttribute());
}This is cleaner than calling the generic getAttribute("href") method when you already know you are working with an HtmlAnchor.
Print both link text and URL
A scraper is often more useful when it records both the text visible to the user and the destination URL.
for (var anchor : page.getAnchors()) {
var text = anchor.asNormalizedText();
var href = anchor.getHrefAttribute();
IO.println(
"%s -> %s".formatted(text, href)
);
}Java 25's IO.println() combines nicely with String.formatted() when output contains multiple values.
Resolve relative links
An anchor might contain a relative URL such as:
<a href="/tutorial/java">Java tutorials</a>If you need the absolute URL, ask the page to resolve the relative value against its base URL:
for (var anchor : page.getAnchors()) {
var href = anchor.getHrefAttribute();
if (!href.isBlank()) {
var absoluteUrl =
page.getFullyQualifiedUrl(href);
IO.println(absoluteUrl);
}
}This is generally more useful for a crawler or content aggregator than printing raw relative links.
Complete Java 25 HtmlUnit scraper
The complete example brings the pieces together:
import org.htmlunit.WebClient;
import org.htmlunit.html.DomNode;
import org.htmlunit.html.HtmlPage;
void main() throws Exception {
var url = "https://www.theserverside.com/";
try (var webClient = new WebClient()) {
webClient.getOptions()
.setCssEnabled(false);
HtmlPage page = webClient.getPage(url);
IO.println(
"Title: " + page.getTitleText()
);
DomNode author = page.querySelector(
"#author > div > a"
);
if (author != null) {
IO.println(
"Author: " +
author.asNormalizedText()
);
}
IO.println();
IO.println("Links:");
for (var anchor : page.getAnchors()) {
var text =
anchor.asNormalizedText();
var href =
anchor.getHrefAttribute();
if (href.isBlank()) {
continue;
}
var absoluteUrl =
page.getFullyQualifiedUrl(href);
IO.println(
"%s -> %s".formatted(
text,
absoluteUrl
)
);
}
}
}This example leaves JavaScript enabled. If the target page is fully server-rendered and JavaScript is unnecessary, add:
webClient.getOptions()
.setJavaScriptEnabled(false);Wait for asynchronous JavaScript
If the page uses JavaScript to load content asynchronously, loading the page and immediately querying the DOM may be too early.
HtmlUnit provides mechanisms for working with its JavaScript engine, but scraper logic should wait only when the target page actually requires asynchronous client-side rendering. Avoid adding arbitrary delays to every request.
For server-rendered pages, keeping the scraper synchronous and simple is both faster and easier to maintain.
HtmlUnit vs. JSoup
HtmlUnit and JSoup overlap, but they solve somewhat different problems.
| Requirement | HtmlUnit | JSoup |
|---|---|---|
| Parse server-rendered HTML | Yes | Yes |
| CSS selectors | Yes | Yes |
| Browser-style navigation | Yes | Limited |
| JavaScript execution | Yes | No browser runtime |
| Form interaction | Yes | HTML-oriented |
| Lightweight HTML scraping | Capable | Often simpler |
If you only need to download static HTML and extract elements, JSoup is often the simpler tool. If the workflow needs browser-like behavior, cookies, form interaction or JavaScript, HtmlUnit offers capabilities a simple HTML parser does not.
Web scraping best practices
A production scraper should do more than successfully parse HTML. It should also behave responsibly.
- Scrape only content you are authorized to access.
- Respect site terms, robots directives and applicable policies.
- Avoid sending requests at a rate that burdens the server.
- Expect CSS selectors and page structure to change.
- Handle missing elements and HTTP failures gracefully.
- Do not disable TLS verification as a shortcut.
- Close every
WebClientwith try-with-resources.
What changed from old HtmlUnit examples?
| Old approach | Modern approach |
|---|---|
net.sourceforge.htmlunit |
org.htmlunit |
| HtmlUnit 2.x | HtmlUnit 5.3.0 |
com.gargoylesoftware.htmlunit.* |
org.htmlunit.* |
| Java 8 source/target | maven.compiler.release=25 |
System.out.println() |
IO.println() |
| Explicit resource cleanup often omitted | Try-with-resources |
| Insecure SSL commonly disabled | Normal certificate validation |
HtmlUnit remains a useful Java headless-browser library, but the API coordinates, package names and Java coding style have changed significantly since the old 2.x examples. With HtmlUnit 5.3.0 and Java 25, the result is cleaner Maven configuration, safer resource management and much less boilerplate.