霸王餐API接口Mock平台:基于Netty+Groovy脚本动态延迟响应
背景与目标
在“霸王餐”类App(如吃喝不愁)的联调与测试阶段,后端服务往往尚未就绪。为提升前端、测试及第三方对接效率,需构建一个高灵活度的Mock平台。该平台需支持:
- 动态路由匹配;
- Groovy脚本编写响应逻辑;
- 可配置延迟响应时间;
- 无需重启即时生效。
本文采用 Netty 作为网络层,结合 Groovy 脚本引擎实现上述能力。

整体架构
平台核心组件包括:
- Netty HTTP Server:接收并分发请求;
- ScriptManager:加载并缓存Groovy脚本;
- DelayController:解析延迟参数并控制响应时机;
- RouteMatcher:基于URI路径匹配对应脚本。
所有脚本存储于 scripts/ 目录,按路径命名,如 /api/order/detail.groovy。
Netty启动与HTTP处理器
package juwatech.cn.mock;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
public class MockServer {
public void start(int port) throws Exception {
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(
new HttpServerCodec(),
new HttpObjectAggregator(65536),
new MockHttpHandler()
);
}
});
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new MockServer().start(9090);
}
}
MockHttpHandler处理逻辑
package juwatech.cn.mock;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import juwatech.cn.script.ScriptManager;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class MockHttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private final ScriptManager scriptManager = new ScriptManager();
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) {
String uri = req.uri();
String scriptPath = "scripts" + uri + ".groovy";
// 支持 ?delay=2000 参数
int delayMs = parseDelay(req);
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(delayMs);
} catch (InterruptedException ignored) {}
return scriptManager.execute(scriptPath, req);
});
future.thenAccept(responseBody -> {
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.OK,
Unpooled.copiedBuffer(responseBody, java.nio.charset.StandardCharsets.UTF_8)
);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}).exceptionally(throwable -> {
FullHttpResponse error = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.INTERNAL_SERVER_ERROR,
Unpooled.copiedBuffer("Script error: " + throwable.getMessage(), java.nio.charset.StandardCharsets.UTF_8)
);
ctx.writeAndFlush(error).addListener(ChannelFutureListener.CLOSE);
return null;
});
}
private int parseDelay(FullHttpRequest req) {
QueryStringDecoder decoder = new QueryStringDecoder(req.uri());
var delays = decoder.parameters().get("delay");
if (delays != null && !delays.isEmpty()) {
try {
return Integer.parseInt(delays.get(0));
} catch (NumberFormatException ignored) {}
}
return 0;
}
}
Groovy脚本管理器
package juwatech.cn.script;
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import io.netty.handler.codec.http.FullHttpRequest;
import java.io.File;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ScriptManager {
private final Map<String, Class<?>> scriptCache = new ConcurrentHashMap<>();
private final GroovyShell shell = new GroovyShell();
public String execute(String scriptPath, FullHttpRequest request) {
try {
File scriptFile = new File(scriptPath);
if (!scriptFile.exists()) {
return "{\"error\":\"Script not found: " + scriptPath + "\"}";
}
Class<?> scriptClass = scriptCache.computeIfAbsent(scriptPath, k -> shell.parse(scriptFile));
Binding binding = new Binding();
binding.setVariable("request", request);
Object instance = scriptClass.getDeclaredConstructor().newInstance();
return (String) scriptClass.getMethod("handle", Binding.class).invoke(instance, binding);
} catch (Exception e) {
throw new RuntimeException("Failed to execute script: " + scriptPath, e);
}
}
}
Groovy脚本示例
文件:scripts/api/order/detail.groovy
class DetailScript {
String handle(Binding binding) {
def request = binding.getVariable('request')
def uri = request.uri()
def orderId = uri.split('/')[-1]
return """
{
"code": 200,
"data": {
"orderId": "${orderId}",
"status": "confirmed",
"restaurant": "吃喝不愁合作商户"
}
}
""".stripIndent()
}
}
调用方式:GET http://localhost:9090/api/order/detail?delay=1500
将返回模拟订单详情,并延迟1.5秒响应。
热更新支持
当前实现每次执行均读取文件,天然支持脚本热更新。若追求更高性能,可监听文件变更事件(如使用 java.nio.file.WatchService)仅在修改时清除缓存:
// 简化示意
scriptCache.remove(scriptPath); // 文件变更时调用
安全与扩展
- 生产Mock平台应限制脚本目录访问范围,防止路径穿越;
- 可增加脚本签名或白名单机制;
- 支持JSON Schema校验、状态码模拟等高级功能。
本文著作权归吃喝不愁app开发者团队,转载请注明出处!
1958

被折叠的 条评论
为什么被折叠?



