La connaissance sacrée du fonctionnement des annotations n'est pas accessible à tous. Il semble que ce soit une sorte de magie: je mets un sort avec un chien sur la méthode / le champ / la classe - et l'élément commence à changer ses propriétés et à en obtenir de nouvelles.

Aujourd'hui, nous allons apprendre la magie des annotations en utilisant Spring Annotations comme exemple: initialiser des champs de haricots.
Comme d'habitude, à la fin de l'article, il y a un lien vers le projet sur GitHub, qui peut être téléchargé et voir comment tout fonctionne.
Dans un
article précédent
, j'ai décrit le fonctionnement de la bibliothèque ModelMapper, qui vous permet de convertir une entité et un DTO l'un dans l'autre. Nous maîtriserons le travail des annotations sur l'exemple de ce mappeur.
Dans le projet, nous avons besoin de quelques entités liées et DTO. Je donnerai sélectivement une paire.
Planète@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; }
Mappeur. La raison pour laquelle il est organisé de cette manière est décrite dans l'
article correspondant.
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); } }
Initialisation du champ.
La classe mapper abstraite a deux champs dans la classe Class que nous devons initialiser dans l'implémentation.
private Class<E> entityClass; private Class<D> dtoClass;
Maintenant, nous le faisons via le constructeur. Pas la solution la plus élégante, quoique tout à fait pour moi. Cependant, je suggère d'aller plus loin et d'écrire une annotation qui définira ces champs sans constructeur.
Pour commencer, écrivez l'annotation elle-même. Aucune dépendance supplémentaire ne doit être ajoutée.
Pour qu'un chien magique apparaisse devant la classe, nous écrirons ce qui suit:
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented public @interface Mapper { Class<?> entity(); Class<?> dto(); }
@Retention (RetentionPolicy.RUNTIME) - définit la politique que l'annotation suivra lors de la compilation. Il y en a trois:
SOURCE - ces annotations ne seront pas prises en compte lors de la compilation. Cette option ne nous convient pas.
CLASS - les annotations seront appliquées lors de la compilation. Cette option est
stratégie par défaut.
RUNTIME - les annotations seront prises en compte lors de la compilation, de plus, la machine virtuelle continuera à les voir comme des annotations, c'est-à-dire qu'elles peuvent être appelées récursivement déjà pendant l'exécution du code, et puisque nous allons travailler avec des annotations via le processeur, c'est l'option qui nous convient .
Cible ({ElementType.TYPE}) - définit sur quoi cette annotation peut être accrochée. Il peut s'agir d'une classe, d'une méthode, d'un champ, d'un constructeur, d'une variable locale, d'un paramètre, etc. - seulement 10 options. Dans notre cas,
TYPE signifie une classe (interface).
Dans l'annotation, nous définissons les champs. Les champs peuvent avoir des valeurs par défaut (par défaut "champ par défaut", par exemple), alors il y a une possibilité de ne pas les remplir. S'il n'y a pas de valeurs par défaut, le champ doit être rempli.
Maintenant, mettons une annotation sur notre implémentation du mappeur et remplissons les champs.
@Component @Mapper(entity = Planet.class, dto = PlanetDto.class) public class PlanetMapper extends AbstractMapper<Planet, PlanetDto> {
Nous avons souligné que l'essence de notre mappeur est Planet.class, et le DTO est PlanetDto.class.
Afin d'injecter les paramètres d'annotation dans notre bean, nous utiliserons bien sûr le BeanPostProcessor. Pour ceux qui ne le savent pas, BeanPostProcessor est exécuté lorsque chaque bean est initialisé. Il existe deux méthodes dans l'interface:
postProcessBeforeInitialization () - exécuté avant l'initialisation du bean.
postProcessAfterInitialization () - exécuté après l'initialisation du bean.
Ce processus est décrit plus en détail dans la vidéo du célèbre Spring-ripper Evgeny Borisov, qui s'appelle: "Evgeny Borisov - Spring-ripper". Je recommande de voir.
Alors voilà. Nous avons un bean avec une annotation
Mapper avec des paramètres contenant des champs de la classe Class. Vous pouvez ajouter n'importe quel champ de n'importe quelle classe aux annotations. Ensuite, nous obtenons ces valeurs de champ et pouvons tout faire avec. Dans notre cas, nous initialisons les champs de bean avec des valeurs d'annotation.
Pour ce faire, nous créons un MapperAnnotationProcessor (selon les règles de Spring, tous les processeurs d'annotation doivent se terminer par ... AnnotationProcessor) et l'hériter de BeanPostProcessor. Ce faisant, nous devrons remplacer ces deux méthodes.
@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; } }
S'il y a un bac, nous l'initialisons avec des paramètres d'annotation. Nous le ferons dans une méthode distincte. La manière la plus simple:
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; }
Lors de l'initialisation du bac, nous les parcourons et si nous trouvons l'annotation
Mapper au-dessus du bac, nous initialisons les champs du bac avec les paramètres d'annotation.
Cette méthode est simple mais pas parfaite et contient une vulnérabilité. Nous ne typifions pas un bac, mais comptons sur une certaine connaissance de ce bac. Et tout code dans lequel le programmeur s'appuie sur ses propres conclusions est mauvais et vulnérable. Oui, et l'Idée jure à l'appel non contrôlé.
La tâche de tout faire correctement est difficile, mais réalisable.
Spring a un excellent composant ReflectionUtils qui vous permet de travailler avec la réflexion de la manière la plus sûre. Et nous allons définir les classes de terrain à travers elle.
Notre méthode init () ressemblera à ceci:
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; }
Dès que nous découvrons que notre composant est marqué avec l'annotation
Mapper , nous appelons ReflectionUtils.doWithFields, qui définira les champs dont nous avons besoin d'une manière plus élégante. Nous nous assurons que le champ existe, obtenons son nom et vérifions que ce nom est ce dont nous avons besoin.
assert field != null; String fieldName = field.getName(); if (!fieldName.equals("entityClass") && !fieldName.equals("dtoClass")) { return; }
Nous mettons le champ à disposition (il est privé).
ReflectionUtils.makeAccessible(field);
Nous définissons la valeur dans le champ.
Class<?> targetClass = fieldName.equals("entityClass") ? mapper.entity() : mapper.dto(); field.set(bean, targetClass);
C'est déjà suffisant, mais nous pouvons en outre protéger le futur code contre les tentatives de le casser en indiquant la mauvaise entité ou DTO dans les paramètres du mappeur (facultatif). Nous vérifions que la classe que nous allons mettre sur le terrain est vraiment adaptée à cela.
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)); }
Cette connaissance est suffisante pour créer une sorte d'annotation et surprendre les collègues du projet avec cette magie. Mais soyez prudent - préparez-vous au fait que tous n'apprécieront pas vos compétences :)
Le projet sur Github est ici:
promoscow@annotations.gitEn plus de l'exemple d'initialisation bin, le projet contient également l'implémentation AspectJ. Je voulais également inclure une description de Spring AOP / AspectJ dans l'article, mais j'ai trouvé que Habré avait déjà un
merveilleux article sur ce sujet, donc je ne le reproduirai pas. Eh bien, je vais laisser le code de travail et le test écrit - cela aidera peut-être quelqu'un à comprendre le travail d'AspectJ.