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

<bean class="pl.koziolekweb.loggerservice.LogServicePostProcessor"></bean>

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.