web-dev-qa-db-ja.com

注釈で配列定数を使用する方法

注釈値に定数を使用したいと思います。

interface Client {

    @Retention(RUNTIME)
    @Target(METHOD)
    @interface SomeAnnotation { String[] values(); }

    interface Info {
        String A = "a";
        String B = "b";
        String[] AB = new String[] { A, B };
    }

    @SomeAnnotation(values = { Info.A, Info.B })
    void works();

    @SomeAnnotation(values = Info.AB)
    void doesNotWork();
}

定数Info.AおよびInfo.Bは注釈で使用できますが、配列Info.ABはこの場所で配列初期化子である必要があるため使用できません。注釈の値は、クラスのバイトコードにインライン化できる値に制限されています。 Infoがロードされるときに構築する必要があるため、これは配列定数では不可能です。この問題の回避策はありますか?

48
Thomas Jung

いいえ、回避策はありません。

45
Thomas Jung

注釈値を列挙型にすると、実際のデータ値のキーになりますか?

例えば.

enum InfoKeys
{
 A("a"),
 B("b"),
 AB(new String[] { "a", "b" }),

 InfoKeys(Object data) { this.data = data; }
 private Object data;
}

@SomeAnnotation (values = InfoKeys.AB)

これはタイプセーフのために改善される可能性がありますが、アイデアは得られます。

14
amarillion

配列を渡す方法はありません直接注釈パラメーター値として、そこにis効果的に同様の動作を得る方法があります(注釈の使用方法に応じて、これはすべてのユースケースで機能するとは限りません)。

次に例を示します。クラスInternetServerがあり、hostnameプロパティがあるとします。通常のJava Validationを使用して、オブジェクトが「予約済み」ホスト名を持たないことを確認します。予約済みホスト名の配列をホスト名検証を処理する注釈に渡すことができます。

警告:Java検証、この種のデータを渡すには「ペイロード」を使用するのがより一般的です。この例をもう少し汎用的にしたいので、カスタムインターフェイスを使用しましたクラス。

// InternetServer.Java -- an example class that passes an array as an annotation value
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.Pattern;

public class InternetServer {

    // These are reserved names, we don't want anyone naming their InternetServer one of these
    private static final String[] RESERVED_NAMES = {
        "www", "wwws", "http", "https",
    };

    public class ReservedHostnames implements ReservedWords {
        // We return a constant here but could do a DB lookup, some calculation, or whatever
        // and decide what to return at run-time when the annotation is processed.
        // Beware: if this method bombs, you're going to get nasty exceptions that will
        // kill any threads that try to load any code with annotations that reference this.
        @Override public String[] getReservedWords() { return RESERVED_NAMES; }
    }

    @Pattern(regexp = "[A-Za-z0-9]{3,}", message = "error.hostname.invalid")
    @NotReservedWord(reserved=ReservedHostnames.class, message="error.hostname.reserved")
    @Getter @Setter private String hostname;
}

// NotReservedWord.Java -- the annotation class
import javax.validation.Constraint;
import javax.validation.Payload;
import Java.lang.annotation.Documented;
import Java.lang.annotation.Retention;
import Java.lang.annotation.Target;

import static Java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static Java.lang.annotation.ElementType.FIELD;
import static Java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({FIELD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy=ReservedWordValidator.class)
@Documented
public @interface NotReservedWord {

    Class<? extends ReservedWords> reserved ();

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String message() default "{err.reservedWord}";

}

// ReservedWords.Java -- the interface referenced in the annotation class
public interface ReservedWords {
    public String[] getReservedWords ();
}

// ReservedWordValidator.Java -- implements the validation logic
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import Java.util.Map;
import Java.util.concurrent.ConcurrentHashMap;

public class ReservedWordValidator implements ConstraintValidator<NotReservedWord, Object> {

    private Class<? extends ReservedWords> reserved;

    @Override
    public void initialize(NotReservedWord constraintAnnotation) {
        reserved = constraintAnnotation.reserved();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        if (value == null) return true;
        final String[] words = getReservedWords();
        for (String Word : words) {
            if (value.equals(Word)) return false;
        }
        return true;
    }

    private Map<Class, String[]> cache = new ConcurrentHashMap<>();

    private String[] getReservedWords() {
        String[] words = cache.get(reserved);
        if (words == null) {
            try {
                words = reserved.newInstance().getReservedWords();
            } catch (Exception e) {
                throw new IllegalStateException("Error instantiating ReservedWords class ("+reserved.getName()+"): "+e, e);
            }
            cache.put(reserved, words);
        }
        return words;
    }
}
2
cobbzilla

これは、コンパイル時に注釈値が一定であるのに、実行時に配列の要素を変更できる(Info.AB[0] = "c";)ためです。

そのことを念頭に置いて、誰かがInfo.ABの要素を変更しようとして混乱することは避けられず、アノテーションの値が変更されることを期待します(変更されません)。また、実行時に注釈値を変更できる場合、コンパイル時に使用される値とは異なります。混乱を想像してみてください!

(ここで混乱は、誰かがバグを発見したことを意味しますmayデバッグを見つけて何時間も費やします。)

1

以前の投稿ですでに述べたように、注釈値はコンパイル時の定数であり、配列値をパラメーターとして使用する方法はありません。

私はこの問題を少し異なって解決しました。

処理ロジックを所有している場合は、それを利用してください。

たとえば、注釈に追加のパラメーターを指定します。

_@Retention(RUNTIME)
@Target(METHOD)
@interface SomeAnnotation { 
    String[] values();
    boolean defaultInit() default false;
}
_

このパラメーターを使用します。

_@SomeAnnotation(defaultInit = true)
void willWork();
_

そして、これはAnnotationProcessorのマーカーになります。これは何でもできます-配列で初期化するか、_String[]_を使用するか、Enum.values()のようなEnumsを使用します。それらを_String[]_にマップします。

これが同様の状況にある人を正しい方向に導くことを願っています。

0
J-Alex