Страницы

Поиск по вопросам

вторник, 31 декабря 2019 г.

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

#java


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

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

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

#ifdef DEBUG
  savePacket();
#endif


а в Java?
    


Ответы

Ответ 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 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.nil(), treeMaker.Select( treeMaker.Ident( elementUtils.getName("this") ), elementUtils.getName("savePacket") ), List.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. И возможно, напишу ещё третий ответ - про написание плагинов для компилятора.

Ответ 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", несмотря на то, что код остаётся неизменным.

Комментариев нет:

Отправить комментарий