您现在的位置是:首页 >技术教程 >Spring核心与设计思想、创建与使用网站首页技术教程

Spring核心与设计思想、创建与使用

求索1024 2023-06-26 08:00:02
简介Spring核心与设计思想、创建与使用

一、Spring是什么

  我们通常所说的 Spring 指的是 Spring Framework(Spring 框架),它是⼀个开源框架。Spring ⽀持⼴泛的应⽤场景,它可以让 Java 企业级的应⽤程序开发起来更简单
  ⽤⼀句话概括 Spring:Spring 是包含了众多⼯具⽅法的 IoC 容器

二、为什么要学习框架

  1. 就像我们装修房子,不使用框架就像我们材料自己买,关于房子的事情都需要亲力亲为,使用框架时就像我们请了装修公司,材料、人员安排等等我们不再关系,而只需要提出需求,跟进进度即可,特点就是高效
  2. 框架更加易用,并且使用起来也是非常简单

  上述优势通过 SpringBoot 和 Servlet 对比能够更容易地看出:

  1. SpringBoot 添加外部 jar 包更容易,不易出错,直接引入即可,而Servlet还需要关注版本问题
  2. 调试、发布项目更加方便,无需配置 Tomcat,因为它里面已经内置了
  3. 添加路由更加方便,无需每个访问地址都添加一个类

三、IoC和DI

(一)IoC

  在认识IoC之前,我们要先知道容器地概念,通过百度,我们可以知道容器是用来容纳某种物品的装置,我们之前学过的List是数据存储容器,Tomcat是Web容器

1. 认识IoC

  首先明确一点,Spring也是一个容器,它是一个IoC容器
什么是 IoC?

  IoC = Inversion of Control 翻译成中⽂是“控制反转”的意思,IoC就是控制反转的思想。“控制反转”是两个词,控制和反转。指的是:之前程序的控制权限是在我们自己手上,现在把控制权交出去,我们只需要使用即可

  1. 一般情况下,我们在 A 类 中,想去调用 B 类中的方法,要去 new B 类对象,通过 对象 去调用 B类中的方法。当前 B 的控制权,是我们手上的
  2. 而控制反转,就是将我们手上的权限,交由 Spring 框架来操作
  3. 此时,我们想要 A 类中调用 B 的时候,告诉框架,我要在 A 中调用 B 了。至于 B 的生命周期,和我们没有任何关系。
  4. 因为我们把控制权 “反转给了” Spring 框架。Spring 会帮我们管理所有的对象(Bean)

2. Spring的核心功能

  通过上述关于IoC的介绍,我们知道了Spring的核心功能就是两点:

  1. 将 Bean(对象)存储到 Spring(容器)中
  2. 将 Bean(对象)从 Spring(容器)中取出来

(二)DI

  只要我们说到IoC,就离不开DI,它们俩的关系就像是切菜和菜刀一样,切菜是一种动作,也是一种想法,该如何实现呢?利用菜刀来实现这个想法
  DI 是 Dependency Injection 的缩写,翻译成中⽂是“依赖注⼊”的意思。具体解释就是在 IoC 容器运行期间,动态的将某种依赖关系注入到对象中。就是为了实现 IoC 的思想,所以,IoC 和 DI 的核心都是为了“控制反转”,,但是区别就是IoC是思想,DI是实现

四、Spring项目的创建

(一)使用 Maven 方式创建一个 Spring 项目

  1. 创建一个普通的 Maven 项目(不需要添加web-app模板)
  2. 添加 Spring 框架支持
    (1)在 pom.xml 当中加入依赖
    <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>
    
    (2)粘贴完之后,刷新 Maven
  3. 在 Java 源文件中添加一个启动类,并编写 main 方法即可
    添加启动类 & 编写main方法

五、Spring项目的使用

(一) 存储 Bean 对象到容器(Spring)中

  1. 如果第一次存储,先在 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>
    
  2. 创建 Bean 对象(Java对象的另一种称呼),和对普通的类实例化一样
    创建bean对象
  3. 在配置文件中将需要保存到容器中的 Bean 对象进行注册,如下图所示,表示将 User 对象以类似字典的形式存入到 Spring 当中,id就是它的标识(通常使用小驼峰),类似于 HashMap 中的key
    注册

(二)从 spring 将 bean 对象读取出来并使用

  1. 得到 spring 上下文对象
  2. 通过上下文对象提供的方法获取自己需要使用的 bean 对象
  3. 使用 bean 对象

从 spring 将 bean 对象读取出来并使用

  除了图上通过 ApplicationContext 来获取 spring 上下文,我们也可以通过 BeanFactory (Bean工厂)来作为 spring 的上下文,代码如下

BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("spring-config.xml"));

  那么这二者有什么区别呢,这就是一个经典面试题:ApplicationContext VS BeanFactory

