Anotasi Kustom Musim Semi untuk DAO yang Lebih Baik

1. Gambaran keseluruhan

Dalam tutorial ini, kami akan menerapkan anotasi Spring khusus dengan pemproses pasca kacang .

Jadi bagaimana ini membantu? Ringkasnya - kita boleh menggunakan semula kacang yang sama dan bukannya membuat banyak kacang yang sama dari jenis yang sama.

Kami akan melakukannya untuk pelaksanaan DAO dalam projek mudah - menggantikan semuanya dengan GenericDao yang fleksibel .

2. Maven

Kami memerlukan JAR spring-core , spring-aop , dan spring-konteks-support untuk mendapatkan ini. Kami hanya boleh menyatakan sokongan-konteks musim semi di pom.xml kami .

 org.springframework spring-context-support 5.2.2.RELEASE  

Sekiranya anda ingin mencari versi baru dari pergantungan Spring - periksa repositori maven.

3. DAO Generik Baru

Sebilangan besar pelaksanaan Spring / JPA / Hibernate menggunakan DAO standard - biasanya satu untuk setiap entiti.

Kami akan menggantikan penyelesaian itu dengan GenericDao ; kami akan menulis pemproses anotasi tersuai dan menggunakan pelaksanaan GenericDao itu :

3.1. DAO generik

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

Dalam senario dunia nyata, anda tentu saja perlu menggunakan PersistenceContext dan benar-benar menyediakan pelaksanaan kaedah ini. Buat masa ini - kami akan menjadikannya sesederhana mungkin.

Sekarang, mari buat anotasi untuk suntikan tersuai.

3.2. Akses Data

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

Kami akan menggunakan penjelasan di atas untuk menyuntikkan GenericDao seperti berikut:

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

Mungkin ada di antara anda yang bertanya, "Bagaimana Spring mengenali anotasi DataAccess kami ?". Tidak - tidak secara lalai.

Tetapi kami dapat memberitahu Spring untuk mengenali anotasi melalui BeanPostProcessor tersuai - mari kita laksanakan ini seterusnya.

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

Seterusnya - inilah pelaksanaan DataAccessFieldCallback yang baru kami gunakan:

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

Sekarang - itu adalah pelaksanaan - tetapi bahagian yang paling penting adalah kaedah doWith () :

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

Ini akan memberitahu Spring untuk menginisialisasi kacang berdasarkan objek yang disuntikkan semasa menjalankan melalui anotasi @DataAccess .

The beanName akan memastikan bahawa kami akan mendapat contoh kacang yang unik kerana - dalam kes ini - kami ingin membuat objek tunggal GenericDao bergantung pada entiti yang disuntikkan melalui anotasi @DataAccess .

Akhirnya, mari kita gunakan pemproses kacang baru ini dalam konfigurasi Spring seterusnya.

3.5. Konfigurasi CustomAnnotation

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

Satu perkara yang penting di sini ialah, nilai anotasi @ComponentScan perlu menunjukkan pakej di mana pemproses pasca kacang khas kami berada dan pastikan ia diimbas dan dikendalikan secara automatik oleh Spring pada waktu runtime.

4. Menguji DAO Baru

Mari kita mulakan dengan ujian berkemampuan Spring dan dua kelas entiti contoh mudah di sini - Orang dan Akaun .

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

Kami menyuntik beberapa contoh GenericDao dengan bantuan anotasi DataAccess . Untuk menguji bahawa kacang baru disuntik dengan betul, kita perlu menutup:

  1. Sekiranya suntikan berjaya
  2. Sekiranya contoh kacang dengan entiti yang sama adalah sama
  3. Sekiranya kaedah dalam GenericDao benar-benar berfungsi seperti yang diharapkan

Titik 1 sebenarnya diliputi oleh Spring itu sendiri - kerana kerangka kerja membuat pengecualian cukup awal jika kacang tidak dapat dipasang.

Untuk menguji titik 2, kita perlu melihat 2 contoh GenericDao yang kedua-duanya menggunakan kelas Person :

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

Kami tidak mahu personGenericDao sama dengan accountGenericDao .

Tetapi kita mahu orang ituGenericDao dan orang lainGenericDao menjadi contoh yang sama.

Untuk menguji titik 3, kami hanya menguji beberapa logik berkaitan kegigihan sederhana di sini:

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

Dalam artikel ini, kami melakukan penerapan anotasi khusus yang sangat menarik pada musim bunga - bersama dengan BeanPostProcessor . Tujuan keseluruhannya adalah untuk menyingkirkan pelbagai implementasi DAO yang biasanya kita ada di lapisan ketekunan kita dan menggunakan pelaksanaan generik yang bagus dan sederhana tanpa kehilangan apa-apa dalam prosesnya.

Pelaksanaan semua contoh dan potongan kode ini terdapat di projek GitHub saya - ini adalah projek berasaskan Eclipse, jadi mudah untuk diimport dan dijalankan sebagaimana mestinya.