Spring Logger Service, przykładowy procesor adnotacji w Springu

Działa od Springa 2.0.X w górę.

Jedną z rzeczy, które wkurzają w Springu jest konieczność pisania kilometrowych plików XML nawet wtedy gdy wiadomo, że dana funkcjonalność jest zazwyczaj dobrze zdefiniowana i jednolita w całym systemie. Wtedy aż prosi się o dodanie jej poprzez adnotację i to najlepiej taką, która jasno mówi z jakim rodzajem usługi mamy do czynienia. Przykładem takiej usługi jest logowanie.

Większość programistów do usranej śmierci pisze w klasach wymagających logowania coś w stylu:

Listing 1. tworzenie loggera

private Logger log = LoggerFactory.getLogger(MyClass.class);

Nawet jeżeli używamy springa to zazwyczaj konfiguracja loggera jest opisana za pomocą wpisu properties w definicji beana. O ile prościej jest zrobić tak:

Listing 2. tworzenie loggera za pomocą adnotacji

@LogService
private Logger log

No to lecim…

Własny procesor adnotacji

Na początek zdefiniujmy sobie adnotację:

Listing 3. Adnotacja usługi dziennika zdarzeń

package pl.koziolekweb.loggerservice;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface LogService {

	public static enum LoggerType {
		log4j, sfl4j, commonsLogger, javaSeLogging;
	}

	LoggerType loggerType() default LoggerType.javaSeLogging;

}

Wewnętrzny typ enum reprezentuje najpopularniejsze loggery na rynku. Domyślnie ustawiłem typ tak by korzystać z loggera JSE.
Oczywiście jeżeli jakiś debil poda typ różny od typu pola to jego problem 😀

Testy

Jako, że Spring wręcz prosi by korzystać z TDD to i oto mamy zestaw testów:

Listing 4. Testy, testy i jeszcze raz testy…

package pl.koziolekweb.loggerservice;

import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.fail;

import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

public class LogServicePostProcessorTest {

	private LogServicePostProcessor logServicePostProcessor;

	@BeforeMethod
	protected void setUp() throws Exception {
		logServicePostProcessor = new LogServicePostProcessor();
	}

	@AfterMethod
	protected void tearDown() throws Exception {
		logServicePostProcessor = null;
	}

	@Test
	public void testPostProcessBeforeInitialization() {
		testJSE();
		testSfl4J();
		testCommons();
		testLog4j();
	}

	@Test(expectedExceptions = { IllegalArgumentException.class })
	public void testPostProcessBeforeInitializationWrongAnnotationType() {
		BadLogBean badLogBean = new BadLogBean();
		logServicePostProcessor.postProcessBeforeInitialization(badLogBean, "");
		fail("IllegalArgumentException should be threw");
	}

	@Test
	public void testLog4j() {
		Log4jLogBean log4jBean = new Log4jLogBean();
		logServicePostProcessor.postProcessBeforeInitialization(log4jBean, "");
		assertNotNull(log4jBean.getLogger());
	}

	@Test
	public void testCommons() {
		CommonsLogBean commonsBean = new CommonsLogBean();
		logServicePostProcessor.postProcessBeforeInitialization(commonsBean, "");
		assertNotNull(commonsBean.getLogger());

	}

	@Test
	public void testJSE() {
		JSELogBean jseBean = new JSELogBean();
		logServicePostProcessor.postProcessBeforeInitialization(jseBean, "");
		assertNotNull(jseBean.getLogger());
	}

	@Test
	public void testSfl4J() {
		Sfl4jLogBean sfl4jLogBean = new Sfl4jLogBean();
		logServicePostProcessor.postProcessBeforeInitialization(sfl4jLogBean, "");
		assertNotNull(sfl4jLogBean.getLogger());
	}

}

Metoda „uwspólniająca” testy to tylko taki mały helper dla pluginu moreUnit. Wiąże on test z metodą po nazwie, a nie wywołaniu co zmusza do robienia tego typu udziwnień.

Implementacja właściwa

Teraz czas na właściwą implementację klasy LogServicePostProcessor:

Listing 5. LogServicePostProcessor, czyli nasz procesorek

package pl.koziolekweb.loggerservice;

import java.lang.reflect.Field;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldCallback;

import pl.koziolekweb.loggerservice.LogService.LoggerType;

