Anotasi Kustom Musim Semi untuk DAO yang Lebih Baik

1. Ikhtisar

Dalam tutorial ini, kami akan menerapkan anotasi Spring kustom dengan prosesor pasca kacang .

Jadi, bagaimana ini membantu? Sederhananya - kita dapat menggunakan kembali kacang yang sama daripada harus membuat beberapa kacang serupa dari jenis yang sama.

Kami akan melakukannya untuk implementasi DAO dalam proyek sederhana - mengganti semuanya dengan satu GenericDao fleksibel .

2. Maven

Kami membutuhkan JAR inti pegas , pegas-aop , dan dukungan konteks pegas agar ini berfungsi. Kami hanya dapat mendeklarasikan dukungan konteks pegas di pom.xml kami .

 org.springframework spring-context-support 5.2.2.RELEASE  

Jika Anda ingin menggunakan versi yang lebih baru dari ketergantungan Spring - lihat repositori maven.

3. DAO Generik Baru

Kebanyakan implementasi Spring / JPA / Hibernate menggunakan DAO standar - biasanya satu untuk setiap entitas.

Kami akan mengganti solusi itu dengan sebuah GenericDao ; kita akan membuat pemroses anotasi khusus dan menggunakan implementasi GenericDao itu :

3.1. DAO umum

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

Dalam skenario dunia nyata, Anda tentu saja perlu menghubungkan PersistenceContext dan benar-benar menyediakan implementasi metode ini. Untuk saat ini - kami akan membuatnya sesederhana mungkin.

Sekarang, mari buat anotasi untuk injeksi khusus.

3.2. Akses data

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

Kami akan menggunakan anotasi di atas untuk memasukkan sebuah GenericDao sebagai berikut:

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

Mungkin beberapa dari Anda bertanya, "Bagaimana Spring mengenali anotasi DataAccess kami ?". Tidak - tidak secara default.

Tapi kita bisa memberi tahu Spring untuk mengenali anotasi melalui BeanPostProcessor kustom - mari kita implementasikan ini selanjutnya.

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

Berikutnya - inilah implementasi DataAccessFieldCallback yang baru saja kita 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 implementasi yang cukup - tetapi bagian terpentingnya adalah metode doWith () :

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

Ini akan memberi tahu Spring untuk menginisialisasi kacang berdasarkan objek yang diinjeksi saat runtime melalui anotasi @DataAccess .

The beanName akan memastikan bahwa kami akan mendapatkan contoh yang unik dari kacang karena - dalam hal ini - kami ingin membuat objek tunggal GenericDao tergantung pada entitas disuntikkan melalui @DataAccess penjelasan.

Terakhir, mari gunakan prosesor kacang baru ini dalam konfigurasi Spring selanjutnya.

3.5. CustomAnnotationConfiguration

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

Satu hal yang penting di sini adalah bahwa, nilai anotasi @ComponentScan harus mengarah ke paket tempat pemroses bean post kustom kami berada dan memastikan bahwa itu dipindai dan disebarkan secara otomatis oleh Spring pada saat runtime.

4. Menguji DAO Baru

Mari kita mulai dengan tes yang mengaktifkan Spring dan dua kelas entitas contoh sederhana di sini - Orang dan Akun .

@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 memasukkan beberapa contoh dari GenericDao dengan bantuan anotasi DataAccess . Untuk menguji apakah kacang baru disuntikkan dengan benar, kita perlu membahas:

  1. Jika injeksi berhasil
  2. Jika instance kacang dengan entitas yang sama adalah sama
  3. Jika metode di GenericDao benar-benar berfungsi seperti yang diharapkan

Poin 1 sebenarnya dicakup oleh Spring itu sendiri - karena kerangka kerja mengeluarkan pengecualian cukup awal jika kacang tidak dapat disambungkan.

Untuk menguji poin 2, kita perlu melihat 2 instance dari GenericDao yang keduanya menggunakan kelas Person :

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

Kami tidak ingin personGenericDao sama dengan akunGenericDao .

Tapi kami ingin personGenericDao dan anotherPersonGenericDao menjadi instance yang persis sama.

Untuk menguji poin 3, kami hanya menguji beberapa logika terkait ketekunan 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

Pada artikel ini kami melakukan implementasi yang sangat keren dari anotasi khusus di Spring - bersama dengan BeanPostProcessor . Tujuan keseluruhannya adalah untuk menyingkirkan beberapa implementasi DAO yang biasanya kami miliki di lapisan persistensi dan menggunakan implementasi umum yang bagus dan sederhana tanpa kehilangan apa pun dalam prosesnya.

Penerapan semua contoh dan cuplikan kode ini dapat ditemukan di proyek GitHub saya - ini adalah proyek berbasis Eclipse, jadi semestinya mudah untuk mengimpor dan menjalankan apa adanya.