Mariusz Prowaźnik

o programowaniu w Javie, Scali i Clojure.


Jak stworzyć archiwum jar wykonywalne z linii poleceń

Podczas zwykłego budowania projektu za pomocą polecenia mavena package w katalogu target powstaje archiwum jar, jednak nie można uruchomić go z linii poleceń, ponieważ nie ma określonej w pliku MANIFEST.MF klasy głównej oraz nie ma załączonych bibliotek zależnych. Próba uruchomienia skutkuje komunikatem:
Failed to load Main-Class manifest attribute from
something.jar

Istnieje bardzo proste rozwiązanie tego problemu - użycie wtyczek Mavena: maven-jar-plugin, maven-shade-plugin. Wystarczy do pliku pom.xml dodać taki kod:
<plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-jar-plugin</artifactId>
 <configuration>
  <archive>
   <manifest>
    <mainClass>mpbj.Main</mainClass>
   </manifest>
  </archive>
 </configuration>
</plugin>

<plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-shade-plugin</artifactId>
 <version>1.2</version>
 <executions>
  <execution>
   <phase>package</phase>
   <goals>
    <goal>shade</goal>
   </goals>
   <configuration>
    <artifactSet>
     <includes>
      <include>commons-collections:commons-collections</include>
      <include>commons-io:commons-io</include>
      <include>commons-cli:commons-cli</include>
     </includes>
    </artifactSet>
    <transformers>
     <transformer implementation="org.apache.maven.plugins.shade.resource.ComponentsXmlResourceTransformer" />
    </transformers>
   </configuration>
  </execution>
 </executions>
</plugin>
Przy czym należy podać odpowiednią nazwę klasy głównej, oraz w sekcji includes wstawić używane biblioteki zewnętrzne.

Apache Commons: Collections

W tym poście dowiesz się do czego i jak można użyć komponentu Collections, zawartego w Apache Commons.

Dobry programista, zanim zacznie rozwiązywać problem, sprawdzi, czy nie można użyć gotowego rozwiązania. A zwykle można. I naprawdę wiele z nich zostało zawartych w bibliotekach Apache Commons (dawniej Jakarta Commons).

Myślałeś kiedyś o tym, że przydałaby się klasa działająca jak HashMap przyjmująca kilka kluczy, albo zachowująca kolejność, iterator dla HashMap czy kolejka z priorytetem? Jeśli tak, to nie jesteś pierwszy - o tym samym pomyśleli autorzy komponentu Collections.

Dodanie zależności
Aby móc używać commons-collections dodaj do pom.xml zależność:
<dependency>
 <groupId>commons-collections</groupId>
 <artifactId>commons-collections</artifactId>
 <version>3.2.1</version>
</dependency>
A najlepiej wyszukaj tę zależność na http://mvnrepository.com bo możliwe, że teraz gdy to czytasz jest już wersja nowsza niż 3.2.1

Poniżej zaprezentuję wybrane klasy i interfejsy z commons-collection.
MultiKeyMap
Jest to mapa, w której kluczem może być od 1 do 5 zmiennych. Jako przykład posłużyć może mapa zawierająca oceny, w której kluczami są nazwa klasy i nr w dzienniku
MultiKeyMap mapaOcen = new MultiKeyMap();

  mapaOcen.put("IIc", 14, "bardzo dobry");
  mapaOcen.put("IIc", 13, "dobry");
  mapaOcen.put("Ic", 13, "dostateczny");
  mapaOcen.get("IIc", 14);
  mapaOcen.get("IIc", 13);
  mapaOcen.get("Ic", 13);
IterableMap, MapIterator
Przeiterowanie mapy z java.util nie jest tak wygodne jak w przypadku setów i list. Nie są dostępne zarówno konstrukcje typu:
HashSet<String> set = new HashSet<String>();
  for (String string : set) {
   System.out.println(string);
  }
jak i
HashSet<String> set = new HashSet<String>();
  while (set.iterator().hasNext())
   System.out.println(set.iterator().next());
Oczywiście, mapę można przeiterować po EntrySet:
HashMap<String, String> mapa = new HashMap<String, String>();
  for (Entry<String, String> entry : mapa.entrySet()) {
   entry.getKey();
   entry.getValue();
  }
(btw, iterowanie po keySet, by potem w pętli wyciągać elementy po kluczu jest błędem) Ale można również użyć odpowiednika z Commons Colections implementującego interfejs IterableMap - klasę HashedMap:
HashedMap mapa = new HashedMap();
  mapa.put("klucz", "wartosc");
  MapIterator iterator = mapa.mapIterator();
  while (iterator.hasNext()){
   String klucz = (String) iterator.next();
   String wartosc = (String) iterator.getValue();
  }
