Java
Cliente para Java 11+ usando java.net.http.HttpClient da stdlib. Sem dependências externas, sem framework. Compatível com Java 11 até versões recentes.
Pré-requisitos
- JDK 11 ou superior.
- Credenciais em variáveis de ambiente:
export BANQER_TENANT=sua-empresa
export BANQER_CLIENT_ID=...
export BANQER_CLIENT_SECRET=...
Cliente completo
// BanqerClient.java
package banqer;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class BanqerClient {
private static final String TENANT = System.getenv("BANQER_TENANT");
private static final String CLIENT_ID = System.getenv("BANQER_CLIENT_ID");
private static final String CLIENT_SECRET = System.getenv("BANQER_CLIENT_SECRET");
private static final String API_BASE = "https://api-" + TENANT + ".banqer.com.br";
private static final String AUTH_URL =
"https://auth.banqer.com.br/realms/" + TENANT + "/protocol/openid-connect/token";
private static final HttpClient HTTP = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(10))
.build();
// Cache simples de token; em ambiente multi-thread, considere ReadWriteLock.
private static final AtomicReference<TokenCache> CACHE = new AtomicReference<>(TokenCache.EMPTY);
private record TokenCache(String accessToken, long expiresAtEpochMs) {
static final TokenCache EMPTY = new TokenCache(null, 0L);
boolean valid() {
return accessToken != null && System.currentTimeMillis() < expiresAtEpochMs;
}
}
public static class BanqerException extends RuntimeException {
public final int status;
public final String reason;
public BanqerException(int status, String reason, String message) {
super("[" + status + " " + reason + "] " + message);
this.status = status;
this.reason = reason;
}
}
private static String formEncode(Map<String, String> params) {
StringBuilder sb = new StringBuilder();
for (var e : params.entrySet()) {
if (sb.length() > 0) sb.append('&');
sb.append(URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8))
.append('=')
.append(URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8));
}
return sb.toString();
}
private static synchronized String fetchToken() {
var current = CACHE.get();
if (current.valid()) return current.accessToken;
var body = formEncode(Map.of(
"grant_type", "client_credentials",
"client_id", CLIENT_ID,
"client_secret", CLIENT_SECRET
));
var req = HttpRequest.newBuilder(URI.create(AUTH_URL))
.header("Content-Type", "application/x-www-form-urlencoded")
.timeout(Duration.ofSeconds(10))
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
try {
var res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() != 200) {
throw new BanqerException(res.statusCode(), "TOKEN_REQUEST_FAILED", res.body());
}
// Parse minimalista (evita dependencia de Jackson/Gson).
String json = res.body();
String token = extractJsonString(json, "access_token");
int expiresIn = Integer.parseInt(extractJsonNumber(json, "expires_in"));
long expAt = System.currentTimeMillis() + (long) (expiresIn * 800L); // 80% do TTL
CACHE.set(new TokenCache(token, expAt));
return token;
} catch (Exception e) {
if (e instanceof BanqerException be) throw be;
throw new BanqerException(0, "NETWORK_ERROR", e.getMessage());
}
}
private static String getToken() {
var c = CACHE.get();
return c.valid() ? c.accessToken : fetchToken();
}
private static final Pattern STR_PAT_TPL = Pattern.compile("\"%s\"\\s*:\\s*\"([^\"]*)\"");
private static final Pattern NUM_PAT_TPL = Pattern.compile("\"%s\"\\s*:\\s*([0-9]+)");
private static String extractJsonString(String json, String key) {
Matcher m = Pattern.compile("\"" + key + "\"\\s*:\\s*\"([^\"]*)\"").matcher(json);
if (!m.find()) throw new BanqerException(0, "PARSE_ERROR", "missing " + key);
return m.group(1);
}
private static String extractJsonNumber(String json, String key) {
Matcher m = Pattern.compile("\"" + key + "\"\\s*:\\s*([0-9]+)").matcher(json);
if (!m.find()) throw new BanqerException(0, "PARSE_ERROR", "missing " + key);
return m.group(1);
}
/**
* Faz uma chamada autenticada e devolve o corpo JSON cru.
* Trate o JSON com a sua biblioteca preferida (Jackson, Gson) na camada de cima.
*/
public static String call(String method, String path, Map<String, String> queryParams) {
var url = new StringBuilder(API_BASE).append(path);
if (queryParams != null && !queryParams.isEmpty()) {
url.append('?').append(formEncode(queryParams));
}
int attempt = 0;
boolean refreshed = false;
while (true) {
attempt++;
String token = getToken();
var req = HttpRequest.newBuilder(URI.create(url.toString()))
.header("Authorization", "Bearer " + token)
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(30))
.method(method, HttpRequest.BodyPublishers.noBody())
.build();
try {
var res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
int status = res.statusCode();
if (status == 401 && !refreshed) {
CACHE.set(TokenCache.EMPTY);
refreshed = true;
continue;
}
if (status == 429 && attempt < 3) {
long wait = parseRetryAfterMs(res.headers().firstValue("retry-after").orElse("5"));
Thread.sleep(wait);
continue;
}
if (status >= 500 && status < 600 && attempt < 3) {
long wait = Math.min((long) (Math.pow(2, attempt) * 1000 + Math.random() * 1000), 30_000);
Thread.sleep(wait);
continue;
}
if (status >= 200 && status < 300) return res.body();
// Erros 4xx fora do 401: bug no cliente. Nao retry.
String body = res.body();
String reason = body.contains("\"reason\"") ? extractJsonString(body, "reason") : "UNKNOWN";
String message = body.contains("\"message\"") ? extractJsonString(body, "message") : body;
throw new BanqerException(status, reason, message);
} catch (BanqerException be) {
throw be;
} catch (Exception e) {
throw new BanqerException(0, "NETWORK_ERROR", e.getMessage());
}
}
}
private static long parseRetryAfterMs(String value) {
try { return Long.parseLong(value) * 1000L; } catch (NumberFormatException e) { return 5000L; }
}
// Demo
public static void main(String[] args) {
String json = call("GET", "/v1/credores", null);
System.out.println(json);
}
}
Uso com Jackson para deserialização
Em produção, use Jackson para mapear o JSON em records:
<!-- pom.xml -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.2</version>
</dependency>
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.DeserializationFeature;
private static final ObjectMapper MAPPER = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
public record Credor(int idCredor, String nome, String nomeSms) {}
public record CredoresResponse(java.util.List<Credor> credores) {}
CredoresResponse r = MAPPER.readValue(
BanqerClient.call("GET", "/v1/credores", null),
CredoresResponse.class
);
FAIL_ON_UNKNOWN_PROPERTIES = false é essencial: a V1 pode adicionar campos novos sem aviso prévio.
Spring Boot
Para aplicações Spring, considere spring-security-oauth2-client, que oferece OAuth2AuthorizedClientManager com cache e renovação integrados. O fluxo é configuração via application.yml, sem cliente HTTP manual.
Validação de JWT no Java
Se sua arquitetura precisar validar o token localmente (raro: o servidor já valida), use Nimbus JOSE+JWT da Connect2id. Carregue o JWKS de https://auth.banqer.com.br/realms/SUA-EMPRESA/protocol/openid-connect/certs e use JWSVerifier.
Erros comuns
| Sintoma | Causa provável |
|---|---|
401 invalid_client no token endpoint | Credenciais erradas, ou tenant errado na URL |
SSLHandshakeException | Cadeia de certificados sem CA do Java. Atualize o JDK. |
Threads bloqueando no fetchToken | Sob alta concorrência o synchronized serializa. Considere Semaphore com single-flight. |