霸王餐API接口Mock平台:基于Netty+Groovy脚本动态延迟响应

霸王餐API接口Mock平台:基于Netty+Groovy脚本动态延迟响应

背景与目标

在“霸王餐”类App(如吃喝不愁)的联调与测试阶段,后端服务往往尚未就绪。为提升前端、测试及第三方对接效率,需构建一个高灵活度的Mock平台。该平台需支持:

  • 动态路由匹配;
  • Groovy脚本编写响应逻辑;
  • 可配置延迟响应时间;
  • 无需重启即时生效。

本文采用 Netty 作为网络层,结合 Groovy 脚本引擎实现上述能力。
在这里插入图片描述

整体架构

平台核心组件包括:

  1. Netty HTTP Server:接收并分发请求;
  2. ScriptManager:加载并缓存Groovy脚本;
  3. DelayController:解析延迟参数并控制响应时机;
  4. 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开发者团队,转载请注明出处!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值