【问题标题】:Accept-Encoding is ignored when matching methods?匹配方法时忽略Accept-Encoding?
【发布时间】:2024-01-16 22:33:01
【问题描述】:

我有一个 JAX-RS REST 资源,它应该能够按照客户通过 Accept-Charset 标头的偏好所指示的不同字符集进行响应。

然而,由于 JAX-RS 似乎默认忽略 Accept-Charset 标头,我编写了两个方法明确说明我想要支持的两个不同字符集:

@GET
@Path("test")
@Produces("text/plain; charset=UTF-8")
public String test_utf8() {
    return "Hello World";
}

@GET
@Path("test")
@Produces("text/plain; charset=cp1047")
public String test_cp1047() {
    return "Hello World";
}

但是,现在使用 curl 调用该方法时:

curl -v -H "Accept-Charset: cp1047;q=1.0, *;q=0" "http://localhost:8080/rest/test" -H "Accept: text/plain"

服务器以 UTF-8 响应:

> GET /rest/test HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.50.3
> Accept-Charset: cp1047;q=1.0, *;q=0
> Accept: text/plain
>
< HTTP/1.1 200 OK
< Connection: keep-alive
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 19
< Date: Mon, 06 Jul 2020 21:29:11 GMT
<
* Curl_http_done: called premature == 0
* Connection #0 to host localhost left intact
Hello World    

另外,服务器日志中会出现一条日志消息:

23:29:11,356 WARN [org.jboss.resteasy.resteasy_jaxrs.i18n](默认 任务2)RESTEASY002142:多个资源方法匹配请求“GET /test"。选择一个。匹配方法:[public java.lang.String org.example.RestResource.test_utf8(), public java.lang.String org.example.RestResource.test_cp1047()]

如何强制服务器接受客户端请求的字符集?

【问题讨论】:

  • 您只能使用一种 REST 方法,而不是使用两种。在方法的正文中,您可以根据您的业务逻辑或请求动态设置响应字符集:return Response.status(Response.Status.OK).entity(...).header("charset=UTF-8").build()

标签: java jax-rs wildfly resteasy


【解决方案1】:

通过创建自定义 ContainerResponseFilter 来解决它,它解析 Accept-Charset 标头并使用客户端请求的“最佳”字符集:

@Provider
@Priority(Priorities.HEADER_DECORATOR)
@HonorAcceptCharset
public class HonorAcceptCharsetFilter implements ContainerResponseFilter {
    private static final Pattern AcceptedEncodingPattern = Pattern.compile("([A-Za-z*0-9_\\-]+)(?:; *[qQ] *= *([0-9]+(?:\\.[0-9]+)?))?");
    public static final String AcceptCharsetHeaderName = "Accept-Charset";
    public static final String ContentTypeHeaderName = "Content-Type";

    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
        MediaType mediaType = responseContext.getMediaType();
        if (mediaType == null)
            return;

        Charset charset = this.determineCharset(requestContext);
        if (charset == null)
            return;

        responseContext.getHeaders().putSingle(ContentTypeHeaderName, mediaType.withCharset(charset.name()));
    }

    private Charset determineCharset(ContainerRequestContext requestContext) {
        String acceptCharset = requestContext.getHeaderString(AcceptCharsetHeaderName);
        if (acceptCharset == null)
            return null;

        List<Map.Entry<Charset, Double>> acceptedCharsets = Arrays.stream(acceptCharset.split(", *"))
                .map(this::parseAcceptedEncoding)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());

        if (acceptCharset.length() == 0)
            return null;
        if (acceptCharset.length() == 1)
            return acceptedCharsets.get(0).getKey();

        OptionalDouble maxQuality = acceptedCharsets.stream().mapToDouble(Map.Entry::getValue).max();
        List<Charset> candidates = acceptedCharsets.stream().filter(it -> it.getValue() == maxQuality.getAsDouble()).map(Map.Entry::getKey).collect(Collectors.toList());
        if (candidates.size() == 1)
            return candidates.get(0);

        if (candidates.stream().anyMatch(it -> it.name().toLowerCase().matches("utf-?8")))
            return StandardCharsets.UTF_8;

        return candidates.get(0);
    }

    private Map.Entry<Charset, Double> parseAcceptedEncoding(String acceptedEncoding) {
        Matcher matcher = AcceptedEncodingPattern.matcher(acceptedEncoding);
        if (!matcher.find())
            return null;

        String charsetName = matcher.group(1);
        if ("*".equals(charsetName))
            return null;
        try {
            if (!Charset.isSupported(charsetName))
                return null;
        } catch (IllegalCharsetNameException ex) {
            return null;
        }

        Charset charset = Charset.forName(charsetName);

        String quality = matcher.group(2);
        double qualityNumber = StringUtils.isAllEmpty(quality) ? 1.0 : Double.parseDouble(quality);
        return new AbstractMap.SimpleEntry<>(charset, qualityNumber);
    }
}

@NameBinding
@Retention(RetentionPolicy.RUNTIME)
public @interface HonorAcceptCharset {
}

现在我只需要一种可以用@HonorAcceptCharset 装饰的方法来启用过滤器(我不想全局启用它,因为我不想将字符集添加到二进制类型。也许过滤器看起来进入媒体类型,并根据预定义的列表确定是否要激活。

默认情况下不启用这种行为仍然让我有些奇怪。

【讨论】: