Добрый день!

Как известно «лень — двигатель прогресса», самое полезное качество программиста, благодаря ей появилось множество замечательных фреймворков и так далее, и тому подобное. Но сегодня я хочу написать не про человеческую лень.

Пару недель назад мне попалась на глаза статья о черновом наброске фичи, новом модификаторе lazy для final полей. И конечно, инициализация логгеров приведена как самый наглядный пример, когда бы эта фича пригодилась. Нет, никто не спорит, конечно логгеры это overhead, создавать их во время старта, потом еще в памяти держать. Брр. Но неужели нельзя написать элегантный костыль решение на старой доброй Java?

И начинаем сразу кодить


Первое решение «в лоб». Имплементируем интерфейс org.slf4j.Logger (я выбрал slf4j, но все это справедливо и для любого другого фреймворка логирования), инкапсулируем реальный логгер, инициализируем его когда вызывается какой-нибудь метод интерфейса. Паттерн Proxy плюс Factory Method. Все просто, все работает.

public class LazyLogger implements Logger {

    private Logger realLogger;
    private Class<?> clazz;

    private LazyLogger(Class<?> clazz) {
        this.clazz = clazz;
    }

    public static Logger getLogger(Class<?> clazz) {
        return new LazyLogger(clazz);
    }

    private Logger getRealLogger() {
        if (realLogger == null)
            realLogger = LoggerFactory.getLogger(this.clazz);
        return realLogger;
    }

    @Override
    public void trace(String msg) {
        getRealLogger().trace(msg);
    }

    @Override
    public void debug(String msg) {
        getRealLogger().debug(msg);
    }
:
:
итд

Но постойте, у интерфейса org.slf4j.Logger около 40 методов, мне что, все их имплементировать руками? И getRealLogger() не выглядит Thread Safe. Так не годится, давайте думать дальше.

Развивая тему


Как вариант, есть Lombok с аннотацией @Delegate.

@AllArgsConstructor(staticName = "getLogger")
public class LazyLogger implements Logger {

  private final static Function<Class<?>, Logger> $function = LoggerFactory::getLogger;
  private Logger $logger = null;
  private final Class<?> clazz;

  @Delegate
  private Logger getLogger() {
	if ($logger == null)
		$logger = $function.apply(clazz);
	return $logger;
  }
}


Использование:
private static final Logger logger = LazyLogger.getLogger(MyClass.class);


@Delegate отвечает за создание в момент компиляции методов интерфейса org.slf4j.Logger, внутри вызывает getLogger() + <нужный метод>. В момент первого вызова метода $function создает реальный логгер. $ добавлены к полям, чтобы скрыть их от Lombok. Он не генерит Getters/Setters, не создает конструкторы для таких полей.

Так, выглядит хорошо, но чего-то не хватает. А, точно! Thread Safe. Сейчас мы в getLogger() напишем double check, да еще с AtomicReference! Но постойте, у Lombok уже есть @Getter(lazy = true)!.

@RequiredArgsConstructor(staticName = "getLogger")
public class LazyLoggerThreadSafe implements Logger {

  private static final Function<Class<?>, Logger> $function = LoggerFactory::getLogger;
  private final Class<?> clazz;

  @Getter(lazy = true, onMethod_ = { @Delegate }, value = AccessLevel.PRIVATE)
  private final Logger logger = createLogger();

