《spring实战》学习笔记:2.装配Bean

声明Bean

Spring容器提供了两种配置Bean的方式,1.使用XML文件作为配置bean对象,                                                                                                                                                  2.基于Java注解的配置bean对象。 

使用XML文件配置bean对象:

  以下是一个典型的Spring 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 -->

</beans>

在标签内可以放置相关的Spring配置信息.

声明一个简单的<bean>

//定义一个老师类
package com.demo.spring;

public class Teacher {
    private String name;
    private String age;
    
    public Teacher(){
        
    }
    public Teacher(String name,String age){
        this.name = name;
        this.age = age;
    }
    void work(){
        System.out.println("jiaoshu");
    }
}

在xml文件中配置一下标签:

<bean id="student" class="com.demo.spring.Teacher"></bean>

<bean>元素是Spring中最基本的配置单元,通过该元素Spring将创建一个对象。当Spring容器加载该Bean时,Spring将使用默认的构造器来实例化该Bean,实际上,student会使用如下代码来创建: 
new com.demo.spring.Teacher();

测试代码:


public class TeacherTest {
    @Test
    public void test(){
        //使用应用上下文创建bean
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("config/application.xml");
        Teacher teacher = (Teacher) context.getBean("teacher");
        teacher.work();
    }
}

测试可知,Teacher对象创建成功。这就是控制反转(IOC),将创建对象的权利交给了Spring容器。原理就是                                   通过配置文件获取全类名,然后通过反射和工厂模式创建对象。

我们知道Teacher类有两个私有成员属性,控制反转虽然创建了对象,但没有对属性赋值。怎么对属性赋值就是所谓的依赖注入(DI)。

依赖注入方式有两种:

通过构造器注入:实际调用的是有参构造。

让bean使用另外一个构造方法创建对象,需修改配置文件如下:此时创建出来的teacher对象是有name和age属性值的。

 <bean id="teacher" class="com.demo.spring.Teacher">
            <constructor-arg name="name" value="数学"></constructor-arg>
            <constructor-arg name="age" value="30"></constructor-arg>
        </bean>

我们知道,一个类的依赖的对象不止是String,基础数据类型,还可能是数组,集合,其他类对象等,注入方式有所不同。

如存在一个学生类:

package com.demo.spring;

public class Student {
    private String name;
    private String age;
    private Teacher teacher;

    public Student(String name, String age,Teacher teacher  ) {
        this.name = name;
        this.age = age;
        this.teacher = teacher;
    }

    public void study(){
        System.out.println("xuexi");
    }
}

它依赖了一个老师类对象。

这配置文件修改如下:

       <bean id="teacher" class="com.demo.spring.Teacher">
            <constructor-arg name="name" value="数学"></constructor-arg>
            <constructor-arg name="age" value="30"></constructor-arg>
        </bean>
        <bean id="student" class="com.demo.spring.Student">
            <constructor-arg name="name" value="小学"></constructor-arg>
            <constructor-arg name="age" value="12"></constructor-arg>
            <constructor-arg name="teacher" ref="teacher"></constructor-arg>
        </bean>

先配置一个Teacher类,然后在构造方法中使用ref标签指向Teachker类的id。可知

1、value:注入一般属性。

2、ref(引用一个对象): 注入对象类型属性。

对于其他集合的构造器注入,配置文件大致如下,当集合中存储的是对象时,将value改成ref

           <constructor-arg>
                <list>
                    <value>1</value>
                    <value>2</value>
                </list>
            </constructor-arg>
            <constructor-arg>
               <set>
                   <value>1</value>
                   <value>2</value>
               </set>
            </constructor-arg>
            <constructor-arg>
                <map>
                    <entry key="name" value="xxx"></entry>
                    <entry key="age" value="12"></entry>
                </map>
            </constructor-arg>

通过设置属性完成依赖注入:实际调用的是set方法,所以对应属性必须存在对应的set方法。

 <bean id="teacher" class="com.demo.spring.Teacher">
           <property name="name" value="xx"></property>
            <property name="age" value="12"></property>
        </bean>

其他对象、集合的依赖注入与构造器注入同理。只是标签不一样了。

使用名称空间

P名称空间、C名称空间

首先它们不是真正的名称空间,是虚拟的。它是嵌入到spring内核中的。

使用p名称空间可以解决我们setter注入时<property>简化

使用c名称空间可以解决我们构造器注入时<constructor-arg>简化

