首页 > 开源 > Java Web 服务器 wastnet mvc 开发实战

Java Web 服务器 wastnet mvc 开发实战

OSChina资讯 2026-09-29 11:54 7 阅读 查看原文

用 wastnet MVC 写接口服务(实战教程)

wastnet 是一款完全自研、零第三方依赖的轻量级 Java 网络应用框架,核心基于 JDK 原生 NIO 构建 Reactor 多路复用模型,HTTP/2 协议栈从 HPACK、Huffman 到 ALPN 均为自主实现。其 wastnet-mvc 模块在核心之上提供注解驱动的 MVC / 轻量 IoC 能力:@Controller、@Endpoint、@ResponseBody、参数绑定(@PathParam / @RequestParam / @RequestHeader / @RequestBody)、HttpMessageConverter、拦截器与 SSE 等一应俱全。本文以项目内置的 MvcDemo 为例,带你从零跑通一个完整的接口服务。

1. 引入依赖

只需引入 wastnet-mvc(它会自动依赖 wastnet-core),基础场景引入 wastnet-core 即可,二者二选一。

<dependency>
    <groupId>io.github.wycst</groupId>
    <artifactId>wastnet-mvc</artifactId>
    <version>1.0.2</version>
</dependency>

HttpMessageConverter 的 JSON 读写需要自行实现。示例中使用 io.github.wycst:wast 提供的 io.github.wycst.wast.json.JSON(需单独引入,也可替换为 Jackson 等任意 JSON 库);若用到模板视图渲染(如 FreeMarker)再按需引入对应库。

<!-- JSON 序列化(示例用,可替换为 Jackson 等) -->
<dependency>
    <groupId>io.github.wycst</groupId>
    <artifactId>wast</artifactId>
    <version>0.0.29.1</version>
</dependency>

2. 三行启动一个 MVC 服务

核心是 AnnotationRouterHandler:扫描包下的 @Controller,再把路由器挂到 HTTPServer 上。

AnnotationRouterHandler router = new AnnotationRouterHandler()
        .scanPackages("com.demo.controller");   // 扫描你的控制器包

HTTPServer.of(8080)
        .requestHandler(router)
        .start();

MvcDemo 在此基础上叠加了 JSON 转换器、视图解析器、属性与配置文件,稍后逐步展开。

3. 第一个 Controller

@Controller 声明控制器,@Endpoint 映射路径,@ResponseBody 表示返回值直接写回(有 HttpMessageConverter 时自动序列化)。@RestController 等价于 @Controller + 默认 @ResponseBody。

@Controller("/api/user")
public class UserController {

    @ResponseBody
    @Endpoint("/list")
    public String list() {
        return userService.listUsers();
    }
}

访问 GET /api/user/list 即返回字符串内容。

4. 参数绑定

4.1 路径变量 @PathParam

wastnet 同时支持两种语法:${id}(自有写法)与 {uid}(Spring 风格)。

@Endpoint("/user/${id}")
public Object a(@PathParam("id") long id) { ... }

@Endpoint("/profile/{uid}")
public Object b(@PathParam("uid") long uid) { ... }

4.2 请求参数 @RequestParam

支持标量、默认值、同名多值与文件上传(MultipartField)。

@Endpoint("/search")
public Object search(@RequestParam("q") String q,
                     @RequestParam(value = "page", required = false, defaultValue = "1") int page,
                     @RequestParam(value = "size", required = false, defaultValue = "10") int size) {
    // ...
}

@Endpoint(value = "/upload", allowMethods = HttpMethod.POST)
public Object upload(@RequestParam("file") MultipartField file) {
    if (file != null && file.isFile()) {
        return file.getFileName() + ", " + file.size();
    }
    return "no file";
}

4.3 请求头 @RequestHeader

@Endpoint("/hdr-single")
public Object single(@RequestHeader("X-Client") String client) { ... }

@Endpoint("/hdr-multi")
public Object multi(@RequestHeader("X-Tags") String[] tags) { ... }

@Endpoint("/hdr-default")
public Object withDefault(@RequestHeader(value = "X-Env", required = false, defaultValue = "dev") String env) { ... }

支持单值、多值(String[])、非必填 + 默认值、类型转换(如 int)。

4.4 请求体 @RequestBody

配合 HttpMessageConverter,请求体 JSON 自动反序列化为 POJO。

public class CreateUserReq {
    public int id;
    public String name;
}

@Endpoint("/create")
public String create(@RequestBody CreateUserReq req) {
    return userService.getUserName(req.id);
}

5. 返回值与 JSON 序列化

