Spring 是包含了众多工具方法的 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 里面要高呢?
反转在哪?
依赖注入和依赖查找:
这是实现控制反转的两种主要方式,两者的区别在于,前者是被动的接收对象,在类A的实例创建过程中即创建了依赖的B对象,通过类型或名称来判断将不同的对象注入到不同的属性中,而后者是主动索取相应类型的对象,获得依赖对象的时间也可以在代码中自由控制。
依赖注入有如下实现方式:
依赖查找更加主动,在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径、key等信息来确定获取对象的状态
Spring 是一个 IoC 容器,说的是对象的创建和销毁的权利都交给 Spring 来管理了,它本身又具备存储对象和获取对象的能力。
创建 Spring 项目和创建 Servlet 项目类似,分为以下 3 步:
创建一个 Maven 项目
添加 Spring 框架支持(spring-context、spring-beans)
直接去中央仓库找就行了,也可以在这里复制:
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.3.26version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-beansartifactId>
<version>5.3.26version>
dependency>
dependencies>
添加启动类
就是创建一个带有 main 方法的类。之前学习 Servlet 没有写过 main 方法,是因为 main 方法是 Tomcat 实现的,这里的 Spring 就不一样了。
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 选项,直接选择就可以生成。如果是社区版,可以复制以下代码:
<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 中,具体操作是在
中添加如下配置:
<bean id="user" class="User">bean>
id
和 class
属性分别指定了该 Bean 的标识符和实现类。如果实现类写在包里面,那么这里还要带上包名路径。
这个
元素,就是告诉 Spring 容器创建一个名为 “user” 的Bean,该Bean的实现类是 “User” 类。其他部分的应用程序或配置可以通过这个唯一的标识符 “user” 来引用和使用这个Bean。
注意:id 不可重复,而 class 可以重复,也就是说,同一个类,可以在 Spring 中注册两次,只要 id 不同即可。一个 id 就代表一个对象。
例:
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 |
返回唯一匹配给定对象类型的 bean 实例(如果有的话) |
T getBean(String name, Class |
返回指定 bean 的一个实例,该实例可以是共享的,也可以是独立的 |
获取上下文对象的其他方法:
BeanFactory context = new XmlBeanFactory(new ClassPathResource("spring-config.xml"));
这种方法不建议使用。
ApplicationContext 和 BeanFactory 的区别是什么?
共同点:都是用来获取 Spring 上下文对象
不同点:
在 Spring 配置文件中设置 Bean 扫描根路径,在该路径下的使用了注解的类会被存储到 Spring 中。
<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>
就是在
里面添加
这一行,其中的 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:
拥有方法注解的方法,返回的对象会被存储到 Spring 中。
注意:
@Bean
一定要和类注解配合使用。因为 Spring 是先扫描哪些类要存储,然后再去扫描这个类里面的方法。@Bean
的命名规则和类注解不一样,它的 bean 对象的 id 就是方法名。例子 :
先定义一个普通的 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")
@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 对象也叫做对象装配,对象注入,是把对象取出来放到某个类中
对象装配(对象注入)的实现方法有以下 3 种:
这是最简单的对象注入方式,和平常的定义属性类似,这里以我们之前存在 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 对象。
优点:
缺点:
给属性添加 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();
}
}
优点:
缺点:
——这是目前官方推荐的写法
在构造方法的上添加 @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
可以省略@Autowired
优点:
缺点:
@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 在 Spring 整个框架中的某种行为模式。比如 singleton单例作用域,就表示 Bean 在整个 Spring 中只有一份,全局共享。
Spring 容器在初始化一个 Bean 实例时,同时会指定该实例的作用域。Spring 有 6 种作用域,最后四种是基于 Spring MVC 生效的:
singleton
@Autowired
注入)都是同一个对象prototype
request
session
application(不常用)
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");
}
}
启动容器
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 销毁
在 Spring 中,Bean 的生命周期指的是 Bean 实例从创建到销毁的整个过程。Spring 容器负责管理 Bean 的生命周期,包括实例化、属性赋值、初始化、销毁等过程。