Primeiro um aviso prévio: os trechos de código publicados são todos exemplos básicos. Você vai precisar para lidar com triviais IOException
s e RuntimeException
s como NullPointerException
, ArrayIndexOutOfBoundsException
e consortes si mesmo.
Preparando
Primeiro precisamos saber pelo menos o URL e o conjunto de caracteres. Os parâmetros são opcionais e dependem dos requisitos funcionais.
String url = "http://example.com";
String charset = "UTF-8"; // Or in Java 7 and later, use the constant: java.nio.charset.StandardCharsets.UTF_8.name()
String param1 = "value1";
String param2 = "value2";
// ...
String query = String.format("param1=%s¶m2=%s",
URLEncoder.encode(param1, charset),
URLEncoder.encode(param2, charset));
Os parâmetros da consulta devem estar no name=value
formato e ser concatenados por &
. Normalmente, você também codifica por URL os parâmetros de consulta com o conjunto de caracteres especificado usando URLEncoder#encode()
.
O String#format()
é apenas por conveniência. Eu prefiro quando preciso do operador de concatenação String +
mais de duas vezes.
Disparando uma solicitação HTTP GET com (opcionalmente) parâmetros de consulta
É uma tarefa trivial. É o método de solicitação padrão.
URLConnection connection = new URL(url + "?" + query).openConnection();
connection.setRequestProperty("Accept-Charset", charset);
InputStream response = connection.getInputStream();
// ...
Qualquer string de consulta deve ser concatenada para a URL usando ?
. O Accept-Charset
cabeçalho pode sugerir ao servidor em que codificação estão os parâmetros. Se você não enviar nenhuma string de consulta, poderá deixar o Accept-Charset
cabeçalho ausente. Se você não precisar definir nenhum cabeçalho, poderá usar o URL#openStream()
método de atalho.
InputStream response = new URL(url).openStream();
// ...
De qualquer maneira, se o outro lado for a HttpServlet
, seu doGet()
método será chamado e os parâmetros estarão disponíveis por HttpServletRequest#getParameter()
.
Para fins de teste, você pode imprimir o corpo da resposta no stdout como abaixo:
try (Scanner scanner = new Scanner(response)) {
String responseBody = scanner.useDelimiter("\\A").next();
System.out.println(responseBody);
}
Disparando uma solicitação HTTP POST com parâmetros de consulta
Definir URLConnection#setDoOutput()
como true
define implicitamente o método de solicitação como POST. O HTTP POST padrão, como fazem os formulários da Web, é do tipo application/x-www-form-urlencoded
em que a cadeia de consulta é gravada no corpo da solicitação.
URLConnection connection = new URL(url).openConnection();
connection.setDoOutput(true); // Triggers POST.
connection.setRequestProperty("Accept-Charset", charset);
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=" + charset);
try (OutputStream output = connection.getOutputStream()) {
output.write(query.getBytes(charset));
}
InputStream response = connection.getInputStream();
// ...
Nota: sempre que você desejar enviar um formulário HTML programaticamente, não esqueça de levar os name=value
pares de qualquer <input type="hidden">
elemento para a string de consulta e, claro, também o name=value
par do <input type="submit">
elemento que você deseja "pressionar" programaticamente (porque isso geralmente é usado no lado do servidor para distinguir se um botão foi pressionado e, em caso afirmativo, qual).
Você também pode lançar o obtido URLConnection
para HttpURLConnection
e usar a sua HttpURLConnection#setRequestMethod()
vez. Mas se você está tentando usar a conexão para a saída você ainda precisa definir URLConnection#setDoOutput()
a true
.
HttpURLConnection httpConnection = (HttpURLConnection) new URL(url).openConnection();
httpConnection.setRequestMethod("POST");
// ...
De qualquer maneira, se o outro lado for a HttpServlet
, seu doPost()
método será chamado e os parâmetros estarão disponíveis por HttpServletRequest#getParameter()
.
Realmente disparando a solicitação HTTP
Você pode disparar a solicitação HTTP explicitamente com URLConnection#connect()
, mas a solicitação será disparada automaticamente sob demanda quando você desejar obter informações sobre a resposta HTTP, como o corpo de resposta usando URLConnection#getInputStream()
etc. Os exemplos acima fazem exatamente isso; portanto, a connect()
chamada é de fato supérflua.
Reunindo informações de resposta HTTP
Status da resposta HTTP :
Você precisa de um HttpURLConnection
aqui. Elenco primeiro, se necessário.
int status = httpConnection.getResponseCode();
Cabeçalhos de resposta HTTP :
for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
System.out.println(header.getKey() + "=" + header.getValue());
}
Codificação de resposta HTTP :
Quando o Content-Type
contém um charset
parâmetro, o corpo da resposta provavelmente é baseado em texto e, em seguida, gostaríamos de processar o corpo da resposta com a codificação de caracteres especificada no servidor.
String contentType = connection.getHeaderField("Content-Type");
String charset = null;
for (String param : contentType.replace(" ", "").split(";")) {
if (param.startsWith("charset=")) {
charset = param.split("=", 2)[1];
break;
}
}
if (charset != null) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(response, charset))) {
for (String line; (line = reader.readLine()) != null;) {
// ... System.out.println(line) ?
}
}
} else {
// It's likely binary content, use InputStream/OutputStream.
}
Manutenção da sessão
A sessão do servidor geralmente é apoiada por um cookie. Alguns formulários da Web exigem que você esteja logado e / ou seja rastreado por uma sessão. Você pode usar a CookieHandler
API para manter os cookies. Você precisa preparar um CookieManager
com um CookiePolicy
de ACCEPT_ALL
antes de enviar todos os pedidos HTTP.
// First set the default cookie manager.
CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL));
// All the following subsequent URLConnections will use the same cookie manager.
URLConnection connection = new URL(url).openConnection();
// ...
connection = new URL(url).openConnection();
// ...
connection = new URL(url).openConnection();
// ...
Observe que isso nem sempre funciona corretamente em todas as circunstâncias. Se isso falhar, o melhor é reunir e definir manualmente os cabeçalhos dos cookies. Basicamente, você precisa pegar todos os Set-Cookie
cabeçalhos da resposta do login ou da primeira GET
solicitação e depois passar isso pelas solicitações subsequentes.
// Gather all cookies on the first request.
URLConnection connection = new URL(url).openConnection();
List<String> cookies = connection.getHeaderFields().get("Set-Cookie");
// ...
// Then use the same cookies on all subsequent requests.
connection = new URL(url).openConnection();
for (String cookie : cookies) {
connection.addRequestProperty("Cookie", cookie.split(";", 2)[0]);
}
// ...
O split(";", 2)[0]
está lá para se livrar de atributos de cookie que são irrelevantes para o lado do servidor, como expires
, path
, etc. Alternativamente, você também pode usar cookie.substring(0, cookie.indexOf(';'))
em vez de split()
.
Modo de transmissão
Por HttpURLConnection
padrão, o buffer será armazenado em todo o corpo da solicitação antes de enviá-lo, independentemente de você ter definido um tamanho fixo de conteúdo connection.setRequestProperty("Content-Length", contentLength);
. Isso pode causar OutOfMemoryException
s sempre que você envia simultaneamente grandes solicitações POST (por exemplo, upload de arquivos). Para evitar isso, você gostaria de definir o HttpURLConnection#setFixedLengthStreamingMode()
.
httpConnection.setFixedLengthStreamingMode(contentLength);
Mas se o tamanho do conteúdo não for realmente conhecido de antemão, você poderá usar o modo de streaming em pedaços, configurando-o de HttpURLConnection#setChunkedStreamingMode()
acordo. Isso definirá o Transfer-Encoding
cabeçalho HTTP para o chunked
qual forçará o envio do corpo da solicitação em pedaços. O exemplo abaixo enviará o corpo em pedaços de 1 KB.
httpConnection.setChunkedStreamingMode(1024);
Agente de usuário
Pode acontecer que uma solicitação retorne uma resposta inesperada, enquanto funciona bem com um navegador da Web real . O lado do servidor provavelmente está bloqueando solicitações com base no User-Agent
cabeçalho da solicitação. Por URLConnection
padrão, o irá configurá-lo para Java/1.6.0_19
onde a última parte é obviamente a versão do JRE. Você pode substituir isso da seguinte maneira:
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"); // Do as if you're using Chrome 41 on Windows 7.
Use a sequência User-Agent de um navegador recente .
Manipulação de erros
Se o código de resposta HTTP for 4nn
(Erro do cliente) ou 5nn
(Erro do servidor), convém ler o HttpURLConnection#getErrorStream()
para ver se o servidor enviou alguma informação útil sobre o erro.
InputStream error = ((HttpURLConnection) connection).getErrorStream();
Se o código de resposta HTTP for -1, algo deu errado com a manipulação de conexões e respostas. A HttpURLConnection
implementação está em JREs mais antigos, de certa forma com problemas para manter as conexões ativas. Você pode desativá-lo configurando a http.keepAlive
propriedade do sistema para false
. Você pode fazer isso programaticamente no início de seu aplicativo:
System.setProperty("http.keepAlive", "false");
Upload de arquivos
Você usaria normalmente a multipart/form-data
codificação para conteúdo POST misto (dados binários e de caracteres). A codificação é descrita em mais detalhes na RFC2388 .
String param = "value";
File textFile = new File("/path/to/file.txt");
File binaryFile = new File("/path/to/file.bin");
String boundary = Long.toHexString(System.currentTimeMillis()); // Just generate some unique random value.
String CRLF = "\r\n"; // Line separator required by multipart/form-data.
URLConnection connection = new URL(url).openConnection();
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
try (
OutputStream output = connection.getOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(output, charset), true);
) {
// Send normal param.
writer.append("--" + boundary).append(CRLF);
writer.append("Content-Disposition: form-data; name=\"param\"").append(CRLF);
writer.append("Content-Type: text/plain; charset=" + charset).append(CRLF);
writer.append(CRLF).append(param).append(CRLF).flush();
// Send text file.
writer.append("--" + boundary).append(CRLF);
writer.append("Content-Disposition: form-data; name=\"textFile\"; filename=\"" + textFile.getName() + "\"").append(CRLF);
writer.append("Content-Type: text/plain; charset=" + charset).append(CRLF); // Text file itself must be saved in this charset!
writer.append(CRLF).flush();
Files.copy(textFile.toPath(), output);
output.flush(); // Important before continuing with writer!
writer.append(CRLF).flush(); // CRLF is important! It indicates end of boundary.
// Send binary file.
writer.append("--" + boundary).append(CRLF);
writer.append("Content-Disposition: form-data; name=\"binaryFile\"; filename=\"" + binaryFile.getName() + "\"").append(CRLF);
writer.append("Content-Type: " + URLConnection.guessContentTypeFromName(binaryFile.getName())).append(CRLF);
writer.append("Content-Transfer-Encoding: binary").append(CRLF);
writer.append(CRLF).flush();
Files.copy(binaryFile.toPath(), output);
output.flush(); // Important before continuing with writer!
writer.append(CRLF).flush(); // CRLF is important! It indicates end of boundary.
// End of multipart/form-data.
writer.append("--" + boundary + "--").append(CRLF).flush();
}
Se o outro lado for a HttpServlet
, seu doPost()
método será chamado e as partes estarão disponíveis por HttpServletRequest#getPart()
(observe, portanto não getParameter()
e assim por diante!). O getPart()
método, no entanto, é relativamente novo, foi introduzido no Servlet 3.0 (Glassfish 3, Tomcat 7, etc.). Antes do Servlet 3.0, sua melhor opção era usar o Apache Commons FileUpload para analisar uma multipart/form-data
solicitação. Consulte também esta resposta para obter exemplos das abordagens FileUpload e Servelt 3.0.
Lidando com sites HTTPS não confiáveis ou mal configurados
Às vezes, você precisa conectar um URL HTTPS, talvez porque esteja escrevendo um raspador da Web. Nesse caso, você pode provavelmente enfrentar uma javax.net.ssl.SSLException: Not trusted server certificate
em alguns sites HTTPS que não mantêm seus certificados SSL até à data, ou um java.security.cert.CertificateException: No subject alternative DNS name matching [hostname] found
ou javax.net.ssl.SSLProtocolException: handshake alert: unrecognized_name
em alguns sites mal configurado HTTPS.
O static
inicializador de execução única a seguir na sua classe de raspador da web deve ser HttpsURLConnection
mais tolerante com esses sites HTTPS e, portanto, não lançar mais essas exceções.
static {
TrustManager[] trustAllCertificates = new TrustManager[] {
new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null; // Not relevant.
}
@Override
public void checkClientTrusted(X509Certificate[] certs, String authType) {
// Do nothing. Just allow them all.
}
@Override
public void checkServerTrusted(X509Certificate[] certs, String authType) {
// Do nothing. Just allow them all.
}
}
};
HostnameVerifier trustAllHostnames = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true; // Just allow them all.
}
};
try {
System.setProperty("jsse.enableSNIExtension", "false");
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCertificates, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier(trustAllHostnames);
}
catch (GeneralSecurityException e) {
throw new ExceptionInInitializerError(e);
}
}
Últimas palavras
O Apache HttpComponents HttpClient é muito mais conveniente em tudo isso :)
Analisando e Extraindo HTML
Se tudo o que você deseja é analisar e extrair dados do HTML, use melhor um analisador de HTML como o Jsoup