Spring Annotations: AOP Magic

    Sacred knowledge of how annotations work is not available to everyone. It seems that this is some kind of magic: put a spell with a dog over a method / field / class - and the element begins to change its properties and get new ones.

    image

    Today we will learn the magic of annotations using the example of using Spring Annotations: initializing bean fields.

    As usual, at the end of the article there is a link to the project on GitHub, which can be downloaded and see how everything works.

    In the previous article, I described the work of the ModelMapper library, which allows you to convert an entity and a DTO into each other. We will master the work of annotations on the example of this maper.

    In the project, we will need a pair of related entities and DTO. I will give selectively one pair.

    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. Why it is arranged exactly as described in the corresponding article .

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


    Initialization of fields.


    The abstract mapper class has two fields of the class Class, which we need to initialize in the implementation.

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

    Now we do it through the constructor. Not the most elegant solution, albeit quite to itself. However, I propose to go ahead and write an annotation that will label these fields without a constructor.

    For a start, let's write the annotation itself. No additional dependencies should be added.

    In order for a magic dog to appear in front of the class, we will write the following:

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

    @Retention (RetentionPolicy.RUNTIME) - defines the policy that the annotation will follow when compiling. There are three of them:

    SOURCE - such annotations will not be taken into account when compiling. This option does not suit us.

    CLASS - annotations will be applied at compilation. This option is the
    default policy.

    RUNTIME - annotations will be taken into account when compiling, moreover, the virtual machine will continue to see them as annotations, that is, they can be called recursively during the execution of the code, and since we are going to work with annotations via the processor, this option will suit us .

    Target ({ElementType.TYPE})- determines what this annotation can be hung on. It can be a class, a method, a field, a constructor, a local variable, a parameter, and so on - just 10 options. In our case, TYPE means class (interface).

    In the annotation we define the fields. Fields can have default values ​​(default “default field”, for example), then it is possible not to fill them. If there are no default values, the field must be filled.

    Now let's hang up the annotation on our implementation of the mapper and fill in the fields.

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

    We have indicated that the essence of our mapper is Planet.class, and DTO is PlanetDto.class.

    In order to inject the annotation parameters into our bin, we, of course, will go to BeanPostProcessor. For those who do not know - BeanPostProcessor is executed when each bean is initialized. There are two methods in the interface:

    postProcessBeforeInitialization () is executed before the initialization of the bean.

    postProcessAfterInitialization () - executed after the initialization of the bean.

    In more detail, this process is described in the video of the famous Spring-Ripper Evgeny Borisov, which is so called: "Evgeny Borisov - Spring-Ripper." I recommend to look.

    So here. We have a bean with annotation mapperwith parameters containing fields of class Class. In the annotation, you can add any field of any class. Then we get these field values ​​and can do anything with them. In our case, we initialize the bean fields with annotation values.

    To do this, we create the MapperAnnotationProcessor (according to the Spring rules, all annotation processors must end with ... AnnotationProcessor) and inherit it from BeanPostProcessor. In this case, we will need to override those two methods.

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

    If there is a bin, we initialize it with the annotation parameters. We do this in a separate method. The easiest way:

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

    When initializing beans, we run through them and if we find the Mapper annotation above the bin , we initialize the bean fields with the annotation parameters.

    This method is simple, but not perfect and contains vulnerability. We do not typify bin, but rely on some of our knowledge about this bin. And any code in which the programmer relies on his own conclusions is bad and vulnerable. And the Idea swears on Unchecked call.

    The task to do everything right is difficult, but feasible.

    In Spring there is a wonderful component ReflectionUtils, which allows you to work with reflection in the most secure way. And we will label the field classes through it.

    Our init () method will look like this:

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

    As soon as we find out that our component is marked with the Mapper annotation , we call ReflectionUtils.doWithFields, which will wrap the fields we need in a more elegant way. We make sure that the field exists, we get its name and check that this name is the one we need.

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

    We make the field accessible (it's private).

    ReflectionUtils.makeAccessible(field);

    We value the value in the field.

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

    This is already enough, but we can additionally protect the future code from attempts to break it by specifying the wrong entity or DTO (optional) in the mapper parameters. We check that the class we are going to mesh in the field is really suitable for this.

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

    This knowledge is quite enough to create some annotation and surprise your project colleagues with this magic. But be careful - be prepared for not everyone will appreciate your skill :)

    The project on Github is here: promoscow@annotations.git

    In addition to the example of initializing the bins, the project also contains AspectJ implementation. I wanted to include in the article also a description of the work of Spring AOP / AspectJ, but I found that on Habré there is already a wonderful article on this subject, so I will not duplicate it. Well, I will leave the working code and the written test - perhaps this will help someone to understand how AspectJ works.

    Also popular now: