# 系列文1:为什么放弃成熟的Spring,我偏要手写轻量IOC容器?

张开发
2026/4/3 21:00:42 15 分钟阅读
# 系列文1:为什么放弃成熟的Spring,我偏要手写轻量IOC容器?
系列文1为什么放弃成熟的Spring我偏要手写轻量IOC容器非科班野生程序员深耕政务信息化20年这套自研Java Web框架支撑过省级新农保、全国首例跨省医保结算等核心民生系统18年稳定运行至今。本系列拆解10个核心架构决策全是政务场景踩坑后的实用解法不求优雅但求落地愿同赛道朋友少走弯路也欢迎懂行大佬轻拍指正。最后感谢豆包、智谱、OpenCode决策是我做的代码是我搓的文字是他们总结的。背景2012年前后Spring已经很成熟了。但我的项目部署环境比较特殊——客户的中间件五花八门有用 Tomcat 的有用 WebLogic 的还有国产中间件的。Spring 的某些功能在这种环境下配置起来很麻烦而且依赖太多出了问题排查困难。更重要的是我的场景其实很简单我只需要实例化、属性注入、路由注册这三件事。Spring 提供的东西远远超出了我的需要引入它反而增加了复杂度。我的方案自己写了一个BeanFactory只做三件事实例化、属性注入、路由注册。1. 三个自定义注解首先定义三个注解用来标注哪些类需要管理和哪些字段需要注入。bean—— 标记需要容器管理的类packagecom.browise.core.annotation;Target(ElementType.TYPE)Retention(RetentionPolicy.RUNTIME)publicinterfacebean{Stringid();booleansingleton()defaulttrue;}这个注解放到类上id就是 Bean 的唯一标识。启动时容器会根据这个 id 把实例放到一个 HashMap 里。property—— 标记需要注入的字段packagecom.browise.core.annotation;Target(ElementType.FIELD)Retention(RetentionPolicy.RUNTIME)publicinterfaceproperty{Stringtype()defaultref;Stringvalue()default;}typeref表示按引用注入从容器里找其他值就是直接赋值。responseMapping—— 标记路由映射packagecom.browise.core.annotation;Target({ElementType.TYPE,ElementType.METHOD})Retention(RetentionPolicy.RUNTIME)publicinterfaceresponseMapping{Stringkey();}类上的key是路径前缀方法上的key是具体路径。拼在一起就是一个完整的 URL 路由。2. BeanFactory 的启动过程BeanFactory的构造函数接收一个包名扫描这个包下的所有类然后三步走第一步实例化所有带bean注解的类publicBeanFactory(Stringurlpackage)throwsInstantiationException,IllegalAccessException,SecurityException,IllegalArgumentException,NoSuchMethodException,InvocationTargetException,ClassNotFoundException,IOException{/* 第一步实例化对象 */SetClass?listfindClassInPackage.getClasses(urlpackage);for(Class?cl:list){booleanisExistcl.isAnnotationPresent(bean.class);if(isExist){bean d(bean)cl.getAnnotation(bean.class);if(map.get(d.id())!null){System.out.println(bean冲突冲突id为d.id());System.in.read();System.exit(1);}// 如果有AOP代理注解也不是Controller路由代理后面创建使用代理if(cl.isAnnotationPresent(aoppoint.class)!cl.isAnnotationPresent(responseMapping.class)){map.put(d.id(),createproxy(cl));}else{try{map.put(d.id(),cl.newInstance());}catch(Exceptione){e.printStackTrace();}}}if(cl.isAnnotationPresent(responseType.class)){responseType t(responseType)cl.getAnnotation(responseType.class);responseFactory.getInstance().register(t.type(),(responseInterface?)cl.newInstance());}if(cl.isAnnotationPresent(cache.class)){cache c(cache)cl.getAnnotation(cache.class);cacheFactory.getInstance().register(c.type(),(cacheBase)cl.newInstance());}}这里有个细节如果一个类同时有aoppoint和responseMapping说明它既是 Controller 又需要代理。这时候不在这里创建代理等第三步路由注册完了再创建因为代理对象创建后还需要重新注入属性。第二步注入property属性/* 第二步注入属性 */for(Class?cl:list){booleanisExistcl.isAnnotationPresent(bean.class);if(isExist){bean d(bean)cl.getAnnotation(bean.class);getField(cl,map.get(d.id()));}}getField()方法遍历类中所有带property注解的字段根据type是ref还是其他值从 HashMap 里取或者直接赋值privatevoidgetField(Class?clazz,Objectinstance)throws...{Field[]fieldsclazz.getDeclaredFields();for(Fieldfield:fields){if(field.isAnnotationPresent(property.class)){property prop(property)field.getAnnotation(property.class);Stringkeyfield.getName();Stringfirstkey.substring(0,1);firstfirst.toUpperCase();keyfirstkey.substring(1);ObjectparamValues1[]newObject[1];ClassparamTypes1[]newClass[1];paramTypes1[0]field.getType();Methodmethod1null;try{method1clazz.getMethod(setkey,paramTypes1);}catch(Exceptione){method1null;}if(ref.equals(prop.type())){paramValues1[0]map.get(prop.value());}else{paramValues1[0]prop.value();}if(method1!null){method1.invoke(instance,paramValues1);}else{field.setAccessible(true);field.set(instance,paramValues1[0]);field.setAccessible(false);}}}}注意这里的处理优先用 setter 方法注入。如果 setter 方法不存在就直接反射设值。这样不管字段是 private 还是没有 setter都能注入成功。第三步扫描responseMapping注册路由/* 第三步扫描配置路由注册control */Iteratoritermap.entrySet().iterator();while(iter.hasNext()){Map.Entryentry(Map.Entry)iter.next();if(entry.getValue().getClass().isAnnotationPresent(responseMapping.class)){responseMapping r(responseMapping)entry.getValue().getClass().getAnnotation(responseMapping.class);getMethodes(entry.getValue(),r.key(),entry.getKey().toString());// 如果有AOP代理注解使用代理if(entry.getValue().getClass().isAnnotationPresent(aoppoint.class)){Objectproxycreateproxy(entry.getValue().getClass());// 因为代理是新创建的对象属性需要重新注入getField(entry.getValue().getClass(),proxy);map.put(entry.getKey().toString(),proxy);}}}}getMethodes()方法扫描类中所有带responseMapping的方法把类路径方法路径拼接成完整 URL注册到一个handlerMap里privatestaticvoidgetMethodes(Objectinstans,Stringkey,Stringid){Method[]methodsinstans.getClass().getMethods();if(methods!null){for(inti0;imethods.length;i){MethodMapmothodmapnewMethodMap();if(methods[i].isAnnotationPresent(responseMapping.class)){responseMapping mmethods[i].getAnnotation(responseMapping.class);Stringconnextkeym.key();ListClassparametertypeListgetMethodInfo(methods[i]);ListClassparameterGenerictypeListgetMethodGenericInfo(methods[i]);ListStringparameterNameListgetMethodParameterNamesByAsm4(instans.getClass(),methods[i]);mothodmap.setBeanid(id);mothodmap.setMothod(methods[i]);mothodmap.setParameterNameList(parameterNameList);mothodmap.setParametertypeList(parametertypeList);mothodmap.setParameterGenerictList(parameterGenerictypeList);handlerMap.put(connext,mothodmap);}if(methods[i].isAnnotationPresent(responseType.class)){responseType bmethods[i].getAnnotation(responseType.class);mothodmap.setReturnHandler(b.type());}}}}注意mothodmap这个变量名源码里就是拼写成mothodmap少了个 e但一直没改跑了十几年也没问题。这里还为每个方法保存了参数类型列表、泛型类型列表、参数名列表——参数名的获取就是系列文5要讲的 ASM 字节码技术。3. 业务代码怎么写有了这三个注解业务代码变得非常干净bean(iduserService)responseMapping(key/user)publicclassUserService{property(typeref,valueuserMapper)privateUserMapperuserMapper;responseMapping(key/query)Trans(readonlytrue)publicDataCenterqueryUser(DataCenterdc,HttpServletRequestrequest,HttpServletResponseresponse){// 业务逻辑}}不需要 XML 配置不需要额外文件。加个beanBeanFactory就管你的生命周期加个property依赖就自动注入加个responseMappingURL 路由就自动注册。4. 同时支持 bean.xml 配置考虑到历史兼容BeanFactory还保留了对bean.xml的支持。扫描注解完了之后还会尝试读取bean.xmltry{initFrombeanXml();}catch(DocumentExceptione){e.printStackTrace();}initFrombeanXml()用 dom4j 解析 XML把 bean 配置也加到 HashMap 里。这样老的 XML 配置方式和新的注解方式可以共存。整个容器的核心数据结构publicclassBeanFactory{privatestaticHashMapString,ObjectmapnewHashMapString,Object();privatestaticHashMapString,ClassclsMapnewHashMapString,Class();publicstaticObjectgetBean(Stringkey){returnmap.get(key);}}就这么简单——一个 HashMap 存实例一个 HashMap 存类信息。getBean()方法就是map.get(key)。为什么不用 Spring我的场景只需要 IOC 路由不需要 Spring 那套庞大的体系启动快扫描 new HashMap完事儿零配置不需要任何 XML 文件或额外的 jar完全可控出了问题我能从源码定位决策原则只加需要的不加不需要的。政务系统的特点是需求明确、变更频繁、部署环境复杂。用一个轻量可控的容器比用一个大而全的框架更实用。出了问题我能直接看源码定位到是哪一行出了错不用去猜 Spring 内部到底干了什么。如果这篇文章对你有启发欢迎点赞收藏。你在实际项目中是自己造轮子还是用现成框架欢迎评论区聊聊。系列导航上一篇[总纲Java Web自研框架18年架构决策复盘]下一篇[系列文2新老代码共存的终极解法——注解路由参数路由平滑迁移]作者许彰午| 非科班野生程序员深耕政务信息化20年标签#Java #IOC #自研框架 #注解 #BeanFactory #政务信息化 #Spring替代 #技术复盘

更多文章