You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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-ΕΨΗΕΛΩΝ

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 18:37:50