spring-framework-reference(5.1.1.RELEASE)中文版——Core部分

前言

最近在学习Spring框架,在学习的同时,借助有道翻译,整理翻译了部分文档,由于尚在学习当中,所以该篇文章将会定时更新,目标在一个月左右时间翻译完全部版本。

虽然大部分内容为翻译,但是其中可能会添加一些我对Spring的理解,最近也在计划建立一个wiki版本,希望同样在学习的你,可以参与进来,我们共同完成该版本文档的翻译与评注。感兴趣的,可以私信联系,方便共同学习。

以下是正式内容,标题未翻译,为了方便与官方文档相对应。

为了防止篇幅过大,该篇主要翻译Core核心部分.

Introduction to the Spring IoC Container and Beans

本章介绍了基于控制反转(IoC)原理的Spring框架实现。IoC也称为依赖注入(dependency injection, DI)。在这个过程中,对象只通过构造函数参数、工厂方法的参数或对象实例在构造或从工厂方法返回后设置的属性来定义它们的依赖关系(也就是说,它们使用的其他对象)。然后容器在创建bean时注入这些依赖项。这个过程从根本上说是bean本身的翻转(因此称为控制反转),它通过使用类或服务定位器模式等机制的直接构造来控制依赖项的实例化或位置。

这里补充IoC的理解框图

传统应用程序示意图

从这里我们就可以看出,为什么叫做翻转。因为本来客户端负责创建用户类,而现在只需要获取用户类。

org.spring.framework.beansorg.springframework.context是Spring Framework IoC容器的基础。BeanFactory接口提供了一种高级配置机制,能够管理任何类型的对象。ApplicationContextBeanFactory的子接口。它增加了:

  • 更容易与Spring的AOP特性集成
  • 消息资源处理
  • 事件发布
  • 应用程序层特定的context,例如用于web应用程序的WebApplicationContext。

简而言之,BeanFactory提供了配置框架和基本功能,而ApplicationContext添加了更多企业特定的功能。ApplicationContext是BeanFactory的一个完整超集,在本章中专门用于描述Spring的IoC容器。有关使用BeanFactory而不是ApplicationContext的更多信息,请参阅BeanFactory。

在Spring中,构成应用程序主干并由Spring IoC容器管理的对象称为bean。bean是由Spring IoC容器实例化、组装和以其他方式管理的对象。另外,bean只是应用程序中的众多对象之一。bean及其之间的依赖关系反映在容器使用的配置元数据中。

Container Overview

org.springframework.context.ApplicationContext接口代表Spring IoC容器,并负责实例化、配置和组装bean。容器通过读取配置元数据获得关于实例化、配置和组装对象的指令。配置元数据用XML、Java注释或Java代码表示。它允许您表达组成应用程序的对象以及这些对象之间的丰富相互依赖关系。

Spring提供了ApplicationContext接口的几个实现。在独立应用程序中,通常创建ClassPathXmlApplicationContextFileSystemXmlApplicationContext实例。虽然XML一直是定义配置元数据的传统格式,但您可以通过提供少量XML配置来声明支持这些额外的元数据格式,指示容器使用Java注释或代码作为元数据格式。

在大多数应用程序场景中,不需要显式用户代码来实例化Spring IoC容器的一个或多个实例。例如,在web应用程序场景中,应用程序的web.xml文件中的8行(或大约8行)样板web描述符XML通常就足够了.

如果您使用Spring工具套件(一个eclipse驱动的开发环境),您可以很容易地通过单击鼠标或击键创建这个样板配置。

下图显示了Spring工作原理的高级视图。您的应用程序类与配置元数据相结合,这样在创建和初始化ApplicationContext之后,您就有了一个完整配置的可执行系统或应用程序。

The Spring IoC container

Configuration Metadata

如上面的图表所示,Spring IoC容器使用了一种配置元数据的形式。这个配置元数据表示您作为应用程序开发人员如何告诉Spring容器实例化、配置和组装应用程序中的对象。

配置元数据传统上以简单直观的XML格式提供,这是本章的主要内容,用于传递Spring IoC容器的关键概念和特性。

基于xml的元数据不是配置元数据的唯一允许形式。Spring IoC容器本身与实际编写配置元数据的格式完全分离。现在,许多开发人员为他们的Spring应用程序选择基于java的配置。

有关使用Spring容器的其他元数据形式的信息,请参阅:

  • Annotation-based configuration: Spring 2.5引入了对基于注释的配置元数据的支持。
  • Java-based configuration: 从Spring 3.0开始,Spring JavaConfig项目提供的许多特性成为核心Spring框架的一部分。因此,您可以通过使用Java而不是XML文件来定义应用程序类外部的bean。要使用这些新特性,请参阅@Configuration、@Bean、@Import和@DependsOn注释。

Spring配置包含至少一个bean定义,通常是容器必须管理的多个bean定义。基于xml的配置元数据将这些bean配置为顶层元素中的元素。基于xml的配置元数据将这些bean配置为顶层元素中的元素。Java配置通常在@Configuration类中使用@ bean注释的方法。

这些bean定义对应于组成应用程序的实际对象。通常,您可以定义服务层对象、数据访问对象(DAOs)、表示对象(如Struts动作实例)、基础设施对象(如Hibernate SessionFactories)、JMS队列等。通常,在容器中不配置细粒度的域对象,因为创建和加载域对象通常是dao和业务逻辑的责任。但是,您可以使用Spring与AspectJ的集成来配置在IoC容器控制之外创建的对象。

下面的例子展示了基于xml的配置元数据的基本结构:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="..." class="...">
        <!-- collaborators and configuration for this bean go here -->
    </bean>

    <bean id="..." class="...">
        <!-- collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions go here -->

</beans>

其中:

  • id:i是标识单个bean定义的字符串。
  • class:定义bean的类型并使用完全限定的classname。

