- Java HTTP Client
SSE客户端则为jakarta.ws.rs.sse.SseEventSource,或java.net.http.HttpClient响应至超时,若存在结束标记,则可调HttpClient.shutdownNow()避免一直等到超时。
implementation("org.glassfish.jersey.media:jersey-media-sse:4.0.0-M2")
implementation("org.apache.cxf:cxf-rt-rs-sse:4.1.0")
H2C:
Netty需要显式设置HttpClient.create().protocol(HttpProtocol.H2C); 而 java.net.http.HttpClient 则不需要。
var bv=Base64.getEncoder().encodeToString("clientId:clientSecret".getBytes());
var r = HttpClient.newHttpClient().send(HttpRequest.newBuilder(
URI.create("https://example.com/oauth2/token"))
.POST(HttpRequest.BodyPublishers.ofString("grant_type=refresh_token&refresh_token=" + rt))
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Authorization", "Basic " + bv) // or JS - btoa("clientId:clientSecret")
.build(), HttpResponse.BodyHandlers.ofString()).body();
System.out.println(r);
POST multipart/form-data数据:
// implementation("org.apache.httpcomponents.client5:httpclient5:5.4.2")
var me = MultipartEntityBuilder.create().addTextBody("substringLength", "36")
//.addTextBody("dataText", "x", ContentType.TEXT_PLAIN.withCharset(Charset.defaultCharset()))
.build();
var baos = new ByteArrayOutputStream();
me.writeTo(baos);
var r = HttpClient.newHttpClient().send(HttpRequest.newBuilder(
java.net.URI.create("https://localhost/path/url-slug"))
.POST(HttpRequest.BodyPublishers.ofByteArray(baos.toByteArray()))
//ContentType.MULTIPART_FORM_DATA.withParameters(new BasicNameValuePair("boundary", ctb)).toString()
.header("Content-Type", me.getContentType())
.build(), HttpResponse.BodyHandlers.ofString()).body();
System.out.println(r);
响应处理:Java26+ HttpResponse.BodyHandlers.ofFileChannel(FileChannel,long,long)。
- Jetty Servlet Server
注意 - Jetty ResourceHandler 默认使用内存映射,常把 docker 搞崩,暂禁用之 _ResourceHandler.setMinMappedFileSize(0);
ServletContextHandler 缺失 WebAppContext 对 @WebServlet 注解的支持,ContextHandlerCollection 可使两者并存。
动态添加:
var h = new ServletHolder(NewServlet.class); //h.setHeldClass(NewServlet.class); // 或构造函数入参
h.setInitOrder(1); // 为 0 则懒加载 Servlet
context.addServlet(h, "/s").getRegistration().setMultipartConfig(mc); // ServletHolder 无此配置。
Jetty反向代理: org.eclipse.jetty.ee10.proxy.AsyncProxyServlet 或 兼容javax或jakarta的Servlet代理库
解决 ResourceHandler + setDirAllowed(true) 报 “Ambiguous URI empty segment”:
server.getConnectors()[0].getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.LEGACY);
或 new HttpConfiguration().setUriCompliance(UriCompliance.LEGACY);
配置TLS证书;
var scf = new SslContextFactory.Server(); scf.setKeyStorePassword("x"); scf.setKeyStorePath("C:\\keystore.p12");
// 尾参 ConnectionFactory[] 顺序通常必须 ALPN 为首,如果未指定 defaultProtocol 则取紧跟其后的首个 ConnectionFactory,即简写为 new ALPNServerConnectionFactory()。
new ServerConnector(server, scf, new ALPNServerConnectionFactory("h2", // 非全称 HTTP/2 且全部小写。
HttpVersion.HTTP_1_1.asString().toLowerCase()), new HTTP2ServerConnectionFactory(hc), new HttpConnectionFactory(hc));
仅 HTTP/1.1 调试用 - if (isWin) { sc = new ServerConnector(server, new HttpConnectionFactory(hc)); }
监听TLS握手:
var listener = new SslHandshakeListener() {
@Override public void handshakeSucceeded(SslHandshakeListener.Event e) throws SSLException {
System.out.println(e.getSSLEngine().getPeerHost());
}
@Override public void handshakeFailed(SslHandshakeListener.Event e, Throwable failure) {
failure.printStackTrace(System.err); // e.getSSLEngine()...
}
};
sc.addBean(listener);
Jetty最精简HttpServer:
// implementation(platform("org.eclipse.jetty:jetty-bom:12.1.2")) // 未包含 org.eclipse.jetty.ee11 等库。
// implementation("org.eclipse.jetty:jetty-server")
public static void main(String[] args) throws Exception {
var server = new Server(); // Android 14 及早期版本均报 Didn't find class "java.util.ServiceLoader$Provider",可换用 jetty-server:9.x 或 NanoHTTPD;Android 15 则添加了该类;desugar_jdk_libs 补全不了。
var sc = new ServerConnector(server);
sc.setPort(8080); // 入参0则端口由系统分配,取端口号:int port = ((ServerConnector) ss.getConnectors()[0]).getLocalPort();
// 或 new ServerConnector(server, sslContextFactory, new ALPNServerConnectionFactory(), new HTTP2ServerConnectionFactory(new HttpConfiguration()));
server.addConnector(sc);
connector.addEventListener(new NetworkConnector.Listener() {
@Override public void onOpen(NetworkConnector nc) { System.out.println(nc.getLocalPort()); }
}); // [可选] 通过监听器获取端口号。
server.setHandler(new Handler.Abstract() {
@Override
public boolean handle(org.eclipse.jetty.server.Request r, Response rr, Callback cb) {
System.out.println("handle");
cb.succeeded(); // org.eclipse.jetty.io.Content.copy(r, rr, cb);
return true;
}
});
/*
// Servlet - implementation("org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.16")
var context = new ServletContextHandler();
context.addServlet(staticServlet.class, "/");
server.setHandler(context);
var rh = new ResourceHandler();
rh.setBaseResourceAsString(tmpDir);
rh.setDirAllowed(true);
// 目录别斜杠结尾,否则会变双斜杠。
server.setHandler(new ContextHandler(rh, "/.well-known/tmp"));
*/
server.start();
}
public static class staticServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest r,
HttpServletResponse rr) throws IOException {
rr.getWriter().println("x");
}
}
HTTP3 Client:
//implementation("org.eclipse.jetty.http3:jetty-http3-client:12.1.8") // jetty-quic-quiche-client:12.1.8
var client = new HTTP3Client(HTTP3ClientQuicConfiguration.configure(new QuicheClientQuicConfiguration()));
client.getHTTP3Configuration().setStreamIdleTimeout(15000);
client.start(); System.out.println("start");
// curl --http3-only -I https://http3.is -vvv
client.connect(new QuicheTransport(new QuicheClientQuicConfiguration()),
new InetSocketAddress("http3.is", 443), new Session.Client.Listener() {
}, new Promise.Invocable.NonBlocking<>() {
@Override public void succeeded(Session.Client result) { System.out.println("ok"); }
@Override public void failed(Throwable x) { x.printStackTrace(System.err); }
});
- NanoHTTPD Web Server
implementation("org.nanohttpd:nanohttpd-webserver:2.3.1")
// https://github.com/NanoHttpd/nanohttpd/blob/master/webserver/src/main/java/org/nanohttpd/webserver/SimpleWebServer.java
var server = new fi.iki.elonen.SimpleWebServer("localhost", 9458, MainActivity.this.getFilesDir(), true, "*");
try {
server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
} catch (IOException ioe) {
System.err.println(ioe);
//System.exit(-1);
}