Una anotación personalizada de primavera para un mejor DAO

1. Información general

En este tutorial, implementaremos una anotación Spring personalizada con un postprocesador de beans .

Entonces, ¿cómo ayuda esto? En pocas palabras, podemos reutilizar el mismo bean en lugar de tener que crear varios beans similares del mismo tipo.

Haremos eso para las implementaciones de DAO en un proyecto simple, reemplazándolas todas con un único GenericDao flexible .

2. Maven

Necesitamos primavera-core , la primavera-AOP y de contexto de soporte del muelle JAR para conseguir este trabajo. Podemos simplemente declarar spring-context-support en nuestro pom.xml .

 org.springframework spring-context-support 5.2.2.RELEASE  

Si desea obtener una versión más reciente de la dependencia de Spring, consulte el repositorio de maven.

3. Nuevo DAO genérico

La mayoría de las implementaciones de Spring / JPA / Hibernate usan el DAO estándar, generalmente uno para cada entidad.

Vamos a reemplazar esa solución con GenericDao ; En su lugar, escribiremos un procesador de anotaciones personalizado y usaremos esa implementación GenericDao :

3.1. DAO genérico

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

En un escenario del mundo real, por supuesto, necesitará conectar un PersistenceContext y proporcionar las implementaciones de estos métodos. Por ahora, haremos que esto sea lo más simple posible.

Ahora, creemos una anotación para una inyección personalizada.

3.2. Acceso a los datos

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

Usaremos la anotación anterior para inyectar un GenericDao de la siguiente manera:

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

Quizás algunos de ustedes pregunten, "¿Cómo reconoce Spring nuestra anotación DataAccess ?". No es así, no por defecto.

Pero podríamos decirle a Spring que reconozca la anotación a través de un BeanPostProcessor personalizado ; implementemos esto a continuación.

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

A continuación, aquí está la implementación de DataAccessFieldCallback que acabamos de usar:

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

Ahora, esa es una gran implementación, pero la parte más importante es el método doWith () :

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

Esto le diría a Spring que inicialice un bean basado en el objeto inyectado en tiempo de ejecución a través de la anotación @DataAccess .

El beanName se asegurará de que obtengamos una instancia única del bean porque, en este caso, queremos crear un solo objeto de GenericDao dependiendo de la entidad inyectada a través de la anotación @DataAccess .

Finalmente, usemos este nuevo procesador de frijoles en una configuración Spring a continuación.

3.5. CustomAnnotationConfiguration

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

Una cosa que es importante aquí es que, el valor de la anotación @ComponentScan debe apuntar al paquete donde se encuentra nuestro postprocesador de bean personalizado y asegurarse de que Spring lo escanee y lo conecte automáticamente en tiempo de ejecución.

4. Prueba del nuevo DAO

Comencemos con una prueba habilitada para Spring y dos clases de entidad de ejemplo simples aquí: Persona y Cuenta .

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

Estamos inyectando algunas instancias de GenericDao con la ayuda de la anotación DataAccess . Para probar que los nuevos frijoles se inyectan correctamente, necesitaremos cubrir:

  1. Si la inyección tiene éxito
  2. Si las instancias de bean con la misma entidad son iguales
  3. Si los métodos en GenericDao realmente funcionan como se esperaba

El punto 1 está realmente cubierto por Spring, ya que el marco genera una excepción bastante temprano si no se puede conectar un bean.

Para probar el punto 2, necesitamos mirar las 2 instancias de GenericDao que ambas usan la clase Person :

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

No queremos que personGenericDao sea ​​igual a accountGenericDao .

Pero queremos que personGenericDao y anotherPersonGenericDao sean exactamente la misma instancia.

Para probar el punto 3, solo probamos una lógica simple relacionada con la persistencia aquí:

@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. Conclusión

En este artículo hicimos una implementación muy interesante de una anotación personalizada en Spring, junto con un BeanPostProcessor . El objetivo general era deshacerse de las múltiples implementaciones DAO que usualmente tenemos en nuestra capa de persistencia y usar una implementación genérica simple y agradable sin perder nada en el proceso.

La implementación de todos estos ejemplos y fragmentos de código se puede encontrar en mi proyecto de GitHub ; este es un proyecto basado en Eclipse, por lo que debería ser fácil de importar y ejecutar tal cual.