{"id":31798286,"url":"https://github.com/czwbig/mini-spring","last_synced_at":"2025-10-10T21:18:51.920Z","repository":{"id":46150728,"uuid":"195923480","full_name":"czwbig/mini-spring","owner":"czwbig","description":"mini-spring 是仿写 Spring 的一个乞丐版 IOC/AOP 框架，总代码规范精炼，仅数百行，同时注释详细，可作为 Spring 框架学习教程。使用了 Java 反射、注解、动态代理等技术以及内嵌 tomcat 服务器，实现了 @Controller、@AutoWired、@Aspect、@Before 等常用注解。可实现简单的访问 uri 映射，控制反转以及不侵入原代码的面向切面编程。","archived":false,"fork":false,"pushed_at":"2021-10-18T12:21:05.000Z","size":34,"stargazers_count":91,"open_issues_count":1,"forks_count":27,"subscribers_count":5,"default_branch":"master","last_synced_at":"2023-03-04T16:57:58.015Z","etag":null,"topics":["java","mini-framework","spring"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/czwbig.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-07-09T03:09:55.000Z","updated_at":"2023-02-11T02:38:28.000Z","dependencies_parsed_at":"2022-09-24T14:50:51.863Z","dependency_job_id":null,"html_url":"https://github.com/czwbig/mini-spring","commit_stats":null,"previous_names":[],"tags_count":null,"template":null,"template_full_name":null,"purl":"pkg:github/czwbig/mini-spring","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/czwbig%2Fmini-spring","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/czwbig%2Fmini-spring/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/czwbig%2Fmini-spring/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/czwbig%2Fmini-spring/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/czwbig","download_url":"https://codeload.github.com/czwbig/mini-spring/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/czwbig%2Fmini-spring/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279005412,"owners_count":26083883,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-10T02:00:06.843Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["java","mini-framework","spring"],"created_at":"2025-10-10T21:18:47.846Z","updated_at":"2025-10-10T21:18:51.905Z","avatar_url":"https://github.com/czwbig.png","language":"Java","readme":"# mini-spring\n\n讲道理，感觉自己有点菜。Spring 源码看不懂，不想强行解释，等多积累些项目经验之后再看吧，但是 Spring 中的控制反转（IOC）和面向切面编程（AOP）思想很重要，为了更好的使用 Spring 框架，有必要理解这两个点，为此，我使用 JDK API 实现了一个玩具级的简陋 IOC/AOP 框架 mini-spring，话不多说，直接开干。\n\n# 环境搭建\u0026快速使用\n\n全部代码已上传 GitHub：[https://github.com/czwbig/mini-spring](https://github.com/czwbig/mini-spring)  \n\n1. 将代码弄到本地并使用 IDE 打开，这里我们用 IDEA；\n2. 使用 Gradle 构建项目，可以使用 IDEA 提供的 GUI 操作，也可以直接使用 `gradle build` 命令；\n\n![](https://upload-images.jianshu.io/upload_images/14923529-d777a3d60ab478bd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n3. 如下图，右击 `mini-spring\\framework_use_test\\build\\libs\\framework_use_test-1.0-SNAPSHOT.jar` ，点击 Run，当然也可以直接使用 `java -jar jarPath.jar` 命令来运行此 jar 包；\n\n![](https://upload-images.jianshu.io/upload_images/14923529-c1fc604fa2cdecd8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n4. 浏览器打开 `localhost:8080/rap` 即可观察到显示 CXK 字母，同时 IDE 控制台会输出：  \n\n```text\nfirst,singing \u003cchicken is too beautiful\u003e.\nand the chicken monster is dancing now.\nCXK rapping...\noh! Don't forget my favorite basketball.\n```  \n  \n下面开始框架的讲解。  \n\n**注意调式的时候也一定要运行 Jar 包，因为 Jar 包中才包含所有类，否则扫描类会出问题**   \n\n# 简介\n\n本项目使用 Java API 以及内嵌 Tomcat 服务器写了一个玩具级 IOC/AOP web 框架。实现了 `@Controller`、`@AutoWired`、`@Component` 、`@Pointcut`、`@Aspect`、`@Before`、`@After` 等 Spring 常用注解。可实现简单的访问 uri 映射，控制反转以及不侵入原代码的面向切面编程。  \n\n讲解代码实现之前，假设读者已经掌握了基础的项目构建、反射、注解，以及 JDK 动态代理知识，项目精简，注释详细，并且总代码 + 注释不足 1000 行，适合用来学习。其中构建工具 Gradle 没用过也不要紧，我也是第一次使用，当成没有 xml 的 Maven 来看就行，下面我会详细解读其构建配置文件。\n\n### 模块组成\n\n项目由两个模块组成，一个是框架本身的模块，实现了框架的 IOC/AOP 等功能，如下图：  \n\n![](https://upload-images.jianshu.io/upload_images/14923529-51a3dad295968b43.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n类比较多，但是大部分都是代码很少的，特别是注解定义接口，不要怕。  \n\n- `aop` 包中是 `After` 等注解的定义接口，以及动态代理辅助类；  \n- `bean` 包中是两个注解定义，以及 `BeanFactory` 这个 Bean 工厂，其中包含了类扫描和 Bean 的初始化的代码；\n- `core` 包是一个 `ClassScanner` 类扫描工具类；\n- `starter` 包是一个框架的启动与初始化类；\n- `web/handler` 包中是 uri 请求的处理器的收集与管理，如查找 `@Controller` 注解修饰的类中的 `@RequestMapping` 注解修饰的方法，用来响应对应 uri 请求。\n- `web/mvc` 包定义了与 webMVC 有关的三个注解；\n- `web/server` 包中是一个嵌入式 Tomcat 服务器的初始化类；\n- `web/servlet` 包中是一个请求分发器，重写的 `service()` 方法定义使用哪个请求处理器来响应浏览器请求；\n\n\n另一个模块是用来测试（使用）框架的模块，如下图：\n\n![](https://upload-images.jianshu.io/upload_images/14923529-e6afc2bee1e0b85b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n就像我们使用 Spring 框架一样，定义 Controller 等来响应请求，代码很简单，就不解释了。  \n\n### 项目构建\n\n根目录下有 `setting.gradle`、`build.gradle` 项目构建文件，其中 `setting.gradle` 指定了项目名以及模块名。  \n\n```text\nrootProject.name = 'mini-spring'\ninclude 'framework'\ninclude 'framework_use_test'\n```\n\n`build.gradle` 是项目构建设置，主要代码如下：\n\n```\nplugins {\n    id 'java'\n}\n\ngroup 'com.caozhihu.spring'\nversion '1.0-SNAPSHOT'\n\nsourceCompatibility = 1.8\n\nrepositories {\n    repositories { maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } }\n//    mavenCentral()\n}\n\ndependencies {\n    testCompile group: 'junit', name: 'junit', version: '4.12'\n}\n```\n\n引入了 gradle 的 java 插件，因为 gradle 不仅仅可以用于 java 项目，也可以用于其他项目，引入了 java 插件定义了项目的文件目录结构等。  \n\n然后就是项目的版本以及 java 源代码适配级别，这里是 JDK 1.8，在后面是指定了依赖仓库，gradle 可以直接使用 maven 仓库。  \n\n最后就是引入项目具体依赖，这里和 maven 一样。  \n\n  \n每个模块也有单独的 `build.gradle` 文件来指定模块的构建设置，这里以 `framework_use_test` 模块的 `build.gradle` 文件来说明：\n\n```text\ndependencies {\n    // 只在单元测试时候引入此依赖\n    testCompile group: 'junit', name: 'junit', version: '4.12'\n    // 项目依赖\n    compile(project(':framework'))\n}\n\njar {\n    manifest {\n        attributes \"Main-Class\": \"com.caozhihu.spring.Application\"\n    }\n    // 固定打包句式\n    from {\n        configurations.runtime.asFileTree.files.collect { zipTree(it) }\n    }\n}\n```\n\n除去和项目根目录下构建文件相同部分，其他的构建代码如上，这里的 dependencies 除了添加 Junit 单元测试依赖之外，还指定了 `framework` 模块。  \n\n下面指定了 jar 包的打包设置，首先使用 manifest 设置主类，否则生成的 jar 包找不到主类清单，会无法运行。还使用了 from 语句来设置打包范围，这是固定句式，用来收集所有的 java 类文件。  \n\n# framework 实现流程\n\n如下图：\n\n![](https://upload-images.jianshu.io/upload_images/14923529-71c8218afa692f36.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n### 启动 tomcat 服务\n\n```java\npublic void startServer() throws LifecycleException {\n        tomcat = new Tomcat();\n        tomcat.setPort(8080);\n        tomcat.start();\n\n        // new 一个标准的 context 容器并设置访问路径；\n        // 同时为 context 设置生命周期监听器。\n        Context context = new StandardContext();\n        context.setPath(\"\");\n        context.addLifecycleListener(new Tomcat.FixContextListener());\n        // 新建一个 DispatcherServlet 对象，这个是我们自己写的 Servlet 接口的实现类，\n        // 然后使用 `Tomcat.addServlet()` 方法为 context 设置指定名字的 Servlet 对象，\n        // 并设置为支持异步。\n        DispatcherServlet servlet = new DispatcherServlet();\n        Tomcat.addServlet(context, \"dispatcherServlet\", servlet)\n                .setAsyncSupported(true);\n\n        // Tomcat 所有的线程都是守护线程，\n        // 如果某一时刻所有的线程都是守护线程，那 JVM 会退出，\n        // 因此，需要为 tomcat 新建一个非守护线程来保持存活，\n        // 避免服务到这就 shutdown 了\n        context.addServletMappingDecoded(\"/\", \"dispatcherServlet\");\n        tomcat.getHost().addChild(context);\n\n        Thread tomcatAwaitThread = new Thread(\"tomcat_await_thread\") {\n            @Override\n            public void run() {\n                TomcatServer.this.tomcat.getServer().await();\n            }\n        };\n\n        tomcatAwaitThread.setDaemon(false);\n        tomcatAwaitThread.start();\n    }\n```\n\n这里看代码注释，结合下面这张 tomcat 架构图就可以理解了。  \n\n![](https://upload-images.jianshu.io/upload_images/14923529-c0450cd489145b96.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n图片来自 http://click.aliyun.com/m/1000014411/  \n\n如果暂时不理解也没关系，不影响框架学习，我只是为了玩一玩内嵌 tomcat，完全可以自己实现一个乞丐版的网络服务器的。    \n\n这里使用的是我们自定义的 Servlet 子类 DispatcherServlet 对象，该类重写了 `service()` 方法，代码如下：\n\n```java\n@Override\n    public void service(ServletRequest req, ServletResponse res) throws IOException {\n        for (MappingHandler mappingHandler : HandlerManager.mappingHandlerList) {\n            // 从所有的 MappingHandler 中逐一尝试处理请求，\n            // 如果某个 handler 可以处理(返回true)，则返回即可\n            try {\n                if (mappingHandler.handle(req, res)) {\n                    return;\n                }\n            } catch (IllegalAccessException e) {\n                e.printStackTrace();\n            } catch (InvocationTargetException e) {\n                e.printStackTrace();\n            }\n        }\n        res.getWriter().println(\"failed!\");\n    }\n```\n\nHandlerManager 和 MappingHandler 处理器后面会讲，这里先不展开。至此，tomcat 服务器启动完成；\n\n### 扫描类\n\n扫描类是通过这句代码完成的：\n\n```java\n// 扫描类\nList\u003cClass\u003c?\u003e\u003e classList = ClassScanner.scannerCLasses(cls.getPackage().getName());\n```\n\n`ClassScanner.scannerCLasses` 方法实现如下：\n\n```java\npublic static List\u003cClass\u003c?\u003e\u003e scannerCLasses(String packageName)\n            throws IOException, ClassNotFoundException {\n        List\u003cClass\u003c?\u003e\u003e classList = new ArrayList\u003c\u003e();\n        String path = packageName.replace(\".\", \"/\");\n        // 线程上下文类加载器默认是应用类加载器，即 ClassLoader.getSystemClassLoader();\n        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();\n\n        // 使用类加载器对象的 getResources(ResourceName) 方法获取资源集\n        // Enumeration 是古老的迭代器版本，可当成 Iterator 使用\n        Enumeration\u003cURL\u003e resources = classLoader.getResources(path);\n        while (resources.hasMoreElements()) {\n            URL url = resources.nextElement();\n            // 获取协议类型，判断是否为 jar 包\n            if (url.getProtocol().contains(\"jar\")) {\n                // 将打开的 url 返回的 URLConnection 转换成其子类 JarURLConnection 包连接\n                JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();\n                String jarFilePath = jarURLConnection.getJarFile().getName();\n                // getClassesFromJar 工具类获取指定 Jar 包中指定资源名的类；\n                classList.addAll(getClassesFromJar(jarFilePath, path));\n            } else {\n                // 简单起见，我们暂时仅实现扫描 jar 包中的类\n                // todo\n            }\n        }\n        return classList;\n    }\n\n    private static List\u003cClass\u003c?\u003e\u003e getClassesFromJar(String jarFilePath, String path) throws IOException, ClassNotFoundException {\n         // 为减少篇幅，这里完整代码就不放出来了\n    }\n```\n\n注释很详细，就不多废话了。\n\n### 初始化Bean工厂\n\n这部分是最重要的，IOC 和 AOP 都在这里实现。  \n\n代码请到在 `BeanFactory` 类中查看，[GitHub 在线查看 BeanFactory](https://github.com/czwbig/mini-spring/blob/master/framework/src/main/java/com/caozhihu/spring/bean/BeanFactory.java)  \n\n注释已经写的非常详细。这里简单说下处理逻辑。   \n\n首先通过遍历上一步类扫描获得类的 Class 对象集合，将被 `@Aspect` 注解的类保存起来，然后初始化其他被 `@Component` 和 `@Controller` 注解的类，并处理类中被 `@AutoWired` 注解的属性，将目标引用对象注入（设置属性的值）到类中，然后将初始化好的对象保存到 Bean 工厂。到这里，控制反转就实现好了。  \n\n接下来是处理被 `@Aspect` 注解的类，并解析他们中被 `@Pointcut`、`@Before` 和 `@After` 注解的方法，使用 JDK 动态代理生成代理对象，并更新 Bean 工厂。  \n\n注意，在处理被 `@Aspect` 注解的类之前，Bean 工厂中的对象依赖已经设置过了就旧的 Bean，更新了 Bean 工厂中的对象后，需要通知依赖了被更新对象的对象重新初始化。  \n\n例如对象 A 依赖对象 B，即 A 的类中有一句\n\n```java\n@AutoWired\nB b;\n```\n\n同时，一个切面类中的切点 `@Pointcut` 的值指向了 B 类对象，然后他像 Bean 工厂更新了 B 对象，但这时 A 中引用的 B 对象，还是之前的旧 B 对象。  \n\n这里我的解决方式是，将带有 `@AutoWired` 属性的类保存起来，处理好 AOP 关系之后，再次初始化这些类，这样他们就能从 Bean 工厂获得新的已经被代理过的对象了。    \n\n至于如何使用 JDK 动态代理处理 AOP 关系的，请参考 [GitHub ProxyDyna 类](https://github.com/czwbig/mini-spring/blob/master/framework/src/main/java/com/caozhihu/spring/aop/ProxyDyna.java)\n中代码，总的来说是，定义一个 `ProxyDyna` 类实现 `InvocationHandler` 接口，然后实现 `invoke()` 方法即可，在 `invoke()` 方法中处理代理增强逻辑。  \n\n然后获取对象的时候，使用 `Proxy.newProxyInstance()` 方法而不是直接 new，如下：  \n\n```java\nProxy.newProxyInstance(target.getClass().getClassLoader(),\n                target.getClass().getInterfaces(), this);\n```  \n\n\n### 初始化Handler\n\nHandlerManager 类中调用 `parseHandlerFromController()` 方法来遍历处理所有的已扫描到的类，来初始化 MappingHandler 对象，方法代码如下：  \n\n```java\nprivate static void parseHandlerFromController(Class\u003c?\u003e aClass) {\n        Method[] methods = aClass.getDeclaredMethods();\n        // 只处理包含了 @RequestMapping 注解的方法\n        for (Method method : methods) {\n            if (method.isAnnotationPresent(RequestMapping.class)) {\n                // 获取赋值 @RequestMapping 注解的值，也就是客户端请求的路径，注意，不包括协议名和主机名\n                String uri = method.getDeclaredAnnotation(RequestMapping.class).value();\n                List\u003cString\u003e params = new ArrayList\u003c\u003e();\n                for (Parameter parameter : method.getParameters()) {\n                    if (parameter.isAnnotationPresent(RequestParam.class)) {\n                        params.add(parameter.getAnnotation(RequestParam.class).value());\n                    }\n                }\n\n                // List.toArray() 方法传入与 List.size() 恰好一样大的数组，可以提高效率\n                String[] paramsStr = params.toArray(new String[params.size()]);\n                MappingHandler mappingHandler = new MappingHandler(uri, aClass, method, paramsStr);\n                HandlerManager.mappingHandlerList.add(mappingHandler);\n            }\n        }\n    }\n```\n\nMappingHandler 对象表示如何处理一次请求，包括请求 uri，应该调用的类，应该调用的方法以及方法参数。  \n\n如此，在 MappingHandler 的 `handle()` 方法中处理请求，直接从 Bean 工厂获取指定类对象，从 response 对象中获取请求参数值，使用反射调用对应方法，并接收方法返回值输出给浏览器即可。  \n\n再回顾我们启动 tomcat 服务器时指定运行的 servlet：  \n\n```java\n@Override\n    public void service(ServletRequest req, ServletResponse res) throws IOException {\n        for (MappingHandler mappingHandler : HandlerManager.mappingHandlerList) {\n            // 从所有的 MappingHandler 中逐一尝试处理请求，\n            // 如果某个 handler 可以处理(返回true)，则返回即可\n            try {\n                if (mappingHandler.handle(req, res)) {\n                    return;\n                }\n            } catch (IllegalAccessException e) {\n                e.printStackTrace();\n            } catch (InvocationTargetException e) {\n                e.printStackTrace();\n            }\n        }\n        res.getWriter().println(\"failed!\");\n    }\n```\n\n一目了然，其 `service()` 方法只是遍历所有的 MappingHandler 对象来处理请求而已。\n\n# 框架使用\n\n测试使用 IOC 和 AOP 功能。这里以定义一个 /rap 路径举例，\n\n**1. 定义Controller**  \n\n\n```java\n@Controller\npublic class RapController {\n    @AutoWired\n    private Rap rapper;\n\n    @RequestMapping(\"/rap\")\n    public String rap() {\n        rapper.rap();\n        return \"CXK\";\n    }\n}\n```\n\nRapController 从 Bean 工厂获取一个 Rap 对象，访问 /rap 路径是，会先执行该对象的 `rap()` 方法，然后返回 \"CXK\" 给浏览器。  \n\n**2. 定义 Rap 接口及其实现类**\n\n```java\npublic interface Rap {\n    void rap();\n}\n// ----another file----\n@Component\npublic class Rapper implements Rap {\n    public void rap() {\n        System.out.println(\"CXK rapping...\");\n    }\n}\n```\n\n接口一定要定义，否则无法使用 AOP，因为我们使用的是 JDK 动态代理，只能代理实现了接口的类（原理是生成一个该接口的增强带向）。Spring 使用的是 JDK 动态代理和 CGLIB 两种方式，CGLIB 可以直接使用 ASM 等字节码生成框架，来生成一个被代理对象的增强子类。  \n\n使用浏览器访问 `http://localhost:8080/rap` ，即可看到 IDE 控制台输出 `CXK rapping...`，可以看到，`@AutoWired` 注解成功注入了对象。  \n\n但如果我们想在 rap 前面先 唱、跳，并且在 rap 后面打篮球，那么就需要定义织面类来面向切面编程。  \n\n定义一个 `RapAspect` 类如下：\n\n```java\n@Aspect\n@Component\npublic class RapAspect {\n\n    // 定义切点，spring的实现中，\n    // 此注解可以使用表达式 execution() 通配符匹配切点，\n    // 简单起见，我们先实现明确到方法的切点\n    @Pointcut(\"com.caozhihu.spring.service.serviceImpl.Rapper.rap()\")\n    public void rapPoint() {\n    }\n\n    @Before(\"rapPoint()\")\n    public void singAndDance() {\n        // 在 rap 之前要先唱、跳\n        System.out.println(\"first,singing \u003cchicken is too beautiful\u003e.\");\n        System.out.println(\"and the chicken monster is dancing now.\");\n    }\n\n    @After(\"rapPoint()\")\n    public void basketball() {\n        // 在 rap 之后别忘记了篮球\n        System.out.println(\"oh! Don't forget my favorite basketball.\");\n    }\n}\n```\n\n织面类 RapAspect 定义了切入点以及前置后置通知等，这样 RapController 中使用 `@AutoWired` 注解引入的 Rap 对象，会被替换为增强的 Rap 代理对象，如此，我们无需改动 RapController 中任何一处代码，就实现了在 `rap()` 方法前后执行额外的代码（通知）。\n\n增加 RapAspect 后，再次访问会在 IDE 控制台输出：  \n\n```text\nfirst,singing \u003cchicken is too beautiful\u003e.\nand the chicken monster is dancing now.\nCXK rapping...\noh! Don't forget my favorite basketball.\n```  \n\n\n# 总结与参考\n\n没啥好说的了\n\n### 参考\ntomcat 使用与框架图：[手写一个简化版Tomcat](https://yq.aliyun.com/articles/630266?utm_content=m_1000014411)\ngradle 配置与 DI 部分实现：[慕课网](https://s.imooc.com/AjZGHfE)\nSpring 常用注解 [how2j SPRING系列教材](http://how2j.cn/k/spring/spring-annotation-ioc-di/1067.html?p=55563)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fczwbig%2Fmini-spring","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fczwbig%2Fmini-spring","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fczwbig%2Fmini-spring/lists"}