方法返回对象(非 void)时,由注册的 HttpMessageConverter 负责写出。下面是 MvcDemo 里用框架内置 JSON 实现的转换器:

AnnotationRouterHandler router = new AnnotationRouterHandler()
    .messageConverter(new HttpMessageConverter() {
        public void write(Object value, ConverterConfig config, HttpResponse response) throws Exception {
            response.contentType(config.getResponseContentType());
            response.body(config.isPretty()
                    ? JSON.toPrettifyJsonString(value)
                    : JSON.toJsonBytes(value));
        }
        public Object read(HttpRequest request, ConverterConfig config, Type type) throws Exception {
            // 从请求体反序列化(流或字节)为 type 类型
            ...
        }
    });

@ResponseBody 或 @RestController 标注的端点,返回值即走该转换器;未标注 @ResponseBody 且返回非 void 时,则交给视图解析器(见第 9 节)。

6. 轻量 DI

@Component 注册托管组件,@Inject 按类型注入,@Value 注入配置,@PostConstruct / @PreDestroy 管理生命周期;@Configuration + @Bean 提供工厂式 Bean。

@Component
public class UserService {
    @Value("${app.prefix:User-}")
    private String namePrefix;

    @PostConstruct
    public void init() { /* 初始化 */ }

    public String getUserName(int id) { return namePrefix + id; }
}

@Controller("/api/user")
public class UserController {
    @Inject
    private UserService userService;   // 自动注入
}

@Configuration
public class TestConfiguration {
    @Bean
    public CreateUserReq createUserReq() { return new CreateUserReq(); }
}

7. 拦截器

实现 RouterInterceptor 并标 @Interceptor 即可被自动注册。不带 type 的是全局拦截器;type = ENDPOINT 的是端点级拦截器,需配合 @WithInterceptor 引用才生效。

// 全局拦截器:打印请求日志
@Interceptor(order = 1)
public class AuthLogInterceptor implements RouterInterceptor {
    public boolean beforeHandle(String path, HttpRequest request, HttpResponse response) {
        System.out.println("[" + request.getMethod() + "] " + path);
        return true;   // 返回 false 将中断请求
    }
}

// 端点级拦截器:校验角色
@Interceptor(value = "admin", order = 1, type = InterceptorType.ENDPOINT)
public class AdminAuthInterceptor implements RouterInterceptor {
    public boolean beforeHandle(String path, HttpRequest request, HttpResponse response) {
        if (!"admin".equals(request.getHeader("X-Role"))) {
            response.status(403).body("Forbidden: admin role required");
            return false;
        }
        return true;
    }
}

// 引用端点级拦截器(类级作用于全部端点,方法级追加)
@RestController("/admin")
@WithInterceptor("admin")
public class AdminController {
    @Endpoint("/users")
    public String users() { return "admin user list"; }
}

8. SSE 服务端推送

@Sse 方法返回 void,并恰好包含一个 SseEmitter 参数(其余 @PathParam / @RequestParam 等照常解析)。用 emitter.emit(...) 推送事件。

@Controller
public class SseDemoController {

    @Sse("/sse-clock")
    public void clock(SseEmitter emitter) throws IOException {
        for (int i = 1; i <= 5; i++) {
            emitter.emit("tick-" + i);   // 仅 data
            Thread.sleep(1000);
        }
        // 方法返回后框架自动关闭连接,无需手动 close
    }

    @Sse("/sse/room/${roomId}")
    public void room(@PathParam("roomId") long roomId, SseEmitter emitter) throws IOException {
        emitter.emit("room-" + roomId + "-msg");
    }

    @Sse("/sse-full")
    public void full(SseEmitter emitter) throws IOException {
        emitter.emit("greeting", "hello", "evt-1", 3000);  // event/data/id/retry
    }
}

说明:@Sse 方法执行完毕后,框架在 finally 中自动关闭连接并释放资源;也可主动调用 emitter.close()(幂等)。客户端用 curl -N 或 EventSource 消费,例如 curl -N http://localhost:8080/sse/room/100。

9. 视图渲染(可选)

未标注 @ResponseBody 且返回非 void 时,返回值交给注册的 ViewResolver。实现 ViewResolver 接口(supports + render),再 addViewResolver(...) 注册即可。

AnnotationRouterHandler router = new AnnotationRouterHandler()
    .addViewResolver(new FreeMarkerViewResolver());   // 返回 ModelAndView 时按模板渲染

MvcDemo 中 /demo/view/user 即走 FreeMarker 模板渲染为 HTML;纯接口场景可忽略本节。

10. 开发热重载