public class LogServicePostProcessor implements BeanPostProcessor {

	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}

	public Object postProcessBeforeInitialization(final Object bean, String beanName) throws BeansException {
		ReflectionUtils.doWithFields(bean.getClass(), new FieldCallback() {
			public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
				LogService logServiceAnnotation = field.getAnnotation(LogService.class);
				if (logServiceAnnotation != null) {
					LoggerType loggerType = logServiceAnnotation.loggerType();
					switch (loggerType) {
					case javaSeLogging:
						setJavaSeLoggerService(bean, field);
						break;
					case log4j:
						setLog4jService(bean, field);
						break;
					case sfl4j:
						setSfl4jService(bean, field);
						break;
					case commonsLogger:
						setCommonsLoggerService(bean, field);
						break;
					default:
						setByType(bean, field);
						break;
					}
				}
			}

		});
		return bean;
	}

	private void setByType(Object bean, Field field) throws IllegalAccessException {
		Class type = field.getType();
		if (type.equals(Log.class))
			setCommonsLoggerService(bean, field);
		if (type.equals(java.util.logging.Logger.class))
			setJavaSeLoggerService(bean, field);
		if (type.equals(org.apache.log4j.Logger.class))
			setLog4jService(bean, field);
		if (type.equals(Logger.class))
			setSfl4jService(bean, field);
	}

	private void setCommonsLoggerService(final Object bean, Field field) throws IllegalAccessException {
		Log log = LogFactory.getLog(bean.getClass());
		setLogger(bean, field, log);
	}

	private void setJavaSeLoggerService(final Object bean, Field field) throws IllegalAccessException {
		java.util.logging.Logger log = java.util.logging.Logger.getLogger(bean.getClass().getName());
		setLogger(bean, field, log);
	}

	private void setLog4jService(final Object bean, Field field) throws IllegalAccessException {
		org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(bean.getClass());
		setLogger(bean, field, log);
	}

	private void setLogger(final Object bean, Field field, Object log) throws IllegalAccessException {
		boolean orginalAccessFlag = field.isAccessible();
		if (!orginalAccessFlag) {
			field.setAccessible(true);
		}
		field.set(bean, log);
		field.setAccessible(orginalAccessFlag);
	}

	private void setSfl4jService(final Object bean, Field field) throws IllegalAccessException {
		Logger log = LoggerFactory.getLogger(bean.getClass());
		setLogger(bean, field, log);
	}

}

Jak widać implementacja ma konstrukcję zbliżoną do konstrukcji cepa. Kilka ifów plus obsługa debilizmów i własnych rozszerzeń typów przez użyszkodnika. Takie państwo w państwie 😉 Przydatne jeżeli komuś zamarzy się dodawanie nowych typów loggerów.

Użycie w pliku xml

By użyć tego rozwiązania w pliku XML springa wystarczy dodać wpis:

Listing 6. Użycie w konfiguracji Springa


Nawet ID nie trzeba podawać.

Podsumowanie i informacje o dostępie

Projekt jest hostowany na Google Code.
Licencja GPLv3. Sorry Winnetou inaczej nie będzie.
Budowany za pomocą Maven2.
To wszystko powinno działać do końca tego tygodnia, bo już ciężkiej kurwicy dostaję z pluginem SVN do Eclipse.
Wszystkie informacje oraz przykłady użycia, jak ktoś nie umie czytać testów będą na stronie projektu.

2 myśli na temat “Spring Logger Service, przykładowy procesor adnotacji w Springu

  1. 2 pytania:

    1. Po co wprowadzac typ @LogService(loggerType = LoggerType.log4j) skoro samo pole jest typu org.apache.log4j.Logger. Czy nie da sie zrobic automatycznej detekcji?

    2. „Licencja GPLv3. Sorry Winnetou inaczej nie będzie.”. Po co wrzuac tak trywialna biblioteke w GPL, skoro sam piszesz ze to „konstrukcja cepa”, wiec albo ktos zaimplementuj cos na wlasna modle albo po prostu ukradnie Twoj kod, zmieni pakiet i tyle. Nie lepsze byloby by BSD, lub cokolwiek malo restrykcyjnego, za to podziekowanie gdzies w README?

  2. @Leszek
    ad1. Da się, ale wtedy w przypadku gdy ktoś użyje niestandardowego loggera to leży. Nie zgadnę klasy fabrykującej. Na chwile obecną ograniczam ilość narzędzi, ale mam już pewien pomysł jak temu zaradzić by było bardziej uniwersalnie i łatwiejsze do rozszerzenia. na razie jest to wersja mocno eksperymentalna 🙂
    ad2. To jest mój plan na rządzenie światem. Jak jakieś wielkie korpo mi podpadnie i odkryję, że użyli mój kod to ich pozwę…. a tak w praktyce to GPL jest trendy i w praktyce mam świadomość, że wszyscy ja olewają. Jednak lepiej mi z myślą, że dzięki temu jestem bliższy rms 🙂

Napisz odpowiedź

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax