19 декабря 2016 в 18:17

Как не писать лишнего из песочницы

Все программисты сталкиваются с boiler-plate кодом. Особенно Android-программисты. Писать шаблонный код — дело неблагодарное и, я уверен, что нет программиста, которому бы это доставляло удовольствие. В один прекрасный день я начал искать решения. Несмотря на то, что идея довольно проста: генерировать шаблонный код в отдельный класс и позже вызывать этот код в рантайме, готовых решений не нашлось, и я принялся за дело. Первая версия была реализована как один из подмодулей одного из рабочих проектов. Более двух лет я был доволен этим решением. Оно действительно работало и работало именно так, как я этого ожидал. Время шло, модуль дополнялся новыми функциями, рефакторился, оптимизировался. В целом PoC можно было назвать успешным, и я решил поделиться проектом с комьюнити.


Спустя 8 месяцев программирования по вечерам, я на Хабре со своим первым в жизни постом. Итак, Jeta — фреймворк для генерации исходного кода, построенного на javax.annotation.processing. Open-Source, Apache 2.0, исходный код на GitHub, артефакты на jCenter, tutorials, samples, unit-tests, в общем все как положено.


Для наглядности, давайте рассмотрим простой пример. В библиотеку входит аннотация @Log. С ее помощью упрощается объявление именованных логгеров внутри класса.


public class LogSample {
    @Log
    Logger logger;
}

Так, для этого класса, Jeta сгенерирует класс LogSample_Metacode с методом applyLogger:


public class LogSample_Metacode implements LogMetacode<LogSample> {
    @Override
    public void applyLogger(LogSample master, NamedLoggerProvider provider) {
        master.logger = (Logger) provider.getLogger(“LogSample”);
    }
}

Из примера видно, что по аннотации @Log генерируется код, который присваивает логгер с именем “LogSample” аннотированному полю. Остается реализовать NamedLoggerProvider который будет поставлять логгеры из библиотеки, которая используется в вашем проекте.


Помимо неявного именования логгеров, которое, как видно из примера, берется из названия класса, можно указать конкретное значение через параметр аннотации, как например @Log(“REST”).


Этот прием избавляет от копи-пасты строки типа:


private final Logger logger = LoggerFactory.getLogger(LogSample.class);

что в свою очередь избавляет проект от логгеров с именами “соседов”, так как часто программисты забывают заменить передаваемый в качестве параметра класс.


Конечно, это довольно простой пример. Тем не менее, он показывает основную идею фреймворка — меньше кода, больше стабильности.


Несмотря на то, что основная цель Jeta — это избавление от шаблонного кода, на приеме, показанном выше, реализовано множество полезных функций, таких как Dependency Injection, Event Bus, Validators и др. Нужно заметить, что все они написаны согласно принципам фреймворка — без Java Reflection и, по возможности, все ошибки находятся на стадии компиляции.


В этой статье мы так же не будем избавляться от выдуманного boiler-plate кейса. Вместо этого мы напишем кое-что полезное, а именно Data Binding (далее DB). Хотя, принципиальной разницы тут нет, и эту статью можно будет использовать как руководство для решения задач, связанных с избавлением от шаблонного кода.


Data-Binding.


Android программисты, возможно, уже знакомы с этим термином. Не так давно Google выпустила Data Binding Library. Для тех из Вас, кто не знаком с этим паттерном, я уверен, что не составит большого труда разобраться с его концепцией из примеров в этой статье. Так же привожу два спойлера с небольшими экскурсами по Android и Data-Binding, соответственно.


Android

Экран, в контексте Android-программирования, называется Activity. Это Java класс наследованный от android.app.Activity. Для каждой активити существует XML-файл с разметкой, называемый Layout. Вот пример Activity из “Hello, World” приложения:


<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent">
   <TextView
       android:id="@+id/text1"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content" />
</LinearLayout>

public class MainActivity extends Activity {
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       TextView text1 = (TextView) findViewById(R.id.text1);
       text1.setText("Hello World!");
   }
}

Строка setContentView(R.layout.activity_main) связывает активити и лейаут через R файл, который генерируется автоматически. Так, для нашего лейаута activity_main.xml, R-файл будет содержать внутренний класс layout c полем activity_main и каким-то уникальным числовым значением. Для TextView, которому мы присвоили id = text1, это будет внутренний класс id и поле text1, соответственно.


Data Binding

Data-binding позволяет писать DSL выражения внутри XML-файла. Вот пример с официального сайта developer.android.com:


<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"/>
   </LinearLayout>
</layout>

Так, в нужный момент, мы связываем объект пользователя (com.example.User) с лейаутом и data-binding автоматически проставляет значения в соответствующие компоненты. Так, первый TextView отобразит имя пользователя, а второй его фамилию.


В этой статье мы напишем свой Data-Binding, правда, пока без преферанса, ну а в конце вас ждет небольшой интерактив.


Перед тем как приступим, пара замечаний по Jeta.


  1. Все специфичные для андроида функции вынесены в отдельную библиотеку — Androjeta. Она расширяет Jeta а значит все, что доступно в Jeta, т.е. для любого Java-проекта, так же доступно в Androjeta.


  2. В терминологии фреймворка сгенерированный класс называется Metacode. Класс, для которого генерируется мета-код, называется Master. Еще есть Controller, который применяет мета-код к мастеру, и Metasitory — это хранилище ссылок на все Metacode-классы. С помощью Metasitory контроллеры находят нужный мета-код.

1. DataBinding проект


Первым делом мы создадим самый обычный Android проект с одной активити и с pojo-классом User. Наша задача — к концу статьи записать имя и фамилию юзера в соответствующие UI-компоненты посредством DB. Для наглядности я буду приводить скриншоты со структурой проекта.


project


2. common модуль


Так как генерация кода происходит на стадии компиляции, и все сопутствующие для этого классы запускаются в отдельном окружении, нам понадобится модуль, который будет доступен и в рантайме и во время кода-генерации. Замечу, что это обычный Java-модуль, который будет содержать два файла — аннотацию DataBind и Metacode-интерфейс DataBindMetacode.


common module


3. apt модуль


apt модуль содержит необходимые для кода-генерации классы. Как уже было сказано, этот модуль зависит от common и будет доступен только на стадии компиляции. Как и common, это обычный Java-модуль, который будет содержать единственный файл — DataBindProcessor. Именно в этом классе мы будем обрабатывать DataBind аннотацию, парсить XML-лейаут и генерировать соответствующий мета-код. Обратите внимание что apt модуль также зависит от org.brooth.androjeta:androjeta-apt:+:noapt, таким образом получая доступ к классам фреймворка.


apt module


4. Подготавливаем app


Прежде чем приступить непосредственно к генерации мета-кода, сначала мы должны подготовить наше приложение. Первый делом мы изменим наш лейаут:


<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:androjeta="http://schemas.jeta.brooth.org/androjeta"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/firstName"
        androjeta:setText="master.user.firstName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/lastName"
        androjeta:setText="master.user.lastName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</LinearLayout>

Небольшое пояснение: мы объявили свой namespace с префиксом “androjeta” и добавили двум TextView атрибуты androjeta:setText с DB-выражениями. Так мы сможем найти и обработать эти выражения в DataBindProcessor, сгенерировав соответствующий мета-код.


package org.brooth.androjeta.samples.databinding;

import android.app.Activity;
import android.os.Bundle;

@DataBind(layout = "activity_main")
public class MainActivity extends Activity {

    final User user = new User("John", "Smith");

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MetaHelper.applyDataBinding(this);
    }
}

Тут важным являются две вещи. Во-первых, мы добавили на активити аннотацию @DataBind, которую ранее мы создали в common модуле. Таким образом, на стадии генерации Jeta найдет этот класс и передаст его в DataBindProcessor. Во-вторых, после того, как мы установили лейаут, мы вызываем MetaHelper.applyDataBind(this). C помощью таких статических методов проще обращаться к мета-коду. Давайте создадим этот класс.


