Spring 学习其一:IOC

一:传统的生产对象的方式

我们一般在 java 中生产一个对象,会在代码中 new 一个对象,然后通过 set 的方式给他注入我们想要的属性。也就是说,java 在编译期间就知道,我们要生产什么对象,要配置哪些属性,就像下面这样:

 1 public class EmployeeTest {
 2 
 3     public static void main(String[] args) {
 4         EmployeeModel employee = new EmployeeModel();
 5         employee.setEmpNo(2001);
 6         employee.setFirstName("Li");
 7         employee.setLastName("lei");
 8         employee.setGender("M");
 9         System.out.println(employee.getFirstName() + " " + employee.getLastName());
10     }
11 
12 }
13 /*output:
14 Li lei*/

二,通过 xml 生产一个对象

而 Spring 提供了另一种方法,通过 xml 文件或者注解的方式,对这个对象进行描述,然后,在运行时让系统根据描述去创建一个对象。这就是 IOC,控制反转。

如果我们对上面例子中的 emplyee 进行描述,会是这样:

  • 这是一个 EmployeeModel 对象
  • 它的名称叫 emplyee 
  • 它有一个属性 empNo 值是 2001
  • 属性 fitstName 值是 Li
  • ...
  • 设置这些值的方式是通过 set

上面的描述用 xml 表示是这样的:

 <bean id = "employee" class ="SpringTest.EmployeeTest.EmployeeModel">
    <property name = "empNo" value = "2001" />
    <property name = "firstName" value = "Li"/>
    <property name = "lastName" value = "Lei"/>
  </bean>

id = "employee"  对应 它的名称叫 emplyee

class ="SpringTest.EmployeeTest.EmployeeModel" 对应 这是一个 EmployeeModel 对象

<property name = "empNo" value = "2001" /> 对应 有一个属性 empNo 值是 2001

而配置值得方式,默认就是使用 set 方式。

这样,我们把一个类用 xml 形式描述出来,然后 Spring 就可以通过读取 这个 xml 并解析它,然后利用反射机制去生成这个对象,这就是 Spring IOC。

如何生成这个对象:

  1. 读取并解析 xml,Spring 的 ApplicationContext 常用来完成读取和解析的工作,并且将对象实例化放置在 Spring 的容器里;
  2. 从容器里获取这个对象。

首先我们在 classpath (src目录下)先创建一个 spring-cfg.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">
</beans>

然后,在 <beans> 元素内加入之前关于 employee 描述的代码:

<?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 = "employee" class ="SpringTest.EmployeeTest.EmployeeModel">
    <property name = "empNo" value = "2001" />
    <property name = "firstName" value = "Li"/>
    <property name = "lastName" value = "Lei"/>
  </bean>
  
</beans> 

然后通过 ApplicationContext  读取这个 xml

ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");

此时,对象已经创建完毕,放置在容器里,我们从容器里获取即可,关于 ClassPathXmlApplicationContext 从字面我们可以理解出它是一个从 classpath 里的 xml 文件创建上下文的类。

如果想通过构造函数来设置属性值,bean 应该这样设置:

  <bean id = "employee" class ="SpringTest.EmployeeTest.EmployeeModel">
    <constructor-arg index = "0" value = "2001"/>
    <constructor-arg index = "1" value = "Li"/>
  </bean>

index 表示第几位参数。

ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");
EmployeeModel employee = (EmployeeModel) ctx.getBean("employee");
System.out.println(employee.getFirstName() + " " + employee.getLastName());

如果我们要 set 的值,是一个对象,或者是一个容器,该如何实现,为了省事,这里直接用 List 包装 salary:

<bean id = "salary1" class ="SpringTest.EmployeeTest.SalaryModel">
    <property name = "empNo" value = "2001" />
    <property name = "salary" value = "10000" />
  </bean>
  <bean id = "salary2" class ="SpringTest.EmployeeTest.SalaryModel">
    <property name = "empNo" value = "2001" />
    <property name = "salary" value = "10000" />
  </bean>
  <bean id = "employee" class ="SpringTest.EmployeeTest.EmployeeModel">
    <property name = "empNo" value = "2001" />
    <property name = "firstName" value = "Li"/>
    <property name = "lastName" value = "Lei"/>
    <property name = "salaries">
      <list>
        <ref bean="salary1" />
        <ref bean="salary2" />
      </list>
    </property>
  </bean>

要有装填 map、properties 等容器的,方式相近,这里不再讨论。

三、用注解的方式生成 Bean

如果觉得 xml 的方式的工作量太大,可以选择注解的方式去生成对象和注入值,依旧是从最简单的基本类型值注入开始,这里只截取使用注解方式注入值的属性:

 1 import java.util.Date;
 2 import java.util.List;
 3 
 4 import org.springframework.beans.factory.annotation.Value;
 5 import org.springframework.stereotype.Component;
 6 
 7 import Test.Salary.model.SalaryModel;
 8 
 9 
10 @Component(value="employee")
11 public class EmployeeModel {
12     
13     @Value("1")
14     private int empNo; 
15     private Date birthDate;
16     @Value("crazy")
17     private String firstName;
18     @Value("runcheng")
19     private String lastName;
20     private String gender;
21     private Date hireDate;
22     private List<SalaryModel> salaries = null;
23     public int getEmpNo() {
24         return empNo;
25     }
26     public void setEmpNo(int empNo) {
27         this.empNo = empNo;
28     }
29     public Date getBirth_date() {
30         return birthDate;
31     }
32     public void setBirth_date(Date birth_date) {
33         this.birthDate = birth_date;
34     }
35     public String getFirstName() {
36         return firstName;
37     }
38     public void setFirstName(String firstName) {
39         this.firstName = firstName;
40     }
41     public String getLastName() {
42         return lastName;
43     }
44     public void setLastName(String lastName) {
45         this.lastName = lastName;
46     }
47     public String getGender() {
48         return gender;
49     }
50     public void setGender(String gender) {
51         this.gender = gender;
52     }
53     public Date getHireDate() {
54         return hireDate;
55     }
56     public void setHireDate(Date hireDate) {
57         this.hireDate = hireDate;
58     }
59     
60     public String toString() {
61         StringBuilder strBuilder = new StringBuilder();
62         strBuilder.append("empNo : ");
63         strBuilder.append(empNo);
64         strBuilder.append(" firstName : ");
65         strBuilder.append(firstName);
66         strBuilder.append(" lastName : ");
67         strBuilder.append(lastName);
68         strBuilder.append("\nsalaries:\n");
69         if(salaries != null) {
70             for(SalaryModel index : salaries) {
71                 strBuilder.append(index.toString());
72                 strBuilder.append("\n");
73             }
74         }
75         return strBuilder.toString();
76     }
77     public List<SalaryModel> getSalaries() {
78         return salaries;
79     }
80     public void setSalaries(List<SalaryModel> salaries) {
81         this.salaries = salaries;
82     }
83 }

首先需要在类的上方使用 @Component({id}) 注解表明这是一个需要被注册成 Bean 的类,id 就是注册成功后它的 bean id。

其次,在 EmployeeModel 类的几个属性上方使用注解 @Value(),括号内是想要注入的值。但是如果我们直接去生成一个 EmployeeModel  对象,这些属性是不会被赋值的。我们需要把它装配到 IOC 容器(即注册成一个 Bean),Spring 才有机会给它注入值,如果不使用上述的 xml 的方式,该如何注册这个 javaBean 呢?

可以设置一个扫描器,告诉 Spring 去扫描这个类,并且生成它的 Bean 放入 IOC 容器。

import org.springframework.context.annotation.ComponentScan;

import Test.Employee.model.EmployeeModel;

@ComponentScan(basePackages = {"Test.Employee.model"},
               basePackageClasses = {EmployeeModel.class})
public class EmployeeConfig {

}