id属性的值引用协作对象。本例中没有显示引用协作对象的XML。有关更多信息,请参见依赖关系。

Instantiating a Container

提供给ApplicationContext构造函数的位置路径或路径是资源字符串,允许容器装载来自各种外部资源(如本地文件系统、Java类路径等)的配置元数据。

ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

注意:在了解了Spring的IoC容器之后,您可能想了解更多关于Spring的资源抽象(如参考资料中所述)的内容,它提供了从URI语法中定义的位置读取InputStream的方便机制。特别是,资源路径用于构造应用程序上下文,正如在应用程序上下文和资源路径中描述的那样。

下面的示例显示了服务层对象(service .xml)配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- services -->

    <bean id="petStore" class="org.springframework.samples.jpetstore.services.PetStoreServiceImpl">
        <property name="accountDao" ref="accountDao"/>
        <property name="itemDao" ref="itemDao"/>
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions for services go here -->

</beans>

下面的示例显示了数据访问对象daos.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="accountDao"
        class="org.springframework.samples.jpetstore.dao.jpa.JpaAccountDao">
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>

    <bean id="itemDao" class="org.springframework.samples.jpetstore.dao.jpa.JpaItemDao">
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions for data access objects go here -->

</beans>

在前面的示例中,服务层包含PetStoreServiceImpl类和JpaAccountDao和JpaItemDao类型的两个数据访问对象(基于JPA对象关系映射标准)。

属性名称元素引用JavaBean属性的名称,ref元素引用另一个bean定义的名称。id和ref元素之间的链接表示协作对象之间的依赖关系。有关配置对象依赖项的详细信息,请参阅依赖项。

Composing XML-based Configuration Metadata

让bean定义跨越多个XML文件可能很有用。通常,每个XML配置文件都代表体系结构中的逻辑层或模块。

您可以使用Application context构造函数从所有这些XML片段加载bean定义。此构造函数接受多个资源位置,如前一节所示。或者,使用一个或多个元素来从另一个或多个文件加载bean定义。下面的例子展示了如何做到这一点:

<beans>
    <import resource="services.xml"/>
    <import resource="resources/messageSource.xml"/>
    <import resource="/resources/themeSource.xml"/>

    <bean id="bean1" class="..."/>
    <bean id="bean2" class="..."/>
</beans>

在前面的示例中,外部bean定义从三个文件加载:service.xml,messageSource.xml和themeSource.xml。

所有位置路径都相对于执行导入操作的定义文件,因此是service.xml必须与执行导入的文件位于相同的目录或classpath位置,而messageSource.xml和themeSource.xml必须位于导入文件位置下方的resources位置。如您所见,前导斜杠被忽略。但是,考虑到这些路径是相对的,最好不要使用斜杠。根据Spring模式,导入的文件的内容,包括顶层元素,必须是有效的XML bean定义。

可以,但不推荐,引用文件在父目录使用一个相对"../”路径。这样做会对当前应用程序之外的文件创建依赖关系。特别是,不建议将此引用用于classpath: URLs(例如,classpath:../services.xml),其中运行时解析过程选择“最近的”classpath根目录,然后查看它的父目录。类路径配置更改可能导致选择不同的、不正确的目录。

您总是可以使用完全限定的资源位置而不是相对路径:例如,文件:C:/config/services。xml或者类路径:/配置/ services . xml。但是,请注意,您正在将应用程序的配置耦合到特定的绝对位置。对于这样的绝对位置,通常最好保持间接——例如,通过在运行时针对JVM系统属性解析的“${…}”占位符。

命名空间本身支持import directive特性。在Spring提供的XML名称空间(例如context和util名称空间)中,除了普通bean定义之外,还有其他配置特性。

The Groovy Bean Definition DSL

作为外部化配置元数据的进一步示例,bean定义也可以在Spring的Groovy bean定义DSL中表示,从Grails框架中可以知道这一点。通常,这样的配置位于".groovy"中。结构如下例所示:

beans {
    dataSource(BasicDataSource) {
        driverClassName = "org.hsqldb.jdbcDriver"
        url = "jdbc:hsqldb:mem:grailsDB"
        username = "sa"
        password = ""
        settings = [mynew:"setting"]
    }
    sessionFactory(SessionFactory) {
        dataSource = dataSource
    }
    myService(MyService) {
        nestedBean = { AnotherBean bean ->
            dataSource = dataSource
        }
    }
}

这种配置样式基本上与XML bean定义等价,甚至支持Spring的XML配置名称空间。它还允许通过importBeans指令导入XML bean定义文件。

Using the Container

ApplicationContext是一个高级工厂的接口,能够维护不同bean及其依赖项的注册表。通过使用方法T getBean(String name, Class requiredType),您可以获取bean的实例。

ApplicationContext允许您读取bean定义并访问它们,如下例所示:

// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

// retrieve configured instance
PetStoreService service = context.getBean("petStore", PetStoreService.class);

// use configured instance
List<String> userList = service.getUsernameList();

使用Groovy配置,bootstrapping看起来非常相似。它有一个不同的context实现类,它支持groovy-aware(但也理解XML bean定义)。下面的示例展示了Groovy配置:

ApplicationContext context = new GenericGroovyApplicationContext("services.groovy", "daos.groovy");

最灵活的变体是GenericApplicationContext和reader delegate相结合——例如,XmlBeanDefinitionReader用于XML文件,如下示例所示:

GenericApplicationContext context = new GenericApplicationContext();
new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml");
context.refresh();

您还可以为Groovy文件使用GroovyBeanDefinitionReader,如下例所示:

GenericApplicationContext context = new GenericApplicationContext();
new GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy");
context.refresh();

您可以在相同的ApplicationContext上混合并匹配这样的读取器委托,从不同的配置源读取bean定义。

