Spúšťanie programov z viacerých zdrojových súborov v Jave

15. 5. 2024
Doba čtení: 8 minut

Sdílet

 Autor: Root.cz s využitím DALL-E
V najnovšej verzii Javy 22 pribudla možnosť spúšťania programov priamo z viacerých zdrojových súborov .java. Ukážeme si, ako sa vytvára malá aplikácia pomocou príkazového riadku, Gradle manažéra a týmto najnovším spôsobom.

Spúšťanie programov z viacerých zdrojových súborov popisuje dokument JEP 458: Launch Multi-File Source-Code Programs. Hlavnými dôvodmi tejto zmeny sú uľahčenie spúšťania jednoduchých programov a zjednodušenie výučby Javy pre začínajúcich programátorov.

Doteraz bolo spúšťanie obmedzené len na jediný súbor. Túto možnosť priniesla pred rokmi Java 11.

Prakticky každý moderný programovací jazyk umožňuje jednoduchým spôsobom spustiť program priamo zo zdrojových súborov alebo triviálnym spôsobom zhotoviť výslednú binárku. (Niektoré programovacie jazyky dokážu oboje.) Java bola v tomto ohľade výnimkou. Spúšanie veľmi jednoduchých programov v Jave je pracné a komplikované. Preto sa väčšinou aj na malé programy používajú komplexné vývojové prostredia a Maven alebo Gradle manažéry.

Klasický postup z príkazovej riadky

Najprv si ukážeme, ako sa spúšťa jednoduchá aplikácia klasickým spôsobom z príkazovej riadky.

    ├── bin
    ├── lib
    │   └── jsoup-1.17.2.jar
    └── src
        └── main
            └── java
                └── com
                    └── example
                        ├── Main.java
                        └── utils
                            ├── Scraper.java
                            └── Utils.java

Majme takúto adresárovú štruktúru projektu. Do adresára bin sa skompiluje výsledné binárne súbory. V adresári lib máme JAR súbor pre JSoup knižnicu. Súbor si môžme stiahnuť z príslušného Maven repozitára. V adresári src máme zdrojové súbory Java programu.

// Main.java
package com.example;

import com.example.utils.Scraper;
import com.example.utils.Utils;
import java.io.IOException;

public class Main {

    public static void main(String[] args) throws IOException {

        String word = new Utils().getRandomWord();
        System.out.println(word);

        String url = "https://example.com";

        if (args.length > 0) {
            url = args[0];
        }

        var scraper = new Scraper(url);

        String title = scraper.getTitle();
        System.err.println(title);
    }
}

V súbore Main.java máme metódu main, ktorá je vstupným bodom do aplikácie. Tento jednoduchý program vypíše náhodné slovo a načíta titulok z webovej stránky.

// Utils.java
package com.example.utils;

import java.util.List;
import java.util.Random;

final public class Utils {

    List<String> words = List.of("sky", "town", "blue", "forest", "snake",
        "book", "pen", "cloud", "cup");

    public String getRandomWord() {

        int idx = new Random().nextInt(words.size());
        return words.get(idx);
    }
}

V súbore Utils.java máme implementáciu metódy, ktorá nám vráti náhodné slovo zo zoznamu.

// Scraper.java
package com.example.utils;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import java.io.IOException;

public final class Scraper {

    private final String url;

    public Scraper(String url) {
        this.url = url;
    }

    public String getTitle() throws IOException {

        Document doc = Jsoup.connect(url).get();
        return doc.title();
    }
}

Nakoniec v Scraper.java  máme metódu, ktorá získa titulok z uvedenej webovej stránky. Nato využívame populárnu knižnicu JSoup.

Ďalej si ukážeme, ako daný program skompilujeme a spustíme. Taktiež si vytvoríme spustiteľný JAR súbor.