package org.brooth.androjeta.samples.databinding;

import org.brooth.jeta.metasitory.MapMetasitory;
import org.brooth.jeta.metasitory.Metasitory;

public class MetaHelper {
    private static MetaHelper instance = new MetaHelper("org.brooth.androjeta.samples");
    private final Metasitory metasitory;

    private MetaHelper(String metaPackage) {
        metasitory = new MapMetasitory(metaPackage);
    }

    public static void applyDataBinding(Object master) {
        new DataBindController<>(instance.metasitory, master).apply();
    }
}

MetaHelper — необязательный класс. Это способ организации обращение к мета-коду. Он служит исключительно для удобства. Подробней об этом классе можно прочитать на этой странице. Тут же нам важно, что метод applyDataBinding передает работу DataBindController-у:


package org.brooth.androjeta.samples.databinding;

import org.brooth.jeta.MasterController;
import org.brooth.jeta.metasitory.Metasitory;

public class DataBindController<M> extends MasterController<M, DataBindMetacode<M>> {

    public DataBindController(Metasitory metasitory, M master) {
        super(metasitory, master, DataBind.class);
    }

    public void apply() {
        for(DataBindMetacode<M> metacode : metacodes)
            metacode.apply(master);
    }
}

Напомню, контроллеры — это классы, которые применяют мета-код к мастерам. Больше информации можно найти на этой странице.


На последнем шаге нам нужно добавить DataBindProcessor в список процессоров, которые Jeta вызывает для генерации мета-кода. Для этого в корневом пакете app модуля (app/src/main/java) мы создадим файл jeta.properties с содержимым:


processors.add = org.brooth.androjeta.samples.databinding.apt.DataBindProcessor
metasitory.package = org.brooth.androjeta.samples
application.package = org.brooth.androjeta.samples.databinding

Подробнее об этом файле и о доступных настройках вы можете найти на этой странице.


5. DataBindProcessor


Думаю, излишне будет комментировать каждый шаг процессора, т.к. ничего инновационного они не содержат. Достаточно описать основные моменты: мы проходимся SAX-парсером по XML-лейауту, находим DB-выражения и генерируем соответствующий Java-код.


Нужно заметить, что Jeta использует JavaPoet — замечательную библиотеку от Square для генерации Java-кода. Рекомендую пройтись по README, если соберетесь писать свой процессор. Ниже привожу исходный код DataBindProcessor:


package org.brooth.androjeta.samples.databinding.apt;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeSpec;

import org.brooth.androjeta.samples.databinding.DataBind;
import org.brooth.androjeta.samples.databinding.DataBindMetacode;
import org.brooth.jeta.apt.ProcessingContext;
import org.brooth.jeta.apt.ProcessingException;
import org.brooth.jeta.apt.RoundContext;
import org.brooth.jeta.apt.processors.AbstractProcessor;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import java.io.File;
import java.io.FileNotFoundException;

import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

public class DataBindProcessor extends AbstractProcessor {
    private static final String XMLNS_PREFIX = "xmlns:";
    private static final String ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android";
    private static final String ANDROJETA_NAMESPACE = "http://schemas.jeta.brooth.org/androjeta";

    private ClassName textViewClassname;
    private ClassName rCLassName;

    private String layoutsPath;

    private String androidPrefix;
    private String androjetaPrefix;
    private String componentId;
    private String componentExpression;

    public DataBindProcessor() {
        super(DataBind.class);
    }

    @Override
    public void init(ProcessingContext processingContext) {
        super.init(processingContext);
        layoutsPath = processingContext.processingEnv().getOptions().get("layoutsPath");
        if (layoutsPath == null)
            throw new ProcessingException("'layoutsPath' not defined");

        String appPackage = processingContext.processingProperties().getProperty("application.package");
        if (appPackage == null)
            throw new ProcessingException("'application.package' not defined");

        textViewClassname = ClassName.bestGuess("android.widget.TextView");
        rCLassName = ClassName.bestGuess(appPackage + ".R");
    }

