【问题标题】:How to force URIBuilder.path(...) to encode parameters like "%AD"? This method doesn't always encode parameters with percentage, correctly如何强制 URIBuilder.path(...) 对“%AD”等参数进行编码?此方法并不总是正确地用百分比编码参数
【发布时间】:2014-02-21 07:21:16
【问题描述】:

如何强制URIBuilder.path(...)"%AD"等参数进行编码?

URIBuilder 的方法 pathreplacePathsegment 并不总是正确地用百分比编码参数。

当参数包含字符“%”后跟两个字符共同构成 URL 编码字符时,“%”不编码为“%25”。

例如

URI uri = UriBuilder.fromUri("https://dummy.com").queryParam("param", "%AD");
String test = uri.build().toString();

“测试”是“https://dummy.com?param=%AD
但应该是“https://dummy.com?param=%25AD”(字符“%”编码为“%25”)

当“%”后面的两个字符是十六进制时,方法UriBuilderImpl.queryParam(...) 的行为如下所示。即,“com.sun.jersey.api.uri.UriComponent.isHexCharacter(char)”方法对“%”后面的字符返回 true。

我认为 UriBuilderImpl 的行为是正确的,因为我猜它试图不对已经编码的参数进行编码。但在我的场景中,我永远不会尝试使用已经编码的参数创建 URL。

我该怎么办?

我的 Web 应用程序使用 Jersey,并且在许多地方我使用 UriBuilder 类构建 URI 或调用 UriInfo 对象的方法 getBaseUriBuilder

每次调用queryParamreplaceQueryParamsegment 方法时,我都可以将“%”替换为“%25”。但我正在寻找一个不那么繁琐的解决方案。

如何让 Jersey 返回我自己的 UriBuilder 实现?

我想创建一个扩展 UriBuilderImpl 的类,它覆盖这些方法并在调用 super.queryParam(...) 或其他任何东西之前执行此替换。

在调用UriBuilder.fromURL(...)、UriInfo.getBaseUriBuilder(...) 等时,有什么方法可以让 Jersey 返回我自己的 UriBuilder 而不是 UriBuilderImpl?

RuntimeDelegate的方法,想到了扩展RuntimeDelegateImpl。我的实现将覆盖方法createUriBuilder(...),这将返回我自己的UriBuilder,而不是UriBuilderImpl。 然后,我将添加文件META-INF/services/javax.ws.rs.ext.RuntimeDelegate,并在其中添加我的RuntimeDelegateImpl 的完整类名。

问题是 jersey-bundle.jar 已经包含一个指向com.sun.jersey.server.impl.provider.RuntimeDelegateImplMETA-INF/services/javax.ws.rs.ext.RuntimeDelegate,因此容器加载该文件而不是我的javax.ws.rs.ext.RuntimeDelegate。因此,它不会加载我的RuntimeDelegateimplementation。

是否可以提供我自己的RuntimeDelegate 实现?

我应该采取不同的方法吗?

