Spring Boot 3升级后Spring HATEOAS HAL-FORMS属性验证提示失效的问题咨询
我最近碰到了一个棘手的问题:升级到Spring Boot 3之后,Spring HATEOAS在HAL-FORMS中不再支持模板关联的属性验证提示了。看起来根源在于Spring Boot切换到了新版本的Hibernate和Jakarta Validation API,但Spring HATEOAS的PropertyUtils组件还没跟上这个变化——它的主分支目前仍在导入javax.validation.constraints.NotNull,和Spring Boot 3依赖的版本存在冲突。
我准备了一个极简的单文件+POM示例,能清晰展示这个问题在Spring Boot 2.7.6下正常工作,升级到3.0.5就失效的情况:
示例POM文件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.6</version> <!-- <version>3.0.5</version> --> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>spring-hateoas-affordance-sample</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-hateoas-affordance-sample</name> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
示例Java代码
package com.example.demo; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; import org.hibernate.validator.constraints.Length; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.config.EnableHypermediaSupport; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.validation.constraints.NotNull; //import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.Value; @SpringBootApplication @EnableHypermediaSupport(type = HypermediaType.HAL_FORMS) public class SpringHateoasAffordanceSampleApplication { public static void main(String[] args) { SpringApplication.run(SpringHateoasAffordanceSampleApplication.class, args); } @RestController class FooController { private static final String TEMPLATE = "Hello, %s!"; @GetMapping("/") public HttpEntity<FooRepresentation> get( @RequestParam(value = "name", defaultValue = "foo") String name) { var controller = methodOn(FooController.class); return new ResponseEntity<>( new FooRepresentation(String.format(TEMPLATE, name)) .add(linkTo(controller.get(name)).withSelfRel()) .add(linkTo(controller.post(new FooModel("foo"))).withRel("foo")) , HttpStatus.OK); } @PostMapping("/foo") public HttpEntity<FooModel> post(@RequestBody FooModel fooModel) { return new ResponseEntity<>(fooModel, HttpStatus.OK); } } @Value @EqualsAndHashCode(callSuper=false) class FooRepresentation extends RepresentationModel<FooRepresentation> { private final String name; } @Data @AllArgsConstructor public class FooModel { @NotNull @Length(min = 2, max = 10) private String name; } }
测试现象对比
- Spring Boot 2.7.6下的正常响应:访问
http://localhost:8080,返回的HAL-FORMS模板里包含了@NotNull和@Length对应的验证提示:
{
"name": "Hello, foo!",
"_links": {
"self": {
"href": "/?name=foo"
},
"foo": {
"href": "/foo"
}
},
"_templates": {
"default": {
"method": "POST",
"properties": [
{
"name": "name",
"required": true,
"minLength": 2,
"maxLength": 10,
"type": "text"
}
],
"target": "/foo"
}
}
}
- Spring Boot 3.0.5下的异常响应:把POM里的版本改成3.0.5,同时把
NotNull的导入换成jakarta.validation.constraints.NotNull,再访问同一个地址,返回的模板里丢失了所有验证相关的提示:
{
"name": "Hello, foo!",
"_links": {
"self": {
"href": "/?name=foo"
},
"foo": {
"href": "/foo"
}
},
"_templates": {
"default": {
"method": "POST",
"properties": [
{
"name": "name",
"type": "text"
}
],
"target": "/foo"
}
}
}
虽然@Length的导入包没有变化,但我猜测它同样是因为版本冲突导致失效的。
想请教大家:有没有什么配置可以临时解决这个问题?或者Spring HATEOAS有没有计划在近期支持Spring Boot 3的这个场景?
备注:内容来源于stack exchange,提问作者g10n

