Eine Spring Custom Annotation für eine bessere DAO

1. Überblick

In diesem Lernprogramm implementieren Sie eine benutzerdefinierte Spring-Anmerkung mit einem Bean-Post-Prozessor .

Wie hilft das? Einfach ausgedrückt - wir können dieselbe Bean wiederverwenden, anstatt mehrere ähnliche Beans desselben Typs erstellen zu müssen.

Wir machen das für die DAO-Implementierungen in einem einfachen Projekt - alle durch ein einziges, flexibles GenericDao zu ersetzen

2. Maven

Wir brauchen spring-core -, spring-aop - und spring-context-support -JARs, damit dies funktioniert. Wir können spring-context-support einfach in unserer pom.xml deklarieren.

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>4.2.2.RELEASE</version>
</dependency>

Wenn Sie eine neuere Version der Spring-Abhängigkeit suchen, besuchen Sie https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.springframework%22%20AND%20a%3A % 22spring-context-support% 22[das Maven-Repository].

** 3. Neuer generischer DAO

**

Die meisten Spring/JPA/Hibernate-Implementierungen verwenden den Standard-DAO - normalerweise einen für jede Entität.

Wir werden diese Lösung durch einen GenericDao ersetzen. Wir werden stattdessen einen benutzerdefinierten Anmerkungsprozessor schreiben und diese GenericDao -Implementierung verwenden:

3.1. Generisches DAO

public class GenericDao<E> {

    private Class<E> entityClass;

    public GenericDao(Class<E> entityClass) {
        this.entityClass = entityClass;
    }

    public List<E> findAll() {
       //...
    }

    public Optional<E> persist(E toPersist) {
       //...
    }
}

In einem realen Szenario müssen Sie natürlich ein http://docs.oracle.com/javaee/7/api/javax/persistence/PersistenceContext.html (PersistenceContext]) anschließen und die Implementierungen dieser Methoden tatsächlich bereitstellen . Im Moment machen wir das so einfach wie möglich.

Jetzt können Sie Anmerkungen für die benutzerdefinierte Injektion erstellen.

3.2. Datenzugriff

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Documented
public @interface DataAccess {
    Class<?> entity();
}

Wir verwenden die obige Anmerkung, um einen GenericDao wie folgt einzufügen:

@DataAccess(entity=Person.class)
private GenericDao<Person> personDao;

Vielleicht fragen einige von Ihnen: "Wie erkennt Spring unsere DataAccess -Annotation?". Es ist nicht - nicht standardmäßig.

Aber wir könnten Spring anweisen, die Anmerkung über eine benutzerdefinierte BeanPostProcessor Lassen Sie uns dies als nächstes implementieren.

3.3. DataAccessAnnotationProcessor

@Component
public class DataAccessAnnotationProcessor implements BeanPostProcessor {

    private ConfigurableListableBeanFactory configurableBeanFactory;

    @Autowired
    public DataAccessAnnotationProcessor(ConfigurableListableBeanFactory beanFactory) {
        this.configurableBeanFactory = beanFactory;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
      throws BeansException {
        this.scanDataAccessAnnotation(bean, beanName);
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName)
      throws BeansException {
        return bean;
    }

    protected void scanDataAccessAnnotation(Object bean, String beanName) {
        this.configureFieldInjection(bean);
    }

    private void configureFieldInjection(Object bean) {
        Class<?> managedBeanClass = bean.getClass();
        FieldCallback fieldCallback =
          new DataAccessFieldCallback(configurableBeanFactory, bean);
        ReflectionUtils.doWithFields(managedBeanClass, fieldCallback);
    }
}

Weiter - hier ist die Implementierung des gerade verwendeten DataAccessFieldCallback :

3.4. DataAccessFieldCallback

public class DataAccessFieldCallback implements FieldCallback {
    private static Logger logger = LoggerFactory.getLogger(DataAccessFieldCallback.class);

    private static int AUTOWIRE__MODE = AutowireCapableBeanFactory.AUTOWIRE__BY__NAME;

    private static String ERROR__ENTITY__VALUE__NOT__SAME = "@DataAccess(entity) "
            + "value should have same type with injected generic type.";
    private static String WARN__NON__GENERIC__VALUE = "@DataAccess annotation assigned "
            + "to raw (non-generic) declaration. This will make your code less type-safe.";
    private static String ERROR__CREATE__INSTANCE = "Cannot create instance of "
            + "type '{}' or instance creation is failed because: {}";

    private ConfigurableListableBeanFactory configurableBeanFactory;
    private Object bean;

    public DataAccessFieldCallback(ConfigurableListableBeanFactory bf, Object bean) {
        configurableBeanFactory = bf;
        this.bean = bean;
    }

    @Override
    public void doWith(Field field)
    throws IllegalArgumentException, IllegalAccessException {
        if (!field.isAnnotationPresent(DataAccess.class)) {
            return;
        }
        ReflectionUtils.makeAccessible(field);
        Type fieldGenericType = field.getGenericType();
       //In this example, get actual "GenericDAO' type.
        Class<?> generic = field.getType();
        Class<?> classValue = field.getDeclaredAnnotation(DataAccess.class).entity();

        if (genericTypeIsValid(classValue, fieldGenericType)) {
            String beanName = classValue.getSimpleName() + generic.getSimpleName();
            Object beanInstance = getBeanInstance(beanName, generic, classValue);
            field.set(bean, beanInstance);
        } else {
            throw new IllegalArgumentException(ERROR__ENTITY__VALUE__NOT__SAME);
        }
    }

    public boolean genericTypeIsValid(Class<?> clazz, Type field) {
        if (field instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) field;
            Type type = parameterizedType.getActualTypeArguments()[0];

            return type.equals(clazz);
        } else {
            logger.warn(WARN__NON__GENERIC__VALUE);
            return true;
        }
    }