相同点:都可以实现从容器中获取bean,都提供了 getBean 方法
不同点:

  1. ApplicationContext 属于 BeanFactory 的子类。BeanFactory 只提供了基础访问 Bean 的方法,而 ApplicationContext 除了拥有 BeanFactory 的所有功能之外,还提供了更多的方法实现,比如对国际化的支持、资源访问的支持、以及事件和传播等方面的支持
  2. 性能方面来说二者不同。BeanFactory 是按需加载 Bean,懒加载;ApplicationContext 是饿汉方式,在创建时会将所有的 Bean 加载起来,以备以后使用

(三)getBean的更多用法

  1. 使用 bean name 获取 bean
User user = (User)context.getBean("user");
  1. 根据 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 注册内容,而现在我们只需要一个注解就可以替代之前的操作,不过在此之前,我们要先进行一些准备工作

  1. 前置工作:配置扫描路径(关键
    在 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 中了

  2. 简单的将 bean 存储到容器中

    1. 使用五大类注解实现
      1. Controller(控制器)
        // 必须添加注解
        @Controller
        public class UserController {
            public void hello() {
                System.out.println("hello, UserController!");
            }
        }
        
      2. Service(服务)
      3. Repository(仓库)
      4. Configuration(配置)
      5. Component(组件)
    2. 通过 @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;
          }
      }
      
  3. 提出疑问:上述将 bean 存储到容器中,明明五大类注解任何一个都可以,为什么还需要五大类注解?
    解答:
      这里就涉及到软件工程的知识了,如下图所示,我们的五大类注解和下面的软件架构分层一一对应,当我们的 bean 很多时,我们就有必要根据不同的功能使用不同的注解对不同的 bean 进行区分,让代码的可读性提高,让程序猿能够直观的判断当前类的用途。Component 就是用于当 bean 不属于下面四种情况时,作为一个工具来使用时使用 Component 来注释
    软件架构分层

  4. 五大类注解之间的关系,这个问题我们可以直接看底层源码,就会发现其他四个类注解都是继承自 Component,换句话说,Compinent 是其他四个类的父类,以 Controller 为例
    五大注解继承关系

  5. 使用五大类注解产生的 bean name 问题。我们读取 bean 时,这个问题是避不开的,因此需要我们着重注意
    解答:
      这个问题我们需要去看注解 bean name 生成器,即:AnnotationBeanNameGenerator,当我们在这个方法中左拐右拐,最终却定位到了decapitalize.java文件,通过查看文件目录结构,我们发现,这个方法是在 jdk 里自带的,换句话说,通过注解生成 bean name 的规则,实际是 jdk 自带的,接下来我们对这个方法进行源码分析,如下图:
    bean name生成规则
      因此我们说,当类名第一个字符和第二个字符都是大写时,bean name 就是它本身,否则就是第一个字符小写

  6. 使用 @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 对象也叫做对象装配,是把对象取出来放到某个类中,有时候也叫对象注⼊。对象注入的实现方式有三种:

  1. 属性注入
  2. 构造方法注入
  3. 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();
    }
}

运行结果
Autowired属性注入

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();
    }
}

运行结果
构造方法注入 Bean 对象

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();
    }
}

运行结果
Setter注入

4. 使用 @Resource 代替 @Autowired

  @Resource 是 Java 官方提供的,@Autowired 是由 Spring 官方提供的,直接将 @Resource 替换为 @Autowired 即可。但是,@Resource 不支持属性注入因此我们说,注入方式有三种,注入方法有这两种

5. 问题一:@Resource VS @Autowired

相同点:都可以实现将一个对象注入到类当中
不同点:

  1. 实现者不同,@Resource是 Java 官方提供的,@Autowired是 Spring 官方提供的
  2. 用法不同:@Autowired 支持属性注入、构造方法注入和Setter注入,但是 @Resource 不支持属性注入
  3. 支持参数不同:@Resource 支持更多的参数设置,比如name、type设置,而 @Autowired 只支持 required 参数设置

6. 问题二:属性注入、构造方法注入与set方法注入有什么区别

答:
  1. 属性注入写法简单,但只能运行在IoC容器下,其他容器会出现问题
  2. Setter注入:早期 Spring 版本推荐的写法,但 Setter 注入没有构造方法注入通用,其他语言写set方法很麻烦
  3. 构造方法注入:通用性更好,它能确保在使用注入对象之前,此注入对象一定初始化过了

7. 问题三:当我们使用 @Bean 保存了多个相同类的不同对象,此时上述代码就会出现问题,会报出不唯一的异常

如下图:
多次保存问题
多次保存问题
  针对这个问题,我们有如下的解决方案:

  1. 精确的描述 bean 的名称(将注入的名称写对)

