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

今天,我们将以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的工作。