如何将地图转换为URL查询字符串?

95

你知道是否有任何实用的类或库,可以将Map转换为URL友好的查询字符串吗?

例如:

我有一个Map:

"param1"=12,
"param2"="cat"

我想要获取:

param1=12&param2=cat

最终输出

relativeUrl+param1=12&param2=cat

好问题。如果没有这样的实用程序,确实有些令人惊讶...我无法快速找到一个。 - Jonik
1
http://java.dzone.com/articles/two-ways-convert-java-map - Bozho
https://dev59.com/SHE85IYBdhLWcg3wXCW1#2810102 - Bozho
20个回答

74

我看到的最强大的现成工具是来自Apache Http Compoments (HttpClient 4.0) 的 URLEncodedUtils 类。

你需要使用的方法是URLEncodedUtils.format()

它不使用映射,因此您可以有重复的参数名称,例如:

  a=1&a=2&b=3

虽然我不建议这种使用参数名称的方式。


3
接近我所需,唯一的问题是需要NameValuePair对象,而我最多只有Map.Entry对象。 - Ula Krukar
3
@Jonik,我会按照您说的做 :) 但我仍然感到惊讶,居然没有一个简单地接受一个Map的方法,这似乎是显而易见的。有很多方法可以将查询字符串转换为Map,但没有任何方法相反地进行转换。 - Ula Krukar
13
@UlaKrukar,之所以使用map<string, string>不是正确的数据结构,是因为它不能保持顺序,也不允许有重复的键。请注意不改变原意并使内容通俗易懂。 - James Moore
3
LinkedHashMap可以很好地运作...来自Groovy的URIBuilder接受Map接口以生成查询字符串。 - Rafael
1
请注意,此函数将空格替换为“+”,而不是“%20”。 - Grief
显示剩余6条评论

50

我使用Java 8和Polygenelubricants的方案找到了一个顺畅的解决方案。

parameters.entrySet().stream()
    .map(p -> urlEncodeUTF8(p.getKey()) + "=" + urlEncodeUTF8(p.getValue()))
    .reduce((p1, p2) -> p1 + "&" + p2)
    .orElse("");

1
这对我非常有效,谢谢。我所要做的就是将?附加到从中返回的字符串,并附加到我的URL上。 - yiwei
1
在 "orElse()" 之前加入一个额外的 "map":.map(s -> "?" + s) - sbzoom
4
数组怎么样? toto[]=4&toto[]=5 - Thomas Decaux
这是非常好的解决方案,对我帮助很大。谢谢。 - Arefe
Java 10引入了URLEncoder.encode(String s, Charset charset),不再抛出UnsupportedEncodingException异常,因此可以简化map调用而无需调用辅助方法。 - Johannes Brodwall

50

这是我草草写的代码,肯定还有改进的地方。

import java.util.*;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

public class MapQuery {
    static String urlEncodeUTF8(String s) {
        try {
            return URLEncoder.encode(s, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new UnsupportedOperationException(e);
        }
    }
    static String urlEncodeUTF8(Map<?,?> map) {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<?,?> entry : map.entrySet()) {
            if (sb.length() > 0) {
                sb.append("&");
            }
            sb.append(String.format("%s=%s",
                urlEncodeUTF8(entry.getKey().toString()),
                urlEncodeUTF8(entry.getValue().toString())
            ));
        }
        return sb.toString();       
    }
    public static void main(String[] args) {
        Map<String,Object> map = new HashMap<String,Object>();
        map.put("p1", 12);
        map.put("p2", "cat");
        map.put("p3", "a & b");         
        System.out.println(urlEncodeUTF8(map));
        // prints "p3=a+%26+b&p2=cat&p1=12"
    }
}

13
自己写并不难,问题是是否有现成的版本可供使用。 - skaffman
1
@skaffman:是的,我写完后才看到这一点,但无论如何,既然我已经写了,我可以利用这个机会让其他人审查我的代码并改进它。即使不是为了OP,它也会对我有益,也许对其他人也有好处。 - polygenelubricants
5
参数映射通常表示为Map<String, List<String>>Map<String, String[]>。同一名称可以有多个值。不确定原始发布者是否聪明地设计了它。 - BalusC
可以考虑使用LinkedHashMap而不是HashMap,因为当API以输入顺序返回值时,使用LinkedHashMap会更好。 - Ryan
Java 10引入了URLEncoder.encode(String s, Charset charset),不需要处理UnsupportedEncodingException - Johannes Brodwall
显示剩余3条评论

24

在Spring Util中,有一种更好的方法...

import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
params.add("key", key);
params.add("storeId", storeId);
params.add("orderId", orderId);
UriComponents uriComponents =     UriComponentsBuilder.fromHttpUrl("http://spsenthil.com/order").queryParams(params).build();
ListenableFuture<ResponseEntity<String>> responseFuture =     restTemplate.getForEntity(uriComponents.toUriString(), String.class);

