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

Quarkus中hibernate-validator无法校验DTO的问题排查与解决

Quarkus接口校验响应异常问题分析与解决

问题场景

基于Quarkus开发的PDF生成接口,当请求参数name为null或空值时,预期返回400状态码及校验提示,但Postman仅显示400状态码,同时提示“Failed to load PDF file.”和“The request cannot be fulfilled due to bad syntax.”。接口相关代码及依赖如下:

核心代码

import com.company.report.dto.ReportDto
import com.company.report.service.ReportService
import javax.enterprise.context.ApplicationScoped
import javax.validation.Valid

import javax.ws.rs.Consumes
import javax.ws.rs.POST
import javax.ws.rs.Path
import javax.ws.rs.Produces
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response

@ApplicationScoped
@Path("/app")
class ReportController(
    private val reportService: ReportService
) {

    @POST
    @Path("/report")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces("application/pdf")
    fun generateReport(@Valid reportDto: ReportDto): Response {
        return Response
            .ok(reportService.generatePdfReport(reportDto), "application/pdf")
            .header("Content-Disposition", "inline; filename=\"Report.pdf\"")
            .build()
    }
}

// ReportDto
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import javax.validation.constraints.NotNull

@JsonIgnoreProperties(ignoreUnknown = true)
class ReportDto(
    @JsonProperty("rows") @NotNull val rows: List<ReportRowDto>
)

// ReportRowDto
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull

@JsonIgnoreProperties(ignoreUnknown = true)
class ReportRowDto (
    @JsonProperty("name") @NotBlank(message = "name cant be blank") @NotNull(message = "name cant be null") val name: String
)

// 异常映射器
@Provider
class ValidationExceptionMapper : ExceptionMapper<ConstraintViolationException> {
    override fun toResponse(exception: ConstraintViolationException): Response {
        return Response.status(Response.Status.BAD_REQUEST)
            .entity("Validation failed: ${exception.message}")
            .type(MediaType.TEXT_PLAIN)
            .build()
    }
}

依赖配置

<!-- 子pom引入的校验依赖 -->
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-validator</artifactId>
</dependency>

<!-- 父pom中无法修改的依赖 -->
<dependency>
  <groupId>javax.annotation</groupId>
  <artifactId>javax.annotation-api</artifactId>
  <version>1.3.2</version>
</dependency>

问题原因

  1. 响应格式预期冲突:接口方法通过@Produces("application/pdf")声明默认返回PDF格式,Postman会以此为预期格式解析响应。当校验失败时,异常映射器返回text/plain格式的错误信息,Postman仍尝试按PDF格式解析文本内容,导致加载失败提示。
  2. 注解兼容性问题:父pom引入的javax.annotation-api 1.3.2(Java EE规范)与Quarkus使用的Jakarta EE注解存在冲突,可能导致ValidationExceptionMapper未被Quarkus正确识别和注册,使得校验异常的响应信息无法正常返回。

解决方法

方法1:修复异常映射器的响应兼容性

保持接口@Produces声明不变,修改ValidationExceptionMapper,确保响应能被客户端正确识别:

@Provider
@Priority(Priorities.ENTITY_CODER) // 提高优先级,确保先于默认异常处理
class ValidationExceptionMapper : ExceptionMapper<ConstraintViolationException> {
    override fun toResponse(exception: ConstraintViolationException): Response {
        val errorMsg = exception.constraintViolations.joinToString("; ") {
            "${it.propertyPath}: ${it.message}"
        }
        return Response.status(Response.Status.BAD_REQUEST)
            .entity("Validation failed: $errorMsg")
            .type(MediaType.TEXT_PLAIN)
            // 允许客户端读取Content-Type头,确保按文本格式解析
            .header("Access-Control-Expose-Headers", "Content-Type")
            .build()
    }
}
  • 加入@Priority注解确保异常映射器优先执行,避免被默认处理逻辑覆盖。
  • 优化错误信息,拼接所有校验失败细节,提高可读性。
  • 添加Access-Control-Expose-Headers,确保客户端能正确读取Content-Type响应头,从而按文本格式解析错误信息。

方法2:排除冲突的javax注解依赖

在子pom中排除父pom引入的javax.annotation-api依赖,使用Quarkus自带的Jakarta注解:

<dependency>
    <groupId>javax.annotation</groupId>
    <artifactId>javax.annotation-api</artifactId>
    <version>1.3.2</version>
    <exclusions>
        <exclusion>
            <groupId>*</groupId>
            <artifactId>*</artifactId>
        </exclusion>
    </exclusions>
    <scope>provided</scope> <!-- 标记为provided,避免与Quarkus依赖冲突 -->
</dependency>

Quarkus本身集成了Jakarta EE的注解实现,排除或标记为provided后,可解决注解冲突导致的异常映射器未注册问题。

方法3:动态指定响应格式(可选)

如果需要更灵活的响应格式控制,可以移除接口方法上的@Produces注解,改为在返回Response时动态指定:

// 移除@Produces("application/pdf")
fun generateReport(@Valid reportDto: ReportDto): Response {
    return Response
        .ok(reportService.generatePdfReport(reportDto), MediaType.APPLICATION_PDF)
        .header("Content-Disposition", "inline; filename=\"Report.pdf\"")
        .build()
}

这样客户端不会被固定预期格式,校验失败返回的text/plain响应能被正确解析。

内容的提问来源于stack exchange,提问作者Pavel

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 16:15:07