Swing a autowirovanie komponentov cez compile-time weaving

S[w|pr]ing

Dá sa prepojiť springácka filozofia dependency injection v swingáckych aplikáciách?

Samozrejme, že áno!

Chce to trochu čiernej mágie, ale ukážeme si, ako môžu vaše JFrame okná mať bez problémom autowireované závislosti deklarované v Springu.

Presnejšie, v aplikácii si ukážeme okno so zoznamom JList, ktoré bude mať dáta doťahované z autowireovaného DAO objektu.

Ingrediencie

Budeme potrebovať:

  • Spring 4.x
  • Maven

Z hľadiska kódu budeme potrebovať:

  • entitu čokoláda
  • DAO objekt pre prístup k “databáze”
  • formulár JFrame pre GUI
  • konfiguráciu @Configuration pre Spring
  • a spúštaciu triedu s main()om.

Entita a DAO

Entita

Entita Chocolate bude jednoduchá: pamätá si názov čokolády a percentuálny obsah… kakaa.

package sk.upjs.ics.novotnyr.restlet;

public class Chocolate {
    private String title;

    private int percentage;

    /* 
        konštruktory,
        gettre a settre
        toString()
    */
}

DAO objekt

DAO objekt bude jednoduchý. Jediné, čo bude vedieť, je vrátiť zoznam čokolád, aj tie budú v pamäti, v zozname List.

Aby sme v ňom mali nejaké dáta, pripravíme vzorové chutné šťavnaté zelené, ehm, teda pripravíme čokolády.

package sk.upjs.ics.novotnyr.restlet;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import org.springframework.stereotype.Component;

@Component
public class ChocolateService  {
    private List<Chocolate> chocolates = new CopyOnWriteArrayList<Chocolate>();

    public ChocolateService() {
        chocolates.add(new Chocolate("Lindt Excellence 70%", 70));
        chocolates.add(new Chocolate("Milka Alpenmilch", 40));
        chocolates.add(new Chocolate("Christmas Angel Figure", 15));
    }

    public List<Chocolate> list() {
        return chocolates;
    }
}

Jediná špecialita je anotácia @Component nad triedou služby: to preto, aby ju Spring vedel nájsť pri automatickom hľadaní beanov v CLASSPATH.

Springácky kontext

Springácky kontext označený @Configuration poslúži na deklaráciu beanov v projekte:

package sk.upjs.ics.novotnyr.restlet;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.aspectj.EnableSpringConfigured;

@Configuration
@ComponentScan("sk.upjs.ics.novotnyr.restlet")
public class SpringApplicationContext {
    // vsetky triedy sa najdu automaticky
}

Explicitne nedeklarujeme žiadne beany, všetky sa nájdu v balíčku sk.upjs.ics.novotnyr.restlet. Teda… zatiaľ pôjde o všetky jeden bean, našu službu.

Ak by tutoriál predstavoval návod k automatickej registrácii beanov, mohol by sa skončiť. Ale my máme úplne iné ciele.

Používateľské rozhranie

GUI formulár vyzerá jednoducho:

Demonštračné GUI

package sk.upjs.ics.novotnyr.restlet;
import java.util.Vector;

import javax.swing.JFrame;
import javax.swing.JList;

public class MainFrame extends JFrame {

    private ChocolateService chocolateService;

    private JList<Chocolate> chocolateList; 

    public MainFrame() {
        chocolateList = new JList<Chocolate>();
        chocolateList.setListData(new Vector<Chocolate>(chocolateService.list()));
        add(chocolateList);

        setDefaultCloseOperation(EXIT_ON_CLOSE);
        pack();
    }
}

Spúšťať toto demo nemá zmysel: uvideli by sme NullPointerException, keďže chocolateService nebol nikdy nainicializovaný.

Napriek tomu sa oplatí mať kód, ktorý ukáže, ako naštartovať GUI:

Spúšťanie GUI

package sk.upjs.ics.novotnyr.restlet;

import javax.swing.SwingUtilities;

public class MainFrameRunner {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                MainFrame mainFrame = new MainFrame();
                mainFrame.setVisible(true);
            }
        });
    }
}

Samozrejme, mohli by sme celý main() rubnúť do triedy MainFrame, ale ako uvidíme neskôr, budú s tým divoké problémy.

Prepojenie GUI a Springu

Poďme drôtovať! Ak by sme boli na webe, bolo by to jednoduché: anotácia @Autowired zoberie bean zo springáckeho kontextu a prepojí ho inou triedou, čím ustanoví kolaboráciu oboch tried.