1
queryParams 是私有的:/ - user3364192
@user3364192 使用 UriComponentsBuilder - MaxZoom
我尝试了上述解决方案,其中包含一些我想在调用UriComponentsBuilder.fromHttpUrl之前省略的空值MultiValueMap。当我添加了params.values().removeIf(entry -> entry.getValue() == null);时,它没有起作用。有人知道为什么我不能使用上述语法从MultiValueMap中删除条目吗? - Orby
1
这并没有回答 OP 的问题,因为问题是如何将 Map 转换为 String,而不是一个 UriComponents 对象。但对于那些只想发出 HTTP 请求的人来说,这很有用。 - axiopisty
我建议不要使用UriComponentsBuilder: 它过于复杂,处理URL查询值的编码存在问题(虽然是有意为之),而且与仅使用params.map { "${urlEncode(it.first)}=${urlEncode(it.second)}" }.joinToString("&")相比并没有太多附加价值。 - Ondra Žižka

20

2016年6月更新

看到太多SO问题的回答使用过时或不充分的方法去解决很常见的问题,我觉得有必要添加一个答案 - 使用一个好的库和一些实例用法来处理parseformat操作。

使用 org.apache.httpcomponents.httpclient 库。这个库包含了一个叫做org.apache.http.client.utils.URLEncodedUtils 的工具类。

例如,可以轻松地从Maven中下载此依赖项:

 <dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5</version>
 </dependency>

为了我的目的,我只需要解析(从查询字符串中读取名称-值对)和格式化(从名称-值对中读取查询字符串)查询字符串。但是,也可以使用等效方法处理URI(请参见下面被注释掉的行)。

// 必需导入

import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;

// 代码片段

public static void parseAndFormatExample() throws UnsupportedEncodingException {
        final String queryString = "nonce=12345&redirectCallbackUrl=http://www.bbc.co.uk";
        System.out.println(queryString);
        // => nonce=12345&redirectCallbackUrl=http://www.bbc.co.uk

        final List<NameValuePair> params =
                URLEncodedUtils.parse(queryString, StandardCharsets.UTF_8);
        // List<NameValuePair> params = URLEncodedUtils.parse(new URI(url), "UTF-8");

        for (final NameValuePair param : params) {
            System.out.println(param.getName() + " : " + param.getValue());
            // => nonce : 12345
            // => redirectCallbackUrl : http://www.bbc.co.uk
        }

        final String newQueryStringEncoded =
                URLEncodedUtils.format(params, StandardCharsets.UTF_8);


        // decode when printing to screen
        final String newQueryStringDecoded =
                URLDecoder.decode(newQueryStringEncoded, StandardCharsets.UTF_8.toString());
        System.out.println(newQueryStringDecoded);
        // => nonce=12345&redirectCallbackUrl=http://www.bbc.co.uk
    }

这个库完全符合我的需求,能够替代一些被毁坏的自定义代码。


10

我希望在 @eclipse 的答案上进行补充,使用 Java 8 的映射和归约。

 protected String formatQueryParams(Map<String, String> params) {
      return params.entrySet().stream()
          .map(p -> p.getKey() + "=" + p.getValue())
          .reduce((p1, p2) -> p1 + "&" + p2)
          .map(s -> "?" + s)
          .orElse("");
  }
额外的map操作会将被缩减的字符串作为输入,并且只在该字符串存在时在前面加上一个?

这个代码可以运行,但是顺序混乱得很厉害,在实际应用中不是问题,但在测试时使用assertThat()肯定会很麻烦。 - M_F
2
如果params参数值包含特殊字符,则不进行编码将会产生问题。 - Yu Jiaao
此答案未对片段进行URL编码,并且重复了已经给出的答案。 - BalusC
这个答案比上面那个早了3年。上面那个是重复的。 - sbzoom

10
如果你真的想构建一个完整的URI,可以尝试使用Apache Http Compoments(HttpClient 4)中的URIBuilder
这并没有真正回答问题,但它回答了我在发现这个问题时所拥有的问题。

8

另一个实现它的方法是'单一类'/无依赖关系方式,处理单个/多个:

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

public class UrlQueryString {
  private static final String DEFAULT_ENCODING = "UTF-8";

  public static String buildQueryString(final LinkedHashMap<String, Object> map) {
    try {
      final Iterator<Map.Entry<String, Object>> it = map.entrySet().iterator();
      final StringBuilder sb = new StringBuilder(map.size() * 8);
      while (it.hasNext()) {
        final Map.Entry<String, Object> entry = it.next();
        final String key = entry.getKey();
        if (key != null) {
          sb.append(URLEncoder.encode(key, DEFAULT_ENCODING));
          sb.append('=');
          final Object value = entry.getValue();
          final String valueAsString = value != null ? URLEncoder.encode(value.toString(), DEFAULT_ENCODING) : "";
          sb.append(valueAsString);
          if (it.hasNext()) {
            sb.append('&');
          }
        } else {
          // Do what you want...for example:
          assert false : String.format("Null key in query map: %s", map.entrySet());
        }
      }
      return sb.toString();
    } catch (final UnsupportedEncodingException e) {
      throw new UnsupportedOperationException(e);
    }
  }

