TheJach.com

Jach's personal blog

(Largely containing a mind-dump to myselves: past, present, and future)
Current favorite quote: "Supposedly smart people are weirdly ignorant of Bayes' Rule." William B Vogt, 2010

Maybe getting back into ABCL

I was dusting off some old scripts the other day and thought let's check how ABCL is these days... unfortunately the last stable release is still from 2023, at 1.9.3. I see there is still quite some activity in the source repo though. And there's a plan to release 1.10 at the upcoming 2026 ELS.

I'm using OpenJDK 21, my distro installed and ran ABCL just fine. My first obstacle was just running swank on a different port with slimv; in the past I think I must have just launched ABCL separately and quickloaded swank and manually ran the launch function. Or actually, I remember loading ABCL into an existing Java program, so I would have started swank from Java code. Well, we'll see if this PR is accepted. (I also ran into some other swank issues that I don't exactly recall but maybe have always been limitations.. e.g. in the debugger, it shows me the stack trace, but if I hit enter on one of the frames (to see its locally bound variables) I get a swank/backend:frame-call not implemented error, and indeed in slime/swank's source code such an implementation is missing for ABCL. Might be attempting a pull request to that project in the future.)

In the past I had come across the library (or "web framework") Javalin, it was sort of advertised as "like Flask, but for Java". And well, look at its Hello World:

import io.javalin.Javalin;


void main() {
var app = Javalin.create(config -> {
config.routes.get("/", ctx -> ctx.result("Hello World"));
}).start(7070);
}


Pretty sweet looking as far as Java goes! But things have changed some. In that past link, I used Javalin 1.2, and now Javalin is at 7.2. I ran into one remark in the modern docs that gave me pause:

Important change in Javalin 7: Routes must now be defined in the config.routes block during application creation. You can no longer add routes after calling .start(). See the migration guide for details.

This is so... antithetical to the Lisp ethos. And just sensible dynamic development ethos. Javalin is dead to me.

But it can still be a useful exercise to get it working with ABCL... And of course, like anything Java, it's never as simple as just a single script. You need to get the jars containing the code somehow, and load them up. With a Lisp script running on ABCL, it's no different. We can avoid the need to make an explicit project though by just dumping the jars in a temporary folder!

So I started out with this, evaluating each form one by one in the REPL:


