El conocimiento sagrado de cómo funcionan las anotaciones no es accesible para todos. Parece que esto es algún tipo de magia: pongo un hechizo con un perro sobre el método / campo / clase, y el elemento comienza a cambiar sus propiedades y obtener otras nuevas.

Hoy aprenderemos la magia de las anotaciones usando Spring Annotations como ejemplo: inicializando campos de beans.
Como de costumbre, al final del artículo hay un enlace al proyecto en GitHub, que se puede descargar y ver cómo funciona todo.
En un
artículo anterior
, describí el funcionamiento de la biblioteca ModelMapper, que le permite convertir una entidad y DTO entre sí. Dominaremos el trabajo de las anotaciones en el ejemplo de este mapeador.
En el proyecto, necesitamos un par de entidades relacionadas y DTO. Daré selectivamente un par.
Planeta@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 Por qué está organizado de esta manera se describe en el
artículo correspondiente.
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); } }
Inicialización de campo.
La clase del asignador abstracto tiene dos campos en la clase Clase que necesitamos inicializar en la implementación.
private Class<E> entityClass; private Class<D> dtoClass;
Ahora hacemos esto a través del constructor. No es la solución más elegante, aunque para mí misma. Sin embargo, sugiero ir más allá y escribir una anotación que establezca estos campos sin un constructor.
Para empezar, escribe la anotación en sí. No es necesario agregar dependencias adicionales.
Para que un perro mágico aparezca delante de la clase, escribiremos lo siguiente:
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented public @interface Mapper { Class<?> entity(); Class<?> dto(); }
@Retention (RetentionPolicy.RUNTIME) : define la política que seguirá la anotación al compilar. Hay tres de ellos:
FUENTE : tales anotaciones no se tendrán en cuenta durante la compilación. Esta opción no nos conviene.
CLASE : las anotaciones se aplicarán en la compilación. Esta opción es
política predeterminada
TIEMPO DE
EJECUCIÓN : las anotaciones se tendrán en cuenta durante la compilación, además, la máquina virtual continuará viéndolas como anotaciones, es decir, ya se pueden llamar recursivamente durante la ejecución del código, y dado que vamos a trabajar con las anotaciones a través del procesador, esta es la opción que nos conviene .
Target ({ElementType.TYPE}) : define en qué se puede colgar esta anotación. Puede ser una clase, método, campo, constructor, variable local, parámetro, etc., solo 10 opciones. En nuestro caso,
TYPE significa una clase (interfaz).
En la anotación, definimos los campos. Los campos pueden tener valores predeterminados (por ejemplo, "campo predeterminado"), entonces existe la posibilidad de no completarlos. Si no hay valores predeterminados, se debe completar el campo.
Ahora pongamos una anotación en nuestra implementación del mapeador y completemos los campos.
@Component @Mapper(entity = Planet.class, dto = PlanetDto.class) public class PlanetMapper extends AbstractMapper<Planet, PlanetDto> {
Señalamos que la esencia de nuestro mapeador es Planet.class, y el DTO es PlanetDto.class.
Para inyectar los parámetros de anotación en nuestro bean, por supuesto, usaremos el BeanPostProcessor. Para aquellos que no saben, BeanPostProcessor se ejecuta cuando se inicializa cada bean. Hay dos métodos en la interfaz:
postProcessBeforeInitialization (): ejecutado antes de inicializar el bean.
postProcessAfterInitialization (): ejecutado después de la inicialización del bean.
Este proceso se describe con más detalle en el video del famoso destripador de primavera Evgeny Borisov, que se llama: "Evgeny Borisov - Destripador de primavera". Recomiendo ver
Entonces aquí. Tenemos un bean con una anotación
Mapper con parámetros que contienen campos de la clase Class. Puede agregar cualquier campo de cualquier clase a las anotaciones. Luego obtenemos estos valores de campo y podemos hacer cualquier cosa con ellos. En nuestro caso, inicializamos los campos de bean con valores de anotación.
Para hacer esto, creamos un MapperAnnotationProcessor (de acuerdo con las reglas de Spring, todos los procesadores de anotación deben terminar en ... AnnotationProcessor) y heredarlo de BeanPostProcessor. Al hacerlo, tendremos que anular esos dos métodos.
@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; } }
Si hay un contenedor, lo inicializamos con parámetros de anotación. Haremos esto en un método separado. La forma más fácil:
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; }
Durante la inicialización del bin, los revisamos y si encontramos la anotación
Mapper encima del bin, inicializamos los campos del bin con los parámetros de anotación.
Este método es simple pero no perfecto y contiene vulnerabilidad. No tipificamos un contenedor, pero confiamos en algún conocimiento de este contenedor. Y cualquier código en el que el programador confíe en sus propias conclusiones es malo y vulnerable. Sí, y la idea jura por la llamada no verificada.
La tarea de hacer todo bien es difícil, pero factible.
Spring tiene un excelente componente ReflectionUtils que le permite trabajar con la reflexión de la manera más segura. Y estableceremos las clases de campo a través de él.
Nuestro método init () se verá así:
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; }
Tan pronto como descubramos que nuestro componente está marcado con la anotación
Mapper , llamamos a ReflectionUtils.doWithFields, que establecerá los campos que necesitamos de una manera más elegante. Nos aseguramos de que el campo exista, obtenga su nombre y verifiquemos que este nombre sea el que necesitamos.
assert field != null; String fieldName = field.getName(); if (!fieldName.equals("entityClass") && !fieldName.equals("dtoClass")) { return; }
Hacemos que el campo esté disponible (es privado).
ReflectionUtils.makeAccessible(field);
Establecemos el valor en el campo.
Class<?> targetClass = fieldName.equals("entityClass") ? mapper.entity() : mapper.dto(); field.set(bean, targetClass);
Esto ya es suficiente, pero también podemos proteger el código futuro de los intentos de romperlo al indicar la entidad o el DTO incorrecto en los parámetros del mapeador (opcional). Verificamos que la clase que vamos a establecer en el campo es realmente adecuada para esto.
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)); }
Este conocimiento es suficiente para crear algún tipo de anotación y sorprender a los colegas en el proyecto con esta magia. Pero tenga cuidado: prepárese para el hecho de que no todos apreciarán su habilidad :)
El proyecto en Github está aquí:
promoscow@annotations.gitAdemás del ejemplo de inicialización bin, el proyecto también contiene la implementación de AspectJ. También quería incluir una descripción de Spring AOP / AspectJ en el artículo, pero descubrí que Habré ya tiene un
artículo maravilloso sobre este tema, por lo que no lo duplicaré. Bueno, dejaré el código de trabajo y la prueba escrita; tal vez esto ayude a alguien a comprender el trabajo de AspectJ.