  public static String buildQueryStringMulti(final LinkedHashMap<String, List<Object>> map) {
    try {
      final StringBuilder sb = new StringBuilder(map.size() * 8);
      for (final Iterator<Entry<String, List<Object>>> mapIterator = map.entrySet().iterator(); mapIterator.hasNext();) {
        final Entry<String, List<Object>> entry = mapIterator.next();
        final String key = entry.getKey();
        if (key != null) {
          final String keyEncoded = URLEncoder.encode(key, DEFAULT_ENCODING);
          final List<Object> values = entry.getValue();
          sb.append(keyEncoded);
          sb.append('=');
          if (values != null) {
            for (final Iterator<Object> listIt = values.iterator(); listIt.hasNext();) {
              final Object valueObject = listIt.next();
              sb.append(valueObject != null ? URLEncoder.encode(valueObject.toString(), DEFAULT_ENCODING) : "");
              if (listIt.hasNext()) {
                sb.append('&');
                sb.append(keyEncoded);
                sb.append('=');
              }
            }
          }
          if (mapIterator.hasNext()) {
            sb.append('&');
          }
        } else {
          // Do what you want...for example:
          assert false : String.format("Null key in query map: %s", map.entrySet());
        }
      }
      return sb.toString();
    } catch (final UnsupportedEncodingException e) {
      throw new UnsupportedOperationException(e);
    }
  }

  public static void main(final String[] args) {
    // Examples: could be turned into unit tests ...
    {
      final LinkedHashMap<String, Object> queryItems = new LinkedHashMap<String, Object>();
      queryItems.put("brand", "C&A");
      queryItems.put("count", null);
      queryItems.put("misc", 42);
      final String buildQueryString = buildQueryString(queryItems);
      System.out.println(buildQueryString);
    }
    {
      final LinkedHashMap<String, List<Object>> queryItems = new LinkedHashMap<String, List<Object>>();
      queryItems.put("usernames", new ArrayList<Object>(Arrays.asList(new String[] { "bob", "john" })));
      queryItems.put("nullValue", null);
      queryItems.put("misc", new ArrayList<Object>(Arrays.asList(new Integer[] { 1, 2, 3 })));
      final String buildQueryString = buildQueryStringMulti(queryItems);
      System.out.println(buildQueryString);
    }
  }
}

当需要时,您可以使用简单的方法(在大多数情况下更易编写)或多种方法。请注意,通过添加 & 符号可以组合两者... 如果您发现任何问题,请在评论中让我知道。


7
这是我使用Java 8和org.apache.http.client.URLEncodedUtils实现的解决方案。它将映射条目映射到BasicNameValuePair列表中,然后使用Apache的URLEncodedUtils将其转换为查询字符串。
List<BasicNameValuePair> nameValuePairs = params.entrySet().stream()
   .map(entry -> new BasicNameValuePair(entry.getKey(), entry.getValue()))
   .collect(Collectors.toList());

URLEncodedUtils.format(nameValuePairs, Charset.forName("UTF-8"));

完美工作!只需要拉入一个依赖项:compile 'org.apache.httpcomponents:httpclient:4.5.2' - Miguel Reyes
你能否解释一下你的第一个代码块中正在发生什么,它创建了nameValuePairs并将其传递给URLEncodedUtils。 - silversunhunter

5

Java本身没有内置实现此功能的方法。但是,由于Java是一种编程语言,我们可以通过编程来实现它!

map
.entrySet()
.stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"))

这样就给了你param1=12&param2=cat。现在我们需要将URL和这段内容拼接起来。你可能会认为可以直接这样做:URL + "?" + theAbove,但如果URL中已经包含问号,你就必须使用"&"将它们连接起来。一种检查的方法是查看URL中是否已经有问号了。

此外,我不太清楚你的映射中有什么内容。如果它是原始的东西,你可能需要用URLEncoder.encode或类似方法保护对e.getKey()e.getValue()的调用。

另一种方法是采取更广泛的视角。你是想将一个映射的内容附加到一个URL上,还是想从Java进程中使用映射中的内容作为(额外的)HTTP参数发出HTTP(S)请求?在后一种情况下,你可以查看像OkHttp这样的HTTP库,它具有一些很好的API来完成这项工作,然后你可以避免在第一次操作中对URL进行任何干扰。


此答案未对片段进行URL编码,并且重复了已经给出的答案。 - BalusC

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接