    public Object getBeanInstance(
      String beanName, Class<?> genericClass, Class<?> paramClass) {
        Object daoInstance = null;
        if (!configurableBeanFactory.containsBean(beanName)) {
            logger.info("Creating new DataAccess bean named '{}'.", beanName);

            Object toRegister = null;
            try {
                Constructor<?> ctr = genericClass.getConstructor(Class.class);
                toRegister = ctr.newInstance(paramClass);
            } catch (Exception e) {
                logger.error(ERROR__CREATE__INSTANCE, genericClass.getTypeName(), e);
                throw new RuntimeException(e);
            }

            daoInstance = configurableBeanFactory.initializeBean(toRegister, beanName);
            configurableBeanFactory.autowireBeanProperties(daoInstance, AUTOWIRE__MODE, true);
            configurableBeanFactory.registerSingleton(beanName, daoInstance);
            logger.info("Bean named '{}' created successfully.", beanName);
        } else {
            daoInstance = configurableBeanFactory.getBean(beanName);
            logger.info(
              "Bean named '{}' already exists used as current bean reference.", beanName);
        }
        return daoInstance;
    }
}

Nun - das ist eine ziemliche Implementierung - aber der wichtigste Teil davon ist die Methode doWith () :

genericDaoInstance = configurableBeanFactory.initializeBean(beanToRegister, beanName);
configurableBeanFactory.autowireBeanProperties(genericDaoInstance, autowireMode, true);
configurableBeanFactory.registerSingleton(beanName, genericDaoInstance);

Dies würde Spring anweisen, ein Bean basierend auf dem zur Laufzeit über die Annotation @ DataAccess injizierten Objekt zu initialisieren.

Der beanName stellt sicher, dass wir eine eindeutige Instanz des Beans erhalten, da wir in diesem Fall ein einzelnes Objekt von GenericDao erstellen möchten, abhängig von der Entität, die über die Annotation @ DataAccess eingefügt wird.

Als nächstes verwenden wir diesen neuen Bean-Prozessor in einer Spring-Konfiguration.

3.5. CustomAnnotationConfiguration

@Configuration
@ComponentScan("com.baeldung.springcustomannotation")
public class CustomAnnotationConfiguration {}

Wichtig ist hierbei, dass der Wert der Annotation @ComponentScan erforderlich ist Verweisen Sie auf das Paket, in dem sich unser Postprozessor für benutzerdefinierte Beans befindet, und stellen Sie sicher, dass er zur Laufzeit von Spring gescannt und automa- tisiert wird.

** 4. Testen des neuen DAO

**

Beginnen wir mit einem Spring-aktivierten Test und zwei einfachen Beispielentitätsklassen - Person und Account .

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={CustomAnnotationConfiguration.class})
public class DataAccessAnnotationTest {

    @DataAccess(entity=Person.class)
    private GenericDao<Person> personGenericDao;
    @DataAccess(entity=Account.class)
    private GenericDao<Account> accountGenericDao;
    @DataAccess(entity=Person.class)
    private GenericDao<Person> anotherPersonGenericDao;

    ...
}

Wir injizieren mit Hilfe der Annotation DataAccess einige Instanzen des GenericDao . Um zu testen, ob die neuen Beans korrekt injiziert werden, müssen wir Folgendes abdecken:

  1. Wenn die Injektion erfolgreich ist

  2. Wenn Bean-Instanzen mit derselben Entität identisch sind

  3. Wenn die Methoden im GenericDao tatsächlich wie erwartet funktionieren

Punkt 1 wird tatsächlich von Spring selbst abgedeckt - da das Gerüst frühzeitig eine Ausnahme auslöst, wenn eine Bean nicht angeschlossen werden kann.

Um Punkt 2 zu testen, müssen wir uns die 2 Instanzen von GenericDao ansehen, die beide die Person -Klasse verwenden:

@Test
public void whenGenericDaoInjected__thenItIsSingleton() {
    assertThat(personGenericDao, not(sameInstance(accountGenericDao)));
    assertThat(personGenericDao, not(equalTo(accountGenericDao)));
    assertThat(personGenericDao, sameInstance(anotherPersonGenericDao));
}

Wir möchten nicht, dass personGenericDao dem accountGenericDao

Wir möchten jedoch, dass PersonenGenericDao und anotherPersonGenericDao genau dieselbe Instanz sind.

Um Punkt 3 zu testen, testen wir hier nur eine einfache Logik für die Persistenz:

@Test
public void whenFindAll__thenMessagesIsCorrect() {
    personGenericDao.findAll();
    assertThat(personGenericDao.getMessage(),
      is("Would create findAll query from Person"));

    accountGenericDao.findAll();
    assertThat(accountGenericDao.getMessage(),
      is("Would create findAll query from Account"));
}

@Test
public void whenPersist__thenMessagesIsCorrect() {
    personGenericDao.persist(new Person());
    assertThat(personGenericDao.getMessage(),
      is("Would create persist query from Person"));

    accountGenericDao.persist(new Account());
    assertThat(accountGenericDao.getMessage(),
      is("Would create persist query from Account"));
}

5. Fazit

In diesem Artikel haben wir in Spring eine sehr coole Implementierung einer benutzerdefinierten Anmerkung vorgenommen - zusammen mit einem BeanPostProcessor . Das übergeordnete Ziel bestand darin, die zahlreichen DAO-Implementierungen, die wir normalerweise in unserer Persistenzschicht haben, zu beseitigen und eine nette, einfache generische Implementierung zu verwenden, ohne dabei etwas zu verlieren.

Die Implementierung all dieser Beispiele und Code-Schnipsel finden Sie in my github project Es sollte einfach zu importieren und so ausgeführt werden, wie es ist.