开发阶段修改 Controller / 配置后无需重启服务,wastnet 内置的 DevHotReloader 会自动重载。它在主类从目录(而非 jar)加载的开发环境下自动启用,监听编译输出目录:

  • 触发:.class 文件变更(默认去抖 1000ms,可用 -Dwastnet.http.hot-reload.debounce=ms 调整);configFiles 指向的配置文件变更也会触发。
  • 机制:每次重载重建子 ClassLoader 只指向项目编译产物,库与框架类委托父加载器,因此重编译的 .class 从磁盘重新读取,注解身份保持稳定。
  • 范围:只清理扫描产物(HTTP / SSE 路由、WebSocket、拦截器、扫描 Bean),保留手工注册的路由;@PreDestroy 先执行再重建。
  • 容错:重载失败会保留上一次状态并打印错误,不会让服务崩溃。
router.hotReload(false);                 // 关闭热重载
router.hotReload(true, false);           // 开启热重载但静音(第二个参数控制是否打印日志)
router.hotReloadWatchExclude("com.example.stable");  // 排除稳定包不参与重载

控制台会打印类似:[dev] hot reload: ReloadController.class changed, reloading... → [dev] hot reload: OK reloaded in 36 ms。以 jar 方式运行时不启用热重载。

11. 完整启动与验证

MvcDemo.main 把以上能力串起来:开启 H2 监控、注册 JSON 转换器与视图解析器、注入属性 app.prefix、扫描包、启动服务器(可选 pemSSL + h2 启用 HTTPS/h2)。

public static void main(String[] args) throws Exception {
    AnnotationRouterHandler router = new AnnotationRouterHandler()
            .messageConverter(/* 见第 5 节 */)
            .addViewResolver(new FreeMarkerViewResolver())
            .property("app.prefix", "Member-")
            .scanPackages("io.github.wycst.wastnet.examples.http.mvc")
            .configFiles("demo.properties");

    HTTPServer server = HTTPServer.of(8080)
            .requestHandler(router)
            .startupBannerEnabled(true);

    server.pemSSL("cert/cert.pem", "cert/server.pem").h2();  // 可选:启用 HTTPS/h2
    server.start();
}

启动后部分可用端点:

端点 说明
GET /api/user/list 返回用户列表(字段注入 + DI)
GET /api/user/get?id=42 按 id 查询
GET /demo/user/${id} / /profile/{uid} 两种路径变量语法
GET /demo/search?q=x&page=2 @RequestParam 默认值
POST /demo/upload 单文件上传(MultipartField)
GET /hdr-single(X-Client: curl) @RequestHeader 绑定
GET /sse-clock SSE 每秒推送(curl -N)
GET /admin/users(X-Role: admin) 端点级拦截器保护

12. 与 Spring MVC 对比:上手更容易

如果你用过 Spring MVC,会发现 wastnet MVC 的注解几乎一一对应,迁移成本极低:

wastnet 注解 Spring 等价 说明
@Controller @Controller 类级,value() 为 base path
@RestController @RestController 等价于 @Controller + 类级 @ResponseBody
@Endpoint @RequestMapping / @GetMapping… 方法级路由,allowMethods 限定方法
@Component / @Configuration+@Bean 同名 托管组件与工厂 Bean
@Inject @Autowired 按类型注入
@Value @Value ${key:default} 占位符
@PathParam / @RequestParam / @RequestBody / @RequestHeader @PathVariable / @RequestParam / @RequestBody / @RequestHeader 参数绑定
@PostConstruct / @PreDestroy javax.annotation.* 生命周期回调(框架自带,无需额外依赖)
@Interceptor + @WithInterceptor HandlerInterceptor 路由级拦截

上手更容易体现在几个方面:

  • 零容器负担:无需引入 Spring 容器、无 starter 依赖爆炸、无版本冲突风险。一个 wastnet-mvc 依赖 + 几行 main 就能跑起一个支持 HTTP/2、SSL、SSE 的服务,不必配置 DispatcherServlet、嵌入式 Tomcat 或一堆自动配置。
  • 开箱即用的网络能力:HTTP/2、PEM 直载 SSL、@Sse 服务端推送、开发热重载都是框架内置,不依赖额外中间件或第三方库。
  • 轻量 DI:@Component / @Inject / @Value 即可完成大部分场景,没有复杂的 Bean 生命周期与代理体系。
  • 平滑迁移:通过 annotationResolver(...) 还能桥接 Spring 的 @RestController / @RequestMapping / @Service / @Autowired / @Value,现有 Spring 注解代码几乎不用改。

相关链接

完整示例见 wastnet-test 模块下的 examples/http/mvc。

完整文档引用

本文为实战速览,注解 MVC 的完整用法见官方文档:annotation-mvc-guide.md。