Gegeben sei der folgende Technologiestack:
– Java Frontend mit dem Standard Widget Toolkit (SWT), geladen und gestartet per Web Start.
– Spring Remoting als Schnittstelle zum Server.
– Spring Webanwendung auf einem Tomcat als Backend.
Das Backend ist Spring-Standard, und wem Spring Remoting nichts sagt, kann hier weiterlesen.
In diesem Blogeintrag soll es um die Kombination von SWT und Spring im Frontend gehen.
Was sind unsere Ziele? Im Prinzip läuft es auf Folgendes hinaus: die UI-Komponenten sollen möglichst dumm sein, da sie etwas schwerer zu testen sind. Die Businesslogik soll sich in einfachen POJOs befinden, mit denen echte Unit-Tests machbar sind. Das Komponentennetz soll durch Dependency Injection zusammengefügt werden, damit keine ServiceLocators die Testbarkeit behindern.
Ich hatte keine SWT-Erfahrung, also war mein erster naiver Gedanke: Okay, dann machen wir halt die UI-Komponenten zu Spring Beans, die Services sind auf Client-Seite Spring Remoting Proxies, und dazwischen ziehen wir eine Schicht von Controllern ein, die jegliche Businesslogik im Frontend übernehmen. Und Spring sorgt dafür, dass alle miteinander reden können.
So einfach war es dann nicht.
Erst einmal haben SWT-UI-Komponenten einen eigenen Lebenszyklus, sie können zu einem beliebigen Zeitpunkt erstellt und zerstört (dispose) und wieder erstellt werden. Der Scope Singleton wäre also in jedem Fall fehl am Platz gewesen, da man dann unter Umständen zerstörte, unbrauchbare UI-Komponenten im ApplicationContext gehabt und natürlich auch unnötig viel Speicher allokiert hätte.
Okay, na dann haben sie halt den Scope Prototype, und wir erzeugen uns eine neue Instanz immer dann, wenn wir sie brauchen.
Auch das ist einfacher gesagt als gemacht. Schauen wir uns doch mal so eine typische UI-Komponente an.
1public class SomeListView extends Composite
2{
3 private TabFolder tabFolder;
4 private SomeFilterComponent someFilterComponent;
5 private SomeSortableGrid grid;
6
7 public SomeListView(Composite parent)
8 {
9 super(parent, SWT.EMBEDDED);
10 setLayout(new GridLayout(1, false));
11 someFilterComponent = new SomeFilterComponent(this, SWT.NONE);
12 someFilterComponent.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
13
14 tabFolder = new TabFolder(this, SWT.NONE);
15 tabFolder.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
16
17 TabItem someListTab = new TabItem(tabFolder, SWT.NONE);
18 someListTab.setText("some list");
19
20 grid = new SomeSortableGrid(tabFolder, SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
21 grid.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
22 grid.setHeaderVisible(true);
23 grid.setSize(this.getSize().x, 400);
24 grid.setAutoHeight(true);
25 grid.setAutoWidth(true);
26
27 campaignListTab.setControl(grid);
28 }
29}
Diese Komponente stellt eine View mit einer Liste da, die durch eine Filterkomponente gefiltert werden kann. Die Liste ist noch in einem Tabreiter untergebracht. SomeFilterComponent und SomeSortableGrid sind wiederum eigens entwickelte UI-Komponenten. Man sieht, dass die Parent-Komponente immer als Konstruktor-Argument in die Child-Komponente übergeben wird. Wir haben hier eine bidirektionale Abhängigkeit, die Dependency Injection schwer macht: Wollte man SomeFilterComponent in SomeListView injizieren, so müsste vorher SomeListView erzeugt werden, damit es im Konstruktor von SomeFilterComponent verwendet werden kann. Das ginge noch mit Singletons, aber unmöglich wird es, wenn man bedenkt, dass beide Komponenten Prototypes sein müssen.
Fazit: SWT – UI-Komponenten können keine Spring-Beans werden.
Was nun? Doch wieder ServiceLocator-Aufrufe in den UI-Komponenten?
Nein, mit ein bisschen AspectJ-Magie gibt es eine elegante Lösung, die erstaunlich wenig Aufwand benötigt. Hier die drei Schritte zur Lösung:
1. Einbinden des Maven-AspectJ-Plugins für compile-time-weaving in unsere pom
2. Verwendung von @Configurable und @Autowired in unseren UI-Komponenten-Klassen
3. Aktivierung der Dependency Injection in der application-context.xml
Auf diese Art und Weise können wir Spring-Beans in ganz normale Klassen (unsere UI-Komponenten) injizieren, die per Konstruktor erzeugt werden.
Hier die Schritte im Einzelnen:
1. Einbinden des Maven-AspectJ-Plugins für compile-time-weaving in unsere pom
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.4</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
<complianceLevel>1.6</complianceLevel>
<Xlint>ignore</Xlint>
<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>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.6.11</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.6.11</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
Diese Konfiguration bewirkt, dass die Aspekte aus spring-aspects in die Klassen eingewoben werden. Interessant ist da für uns nur der AnnotationBeanConfigurerAspect, der bei jeder Klasse eingewoben wird, die mit @Configurable annotiert ist und der bewirkt, dass in diesen Klassen die @Autowired – Annotation verwendet werden kann, um Abhängigkeiten zu injizieren. Das führt uns direkt zum nächsten Schritt.
2. Verwendung von @Configurable und @Autowired in unseren UI-Komponenten-Klassen:
1@Configurable(preConstruction = true)
2public class SomeListView extends Composite
3{
4 private TabFolder tabFolder;
5 private SomeFilterComponent someFilterComponent;
6 private SomeSortableGrid grid;
7 @Autowired
8 private SomeController someController;
9
10 public SomeListView(Composite parent)
11 {
12 super(parent, SWT.EMBEDDED);
13 setLayout(new GridLayout(1, false));
14 someFilterComponent = new SomeFilterComponent(this, SWT.NONE);
15 someFilterComponent.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
16
17 tabFolder = new TabFolder(this, SWT.NONE);
18 tabFolder.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
19
20 TabItem someListTab = new TabItem(tabFolder, SWT.NONE);
21 someListTab.setText("some list");
22
23 grid = new SomeSortableGrid(tabFolder, SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
24 grid.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
25 grid.setHeaderVisible(true);
26 grid.setSize(this.getSize().x, 400);
27 grid.setAutoHeight(true);
28 grid.setAutoWidth(true);
29
30 campaignListTab.setControl(grid);
31 someController.doSomething();
32 }
33}
Durch @Configurable sagen wir AspectJ, dass der AnnotationBeanConfigurerAspect in dieser Klasse eingewoben werden soll. ‚preConstruction = true‘ ist eine Zusatzangabe, die dazu führt, dass Abhängigkeiten sogar vor dem Aufruf des Konstruktors gesetzt werden, deswegen kann SomeController auch schon im Konstruktor verwendet werden. Abhängigkeiten, die injiziert werden sollen, müssen mit @Autowired annotiert werden, wie das hier mit dem SomeController geschieht.
3. Aktivierung der Dependency Injection in der application-context.xml:
Dass der Aspekt eingewoben ist, heißt noch nicht, dass er aktiv werden kann. AspectJ-Aspekte sind üblicherweise static, also müssen wir unseren ApplicationContext static an einer Stelle bekannt machen, an der der Aspekt nachsehen kann. Das übernimmt Spring für uns transparent, in dem wir Folgendes in unsere ApplicationContext-xml-Datei einfügen:
<context:spring-configured/>
Damit das Autowiring per Annotationen funktioniert, brauchen wir auch noch folgenden Eintrag:
<context:annotation-config/>
Das war’s. Jetzt können wir SomeListView per Konstruktor erzeugen und die Abhängigkeiten werden direkt injiziert. Kein ServiceLocator, kein Gluecode. Alles, was irgendwie nach Businesslogik riecht, wird in einen Controller (oder wie man es auch immer nennen möchte) ausgelagert, der injiziert wird. Die UI-Komponenten bleiben so dumm wie möglich.
Nachtrag zur Testbarkeit
Wenn man sich den Code zur SomeListView noch einmal ansieht, merkt man schnell, dass dort immer ein Nullpointer fliegen wird, wenn man den Konstruktor aufruft ohne einen ApplicationContext entsprechend konfiguriert zu haben. Wir haben einfach akzeptiert, dass man SWT-UI-Komponenten in Standard-Unit-Tests nicht gut testen kann (das geht auch ohne Spring nicht wirklich, schließlich sind die Komponenten durch Konstruktorenaufrufe eng verwoben). Für den Test einer UI-Komponente nutzen für folgende Basis-Klasse:
1@ContextConfiguration("classpath:conf/some-config-test.xml")
2@RunWith(SpringJUnit4ClassRunner.class)
3@DirtiesContext
4public abstract class AbstractViewTest
5{
6 protected Shell shell;
7
8 @Autowired
9 protected SomeController someControllerMock;
10
11 @Before
12 public void setUp() throws Exception
13 {
14 assertNotNull(someControllerMock);
15
16 shell = new Shell(Display.getDefault(), SWT.NONE);
17 }
18}
Die some-config-test.xml sieht so aus:
<bean class="org.mockito.Mockito" factory-method="mock" >
<constructor-arg value="de.codecentric.client.controller.SomeController"/>
</bean>
<context:annotation-config/>
<context:spring-configured/>
Eine konkrete Testklasse kann dann so aussehen:
1public class SomeListViewTest extends AbstractViewTest
2{
3 private SomeListView someListView;
4
5 @Before
6 public void setUp() throws Exception
7 {
8 super.setUp();
9 SomeObject something = new SomeObject();
10
11 when(someControllerMock.doSomething()).thenReturn(something);
12 someListView = new SomeListView(shell);
13 }
14
15 @Test
16 public void testSomeListView()
17 {
18 Control[] children = someListView.getChildren();
19 assertEquals(2, children.length);
20
21 assertTrue(children[0] instanceof SomeFilterComponent);
22 assertTrue(children[1] instanceof TabFolder);
23
24 TabFolder tabFolder = (TabFolder) children[1];
25 Control[] tabFolderChildren = tabFolder.getChildren();
26 assertEquals(1, tabFolderChildren.length);
27 assertTrue(tabFolderChildren[0] instanceof SomeSortableGrid);
28
29 }
30}
Es wird also automatisch der someControllerMock in die someListView injiziert, wenn diese per Konstruktor erzeugt wird, und jegliche Prüfungen können dann auf dem Mock stattfinden.
Weitere Beiträge
von Tobias Flohre
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Tobias Flohre
Senior Software Developer
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.