Spring 的设计思想、创建和使用、Bean 作用域和生命周期
文章目录
Spring 设计思想
Spring 是什么?
Spring 是包含了众多工具方法的 IoC 容器
什么是 IoC?
控制反转(英语:Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。
技术描述:
往往一个类中都需要获取与其合作的类的对象(也就是它所依赖的对象)的引用。如果这个获取过程要靠自身实现,那么这将导致代码高度耦合并且难以维护和调试。
比如,Class A中用到了Class B的对象b,一般情况下,需要在A的代码中显式地用 new 建立 B 的对象。
采用依赖注入技术之后,A 的代码只需要定义一个 private 的B对象,不需要直接 new 来获得这个对象,而是通过相关的容器控制程序来将B对象在外部new出来并通过构造方法或其他方法注入到A类里的引用中。而具体获取的方法、对象被获取时的状态由配置文件(如XML)来指定。
为什么说在 A 里面 new B 的对象的耦合度会比在外面 new B 对象然后注入到 A 里面要高呢?
- 比如 B 的构造方法发生改变,new B 需要传入其他类型的参数了,那么 A 类里面的 new B 就要跟着修改,耦合度就比较高了,把 new B 放到外面,A 和 B 之间的耦合度就没这么高了。
反转在哪?
- 依赖对象的获得被反转了
依赖注入和依赖查找:
这是实现控制反转的两种主要方式,两者的区别在于,前者是被动的接收对象,在类A的实例创建过程中即创建了依赖的B对象,通过类型或名称来判断将不同的对象注入到不同的属性中,而后者是主动索取相应类型的对象,获得依赖对象的时间也可以在代码中自由控制。
依赖注入有如下实现方式:
- 基于接口。实现特定接口以供外部容器注入所依赖类型的对象。
- 基于 set 方法。实现特定属性的public set方法,来让外部容器调用传入所依赖类型的对象。
- 基于构造函数。实现特定参数的构造函数,在新建对象时传入所依赖类型的对象。
- 基于注解。基于Java的注解功能,在私有变量前加“@Autowired”等注解,不需要显式的定义以上三种代码,便可以让外部容器传入对应的对象。该方案相当于定义了public的set方法,但是因为没有真正的set方法,从而不会为了实现依赖注入导致暴露了不该暴露的接口(因为set方法只想让容器访问来注入而并不希望其他依赖此类的对象访问)。
依赖查找更加主动,在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径、key等信息来确定获取对象的状态
Spring 是一个 IoC 容器,说的是对象的创建和销毁的权利都交给 Spring 来管理了,它本身又具备存储对象和获取对象的能力。
Spring 创建和使用
创建 Spring 项目
创建 Spring 项目和创建 Servlet 项目类似,分为以下 3 步:
-  创建一个 Maven 项目 
-  添加 Spring 框架支持(spring-context、spring-beans) 直接去中央仓库找就行了,也可以在这里复制: <dependencies> <!-- https://mvnrepository.com/artifact/org.springframework/spring-context --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.26</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-beans --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>5.3.26</version> </dependency> </dependencies>
-  添加启动类 就是创建一个带有 main 方法的类。之前学习 Servlet 没有写过 main 方法,是因为 main 方法是 Tomcat 实现的,这里的 Spring 就不一样了。 
注册 Bean 对象
Bean 对象就是普通的 Java 对象。
-  定义一个 Bean public class User { public void hi() { System.out.println("Hello"); } }
-  将 Bean 注册到 Spring(并非真正存储,而是告诉 Spring,此 Bean 需要托管给 Spring) 首先在 resources 里面创建 xml 文件,名字随意,如果是 IDEA 专业版,在右键 resources->new 会看到 XML Configuration File-> Spring Config 选项,直接选择就可以生成。如果是社区版,可以复制以下代码: <?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>接下来,要将 User 对象注册到 Spring 中,具体操作是在 <beans>中添加如下配置:<bean id="user" class="User"></bean>id和class属性分别指定了该 Bean 的标识符和实现类。如果实现类写在包里面,那么这里还要带上包名路径。这个 <bean>元素,就是告诉 Spring 容器创建一个名为 “user” 的Bean,该Bean的实现类是 “User” 类。其他部分的应用程序或配置可以通过这个唯一的标识符 “user” 来引用和使用这个Bean。注意:id 不可重复,而 class 可以重复,也就是说,同一个类,可以在 Spring 中注册两次,只要 id 不同即可。一个 id 就代表一个对象。 
获取并使用 Bean 对象
- 得到 Spring 上下文对象,因为对象都交给 Spring 管理了,所以获取对象要从 Spring 中获取,那么就要先得到 Spring 上下文
- 通过 Spring 上下文,获取某一个指定的 Bean 对象
- 使用 Bean 对象
例:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App {
    public static void main(String[] args) {
        // 1.得到 Spring 上下文对象
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
        // 2.从 Spring 中获取 bean 对象
        //User u = new User(); 传统方式,在 spring 中不推荐使用
        User user = (User) context.getBean("user");
        // 3.使用 bean
        user.hi();
    }
}
// 输出:Hello
获取 bean 的其他方法:
通过类对象
User user = context.getBean(User.class);
通过此方法获取,有一个弊端,如果同一个类型的 Bean 在 xml 中注册了两次或多次,那么 Spring 就不知道你要获取的是哪个,从而报错。
根据 String 和 Class 获取 bean
User user = context.getBean("user", User.class);
这种方法就避免了上一个方法的弊端,而且不需要强制类型转换。
常用方法总结:
| 方法 | 说明 | 
|---|---|
| Object getBean(String name) | 返回指定 bean 的一个实例,该实例可以是共享的,也可以是独立的 | 
| T getBean(Class<T> requiredType) | 返回唯一匹配给定对象类型的 bean 实例(如果有的话) | 
| T getBean(String name, Class<T> requiredType) | 返回指定 bean 的一个实例,该实例可以是共享的,也可以是独立的 | 
获取上下文对象的其他方法:
BeanFactory context = new XmlBeanFactory(new ClassPathResource("spring-config.xml"));
这种方法不建议使用。
ApplicationContext 和 BeanFactory 的区别是什么?
共同点:都是用来获取 Spring 上下文对象
不同点:
- 继承关系和功能:ApplicationContext 是 BeanFactory 的子类,BeanFactory 只具备最基本的访问容器的能力,而 ApplicationContext 还提供了对国际化支持,资源访问支持,以及事件传播等方面的支持
- 性能和加载方式:BeanFactory 按需加载,当需要使用 bean 的时候再加载,ApplicationContext 一次性加载所有的 bean
Spring 更方便地存储和读取对象
配置文件
在 Spring 配置文件中设置 Bean 扫描根路径,在该路径下的使用了注解的类会被存储到 Spring 中。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:content="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 https://www.springframework.org/schema/context/spring-context.xsd">
    <content:component-scan base-package="com.cero"></content:component-scan>
</beans>
就是在 <beans> 里面添加 <content:component-scan base-package="com.cero"></content:component-scan> 这一行,其中的 base-package=“” 自己设置。
使用注解
使用注解 将 Bean 对象更简单地存储到 Spring
注解类型有两种
-  类注解: @Controller@Service@Repository@Component@Configuration@Controller控制器,用来验证前端传递的参数@Service服务层,服务调用的编排和汇总@Repository仓库,直接操作数据库@Component组件,通用化的工具类@Configuration配置,项目的所有配置
-  方法注解: @Bean
使用类注解
package com.cero.service;
import org.springframework.stereotype.Service;
@Service
public class UserService {
    public void doService() {
        System.out.println("Do user service");
    }
}
import com.cero.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
        // 使用注解的方式,bean 的 id 是类名的小驼峰
        UserService userService = context.getBean("userService", UserService.class);
        userService.doService();
    }
}
虽然我们没有在配置文件中显式写出 id,但是 Spring 存储的时候会自动把 id 命名成小驼峰。
这些注解的功能是一样的,但为什么要有这么多注解?
- 为了主程序员看到类注解之后,就能直接了解当前类的用途比如: 
  - @Controller表示的业务逻辑层
