如何为Spring验证的Entity类注入Java Clock以支持测试?
解决方案
核心思路是将验证逻辑从实体类中分离,通过自定义Spring校验注解+Spring管理的校验器实现Clock的依赖注入,让实体类仅作为数据载体,避免实体类依赖Spring容器的问题。
步骤1:重构Message实体类
移除Validator接口实现,回归实体类本职:
@Entity @EntityListeners(AuditingEntityListener.class) public class Message { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id", length = 100, nullable = false, unique = true) private long id; @Column(name = "start", length = 20, nullable = true) @NotEarlierThanSixDaysAgo // 添加自定义校验注解 private LocalDateTime start; public Message() {} // getter、setter方法 public LocalDateTime getStart() { return start; } public void setStart(LocalDateTime start) { this.start = start; } }
步骤2:创建自定义校验注解
import jakarta.validation.Constraint; import jakarta.validation.Payload; import java.lang.annotation.*; @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = NotEarlierThanSixDaysAgoValidator.class) public @interface NotEarlierThanSixDaysAgo { String message() default "start不能早于当前日期6天前"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
步骤3:实现Spring管理的校验器
该校验器作为Spring Bean,可直接注入Clock:
import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.time.Clock; @Component public class NotEarlierThanSixDaysAgoValidator implements ConstraintValidator<NotEarlierThanSixDaysAgo, LocalDateTime> { private final Clock clock; // 构造注入Clock @Autowired public NotEarlierThanSixDaysAgoValidator(Clock clock) { this.clock = clock; } @Override public void initialize(NotEarlierThanSixDaysAgo constraintAnnotation) { // 初始化逻辑(按需添加) } @Override public boolean isValid(LocalDateTime start, ConstraintValidatorContext context) { if (start == null) { return true; // 允许null,可根据需求调整 } LocalDateTime sixDaysAgo = LocalDateTime.now(clock).minusDays(6); return !start.isBefore(sixDaysAgo); } }
步骤4:配置Clock Bean
在配置类中定义默认Clock实现,测试时可替换:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.time.Clock; @Configuration public class ClockConfig { @Bean public Clock clock() { return Clock.systemDefaultZone(); } }
步骤5:测试时替换固定时间的Clock
在测试类中通过@MockBean替换默认Clock,实现固定时间测试:
import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import jakarta.validation.Validator; import java.time.Clock; import java.time.LocalDateTime; import java.time.ZoneId; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.when; @SpringBootTest public class MessageValidationTest { @Autowired private Validator validator; @MockBean private Clock clock; @Test void testStartValidation() { // 固定当前时间为2024-05-20 12:00:00 LocalDateTime fixedNow = LocalDateTime.of(2024, 5, 20, 12, 0); when(clock.instant()).thenReturn(fixedNow.atZone(ZoneId.systemDefault()).toInstant()); when(clock.getZone()).thenReturn(ZoneId.systemDefault()); Message message = new Message(); // 测试6天前的时间:校验通过 message.setStart(fixedNow.minusDays(6)); var violations = validator.validate(message); assertTrue(violations.isEmpty()); // 测试7天前的时间:校验失败 message.setStart(fixedNow.minusDays(7)); violations = validator.validate(message); assertFalse(violations.isEmpty()); assertEquals("start不能早于当前日期6天前", violations.iterator().next().getMessage()); } }
方案优势
- 实体类不再依赖Spring容器,从数据库加载时无注入问题
- 校验器由Spring管理,可正常注入Clock,满足测试时替换固定时间的需求
- 符合单一职责原则,实体类负责数据存储,验证逻辑单独维护
内容的提问来源于stack exchange,提问作者pbuchheit
相关产品推荐
相关产品推荐

