Spring Framework 学习笔记1:基础
1.简介
1.1.生态和发展史
关于 Spring 的生态和发展史,可以观看这个视频。
1.2.系统架构
关于 Spring 的系统架构,可以观看这个视频。
2.Ioc
Spring 的核心概念是 Ioc (Inversion Of Control),它的目的是降低代码的耦合度,让对象不再由用户创建,而是由 Ioc 容器(Ioc Container)来创建和管理。
这里用一个简单示例说明 Spring 如何通过 Ioc 思想来对对象创建进行解耦。
这个项目结构很简单:
├─src
│ ├─main
│ │ ├─java
│ │ │ └─cn
│ │ │ └─icexmoon
│ │ │ └─springdemo
│ │ │ │ Application.java
│ │ │ │
│ │ │ ├─dao
│ │ │ │ │ UserDao.java
│ │ │ │ │
│ │ │ │ └─impl
│ │ │ │ UserDaoImpl.java
│ │ │ │
│ │ │ └─service
│ │ │ │ UserService.java
│ │ │ │
│ │ │ └─impl
│ │ │ UserServiceImpl.java
│ │ │
│ │ └─resources
│ └─test
│ └─java
项目中的各种对象之间的依赖都是直接用new
创建的:
public class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoImpl();
@Override
public void save(){
System.out.println("UserServiceImpl.save() is called.");
userDao.save();
}
}
入口类也是简单的new
了一个 Service 并执行具体方法:
public class Application {
public static void main(String[] args) {
UserService userService = new UserServiceImpl();
userService.save();
}
}
虽然这里实现类都有对应的接口,我们也都使用接口进行调用,但具体实现类的创建是用new
,这种耦合是无法避免的。假设我们要用另一个 UserDao 的实现来替换当前实现:
public class UserDaoImpl2 implements UserDao {
@Override
public void save() {
System.out.println("UserDaoImpl2.save() is called.");
}
}
就必须修改 UserService 的实现类中相应的 new 语句:
public class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoImpl2();
// ...
}
下面我们看 Spring 是如何做的。
2.1.依赖
首先需要添加 Spring Framework 的依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.10</version>
</dependency>
2.2.Spring Bean Definition
由 Ioc 容器创建和管理的对象被称作 Spring Bean,我们需要“告诉” Spring 框架需要创建哪些 Spring Bean 以及如何创建。具体来说需要用一个 XML 作为 Spring Bean 的定义文件(Spring Bean Definition)。
通过 Idea 在 Resource
目录下创建一个 application.xml 作为 Spring Bean 的定义文件。
添加 spring-context 依赖后,Idea 的创建 XML Configuration File 菜单中会出现一个子菜单 Spring Config,该菜单可以添加一个 Spring Bean 定义文件的模版。
初始的模版内容如下:
<?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>
按需要添加 Spring Bean 定义:
<?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="userService" class="cn.icexmoon.springdemo.service.impl.UserServiceImpl"/>
<bean id="userDao" class="cn.icexmoon.springdemo.dao.impl.UserDaoImpl"/>
</beans>
bean 节点的id
属性指定 Bean 名称,class
属性指定 Bean 类型。
2.3.Ioc 容器
接下来要创建 Ioc 容器,并用 Ioc 容器加载 Bean 定义,然后通过容器来获取对象。
public class Application {
public static void main(String[] args) {
//创建 IOC 容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("application.xml");
UserService userService = (UserService) ctx.getBean("userService");
userService.save();
}
}
2.4.依赖注入
现在入口类中实现了 Ioc,但 UserServiceImpl 中依然用 new 的方式创建依赖对象:
public class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoImpl2();
// ...
}
如果一个 Spring Bean 依赖的对象是另一个 Spring Bean,我们可以通过 Spring Bean 定义告诉 Spring 它们之间的依赖关系,并由 Spring 自动完成相应的依赖创建,这种方式叫做依赖注入(DI,Dependency Injection)。
在这个示例中,现在userService
和userDao
都已经被定义为 Spring Bean,所以可以:
<bean id="userService" class="cn.icexmoon.springdemo.service.impl.UserServiceImpl">
<property name="userDao" ref="userDao"/>
</bean>
<bean id="userDao" class="cn.icexmoon.springdemo.dao.impl.UserDaoImpl"/>
property
标签说明了userService
Bean 中的userDao
属性对应的另一个 Bean 是userDao
。
如果你用的是 Idea,应该可以注意到此时property
的 name
属性是标红的,因为 Spring 需要使用 set 方法实现依赖注入,所以我们还要为其添加一个 set 方法:
public class UserServiceImpl implements UserService {
@Setter
private UserDao userDao;
// ...
}
当然,现在不需要使用new
了。
这里我使用 Lombok 添加 set 方法,需要添加相关的 Lombok 依赖,这里不再赘述。
现在已经将对象创建进行了解耦,如果我们要使用UserDaoImpl2
作为实现而非UserDaoImpl
,只需要修改 Bean 定义即可,不需要修改代码:
<bean id="userDao" class="cn.icexmoon.springdemo.dao.impl.UserDaoImpl2"/>
3.Spring Bean
3.1.生命周期
Spring Bean 有生命周期,我们可以利用一些生命周期回调在 Bean 的特定阶段执行代码。
在 UserDao 中定义两个方法,分别代表在 Bean 创建后和 Bean 销毁前需要执行的回调方法:
public class UserDaoImpl2 implements UserDao{
// ...
public void afterConstruct(){
System.out.println("UserDaoImpl2 has constructed.");
}
public void beforeDestroyed(){
System.out.println("UserDaoImpl2 will be destroyed.");
}
}
要让这两个方法生效,还必须在 Bean 定义中告诉 Spring 这两个方法是生命周期回调方法:
<bean id="userDao"
class="cn.icexmoon.springdemo.dao.impl.UserDaoImpl2"
init-method="afterConstruct"
destroy-method="beforeDestroyed"/>
如果实际运行程序,你会发现beforeDestroyed
方法并不会被执行。
这是因为主程序执行完毕后,Java 虚拟机会直接进行垃圾回收,并不会通知 Ioc 容器,Ioc 容器自然也不会调用相应 Bean 的“销毁前回调方法”。
解决这个问题有两种方式,第一种是主动关闭 Ioc 容器:
public class Application {
public static void main(String[] args) {
//创建 IOC 容器
ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("application.xml");
UserService userService = (UserService) ctx.getBean("userService");
userService.save();
// 关闭 IOC 容器
ctx.close();
}
}
需要注意的是,通常使用的 Ioc 接口ApplicationContext
并没有close
方法,所以这里必须使用一个上层接口ConfigurableApplicationContext
作为引用。
第二种方式是将 Ioc 容器注册到 Java 虚拟机,这样 Java 虚拟机在程序执行完进行垃圾回收时就会通知 Ioc 容器,Ioc 容器自然就可以完成包括 Bean 生命周期回调之类的清理工作:
public class Application {
public static void main(String[] args) {
// 创建 IOC 容器
ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("application.xml");
// 注册关闭钩子
ctx.registerShutdownHook();
UserService userService = (UserService) ctx.getBean("userService");
userService.save();
}
}
除了在 Bean 定义中指定生命周期方法外,还可以实现生命周期回调的相关接口:
public class UserDaoImpl2 implements UserDao, InitializingBean, DisposableBean {
// ...
@Override
public void destroy() throws Exception {
System.out.println("UserDaoImpl2 will be destroyed.");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("UserDaoImpl2 has constructed.");
}
}
这样做就不再需要 XML 中定义相关回调方法:
<bean id="userDao"
class="cn.icexmoon.springdemo.dao.impl.UserDaoImpl2"/>
3.2.作用域
Bean 有作用域,默认是单例(Singleton)。也就是说用同一个名称(或类型)从 Ioc 容器中获取到的会是同一个 Bean 实例:
public class Application {
public static void main(String[] args) {
// ...
UserService userService = (UserService) ctx.getBean("userService");
UserService userService2 = (UserService) ctx.getBean("userService");
String equalResult = userService == userService2 ? "是同一个对象" : "不是同一个对象";
System.out.println(equalResult);
userService.save();
}
}
可以在 Bean 定义中改变这一点:
<bean id="userService"
class="cn.icexmoon.springdemo.service.impl.UserServiceImpl"
scope="prototype">
<property name="userDao" ref="userDao"/>
</bean>
现在 userService
Bean 的作用域是原型(Prototype),即每次获取都会产生一个新的实例。
如果是 Web 开发,还会有其他作用域,比如
request
等。
3.3.实例化
3.3.1.构造器
一般情况下,Spring Bean 是通过无参构造器实现的实例创建:
public class UserServiceImpl implements UserService {
// ...
public UserServiceImpl() {
System.out.println("UserServiceImpl's constructor is called.");
}
}
运行程序后可以看到这个构造器被 Ioc 容器调用并产生输出。
当然,一般并不需要我们显式创建无参构造器,一个类没有任何构造器时会有一个默认的无参构造器。
3.3.2.静态工厂
有时候,对于一些需要复杂初始化逻辑的对象,我们会使用工厂模式进行创建。在 Spring 中,同样可以用工厂模式创建 Bean 实例。
假设有一个用于创建 UserDao 实例的静态工厂:
public class UserDaoFactory {
public static UserDao createUserDao(){
System.out.println("UserDaoFactory.createUserDao() is called.");
return new UserDaoImpl2();
}
}
在 Bean 定义中我们不再使用具体的类型创建 userDao,而是改为使用静态工厂:
<bean id="userDao"
class="cn.icexmoon.springdemo.dao.factory.UserDaoFactory"
factory-method="createUserDao"/>
这里的class
是工厂类,factory-method
是具体创建 Bean 实例的方法。
3.3.3.实例工厂
并非所有的工厂模式都是静态工厂,有一些会使用实例工厂。即工厂类本身有一些状态属性,需要先创建工厂类的实例,再用工厂实例创建目标对象。
假设有这样一个工厂类:
public class UserDaoFactory2 {
public UserDao createUserDao(){
System.out.println("UserDaoFactory2.createUserDao() is called.");
return new UserDaoImpl2();
}
}
此时就需要将工厂实例也定义为 Spring Bean,然后用这个工厂实例完成目标 Bean 实例的创建:
<bean id="userDao"
factory-bean="userDaoFactory2"
factory-method="createUserDao"/>
<bean id="userDaoFactory2" class="cn.icexmoon.springdemo.dao.factory.UserDaoFactory2"/>
3.4.依赖注入
3.4.1.Setter 注入
之前在 Ioc 中介绍过通过 set 方法完成依赖注入。这种方式也叫做 Setter 注入,除了可以用 Setter 注入其它的 Bean 实例外,还可以注入基本类型或者 String 类型的数据:
@Setter
public class UserDaoImpl implements UserDao {
private String name;
private int age;
@Override
public void save() {
System.out.println("UserDaoImpl.save() is called.");
System.out.printf("Name is %s and age is %d%n", name, age);
}
}
Bean 定义:
<bean id="userDao"
class="cn.icexmoon.springdemo.dao.impl.UserDaoImpl">
<property name="age" value="11"/>
<property name="name" value="Tom"/>
</bean>
3.4.2.构造器注入
除了通过 Setter 进行注入,还可以通过构造器进行注入。
比如,用构造器注入其它 Bean:
public class UserServiceImpl implements UserService {
private UserDao userDao;
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
// ...
}
这里不再需要 Set 方法。
Bean 定义:
<bean id="userService"
class="cn.icexmoon.springdemo.service.impl.UserServiceImpl">
<constructor-arg name="userDao" ref="userDao"/>
</bean>
同样可以通过构造器注入简单类型:
public class UserDaoImpl implements UserDao {
private String name;
private int age;
public UserDaoImpl(String name, int age) {
this.name = name;
this.age = age;
}
// ...
}
Bean 定义:
<bean id="userDao"
class="cn.icexmoon.springdemo.dao.impl.UserDaoImpl">
<constructor-arg name="age" value="11"/>
<constructor-arg name="name" value="tom"/>
</bean>
上面用构造器参数名称来“定位”参数进行注入的方式是最常见的,但这样意味着构造器的形参名称不能改变。
所以构造器注入存在一些变种写法,比如指定参数位置:
<bean id="userDao"
class="cn.icexmoon.springdemo.dao.impl.UserDaoImpl">
<constructor-arg index="0" value="tom"/>
<constructor-arg index="1" value="11"/>
</bean>
这样做的缺陷是构造器形参的位置不能改变。
还比如通过参数类型进行匹配:
<bean id="userDao"
class="cn.icexmoon.springdemo.dao.impl.UserDaoImpl">
<constructor-arg type="java.lang.String" value="tom"/>
<constructor-arg type="int" value="11"/>
</bean>
这样做的缺陷是当形参中存在多个形参类型相同的情况,就无法完成匹配。
3.4.3.自动装配
使用自动装配可以省略手动注入的步骤:
<bean id="userService"
class="cn.icexmoon.springdemo.service.impl.UserServiceImpl">
<constructor-arg name="userDao" ref="userDao"/>
</bean>
将其改为自动装配:
<bean id="userService"
class="cn.icexmoon.springdemo.service.impl.UserServiceImpl"
autowire="byType">
</bean>
这样 Spring 就可以根据userService
中依赖的类型来自动查找符合条件的 Bean 并进行注入。
现在运行会出现一个空指针异常,因为自动装配是通过 Setter 注入实现的,所以需要为相应的属性添加 Setter:
public class UserServiceImpl implements UserService {
@Setter
private UserDao userDao;
// ...
}
除了按照类型自动装配,还可以按照名称自动装配:
<bean id="userService"
class="cn.icexmoon.springdemo.service.impl.UserServiceImpl"
autowire="byName">
</bean>
自动装配不能针对简单类型,只能针对其他的 Spring Bean,如果指定了 Setter 注入或构造器注入,自动装配会失效。
3.4.4.集合注入
如果属性依赖是集合,同样可以通过 Bean 定义完成注入:
@Setter
public class CollectionServiceImpl implements CollectionService {
private Object[] array;
private List<Object> list;
private Set<Object> set;
private Map<String, Object> map;
private Properties properties;
@Override
public void print() {
System.out.println("array:" + Arrays.toString(array));
System.out.println("list:" + list);
System.out.println("set:" + set);
System.out.println("map:" + map);
System.out.println("properties:" + properties);
}
}
public interface CollectionService {
void print();
}
public class Application {
public static void main(String[] args) {
// ...
CollectionService collectionService = ctx.getBean(CollectionService.class);
collectionService.print();
}
}
Bean 定义:
<bean class="cn.icexmoon.springdemo.service.impl.CollectionServiceImpl">
<property name="array">
<array>
<value>1</value>
<value>2</value>
<value>3</value>
</array>
</property>
<property name="list">
<list>
<value>Tom</value>
<value>LiLei</value>
<value>Jack</value>
</list>
</property>
<property name="set">
<set>
<value>1</value>
<value>2</value>
<value>3</value>
</set>
</property>
<property name="map">
<map>
<entry key="country" value="china"/>
<entry key="province" value="sichuan"/>
<entry key="city" value="chengdu"/>
</map>
</property>
<property name="properties">
<props>
<prop key="country">china</prop>
<prop key="province">sichuan</prop>
<prop key="city">chengdu</prop>
</props>
</property>
</bean>
除了注入字面量,也可以注入对其他 Bean 的引用。
比如说现在有多个 Person 类型的 Bean:
@Setter
@ToString
public class Person {
private String name;
private Integer age;
}
<bean id="tom" class="cn.icexmoon.springdemo.entity.Person">
<property name="name" value="Tom"/>
<property name="age" value="11"/>
</bean>
<bean id="liLei" class="cn.icexmoon.springdemo.entity.Person">
<property name="name" value="LiLei"/>
<property name="age" value="20"/>
</bean>
<bean id="jack" class="cn.icexmoon.springdemo.entity.Person">
<property name="name" value="Jack"/>
<property name="age" value="25"/>
</bean>
将其注入到 CollectionService 的一个集合属性中:
@Setter
public class CollectionServiceImpl implements CollectionService {
// ...
private List<Person> persons;
@Override
public void print() {
// ...
System.out.println("persons:" + persons);
}
}
<bean class="cn.icexmoon.springdemo.service.impl.CollectionServiceImpl">
<property name="persons">
<list>
<ref bean="jack"/>
<ref bean="liLei"/>
<ref bean="tom"/>
</list>
</property>
<!-- ... -->
</bean>
5.案例:Spring 管理第三方数据源
步骤为先添加相应依赖,然后将数据源对象定义为 Bean,再通过依赖注入的方式传入数据库连接相关信息。
添加依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
Bean 定义:
<bean class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="root"/>
<property name="password" value="mysql"/>
<property name="url" value="jdbc://localhost:3306/test"/>
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
</bean>
获取 Bean 实例:
DruidDataSource druidDataSource = ctx.getBean(DruidDataSource.class);
System.out.println(druidDataSource);
System.out.println("username:" + druidDataSource.getUsername());
System.out.println("password:"+druidDataSource.getPassword());
System.out.println("url:"+druidDataSource.getUrl());
System.out.println("driver:"+druidDataSource.getDriverClassName());
详细演示请观看这个视频。
6.从 properties 文件读取属性
上面的案例中,数据库连接信息直接被写在 Bean 定义文件中,这样做是不太好的,一般这些信息会使用单独的 properties 文件进行保存。
比如在 resource 目录下创建一个jdbc.properties
:
jdbc.username=root
jdbc.password=mysql
jdbc.url=jdbc://localhost:3306/test
jdbc.driver=com.mysql.jdbc.Driver
在 Bean 定义中读取 properties 文件,需要先引入一个 XML 命名空间context
:
<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">
<!-- ... -->
</beans>
利用context:property-placeholder
标签读取 properties 中的属性并导入占位符:
<context:property-placeholder location="jdbc.properties"/>
placeholder 是占位符的意思,即
${}
符号。
使用占位符进行注入:
<bean class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="url" value="${jdbc.url}"/>
<property name="driverClassName" value="${jdbc.driver}"/>
</bean>
Spring 也会读取系统的环境变量,比如:
<property name="username" value="${username}"/>
虽然 properties 文件中没有定义,但实际上${username}
的值是系统当前的用户名。
此时即使你在 properties 文件中定义了这个属性:
username=666
依然会是系统变量生效,也就是说系统环境变量优先级是高于 properties 中定义的属性的。
如果不希望在加载占位符属性时系统变量生效,可以:
<context:property-placeholder location="jdbc.properties" system-properties-mode="NEVER"/>
如果需要加载多个 properties 文件:
<context:property-placeholder location="jdbc.properties,other.properties" system-properties-mode="NEVER"/>
可以使用,
或空格进行分隔。
此外,还可以使用通配符加载多个文件:
<context:property-placeholder location="*.properties" system-properties-mode="NEVER"/>
默认情况下location
中的文件名实际上都指的是从当前类路径(Class Path)中加载的文件,不过最好还是显式指定:
<context:property-placeholder location="classpath:*.properties" system-properties-mode="NEVER"/>
但这样不会加载当前项目依赖的 jar 包中的配置文件,如果需要加载,可以:
<context:property-placeholder location="classpath*:*.properties" system-properties-mode="NEVER"/>
7.BeanFactory
BeanFactory
同样是一个表示 IoC 容器的接口,Spring 1.0 使用这个接口实现 IoC容器。
现在经常使用的ApplicatonContext
接口扩展自BeanFactory
:
之前使用的ConfigurableApplicationContext
接口扩展了Lifecycle
接口,因此有close
方法。
7.1.延迟加载 Bean
ApplicationContext
的实现类在加载 Bean 时默认为急切加载,比如:
public class CtxApplicaton {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("application.xml");
}
}
如果要让某些 Bean 使用“延迟加载”,可以:
<bean id="userService"
class="cn.icexmoon.springdemo.service.impl.UserServiceImpl"
autowire="byName"
lazy-init="true"/>