Josip Kovaček
Autor:
Josip Kovaček

Java developer

SpringBoot i JavaFx kuharica

Cilj ovog članka je prikaz integracije Spring Boot i JavaFx tehnologija koje omogućuju održiv i rapidan razvoj java desktop aplikacija. Integracija će biti prikazana kroz demo projekt koji će postupno biti nadograđivan sa funkcionalnostima i praksama poput razdvajanja prezentacijske i poslovne logike, dependency injection i clean code principima. Članak će biti više tehničke naravi budući da se radi o integraciji dva alata kroz kod. 

Long story short – Kako bi navedene tehnologije dobro surađivale potrebno je dati prednost JavaFx aplikaciji da se pokrene prva kroz main() metodu, a zatim uključiti pokretanje spring aplikacijskog konteksta kroz nadjačanu (overriden) JavaFx init() metodu. Također za ispravno funkcioniranje dependency injectiona potrebno je kreirati programsku logiku koja postavlja spring managed instancu JavaFx kontrolera prije svakog FxmlLoader.load() poziva.

Rapidni razvoj aplikacija zahtijeva tehnologije i alate koji omogućuju da se stvari brzo naprave. Kada radimo na velikim projektima koji se razvijaju kroz duži vremenski period, uobičajeno je da se na njima izmjenjuje više ljudi i timova kroz različite faze života projekta. Ovdje osim što je važna brzina razvoja,  važno je i da to bude održiv razvoj i da novi članovi tima mogu što prije nastaviti rad na projektu. Jedna takva tehnologija koja je u jakom usponu je Spring Boot koji je postavio nove standarde sa svojom jednostavnošću, fleksibilnošću i convention over configuration pristupom. Spring Boot uz rapidan razvoj i jednostavnost korištenja i održavanja donosi sa sobom i sve prednosti Spring frameworka poput dependency injectiona.

Kod razvoja desktop Java aplikacije za izradu grafičkog sučelja prirodan izbor su alati koje framework već uključuje, među popularnim alatima nalaze se Swing i JavaFx. Ovdje će fokus biti na JavaFx zbog svojih prednosti nad ostalim alatima.

JavaFx, dugo očekivani nastavak Swing alata, je donio poboljšanja vezana uz čišći kod poput pisanja view dijela u FXML sintaksi baziranoj na XML-u i mogućnost stiliziranja komponenti sa CSS-om. Uz korištenje Gluon Scene Builder alata omogućeno je drag’n’drop kreiranje kompleksnih layoutova i komponenti na ekranu koji su odvojeni od koda kao zasebne fxml datoteke. U odnosu na Swing, JavaFx je bliska MVC pristupu gdje su biznis logika (kontroleri/servisi), view (FXML, CSS) i model (POJO klase) komponente jasno razdvojene. Sve to doprinosi rapidnom razvoju, čišćem kodu i lakšem održavanju. Uz to JavaFx je dio standardne Jave od verzije 7/8 te je prirodan izbor za razvoj desktop java aplikacija. 

Kako povezati ove dvije tehnologije i dobiti najbolje od oba svijeta?

Kako je JavaFx već uključena u JRE/JDK od Java verzije 7u6, preostaje preuzeti Spring Boot. Ukoliko smo na nekoj starijoj verziji Jave, potrebno je ručno dodati JavaFx na classpath ili ga referencirati kao sistemski dependency u nekom od dependency management alata. Spring Boot alat je moguće preuzeti na https://start.spring.io stranici koja omogućuje kreiranje kostura aplikacije i dodavanje dodatnih dependency-a kroz par klikova. 

Za ovaj demo nije potrebno uključivati ništa od dodatnih opcija, pod group i artifact možemo (opcionalno) upisati ime projekta (artifact) i grupu u kojoj se projekt nalazi te nakon toga odaberemo Generate Project, nakon čega će početi preuzimanje demo projekta u zip formatu. Projekt importamo u omiljeni IDE nakon čega je vidljiva struktura na slici, sa glavnom klasom naziva FxbootApplication.java, pri čemu main() metoda pokreće Spring aplikaciju i kontekst. 

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

Ako pogledamo klasičan JavaFx HelloWorld program, glavna klasa nasljeđuje javafx.application.Application klasu i main() metoda pokreće javafx.application.Application.launch() metodu, a JavaFx kasnije poziva start() metodu u kojoj nam predaje Stage komponentu na koju se možemo nadovezati sa našim elementima i programskom logikom.  

public class FxbootApplication extends Application {

public static void main(String[] args) {
Application.launch();
}

@Override
public void start(Stage primaryStage) throws Exception {
Pane helloPane = new Pane(new Label("Hello JavaFx"));
primaryStage.setScene(new Scene(helloPane));
primaryStage.show();
}
}


JavaFx Hello World nakon pokretanja

Jedno od prvih pitanja je svakako u kojem smjeru krenuti odavde a da obje tehnologije surađuju. Nakon par Google-Fu – ova dolazimo do jednog mogućeg rješenja:

@SpringBootApplication
public class FxbootApplication extends Application {
    public static void main(String[] args) {
        Application.launch();
    }
    @Override
   
public void init() {
        SpringApplication.run(getClass()).getAutowireCapableBeanFactory().autowireBean(this);
    }
    @Override
   
public void start(Stage primaryStage) throws Exception {
        Pane helloFxPane = new Pane(new Label("Hello JavaFx"));
        primaryStage.setScene(new Scene(helloFxPane));
        primaryStage.show();
    }
}

Proučavanjem javafx.application.Application.start() dokumentacije dolazi se do informacije da za razliku od start() metode koja pokreće GUI thread i crta naše komponente na ekran, init() metoda pokreće non-GUI thread što je čini idealnim mjestom za inicijalizaciju spring konteksta. Ovdje se usput registrira glavna FxbootApplication klasa u spring kontejner sa pozivom getAutowireCapableBeanFactory().autowireBean(this); tako da iz nje možemo koristiti dependency injection što će biti korisno par koraka kasnije.

Nakon ovoga možemo otvoriti šampanjac i čestitati si na uspješnoj integraciji Spring Boot-a i JavaFx. Sve dok ne napravimo neki realniji primjer: Kreirali smo FXML view komponentu sa welcome labelom u welcome.fxml datoteci i pripadajući JavaFx kontroler WelcomeController.java koji će postavljati vrijednost welcome labele kroz poziv servisa GreetingService.getWelcomeGreeting(). Zbog kratkoće primjera, welcomeGreeting vrijednost je zapisana u samom servisu, no logika ostaje ista i kad bi iza servisa postojao neki drugi servis ili data access sloj sa bazom.  

<Pane xmlns:fx="http://javafx.com/fxml/1" fx:controller="hr.kingict.java.controller.WelcomeController">
    <Label fx:id="welcomeLabel"/>
</Pane>

welcome.fxml

@Component
public class WelcomeController implements Initializable{
    @FXML public Label welcomeLabel;

    @Autowired
   
private GreetingService greetingService;

    @Override
   
public void initialize(URL location, ResourceBundle resources) {
        welcomeLabel.setText(greetingService.getWelcomeGreeting());
    }
    public String getWelcomeMessage() {
        return greetingService.getWelcomeGreeting();
    }
}

welcomeController.java

@Service
public class GreetingService {
public String getWelcomeGreeting() {
return "Welcome and have a nice day!";
}
}

GreetingService.java

@Override
public void start(Stage primaryStage) throws Exception {
System.out.println(welcomeController.getWelcomeMessage());
// primaryStage.setScene(new Scene(new Pane(new Label("Hello JavaFx"))));
Parent welcomePane = FXMLLoader.load(getClass().getResource("/welcome.fxml"));
primaryStage.show();
}

FxbootApplication.java nakon zadnje izmjene

Pokretanjem main() metode trebala bi se u konzoli ispisati greeting poruka i na grafičkom sučelju iscrtati labela sa porukom iz GreetingService servisa.

U konzoli se može vidjeti ispis greeting poruke iz registriranog WelcomeController kontrolera u FxbootApplication klasi i odmah nakon toga stacktrace ispis do kojeg je došlo jer se aplikacija srušila sa NullPointerException iznimkom.

Odlaskom na zadnji link u stacktraceu optuženi za iznimku ispada linija 22 iz WelcomeController klase.

welcomeLabel.setText(greetingService.getWelcomeGreeting());

