From fd2cb0112633b383c19538ef91320986dfa872fa Mon Sep 17 00:00:00 2001 From: yzj Date: Wed, 22 Oct 2025 11:57:45 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E6=96=B0=E5=A2=9E=20QuarkusHandler?= =?UTF-8?q?=20=E5=AF=B9=E8=B1=A1=EF=BC=8C=E6=96=B9=E4=BE=BF=E6=AD=A3?= =?UTF-8?q?=E5=B8=B8=E8=BF=90=E8=A1=8C=E8=B0=83=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- solon-ai-in-quarkus/pom.xml | 6 + .../mcpserver/DynamicRoutingFilter.java | 131 ------ .../webapp/mcpserver/McpServerConfig.java | 14 +- .../java/webapp/mcpserver/PathMatcher.java | 46 -- .../mcpserver/handle/QuarkusContext.java | 445 ++++++++++++++++++ .../mcpserver/handle/QuarkusHandler.java | 118 +++++ 6 files changed, 575 insertions(+), 185 deletions(-) delete mode 100644 solon-ai-in-quarkus/src/main/java/webapp/mcpserver/DynamicRoutingFilter.java delete mode 100644 solon-ai-in-quarkus/src/main/java/webapp/mcpserver/PathMatcher.java create mode 100644 solon-ai-in-quarkus/src/main/java/webapp/mcpserver/handle/QuarkusContext.java create mode 100644 solon-ai-in-quarkus/src/main/java/webapp/mcpserver/handle/QuarkusHandler.java diff --git a/solon-ai-in-quarkus/pom.xml b/solon-ai-in-quarkus/pom.xml index f5ba0b1..72f835c 100644 --- a/solon-ai-in-quarkus/pom.xml +++ b/solon-ai-in-quarkus/pom.xml @@ -66,6 +66,12 @@ org.noear solon-web-vertx + + + io.vertx + vertx-core + + diff --git a/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/DynamicRoutingFilter.java b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/DynamicRoutingFilter.java deleted file mode 100644 index 75ed001..0000000 --- a/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/DynamicRoutingFilter.java +++ /dev/null @@ -1,131 +0,0 @@ -package webapp.mcpserver; - -import io.quarkus.arc.Arc; -import io.quarkus.arc.InstanceHandle; -import io.quarkus.arc.ManagedContext; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.RoutingContext; -import jakarta.inject.Inject; -import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.container.ContainerRequestFilter; -import jakarta.ws.rs.container.PreMatching; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.ext.Provider; -import org.noear.solon.web.vertx.VxWebHandler; - -import java.io.IOException; -import java.lang.reflect.Method; - -@Provider -@PreMatching // 必须使用这个标记,不然不存在的路径无法执行,直接会被拦截 -public class DynamicRoutingFilter implements ContainerRequestFilter { - - - @Inject - RoutingContext routingContext; - - @Inject - Router router; - - @Inject - VxWebHandler handler; - - - @Inject - HttpServerRequest request; - - @Override - public void filter(ContainerRequestContext ctx) throws IOException { -// -// System.out.println(request.params()); -// System.out.println(request.headers()); -// -// System.out.println(request.body()); - - - String realPath = ctx.getUriInfo().getPath(); -// String method = ctx.getMethod(); - String patternPath = "/api/hello/:name"; - -// router.routeWithRegex("/mcp/.*").handler(req -> { -// // 获取上下文 -// ManagedContext requestContext = Arc.container().requestContext(); -// // 激活上下文 -// requestContext.activate(); -// handler.handle(req.request()); -// }); - -// router.route(patternPath).handler(rc -> { -// // 这个的目的是激活 router 对象,当第二次进入的时候,就可以获取到实际的动态 name 了,算是一个bug,所以还是需要直接用 request 直接获取参数即可 -// rc.next(); -// }); - -// if (realPath.contains("mcp")){ -// // 获取请求上下文 -// ManagedContext requestContext = Arc.container().requestContext(); -// // 激活上下文 -// requestContext.activate(); -// handler.handle(request); -// } - - // 如果符合规则的话,则会触发实现 - if(PathMatcher.isMatch(patternPath,realPath)){ - String target = "org.noear.quarkus.path.HelloNamePath#hello"; - // 解析 target e.g. "com.example.Hello#hello" - String[] parts = target.split("#"); - String className = parts[0]; - String methodName = parts[1]; - - try { - Class cls = Class.forName(className, false, Thread.currentThread().getContextClassLoader()); - - Object bean = null; - try { - InstanceHandle handle = Arc.container().instance(cls); - if (handle != null && handle.isAvailable()) { - bean = handle.get(); - } - } catch (Exception ignored) {} - - // 优先尝试接收 ContainerRequestContext - try { - // 这个就是对应的参数对象方法获取了 - Method m = cls.getMethod(methodName, RoutingContext.class); - // 这个就能触发内部方法,并且quarkus的相应注入对象,就能获取到, 其他拦截前的响应都会失效,但是通过 ctx.abortWith 即可触发 quarkus 自带的一系列后置响应拦截实现 - Object ret = m.invoke(bean, routingContext); - // 如果方法自己写响应可以返回 null 或 void —— 你需要决定如何判断 - if (ret instanceof Response) { - ctx.abortWith((Response) ret); - } else if (ret instanceof String) { - ctx.abortWith(Response.ok(ret).build()); - } else { - ctx.abortWith(Response.noContent().build()); - } - return; - } catch (NoSuchMethodException ex) { - // 尝试无参方法 - Method m = cls.getMethod(methodName); - Object ret = m.invoke(bean); - if (ret instanceof Response) { - ctx.abortWith((Response) ret); - } else if (ret instanceof String) { - ctx.abortWith(Response.ok(ret).build()); - } else { - ctx.abortWith(Response.noContent().build()); - } - return; - } - } catch (Throwable t) { - // 出错返回 500 - ctx.abortWith(Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity("dynamic route invoke error: " + t.getMessage()).build()); - } - } - - - - - - } -} diff --git a/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/McpServerConfig.java b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/McpServerConfig.java index d4345f2..339b7c8 100644 --- a/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/McpServerConfig.java +++ b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/McpServerConfig.java @@ -21,6 +21,7 @@ import org.noear.solon.ai.mcp.server.annotation.McpServerEndpoint; import org.noear.solon.ai.mcp.server.prompt.MethodPromptProvider; import org.noear.solon.ai.mcp.server.resource.MethodResourceProvider; import org.noear.solon.web.vertx.VxWebHandler; +import webapp.mcpserver.handle.QuarkusHandler; import webapp.mcpserver.tool.McpServerTool2; import java.util.Set; @@ -41,21 +42,18 @@ public class McpServerConfig extends AbstractVerticle { @Inject - VxWebHandler handler; + QuarkusHandler handler; @Inject BeanManager beanManager; // 注入BeanManager @Produces @ApplicationScoped - public VxWebHandler handler() { - System.out.println("=== VxWebHandler ==="); - return new VxWebHandler(); + public QuarkusHandler handler() { + System.out.println("=== QuarkusHandler ==="); + return new QuarkusHandler(); } - public McpServerConfig() { - // this.handler = new VxWebHandler(); - } @PostConstruct @Override @@ -71,7 +69,7 @@ public class McpServerConfig extends AbstractVerticle { app.filter(new McpServerAuth()); }); - //手动构建 mcp 服务端点(只是演示,可以去掉) + //手动构建 mcp 服务端点(只是演示,可以去掉) -- 目前该模块未打通 quarkus 框架上运行的案例,请使用 quarkusCom2Endpoint() 的方式构建即可 McpServerEndpointProvider endpointProvider = McpServerEndpointProvider.builder() .name("McpServerTool2") .channel(McpChannel.SSE) diff --git a/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/PathMatcher.java b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/PathMatcher.java deleted file mode 100644 index 7720a21..0000000 --- a/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/PathMatcher.java +++ /dev/null @@ -1,46 +0,0 @@ -package webapp.mcpserver; - -public class PathMatcher { - public static boolean isMatch(String patternPath, String realPath) { - // 分割路径为片段(过滤空字符串) - String[] patternSegments = splitPath(patternPath); - String[] realSegments = splitPath(realPath); - - // 片段数量不同,直接不匹配 - if (patternSegments.length != realSegments.length) { - return false; - } - - // 逐个对比片段 - for (int i = 0; i < patternSegments.length; i++) { - String patternSeg = patternSegments[i]; - String realSeg = realSegments[i]; - - // 动态参数片段(:xxx)可匹配任意非空片段 - if (patternSeg.startsWith(":")) { - // 确保动态参数对应的值非空(根据业务需求可调整) - if (realSeg.isEmpty()) { - return false; - } - } else { - // 静态片段必须完全相等 - if (!patternSeg.equals(realSeg)) { - return false; - } - } - } - - return true; - } - - // 分割路径为片段,过滤空字符串(处理连续/或首尾/的情况) - private static String[] splitPath(String path) { - return path.split("/"); - } - - public static void main(String[] args) { - String path = "/api/hello/:name"; - String realPath = "/api/hello/MrYang"; - System.out.println(isMatch(path, realPath)); // 输出:true - } -} diff --git a/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/handle/QuarkusContext.java b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/handle/QuarkusContext.java new file mode 100644 index 0000000..e8f8b57 --- /dev/null +++ b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/handle/QuarkusContext.java @@ -0,0 +1,445 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package webapp.mcpserver.handle; + +import io.netty.buffer.ByteBufInputStream; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.http.impl.CookieImpl; +import org.noear.solon.Utils; +import org.noear.solon.core.handle.ContextAsyncListener; +import org.noear.solon.core.handle.Cookie; +import org.noear.solon.core.handle.UploadedFile; +import org.noear.solon.core.util.IoUtil; +import org.noear.solon.core.util.MultiMap; +import org.noear.solon.server.ServerProps; +import org.noear.solon.server.handle.AsyncContextState; +import org.noear.solon.server.handle.ContextBase; +import org.noear.solon.server.util.DecodeUtils; +import org.noear.solon.server.util.RedirectUtils; +import org.noear.solon.web.vertx.RequestInputStream; +import org.noear.solon.web.vertx.ResponseOutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.net.URI; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; + +public class QuarkusContext extends ContextBase { + static final Logger log = LoggerFactory.getLogger(QuarkusContext.class); + private HttpServerRequest _request; + private HttpServerResponse _response; + private Buffer _requestBody; + private boolean _loadMultipartFormData = false; + private URI _uri; + private String _url; + private long contentLength = -2L; + private InputStream bodyAsStream; + private MultiMap _paramMap; + private MultiMap _cookieMap; + private MultiMap _headerMap; + private ResponseOutputStream responseOutputStream; + private ByteArrayOutputStream _outputStreamTmp; + private int _status = 200; + private boolean _headers_sent = false; + private boolean _allows_write = true; + protected final AsyncContextState asyncState = new AsyncContextState(); + + protected HttpServerRequest innerGetRequest() { + return this._request; + } + + public HttpServerResponse innerGetResponse() { + return this._response; + } + + public QuarkusContext(HttpServerRequest request, Buffer requestBody) { + this._request = request; + this._requestBody = requestBody; + this._response = request.response(); + } + + private void loadMultipartFormData() { + if (!this._loadMultipartFormData) { + this._loadMultipartFormData = true; + if (this.isMultipartFormData()) { + DecodeUtils.decodeMultipart(this, new ByteBufInputStream(this._requestBody.getByteBuf()), this._fileMap); + } + + } + } + + public boolean isHeadersSent() { + return this._headers_sent; + } + + public Object request() { + return this._request; + } + + public String remoteIp() { + return this._request.remoteAddress().host(); + } + + public int remotePort() { + return this._request.remoteAddress().port(); + } + + public String method() { + return this._request.method().name(); + } + + public String protocol() { + return "http"; + } + + public URI uri() { + if (this._uri == null) { + this._uri = this.parseURI(this.url()); + } + + return this._uri; + } + + public boolean isSecure() { + return false; + } + + public String url() { + if (this._url == null) { + String tmp = this._request.absoluteURI(); + int idx = tmp.indexOf(63); + if (idx < 0) { + this._url = tmp; + } else { + this._url = tmp.substring(0, idx); + } + } + + return this._url; + } + + public long contentLength() { + if (this.contentLength < -1L) { + this.contentLength = DecodeUtils.decodeContentLengthLong(this); + } + + return this.contentLength; + } + + public String queryString() { + return this._request.query(); + } + + public InputStream bodyAsStream() throws IOException { + if (this.bodyAsStream != null) { + return this.bodyAsStream; + } else { + if (this._requestBody == null) { + this.bodyAsStream = new ByteArrayInputStream(new byte[0]); + } else { + this.bodyAsStream = new RequestInputStream(this._requestBody.getByteBuf(), ServerProps.request_maxBodySize); + } + + return this.bodyAsStream; + } + } + + public String body(String charset) throws IOException { + try { + return super.body(charset); + } catch (Exception var3) { + Exception e = var3; + throw DecodeUtils.status4xx(this, e); + } + } + + public MultiMap paramMap() { + this.paramsMapInit(); + return this._paramMap; + } + + private void paramsMapInit() { + if (this._paramMap == null) { + this._paramMap = new MultiMap(); + + try { + DecodeUtils.decodeFormUrlencoded(this, false); + if (this.autoMultipart()) { + this.loadMultipartFormData(); + } + + Iterator var4 = this._request.params().iterator(); + + Map.Entry kv; + while(var4.hasNext()) { + kv = (Map.Entry)var4.next(); + this._paramMap.add((String)kv.getKey(), (String) kv.getValue()); + } + + var4 = this._request.formAttributes().iterator(); + + while(var4.hasNext()) { + kv = (Map.Entry)var4.next(); + this._paramMap.add((String)kv.getKey(), (String) kv.getValue()); + } + } catch (Exception var3) { + Exception e = var3; + throw DecodeUtils.status4xx(this, e); + } + } + + } + + public MultiMap fileMap() { + if (this.isMultipartFormData()) { + this.loadMultipartFormData(); + } + + return this._fileMap; + } + + public MultiMap cookieMap() { + if (this._cookieMap == null) { + this._cookieMap = new MultiMap(false); + DecodeUtils.decodeCookies(this, this.header("Cookie")); + } + + return this._cookieMap; + } + + public MultiMap headerMap() { + if (this._headerMap == null) { + this._headerMap = new MultiMap(); + Iterator var1 = this._request.headers().iterator(); + + while(var1.hasNext()) { + Map.Entry kv = (Map.Entry)var1.next(); + this._headerMap.add((String)kv.getKey(), kv.getValue()); + } + } + + return this._headerMap; + } + + public Object response() { + return this._response; + } + + protected void contentTypeDoSet(String contentType) { + if (this.charset != null && contentType != null && contentType.length() > 0 && contentType.indexOf(";") < 0) { + this.headerSet("Content-Type", contentType + ";charset=" + this.charset); + } else { + this.headerSet("Content-Type", contentType); + } + } + + private ResponseOutputStream responseOutputStream() { + if (this.responseOutputStream == null) { + this.responseOutputStream = new ResponseOutputStream(this._response, 512); + } + + return this.responseOutputStream; + } + + public OutputStream outputStream() throws IOException { + this.sendHeaders(false); + if (this._allows_write) { + return this.responseOutputStream(); + } else { + if (this._outputStreamTmp == null) { + this._outputStreamTmp = new ByteArrayOutputStream(); + } else { + this._outputStreamTmp.reset(); + } + + return this._outputStreamTmp; + } + } + + public void output(byte[] bytes) { + try { + OutputStream out = this.outputStream(); + if (this._allows_write) { + out.write(bytes); + } + } catch (Throwable var3) { + Throwable ex = var3; + throw new RuntimeException(ex); + } + } + + public void output(InputStream stream) { + try { + OutputStream out = this.outputStream(); + if (this._allows_write) { + IoUtil.transferTo(stream, out); + } + } catch (Throwable var3) { + Throwable ex = var3; + throw new RuntimeException(ex); + } + } + + public void headerSet(String name, String val) { + this._response.headers().set(name, val); + } + + public void headerAdd(String name, String val) { + this._response.headers().add(name, val); + } + + public String headerOfResponse(String name) { + return this._response.headers().get(name); + } + + public Collection headerValuesOfResponse(String name) { + return this._response.headers().getAll(name); + } + + public Collection headerNamesOfResponse() { + return this._response.headers().names(); + } + + public void cookieSet(Cookie cookie) { + CookieImpl c = new CookieImpl(cookie.name, cookie.value); + if (cookie.maxAge >= 0) { + c.setMaxAge((long)cookie.maxAge); + } + + if (Utils.isNotEmpty(cookie.domain)) { + c.setDomain(cookie.domain); + } + + if (Utils.isNotEmpty(cookie.path)) { + c.setPath(cookie.path); + } + + c.setSecure(cookie.secure); + c.setHttpOnly(cookie.httpOnly); + this._response.addCookie(c); + } + + public void redirect(String url, int code) { + url = RedirectUtils.getRedirectPath(url); + this.headerSet("Location", url); + this.statusDoSet(code); + } + + public int status() { + return this._status; + } + + protected void statusDoSet(int status) { + this._status = status; + } + + public void contentLength(long size) { + if (!this._headers_sent) { + this._response.putHeader("Content-Length", String.valueOf(size)); + } + + } + + public void flush() throws IOException { + if (this._allows_write) { + this.outputStream().flush(); + } + + } + + public void close() throws IOException { + this._response.close(); + } + + public void innerCommit() throws IOException { + try { + if (!this.getHandled() && this.status() < 200) { + this.status(404); + this.sendHeaders(true); + this.flush(); + this._response.send(); + } else { + this.sendHeaders(true); + this.flush(); + this._response.send(); + } + } finally { + if (!this._response.ended()) { + this._response.end(); + } + + } + + } + + private void sendHeaders(boolean isCommit) throws IOException { + if (!this._headers_sent) { + this._headers_sent = true; + if ("HEAD".equals(this.method())) { + this._allows_write = false; + } + + if (this.sessionState() != null) { + this.sessionState().sessionPublish(); + } + + this._response.setStatusCode(this.status()); + if (!isCommit && this._allows_write) { + if (!this._response.headers().contains("Content-Length")) { + this._response.setChunked(true); + } + } else { + this._response.setChunked(true); + } + } + + } + + public boolean asyncSupported() { + return true; + } + + public boolean asyncStarted() { + return this.asyncState.isStarted; + } + + public void asyncListener(ContextAsyncListener listener) { + this.asyncState.addListener(listener); + } + + public void asyncStart(long timeout, Runnable runnable) { + if (!this.asyncState.isStarted) { + this.asyncState.isStarted = true; + this.asyncState.asyncDelay(timeout, this, this::innerCommit); + if (runnable != null) { + runnable.run(); + } + + this.asyncState.onStart(this); + } + + } + + public void asyncComplete() { + if (this.asyncState.isStarted) { + try { + this.innerCommit(); + } catch (Throwable var5) { + Throwable e = var5; + log.warn("Async completion failed", e); + this.asyncState.onError(this, e); + } finally { + this.asyncState.onComplete(this); + } + } + + } +} diff --git a/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/handle/QuarkusHandler.java b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/handle/QuarkusHandler.java new file mode 100644 index 0000000..5209243 --- /dev/null +++ b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/handle/QuarkusHandler.java @@ -0,0 +1,118 @@ +package webapp.mcpserver.handle; + + +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; +import org.noear.solon.Solon; +import org.noear.solon.core.handle.Context; +import org.noear.solon.core.handle.Handler; +import org.noear.solon.lang.Nullable; +import org.noear.solon.web.vertx.VxHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; + +public class QuarkusHandler implements VxHandler { + static final Logger log = LoggerFactory.getLogger(QuarkusHandler.class); + @Nullable + private Executor executor; + @Nullable + private Handler handler; + + public QuarkusHandler() { + } + + protected void preHandle(Context ctx) throws IOException { + } + + public void setHandler(Handler handler) { + this.handler = handler; + } + + public void setExecutor(Executor executor) { + this.executor = executor; + } + + + public void handle(HttpServerRequest request) { + HttpServerResponse response = request.response(); + + try { + // GET 请求没有 body,直接处理 + if ("GET".equals(request.method().name())) { + this.handleDo(request, null, false); + } else { + // 对于非 GET 请求,我们使用一个变量来存储 body + // Buffer 可以动态增长,非常适合收集数据块 + Buffer body = Buffer.buffer(); + + // 1. 设置数据块处理器,用于接收 body 数据 + request.handler(body::appendBuffer); + + // 2. 设置请求结束处理器,在请求完全接收后(无论有无 body)触发 + request.endHandler(v -> { + // 请求已经结束,body 中包含了完整的请求体(如果有的话) + this.handleDo(request, body, true); + }); + + // (可选但推荐) 3. 设置异常处理器,防止客户端突然断开连接等问题 + request.exceptionHandler(ex -> { + log.error("Request processing failed", ex); + if (!response.ended()) { + response.setStatusCode(500).end("Request failed"); + } + }); + + } + } catch (Throwable var4) { + Throwable ex = var4; + log.warn(ex.getMessage(), ex); + if (!response.ended()) { + response.setStatusCode(500); + response.end(); + } + } + } + + private void handleDo(HttpServerRequest request, Buffer requestBody, boolean disPool) { + QuarkusContext ctx = new QuarkusContext(request, requestBody); + if (this.executor != null && !disPool) { + try { + this.executor.execute(() -> { + this.handle0(ctx); + }); + } catch (RejectedExecutionException var6) { + this.handle0(ctx); + } + } else { + this.handle0(ctx); + } + + } + + private void handle0(QuarkusContext ctx) { + try { + ctx.contentType("text/plain;charset=UTF-8"); + this.preHandle(ctx); + if (this.handler == null) { + Solon.app().tryHandle(ctx); + } else { + this.handler.handle(ctx); + } + + if (!ctx.asyncStarted()) { + ctx.innerCommit(); + } + } catch (Throwable var3) { + Throwable e = var3; + log.warn(e.getMessage(), e); + ctx.innerGetResponse().setStatusCode(500); + ctx.innerGetResponse().end(); + } + + } +} -- Gitee