Hej, dziś pokażę jak działają adnotacje w Javie na przykładzie prostego konwertera obiektu do formatu JSON.
Po co nam adnotacje? To wygodny sposób na konwertowanie danych, albo sterowanie zachowaniem programu po podstawie oznaczeń na polach, klasach i metodach. Zapewne spotkałeś/aś się z adnotacjami podczas implementowania bibliotek ORM (np. Hibernate) albo Jersey lub miałeś do czynienia z frameworkiem Spring Boot.
Ten wpis nie wyjaśni ci pojęcia refleksji w Javie, ale jeśli zostawisz like’a na stronie FB, lub zapiszesz się na newsletter, to na pewno nie ominie cię wpis o refleksach w Javie, który zamierzam napisać 🙂
Od razu przejdę do konkretów i utworzę klasę reprezentującą model danych:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public class Movie { private String title; private String author; private String yearOfProduction; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public String getYearOfProduction() { return yearOfProduction; } public void setYearOfProduction(String yearOfProduction) { this.yearOfProduction = yearOfProduction; } public String[] getTags() { return tags; } public void setTags(String[] tags) { this.tags = tags; } |
Jak widać powyżej, jest to zwykła klasa modelowa Movie, która posiada pola prywatne oraz getter i settery.
Teraz zajmę się tworzeniem kilku klas reprezentujących adnotacje. Chciałbym oznaczać klasy modelowe i wskazywać w ten sposób zasoby, które podlegają konwersji na JSON:
1 2 3 4 5 6 |
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface ModelToJson { } |
Objaśnienie linijka po linijce:
@Retention(RetentionPolicy.RUNTIME) oznacza, że adnotacja ma być czytana podczas kompilacji programu.
@Target(ElementType.TYPE) dotyczy miejsca, które może zostać oznaczone naszą adnotacją. TYPE oznacza klasę. Jeszcze jest m.in METHOD oraz FIELD co jasno oznacza przeznaczenie adnotacji.
public @interface ModelToJson to inicjalizacja klasy, która jest interfejsem. Adnotacje dodatkowo mają przedrostek @ przed słowem kluczowym interface.
Reasumując, adnotacja @ModelToJson będzie używana do oznaczania klasy modelowej przeznaczonej do konwersji.
Poniżej tworzę adnotację, która będzie oznaczać pole klasy, jako biorące udział w konwersji na format JSON. Innymi słowy, pole nie oznaczone tą adnotacją zostanie pominięte podczas konwersji modelu na JSON.
1 2 3 4 5 6 7 |
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface JsonElement { public String key() default ""; } |
Dodałem abstrakcyjna metodę String key() i oznaczyłem ją domyślnie na wartość pustego stringa. Dzięki temu, będzie można zmienić nazwę konwertowanego pola na dowolną zawartą w adnotacji.
Dorzucam klasę własnego wyjątku, choć nie jest to niezbędne:
1 2 3 4 5 6 7 8 |
public class JsonSerializationException extends RuntimeException { public JsonSerializationException(String message) { super(message); } } |
Teraz czas na implementację logiki konwertera, czyli metody, które przekonwertują model Movie na stringowego JSONa. Poniżej pojedyncze metody z yjaśnieniem działania, a na samym dole cała klasa już „sklejona” 🙂
metoda: checkIfSerializable():
1 2 3 4 5 6 7 8 9 10 11 |
private void checkIfSerializable(Object object) { if (Objects.isNull(object)) { throw new JsonSerializationException("Can't serialize a null object"); } Class<?> clazz = object.getClass(); if (!clazz.isAnnotationPresent(ModelToJson.class)) { throw new JsonSerializationException("The class " + clazz.getSimpleName() + " is not annotated with JsonSerializable"); } } |
Powyższa metoda przyjmuje ogólny obiekt jako parametr i za pomocą refleksji sprawdza, czy posiada adnotację @ModelToJson.
Metoda getJsonString():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
private String getJsonString(Object object) throws IllegalArgumentException, IllegalAccessException { Class<?> clazz = object.getClass(); Map<String, String> jsonElementsMap = new HashMap<>(); for (Field field : clazz.getDeclaredFields()) { field.setAccessible(true); if (field.isAnnotationPresent(JsonElement.class)) { jsonElementsMap.put(getKey(field), (String) field.get(object)); } } String jsonString = jsonElementsMap.entrySet().stream() .map(entry -> "\"" + entry.getKey() + "\":\"" + entry.getValue() + "\"") .collect(Collectors.joining(",")); return "{" + jsonString + "}"; } |
Powyższa metoda przyjmuje ogólny objekt jako parametr i za pomocą refleksji pobiera dane o polach klasy oraz spradza, czy pole posiada adnotację @JsonElement, jeśli posiada, to konwertuje tworząc stringa z kluczy (nazwa pola) i wartości (wartość pola).
Metoda getKey() sprawdza, czy nazwa pola musi zostać zmieniona na wartość z adnotacji, jeśli nie, to zostawia oryginalną nazwę pola.
1 2 3 4 5 6 |
private String getKey(Field field) { String value = field.getAnnotation(JsonElement.class).key(); return value.isEmpty() ? field.getName() : value; } |
Metoda convertToJson() służy jako główna metoda konwersji i będzie wywoływana wraz z przekazaniem modelu. Wykorzystałem tutaj również wyjątek rzucany podczas błędu konwersji. Obecna implementacja nie rozwiązuje problemu konwersji tablic na JSON, dlatego ten wyjątek się przyda 🙂
1 2 3 4 5 6 7 8 9 10 |
public String convertToJson(Object object) throws JsonSerializationException { try { checkIfSerializable(object); return getJsonString(object); } catch (Exception e) { throw new JsonSerializationException(e.getMessage()); } } |
Poniżej cała klasa ClassToJsonConverter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; public class ClassToJsonConverter { public String convertToJson(Object object) throws JsonSerializationException { try { checkIfSerializable(object); return getJsonString(object); } catch (Exception e) { throw new JsonSerializationException(e.getMessage()); } } private void checkIfSerializable(Object object) { if (Objects.isNull(object)) { throw new JsonSerializationException("Can't serialize a null object"); } Class<?> clazz = object.getClass(); if (!clazz.isAnnotationPresent(ModelToJson.class)) { throw new JsonSerializationException("The class " + clazz.getSimpleName() + " is not annotated with JsonSerializable"); } } private String getJsonString(Object object) throws IllegalArgumentException, IllegalAccessException { Class<?> clazz = object.getClass(); Map<String, String> jsonElementsMap = new HashMap<>(); for (Field field : clazz.getDeclaredFields()) { field.setAccessible(true); if (field.isAnnotationPresent(JsonElement.class)) { jsonElementsMap.put(getKey(field), (String) field.get(object)); } } String jsonString = jsonElementsMap.entrySet().stream() .map(entry -> "\"" + entry.getKey() + "\":\"" + entry.getValue() + "\"") .collect(Collectors.joining(",")); return "{" + jsonString + "}"; } private String getKey(Field field) { String value = field.getAnnotation(JsonElement.class).key(); return value.isEmpty() ? field.getName() : value; } } |
Mam nadzieję, że zachęciłem cię do dalszej zabawy z konwerterem oraz refleksą w Javie. Pamiętaj o FB 🙂
Tutaj znajdziesz kod w repozytorium do powyższego przykładu.
Powodzenia 🙂
[vicomi_feelbacks]