Un'annotazione personalizzata primaverile per un DAO migliore

1. Panoramica

In questo tutorial, implementeremo un'annotazione Spring personalizzata con un post-processore di bean .

Allora in che modo questo aiuta? In poche parole: possiamo riutilizzare lo stesso bean invece di dover creare più bean simili dello stesso tipo.

Lo faremo per le implementazioni DAO in un progetto semplice, sostituendole tutte con un unico GenericDao flessibile .

2. Maven

Abbiamo bisogno di JAR spring-core , spring-aop e spring-context-support per farlo funzionare. Possiamo semplicemente dichiarare spring-context-support nel nostro pom.xml .

 org.springframework spring-context-support 5.2.2.RELEASE  

Se desideri una versione più recente della dipendenza Spring, controlla il repository maven.

3. Nuovo DAO generico

La maggior parte delle implementazioni Spring / JPA / Hibernate utilizza lo standard DAO, di solito uno per ciascuna entità.

Sostituiremo quella soluzione con un GenericDao ; scriveremo invece un processore di annotazioni personalizzato e utilizzeremo l' implementazione di GenericDao :

3.1. DAO generico

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

In uno scenario del mondo reale, ovviamente dovrai collegare un PersistenceContext e fornire effettivamente le implementazioni di questi metodi. Per ora, lo renderemo il più semplice possibile.

Ora, creiamo un'annotazione per l'iniezione personalizzata.

3.2. Accesso ai dati

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

Useremo l'annotazione sopra per iniettare un GenericDao come segue:

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

Forse qualcuno di voi chiede: "In che modo Spring riconosce la nostra annotazione DataAccess ?". Non lo fa - non per impostazione predefinita.

Ma potremmo dire a Spring di riconoscere l'annotazione tramite un BeanPostProcessor personalizzato : implementiamolo successivamente.

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); } } 

Avanti: ecco l'implementazione del DataAccessFieldCallback che abbiamo appena usato:

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; } } 

Ora, è piuttosto un'implementazione, ma la parte più importante è il metodo doWith () :

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

Ciò direbbe a Spring di inizializzare un bean in base all'oggetto iniettato in fase di esecuzione tramite l' annotazione @DataAccess .

Il beanName farà in modo che avremo un caso unico di fagiolo, perché - in questo caso - noi vogliamo creare singolo oggetto di GenericDao a seconda dell'entità iniettato attraverso il @DataAccess annotazione.

Infine, utilizziamo questo nuovo processore di bean in una configurazione Spring.

3.5. CustomAnnotationConfiguration

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

Una cosa importante qui è che il valore dell'annotazione @ComponentScan deve puntare al pacchetto in cui si trova il nostro post processore bean personalizzato e assicurarsi che sia scansionato e autowired da Spring in fase di runtime.

4. Test del nuovo DAO

Cominciamo con un test abilitato per la primavera e due semplici classi di entità di esempio qui: Persona e Account .

@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; ... }

Stiamo iniettando alcune istanze di GenericDao con l'aiuto dell'annotazione DataAccess . Per verificare che i nuovi fagioli vengano iniettati correttamente, dovremo coprire:

  1. Se l'iniezione ha successo
  2. Se le istanze del bean con la stessa entità sono le stesse
  3. Se i metodi nel GenericDao funzionano effettivamente come previsto

Il punto 1 è in realtà coperto dalla stessa Spring, poiché il framework lancia un'eccezione abbastanza presto se un bean non può essere collegato.

Per testare il punto 2, dobbiamo guardare le 2 istanze di GenericDao che utilizzano entrambe la classe Person :

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

Non vogliamo che personGenericDao sia uguale a accountGenericDao .

Ma vogliamo che personGenericDao e anotherPersonGenericDao siano esattamente la stessa istanza.

Per testare il punto 3, testiamo solo una semplice logica relativa alla persistenza qui:

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

In questo articolo abbiamo realizzato un'implementazione molto interessante di un'annotazione personalizzata in Spring, insieme a un BeanPostProcessor . L'obiettivo generale era sbarazzarsi delle molteplici implementazioni DAO che di solito abbiamo nel nostro livello di persistenza e utilizzare un'implementazione generica semplice e piacevole senza perdere nulla nel processo.

L'implementazione di tutti questi esempi e frammenti di codice può essere trovata nel mio progetto GitHub : questo è un progetto basato su Eclipse, quindi dovrebbe essere facile da importare ed eseguire così com'è.