Frühlingsanmerkungen: AOP Magic

    Heiliges Wissen über die Funktionsweise von Anmerkungen steht nicht jedem zur Verfügung. Es scheint, dass dies eine Art Magie ist: Setzen Sie einen Zauber mit einem Hund über eine Methode / ein Feld / eine Klasse - und das Element beginnt, seine Eigenschaften zu ändern und neue Eigenschaften zu erhalten.

    Bild

    Heute lernen wir die Magie von Annotationen anhand des Beispiels von Spring Annotations: Bean-Felder werden initialisiert.

    Wie üblich gibt es am Ende des Artikels einen Link zum Projekt auf GitHub, das heruntergeladen werden kann und wie alles funktioniert.

    In dem vorherigen Artikel habe ich die Arbeit der ModelMapper-Bibliothek beschrieben, mit der Sie eine Entität und ein DTO ineinander konvertieren können. Wir werden die Arbeit von Anmerkungen am Beispiel dieses Mapers beherrschen.

    Im Projekt benötigen wir ein Paar zusammengehöriger Entitäten und DTO. Ich werde ein einzelnes Paar zitieren.

    Planet
    @Entity@Table(name = "planets")
    @EqualsAndHashCode(callSuper = false)
    @Setter@AllArgsConstructor@NoArgsConstructorpublicclassPlanetextendsAbstractEntity{
        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;
        }
    }


    Planetdto
    @EqualsAndHashCode(callSuper = true)
    @DatapublicclassPlanetDtoextendsAbstractDto{
        private String name;
        private List<ContinentDto> continents;
    }


    Mapper Warum es genau wie im entsprechenden Artikel beschrieben angeordnet ist .

    EntityDtoMapper
    publicinterfaceEntityDtoMapper<EextendsAbstractEntity, DextendsAbstractDto> {
        E toEntity(D dto);
        D toDto(E entity);
    }


    AbstractMapper
    @SetterpublicabstractclassAbstractMapper<EextendsAbstractEntity, DextendsAbstractDto> implementsEntityDtoMapper<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;
        }
        @PostConstructpublicvoidinit(){
        }
        @Overridepublic E toEntity(D dto){
            return Objects.isNull(dto)
                    ? null
                    : mapper.map(dto, entityClass);
        }
        @Overridepublic 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();
            };
        }
        voidmapSpecificFields(E source, D destination){
        }
        voidmapSpecificFields(D source, E destination){
        }
    }


    Planet Mapper
    @ComponentpublicclassPlanetMapperextendsAbstractMapper<Planet, PlanetDto> {
        PlanetMapper() {
            super(Planet.class, PlanetDto.class);
        }
    }


    Initialisierung von Feldern.


    Die abstrakte Mapper-Klasse hat zwei Felder der Klasse Class, die wir in der Implementierung initialisieren müssen.

    private Class<E> entityClass;
        private Class<D> dtoClass;

    Jetzt machen wir es durch den Konstruktor. Nicht die eleganteste Lösung, wenn auch ganz für sich. Trotzdem schlage ich vor, eine Annotation zu schreiben, die diese Felder ohne Konstruktor benennt.

    Als Erstes schreiben wir die Anmerkung selbst. Es sollten keine zusätzlichen Abhängigkeiten hinzugefügt werden.

    Damit ein Zauberhund vor der Klasse erscheint, schreiben wir Folgendes:

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE})
    @Documentedpublic@interface Mapper {
        Class<?> entity();
        Class<?> dto();
    }

    @Retention (RetentionPolicy.RUNTIME) - Definiert die Richtlinie, auf die die Anmerkung beim Kompilieren folgt. Es gibt drei davon:

    SOURCE - solche Anmerkungen werden beim Kompilieren nicht berücksichtigt. Diese Option passt nicht zu uns.

    CLASS - Anmerkungen werden beim Kompilieren angewendet. Diese Option ist die
    Standardrichtlinie.

    RUNTIME - Annotationen werden beim Kompilieren berücksichtigt. Darüber hinaus werden sie von der virtuellen Maschine weiterhin als Annotationen angezeigt, dh sie können während der Ausführung des Codes rekursiv aufgerufen werden. Da wir über den Prozessor mit Annotationen arbeiten, ist diese Option für uns geeignet .

    Ziel ({ElementType.TYPE})- bestimmt, worauf diese Anmerkung aufgehängt werden kann. Dies kann eine Klasse, eine Methode, ein Feld, ein Konstruktor, eine lokale Variable, ein Parameter usw. sein - nur 10 Optionen. In unserem Fall bedeutet TYPE eine Klasse (Interface).

    In der Annotation definieren wir die Felder. Felder können Standardwerte haben (Standard-Standardfeld, zum Beispiel), dann können sie nicht gefüllt werden. Wenn keine Standardwerte vorhanden sind, muss das Feld ausgefüllt werden.

    Lassen Sie uns jetzt die Anmerkung zu unserer Implementierung des Mapper aufhängen und die Felder ausfüllen.

    @Component@Mapper(entity = Planet.class, dto = PlanetDto.class)
    publicclassPlanetMapperextendsAbstractMapper<Planet, PlanetDto> {

    Wir haben darauf hingewiesen, dass der Kern unseres Mapers Planet.class ist und DTO PlanetDto.class.

    Um Anmerkungsparameter in unsere Bin einzufügen, gehen wir natürlich zu BeanPostProcessor. Für diejenigen, die es nicht wissen - BeanPostProcessor wird ausgeführt, wenn die Bean initialisiert wird. In der Schnittstelle gibt es zwei Methoden:

    postProcessBeforeInitialization () wird vor der Initialisierung des Beans ausgeführt.

    postProcessAfterInitialization () - wird nach der Initialisierung der Bean ausgeführt.

    Ausführlicher wird dieser Vorgang in dem Video des berühmten Spring-Ripper Evgeny Borisov beschrieben, das so genannt wird: "Evgeny Borisov - Spring-Ripper". Ich empfehle das Ansehen.

    Also. Wir haben einen Anmerkungs-Bin- Mappermit Parametern, die Felder der Klasse Class enthalten. In der Annotation können Sie jedes Feld jeder Klasse hinzufügen. Dann erhalten wir diese Feldwerte und können alles mit ihnen machen. In unserem Fall initialisieren wir die Bean-Felder mit Anmerkungswerten.

    Dazu erstellen wir den MapperAnnotationProcessor (gemäß den Spring-Regeln müssen alle Annotation-Prozessoren mit ... AnnotationProcessor enden) und erben sie von BeanPostProcessor. In diesem Fall müssen wir diese beiden Methoden überschreiben.

    @ComponentpublicclassMapperAnnotationProcessorimplementsBeanPostProcessor{
        @Overridepublic Object postProcessBeforeInitialization(@Nullable Object bean, String beanName){
            return Objects.nonNull(bean) ? init(bean) : null;
        }
        @Overridepublic Object postProcessAfterInitialization(@Nullable Object bean, String beanName){
            return bean;
        }
    }

    Wenn es eine Bin gibt, initialisieren wir sie mit den Anmerkungsparametern. Wir machen das in einer separaten Methode. Der einfachste Weg:

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

    Wenn wir die Beans initialisieren, überfahren wir sie und wenn wir die Mapper- Annotation über der Bin finden , initialisieren wir die Bean-Felder mit den Annotationsparametern.

    Diese Methode ist einfach, aber nicht perfekt und enthält Schwachstellen. Wir typisieren nicht bin, sondern stützen uns auf einiges Wissen über diese Ablage. Und jeder Code, in dem sich der Programmierer auf seine eigenen Schlussfolgerungen stützt, ist schlecht und anfällig. Und die Idee schwört auf ungeprüften Anruf.

    Die Aufgabe, alles richtig zu machen, ist schwierig, aber machbar.

    Im Frühling gibt es eine wundervolle Komponente ReflectionUtils, mit der Sie so sicher wie möglich mit Reflektion arbeiten können. Und wir werden Klassenfelder dadurch kennzeichnen.

    Unsere init () - Methode sieht folgendermaßen aus:

    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)) {
                        thrownew IllegalArgumentException(String.format("Unable to assign Class %s to expected Class %s",
                                targetClass, expectedClass));
                    }
                    field.set(bean, targetClass);
                });
            }
            return bean;
        }

    Sobald wir herausfinden, dass unsere Komponente mit der Mapper- Annotation markiert ist , rufen wir ReflectionUtils.doWithFields auf, die die Felder, die wir benötigen, auf elegante Weise umschließen. Wir stellen sicher, dass das Feld existiert, wir erhalten seinen Namen und prüfen, ob dieser Name der ist, den wir brauchen.

    assert field != null;
    String fieldName = field.getName();
    if (!fieldName.equals("entityClass") && !fieldName.equals("dtoClass")) {
        return;
    }

    Wir machen das Feld zugänglich (es ist privat).

    ReflectionUtils.makeAccessible(field);

    Wir schätzen den Wert im Feld.

    Class<?> targetClass = fieldName.equals("entityClass") ? mapper.entity() : mapper.dto();
    field.set(bean, targetClass);

    Dies ist bereits ausreichend, aber wir können den zukünftigen Code zusätzlich vor Versuchen schützen, ihn zu beschädigen, indem Sie die falsche Entität oder DTO (optional) in den Mapper-Parametern angeben. Wir prüfen, ob die Klasse, die wir in das Feld integrieren, wirklich dafür geeignet ist.

    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)) {
        thrownew IllegalArgumentException(String.format("Unable to assign Class %s to expected Class: %s",
                targetClass, expectedClass));
    }

    Dieses Wissen reicht aus, um Anmerkungen zu erstellen und Ihre Projektkollegen mit dieser Magie zu überraschen. Aber seien Sie vorsichtig, seien Sie darauf vorbereitet, dass nicht jeder Ihr Können zu schätzen weiß :)

    Das Projekt auf Github ist hier: promoscow@annotations.git

    Neben dem Beispiel für das Initialisieren der Behälter enthält das Projekt auch die Implementierung von AspectJ. Ich wollte in den Artikel auch eine Beschreibung der Arbeit von Spring AOP / AspectJ aufnehmen, aber ich habe festgestellt, dass es auf Habré bereits einen wunderbaren Artikel zu diesem Thema gibt, daher werde ich ihn nicht duplizieren. Nun, ich werde den Arbeitscode und den schriftlichen Test verlassen - vielleicht hilft dies jemandem zu verstehen, wie AspectJ funktioniert.

    Jetzt auch beliebt: