O conhecimento sagrado de como as anotações funcionam está longe de ser acessível a todos. Parece que isso é algum tipo de mágica: coloquei um feitiço com um cachorro sobre o método / campo / classe - e o elemento começa a mudar suas propriedades e obter novas.

Hoje vamos aprender a mágica das anotações usando as Anotações da Primavera como exemplo: inicializando campos de bean.
Como de costume, no final do artigo, há um link para o projeto no GitHub, que pode ser baixado e ver como tudo funciona.
Em um
artigo anterior
, descrevi a operação da biblioteca ModelMapper, que permite converter uma entidade e um DTO entre si. Dominaremos o trabalho das anotações no exemplo deste mapeador.
No projeto, precisamos de algumas entidades relacionadas e do DTO. Vou dar seletivamente um 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; }
Mapeador. Por que está organizado dessa maneira é descrito no
artigo correspondente.
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); } }
Inicialização de campo.
A classe mapeador abstrata possui dois campos na classe Class que precisamos inicializar na implementação.
private Class<E> entityClass; private Class<D> dtoClass;
Agora fazemos isso através do construtor. Não é a solução mais elegante, embora para mim mesma. No entanto, sugiro ir além e escrever uma anotação que definirá esses campos sem um construtor.
Para começar, escreva a anotação em si. Nenhuma dependência adicional precisa ser adicionada.
Para que um cão mágico apareça na frente da classe, escreveremos o seguinte:
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented public @interface Mapper { Class<?> entity(); Class<?> dto(); }
@Retention (RetentionPolicy.RUNTIME) - define a política que a anotação seguirá ao compilar. Existem três deles:
FONTE - essas anotações não serão levadas em consideração durante a compilação. Esta opção não nos convém.
CLASS - anotações serão aplicadas na compilação. Esta opção é
política padrão.
RUNTIME - as anotações serão levadas em consideração durante a compilação; além disso, a máquina virtual continuará a vê-las como anotações, ou seja, elas podem ser chamadas recursivamente já durante a execução do código e, como trabalharemos com anotações através do processador, esta é a opção que mais nos convém. .
Alvo ({ElementType.TYPE}) - define em que esta anotação pode ser pendurada. Pode ser uma classe, método, campo, construtor, variável local, parâmetro e assim por diante - apenas 10 opções. No nosso caso,
TYPE significa uma classe (interface).
Na anotação, definimos os campos. Os campos podem ter valores padrão ("campo padrão" padrão, por exemplo), então existe a possibilidade de não preenchê-los. Se não houver valores padrão, o campo deverá ser preenchido.
Agora vamos colocar uma anotação em nossa implementação do mapeador e preencher os campos.
@Component @Mapper(entity = Planet.class, dto = PlanetDto.class) public class PlanetMapper extends AbstractMapper<Planet, PlanetDto> {
Observamos que a essência do nosso mapeador é Planet.class e o DTO é PlanetDto.class.
Para injetar os parâmetros de anotação em nosso bean, é claro que usaremos o BeanPostProcessor. Para quem não sabe, o BeanPostProcessor é executado quando cada bean é inicializado. Existem dois métodos na interface:
postProcessBeforeInitialization () - executado antes de inicializar o bean.
postProcessAfterInitialization () - executado após a inicialização do bean.
Esse processo é descrito em mais detalhes no vídeo do famoso estripador da primavera Evgeny Borisov, chamado: "Evgeny Borisov - estripador da primavera". Eu recomendo ver.
Então aqui. Temos um bean com uma anotação
Mapper com parâmetros que contêm campos da classe Class. Você pode adicionar qualquer campo de qualquer classe às anotações. Em seguida, obtemos esses valores de campo e podemos fazer qualquer coisa com eles. No nosso caso, inicializamos os campos do bean com valores de anotação.
Para fazer isso, criamos um MapperAnnotationProcessor (de acordo com as regras do Spring, todos os processadores de anotação devem terminar em ... AnnotationProcessor) e herdá-lo do BeanPostProcessor. Ao fazer isso, precisaremos substituir esses dois 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; } }
Se houver uma posição, nós a inicializamos com parâmetros de anotação. Faremos isso em um método separado. A maneira mais 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 a inicialização do compartimento, nós os percorremos e, se encontrarmos a anotação
Mapper acima do compartimento, inicializamos os campos do compartimento com os parâmetros de anotação.
Este método é simples, mas não perfeito e contém vulnerabilidade. Não tipificamos uma posição, mas confiamos em algum conhecimento dessa posição. E qualquer código no qual o programador se baseia em suas próprias conclusões é ruim e vulnerável. Sim, e a Idéia xinga na ligação Desmarcada.
A tarefa de fazer tudo certo é difícil, mas viável.
O Spring possui um ótimo componente ReflectionUtils que permite trabalhar com reflexão da maneira mais segura. E definiremos as classes de campo através dele.
Nosso método init () ficará assim:
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; }
Assim que descobrimos que nosso componente está marcado com a anotação
Mapper , chamamos ReflectionUtils.doWithFields, que definirá os campos necessários de uma maneira mais elegante. Garantimos que o campo existe, obtenha seu nome e verifique se esse nome é o que precisamos.
assert field != null; String fieldName = field.getName(); if (!fieldName.equals("entityClass") && !fieldName.equals("dtoClass")) { return; }
Disponibilizamos o campo (é privado).
ReflectionUtils.makeAccessible(field);
Definimos o valor no campo.
Class<?> targetClass = fieldName.equals("entityClass") ? mapper.entity() : mapper.dto(); field.set(bean, targetClass);
Isso já é suficiente, mas podemos proteger adicionalmente o código futuro de tentativas de quebrá-lo, indicando a entidade ou o DTO errado nos parâmetros do mapeador (opcional). Verificamos que a classe que vamos definir no campo é realmente adequada para isso.
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)); }
Esse conhecimento é suficiente para criar algum tipo de anotação e surpreender os colegas do projeto com essa mágica. Mas tenha cuidado - esteja preparado para o fato de que nem todos apreciarão sua habilidade :)
O projeto no Github está aqui:
promoscow@annotations.gitAlém do exemplo de inicialização no depósito, o projeto também contém a implementação do AspectJ. Eu também queria incluir uma descrição do Spring AOP / AspectJ no artigo, mas descobri que Habré já tem um
artigo maravilhoso sobre esse assunto, por isso não o duplicarei. Bem, deixarei o código de trabalho e a prova escrita - talvez isso ajude alguém a entender o trabalho do AspectJ.