Eine benutzerdefinierte Frühlingsanmerkung für ein besseres DAO

1. Übersicht

In diesem Tutorial implementieren wir eine benutzerdefinierte Spring-Annotation mit einem Bean-Postprozessor .

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

Wir werden das für die DAO-Implementierungen in einem einfachen Projekt tun - indem wir alle durch ein einziges, flexibles GenericDao ersetzen .

2. Maven

Wir benötigen JARs mit Spring-Core- , Spring-Aop- und Spring-Context-Unterstützung , damit dies funktioniert. Wir können einfach die Spring-Context-Unterstützung in unserer pom.xml deklarieren .

 org.springframework spring-context-support 5.2.2.RELEASE  

Wenn Sie sich für eine neuere Version der Spring-Abhängigkeit entscheiden möchten, lesen Sie das Maven-Repository.

3. Neues generisches DAO

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

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

3.1. Generisches DAO

public class GenericDao { private Class entityClass; public GenericDao(Class entityClass) { this.entityClass = entityClass; } public List findAll() { // ... } public Optional persist(E toPersist) { // ... } } 

In einem realen Szenario müssen Sie natürlich einen PersistenceContext einbinden und die Implementierungen dieser Methoden bereitstellen. Im Moment machen wir das so einfach wie möglich.

Jetzt können Sie eine Anmerkung 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 werden die obige Annotation verwenden, um ein GenericDao wie folgt zu injizieren :

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

Vielleicht fragen einige von Ihnen: "Woran erkennt Spring unsere DataAccess- Annotation?". Dies ist nicht der Fall - nicht standardmäßig.

Wir könnten Spring jedoch anweisen , die Annotation über einen benutzerdefinierten BeanPostProcessor zu erkennen. 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 doWith () -Methode:

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

Dies würde Spring anweisen, eine Bean basierend auf dem Objekt zu initialisieren, das zur Laufzeit über die Annotation @DataAccess injiziert wird .

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

Lassen Sie uns abschließend diesen neuen Bean-Prozessor in einer Spring-Konfiguration verwenden.

3.5. CustomAnnotationConfiguration

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

Eine wichtige Sache ist, dass der Wert der Annotation @ComponentScan auf das Paket verweisen muss, in dem sich unser benutzerdefinierter Bean-Postprozessor befindet, und sicherstellen muss, dass er zur Laufzeit von Spring gescannt und automatisch verdrahtet wird.

4. Testen des neuen DAO

Beginnen wir hier mit einem Spring-fähigen Test und zwei einfachen Beispielentitätsklassen - Person und Konto .

@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes={CustomAnnotationConfiguration.class}) public class DataAccessAnnotationTest { @DataAccess(entity=Person.class) private GenericDao personGenericDao; @DataAccess(entity=Account.class) private GenericDao accountGenericDao; @DataAccess(entity=Person.class) private GenericDao anotherPersonGenericDao; ... }

Wir sind einige Beispiele der Injektion GenericDao mit Hilfe der Data Access Anmerkung. Um zu testen, ob die neuen Bohnen richtig injiziert wurden, 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 Framework ziemlich früh eine Ausnahme auslöst, wenn eine Bean nicht verkabelt werden kann.

Um Punkt 2 zu testen, müssen wir uns die beiden 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 entspricht .

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

Um Punkt 3 zu testen, testen wir hier nur eine einfache persistenzbezogene Logik:

@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. Schlussfolgerung

In diesem Artikel haben wir eine sehr coole Implementierung einer benutzerdefinierten Annotation im Frühjahr durchgeführt - zusammen mit einem BeanPostProcessor . Das übergeordnete Ziel war es, die vielen DAO-Implementierungen, die wir normalerweise in unserer Persistenzschicht haben, loszuwerden und eine schöne, einfache generische Implementierung zu verwenden, ohne dabei etwas zu verlieren.

Die Implementierung all dieser Beispiele und Codefragmente finden Sie in meinem GitHub-Projekt - dies ist ein Eclipse-basiertes Projekt, daher sollte es einfach zu importieren und unverändert auszuführen sein.