@Autowired
private ChocolateService chocolateService;

Ani toto však nebude fungovať!

Spring totiž vie drôtovať len závislosti objektov, ktoré má pod svojou správou. ChocolateService je pod správou Springu, pretože sa nachádza v aplikačnom kontexte (dostal sa doňho vďaka @ComponentScan).

Na druhej strane, formulár MainFrame nie je pod správou Springu, pretože nemá žiadne anotácie, nie je v kontexte explicitne deklarovaný a navyše sme jeho inštanciu vytvorili ručne cez new.

Inými slovami

Do objektu, ktorý bol vytvorený ručne cez new, Spring nedokáže natlačiť / nainjektovať / naautowireovať závislosti.

Ako z toho von?! AspectJ a Spring

Teda, prepáčte, dokáže. Ale chce to aspektovo orientované magické triky.

Spring bude musieť vedieť o ručnom vytváraní objektov, bude musieť vedieť odchytávať javy, keď sa vytvoria nové inštancie objektov, a niekde medzi požiadavkou na vytvorenie objektu a spustením konštruktora tohto objektu dynamicky dodá kód, ktorý naautowireuje do objektu závislosti.

Táto mágia, weaving (“votkanie kódu do objektu”) sa dá urobiť dokonca dvoma spôsobmi:

  • za pomoci upraveného kompilátora Java kódu (ajc, AspectJ compiler), ktorý dokáže zabezpečiť weaving
  • alebo za pomoci Java agenta, ktorý dokáže monitorovať nízkoúrovňové udalosti nad objektami (napr. “ide sa vytvoriť nový objekt príslušného typu”)

Zatiaľ si ukážeme prvú možnosť, tzv. compile time weaving.

Compile Time Weaving

Weaving prebehne v čase kompilácie nášho Java kódu a to pomocou upraveného Java kompilátora z projektu *AspectJ.

Budeme potrebovať:

  • deklarovať plug-in pre AspectJ kompilátor v pom.xml
  • deklarovať závislosť na module spring-aspects, ktorý sa postará o prepojenie AspectJ so Springom.
  • deklarovať niekoľko náhodných závislostí do pom.xml, pretože kompilátor má svoje muchy

Deklarácia AspectJ kompilátora v Mavene

Kompilátor pre AspectJ deklarujeme ako Maven plug-in:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.7</version>
    <configuration>
        <complianceLevel>1.6</complianceLevel>
        <aspectLibraries>
            <aspectLibrary>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aspects</artifactId>
            </aspectLibrary>
        </aspectLibraries>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
                <goal>test-compile</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Deklarujeme:

  • kompatibilitu s Javou 1.6
  • závislosť na Spring module spring-aspects (ten ihneď dodáme medzi projektové závislosti)
  • a zapneme weaving pre kompiláciu zdrojákov (compile) i testov (test-compile).

Závislosť na spring-aspects

Do pom.xml dodáme závislosť:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>4.1.2.RELEASE</version>
</dependency>

Náhodné závislosti

