使用javax.validation.Validator未检测到约束违规?对象未赋值问题
问题描述
当对象未被完整赋值时,使用javax.validation.Validator未检测到预期的约束违规。测试场景中,配置文件缺少数据库密码字段,但校验结果显示0个约束违规,与预期不符。
配置文件 multi-tenant-database-config-invalid-no-schema.yml
tenants: "001": tenantId: "001" configuration: database: url: "jdbc:postgresql://localhost:5432/postgres?currentSchema=001" username: "postgres"
JUnit测试类
public class AppTest { @Test public void testInvalid() throws Exception { Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); File multiTenantConfigurationFile = getMultiTenantTestConfiguration("config/multi-tenant-database-config-invalid-no-schema.yml"); DatabaseEnabledTenants databaseEnabledTenants = objectMapper.readValue(multiTenantConfigurationFile, DatabaseEnabledTenants.class); Set<ConstraintViolation<DatabaseEnabledTenants>> constraintViolations = validator .validate(databaseEnabledTenants); Assertions.assertEquals(0, constraintViolations.size()); // <- 预期此处失败! } private File getMultiTenantTestConfiguration(String filename) throws URISyntaxException { ClassLoader classLoader = getClass().getClassLoader(); URL resource = classLoader.getResource(filename); File file = new File(resource.toURI()); return file; } }
相关实体类
DatabaseEnabledTenants.java
@Valid @Component @ConfigurationProperties(prefix = "multi-tenancy") public class DatabaseEnabledTenants { @Valid @NotNull private Map<String, DatabaseEnabledTenant> tenants = new HashMap<>(); public Map<String, DatabaseEnabledTenant> getTenants() { return tenants; } public void setTenants(Map<String, DatabaseEnabledTenant> tenants) { this.tenants = tenants; } }
DatabaseEnabledTenant.java
@Valid public class DatabaseEnabledTenant extends TenantDetails { @Valid @NotNull private Configuration configuration; /** 空构造函数,后续赋值。 */ public DatabaseEnabledTenant() {} public Configuration getConfiguration() { return configuration; } public void setConfiguration(Configuration configuration) { this.configuration = configuration; } }
Configuration.java
public class Configuration { @NotNull private Database database; public Configuration() {} public Database getDatabase() { return database; } public void setDatabase(Database database) { this.database = database; } }
Database.java
public class Database { @JsonProperty("url") @NotBlank(message = "URL不得在租户配置中为空") @SchemaNameInJdbcUrlConstraint private String jdbcUrl; @NotBlank(message = "数据库用户名不得在租户配置中为空") private String username; @NotNull(message = "数据库密码不得在租户配置中为null") private String password; public Database() {} public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
原因分析
1. 嵌套对象未添加@Valid注解触发递归校验
Configuration类的database字段仅标注了@NotNull,缺少@Valid注解。Bean Validation需要通过@Valid来触发嵌套对象的递归校验,因此只会检查database是否不为null,不会深入校验Database类内部的password、jdbcUrl等字段的约束。
2. Database类字段与getter/setter不匹配
Database类中字段定义为jdbcUrl,但对应的getter/setter为getUrl()/setUrl(),且方法内部操作未定义的url变量(存在编译错误):
- ObjectMapper依赖
@JsonProperty("url")能将配置值映射到jdbcUrl字段,但Bean Validation默认基于JavaBean属性(getter方法对应的属性名url)校验,字段jdbcUrl上的约束无法关联到属性url,导致@NotBlank和自定义约束失效。 - 代码存在语法错误,未定义
url变量,无法正常编译运行。
3. @ConfigurationProperties前缀在测试场景未生效
测试中使用ObjectMapper直接反序列化YAML,未启用Spring的@ConfigurationProperties绑定机制,因此@ConfigurationProperties(prefix = "multi-tenancy")不生效。不过当前配置文件根节点tenants与类字段名匹配,反序列化正常,并非校验失败的直接原因。
解决方案
1. 为Configuration类的database字段添加@Valid注解
修改Configuration.java,添加@Valid以触发嵌套校验:
public class Configuration { @Valid @NotNull private Database database; // 原getter/setter保持不变 }
2. 修正Database类的字段与getter/setter匹配问题
方案一:统一字段名与getter/setter
将字段名改为url,确保与getter/setter一致:
public class Database { @JsonProperty("url") @NotBlank(message = "URL不得在租户配置中为空") @SchemaNameInJdbcUrlConstraint private String url; @NotBlank(message = "数据库用户名不得在租户配置中为空") private String username; @NotNull(message = "数据库密码不得在租户配置中为null") private String password; public Database() {} public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } // 原username、password的getter/setter不变 }
方案二:调整getter/setter适配字段名
保持字段名为jdbcUrl,修改getter/setter并调整@JsonProperty位置:
public class Database { @NotBlank(message = "URL不得在租户配置中为空") @SchemaNameInJdbcUrlConstraint private String jdbcUrl; @NotBlank(message = "数据库用户名不得在租户配置中为空") private String username; @NotNull(message = "数据库密码不得在租户配置中为null") private String password; public Database() {} @JsonProperty("url") public String getJdbcUrl() { return jdbcUrl; } @JsonProperty("url") public void setJdbcUrl(String jdbcUrl) { this.jdbcUrl = jdbcUrl; } // 原username、password的getter/setter不变 }
3. (可选)测试中启用Spring配置绑定
若需模拟Spring的@ConfigurationProperties绑定,可使用@SpringBootTest配合@TestPropertySource,或通过ConfigurationPropertiesBindingPostProcessor完成绑定,确保前缀配置生效。
内容的提问来源于stack exchange,提问作者bobbyrne01