    @Override
    public boolean process(TypeSpec.Builder builder, final RoundContext roundContext) {
        TypeElement element = roundContext.metacodeContext().masterElement();
        ClassName masterClassName = ClassName.get(element);
        builder.addSuperinterface(ParameterizedTypeName.get(
                ClassName.get(DataBindMetacode.class), masterClassName));

        final MethodSpec.Builder methodBuilder = MethodSpec.
                methodBuilder("apply")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .returns(void.class)
                .addParameter(masterClassName, "master");

        String layoutName = element.getAnnotation(DataBind.class).layout();
        String layoutPath = layoutsPath + File.separator + layoutName + ".xml";
        File layoutFile = new File(layoutPath);
        if (!layoutFile.exists())
            throw new ProcessingException(new FileNotFoundException(layoutPath));

        androidPrefix = null;
        androjetaPrefix = null;

        try {
            SAXParserFactory factory = SAXParserFactory.newInstance();
            SAXParser saxParser = factory.newSAXParser();
            saxParser.parse(layoutFile, new DefaultHandler() {
                        @Override
                        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
                            for (int i = 0; i < attributes.getLength(); i++) {
                                if (androidPrefix == null &&
                                        attributes.getQName(i).startsWith(XMLNS_PREFIX) &&
                                        attributes.getValue(i).equals(ANDROID_NAMESPACE)) {
                                    androidPrefix = attributes.getQName(i).substring(XMLNS_PREFIX.length());
                                    continue;
                                }

                                if (androjetaPrefix == null &&
                                        attributes.getQName(i).startsWith(XMLNS_PREFIX) &&
                                        attributes.getValue(i).equals(ANDROJETA_NAMESPACE)) {
                                    androjetaPrefix = attributes.getQName(i).substring(XMLNS_PREFIX.length());
                                    continue;
                                }

                                if (componentId == null && androidPrefix != null &&
                                        attributes.getQName(i).equals(androidPrefix + ":id")) {
                                    componentId = attributes.getValue(i).substring("@+id/".length());
                                    continue;
                                }

                                if (componentExpression == null && androjetaPrefix != null &&
                                        attributes.getQName(i).equals(androjetaPrefix + ":setText")) {
                                    componentExpression = attributes.getValue(i);
                                }
                            }
                        }

                        @Override
                        public void endElement(String uri, String localName, String qName) throws SAXException {
                            if (componentExpression == null)
                                return;

                            if (componentId == null)
                                throw new ProcessingException("Failed to process expression '" +
                                        componentExpression + "', component has no id");

                            methodBuilder.addStatement("(($T) master.findViewById($T.id.$L))\n\t.setText($L)",
                                    textViewClassname, rCLassName, componentId, componentExpression);

                            componentId = null;
                            componentExpression = null;
                        }
                    }
            );

        } catch (Exception e) {
            throw new ProcessingException(e);
        }

        builder.addMethod(methodBuilder.build());
        return false;
    }
}

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


Для начала удостоверимся, что все работает. Для этого в директории проекта выполним команду:


./gradlew assemble

Если в выводе нет никаких ошибок, и вы видите запись:


Note: Metacode built in Xms

значит все ОК, и по пути /app/build/generated/source/apt/ мы сможем увидеть сгенерированный код:


metacode


Как видно, мета-код отформатирован и хорошо читаем, следовательно, его легко отлаживать. Так же, важным плюсом является то, что все возможные ошибки обнаружатся на стадии компиляции. Так, если добавить @DataBind на Activity у которой нет поля user, передать в параметры неправильное название лейаута или ошибиться в DB-выражении, то сгенерированный код не скомпилируется и проект не соберется.


На этом этапе вы можете запустить приложение, и, как ожидается, на экране вы увидите данные о пользователе user.


7. Заключение .


Прошу отнестись к примеру именно как к Proof-Of-Concept, а не как к готовому решению. К тому же, его задача — продемонстрировать работу фреймворка, и не факт, что Jeta-DB пойдет в лайв.


