المعرفة المقدسة لكيفية عمل التعليقات التوضيحية ليست متاحة للجميع. يبدو أن هذا هو نوع من السحر: وضعت تعويذة مع كلب على الطريقة / الحقل / الفئة - ويبدأ العنصر في تغيير خصائصه والحصول على خصائص جديدة.

اليوم سوف نتعلم سحر التعليقات التوضيحية باستخدام Spring Annotations كمثال: تهيئة حقول الفاصوليا.
كالعادة ، يوجد في نهاية المقال رابط للمشروع على GitHub ، والذي يمكن تنزيله ومعرفة كيف يعمل كل شيء.
في
مقال سابق
، وصفت تشغيل مكتبة ModelMapper ، والتي تتيح لك تحويل كيان و DTO إلى بعضها البعض. سنتقن عمل التعليقات التوضيحية على مثال هذا المخطط.
في المشروع ، نحتاج إلى اثنين من الكيانات ذات الصلة و DTO. سأقدم بشكل انتقائي زوج واحد.
الكوكب@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; } }
كوكب الأرض @EqualsAndHashCode(callSuper = true) @Data public class PlanetDto extends AbstractDto { private String name; private List<ContinentDto> continents; }
مخطط لماذا يتم ترتيب ذلك بهذه الطريقة في
المقالة المقابلة.
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); } }
تهيئة المجال.
تحتوي فئة معين الرسم التجريدي على حقلين في الفصل الدراسي نحتاج إلى التهيئة في التنفيذ.
private Class<E> entityClass; private Class<D> dtoClass;
الآن نحن نفعل هذا من خلال المنشئ. ليس الحل الأكثر أناقة ، وإن كان ذلك بنفسي. ومع ذلك ، أقترح الذهاب إلى أبعد من ذلك وكتابة تعليق توضيحي من شأنه تعيين هذه الحقول بدون مُنشئ.
بادئ ذي بدء ، اكتب التعليق التوضيحي نفسه. لا حاجة إلى إضافة تبعيات إضافية.
من أجل ظهور كلب سحري أمام الفصل ، سنكتب ما يلي:
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented public @interface Mapper { Class<?> entity(); Class<?> dto(); }
Retention (RetentionPolicy.RUNTIME) - يحدد السياسة التي
سيتبعها التعليق التوضيحي عند التحويل البرمجي. هناك ثلاثة منهم:
المصدر - لن يتم أخذ هذه التعليقات التوضيحية في الاعتبار أثناء التجميع. هذا الخيار لا يناسبنا.
CLASS - سيتم تطبيق التعليقات التوضيحية عند التجميع. هذا الخيار هو
السياسة الافتراضية.
RUNTIME - سيتم أخذ التعليقات التوضيحية في الاعتبار أثناء
الترجمة ، علاوة على ذلك ، سيستمر الجهاز الظاهري في
رؤيتهم كتعليقات توضيحية ، أي أنه يمكن استدعاءهم بشكل متكرر بالفعل أثناء تنفيذ التعليمات البرمجية ، وبما أننا سنعمل مع التعليقات التوضيحية من خلال المعالج ، فهذا هو الخيار الذي يناسبنا .
الهدف ({ElementType.TYPE}) - يحدد ما يمكن تعليق هذا التعليق التوضيحي عليه. يمكن أن يكون فئة ، أسلوب ، حقل ، مُنشئ ، متغير محلي ، معلمة ، وما إلى ذلك - فقط 10 خيارات. في حالتنا ،
TYPE تعني فئة (واجهة).
في الشرح ، نعرّف الحقول. يمكن أن تحتوي الحقول على قيم افتراضية ("الحقل الافتراضي" الافتراضي ، على سبيل المثال) ، ثم هناك إمكانية لعدم تعبئتها. إذا لم تكن هناك قيم افتراضية ، فيجب ملء الحقل.
الآن لنضع تعليقًا على تطبيقنا للمخطط ونملأ الحقول.
@Component @Mapper(entity = Planet.class, dto = PlanetDto.class) public class PlanetMapper extends AbstractMapper<Planet, PlanetDto> {
أشرنا إلى أن جوهر معين لدينا هو Planet.class ، و DTO هو PlanetDto.class.
من أجل حقن معلمات التعليق التوضيحي في الحبة الخاصة بنا ، سنستخدم بالطبع BeanPostProcessor. بالنسبة لأولئك الذين لا يعرفون ، يتم تنفيذ BeanPostProcessor عند تهيئة كل حبة. هناك طريقتان في الواجهة:
postProcessBeforeInitialization () - يتم تنفيذه قبل تهيئة الحبة.
postProcessAfterInitialization () - يتم تنفيذه بعد تهيئة الحبة.
تم وصف هذه العملية بمزيد من التفصيل في مقطع الفيديو الخاص ببرنامج Spring-ripper الشهير Evgeny Borisov ، والذي يسمى: "Evgeny Borisov - Spring-ripper". أوصي أن أرى.
لذلك هنا. لدينا حبة تحتوي على تعليق توضيحي
للخطة مع معلمات تحتوي على حقول فئة الفصل. يمكنك إضافة أي حقول من أي فئات إلى التعليقات التوضيحية. ثم نحصل على قيم الحقول هذه ويمكننا فعل أي شيء معها. في حالتنا ، نقوم بتهيئة حقول الفول بقيم التعليق التوضيحي.
للقيام بذلك ، نقوم بإنشاء MapperAnnotationProcessor (وفقًا لقواعد Spring ، يجب أن تنتهي جميع معالجات التعليقات التوضيحية بـ ... AnnotationProcessor) وترثه من BeanPostProcessor. عند القيام بذلك ، سنحتاج إلى تجاوز هاتين الطريقتين.
@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; } }
إذا كان هناك صندوق ، فإننا نهيئه بمعلمات التعليق التوضيحي. سنفعل ذلك بطريقة منفصلة. أسهل طريقة:
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; }
عند تهيئة الحاوية ، يتم تشغيلها من خلالها وإذا وجدنا التعليق التوضيحي
للخريطة أعلى الحاوية ، فإننا نقوم بتهيئة حقول الحاوية بمعلمات التعليق التوضيحي.
هذه الطريقة بسيطة ولكنها ليست مثالية وتحتوي على ثغرة أمنية. نحن لا نقيم حاوية ، لكن نعتمد على بعض المعرفة بهذه السلة. وأي كود يعتمد عليه المبرمج بناءً على استنتاجاته سيء وضعيف. نعم ، وتقسم Idea عند المكالمة غير المحددة.
مهمة القيام بكل ما هو صعب مهمة صعبة ، ولكنها ممكنة.
يحتوي Spring على مكون ReflectionUtils رائع يسمح لك بالعمل مع التفكير بأكثر الطرق أمانًا. وسنضع الطبقات الميدانية من خلالها.
ستبدو طريقة init () كما يلي:
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; }
بمجرد اكتشاف أن مكوننا تم وضع علامة عليه مع تعليق توضيحي
معين ، فإننا نسمي ReflectionUtils.doWithFields ، والذي سيحدد الحقول التي نحتاجها بطريقة أكثر أناقة. نتأكد من وجود الحقل والحصول على اسمه والتحقق من أن هذا الاسم هو ما نحتاجه.
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(); field.set(bean, targetClass);
هذا يكفي بالفعل ، ولكن يمكننا أيضًا حماية الشفرة المستقبلية من محاولات كسرها من خلال الإشارة إلى الكيان الخطأ أو 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)); }
هذه المعرفة كافية لإنشاء نوع من التعليق التوضيحي وفاجأة الزملاء في المشروع بهذا السحر. ولكن كن حذرا - كن مستعدا لحقيقة أن ليس كل شيء سوف نقدر مهارتك :)
المشروع على جيثب هنا:
promoscow@annotations.gitبالإضافة إلى مثال تهيئة صندوق ، يحتوي المشروع أيضًا على تطبيق AspectJ. أردت أيضًا تضمين وصف Spring AOP / AspectJ في المقالة ، لكنني وجدت أن Habré لديه بالفعل
مقال رائع حول هذا الموضوع ، لذلك لن أكرره. حسنًا ، سأترك رمز العمل والاختبار المكتوب - ربما يساعد هذا شخص ما على فهم عمل AspectJ.