用 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。