$ javac -d bin -cp lib/jsoup-1.17.2.jar src/main/java/com/example/Main.java src/main/java/com/example/utils/*.java

Pomocou nástroja javac skompilujeme súbory do Java byte kódu. Pomocou prepínača -d uvedemie kam sa majú skompilované súbory umiestniť. Prepínačom -cp informujeme kompilátor, kde sa nachádza naša externá knižnica. Na konci uvádzame cestu ku zdrojovým súborom.

$ java -cp bin:lib/jsoup-1.17.2.jar com.example.Main https://webcode.me
blue
My html page

Pomocou nástroja java spustíme nakoniec náš program. Najprv špecifikujeme adresáre, v ktorých sa nachádza spustiteľný byte kód. Potom nasleduje plný názov programu, bez uvedenia .class prípony. Na konci máme voliteľný argument nášho programu. (Na operačnom systéme Windows treba namiesto dvojbodky použiť bodkočiarku.)

V Jave je štandardom zlúčiť binárne súbory a prislúchajúce zdroje do JAR súborov.

Manifest-Version: 1.0
Main-Class: com.example.Main
Class-Path: ../lib/jsoup-1.17.2.jar

Na vytvorenie spustiteľného JAR súboru si vytvoríme manifest.txt, ktorý umiestnime to bin adresára. V ňom si uvedemie cestu k nášmu programu a k použitej externej knižnici. Posledný riadok v súbore musí byť ukončený novým riadkom, ináč sa súbor nespracuje správne. (Táto „feature“ vás môže stáť hodiny vášho času.)

$ jar -cvfm main.jar manifest.txt com ../lib

V podadresári bin si vytvoríme pomocou nástroja jar náš spustiteľný program.

$ java -jar main.jar
book
Example Domain

JAR súbory spúšťame pomocou -jar prepínača nástroja java.

Z uvedeného je jasné, aké je to zdĺhavé a prácne. Celý tento proces je náchylný ku chybám. Existujú ďalej určité rozdiely medzi postupom na Linuxe a na Windows, ktoré ďalej komplikujú tento proces.

Gradle manažér

Komunita vývojárov vytvorila dva veľmi rozšírené projektové manažéry: Gradle a Maven. Ukážeme si, ako sa použije Gradle na spustenie aplikácie a na tvorbu spustiteľného JAR súboru.

$ gradle -version

------------------------------------------------------------
Gradle 8.7
------------------------------------------------------------

Build time:   2024-03-22 15:52:46 UTC
Revision:     650af14d7653aa949fce5e886e685efc9cf97c10

Kotlin:       1.9.22
Groovy:       3.0.17
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          21.0.2 (Amazon.com Inc. 21.0.2+13-LTS)
OS:           Linux 6.5.0-27-generic amd64

Najnovšia verzia Gradle 8.7 zatiaľ nepodporuje Javu 22. Preto si potrebujeme pre potreby tohto príkladu nastaviť nižšiu verziu Javy. Ak používame aplikáciu sdkman, použijeme príkaz sdk use java, alebo nastavíme premennú JAVA_HOME. Ináč dostaneme chybovú hlášku Unsupported class file major version 66.

$ gradle init

Projekt inicializujeme pomocou gradle init príkazu. Gradle používa na konfiguráciu interaktívne menu. Vyberieme si voľby Application/Java/Single application project/Groovy/Spock. (Posledné dve nie sú pre náš príklad podstatné.)

Z predchádzajúceho príkladu použijeme všetky tri súbory. Upravíme si adresárovú štruktúru, ktorú nám vytvoril Gradle. (Zmeníme org.example na com.example.) Tiež zakomentujeme testovacie súbory.

Ďalej si upravíme build.gradle súbor. Tento súbor je centrálnym konfiguračným súborom pre manažment projektov. Na konfiguráciu sa používajú jazyky Groovy alebo najnovšie tiež Kotlin.

dependencies {
    ...
    implementation 'org.jsoup:jsoup:1.17.2'
}

Pridáme závislosť pre JSoup knižnicu.

application {
    mainClass = 'com.example.Main'
}

Zadefinujeme si cestu k vstupnej triede aplikácie.

$ gradle -q run
cup
Example Domain

Takto si teraz môžeme spustiť aplikáciu pomocou Gradle.

jar {
    manifest {
        attributes(
        'Main-Class': 'com.example.Main'
        )
    }
    from {
        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

Týmito konfiguračnými nastaveniami si vytvoríme spustiteľný JAR súbor.

$ gradle build
$ java -jar app/build/libs/app.jar https://webcode.me
sky
My html page

Zostavíme si aplikáciu pomocou gradle build. Spustiteľný JAR súbor sa nám skopíruje do app/build/libs podadresára. Ten nakoniec spustíme pomocou java -jar.

Gradle nám výrazne zjednodušil našu prácu. Použitie Gradle pre malé programy je možno prirovnať ku kanónom na vrabce.

Najnovší postup

Pre tento nový postup sa potrebujeme prepnúť na Java verziu 22. Vytvoríme jednoduchšiu adresárovú štruktúru:

├── lib
│   └── jsoup-1.17.2.jar
├── Main.java
└── utils
    ├── Scraper.java
    └── Utils.java

Main.java súbor si upravíme, ostatné dva súbory sa nemenia.

import utils.Scraper;
import utils.Utils;
import java.io.IOException;

void main(String[] args) throws IOException {

    String word = new Utils().getRandomWord();
    System.out.println(word);

    String url = "https://example.com";

    if (args.length > 0) {
        url = args[0];
    }

    var scraper = new Scraper(url);

    String title = scraper.getTitle();
    System.err.println(title);
}

V príklade tiež využijeme novinku z Javy 21 Unnamed Classes and Instance Main Methods, ktorá nám umožňuje zjednodušiť definíciu metódy main.

$ java --enable-preview --source 22 -cp lib/jsoup-1.17.2.jar Main.java
cup
Example Domain

Program nemusíme zvlášť kompilovať, rovno ho spúšťame nástrojom java. Kvôli zjednodušenej main metóde potrebujeme použiť --enable-preview prepínač. Verziu Javy uvedemie prepínačom --source. Na konci riadku je Main.java súbor, ktorý je vstupným bodom do nášho programu. Java daný súbor priamo skompiluje a zároveň spustí. Takto po novom môžeme jednoducho spúšťať programy, ktoré sa skladajú z viacerých Java zdrojových súborov. Je možné použiť aj externé knižnice.

Automatický manažment závislostí (ako to má napr. Groovy) zatiaľ nie je zabudovaný. JEP 458 zmieňuje možnosť budúceho JEP dokumentu, ktorý by to mohol v budúcnosti priniesť.

Príklad v jazyku Go

Pre porovnanie si uvedieme obdobný príklad v jazyku Go. Kód sa nachádza v dvoch súboroch main.go a utils.go.

$ go mod init com/example/first

Inicializácia projektu prebehne pomocou príkazu go mod init.

// main.go
package main

import (
    "flag"
    "fmt"
)

func main() {

    url := flag.String("u", "https://something.com", "website URL")
    flag.Parse()

    word := GetRandomWord()
    fmt.Println(word)

    title := GetTitle(*url)
    fmt.Println(title)
}

Go má module flag na jednoduché parsovanie argumentov.

// utils.go
package main

import (
    "math/rand"

    "github.com/gocolly/colly/v2"
)

func GetRandomWord() string {

    var words []string = []string{"sky", "town", "blue", "forest", "snake",
        "book", "pen", "cloud", "cup"}

    idx := rand.Intn(len(words))

    return words[idx]
}

func GetTitle(url string) string {

    var title string

    c := colly.NewCollector()

    c.OnHTML("title", func(e *colly.HTMLElement) {
        title = e.Text
    })

    c.Visit(url)

    return title
}

Na parsovanie titulku stránky použijeme knižnicku gocolly. Na stiahnutie závislostí môžeme použiť príkazy go mod tidy alebo go get.

$ go mod tidy
go: finding module for package github.com/gocolly/colly/v2
go: found github.com/gocolly/colly/v2 in github.com/gocolly/colly/v2 v2.1.0
$ go run main.go utils.go
pen
Something.

Pomocou príkazu go run môžme náš kód priamo spustiť, ako keby sa jednalo o klasický skript.

$ go build
$ ./first -u https://root.cz
sky
Root.cz - informace nejen ze světa Linuxu

Kompilácia programu do binárky je triviálna, stačí použiť príkaz go build.

Podobne to má aj .NET (dotnet run/build) alebo Rust (cargo run/build).

bitcoin_skoleni

Uľahčenie práce

V poslednej dobe priniesla Java množstvo zaujímavých noviniek. Viaceré z nich uľahčujú vývojárom ich prácu. Možnosť spúšťania programov z viacerých zdrojových súborov ocenia nielen lektori, ale aj tí, ktorí radi experimentujú a vytváranú pri tom množstvo menších programov.

Osobne si myslím, že v tomto ohľade ide vývoj jazyka správnym smerom. Ak by sa v budúcnosti doplnil automatický manažment závislostí, mohla by sa Java využiť aj na skriptovania pomerne väčších programov.

Autor článku

Od roku 2006 sa venujem písaniu o počítačových technológiách, predovšetkých programovacím jazykom, grafickému užívateľskému rozhraniu a databázam.