Статьи

Учебник: написание собственного расширения CDI

Сегодня я покажу вам, как написать расширение CDI.

CDI обеспечивает простой способ расширения функциональности, например,

  • добавление собственных областей,
  • включение базовых классов Java для расширения,
  • использование метаданных аннотации для дополнения или модификации,
  • и многое другое.

В этом уроке мы реализуем расширение, которое будет вставлять свойства из файла свойств, как обычно я предоставлю исходники на github [Update: sources @ github ].

Цель

Предоставление расширения, которое позволяет нам делать следующее

1
2
3
4
5
6
7
8
@PropertyFile("myProps.txt")
public class MyProperties {
  @Property("version")
  Integer version;
  
  @Property("appname")
  String appname;
}

где версия и имя приложения определены в файле myProps.txt .

подготовка

Сначала нам нужна зависимость API CDI

1
dependencies.compile "javax.enterprise:cdi-api:1.1-20130918"

Теперь мы можем начать. Итак, начнем

Намокнуть

Основы

Точкой входа для каждого расширения CDI является класс, который реализует javax.enterprise.inject.spi.Extension

1
2
3
4
5
6
package com.coderskitchen.propertyloader;
  
import javax.enterprise.inject.spi.Extension;
public class PropertyLoaderExtension implements Extension {
//  More code later
}

Кроме того, мы должны добавить полное имя этого класса в файл с именем javax.enterprise.inject.spi.Extension в каталоге META-INF / services .

javax.enterprise.inject.spi.Extension

1
2
     
com.coderskitchen.propertyloader.PropertyLoaderExtension

Это основные шаги для написания расширения CDI.

Исходная информация
CDI использует архитектуру провайдера услуг Java SE, поэтому нам нужно реализовать интерфейс маркера и добавить файл с FQN реализующего класса.

Погружение глубже

Теперь мы должны выбрать правильное событие для прослушивания.

Исходная информация
Спецификации CDI определяют несколько событий, которые запускаются контейнером во время инициализации приложения.
Например, BeforeBeanDiscovery запускается до запуска контейнера с обнаружением компонента.

Для этого урока нам нужно прослушать событие ProcessInjectionTarget . Это событие вызывается для каждого отдельного Java-класса, интерфейса или перечисления, которые обнаружены и могут быть созданы контейнером во время выполнения.
Итак, давайте добавим наблюдателя для этого события:

1
2
public <T> void initializePropertyLoading(final @Observes ProcessInjectionTarget<T> pit) {
}

ProcessInjectionTarget предоставляет доступ к базовому классу через метод getAnnotatedType и создаваемый экземпляр через getInjectionTarget. Мы используем annotatedType для получения аннотаций в классе, чтобы проверить, доступен ли @PropertyFile . Если нет, мы вернемся непосредственно как короткое замыкание.

Впоследствии InjectionTarget используется для перезаписи текущего поведения и установки значений из файла свойств.

1
2
3
4
5
6
public <T> void initializePropertyLoading(final @Observes ProcessInjectionTarget<T> pit) {
            AnnotatedType<T> at = pit.getAnnotatedType();
            if(!at.isAnnotationPresent(PropertyFile.class)) {
                    return;
            }
    }

Для этого урока мы предполагаем, что файл свойств расположен непосредственно в корне пути к классам. С этим предположением мы можем добавить следующий код для загрузки свойств из файла

01
02
03
04
05
06
07
08
09
10
PropertyFile propertyFile = at.getAnnotation(PropertyFile.class);
String filename = propertyFile.value();
InputStream propertiesStream = getClass().getResourceAsStream("/" + filename);
Properties properties = new Properties();
try {
    properties.load(propertiesStream);
    assignPropertiesToFields(at.getFields, properties); // Implementation follows
} catch (IOException e) {
    e.printStackTrace();
}

Теперь мы можем присвоить значения свойств полям. Но для CDI мы должны сделать это немного по-другому. Мы должны использовать InjectionTarget и переопределить текущий AnnotatedType. Это позволяет CDI гарантировать, что все может произойти в правильном порядке.

Для достижения этого мы используем окончательную карту <Field, Object>, где мы можем сохранить текущие назначения для последующего использования в InjectionTarget . Сопоставление выполняется в методе assignPropertiesToFields .

