chapter05_从spring.xml读取Bean

一、简化Bean的注册

如果每次注册一个Bean,都要像上节一样,手动写PropertyValues相关的代码,那太复杂了,我们希望读取XML文件,自动注册Bean,这样对于使用者,甚至不知道有BeanDefinition的存在

二、统一处理资源文件

新建资源接口,Spring对所有的资源文件,统一处理

  • 一个资源,最重要的就是拿到输入流,拿到输入流就可以读取文件
java 复制代码
public interface Resource {

    InputStream getInputStream() throws IOException;
}

提供三个资源实现类,分别读取不同类型的文件,这就是策略模式

类路径下的文件(最常用)

java 复制代码
public class ClassPathResource implements Resource{

    private final String path;
    private final ClassLoader classLoader;

    public ClassPathResource(String path) {
        this(path, null);
    }

    public ClassPathResource(String path, ClassLoader classLoader) {
        Assert.notNull(path, "Path must not be null");
        this.path = path;
        this.classLoader = classLoader != null ? classLoader : ClassUtil.getClassLoader();
    }

    @Override
    public InputStream getInputStream() throws IOException {
        InputStream is = classLoader.getResourceAsStream(path);
        if (is == null) {
            throw new FileNotFoundException(path + " cannot be opened because it does not exist");
        }
        return is;
    }
}

文件系统下的文件

java 复制代码
public class FileSystemResource implements Resource{

    private File file;

    public FileSystemResource(File file) {
        this.file = file;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        return Files.newInputStream(file.toPath());
    }
}

网络文件

java 复制代码
public class UrlResource implements Resource{

    private final URL url;

    public UrlResource(URL url) {
        Assert.notNull(url,"URL must not be null");
        this.url = url;
    }
    @Override
    public InputStream getInputStream() throws IOException {
        URLConnection con = url.openConnection();
        return con.getInputStream();
    }
}

资源加载器接口,简化资源类的使用,自动根据路径选择合适的加载类

  • 这又属于工厂方法设计模式
java 复制代码
/**
 * @Author 孤风雪影
 * @Email gitee.com/efairy520
 * @Date 2025/1/2 22:16
 * @Version 1.0
 */
public interface ResourceLoader {

    String CLASSPATH_URL_PREFIX = "classpath:";

    Resource getResource(String location);
}

资源加载器接口实现

  • 根据路径前缀,默认就是使用classpath策略
java 复制代码
/**
 * @Author 孤风雪影
 * @Email gitee.com/efairy520
 * @Date 2025/1/2 22:18
 * @Version 1.0
 */
public class DefaultResourceLoader implements ResourceLoader{

    @Override
    public Resource getResource(String location) {
        Assert.notNull(location, "Location must not be null");
        if (location.startsWith(CLASSPATH_URL_PREFIX)) {
            //使用类路径加载器,去掉前缀
            return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()));
        }
        else {
            try {
                URL url = new URL(location);
                return new UrlResource(url);
            } catch (MalformedURLException e) {
                return new FileSystemResource(new File(location));
            }
        }
    }
}

三、从文件中读取Bean

定义BeanDefinitionReader接口,从文件中读取BeanDefinition,并且注册到Bean工厂,这里有三要素

  • 资源文件
  • Bean工厂
  • 读取BeanDefinition的逻辑(单个资源,多个资源,位置字符串)
java 复制代码
/**
 * @Author 孤风雪影
 * @Email gitee.com/efairy520
 * @Date 2025/1/2 22:26
 * @Version 1.0
 */
public interface BeanDefinitionReader {
    BeanDefinitionRegistry getRegistry();

    ResourceLoader getResourceLoader();

    void loadBeanDefinitions(Resource resource);

    void loadBeanDefinitions(Resource... resources);

    void loadBeanDefinitions(String location);
}

用抽象类AbstractBeanDefinitionReader实现接口,模板方法设计模式

  • Bean工厂和资源加载器都是确定的,抽象类直接实现
  • 只有加载BeanDefinition是不确定的逻辑,交给具体的策略子类实现
java 复制代码
public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader {

    private final BeanDefinitionRegistry registry;

    private final ResourceLoader resourceLoader;

    public AbstractBeanDefinitionReader(BeanDefinitionRegistry registry, ResourceLoader resourceLoader) {
        this.registry = registry;
        this.resourceLoader = resourceLoader;
    }

    public AbstractBeanDefinitionReader(BeanDefinitionRegistry registry) {
        this(registry, new DefaultResourceLoader());
    }

    @Override
    public BeanDefinitionRegistry getRegistry() {
        return registry;
    }

    @Override
    public ResourceLoader getResourceLoader() {
        return resourceLoader;
    }

}

XmlBeanDefinitionReader做具体实现,策略模式

