系列文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 配置,不需要额外文件。加个 @beanBeanFactory 就管你的生命周期;加个 @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替代 #技术复盘

更多推荐