Использование аннотации является повседневной задачей для разработчика Java. Если ничего более простого, аннотация @Override
должна зазвонить. Создание аннотаций немного сложнее. Использование «самодельных» аннотаций во время выполнения с помощью отражения или создание обработчика аннотаций, вызываемых во время компиляции, снова представляет собой один уровень сложности. Но мы редко «реализуем» интерфейс аннотаций. Кто-то тайно, за кулисами, конечно, делает для нас.
Когда у нас есть аннотация:
1
2
3
4
5
|
@Retention (RetentionPolicy.RUNTIME) @Target (ElementType.TYPE) public @interface AnnoWithDefMethod { String value() default "default value string" ; } |
затем класс, аннотированный этой аннотацией
1
2
3
|
@AnnoWithDefMethod ( "my default value" ) public class AnnotatedClass { } |
и, наконец, мы, когда получаем аннотацию во время выполнения выполнения
1
|
AnnoWithDefMethod awdm = AnnotatedClass. class .getAnnotation(AnnoWithDefMethod. class ); |
тогда что мы получаем в переменную awdm
? Это объект. Объекты являются экземплярами классов, а не интерфейсами. Это означает, что кто-то под капотом среды выполнения Java «реализовал» интерфейс аннотации. Мы даже можем распечатать характеристики объекта:
1
2
3
4
5
6
7
|
System.out.println(awdm.value()); System.out.println(Integer.toHexString(System.identityHashCode(awdm))); System.out.println(awdm.getClass()); System.out.println(awdm.annotationType()); for (Method m : awdm.getClass().getDeclaredMethods()) { System.out.println(m.getName()); } |
чтобы получить результат что-то вроде
1
2
3
4
5
6
7
8
9
|
my default value 60e53b93 class com.sun.proxy.$Proxy1 interface AnnoWithDefMethod value equals toString hashCode annotationType |
Таким образом, нам не нужно реализовывать интерфейс аннотаций, но мы можем, если захотим. Но зачем нам это? До сих пор я встречал одну ситуацию, в которой это было решением: настройка внедрения зависимостей.
Guice — это DI-контейнер Google. Конфигурация привязки дается в виде Java-кода декларативным образом, как описано на странице документации . Вы можете привязать тип к реализации, просто объявив
1
|
bind(TransactionLog. class ).to(DatabaseTransactionLog. class ); |
так что все TransactionLog
экземпляры TransactionLog
будут иметь DatabaseTransactionLog
. Если вы хотите, чтобы различные имплантации вставлялись в разные поля в вашем коде, вы должны каким-то образом сообщить об этом Guice, например, создать аннотацию, поместить аннотацию в поле или аргумент конструктора и объявить
1
2
3
|
bind(CreditCardProcessor. class ) .annotatedWith(PayPal. class ) .to(PayPalCreditCardProcessor. class ); |
Это требует, чтобы PayPal
был интерфейсом аннотации, и вам необходимо написать новый интерфейс аннотации, сопровождающий каждую реализацию CreditCardProcessor
или даже больше, чтобы вы могли сигнализировать и отделять тип реализации в конфигурации привязки. Это может быть излишним, просто слишком много классов аннотаций.
Вместо этого вы также можете использовать имена. Вы можете аннотировать цель внедрения с помощью аннотации @Named("CheckoutPorcessing")
и настраивать привязку
1
2
3
|
bind(CreditCardProcessor. class ) .annotatedWith(Names.named( "CheckoutProcessing" )) .to(CheckoutCreditCardProcessor. class ); |
Это техника, которая хорошо известна и широко используется в DI-контейнерах. Вы указываете тип (интерфейс), создаете реализации и, наконец, определяете тип привязки, используя имена. В этом нет никаких проблем, за исключением того, что трудно заметить, когда вы печатаете porcessing вместо обработки. Такая ошибка остается скрытой до тех пор, пока не произойдет сбой привязки (времени выполнения). Вы не можете просто использовать final static String
для хранения фактического значения, поскольку ее нельзя использовать в качестве параметра аннотации. Вы можете использовать такое постоянное поле в определении привязки, но это все еще дублирование.
Идея состоит в том, чтобы использовать что-то другое вместо String. То, что проверено компилятором. Очевидный выбор — использовать класс. Для реализации этого кода можно создать обучение на основе кода NamedImpl
, который является классом, реализующим интерфейс аннотации . Код выглядит примерно так (Примечание: Klass
— это интерфейс аннотации, который здесь не указан):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
class KlassImpl implements Klass { Class<? extends Annotation> annotationType() { return Klass. class } static Klass klass(Class value){ return new KlassImpl(value: value) } public boolean equals(Object o) { if (!(o instanceof Klass)) { return false ; } Klass other = (Klass)o; return this .value.equals(other.value()); } public int hashCode() { return 127 * "value" .hashCode() ^ value.hashCode(); } Class value @Override Class value() { return value } } |
Фактическая привязка будет выглядеть примерно так
1
2
3
4
5
6
7
8
9
|
@Inject public RealBillingService( @Klass (CheckoutProcessing. class ) CreditCardProcessor processor, TransactionLog transactionLog) { ... } bind(CreditCardProcessor. class ) .annotatedWith(Klass.klass(CheckoutProcessing. class )) .to(CheckoutCreditCardProcessor. class ); |
В этом случае любая опечатка может быть обнаружена компилятором. Что происходит на самом деле за кулисами, и почему нас попросили реализовать интерфейс аннотаций?
Когда привязка настроена, мы предоставляем объект. Вызов Klass.klass(CheckoutProcessing.class)
создаст экземпляр KlassImpl
и когда Guice попытается решить, является ли действительная конфигурация привязки действительной для привязки CheckoutCreditCardProcessor
к аргументу RealBillingService
в конструкторе RealBillingService
он просто вызывает метод equals()
для объект аннотации. Если экземпляр, созданный средой выполнения Java (помните, что среда выполнения Java создает экземпляр с именем, подобным class com.sun.proxy.$Proxy1
), и предоставленный нами экземпляр равны, тогда используется конфигурация привязки, в противном случае некоторые другие привязки должны совпадение.
Есть еще один улов. Недостаточно реализовать equals()
. Вы можете (и если вы являетесь Java-программистом (и почему вы еще читаете эту статью (вы, конечно, не программист на LISP)), вы также должны помнить, что если вы переопределяете equals()
вы должны переопределить также hashCode()
, И на самом деле вы должны предоставить реализацию, которая выполняет те же вычисления, что и класс, созданный во время выполнения Java. Причина этого заключается в том, что приложение не может выполнить сравнение напрямую. Может случиться (и это случается), что Guice ищет объекты аннотаций на карте. В этом случае хеш-код используется для идентификации сегмента, в котором должен быть сравниваемый объект, а метод equals()
впоследствии используется для проверки идентичности. Если метод hashCode()
возвращает другое число в случае созданной среды выполнения Java и объектов out, они даже не будут совпадать. equals()
вернул бы true, но он никогда не вызывается для них, потому что объект не найден на карте.
Фактический алгоритм для метода hashCode
описан в документации интерфейса java.lang.annotation
. Я уже видел эту документацию раньше, но понял причину, по которой алгоритм был определен, когда я впервые использовал Guice и реализовал аналогичный класс реализации интерфейса аннотаций.
И последнее, что класс также должен реализовывать annotationType()
. Почему? Если я когда-нибудь это пойму, я напишу об этом.
Ссылка: | Внедрение интерфейса аннотации от нашего партнера JCG Питера Верхаса в блоге Java Deep . |