(defparameter *tmp-pom* #p"/tmp/javalin-pom.xml")
(defparameter *tmp-jar-dir* #p"/tmp/javalin-jars/")

(with-open-file (s *tmp-pom* :direction ':output :if-exists ':supersede)
(format s "<project>
<modelVersion>4.0.0</modelVersion>
<groupId>tmp</groupId>
<artifactId>tmp</artifactId>
<version>1</version>
<dependencies>
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin</artifactId>
<version>7.2.0</version>
</dependency>
</dependencies>
</project>"))

(uiop:delete-directory-tree *tmp-jar-dir* :validate t :if-does-not-exist ':ignore)
(ensure-directories-exist *tmp-jar-dir* :verbose t)

(uiop:run-program
(list "mvn" "-f" (namestring *tmp-pom*)
"dependency:copy-dependencies"
(uiop:strcat "-DoutputDirectory=" (namestring *tmp-jar-dir*))
"-DincludeTransitive=true"))

(java:add-to-classpath (uiop:directory-files *tmp-jar-dir* "*.jar"))


Here I'm just making a temporary maven pom file and shelling out to invoke it and dump all the necessary jars somewhere, then loading them dynamically. It works great. I can add other dependencies if I need to (as we'll get to later). Next comes the actual equivalent to the main function in the Java code: make an application server and start it.


(defparameter *server*
(java:jstatic "create" "io.javalin.Javalin"
(java:jinterface-implementation
"java.util.function.Consumer" "accept"
(lambda (config) ; io.javalin.config.JavalinConfig
(java:jcall "get" (java:jfield "routes" config)
"/"
(java:jinterface-implementation
"io.javalin.http.Handler" "handle"
(lambda (ctx) ; io.javalin.http.servlet.JavalinServletContext
(java:jcall "result" ctx
"Hello Javalin!"))))))))

(java:jcall "start" *server* 7070)


It works! You'll see a warning about implementing a dummy andThen method for the java.util.function.Consumer interface, but it works.

Of course, it doesn't support Lisp goodness of redefining the handler's function body. For that you'll need to write a separate function, and you can't just pass it directly, you need to call it in the wrapped lambda:


(java:jcall "stop" *server*) ; first, stop the server as we'll be making a new one

(defun index (ctx)
(java:jcall "result" ctx
"Hello Javalin!"))

(defparameter *server*
(java:jstatic "create" "io.javalin.Javalin"
(java:jinterface-implementation
"java.util.function.Consumer" "accept"
(lambda (config) ; io.javalin.config.JavalinConfig
(java:jcall "get" (java:jfield "routes" config)
"/"
(java:jinterface-implementation
"io.javalin.http.Handler" "handle"
(lambda (ctx) ; io.javalin.http.servlet.JavalinServletContext
(index ctx))))))))


Now we can make changes to the index function and recompile them with swank and see them reflected without having to restart the program or server. Excellent.

For another silly example, here's a file server (for one directory -- though technically sub-files are accessible, I just didn't do anything to support navigating to them):


(defparameter *file-server*
(java:jstatic "create" "io.javalin.Javalin"
(java:jinterface-implementation
"java.util.function.Consumer" "accept"
(lambda (config) ; io.javalin.config.JavalinConfig
(java:jcall "get" (java:jfield "routes" config)
"/"
(java:jinterface-implementation
"io.javalin.http.Handler" "handle"
(lambda (ctx) ; io.javalin.http.servlet.JavalinServletContext
(java:jcall "html" ctx
(with-output-to-string (s)
(format s "<h1>Index of /</h1><hr/>~%")
(loop for file in (sort (uiop:directory-files (uiop:getcwd)) #'string< :key #'namestring)
do
(format s "<a href=\"/~a~:*\">~a</a><br/>~%" (file-namestring file))))))))
(java:jcall "add" (java:jfield "staticFiles" config)
(namestring (uiop:getcwd))
(java:jfield "io.javalin.http.staticfiles.Location" "EXTERNAL"))))))


(Note that this is insecure in many ways. Note StringEscapeUtils lives in org.apache.commons.)

These jinterface implementations kind of suck. Java indeed has "lambdas" but they are implemented as basically an interface with an annotation and one abstract method. With some reflection magic, we can test for that:


(defun functional-interface? (class-name)
(let* ((class (java:jclass class-name))
(annotation (java:jclass "java.lang.FunctionalInterface")))
(java:jcall "isAnnotationPresent" class annotation)))

(functional-interface? "java.util.function.Consumer") ; -> t
(functional-interface? "io.javalin.http.Handler") ; -> t
(functional-interface? "java.util.List") ; -> nil


With more reflection magic, we can eliminate the need to specify the name of that abstract method and reduce some boilerplate:


(defun jlambda (interface-name fn)
(let* ((class (java:jclass interface-name))
(methods (remove-if-not
(lambda (method)
(let ((modifiers (java:jcall "getModifiers" method)))
(java:jstatic "isAbstract" "java.lang.reflect.Modifier" modifiers)))
(java:jcall "getMethods" class)))
(single-abstract-method (elt methods 0)))
(java:jinterface-implementation
interface-name
(java:jmethod-name single-abstract-method)
fn)))

; now for e.g. the config lambda we could write:
(jlambda "java.util.function.Consumer"
(lambda (config)
; whatever
))
; or the http handler lambda we could write:
(jlambda "io.javalin.http.Handler"
(lambda (ctx)
whatever))


This frees us from having to additionally know about the method names accept/handle/whatever at least.

In the past (I no longer have the code), I created some sort of macro that I think I called jimport. It was stupid-simple, basically just something like this:


(defmacro jimport (class-name)
(let ((symbol (intern class-name)))
`(defvar ,symbol (java:jclass ,class-name))))

(jimport "java.util.ArrayList")
(jimport "java.lang.System")


This lets me write things like |java.uti and press tab, and have my editor automatically complete it to |java.util.ArrayList| This makes it visually stand out in the source code too as a known Java thing.

Though now that I think about it, it's possible I did a bit more work to make the defvar symbol be a less verbose |ArrayList|. Or perhaps it was both. I forget.


(let ((list (java:jnew |java.util.ArrayList|)))
(java:jcall "add" list 3)
(java:jcall "println" (java:jfield |java.lang.System| "out") list)
(string list)) ; -> "[3]"


I think I used to have something similar for method names too, though they probably were just defvar'd to be the strings themselves.

In theory a set of macros could be made that are richer... lots of info is available through reflection.


(defun method-info (class)
(let ((info (make-hash-table :test #'equal)))
(loop for method across (java:jclass-methods class)
do
(let* ((method-name (java:jmethod-name method))
(methods-by-name (gethash method-name info (make-array 0 :adjustable t :fill-pointer 0))))
(vector-push-extend (list :static? (java:jmember-static-p method) :name (java:jmethod-name method)
:params (java:jmethod-params method) :return-type (java:jmethod-return-type method) :method method)
methods-by-name)
(setf (gethash method-name info) methods-by-name)))
info))

(jimport "io.javalin.Javalin")

(defparameter *method-info* (method-info |io.javalin.Javalin|))
(ql:quickload "alexandria")
(alexandria:hash-table-keys *method-info*) ; -> ("jettyServer" "port" "wait" "equals" "create" "start" "hashCode" "getClass" "toString" "javalinServlet" "stop" "notify" "notifyAll")
(gethash "create" *method-info*) ; -> #((:STATIC? T :NAME "create" :PARAMS #() :RETURN-TYPE #<java class io.javalin.Javalin> :METHOD #<method public static io.javalin.Javalin io.javalin.Javalin.create()>) (:STATIC? T :NAME "create" :PARAMS #(#<java class java.util.function.Consumer>) :RETURN-TYPE #<java class io.javalin.Javalin> :METHOD #<method public static io.javalin.Javalin io.javalin.Javalin.create(java.util.function.Consumer)>))


But I don't know quite what to do to help things like lambdas or other info that relies on type-erased generic arguments...

Well, this post is just my scratch notes mainly. One last thing to close off on, however, is to harp once again on a theme from my language envy post. Javalin of course ties into the Micrometer framework, by registering a simple plugin you get nice Jetty-related metrics on all routes for free. Let's apply this to our example above. First add the micrometer plugin and micrometer itself dependencies to the maven xml and re-run the flow to get the jars where you want and loaded:


<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin-micrometer</artifactId>
<version>7.2.0</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.16.5</version>
</dependency>


(Side note, (require :jss) and (jss:find-java-class "str") is helpful for finding the full package-specified name of a class. The jss contrib has a lot going for it, and should be worth carefully looking at if one plans to do a lot of ABCL. I just hate tab-complete-unfriendly strings for class/method names..)

Next create the registry and plugin objects:


(jimport "io.micrometer.prometheusmetrics.PrometheusMeterRegistry")
(jimport "io.micrometer.prometheusmetrics.PrometheusConfig")
(jimport "io.javalin.micrometer.MicrometerPlugin")

(defparameter *registry* (java:jnew |io.micrometer.prometheusmetrics.PrometheusMeterRegistry| (jfield |io.micrometer.prometheusmetrics.PrometheusConfig| "DEFAULT")))
(defparameter *micrometer-plugin* (java:jnew |io.javalin.micrometer.MicrometerPlugin|
(java:jinterface-implementation
"java.util.function.Consumer" "accept"
(lambda (config)
(setf (jfield "registry" config) *registry*)))))


Now when creating our server, we just register the plugin, and optionally (could be more secure to put it on a different server's port) create a route to serve the metrics:


(defparameter *server*
(java:jstatic "create" "io.javalin.Javalin"
(java:jinterface-implementation
"java.util.function.Consumer" "accept"
(lambda (config) ; io.javalin.config.JavalinConfig
(java:jcall "registerPlugin" config *micrometer-plugin*)
(java:jcall "get" (java:jfield "routes" config)
"/metrics"
(java:jinterface-implementation
"io.javalin.http.Handler" "handle"
(lambda (ctx)
(let ((metrics (java:jcall "scrape" *registry*)))
(java:jcall "contentType" ctx "text/plain; version=0.0.4; charset=utf-8")
(java:jcall "result" ctx metrics)))))
(java:jcall "get" (java:jfield "routes" config)
"/"
(java:jinterface-implementation
"io.javalin.http.Handler" "handle"
(lambda (ctx) ; io.javalin.http.servlet.JavalinServletContext
(java:jcall "result" ctx
"Hello Javalin!"))))))))


Start the server, visit the index, visit the /metrics route, and see some stats already. Refresh the metrics route, and see more as the metrics route itself starts getting added to the stats. (This is another reason you may want to serve the metrics route itself off the main application server, if you don't particularly care about measuring such info of the metrics route vs. your actual routes.)

After a few refreshes of each I get this data:


# HELP http_server_requests_seconds HTTP server request metrics
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/metrics"} 5
http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/metrics"} 0.009
http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="root"} 3
http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",uri="root"} 0.0
# HELP http_server_requests_seconds_max HTTP server request metrics
# TYPE http_server_requests_seconds_max gauge
http_server_requests_seconds_max{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/metrics"} 0.007
http_server_requests_seconds_max{exception="None",method="GET",outcome="SUCCESS",status="200",uri="root"} 0.0


It's rather nice to just get this "for free" for all routes! Consider what I had to do to get this in Lisp. It wasn't a ton of work, but it wasn't as free, so again, this is nice.

(I should probably log the http response code too, huh? My metrics look like the following.)


# TYPE http_requests_total counter
# HELP http_requests_total Total count of http requests since the app started. Resets to 0 on app restart.
http_requests_total{route="about-us", path="/about-us", method="GET", app="secresoft"} 59
# TYPE http_request_duration_seconds summary
# HELP http_request_duration_seconds Duration of requests
http_request_duration_seconds{route="about-us", path="/about-us", method="GET", app="secresoft", quantile="0.5"} 1.2540817260742188e-4
http_request_duration_seconds{route="about-us", path="/about-us", method="GET", app="secresoft", quantile="0.9"} 1.4901161193847656e-4
http_request_duration_seconds{route="about-us", path="/about-us", method="GET", app="secresoft", quantile="0.99"} 1.678466796875e-4
http_request_duration_seconds_sum{route="about-us", path="/about-us", method="GET", app="secresoft"} 0.009367227554321289
http_request_duration_seconds_count{route="about-us", path="/about-us", method="GET", app="secresoft"} 59


Well that's it for now, we'll see if this sidequest results in me trying to use ABCL more or not. (My bets are on not, at least for the rest of the year. I'll look forward to seeing what's new in the 1.10 release though.)


Posted on 2026-05-02 by Jach

Tags: lisp, programming

Permalink: https://www.thejach.com/view/id/454

Trackback URL: https://www.thejach.com/view/2026/5/maybe_getting_back_into_abcl

Back to the top

Back to the first comment

Comment using the form below

(Only if you want to be notified of further responses, never displayed.)

Your Comment:

LaTeX allowed in comments, use $$\$\$...\$\$$$ to wrap inline and $$[math]...[/math]$$ to wrap blocks.