书接上回,作为一个从.NET转Java的开发人员,Spring框架肯定是无法绕过的。在学习 Spring 框架的过程中,我们往往只是停留在“使用”层面,而对其底层实现机制知之甚少。但我始终觉得,学习框架的过程中,理解其设计思想和实现原理是非常重要的。因此,我决定手写一个微型的Spring框架,来深入理解Spring的核心设计思想。为了加深我对 Spring 的理解,并锻炼 Java 框架设计能力,我决定手写一个微型的 Spring 框架 —— Javelin。
这篇文章我们将实现:
一个简单的 IoC 容器 JavelinContext,用于管理 Bean 实例的生命周期。
支持 @RestController、@GetMapping、@PostMapping 等注解的自动扫描与注册。
基于 HttpServer 的端口监听与请求路由分发。
本文适合有 Java 或 .NET 框架开发经验的读者,特别是希望深入理解 Spring、探索其核心设计思想的人群。
为什么要手写一个 Spring 框架? 首先Spring 是 Java 生态中最具代表性的框架之一,而作为一个搬砖多年的开发人员,CURD显然不再是我们的追求。但,学习框架的设计思想和实现原理还是非常有价值的。Sping框架核心理念在于控制反转(IoC)、面向切面编程(AOP)和基于注解的声明式编程。手写框架可以帮助我们:
理解 IoC 容器原理 :Bean 是如何被发现、实例化、依赖注入的?
掌握路由注册机制 :Spring MVC 如何基于注解将请求分发给具体的方法?
探索类扫描与反射机制 :如何实现自动发现注解的类与方法?
对比 .NET :Spring 与 ASP.NET Core 的设计思路在某些地方是相通的,比如 @RestController 和 [ApiController],但实现方式有差异。
通过实战构建 Javelin,我们能够站在框架设计者的角度重新思考“约定优于配置”的设计哲学。
项目结构 我们手写的框架名叫 Javelin,目前已经实现了三个核心模块:
模块
功能
JavelinContext
提供简化版 IoC 容器,实现基于注解的构造函数注入
Router
自动扫描 @RestController,绑定 GET/POST 请求路径
AppStartup
应用启动入口,配置端口监听、类扫描路径等
项目结构如下:
1 2 3 4 5 6 javelin-core/ ├── annotations/ // 自定义注解(@RestController, @Inject 等) ├── context/ // IoC 容器 ├── rest/ // 路由注册 ├── core/ // 类扫描器 └── startup/ // 启动类
构建核心 IoC 容器 JavelinContext 我们首先需要一个容器类来负责扫描、实例化 Bean,并支持构造函数注入。代码结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class JavelinContext { private final Map<Class<?>, Object> singletonMap = new HashMap <>(); public <T> T getBean (Class<T> clazz) { if (singletonMap.containsKey(clazz)) { return (T) singletonMap.get(clazz); } T instance = (T) createBean(clazz); singletonMap.put(clazz, instance); return instance; } public <T> T createBean (Class<T> clazz) { try { Constructor<?>[] constructors = clazz.getDeclaredConstructors(); injectConstructor = clazz.getDeclaredConstructor(); return (T) injectConstructor.newInstance(args.toArray()); } catch (Exception e) { throw new RuntimeException ("Failed to instantiate: " + clazz.getName(), e); } } }
这段代码的核心思路与 .NET Core 的构造函数注入类似(例如通过 IServiceCollection.AddTransient() 注册服务并注入)。
自动路由注册 Router 我们手动实现了基于注解的请求路由注册机制。Javelin 支持如下注解:
控制器注解
1 2 3 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface RestController {}
请求映射注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Target(ElementType.ANNOTATION_TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface HttpMethodMapping { String method () ; } @HttpMethodMapping(method = "POST") @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface PostMapping { String value () ; }
而 Router 类的核心逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class Router { public void registerRoutes (HttpServer server, Set<Class<?>> classes, JavelinContext context) { for (Class<?> clazz : classes) { if (!clazz.isAnnotationPresent(RestController.class)) continue ; Object controller = context.getBean(clazz); for (Method method : clazz.getDeclaredMethods()) { HttpMethodMapping mapping = annotation.annotationType().getAnnotation(HttpMethodMapping.class); if (mapping != null ) { String httpMethod = mapping.method(); try { String path = (String) annotation.annotationType().getMethod("value" ).invoke(annotation); registerHandler(server, path, httpMethod, controllerInstance, method); } catch (Exception e) { e.printStackTrace(); } } } } } private void registerHandler (HttpServer server, String path, String expectedMethod, Object controllerInstance, Method method) { server.createContext(path, exchange -> { try { } catch (Exception e) { exchange.sendResponseHeaders(500 , 0 ); e.printStackTrace(); } }); System.out.println("➡️ [" + httpMethod + "] " + path + " → " + controller.getClass().getSimpleName() + "." + method.getName()); } }
.NET 中这部分功能相当于 ASP.NET Core 中的 MapControllers() + [HttpGet("/api/foo")] 等特性。
启动类 AppStartup 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class AppStartup { public static void run (Class<?> appClass, String[] args) { String basePackage = appClass.getPackage().getName(); Set<Class<?>> controllers = ClassScanner.scan(basePackage); int port = resolvePort(); HttpServer server; try { server = HttpServer.create(new InetSocketAddress (port), 0 ); } catch (IOException e) { throw new RuntimeException ("Failed to start HTTP server" , e); } JavelinContext context = new JavelinContext (); new Router ().registerRoutes(server, controllers, context); server.start(); System.out.println("🚀 Server started at http://localhost:" + port); } }
Service 注入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController public class HelloController { private UserService userService; @Inject public HelloController (UserService userService) { this .userService = userService; } @GetMapping("/hello") public String hello () { return "Hello World from " + userService.getUserById(1 ) ; } }
接下面让我们看看我们的Hello World 当我们用get方式访问post时,会报405错误。
💡和 .NET 框架的对比分析 在开发 Javelin 的过程中,我时常联想到 .NET 的实现,尤其是 ASP.NET Core,它与 Spring 在设计理念上高度一致,但细节又有一些差异,下面进行系统性对比:
控制器与路由
特性
Javelin (Java)
ASP.NET Core (.NET)
控制器注解/特性
@RestController
[ApiController]
路由声明方式
@GetMapping(“/path”)
[HttpGet(“path”)]
路由注册机制
手动反射扫描注册
中间件动态注册至 EndpointRouting
请求处理
HttpExchangeHandler
ControllerInvoker 中间件链
依赖注入与服务容器
特性
Javelin (Java)
ASP.NET Core (.NET)
服务注册方式
自动递归构造,无需注册
显式调用 services.AddXyz()
注入方式
构造函数 + @Inject
构造函数 + 内建 IoC 容器
生命周期管理
默认单例
支持 Transient / Scoped / Singleton
循环依赖检测
暂不支持
默认会在启动时检测并抛出异常
底层运行机制
特性
Javelin
ASP.NET Core
HTTP 服务器
com.sun.net.httpserver.HttpServer
Kestrel 内置服务器
请求派发模型
映射到类方法,反射调用
中间件链 + 路由匹配 + ControllerInvoker
可以看出,Javelin 更加简洁直观,适合做教学或理解框架底层流程,而 ASP.NET Core 更加强大和工程化,适用于复杂企业级应用。
总结与下步计划 通过上面的内容,我们已经实现了 Spring 框架的三个关键能力:
简易 IoC 容器支持构造函数注入
使用 @RestController/@GetMapping/@PostMapping 注解注册请求处理器
类似 Spring Boot 的端口监听启动类
后续我们将继续拓展:
参数绑定(如 @RequestParam, @RequestBody)
中间件机制(如拦截器)
全局异常处理
JSON 返回支持
生命周期管理
敬请期待下一篇:实现请求参数自动注入与 JSON 响应支持 !
由于篇幅原因,示例中的代码仅展示了部分关键实现细节,完整代码请参考GitHub 仓库。