  private Logger createLogger() {
	return $function.apply(clazz);
  }
}


Использование:
private static final Logger logger = LazyLoggerThreadSafe.getLogger(MyClass.class);


Что же здесь происходит? Дело в том, что Annotation Processor, который используется Lombok, для обработки аннотаций может несколько раз проходить по исходному коду, чтобы обработать аннотации которые были сгенерены на предыдущем шаге. Про это можно почитать здесь. Во время первого прохода @Getter(lazy = true) генерит getLogger() c Lazy инициализацией и аннотирует его @Delegate. А во время второго прохода генеряться сами методы от @Delegate.

И на десерт


А что, если я хочу Lazy инициализировать другой объект, не логгер? Если мне нужна этакая универсальная Lazy Factory, куда я буду передавать только Supplier создающий реальный объект? @Delegate уже нас не спасет, ему нужен конкретный класс, с конкретным набором методов. Но не беда, используем Dynamic Proxy:

@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class LazyFactory<I> {

	private Class<I> interfaceClass;
	private Supplier<I> supplier;

	@SuppressWarnings("unchecked")
	private I getLazyObject() {
		return (I) Proxy.newProxyInstance(
				LazyFactory.class.getClassLoader(),
				new Class[] { interfaceClass },
				new LazyFactory.DynamicInvocationHandler());
	}

	public static <T> T getLazy(Class<T> interfaceClass, Supplier<T> supplier) {
		return new LazyFactory<T>(interfaceClass, supplier).getLazyObject();
	}

	private class DynamicInvocationHandler implements InvocationHandler {

		@Getter(lazy = true)
		private final I internalObject = supplier.get();

		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			return method.invoke(getInternalObject(), args);
		}
	}
}

Использование:

public interface Hello {
    void sayHello();
}

public class LazyHello implements Hello {
    
    public LazyHello() {
        System.out.println("LazyHello under constuction");
    }

    @Override
    public void sayHello() {
        System.out.println("I'm very lazy for saying Hello..");
    }
}

private static final Hello lazyObj = LazyFactory.getLazy(Hello.class, LazyHello::new);
lazyObj.sayHello();


Как видите, кода тоже совсем не много, отчасти благодаря Lombok. Пару слов, как это работает. LazyFactory в статическом методе возвращает динамичский прокси. Внутри DynamicInvocationHandler «реальный объект», но он будет создан только тогда, когда будет вызван invoke() DynamicInvocationHandler-а, то есть один из методов интерфейса I.
За создание «реального объекта» отвечает getInternalObject() который генерится @Getter(lazy = true).

Тему можно развивать дальше, но уже сейчас видно, что lazy инициализация чего угодно это просто, лаконично и легко встраивается в существующий код.

Спасибо за внимание, всего хорошего!

Ссылки:

JEP draft: Lazy Static Final Fields
Lombok

Комментарии (8)


  1. zagayevskiy
    21.09.2018 17:48

    Lombok — это не старая добрая джава.


  1. kalapanga
    21.09.2018 17:57
    +1

    элегантный костыль решение
    Авторы, пожалуйста, забудьте этот дурацкий «художественный приём» с зачёркиванием. Это чистовик статьи, а не черновая бумажка в сортир. Даже не читаете, что у вас получается — «элегантный решение».


    1. dimpon Автор
      21.09.2018 17:59

      Спасибо за комментарий. Забуду. Но я видел на Хабре многие используют этот прием.


      1. Trumanbaz
        22.09.2018 11:55

        В данном случае можно сделать как-то так:
        «неужели нельзя написать элегантный костыль элегантное решение»


    1. fcoder
      21.09.2018 18:32
      +2

      Ну статья на хабре, не научная публикация. А такой приём гораздо лучше скобок позволяет раскрыть, что на самом деле хотел сказать автор.


    1. Exosphere
      21.09.2018 18:56

      Обозначение иронии, не больше. Нормальный приём.


  1. CyberSoft
    22.09.2018 00:05

    Предлагаете вместо одного модификатора писать вот эту тягомотину? Нет уж, пусть лучше будет ещё один модификатор.

    JEP всё-таки решает проблему на корню, а не добавляет костылей


    1. dimpon Автор
      22.09.2018 13:00

      Добрый день!
      Ну во-первых модификатора пока нет, и неизвестно появится ли он. А во-вторых решение в частном случае занимает 7 строк, а в общем 22. Код читаемый и понятный, а совсем не «тягомотина». Существующий код приложения, которого иногда гигабайты, не меняется. Показать это и была цель статьи.