调用构造程序创建 Bean
首先,假定你打算开发一个在线销售产品的购物应用程序。先创建一个 Product类,这个类有多个属性,例如产品名称和价格。因为商店中有许多类型的产品,所以你定义 Product 类为抽象类,用于不同产品子类的扩展。
这次我们新建一个名为“sesametech.springrecipes.s006”包,在该包下创建“Product”类,主要代码如下:
public abstract class Product {
private String name;
private double price;
public Product() {
}
public Product(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return name + " " + price;
}
getter & setter …
}
然后创建两个产品子类 Battery(电池)和 Disk(光盘),每个类都有自己的属性,主要代码分别如下:
public class Battery extends Product {
private boolean rechargeable; // 是否为可充电电池
public Battery() {
super();
}
public Battery(String name, double price) {
super(name, price);
}
getter & setter …
}
public class Disk extends Product {
private int capacity; // 容量
public Disk() {
super();
}
public Disk(String name, double price) {
super(name, price);
}
getter & setter …
}
为了在 Spring IOC 容器中定义这些产品,我们在“sesametech.springrecipes.s006”包下创建“beans.xml”配置文件,内容如下:
<bean
id="aaa"
class="sesametech.springrecipes.s006.Battery">
<!-- 通过设值方法配置 Bean 属性 -->
<property name="name"value="AAA" />
<property name="price"value="2.5" />
<property name="rechargeable"value="true" />
</bean>
<bean
id="cdrw"
class="sesametech.springrecipes.s006.Disk">
<!-- 通过设值方法配置 Bean 属性 -->
<property name="name"value="CD-RW" />
<property name="price"value="1.5" />
<property name="capacity"value="1024" />
</bean>
最后,我们在“sesametech.springrecipes.s006”包下,创建“ProductTest”测试类并测试它,主要代码如下:
@SuppressWarnings("resource")
public static void main(String[] args) {
context = newClassPathXmlApplicationContext("sesametech/springrecipes/s006/beans.xml");
product = (Product) context.getBean("aaa");
System.out.println(product.toString());
product = (Product) context.getBean("cdrw");
System.out.println(product.toString());
}
现在,我们来大概分析一下这个应用程序。首先,它没有指定任何<constructor-arg> 标签来配置 Bean 属性,这样Spring 将会默认调用不带参数的构造方法来实例化相关的类,并通过设值方法注入属性值。前面的 Bean 配置等价于如下代码片段:
aaa = new Battery();
aaa.setName("AAA");
aaa.setPrice(2.5);
aaa.setRechargeable(true);
cdrw = new Disk();
cdrw.setName("CD-RW");
cdrw.setPrice(1.5);
cdrw.setCapacity(1024);
但是,如果有一个或者多个 <constructor-arg> 标签,Spring 将匹配参数最合适的构造方法。作为例子,我们将前面的“beans.xml”配置文件拷贝一份,并更名为“beans-constructor.xml”,并按如下所示修改配置文件:
<bean
id="aaa"
class="sesametech.springrecipes.s006.Battery">
<constructor-arg value="AAA"/>
<constructor-arg value="2.5"/>
<property name="rechargeable"value="true" />
</bean>
<bean
id="cdrw"
class="sesametech.springrecipes.s006.Disk">
<constructor-arg value="CD-RW"/>
<constructor-arg value="1.5"/>
<property name="capacity"value="1024" />
</bean>
同时,我们修改一下“ProductTest”测试类中 main() 方法里的应用程序上下文加载配置文件的路径,如下所示:
//ApplicationContext context = new ClassPathXmlApplicationContext("sesametech/springrecipes/s006/beans.xml");
context = newClassPathXmlApplicationContext("sesametech/springrecipes/s006/beans-constructor.xml");
此时,看起来运行一切正常。因为 Product 和子类在构造方法上没有任何歧义,前述的 Bean 配置等价于下面的代码片段:
aaa = new Battery("AAA", 2.5);
aaa.setRechargeable(true);
cdrw = new Disk("CD-RW", 1.5);
cdrw.setCapacity(1024);
解决构造方法歧义
当你为 Bean 指定一个或者多个构造方法时参数时,Spring将试图在 Bean 类中寻找对应的构造方法并传递用于实例化Bean 的参数。但是,如果你的参数可以同时适用多个构造方法,就可能会造成 Spring 在匹配构造方法时出现歧义。这样的话,Spring 可能无法调用你所预期的构造方法。为了解决这个问题,Spring给出一种解决方案,允许你为 <constructor-arg> 标签指定type 和 index 属性,以帮助 Spring 查找预期的构造方法。
作为示例,我们在上面的“Disk”子类中增加一个“type”属性,同时增加一个构造方法,并覆写“Product”父类的toString() 方法,代码片段如下:
private int capacity; // 容量
private String type; // 类型
public Disk() {
super();
}
public Disk(String name, String type) {
this.setName(name);
this.setType(type);
}
public Disk(String name, double price) {
super(name, price);
}
@Override
public String toString() {
return super.toString() + " " + type + " " + capacity;
}
……
然后,在“beans-constructor.xml”配置文件中增加一个 ID 为“dvdrw”的 bean 定义,如下所示:
<bean
id="dvdrw"
class="sesametech.springrecipes.s006.Disk">
<constructor-arg value="DVD-RW"/>
<constructor-arg value="3.5"/>
<property name="type" value="DVD"/>
<property name="capacity"value="4096" />
</bean>
最后,我们在“ProductTest”测试类中,增加以下代码:
Diskdvdrw = (Disk) context.getBean("dvdrw");
System.out.println(dvdrw.toString());
我们的意图是,配置一个 name 为“DVD-RW”、price为“3.5”、type 为“DVD”、capacity 是“4096”的产品,但此时你若运行此程序将会得到如下结果:
AAA 2.5
CD-RW 0.0 1.5 1024
DVD-RW 0.0 DVD 4096
price 变成了“0.0”并且 CD-RW 的 type 变成了“1.5”。这个意外结果的起因是调用了第一个带有 name 和 type 参数的构造方法,而不是第二个带有 name 和 price 参数的构造方法。这是因为 Spring 默认将两个参数都解析为String 类型,而第一个构造方法不需要类型转换,因此被认定为最合适的。为了达到我们的预期,你必须设置<constructor-arg> 中的 type 属性。因此,我们把配置文件修改成如下所示:
<bean
id="cdrw"
class="sesametech.springrecipes.s006.Disk">
<constructor-arg type="java.lang.String"value="CD-RW" />
<constructor-arg type="double" value="1.5" />
<property name="type"value="CD" />
<property name="capacity"value="1024" />
</bean>
<bean
id="dvdrw"
class="sesametech.springrecipes.s006.Disk">
<constructor-arg type="java.lang.String"value="DVD-RW" />
<constructor-arg type="double"value="3.5" />
<property name="type"value="DVD" />
<property name="capacity"value="4096" />
</bean>
再次运行程序,如果不出意外,应该得到正常的结果,如下所示:
AAA 2.5
CD-RW 1.5 CD 1024
DVD-RW 3.5 DVD 4096
现在,我们再为“Disk”子类增加一个“discount”折扣属性,然后再增加一个新的构造方法,如下所示:
public class Disk extends Product {
private int capacity; // 容量
private String type; // 类型
private double discount; // 折扣
public Disk() {
super();
}
public Disk(String name, String type) {
this.setName(name);
this.setType(type);
}
public Disk(String name, double price) {
super(name, price);
}
public Disk(double discount, String name) {
this.setDiscount(discount);
this.setName(name);
}
@Override
public String toString() {
return super.toString() + " " + discount + " " + type + " " + capacity;
}
getter & setter …
}
此时,再次运行程序,可能得到正确的这结果,或者得到如下意外的结果:
AAA 2.5
CD-RW 0.0 1.5 CD 1024
DVD-RW 0.0 3.5 DVD 4096
price 为“0.0”,而本应该是 price 的值却变成了 discount 了。这种不确定性的起因是 Spring 在内部对每个构造方法与参数的兼容性评分造成的。但Spring 在评分过程中并没有考虑参数出现在 XML 配置文件中的顺序。这意味着,Spring对第一个构造方法和第三个构造方法的评分是相同的,而具体选择哪一个构造方法又取决于匹配的顺序。根据 JavaReflection API 中的 Class.getDeclaredConstructors() 方法,返回的构造方法顺序将是任意的、无序的,可能与声明的顺序不同或者恰好相同。所有这些因素导致了构造方法匹配中的歧义。
为了避免这个问题,你必须通过<constructor-arg> 中的 index 属性,明确指出参数的索引位置。设置了type 和 index 属性后,Spring 就可以精确地为 Bean 找到预期的构造方法。但是,如果相当确定构造方法不会导致歧义,就可以忽略type 和 index 属性设置。
我们再次将上面示例中的配置文件改成如下所示:
<bean
id="cdrw"
class="sesametech.springrecipes.s006.Disk">
<constructor-arg type="java.lang.String"index="0" value="CD-RW"/>
<constructor-arg type="double"index="1" value="1.5"/>
<property name="discount"value="0.7" />
<property name="type"value="CD" />
<property name="capacity"value="1024" />
</bean>
<bean
id="dvdrw"
class="sesametech.springrecipes.s006.Disk">
<constructor-arg type="java.lang.String"index="0" value="DVD-RW"/>
<constructor-arg type="double"index="1" value="3.5"/>
<property name="discount"value="0.9" />
<property name="type"value="DVD" />
<property name="capacity"value="4096" />
</bean>
好了,今天就写到这里了,继续加油~