当前位置:首页 > 技术 > 正文内容

基于 XML 配置实现 Java 控制反转(IOC)的底层原理

访客 技术 2026年8月9日 1

在传统的 JavaEE 开发中,业务逻辑层(Service/Biz)通常需要手动获取持久层(DAO)的实例。传统的做法是通过工厂模式直接调用获取方法,但这会导致代码耦合度较高。通过引入控制反转(IOC)的思想,我们可以让容器自动将 DAO 注入到 Service 中,从而实现解耦。本文将探讨如何通过解析 XML 配置文件,结合 Java 反射机制手动实现一个简易的 IOC 容器。

基础:解析 XML 配置文件

在实现对象注入之前,首先需要掌握如何解析 XML 数据。Java 提供了多种 XML 解析方式,这里我们使用标准的 DOM 接口进行处理。以下是一个简单的 XML 示例及其解析逻辑。

<?xml version="1.0" encoding="UTF-8"?>
<registry>
    <entity id="101">
        <name>Archer</name>
        <level>A</level>
    </entity>
    <entity id="102">
        <name>Lancer</name>
        <level>B</level>
    </entity>
</registry>

对应的解析代码实现如下,通过递归遍历节点并根据节点类型输出内容:

public class SimpleXmlReader {
    public static void main(String[] args) throws Exception {
        InputStream is = SimpleXmlReader.class.getResourceAsStream("/registry.xml");
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db = dbf.newDocumentBuilder();
        Document doc = db.parse(is);

        Element root = doc.getDocumentElement();
        traverseNodes(root);
    }

    private static void traverseNodes(Node node) {
        short type = node.getNodeType();
        if (type == Node.ELEMENT_NODE) {
            Element el = (Element) node;
            System.out.print("<" + el.getTagName());
            NamedNodeMap attrs = el.getAttributes();
            for (int i = 0; i < attrs.getLength(); i++) {
                Node attr = attrs.item(i);
                System.out.print(" " + attr.getNodeName() + "=\"" + attr.getNodeValue() + "\"");
            }
            System.out.print(">");
            NodeList children = el.getChildNodes();
            for (int j = 0; j < children.getLength(); j++) {
                traverseNodes(children.item(j));
            }
            System.out.print("</" + el.getTagName() + ">");
        } else if (type == Node.TEXT_NODE) {
            String val = node.getNodeValue().trim();
            if (!val.isEmpty()) System.out.print(val);
        }
    }
}

进阶:对象实例化与 Setter 注入

实现 IOC 的核心是利用反射。我们通过读取配置文件中的全类名,使用 Class.forName().newInstance() 创建实例。接着,通过解析 property 标签获取属性名和属性值,拼接成 Setter 方法名并进行调用。

定义一个简单的 Bean 类:

public class Member {
    private String uid;
    private String nickname;

    public void setUid(String uid) { this.uid = uid; }
    public void setNickname(String nickname) { this.nickname = nickname; }
    
    @Override
    public String toString() {
        return "Member{uid='" + uid + "', nickname='" + nickname + "'}";
    }
}

对应的 beans-config.xml 如下:

<beans>
    <bean class="com.example.Member">
        <property name="uid" value="U001" />
        <property name="nickname" value="Echo" />
    </bean>
</beans>

通过反射实现自动赋值的逻辑:

public void injectProperties(Object target, String fieldName, String fieldValue) {
    try {
        String methodName = "set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
        Method[] methods = target.getClass().getMethods();
        for (Method m : methods) {
            if (m.getName().equals(methodName)) {
                m.invoke(target, fieldValue);
                break;
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

实战:实现 Service 与 DAO 的依赖注入

在生产环境下,我们需要一个容器来管理单例对象。首先在 XML 中定义 Bean 的 ID、类路径以及依赖关系(使用 ref 属性)。

<?xml version="1.0" encoding="UTF-8"?>
<beans>
    <bean id="userDao" class="com.example.dao.UserDaoImpl" />
    <bean id="userService" class="com.example.service.UserServiceImpl">
        <property name="userDao" ref="userDao" />
    </bean>
</beans>

构建 BeanFactory 容器,其初始化过程分为两步: 1. 扫描与实例化:遍历所有的 bean 标签,根据 class 属性创建对象,并存入 Map 缓存。 2. 依赖装配:再次遍历节点,解析 property 中的 ref 属性,从 Map 中提取已实例化的对象并注入。

public class BeanContainer {
    private static Map<String, Object> singletonPool = new HashMap<>();

    static {
        loadBeans("/application-context.xml");
    }

    public static void loadBeans(String path) {
        try {
            InputStream is = BeanContainer.class.getResourceAsStream(path);
            Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(is);
            NodeList beanNodes = doc.getElementsByTagName("bean");

            // 阶段一:实例化
            for (int i = 0; i < beanNodes.getLength(); i++) {
                Element node = (Element) beanNodes.item(i);
                String id = node.getAttribute("id");
                String clazz = node.getAttribute("class");
                singletonPool.put(id, Class.forName(clazz).newInstance());
            }

            // 阶段二:装配依赖
            for (int i = 0; i < beanNodes.getLength(); i++) {
                Element node = (Element) beanNodes.item(i);
                String id = node.getAttribute("id");
                Object currentBean = singletonPool.get(id);

                NodeList props = node.getElementsByTagName("property");
                for (int j = 0; j < props.getLength(); j++) {
                    Element p = (Element) props.item(j);
                    String pName = p.getAttribute("name");
                    String pRef = p.getAttribute("ref");

                    if (!pRef.isEmpty()) {
                        Object dependency = singletonPool.get(pRef);
                        performInjection(currentBean, pName, dependency);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void performInjection(Object bean, String name, Object value) throws Exception {
        String setterName = "set" + name.substring(0, 1).toUpperCase() + name.substring(1);
        Method method = bean.getClass().getMethod(setterName, value.getClass().getInterfaces()[0]); 
        // 简化处理,假设参数类型匹配接口
        method.invoke(bean, value);
    }
}

这种模式模仿了 Spring 框架早期的实现方式。在 Web 应用中,可以在 Servlet 容器启动时(如通过 ServletContextListener)触发该加载流程。虽然手动实现效率与完善度不及成熟框架,但通过这个过程可以深入理解 Java 反射机制在解耦设计中的关键作用。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。