Spring Framework 学习笔记1:基础

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)。

在这个示例中,现在userServiceuserDao都已经被定义为 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,应该可以注意到此时propertyname 属性是标红的,因为 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

image-20230821170738499

之前使用的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"/>

8.参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值