Žiaľ, kompilátor AspectJ má problém so závislosťami. Ak narazí v JAR súbore niektorej zo závislostí na triedu, ktorá sa síce nepoužíva, ale je importnutá, začne sa dožadovať JAR archívu, ktorý obsahuje importovanú triedu. (Túto “vlastnosť” je vidieť popísanú v dokumentácii AspectJ v sekcii cantFindType, prípadne na fórach projektu Spring Roo.

Modul spring-aspects tak závisí na spring-tx, a napriek tomu, že náš projekt transakcie nevyužíva, musíme túto závislosť kvôli kompilátoru dodať do projektu.

To isté sa týka ďalších závislostí. V sume musíme dodať:

<!-- Vysvetlenie na http://stackoverflow.com/a/9637279 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>4.1.2.RELEASE</version>
</dependency>

<!-- Vysvetlenie na http://bit.ly/1HvRC0X -->
<dependency>
    <groupId>org.hibernate.javax.persistence</groupId>
    <artifactId>hibernate-jpa-2.0-api</artifactId>
    <version>1.0.0.Final</version>
</dependency>

<!-- Požadovaná závislosť kvôli weavingu -->
<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
    <version>1.0.0</version>
</dependency>   

Podpora pre weaving v triedach

Zapnutie weavingu v triedach

Ak máme nachystanú infraštruktúru pre kompilovanie Java tried s podporou weavingu, poďme zapnúť podporu v MainFrame. Tento formulár chce mať autowirenuté závislosti i napriek tomu, že jeho životný cyklus a vytváranie riešime sami.

Použime na to anotáciu @Configurable, ktorou vieme vyhlásiť triedu za cieľ procesu weavingu a následného autowirovania závislostí.

Formulár bude vyzerať nasledovne:

@Configurable(preConstruction = true)
public class MainFrame extends JFrame {

Vlastnosť preConstruction

V anotácii deklarovali vlastnosť preConstruction. Tá hovorí, že závislosti budú autowirenuté ešte pred spustením konštruktora. To je situácia, ktorú potrebujeme, lebo už v konštruktore potrebujeme vidieť inštanciu čokoládovej služby ChocolateService.

Ak by sme preConstruction neuviedli, weaving by nemal veľký zmysel, pretože by sme stále získavali NullPointerException na chýbajúcej inštancii služby.

Namiesto preConstruction môžeme tiež inicializáciu komponentov presunúť do vhodnej metódy, napr. initComponents() (čo robí napríklad NetBeans), ale musíme sa postarať o to, aby sa táto metóda zavolala ručne. (Je zrejmé, že sa nesmie volať v konštruktore, lebo s vypnutým preConstruction nemáme v konštruktore ešte autowirenuté anotácie.)

@Configurable nesmie byť @Component

Dokumentácia Springu uvádza, že trieda s anotáciou @Configurable nesmie byť súčasne registrovaná v aplikačnom kontexte Springu. V opačnom prípade uvidíte duplicitnú inicializáciu konštruktora: raz kvôli kódu vo weavingu a druhýkrát kvôli inicializácii zavolanej Springom.

Zapnutie compile time weavingu v aplikačnom kontexte Springu

Posledný krok spočíva v zapnutí podpory pre anotácie @Configurable. Dosiahneme to dodaním anotácia @EnableSpringConfigured nad triedu @Configuration.

@Configuration
@EnableSpringConfigured
@ComponentScan("sk.upjs.ics.novotnyr.restlet")
public class SpringApplicationContext {

}

Spustenie aplikačného kontextu

Pri štarte aplikácie nesmieme zabudnúť na inicializáciu aplikačného kontextu Springu!

package sk.upjs.ics.novotnyr.restlet;

import javax.swing.SwingUtilities;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MainFrameRunner {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                AnnotationConfigApplicationContext context 
                    = new AnnotationConfigApplicationContext(SpringApplicationContext.class);

                MainFrame mainFrame = new MainFrame();
                mainFrame.setVisible(true);
            }
        });
    }
}

Kontext inicializujeme v event dispatch threade Swingu, aby nedošlo k náhlym race-conditions (nezabúdajme, že v hlavnom vlákne sa štartuje Swing, a v EDT vlákne spúšťa springácky kontext).

Spúšťanie výslednej aplikácie

Ak chceme spustiť hotovú aplikáciu, nesmieme zabúdať na to, že musíme triedy kompilovať AspectJ kompilátorom. Najjednoduchšia možnosť je dodať do pom.xml cieľ pre spúšťanie aplikácie cez plug-in exec-maven-plugin.

Dodajme deklaráciu:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>1.2.1</version>
    <executions>
        <execution>
            <id>default-cli</id>
            <!-- compile-time-weaving -->
            <goals>
                <goal>java</goal>
            </goals>
            <configuration>
                <mainClass>sk.upjs.ics.novotnyr.restlet.MainFrameRunner</mainClass>
            </configuration>
        </execution>
    </executions>
</plugin>

Aplikáciu potom spustíme cez

mvn clean compile exec:java

Prečo nemáme main() priamo v hlavnom okne? Pri weavingu pomocou Springu a @Configurable platí pravidlo, že triedy, ktoré sa nainicializujú, či vytvoria skôr, než sa spustí aplikačný kontext Springu, nebudú mať autowirenuté závislosti. Ak by sme mali main() v triede MainFrame, JVM načíta kód triedy skôr, než sa k nej dostane Spring. Preto musíme spúšťanie aplikácie vysunúť do samostatnej triedy.

Záver

  • využili sme compile-time weaving v Maven plug-ine
  • triedy pre weavovanie sme anotovali cez @Configurable
  • podporu pre ne sme dosiahli cez @EnableSpringConfired

Rozhodne sa oplatí si stiahnuť kompletný kód z GitHubu.

Nabudúce

V budúcom dieli si ukážeme podporu pre load-time weaving. Namiesto kompilovania tried cez AspectJ využijeme Java agenta!

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *