系列文1:为什么放弃成熟的Spring,我偏要手写轻量IOC容器?
系列文1:为什么成熟 Spring 不用,偏要手写轻量 IOC?一位政务老程序员的真实选择
非科班野生程序员,深耕政务信息化20年,这套自研Java Web框架支撑过省级新农保、全国跨省医保结算等核心民生系统,18年稳定运行至今。本系列拆解10个核心架构决策,全是政务场景踩坑后的实用解法,不求优雅但求落地,愿同赛道朋友少走弯路,也欢迎懂行大佬轻拍指正。最后感谢豆包、智谱、OpenCode,决策是我做的,代码是我搓的,文字是他们总结的。
文章目录
背景
2012年前后,Spring已经很成熟了。但我的项目部署环境比较特殊——客户的中间件五花八门,有用 Tomcat 的,有用 WebLogic 的,还有国产中间件的。Spring 的某些功能在这种环境下配置起来很麻烦,而且依赖太多,出了问题排查困难。
更重要的是,我的场景其实很简单:我只需要实例化、属性注入、路由注册这三件事。Spring 提供的东西远远超出了我的需要,引入它反而增加了复杂度。
我的方案
自己写了一个 BeanFactory,只做三件事:实例化、属性注入、路由注册。
1. 三个自定义注解
首先定义三个注解,用来标注"哪些类需要管理"和"哪些字段需要注入"。
@bean —— 标记需要容器管理的类
package com.browise.core.annotation;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface bean {
String id();
boolean singleton() default true;
}
这个注解放到类上,id 就是 Bean 的唯一标识。启动时容器会根据这个 id 把实例放到一个 HashMap 里。
@property —— 标记需要注入的字段
package com.browise.core.annotation;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface property {
String type() default "ref";
String value() default "";
}
type="ref" 表示按引用注入(从容器里找),其他值就是直接赋值。
@responseMapping —— 标记路由映射
package com.browise.core.annotation;
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface responseMapping {
String key();
}
类上的 key 是路径前缀,方法上的 key 是具体路径。拼在一起就是一个完整的 URL 路由。
2. BeanFactory 的启动过程
BeanFactory 的构造函数接收一个包名,扫描这个包下的所有类,然后三步走:
第一步:实例化所有带 @bean 注解的类
public BeanFactory(String urlpackage)
throws InstantiationException, IllegalAccessException, SecurityException, IllegalArgumentException,
NoSuchMethodException, InvocationTargetException, ClassNotFoundException, IOException {
/* 第一步:实例化对象 */
Set<Class<?>> list = findClassInPackage.getClasses(urlpackage);
for (Class<?> cl : list) {
boolean isExist = cl.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 (Exception e) {
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) {
boolean isExist = cl.isAnnotationPresent(bean.class);
if (isExist) {
bean d = (bean) cl.getAnnotation(bean.class);
getField(cl, map.get(d.id()));
}
}
getField() 方法遍历类中所有带 @property 注解的字段,根据 type 是 "ref" 还是其他值,从 HashMap 里取或者直接赋值:
private void getField(Class<?> clazz, Object instance) throws ... {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(property.class)) {
property prop = (property) field.getAnnotation(property.class);
String key = field.getName();
String first = key.substring(0, 1);
first = first.toUpperCase();
key = first + key.substring(1);
Object paramValues1[] = new Object[1];
Class paramTypes1[] = new Class[1];
paramTypes1[0] = field.getType();
Method method1 = null;
try {
method1 = clazz.getMethod("set" + key, paramTypes1);
} catch (Exception e) {
method1 = null;
}
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 */
Iterator iter = map.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (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)) {
Object proxy = createproxy(entry.getValue().getClass());
// 因为代理是新创建的对象,属性需要重新注入
getField(entry.getValue().getClass(), proxy);
map.put(entry.getKey().toString(), proxy);
}
}
}
}
getMethodes() 方法扫描类中所有带 @responseMapping 的方法,把类路径+方法路径拼接成完整 URL,注册到一个 handlerMap 里:
private static void getMethodes(Object instans, String key, String id) {
Method[] methods = instans.getClass().getMethods();
if (methods != null) {
for (int i = 0; i < methods.length; i++) {
MethodMap mothodmap = new MethodMap();
if (methods[i].isAnnotationPresent(responseMapping.class)) {
responseMapping m = methods[i].getAnnotation(responseMapping.class);
String connext = key + m.key();
List<Class> parametertypeList = getMethodInfo(methods[i]);
List<Class> parameterGenerictypeList = getMethodGenericInfo(methods[i]);
List<String> parameterNameList = getMethodParameterNamesByAsm4(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 b = methods[i].getAnnotation(responseType.class);
mothodmap.setReturnHandler(b.type());
}
}
}
}
注意 mothodmap 这个变量名,源码里就是拼写成 mothodmap(少了个 e),但一直没改,跑了十几年也没问题。这里还为每个方法保存了参数类型列表、泛型类型列表、参数名列表——参数名的获取就是系列文5要讲的 ASM 字节码技术。
3. 业务代码怎么写
有了这三个注解,业务代码变得非常干净:
@bean(id = "userService")
@responseMapping(key = "/user")
public class UserService {
@property(type = "ref", value = "userMapper")
private UserMapper userMapper;
@responseMapping(key = "/query")
@Trans(readonly = true)
public DataCenter queryUser(DataCenter dc, HttpServletRequest request,
HttpServletResponse response) {
// 业务逻辑
}
}
不需要 XML 配置,不需要额外文件。加个 @bean,BeanFactory 就管你的生命周期;加个 @property,依赖就自动注入;加个 @responseMapping,URL 路由就自动注册。
4. 同时支持 bean.xml 配置
考虑到历史兼容,BeanFactory 还保留了对 bean.xml 的支持。扫描注解完了之后,还会尝试读取 bean.xml:
try {
initFrombeanXml();
} catch (DocumentException e) {
e.printStackTrace();
}
initFrombeanXml() 用 dom4j 解析 XML,把 bean 配置也加到 HashMap 里。这样老的 XML 配置方式和新的注解方式可以共存。
整个容器的核心数据结构
public class BeanFactory {
private static HashMap<String, Object> map = new HashMap<String, Object>();
private static HashMap<String, Class> clsMap = new HashMap<String, Class>();
public static Object getBean(String key) {
return map.get(key);
}
}
就这么简单——一个 HashMap 存实例,一个 HashMap 存类信息。getBean() 方法就是 map.get(key)。
为什么不用 Spring
- 我的场景只需要 IOC + 路由,不需要 Spring 那套庞大的体系
- 启动快:扫描 + new + HashMap,完事儿
- 零配置:不需要任何 XML 文件或额外的 jar
- 完全可控:出了问题我能从源码定位
决策原则
只加需要的不加不需要的。
政务系统的特点是需求明确、变更频繁、部署环境复杂。用一个轻量可控的容器,比用一个大而全的框架更实用。出了问题,我能直接看源码定位到是哪一行出了错,不用去猜 Spring 内部到底干了什么。
如果这篇文章对你有启发,欢迎点赞收藏。你在实际项目中是自己造轮子还是用现成框架?欢迎评论区聊聊。
系列导航:
作者:许彰午 | 非科班野生程序员,深耕政务信息化20年
标签: #Java #IOC #自研框架 #注解 #BeanFactory #政务信息化 #Spring替代 #技术复盘
更多推荐
所有评论(0)