避免java.net.URL的弃用

ttisahbt  于 2023-04-04  发布在  Java
关注(0)|答案(1)|浏览(420)

我正在将我的代码迁移到Java 20。
在这个版本中,java.net.URL #URL(java.lang.String)被弃用了。不幸的是,我有一个类,在那里我发现没有旧的URL构造函数的替代品。

package com.github.bottomlessarchive.loa.url.service.encoder;

import io.mola.galimatias.GalimatiasParseException;
import org.springframework.stereotype.Service;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Optional;

/**
 * This service is responsible for encoding existing {@link URL} instances to valid
 * <a href="https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier">resource identifiers</a>.
 */
@Service
public class UrlEncoder {

    /**
     * Encodes the provided URL to a valid
     * <a href="https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier">resource identifier</a> and return
     * the new identifier as a URL.
     *
     * @param link the url to encode
     * @return the encoded url
     */
    public Optional<URL> encode(final String link) {
        try {
            final URL url = new URL(link);

            // We need to further validate the URL because the java.net.URL's validation is inadequate.
            validateUrl(url);

            return Optional.of(encodeUrl(url));
        } catch (GalimatiasParseException | MalformedURLException | URISyntaxException e) {
            return Optional.empty();
        }
    }

    private void validateUrl(final URL url) throws URISyntaxException {
        // This will trigger an URISyntaxException. It is needed because the constructor of java.net.URL doesn't always validate the
        // passed url correctly.
        new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef());
    }

    private URL encodeUrl(final URL url) throws GalimatiasParseException, MalformedURLException {
        return io.mola.galimatias.URL.parse(url.toString()).toJavaURL();
    }
}

幸运的是,我也为这个类做了测试:

package com.github.bottomlessarchive.loa.url.service.encoder;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;

class UrlEncoderTest {

    private final UrlEncoder underTest = new UrlEncoder();

    @ParameterizedTest
    @CsvSource(
            value = {
                    "http://www.example.com/?test=Hello world,http://www.example.com/?test=Hello%20world",
                    "http://www.example.com/?test=ŐÚőúŰÜűü,http://www.example.com/?test=%C5%90%C3%9A%C5%91%C3%BA%C5%B0%C3%9C%C5%B1%C3%BC",
                    "http://www.example.com/?test=random word £500 bank $,"
                            + "http://www.example.com/?test=random%20word%20%C2%A3500%20bank%20$",
                    "http://www.aquincum.hu/wp-content/uploads/2015/06/Aquincumi-F%C3%BCzetek_14_2008.pdf,"
                            + "http://www.aquincum.hu/wp-content/uploads/2015/06/Aquincumi-F%C3%BCzetek_14_2008.pdf",
                    "http://www.aquincum.hu/wp-content/uploads/2015/06/Aquincumi-F%C3%BCzetek_14 _2008.pdf,"
                            + "http://www.aquincum.hu/wp-content/uploads/2015/06/Aquincumi-F%C3%BCzetek_14%20_2008.pdf"
            }
    )
    void testEncodeWhenUsingValidUrls(final String urlToEncode, final String expected) throws MalformedURLException {
        final Optional<URL> result = underTest.encode(urlToEncode);

        assertThat(result)
                .contains(new URL(expected));
    }

    @ParameterizedTest
    @CsvSource(
            value = {
                    "http://промкаталог.рф/PublicDocuments/05-0211-00.pdf"
            }
    )
    void testEncodeWhenUsingInvalidUrls(final String urlToEncode) {
        final Optional<URL> result = underTest.encode(urlToEncode);

        assertThat(result)
                .isEmpty();
    }
}

它使用的唯一依赖项是galamatias URL库。
有没有人知道如何删除new URL(link)代码片段,同时保持功能不变?
我尝试了各种方法,例如使用java.net.URI#create,但它并没有产生与以前的解决方案完全相同的结果。例如,包含非编码字符(如http://www.example.com/?test=Hello world中的空格)的URL导致IllegalArgumentException。这是由URL类解析的,没有给出错误。(我的数据中包含了很多这样的链接)。另外,像http://промкаталог.рф/PublicDocuments/05-0211-00.pdf这样的URL转换失败的链接可以通过URI.create成功转换为URI。

enxuqcxy

enxuqcxy1#

问题是