然后,可以使用getBean检索bean的实例。ApplicationContext接口有一些用于检索bean的其他方法,但理想情况下,应用程序代码不应该使用它们。实际上,您的应用程序代码根本不应该调用getBean()方法,因此根本不需要依赖Spring api。例如,Spring与web框架的集成为各种web框架组件(如控制器和jsf管理的bean)提供了依赖注入,允许您通过元数据(如自动连接注释)声明对特定bean的依赖。

Bean Overview

Spring IoC容器管理一个或多个bean。这些bean是使用提供给容器的配置元数据创建的(例如,XML 定义的形式)。

在容器本身中,这些bean定义被表示为BeanDefinition对象,其中包含以下元数据(以及其他信息):

  • 包限定类名:通常是定义的bean的实际实现类。
  • Bean行为配置元素,它说明Bean在容器中的行为(范围、生命周期回调等等)。
  • 对bean执行其工作所需的其他bean的引用。这些引用也称为协作者或依赖关系。
  • 在新创建的对象中要设置的其他配置设置——例如,池的大小限制或管理连接池的bean中要使用的连接数。

此元数据转换为组成每个bean定义的一组属性。下表描述了这些属性:

Property Explained in...
Class 实例化Bean
Name 命名Beans
Scope Bean生成期
Contructor arguments 依赖注入
Properties 依赖注入
Autowiring mode 自动装配合作者
Lazy initialization mdoe 初始化回调
Destruction method 毁坏回调

除了包含关于如何创建特定bean的信息的bean定义之外,ApplicationContext实现还允许注册容器之外创建的现有对象(由用户创建)。
通过getBeanFactory()方法访问ApplicationContext的BeanFactory,该方法返回BeanFactory DefaultListableBeanFactory实现。DefaultListableBeanFactory通过registerSingleton(..)和registerBeanDefinition(..)方法支持此注册。但是,典型的应用程序只使用通过元数据bean定义定义的bean。

Bean元数据和手动提供的单例实例需要尽早注册,以便容器在自动连接和其他自省步骤期间正确地推断它们。虽然在一定程度上支持覆盖现有的元数据和现有的单例实例,但是在运行时注册新bean(与对工厂的实时访问同时进行)并没有得到正式支持,并且可能导致并发访问异常、bean容器中的不一致状态,或者两者都有。

Naming Beans

每个bean都有一个或多个标识符。这些标识符在承载bean的容器中必须是唯一的。bean通常只有一个标识符。但是,如果需要一个以上的别名,则可以将额外的别名看作别名。

在基于xml的配置元数据中,可以使用id属性、name属性或两者来指定bean标识符。id属性允许您指定一个id。按照惯例,这些名称是字母数字(“myBean”、“someService”等),但它们也可以包含特殊字符。如果希望为bean引入其他别名,还可以在name属性中指定它们,用逗号(、)、分号(;)或空格分隔。作为历史记录,在Spring 3.1之前的版本中,id属性被定义为xsd: id类型,这限制了可能的字符。在3.1中,它被定义为xsd:string类型。请注意,bean id惟一性仍然由容器强制执行,但不再由XML解析器强制执行。

您不需要为bean提供名称或id。如果没有显式地提供名称或id,容器将为该bean生成唯一的名称。但是,如果希望通过引用ref元素或服务定位器样式查找来引用bean的名称,则必须提供名称。不提供名称的动机与使用内部bean和自动连接合作者有关。

Bean 命名约定

约定是在命名bean时使用标准Java约定作为实例字段名。也就是说,bean名称以小写字母开头,然后以驼色大小写字母开头。这些名字的例子包括accountManager,accountService, userDao, loginController, 等等。

一致地命名bean使您的配置更容易阅读和理解。另外,如果您使用Spring AOP,那么在向一组名称相关的bean应用建议时,它会有很大帮助。


注:通过类路径中的组件扫描,Spring为未命名的组件生成bean名称,遵循前面描述的规则:本质上,使用简单的类名并将其初始字符转换为小写。然而,在(不寻常的)特殊情况下,当有一个以上的字符,并且第一和第二字符都是大写字母时,原始的大小写保留。这些规则与
java.beans.Introspector.decapitalize(此处使用Spring)定义的规则相同。

Aliasing a Bean outside the Bean Definition

在bean定义本身中,通过使用id属性指定的最多一个名称和name属性中任意数量的其他名称的组合,可以为bean提供多个名称。这些名称可以等效于同一bean的别名,并且在某些情况下非常有用,例如让应用程序中的每个组件通过使用特定于该组件本身的bean名称引用公共依赖项。

然而,指定bean实际定义的所有别名并不总是足够的。
有时需要为其他地方定义的bean引入别名。在大型系统中,配置通常在每个子系统之间进行分配,每个子系统都有自己的一组对象定义。在基于xml的配置元数据中,可以使用元素来完成此任务。下面的例子展示了如何做到这一点:

<alias name="fromName" alias="toName"/>

在这种情况下,命名为fromName的bean(在同一个容器中)也可以在使用这个别名定义之后称为toName。
例如,子系统A的配置元数据可以通过subsystemA-dataSource的名称引用数据源。子系统B的配置元数据可以通过subsystemB-dataSource的名称引用数据源。在组合使用这两个子系统的主应用程序时,主应用程序以myApp-dataSource的名称引用数据源。要使所有三个名称都指向同一个对象,可以向配置元数据添加以下别名定义:

<alias name="subsystemA-dataSource" alias="subsystemB-dataSource"/>
<alias name="subsystemA-dataSource" alias="myApp-dataSource" />

现在,每个组件和主应用程序都可以通过唯一的名称引用数据源,并且保证不会与任何其他定义冲突(有效地创建名称空间),但是它们引用的是同一个bean。

Java-Configuration

如果使用Javaconfiguration,那么可以使用@Bean注释来提供别名。有关详细信息,请参阅使用@Bean注释。

Instantiating Beans

bean定义本质上是创建一个或多个对象的配方。容器在请求时查看命名bean的配方,并使用该bean定义封装的配置元数据来创建(或获取)实际对象。

如果使用基于xml的配置元数据,则指定要在元素的class属性中实例化的对象的类型(或类)。这个类属性(在内部是BeanDefinition实例上的类属性)通常是强制的。(对于异常,请参阅使用实例工厂方法和Bean定义继承进行实例化。)您可以通过以下两种方式之一来使用类属性:

  • 通常,在容器本身通过反射性地调用其构造函数直接创建bean的情况下,指定要构造的bean类,这在某种程度上类似于使用new操作符的Java代码。
  • 在不太常见的情况下,在容器调用类上的静态工厂方法来创建bean时,指定包含用于创建对象的静态工厂方法的实际类。从静态工厂方法调用返回的对象类型可能完全是同一个类或另一个类。

Inner class names
如果要为静态嵌套类配置bean定义,则必须使用嵌套类的二进制名称。

例如,如果在com.example中有一个名为SomeThing的类。这个SomeThing类有一个名为OtherThing的静态嵌套类,bean定义上的class属性的值应该是com.example.SomeThing$OtherThing。

注意,在名称中使用$字符将嵌套类名称与外部类名称分隔开来。

Instantiation with a Constructor

当您使用构造函数方法创建bean时,所有普通类都可以使用,并且与Spring兼容。也就是说,正在开发的类不需要实现任何特定的接口或以特定的方式进行编码。简单地指定bean类就足够了。然而,根据对特定bean使用的IoC类型的不同,您可能需要一个默认(空)构造函数

Spring IoC容器实际上可以管理您希望它管理的任何类。它不限于管理真正的JavaBeans。大多数Spring用户更喜欢实际的javaBeans,只有一个默认的(无参数的)构造函数,以及根据容器中的属性建模的适当的setter和getter。您还可以在容器中拥有更多具有外来的非bean风格的类。例如,如果您需要使用一个完全不遵守JavaBean规范的遗留连接池,Spring也可以对其进行管理。

使用基于xml的配置元数据,您可以如下所示指定bean类:

<bean id="exampleBean" class="examples.ExampleBean"/>

<bean name="anotherExample" class="examples.ExampleBeanTwo"/>

有关向构造函数提供参数(如果需要)和在构造对象之后设置对象实例属性的机制的详细信息,请参见注入依赖项。

Instantiation with a Static Factory Method

在定义使用静态工厂方法创建的bean时,使用class属性指定包含静态工厂方法的类和名为factory-method的属性来指定工厂方法本身的名称。您应该能够调用这个方法(使用可选参数,稍后将进行描述),并返回一个活动对象,该对象随后将被视为通过构造函数创建的对象。这种bean定义的一种用法是在遗留代码中调用静态工厂。

下面的bean定义指定通过调用工厂方法创建bean。定义没有指定返回对象的类型(类),只指定包含工厂方法的类。在本例中,createInstance()方法必须是一个静态方法。下面的例子展示了如何指定工厂方法:

<bean id="clientService"
    class="examples.ClientService"
    factory-method="createInstance"/>

下面的示例显示了一个将与前面的bean定义一起工作的类:

public class ClientService {
    private static ClientService clientService = new ClientService();
    private ClientService() {}

    public static ClientService createInstance() {
        return clientService;
    }
}

有关向工厂方法提供(可选的)参数以及在从工厂返回对象后设置对象实例属性的机制的详细信息,请参阅依赖项和配置的详细信息。

Instantiation by Using an Instance Factory Method

与通过静态工厂方法实例化类似,使用实例工厂方法实例化从容器调用现有bean的非静态方法来创建新的bean。要使用这种机制,请保持类属性为空,并在factory-bean属性中指定当前(或父或祖先)容器中的bean的名称,该容器包含要调用来创建对象的实例方法。使用工厂方法属性设置工厂方法本身的名称。下面的示例展示了如何配置这样的bean:

<!-- the factory bean, which contains a method called createInstance() -->
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
    <!-- inject any dependencies required by this locator bean -->
</bean>

<!-- the bean to be created via the factory bean -->
<bean id="clientService"
    factory-bean="serviceLocator"
    factory-method="createClientServiceInstance"/>

下面的例子显示了相应的Java类:

public class DefaultServiceLocator {

    private static ClientService clientService = new ClientServiceImpl();

    public ClientService createClientServiceInstance() {
        return clientService;
    }
}

一个工厂类也可以容纳多个工厂方法,如下例所示:

<bean id="serviceLocator" class="examples.DefaultServiceLocator">
    <!-- inject any dependencies required by this locator bean -->
</bean>

<bean id="clientService"
    factory-bean="serviceLocator"
    factory-method="createClientServiceInstance"/>

<bean id="accountService"
    factory-bean="serviceLocator"
    factory-method="createAccountServiceInstance"/>

下面的例子显示了相应的Java类:

public class DefaultServiceLocator {

    private static ClientService clientService = new ClientServiceImpl();

    private static AccountService accountService = new AccountServiceImpl();

    public ClientService createClientServiceInstance() {
        return clientService;
    }

    public AccountService createAccountServiceInstance() {
        return accountService;
    }
}

这种方法表明,可以通过依赖注入(dependency injection)对工厂bean本身进行管理和配置。请参阅详细的依赖关系和配置。

在Spring文档中,"factory bean"指的是在Spring容器中配置并通过实例或静态工厂方法创建对象的bean。相比之下,FactoryBean(注意大写)指特定于spring的FactoryBean。

Dependencies

一个典型的企业应用程序不包含单个对象(或Spring术语中的bean)。即使是最简单的应用程序,也有一些对象一起工作,以显示最终用户所看到的一致的应用程序。
下一节将解释如何从定义独立的许多bean定义过渡到一个完全实现的应用程序,在这个应用程序中,对象协作以实现目标。

Dependency Injection

依赖注入(Dependency injection)是一个过程,在这个过程中,对象只通过构造函数参数、工厂方法的参数或从工厂方法构造或返回的对象实例上设置的属性来定义它们的依赖关系(也就是说,它们工作的其他对象)。然后容器在创建bean时注入这些依赖项。这个过程本质上是bean本身的翻转(因此称为控制翻转),它通过使用类或服务定位器模式的直接构造来控制其依赖项的实例化或位置。

使用DI原则,代码更简洁,当对象具有依赖关系时,解耦更有效。对象不查找依赖项,也不知道依赖项的位置或类。因此,您的类变得更容易测试,特别是当依赖关系是在接口或抽象基类上时,这允许存根或模拟实现在单元测试中使用。

DI存在于两种主要变体中:基于构造函数的依赖项注入和基于Setter的依赖项注入。

Constructor-based Dependency Injection

基于构造函数的DI由容器调用构造函数来完成,该构造函数有许多参数,每个参数都表示依赖关系。调用带有特定参数的静态工厂方法来构造bean几乎是等价的,本文将把参数类似地处理给构造函数和静态工厂方法。下面的例子展示了一个只能通过构造函数注入依赖注入的类:

public class SimpleMovieLister {

    // the SimpleMovieLister has a dependency on a MovieFinder
    private MovieFinder movieFinder;

    // a constructor so that the Spring container can inject a MovieFinder
    public SimpleMovieLister(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // business logic that actually uses the injected MovieFinder is omitted...
}

注意,这个类没有什么特别之处。它是一个POJO,不依赖于容器特定的接口、基类或注释。

Constructor Argument Resolution

构造函数参数解析匹配通过使用参数的类型来实现。如果bean定义的构造函数参数中不存在潜在的歧义,那么在bean定义中定义构造函数参数的顺序就是在实例化bean时将这些参数提供给适当的构造函数的顺序。考虑以下类:

package x.y;

public class ThingOne {

    public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
        // ...
    }
}

假设ThingTwo和ThingThree类不通过继承关联,则不存在潜在的歧义。因此,下面的配置可以很好地工作,您不需要在元素<constructor-arg/>中显式指定构造函数参数索引或类型。

<beans>
    <bean id="thingOne" class="x.y.ThingOne">
        <constructor-arg ref="thingTwo"/>
        <constructor-arg ref="thingThree"/>
    </bean>

    <bean id="thingTwo" class="x.y.ThingTwo"/>

    <bean id="thingThree" class="x.y.ThingThree"/>
</beans>

当引用另一个bean时,类型是已知的,并且可以进行匹配(就像前面的例子那样)。当使用简单类型(如true)时,Spring无法确定值的类型,因此在没有帮助的情况下无法按类型匹配。考虑以下类:

package examples;

public class ExampleBean {

    // Number of years to calculate the Ultimate Answer
    private int years;

    // The Answer to Life, the Universe, and Everything
    private String ultimateAnswer;

    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}

Constructor argument type matching

在前面的场景中,如果使用type属性显式地指定构造函数参数的类型,容器可以使用与简单类型匹配的类型。如下例所示:

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg type="int" value="7500000"/>
    <constructor-arg type="java.lang.String" value="42"/>
</bean>

Constructor argument index

可以使用index属性显式地指定构造函数参数的索引,如下例所示:

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg index="0" value="7500000"/>
    <constructor-arg index="1" value="42"/>
</bean>

除了解决多个简单值的模糊性之外,指定索引还可以解决构造函数具有相同类型的两个参数的模糊性问题。

基于0的指数

Constructor argument name

您还可以使用构造函数参数名来消除值歧义,如下例所示:

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg name="years" value="7500000"/>
    <constructor-arg name="ultimateAnswer" value="42"/>
</bean>

请记住,要使这一工作开箱即用,您的代码必须在启用调试标志的情况下进行编译,以便Spring可以从构造函数查找参数名。如果不能或不想使用调试标志编译代码,可以使用@ConstructorProperties JDK注释显式地为构造函数参数命名。然后示例类必须如下所示:

package examples;

public class ExampleBean {

    // Fields omitted

    @ConstructorProperties({"years", "ultimateAnswer"})
    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}

Setter-based Dependency Injection

在调用无参数构造函数或无参数静态工厂方法实例化bean之后,容器调用bean上的setter方法就可以实现基于setter的DI。

下面的示例显示了一个只能通过使用纯setter注入进行依赖注入的类。这个类是常规Java。它是一个POJO,不依赖于容器特定的接口、基类或注释。

public class SimpleMovieLister {

    // the SimpleMovieLister has a dependency on the MovieFinder
    private MovieFinder movieFinder;

    // a setter method so that the Spring container can inject a MovieFinder
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // business logic that actually uses the injected MovieFinder is omitted...
}

ApplicationContext为其管理的bean支持基于构造函数和基于setter的DI。在通过构造函数方法注入一些依赖项之后,它还支持基于setter的DI。
您可以以BeanDefinition的形式配置依赖关系,并将其与PropertyEditor实例一起使用,以将属性从一种格式转换为另一种格式。然而,大多数Spring用户并不直接使用这些类(即通过编程方式),而是使用XML bean定义、带注释的组件(即用@Component、@Controller等注释的类)或基于java的@Configuration类中的@Bean方法。然后,这些源在内部转换为BeanDefinition实例,并用于加载整个Spring IoC容器实例。

Constructor-based or setter-based DI?

由于可以混合使用基于构造函数和基于setter的DI,因此使用构造函数处理强制依赖关系和setter方法或配置方法处理可选依赖关系是一个很好的经验法则。注意,在setter方法上使用@Required注释可以使属性成为必需的依赖项。

Spring团队通常提倡构造函数注入,因为它允许您将应用程序组件实现为不可变对象,并确保所需的依赖项不为空。此外,构造注入的组件总是以完全初始化的状态返回给客户端(调用)代码。作为补充说明,大量的构造函数参数是一种糟糕的代码味道,这意味着类可能有太多的职责,应该重构,以便更好地处理关注点的适当分离。

Setter注入主要应该只用于可选的依赖项,这些依赖项可以在类中分配合理的默认值。否则,在代码使用依赖项的任何地方都必须执行非空检查。setter注入的一个好处是,setter方法使该类的对象可以稍后重新配置或重新注入。因此,通过JMX MBean进行管理是setter注入的一个引人注目的用例。

用对特定类最有意义的DI样式。有时候,在处理没有源代码的第三方类时,会为您做出选择。例如,如果第三方类不公开任何setter方法,那么构造函数注入可能是惟一可用的DI形式。

Dependency Resolution Process

容器执行以下bean依赖项解析:

  • 使用描述所有bean的配置元数据创建和初始化ApplicationContext。配置元数据可以由XML、Java代码或注释指定。
  • 对于每个bean,其依赖关系以属性、构造函数参数或静态工厂方法参数的形式表示(如果使用静态工厂方法而不是普通构造函数)。这些依赖项在bean实际创建时提供给bean。
  • 每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个bean的引用。
  • 值的每个属性或构造函数参数都从其指定格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring可以将字符串格式提供的值转换为所有内置类型,如int、long、string、boolean等。

在创建容器时,Spring容器验证每个bean的配置。然而,直到真正创建bean时,才会设置bean属性本身。当创建容器时,将创建单实例作用域并设置为预实例化的bean(默认)。作用域在Bean作用域中定义。否则,只有在请求bean时才会创建它。创建bean可能会导致创建bean的图,因为创建和分配了bean的依赖项及其依赖项(等等)。请注意,这些依赖项之间的解析不匹配可能出现得较晚——即在第一次创建受影响的bean时。

如果使用主构造函数注入,则可以创建不可解析的循环依赖场景。

Circular dependencies

例如:类A需要通过构造函数注入的类B实例,类B需要通过构造函数注入的类A实例。如果将类A和类B配置为相互注入的bean,那么Spring IoC容器将在运行时检测到此循环引用,并抛出BeanCurrentlyInCreationException。

一种可能的解决方案是编辑由setter而不是构造器配置的一些类的源代码。或者,避免构造函数注入,只使用setter注入。换句话说,尽管不建议使用setter注入配置循环依赖项。

与典型的情况(没有循环依赖项)不同的是,Bean A和bean B之间的循环依赖项强制在完全初始化自身之前将一个bean注入另一个bean(典型的鸡和蛋的场景)。

您通常可以相信Spring会做正确的事情。它在容器装载时检测配置问题,例如对不存在的bean和循环依赖项的引用。在实际创建bean时,Spring尽可能晚地设置属性并解析依赖关系。这意味着,如果在创建该对象或其依赖项时出现问题,那么在以后请求对象时,正确加载的Spring容器可以生成异常——例如,bean由于丢失或无效属性而抛出异常。某些配置问题可能会延迟可见性,这就是为什么ApplicationContext实现在默认情况下预实例化单例bean。
在实际需要这些bean之前先花一些时间和内存来创建它们,在创建ApplicationContext时(而不是稍后),您会发现配置问题。您仍然可以覆盖这个默认行为,以便单例bean能够惰性地初始化,而不是被预先实例化。问题

如果不存在循环依赖项,当一个或多个协作bean被注入到依赖bean中时,每个协作bean在被注入到依赖bean之前都被完全配置好了。这意味着,如果bean A依赖于Bean B,那么在调用bean A上的setter方法之前,Spring IoC容器已经完全配置了Bean B。换句话说,bean被实例化(如果它不是一个预先实例化的单例对象),它的依赖关系被设置,相关的生命周期方法(例如配置的init方法或InitializingBean回调方法)被调用。

Examples of Dependency Injection

下面的示例将基于xml的配置元数据用于基于setter的DI。Spring XML配置文件的一小部分指定了一些bean定义,如下所示:

<bean id="exampleBean" class="examples.ExampleBean">
    <!-- setter injection using the nested ref element -->
    <property name="beanOne">
        <ref bean="anotherExampleBean"/>
    </property>

    <!-- setter injection using the neater ref attribute -->
    <property name="beanTwo" ref="yetAnotherBean"/>
    <property name="integerProperty" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

下面的例子显示了相应的ExampleBean类:

public class ExampleBean {

    private AnotherBean beanOne;

    private YetAnotherBean beanTwo;

    private int i;

    public void setBeanOne(AnotherBean beanOne) {
        this.beanOne = beanOne;
    }

    public void setBeanTwo(YetAnotherBean beanTwo) {
        this.beanTwo = beanTwo;
    }

    public void setIntegerProperty(int i) {
        this.i = i;
    }
}

在前面的示例中,声明setter以匹配XML文件中指定的属性。下面的示例使用基于构造函数的DI:

<bean id="exampleBean" class="examples.ExampleBean">
    <!-- constructor injection using the nested ref element -->
    <constructor-arg>
        <ref bean="anotherExampleBean"/>
    </constructor-arg>

    <!-- constructor injection using the neater ref attribute -->
    <constructor-arg ref="yetAnotherBean"/>

    <constructor-arg type="int" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

下面的例子显示了相应的ExampleBean类:

public class ExampleBean {

    private AnotherBean beanOne;

    private YetAnotherBean beanTwo;

    private int i;

    public ExampleBean(
        AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
        this.beanOne = anotherBean;
        this.beanTwo = yetAnotherBean;
        this.i = i;
    }
}

bean定义中指定的构造函数参数用作ExampleBean的构造函数的参数。

现在考虑这个例子的一个变体,在这个例子中,Spring不是使用构造函数,而是调用静态工厂方法来返回对象的实例:

<bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance">
    <constructor-arg ref="anotherExampleBean"/>
    <constructor-arg ref="yetAnotherBean"/>
    <constructor-arg value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

下面的例子显示了相应的ExampleBean类:

public class ExampleBean {

    // a private constructor
    private ExampleBean(...) {
        ...
    }

    // a static factory method; the arguments to this method can be
    // considered the dependencies of the bean that is returned,
    // regardless of how those arguments are actually used.
    public static ExampleBean createInstance (
        AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {

        ExampleBean eb = new ExampleBean (...);
        // some other operations...
        return eb;
    }
}

静态工厂方法的参数由元素提供,与实际使用的构造函数完全相同。工厂方法返回的类的类型不必与包含静态工厂方法的类的类型相同(尽管在本例中是这样)。实例(非静态)工厂方法可以以一种基本相同的方式使用(除了使用factory-bean属性而不是类属性之外),因此我们在这里不讨论这些细节。

在静态工厂类中的静态方法返回的类可以时任意类型的。

Dependencies and Configuration in Detail

如前一节所述,可以将bean属性和构造函数参数定义为对其他托管bean(协作者)的引用或内联定义的值。为此,Spring基于xml的配置元数据支持其元素中的子元素类型。

Straight Values (Primitives, Strings, and so on)

元素的value属性将属性或构造函数参数指定为人类可读的字符串表示形式。Spring的转换服务用于将这些值从字符串转换为属性或参数的实际类型。下面的例子显示了正在设置的各种值:

<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <!-- results in a setDriverClassName(String) call -->
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
    <property name="username" value="root"/>
    <property name="password" value="masterkaoli"/>
</bean>

下面的示例使用p-namespace来实现更简洁的XML配置:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close"
        p:driverClassName="com.mysql.jdbc.Driver"
        p:url="jdbc:mysql://localhost:3306/mydb"
        p:username="root"
        p:password="masterkaoli"/>

</beans>

前面的XML更简洁。但是,在运行时而不是在设计时发现拼写错误,除非使用支持在创建bean定义时自动完成属性的IDE(如IntelliJ IDEA或Spring工具套件)。强烈建议提供这种IDE援助。

您还可以配置java.util.Properties实例如下:

<bean id="mappings"
    class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

