找回密码
 立即注册
搜索
查看: 191|回复: 5

Java Web 服务器 wastnet mvc 开发实战

[复制链接]

主题

0

回帖

0

积分

积分
0
发表于 2026-9-29 12:30:26 | 显示全部楼层 |阅读模式
用 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 即可,二者二选一。

  1. <dependency>
  2.     <groupId>io.github.wycst</groupId>
  3.     <artifactId>wastnet-mvc</artifactId>
  4.     <version>1.0.2</version>
  5. </dependency>
复制代码

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

  1. <!-- JSON 序列化(示例用,可替换为 Jackson 等) -->
  2. <dependency>
  3.     <groupId>io.github.wycst</groupId>
  4.     <artifactId>wast</artifactId>
  5.     <version>0.0.29.1</version>
  6. </dependency>
复制代码
2. 三行启动一个 MVC 服务

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

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

  3. HTTPServer.of(8080)
  4.         .requestHandler(router)
  5.         .start();
复制代码

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

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

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

  3.     @ResponseBody
  4.     @Endpoint("/list")
  5.     public String list() {
  6.         return userService.listUsers();
  7.     }
  8. }
复制代码

访问 GET /api/user/list 即返回字符串内容。
4. 参数绑定
4.1 路径变量 @PathParam

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

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

  3. @Endpoint("/profile/{uid}")
  4. public Object b(@PathParam("uid") long uid) { ... }
复制代码
4.2 请求参数 @RequestParam

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

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

  7. @Endpoint(value = "/upload", allowMethods = HttpMethod.POST)
  8. public Object upload(@RequestParam("file") MultipartField file) {
  9.     if (file != null && file.isFile()) {
  10.         return file.getFileName() + ", " + file.size();
  11.     }
  12.     return "no file";
  13. }
复制代码
4.3 请求头 @RequestHeader

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

  3. @Endpoint("/hdr-multi")
  4. public Object multi(@RequestHeader("X-Tags") String&#91;&#93; tags) { ... }

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

支持单值、多值(String&#91;&#93;)、非必填 + 默认值、类型转换(如 int)。
4.4 请求体 @RequestBody

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

  1. public class CreateUserReq {
  2.     public int id;
  3.     public String name;
  4. }

  5. @Endpoint("/create")
  6. public String create(@RequestBody CreateUserReq req) {
  7.     return userService.getUserName(req.id);
  8. }
复制代码
5. 返回值与 JSON 序列化

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

  1. AnnotationRouterHandler router = new AnnotationRouterHandler()
  2.     .messageConverter(new HttpMessageConverter() {
  3.         public void write(Object value, ConverterConfig config, HttpResponse response) throws Exception {
  4.             response.contentType(config.getResponseContentType());
  5.             response.body(config.isPretty()
  6.                     ? JSON.toPrettifyJsonString(value)
  7.                     : JSON.toJsonBytes(value));
  8.         }
  9.         public Object read(HttpRequest request, ConverterConfig config, Type type) throws Exception {
  10.             // 从请求体反序列化(流或字节)为 type 类型
  11.             ...
  12.         }
  13.     });
复制代码

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

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

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

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

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

  9. @Controller("/api/user")
  10. public class UserController {
  11.     @Inject
  12.     private UserService userService;   // 自动注入
  13. }

  14. @Configuration
  15. public class TestConfiguration {
  16.     @Bean
  17.     public CreateUserReq createUserReq() { return new CreateUserReq(); }
  18. }
复制代码
7. 拦截器

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

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

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

  20. // 引用端点级拦截器(类级作用于全部端点,方法级追加)
  21. @RestController("/admin")
  22. @WithInterceptor("admin")
  23. public class AdminController {
  24.     @Endpoint("/users")
  25.     public String users() { return "admin user list"; }
  26. }
复制代码
8. SSE 服务端推送

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

  1. @Controller
  2. public class SseDemoController {

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

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

  15.     @Sse("/sse-full")
  16.     public void full(SseEmitter emitter) throws IOException {
  17.         emitter.emit("greeting", "hello", "evt-1", 3000);  // event/data/id/retry
  18.     }
  19. }
复制代码

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

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

  1. AnnotationRouterHandler router = new AnnotationRouterHandler()
  2.     .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 先执行再重建。
• 容错:重载失败会保留上一次状态并打印错误,不会让服务崩溃。


  1. router.hotReload(false);                 // 关闭热重载
  2. router.hotReload(true, false);           // 开启热重载但静音(第二个参数控制是否打印日志)
  3. router.hotReloadWatchExclude("com.example.stable");  // 排除稳定包不参与重载
复制代码

控制台会打印类似:&#91;dev&#93; hot reload: ReloadController.class changed, reloading... → &#91;dev&#93; hot reload: OK reloaded in 36 ms。以 jar 方式运行时不启用热重载。
11. 完整启动与验证

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

  1. public static void main(String&#91;&#93; args) throws Exception {
  2.     AnnotationRouterHandler router = new AnnotationRouterHandler()
  3.             .messageConverter(/* 见第 5 节 */)
  4.             .addViewResolver(new FreeMarkerViewResolver())
  5.             .property("app.prefix", "Member-")
  6.             .scanPackages("io.github.wycst.wastnet.examples.http.mvc")
  7.             .configFiles("demo.properties");

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

  11.     server.pemSSL("cert/cert.pem", "cert/server.pem").h2();  // 可选:启用 HTTPS/h2
  12.     server.start();
  13. }
复制代码

启动后部分可用端点:


端点         说明

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。

原文链接

打赏作者

当前余额:0 Token,打赏后立即到账

主题

0

回帖

0

积分

积分
0
发表于 2026-9-29 12:45:01 | 显示全部楼层
好文,Java的趋势基本被楼主说中了。

主题

0

回帖

0

积分

积分
0
发表于 2026-9-29 13:15:02 | 显示全部楼层
已阅,受益匪浅,楼主加油更新。

主题

0

回帖

0

积分

积分
0
发表于 2026-9-29 14:15:01 | 显示全部楼层
已阅,受益匪浅,楼主加油更新。

主题

0

回帖

0

积分

积分
0
发表于 2026-9-29 14:45:01 | 显示全部楼层
写得不错,wastnet那部分说到点子上了,顶一个。

主题

0

回帖

0

积分

积分
0
发表于 2026-9-29 15:15:01 | 显示全部楼层
签到留名,好帖子不能沉,wastnet顶上去。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

{ template common/footer}