java 复制代码
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {

    public XmlBeanDefinitionReader(BeanDefinitionRegistry registry, ResourceLoader resourceLoader) {
        super(registry, resourceLoader);
    }

    public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
        super(registry);
    }

    @Override
    public void loadBeanDefinitions(Resource resource) {
        try {
            InputStream is = resource.getInputStream();
            doLoadBeanDefinitions(is);
        } catch (IOException e) {
            throw new RuntimeException("IOException parsing XML document from " + resource, e);
        }
    }

    @Override
    public void loadBeanDefinitions(Resource... resources) {
        for (Resource resource : resources) {
            loadBeanDefinitions(resource);
        }
    }

    @Override
    public void loadBeanDefinitions(String location) {
        ResourceLoader resourceLoader = getResourceLoader();
        Resource resource = resourceLoader.getResource(location);
        loadBeanDefinitions(resource);
    }

    /**
     * 真正解析XMl文件的方法
     *
     * @param inputStream
     */
    private void doLoadBeanDefinitions(InputStream inputStream) {
        Document doc = XmlUtil.readXML(inputStream);
        Element root = doc.getDocumentElement();
        NodeList childNodes = root.getChildNodes();
        for (int i = 0; i < childNodes.getLength(); i++) {
            // 判断元素
            if (!(childNodes.item(i) instanceof Element)) continue;
            // 判断对象
            if (!"bean".equals(childNodes.item(i).getNodeName())) continue;

            // 解析标签
            Element bean = (Element) childNodes.item(i);
            String id = bean.getAttribute("id");
            String name = bean.getAttribute("name");
            String className = bean.getAttribute("class");
            // 获取 Class,方便获取类中的名称
            Class<?> clazz = null;
            try {
                clazz = Class.forName(className);
            } catch (ClassNotFoundException e) {
                throw new RuntimeException("不存在的类名" + className);
            }
            // 优先级 id > name,此处是Bean自己的id和name
            String beanName = StrUtil.isNotEmpty(id) ? id : name;
            if (StrUtil.isEmpty(beanName)) {
                beanName = StrUtil.lowerFirst(clazz.getSimpleName());
            }

            // 定义Bean
            BeanDefinition beanDefinition = new BeanDefinition(clazz);
            // 读取属性并填充
            for (int j = 0; j < bean.getChildNodes().getLength(); j++) {
                if (!(bean.getChildNodes().item(j) instanceof Element)) continue;
                if (!"property".equals(bean.getChildNodes().item(j).getNodeName())) continue;
                // 解析标签:property
                Element property = (Element) bean.getChildNodes().item(j);
                String attrName = property.getAttribute("name");
                String attrValue = property.getAttribute("value");
                String attrRef = property.getAttribute("ref");
                // 获取属性值:引入对象、值对象
                Object value = StrUtil.isNotEmpty(attrRef) ? new BeanReference(attrRef) : attrValue;
                // 创建属性信息
                PropertyValue propertyValue = new PropertyValue(attrName, value);
                beanDefinition.getPropertyValues().addPropertyValue(propertyValue);
            }
            if (getRegistry().containsBeanDefinition(beanName)) {
                throw new RuntimeException("Duplicate beanName[" + beanName + "] is not allowed");
            }
            // 注册 BeanDefinition
            getRegistry().registerBeanDefinition(beanName, beanDefinition);
        }
    }
}

BeanDefinitionReader接口、资源接口,层次结构图

四、测试

新建Person类

java 复制代码
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class Person {
    private String name;
    private int age;
    private Cat cat;
}

新建Cat类

java 复制代码
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class Cat {
    private String name;
    private int weight;
}

编写一个spring.xml文件

java 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans>

    <bean id="cat" class="cn.shopifymall.springframework.test.bean.Cat">
        <property name="name" value="tomcat"/>
        <property name="weight" value="2000"/>
    </bean>

    <bean id="person" class="cn.shopifymall.springframework.test.bean.Person">
        <property name="name" value="10001"/>
        <property name="age" value="18"/>
        <property name="cat" ref="cat"/>
    </bean>

</beans>

新建测试类

java 复制代码
public class ApiTest {

    @Test
    public void testGetBeanFromXml() {
        // 1.初始化 BeanFactory
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

        // 2. 读取配置文件&注册Bean
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
        reader.loadBeanDefinitions("classpath:spring.xml");

        // 3. 获取Bean对象调用方法
        Person person = (Person) beanFactory.getBean("person");
        System.out.println("person:" + person);
    }
}

控制台输出

java 复制代码
person:Person(name=10001, age=18, cat=Cat(name=tomcat, weight=2000))

五、总结

  • 通过引入spring.xml配置文件,我们就可以简化Bean的注册
  • 用户只需要编写一个xml文件,由XmlBeanDefinitionReader自动解析xml文件,生成BeanDefinition并注册到BeanFactory