文章目录
- Spring 设计思想
- Spring 是什么?
- 什么是 IoC?
- Spring 创建和使用
- 创建 Spring 项目
- 注册 Bean 对象
- 获取并使用 Bean 对象
- Spring 更方便地存储和读取对象
- 配置文件
- 使用注解
- 使用类注解
- 使用方法注解
- 获取 Bean 对象
- 属性注入
- Setter 注入
- 构造方法注入
- `@Resource`
- Bean 作用域和生命周期
- 认识作用域
- 设置作用域
- 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容器关闭之前调用它,可以在销毁方法里面做一些资源释放的工作,比如关闭连接、清理缓存等。