본문 바로가기

Web/spring

[Spring] A Spring Custom Annotation for a Better DAO

728x90

a custom Spring annotation with a bean post-processor.

 

 

 

 

 

 

예제로 사용할 Generic 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) {
        // ...
    }
}

 

 

Data Access

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

 

 

 

BeanPostProcessor 를 통해 애노테이션을 인식할 수 있도록 함

 

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

 

 

 

 

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

 

 

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

 

 

doWith() 메소드 쪽이 중요하다

 

 

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

이것은 @DataAccess 주석 을 통해 런타임에 주입된 객체를 기반으로 빈을 초기화한다

 

 

 

 

beanName은 @DataAccess 애노테이션을 통해 주입된 엔티티에 따라 GenericDao 단일 객체 생성으로 고유한 빈 인스턴스를 얻을 수 있는지 확인한다

 

 

CustomAnnotationConfiguration

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

 

 

@ComponentScan 애노테이션 값이 beanPostProcessor 패키지를 가르켜야한다

런타임시 Spring에 의해 스캔되고 자동연결되는지 확인하게 된다

 

 

 

 

 

 

 

 

Testing the New DAO 

Let's start with a Spring enabled test and two simple example entity classes here – Person and 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;

    ...
}

DataAccess 로 GenericDao에 인스턴스를 주입하고 있다

  1. If injection is successful // 실제 스프링에서 프레임워크가 미리 예외발생시킴
  2. If bean instances with the same entity are the same // Person 클래스를 사용하는 인스턴스 참조
  3. If the methods in the GenericDao actually work as expected // 지속적 관련 논리 테스트 personGenericDao  anotherPersonGenericDao 가 같은지

 

2번 Person 클래스 인스턴스 참조

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

 

 

 

3번 인스턴스 지속성 관련 논리 테스트

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

 

728x90

'Web > spring' 카테고리의 다른 글

JPA 프레임워크의 개념과 구조  (0) 2023.11.12
[Spring] Assertions in JUnit 4 and JUnit 5  (0) 2023.04.25
[Spring] Testing @Cacheable on Spring Data Repositories  (1) 2023.04.24
[Spring Test]  (0) 2023.04.22
[JPA Repository] supported keyword  (0) 2023.04.20