- @Service服务层
- @Repository持久层
- @Configuration配置层
 
程序的工程分层,设备流程如下:

五大类注解之间的关系:
- @Controller- @Service- @Repository- @Configuration都是基于- @Component,它们的作用都是将 Bean 存储到 Spring 中
这一点通过看源代码可以发现
使用注解方式时 Bean 的 id:
- 默认情况下,就是原类名的首字母小写,如:StudentController->studentController
- 特例:当首字母和第二个字母都是大写的情况下,那么 Bean 的名称为原类名,如:SController->SController
使用方法注解
拥有方法注解的方法,返回的对象会被存储到 Spring 中。
注意:
- 方法注解 @Bean一定要和类注解配合使用。因为 Spring 是先扫描哪些类要存储,然后再去扫描这个类里面的方法。
- 方法注解 @Bean的命名规则和类注解不一样,它的 bean 对象的 id 就是方法名。
- 使用了方法注解的方法必须是无参的,因为 Spring 初始化存储时,无法提供相应的参数。
例子 :
先定义一个普通的 User 类
package com.cero.model;
public class User {
    private int id;
    private String name;
    private int age;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
定义一个 UserBeans 类,其中的 user 方法会返回一个 User 对象,该对象会被存到 Spring:
package com.cero.controller;
import com.cero.model.User;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
@Component
public class UserBeans {
    @Bean
    public User getUser() {
        User user = new User();
        user.setId(1);
        user.setName("张三");
        user.setAge(18);
        return user;
    }
}
获取存储在 Spring 里的 User 对象:
import com.cero.model.User;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
        User user = context.getBean("getUser", User.class);
        System.out.println(user);
    }
}
// 输出:User{id=1, name='张三', age=18}
@Bean 重命名
思考方法注解的命名规则,如果有两个同名的方法返回了不同的对象怎么办,这两个对象怎么区分呢?
如下代码,有两个同名方法,它们返回的对象都将被注册到 Spring 中
package com.cero.controller;
import com.cero.model.User;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
@Component
public class UserBeans {
    @Bean
    public User getUser() {
        User user = new User();
        user.setId(1);
        user.setName("张三");
        user.setAge(18);
        return user;
    }
}
@Component
class UserBeans2 {
    @Bean
    public User getUser() {
        User user = new User();
        user.setId(1);
        user.setName("李四");
        user.setAge(80);
        return user;
    }
}
获取对象并打印:
import com.cero.model.User;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
        User user = context.getBean("getUser", User.class);
        System.out.println(user);
    }
}
// 输出:User{id=1, name='李四', age=80}
结果并没有报错,而是打印了李四,说明两个对象都存了,只是李四把张三给覆盖了。
而我要想分别获得这两个方法返回的对象怎么办?
答案是使用 @Bean 重命名:
给注解提供 name 参数,具体有 3 种写法:
- @Bean(name = "user1")
- 也可以省略 name :@Bean("user1")
- 使用 {}来命名多个名字:@Bean(name = {"user1", "user2"})
- 使用{}的 name 也可以省略@Bean({"user1", "user2"})
package com.cero.controller;
import com.cero.model.User;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
@Component
public class UserBeans {
    @Bean(name = "user1")
    public User getUser() {
        User user = new User();
        user.setId(1);
        user.setName("张三");
        user.setAge(18);
        return user;
    }
}
@Component
class UserBeans2 {
    @Bean("user2")
    public User getUser() {
        User user = new User();
        user.setId(1);
        user.setName("李四");
        user.setAge(80);
        return user;
    }
}
这样它们返回的对象就不再使用默认命名规则了,而是使用由程序员指定的名字。
import com.cero.model.User;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
        User user1 = context.getBean("user1", User.class);
        User user2 = context.getBean("user2", User.class);
        System.out.println(user1);
        System.out.println(user2);
    }
}
/* 输出:
User{id=1, name='张三', age=18}
User{id=1, name='李四', age=80}
*/
获取 Bean 对象
获取 Bean 对象也叫做对象装配,对象注入,是把对象取出来放到某个类中
对象装配(对象注入)的实现方法有以下 3 种:
- 属性注入
- 构造方法注入
- Setter 注入
属性注入
这是最简单的对象注入方式,和平常的定义属性类似,这里以我们之前存在 Spring 中的 UserService 为例,
在定义了 private UserService userService; 这个属性之后,在上面加上 @Autowired 注解。然后我们就可以直接调用 UserService 里的方法了。
属性注入的时候,Spring 会优先按照类型在容器中进行查找,如果找不到相同类型的 bean 对象,就会抛异常,如果找到相同类的多个 bean 对象,就再按照属性名查找,找到就赋值到该属性上,找不到就抛异常。
package com.cero.controller;
import com.cero.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
    // 从 Spring 中读取 UserService
    // 属性注入
    @Autowired
    private UserService userService;
    public void hi() {
        userService.doService();
    }
}
import com.cero.controller.UserController;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
        // 获取 UserController,并调用其中的 hi 方法
        UserController userController = context.getBean("userController", UserController.class);
        userController.hi();
    }
}
// 输出:Do user service
一个细节问题,我的 main 方法可以使用属性注入的方式获取 UserController 对象吗?
import com.cero.controller.UserController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class App {
    @Autowired
    private static UserController userController;
    public static void main(String[] args) {
        userController.hi();
    }
}
/* 输出:
Exception in thread "main" java.lang.NullPointerException
	at App.main(App.java:11)
*/
抛出了空指针异常,说明没有注入成功。这是因为,main 方法是静态方法,它的执行时间是比 Spring 装配的时机要早的,所以它并不能通过属性注入的方式获取 Bean 对象。
优点:
- 属性注入最大的优点就是实现简单,使用简单
缺点:
- 功能性问题:无法注入一个 final 修饰的属性,因为 Java 规定了 final 修饰的变量,要么在定义时直接赋值,要么使用构造方法赋值。这里的属性注入无法满足这两个条件,所以无法注入。
- 通用性问题:只能适应于 IoC 容器
- 设计原则问题:更容易违背单一设计原则。因为使用简单,所以滥用的风险更大
Setter 注入
给属性添加 Setter 方法,然后在 Setter 方法上添加 @Autowired 注解
package com.cero.controller;
import com.cero.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
    // 从 Spring 中读取 UserService
    // setter 注入
    private UserService userService;
    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
    public void hi() {
        userService.doService();
    }
}
优点:
- 符合单一设计原则。一个 Setter 只针对一个对象
缺点:
- 不能注入 final 修饰的对象。原因和属性注入一样。
- 注入的对象可能被改变。因为 Setter 可能被调用多次。
构造方法注入
——这是目前官方推荐的写法
在构造方法的上添加 @Autowired
package com.cero.controller;
import com.cero.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
    // 从 Spring 中读取 UserService
    // 构造方法注入
    private UserService userService;
    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }
    public void hi() {
        userService.doService();
    }
}
注意:
- 当这个类只有一个构造方法的时候,@Autowired可以省略
- 构造方法的参数必须是 Spring 容器中有的
- 一个类,最多只能在一个构造方法上添加 @Autowired
优点:
- 可以注入 final 修饰的对象
- 注入的对象不会被修改
- 依赖对象在使用前会被完全初始化。
- 通用性更好,因为构造方法是 Java(JDK)支持,所以更换任何的框架,它都是适用的。
缺点:
- 容易违背单一设计原则,因为一个构造方法可以传入多个参数。
@Resource
 