p:<属性名>="xxx" 引入常量值

p:<属性名>-ref="xxx" 引用其它Bean对象

使用名称空间后配置文件简化如下:

<beans xmlns="http://www.springframework.org/schema/beans"
        //引入p名称空间
       xmlns:p="http://www.springframework.org/schema/p"
        //引入cm名称空间
       xmlns:c="http://www.springframework.org/schema/c"
       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="teacher" class="com.demo.spring.Teacher" p:age="12" p:name="xx">
        </bean>
</beans>

SpEL基本用法

字面值
比如<property name="count" value="#{5}"/>,#{ }标记会提示Spring这是个SpEL表达式。

 <bean id="teacher" class="com.demo.spring.Teacher">
        <property name="name" value="xx"></property>
        <property name="age" value="30"></property>
    </bean>
        <bean id="student" class="com.demo.spring.Student">
            <property name="name" value="#{teacher.name}"></property>
            <property name="age" value="#{teacher.age}-10"></property>
            <property name="teacher" ref="teacher"></property>
        </bean>

如上配置,代表学生的名称引用teacher的名称,age是teacher的age-10
表达式还可以调用其他Bean的方法: 

<property name="name" value="#{teacher.work()}"></property>

操作类:可以使用T()运算符调用类作用域的方法和常量。比如: 
<property name="multiplier" value="#{T(java.lang.Math).PI}"/> 
<property name="randomNumber" value="#{T(java.lang.Math).random()}"/>

使用注解配置bean对象:

1.当需要创建的bean的类是自己创建的时

创建一个Teacher类如下:在类上使用了@Componet

package com.demo.spring;

import org.springframework.stereotype.Component;
@Component
public class Teacher {
    private String name;
    private String age;
    void work(){
        System.out.println("jiaoshu");
    }

}

@Componet:表明该类会作为组件类,并告知Spring要为这个类创建bean实例。默认名字是类名首字母小写,也可以自己指定

@ComponentScan("teacher")

Spring的框架中提供了与@Component注解等效的三个注解:

  • @Repository 用于对DAO实现类进行标注
  • @Service 用于对Service实现类进行标注
  • @Controller 用于对Controller实现类进行标注

但配置了这个注解还不够,因为spring的组件扫描是默认关闭的,还必须显式配置一下Spring,命令它开启扫描,寻找带有@Componet的类。开启方式有两种:

 

1.使用注解@ComponentScan开启扫描。我们可以直接在需要创建bean的类上添加该注解,但是为了方便,一般会创建一个配置类

@Configuration
@ComponentScan
public class Config {
}

@ComponentScan默认是扫描当前类所在路径及其路径。可以通过value属性指定单个包名或者basePackages指定多个包名

@Configuration
@ComponentScan("com.demo.spring")
public class Config {
}
@Configuration
@ComponentScan(basePackages = {"com.demo.spring","com.demo"})
public class Config {
}

2.使用xml配置文件开启扫描,可以配置多个开启多个路径

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
    
    <context:component-scan base-package="com.demo.spring"/>
    <context:component-scan base-package="com.demo"/>
    
</beans>>

这样就可以创建bean了,测试代码如下:

package com.demo.spring;

import com.demo.Config;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Config.class)
public class TeacherTest {
    @Autowired
    private Teacher teacher;
    @Test
    public void test(){
        teacher.work();
    }
}

这样也只是完成了控制反转(IOC),还没有实现依赖注入(DI).

@Value: 简单属性注入

@Autowired:复杂属性注入,

@Component
public class Student {
    @Value("xx")
    private String name;
    @Value("12")
    private String age;
    @Autowired
    private Teacher teacher;

    public void study(){
        System.out.println("xuexi");
    }
}

当有多个bean满足依赖关系时,

             1.使用@Primary注解指定首选bean,这只能指定一个。

@Component
@Primary
public class English implements Course {
}

           2.使用@Qualifier指定唯一的bean。在扫描的时候可以通过@Qualifier或@Component指定创建的bean的名称,注入时通过@Qualifier指定

@Component("test")
public class English implements Course {
}
或者
@Component
@Qualifier("test")
public class English implements Course {
}


@Autowired
@Qualifier("test")
private Course course ;

       3.自定义注解。 

使用注解进行依赖注入时不需要设置set方法。猜测底层是利用反射进行暴力访问得到私有属性进行赋值。

使用注解进行控制反转时,默认的bean名字就是类名的首字母小写。

