Frühlingsanmerkungen: AOP Magic

Das heilige Wissen darüber, wie Anmerkungen funktionieren, ist nicht jedem zugänglich. Es scheint, dass dies eine Art Magie ist: Ich habe die Methode / das Feld / die Klasse mit einem Hund verzaubert - und das Element beginnt, seine Eigenschaften zu ändern und neue zu erhalten.

Bild

Heute lernen wir die Magie von Annotationen am Beispiel von Spring Annotations: Initialisieren von Bean-Feldern.

Wie üblich befindet sich am Ende des Artikels ein Link zum Projekt auf GitHub, der heruntergeladen werden kann und zeigt, wie alles funktioniert.

In einem früheren Artikel habe ich die Funktionsweise der ModelMapper-Bibliothek beschrieben, mit der Sie eine Entität und ein DTO ineinander konvertieren können. Wir werden die Arbeit der Anmerkungen am Beispiel dieses Mappers beherrschen.

In dem Projekt benötigen wir einige verwandte Einheiten und DTO. Ich werde selektiv ein Paar geben.

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. Warum es so angeordnet ist, wird im entsprechenden Artikel beschrieben .

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


Feldinitialisierung.


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

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

Jetzt machen wir das durch den Konstruktor. Nicht die eleganteste Lösung, wenn auch ganz für mich. Ich empfehle jedoch, weiter zu gehen und eine Anmerkung zu schreiben, mit der diese Felder ohne Konstruktor festgelegt werden.

Schreiben Sie zunächst die Anmerkung selbst. Es müssen keine zusätzlichen Abhängigkeiten hinzugefügt werden.

Damit ein magischer Hund vor der Klasse erscheint, schreiben wir Folgendes:

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

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

QUELLE - Solche Anmerkungen werden bei der Kompilierung nicht berücksichtigt. Diese Option passt nicht zu uns.

KLASSE - Anmerkungen werden bei der Kompilierung angewendet. Diese Option ist
Standardrichtlinie.

RUNTIME - Annotationen werden bei der Kompilierung berücksichtigt. Darüber hinaus werden sie von der virtuellen Maschine weiterhin als Annotationen betrachtet, dh sie können bereits während der Codeausführung rekursiv aufgerufen werden. Da wir mit Annotationen über den Prozessor arbeiten, ist dies die Option, die zu uns passt .

Ziel ({ElementType.TYPE}) - Definiert, an was diese Anmerkung gehä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 (Schnittstelle).

In der Anmerkung definieren wir die Felder. Felder können Standardwerte haben (z. B. "Standardfeld"), dann besteht die Möglichkeit, diese nicht zu füllen. Wenn es keine Standardwerte gibt, muss das Feld ausgefüllt werden.

Fügen wir nun eine Anmerkung zu unserer Implementierung des Mappers hinzu und füllen die Felder aus.

 @Component @Mapper(entity = Planet.class, dto = PlanetDto.class) public class PlanetMapper extends AbstractMapper<Planet, PlanetDto> { 

Wir haben darauf hingewiesen, dass die Essenz unseres Mappers Planet.class und das DTO PlanetDto.class ist.

Um die Annotationsparameter in unsere Bean einzufügen, verwenden wir natürlich den BeanPostProcessor. Für diejenigen, die es nicht wissen, wird BeanPostProcessor ausgeführt, wenn jede Bean initialisiert wird. Es gibt zwei Methoden in der Schnittstelle:

postProcessBeforeInitialization () - wird ausgeführt, bevor die Bean initialisiert wird.

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

Dieser Vorgang wird im Video des berühmten Spring-Rippers Evgeny Borisov ausführlicher beschrieben, das heißt: "Evgeny Borisov - Spring-Ripper". Ich empfehle zu sehen.

Also. Wir haben eine Bean mit einer Mapper- Annotation mit Parametern, die Felder der Class-Klasse enthalten. Sie können den Anmerkungen beliebige Felder beliebiger Klassen hinzufügen. Dann bekommen wir diese Feldwerte und können alles damit machen. In unserem Fall initialisieren wir die Bean-Felder mit Anmerkungswerten.

Dazu erstellen wir einen MapperAnnotationProcessor (gemäß den Regeln von Spring müssen alle Annotationsprozessoren mit ... AnnotationProcessor enden) und erben ihn von BeanPostProcessor. Dabei müssen wir diese beiden Methoden überschreiben.

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

Wenn es einen Behälter gibt, initialisieren wir ihn mit Anmerkungsparametern. Wir werden dies in einer separaten Methode tun. 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; } 

Während der Bin-Initialisierung durchlaufen wir sie und wenn wir die Mapper- Annotation über dem Bin finden, initialisieren wir die Bin-Felder mit den Annotationsparametern.

Diese Methode ist einfach, aber nicht perfekt und enthält Schwachstellen. Wir typisieren keinen Behälter, sondern verlassen uns auf einige Kenntnisse über diesen Behälter. Und jeder Code, in dem sich der Programmierer auf seine eigenen Schlussfolgerungen stützt, ist schlecht und anfällig. Ja, und die Idee schwört auf ungeprüften Anruf.

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

Spring hat eine großartige ReflectionUtils-Komponente, mit der Sie auf sicherste Weise mit Reflection arbeiten können. Und wir werden die Feldklassen dadurch festlegen.

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

Sobald wir feststellen, dass unsere Komponente mit der Mapper- Annotation markiert ist, rufen wir ReflectionUtils.doWithFields auf, wodurch die benötigten Felder auf elegantere Weise festgelegt werden. Wir stellen sicher, dass das Feld existiert, erhalten seinen Namen und überprüfen, ob dieser Name das ist, was wir brauchen.

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

Wir stellen das Feld zur Verfügung (es ist privat).

 ReflectionUtils.makeAccessible(field); 

Wir setzen den Wert in das Feld.

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

Dies ist bereits ausreichend, aber wir können zukünftigen Code zusätzlich vor Versuchen schützen, ihn zu beschädigen, indem wir in den Mapper-Parametern die falsche Entität oder das falsche DTO angeben (optional). Wir prüfen, ob die Klasse, die wir im Feld einstellen werden, 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)) { throw new IllegalArgumentException(String.format("Unable to assign Class %s to expected Class: %s", targetClass, expectedClass)); } 

Dieses Wissen reicht aus, um eine Art Annotation zu erstellen und Kollegen im Projekt mit dieser Magie zu überraschen. Aber seien Sie vorsichtig - seien Sie darauf vorbereitet, dass nicht alle Ihre Fähigkeiten schätzen werden :)

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

Neben dem Beispiel für die Bin-Initialisierung enthält das Projekt auch die AspectJ-Implementierung. Ich wollte auch eine Beschreibung von Spring AOP / AspectJ in den Artikel aufnehmen, fand aber heraus, dass Habré bereits einen wunderbaren Artikel zu diesem Thema hat, sodass ich ihn nicht duplizieren werde. Nun, ich lasse den Arbeitscode und den schriftlichen Test - vielleicht hilft dies jemandem, die Arbeit von AspectJ zu verstehen.

Source: https://habr.com/ru/post/de439594/


All Articles