Programowanie zaawansowane #6: własne adnotacje

Znasz już kilka adnotacji (np. @Override). Teraz czas przyjrzeć im się z bliska. Nazwy adnotacji zaczynają się od znaku małpy (@).

Adnotacje, w zależności od jej funkcji, z reguły stawiasz przed nazwą:

  • klasy
  • interfejsu
  • pola
  • metody
  • lub parametru metody.

Adnotacja może zawierać składowe, takie jak:

  • wartość typu prostego
  • String
  • obiekt Class
  • enumy
  • inne adnotacje
  • tablicę powyższych

Żadna ze składowych nie może być nullem, chociaż może mieć domyślne wartości. Wszystkie wartości w adnotacji są traktowane jako stałe. Dzieje się tak dlatego, że adnotacje są interpretowane przez kompilator przed wykonanie danego kodu.

Możesz stawiać ich dowolnie dużo, w zależności od potrzeb.

Możesz też stworzyć własną adnotację i wykorzystywać ją w kodzie.

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonField {
    String value() default "empty";
}

Powyższy kod przypomina zwykły interfejs z jedną sygnaturą. Na pierwszy rzut oka ma on kilka różnic. Po pierwsze przed słowem kluczowym interface postawiłem znak małpy. Jest to obowiązkowa konwencja, która czyni interfejs adnotacją. Wszelkie inne zmiany są już nieobligatoryjne. Drugą wyróżniającą się rzeczą są adnotacje Target i Retention. Pierwsza z nich określa do jakiego elementu kodu będzie można stosować adnotację (w moim przypadku jest to pole) oraz w jakim momencie przetwarzania kodu będzie ona użyta (runtime*). Ostatnią dodatkową kwestią są sygnatury, które będziesz wykorzystywać w trakcie użycia adnotacji. Tutaj wykorzystałem tylko jedną opcję z możliwością domyślnej wartości (default „empty”).

Teraz wykorzystam stworzoną adnotację w zwykłej klasie typu POJO.

public class InternalApi {

	@JsonField("ver")
	private String version;
	@JsonField("snap")
	private String snapshot;
	@JsonField("dependency")
	private String dependency;
	
	public InternalApi() {
	}
	
	public InternalApi(String version, String snapshot, String dependency) {
		this.version = version;
		this.snapshot = snapshot;
		this.dependency = dependency;
	}

	public String getVersion() {
		return version;
	}

	public void setVersion(String version) {
		this.version = version;
	}

	public String getSnapshot() {
		return snapshot;
	}

	public void setSnapshot(String snapshot) {
		this.snapshot = snapshot;
	}

	public String getDependency() {
		return dependency;
	}

	public void setDependency(String dependency) {
		this.dependency = dependency;
	}

	@Override
	public String toString() {
		return "InternalApi [version=" + version + ", snapshot=" + snapshot + ", dependency=" + dependency + "]";
	}
	
}

Adnotacja @JsonField mówi programiście: „zastosuj mnie na jakimś polu, a nadpiszę jego nazwę”. Jeśli nie wypiszesz nowej nazwy w ciele adnotacji, zostanie użyta nazwa domyślna zadeklarowana w jej implementacji. Mając na myśli ‚nazwa’ nie myl jej z wartością zmiennej. Ta będzie ustalana w trakcie użycia klasy InternalApi.

Teraz, aby wykorzystać moją adnotację, muszę stworzyć odpowiedni kod, który korzystając z mechanizmu refleksji „wydobędzie” z wartości wpisanej do adnotacji nazwę pola i wpisze go do nowo powstałego tekstu typu JSON**. Metody serialize i getSerializedKey nie posiadają niczego nowego w stosunku do lekcji odnośnie „refleksji”. Jest to tworzona mapa, w której kluczem będzie nazwa wpisana w adnotacji (a nie nazwa pola) a jej wartością tekst wpisany w konstruktorze klasy InternalApi. Ciekawą metodą jest natomiast toJsonString, która to parsuje mapę do postaci typu JSON.

public class JsonSerializer {
	public static String serialize(Object object) throws IllegalAccessException {
		Class<?> objectClass = object.getClass();
		Map<String, String> jsonElements = new HashMap<>();

		for (Field field : objectClass.getDeclaredFields()) {
			field.setAccessible(true);
			if (field.isAnnotationPresent(JsonField.class)) {
				jsonElements.put(getSerializedKey(field), (String) field.get(object));
			}
		}

		return toJsonString(jsonElements);
	}

	private static String toJsonString(Map<String, String> jsonMap) { 
		StringBuilder elementsStringBuilder = new StringBuilder("");
		
		for (Map.Entry<String, String> element : jsonMap.entrySet()) {
			elementsStringBuilder.append("\"," + element.getKey() + "\":\"" + element.getValue());
		}
		String elementsString = elementsStringBuilder.toString().replaceFirst(",", "");
	
		return "{" + elementsString + "}";
	}

	private static String getSerializedKey(Field field) {
		String annotationValue = field.getAnnotation(JsonField.class).value();
		if (annotationValue.isEmpty()) {
			return field.getName();
		} else {
			return annotationValue;
		}
	}
}

Praktyczne użycie adnotacji:

public class ApiMain {

	public static void main(String[] args) throws IllegalAccessException {
		InternalApi api = new InternalApi("18.1.1", "RC", "Spring core");
		System.out.println(api);
		System.out.println(JsonSerializer.serialize(api));
	}

}

Na konsoli powinno się wyświetlić:

InternalApi [version=18.1.1, snapshot=RC, dependency=Spring core]
{"ver":"18.1.1","dependency":"Spring core","snap":"RC"}

Inne standardowe adnotacje używane w Javie:

  • Deprecated – Oznacza, że element klasy lub ona sama jest przestarzała i powinna zostać użyta jej nowsza wersja.
  • SuppressWarnings – Wyłącza ostrzeżenia danego typu.
  • FunctionalInterface – Stosuje się go od Javy 8 jako interfejs funkcyjny (więcej w kursie odnośnie zmian w Javie 8).
  • PostConstruct, PreDestroy – Metoda o tej adnotacji będzie wykonywana zaraz po wstrzyknięciu obiektu lub jego zniszczeniu. Więcej o wstrzykiwaniu zależności w kursie Java – frameworki.
  • Resource – Oznacza jako zasób do użycia w innym miejscu.

* Różnica pomiędzy run time a compile time na przykładzie języka C: https://www.javatpoint.com/compile-time-vs-runtime

** Czym jest JSON: https://www.w3schools.com/whatis/whatis_json.asp

Dodaj komentarz