所以这时候在配置文件中配置必要的bean时不要配置名字为类名首字母小写的bean,以免冲突。

2.当使用的是第三方的类时

此时无法在类上添加@Componet注解,所以无法进行扫描,也无法使用@Autowried完成依赖注入,这时就需要使用@Bean标签。

@Bean标签作用于方法上,使用如下:我们假设Teacher这个类是第三方类,为了获取它的bean,创建一个方法返回一个Teacher实例,然后在方法上使用@Bean注解。

public class Config {
    @Bean
    public Teacher getTeacher(){
        return new Teacher();
    }
}

Spring会将配置了@Bean的方法生成一个bean放入容器,之后不管这个方法调用多少次,都会返回那一个bean,当然可以使用@Scope配置作用域,与以下使用xml配置文件作用相同。生成的bean默认id为类名小写,可以自己指定@bean(name="")。

<bean id="student" class="com.demo.spring.Teacher"></bean>

@Bean实现依赖注入,Student类依赖一个Teacher类。

public class Config {
    @Bean
    public Teacher getTeacher(){
        return new Teacher();
    }
    @Bean
    public Student getStudent1(Teacher teacher){
    //构造方法注入
        return new Student(getTeacher());
    } 
    @Bean
    public Student getStudent2(Teacher teacher){
        Student student = new Student();
        //属性注入
        student.setTeacher(getTeacher());
        return student;
    }

为了方便管理,将使用@Bean生成bean的方法都集中在同一个包下的不同Config类中。

导入和混合配置

1.当一个XML配置文件过大时,我们可以将它拆成多个小的配置文件,然后在一个配置文件中引入另一个配置文件,或者在最后新建一个配置文件,然后引入所有其他配置文件。使用方法如下:使用import标签。

<?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">
    <import resource="application1.xml"></import>
</beans>

2.当使用Config类配置时,如果一个Config类过大,也可以拆分成多个Config类,然后在一个Config类中引入另一个Config类,或者在最后新建一个Config类,然后引入所有Config类。使用方法如下:使用@Import。

//批量引入
@Import({Config2.class, Config1.class})
public class Config3 {
}
//单个引入
@Import(Config1.class)
public class Config2 {
}

 3.Config类中引入XML配置文件,使用注解ImportResource

@ImportResource("classpath:application.xml")
public class Config2 {
}

4.XML配置文件中引入Config类

<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  class="com.demo.spring.Config2"></bean>
</beans>

 

Bean的作用域

《spring实战》学习笔记:2.装配Bean

可以通过注解@Scope去设置,或者在配置文件中设置。默认时单例。

@Component
@Scope("prototype")
@ComponentScan("teacher")
public class Teacher

或者

<bean id="account" class="com.foo.DefaultAccount" scope="prototype"/> 

当使用会话和请求作用域时,除了设置作用域,还要设置代理,解决将会话或请求bean注入到单例bean中的问题

@Scope(value = WebApplicationContext.SCOPE_SESSION,
       proxyMode = ScopedProxyMode.INTERFACES)
public class Math implements Course {...}

如有一个StoreService要处理ShoppingCar. StoreService是单例的,而ShoopingCar是会话的。怎么保证StoreService每次处理的ShoopingCar就是当前会话的?

@Component
public class StoreService {
    @Autowired
    private void setShoppingCart(ShoppingCart shoppingCart){
        this.shoppingCart = shoppingCart;
    }
}

实现原理如下图:Spring会注入一个到ShoppingCart bean的代理,这个代理会暴露与ShoppingCart相同的方法,所以StoreService会认为它就是一个购物车。但是,当StoreService调用ShoppingCart的方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的ShoppingCart bean。
当代理的是接口时使用 proxyMode = ScopedProxyMode.INTERFACES,使用的是JDK动态代理

当代理的是接口时使用 proxyMode = ScopedProxyMode.TARGET_CLASS,使用的是CGLib动态代理

 

《spring实战》学习笔记:2.装配Bean

当然也可以使用xml配置代理方式:配置文件默认使用的是CGLib动态代理模式

   <bean id="cart" class="com.demo.ShoppingCart">
       <aop:scoped-proxy/>//开启CGLib代理
   </bean>

  <bean id="cart" class="com.demo.ShoppingCart">
       <aop:scoped-proxy proxy-target-class="false"/>//开启JDK代理
   </bean>

 

参考资料:《Spring实战》