您现在的位置是:首页 >技术教程 >Spring核心与设计思想、创建与使用网站首页技术教程
Spring核心与设计思想、创建与使用
文章目录
一、Spring是什么
我们通常所说的 Spring 指的是 Spring Framework(Spring 框架),它是⼀个开源框架。Spring ⽀持⼴泛的应⽤场景,它可以让 Java 企业级的应⽤程序开发起来更简单
⽤⼀句话概括 Spring:Spring 是包含了众多⼯具⽅法的 IoC 容器
二、为什么要学习框架
- 就像我们装修房子,不使用框架就像我们材料自己买,关于房子的事情都需要亲力亲为,使用框架时就像我们请了装修公司,材料、人员安排等等我们不再关系,而只需要提出需求,跟进进度即可,特点就是高效
- 框架更加易用,并且使用起来也是非常简单
上述优势通过 SpringBoot 和 Servlet 对比能够更容易地看出:
- SpringBoot 添加外部 jar 包更容易,不易出错,直接引入即可,而Servlet还需要关注版本问题
- 调试、发布项目更加方便,无需配置 Tomcat,因为它里面已经内置了
- 添加路由更加方便,无需每个访问地址都添加一个类
三、IoC和DI
(一)IoC
在认识IoC之前,我们要先知道容器地概念,通过百度,我们可以知道容器是用来容纳某种物品的装置,我们之前学过的List是数据存储容器,Tomcat是Web容器
1. 认识IoC
首先明确一点,Spring也是一个容器,它是一个IoC容器
什么是 IoC?
IoC = Inversion of Control 翻译成中⽂是“控制反转”的意思,IoC就是控制反转的思想。“控制反转”是两个词,控制和反转。指的是:之前程序的控制权限是在我们自己手上,现在把控制权交出去,我们只需要使用即可
- 一般情况下,我们在 A 类 中,想去调用 B 类中的方法,要去 new B 类对象,通过 对象 去调用 B类中的方法。当前 B 的控制权,是我们手上的
- 而控制反转,就是将我们手上的权限,交由 Spring 框架来操作
- 此时,我们想要 A 类中调用 B 的时候,告诉框架,我要在 A 中调用 B 了。至于 B 的生命周期,和我们没有任何关系。
- 因为我们把控制权 “反转给了” Spring 框架。Spring 会帮我们管理所有的对象(Bean)
2. Spring的核心功能
通过上述关于IoC的介绍,我们知道了Spring的核心功能就是两点:
- 将 Bean(对象)存储到 Spring(容器)中
- 将 Bean(对象)从 Spring(容器)中取出来
(二)DI
只要我们说到IoC,就离不开DI,它们俩的关系就像是切菜和菜刀一样,切菜是一种动作,也是一种想法,该如何实现呢?利用菜刀来实现这个想法
DI 是 Dependency Injection 的缩写,翻译成中⽂是“依赖注⼊”的意思。具体解释就是在 IoC 容器运行期间,动态的将某种依赖关系注入到对象中。就是为了实现 IoC 的思想,所以,IoC 和 DI 的核心都是为了“控制反转”,,但是区别就是IoC是思想,DI是实现
四、Spring项目的创建
(一)使用 Maven 方式创建一个 Spring 项目
- 创建一个普通的 Maven 项目(不需要添加web-app模板)
- 添加 Spring 框架支持
(1)在 pom.xml 当中加入依赖
(2)粘贴完之后,刷新 Maven<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>5.2.3.RELEASE</version> </dependency> </dependencies>
- 在 Java 源文件中添加一个启动类,并编写 main 方法即可
五、Spring项目的使用
(一) 存储 Bean 对象到容器(Spring)中
- 如果第一次存储,先在 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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> </beans>
- 创建 Bean 对象(Java对象的另一种称呼),和对普通的类实例化一样
- 在配置文件中将需要保存到容器中的 Bean 对象进行注册,如下图所示,表示将 User 对象以类似字典的形式存入到 Spring 当中,id就是它的标识(通常使用小驼峰),类似于 HashMap 中的key
(二)从 spring 将 bean 对象读取出来并使用
- 得到 spring 上下文对象
- 通过上下文对象提供的方法获取自己需要使用的 bean 对象
- 使用 bean 对象
除了图上通过 ApplicationContext 来获取 spring 上下文,我们也可以通过 BeanFactory (Bean工厂)来作为 spring 的上下文,代码如下
BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("spring-config.xml"));
那么这二者有什么区别呢,这就是一个经典面试题:ApplicationContext VS BeanFactory
相同点:都可以实现从容器中获取bean,都提供了 getBean 方法
不同点:
- ApplicationContext 属于 BeanFactory 的子类。BeanFactory 只提供了基础访问 Bean 的方法,而 ApplicationContext 除了拥有 BeanFactory 的所有功能之外,还提供了更多的方法实现,比如对国际化的支持、资源访问的支持、以及事件和传播等方面的支持
- 性能方面来说二者不同。BeanFactory 是按需加载 Bean,懒加载;ApplicationContext 是饿汉方式,在创建时会将所有的 Bean 加载起来,以备以后使用
(三)getBean的更多用法
- 使用 bean name 获取 bean
User user = (User)context.getBean("user");
- 根据 bean type 获取 bean
User user = context.getBean(User.class);
这个写法虽然简单,但是存在问题,如果两个类对象类名相同,但包不同,同时保存到了 spring 中,那么此时再通过这种方法进行获取 bean ,就会报出类不唯一的异常
3. 根据 bean name 和 bean type 获取 bean
User user = context.getBean("user", User.class);
这种写法和第一种比较看似只是不用强转,但是如果返回的 bean 是 null,此时进行强转就会直接抛出空引用异常,而第三种方法不会存在这个问题,因此这种写法要比第一种更加健壮,所以更加推荐这种写法
六、Spring更简单的存储和读取对象的方式
上述存储和读取 bean 对象的方式只是最原始的方式,仍然不够简洁和方便;而在 Spring 中,想要更简单的存储和读取对象的核心是使用注解,这也就是我们下来要介绍的内容
(一)存储 Bean 对象
我们之前存储 bean 时,是通过在配置文件中添加一行 bean 注册内容,而现在我们只需要一个注解就可以替代之前的操作,不过在此之前,我们要先进行一些准备工作
-
前置工作:配置扫描路径(关键)
在 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" 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.beans"></content:component-scan> </beans>
注意:在标签中,base-package表示的就是扫描根路径,这个路径是我们自己设置的,而我们以后要存储的类,都会写入到这个路径的包/子包底下,只要在这个包底下的类加了注解,就表示存储到 spring 中了
-
简单的将 bean 存储到容器中
- 使用五大类注解实现
- Controller(控制器)
// 必须添加注解 @Controller public class UserController { public void hello() { System.out.println("hello, UserController!"); } }
- Service(服务)
- Repository(仓库)
- Configuration(配置)
- Component(组件)
- Controller(控制器)
- 通过 @Bean 方法注解实现
注意,@Bean 注解只能添加到方法上,而不能添加到类上;@Bean必须搭配五大类注解使用;读取时 bean name 不是类名,而是方法名(要求:必须完全一致,不再是之前五大类注解的规则)(不一定,可以参考下方第六点)// 使用 // 注意这里的 bean name 是方法名,不是类名 User user = context.getBean("GetUser", User.class); System.out.println(user); // 保存 @Component public class UserBeans { /** * 注意:只使用一个 @Bean 是无法将对象存储到容器中的 * 必须在类上也搭配一个五大类注解,这样做的目的是为了提高效率 * @return */ @Bean public User GetUser() { User user = new User(); user.id = 1; user.name = "张三"; return user; } }
- 使用五大类注解实现
-
提出疑问:上述将 bean 存储到容器中,明明五大类注解任何一个都可以,为什么还需要五大类注解?
解答:
这里就涉及到软件工程的知识了,如下图所示,我们的五大类注解和下面的软件架构分层一一对应,当我们的 bean 很多时,我们就有必要根据不同的功能使用不同的注解对不同的 bean 进行区分,让代码的可读性提高,让程序猿能够直观的判断当前类的用途。Component 就是用于当 bean 不属于下面四种情况时,作为一个工具来使用时使用 Component 来注释
-
五大类注解之间的关系,这个问题我们可以直接看底层源码,就会发现其他四个类注解都是继承自 Component,换句话说,Compinent 是其他四个类的父类,以 Controller 为例
-
使用五大类注解产生的 bean name 问题。我们读取 bean 时,这个问题是避不开的,因此需要我们着重注意
解答:
这个问题我们需要去看注解 bean name 生成器,即:AnnotationBeanNameGenerator
,当我们在这个方法中左拐右拐,最终却定位到了decapitalize.java
文件,通过查看文件目录结构,我们发现,这个方法是在 jdk 里自带的,换句话说,通过注解生成 bean name 的规则,实际是 jdk 自带的,接下来我们对这个方法进行源码分析,如下图:
因此我们说,当类名第一个字符和第二个字符都是大写时,bean name 就是它本身,否则就是第一个字符小写 -
使用 @bean 的时候,我们想要读取 Bean 对象,只能使用方法名吗,答案是不一定,我们可以人为的给这个方法返回的 bean 设置一个 name,甚至可以给他起多个 name ,如下方代码:
// 如果只有一个 name ,不需要加"{}" @Bean(name = {"userInfo", "user1"}) public User GetUser() { User user = new User(); user.id = 1; user.name = "张三"; return user; }
注意,在这种情况下方法名将无法再使用
(二)读取 Bean 对象
获取 bean 对象也叫做对象装配,是把对象取出来放到某个类中,有时候也叫对象注⼊。对象注入的实现方式有三种:
- 属性注入
- 构造方法注入
- Setter 注入
三种对象注入都可以通过 @Autowired 实现
1. 属性注入
下面我们将 UserService 类注入到 UserController2 类中
// 将 UserService 注入到 UserController2
@Controller
public class UserController2 {
@Autowired
private UserService userService;
public void hello() {
userService.hello();
}
}
// 将 UserController2 注入到 App 中并执行
public class App {
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
UserController2 userController2 =
context.getBean("userController2", UserController2.class);
userController2.hello();
}
}
运行结果
2. 构造方法注入
我们通过构造方法和@Autowired将 UserService 类注入到 UserController3 类中,如果当前类中只有一个构造方法,@Autowired可以省略
@Controller
public class UserController3 {
private UserService userService;
//只有一个构造方法时可以省略@Autowired
public UserController3(UserService userService) {
this.userService = userService;
}
public void hello() {
userService.hello();
}
}
public class App {
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
UserController3 userController = context.getBean(UserController3.class);
userController.hello();
}
}
运行结果
3. Setter 注入
为需要注入的类提供一个set方法
@Controller
public class UserController4 {
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
public void hello() {
userService.hello();
}
}
public class App {
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
UserController4 userController4 = context.getBean(UserController4.class);
userController4.hello();
}
}
运行结果
4. 使用 @Resource 代替 @Autowired
@Resource 是 Java 官方提供的,@Autowired 是由 Spring 官方提供的,直接将 @Resource 替换为 @Autowired 即可。但是,@Resource 不支持属性注入,因此我们说,注入方式有三种,注入方法有这两种
5. 问题一:@Resource VS @Autowired
相同点:都可以实现将一个对象注入到类当中
不同点:
- 实现者不同,@Resource是 Java 官方提供的,@Autowired是 Spring 官方提供的
- 用法不同:@Autowired 支持属性注入、构造方法注入和Setter注入,但是 @Resource 不支持属性注入
- 支持参数不同:@Resource 支持更多的参数设置,比如name、type设置,而 @Autowired 只支持 required 参数设置
6. 问题二:属性注入、构造方法注入与set方法注入有什么区别
答:
1. 属性注入写法简单,但只能运行在IoC容器下,其他容器会出现问题
2. Setter注入:早期 Spring 版本推荐的写法,但 Setter 注入没有构造方法注入通用,其他语言写set方法很麻烦
3. 构造方法注入:通用性更好,它能确保在使用注入对象之前,此注入对象一定初始化过了
7. 问题三:当我们使用 @Bean 保存了多个相同类的不同对象,此时上述代码就会出现问题,会报出不唯一的异常
如下图:
针对这个问题,我们有如下的解决方案:
- 精确的描述 bean 的名称(将注入的名称写对)
- 使用 @Resource 设置 name 的方式来重命名注入对象
- 如果工作时要求不能使用 @Resource,并且我们业务代码已经写了很多,采用第一种方法改起来很麻烦,我们就可以使用 @Qualifier + @Autowired 来筛选 bean 对象
七、Bean 作用域和生命周期
(一)Bean 作用域的问题
1. 案例
由上述知识我们知道,Spring 是用来读取和存储 Bean 的,因此 Bean 是 Spring 最核心的操作资源,因此我们需要更深入的了解一下 Bean。其中我们的预期结果是公共的 Bean 经过注入之后可以在各自的类进行修改,但是不能影响到其他的类,但是经过测试发现结果并不是这样
- 测试类1,注入对象后将将象的 name 设为“王五”,并返回
- 测试类2,注入对象后直接返回
- 结果
注意,作者在测试这里的代码时,恰好遇到了一个需要注意的问题,在有 Spring 存在的项目中,Spring 的注入要统一使用,类的一步步引用,要么为全部注入,要么都不注入,否则会出错。代到当前测试例子中,由于作者刚开始进行测试时在 app 类中两个类都是 new 出来的,导致两个类中并没有对 User 类进行注入,在操作时就会报出空指针异常
言归正传,我们在上面进行测试时,发现结果和预期并不相同,UserTestBean2中对对象进行了修改,居然影响到了UserTestBean1,那么问题是什么呢?
2. 原因分析
上述问题的原因就是因为 Bean 默认情况下是单例模式(singleton),换句话说所有类使用的是同一个对象,这样做可以很大程度上提高性能
(二)作用域的定义
限定程序中变量的可⽤范围叫做作⽤域,或者说在源代码中定义变量的某个区域就叫做作⽤域。
⽽ Bean 的作⽤域是指 Bean 在 Spring 整个框架中的某种⾏为模式,⽐如 singleton 单例作⽤域,就表示 Bean 在整个 Spring 中只有⼀份,它是全局共享的,那么当其他⼈修改了这个值之后,那么另⼀个⼈读取到的就是被修改的值
1. Bean 的六种作用域
Spring 容器在初始化一个 Bean 的实例时,同时会指定该实例的作用域。Spring 有六种作用域,最后四种是基于 Spring MVC 生效的:
- singleton:单例作用域(Spring默认选择该作用域)
- prototype:原型作用域(多例作用域)
- request:请求作用域
- session:会话作用域
- application:全局作用域
- websocket:HTTP WebSocket 作用域
使用场景
- singleton:通常无状态的 Bean 使用该作用域。无状态表示 Bean 对象的属性状态不需要更新
- prototype:通常有状态(即需要进行修改)的 Bean 使用该作用域
- request:一次http的请求和相应的共享Bean
- session:用户会话的共享Bean,比如记录一个用户的登录信息
- application:Web应用的上下文信息,比如:记录一个应用的共享信息
- websocket:Websocket的每次会话中,保存了一个Map结构的头信息,将用来包裹客户端消息头,第一次初始化后,直到 WebSocket 结束都是同一个Bean
单例作用域(singleton)VS 全局作用域(application)
- singleton 是 Spring Core 的作用域;application 是 Spring Web 中的作用域
- singleton 作用于 Ioc 的容器,而 application 作用于 Servlet 容器
2. 设置作用域
使用 @Scope 标签就可以用来声明 Bean 的作用域。@Scope 标签既可以修饰方法也可以修饰类,@Scope 有两种设置方式:
- 直接设置值:@Scope(“prototype”)
- 使用枚举设置:@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
示例:
// @Scope("prototype")
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Bean(name = "user2")
public User getUser2() {
User user = new User();
user.setId(2);
user.setName("李四");
return user;
}
(三)Spring 执行流程和 Bean 的生命周期
1. Spring 执行流程
- 启动 Spring 容器
- 根据配置完成 Bean 初始化(分配内存空间)
- 注册 Bean 对象到容器中
- 装配 Bean 到需要的类中
2. Bean 生命周期
- 实例化 Bean (为 Bean 分配内存空间)
- 设置属性(Bean 注入和装配)
- Bean 初始化
- 实现了各种 Aware 通知的方法
- 执行初始化的前置方法,依赖注入操作之后被执行
- 执行构造方法,两种执行方式,一种是执行 @PostConstruct(优先,注解时代的产物),另一种是执行 init-method 方法(xml时代的产物)
- 执行初始化的后置方法
- 使用 Bean
- 销毁 Bean
(1)@PreDestroy
(2)重写 @DisposableBean 接口方法
(3)destroy-method
注意:步骤二与步骤三顺序不能交换,因为在步骤三中的构造方法中可能会用到注入对象的方法