Собственно, обещанный интерактив. Напишите в комментариях, что бы вы хотели видеть в Data-Binding-е. Возможно, вам не хватает каких — то возможностей в реализации от Google. Возможно, вы хотите избавиться от какого-то еще boiler-plate кейса. Также, буду благодарен за любые другие замечания или пожелания. Я, в свою очередь, постараюсь выбрать самое интересное и реализовывать в будущих версиях.


Спасибо, что дочитали до конца.
Happy code-generating! :)


» Официальный сайт
» Исходный код примера на GitHub
» Jeta на GitHub
» Androjeta на GitHub

Oleg @brooth
карма
5,0
рейтинг 0,0
Java/Android Developer
Похожие публикации
Самое читаемое Разработка

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

  • +2
    Чем ваш проект отличается от известного lombok?
    • +1

      Базовое отличие в том, что lombok меняет синтаксическое дерево, т.е. hacking, т.е. happy debugging…
      AST — это интерфес, реализация зависит от jdk, Поэтому у людей и вознитают вопросы типа таких, таких, или таких, и т.д.
      Jeta не делает никакой магии. Только генерирует исходиники. А вы их вызываете… когда и как вам необходимо

      • 0
        Тогда осталось только добавить проект java-as-script и можно компилировать и дебажить на лету. А пример как это работает есть в проекте alarm-system. С android это правда не подружить…

        java -Dlogin=...YOUR_EMAIL...@mail.ru -Dpassword=******* -DscriptPath=https://raw.githubusercontent.com/igor-suhorukov/alarm-system/master/src/main/java/com/github/igorsuhorukov/alarmsys/AlarmSystem.java -jar java-as-script-1.0.jar
        
        • 0
          Если вы делаете ссылку на свою библиотеку, то хороший тон сделать read.me с описанием, что это. В код не полез а из вашего описания- непонятно.
          • 0
            Спасибо, верное замечание! В ближайшее время опубликую статьи на русском и английском и на основе публикаций нормальный README.md.

            Это бандл java 8 компилятора и загрузчика классов: janino commons-compiler-jdk + eclipse java compiler + maven dependency resolver. Фактически это javax.tools.JavaCompiler который есть в maven репозитарии и который работает в любой JRE и ему не нужен tools.jar
      • +1
        А можете таки разжевать дальше, раз статья предполагает знакомство и чтобы не лезть в документацию?
        Ну сгенерился для логгера `LogSample_Metacode ` и что дальше?
        1. куда он складывается? очевидно в стороне от сорсов, и в репозиторий не чекинится?
        2. в какой момент он отрабатывает и сетиит логгер?
        • +1
          Покопался таки в документации
          1. Да, JSR 269 Pluggable Annotation Processing API
          2. Из доки Jeta нужно еще создавать собственные Helper-ы и дергать их из кода проекта и они в рантайме будут вымолнять свой рефлекшен.

          Lombok после этого мне больше нравится. Особенно с поддержкой в IDEA процессинга аннотаций и декомпиляции, когда можно просто открыв class посмотреть, что стало с исхониками.
          • 0
            куда он складывается? очевидно в стороне от сорсов, и в репозиторий не чекинится?

            Зависит от apt plugin-а, как правило ./build/generated/source/apt/, в статье это есть.


            в какой момент он отрабатывает и сетиит логгер?

            Когда вы вызываете соответствующий контроллер.


            они в рантайме будут вымолнять свой рефлекшен.

            Никакого рефлекшена. Контроллер обращается в репозиторий (что по сути является HashMap), получает Metacode, и вызывает соответствующий метод.


            чтобы не лезть в документацию

            Иногда нужно просто взять и залесть в документацию.

      • 0

        Ага, 3 из 4 вопросов — это один и тот же "как оно работает?". У людей, не знакомых с annotation processing, этот же вопрос будет и про вашу либу. Менять байткод или генерить сорцы — уже следующий вопрос. Более того, подход с генерацией накладывает очень сильное ограничение — невозможность модификации своих классов. Рассмотрим, например, единственный пример с сайта (@Log). Если верить посту, то будет сгенерирован новый LogSample_Metacode, и потом еще надо будет руками вызвать у него apply, передав туда наш LogSample и еще какой-то провайдер, который тоже надо написать… Да еще и логгер внезапно стал публичным… В общем, нифига не равноценная замена private final (который еще и static обычно (хотя его-то как раз просто доделать в вашем варианте)).


        Ну а 4й вопрос — так у вас те же проблемы, а на мой взгляд, так даже хуже. Т.к. если с lombok без плагина идея никогда не видит новых классов/методов, то у вас она видит с опозданием ровно на один билд.

        • 0
          невозможность модификации своих классов

          Зачем вы хотите чтобы что-то модифицировалло чего-то в ваших исходниках? Почему вы не делаете это руками? Похоже на какой то boiler-plate code, который вы не хотите видить в своих исходниках. Тогда Jeta это то, о чем вы всегда мечтали.


          … надо будет руками вызвать у него apply, передав туда наш LogSample и еще какой-то провайдер...

          Один раз вы пишите провайдер. Один раз, в базовом класе, вы вызываете LogController, передаете туда this и забываете.


          ...private final

          Это да. Некоторые программисты только так и живут.


          … у вас она видит с опозданием ровно на один билд.

          Вам вообще не будет дела что и как там генерится. В вашем коде только аннотации. Остальное — забота библиотеки.

          • 0
            Похоже на какой то boiler-plate code, который вы не хотите видить в своих исходниках. Тогда Jeta это то, о чем вы всегда мечтали.

            Да, конечно же все ради избавления от бойлерплейта. Только вот Jeta, в отличии от Lombok, не избавляет меня от бойлерплейта, а просто меняет его.


            Вам вообще не будет дела что и как там генерится.

            Я про другое. Вопрос в том, какие классы/методы предлагает мне идея при комплишене. И, если я напишу вызов метода руками, считает ли она что такого метода нет и надо выделить его красным. В вашем варианте сразу после того, как я поставил аннотацию, идея не начнет автоматически видеть новый метод (он ведь еще не сгенерирован). Сначала надо сделать билд.

            • +2
              В вашем варианте сразу после того, как я поставил аннотацию, идея не начнет автоматически видеть новый метод (он ведь еще не сгенерирован). Сначала надо сделать билд.

              Разве с другой стороны это не плюс? Например, плагин lombok под идею поддерживает не все аннотации, да и при появлении новых будет задержка в обновлении плагина.
              А подход автора позволяет всегда иметь поддержку IDE.

          • +1

            Поймите правильно, я не говорю что вы сделали какое-то говно. На самом деле, для самообразовательных/развлекательных целей это интересная штука. Я спорю лишь с тем, что jeta для практического использования чем-то лучше lombok. В силу ограничений, накладываемых выбором технологии (генерация новых исходников вместо модификации байткода имеющихся классов).

          • 0

            А по поводу питона — на мой взгляд убирание убирание инкапсуляции из языка — минус, а не плюс.

  • +1
    избавляет от копи-пасты строки типа

    Отвратительный пример. Никто никогда не копипастит логгер. В идее с помощью smart completion данная строчка пишется значительно быстрее чем "найти класс, из которого копипастить; скопировать; вернуться в исходный класс; вставить; заменить класс внутри getLogger". А на самом деле, если потратить 5 минут времени, то можно написать live template и после этого объявлять логгер как "logger<TAB>". Так что в данном конкретном примере ваше решение только все переусложняет.

    • +1

      Я и не думал заманить Вас одной лишь возможностью писать


      @Log 
      Logger logger

      вместо


      private final Logger logger = LoggerFactory.getLogger(LogSample.class);

      Этот пример тут для илюстрации идеи, потенциала, если хотите. Если в вашем проекте есть темплейтный код, который Вам приходится постоянно писать, то точно так же как в примере с логгером Вы можете от него избавляться.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.