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:
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ýmpreConstruction
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 malimain()
v triedeMainFrame
, 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!