<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Deniss Larka</title>
    <description>The latest articles on DEV Community by Deniss Larka (@deniss_larka).</description>
    <link>https://dev.to/deniss_larka</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3977066%2F1e65bada-d789-45f3-8a83-fec9a67ccd4a.png</url>
      <title>DEV Community: Deniss Larka</title>
      <link>https://dev.to/deniss_larka</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/deniss_larka"/>
    <language>en</language>
    <item>
      <title>Write your pages in PHP. Run them on the JVM.</title>
      <dc:creator>Deniss Larka</dc:creator>
      <pubDate>Sat, 15 Aug 2026 10:39:16 +0000</pubDate>
      <link>https://dev.to/deniss_larka/write-your-pages-in-php-run-them-on-the-jvm-j6</link>
      <guid>https://dev.to/deniss_larka/write-your-pages-in-php-run-them-on-the-jvm-j6</guid>
      <description>&lt;p&gt;druvu-lib-web 2.0.0 is out: a small Java web framework on Jetty 12 whose template language is PHP. Not "PHP-like" — the 2.0 engine runs real PHP 8 layouts: variables, every control structure, string interpolation, heredocs, includes, arrow functions, and 115 standard functions, with semantics settled by running whole templates against PHP 8 and matching its output byte for byte. There is no PHP runtime anywhere — the engine is Java, your handlers are Java, and &lt;code&gt;request.setAttribute("customer", customer)&lt;/code&gt; becomes &lt;code&gt;$customer&lt;/code&gt; in the page.&lt;/p&gt;

&lt;p&gt;One thing is deliberately different from PHP: output is HTML-escaped by default, with &lt;code&gt;raw()&lt;/code&gt; as the opt-out — forgetting is safe instead of being a hole. (That's also the main breaking change from 1.x.) Repo, docs and a runnable example: &lt;a href="https://github.com/DenissLarka/druvu-lib-web" rel="noopener noreferrer"&gt;https://github.com/DenissLarka/druvu-lib-web&lt;/a&gt;. If you try it and a template renders differently than real PHP would — that's a bug by definition, and &lt;a href="https://github.com/DenissLarka/druvu-lib-web/issues/23" rel="noopener noreferrer"&gt;I want to hear about it&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>java</category>
      <category>php</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What JConsole would look like if anyone had kept going</title>
      <dc:creator>Deniss Larka</dc:creator>
      <pubDate>Mon, 03 Aug 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/deniss_larka/what-jconsole-would-look-like-if-anyone-had-kept-going-41g8</link>
      <guid>https://dev.to/deniss_larka/what-jconsole-would-look-like-if-anyone-had-kept-going-41g8</guid>
      <description>&lt;p&gt;There's a kind of Java engineer for whom JMX is not a monitoring buzzword but a daily&lt;br&gt;
instrument: you connect to a live JVM, read a queue depth, flip a feature toggle, invoke the&lt;br&gt;
operation somebody exposed years ago for exactly this situation. I'm one of them — my day job&lt;br&gt;
keeps me close to production JVMs where guessing is expensive. And the universal client for&lt;br&gt;
this work is JConsole: it ships with every JDK, speaks to any JVM, and needs no server, no&lt;br&gt;
agent, no vendor.&lt;/p&gt;

&lt;p&gt;It also fights &lt;em&gt;you&lt;/em&gt;, every single day, in the same three places.&lt;/p&gt;

&lt;p&gt;Every operation argument is a raw text field. The MBean wants an ISO date, or one of exactly&lt;br&gt;
three legal values, or a file path — JConsole hands you an empty string box and wishes you&lt;br&gt;
luck. There are no saved connections: every morning, the same &lt;code&gt;host:port&lt;/code&gt; typed from memory,&lt;br&gt;
for every JVM in your care. And remote connections ride RMI, whose two-port firewall dance is&lt;br&gt;
&lt;a href="https://dev.to/deniss_larka/the-jdks-forgotten-jmx-protocol-4pci"&gt;a story of its own&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;None of it is hard to fix. It simply never was.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz7ohhsjluiku715vlq9d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz7ohhsjluiku715vlq9d.png" alt="JConsoleBooster" width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The rule I'm breaking
&lt;/h3&gt;

&lt;p&gt;The 2026-approved answer is to stop using a desktop tool: ship the metrics to an APM, build a&lt;br&gt;
Grafana dashboard, deploy an agent. All three are good answers. One still opening a window in 2026&lt;br&gt;
is somebody who cannot let go of his childhood toys, and I have decided to accept the charge.&lt;/p&gt;

&lt;p&gt;The toys have one advantage. None of the approved answers covers the moment that actually&lt;br&gt;
defines JVM ops: you, one specific production JVM, right now — read this value, invoke this&lt;br&gt;
operation, watch what happens. For that, I want a direct&lt;br&gt;
connection, no platform in between, nothing to deploy before I can look.&lt;/p&gt;

&lt;p&gt;JConsole has survived twenty years — and it's open source. Meanwhile the JDK quietly solved the historical pain of &lt;em&gt;shipping&lt;/em&gt; desktop Java: &lt;code&gt;jlink&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;jpackage&lt;/code&gt; make a self-contained, installable app practical — own runtime bundled, no JDK&lt;br&gt;
required on the target machine.&lt;/p&gt;

&lt;p&gt;The pieces were on the table. I forked JConsole and started fixing my own daily frictions.&lt;br&gt;
The result is &lt;a href="https://druvu.com/downloads/jconsole-booster.html" rel="noopener noreferrer"&gt;JConsoleBooster&lt;/a&gt; — and three&lt;br&gt;
changes carry it.&lt;/p&gt;

&lt;p&gt;It did occur to me, somewhere around the third weekend, that Oracle ships a thing called VisualVM, and that a reasonable person would have opened it before hand-editing a twenty-year-old&lt;br&gt;
Swing dialog. I gave the thought the consideration it deserved and went back to the dialog.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Operations become forms, not string fields
&lt;/h3&gt;

&lt;p&gt;MBean operation parameters can declare lightweight markup — a combo of legal values, a date, a&lt;br&gt;
file — and the Operations tab renders real widgets for them: a dropdown instead of a string&lt;br&gt;
box, a date picker instead of an ISO-format guessing game, a file chooser instead of a&lt;br&gt;
hand-typed path. Results that are JSON come back pretty-printed instead of as a one-line brick.&lt;br&gt;
An operation that returns a file (a report, an Excel export) downloads like a file, not like a&lt;br&gt;
&lt;code&gt;byte[]&lt;/code&gt; toString.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp9zy4hu843bpomubmd42.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp9zy4hu843bpomubmd42.png" alt="Operations markup" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftagt468h9zr9y2hwgmff.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftagt468h9zr9y2hwgmff.png" alt="An operation that returned a PDF, offered as Download or Open instead of a byte array" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nothing about your MBeans has to change wholesale — the markup is opt-in, plain JConsole&lt;br&gt;
renders the same operations as plain strings.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Your whole estate, one menu
&lt;/h3&gt;

&lt;p&gt;Connections live in a bookmarks file: plain text, grouped, with &lt;code&gt;*bold*&lt;/code&gt; and &lt;code&gt;[red ALERT]&lt;/code&gt;&lt;br&gt;
markup for the entries that deserve visual weight — production vs staging, the box that pages&lt;br&gt;
you at night. The file is yours: version it, share it with the team, edit it in any editor.&lt;br&gt;
JConsoleBooster renders it as a menu, and any JVM in your care is one click away instead of a&lt;br&gt;
&lt;code&gt;host:port&lt;/code&gt; recital.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwx7yxd83p051d3d2ztfw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwx7yxd83p051d3d2ztfw.png" alt="The bookmarks menu" width="701" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Installs — and updates — like a real app
&lt;/h3&gt;

&lt;p&gt;Signed and notarized &lt;code&gt;.dmg&lt;/code&gt; for macOS (Apple Silicon), signed &lt;code&gt;.msix&lt;/code&gt; for Windows, &lt;code&gt;.AppImage&lt;/code&gt;&lt;br&gt;
for Linux — each with the Java runtime bundled via &lt;code&gt;jlink&lt;/code&gt;, so the target machine needs no JDK,&lt;br&gt;
no &lt;code&gt;JAVA_HOME&lt;/code&gt;, no ceremony.&lt;/p&gt;

&lt;p&gt;And once it's installed, it keeps itself current. A running copy notices the next release,&lt;br&gt;
downloads it and applies it — no download page, no reinstall; on Windows that took about two&lt;br&gt;
and a half seconds for a 37 MB package. The three operating systems hold strong and entirely&lt;br&gt;
incompatible views on how an application may replace itself while running, and I have given up&lt;br&gt;
trying to change their minds: Windows swaps the package and restarts, macOS mounts the new build&lt;br&gt;
and asks you to quit and drag it across, Linux replaces the file and asks for a restart.&lt;/p&gt;

&lt;p&gt;The Linux AppImage is unsigned — Linux has no OS signing gate — use SHA-256, as Mr. Stallman would.&lt;/p&gt;

&lt;h3&gt;
  
  
  The transport, in one paragraph
&lt;/h3&gt;

&lt;p&gt;For remote JVMs, JConsoleBooster's default transport is JMXMP — the JDK's forgotten&lt;br&gt;
single-socket JMX protocol: type a bare &lt;code&gt;host:port&lt;/code&gt; and you connect over one fixed port through&lt;br&gt;
the firewall, no RMI two-port dance. I revived and hardened that library as a separate project;&lt;br&gt;
the full story is in&lt;br&gt;
&lt;a href="https://dev.to/deniss_larka/the-jdks-forgotten-jmx-protocol-4pci"&gt;The JDK's forgotten JMX protocol&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  When the box has no screen
&lt;/h3&gt;

&lt;p&gt;The JVMs that most need poking often sit on machines with no display at all. So the same tool&lt;br&gt;
runs headless: a REPL that speaks JMXMP, or &lt;code&gt;-e&lt;/code&gt; for a scripted one-liner, with the same&lt;br&gt;
transport, certificate-trust and credential rules the window enforces — the GUI is a front end,&lt;br&gt;
not the product. There's an optional single-jar build for when copying one file to a bastion&lt;br&gt;
host is the entire deployment story.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tested daily, where it matters
&lt;/h3&gt;

&lt;p&gt;I run JConsoleBooster every working day against real production JVMs. Every feature above&lt;br&gt;
exists because the default behaviour cost me time in a real workflow — and earned its place by&lt;br&gt;
surviving daily use since. Whatever fights me next becomes the next release; that loop is the&lt;br&gt;
development model.&lt;/p&gt;

&lt;p&gt;Right now, that's the updater. It asks nobody's permission for anything — an excellent quality in&lt;br&gt;
a background service and a poor one in a colleague, particularly one who closes your window&lt;br&gt;
while you are still using it. So that's what's next.&lt;/p&gt;

&lt;p&gt;And one idea I keep turning over without deciding: a tab that renders HTML an MBean hands back.&lt;br&gt;
Your service exposes a single method that returns a form; the app draws it, posts your answer&lt;br&gt;
back, draws whatever comes next. An operations console for your service, for the days you need&lt;br&gt;
to control the thing and don't want to build a front end for it. Tell me if you'd use that — or&lt;br&gt;
why it's a terrible idea.&lt;/p&gt;

&lt;p&gt;Downloads: &lt;a href="https://druvu.com/downloads/jconsole-booster.html" rel="noopener noreferrer"&gt;druvu.com/downloads/jconsole-booster&lt;/a&gt;&lt;br&gt;
— pick your OS. Repo:&lt;br&gt;
&lt;a href="https://github.com/DenissLarka/jconsole-booster" rel="noopener noreferrer"&gt;github.com/DenissLarka/jconsole-booster&lt;/a&gt; — issues welcome. If JMX is part of your day, I'd genuinely like to hear what still fights you.&lt;/p&gt;

</description>
      <category>java</category>
      <category>jvm</category>
      <category>opensource</category>
      <category>ui</category>
    </item>
    <item>
      <title>The JDK's forgotten JMX protocol</title>
      <dc:creator>Deniss Larka</dc:creator>
      <pubDate>Sat, 11 Jul 2026 15:36:29 +0000</pubDate>
      <link>https://dev.to/deniss_larka/the-jdks-forgotten-jmx-protocol-4pci</link>
      <guid>https://dev.to/deniss_larka/the-jdks-forgotten-jmx-protocol-4pci</guid>
      <description>&lt;p&gt;Every Java engineer who has connected JConsole — or JDK Mission Control — to a server in&lt;br&gt;
another network segment knows the ritual. Open the JMX port. Discover that RMI quietly opened a &lt;em&gt;second&lt;/em&gt; port — random by&lt;br&gt;
default. Pin it with a system property nobody remembers without searching. File a firewall&lt;br&gt;
ticket for both. Wait.&lt;/p&gt;

&lt;p&gt;What fewer people know: the JMX specification shipped with a second remote transport that has&lt;br&gt;
none of these problems. One socket, one port, TLS underneath if you want it. It's called JMXMP —&lt;br&gt;
the JMX Messaging Protocol. It lost for the least mysterious reason in software — RMI shipped&lt;br&gt;
by default, JMXMP was a separate download, and defaults win — and its reference implementation&lt;br&gt;
has been effectively abandoned since around 2008. Yet it never quite died. Code that refuses&lt;br&gt;
to die usually knows something.&lt;/p&gt;

&lt;p&gt;I didn't set out to resurrect it. I fell into it.&lt;/p&gt;
&lt;h3&gt;
  
  
  The port dance, briefly
&lt;/h3&gt;

&lt;p&gt;The default remote JMX stack rides on RMI. The connection URL tells you most of the story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service:jmx:rmi:///jndi/rmi://host:1099/jmxrmi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'll spare you the full anatomy behind that URL — there's a JNDI lookup in it, and that second,&lt;br&gt;
dynamically assigned port from the ritual above; few people ever learn the details, which is&lt;br&gt;
rather the point. Dynamic ports were a reasonable design for 1999's flat networks. Between&lt;br&gt;
today's firewalls, NAT, and containers, they're friction — not because RMI is bad, but because&lt;br&gt;
the network it was designed for no longer exists.&lt;/p&gt;

&lt;p&gt;The JMXMP URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service:jmx:jmxmp://host:9875
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One socket. TCP in, TCP out. That's the whole networking story.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I ended up in this codebase
&lt;/h3&gt;

&lt;p&gt;I maintain &lt;a href="https://druvu.com" rel="noopener noreferrer"&gt;JConsoleBooster&lt;/a&gt;, a modernized JConsole. It shipped fine for&lt;br&gt;
years on the 2008-era JMXMP jar — the one historically distributed as &lt;code&gt;jmxremote_optional&lt;/code&gt; /&lt;br&gt;
&lt;code&gt;jmx-optional&lt;/code&gt;, out of Sun's OpenDMK project, republished over the years by several parties&lt;br&gt;
because people kept needing single-socket JMX.&lt;/p&gt;

&lt;p&gt;Then I moved the app to a &lt;code&gt;jlink&lt;/code&gt;-built runtime. An automatic module from 2008 does not&lt;br&gt;
cooperate with the module system: no &lt;code&gt;module-info&lt;/code&gt;, split-package hazards, services declared&lt;br&gt;
in ways &lt;code&gt;jlink&lt;/code&gt; can't see. The choices were: abandon the transport, or take responsibility&lt;br&gt;
for the code.&lt;/p&gt;

&lt;p&gt;So I forked it and took the code apart — 87 files of 2007 Sun code. I won't pretend I read&lt;br&gt;
every line myself; it's 2026, and AI did much of the first-pass reading. The rule that&lt;br&gt;
mattered: nothing got deleted, changed, or believed without a proof — a test, a&lt;br&gt;
zero-references check, an API diff.&lt;/p&gt;
&lt;h3&gt;
  
  
  What's inside a library frozen in 2008
&lt;/h3&gt;

&lt;p&gt;Some of what I found is what you'd expect from code written for a platform that no longer&lt;br&gt;
exists. Some of it surprised me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dead RMI plumbing — in the socket-protocol library.&lt;/strong&gt; Proxy references, unmarshalling
helpers, an RMI exporter. The generic connector framework was written to serve both
transports, and the RMI half stayed behind like scaffolding nobody removed. I deleted it
after proving zero references.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plaintext defaults.&lt;/strong&gt; Encryption and authentication existed — as options, off unless asked.
Normal for 2007; indefensible for something you point at a JVM in production today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An authorization check that fails open on current JDKs.&lt;/strong&gt; It was written for the
SecurityManager era: it asks the runtime for the caller's identity through an API that, on a
modern JDK, always answers "nobody" — and it treats "nobody" as "security isn't enabled here,
let it through." On the JVMs of its time, correct. On today's, an allow-all with extra steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;And the part that made the effort worth it: the protocol itself is fine.&lt;/strong&gt; Message-based,
single connection, SASL negotiation for auth, TLS for transport — the design decisions Sun
made hold up. The packaging aged. The idea didn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters. This isn't a rewrite-because-old-code-is-bad story. The original&lt;br&gt;
authors solved the right problem well; the code just outlived the platform assumptions around&lt;br&gt;
it.&lt;/p&gt;
&lt;h3&gt;
  
  
  The second life
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/DenissLarka/druvu-lib-jmxmp" rel="noopener noreferrer"&gt;druvu-lib-jmxmp&lt;/a&gt; 2.0.0, on Maven Central,&lt;br&gt;
Java 21 baseline — the same protocol, newborn. Three changes carry the release; everything&lt;br&gt;
else is detail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. It's a real Java module — and still a drop-in.&lt;/strong&gt; The monolith is now three JPMS modules&lt;br&gt;
(&lt;code&gt;com.druvu.jmxmp.common&lt;/code&gt; / &lt;code&gt;server&lt;/code&gt; / &lt;code&gt;client&lt;/code&gt;), and &lt;code&gt;jlink&lt;/code&gt; treats them as first-class&lt;br&gt;
citizens. The public API — &lt;code&gt;javax.management.remote.jmxmp&lt;/code&gt; and friends — is kept frozen,&lt;br&gt;
verified by a snapshot test that diffs the exported surface against the original. If your code&lt;br&gt;
compiled against &lt;code&gt;jmxremote_optional&lt;/code&gt;, it compiles against this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The server refuses to run open.&lt;/strong&gt; Authentication is mandatory — the server env can't be&lt;br&gt;
built without it — and the connection is encrypted either way: bring your own TLS context, or&lt;br&gt;
the server generates an ephemeral self-signed certificate at startup. This costs adoption&lt;br&gt;
convenience, and I decided that's the&lt;br&gt;
right trade: nobody exposes a management port to the world &lt;em&gt;on purpose&lt;/em&gt;, but unauthenticated&lt;br&gt;
JMX endpoints keep showing up in the wild by accident, and they're a documented entry point for&lt;br&gt;
cryptominers. This library's answer is that the accident can't happen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JmxmpServerSecurity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sslContext&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;// optional — omit it, get an ephemeral self-signed cert&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authenticator&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;myAuthenticator&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// SASL/PLAIN — required&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;                         &lt;span class="c1"&gt;// refuses to build without an authenticator&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mandatory authentication buys something besides keeping strangers out. Plenty of teams use&lt;br&gt;
JMX operations as the low-ceremony alternative to building an admin UI: change a setting at&lt;br&gt;
runtime, flip a feature toggle, trigger a business operation. It works — until something&lt;br&gt;
changes in production and nobody can say who changed it, or when. With every connection&lt;br&gt;
authenticated, the operation knows its caller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;switchFeature&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;operator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Subject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getPrincipals&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;iterator&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{} switched feature '{}' {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"on"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"off"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// log: "john switched feature 'beta-checkout' on"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The connector runs each request inside the authenticated subject's context, so&lt;br&gt;
&lt;code&gt;Subject.current()&lt;/code&gt; in your MBean is the operator who issued the call — an audit trail with&lt;br&gt;
no extra plumbing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. It fails closed.&lt;/strong&gt; The SecurityManager-era identity lookup is gone; the server reads the&lt;br&gt;
authenticated subject from &lt;code&gt;Subject.current()&lt;/code&gt;, and a request with no subject is refused, not&lt;br&gt;
waved through. Deserialization — the classic JMX attack surface — goes through a&lt;br&gt;
deny-by-default filter (&lt;code&gt;JmxmpSerialFilter&lt;/code&gt;): allow-listed classes deserialize, everything else&lt;br&gt;
doesn't, before any application code sees the bytes. Authorization beyond authentication is&lt;br&gt;
there if you need it: a small RBAC SPI with a programmatic policy builder, default-deny.&lt;/p&gt;

&lt;p&gt;The precise diffs — what was removed, what each fix looks like — are in the repo history and&lt;br&gt;
release notes. I'd rather link them than paraphrase them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Did it survive contact with reality?
&lt;/h3&gt;

&lt;p&gt;The test matrix runs the connector in every module arrangement — both sides modular, both on&lt;br&gt;
the classpath, mixed. That matrix earned its keep: it caught the freshly-modularized library&lt;br&gt;
silently behaving as a classpath citizen in one spot, where a &lt;code&gt;Class.forName&lt;/code&gt; reached across a&lt;br&gt;
module seam it had no right to cross. "It compiles with a module-info" and "it's modular" are&lt;br&gt;
different claims; only a test can tell them apart.&lt;/p&gt;

&lt;p&gt;And it runs in production of a sort: JConsoleBooster ships on it — every remote connection the&lt;br&gt;
app makes goes through this library.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where it stands on security claims
&lt;/h3&gt;

&lt;p&gt;Deliberately narrow. I'm not going to call the library "secure" — that's an adjective, not a&lt;br&gt;
fact. The facts: the fail-open path is fixed, transport encryption and authentication are&lt;br&gt;
non-optional, deserialization is allow-listed, and the delta for each is inspectable commit by&lt;br&gt;
commit.&lt;/p&gt;

&lt;p&gt;It's maintained by one person, best-effort, latest-version-only — the support model is&lt;br&gt;
spelled out in SECURITY.md. GitHub private vulnerability reporting is switched on. If you&lt;br&gt;
can break it, I want to know.&lt;/p&gt;

&lt;h3&gt;
  
  
  When you should not use this
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An SSH tunnel to the default RMI connector already works for you.&lt;/strong&gt; Then it works. This
is for when the tunnel is the friction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're below Java 21.&lt;/strong&gt; The library assumes a modern JDK; the old republications exist
and stay where they are.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need anonymous local monitoring.&lt;/strong&gt; JConsole attaching to a local PID doesn't involve
remote JMX at all — nothing to change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your daily client is JDK Mission Control.&lt;/strong&gt; JMC speaks the default RMI connector; teaching
it JMXMP is a plugin dance this library doesn't attempt to solve. Stock JConsole could
historically be coaxed into it with the connector on its classpath;
&lt;a href="https://druvu.com" rel="noopener noreferrer"&gt;JConsoleBooster&lt;/a&gt; speaks it out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your security model lives in the old access-file / SecurityManager mechanics.&lt;/strong&gt; That
machinery is gone here, not emulated; migrating means adopting the SASL + RBAC model.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Coordinates
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.druvu&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;druvu-lib-jmxmp&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${latest.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;a href="https://github.com/DenissLarka/druvu-lib-jmxmp" rel="noopener noreferrer"&gt;github.com/DenissLarka/druvu-lib-jmxmp&lt;/a&gt;&lt;br&gt;
— issues and the pinned "what next" thread are the feedback channel. If you're running JMXMP&lt;br&gt;
somewhere unusual, I'd genuinely like to hear what it's doing there.&lt;/p&gt;

</description>
      <category>java</category>
      <category>jvm</category>
      <category>opensource</category>
    </item>
    <item>
      <title>ServiceLoader is great — until you want a constructor argument</title>
      <dc:creator>Deniss Larka</dc:creator>
      <pubDate>Thu, 11 Jun 2026 11:59:59 +0000</pubDate>
      <link>https://dev.to/deniss_larka/serviceloader-is-great-until-you-want-a-constructor-argument-2767</link>
      <guid>https://dev.to/deniss_larka/serviceloader-is-great-until-you-want-a-constructor-argument-2767</guid>
      <description>&lt;p&gt;I was building a small library that reads accounting files — GnuCash first, other formats maybe later. It's the textbook case for pluggable implementations: an &lt;code&gt;AccBook&lt;/code&gt; interface in the API module, one implementation per format in its own module, and the application picks up whichever one is on the path.&lt;/p&gt;

&lt;p&gt;Why not &lt;code&gt;AccBook book = new GnucashAccBook(path)&lt;/code&gt; and move on? For one format in one app — do exactly that. But the moment your code says &lt;code&gt;new GnucashAccBook&lt;/code&gt;, it depends on that implementation: the GnuCash module has to be on the compile path, its classes exported, its name written in every place that opens a book. Add a second format and each of those places grows an &lt;code&gt;if&lt;/code&gt;. The interface stops protecting you once you name the class behind it.&lt;/p&gt;

&lt;p&gt;Java already ships the tool for exactly this. No framework, no container, works on the module path: &lt;code&gt;ServiceLoader&lt;/code&gt;. So I reached for it. Then I hit a wall I'd hit before and forgotten about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive version
&lt;/h2&gt;

&lt;p&gt;The interface is plain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;AccBook&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Account&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;accounts&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Discovery is genuinely easy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;AccBook&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ServiceLoader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AccBook&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;findFirst&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But my &lt;code&gt;GnucashAccBook&lt;/code&gt; can't do anything without the file path. And &lt;code&gt;ServiceLoader&lt;/code&gt; only knows how to call a no-arg constructor (or a static &lt;code&gt;provider()&lt;/code&gt; method). There is nowhere to hand it the &lt;code&gt;Path&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So you reach for one of two workarounds, and both are worse than they look.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workaround 1 — construct empty, configure after:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;AccBook&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ServiceLoader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AccBook&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;findFirst&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="nc"&gt;Configurable&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// ...and hope nobody touched `book` before this line&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the object exists in an invalid state between construction and &lt;code&gt;init&lt;/code&gt;. You've swapped a constructor argument for a runtime contract the compiler can't see. (And &lt;code&gt;findFirst()&lt;/code&gt; quietly picks one if two implementations are on the path — hold that thought.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workaround 2 — put a factory behind ServiceLoader:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;AccBookFactory&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;AccBook&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;AccBook&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ServiceLoader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AccBookFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findFirst&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one is actually the right shape. But now every project grows an &lt;code&gt;XFactory&lt;/code&gt; interface, the &lt;code&gt;findFirst().orElseThrow()&lt;/code&gt; dance, and — if you care that two implementations on the path is a mistake — your own "throw if more than one" check. I wrote that glue three times before I admitted it was a pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extracting the pattern
&lt;/h2&gt;

&lt;p&gt;So I pulled it into a tiny library, &lt;code&gt;druvu-lib-loader&lt;/code&gt;. It is the factory-behind-&lt;code&gt;ServiceLoader&lt;/code&gt; pattern, made generic once, with the implementation's arguments passed through a type-keyed map.&lt;/p&gt;

&lt;p&gt;The implementation side becomes a factory that &lt;em&gt;receives&lt;/em&gt; its arguments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GnucashBookFactory&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ComponentFactory&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AccBook&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;AccBook&lt;/span&gt; &lt;span class="nf"&gt;createComponent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Dependencies&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GnucashAccBook&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDependency&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Class&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AccBook&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Seasoned Spring developers will recognize the silhouette of an old friend.&lt;/span&gt;
        &lt;span class="c1"&gt;// This one retires right after construction, though — no container moves in.&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;AccBook&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You register it exactly like any other service — &lt;code&gt;META-INF/services&lt;/code&gt;, or in &lt;code&gt;module-info.java&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;provides&lt;/span&gt; &lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;druvu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ComponentFactory&lt;/span&gt;
    &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;myapp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;gnucash&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;GnucashBookFactory&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the API module you give callers one obvious entry point — a &lt;code&gt;static load&lt;/code&gt; right on the interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;AccBook&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Account&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;accounts&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;AccBook&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ComponentLoader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AccBook&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Dependencies&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So everything the caller ever sees is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;AccBook&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AccBook&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The constructor argument is passed. A single implementation is guaranteed — &lt;code&gt;ComponentLoader.load&lt;/code&gt; throws if two factories match the type, instead of silently choosing one. No container, no annotations, no classpath scanning.&lt;/p&gt;

&lt;p&gt;And when multiple implementations &lt;em&gt;are&lt;/em&gt; the point — a plugin system, an exporter per format — the same pattern flips around:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Exporter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;exporters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MultiComponentLoader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exporter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every registered factory gets the same &lt;code&gt;Dependencies&lt;/code&gt;, you get all the instances. Same discovery, same argument-passing, opposite cardinality rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not magic
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AccBook.load(path)&lt;/code&gt; isn't reflection sorcery and it isn't a new DI framework. Underneath it is still &lt;code&gt;ServiceLoader&lt;/code&gt;, still the JPMS &lt;code&gt;provides&lt;/code&gt;/&lt;code&gt;uses&lt;/code&gt; you would have written anyway. The &lt;code&gt;static load&lt;/code&gt; is a plain method you write once. The only thing the library adds is the part the JDK left out: a typed way to pass arguments to the implementation, plus a fail-fast "exactly one" rule.&lt;/p&gt;

&lt;p&gt;One honest caveat. The compiler checks what you put &lt;em&gt;into&lt;/em&gt; &lt;code&gt;Dependencies&lt;/code&gt; — &lt;code&gt;Dependencies.of(Path.class, path)&lt;/code&gt; won't accept a &lt;code&gt;String&lt;/code&gt; where a &lt;code&gt;Path&lt;/code&gt; belongs. But it can't check that you supplied everything the factory will ask for. Forget the &lt;code&gt;Path&lt;/code&gt; and the code still compiles — it fails only when &lt;code&gt;load()&lt;/code&gt; runs and &lt;code&gt;getDependency(Path.class)&lt;/code&gt; comes up empty. This is a real difference from Spring: Spring checks all its wiring at startup, so a missing bean stops the app from booting. Here there is no startup check — the check happens when you call &lt;code&gt;load()&lt;/code&gt;. Call it at startup and you catch the mistake at startup. Call it later, and you catch it later. The good news: the &lt;code&gt;load(...)&lt;/code&gt; call and the factory that reads it sit right next to each other, so when it fails, there is one obvious place to look. But the compiler won't catch it for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  When not to use it
&lt;/h2&gt;

&lt;p&gt;If your implementations have no-arg constructors, you don't need any of this — plain &lt;code&gt;ServiceLoader&lt;/code&gt; is the right answer, and the library falls back to it anyway. If you already run inside Spring or Guice, use the container you have; this isn't trying to replace it. It earns its place in one specific spot: libraries and modular components that need pluggable, &lt;em&gt;argument-taking&lt;/em&gt; implementations without dragging in a container.&lt;/p&gt;

&lt;h2&gt;
  
  
  That's the whole idea
&lt;/h2&gt;

&lt;p&gt;It's on Maven Central as &lt;code&gt;com.druvu:druvu-lib-loader&lt;/code&gt; — source and docs on &lt;a href="https://github.com/DenissLarka/druvu-lib-loader" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, Apache-2.0, Java 21+. If you've ever hand-written the factory-behind-&lt;code&gt;ServiceLoader&lt;/code&gt; glue yourself, I'd genuinely like to know whether this matches how you did it — or where you'd have done it differently. &lt;a href="https://github.com/DenissLarka/druvu-lib-loader/issues" rel="noopener noreferrer"&gt;Issues&lt;/a&gt; and disagreement welcome.&lt;/p&gt;

</description>
      <category>java</category>
      <category>architecture</category>
      <category>tutorial</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
