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。
如何生成这个对象:
- 读取并解析 xml,Spring 的 ApplicationContext 常用来完成读取和解析的工作,并且将对象实例化放置在 Spring 的容器里;
- 从容器里获取这个对象。
首先我们在 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 应用中使用,一次请求中只创建一个实例,不同的请求会创建不同的实例。