1
2
3
4
5
6
7
8
9
private <T> void assignPropertiesToFields(Set<AnnotatedField<? super T>> fields, Properties properties) {
        for (AnnotatedField<? super T> field : fields) {
            if(field.isAnnotationPresent(Property.class)) {
                Property property = field.getAnnotation(Property.class);
                String value = properties.getProperty(property.value());
                Type baseType = field.getBaseType();
                fieldValues.put(memberField, value);
            }
        }

В качестве последнего шага мы теперь создадим новый InjectionTarget, чтобы назначить значения поля всем вновь созданным экземплярам базового класса.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
final InjectionTarget<T> it = pit.getInjectionTarget();
    InjectionTarget<T> wrapped = new InjectionTarget<T>() {
      @Override
      public void inject(T instance, CreationalContext<T> ctx) {
        it.inject(instance, ctx);
        for (Map.Entry<Field, Object> property: fieldValues.entrySet()) {
          try {
            Field key = property.getKey();
            key.setAccessible(true);
            Class<?> baseType = key.getType();
            String value = property.getValue().toString();
            if (baseType == String.class) {
              key.set(instance, value);
            else if (baseType == Integer.class) {
              key.set(instance, Integer.valueOf(value));
            } else {
              pit.addDefinitionError(new InjectionException("Type " + baseType + " of Field " + key.getName() + " not recognized yet!"));
            }
          } catch (Exception e) {
            pit.addDefinitionError(new InjectionException(e));
          }
        }
      }
  
      @Override
      public void postConstruct(T instance) {
        it.postConstruct(instance);
      }
  
      @Override
      public void preDestroy(T instance) {
        it.dispose(instance);
      }
  
      @Override
      public void dispose(T instance) {
        it.dispose(instance);
      }
  
      @Override
      public Set<InjectionPoint> getInjectionPoints() {
        return it.getInjectionPoints();
      }
  
      @Override
      public T produce(CreationalContext<T> ctx) {
        return it.produce(ctx);
      }
    };
    pit.setInjectionTarget(wrapped);

Это все, что нужно для магии. Наконец, вот полный код ProperyLoaderExtension.

PropertyLoaderExtension

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
package com.coderskitchen.propertyloader;
  
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.InjectionException;
import javax.enterprise.inject.spi.AnnotatedField;
import javax.enterprise.inject.spi.AnnotatedType;
import javax.enterprise.inject.spi.Extension;
import javax.enterprise.inject.spi.InjectionPoint;
import javax.enterprise.inject.spi.InjectionTarget;
import javax.enterprise.inject.spi.ProcessInjectionTarget;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
  
public class PropertyLoaderExtension implements Extension {
  
  final Map<Field, Object> fieldValues = new HashMap<Field, Object>();
  
  public <T> void initializePropertyLoading(final @Observes ProcessInjectionTarget<T> pit) {
    AnnotatedType<T> at = pit.getAnnotatedType();
    if(!at.isAnnotationPresent(PropertyyFile.class)) {
      return;
    }
    PropertyyFile propertyyFile = at.getAnnotation(PropertyyFile.class);
    String filename = propertyyFile.value();
    InputStream propertiesStream = getClass().getResourceAsStream("/" + filename);
    Properties properties = new Properties();
    try {
      properties.load(propertiesStream);
      assignPropertiesToFields(at.getFields(), properties);
  
    } catch (IOException e) {
      e.printStackTrace();
    }
  
    final InjectionTarget<T> it = pit.getInjectionTarget();
    InjectionTarget<T> wrapped = new InjectionTarget<T>() {
      @Override
      public void inject(T instance, CreationalContext<T> ctx) {
        it.inject(instance, ctx);
        for (Map.Entry<Field, Object> property: fieldValues.entrySet()) {
          try {
            Field key = property.getKey();
            key.setAccessible(true);
            Class<?> baseType = key.getType();
            String value = property.getValue().toString();
            if (baseType == String.class) {
              key.set(instance, value);
            else if (baseType == Integer.class) {
              key.set(instance, Integer.valueOf(value));
            } else {
              pit.addDefinitionError(new InjectionException("Type " + baseType + " of Field " + key.getName() + " not recognized yet!"));
            }
          } catch (Exception e) {
            pit.addDefinitionError(new InjectionException(e));
          }
        }
      }
  
      @Override
      public void postConstruct(T instance) {
        it.postConstruct(instance);
      }
  
      @Override
      public void preDestroy(T instance) {
        it.dispose(instance);
      }
  
      @Override
      public void dispose(T instance) {
        it.dispose(instance);
      }
  
      @Override
      public Set<InjectionPoint> getInjectionPoints() {
        return it.getInjectionPoints();
      }
  
      @Override
      public T produce(CreationalContext<T> ctx) {
        return it.produce(ctx);
      }
    };
    pit.setInjectionTarget(wrapped);
  }
  
  private <T> void assignPropertiesToFields(Set<AnnotatedField<? super T>> fields, Properties properties) {
    for (AnnotatedField<? super T> field : fields) {
      if(field.isAnnotationPresent(Propertyy.class)) {
        Propertyy propertyy = field.getAnnotation(Propertyy.class);
        Object value = properties.get(propertyy.value());
        Field memberField = field.getJavaMember();
        fieldValues.put(memberField, value);
      }
    }
  }
}

Загрузки

Полный исходный код будет доступен на git hub до вечера понедельника. Архив jar доступен здесь PropertyLoaderExtension .

Финальные заметки

Этот урок показал, как просто вы можете добавить новые функции в структуру CDI. В нескольких строках кода был добавлен механизм загрузки и внедрения рабочих свойств. События, которые запускаются в течение жизненных циклов приложения, предоставляют слабосвязанный и мощный подход для добавления новых функций, перехвата создания компонента или изменения поведения.

Внедрение свойства также может быть достигнуто путем введения набора методов-производителей, но этот подход требует одного метода-производителя для каждого типа поля. При таком общем подходе вам просто нужно добавить новые конвертеры для включения ввода других типов значений.