解决方案一

  1. 使用 @Resource 设置 name 的方式来重命名注入对象

Resource注解

  1. 如果工作时要求不能使用 @Resource,并且我们业务代码已经写了很多,采用第一种方法改起来很麻烦,我们就可以使用 @Qualifier + @Autowired 来筛选 bean 对象

Qualifier的使用

七、Bean 作用域和生命周期

(一)Bean 作用域的问题

1. 案例

  由上述知识我们知道,Spring 是用来读取和存储 Bean 的,因此 Bean 是 Spring 最核心的操作资源,因此我们需要更深入的了解一下 Bean。其中我们的预期结果是公共的 Bean 经过注入之后可以在各自的类进行修改,但是不能影响到其他的类但是经过测试发现结果并不是这样

  1. 测试类1,注入对象后将将象的 name 设为“王五”,并返回
    测试类1
  2. 测试类2,注入对象后直接返回
    测试类2
  3. 结果
    测试结果

  注意,作者在测试这里的代码时,恰好遇到了一个需要注意的问题,在有 Spring 存在的项目中,Spring 的注入要统一使用,类的一步步引用,要么为全部注入,要么都不注入,否则会出错。代到当前测试例子中,由于作者刚开始进行测试时在 app 类中两个类都是 new 出来的,导致两个类中并没有对 User 类进行注入,在操作时就会报出空指针异常
  言归正传,我们在上面进行测试时,发现结果和预期并不相同,UserTestBean2中对对象进行了修改,居然影响到了UserTestBean1,那么问题是什么呢?

2. 原因分析

  上述问题的原因就是因为 Bean 默认情况下是单例模式(singleton),换句话说所有类使用的是同一个对象,这样做可以很大程度上提高性能

(二)作用域的定义

  限定程序中变量的可⽤范围叫做作⽤域,或者说在源代码中定义变量的某个区域就叫做作⽤域。
  ⽽ Bean 的作⽤域是指 Bean 在 Spring 整个框架中的某种⾏为模式,⽐如 singleton 单例作⽤域,就表示 Bean 在整个 Spring 中只有⼀份,它是全局共享的,那么当其他⼈修改了这个值之后,那么另⼀个⼈读取到的就是被修改的值

1. Bean 的六种作用域

Spring 容器在初始化一个 Bean 的实例时,同时会指定该实例的作用域。Spring 有六种作用域,最后四种是基于 Spring MVC 生效的:

  1. singleton:单例作用域(Spring默认选择该作用域)
  2. prototype:原型作用域(多例作用域)
  3. request:请求作用域
  4. session:会话作用域
  5. application:全局作用域
  6. websocket:HTTP WebSocket 作用域

使用场景

  1. singleton:通常无状态的 Bean 使用该作用域。无状态表示 Bean 对象的属性状态不需要更新
  2. prototype:通常有状态(即需要进行修改)的 Bean 使用该作用域
  3. request:一次http的请求和相应的共享Bean
  4. session:用户会话的共享Bean,比如记录一个用户的登录信息
  5. application:Web应用的上下文信息,比如:记录一个应用的共享信息
  6. websocket:Websocket的每次会话中,保存了一个Map结构的头信息,将用来包裹客户端消息头,第一次初始化后,直到 WebSocket 结束都是同一个Bean

单例作用域(singleton)VS 全局作用域(application)

  1. singleton 是 Spring Core 的作用域;application 是 Spring Web 中的作用域
  2. singleton 作用于 Ioc 的容器,而 application 作用于 Servlet 容器

2. 设置作用域

  使用 @Scope 标签就可以用来声明 Bean 的作用域。@Scope 标签既可以修饰方法也可以修饰类,@Scope 有两种设置方式:

  1. 直接设置值:@Scope(“prototype”)
  2. 使用枚举设置:@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 执行流程

  1. 启动 Spring 容器
  2. 根据配置完成 Bean 初始化(分配内存空间)
  3. 注册 Bean 对象到容器中
  4. 装配 Bean 到需要的类中

2. Bean 生命周期

  1. 实例化 Bean (为 Bean 分配内存空间)
  2. 设置属性(Bean 注入和装配)
  3. Bean 初始化
  • 实现了各种 Aware 通知的方法
  • 执行初始化的前置方法,依赖注入操作之后被执行
  • 执行构造方法,两种执行方式,一种是执行 @PostConstruct(优先,注解时代的产物),另一种是执行 init-method 方法(xml时代的产物)
  • 执行初始化的后置方法
  1. 使用 Bean
  2. 销毁 Bean

(1)@PreDestroy
(2)重写 @DisposableBean 接口方法
(3)destroy-method

注意:步骤二与步骤三顺序不能交换,因为在步骤三中的构造方法中可能会用到注入对象的方法

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。