Pengetahuan suci tentang cara kerja anotasi tidak dapat diakses oleh semua orang. Tampaknya ini semacam sihir: Saya menggunakan mantra pada anjing di atas metode / bidang / kelas - dan elemennya mulai mengubah propertinya dan mendapatkan yang baru.

Hari ini kita akan belajar keajaiban anotasi menggunakan Anotasi Musim Semi sebagai contoh: menginisialisasi ladang kacang.
Seperti biasa, di akhir artikel ada tautan ke proyek di GitHub, yang dapat diunduh dan melihat bagaimana semuanya berjalan.
Dalam
artikel sebelumnya
, saya menggambarkan operasi perpustakaan ModelMapper, yang memungkinkan Anda untuk mengkonversi entitas dan DTO menjadi satu sama lain. Kami akan menguasai karya anotasi pada contoh mapper ini.
Dalam proyek ini, kita membutuhkan beberapa entitas terkait dan DTO. Saya akan memberikan satu pasang secara selektif.
Planet@Entity @Table(name = "planets") @EqualsAndHashCode(callSuper = false) @Setter @AllArgsConstructor @NoArgsConstructor public class Planet extends AbstractEntity { private String name; private List<Continent> continents; @Column(name = "name") public String getName() { return name; } @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "planet") public List<Continent> getContinents() { return continents; } }
Planetetto @EqualsAndHashCode(callSuper = true) @Data public class PlanetDto extends AbstractDto { private String name; private List<ContinentDto> continents; }
Mapper. Mengapa diatur sedemikian rupa dijelaskan dalam
artikel terkait.
EntityDtoMapper public interface EntityDtoMapper<E extends AbstractEntity, D extends AbstractDto> { E toEntity(D dto); D toDto(E entity); }
AbstractMapper @Setter public abstract class AbstractMapper<E extends AbstractEntity, D extends AbstractDto> implements EntityDtoMapper<E, D> { @Autowired ModelMapper mapper; private Class<E> entityClass; private Class<D> dtoClass; AbstractMapper(Class<E> entityClass, Class<D> dtoClass) { this.entityClass = entityClass; this.dtoClass = dtoClass; } @PostConstruct public void init() { } @Override public E toEntity(D dto) { return Objects.isNull(dto) ? null : mapper.map(dto, entityClass); } @Override public D toDto(E entity) { return Objects.isNull(entity) ? null : mapper.map(entity, dtoClass); } Converter<E, D> toDtoConverter() { return context -> { E source = context.getSource(); D destination = context.getDestination(); mapSpecificFields(source, destination); return context.getDestination(); }; } Converter<D, E> toEntityConverter() { return context -> { D source = context.getSource(); E destination = context.getDestination(); mapSpecificFields(source, destination); return context.getDestination(); }; } void mapSpecificFields(E source, D destination) { } void mapSpecificFields(D source, E destination) { } }
PlanetMapper @Component public class PlanetMapper extends AbstractMapper<Planet, PlanetDto> { PlanetMapper() { super(Planet.class, PlanetDto.class); } }
Inisialisasi lapangan.
Kelas mapper abstrak memiliki dua bidang di kelas Kelas yang perlu kita inisialisasi dalam implementasi.
private Class<E> entityClass; private Class<D> dtoClass;
Sekarang kita lakukan ini melalui konstruktor. Bukan solusi yang paling elegan, meskipun cukup untuk diriku sendiri. Namun, saya sarankan untuk melangkah lebih jauh dan menulis anotasi yang akan mengatur bidang ini tanpa konstruktor.
Untuk mulai dengan, tulis anotasi itu sendiri. Tidak ada dependensi tambahan yang perlu ditambahkan.
Agar seekor anjing ajaib muncul di depan kelas, kami akan menulis yang berikut:
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented public @interface Mapper { Class<?> entity(); Class<?> dto(); }
@Retention (RetentionPolicy.RUNTIME) - menetapkan kebijakan yang akan diikuti oleh anotasi saat kompilasi. Ada tiga di antaranya:
SUMBER - anotasi semacam itu tidak akan diperhitungkan saat kompilasi. Opsi ini tidak cocok untuk kita.
CLASS - anotasi akan diterapkan pada kompilasi. Opsi ini
kebijakan standar.
RUNTIME - anotasi akan diperhitungkan selama kompilasi, apalagi, mesin virtual akan terus melihatnya sebagai anotasi, yaitu, mereka dapat dipanggil secara rekursif selama eksekusi kode, dan karena kami akan bekerja dengan anotasi melalui prosesor, ini adalah opsi yang cocok untuk kami. .
Target ({ElementType.TYPE}) - menentukan apa yang bisa dianotasi oleh penjelasan ini. Itu bisa berupa kelas, metode, bidang, konstruktor, variabel lokal, parameter, dan sebagainya - hanya 10 opsi. Dalam kasus kami,
TYPE berarti kelas (antarmuka).
Dalam anotasi, kami mendefinisikan bidang. Fields dapat memiliki nilai default (default "field default", misalnya), maka ada kemungkinan untuk tidak mengisinya. Jika tidak ada nilai default, bidang harus diisi.
Sekarang mari kita beri penjelasan tentang implementasi mapper kita dan isi kolomnya.
@Component @Mapper(entity = Planet.class, dto = PlanetDto.class) public class PlanetMapper extends AbstractMapper<Planet, PlanetDto> {
Kami menunjukkan bahwa esensi dari mapper kami adalah Planet.class, dan DTO adalah PlanetDto.class.
Untuk menyuntikkan parameter anotasi ke dalam kacang kami, kami, tentu saja, akan menggunakan BeanPostProcessor. Bagi mereka yang tidak tahu, BeanPostProcessor dieksekusi ketika setiap kacang diinisialisasi. Ada dua metode di antarmuka:
postProcessBeforeInisialisasi () - dijalankan sebelum menginisialisasi kacang.
postProcessAfterInisialisasi () - dijalankan setelah inisialisasi bean.
Proses ini dijelaskan lebih rinci dalam video Spring-ripper Evgeny Borisov yang terkenal, yang disebut: "Evgeny Borisov - Spring-ripper." Saya rekomendasikan untuk melihat.
Jadi disini. Kami memiliki kacang dengan anotasi
Mapper dengan parameter yang berisi bidang kelas Class. Anda bisa menambahkan bidang apa saja dari kelas apa saja ke anotasi. Kemudian kita mendapatkan nilai-nilai bidang ini dan dapat melakukan apa saja dengan mereka. Dalam kasus kami, kami menginisialisasi bidang kacang dengan nilai anotasi.
Untuk melakukan ini, kami membuat MapperAnnotationProcessor (sesuai dengan aturan Spring, semua prosesor anotasi harus diakhiri dengan ... AnnotationProcessor) dan mewarisinya dari BeanPostProcessor. Dengan melakukan itu, kita perlu mengganti kedua metode itu.
@Component public class MapperAnnotationProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(@Nullable Object bean, String beanName) { return Objects.nonNull(bean) ? init(bean) : null; } @Override public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { return bean; } }
Jika ada nampan, kami inisialisasi dengan parameter anotasi. Kami akan melakukan ini dalam metode terpisah. Cara termudah:
private Object init(Object bean) { Class<?> managedBeanClass = bean.getClass(); Mapper mapper = managedBeanClass.getAnnotation(Mapper.class); if (Objects.nonNull(mapper)) { ((AbstractMapper) bean).setEntityClass(mapper.entity()); ((AbstractMapper) bean).setDtoClass(mapper.dto()); } return bean; }
Selama inisialisasi bin, kami menjalankannya dan jika kami menemukan anotasi
Mapper di atas nampan, kami menginisialisasi bidang bin dengan parameter anotasi.
Metode ini sederhana tetapi tidak sempurna dan mengandung kerentanan. Kami tidak menentukan tempat sampah, tetapi mengandalkan pengetahuan tentang tempat sampah ini. Dan setiap kode di mana programmer bergantung pada kesimpulannya sendiri adalah buruk dan rentan. Ya, dan Ide bersumpah pada panggilan Tidak Dicentang.
Tugas melakukan segalanya dengan benar itu sulit, tetapi layak dilakukan.
Spring memiliki komponen ReflectionUtils yang hebat yang memungkinkan Anda bekerja dengan refleksi dengan cara paling aman. Dan kita akan mengatur kelas lapangan melaluinya.
Metode init () kami akan terlihat seperti ini:
private Object init(Object bean) { Class<?> managedBeanClass = bean.getClass(); Mapper mapper = managedBeanClass.getAnnotation(Mapper.class); if (Objects.nonNull(mapper)) { ReflectionUtils.doWithFields(managedBeanClass, field -> { assert field != null; String fieldName = field.getName(); if (!fieldName.equals("entityClass") && !fieldName.equals("dtoClass")) { return; } ReflectionUtils.makeAccessible(field); Class<?> targetClass = fieldName.equals("entityClass") ? mapper.entity() : mapper.dto(); Class<?> expectedClass = Stream.of(ResolvableType.forField(field).getGenerics()).findFirst() .orElseThrow(() -> new IllegalArgumentException("Unable to get generic type for " + fieldName)).resolve(); if (expectedClass != null && !expectedClass.isAssignableFrom(targetClass)) { throw new IllegalArgumentException(String.format("Unable to assign Class %s to expected Class %s", targetClass, expectedClass)); } field.set(bean, targetClass); }); } return bean; }
Segera setelah kami mengetahui bahwa komponen kami ditandai dengan anotasi
Mapper , kami memanggil ReflectionUtils.doWithFields, yang akan mengatur bidang yang kami butuhkan dengan cara yang lebih elegan. Kami memastikan bahwa bidang itu ada, dapatkan namanya, dan periksa apakah nama ini yang kami butuhkan.
assert field != null; String fieldName = field.getName(); if (!fieldName.equals("entityClass") && !fieldName.equals("dtoClass")) { return; }
Kami membuat bidang tersedia (ini pribadi).
ReflectionUtils.makeAccessible(field);
Kami menetapkan nilai di bidang.
Class<?> targetClass = fieldName.equals("entityClass") ? mapper.entity() : mapper.dto(); field.set(bean, targetClass);
Ini sudah cukup, tetapi kita juga dapat melindungi kode di masa depan dari upaya untuk mematahkannya dengan menunjukkan entitas yang salah atau DTO dalam parameter mapper (opsional). Kami memeriksa bahwa kelas yang akan kita atur di lapangan benar-benar cocok untuk ini.
Class<?> expectedClass = Stream.of(ResolvableType.forField(field).getGenerics()).findFirst() .orElseThrow(() -> new IllegalArgumentException("Unable to get generic type for " + fieldName)).resolve(); if (expectedClass != null && !expectedClass.isAssignableFrom(targetClass)) { throw new IllegalArgumentException(String.format("Unable to assign Class %s to expected Class: %s", targetClass, expectedClass)); }
Pengetahuan ini cukup untuk membuat semacam anotasi dan mengejutkan rekan-rekan di proyek dengan sihir ini. Tapi hati-hati - bersiaplah untuk kenyataan bahwa tidak semua akan menghargai keahlian Anda :)
Proyek di Github ada di sini:
promoscow@annotations.gitSelain contoh inisialisasi bin, proyek ini juga berisi implementasi AspectJ. Saya juga ingin memasukkan deskripsi Spring AOP / AspectJ dalam artikel tersebut, tetapi saya menemukan bahwa Habré sudah memiliki
artikel yang bagus tentang subjek ini, jadi saya tidak akan menduplikasinya. Baiklah, saya akan meninggalkan kode kerja dan tes tertulis - mungkin ini akan membantu seseorang memahami pekerjaan AspectJ.