    <!-- typed as a java.util.Properties -->
    <property name="properties">
        <value>
            jdbc.driver.className=com.mysql.jdbc.Driver
            jdbc.url=jdbc:mysql://localhost:3306/mydb
        </value>
    </property>
</bean>

Spring容器通过使用JavaBeansPropertyEditor机制将元素转化为java.util.Properties实例。这是一个很好的快捷方式,也是Spring团队支持使用嵌套元素而不是value属性样式的少数几个地方之一。

The idref element

idref元素只是将容器中另一个bean的id(字符串值,而不是引用)传递给<构造函数-arg/>或元素的一种防错方法。

<bean id="theTargetBean" class="..."/>

<bean id="theClientBean" class="...">
    <property name="targetName">
        <idref bean="theTargetBean"/>
    </property>
</bean>

前面的bean定义片段与下面的片段完全等价(在运行时):

<bean id="theTargetBean" class="..." />

<bean id="client" class="...">
    <property name="targetName" value="theTargetBean"/>
</bean>

第一种形式比第二种更可取,因为使用idref标记可以让容器在部署时验证所引用的命名bean实际存在。在第二个变体中,对传递给客户机bean的targetName属性的值不执行验证。只有当客户机bean实际实例化时,才会发现输入错误(最有可能导致致命的结果)。如果客户机bean是多例,那么这种类型和由此产生的异常可能在部署容器很久之后才会被发现。

4.0 bean XSD不再支持idref元素上的local属性,因为它不再为常规bean引用提供值。升级到4.0架构时,将现有的idref local引用更改为idref bean。

元素带来价值的一个常见地方(至少在Spring 2.0之前的版本中)是ProxyFactoryBean定义中的AOP拦截器配置。在指定拦截器名称时使用元素可以防止对拦截器ID的拼写错误。

References to Other Beans (Collaborators)

ref元素是<construct -arg/>或定义元素中的最后一个元素。在这里,您将bean的指定属性值设置为对容器管理的另一个bean(合作者)的引用。引用的bean是要设置其属性的bean的依赖项,并且在设置属性之前根据需要对其进行初始化(如果合作者是单例bean,则容器可能已经对其进行了初始化)。所有引用最终都是对另一个对象的引用。作用域和验证取决于是否通过bean、本地属性或父属性指定其他对象的ID或名称。

通过标记的bean属性指定目标bean是最通用的形式,允许创建对相同容器或父容器中的任何bean的引用,而不管它是否在相同的XML文件中。bean属性的值可能与目标bean的id属性相同,也可能与目标bean的name属性中的值相同。下面的例子展示了如何使用ref元素:

<ref bean="someBean"/>

通过父属性指定目标bean将创建对当前容器的父容器中的bean的引用。父属性的值可能与目标bean的id属性或目标bean的name属性中的一个值相同。目标bean必须位于当前bean的父容器中。当您有一个容器层次结构,并且希望使用与父bean同名的代理将现有bean包装在父容器中时,您应该主要使用这个bean引用变体。下面展示如何使用parent属性

<!-- in the parent context -->
<bean id="accountService" class="com.something.SimpleAccountService">
    <!-- insert dependencies as required as here -->
</bean>
<!-- in the child (descendant) context -->
<bean id="accountService" <!-- bean name is the same as the parent bean -->
    class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="target">
        <ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
    </property>
    <!-- insert other configuration and dependencies as required here -->
</bean>

我们要注意如何引用的parent。

Inner Beans

元素在元素中定义了一个内部bean,如下例所示:

<bean id="outer" class="...">
    <!-- instead of using a reference to a target bean, simply define the target bean inline -->
    <property name="target">
        <bean class="com.example.Person"> <!-- this is the inner bean -->
            <property name="name" value="Fiona Apple"/>
            <property name="age" value="25"/>
        </bean>
    </property>
</bean>

内部bean定义不需要定义ID或名称。如果指定,容器不会使用这样的值作为标识符。容器在创建时也会忽略范围标志,因为内部bean总是匿名的,并且总是与外部bean一起创建的。不可能独立访问内部bean,也不可能将它们注入协作bean(而不是封闭bean)中。

一种特例,可以从自定义作用域接收销毁回调——例如,对于单例bean中包含的请求作用域的内部bean。内部bean实例的创建与其包含的bean绑定在一起,但是销毁回调允许它参与请求范围的生命周期。这不是一个常见的场景。内部bean通常只是共享其包含bean的作用域。

Collections

元素分别设置Java集合类型列表、集合、映射和属性的属性和参数。下面的例子展示了如何使用它们:

<bean id="moreComplexObject" class="example.ComplexObject">
    <!-- results in a setAdminEmails(java.util.Properties) call -->
    <property name="adminEmails">
        <props>
            <prop key="administrator">administrator@example.org</prop>
            <prop key="support">support@example.org</prop>
            <prop key="development">development@example.org</prop>
        </props>
    </property>
    <!-- results in a setSomeList(java.util.List) call -->
    <property name="someList">
        <list>
            <value>a list element followed by a reference</value>
            <ref bean="myDataSource" />
        </list>
    </property>
    <!-- results in a setSomeMap(java.util.Map) call -->
    <property name="someMap">
        <map>
            <entry key="an entry" value="just some string"/>
            <entry key ="a ref" value-ref="myDataSource"/>
        </map>
    </property>
    <!-- results in a setSomeSet(java.util.Set) call -->
    <property name="someSet">
        <set>
            <value>just some string</value>
            <ref bean="myDataSource" />
        </set>
    </property>
</bean>

映射键或值或集值的值也可以是以下任何元素:

bean | ref | idref | list | set | map | props | value | null 

Collection Merging

Spring容器还支持合并集合。应用程序开发人员可以定义父元素元素,并具有子元素元素从父集合继承和覆盖值。也就是说,子集合的值是合并父集合和子集合的元素的结果,子集合元素覆盖父集合中指定的值。

Spring容器还支持合并集合。应用程序开发人员可以定义父元素元素,并具有子元素元素从父集合继承和覆盖值。也就是说,子集合的值是合并父集合和子集合的元素的结果,子集合元素覆盖父集合中指定的值。

下面的例子演示了集合合并:

<beans>
    <bean id="parent" abstract="true" class="example.ComplexObject">
        <property name="adminEmails">
            <props>
                <prop key="administrator">administrator@example.com</prop>
                <prop key="support">support@example.com</prop>
            </props>
        </property>
    </bean>
    <bean id="child" parent="parent">
        <property name="adminEmails">
            <!-- the merge is specified on the child collection definition -->
            <props merge="true">
                <prop key="sales">sales@example.com</prop>
                <prop key="support">support@example.co.uk</prop>
            </props>
        </property>
    </bean>
<beans>

注意,在子bean定义的adminemail属性的元素上使用merge=true属性。当容器解析并实例化子bean时,生成的实例具有adminEmail属性集合,其中包含将子bean的adminemail集合与父组件的adminemail集合合并的结果。下面的清单显示了结果:

administrator=administrator@example.com
sales=sales@example.com
support=support@example.co.uk

子属性集合的值集继承了来自父元素的所有属性元素,而子元素的支持值覆盖了父集合中的值。
这种合并行为同样适用于集合类型。在元素的特定情况下,将维护与列表集合类型(即有序值集合的概念)关联的语义。父列表的值在所有子列表的值之前.对于映射、集合和属性集合类型,不存在排序。因此,对于位于容器内部使用的关联映射、集合和属性实现类型之下的集合类型,没有任何排序语义。

Limitations of Collection Merging

不能合并不同的集合类型(例如映射和列表)。如果您确实试图这样做,则会抛出一个适当的异常。merge属性必须在较低的继承子定义上指定。在父集合定义上指定merge属性是多余的,不会导致所需的合并。

Strongly-typed collection

随着Java 5中泛型类型的引入,您可以使用强类型集合。也就是说,可以声明一个集合类型,使其只能包含(例如)字符串元素。如果使用Spring依赖于将强类型集合注入bean,则可以利用Spring的类型转换支持,以便在添加到集合之前将强类型集合实例的元素转换为适当的类型。下面的Java类和bean定义说明了如何做到这一点:

public class SomeClass {

    private Map<String, Float> accounts;

    public void setAccounts(Map<String, Float> accounts) {
        this.accounts = accounts;
    }
}
<beans>
    <bean id="something" class="x.y.SomeClass">
        <property name="accounts">
            <map>
                <entry key="one" value="9.99"/>
                <entry key="two" value="2.75"/>
                <entry key="six" value="3.99"/>
            </map>
        </property>
    </bean>
</beans>

当为注入准备好某某bean的accounts属性时,强类型映射的元素类型的泛型信息可以通过反射得到。因此,Spring的类型转换基础结构将各种值元素识别为浮点类型,并将字符串值(9.99、2.75和3.99)转换为实际的浮点类型。

Null and Empty String Values

Spring将属性等的空参数视为空字符串。以下基于xml的配置元数据片段将email属性设置为空字符串值("")。

<bean class="ExampleBean">
    <property name="email" value=""/>
</bean>

上述示例相当于以下Java代码:

exampleBean.setEmail("");

元素处理空值。下面的清单显示了一个示例:

<bean class="ExampleBean">
    <property name="email">
        <null/>
    </property>
</bean>

上述示例相当于以下Java代码:

exampleBean.setEmail(null);

使用p-namespace的XML快捷方式

posted @ 2018-10-24 21:32  NinWoo  阅读(...)  评论(... 编辑 收藏