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에 인스턴스를 주입하고 있다
- If injection is successful // 실제 스프링에서 프레임워크가 미리 예외발생시킴
- If bean instances with the same entity are the same // Person 클래스를 사용하는 인스턴스 참조
- 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 |