Kilka razy (… ok może kilkanaście ;) ) zdarzyło mi się napisać klasy reprezentujące encje Hibernate i zapomnieć dopisać je do XML’a z mappingiem. Te niedopatrzenie wychodzi na jaw w momencie deploy’u aplikacji. No ale żeby wiedzieć co jest grane trzeba zajrzeć do logów. Wiadomo nie zajmuje to może jakoś specjalnie wiele czasu ale na pewno rozbija rytm pracy i nie potrzebnie zmusza do przeszukiwania logów.
Plan jest taki: Piszemy jUnitowy test, który sprawdzi czy czasem nie zapomnieliśmy o umieszczeniu wpisu z mappingiem w XML’u dla którejś z encji.
Każda encja jest oznaczona adnotacją @Entity (jeśli używamy adnotacji oczywiście), więc najpierw musimy znaleźć wszystkie klasy oznaczone tą adnotacją, a potem sprawdzić czy odpowiedni wpis znajduje się w pliku XML.
Aby znaleźć wszystkie klasy oznaczone @Entity najlepszym rozwiązaniem wydaje się użycie mechanizmu refleksji. Ja poszedłem na łatwiznę i wykorzystałem bibliotekę Reflections – która dostarcza wysokopoziomowego API.
OK w takim razie w jaki sposób wyszukać wszystkie klasy adnotowane @Entity ?
Set> set = new Reflections(pakiet).getTypesAnnotatedWith(Entity.class); |
W ten sposób zyskujemy dostęp do wszystkich klas adnotowanych @Entity znajdujących się w pakiecie pl.kedziorski. Z obiektu Class łatwo wyciągnąć pełną nazwę klasy, którą umieszcza się w pliku z mappingami.
clazz.getCanonicalName(); |
Teraz wystarczy już tylko sprawdzić czy plik XML z mappingiem zawiera znalezioną nazwę.
private Boolean isContainingMapping(String className){ BufferedReader br; try { br = new BufferedReader(new FileReader(hibernateMappingFilePath)); String line; while ((line = br.readLine()) != null) { if(line.contains(className)) { br.close(); return true; } } br.close(); return false; } catch (IOException e) { return false; } } |
Akurat tu sprawdzam linijka po linijce – można by wykorzystać fakt, że mamy do czynienia z plikiem XML i zrobić to znacznie ładniej, ale nie o to mi chodzi. Całość testu, który zabezpiecza zapominalskich przed niepotrzebnym wrzucaniem builda, który i tak się nie uruchomi wygląda następująco:
import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.util.Set; import javax.persistence.Entity; import org.junit.Assert; import org.junit.Test; import org.reflections.Reflections; public class HibernateMappingsTest { private static final String hibernateMappingFilePath = "src/main/webapp/WEB-INF/database/DataSource.xml"; private static final String pakiet = "pl.kedziorski"; @Test public void testMappings(){ Set> set = new Reflections(pakiet).getTypesAnnotatedWith(Entity.class); for (Class<!--?--> clazz : set) { Assert.assertTrue(isContainMapping(clazz.getCanonicalName())); } Assert.assertTrue(true); } private Boolean isContainMapping(String className){ BufferedReader br; try { br = new BufferedReader(new FileReader(hibernateMappingFilePath)); String line; while ((line = br.readLine()) != null) { if(line.contains(className)) { br.close(); return true; } } br.close(); return false; } catch (IOException e) { return false; } } } |
W podobny sposób wykorzystałem Reflections do wymuszania pewnej konwencji w projekcie – otóż każda moja encja ma statyczne pole Null , które zawiera NullObject tej klasy. Dodatkowo implementuje interfejs NullObject i metodę isNull(). Wykorzystuje te pole m.in w generycznym DAO i ważne jest aby było ono w każdej encji – no ale niestety czasami zdarza mi się o nim zapomnieć (… czasami przez kopiuj-wklej pole Null ma nie ten typ co powinno) – i przed tym również można dość łatwo zabezpieczyć się korzystając z mechanizmu reflekcji i testu. Kod poniżej już bez omawiania.
import java.lang.reflect.Field; import java.util.Set; import org.junit.Assert; import org.junit.Test; import org.reflections.Reflections; import pl.kedziorski.NullObject; public class NullPropertyTest { private static final String pakiet = "pl.kedziorski"; @Test public void testNullPropertyPresence() throws SecurityException, NoSuchFieldException{ Set> set = new Reflections(pakiet).getSubTypesOf(NullObject.class); for (Class<!--? extends NullObject--> clazz : set) { clazz.getField("Null"); } Assert.assertTrue(true); } @Test public void testNullPropertyType() throws SecurityException, NoSuchFieldException{ Set> set = new Reflections(pakiet).getSubTypesOf(NullObject.class); for (Class<!--? extends NullObject--> clazz : set) { if(!clazz.getSimpleName().startsWith("Null")){ Field field = clazz.getField("Null"); Assert.assertTrue("Złamana konwencja NullObject!! Niepoprawny typ pola 'Null'. Klasa: "+clazz.getCanonicalName(),field.getDeclaringClass().equals(clazz)); } } } @Test public void testNullPropertyValueNotSimpleNull() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException{ Set> set = new Reflections(pakiet).getSubTypesOf(NullObject.class); for (Class<!--? extends NullObject--> clazz : set) { if(!clazz.getSimpleName().startsWith("Null")){ Field field = clazz.getField("Null"); Assert.assertNotNull("Złamana konwencja NullObject!! Pole 'Null' == null. Klasa: "+clazz.getCanonicalName(), field.get(null)); } } } @Test public void testNullPropertyValue() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException{ Set> set = new Reflections(pakiet).getSubTypesOf(NullObject.class); for (Class<!--? extends NullObject--> clazz : set) { if(!clazz.getSimpleName().startsWith("Null")){ Field field = clazz.getField("Null"); Assert.assertEquals("Złamana konwencja NullObject!! Klasa: "+clazz.getCanonicalName(),"Null"+clazz.getSimpleName(), field.get(null).getClass().getSimpleName()); } } } } |
Mam nadzieje że komuś się przyda – jeśli ktoś ma jakiś pomysł przed czym jeszcze można by się zabezpieczyć w ten sposób – będę wdzięczny za komentarz. Jeśli jest jakaś ciekawsza metoda to również proszę o wpis z namiarem w komentarzu;)