А как в Java реализуется условная компиляция?
Скажем, у меня есть TCP сервер. Мне нужно на этапе отладки складывать в файл пришедшие пакеты. Потом, в релиз версии, этот функционал нужно будет отключить.
Как такое принято реализовывать? В Си это делается так
#ifdef DEBUG
savePacket();
#endif
а в Java?
Есть ещё один способ эмулировать макросы препроцессора - процессоры аннотаций + 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. И возможно, напишу ещё третий ответ - про написание плагинов для компилятора.
В 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", несмотря на то, что код остаётся неизменным.
Сборка персонального компьютера от Artline: умный выбор для современных пользователей