它的用法和 @Autowired 是一样的。
@Resource 和 @Autowired 的区别:
-  @Resource来自于 JDK,@Autowired来自 Spring
-  @Resource支持属性注入和 Setter 注入,但是不支持构造方法注入
-  @Resource支持 name 参数@Resource(name = "user2") private User user;上述代码读取 Spring 中名称为 “user2” 的对象,赋值给 user 
@Autowired 不支持传入 name 参数,但是它可以配合 @Qualifier 注解来达到相同的效果,上述代码等价于:
@Autowired
@Qualifier(value = "user2")
private User user;
Bean 作用域和生命周期
认识作用域
程序中限定变量的可用范围叫做作用域。
Bean 作用域是指 Bean 在 Spring 整个框架中的某种行为模式。比如 singleton单例作用域,就表示 Bean 在整个 Spring 中只有一份,全局共享。
Spring 容器在初始化一个 Bean 实例时,同时会指定该实例的作用域。Spring 有 6 种作用域,最后四种是基于 Spring MVC 生效的:
- singleton:单例作用域
- prototype:原型作用域(多例作用域)
- request:请求作用域
- session:会话作用域
- application:全局作用域
- websocket:HTTP WebSocket 作用域
singleton
- 描述:该作用域下的 Bean 在 IoC 容器中只存在一个实例,获取 Bean(即通过 applicationContext.getBean等方法获取)及装配 Bean(即通过 @Autowired注入)都是同一个对象
- 场景:通常无状态的 Bean 使用该作用域。无状态即 Bean 对象的属性状态不需要更新。比如 A 和 B 都用到了这个 Bean,因为 A 和 B 共享这一个 Bean 对象,如果 A 把这个 Bean 对象内的属性修改了,那么就会影响到 B,造成不便。
- 注:Spring 默认选择该作用域
prototype
- 描述:每次对该作用域下的 Bean 的请求都会创建新的实例:获取 Bean 和装配 Bean 都是新的对象实例。
- 场景:通常有状态的 Bean 使用该作用域
request
- 描述:每次 http 请求都会创建新的 Bean 实例,类似于 prototype
- 场景:一次 http 的请求和响应的共享 Bean
- 注:限定在 SpringMVC 中使用
session
- 描述:每次 http 会话都会创建新的 Bean 实例
- 场景:用户会话的共享 Bean,比如:记录一个用户的登录信息
- 注:限定在 SpringMVC 中使用
application(不常用)
- 描述:在一个 http servlet Context 中,定义一个 Bean 实例
- 场景:Web 应用的上下文信息,比如:记录一个应用的共享信息
- 注:限定在 SpringMVC 中使用
websocket(不常用)
- 描述:在一个 WebSocket 的生命周期中,定义一个 Bean 实例
- 注:限定在 Spring WebSocket 中使用
设置作用域
使用 @Scope 标签用来声明 Bean 的作用域
@Scope 标签既可以修饰方法也可以修饰类,@Scope 有两种设置方式:
- 直接设置值:@Scope("prototype")
- 使用类似枚举的方式设置:@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
例:
package com.cero.service;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Controller
public class UserService {
    public void doService() {
        System.out.println("Do user service");
    }
}
Spring 执行流程
-  启动容器 ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");加载配置文件(类加载路径下的 spring-config.xml) 
-  Bean 的初始化(根据配置文件中的 bean,以及 base-package 路径下有类注解的类进行初始化) <content:component-scan base-package="com.cero"></content:component-scan> <bean id="userConfiguration" class="com.cero.config.UserConfiguration"></bean>
-  注册 Bean 对象到容器中 
-  使用 Bean 
-  Spring 销毁 
Bean 生命周期
在 Spring 中,Bean 的生命周期指的是 Bean 实例从创建到销毁的整个过程。Spring 容器负责管理 Bean 的生命周期,包括实例化、属性赋值、初始化、销毁等过程。
- 实例化 Bean(为 Bean 分配内存空间)
- 设置属性,Spring通过反射机制给Bean的属性赋值
- Bean 初始化,如果Bean配置了初始化方法,Spring就会调用它。初始化方法是在Bean创建并赋值之后调用,可以在这个方法里面写一些业务处理代码或者做一些初始化的工作。 
  - 执行各种通知
- 执行初始化前置方法(xml 里面定义 init-method | @PostConstruct)
- 执行初始化方法
- 执行初始化的后置方法
 
- 使用 Bean
- 销毁 Bean(xml destroy-method | @PreDestroy),如果Bean配置了销毁方法,Spring会在所有Bean都已经使用完毕,且IOC容器关闭之前调用它,可以在销毁方法里面做一些资源释放的工作,比如关闭连接、清理缓存等。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!