|
|
用 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 注解代码几乎不用改。
相关链接
• Gitee:https://gitee.com/xiaoch0209/wastnet
• GitHub:https://github.com/wycst02/wastnet
• 协议:Apache License 2.0
完整示例见 wastnet-test 模块下的 examples/http/mvc。
完整文档引用
本文为实战速览,注解 MVC 的完整用法见官方文档:annotation-mvc-guide.md。
原文链接 |
|