Условная компиляция

178
20 июля 2018, 00:40

А как в Java реализуется условная компиляция?

Скажем, у меня есть TCP сервер. Мне нужно на этапе отладки складывать в файл пришедшие пакеты. Потом, в релиз версии, этот функционал нужно будет отключить.

Как такое принято реализовывать? В Си это делается так

#ifdef DEBUG
  savePacket();
#endif

а в Java?

Answer 1

Есть ещё один способ эмулировать макросы препроцессора - процессоры аннотаций + Java Compiler API. Этот способ сложнее, но не требует дополнительных библиотек и не выполняет никаких дополнительных действий в рантайме, все изменения в код вносятся в процессе его компиляции, между этапом разбора исходного текста и преобразования его в байткод.

Аннотация для активации отладочных действий:

package com.example;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)  // Аннотация существует только до компиляции
@Target(ElementType.METHOD)
public @interface Debug {
    // Чтобы отключать аннотацию, не убирая её
    // но можно и без этого
    boolean value() default true;
}

Жертва эксперимента:

import com.example.Debug;
public class SomeServer {
    private void savePacket() {
        System.out.println("Packet saved");
    }
    @Debug
    private void receivePacket() {
        System.out.println("Packet received");
    }
    public static void main(String[] args) {
        SomeServer srv = new SomeServer();
        srv.receivePacket();
    }
}

Процессор:

package com.example;
import com.sun.tools.javac.model.JavacElements;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.List;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.annotation.processing.SupportedOptions;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
@SupportedOptions("debug")
@SupportedAnnotationTypes("com.example.Debug")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class DebugProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (roundEnv.processingOver())
            return false;
        // Получаем параметр указывающий на отладочную сборку
        boolean enabled = Boolean.parseBoolean(
          processingEnv.getOptions().getOrDefault("debug", "false"));
        Context ctx = ((JavacProcessingEnvironment) processingEnv).getContext();
        JavacElements elementUtils = (JavacElements) processingEnv.getElementUtils();
        TreeMaker treeMaker = TreeMaker.instance(ctx);
        // Обходим методы помеченные аннотацией @Debug
        for (Element element : roundEnv.getElementsAnnotatedWith(Debug.class)) {
            Debug debug = element.getAnnotation(Debug.class);
            if (debug.value() && enabled) {
                JCTree.JCMethodDecl jcMethodDecl = (JCTree.JCMethodDecl) elementUtils.getTree(element);
                // И добавляем в конец метода вызов другого метода
                treeMaker.pos = jcMethodDecl.pos;
                jcMethodDecl.body = treeMaker.Block(0, List.of(
                    jcMethodDecl.body,
                    treeMaker.Exec(
                        treeMaker.Apply(
                            List.<JCTree.JCExpression>nil(),
                            treeMaker.Select(
                                treeMaker.Ident(
                                    elementUtils.getName("this")
                                ),
                                elementUtils.getName("savePacket")
                            ),
                            List.<JCTree.JCExpression>nil()
                        )
                    )
                ));
            }
        }
        return false;
    }
}

Собираем аннотацию

$ javac -d build Debug.java

Собираем процессор

$ javac -cp build:"$JAVA_HOME/lib/tools.jar" -d build DebugProcessor.java

А теперь собираем SomeServer с использованием процессора

$ javac -cp build -processor com.example.DebugProcessor -Adebug=true SomeServer.java

Или можно собрать сервисный jar и положить в classpath, чтобы процессор использовался автоматически. Для этого DebugProcessor.class надо упаковать в jar вместе с файлом META-INF/services/javax.annotation.processing.Processor содержащим строку com.example.DebugProcessor. Тогда при сборке останется только указывать параметр -Adebug=true, когда нужны отладочные действия, и просто не указывать в другом случае.

P.S. По-хорошему надо было ещё метод savePacket тоже генерировать на лету, но я несколько притомился. Может дополню ответ в другой день.
P.P.S. И возможно, напишу ещё третий ответ - про написание плагинов для компилятора.

Answer 2

В Java так просто не делают. Если уж очень надо, то самое лучшее (из того, что пришло мне в голову) - это АОП.

UPDATE: Покажу пример использования АОП с AspectJ. Для наглядности не буду использовать ни внедрения аспектов контейнером, ни связывания Maven'ом, всё ручками.

Аннотация для активации отладочных действий:

package com.example;
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.METHOD)
public @interface Debug {}

Жертва эксперимента:

package com.example;
public class SomeServer {
    @Debug
    public void receivePacket() {
        System.out.println("Packet received");
    }
    public static void main(String[] args) {
        SomeServer srv = new SomeServer();
        srv.receivePacket();
    }
}

Аспект:

package com.example;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.JoinPoint;
aspect DebugAspect {
    // Добавляем в класс сервера метод
    // выполняющий отладочные действия
    private void SomeServer.savePacket() {
        System.out.println("Packet saved");
    }
    // Вызываем добавленный метод при выполнении
    // любого метода помеченного аннотацией @Debug
    @After("@annotation(Debug) && execution(* *(..))")
    public void after(JoinPoint joinPoint) {
        SomeServer srv = (SomeServer) joinPoint.getTarget();
        srv.savePacket();
    }
}

Создаём в каталоге с исходными файлами подкаталог deps и скачиваем в него aspectjrt-1.9.1.jar, aspectjtools-1.9.1.jar и aspectjweaver-1.9.1.jar.

Выполняем сборку со связыванием

$ java -cp deps/* org.aspectj.tools.ajc.Main -d build -source 1.8 -target 1.8 DebugAspect.aj Debug.java SomeServer.java

Запускаем

$ java -cp deps/* com.example.SomeServer

и получаем

Packet received
Packet saved

Если выполнить сборку без связывания (или убрать аннотацию @Debug), будет выведена только строка "Packet received", несмотря на то, что код остаётся неизменным.

READ ALSO
Java RegExpr заменить все вхождения

Java RegExpr заменить все вхождения

нужно заменить данное выражение на одно replaceAll с несколькими вхождениями, то есть либо &nbsp;, либо </td>, либо <td>

188
Построение ФРТ по данным сенсоров

Построение ФРТ по данным сенсоров

Имеются показания гироскопа и акселерометра, как из них построить ФРТ? ФРТ планируется использовать для деконволюции изображения методом...

158
Бинарный(двоичный) поиск

Бинарный(двоичный) поиск

Пишу домашку, нужно написать поиск, который будет выбирать границы и искать в той части массива, что может идти не так в моём коде?

216
Как исправить на сайте ошибки валидации wc3?

Как исправить на сайте ошибки валидации wc3?

На сайте есть 3 ошибки валидации WC3 - как их исправить?

191