以上就是扫描器的全部内容,这个类不具备任何价值,所以它内部是空的,它的作用的是提供了一个空间放置上方的注解:@ComponentScan,从名称就可以看出,它的作用是扫描被标记为 Component 的类。它的参数包括

basePackages:需要扫描的包;

basePaceageClasses:需要扫描的类。

当 @Component 不设置任何参数时,它会只扫描当前的包里的 Component,设置 basePackages 可以让他扫描我们指定的包,这个指定是可以向下拓展的,即我们制定扫描了包 A,包 A.B A.B.C A.D 都会被扫描。这里有个疑问,如果我们制定了包之后,它还会去扫描当前包吗?经过测试,答案是不会。从使用情况可以看出,basePackages 接受的是一个数组,也就是我们可以用 ,分隔的方式,制定多个包。

basePaceageClasses 直接指定需要扫描的类。

两者可同时存在。

设置了扫描器,我们就可以通过加载扫描器的方式,去注册 bean 了:

public class EmployeeTest {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(EmployeeConfig.class);
        EmployeeModel model = (EmployeeModel)ctx.getBean("employee");
        System.out.println(model);
        
    }

}

输出结果为:

empNo : 1 firstName : crazy lastName : runcheng
salaries:

四、自动装配

上述例子实现了通过注解的方式注入普通值,现在演示如何注入一个对象。

 我们重新建一个类,并在 employee 里添加一个该类的对象作为属性,当然,不要忘记给它添加 @Component 和 @Value 注解:

@Component(value="wife")
public class Wife {
    
    @Value("lingzhilin")
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
}

然后,我们再 EmployeeModel 里添加一个该类的对象,以及它的 set 和 get

private Wife wife = null;
    public Wife getWife() {
        return wife;
    }
    public void setWifr(Wife wife) {
        this.wife = wife;
    }

我们两种方式,给它注入值,在属性上方添加 @Autowired 注解 或者在 set 上方添加即:

@Autowired
private Wife wife = null;

或:

@Autowired
public void setWifr(Wife wife) {
    this.wife = wife;
}

自动装配 Autowired 是根据对应的类型来装配的,这样会有一个问题,如果我们有多个 Wife 该怎么办,这里不再掩饰,Spring 会报错,抛出异常。

结果办法是使用 @Primary 指定优先级(或优先选择被 Primary 指定的,但是多个 Primary 时会出错,不推荐),或者使用 @Qualifer:

@Autowired
@Qualifier("wife")
private Wife wife = null;

 五、注解方法

注解大大方便了我们注册 bean 的步骤,但是,当我们使用第三方的包时,是不可能再别人的类的前面加上 @Component 注解的。这是我们可以通过新建一个类取继承该类的方式,又或者使用 @Bean 给方法添加注解,把返回值注册成一个 Bean。

Bean 的使用方式为 @Bean(name = {id}),将该注解放在方法的上方即可。

六、Profile

Bean id 是唯一的,也就是我们在根据 bean id 使用一个 bean 时,这个 Bean 是确定。但是,我们可能会遇到这样的情况,比如在测试环境时,我们有大量的 Bean 是针对测试环境,而去生产环境时,有需要去修改这些 Bean。这样的工作量很大,有没有一种办法,能够用最少的工作量去实现两个环境之间的灵活切换?Profile 就可以解决这个问题。

我们用一个很简单的程序来测试 Prifile

首先是一个用来输出某个字符串的类:

public class PrintEnv {
    private String env = null;
    public void PrintIt() {
        System.out.println(env);
    }
}

这段代码唯一的作用就是输出 env,现在我们在 spring-cfg.xml 中配置这个类:

<beans> 
<bean id = "printEnv" class = "ProfileTest.PrintEnv">
    <property name = "env" value = "test"/>
  </bean>
</beans> 

我们通过 IOC 容器去装配并且获取这个 Bean 的时候,调用它的 PrintIt() 方法,会输出 test。

现在我们利用 Profile 创建两个 id 一样的 Bean,profile 是 <beans> 的属性:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3     xmlns="http://www.springframework.org/schema/beans" 
 4     xmlns:aop="http://www.springframework.org/schema/aop"
 5     xmlns:context="http://www.springframework.org/schema/context" 
 6     xmlns:tx="http://www.springframework.org/schema/tx"
 7     xmlns:cache="http://www.springframework.org/schema/cache" 
 8     xmlns:p="http://www.springframework.org/schema/p"
 9     xsi:schemaLocation="http://www.springframework.org/schema/beans 
10      http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
11      http://www.springframework.org/schema/aop
12      http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
13      http://www.springframework.org/schema/context
14      http://www.springframework.org/schema/context/spring-context-4.0.xsd
15      http://www.springframework.org/schema/tx
16      http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
17      http://www.springframework.org/schema/cache 
18      http://www.springframework.org/schema/cache/spring-cache-4.0.xsd">
19     <beans profile = "test">
20       <bean id = "printEnv" class = "ProfileTest.PrintEnv">
21         <property name = "env" value = "test"/>
22      </bean>
23     </beans>
24     <beans profile = "dev">
25       <bean id = "printEnv" class = "ProfileTest.PrintEnv">
26         <property name = "env" value = "dev"/>
27       </bean>
28   </beans>
29 </beans>

这里贴上整段代码,是因为新建的 两个 <beans> 是 最外面的 <beans> 的子元素。

public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(
                "spring-cfg.xml");
        PrintEnv printEnv = (PrintEnv)ctx.getBean("printEnv");
    }

如果直接像上面一样去获取 printEnv 是会抛出 No bean named 'printEnv' is defined 异常的,我们必须为 Profile 指定一个选择(到底是 test 还是 dev)

常用的方法有:

SpringMVC 配置:后面再说

JNDI 配置:忽略

环境变量配置:不推荐

JVM 启动参数配置:需配合 Tomcat 后面再说

七、加载 properties 文件

在 mybatis 时,我们用过 <properties resource="jdbc.properties"/> 的方式加载过属性文件,在 Spring 里加载的方式为:

<context:property-placeholder ignore-resource-not-found="true" location="classpath:jdbc.properties" />

其中 ignore...属性表示是否允许文件找不到,为 false 时,找不到文件会抛出异常。

如果需要配置多个文件,可以用下面的方式:

  <bean id="propertyConfigurer"
        class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>classpath:jdbc.properties</value>
            </list>
        </property>
    </bean>

 

该方法通过生产一个 PropertyPlaceholderConfigurer 的 Bean 然后通过这个 Bean 去加载。

八、条件化转载 Bean

除了用 profiles 这种比较全局的方式去条件化装载外,还可以通过 Conditional 的方式去实现,其格式为

@Conditional(Class class)

class 是一个实现了 Condition 接口的类,Conditional 会根据接口方法 matches 返回的布尔值判断是否将注释的内容加载为 Bean,常用来判断数据库配置是否加载完成:

@Bean(name = "dataSource")
    @Conditional({DataSourceCondition.class})
    public DataSource getDataSource() {
        ...
    }
public class DataSourceCondition implements Condition {

    @Override
    public boolean matches(ConditionContext arg0, AnnotatedTypeMetadata arg1) {
        Environment env = arg0.getEnvironment();
        return env.containsProperty("jdbc.database.driver")
            ...
    }

}

九、bean 的作用域

Spring 给 bean 提供了 4 中作用域:

  • 单例(singleton):默认选项,在整个应用中,spring 只为其生成一个 Bean 的实例;
  • 原型(prototype):每次注入或者通过 IOC 容器获取时,Spring 都会创建一个新的实例;
  • 会话(session):在 Web 应用中使用,会话过程中只创建一个实例;
  • 请求(request):在 Web 应用中使用,一次请求中只创建一个实例,不同的请求会创建不同的实例。
posted @ 2018-10-01 19:17  crazy_runcheng  阅读(175)  评论(0编辑  收藏  举报