主要的问题似乎是UrlEncoder服务处理的是编码、未编码和部分编码的URL的混合。
这会导致歧义,因为某些字符在编码和未编码时可能具有不同的含义。例如,给定一个部分编码的URL,判断一个字符(如'&')是查询参数的一部分(因此应该编码)还是作为分隔符(因此不应该编码)并不容易:

https://www.example.com/test?firstQueryParam=hot%26cold&secondQueryParam=test

雪上加霜的是,由于历史/向后兼容性的原因,Java的URI实现偏离了RFC 3986和RFC 3987。下面是关于URI的一些怪癖的有趣阅读:Updating URI support for RFC 3986 and RFC 3987 in the JDK .
在不了解原始URL的情况下,通过重新编码来“修复”错误编码的URL并不是一个简单的问题。使用充满怪癖的编码器和解码器来修复错误编码的URL甚至更难。一个足够好的“最大努力”启发式将是我个人的建议。

简单的尽力而为解决方案

所以好消息是我已经设法实现了一个通过上述所有测试的解决方案。该解决方案利用了Spring Web UriUtilsUriComponentsBuilder。蛋糕上的樱桃是你可能不再需要galimatias了。
代码如下:

public class UrlEncoder {

    public Optional<URL> encode(final String link) {
        try {
            final URI validatedURI = reencode(link).parseServerAuthority();
            return Optional.of(validatedURI.toURL());
        } catch (MalformedURLException | URISyntaxException e) {
            return Optional.empty();
        }
    }

    private URI reencode(String url) { // best effort
        final String decodedUrl = UriUtils.decode(url, StandardCharsets.UTF_8);
        return UriComponentsBuilder.fromHttpUrl(decodedUrl).build().encode().toUri();
    }
}

以下是它的要点:

  • reencode →通过解码和重新编码来“修复”URL编码的最佳尝试
  • parseServerAuthority() →作为以前validateUrl(url);方法的替代。

对与号和其他特殊字符进行双重编码

如前所述,虽然上面的代码通过了所有测试,但由于模糊性,很容易产生无法通过的URL。例如,通过编码器运行上面的URL将导致:

https://www.example.com/test?firstQueryParam=hot&cold&secondQueryParam=test

这是一个完全有效的URL,但可能不是人们要寻找的。
这是一个危险的领域,但是可以实现一个更“自以为是”的重新编码算法。例如,下面的代码通过确保%26不被解码来处理&字符:

private final char PERCENT_SIGN = '%';
private final String ENCODED_PERCENT_SIGN = "25";
private final String[] CODES_TO_DOUBLE_ENCODE = new String[]{
        "26" // code for '&'
};

private URI reencode(String url) throws URISyntaxException { // best effort
    final String urlWithDoubleEncodedSpecialCharacters = doubleEncodeSpecialCharacters(url);

    final String decodedUrl = UriUtils.decode(urlWithDoubleEncodedSpecialCharacters, StandardCharsets.UTF_8);
    final String encodedUrl = UriComponentsBuilder.fromHttpUrl(decodedUrl).toUriString();
    final String encodedUrlWithSpecialCharacters = decodeSpecialCharacters(encodedUrl);

    return URI.create(encodedUrlWithSpecialCharacters);
}

private String doubleEncodeSpecialCharacters(String url) {
    StringBuilder sb = new StringBuilder(url);
    for (String code : CODES_TO_DOUBLE_ENCODE) {
        String codeString = PERCENT_SIGN + code;
        int index = sb.indexOf(codeString);
        while (index != -1) {
            sb.insert(index + 1, ENCODED_PERCENT_SIGN);
            index = sb.indexOf(codeString, index + 3);
        }
    }
    return sb.toString();
}

private String decodeSpecialCharacters(String url) {
    StringBuilder sb = new StringBuilder(url);
    for (String code : CODES_TO_DOUBLE_ENCODE) {
        String codeString = PERCENT_SIGN + ENCODED_PERCENT_SIGN + code;
        int index = sb.indexOf(codeString);
        while (index != -1) {
            sb.delete(index + 2, index + 4);
            index = sb.indexOf(codeString, index + 5);
        }
    }
    return sb.toString();
}

上面的解决方案可以被修改以处理其他转义序列(例如,处理所有RFC 3986的保留字符),以及使用更复杂的试探法(例如,对查询参数做一些与路径参数不同的事情)。
然而,作为一个曾经经历过这个兔子洞的人,我可以告诉你,一旦你知道你正在处理错误编码的URL,就没有完美的“修复”。

相关问题