05 策略+工厂优雅处理(解析 XML 文件、实现Bean 自动注入)

89 阅读5分钟

前言

桃李春风一杯酒,江南夜雨十年灯。

前一节我们已经完成了Bean的属性注入。但是在测试的过程中,能很明显的感觉到,需要配置初始化很多的属性。spring 解决的问题就是 Bean 的管理问题,所以肯定不会让大家去这样操作滴。 为了解决这个问题,spring提供了 通过 xml文件配置的方式完成bean的注入。通过在xml中配置bean的基本信息,来解决bean的注入问题。简单了解下xml,我们看一下 GPT 的回答。(看起来很不错!)

XML是什么?:XML是一种可扩展标记语言(Extensible Markup Language),用于描述和传输数据。它是一种文本格式,类似于HTML,但被设计为更通用的标记语言,可用于表示任何结构化数据。XML的语法规则要求所有元素必须有一个开始标签和一个结束标签,并且标签必须成对出现。它的主要用途是在网络上传输数据,但也可以用于本地数据存储和处理。XML被广泛应用于Web服务、电子商务、数据库管理、配置文件等领域。

实现

资源解析、注册、加载

该功能作为 spring 的一个增强的功能,主要的话就是 两个模块 资源解析与注册资源加载资源加载是一个独立的模块,通过 资源注册与解析,来与给spring提供服务

资源加载

定义 cn.anoxia.springframework.core.io.Resource 接口,提供统一闸口方法。

/**
* 资源加载接口
*/
public interface Resource {

    InputStream getInputStream() throws IOException;

}

资源加载的方式通常有多中,不同的方式只需要 实现 getInputStream 方法,调用着不必关心内部的实现,实现隔离。这里使用 策略模式+工厂模式 去设计。(通常遇到这种有多种不同的处理方式、我们都可以考虑使用策略+工厂来设计代码结构)。这样设计的好处就是。

  • 1、每一种不同的类型的 职责的单一 的,只需要管理自己的处理逻辑。
  • 2、如果要新增一种处理逻辑,只需要添加一个新的类就可以,不会相互污染。
  • 3、用工厂可以很方便的管理起来所以的 实现类

我们主要使用的是三种资源加载的方式,classpath 下 的文件加载、系统文件加载、网络文件加载

ClassPathResource 文件加载器

加载 classpath 下的文件

/**
 * @author huangle
 * @date 2023/2/9 16:42
 */
public class ClassPathResource implements Resource{

    private final String path;

    private ClassLoader classLoader;


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

    public ClassPathResource(String path, ClassLoader classLoader) {
        Assert.assertNotNull(path,"path not null!");
        this.path = path;
        this.classLoader = (classLoader != null ? classLoader : ClassUtil.getContextClassLoader());
    }

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

SystemFileResource 加载器

通过文件,或者文件路径来加载系统文件

/**
 * @author huangle
 * @date 2023/2/9 16:42
 */
public class FileSystemResource implements Resource{

    private final File file;
    private final String path;


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

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

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

    public final String getPath() {
        return path;
    }
}

UrlResource 加载器

加载网络上的资源

public class UrlResource implements Resource {

    private final URL url;

    public UrlResource(URL url) {
        Assert.assertNotNull("url must not be null", url);
        this.url = url;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        URLConnection connection = this.url.openConnection();
        try {
            return connection.getInputStream();
        } catch (IOException e) {
            // 关闭链接-资源
            if (connection instanceof HttpURLConnection) {
                ((HttpURLConnection) connection).disconnect();
            }
            throw e;
        }
    }
}

使用时,统一使用到的是 ResourceLoader资源包装器,这个接口提供获取资源的方式。

注:这里 ResourceLoader 担任的其实就是一个 工厂的职责。这是还抽了一层给下层去完成了。

/**
 * 资源包装器
 */
public interface ResourceLoader {

    String CLASSPATH_URL_PREFIX = "classpath:";

    Resource getResource(String location);

}

默认的实现类 完成资源的 获取 DefaultResourceLoader。(工厂的具体处理逻辑)

/**
 * 封装获取资源加载器,实现细节不暴露给外部,只暴露接口
 * @author huangle
 * @date 2023/2/10 10:50
 */
public class DefaultResourceLoader implements ResourceLoader {
    @Override
    public Resource getResource(String location) {
        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(location);
            }
        }
    }
}

资源的解析与注册

BeanDefinitionReader 定义加载注册资源的方式。

/**
 * bean 定义读取接口
 * 这里需要注意 getRegistry()、getResourceLoader(),
 * 都是用于提供给后面三个方法的工具,加载和注册,这两个方法的实现会包装到抽象类中
 */
public interface BeanDefinitionReader {

    BeanDefinitionRegistry getRegistry();

    ResourceLoader getResourceLoader();

    void loadBeanDefinitions(Resource resource) throws BeansException;

    void loadBeanDefinitions(Resource... resources) throws BeansException;

    void loadBeanDefinitions(String location) throws BeansException;

}

BeanDefinitionReader 接口其实相当于 定义了所有的处理逻辑(接口定义行为、规范),如果对 接口和抽象类不是很理解。可以主要 理解一下 他们两者主要的用途

抽象类通常用于定义一些通用的方法,而接口通常用于定义一些规范和约定,以便多个类实现它们。

AbstractBeanDefinitionReader抽象类统一实现公共的方法,后面的只需要继承他,就可以获得一些公共参数的使用。这里使用抽象类的思想,是因为抽象类可以选择 实现接口的方法,不用强制去实现全部方法。

/**
 * @author huangle
 * @date 2023/2/10 10:59
 */
public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader{

    private final BeanDefinitionRegistry beanDefinitionRegistry;

    private ResourceLoader resourceLoader;

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

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

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

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

XmlBeanDefinitionReader具体加载实现,通过解析 xml 文件,来完成bean的注入

/**
 * @author huangle
 * @date 2023/2/10 11:02
 */
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {

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

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

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

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

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


    protected void doLoadBeanDefinitions(InputStream inputStream) throws Exception {
        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;
            }
            // 解析bean标签
            Element bean = (Element) childNodes.item(i);
            String id = bean.getAttribute("id");
            String name = bean.getAttribute("name");
            String calssName = bean.getAttribute("class");
            Class<?> clazz = Class.forName(calssName);
            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;
                }
                // 设置属性
                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 BeansException("Duplicate beanName[" + beanName + "] is not allowed");
            }
            // 注册bean
            getRegistry().registerBeanDefinition(beanName,beanDefinition);

        }
    }
}

测试

这个时候,我们只需要定义 DefaultListableBeanFactory, XmlBeanDefinitionReader 和配置 spring.xml 文件,就可以完成bean的注入。

@Test
    public void testXml() throws BeansException {
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        XmlBeanDefinitionReader definitionReader = new XmlBeanDefinitionReader(beanFactory);
        definitionReader.loadBeanDefinitions("classpath:spring.xml");
        UserService userService = (UserService) beanFactory.getBean("userService");
        System.out.println(userService.say());
    }
<?xml version="1.0" encoding="UTF-8"?>
<beans>

    <bean id="userDao" class="cn.anoxia.springframework.beans.factory.support.UserDao"/>

    <bean id="userService" class="cn.anoxia.springframework.beans.factory.support.UserService">
        <property name="name" value="Anoxia"/>
        <property name="userDao" ref="userDao"/>
    </bean>

</beans>