春季注释:AOP Magic

并非所有人都能获得有关批注如何工作的神圣知识。 似乎这是一种魔术:我在方法/字段/类上放了一条狗狗的咒语-元素开始更改其属性并获得新属性。

图片

今天,我们将以Spring注释为例学习注释的神奇之处:初始化bean字段。

像往常一样,在文章结尾处,有一个指向GitHub上项目的链接,可以下载该链接并查看一切工作方式。

在上一篇文章中,我描述了ModelMapper库的操作,该库使您可以将实体和DTO相互转换。 我们将在此映射器的示例上掌握批注的工作。

在项目中,我们需要几个相关的实体和DTO。 我将选择性地给一对。

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


映射器。 在相应的文章中描述了为什么以这种方式安排它。

EntityDtoMapper
 public interface EntityDtoMapper<E extends AbstractEntity, D extends AbstractDto> { E toEntity(D dto); D toDto(E entity); } 


抽象映射器
 @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) { } } 


行星地图
 @Component public class PlanetMapper extends AbstractMapper<Planet, PlanetDto> { PlanetMapper() { super(Planet.class, PlanetDto.class); } } 


字段初始化。


抽象的映射器类在Class类中有两个字段,我们需要在实现中对其进行初始化。

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

现在,我们通过构造函数执行此操作。 这不是最优雅的解决方案,尽管对我而言确实如此。 但是,我建议更进一步并编写注释,以在没有构造函数的情况下设置这些字段。

首先,编写注释本身。 无需添加其他依赖项。

为了使魔术狗出现在班上,我们将编写以下内容:

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

@Retention(RetentionPolicy.RUNTIME) -定义注释在编译时将遵循的策略。 其中有三个:

-编译期间不会考虑此类注释。 此选项不适合我们。

CLASS-注释将在编译时应用。 这个选项是
默认策略。

RUNTIME-注释将在编译期间考虑在内,此外,虚拟机将继续将其视为注释,也就是说,可以在代码执行过程中以递归方式调用它们,并且由于我们将通过处理器使用注释,因此这是适合我们的选项。

目标 ({ElementType.TYPE}) -定义可以在此注释上挂起的内容。 它可以是类,方法,字段,构造函数,局部变量,参数等-仅有10个选项。 在我们的例子中, TYPE表示类(接口)。

在注释中,我们定义字段。 字段可以具有默认值(例如,默认的“默认字段”),那么有可能不填写它们。 如果没有默认值,则必须填写该字段。

现在,让我们在映射器的实现上添加注释并填写字段。

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

我们指出,映射器的本质是Planet.class,而DTO是PlanetDto.class。

为了将注释参数注入到我们的bean中,我们当然将使用BeanPostProcessor。 对于那些不知道的人,在初始化每个bean时执行BeanPostProcessor。 界面中有两种方法:

postProcessBeforeInitialization()-在初始化bean之前执行。

postProcessAfterInitialization()-在bean初始化之后执行。

著名的开膛手Evgeny Borisov的视频中对此过程进行了更详细的介绍,该视频被称为:“ Evgeny Borisov-Spring-ripper”。 我建议看看。

所以在这里。 我们有一个带有Mapper批注的bean,其参数包含Class类的字段。 您可以将任何类的任何字段添加到注释中。 然后,我们获得这些字段值,并可以对它们执行任何操作。 在本例中,我们使用注释值初始化bean字段。

为此,我们创建一个MapperAnnotationProcessor(根据Spring的规则,所有注释处理器都必须以... AnnotationProcessor结尾)并从BeanPostProcessor继承它。 为此,我们将需要覆盖这两种方法。

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

如果有垃圾箱,我们将使用注释参数对其进行初始化。 我们将使用单独的方法进行此操作。 最简单的方法:

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

在bin初始化期间,我们将遍历它们,如果在bin上方找到了Mapper注释,则将使用注释参数初始化bin字段。

此方法简单但不完美,并且存在漏洞。 我们不代表垃圾箱,而是依靠对该垃圾箱的一些了解。 程序员依赖于其结论的任何代码都是不好的,而且容易受到攻击。 是的,在未选中的电话会议上,您的意见宣誓就职。

做好一切事情的任务很困难,但可行。

Spring具有出色的ReflectionUtils组件,可让您以最安全的方式处理反射。 我们将通过它设置字段类。

我们的init()方法如下所示:

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

一旦发现我们的组件已用Mapper批注标记,我们将调用ReflectionUtils.doWithFields,这将以一种更加优雅的方式设置我们需要的字段。 我们确保该字段存在,获取其名称并检查该名称是否是我们所需要的。

 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(); field.set(bean, targetClass); 

这已经足够了,但是我们还可以通过在映射器参数(可选)中指示错误的实体或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)); } 

这些知识足以在这种项目中创建某种注释,并使项目同事感到惊讶。 但要小心-为并非所有人都会欣赏您的技能做好准备:)

Github上的项目在这里: promoscow@annotations.git

除了bin初始化示例外,该项目还包含AspectJ实现。 我还想在文章中包含有关Spring AOP / AspectJ的描述,但是我发现Habré在该主题上已经有一篇很棒的文章 ,因此我不会重复。 好吧,我将保留工作代码和书面测试-也许这将有助于某人理解AspectJ的工作。

Source: https://habr.com/ru/post/zh-CN439594/


All Articles