Daljnjim debugiranjem dolazimo do činjenice da je krivac neinicijalizirani GreetingService koji je null u trenutku kad javaFx dohvaća i parsira welcome.fxml datoteku. Wait, what. Servis u kontroleru je neinicijaliziran (null), a samo liniju prije smo iz istog registriranog WelcomeController kontrolera u FxbootApplication klasi dobili pozdravnu poruku iz servisa (ispisana prije stacktracea). What kind of sorcery is this?

There shall be only one.

Pretpostavka je ukoliko koristimo dependency injection (DI), onda DI kontejner po defaultu čuva jedinstvenu instancu registrirane klase. U gornjem slučaju ista instanca se ponaša različito. U jednom slučaju imamo servis koji vraća poruku, dok je u drugom neinicijaliziran. Provjerom sa dobrim starim Object.toString() nad WelcomeController instancom registriranom u FxbootApplication klasi i instancom koja je aktivna kad se pozove JavaFx initialize() metoda u kontroleru dobivamo različite hex/hash vrijednosti instance. Sada je jasno da imamo dvije instance, jednu spring managed i jednu nepoznatu koja baca NPE iznimku i očito ne zna ništa o DI, pitanje je samo otkuda dolazi ova druga.

Nakon daljnje primjene Google - Fu tehnike i pretražujući po FxmlLoader.load() dokumentaciji dolazimo do interesantne činjenice da JavaFx instancira svoju instancu kontrolera koja naravno ne zna ništa o DI.  Zahvalit ćemo JavaFx-u na ovom trudu, no DI way of things nam je ipak interesantniji princip. Kad bi samo postojao način da objasnimo JavaFx FxmlLoaderu da uzme već postojeću DI instancu za kontroler. 

Na sreću, prije poziva FxmlLoader.load() metode moguće je proslijediti DI instancu kontrolera kroz poziv FXMLLoader.setControllerFactory(). 

@Override
public void start(Stage primaryStage) throws Exception {
    System.out.println(welcomeController.getWelcomeMessage() + " " + welcomeController.toString());
// Parent welcomePane = FXMLLoader.load(getClass().getResource("/welcome.fxml"));
   
Parent welcomePane = loadFxml("/welcome.fxml");
    primaryStage.setScene(new Scene(welcomePane));
    primaryStage.show();
}
private Parent loadFxml(String view) {
    FXMLLoader loader = new FXMLLoader(getClass().getResource(view));
    loader.setControllerFactory(param -> welcomeController);
    try {
        loader.load();
    } catch (IOException ex) {
        System.err.println("IOException while loading resource " + view);        }
    return loader.getRoot();
}

Ovime je završena integracija, FXMLLoader će kod parsiranja fxml datoteke koristiti kontroler registriran kroz DI.

Napomena: Gornji primjer koristi lambda expression uveden u Javi 8, ukoliko smo na starijoj verziji, potrebno je zamijeniti liniju 

loader.setControllerFactory(param -> welcomeController);

sa sljedećim blokom koda (odličan primjer smanjene verboznosti koda u Javi 8)

loader.setControllerFactory(new Callback<Class<?>, Object>() {
@Override
public Object call(Class<?> param) {
return welcomeController;
}
});

Kako smo na početku spomenuli clean code principe, daljnja nadogradnja se sastoji od izdvajanja logike oko postavljanja ispravne instance kontrolera izvan FxbootApplication klase i kreiranja logike za jednostavniju navigaciju između više ekrana. Navigacija nije potrebna ukoliko imamo samo jedan ekran u aplikaciji.

Navigacija

Za primjer implementacije navigacije nadogradit ćemo postojeći demo sa još jednim ekranom, user.fxml i pripadajućim kontrolerom UserController.java. Ekran će se otvarati klikom na gumb u welcome.fxml view-u i prikazivati listu korisnika koje ćemo dohvaćati iz servisnog sloja, 

Kako bi navigacija bila što jednostavnija, pretvaramo je u spring managed komponentu (@Component anotacija) koju ćemo po potrebi registrirati u fxml kontrolerima. Klasa za navigaciju će imati referencu na sve postojeće kontrolere i logiku za postavljanje odgovarajućeg kontrolera kod promjene ekrana. Sama akcija navigiranja će se pozivati iz bilo kojeg kontrolera sa show...View() metodom. 

@Component
public class Navigation {
private static final Logger LOG = LoggerFactory.getLogger(Navigation.class);
private static final String WELCOME_VIEW = "/fxml/welcome.fxml";
private static final String USER_VIEW = "/fxml/user.fxml";
private static final String APP_CSS = "/css/application.css";
private Stage stage;

@Autowired
private WelcomeController welcomeController;

@Autowired
private UserController userController;

public void showWelcomeView() {
show(WELCOME_VIEW);
}
public void showUserView() {
show(USER_VIEW);
}
private void show(String view) {
Scene scene = new Scene(loadFxml(view), 400, 400);
stage.setScene(scene);
stage.show();
}
private Parent loadFxml(String view) {
FXMLLoader loader = new FXMLLoader(getClass().getResource(view));
loader.setControllerFactory(param -> getViewController(view));
try {
loader.load();
} catch (IOException ex) {
LOG.error("IOException while loading resource {}: ", view, ex);
}
Parent root = loader.getRoot();
root.getStylesheets().add(getClass().getResource(APP_CSS).toExternalForm());
return root;
}
private Object getViewController(String view) {
if (USER_VIEW.equals(view)) {
return userController;
}
return welcomeController;
}
public void setStage(Stage stage) {
this.stage = stage;
}
}

Navigation.java klasa

@SpringBootApplication
public class FxbootApplication extends Application {

@Autowired
private Navigation navigation;

public static void main(String[] args) {
Application.launch(args);
}

@Override
public void init() {
SpringApplication.run(getClass())
.getAutowireCapableBeanFactory().autowireBean(this);
}

@Override
public void start(Stage stage) {
navigation.setStage(stage);
navigation.showWelcomeView();
}
}

FxbootApplication.java klasa nakon izdvajanja logike za ispravno učitavanje fxml-ova


welcome i user ekrani povezani kroz navigaciju

Možemo zaključiti da iako je tokom integracije JavaFX i Spring Boot tehnologija bilo ponekog iskrenja uz par turbulencija na početku, izazovi integracije su se relativno brzo riješili te je trud dobro uložen jer dalje gradimo na temeljima koji omogućuju brz i održiv razvoj, odnosno najbolje iz oba svijeta.

Popularne teme
.NET ABAP ADFS Agile Always On Anemic Model Angular Azure Backbone benchmark BI BI projekti Bootstrap building people business inteligence Business Intelligence Change Chrome CI CITCON Claims compile Continuous Delivery continuous deployment Continuous Integration CSR d3js data data visualization Data visualization alati DDD dekompozicija dependency injection dinamička forma dinamički parametri dinamički query distribuirani razvoj Domain-Driven design DOP društvena odgovornost edge-based video analytics Eliminating waste enkapsulacija enterprise razvoj softvera ERP ETL Excel FIORI Frontend game Geopackage GPKG GIS Git Groovy heat map HICCUPS Hichert HTML IBCS interoperability invision IoT IPSO izvještavanje java JavaFX Javascript Jazz Build Engine JBE Jenkins jquery jqueryui jsfiddle JVM Kaizen Kanban king KING ICT Kingovci Knockout kvaliteta lambde leadership Lean legacy code M language Management Maven Metodologija microservices Microsoft mobile Mobility mockups moć monday game NetWeaver network nodejs oblikovni obrasci OGC OKR open source optimizacija organizacija organizacijska struktura OutOfMemoryError outsourcing overengineering paginacija Performance performanse PERT PMI PMP; Agile; Project management; Scrum; KING ICT; razvoj; metodologija podatkovni skup pouzdanost Power BI Power Map Power Pivot Power Query Power View pretraga proces procjena Product Owner programming proizvod Project manager projektni plan radar Rational Team Concert razvoj tima refaktoriranje Release resize responsive charts REST retrospektiva Rich-Domain model Roko Roić rolling wave planning RTC SAP scale scatterplot chart Scrum scrum team scrum tim service boundaries single responsibility principle Single Sign-On smart metering SoapUI social responsibility softver Software software prototyping Software Testing Club Spring Boot SQL standard sustav videonadzora svg tdd Team team building team development Team Foundation Server tech tehnologije terminski plan Testing tim timesheet timovi Toggl.com touch transakcijski nadzor tražilica underengineering unit testing Uspjeh Visual Studio vodstvo vodstvo leadership moć društvena odgovornost DOP social responsibility CSR vođenje projekata WBS Web Zagreb STC

PRIJAVA NA NEWSLETTER

Najnovije novosti iz ICT svijeta