Jbang, Make Java Fun & Simple again!

Jbang, Make Java Fun & Simple again!

Try out Java. It ain't all AbstractSingletonProxyFactoryBean

Java has been around decades with it being the de-facto runtime for enterprise applications. This makes it geared towards seasoned developers with all the complicated tooling around Build Systems, handling JARs, tuning GCs.
All these newcomers overwhelmed to the point on not trying the language. In this time, a lot of interpreted as well as complied language have made scripting easier. Runtimes/Languages such as Go, Node.js, Python, Ruby makes it easier to write one-off scripts. These scripts could be a background job like reading a file, writing CLIs, writing Serverless functions for one-off tasks, or it might be just learning some new syntax.

Thankfully, Java has got an answer which is JBang.

Getting Started

Downloading JBang:

Use your favourite package manager to install JBang.

  • Simple CURL: curl -Ls https://sh.jbang.dev | bash -s - app setup

  • SDKMan: sdk install jbang

  • OS Installation: For Windows, you can use the choco install jbang command and for Mac you can use brew install jbangdev/tap/jbang

Dependencies

Since you do not need a build system, you can pass the Dependencies and even arguments as comments which will tell JBang to download and use the arguments.

///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS info.picocli:picocli:4.7.5
//JAVA_OPTIONS -Djava.library.path=.

Applications

One-off scripts are more handy than you think. Of course, you won't use in Production use-cases, but having it in your tool bit is always a good thing.

Application 1: Dead Simple API Querying

You can create a dead simple Spring Boot API in ~25 lines.

//usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 21
//DEPS org.springframework.boot:spring-boot-starter-web:3.2.5

package com.makariev.examples.jbang;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestParam;

@SpringBootApplication
@RestController
public class SpringRestApi {

    public static void main(String[] args) {
        SpringApplication.run(SpringRestApi.class, args);
    }

    @GetMapping("/")
    public String JBangRestAPI(
        @RequestParam(value = "name", defaultValue = "Jbang!") String name) {
        return "Hello, " + name;
    }
}

Application 2: Reading from embedded DB

You don't need a bloated DB. DuckDB is a very powerful embedded OLAP database that can be used to do some quick querying and analysis of data. All this in ~80 lines of code.

///usr/bin/env jbang "$0" "$@" ; exit $?
// Update the Quarkus version to what you want here or run jbang with
// `-Dquarkus.version=<version>` to override it.
//DEPS io.quarkus:quarkus-bom:${quarkus.version:2.4.0.Final}@pom
//DEPS io.quarkus:quarkus-resteasy
//DEPS org.duckdb:duckdb_jdbc:1.0.0
//JAVAC_OPTIONS -parameters
//JAVA_OPTIONS -Dquarkus.profile=dev -Dquarkus.live-reload.enabled=true

import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.duckdb.DuckDBDriver;

@Path("/")
@ApplicationScoped
public class QuarkusRestApi {

    @GET
    public int getDataFromCSV() {

        Connection conn = null;
        Date currentDate = null;
        try {
            conn = createDBConnection();

        } catch (Exception exception) {
            System.out.println("DuckDB Initialization exception" + exception);
        }

        try {
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery("SELECT current_date");
            currentDate = currentDate = rs.next() ? rs.getDate(1) : null;
        } catch (Exception exception) {
            System.out.println("DuckDB Execution exception" + exception);
        }

        int columnCount = 0;
        try {
        var InsertQuery = """
            CREATE TABLE customer AS 
            SELECT * FROM read_csv('customer.csv')
            """;
        Statement stmt = conn.createStatement();
        stmt.execute(InsertQuery);
        ResultSet rs = stmt.executeQuery("SELECT * FROM customer");
        System.out.println(rs);
            ResultSetMetaData metaData = rs.getMetaData();
            columnCount = metaData.getColumnCount();

            while (rs.next()) {
                List<String> rowData = IntStream.rangeClosed(0, columnCount)
                        .mapToObj(i -> {
                            try {
                                return rs.getString(i);
                            } catch (SQLException e) {
                                return "SQL Exception while reading row";
                            }
                        })
                        .collect(Collectors.toList());
                System.out.println(String.join("\t", rowData));
            }

} catch (Exception exception) {
            System.out.println("DuckDB Execution exception" + exception);
        }
        return columnCount;
    }

    private Connection createDBConnection() throws SQLException{
        return DriverManager.getConnection("jdbc:duckdb:");
    }


}

Application 3: CLI using Picocli

Creating CLI using Picocli with JBang is now simple and possible in 40 lines!

///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS info.picocli:picocli:4.7.5
//JAVA_OPTIONS -Djava.library.path=.

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;

import java.util.concurrent.Callable;

@Command(name = "aymanCli", mixinStandardHelpOptions = true, version = "aymanCli 1.0",
         description = "CLI application using Picocli and JBang")
class AymanCLI implements Callable<Integer> {

    @Command(name = "greet", description = "Greet a person")
    int greet(@Parameters(paramLabel = "NAME", description = "Name of the person to greet") String name,
              @Option(names = {"-l", "--language"}, description = "Language of greeting") String language) {
        String greeting = language != null && language.equalsIgnoreCase("es") ? "Hola" : "Hello";
        System.out.printf("%s, %s!%n", greeting, name);
        return 0;
    }

    @Command(name = "add", description = "Add two numbers")
    int add(@Parameters(paramLabel = "NUM1", description = "First number") int num1,
            @Parameters(paramLabel = "NUM2", description = "Second number") int num2) {
        int result = num1 + num2;
        System.out.printf("%d + %d = %d%n", num1, num2, result);
        return 0;
    }

    @Override
    public Integer call() {
        System.out.println("Please use a subcommand. Use --help for more information.");
        return 0;
    }

    public static void main(String... args) {
        int exitCode = new CommandLine(new AymanCLI()).execute(args);
        System.exit(exitCode);
    }
}

Conclusion

Code can be found and played around from this Replit.

https://replit.com/@AymanArif1/JBangScripts#README.md

So, give Java another chance. It ain't that verbose now.

Did you find this article valuable?

Support Ayman Tech Musings by becoming a sponsor. Any amount is appreciated!