Inne klasy implementujące interfejs IterableMap to np: MultiKeyMap, IdentityMap, LinkedMap, LRUMap, TreeBidiMap
OrderedMap i OrderedMapIterator
Kolekcje implementujące ten interfejs charakteryzują się ustaloną kolejnością elementów, a dzięki OrderedMapIterator można iterować mapę w obie strony.
Zatem kolekcja LinkedMap przechowuje obiekty w takiej kolejności, w jakiej zostały dodane:
OrderedMap mapa = new LinkedMap();
  mapa.put("tomek", "wartosc1");
  mapa.put("ada", "wartosc2");
  OrderedMapIterator iterator = mapa.orderedMapIterator();
  while (iterator.hasNext()) {
   System.out.println((String) iterator.next() + " = "
     + (String) iterator.getValue());
  }
  while (iterator.hasPrevious()) {
   System.out.println((String) iterator.previous() + " = "
     + (String) iterator.getValue());
  
tomek = wartosc1
ada = wartosc2
ada = wartosc2
tomek = wartosc1
a klasa TreeBidiMap w kolejności posortowanej:
OrderedMap mapa = new TreeBidiMap();
  mapa.put("tomek", "wartosc1");
  mapa.put("ada", "wartosc2");
  OrderedMapIterator iterator = mapa.orderedMapIterator();
  while (iterator.hasNext()) {
   System.out.println((String) iterator.next() + " = "
     + (String) iterator.getValue());
  }
  while (iterator.hasPrevious()) {
   System.out.println((String) iterator.previous() + " = "
     + (String) iterator.getValue());
  
ada = wartosc2
tomek = wartosc
tomek = wartosc1
ada = wartosc2
Klucze używane w TreeBidiMap muszą implementować interfejs Comparable.
Kolekcja LinkedMap jest odpowiednikiem java.util.LinkedHashMap i w zasadzie różni się tym, że jest dostępny dla niej iterator. W java.util jest też kolekcja TreeMap w której elementy są posortowane i nawet ma jedną przewagę nad TreeBidiMap, tj przyjmuje w konstruktorze Comparator co pozwala na większą swobodę w określania sposobu porównywania elementów (można np zmienić sposób porównania String'ów), ale TreeBidiMap dysponuje inną ciekawą cechą, dzięki zaimplementowaniu interfejsu BidiMap
BidiMap
W mapach dwukierunkowych można nie tylko wyszukiwać wartość po kluczu, ale również wyszukać klucz po wartości, albo stworzyć mapę odwrotną - w której wartości stają się kluczami, a klucze wartościami:
BidiMap mapa = new TreeBidiMap();
  mapa.put("klucz", "wartosc");
  mapa.put("inny klucz", "inna wartosc");
  mapa.get("klucz");
  mapa.getKey("inna wartosc");
  mapa.inverseBidiMap();  
Jako, że wartości w TreeBidiMap są unikalne, włożenie do mapy elementu o istniejącej wartości nadpisze istniejący klucz:
BidiMap mapa = new TreeBidiMap();
  mapa.put("inny klucz", "inna wartosc");
  mapa.put("cos", "inna wartosc");
  System.out.println(mapa);
{cos=inna wartosc}
Bag
Bag to kolekcja z licznikiem wystąpień elementu. Podstawowe implementacje to HashBag i TreeBag. Przykład:
Bag bag = new HashBag();
  bag.add("element", 16); // dodaje 16 elementów
  bag.remove("element", 2); // zdejmu 2 elementy
  bag.getCount("element"); // zwraca licznik elementów, w tym przypadku 14

To oczywiście tylko kilka z wielu klas i interfejsów z Apache Commons. Więcej informacji znajdziesz tutaj: http://commons.apache.org/collections/api-release/index.html

Obsługa wyjątków: Finally

Słowo kluczowe finally stosujemy, gdy chcemy, by jakaś część kodu wykonała się niezależnie od tego, czy w bloku try-catch zostanie wyrzucony wyjątek. Zatem po wykonaniu takiego kodu:

public class Test {
 public static void main(String[] args) {
  try {
   String a = null;
   System.out.println(a.charAt(0));
  } catch (NullPointerException ex) {
   System.out.println("Null pointer!");
  } finally {
   System.out.println("Hello world!");
  }
 }
}
otrzymamy:
Null pointer!
Hello world!
Gdyby zmienić String a = null na String a ="arbuz" otrzymamy:
a
Hello world!
Wydawałoby się, że na tym można skończyć pisać o try-finally, jednak istnieje kilka ciekawych przypadków, o których do niedawna nie wiedziałem. Np. kod w bloku finally wykona się nawet jeśli w bloku try zostanie zamieszczona instrukcja kończąca wykonanie metody i zwracająca wartość return:
package com.blogspot.evojava;

public class Test {
 public static void main(String[] args) {
  try {
   return;
  } finally {
   System.out.println("Hello world!");
  }
 }
}
Wynik:
Hello world!
Podobnie jest w przypadku instrukcji typu break, continue:
package com.blogspot.evojava;

public class App {
 public static void main(String[] args) {
  for (int i = 0; i < 5; i++) {
   System.out.println("Hello World! " + i);
   try {
    break;
   } finally {
    continue;
   }
  }
 }
Hello World! 0
Hello World! 1
Hello World! 2
Hello World! 3
Hello World! 4
Jedynym sposobem, żeby "powstrzymać" finally jest zamknięcie JVM za pomocą System.exit(0):
package com.blogspot.evojava;

public class Test {
 public static void main(String[] args) {
  try {
   System.exit(0);
  } finally {
   System.out.println("Hello world!");
  }
 }

Należy pamiętać też o tym, że jeśli w bloku finally zostanie wyrzucony wyjątek, to dalsze przetwarzanie zostanie przerwane:
package com.blogspot.evojava;

public class Test {
 public static void main(String[] args) throws Exception {
  String str = null;
  try {
  } finally {
   str.compareTo("a");
   System.out.println(" world!");
  }
 }
Exception in thread "main" java.lang.NullPointerException
 at com.blogspot.evojava.Test.main(Test.java:8)
I pozostaje jeszcze inna ważna sprawa: niepoprawne skonstruowanie bloku try-finally może skutkować bardzo niebezpiecznym dla programu błędem. Przykład podany jest poniżej:
package com.blogspot.evojava;

public class Test {
 public static void main(String[] args) throws Exception {
  try {
   throw new Exception("Ważny wyjątek");
  } finally {
   throw new Exception("Nieistotny wyjątek");
  }
 }
}
Exception in thread "main" java.lang.Exception: Nieistotny wyjątek
 at com.blogspot.evojava.Test.main(Test.java:8)
W kodzie tym jeden z wyjątków w ogóle nie został obsłużony, ani nigdzie nie został przekazany. Mógłby być powodem złego działania programu, a programista w ogóle by się o nim nie dowiedział, bo wszelki ślad po wyjątku zaginął. W bardziej skomplikowanym kodzie taki błąd może być bardzo kosztowny i trudny do odnalezienia. Dlatego warto wiedzieć o takim przypadku.

Chrome i problem z JSF

Natknąłem się niedawno na problem związany z renderowaniem stron JSF przez Chrome. W przypadku innych przeglądarek (Opera, Firefox) wszystko było ok, ale w Chrome, mimo, że strona wyświetlała się prawidłowo, to nie działały wywołania akcji za pomocą commandButton:
<h:commandButton action="edytujDaneKonta" value="#{tekstInter.edytuj}"><f:setPropertyActionListener target="#{MOKStanSesji.wybraneKonto}" value="#{konto}" /></h:commandButton>

By rozwiązać problem wystarczy dodać <f:view contentType="text/html">

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core" >
    <f:view contentType="text/html">
  <!-- zawartość -->
    </f:view>
</html>

Java Puzzlers

Ostatnio wpadła w moje ręce bardzo interesująca książka: Java Puzzlers autorstwa Joshua Bloch, Neal Gafter. Opisuje ona ciekawe "kruczki" języka java w postaci 95 czyli krókich i pozornie prostych kawałków kodu. Do każdego z Puzzli, bo tak autorzy je nazywają, dołączony jest komentarz, w którym omawiają, dlaczego program zwraca zaskakujący i zupełnie inny wynik, niżby się wydawało po przeanalizowaniu działania kodu.

Co tu dużo mówić, czas na kilka przykładów:

Puzzle 3: Long Division

public static void main(String[] args) {
        final long MICROS_PER_DAY = 24 * 60 * 60 * 1000 * 1000;
        final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000;
        System.out.println(MICROS_PER_DAY / MILLIS_PER_DAY);
    }
Jaki będzie wynik działania programu? Na pierwszy rzut oka
1000
W rzeczywistości
5
Dzieje sie tak, ponieważ 24 * 60 * 60 * 1000 * 1000 składa się z wyrażeń typu int, więc wynik jest też typu int, a rzutowana do long jest dopiero podczas stworzenia stałej MICROS_PER_DAY. Ponieważ wartość 24 * 60 * 60 * 1000 * 1000 jest zbyt duża by być przechowana jako int, otrzymujemy błędny wynik.

Aby program zadziałał prawidłowo, należy go zmodyfikować w taki sposób:
public static void main(String[] args) {
        final long MICROS_PER_DAY = 24L * 60 * 60 * 1000 * 1000;
        final long MILLIS_PER_DAY = 24L * 60 * 60 * 1000;
        System.out.println(MICROS_PER_DAY / MILLIS_PER_DAY);
    }

Puzzle 20: What's My Class?

package com.javapuzzlers;
public class Me {
   public static void main(String[] args) {
      System.out.println(
         Me.class.getName().replaceAll(".", "/") + ".class");
   }

}
Najpierw zostaje pobrana nazwa klasy, czyli com.javapuzzlers.Me, a potem wydawałoby się, że następuje zamiana wszystkich łańcuchów "." na "\" i zostaje dodany łańcuch ".class". Spodziewany wynik: com/javapuzzlers/Me.class
Wynik rzeczywisty: ///////////////////.class

Winny temu jest fakt, że metoda String.replaceAll przyjmuje jako pierwszy parametr wyrażenie regularne, a wyrażenie regularne "." oznacza dowolny znak.
Jak powinien wyglądać kod, aby zwracał oczekiwany rezultat:
package com.javapuzzlers;
public class Me {
   public static void main(String[] args) {
      System.out.println(
         Me.class.getName().replaceAll("\\.", "/") + ".class");
   }
}
Albo jeszcze lepiej:
package com.javapuzzlers;
import java.util.regex.Pattern;
public class Me {
   public static void main(String[] args) {
      System.out.println(Me.class.getName().
          replaceAll(Pattern.quote("."), "/") + ".class");
    }
}

Puzzle 54: Null and Void

public class Null {
    public static void greet() {
        System.out.println("Hello world!");
    }
    public static void main(String[] args) {
        ((Null) null).greet();
    }
}
Gdy analizowałem ten kod, stwierdziłem, że powinien wyrzucić NullPointerException. No bo jak można wywołać metodę dla null? Jednak po wykonaniu, program wyprintuje "Hello world!".
Metoda greet() jest statyczna. Dlatego, podczas rzutowania (Null) null wartość null, jest ignorowana.

Puzzle 67: All Strung Out

public class StrungOut {

    public static void main(String[] args) {
        String s = new String("Hello world");
        System.out.println(s);
    }
}

class String {

    private final java.lang.String s;

    public String(java.lang.String s) {
        this.s = s;
    }
    public java.lang.String toString() {
        return s;
    }
}
Kod, choć dziwny, wydawałoby się, że zadziała i wyrzuci Hello world. Jednak nie zadziała:
Exception in thread "main" java.lang.NoSuchMethodError: main
Dziwne, no przecież jest tam metoda main. Ale jednak nie ma, bo oczekiwana jest metoda main przyjmująca jako parametr tablicę typu java.lang.String, a ta tutaj przyjmuje tablicę obiektów typu stworzonego przez nas.

Eclipse - podstawowa konfiguracja

Zainstalowanie Eclipse i skonfigurowanie środowiska pracy nie jest trudne. Jednak warto poświęcić temu nieco więcej uwagi, by zwiększyć w ten sposób możliwości, które nam oferuje Eclipse i sprawniej go wykorzystywać.

1. Ściągnięcie JDK, Eclipse i Maven

Odnalezienie odpowiednich plików za pomocą google jest banalnie proste, ale dla wygody podam linki:
Ściągnięte Eclipse i Maven wystarczy rozpakować.

2. Ustawienie zmiennych systemowych

Ścieżki w którym znajduje się JDK i Maven powinny zostać dodane do PATH, aby za każdym razem, gdy wydamy pod konsolą polecenie np. java czy mvn, system przeszukał podane ścieżki i odnalazł odpowiedni plik. Ustawienie zmiennych systemowych w Windows 7:
  1. naciskamy Windows Key + Pause, pojawia się Panel sterowania\System i zabezpieczenia\System
  2. klikamy zaawansowane ustawienia systemu, zakładka zaawansowane, przycisk zmienne środowiskowe
  3. powinno otworzyć się takie okno:

    Dodajemy zmienne JAVA_HOME i MAVEN_HOME, zawierające odpowiednio ścieżkę do katalogu javy i mavena.
  4. Na koniec należy odszukać zmienną PATH i zmodyfikować jej zawartość, dodając na końcu %JAVA_HOME%\bin;%MAVEN_HOME%\bin

Aby sprawdzić, czy wszystko działa, otwieramy konsolę i wpisujemy:
java -version
Polecenie to powinno zwrócić wersję javy, np:
java version "1.6.0_20"
Java(TM) SE Runtime Environment (build 1.6.0_20-b02)
Java HotSpot(TM) Client VM (build 16.3-b01, mixed mode, sharing)
Gdy to zadziała, sprawdzamy:
mvn -v
co powinno zwrócić coś w stylu:
Apache Maven 2.2.1 (r801777; 2009-08-06 21:16:01+0200)
Java version: 1.6.0_16
Java home: C:\Program Files\Java\jdk1.6.0_16\jre

3. JDK

Mimo, że skompilowanie kodów źródłowych i zbudowanie projektu załatwia za nas IDE, dobrze jest wiedzieć co się dzieje "pod spodem", a dokładnie, jakie narzędzia wchodzą w skład Java Development Kit i jak ich używać. Są to między innymi:
  • java - interpreter javy, umożliwia uruchomienie skompilowanych programów
  • javac - kompilator javy
  • jar - narzędzie do tworzenia, modyfikacji, rozpakowywania archiwów jar
  • javah - generator nagłówków C, używany np gdy jest potrzeba stworzenia kodu Java i C, który by współpracował ze sobą
  • jdb - debugger javy
  • javadoc - generator dokumentów javy
  • javap - disassembler javy
Dzięki tym narzędziom, można napisać kod java w notatniku, a potem skompilować i uruchomić go z lini poleceń. Jak to wygląda w praktyce pokażę na prostej klasie :
package helloworld;

public class Main {
 public static void main(String[] args) {
  System.out.println("hello world");
 }
}
Kod źródłowy znajduje się w katalogu src\helloworld\, skompilujemy go do bin/helloworld.
javac src\helloworld\Main.java -d bin
-d określa docelowy katalog dla skompilowanych klas. Gdybyśmy korzystali z jakichś zewnętrznych bibliotek, trzeba by było dodać je do classpath (określa gdzie JVM ma szukać klas i paczek).
javac src\helloworld\Main.java -classpath lib/jakislib.jar -d bin
Uruchamiamy program za pomocą:
cd bin
java helloworld.Main
lub
java -cp bin helloworld.Main
gdzie poprzez -cp określamy classpath. Uruchamiając kod, trzeba dodać do classpath użyte biblioteki.

Skompilowane klasy zwykle pakuje się do archiwum jar. Przytoczę tutaj definicję z Wikipedii:
JAR (ang. Java ARchive) – archiwum ZIP używane do strukturalizacji i kompresji plików klas języka Java oraz powiązanych z nimi metadanych.

Archiwum JAR składa się z pliku manifestu umieszczonego w ścieżce META-INF/MANIFEST.MF, który informuje o sposobie użycia i przeznaczeniu archiwum. Archiwum JAR, o ile posiada wyszczególnioną klasę główną, może stanowić osobną aplikację.
Aby stworzyć archiwum jar, musimy najpierw stworzyć plik MANIFEST.MF, w którym określimy główną klasę
Main-Class: helloworld.Main
potem używamy narzędzia jar:
jar cvfm main.jar MANIFEST.MF -C bin \
to powinno zwrócić wynik podobny do tego:
added manifest
adding: helloworld/(in = 0) (out= 0)(stored 0%)
adding: helloworld/Main.class(in = 424) (out= 290)(deflated 31%)
Następnie:
java -jar main.jar
co powinno zwrócić
hello world

4. Maven

Z Wikipedii:
Apache Maven jest narzędziem automatyzującym budowę oprogramowania na platformę Java. Poszczególne funkcjonalności Mavena realizowane są poprzez wtyczki, które są automatycznie pobierane przy ich pierwszym wykorzystaniu. Plik określający sposób budowy aplikacji nosi nazwę POM-u (ang. Project Object Model).

Po co używać Mavena? Bardzo wiele ułatwia. Dzięki niemu na przykład nie trzeba ręcznie ściągać bibliotek używanych w projekcie, wiele procesów związanych budowaniem i uruchamianiem projektów jest zautomatyzowana, dostępna jest duża ilość archetypów - które dostarczają podstawową strukturę dla projektów różnego typu. Cała konfiguracja zawarta jest w jednym pliku - pom.xml.

Dobry opis tego, jak można używać Mavena zająłby wiele więcej niż cały ten post, dlatego zawartość tego punktu należy traktować raczej jako prosty przykład jego zastosowania.

Aby stworzyć projekt, wydajemy pod konsolą polecenie
mvn archetype:generate
Maven zapyta wtedy o dodatkowe parametry:
  • nr archetypu - z wyświetlonej listy, można sobie wybrać rodzaj projektu, który chcemy stworzyć - wybieramy domyślny
  • version - wybieramy najnowszą
  • groupId - wymyślamy nazwę grupy dla projektu np. com.blogspot.evojava
  • artifactId - wymyślamy nazwę dla projektu np. testApp
  • version - podajemy nr wersji dla projektu, można domyślną
  • package - nazwa paczki dla kodu, zostawiamy domyślną, czyli nazwę grupy
Zatwierdzamy i projekt został utworzony. Teraz, aby można było go otworzyć za pomocą Eclipse, wchodzimy do katalogu projektu i wydajemy polecenie:
mvn eclipse:eclipse

Pierwsze wykonanie tych poleceń może trochę potrwać, ze względu na to, że maven potrzebuje ściągnąć kilka potrzebnych zależności. Przy kolejnym wywołaniu tych poleceń już tego robić nie będzie.

Otwieramy projekt w Eclipse poprzez zaimportowanie go do workspace (File -> Import -> General -> Existing Project Into Workspace). W projekcie mamy następujące pliki:


Jak widać został stowrzony katalog src/main/java, a w nim przykładową klasę, oraz katalog src/test/java z przykładową klasą testów jednostkowych. I co najważniejsze, plik
pom.xml.:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.blogspot.evojava</groupId>
  <artifactId>testApp</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>testApp</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>
W sekcji dependencies są zależności - biblioteki wymagane do działania programu. W tym przypadku jest to junit, bez której nie będą działać testy jednostkowe. W tym momencie Eclipse sygnalizuje czerwonym wykrzyknikiem brak bibliotek, ale podczas pierwszego budowania projektu maven ściągnie z repozytoriów zależne biblioteki.

Jednak, żeby ściągnięte przez Mavena biblioteki były widziane przez Eclipse, trzeba dodać Classpath Variable:
  1. Window -> Preferences -> Java -> Build Path -> Classpath Variables
  2. New ->
    Name: M2_REPO
    Path: {UserDir}/.m2/repository
Jak zbudować projekt? Na przykład poleceniem
mvn package
Jeżeli wszystko przebiegnie prawidłowo, na konsoli wyświetli się komunikat podobny do tego:
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 8 seconds
[INFO] Finished at: Wed Aug 18 18:49:18 CEST 2010
[INFO] Final Memory: 8M/17M
[INFO] ------------------------------------------------------------------------
package jest jednym z celów mavena. W głównym cyklu zawiera się 8 celów, które wykonywane są jeden po drugim. Aby przejść do kolejnego, wywołanie poprzedniego musi zakończyć się sukcesem.
Z wikipedii:
  • validate - sprawdzenie, czy projekt jest poprawny i czy wszystkie niezbędne informacje zostały określone
  • compile - kod źródłowy jest kompilowany
  • test - przeprowadzane są testy jednostkowe
  • package - budowana jest paczka dystrybucyjna
  • integration-test - zbudowany projekt umieszczany jest w środowisku testowym, gdzie przeprowadzane są testy integracyjne
  • verify - sprawdzenie, czy paczka jest poprawna
  • install - paczka umieszczana jest w repozytorium lokalnym - może być używana przez inne projekty jako zależność
  • deploy - paczka umieszczana jest w repozytorium zdalnym (opublikowana)
Wywołanie któregoś z celów powoduje wywołanie również realizację wcześniejszych. Zatem po użyciu mvn package spowoduje ściągnięcie zależności (w tym przypadku biblioteki junit) i umieszczenie ich w repozytorium lokalnym mavena (znajduje się w {UserDir}/.m2/repository), skompilowanie klas, uruchomienie testów jednostkowych i stowrzenie pliku jar w katalogu target.

Aby zmiany zostały zauważone w eclipse, należy ponownie wywołać polecenie
mvn eclipse:eclipse
i odświeżyć projekt (F5).


W folderze target pojawił się między innymi plik .jar i raporty, zniknął czerwony wykrzyknik oznaczający brak wymaganych bibliotek. Można teraz uruchomić projekt (Alt+Shift+X,J).

Próba uruchomienia projektu bez wcześniejszego zbudowania spowodowałaby wyrzucenie wyjątku przez Classloader'a, który nie był w stanie znaleźć skompilowanych klas:
java.lang.NoClassDefFoundError: com/blogspot/evojava/App
Caused by: java.lang.ClassNotFoundException: com.blogspot.evojava.App
 at java.net.URLClassLoader$1.run(Unknown Source)
 at java.security.AccessController.doPrivileged(Native Method)
 at java.net.URLClassLoader.findClass(Unknown Source)
 at java.lang.ClassLoader.loadClass(Unknown Source)
 at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
 at java.lang.ClassLoader.loadClass(Unknown Source)
Exception in thread "main" 

Aby wyczyścić wygenerowane przez mavena pliki i katalogi podczas budowania należy użyć
mvn clean
Najczęściej łączy się te polecenia w jeden ciąg, np:
mvn clean package eclipse:eclipse

5. Instalacja wtyczek

Przyda nam się również kilka wtyczek, które rozszerzą funkcjonalność Eclipse. W przypadku systemów Windows 7 i Vista z włączonym UAC, mogą wystąpić pewne problemy przy instalacji wtyczek. To znaczy wtyczka wydaje się instalować poprawnie, ale jej nie ma. Aby temu zapobiec, na potrzeby instalacji dodatków należy uruchomić Eclipse jako administrator.

Kolejna rzecz, eclipse domyślnie używa jre zamiast jdk i zalecam to zmienić:
  1. Wchodzimy do Window->Preferences->Java->Installed JRE's
  2. klikamy Add->Standard VM.
  3. klikamy directory i wyszukujemy katalog w którym znajduje się JDK


  4. klikamy Finish i zaznaczamy dodane jre
  5. dodaj do pliku eclipse.ini (katalog eclipse) przed -vmargs linijkę:
    -vm
    C:\Program Files (x86)\Java\jdk1.6.0_20\bin\javaw.exe
    podając oczywiście własny katalog do javy
5.a. Subclipse
Subclipse umożliwia korzystanie repozytoriów SVN za pomocą Eclipse. Instalacja:
  1. Help -> Install New Software
  2. w pole Work With wklej: http://subclipse.tigris.org/update_1.6.x
  3. next -> next -> finish
  4. zrestartuj eclipse
  5. sprawdź czy w Window->Preferences->Team znajduje się sekcja SVN, jeśli tak, to wtyczka jest zainstalowana
Teraz wystarczy wybrać Window->Show View->Other->SVN->SVN Repositories i otwiera się okno, w którym można zarządzać repozytoriami. Wygodnie commit'uje i update'uje się przy użyciu perspektywy Team Synchronizing (Window->Open Perspective->Other->Team Synchronizing)

5.b. m2eclipse
Wtyczka ta umożliwia integracje Mavena z Eclipse. Nie jest ona pozbawiona wad, jednak może się przydać.
Instalacja wtyczki:
  1. Help -> Install New Software
  2. w pole Work With wklej: http://m2eclipse.sonatype.org/sites/m2e
  3. next -> next -> finish
  4. zrestartuj eclipse
  5. gdy już się zainstaluje, podaj ścieżkę katalogu w którym znajduje się Maven: Window -> Preferences -> Maven -> Instalations -> Add. Jeśli nie ma sekcji Maven w preferencjach, to znaczy że wystąpiły jakieś problemy przy instalacji wtyczki.
Teraz po kliknięciu prawym przyciskiem myszy na projekcie i wybierając z menu kontekstowego Run As, zobaczymy opcje dodane przez m2eclipse:

W menu tworzenia nowego projektu została dodana nowa sekcja:

6. Widoki i perspektywy

Dowolny widok można otworzyć wybierając Window->Show View. Dostępne są tam zarówno podstawowe widoki takie jak Console, czy Package Explorer (warto to wiedzieć chociażby na wypadek gdybyśmy przez przypadek zamknęli któryś z tych widoków), jak i takie które się używa rzadziej. Nie będę opisywał tutaj ich, najlepiej samemu sobie je zobaczyć.

Perspektywy zawierają predefiniowany układ widoków zorientowany na jakieś konkretne zadanie. Można się między nimi szybko przełączać, np. pisząc kod używamy perspektywy Java, postanawiamy zrobić commit na repozytorium, więc przełączamy się na perspektywę Team Synchronizing, a gdy już skończymy synchronizować kod, wracamy do perspektywy Java i dalej kodujemy.
Wybrać perspektywę możemy wybierając w Window->Open Perspective, lub wybierając z toolbar'a w prawym górnym rogu:

Do najczęściej przeze mnie używanych perspektyw należą: Java, Java EE, Team Synchronizing, Debug.

7. Debugger

Nieodłącznym elementem pracy programisty jest wyszukiwanie błędów i naprawianie ich. W pewnym momencie trzeba przestać kodować i zacząć badać co się dzieje w programie i dlaczego nie działa on tak jak powinien. Kiedyś w tym celu modyfikowałem kod tak, by wyrzucał mi na konsolę wartości zmiennych. Teraz wiem, że lepiej użyć do tego narzędzia do debuggowania. Krótki opis tego, jak go używać, zamieszczam tutaj.
Klikając dwukrotnie na lewo od kodu, można wstawić Breakpoint. Określi on miejsce, gdzie podczas Debugowania wykonanie programu się zatrzyma i będzie można podejrzeć np wartości zmiennych:
powyżej wstawione są dwa breakpointy na lini 8 i 10. Teraz wystarczy uruchomic debugowanie:


Eclipse automatycznie przełączy na perspektywę Debug:
W widoku Variables można podejrzeć wartości zmiennych. Wznowić wykonywanie programu można za pomocą zaznaczonego toolbar'a, a za pomocą Step Over, i Step Into z menu Run można wykonywać program krokowo, linia po linii.

8. Skróty klawiaturowe

Beż używania skrótów klawiaturowych korzystanie z Eclipse byłoby o wiele bardziej czasochłonne. Zamieszczam tutaj ściągawkę z tymi, które najczęściej używam i które są według mnie najbardziej przydatne.
  • Ctr+Spacja - Content Assist - wyświetla podpowiedzi nazw zmiennych, nazw metod, parametrów metod.
  • Ctr+Shift+O - Organize Imports - wstawia potrzebne importy, usuwa zbędne
  • Ctr+Shift+F - Format - formatuje ładnie kod
  • Shift+Alt+S - Source - wyświetla menu, z którego można wybrać wygenerowanie np setterów i getterów, konstruktorów, toStringów itp.
  • Ctr+/ - Toggle comment - komentuje / odkomentowuje zaznaczone linie
  • Alt+Shift+X - po użyciu tego skrótu z lewej u dołu wyskakuje okienko z kolejnymi skrótami uruchamiającymi w różny sposób projekt
  • Ctr+M - Maximize Active View - maksymalizuje okno
  • Ctr+1 - Quick Fix - wyświetla menu z proponowanymi rozwiązaniami jakiegoś błedu
  • Ctr+D - Delete Line - usuwa linię
  • Alt+dół - Move lines down - przenosi linię w dół, można tak samo w górę
  • Ctr+Alt+dół - Copy Lines - kopiuje linie
  • Ctr+T - Quick Hierarchy - umożliwia szybkie przejście do klas dziedziczących lub implementujących zaznaczony element
  • Ctr+Shift+T - Open Type - wyświetla klasę lub interfejs o wpisanej nazwie. Bardzo użyteczne gdy mamy wiele klas w projekcie
  • Ctr+Alt+H - Call Hierarchy - wyświetla wywołania danej metody w innych klasach
  • F3 - Open Declaration - otwiera deklarację
  • F4 - Open Type Hierarchy - działanie takie jak Quick Hierarchy, tylko wyświetla wyniki nowym oknie
  • Ctr+wskazanie myszką - wyświetla się menu, z którego możemy przejść do implementacji lub deklaracji metody lub klasy
  • Alt+Shift+R - Rename - zmienia nazwę metody, zmiennej, czy klasy, uwzględniając wszystkie odwołania do niej
Oczywiście można w ustawieniach (Window->Preferences->General->Keys) zmienić przypisania skrótów klawiaturowych. Np. polecam znaleźć polecenie "Next Editor" i zmienić na Ctr+Tab, bo domyślny skrót nie jest zbyt wygodny.

To tyle jeśli chodzi o podstawowe kwestie wokół środowiska Eclipse. Mając tę wiedzę, tworzenie nieskomplikowanych projektów nie powinno sprawiać problemów :)

Wzorce projektowe - Builder

Builder jest wzorcem konstrukcyjnym - to znaczy, że jego zadaniem jest stworzenie i konfiguracja obiektu. Jego przydatność jest na tyle duża, że został mu poświęcony podrozdział książki "Effective Java 2nd edition".

Jak i kiedy zaimplementować ten wzorzec? Załóżmy, że napisaliśmy klasę o dużej ilości pól. Przykładowo:
public class Pojazd {
 private String marka;
 private Integer kola;
 private Integer poduszkiPowietrzne;
 private Integer predkoscMax;
 private String rejestracja;
//...
}
Konstruktor dla takiej klasy musiałby przyjmować dużo parametrów, w przypadku większych klas jego deklaracja byłaby bardzo długa i używanie go często skutkowałoby pomyłkami.
public Pojazd(String marka, String rejestracja, Integer kola,
   Integer poduszkiPowietrzne, Integer predkoscMax) {
  this.marka = marka;
  this.rejestracja = rejestracja;
  this.kola = kola;
  this.poduszkiPowietrzne = poduszkiPowietrzne;
  this.predkoscMax = predkoscMax;
 }
}
Pojazd pojazd = new Pojazd("marka", "rejestracja", 4, 2, 100);}
A co gdy nie wszystkie pola są wymagane? Trzeba by wtedy stworzyć różne konstruktory, albo przekazywać nulle jako parametry.
public Pojazd(String marka, String rejestracja, Integer kola,
   Integer poduszkiPowietrzne, Integer predkoscMax) {
  // ...
 }

 public Pojazd(String marka, String rejestracja, Integer kola,
   Integer poduszkiPowietrzne) {
  // ...
 }

 public Pojazd(String marka, String rejestracja, Integer kola) {
  // ...
 }

 public Pojazd(String marka, String rejestracja) {
  // ...
 }

Właśnie w takich sytuacjach warto użyć wzorca Builder. Do klasy Pojazd dodajemy wewnętrzną klasę statyczną Builder, zawierającą metody o nazwach odpowiadających nazwom pól klasy Pojazd, oraz metodę build(), która będzie tworzyć obiekt klasy Pojazd. Ten sam kod będzie wyglądać w ten sposób:
public class Pojazd {
 private String marka;
 private String rejestracja;
 private Integer kola;
 private Integer poduszkiPowietrzne;
 private Integer predkoscMax;

 public static class Builder {
  // parametr obowiązkowy
  private final Integer kola;

  // parametry opcjonalne
  private String marka = null;
  private String rejestracja = null;
  private Integer poduszkiPowietrzne = null;
  private Integer predkoscMax = null;

  public Builder(Integer kola) {
   this.kola = kola;
  }

  public Builder marka(String marka) {
   this.marka = marka;
   return this;
  }

  public Builder rejestracja(String rejestracja) {
   this.rejestracja = rejestracja;
   return this;
  }

  public Builder poduszkiPowietrzne(Integer poduszkiPowietrzne) {
   this.poduszkiPowietrzne = poduszkiPowietrzne;
   return this;
  }

  public Builder predkoscMax(Integer predkoscMax) {
   this.predkoscMax = predkoscMax;
   return this;
  }

  public Pojazd build() {
   return new Pojazd(this);
  }
 }

 private Pojazd(Builder builder) {
  this.marka = builder.marka;
  this.rejestracja = builder.rejestracja;
  this.kola = builder.kola;
  this.poduszkiPowietrzne = builder.poduszkiPowietrzne;
  this.predkoscMax = builder.predkoscMax;
 }
}
Stworzenie obiektu:
Pojazd pojazd = new Pojazd.Builder(4).marka("marka")
    .poduszkiPowietrzne(4).predkoscMax(300)
    .rejestracja("rejestracja").build();
Parametry obowiązkowe są przekazywane jako parametry konstruktora, a parametry opcjonalne jako parametry jednoargumentowych metod. Co ważne, metody te zwracają this, przez co możliwe jest używanie ich "w ciągu". Wywołanie metody build tworzy obiekt klasy Pojazd z ustawionymi polami.

Uruchamianie testów JMeter'a poleceniem Maven'a

JMeter jest narzędziem umożliwiającym testowanie wydajności aplikacji. Szukałem sposobu na zautomatyzowanie jego użycia, natrafiając przy tym na informacje o różnych pluginach mavena, np jmeter-maven-plugin, chronos-maven-plugin, niestety nie udało mi się zmusić je do tego by działały tak jak bym chciał.

Efekt najbliższy oczekiwanemu uzyskałem wykorzystując exec-maven-plugin i konfigurując go tak, aby uruchomił zewnętrznego JMeter'a.

1. Instalacja JMeter'a
Ponieważ sposób ten wykorzystuje zewnętrznego JMeter'a, musimy go najpierw zainstalować. Wystarczy go ściągnąć i rozpakować.

2. Modyfikacja pom.xml
W zależności od tego gdzie został zainstalowany JMeter, ustawiamy property ze ścieżką:
<properties>
  <jmeter.home>C:\Program Files (x86)\jakarta-jmeter-2.4</jmeter.home>
</properties>

I konfigurujemy plugin: (sekcja build->plugins)
   <plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>1.1</version>
    <configuration>
     <executable>java</executable>
     <arguments>
      <argument>-jar</argument>
      <argument>${jmeter.home}\bin\ApacheJMeter.jar</argument>
      <argument>-n</argument>
      <argument>-t</argument>
      <argument>src\test\resources\JmeterTest.jmx</argument>
      <argument>-l</argument>
      <argument>results/jtl/MyJmeterTestOutput.jtl</argument>
      <argument>-Duser.classpath</argument>
      <classpath>
       <dependency>${project.build.directory}\${project.build.finalName}\WEB-INF\classes</dependency>
      </classpath>
     </arguments>
    </configuration>
   </plugin>
Zamiast src\test\resources\JmeterTest.jmx podajemy ścieżkę do testu JMeter, w pliku results/jtl/MyJmeterTestOutput.jtl zostaną zamieszczone wyniki.

3. Uruchomienie testów
Testy uruchamiamy poleceniem
mvn exec:exec


Deploy na Tomcat'a za pomocą pluginu Maven'a

Wykorzystujac plugin Mavena tak, da się wdrożyć projekt na serwer Tomcat za pomocą jednego polecenia. W tym celu należy skonfigurować trzy pliki:
  1. tomcat_users.xml
  2. settings.xml (w katalogu mavena)
  3. pom.xml
1. tomcat_users.xml
Plik ten znajduje się w {TomcatDir}/conf 
Jego konfiguracja zależy od wersji zainstalowanego tomcat'a. Polega ona na dodaniu użytkownika, oraz roli na potrzeby plugina maven'a.

Tak, więc dla Tomcata 6 będzie to dodanie wpisów (chyba, że już tam są):
<role rolename="manager"/>
<user username="test" password="test" roles="manager"/>
a w przypadku Tomcata 7:
<role rolename="manager"/>
<role rolename="manager-script"/>

<user username="test" password="test" roles="manager,manager-script"/>
2. settings.xml
Plik ten znajduje się w {MavenDir}/conf
W sekcji servers dodajemy serwer:
<server>

<id>tomcat_local</id>
      <username>test</username>
      <password>test</password>
<server/>
Można pominąć konfigurowanie tego pliku, jeśli poda się informacje o użytkowniku i haśle w pom.xml
3. pom.xml
Do pliku pom.xml należy dodać konfigurację pluginu. Do sekcji build/plugins dopisujemy:

<plugin>   <artifactId>tomcat-maven-plugin</artifactId>
<configuration>
</configuration>
</plugin>

Zawartość configuration zależy od wersji Tomcata. W wersji 6:
<url>http://localhost:8080/manager</url>
W wersji 7:
<url>http://localhost:8080/manager/text</url>
Podajemy oczywiście adres i port serwera na który chcemy wdrożyć projekt.
Poza tym, jeśli skonfigurowaliśmy settings.xml, to do configuration dodajemy
<server>tomcat_local</server>
jeśli nie, to
<username>test</username>
<password>test</password>

Przykładowa konfiguracja:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>tomcat-maven-plugin</artifactId>
  <configuration>
         <url>http://localhost:8080/manager/text</url>
 <username>test</username>
   <password>test</password>
    </configuration>
</plugin>

Teraz wystarczy uruchomić serwer Tomcat, a projekt (po wcześniejszym wejściu do jego folderu) wdrażamy poleceniem:
mvn tomcat:deploy
lub (gdy projekt jest już wdrożony)
mvn tomcat:redeploy

Walidacja XML po XML Schema w Javie

Jest wiele sposobów w Javie, aby sprawdzić poprawność dokumentu XML pod kątem zgodności z XML. Schema. Jeden z nich zaprezentuję poniżej, wykorzystując biblioteki javax.xml.

Założyłem, że XML Schema będzie trzymana w pliku, a XML do walidacji przekazywany jako parametr typu String. Zatem klasa będzie takiej postaci:

public SchemaValidator{
           private static final String XSD_FILE = "someSchema.xsd"; 
           public boolean validate(String xml) throws ValidationException{
           //implementacja
          }
}
ValidationException to wyjątek który powinniśmy stworzyć sami, rozszerzając klasę Exception. Posłuży on do opakowania wyjątków związanych np z brakiem dostępu do pliku z XML Schema.

Czas na implementację metody validate. Na początek tworzymy SchemaFactory, oraz Schema:
SchemaFactory factory = 
       SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
URL resource = this.getClass().getClassLoader().getResource(XSD_FILE);
Schema schema;
try {
         schema = factory.newSchema(new File(resource.toURI()));
} catch (Exception e) {
         throw new ValidationException(e);
}

Należy zwrócić uwagę na linijkę:
URL resource = this.getClass().getClassLoader().getResource(XSD_FILE)

Plik schema powinien być wcześniej umieszczony w folderze src/main/resourcesWykonanie tego kodu powoduje pobranie go jako zasób. Można to zrobić oczywiście w inny sposób - w XSD_FILE zamieścić ścieżkę pliku (np  src/main/resources/someSchema.xsd) i wtedy zmienić parametry wywołania newSchema na
schema = factory.newSchema(new File(XSD_FILE));
Kolejnym krokiem jest:
Validator validator = schema.newValidator();
       Source source = new StreamSource(new ByteArrayInputStream(xml.getBytes()));

Konstruktor klasy StreamSource przyjmuje parametr typu InputStream, a dysponujemy xml'em String'u. Użycie metody getBytes() i stworzenie obiektu  ByteArrayInputStream rozwiązuje ten problem.

Na koniec walidujemy source. Jeśli xml się nie zwaliduje, wyrzucony zostanie wyjątek SAXException.
try {
   validator.validate(source);
   System.out.println("XML Validation: OK");
  } catch (SAXException ex) {
   System.out.println("XML is not valid "+ex.getMessage());
   return false;
  } catch (IOException e) {
   throw new ValidationException(e);
  }
  
  return true;