Je suis sur le point de commettre un piratage temporaire moche afin de contourner un problème de blocage en attendant la résolution d'une ressource externe. En plus de le marquer avec un gros commentaire effrayant et un groupe de FIXME, j'aimerais que le compilateur envoie un message d'avertissement évident comme rappel afin que nous n'oubliions pas de le supprimer. Par exemple, quelque chose comme:
[javac] com.foo.Hacky.Java:192: warning: FIXME temporary hack to work around library bug, remove me when library is fixed!
Est-il possible de provoquer un avertissement intentionnel du compilateur avec un message de mon choix? En cas d'échec, quelle est la chose la plus facile à ajouter au code pour lancer un avertissement existant, avec éventuellement un message dans une chaîne sur la ligne incriminée afin qu'il soit imprimé dans le message d'avertissement?
EDIT: Les tags obsolètes ne semblent rien faire pour moi:
/**
* @deprecated "Temporary hack to work around remote server quirks"
*/
@Deprecated
private void doSomeHackyStuff() { ... }
Aucune erreur de compilation ou d'exécution dans Eclipse ou Sun Javac 1.6 (en cours d'exécution depuis un script ant), et la fonction est définitivement exécutée.
Une technique que j'ai déjà utilisée consiste à intégrer cela au test unitaire (vous faites test unitaire, non?). En gros, vous créez un test unitaire qui échoue une fois le correctif des ressources externes atteint. Ensuite, vous commentez ce test unitaire pour dire aux autres comment effacer votre piratage informatique une fois le problème résolu.
Ce qui est vraiment génial avec cette approche est que le déclencheur pour annuler votre piratage informatique est une solution au problème principal lui-même.
Je pense qu'une annotation personnalisée, qui sera traitée par le compilateur, est la solution. J'écris souvent des annotations personnalisées pour faire des choses au moment de l'exécution, mais je n'ai jamais essayé de les utiliser au moment de la compilation. Je ne peux donc que vous donner des indications sur les outils dont vous pourriez avoir besoin:
Je ne sais pas si cette solution est vraiment réalisable. Je vais essayer de le mettre en œuvre moi-même quand je trouverai du temps.
Modifier
J'ai implémenté avec succès ma solution. Et en prime, j'ai utilisé le service fournisseur de services de Java pour en simplifier l'utilisation. En fait, ma solution est un fichier jar contenant 2 classes: l'annotation personnalisée et le processeur d'annotation. Pour l'utiliser, ajoutez simplement ce fichier dans le chemin de classe de votre projet et annotez ce que vous voulez! Cela fonctionne très bien dans mon IDE (NetBeans).
Code de l'annotation:
package fr.barjak.hack;
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.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.LOCAL_VARIABLE, ElementType.METHOD, ElementType.PACKAGE, ElementType.PARAMETER, ElementType.TYPE})
public @interface Hack {
}
Code du processeur:
package fr.barjak.hack_processor;
import Java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;
@SupportedAnnotationTypes("fr.barjak.hack.Hack")
public class Processor extends AbstractProcessor {
private ProcessingEnvironment env;
@Override
public synchronized void init(ProcessingEnvironment pe) {
this.env = pe;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
for (TypeElement te : annotations) {
final Set< ? extends Element> elts = roundEnv.getElementsAnnotatedWith(te);
for (Element elt : elts) {
env.getMessager().printMessage(Kind.WARNING,
String.format("%s : thou shalt not hack %s", roundEnv.getRootElements(), elt),
elt);
}
}
}
return true;
}
}
Pour activer le fichier JAR résultant en tant que fournisseur de services, ajoutez le fichier META-INF/services/javax.annotation.processing.Processor
dans le fichier JAR. Ce fichier est un fichier acsii qui doit contenir le texte suivant:
fr.barjak.hack_processor.Processor
Une approche rapide et pas si sale peut consister à utiliser une annotation @SuppressWarnings
avec un argument délibérément incorrect String
:
@SuppressWarnings("FIXME: this is a hack and should be fixed.")
Cela générera un avertissement car le compilateur ne le reconnaît pas comme un avertissement spécifique à supprimer:
@SuppressWarnings non pris en charge ("FIXME: ceci est un hack et devrait être Corrigé.")
Un bon hack en mérite un autre ... Je génère généralement des avertissements du compilateur dans le but décrit en introduisant une variable inutilisée dans la méthode hacky, ainsi: /**
* @deprecated "Temporary hack to work around remote server quirks"
*/
@Deprecated
private void doSomeHackyStuff() {
int FIXMEtemporaryHackToWorkAroundLibraryBugRemoveMeWhenLibraryIsFixed;
...
}
Cette solution n’est pas aussi agréable qu’une annotation personnalisée, mais elle présente l’avantage de ne nécessiter aucune préparation préalable (en supposant que le compilateur soit déjà configuré pour émettre des avertissements pour les variables non utilisées). Je suggérerais que cette approche ne convient que pour les piratages de courte durée. Pour les hackers de longue durée, je dirais que l’effort de créer une annotation personnalisée serait justifié.
J'ai écrit une bibliothèque qui le fait avec des annotations: Lightweight Javac @Warning Annotation
L'utilisation est très simple:
// some code...
@Warning("This method should be refactored")
public void someCodeWhichYouNeedAtTheMomentButYouWantToRefactorItLater() {
// bad stuff going on here...
}
Et le compilateur lancera un message d'avertissement avec votre texte
que diriez-vous de marquer la méthode ou la classe comme @Deprecated? docs ici . Notez qu'il existe à la fois une @Deprecated et une @deprecated - la version D principale est l'annotation et la minuscule d est la version javadoc. La version javadoc vous permet de spécifier une chaîne arbitraire expliquant ce qui se passe. Mais les compilateurs ne sont pas obligés d'émettre un avertissement lorsqu'ils le voient (bien que beaucoup le fassent). L'annotation devrait toujours provoquer un avertissement, même si je ne pense pas que vous puissiez y ajouter une explication.
UPDATE voici le code que je viens de tester avec: Sample.Java contient:
public class Sample {
@Deprecated
public static void foo() {
System.out.println("I am a hack");
}
}
SampleCaller.Java contient:
public class SampleCaller{
public static void main(String [] args) {
Sample.foo();
}
}
quand je lance "javac Sample.Java SampleCaller.Java", j'obtiens le résultat suivant:
Note: SampleCaller.Java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
J'utilise le javac 1.6 de Sun. Si vous souhaitez un avertissement honnête, pas une simple note, utilisez l’option -Xlint. Peut-être que cela se répercutera correctement à travers Ant.
Nous pouvons le faire avec des annotations!
Pour générer une erreur, utilisez Messager
pour envoyer un message avec Diagnostic.Kind.ERROR
. Petit exemple:
processingEnv.getMessager().printMessage(
Diagnostic.Kind.ERROR, "Something happened!", element);
Voici une annotation assez simple que j'ai écrite juste pour tester cela.
Cette annotation @Marker
indique que la cible est une interface de marqueur:
package marker;
import Java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Marker {
}
Et le processeur d'annotation provoque une erreur si ce n'est pas le cas:
package marker;
import javax.annotation.processing.*;
import javax.lang.model.*;
import javax.lang.model.element.*;
import javax.lang.model.type.*;
import javax.lang.model.util.*;
import javax.tools.Diagnostic;
import Java.util.Set;
@SupportedAnnotationTypes("marker.Marker")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public final class MarkerProcessor extends AbstractProcessor {
private void causeError(String message, Element e) {
processingEnv.getMessager()
.printMessage(Diagnostic.Kind.ERROR, message, e);
}
private void causeError(
Element subtype, Element supertype, Element method) {
String message;
if (subtype == supertype) {
message = String.format(
"@Marker target %s declares a method %s",
subtype, method);
} else {
message = String.format(
"@Marker target %s has a superinterface " +
"%s which declares a method %s",
subtype, supertype, method);
}
causeError(message, subtype);
}
@Override
public boolean process(
Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
Elements elementUtils = processingEnv.getElementUtils();
boolean processMarker = annotations.contains(
elementUtils.getTypeElement(Marker.class.getName()));
if (!processMarker)
return false;
for (Element e : roundEnv.getElementsAnnotatedWith(Marker.class)) {
ElementKind kind = e.getKind();
if (kind != ElementKind.INTERFACE) {
causeError(String.format(
"target of @Marker %s is not an interface", e), e);
continue;
}
if (kind == ElementKind.ANNOTATION_TYPE) {
causeError(String.format(
"target of @Marker %s is an annotation", e), e);
continue;
}
ensureNoMethodsDeclared(e, e);
}
return true;
}
private void ensureNoMethodsDeclared(
Element subtype, Element supertype) {
TypeElement type = (TypeElement) supertype;
for (Element member : type.getEnclosedElements()) {
if (member.getKind() != ElementKind.METHOD)
continue;
if (member.getModifiers().contains(Modifier.STATIC))
continue;
causeError(subtype, supertype, member);
}
Types typeUtils = processingEnv.getTypeUtils();
for (TypeMirror face : type.getInterfaces()) {
ensureNoMethodsDeclared(subtype, typeUtils.asElement(face));
}
}
}
Par exemple, ce sont des utilisations correctes de @Marker
:
@Marker
interface Example {}
@Marker
interface Example extends Serializable {}
Mais ces utilisations de @Marker
provoqueront une erreur de compilation:
@Marker
class Example {}
@Marker
interface Example {
void method();
}
Voici un article de blog que j'ai trouvé très utile pour commencer sur le sujet:
Petite remarque: le commentaire ci-dessous indique que, étant donné que MarkerProcessor
fait référence à Marker.class
, il a une dépendance à la compilation. J'ai écrit l'exemple ci-dessus en supposant que les deux classes iraient dans le même fichier JAR (par exemple, marker.jar
), mais ce n'est pas toujours possible.
Par exemple, supposons qu’il existe un JAR d’application avec les classes suivantes:
com.acme.app.Main
com.acme.app.@Ann
com.acme.app.AnnotatedTypeA (uses @Ann)
com.acme.app.AnnotatedTypeB (uses @Ann)
Ensuite, le processeur pour @Ann
existe dans un fichier JAR séparé, utilisé lors de la compilation du fichier JAR de l'application:
com.acme.proc.AnnProcessor (processes @Ann)
Dans ce cas, AnnProcessor
ne pourrait pas faire directement référence au type @Ann
, car cela créerait une dépendance JAR circulaire. Il ne pourrait référencer que @Ann
par String
name ou TypeElement
/TypeMirror
.
Ici montre un tutoriel sur les annotations et en bas, un exemple de définition de vos propres annotations. Malheureusement, un rapide survol du tutoriel indique que ceux-ci ne sont disponibles que dans le javadoc ...
Annotations utilisées par le compilateur Il existe trois types d'annotation qui sont prédéfinis par la spécification de langue elle-même: @Deprecated, @Override et @SuppressWarnings.
Il semble donc que tout ce que vous pouvez réellement faire est d’ajouter une balise @Deprecated que le compilateur imprimera ou de placer une balise personnalisée dans les javadocs qui indique le hack.
Vous devriez utiliser un outil de compilation, comme ant ou maven. Lors de la compilation, vous devez définir certaines tâches pouvant générer des journaux (tels que des messages ou des avertissements) concernant vos balises FIXME, par exemple.
Et si vous voulez des erreurs, c'est aussi possible. Comme arrêter la compilation quand vous avez laissé du TODO dans votre code (pourquoi pas?)
Pour faire apparaître un quelconque avertissement, j’ai constaté que les variables inutilisées et les @SuppressWarnings personnalisés ne fonctionnaient pas pour moi, mais un casting inutile ne fonctionnait pas:
public class Example {
public void warn() {
String fixmePlease = (String)"Hello";
}
}
Maintenant, quand je compile:
$ javac -Xlint:all Example.Java
ExampleTest.Java:12: warning: [cast] redundant cast to String
String s = (String) "Hello!";
^
1 warning
Si vous utilisez IntelliJ. Vous pouvez aller à: Préférences> Editeur> TODO et ajouter "\ bhack.b *" ou n’importe quel autre motif.
Si vous faites ensuite un commentaire comme // HACK: temporary fix to work around server issues
Ensuite, dans la fenêtre de l’outil TODO, il s’affichera bien, ainsi que tous vos autres modèles définis, lors de l’édition.