From 32ea1e0a1e7a0001eff0b1e39be26367fdc5ebec Mon Sep 17 00:00:00 2001 From: xsalefter Date: Wed, 21 Oct 2015 00:50:44 +0700 Subject: [PATCH] Initial commit for "Spring Custom Annotation with Dynamic Qualifier" article. --- .../CustomAnnotationConfiguration.java | 9 ++ .../baeldung/customannotation/DataAccess.java | 14 +++ .../DataAccessAnnotationProcessor.java | 39 +++++++ .../DataAccessFieldCallback.java | 104 ++++++++++++++++++ .../baeldung/customannotation/GenericDAO.java | 30 +++++ .../baeldung/customannotation/Account.java | 40 +++++++ .../customannotation/BeanWithGenericDAO.java | 17 +++ .../DataAccessAnnotationTest.java | 57 ++++++++++ .../DataAccessFieldCallbackTest.java | 53 +++++++++ .../org/baeldung/customannotation/Person.java | 31 ++++++ 10 files changed, 394 insertions(+) create mode 100644 spring-all/src/main/java/org/baeldung/customannotation/CustomAnnotationConfiguration.java create mode 100644 spring-all/src/main/java/org/baeldung/customannotation/DataAccess.java create mode 100644 spring-all/src/main/java/org/baeldung/customannotation/DataAccessAnnotationProcessor.java create mode 100644 spring-all/src/main/java/org/baeldung/customannotation/DataAccessFieldCallback.java create mode 100644 spring-all/src/main/java/org/baeldung/customannotation/GenericDAO.java create mode 100644 spring-all/src/test/java/org/baeldung/customannotation/Account.java create mode 100644 spring-all/src/test/java/org/baeldung/customannotation/BeanWithGenericDAO.java create mode 100644 spring-all/src/test/java/org/baeldung/customannotation/DataAccessAnnotationTest.java create mode 100644 spring-all/src/test/java/org/baeldung/customannotation/DataAccessFieldCallbackTest.java create mode 100644 spring-all/src/test/java/org/baeldung/customannotation/Person.java diff --git a/spring-all/src/main/java/org/baeldung/customannotation/CustomAnnotationConfiguration.java b/spring-all/src/main/java/org/baeldung/customannotation/CustomAnnotationConfiguration.java new file mode 100644 index 0000000000..b8f312d778 --- /dev/null +++ b/spring-all/src/main/java/org/baeldung/customannotation/CustomAnnotationConfiguration.java @@ -0,0 +1,9 @@ +package org.baeldung.customannotation; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan("org.baeldung.customannotation") +public class CustomAnnotationConfiguration { +} diff --git a/spring-all/src/main/java/org/baeldung/customannotation/DataAccess.java b/spring-all/src/main/java/org/baeldung/customannotation/DataAccess.java new file mode 100644 index 0000000000..11bc30a84a --- /dev/null +++ b/spring-all/src/main/java/org/baeldung/customannotation/DataAccess.java @@ -0,0 +1,14 @@ +package org.baeldung.customannotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) +@Documented +public @interface DataAccess { + Classentity(); +} diff --git a/spring-all/src/main/java/org/baeldung/customannotation/DataAccessAnnotationProcessor.java b/spring-all/src/main/java/org/baeldung/customannotation/DataAccessAnnotationProcessor.java new file mode 100644 index 0000000000..7902da746e --- /dev/null +++ b/spring-all/src/main/java/org/baeldung/customannotation/DataAccessAnnotationProcessor.java @@ -0,0 +1,39 @@ +package org.baeldung.customannotation; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.stereotype.Component; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.FieldCallback; + +@Component +public class DataAccessAnnotationProcessor implements BeanPostProcessor { + + private ConfigurableListableBeanFactory configurableListableBeanFactory; + + @Autowired + public DataAccessAnnotationProcessor(ConfigurableListableBeanFactory bf) { + configurableListableBeanFactory = bf; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + scanDataAccessAnnotation(bean, beanName); + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) + throws BeansException { + return bean; + } + + protected void scanDataAccessAnnotation(Object bean, String beanName) { + Class managedBeanClass = bean.getClass(); + FieldCallback fcb = new DataAccessFieldCallback(configurableListableBeanFactory, bean); + ReflectionUtils.doWithFields(managedBeanClass, fcb); + } +} diff --git a/spring-all/src/main/java/org/baeldung/customannotation/DataAccessFieldCallback.java b/spring-all/src/main/java/org/baeldung/customannotation/DataAccessFieldCallback.java new file mode 100644 index 0000000000..16526fa56f --- /dev/null +++ b/spring-all/src/main/java/org/baeldung/customannotation/DataAccessFieldCallback.java @@ -0,0 +1,104 @@ +package org.baeldung.customannotation; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.FieldCallback; + + +public final 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 configurableListableBeanFactory; + private Object bean; + + public DataAccessFieldCallback(final ConfigurableListableBeanFactory bf, final Object bean) { + configurableListableBeanFactory = bf; + this.bean = bean; + } + + @Override + public void doWith(final Field field) + throws IllegalArgumentException, IllegalAccessException { + if (!field.isAnnotationPresent(DataAccess.class)) { + return; + } + ReflectionUtils.makeAccessible(field); + final Type fieldGenericType = field.getGenericType(); + // In this example, get actual "GenericDAO' type. + final Class generic = field.getType(); + final Class classValue = field.getDeclaredAnnotation(DataAccess.class).entity(); + + if (genericTypeIsValid(classValue, fieldGenericType)) { + final String beanName = classValue.getSimpleName() + generic.getSimpleName(); + final Object beanInstance = getBeanInstance(beanName, generic, classValue); + field.set(bean, beanInstance); + } else { + throw new IllegalArgumentException(ERROR_ENTITY_VALUE_NOT_SAME); + } + } + + + /** + * For example, if user write: + *
+     * @DataAccess(entity=Person.class) 
+     * private GenericDAO<Account> personGenericDAO;
+     * 
+ * then this is should be failed. + */ + public boolean genericTypeIsValid(final Class clazz, final Type field) { + if (field instanceof ParameterizedType) { + final ParameterizedType parameterizedType = (ParameterizedType) field; + final Type type = parameterizedType.getActualTypeArguments()[0]; + + return type.equals(clazz); + } else { + logger.warn(WARN_NON_GENERIC_VALUE); + return true; + } + } + + + + public final Object getBeanInstance(final String beanName, final Class genericClass, final Class paramClass) { + Object daoInstance = null; + if (!configurableListableBeanFactory.containsBean(beanName)) { + logger.info("Creating new DataAccess bean named '{}'.", beanName); + + Object toRegister = null; + try { + final Constructor ctr = genericClass.getConstructor(Class.class); + toRegister = ctr.newInstance(paramClass); + } catch (final Exception e) { + logger.error(ERROR_CREATE_INSTANCE, genericClass.getTypeName(), e); + throw new RuntimeException(e); + } + + daoInstance = configurableListableBeanFactory.initializeBean(toRegister, beanName); + configurableListableBeanFactory.autowireBeanProperties(daoInstance, AUTOWIRE_MODE, true); + configurableListableBeanFactory.registerSingleton(beanName, daoInstance); + logger.info("Bean named '{}' created successfully.", beanName); + } else { + daoInstance = configurableListableBeanFactory.getBean(beanName); + logger.info("Bean named '{}' already exist used as current bean reference.", beanName); + } + return daoInstance; + } +} diff --git a/spring-all/src/main/java/org/baeldung/customannotation/GenericDAO.java b/spring-all/src/main/java/org/baeldung/customannotation/GenericDAO.java new file mode 100644 index 0000000000..1916b7de6e --- /dev/null +++ b/spring-all/src/main/java/org/baeldung/customannotation/GenericDAO.java @@ -0,0 +1,30 @@ +package org.baeldung.customannotation; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +public class GenericDAO { + + private Class entityClass; + private String message; + + public GenericDAO(Class entityClass) { + this.entityClass = entityClass; + } + + public List findAll() { + message = "Would create findAll query from " + entityClass.getSimpleName(); + return Collections.emptyList(); + } + + public Optional persist(E toPersist) { + message = "Would create persist query from " + toPersist.getClass().getSimpleName(); + return Optional.empty(); + } + + /** Only used for unit-testing. */ + public final String getMessage() { + return message; + } +} diff --git a/spring-all/src/test/java/org/baeldung/customannotation/Account.java b/spring-all/src/test/java/org/baeldung/customannotation/Account.java new file mode 100644 index 0000000000..04545e5b83 --- /dev/null +++ b/spring-all/src/test/java/org/baeldung/customannotation/Account.java @@ -0,0 +1,40 @@ +package org.baeldung.customannotation; + +import java.io.Serializable; + +public class Account implements Serializable { + + private static final long serialVersionUID = 7857541629844398625L; + + private Long id; + private String email; + private Person person; + + public Account() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public Person getPerson() { + return person; + } + + public void setPerson(Person person) { + this.person = person; + } + +} diff --git a/spring-all/src/test/java/org/baeldung/customannotation/BeanWithGenericDAO.java b/spring-all/src/test/java/org/baeldung/customannotation/BeanWithGenericDAO.java new file mode 100644 index 0000000000..32d4660f41 --- /dev/null +++ b/spring-all/src/test/java/org/baeldung/customannotation/BeanWithGenericDAO.java @@ -0,0 +1,17 @@ +package org.baeldung.customannotation; + +import org.springframework.stereotype.Repository; + +@Repository +public class BeanWithGenericDAO { + + @DataAccess(entity=Person.class) + private GenericDAO personGenericDAO; + + public BeanWithGenericDAO() {} + + public GenericDAO getPersonGenericDAO() { + return personGenericDAO; + } + +} diff --git a/spring-all/src/test/java/org/baeldung/customannotation/DataAccessAnnotationTest.java b/spring-all/src/test/java/org/baeldung/customannotation/DataAccessAnnotationTest.java new file mode 100644 index 0000000000..ec0d46876e --- /dev/null +++ b/spring-all/src/test/java/org/baeldung/customannotation/DataAccessAnnotationTest.java @@ -0,0 +1,57 @@ +package org.baeldung.customannotation; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@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; + + @Test + public void whenGenericDAOInitialized_thenNotNull() { + assertThat(personGenericDAO, is(notNullValue())); + assertThat(accountGenericDAO, is(notNullValue())); + } + + @Test + public void whenGenericDAOInjected_thenItIsSingleton() { + assertThat(personGenericDAO, not(sameInstance(accountGenericDAO))); + assertThat(personGenericDAO, not(equalTo(accountGenericDAO))); + + assertThat(personGenericDAO, sameInstance(anotherPersonGenericDAO)); + } + + @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_thenMakeSureThatMessagesIsCorrect() { + 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")); + } +} diff --git a/spring-all/src/test/java/org/baeldung/customannotation/DataAccessFieldCallbackTest.java b/spring-all/src/test/java/org/baeldung/customannotation/DataAccessFieldCallbackTest.java new file mode 100644 index 0000000000..f025a3e00a --- /dev/null +++ b/spring-all/src/test/java/org/baeldung/customannotation/DataAccessFieldCallbackTest.java @@ -0,0 +1,53 @@ +package org.baeldung.customannotation; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertThat; + +import java.lang.reflect.Type; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = { CustomAnnotationConfiguration.class }) +public class DataAccessFieldCallbackTest { + + @Autowired + private ConfigurableListableBeanFactory configurableListableBeanFactory; + + @Autowired + private BeanWithGenericDAO beanWithGenericDAO; + + @Rule + public ExpectedException ex = ExpectedException.none(); + + @Test + public void whenObjectCreated_thenObjectCreationIsSuccessful() { + final DataAccessFieldCallback dataAccessFieldCallback = new DataAccessFieldCallback(configurableListableBeanFactory, beanWithGenericDAO); + assertThat(dataAccessFieldCallback, is(notNullValue())); + } + + @Test + public void whenMethodGenericTypeIsValidCalled_thenReturnCorrectValue() + throws NoSuchFieldException, SecurityException { + final DataAccessFieldCallback callback = new DataAccessFieldCallback(configurableListableBeanFactory, beanWithGenericDAO); + final Type fieldType = BeanWithGenericDAO.class.getDeclaredField("personGenericDAO").getGenericType(); + final boolean result = callback.genericTypeIsValid(Person.class, fieldType); + assertThat(result, is(true)); + } + + @Test + public void whenMethodGetBeanInstanceCalled_thenReturnCorrectInstance() { + final DataAccessFieldCallback callback = new DataAccessFieldCallback(configurableListableBeanFactory, beanWithGenericDAO); + final Object result = callback.getBeanInstance("personGenericDAO", GenericDAO.class, Person.class); + assertThat((result instanceof GenericDAO), is(true)); + } +} diff --git a/spring-all/src/test/java/org/baeldung/customannotation/Person.java b/spring-all/src/test/java/org/baeldung/customannotation/Person.java new file mode 100644 index 0000000000..3babc8f0a2 --- /dev/null +++ b/spring-all/src/test/java/org/baeldung/customannotation/Person.java @@ -0,0 +1,31 @@ +package org.baeldung.customannotation; + +import java.io.Serializable; + +public class Person implements Serializable { + + private static final long serialVersionUID = 9005331414216374586L; + + private Long id; + private String name; + + public Person() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +}