如何在Spring测试中实现带可读属性的自定义注解?
问题描述
我熟悉复合注解,但调研后发现它满足不了我的特定需求。我想创建一个测试用的自定义类注解,带属性来定制测试上下文,比如这样的示例:
public @interface MyTestingAnnotation { String myTestingAttribute(); }
这个注解的属性值要能被自定义的@TestConfiguration读取。
实际场景是我需要模拟时钟,让测试在特定时间点运行,避免依赖硬件时钟,所以定义了java.time.Clock Bean。目前只能通过注解启用模拟时钟,但指定时间得依赖@TestPropertySource,相关配置类如下:
/** * 为{@link Clock} Bean提供模拟配置 * <p></p> * 注入到所有需要时间访问的Spring Bean中,可设置为固定时间以实现可重复测试 * 通常所有场景下时区默认设为UTC * <p></p> * 该Bean读取属性<code>clock.fixedInstant</code>来确定要模拟给所有Bean的当前时间 * 默认值是一个已知时间:<code>2023年11月4日15:45:32 UTC</code> * 单个测试可以通过以下方式覆盖固定时间: * <code>@TestPropertySource(properties = "clock.fixed-instant:[... ISO 8601 ...]")</code> */ @TestConfiguration public class MockClockConfiguration { @Value("${clock.fixed-instant:2023-11-04T15:45:32.000000Z}") private OffsetDateTime clockFixedInstant; private static final ThreadLocal<OffsetDateTime> fixedInstantLocal = new ThreadLocal<>(); /** * 测试用的便捷方法,用于获取固定时间 * @return */ public static OffsetDateTime getFixedInstant(){ return fixedInstantLocal.get(); } @Primary @Bean public Clock fixedClock() { fixedInstantLocal.set(clockFixedInstant); return Clock.fixed(Instant.from(clockFixedInstant), clockFixedInstant.getOffset()); } }
我希望改成用@MockClock(at = "2024-02-15T12:35:00+04:00")这样的注解标注测试类,不用再写Property Source的语法。我知道怎么在自定义注解里用@AliasFor,目前只能通过元注解的@Import(MockClockConfiguration.class)引入配置,请问在Spring里怎么实现这个需求?
解决方案
要实现通过自定义注解直接传递时间参数给MockClock配置,你需要结合自定义注解和ImportBeanDefinitionRegistrar来读取注解属性并动态注册Clock Bean,具体步骤如下:
1. 定义@MockClock自定义注解
创建目标注解,指定作用在类上,保留到运行时,并导入自定义的注册器(代替直接导入原配置类):
import org.springframework.context.annotation.Import; import java.lang.annotation.*; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import(MockClockRegistrar.class) // 导入自定义注册器 public @interface MockClock { // 默认值沿用原配置的固定时间 String at() default "2023-11-04T15:45:32.000000Z"; }
2. 实现ImportBeanDefinitionRegistrar读取注解属性
创建MockClockRegistrar类,实现Spring的ImportBeanDefinitionRegistrar接口,用来读取测试类上的@MockClock注解属性,然后动态注册Clock Bean:
import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.type.AnnotationMetadata; import java.time.OffsetDateTime; import java.time.Clock; import java.time.Instant; public class MockClockRegistrar implements ImportBeanDefinitionRegistrar { private static final ThreadLocal<OffsetDateTime> fixedInstantLocal = new ThreadLocal<>(); @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { // 读取当前测试类上的@MockClock注解属性 if (importingClassMetadata.hasAnnotation(MockClock.class.getName())) { String atValue = (String) importingClassMetadata.getAnnotationAttributes(MockClock.class.getName()) .get("at"); OffsetDateTime fixedInstant = OffsetDateTime.parse(atValue); fixedInstantLocal.set(fixedInstant); // 构建Clock Bean的定义 BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(Clock.class, () -> { return Clock.fixed(Instant.from(fixedInstant), fixedInstant.getOffset()); }); builder.setPrimary(true); // 标记为Primary,覆盖默认Clock Bean registry.registerBeanDefinition("fixedClock", builder.getBeanDefinition()); } } // 保留原有的便捷方法,供测试获取固定时间 public static OffsetDateTime getFixedInstant() { return fixedInstantLocal.get(); } }
3. 测试类中使用@MockClock注解
现在你可以直接在测试类上标注@MockClock并指定时间,无需再使用@TestPropertySource:
@SpringBootTest @MockClock(at = "2024-02-15T12:35:00+04:00") public class TimeDependentTest { @Autowired private Clock clock; @Test void testFixedTime() { // 验证时钟时间是否符合注解指定的值 OffsetDateTime expected = OffsetDateTime.parse("2024-02-15T12:35:00+04:00"); assertEquals(expected.toInstant(), clock.instant()); assertEquals(expected.getOffset(), clock.getZone().getRules().getOffset(clock.instant())); } }
关键说明
ImportBeanDefinitionRegistrar:这个接口允许你在Spring上下文初始化阶段,读取注解元数据并动态注册Bean定义,完美适配从自定义注解获取参数并配置Bean的场景。- 替代原配置类:原
MockClockConfiguration的逻辑被整合到注册器中,避免了通过@Value读取配置属性的环节,直接从注解获取参数更直观。 Primary标记:确保注册的Clock Bean优先被注入,覆盖系统中可能存在的其他Clock Bean。
内容的提问来源于stack exchange,提问作者usr-local-ΕΨΗΕΛΩΝ