【问题讨论】:

    标签: jersey jax-rs jersey-1.0


    【解决方案1】:

    UriBuilder

    这可以在来自 Jersey 的 UriComponent 或直接来自 Java 的 URLEncoder 的帮助下实现:

    UriBuilder.fromUri("https://dummy.com")
            .queryParam("param",
                    UriComponent.encode("%AD",
                        UriComponent.Type.QUERY_PARAM_SPACE_ENCODED))
            .build();
    

    结果:

    https://dummy.com/?param=%25AD
    

    或者:

    UriBuilder.fromUri("https://dummy.com")
            .queryParam("param", URLEncoder.encode("%AD", "UTF-8"))
            .build()
    

    将导致:

    https://dummy.com/?param=%25AD
    

    对于更复杂的示例(即在查询参数中编码 JSON),这种方法也是可能的。假设您有一个像 {"Entity":{"foo":"foo","bar":"bar"}} 这样的 JSON。使用UriComponent 编码时,查询参数的结果如下所示:

    https://dummy.com/?param=%7B%22Entity%22:%7B%22foo%22:%22foo%22,%22bar%22:%22bar%22%7D%7D
    

    这样的 JSON 甚至可以通过 @QueryParam 注入到资源字段/方法参数中(参见 JSON in Query Params or How to Inject Custom Java Types via JAX-RS Parameter Annotations)。


    您使用哪个泽西岛版本?在您提到 Jersey 2 的标签中,但在 RuntimeDelegate 部分中您使用的是 Jersey 1 的东西。

    【讨论】:

    • 对不起。我使用 Jersey 1。我编辑了帖子以删除标签“jersey 2x”并添加了“Jersey 1x”。
    • UriBuilder.fromUri("https://dummy.com").queryParam("param", URLEncoder.encode("%AD", "UTF-8")).build() 是一个解决方案。但是它并不理想,因为它意味着将所有调用修改为 UriBuilder 以添加 URLEncoder.encode(...) 事物。无论如何,我都会给你赏金,因为我只有两个解决方案,而你的解决方案比另一个更好。 (在 Jersey 1x 中,我必须使用 UriComponent。我猜 URLEncoder 属于 Jersy 2x)
    • URLEncoder 是 JDK 中可用的 Java 类。 UriComponent 来自泽西岛 (1 & 2)。我知道这并不像您期望的那样方便,但在某些情况下我们无法自动进行编码。
    • 如果您事先不知道查询参数值(通常是这种情况),在将其传递给 UriBuilder 之前对其进行盲目编码可能会导致问题。例如,如果您的值是“hello world”,则对其进行编码会将其转换为“hello+world”,然后 UriBuilder 会将其转换为“hello%2Bworld”(双重编码)。问题是 UriBuilder 也进行编码......理想情况下,当调用者按照建议显式完成编码时,UriBuilder 不应该做任何事情。
    【解决方案2】:

    看看下面的例子是否有帮助。下面链接的线程对可用功能及其不同输出进行了广泛的讨论。

    以下内容:

    1. UriBuilder.fromUri("http://localhost:8080").queryParam("name", "{value}").build("%20");
    2. UriBuilder.fromUri("http://localhost:8080").queryParam("name", "{value}").buildFromEncoded("%20");
    3. UriBuilder.fromUri("http://localhost:8080").replaceQuery("name={value}).build("%20");
    4. UriBuilder.fromUri("http://localhost:8080").replaceQuery("name={value}).buildFromEncoded("%20");

    将输出:

    1. http://localhost:8080?name=%2520
    2. http://localhost:8080?name=%20
    3. http://localhost:8080?name=%2520
    4. http://localhost:8080?name=%20

    通过http://comments.gmane.org/gmane.comp.java.jsr311.user/71

    另外,based on the Class UriBuilder documentation,下面的例子展示了如何获得你想要的东西。

    URI 模板在 URI 的大多数组件中都是允许的,但它们的值 仅限于特定组件。例如

    UriBuilder.fromPath("{arg1}").build("foo#bar");
    

    将导致对“#”进行编码,使得生成的 URI 为 “foo%23bar”。要创建 URI "foo#bar",请使用

    UriBuilder.fromPath("{arg1}").fragment("{arg2}").build("foo", "bar")
    

    相反。 URI 模板名称和分隔符从不编码,但它们的 在构建 URI 时对值进行编码。模板参数正则 构建 URI 时忽略表达式,即没有验证 执行。

    【讨论】:

    • 使用模板的问题是我在几个地方而不是一个地方构建 URL。即:我有一个向 URL 添加一些段的方法,然后,另一个添加更多,或者根据条件添加参数。然后,另一种方法......因此,当我调用build()时,我不知道URL有多少个参数,也不知道这些参数的值。
    【解决方案3】:

    可以在启动时手动覆盖球衣中的默认行为,例如使用调用RuntimeDelegate.setInstance(yourRuntimeDelegateImpl) 的静态助手。

    因此,如果您想要一个对百分比进行编码的 UriBuilder,即使它们看起来像是已编码序列的一部分,则如下所示:

    [...]
    import javax.ws.rs.core.UriBuilder;
    import javax.ws.rs.ext.RuntimeDelegate;
    
    import com.sun.jersey.api.uri.UriBuilderImpl;
    import com.sun.ws.rs.ext.RuntimeDelegateImpl;
    // or for jersey2:
    // import org.glassfish.jersey.uri.internal.JerseyUriBuilder;
    // import org.glassfish.jersey.internal.RuntimeDelegateImpl;
    
    public class SomeBaseClass {
    
        [...]
    
        // this is the lengthier custom implementation of UriBuilder
        // replace this with your own according to your needs
        public static class AlwaysPercentEncodingUriBuilder extends UriBuilderImpl {
    
            @Override
            public UriBuilder queryParam(String name, Object... values) {
                Object[] encValues = new Object[values.length];
                for (int i=0; i<values.length; i++) {
                    String value = values[i].toString(); // TODO: better null check here, like in base class
                    encValues[i] = percentEncode(value);
                }
                return super.queryParam(name, encValues);
            }
    
            private String percentEncode(String value) {
                StringBuilder sb = null;
                for (int i=0;  i < value.length(); i++) {
                    char c = value.charAt(i);
                    // if this condition is is true, the base class will not encode the percent
                    if (c == '%' 
                        && i + 2 < value.length()
                        && isHexCharacter(value.charAt(i + 1)) 
                        && isHexCharacter(value.charAt(i + 2))) {
                        if (sb == null) {
                            sb = new StringBuilder(value.substring(0, i));
                        }
                        sb.append("%25");
                    } else {
                        if (sb != null) sb.append(c);
                    }
                }
                return (sb != null) ? sb.toString() : value;
            }
    
            // in jersey2 one can call public UriComponent.isHexCharacter
            // but in jersey1 we need to provide this on our own
            private static boolean isHexCharacter(char c) {
                return ('0' <= c && c <= '9')
                    || ('A' <=c && c <= 'F')
                    || ('a' <=c && c <= 'f');
            }
        }
    
        // here starts the code to hook up the implementation
        public static class AlwaysPercentEncodingRuntimeDelegateImpl extends RuntimeDelegateImpl {
            @Override
            public UriBuilder createUriBuilder() {
                return new AlwaysPercentEncodingUriBuilder();
            }
        }
    
        static {
            RuntimeDelegate myDelegate = new AlwaysPercentEncodingRuntimeDelegateImpl();
            RuntimeDelegate.setInstance(myDelegate);
        }
    
    }
    

    警告:当然,这种方式不是很可配置,如果你在一些可能被其他人重用的库代码中这样做,这可能会引起一些麻烦。

    例如,当我在 Confluence 插件中编写 rest 客户端时,我遇到了与 OP 相同的问题,并以“手动编码每个参数”解决方案结束,因为插件是通过 OSGi 加载的,因此根本无法触摸RuntimeDelegateImpl(而不是在运行时获取java.lang.ClassNotFoundException: com.sun.ws.rs.ext.RuntimeDelegateImpl)。

    (只是为了记录,在 jersey2 中这看起来非常相似;尤其是挂钩自定义 RuntimeDelegateImpl 的代码是相同的。)

    【讨论】:

    • 谢谢克莱门斯!我希望我当时知道RuntimeDelegate.setInstance(yourRuntimeDelegateImpl) 的方法。我目前所做的是创建自己的UriBuilder 实现,并将所有出现的UriBuilder.fromUri(...) 替换为MyUriBuilder...。当然我的解决方案很容易出错,因为如果有新的开发人员进来,她可能会简单地使用UriBuilder 而不是MyUriBuilder
    猜你喜欢
    • 2018-09-09
    • 1970-01-01
    • 2018-01-22
    相关资源
    最近更新 更多