Spring Ioc 源码分析之Bean的加载和构造

我们都知道,Spring Ioc和Aop是Spring的核心的功能,因此花一点时间去研究还是很有意义的,如果仅仅是知其所以然,也就体会不到大师设计Spring的精华,还记得那句话,Spring为JavaEE开发带来了春天。

IOC就是Inversion of control 也就是控制反转的意思,另一种称呼叫做依赖注入,这个可能更直观一点,拿个例子来说吧:

@Component
public class UserService {
    @Autowired
    private UserMapper mapper;
}

比如在UserService可能要调用一个Mapper,这个Mapper去做DAO的操作,在这里我们直接通过@Autowired注解去注入这个Mapper,这个就叫做依赖注入,你想要什么就注入什么,不过前提它是一个Bean。至于是怎么注入的,那是Spring容器做的事情,也是我们今天去探索的。

在进行分析之前,我先声明一下,下面的这些代码并不是从spring 源码中直接拿过来,而是通过一步步简化,抽取spring源码的精华,如果直接贴源码,我觉得可能很多人都会被吓跑,而且还不一定能够学到真正的东西。

Spring要去管理Bean首先要把Bean放到容器里,那么Spring是如何获得Bean的呢?

  • 首先,Spring有一个数据结构,BeanDefinition,这里存放的是Bean的内容和元数据,保存在BeanFactory当中,包装Bean的实体:
public class BeanDefinition {
    //真正的Bean实例
    private Object bean;
    //Bean的类型信息
    private Class beanClass;
    //Bean类型信息的名字
    private String beanClassName;
    //用于bean的属性注入 因为Bean可能有很多熟属性
    //所以这里用列表来进行管理
    private PropertyValues propertyValues = new PropertyValues();
}
  • PerpertyValues存放Bean的所有属性
public class PropertyValues {
    private final List<PropertyValue> propertyValueList = new ArrayList<PropertyValue>();
}
  • PropertyValue存放的是每个属性,可以看到两个字段,name和valu。name存放的就是属性名称,value是object类型,可以是任何类型
public class PropertyValue {
    private final String name;
    private final Object value;
}

定义好这些数据结构了,把Bean装进容器的过程,其实就是其BeanDefinition构造的过程,那么怎么把一些类装入的Spring容器呢?

  • Spring有个接口就是获取某个资源的输入流,获取这个输入流后就可以进一步处理了:
public interface Resource {
    InputStream getInputStream() throws IOException;
}
  • UrlResource 是对Resource功能的进一步扩展,通过拿到一个URL获取输入流。
public class UrlResource implements Resource {
    private final URL url;
    public UrlResource(URL url) {
        this.url = url;
    }
    @Override
    public InputStream getInputStream() throws IOException{
        URLConnection urlConnection = url.openConnection();
        urlConnection.connect();
        return urlConnection.getInputStream();
    }
  • ResourceLoader是资源加载的主要方法,通过location定位Resource,
    然后通过上面的UrlResource获取输入流:
public class ResourceLoader {
    public Resource getResource(String location){
        URL resource = this.getClass().getClassLoader().getResource(location);
        return new UrlResource(resource);
    }
}
  • 大家可能会对上面的这段代码产生疑问:
     URL resource = this.getClass().getClassLoader().getResource(location);

为什么通过得到一个类的类类型,然后得到对应的类加载器,然后调用类加载器的Reource怎么就得到了URL这种类型呢?
我们来看一下类加载器的这个方法:

 public URL getResource(String name) {
        URL url;
        if (parent != null) {
            url = parent.getResource(name);
        } else {
            url = getBootstrapResource(name);
        }
        if (url == null) {
            url = findResource(name);
        }
        return url;
    }

从这个方法中我们可以看出,类加载器去加载资源的时候,先会去让父类加载器去加载,如果父类加载器没有的话,会让根加载器去加载,如果这两个都没有加载成功,那就自己尝试去加载,这个一方面为了java程序的安全性,不可能你用户自己随便写一个加载器,就用你用户的。

  • 接下来我们看一下重要角色,这个是加载BeanDefinition用的。
public interface BeanDefinitionReader {
    void loadBeanDefinitions(String location) throws Exception;
}
  • 这个接口是用来从配置中读取BeanDefinition:
    其中registry key是bean的id,value存放资源中所有的BeanDefinition,
public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader {

    private Map<String,BeanDefinition> registry;

    private ResourceLoader resourceLoader;

    protected AbstractBeanDefinitionReader(ResourceLoader resourceLoader) {
        this.registry = new HashMap<String, BeanDefinition>();
        this.resourceLoader = resourceLoader;
    }

    public Map<String, BeanDefinition> getRegistry() {
        return registry;
    }

    public ResourceLoader getResourceLoader() {
        return resourceLoader;
    }
}
  • 最后我们来看一个通过读取Xml文件的BeanDefinitionReader:
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
    /**
     * 构造函数 传入我们之前分析过的ResourceLoader 这个通过
     * location 可以 加载到Resource
     */
    public XmlBeanDefinitionReader(ResourceLoader resourceLoader) {
        super(resourceLoader);
    }

    /**
     * 这个方法其实是BeanDefinitionReader这个接口中的方法
     * 作用就是通过location来构造BeanDefinition
     */
    @Override
    public void loadBeanDefinitions(String location) throws Exception {
        //把location传给ResourceLoader拿到Resource,然后获取输入流
        InputStream inputStream = getResourceLoader().getResource(location).getInputStream();
        //接下来进行输入流的处理
        doLoadBeanDefinitions(inputStream);
    }

    protected void doLoadBeanDefinitions(InputStream inputStream) throws Exception {
        //因为xml是文档对象,所以下面进行一些处理文档工具的构造
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder docBuilder = factory.newDocumentBuilder();
        //把输入流解析成一个文档,java可以处理的文档
        Document doc = docBuilder.parse(inputStream);
        // 处理这个文档对象 也就是解析bean
        registerBeanDefinitions(doc);
        inputStream.close();
    }

    public void registerBeanDefinitions(Document doc) {
        //得到文档的根节点,知道根节点后获取子节点就是通过层级关系处理就行了
        Element root = doc.getDocumentElement();
        //解析根节点 xml的根节点
        parseBeanDefinitions(root);
    }

    protected void parseBeanDefinitions(Element root) {
        NodeList nl = root.getChildNodes();
        for (int i = 0; i < nl.getLength(); i++) {
            Node node = nl.item(i);
            //element有属性的包装
            if (node instanceof Element) {
                Element ele = (Element) node;
                processBeanDefinition(ele);
            }
        }
    }

    protected void processBeanDefinition(Element ele) {
        /**
         *  <bean id="object***" class="com.***.***"/>
         */
        //获取element的id
        String name = ele.getAttribute("id");
        //获取element的class
        String className = ele.getAttribute("class");
        BeanDefinition beanDefinition = new BeanDefinition();
        //处理这个bean的属性
        processProperty(ele, beanDefinition);
        //设置BeanDefinition的类名称
        beanDefinition.setBeanClassName(className);
        //registry是一个map,存放所有的beanDefinition
        getRegistry().put(name, beanDefinition);
    }



    private void processProperty(Element ele, BeanDefinition beanDefinition) {
        /**
         *类似这种:
         <bean id="userServiceImpl" class="com.serviceImpl.UserServiceImpl">
         <property name="userDao" ref="userDaoImpl"> </property>
         </bean>
         */
        NodeList propertyNode = ele.getElementsByTagName("property");
        for (int i = 0; i < propertyNode.getLength(); i++) {
            Node node = propertyNode.item(i);
            if (node instanceof Element) {
                Element propertyEle = (Element) node;
                //获得属性的名称
                String name = propertyEle.getAttribute("name");
                //获取属性的值
                String value = propertyEle.getAttribute("value");
                if (value != null && value.length() > 0) {
                    //设置这个bean对应definition里的属性值
                    beanDefinition.getPropertyValues().addPropertyValue(new PropertyValue(name, value));
                } else {
                    //value是Reference的话  就会进入到这里处理
                    String ref = propertyEle.getAttribute("ref");
                    if (ref == null || ref.length() == 0) {
                        throw new IllegalArgumentException("Configuration problem: <property> element for property '"
                                + name + "' must specify a ref or value");
                    }
                    //构造一个BeanReference 然后把这个引用方到属性list里
                    BeanReference beanReference = new BeanReference(ref);
                    beanDefinition.getPropertyValues().addPropertyValue(new PropertyValue(name, beanReference));
                }
            }
        }
    }
}

  • 上面用到了BeanReference(如下),其实这个和PropertyValue类似,用不同的类型是为了更好的区分:
public class BeanReference {
     private String name;
     private Object bean;
}

好了,到现在我们已经分析完了Spring是如何找到Bean并加载进入Spring容器的,这里面最主要的数据结构就是BeanDefinition,ReourceLoader来完成资源的定位,读入,然后获取输入流,进一步的处理,这个过程中有对xml文档的解析和对属性的填充。
 由于自己能力有限,有些地方表述的不准确,还请大家谅解~~,

推荐阅读更多精彩内容