add http server engine

This commit is contained in:
Looly
2024-11-18 01:53:18 +08:00
parent 99a2f5f4a2
commit e3a60ad93f
39 changed files with 1925 additions and 910 deletions

View File

@@ -37,6 +37,10 @@
<httpclient5.version>5.4.1</httpclient5.version>
<httpclient4.version>4.5.14</httpclient4.version>
<okhttp.version>4.12.0</okhttp.version>
<!-- 固定 2.2.x支持到JDK8 -->
<undertow.version>2.2.36.Final</undertow.version>
<jetty.version>12.0.15</jetty.version>
<tomcat.version>11.0.1</tomcat.version>
</properties>
<dependencies>
@@ -100,6 +104,26 @@
<scope>provided</scope>
</dependency>
<!-- 第三方HTTP服务器库 -->
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-core</artifactId>
<version>${undertow.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${jetty.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
<scope>provided</scope>
</dependency>
<!-- 仅用于测试 -->
<dependency>
<groupId>org.dromara.hutool</groupId>

View File

@@ -27,7 +27,7 @@ import org.dromara.hutool.http.client.Response;
import org.dromara.hutool.http.client.engine.ClientEngine;
import org.dromara.hutool.http.client.engine.ClientEngineFactory;
import org.dromara.hutool.http.meta.Method;
import org.dromara.hutool.http.server.SimpleServer;
import org.dromara.hutool.http.server.engine.sun.SimpleServer;
import java.nio.charset.Charset;
import java.util.Collection;

View File

@@ -16,17 +16,13 @@
package org.dromara.hutool.http.multipart;
import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.io.IORuntimeException;
import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.core.io.file.FileNameUtil;
import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.text.StrUtil;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.NoSuchFileException;
import java.io.*;
/**
* 上传的文件对象
@@ -80,9 +76,9 @@ public class UploadFile {
*
* @param destPath 目标文件路径
* @return 目标文件
* @throws IOException IO异常
* @throws IORuntimeException IO异常
*/
public File write(final String destPath) throws IOException {
public File write(final String destPath) throws IORuntimeException {
if (data != null || tempFile != null) {
return write(FileUtil.file(destPath));
}
@@ -95,9 +91,9 @@ public class UploadFile {
*
* @param destination 目标文件
* @return 目标文件
* @throws IOException IO异常
* @throws IORuntimeException IO异常
*/
public File write(File destination) throws IOException {
public File write(File destination) throws IORuntimeException {
assertValid();
if (destination.isDirectory()) {
@@ -113,7 +109,7 @@ public class UploadFile {
throw new NullPointerException("Temp file is null !");
}
if(!this.tempFile.exists()){
throw new NoSuchFileException("Temp file: [" + this.tempFile.getAbsolutePath() + "] not exist!");
throw new IORuntimeException("Temp file: [" + this.tempFile.getAbsolutePath() + "] not exist!");
}
FileUtil.move(tempFile, destination, true);
@@ -123,9 +119,9 @@ public class UploadFile {
/**
* @return 获得文件字节流
* @throws IOException IO异常
* @throws IORuntimeException IO异常
*/
public byte[] getFileContent() throws IOException {
public byte[] getFileContent() throws IORuntimeException {
assertValid();
if (data != null) {
@@ -139,9 +135,9 @@ public class UploadFile {
/**
* @return 获得文件流
* @throws IOException IO异常
* @throws IORuntimeException IO异常
*/
public InputStream getFileInputStream() throws IOException {
public InputStream getFileInputStream() throws IORuntimeException {
assertValid();
if (data != null) {
@@ -278,11 +274,11 @@ public class UploadFile {
/**
* 断言是否文件流可用
*
* @throws IOException IO异常
* @throws IORuntimeException IO异常
*/
private void assertValid() throws IOException {
private void assertValid() throws IORuntimeException {
if (!isUploaded()) {
throw new IOException(StrUtil.format("File [{}] upload fail", getFileName()));
throw new IORuntimeException(StrUtil.format("File [{}] upload fail", getFileName()));
}
}
// ---------------------------------------------------------------------------- Private method end

View File

@@ -1,446 +0,0 @@
/*
* Copyright (c) 2013-2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.hutool.http.server;
import org.dromara.hutool.core.io.IORuntimeException;
import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.net.url.UrlEncoder;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.core.util.ByteUtil;
import org.dromara.hutool.core.util.ObjUtil;
import org.dromara.hutool.http.meta.ContentType;
import org.dromara.hutool.http.meta.HeaderName;
import org.dromara.hutool.http.meta.HttpStatus;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import java.io.*;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
/**
* Http响应对象用于写出数据到客户端
*/
@SuppressWarnings("resource")
public class HttpServerResponse extends HttpServerBase {
private Charset charset;
/**
* 是否已经发送了Http状态码如果没有提前写出状态码
*/
private boolean isSendCode;
/**
* 构造
*
* @param httpExchange {@link HttpExchange}
*/
public HttpServerResponse(final HttpExchange httpExchange) {
super(httpExchange);
}
/**
* 发送HTTP状态码Content-Length为0不定长度会输出Transfer-encoding: chunked
*
* @param httpStatusCode HTTP状态码见HttpStatus
* @return this
*/
public HttpServerResponse send(final int httpStatusCode) {
return send(httpStatusCode, 0);
}
/**
* 发送成功状态码
*
* @return this
*/
public HttpServerResponse sendOk() {
return send(HttpStatus.HTTP_OK);
}
/**
* 发送成功状态码
*
* @param bodyLength 响应体长度默认0表示不定长度会输出Transfer-encoding: chunked
* @return this
* @since 5.5.7
*/
public HttpServerResponse sendOk(final int bodyLength) {
return send(HttpStatus.HTTP_OK, bodyLength);
}
/**
* 发送404错误页
*
* @param content 错误页页面内容默认text/html类型
* @return this
*/
public HttpServerResponse send404(final String content) {
return sendError(HttpStatus.HTTP_NOT_FOUND, content);
}
/**
* 发送错误页
*
* @param errorCode HTTP错误状态码见HttpStatus
* @param content 错误页页面内容默认text/html类型
* @return this
*/
public HttpServerResponse sendError(final int errorCode, final String content) {
send(errorCode);
setContentType(ContentType.TEXT_HTML.toString());
return write(content);
}
/**
* 发送HTTP状态码
*
* @param httpStatusCode HTTP状态码见HttpStatus
* @param bodyLength 响应体长度默认0表示不定长度会输出Transfer-encoding: chunked
* @return this
*/
public HttpServerResponse send(final int httpStatusCode, final long bodyLength) {
if (this.isSendCode) {
throw new IORuntimeException("Http status code has been send!");
}
try {
this.httpExchange.sendResponseHeaders(httpStatusCode, bodyLength);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
this.isSendCode = true;
return this;
}
/**
* 获得所有响应头,获取后可以添加新的响应头
*
* @return 响应头
*/
public Headers getHeaders() {
return this.httpExchange.getResponseHeaders();
}
/**
* 添加响应头,如果已经存在,则追加
*
* @param header 头key
* @param value 值
* @return this
*/
public HttpServerResponse addHeader(final String header, final String value) {
getHeaders().add(header, value);
return this;
}
/**
* 设置响应头,如果已经存在,则覆盖
*
* @param headerName 头key
* @param value 值
* @return this
*/
public HttpServerResponse setHeader(final HeaderName headerName, final String value) {
return setHeader(headerName.getValue(), value);
}
/**
* 设置响应头,如果已经存在,则覆盖
*
* @param header 头key
* @param value 值
* @return this
*/
public HttpServerResponse setHeader(final String header, final String value) {
getHeaders().set(header, value);
return this;
}
/**
* 设置响应头,如果已经存在,则覆盖
*
* @param header 头key
* @param value 值列表
* @return this
*/
public HttpServerResponse setHeader(final String header, final List<String> value) {
getHeaders().put(header, value);
return this;
}
/**
* 设置所有响应头,如果已经存在,则覆盖
*
* @param headers 响应头map
* @return this
*/
public HttpServerResponse setHeaders(final Map<String, List<String>> headers) {
getHeaders().putAll(headers);
return this;
}
/**
* 设置Content-Type头类似于:text/html;charset=utf-8<br>
* 如果用户传入的信息无charset信息自动根据charset补充charset设置见{@link #setCharset(Charset)}
*
* @param contentType Content-Type头内容
* @return this
*/
public HttpServerResponse setContentType(String contentType) {
if (null != contentType && null != this.charset) {
if (!contentType.contains(";charset=")) {
contentType = ContentType.build(contentType, this.charset);
}
}
return setHeader(HeaderName.CONTENT_TYPE, contentType);
}
/**
* 设置Content-Length头
*
* @param contentLength Content-Length头内容
* @return this
*/
public HttpServerResponse setContentLength(final long contentLength) {
return setHeader(HeaderName.CONTENT_LENGTH, String.valueOf(contentLength));
}
/**
* 设置响应的编码
*
* @param charset 编码
* @return this
*/
public HttpServerResponse setCharset(final Charset charset) {
this.charset = charset;
return this;
}
/**
* 设置属性
*
* @param name 属性名
* @param value 属性值
* @return this
*/
public HttpServerResponse setAttr(final String name, final Object value) {
this.httpExchange.setAttribute(name, value);
return this;
}
/**
* 获取响应数据流
*
* @return 响应数据流
*/
public OutputStream getOut() {
if (!this.isSendCode) {
sendOk();
}
return this.httpExchange.getResponseBody();
}
/**
* 获取响应数据流
*
* @return 响应数据流
*/
public PrintWriter getWriter() {
final Charset charset = ObjUtil.defaultIfNull(this.charset, DEFAULT_CHARSET);
return new PrintWriter(new OutputStreamWriter(getOut(), charset));
}
/**
* 写出数据到客户端
*
* @param data 数据
* @param contentType Content-Type类型
* @return this
*/
public HttpServerResponse write(final String data, final String contentType) {
setContentType(contentType);
return write(data);
}
/**
* 写出数据到客户端
*
* @param data 数据
* @return this
*/
public HttpServerResponse write(final String data) {
final Charset charset = ObjUtil.defaultIfNull(this.charset, DEFAULT_CHARSET);
return write(ByteUtil.toBytes(data, charset));
}
/**
* 写出数据到客户端
*
* @param data 数据
* @param contentType 返回的类型
* @return this
*/
public HttpServerResponse write(final byte[] data, final String contentType) {
setContentType(contentType);
return write(data);
}
/**
* 写出数据到客户端
*
* @param data 数据
* @return this
*/
public HttpServerResponse write(final byte[] data) {
final ByteArrayInputStream in = new ByteArrayInputStream(data);
return write(in, in.available());
}
/**
* 返回数据给客户端
*
* @param in 需要返回客户端的内容
* @param contentType 返回的类型
* @return this
* @since 5.2.6
*/
public HttpServerResponse write(final InputStream in, final String contentType) {
return write(in, 0, contentType);
}
/**
* 返回数据给客户端
*
* @param in 需要返回客户端的内容
* @param length 内容长度默认0表示不定长度会输出Transfer-encoding: chunked
* @param contentType 返回的类型
* @return this
* @since 5.2.7
*/
public HttpServerResponse write(final InputStream in, final int length, final String contentType) {
setContentType(contentType);
return write(in, length);
}
/**
* 写出数据到客户端
*
* @param in 数据流
* @return this
*/
public HttpServerResponse write(final InputStream in) {
return write(in, 0);
}
/**
* 写出数据到客户端
*
* @param in 数据流
* @param length 指定响应内容长度默认0表示不定长度会输出Transfer-encoding: chunked
* @return this
*/
public HttpServerResponse write(final InputStream in, final int length) {
if (!isSendCode) {
sendOk(Math.max(0, length));
}
OutputStream out = null;
try {
out = this.httpExchange.getResponseBody();
IoUtil.copy(in, out);
} finally {
IoUtil.closeQuietly(out);
IoUtil.closeQuietly(in);
}
return this;
}
/**
* 返回文件给客户端(文件下载)
*
* @param file 写出的文件对象
* @return this
* @since 5.2.6
*/
public HttpServerResponse write(final File file) {
return write(file, null);
}
/**
* 返回文件给客户端(文件下载)
*
* @param file 写出的文件对象
* @param fileName 文件名
* @return this
* @since 5.5.8
*/
public HttpServerResponse write(final File file, String fileName) {
final long fileSize = file.length();
if(fileSize > Integer.MAX_VALUE){
throw new IllegalArgumentException("File size is too bigger than " + Integer.MAX_VALUE);
}
if(StrUtil.isBlank(fileName)){
fileName = file.getName();
}
final String contentType = FileUtil.getMimeType(fileName, ContentType.OCTET_STREAM.getValue());
BufferedInputStream in = null;
try {
in = FileUtil.getInputStream(file);
write(in, (int)fileSize, contentType, fileName);
} finally {
IoUtil.closeQuietly(in);
}
return this;
}
/**
* 返回文件数据给客户端(文件下载)
*
* @param in 需要返回客户端的内容
* @param contentType 返回的类型
* @param fileName 文件名
* @since 5.2.6
*/
public void write(final InputStream in, final String contentType, final String fileName) {
write(in, 0, contentType, fileName);
}
/**
* 返回文件数据给客户端(文件下载)
*
* @param in 需要返回客户端的内容
* @param length 长度
* @param contentType 返回的类型
* @param fileName 文件名
* @return this
* @since 5.2.7
*/
public HttpServerResponse write(final InputStream in, final int length, final String contentType, final String fileName) {
final Charset charset = ObjUtil.defaultIfNull(this.charset, DEFAULT_CHARSET);
if (!contentType.startsWith("text/")) {
// 非文本类型数据直接走下载
setHeader(HeaderName.CONTENT_DISPOSITION, StrUtil.format("attachment;filename={}", UrlEncoder.encodeAll(fileName, charset)));
}
return write(in, length, contentType);
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright (c) 2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.hutool.http.server;
import javax.net.ssl.SSLContext;
/**
* 服务器配置
*
* @author Looly
*/
public class ServerConfig {
/**
* 创建配置
*
* @return 配置
*/
public static ServerConfig of(){
return new ServerConfig();
}
private String host = "localhost";
private int port = 8888;
private String root;
private SSLContext sslContext;
/**
* 获取服务器地址默认127.0.0.1
*
* @return 服务器地址
*/
public String getHost() {
return host;
}
/**
* 设置服务器地址默认127.0.0.1
*
* @param host 服务器地址
* @return this
*/
public ServerConfig setHost(final String host) {
this.host = host;
return this;
}
/**
* 获取服务器端口
*
* @return 服务器端口
*/
public int getPort() {
return port;
}
/**
* 设置服务器端口
*
* @param port 服务器端口
* @return this
*/
public ServerConfig setPort(final int port) {
this.port = port;
return this;
}
/**
* 获取服务器根目录
*
* @return 服务器根目录
*/
public String getRoot() {
return root;
}
/**
* 设置服务器根目录
*
* @param root 服务器根目录
* @return this
*/
public ServerConfig setRoot(final String root) {
this.root = root;
return this;
}
/**
* 获取SSL上下文
*
* @return SSL上下文
*/
public SSLContext getSslContext() {
return sslContext;
}
/**
* 设置SSL上下文
*
* @param sslContext SSL上下文
* @return this
*/
public ServerConfig setSslContext(final SSLContext sslContext) {
this.sslContext = sslContext;
return this;
}
}

View File

@@ -0,0 +1,328 @@
/*
* Copyright (c) 2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.hutool.http.server;
import org.dromara.hutool.core.io.IORuntimeException;
import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.core.map.multi.ListValueMap;
import org.dromara.hutool.core.net.url.UrlQueryUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.core.util.CharsetUtil;
import org.dromara.hutool.core.util.ObjUtil;
import org.dromara.hutool.http.meta.ContentTypeUtil;
import org.dromara.hutool.http.meta.HeaderName;
import org.dromara.hutool.http.meta.Method;
import org.dromara.hutool.http.multipart.MultipartFormData;
import org.dromara.hutool.http.multipart.UploadSetting;
import org.dromara.hutool.http.useragent.UserAgent;
import org.dromara.hutool.http.useragent.UserAgentUtil;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Collection;
/**
* 服务端请求对象,用于获取请求参数等
*
* @author looly
* @since 6.0.0
*/
public interface ServerRequest {
/**
* 默认编码用于获取请求头和响应头编码默认为UTF-8
*/
Charset DEFAULT_CHARSET = CharsetUtil.UTF_8;
// region ----- method
/**
* 获取请求方法
*
* @return 请求方法
*/
String getMethod();
/**
* 是否为GET请求
*
* @return 是否为GET请求
*/
default boolean isGetMethod() {
return Method.GET.name().equalsIgnoreCase(getMethod());
}
/**
* 是否为POST请求
*
* @return 是否为POST请求
*/
default boolean isPostMethod() {
return Method.POST.name().equalsIgnoreCase(getMethod());
}
// endregion
/**
* 获取请求路径,包含请求参数部分
*
* @return 请求路径,包含请求参数部分
*/
String getPath();
/**
* 获取请求参数包括pathVariable和queryString
*
* @return 请求参数包括pathVariable和queryString
*/
String getQuery();
// region ----- header
/**
* 获取请求头
*
* @param name 请求头名
* @return 请求头
*/
String getHeader(final String name);
/**
* 获得请求header中的信息
*
* @param headerNameKey 头信息的KEY
* @return header值
*/
default String getHeader(final HeaderName headerNameKey) {
return getHeader(headerNameKey.toString());
}
/**
* 获得请求header中的信息
*
* @param headerKey 头信息的KEY
* @param charset 字符集
* @return header值
*/
default String getHeader(final String headerKey, final Charset charset) {
final String header = getHeader(headerKey);
if (null != header) {
return CharsetUtil.convert(header, CharsetUtil.ISO_8859_1, charset);
}
return null;
}
/**
* 获取Content-Type头信息
*
* @return Content-Type头信息
*/
default String getContentType() {
return getHeader(HeaderName.CONTENT_TYPE);
}
/**
* 获取Content-Length头信息单位字节
*
* @return Content-Length头信息单位字节
*/
default long getContentLength() {
final String contentLength = getHeader(HeaderName.CONTENT_LENGTH);
return StrUtil.isEmpty(contentLength) ? -1 : Long.parseLong(contentLength);
}
/**
* 获取编码获取失败默认使用UTF-8获取规则如下
*
* <pre>
* 1、从Content-Type头中获取编码类似于text/html;charset=utf-8
* </pre>
*
* @return 编码默认UTF-8
*/
default Charset getCharset() {
final String contentType = getContentType();
return ObjUtil.defaultIfNull(ContentTypeUtil.getCharset(contentType), DEFAULT_CHARSET);
}
/**
* 获得User-Agent
*
* @return User-Agent字符串
*/
default String getUserAgentStr() {
return getHeader(HeaderName.USER_AGENT);
}
/**
* 获得User-Agent未识别返回null
*
* @return User-Agent字符串未识别返回null
*/
default UserAgent getUserAgent() {
return UserAgentUtil.parse(getUserAgentStr());
}
/**
* 获得Cookie信息字符串
*
* @return cookie字符串
*/
default String getCookiesStr() {
return getHeader(HeaderName.COOKIE);
}
/**
* 是否为Multipart类型表单此类型表单用于文件上传
*
* @return 是否为Multipart类型表单此类型表单用于文件上传
*/
default boolean isMultipart() {
if (!isPostMethod()) {
return false;
}
final String contentType = getContentType();
if (StrUtil.isBlank(contentType)) {
return false;
}
return contentType.toLowerCase().startsWith("multipart/");
}
// endregion
// region ----- body
/**
* 获取请求体流
*
* @return 请求体流
*/
InputStream getBodyStream();
/**
* 获取请求体文本可以是form表单、json、xml等任意内容<br>
* 使用{@link #getCharset()}判断编码判断失败使用UTF-8编码
*
* @return 请求
*/
default String getBody() {
return getBody(getCharset());
}
/**
* 获取请求体文本可以是form表单、json、xml等任意内容
*
* @param charset 编码
* @return 请求
*/
default String getBody(final Charset charset) {
return StrUtil.str(getBodyBytes(), charset);
}
/**
* 获取body的bytes数组
*
* @return body的bytes数组
*/
default byte[] getBodyBytes() {
return IoUtil.readBytes(getBodyStream(), true);
}
/**
* 获得MultiPart表单内容多用于获得上传的文件
*
* @return MultipartFormData
* @throws IORuntimeException IO异常
* @since 5.3.0
*/
default MultipartFormData getMultipart() throws IORuntimeException {
return parseMultipart(new UploadSetting());
}
/**
* 获得multipart/form-data 表单内容<br>
* 包括文件和普通表单数据<br>
* 在同一次请求中,此方法只能被执行一次!
*
* @param uploadSetting 上传文件的设定,包括最大文件大小、保存在内存的边界大小、临时目录、扩展名限定等
* @return MultiPart表单
* @throws IORuntimeException IO异常
* @since 5.3.0
*/
default MultipartFormData parseMultipart(final UploadSetting uploadSetting) throws IORuntimeException {
final MultipartFormData formData = new MultipartFormData(uploadSetting);
try {
formData.parseRequestStream(getBodyStream(), getCharset());
} catch (final IOException e) {
throw new IORuntimeException(e);
}
return formData;
}
// endregion
// region ----- param
/**
* 获取指定名称的参数值,取第一个值
* @param name 参数名
* @return 参数值
* @since 5.5.8
*/
default String getParam(final String name){
return getParams().getValue(name, 0);
}
/**
* 获取指定名称的参数值
*
* @param name 参数名
* @return 参数值
* @since 5.5.8
*/
default Collection<String> getParams(final String name){
return getParams().get(name);
}
/**
* 获取参数Map
*
* @return 参数map
*/
default ListValueMap<String, String> getParams() {
final ListValueMap<String, String> params = new ListValueMap<>();
final Charset charset = getCharset();
//解析URL中的参数
final String query = getQuery();
if(StrUtil.isNotBlank(query)){
params.putAll(UrlQueryUtil.decodeQueryList(query, charset));
}
// 解析multipart中的参数
if(isMultipart()){
params.putAll(getMultipart().getParamListMap());
} else{
// 解析body中的参数
final String body = getBody();
if(StrUtil.isNotBlank(body)){
params.putAll(UrlQueryUtil.decodeQueryList(body, charset));
}
}
return params;
}
// endregion
}

View File

@@ -0,0 +1,334 @@
/*
* Copyright (c) 2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.hutool.http.server;
import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.net.url.UrlEncoder;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.core.util.ByteUtil;
import org.dromara.hutool.core.util.CharsetUtil;
import org.dromara.hutool.core.util.ObjUtil;
import org.dromara.hutool.http.meta.ContentType;
import org.dromara.hutool.http.meta.HeaderName;
import java.io.*;
import java.nio.charset.Charset;
import java.util.List;
/**
* 服务端响应接口,用于写出数据
*
* @author Looly
*/
public interface ServerResponse {
/**
* 默认编码用于获取请求头和响应头编码默认为UTF-8
*/
Charset DEFAULT_CHARSET = CharsetUtil.UTF_8;
/**
* 设置状态码
*
* @param statusCode 状态码
* @return this
*/
ServerResponse setStatus(int statusCode);
/**
* 设置编码默认为UTF-8
*
* @param charset 编码
* @return this
*/
ServerResponse setCharset(Charset charset);
/**
* 获取编码默认为UTF-8
*
* @return 编码
*/
Charset getCharset();
/**
* 添加响应头,如果已经存在,则追加
*
* @param header 头key
* @param value 值
* @return this
*/
ServerResponse addHeader(final String header, final String value);
/**
* 设置响应头,如果已经存在,则覆盖
*
* @param header 头key
* @param value 值
* @return this
*/
ServerResponse setHeader(final String header, final String value);
/**
* 设置响应头,如果已经存在,则覆盖
*
* @param headerName 头key
* @param value 值
* @return this
*/
default ServerResponse setHeader(final HeaderName headerName, final String value) {
return setHeader(headerName.getValue(), value);
}
/**
* 设置响应头,如果已经存在,则覆盖
*
* @param header 头key
* @param value 值列表如果为空删除该header
* @return this
*/
default ServerResponse setHeader(final String header, final List<String> value) {
// 去除原有header
setHeader(header, (String) null);
if(null == value){
return this;
}
// 加入新的header
for (final String valueItem : value) {
addHeader(header, valueItem);
}
return this;
}
/**
* 设置Content-Type头类似于:text/html;charset=utf-8<br>
* 如果用户传入的信息无charset信息自动根据charset补充charset设置见{@link #setCharset(Charset)}
*
* @param contentType Content-Type头内容
* @return this
*/
default ServerResponse setContentType(String contentType) {
if (null != contentType) {
final Charset charset = getCharset();
if (null != charset && !contentType.contains(";charset=")) {
contentType = ContentType.build(contentType, charset);
}
}
return setHeader(HeaderName.CONTENT_TYPE, contentType);
}
/**
* 设置Content-Length头-1表示移除头
*
* @param contentLength Content-Length头内容
* @return this
*/
default ServerResponse setContentLength(final long contentLength) {
return setHeader(HeaderName.CONTENT_LENGTH,
contentLength < 0 ? null : String.valueOf(contentLength));
}
/**
* 获取输出流,用于写出数据
*
* @return 输出流
*/
OutputStream getOutputStream();
/**
* 获取响应数据流
*
* @return 响应数据流
*/
default PrintWriter getWriter() {
final Charset charset = ObjUtil.defaultIfNull(getCharset(), DEFAULT_CHARSET);
return new PrintWriter(new OutputStreamWriter(getOutputStream(), charset));
}
/**
* 写出数据到客户端
*
* @param data 数据
* @param contentType Content-Type类型
* @return this
*/
default ServerResponse write(final String data, final String contentType) {
setContentType(contentType);
return write(data);
}
/**
* 写出数据到客户端
*
* @param data 数据
* @return this
*/
default ServerResponse write(final String data) {
final Charset charset = ObjUtil.defaultIfNull(getCharset(), DEFAULT_CHARSET);
return write(ByteUtil.toBytes(data, charset));
}
/**
* 写出数据到客户端
*
* @param data 数据
* @param contentType 返回的类型
* @return this
*/
default ServerResponse write(final byte[] data, final String contentType) {
setContentType(contentType);
return write(data);
}
/**
* 写出数据到客户端
*
* @param data 数据
* @return this
*/
default ServerResponse write(final byte[] data) {
final ByteArrayInputStream in = new ByteArrayInputStream(data);
return write(in, in.available());
}
/**
* 返回数据给客户端
*
* @param in 需要返回客户端的内容
* @param contentType 返回的类型
* @return this
* @since 5.2.6
*/
default ServerResponse write(final InputStream in, final String contentType) {
return write(in, 0, contentType);
}
/**
* 返回数据给客户端
*
* @param in 需要返回客户端的内容
* @param length 内容长度默认0表示不定长度会输出Transfer-encoding: chunked
* @param contentType 返回的类型
* @return this
* @since 5.2.7
*/
default ServerResponse write(final InputStream in, final int length, final String contentType) {
setContentType(contentType);
return write(in, length);
}
/**
* 写出数据到客户端
*
* @param in 数据流
* @return this
*/
default ServerResponse write(final InputStream in) {
return write(in, 0);
}
/**
* 写出数据到客户端
*
* @param in 数据流
* @param length 指定响应内容长度默认0表示不定长度会输出Transfer-encoding: chunked
* @return this
*/
default ServerResponse write(final InputStream in, final int length) {
setContentLength(length);
OutputStream out = null;
try {
out = getOutputStream();
IoUtil.copy(in, out);
} finally {
IoUtil.closeQuietly(out);
IoUtil.closeQuietly(in);
}
return this;
}
/**
* 返回文件给客户端(文件下载)
*
* @param file 写出的文件对象
* @return this
* @since 5.2.6
*/
default ServerResponse write(final File file) {
return write(file, null);
}
/**
* 返回文件给客户端(文件下载)
*
* @param file 写出的文件对象
* @param fileName 文件名
* @return this
*/
default ServerResponse write(final File file, String fileName) {
final long fileSize = file.length();
if (fileSize > Integer.MAX_VALUE) {
throw new IllegalArgumentException("File size is too bigger than " + Integer.MAX_VALUE);
}
if (StrUtil.isBlank(fileName)) {
fileName = file.getName();
}
final String contentType = FileUtil.getMimeType(fileName, ContentType.OCTET_STREAM.getValue());
BufferedInputStream in = null;
try {
in = FileUtil.getInputStream(file);
write(in, (int) fileSize, contentType, fileName);
} finally {
IoUtil.closeQuietly(in);
}
return this;
}
/**
* 返回文件数据给客户端(文件下载)
*
* @param in 需要返回客户端的内容
* @param contentType 返回的类型
* @param fileName 文件名
* @return this
*/
default ServerResponse write(final InputStream in, final String contentType, final String fileName) {
return write(in, 0, contentType, fileName);
}
/**
* 返回文件数据给客户端(文件下载)
*
* @param in 需要返回客户端的内容
* @param length 长度
* @param contentType 返回的类型
* @param fileName 文件名
* @return this
*/
default ServerResponse write(final InputStream in, final int length, final String contentType, final String fileName) {
final Charset charset = ObjUtil.defaultIfNull(getCharset(), DEFAULT_CHARSET);
if (!contentType.startsWith("text/")) {
// 非文本类型数据直接走下载
setHeader(HeaderName.CONTENT_DISPOSITION, StrUtil.format("attachment;filename={}", UrlEncoder.encodeAll(fileName, charset)));
}
return write(in, length, contentType);
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (c) 2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.hutool.http.server.engine;
import org.dromara.hutool.http.server.ServerConfig;
import org.dromara.hutool.http.server.handler.HttpHandler;
/**
* 服务端引擎抽象类,实现重置引擎功能
*
* @author looly
*/
public abstract class AbstractServerEngine implements ServerEngine {
protected ServerConfig config;
protected HttpHandler handler;
@Override
public AbstractServerEngine init(final ServerConfig config) {
this.config = config;
reset();
return this;
}
@Override
public AbstractServerEngine setHandler(final HttpHandler handler) {
this.handler = handler;
return this;
}
/**
* 重置引擎
*/
protected abstract void reset();
/**
* 初始化引擎,实现逻辑中如果初始化完成,不再重新初始化
*/
protected abstract void initEngine();
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.hutool.http.server.engine;
import org.dromara.hutool.http.server.ServerConfig;
import org.dromara.hutool.http.server.handler.HttpHandler;
/**
* HTTP服务器引擎
*
* @author looly
* @since 6.0.0
*/
public interface ServerEngine {
/**
* 初始化HTTP服务器
*
* @param config 配置项
* @return this
*/
ServerEngine init(ServerConfig config);
/**
* 设置请求处理器
*
* @param handler 请求处理器
* @return this
*/
ServerEngine setHandler(HttpHandler handler);
/**
* 启动HTTP服务器
*/
void start();
/**
* 获取原始引擎的钩子方法,用于自定义特殊属性,如插件等
*
* @return 对应HTTP服务器实现的引擎对象
* @since 6.0.0
*/
Object getRawEngine();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013-2024 Hutool Team and hutool.cn
* Copyright (c) 2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,8 +15,8 @@
*/
/**
* {@link com.sun.net.httpserver.HttpServer} 封装
* Jetty引擎实现
*
* @author looly
* @author Looly
*/
package org.dromara.hutool.http.server.action;
package org.dromara.hutool.http.server.engine.jetty;

View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) 2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* HTTP服务器引擎包
*
* @author Looly
* @since 6.0.0
*/
package org.dromara.hutool.http.server.engine;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.dromara.hutool.http.server;
package org.dromara.hutool.http.server.engine.sun;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpContext;
@@ -36,8 +36,8 @@ import java.net.URI;
public class HttpExchangeWrapper extends HttpExchange implements Wrapper<HttpExchange> {
private final HttpExchange raw;
private final HttpServerRequest request;
private final HttpServerResponse response;
private final SunServerRequest request;
private final SunServerResponse response;
/**
* 构造
@@ -46,8 +46,8 @@ public class HttpExchangeWrapper extends HttpExchange implements Wrapper<HttpExc
*/
public HttpExchangeWrapper(final HttpExchange raw) {
this.raw = raw;
this.request = new HttpServerRequest(this);
this.response = new HttpServerResponse(this);
this.request = new SunServerRequest(this);
this.response = new SunServerResponse(this);
}
@Override
@@ -60,7 +60,7 @@ public class HttpExchangeWrapper extends HttpExchange implements Wrapper<HttpExc
*
* @return 请求
*/
public HttpServerRequest getRequest() {
public SunServerRequest getRequest() {
return request;
}
@@ -69,7 +69,7 @@ public class HttpExchangeWrapper extends HttpExchange implements Wrapper<HttpExc
*
* @return 响应
*/
public HttpServerResponse getResponse() {
public SunServerResponse getResponse() {
return response;
}

View File

@@ -14,25 +14,20 @@
* limitations under the License.
*/
package org.dromara.hutool.http.server;
package org.dromara.hutool.http.server.engine.sun;
import com.sun.net.httpserver.*;
import org.dromara.hutool.core.io.IORuntimeException;
import org.dromara.hutool.core.lang.Console;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.core.thread.GlobalThreadPool;
import org.dromara.hutool.http.server.action.Action;
import org.dromara.hutool.http.server.action.RootAction;
import org.dromara.hutool.http.server.filter.HttpFilter;
import org.dromara.hutool.http.server.filter.SimpleFilter;
import org.dromara.hutool.http.server.handler.ActionHandler;
import org.dromara.hutool.http.server.ServerConfig;
import org.dromara.hutool.http.server.engine.sun.filter.HttpFilter;
import org.dromara.hutool.http.server.engine.sun.filter.SimpleFilter;
import org.dromara.hutool.http.server.handler.RootHandler;
import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
/**
@@ -43,8 +38,7 @@ import java.util.concurrent.Executor;
*/
public class SimpleServer {
private final HttpServer server;
private final List<Filter> filters;
private final SunHttpServerEngine engine;
/**
* 构造
@@ -71,39 +65,23 @@ public class SimpleServer {
* @param address 监听地址
*/
public SimpleServer(final InetSocketAddress address) {
this(address, (HttpsConfigurator) null);
this(address, null);
}
/**
* 构造
*
* @param address 监听地址
* @param address 监听地址
* @param sslContext ssl配置
*/
public SimpleServer(final InetSocketAddress address, final SSLContext sslContext) {
this(address, new HttpsConfigurator(sslContext));
}
this.engine = new SunHttpServerEngine();
/**
* 构造
*
* @param address 监听地址
* @param configurator https配置信息用于使用自定义SSLTLS证书等
*/
public SimpleServer(final InetSocketAddress address, final HttpsConfigurator configurator) {
try {
if(null != configurator){
final HttpsServer server = HttpsServer.create(address, 0);
server.setHttpsConfigurator(configurator);
this.server = server;
} else{
this.server = HttpServer.create(address, 0);
}
} catch (final IOException e) {
throw new IORuntimeException(e);
}
setExecutor(GlobalThreadPool.getExecutor());
filters = new ArrayList<>();
final ServerConfig serverConfig = ServerConfig.of()
.setHost(address.getHostName())
.setPort(address.getPort())
.setSslContext(sslContext);
this.engine.init(serverConfig);
}
/**
@@ -115,7 +93,6 @@ public class SimpleServer {
* <li>{@link #setRoot(String)} </li>
* <li>{@link #createContext(String, HttpHandler)} </li>
* <li>{@link #addHandler(String, HttpHandler)}</li>
* <li>{@link #addAction(String, Action)} </li>
* </ul>
*
* @param filter {@link Filter} 请求过滤器
@@ -123,7 +100,7 @@ public class SimpleServer {
* @since 5.5.7
*/
public SimpleServer addFilter(final Filter filter) {
this.filters.add(filter);
this.engine.addFilter(filter);
return this;
}
@@ -136,7 +113,6 @@ public class SimpleServer {
* <li>{@link #setRoot(String)} </li>
* <li>{@link #createContext(String, HttpHandler)} </li>
* <li>{@link #addHandler(String, HttpHandler)}</li>
* <li>{@link #addAction(String, Action)} </li>
* </ul>
*
* @param filter {@link Filter} 请求过滤器
@@ -177,10 +153,22 @@ public class SimpleServer {
public HttpContext createContext(String path, final HttpHandler handler) {
// /开头的路径会报错
path = StrUtil.addPrefixIfNot(path, StrUtil.SLASH);
final HttpContext context = this.server.createContext(path, handler);
// 增加整体过滤器
context.getFilters().addAll(this.filters);
return context;
return this.engine.createContext(path, handler);
}
/**
* 增加请求处理规则使用默认的{@link RootHandler}默认从当前项目根目录读取页面
*
* @param path 路径例如:/a/b 或者 a/b
* @param action 处理器包括请求和响应处理
* @return this
* @since 6.0.0
*/
public SimpleServer addAction(final String path, final org.dromara.hutool.http.server.handler.HttpHandler action) {
return addHandler(path, exchange -> {
final HttpExchangeWrapper exchangeWrapper = new HttpExchangeWrapper(exchange);
action.handle(exchangeWrapper.getRequest(), exchangeWrapper.getResponse());
});
}
/**
@@ -200,18 +188,8 @@ public class SimpleServer {
* @return this
*/
public SimpleServer setRoot(final File root) {
return addAction("/", new RootAction(root));
}
/**
* 增加请求处理规则
*
* @param path 路径
* @param action 处理器
* @return this
*/
public SimpleServer addAction(final String path, final Action action) {
return addHandler(path, new ActionHandler(action));
this.engine.setHandler(new RootHandler(root));
return this;
}
/**
@@ -221,7 +199,7 @@ public class SimpleServer {
* @return this
*/
public SimpleServer setExecutor(final Executor executor) {
this.server.setExecutor(executor);
this.engine.setExecutor(executor);
return this;
}
@@ -231,7 +209,7 @@ public class SimpleServer {
* @return {@link HttpServer}
*/
public HttpServer getRawServer() {
return this.server;
return this.engine.getRawEngine();
}
/**
@@ -240,7 +218,7 @@ public class SimpleServer {
* @return {@link InetSocketAddress}
*/
public InetSocketAddress getAddress() {
return this.server.getAddress();
return getRawServer().getAddress();
}
/**
@@ -249,6 +227,6 @@ public class SimpleServer {
public void start() {
final InetSocketAddress address = getAddress();
Console.log("Hutool Simple Http Server listen on 【{}:{}】", address.getHostName(), address.getPort());
this.server.start();
this.engine.start();
}
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright (c) 2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.hutool.http.server.engine.sun;
import com.sun.net.httpserver.*;
import org.dromara.hutool.core.io.IORuntimeException;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.core.thread.GlobalThreadPool;
import org.dromara.hutool.http.server.engine.AbstractServerEngine;
import org.dromara.hutool.http.server.ServerConfig;
import org.dromara.hutool.http.server.engine.sun.filter.HttpFilter;
import org.dromara.hutool.http.server.engine.sun.filter.SimpleFilter;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
/**
* 基于Sun HttpServer的HTTP服务器引擎实现
*
* @author looly
* @since 6.0.0
*/
public class SunHttpServerEngine extends AbstractServerEngine {
private HttpServer server;
private List<Filter> filters;
/**
* 构造
*/
public SunHttpServerEngine() {
}
@Override
public void start() {
initEngine();
this.server.start();
}
@Override
public HttpServer getRawEngine() {
return this.server;
}
/**
* 增加请求过滤器,此过滤器对所有请求有效<br>
* 此方法需在以下方法前之前调用:
*
* <ul>
* <li>{@link #createContext(String, HttpHandler)} </li>
* </ul>
*
* @param filter {@link Filter} 请求过滤器
* @return this
* @since 5.5.7
*/
public SunHttpServerEngine addFilter(final Filter filter) {
if (null == this.filters) {
this.filters = new ArrayList<>();
}
this.filters.add(filter);
return this;
}
/**
* 增加请求过滤器,此过滤器对所有请求有效<br>
* 此方法需在以下方法前之前调用:
*
* <ul>
* <li>{@link #createContext(String, HttpHandler)} </li>
* </ul>
*
* @param filter {@link Filter} 请求过滤器
* @return this
* @since 5.5.7
*/
public SunHttpServerEngine addFilter(final HttpFilter filter) {
return addFilter(new SimpleFilter() {
@Override
public void doFilter(final HttpExchange httpExchange, final Chain chain) throws IOException {
final HttpExchangeWrapper httpExchangeWrapper = new HttpExchangeWrapper(httpExchange);
filter.doFilter(httpExchangeWrapper.getRequest(), httpExchangeWrapper.getResponse(), chain);
}
});
}
/**
* 创建请求映射上下文,创建后,用户访问指定路径可使用{@link HttpHandler} 中的规则进行处理
*
* @param path 路径,例如:/a/b 或者 a/b
* @param handler 处理器,包括请求和响应处理
* @return {@link HttpContext}
* @since 5.5.7
*/
public HttpContext createContext(String path, final HttpHandler handler) {
// 非/开头的路径会报错
path = StrUtil.addPrefixIfNot(path, StrUtil.SLASH);
final HttpContext context = this.server.createContext(path, handler);
// 增加整体过滤器
context.getFilters().addAll(this.filters);
return context;
}
/**
* 设置自定义线程池
*
* @param executor {@link Executor}
* @return this
*/
public SunHttpServerEngine setExecutor(final Executor executor) {
this.server.setExecutor(executor);
return this;
}
@Override
protected void reset() {
if (null != this.server) {
this.server.stop(0);
this.server = null;
}
}
@Override
protected void initEngine() {
final ServerConfig config = this.config;
final InetSocketAddress address = new InetSocketAddress(config.getHost(), config.getPort());
final SSLContext sslContext = config.getSslContext();
try {
if (null != sslContext) {
final HttpsServer server = HttpsServer.create(address, 0);
server.setHttpsConfigurator(new HttpsConfigurator(sslContext));
this.server = server;
} else {
this.server = HttpServer.create(address, 0);
}
} catch (final IOException e) {
throw new IORuntimeException(e);
}
setExecutor(GlobalThreadPool.getExecutor());
createContext("/", exchange -> SunHttpServerEngine.this.handler.handle(
new SunServerRequest(exchange),
new SunServerResponse(exchange)
));
}
}

View File

@@ -14,14 +14,12 @@
* limitations under the License.
*/
package org.dromara.hutool.http.server;
package org.dromara.hutool.http.server.engine.sun;
import org.dromara.hutool.core.util.CharsetUtil;
import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpExchange;
import java.io.Closeable;
import java.nio.charset.Charset;
/**
* HttpServer公用对象提供HttpExchange包装和公用方法
@@ -29,18 +27,16 @@ import java.nio.charset.Charset;
* @author looly
* @since 5.2.6
*/
public class HttpServerBase implements Closeable {
public class SunServerBase implements Closeable {
final static Charset DEFAULT_CHARSET = CharsetUtil.UTF_8;
final HttpExchange httpExchange;
protected final HttpExchange httpExchange;
/**
* 构造
*
* @param httpExchange {@link HttpExchange}
*/
public HttpServerBase(final HttpExchange httpExchange) {
public SunServerBase(final HttpExchange httpExchange) {
this.httpExchange = httpExchange;
}
@@ -49,7 +45,7 @@ public class HttpServerBase implements Closeable {
*
* @return {@link HttpExchange}对象
*/
public HttpExchange getHttpExchange() {
public HttpExchange getExchange() {
return this.httpExchange;
}
@@ -60,7 +56,7 @@ public class HttpServerBase implements Closeable {
* @since 5.5.7
*/
public HttpContext getHttpContext() {
return getHttpExchange().getHttpContext();
return getExchange().getHttpContext();
}
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013-2024 Hutool Team and hutool.cn
* Copyright (c) 2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.dromara.hutool.http.server;
package org.dromara.hutool.http.server.engine.sun;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
@@ -26,19 +26,15 @@ import org.dromara.hutool.core.map.CaseInsensitiveMap;
import org.dromara.hutool.core.map.MapUtil;
import org.dromara.hutool.core.map.multi.ListValueMap;
import org.dromara.hutool.core.net.NetUtil;
import org.dromara.hutool.http.multipart.MultipartFormData;
import org.dromara.hutool.http.multipart.UploadSetting;
import org.dromara.hutool.core.net.url.UrlQueryUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.core.util.CharsetUtil;
import org.dromara.hutool.core.util.ObjUtil;
import org.dromara.hutool.http.meta.ContentTypeUtil;
import org.dromara.hutool.http.meta.HeaderName;
import org.dromara.hutool.http.meta.Method;
import org.dromara.hutool.http.useragent.UserAgent;
import org.dromara.hutool.http.useragent.UserAgentUtil;
import org.dromara.hutool.http.multipart.MultipartFormData;
import org.dromara.hutool.http.multipart.UploadSetting;
import org.dromara.hutool.http.server.ServerRequest;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpCookie;
import java.net.URI;
@@ -47,12 +43,11 @@ import java.util.Collection;
import java.util.Map;
/**
* Http请求对象{@link HttpExchange}
* Sun Http请求包
*
* @author looly
* @since 5.2.6
* @author Looly
*/
public class HttpServerRequest extends HttpServerBase {
public class SunServerRequest extends SunServerBase implements ServerRequest {
private Map<String, HttpCookie> cookieCache;
private ListValueMap<String, String> paramsCache;
@@ -65,37 +60,15 @@ public class HttpServerRequest extends HttpServerBase {
*
* @param httpExchange {@link HttpExchange}
*/
public HttpServerRequest(final HttpExchange httpExchange) {
public SunServerRequest(final HttpExchange httpExchange) {
super(httpExchange);
}
/**
* 获得Http Method
*
* @return Http Method
*/
@Override
public String getMethod() {
return this.httpExchange.getRequestMethod();
}
/**
* 是否为GET请求
*
* @return 是否为GET请求
*/
public boolean isGetMethod() {
return Method.GET.name().equalsIgnoreCase(getMethod());
}
/**
* 是否为POST请求
*
* @return 是否为POST请求
*/
public boolean isPostMethod() {
return Method.POST.name().equalsIgnoreCase(getMethod());
}
/**
* 获得请求URI
*
@@ -105,20 +78,12 @@ public class HttpServerRequest extends HttpServerBase {
return this.httpExchange.getRequestURI();
}
/**
* 获得请求路径Path
*
* @return 请求路径
*/
@Override
public String getPath() {
return getURI().getPath();
}
/**
* 获取请求参数
*
* @return 参数字符串
*/
@Override
public String getQuery() {
return getURI().getQuery();
}
@@ -132,59 +97,12 @@ public class HttpServerRequest extends HttpServerBase {
return this.httpExchange.getRequestHeaders();
}
/**
* 获得请求header中的信息
*
* @param headerNameKey 头信息的KEY
* @return header值
*/
public String getHeader(final HeaderName headerNameKey) {
return getHeader(headerNameKey.toString());
@Override
public String getHeader(final String name) {
return getHeaders().getFirst(name);
}
/**
* 获得请求header中的信息
*
* @param headerKey 头信息的KEY
* @return header值
*/
public String getHeader(final String headerKey) {
return getHeaders().getFirst(headerKey);
}
/**
* 获得请求header中的信息
*
* @param headerKey 头信息的KEY
* @param charset 字符集
* @return header值
*/
public String getHeader(final String headerKey, final Charset charset) {
final String header = getHeader(headerKey);
if (null != header) {
return CharsetUtil.convert(header, CharsetUtil.ISO_8859_1, charset);
}
return null;
}
/**
* 获取Content-Type头信息
*
* @return Content-Type头信息
*/
public String getContentType() {
return getHeader(HeaderName.CONTENT_TYPE);
}
/**
* 获取编码获取失败默认使用UTF-8获取规则如下
*
* <pre>
* 1从Content-Type头中获取编码类似于text/html;charset=utf-8
* </pre>
*
* @return 编码默认UTF-8
*/
@Override
public Charset getCharset() {
if(null == this.charsetCache){
final String contentType = getContentType();
@@ -194,33 +112,6 @@ public class HttpServerRequest extends HttpServerBase {
return this.charsetCache;
}
/**
* 获得User-Agent
*
* @return User-Agent字符串
*/
public String getUserAgentStr() {
return getHeader(HeaderName.USER_AGENT);
}
/**
* 获得User-Agent未识别返回null
*
* @return User-Agent字符串未识别返回null
*/
public UserAgent getUserAgent() {
return UserAgentUtil.parse(getUserAgentStr());
}
/**
* 获得Cookie信息字符串
*
* @return cookie字符串
*/
public String getCookiesStr() {
return getHeader(HeaderName.COOKIE);
}
/**
* 获得Cookie信息列表
*
@@ -238,9 +129,9 @@ public class HttpServerRequest extends HttpServerBase {
public Map<String, HttpCookie> getCookieMap() {
if (null == this.cookieCache) {
cookieCache = MapUtil.view(MapUtil.putAll(
new CaseInsensitiveMap<>(),
NetUtil.parseCookies(getCookiesStr()),
HttpCookie::getName));
new CaseInsensitiveMap<>(),
NetUtil.parseCookies(getCookiesStr()),
HttpCookie::getName));
}
return cookieCache;
}
@@ -255,60 +146,7 @@ public class HttpServerRequest extends HttpServerBase {
return getCookieMap().get(cookieName);
}
/**
* 是否为Multipart类型表单此类型表单用于文件上传
*
* @return 是否为Multipart类型表单此类型表单用于文件上传
*/
public boolean isMultipart() {
if (!isPostMethod()) {
return false;
}
final String contentType = getContentType();
if (StrUtil.isBlank(contentType)) {
return false;
}
return contentType.toLowerCase().startsWith("multipart/");
}
/**
* 获取请求体文本可以是form表单jsonxml等任意内容<br>
* 使用{@link #getCharset()}判断编码判断失败使用UTF-8编码
*
* @return 请求
*/
public String getBody() {
return getBody(getCharset());
}
/**
* 获取请求体文本可以是form表单jsonxml等任意内容
*
* @param charset 编码
* @return 请求
*/
public String getBody(final Charset charset) {
return StrUtil.str(getBodyBytes(), charset);
}
/**
* 获取body的bytes数组
*
* @return body的bytes数组
*/
public byte[] getBodyBytes(){
if(null == this.bodyCache){
this.bodyCache = IoUtil.readBytes(getBodyStream(), true);
}
return this.bodyCache;
}
/**
* 获取请求体的流流中可以读取请求内容包括请求表单数据或文件上传数据
*
* @return
*/
@Override
public InputStream getBodyStream() {
InputStream bodyStream = this.httpExchange.getRequestBody();
@@ -330,32 +168,23 @@ public class HttpServerRequest extends HttpServerBase {
return bodyStream;
}
/**
* 获取指定名称的参数值取第一个值
* @param name 参数名
* @return 参数值
* @since 5.5.8
*/
public String getParam(final String name){
return getParams().getValue(name, 0);
@Override
public byte[] getBodyBytes(){
if(null == this.bodyCache){
this.bodyCache = IoUtil.readBytes(getBodyStream(), true);
}
return this.bodyCache;
}
/**
* 获取指定名称的参数值
*
* @param name 参数名
* @return 参数值
* @since 5.5.8
*/
public Collection<String> getParams(final String name){
return getParams().get(name);
@Override
public MultipartFormData getMultipart() throws IORuntimeException {
if(null == this.multipartFormDataCache){
this.multipartFormDataCache = parseMultipart(new UploadSetting());
}
return this.multipartFormDataCache;
}
/**
* 获取参数Map
*
* @return 参数map
*/
@Override
public ListValueMap<String, String> getParams() {
if (null == this.paramsCache) {
this.paramsCache = new ListValueMap<>();
@@ -436,39 +265,4 @@ public class HttpServerRequest extends HttpServerBase {
ip = this.httpExchange.getRemoteAddress().getHostName();
return NetUtil.getMultistageReverseProxyIp(ip);
}
/**
* 获得MultiPart表单内容多用于获得上传的文件
*
* @return MultipartFormData
* @throws IORuntimeException IO异常
* @since 5.3.0
*/
public MultipartFormData getMultipart() throws IORuntimeException {
if(null == this.multipartFormDataCache){
this.multipartFormDataCache = parseMultipart(new UploadSetting());
}
return this.multipartFormDataCache;
}
/**
* 获得multipart/form-data 表单内容<br>
* 包括文件和普通表单数据<br>
* 在同一次请求中此方法只能被执行一次
*
* @param uploadSetting 上传文件的设定包括最大文件大小保存在内存的边界大小临时目录扩展名限定等
* @return MultiPart表单
* @throws IORuntimeException IO异常
* @since 5.3.0
*/
public MultipartFormData parseMultipart(final UploadSetting uploadSetting) throws IORuntimeException {
final MultipartFormData formData = new MultipartFormData(uploadSetting);
try {
formData.parseRequestStream(getBodyStream(), getCharset());
} catch (final IOException e) {
throw new IORuntimeException(e);
}
return formData;
}
}

View File

@@ -0,0 +1,227 @@
/*
* Copyright (c) 2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.hutool.http.server.engine.sun;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import org.dromara.hutool.core.io.IORuntimeException;
import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.http.meta.ContentType;
import org.dromara.hutool.http.meta.HttpStatus;
import org.dromara.hutool.http.server.ServerResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
/**
* SunHttp服务器响应对象
*
* @author looly
* @since 6.0.0
*/
public class SunServerResponse extends SunServerBase implements ServerResponse {
private Charset charset;
/**
* 是否已经发送了Http状态码如果没有提前写出状态码
*/
private boolean isSendCode;
/**
* 构造
*
* @param httpExchange {@link HttpExchange}
*/
public SunServerResponse(final HttpExchange httpExchange) {
super(httpExchange);
}
@Override
public SunServerResponse setStatus(final int statusCode) {
return send(statusCode, 0);
}
/**
* 发送成功状态码
*
* @return this
*/
public SunServerResponse sendOk() {
return setStatus(HttpStatus.HTTP_OK);
}
/**
* 发送成功状态码
*
* @param bodyLength 响应体长度默认0表示不定长度会输出Transfer-encoding: chunked
* @return this
* @since 5.5.7
*/
public SunServerResponse sendOk(final int bodyLength) {
return send(HttpStatus.HTTP_OK, bodyLength);
}
/**
* 发送404错误页
*
* @param content 错误页页面内容默认text/html类型
* @return this
*/
public SunServerResponse send404(final String content) {
return sendError(HttpStatus.HTTP_NOT_FOUND, content);
}
/**
* 发送错误页
*
* @param errorCode HTTP错误状态码见HttpStatus
* @param content 错误页页面内容默认text/html类型
* @return this
*/
@SuppressWarnings("resource")
public SunServerResponse sendError(final int errorCode, final String content) {
setStatus(errorCode);
setContentType(ContentType.TEXT_HTML.toString());
write(content);
return this;
}
/**
* 发送HTTP状态码
*
* @param httpStatusCode HTTP状态码见HttpStatus
* @param bodyLength 响应体长度默认0表示不定长度会输出Transfer-encoding: chunked
* @return this
*/
public SunServerResponse send(final int httpStatusCode, final long bodyLength) {
if (this.isSendCode) {
throw new IORuntimeException("Http status code has been send!");
}
try {
this.httpExchange.sendResponseHeaders(httpStatusCode, bodyLength);
} catch (final IOException e) {
throw new IORuntimeException(e);
}
this.isSendCode = true;
return this;
}
@Override
public SunServerResponse setCharset(final Charset charset) {
this.charset = charset;
return this;
}
@Override
public Charset getCharset() {
return this.charset;
}
/**
* 获得所有响应头,获取后可以添加新的响应头
*
* @return 响应头
*/
public Headers getHeaders() {
return this.httpExchange.getResponseHeaders();
}
@Override
public SunServerResponse addHeader(final String header, final String value) {
getHeaders().add(header, value);
return this;
}
@Override
public SunServerResponse setHeader(final String header, final String value) {
getHeaders().set(header, value);
return this;
}
/**
* 设置响应头,如果已经存在,则覆盖
*
* @param header 头key
* @param value 值列表
* @return this
*/
public SunServerResponse setHeader(final String header, final List<String> value) {
getHeaders().put(header, value);
return this;
}
/**
* 设置所有响应头,如果已经存在,则覆盖
*
* @param headers 响应头map
* @return this
*/
public SunServerResponse setHeaders(final Map<String, List<String>> headers) {
getHeaders().putAll(headers);
return this;
}
/**
* 设置属性
*
* @param name 属性名
* @param value 属性值
* @return this
*/
public SunServerResponse setAttr(final String name, final Object value) {
this.httpExchange.setAttribute(name, value);
return this;
}
@SuppressWarnings("resource")
@Override
public OutputStream getOutputStream() {
if (!this.isSendCode) {
sendOk();
}
return this.httpExchange.getResponseBody();
}
/**
* 写出数据到客户端
*
* @param in 数据流
* @param length 指定响应内容长度默认0表示不定长度会输出Transfer-encoding: chunked
* @return this
*/
@SuppressWarnings("resource")
public SunServerResponse write(final InputStream in, final int length) {
if (!isSendCode) {
sendOk(Math.max(0, length));
}
OutputStream out = null;
try {
out = this.httpExchange.getResponseBody();
IoUtil.copy(in, out);
} finally {
IoUtil.closeQuietly(out);
IoUtil.closeQuietly(in);
}
return this;
}
}

View File

@@ -14,12 +14,12 @@
* limitations under the License.
*/
package org.dromara.hutool.http.server.filter;
package org.dromara.hutool.http.server.engine.sun.filter;
import org.dromara.hutool.core.exception.ExceptionUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.http.server.HttpServerRequest;
import org.dromara.hutool.http.server.HttpServerResponse;
import org.dromara.hutool.http.server.engine.sun.SunServerRequest;
import org.dromara.hutool.http.server.engine.sun.SunServerResponse;
/**
* 默认异常处理拦截器
@@ -31,7 +31,7 @@ public class DefaultExceptionFilter extends ExceptionFilter{
private final static String TEMPLATE_ERROR = "<!DOCTYPE html><html><head><title>Hutool - Error report</title><style>h1,h3 {color:white; background-color: gray;}</style></head><body><h1>HTTP Status {} - {}</h1><hr size=\"1\" noshade=\"noshade\" /><p>{}</p><hr size=\"1\" noshade=\"noshade\" /><h3>Hutool</h3></body></html>";
@Override
public void afterException(final HttpServerRequest req, final HttpServerResponse res, final Throwable e) {
public void afterException(final SunServerRequest req, final SunServerResponse res, final Throwable e) {
String content = ExceptionUtil.stacktraceToString(e);
content = content.replace("\n", "<br/>\n");
content = StrUtil.format(TEMPLATE_ERROR, 500, req.getURI(), content);

View File

@@ -14,11 +14,11 @@
* limitations under the License.
*/
package org.dromara.hutool.http.server.filter;
package org.dromara.hutool.http.server.engine.sun.filter;
import com.sun.net.httpserver.Filter;
import org.dromara.hutool.http.server.HttpServerRequest;
import org.dromara.hutool.http.server.HttpServerResponse;
import org.dromara.hutool.http.server.engine.sun.SunServerRequest;
import org.dromara.hutool.http.server.engine.sun.SunServerResponse;
/**
* 异常处理过滤器
@@ -28,9 +28,9 @@ import org.dromara.hutool.http.server.HttpServerResponse;
public abstract class ExceptionFilter implements HttpFilter {
@Override
public void doFilter(final HttpServerRequest req, final HttpServerResponse res, final Filter.Chain chain) {
public void doFilter(final SunServerRequest req, final SunServerResponse res, final Filter.Chain chain) {
try {
chain.doFilter(req.getHttpExchange());
chain.doFilter(req.getExchange());
} catch (final Throwable e) {
afterException(req, res, e);
}
@@ -39,9 +39,9 @@ public abstract class ExceptionFilter implements HttpFilter {
/**
* 异常之后的处理逻辑
*
* @param req {@link HttpServerRequest}
* @param res {@link HttpServerResponse}
* @param req {@link SunServerRequest}
* @param res {@link SunServerResponse}
* @param e 异常
*/
public abstract void afterException(final HttpServerRequest req, final HttpServerResponse res, final Throwable e);
public abstract void afterException(final SunServerRequest req, final SunServerResponse res, final Throwable e);
}

View File

@@ -14,11 +14,11 @@
* limitations under the License.
*/
package org.dromara.hutool.http.server.filter;
package org.dromara.hutool.http.server.engine.sun.filter;
import org.dromara.hutool.http.server.HttpServerRequest;
import org.dromara.hutool.http.server.HttpServerResponse;
import com.sun.net.httpserver.Filter;
import org.dromara.hutool.http.server.engine.sun.SunServerRequest;
import org.dromara.hutool.http.server.engine.sun.SunServerResponse;
import java.io.IOException;
@@ -33,10 +33,10 @@ public interface HttpFilter {
/**
* 执行过滤
* @param req {@link HttpServerRequest} 请求对象用于获取请求内容
* @param res {@link HttpServerResponse} 响应对象用于写出内容
* @param req {@link SunServerRequest} 请求对象用于获取请求内容
* @param res {@link SunServerResponse} 响应对象用于写出内容
* @param chain {@link Filter.Chain}
* @throws IOException IO异常
*/
void doFilter(HttpServerRequest req, HttpServerResponse res, Filter.Chain chain) throws IOException;
void doFilter(SunServerRequest req, SunServerResponse res, Filter.Chain chain) throws IOException;
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.dromara.hutool.http.server.filter;
package org.dromara.hutool.http.server.engine.sun.filter;
import com.sun.net.httpserver.Filter;

View File

@@ -17,4 +17,4 @@
/**
* {@link com.sun.net.httpserver.Filter} 实现包装
*/
package org.dromara.hutool.http.server.filter;
package org.dromara.hutool.http.server.engine.sun.filter;

View File

@@ -0,0 +1,22 @@
/*
* Copyright (c) 2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* {@link com.sun.net.httpserver.HttpServer}引擎实现包
*
* @author Looly
*/
package org.dromara.hutool.http.server.engine.sun;

View File

@@ -0,0 +1,22 @@
/*
* Copyright (c) 2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Tomcat引擎实现
*
* @author Looly
*/
package org.dromara.hutool.http.server.engine.tomcat;

View File

@@ -0,0 +1,82 @@
/*
* Copyright (c) 2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.hutool.http.server.engine.undertow;
import io.undertow.Undertow;
import org.dromara.hutool.core.lang.Assert;
import org.dromara.hutool.http.server.engine.AbstractServerEngine;
import javax.net.ssl.SSLContext;
/**
* Undertow引擎实现
*
* @author looly
*/
public class UndertowEngine extends AbstractServerEngine {
private Undertow undertow;
/**
* 构造
*/
public UndertowEngine() {
// issue#IABWBL JDK8下在IDEA旗舰版加载Spring boot插件时启动应用不会检查字段类是否存在
// 此处构造时调用下这个类,以便触发类是否存在的检查
Assert.notNull(Undertow.class);
}
@Override
public void start() {
initEngine();
undertow.start();
}
@Override
public Undertow getRawEngine() {
return this.undertow;
}
@Override
protected void reset() {
if(null != this.undertow){
this.undertow.stop();
this.undertow = null;
}
}
@Override
protected void initEngine() {
if (null != this.undertow) {
return;
}
final Undertow.Builder builder = Undertow.builder()
.setHandler(exchange -> {
this.handler.handle(
new UndertowRequest(exchange),
new UndertowResponse(exchange));
});
final SSLContext sslContext = this.config.getSslContext();
if(null != sslContext){
builder.addHttpsListener(this.config.getPort(), this.config.getHost(), sslContext);
}else{
builder.addHttpListener(this.config.getPort(), this.config.getHost());
}
this.undertow = builder.build();
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.hutool.http.server.engine.undertow;
import io.undertow.server.HttpServerExchange;
/**
* Undertow请求对象基类用于获取原始Undertow请求对象
*
* @author looly
* @since 6.0.0
*/
public class UndertowExchangeBase {
final HttpServerExchange exchange;
/**
* 构造
*
* @param exchange Undertow请求对象
*/
public UndertowExchangeBase(final HttpServerExchange exchange) {
this.exchange = exchange;
}
/**
* 获取原始Undertow请求对象
*
* @return 原始Undertow请求对象
*/
public HttpServerExchange getExchange(){
return this.exchange;
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright (c) 2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.hutool.http.server.engine.undertow;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.HeaderMap;
import org.dromara.hutool.core.util.CharsetUtil;
import org.dromara.hutool.http.server.ServerRequest;
import java.io.InputStream;
import java.nio.charset.Charset;
/**
* Undertow请求对象
*
* @author looly
* @since 6.0.0
*/
public class UndertowRequest extends UndertowExchangeBase implements ServerRequest {
/**
* 构造
*
* @param exchange Undertow请求对象
*/
public UndertowRequest(final HttpServerExchange exchange) {
super(exchange);
}
@Override
public String getMethod() {
return this.exchange.getRequestMethod().toString();
}
@Override
public String getPath() {
return this.exchange.getRelativePath();
}
@Override
public String getQuery() {
return this.exchange.getQueryString();
}
@Override
public String getHeader(final String name) {
return this.exchange.getRequestHeaders().getFirst(name);
}
/**
* 获取所有请求头
*
* @return 请求头
*/
public HeaderMap getHeaders() {
return this.exchange.getRequestHeaders();
}
@Override
public Charset getCharset() {
return CharsetUtil.charset(this.exchange.getRequestCharset());
}
@Override
public InputStream getBodyStream() {
return this.exchange.getInputStream();
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright (c) 2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.hutool.http.server.engine.undertow;
import io.undertow.io.Sender;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.HttpString;
import org.dromara.hutool.http.server.ServerResponse;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.List;
/**
* Undertow响应对象
*
* @author looly
* @since 6.0.0
*/
public class UndertowResponse extends UndertowExchangeBase implements ServerResponse {
private Charset charset;
/**
* 构造
*
* @param exchange Undertow exchange
*/
public UndertowResponse(final HttpServerExchange exchange) {
super(exchange);
this.charset = DEFAULT_CHARSET;
}
/**
* 获取原始的HttpServerExchange对象
*
* @return HttpServerExchange对象
*/
public HttpServerExchange getExchange() {
return exchange;
}
@Override
public ServerResponse setStatus(final int statusCode) {
this.exchange.setStatusCode(statusCode);
return this;
}
@Override
public ServerResponse setCharset(final Charset charset) {
this.charset = charset;
return this;
}
@Override
public Charset getCharset() {
return this.charset;
}
@Override
public ServerResponse addHeader(final String header, final String value) {
this.exchange.getResponseHeaders().add(new HttpString(header), value);
return this;
}
@Override
public ServerResponse setHeader(final String header, final String value) {
this.exchange.getResponseHeaders().put(new HttpString(header), value);
return this;
}
@Override
public ServerResponse setHeader(final String header, final List<String> value) {
this.exchange.getResponseHeaders().putAll(new HttpString(header), value);
return this;
}
/**
* 获取Sender对象用于发送数据
*
* @return Sender对象
*/
public Sender getSender() {
return this.exchange.getResponseSender();
}
@Override
public OutputStream getOutputStream() {
return this.exchange.getOutputStream();
}
@Override
public ServerResponse write(final byte[] data) {
getSender().send(ByteBuffer.wrap(data));
return this;
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) 2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Undertow引擎实现
*
* @author Looly
* @since 6.0.0
*/
package org.dromara.hutool.http.server.engine.undertow;

View File

@@ -1,63 +0,0 @@
/*
* Copyright (c) 2013-2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.hutool.http.server.handler;
import org.dromara.hutool.http.server.HttpExchangeWrapper;
import org.dromara.hutool.http.server.HttpServerRequest;
import org.dromara.hutool.http.server.HttpServerResponse;
import org.dromara.hutool.http.server.action.Action;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
/**
* Action处理器用于将HttpHandler转换为Action形式
*
* @author looly
* @since 5.2.6
*/
public class ActionHandler implements HttpHandler {
private final Action action;
/**
* 构造
*
* @param action Action
*/
public ActionHandler(final Action action) {
this.action = action;
}
@Override
public void handle(final HttpExchange httpExchange) throws IOException {
final HttpServerRequest request;
final HttpServerResponse response;
if (httpExchange instanceof HttpExchangeWrapper) {
// issue#3343 当使用Filter时可能读取了请求参数此时使用共享的req和res可复用缓存
final HttpExchangeWrapper wrapper = (HttpExchangeWrapper) httpExchange;
request = wrapper.getRequest();
response = wrapper.getResponse();
} else {
request = new HttpServerRequest(httpExchange);
response = new HttpServerResponse(httpExchange);
}
action.doAction(request, response);
httpExchange.close();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2013-2024 Hutool Team and hutool.cn
* Copyright (c) 2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,29 +14,23 @@
* limitations under the License.
*/
package org.dromara.hutool.http.server.action;
package org.dromara.hutool.http.server.handler;
import org.dromara.hutool.http.server.HttpServerRequest;
import org.dromara.hutool.http.server.HttpServerResponse;
import java.io.IOException;
import org.dromara.hutool.http.server.ServerRequest;
import org.dromara.hutool.http.server.ServerResponse;
/**
* 请求处理接口<br>
* 当用户请求某个Path则调用相应Action的doAction方法
* HTTP请求处理器
*
* @author Looly
* @since 5.2.6
*/
@FunctionalInterface
public interface Action {
public interface HttpHandler {
/**
* 处理请求
*
* @param request 请求对象
* @param response 响应对象
* @throws IOException IO异常
*/
void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException;
void handle(ServerRequest request, ServerResponse response);
}

View File

@@ -14,12 +14,13 @@
* limitations under the License.
*/
package org.dromara.hutool.http.server.action;
package org.dromara.hutool.http.server.handler;
import org.dromara.hutool.core.collection.ListUtil;
import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.http.server.HttpServerRequest;
import org.dromara.hutool.http.server.HttpServerResponse;
import org.dromara.hutool.http.meta.HttpStatus;
import org.dromara.hutool.http.server.ServerRequest;
import org.dromara.hutool.http.server.ServerResponse;
import java.io.File;
import java.util.List;
@@ -30,8 +31,11 @@ import java.util.List;
* @author looly
* @since 5.2.6
*/
public class RootAction implements Action {
public class RootHandler implements HttpHandler {
/**
* 默认主页文件名
*/
public static final String DEFAULT_INDEX_FILE_NAME = "index.html";
private final File rootDir;
@@ -42,7 +46,7 @@ public class RootAction implements Action {
*
* @param rootDir 网页根目录
*/
public RootAction(final String rootDir) {
public RootHandler(final String rootDir) {
this(new File(rootDir));
}
@@ -51,7 +55,7 @@ public class RootAction implements Action {
*
* @param rootDir 网页根目录
*/
public RootAction(final File rootDir) {
public RootHandler(final File rootDir) {
this(rootDir, DEFAULT_INDEX_FILE_NAME);
}
@@ -61,7 +65,7 @@ public class RootAction implements Action {
* @param rootDir 网页根目录
* @param indexFileNames 主页文件名列表
*/
public RootAction(final String rootDir, final String... indexFileNames) {
public RootHandler(final String rootDir, final String... indexFileNames) {
this(new File(rootDir), indexFileNames);
}
@@ -72,13 +76,13 @@ public class RootAction implements Action {
* @param indexFileNames 主页文件名列表
* @since 5.4.0
*/
public RootAction(final File rootDir, final String... indexFileNames) {
public RootHandler(final File rootDir, final String... indexFileNames) {
this.rootDir = rootDir;
this.indexFileNames = ListUtil.of(indexFileNames);
}
@Override
public void doAction(final HttpServerRequest request, final HttpServerResponse response) {
public void handle(final ServerRequest request, final ServerResponse response) {
final String path = request.getPath();
File file = FileUtil.file(rootDir, path);
@@ -99,6 +103,7 @@ public class RootAction implements Action {
}
}
response.send404("404 Not Found !");
response.setStatus(HttpStatus.HTTP_NOT_FOUND);
response.write("404 Not Found !");
}
}

View File

@@ -16,17 +16,16 @@
package org.dromara.hutool.http.server;
import org.dromara.hutool.core.io.IORuntimeException;
import org.dromara.hutool.http.HttpUtil;
import org.dromara.hutool.http.server.filter.DefaultExceptionFilter;
import java.io.IOException;
import org.dromara.hutool.http.server.engine.sun.filter.DefaultExceptionFilter;
public class ExceptionServerTest {
public static void main(final String[] args) {
HttpUtil.createServer(8888)
.addFilter(new DefaultExceptionFilter())
.addAction("/", (req, res) -> {
throw new IOException("Test Exception");
throw new IORuntimeException("Test Exception");
})
.start();
}

View File

@@ -20,6 +20,7 @@ import org.dromara.hutool.core.date.DateUtil;
import org.dromara.hutool.core.lang.Console;
import org.dromara.hutool.core.map.multi.ListValueMap;
import org.dromara.hutool.http.HttpUtil;
import org.dromara.hutool.http.server.engine.sun.SimpleServer;
/**
* http://localhost:8888/?name=hutool
@@ -33,7 +34,7 @@ public class Issue3343Test {
Console.log(" > from : " + req.getClientIP());
// 过滤器中获取请求参数
Console.log(" > params : " + req.getParams());
chain.doFilter(req.getHttpExchange());
chain.doFilter(req.getExchange());
});
server.addAction("/", Issue3343Test::index);
@@ -41,9 +42,9 @@ public class Issue3343Test {
server.start();
}
private static void index(HttpServerRequest request, HttpServerResponse response) {
private static void index(final ServerRequest request, final ServerResponse response) {
// 具体逻辑中再次获取请求参数
ListValueMap<String, String> params = request.getParams();
final ListValueMap<String, String> params = request.getParams();
Console.log("index params: " + params);
response.getWriter().write("GOT: " + params);
}

View File

@@ -19,18 +19,19 @@ package org.dromara.hutool.http.server;
import org.dromara.hutool.core.data.id.IdUtil;
import org.dromara.hutool.http.HttpUtil;
import org.dromara.hutool.http.meta.ContentType;
import org.dromara.hutool.http.server.engine.sun.SimpleServer;
public class Issue3723Test {
public static void main(final String[] args) {
final SimpleServer server = HttpUtil.createServer(8888);
server.addFilter((req, res, chain) -> {
final String requestId = IdUtil.fastSimpleUUID();
req.getHttpExchange().setAttribute("requestId", requestId);
req.getExchange().setAttribute("requestId", requestId);
res.addHeader("X-Request-Id", requestId);
res.write("new Content");
chain.doFilter(req.getHttpExchange());
chain.doFilter(req.getExchange());
});
server.addAction("/", (req, res)-> res.write("Hello Hutool Server", ContentType.JSON.getValue()));
server.start();

View File

@@ -19,8 +19,10 @@ package org.dromara.hutool.http.server;
import org.dromara.hutool.core.date.DateUtil;
import org.dromara.hutool.core.date.StopWatch;
import org.dromara.hutool.core.lang.Console;
import org.dromara.hutool.http.multipart.MultipartFormData;
import org.dromara.hutool.http.HttpUtil;
import org.dromara.hutool.http.multipart.MultipartFormData;
import org.dromara.hutool.http.server.engine.sun.SimpleServer;
import org.dromara.hutool.http.server.engine.sun.SunServerRequest;
import java.util.concurrent.TimeUnit;
@@ -35,7 +37,7 @@ public class IssueI6Q30XTest {
public static void main(String[] args) {
final SimpleServer server = HttpUtil.createServer(8888);
server.addAction("/file", (request, response) -> {
Console.log(request.getHeaders().entrySet());
Console.log(((SunServerRequest)request).getHeaders().entrySet());
final StopWatch stopWatch = DateUtil.createStopWatch();
stopWatch.start();

View File

@@ -19,25 +19,26 @@ package org.dromara.hutool.http.server;
import org.dromara.hutool.core.lang.Console;
import org.dromara.hutool.http.HttpUtil;
import org.dromara.hutool.http.meta.HeaderName;
import org.dromara.hutool.http.meta.HttpStatus;
public class RedirectServerTest {
public static void main(final String[] args) {
HttpUtil.createServer(8888).addAction("/redirect1", (request, response) -> {
response.addHeader(HeaderName.LOCATION.getValue(),"http://localhost:8888/redirect2");
response.addHeader(HeaderName.SET_COOKIE.getValue(),"redirect1=1; path=/; HttpOnly");
response.send(301);
response.setStatus(301);
}).addAction("/redirect2", (request, response) -> {
response.addHeader(HeaderName.LOCATION.getValue(),"http://localhost:8888/redirect3");
response.addHeader(HeaderName.SET_COOKIE.getValue(), "redirect2=2; path=/; HttpOnly");
response.send(301);
response.setStatus(301);
}).addAction("/redirect3", (request, response) -> {
response.addHeader(HeaderName.LOCATION.getValue(),"http://localhost:8888/redirect4");
response.addHeader(HeaderName.SET_COOKIE.getValue(),"redirect3=3; path=/; HttpOnly");
response.send(301);
response.setStatus(301);
}).addAction("/redirect4", (request, response) -> {
final String cookie = request.getHeader(HeaderName.COOKIE);
Console.log(cookie);
response.sendOk();
response.setStatus(HttpStatus.HTTP_OK);
}).start();
}
}

View File

@@ -19,10 +19,11 @@ package org.dromara.hutool.http.server;
import org.dromara.hutool.core.collection.ListUtil;
import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.lang.Console;
import org.dromara.hutool.http.multipart.UploadFile;
import org.dromara.hutool.http.HttpUtil;
import org.dromara.hutool.http.meta.ContentType;
import org.dromara.hutool.http.meta.HeaderName;
import org.dromara.hutool.http.HttpUtil;
import org.dromara.hutool.http.multipart.UploadFile;
import org.dromara.hutool.http.server.engine.sun.SunServerRequest;
import org.dromara.hutool.json.JSONUtil;
import java.net.HttpCookie;
@@ -33,13 +34,13 @@ public class SimpleServerTest {
HttpUtil.createServer(8888)
.addFilter(((req, res, chain) -> {
Console.log("Filter: " + req.getPath());
chain.doFilter(req.getHttpExchange());
chain.doFilter(req.getExchange());
}))
// 设置默认根目录classpath/html
.setRoot(FileUtil.file("html"))
// get数据测试返回请求的PATH
.addAction("/get", (request, response) ->
response.write(request.getURI().toString(), ContentType.TEXT_PLAIN.toString())
response.write(((SunServerRequest)request).getURI().toString(), ContentType.TEXT_PLAIN.toString())
)
// 返回JSON数据测试
.addAction("/restTest", (request, response) -> {

View File

@@ -0,0 +1,33 @@
/*
* Copyright (c) 2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.hutool.http.server.engine;
import org.dromara.hutool.core.lang.Console;
import org.dromara.hutool.http.server.ServerConfig;
import org.dromara.hutool.http.server.engine.undertow.UndertowEngine;
public class UndertowTest {
public static void main(String[] args) {
final UndertowEngine undertowEngine = new UndertowEngine();
undertowEngine.init(ServerConfig.of());
undertowEngine.setHandler((request, response) -> {
Console.log(request.getPath());
response.write("Hutool Undertow response test");
});
undertowEngine.start();
}
}