Spring世界--IoC&DI

 2023-02-15
原文作者:MongieLee 原文地址:https://juejin.cn/post/7101145217319288839

Java世界的基石--Spring

Spring中有3个核心的概念:控制反转(IoC:Inversion of Controller),依赖注入(DI:Dependecy Injection),面向切面编程(AOP:Aspect Oriented Programming

Spring的主要作用就是降低代码间耦合度,让对象和对象之间的关系不再是使用代码关联,而是通过配置,配置大于一切,开发者只需要将对象生成说明书写好,就不需要再手动的创建对象,统一让Spring容器去统一管理,自动注入依赖。而AOP使系统服务得到了最大的复用,开发者不需要再将这些和业务本无关的服务混入到业务逻辑代码中,Spring会进行代码组织。

控制反转,依赖注入

    public class Car{
        public void run(){
            System.out.println("car is running");
        }
    }
    
    public class Person{
        private Car car;
        
        public Person(Car car){
            this.car = car;
        }
        
        public void say(){
            System.out.println("person start off!");
            car.run();
        }
    }

上面这段例子中的依赖关系:Personsay函数中需要调用Car对象中的run方法,此时Person依赖了CarPersonCar是依赖关系。

  • 代码存在的问题

    • 由于Person依赖了Car,所以在创建Person对象前需要先new Car(),然后再new Person(car),代码本身没有问题,但当很多地方都用到依赖对象,并且后续需要新增依赖等操作,代码和工作量都会相对大,所有对象的创建都是由开发者去控制。

一个新思路,找一个第三方托管这些对象,类似美团跑腿,把需要的沐浴露洗发水,牙膏鸡肉卷列一份清单出来,像牙膏鸡肉卷依赖了牙膏,就把牙膏也给买来,买完后放到一个桶里,可以称之为容器,当需要时,可以从这个桶里找需要的对象,创建和组装的过程就不用再手动管理了,Spring就实现了这个功能,Spring容器 控制反转 的概念就是如此。

使用Spring

先说说Bean的概念

所有由Spring管理的对象统一称为Bean,本质上就是普通的对象,和直接new XXX出来的没区别,只不过由Spring容器所管理着,需要通过清单告诉Spring容器需要创建哪些bean,这些配置称为bean定义配置的元数据信息,Spring容器读取这些信息就知道怎么玩了

新建一个Maven项目

pom文件添加Spring依赖

        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-context</artifactId>
          <version>5.3.10</version>
        </dependency>

刷新maven后,会自动下载Spring的核心包

202301012020360551.png

动手体验:先写一个对象,这里类名为Person,配置里需要对应

    package com.demo;
    
    public class Person {
        private String name;
        private int age;
    
        public Person() {
            System.out.println("Person constructor");
        }
    }

src/main/resources目录下新建一个bean.xml(叫啥无所谓,自己喜欢),这个beans标签格式不需要背,用idea新建xml时有spring配置的模板选项,或者用的时候cv即可,在子标签添加一个<bean>标签,idbean的名字,后续从容器取出时需要用到,class为对应的全限定类名

可以了解的XML配置知识

beans是根元素,下面可以包含任意数量的importbeanalias标签
这里列出的属性只需要记idclass,其他的用的不多了解即可
<bean id="bean唯一标识,有重复就会报错" name="bean的名称,也可以成为别名" class="完整类型名称" factory-bean="工厂bean名称" factory-method="工厂方法" />
id和name的规则

  1. id存在的时候,不管name有没有,取id为bean的名称,name就是别名,起同等效果共存
  2. id不存在,此时要看name有没有,name的值可以通过逗号和分号还有空格分隔,会按照分割规则得到一个数组,数组的第一个元素作为bean的名称,其他的作为bean的别名
  3. idname都存在,idbean名称,name为别名
  4. 都不存在时,bean名称会自动生成,格式为全限定类名#序号,别名则为全限定类名,序号从0开始,同类型则递增,且只有第一个bean拥有别名
  5. context.getBeanDefinitionNames方法可以获取所有bean的名称数组
  6. context.getAliases(beanDefinitionName)传入bean的名称可以获得该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"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
        <bean id="person" class="com.demo.Person"/>
    </beans>

新建一个main方法,通过Spring提供的ClassPathXmlApplicationContext类创建容器,容器提供了一个getBean方法获取bean的实例,传入的字符串就是xmlbean标签的id

    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class Application {
        public static void main(String[] args) {
            ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("Application.xml");
            Person person = (Person) context.getBean("person");
            System.out.println(person);
        }
    }

查看控制台,成功获取了Person对象并输出

202301012020365892.png

这便是SpringIOC体验,开发者不用再去写new XXX对象,要什么问Spring管家要即可。Spring会读取XML配置,通过反射去创建对象,感兴趣可以看看这篇文章,模拟了一个最基本的反射容器管理: Spring--手写一个简易的IoC容器,附思路原理

再看看依赖注入

注入分为简单类型和引用类型注入,在xml配置中

  • 简单类型(包含String)使用value属性,引用类型使用ref属性

  • 注入又分为setter注入和构造函数注入

    • 使用setter注入,必须提供无参构造方法和对应的setXXX方法,可以理解为先新建一个空对象再进行属性的setter注入
    • 使用构造函数注入,又分为属性名和构造方法参数下标,使用下标注入时需要和构造方法的参数类型一一对应,不推荐使用下标方式【应该说不推荐XML形式:),后面会介绍注解形式】

再体验下依赖注入功能

  1. 新建Cat类,只需要有空参构造
  2. Person类添加一个String类型的name属性,一个Integer类型的age属性,一个Cat类型的cat属性。再为Person类添加全属性的gettersetter方法,以及全参无参构造方法 下面演示了3种实现,先修改xml文件配置
    <!--通过属性名配置注入,位置随意-->
    <bean id="person" class="com.demo.Person">
      <property name="name" value="jack"/>
      <property name="age" value="10"/>
      <property name="cat" ref="cat"/>
    </bean>
    
    <!--通过构造方法的参数名注入,位置随意-->
    <bean id="person2" class="com.demo.Person">
      <constructor-arg name="name" value="mike"/>
      <constructor-arg name="age" value="20"/>
      <constructor-arg name="cat" ref="cat"/>
    </bean>
    
    <!--通过构造方法参数的下标注入(位置要讲究一对一匹配类型)-->
    <bean id="person3" class="com.demo.Person">
      <constructor-arg index="0" value="jay"/>
      <constructor-arg index="1" value="30"/>
      <constructor-arg index="2" ref="cat"/>
    </bean>

beanname从容器中分别取出3个同类型的bean

    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("Application.xml");
        Person person = (Person) context.getBean("person");
        System.out.println(person.getName() + "," + person.getAge() + "," + person.getCat() + "\n");
    
        Person person2 = (Person) context.getBean("person2");
        System.out.println(person2.getName() + "," + person2.getAge() + "," + person2.getCat() + "\n");
    
        Person person3 = (Person) context.getBean("person3");
        System.out.println(person3.getName() + "," + person3.getAge() + "," + person3.getCat() + "\n");
    }

xml中配置的值和预期一样被注入到对象中,可以看到3个不同的Person对象,里面的Cat对象都是同一个实例,说明Springbean默认是单例模式(可以配置其他模式)

202301012020378873.png

bean作用域scope

有些bean想要多例,有些想要单例,spring提供了作用域的配置,bean标签配置scope属性

  1. singleton,不填写默认就是singleton,整个容器中,该bean只会创建一次,每次需要用时,都返回同一个对象(getBean或者依赖注入时),默认是在启动spring容器时生成,bean被设置lazy-init属性为true时,则需要被用到再生成。单例bean是整个容器共享的,如果设计到修改更新操作有线程安全的问题,需要注意使用。
  2. prototypescope设置为prototype,则表示该bean是多例的,每次获取时都会生成一个新的实例对象,如果对象的创建比较繁琐消耗时间,则会有性能问题。
  3. request,每个http请求都会创建一个bean实例(3,4,5需要结合Spring web
  4. sessionsession级别共享,不同session则创建不同的实例
  5. application 全局应用共享

bean标签中的引用类型自动注入,还可以使用标签的属性autowire

  1. 设置为byType相当于注解的Autowired(后面会说)
  2. 设置为byName相当于注解的Autowired+Qualifier属性名要与bean名一致

xml中的成员变量的property标签就不用写了

        <bean id="person" class="com.demo.Person" autowire="byName">
            <property name="name" value="jack"/>
            <property name="age" value="10"/>
            <!--autowire设置里byName,这玩意就不用写了 <property name="cat" ref="cat"/>-->
        </bean>

import导入

当项目越来越大,所有配置都在一个xml中则难以管理,可以按照业务层去拆分,如按照经典的三层架构,controllerservicedao分别新建一个文件夹,文件夹里专门放入对应模块的xml配置,再用一个主入口xml来导入配置

202301012020385404.png 关于XML的配置就说到这里,其实还有很多内容,但目前基于Springboot(一个让开发更简单的集成方案)用注解为主流方案,所有对xml有兴趣的可自行摸索学习,即使不用也应该简单了解,毕竟是曾经的方案

注解Annotation声明Bean

@Configuration和@Bean

  • @Configuration这个注解用在类上,可以理解为beans标签,要配合@Bean注解使用
  • @Bean可以理解为bean标签,用于声明方法是一个bean
    @Configuration
    public class BaseConfig {
        @Bean
        public String testAn(){
            return "testAn";
        }
    }

修改启动类,ClassPathXmlApplicationContext要改为AnnotationConfigApplicationContext,参数为配置类的Class对象

    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BaseConfig.class);
    String testAn = (String) context.getBean("testAn");
    System.out.println(testAn);

运行后,Spring会取扫描类中@Configuration@Bean注解,凡是带有这两个注解的类和方法都会被注册成bean,类的bean名称会使用小驼峰形式:BaseConfig转成baseConfig,示例获取了配置的testAn并输出

202301012020397535.png

知识补充环节

  • 即使类不添加@Configuration注解,类中带有@Bean的方法也会被注册成bean,如果获取对应的Configuration配置bean并直接输出,可以看到会带有cglib字眼com.demo.BaseConfig$$EnhancerBySpringCGLIB$$75472aeb@429bd883,说明这个类是被代理生成的,所有被@Bean修饰的方法都会被拦截动态增强
  • 如果类不加@Configuration注解,容器也会管理bean,但这些bean的类没有被代理,所以每次都会获取一个新的bean对象,非单例模式

为了方便注册Bean,还提供了其他注解

  1. @Controller 专门用来创建控制器的对象(Servlet),这种对象可以接受用户的请求,可以返回处理结果给客户端
  2. @Service 专门用来创建业务逻辑层的对象,负责访问数据,处理完后返回给界面层
  3. @Repository 专门用来创建数据访问层的对象
  4. @Component 可以创建任意对象,创建对象的默认名称是驼峰的命名法,也可以指定名称 这些类都是一个作用,只是带有语义

@ComponentScan和@ComponetnScans

使用new AnnotationConfigApplicationContext(BaseConfig.class)容器只会去注册BaseConfig配置类和其成员bean,但一个项目显然会有很多地方需要注册bean,并且通常项目会进行分层,使用@ComponentScan可用于指定Spring容器需要扫描哪些包和哪些类所在的包中带有特定注解的类

新建一个dao包,并新建一个UserDao添加上@Repository(上面说的四种随意使用,不重要)

202301012020403986.png 此时需要做指定扫描行为,才能让Spring加载UserDao作为容器成员bean,演示三种使用方式

    // 1. 不给注解任何属性值,则会默认扫描当前类所在包下的所有子类和子包
    @Configuration
    @ComponentScan
    public class BaseConfig {
    }
    
    // 2. 指定包名,会去扫描com.demo.dao包下的所有子类,如果要扫描多个包子传入一个包名字符串数组
    @Configuration
    @ComponentScan("com.demo.dao")
    public class BaseConfig {
    }
    
    // 3. 指定类对象,会扫描对应类所在包下的所有子类,扫描多个类则传入类对象数组
    @Configuration
    @ComponentScan(basePackageClasses = UserDao.class)
    public class BaseConfig {
    }

目前来说以上3种效果等价

@ComponentScans注解的使用为传入@ComponentScan注解的数组

注解也可以和XML配置文件结合使用,需要在XML中配置如下标签,base-package为需要扫描的包路径,可配置多个指定标签扫描多个包
<context:component-scan base-package="com.demo"/>

@Import

作用类似于XML中的import标签,该注解常用于将一个第三方类注册为bean,因为第三方类库里没有自带Spring的注解,当然也可以选择声明bean,但这样需要多写一个方法,import能更好的完成这项任务,bean的名称默认是类的全限定类名(类名不会转小驼峰)

    @Import(UserService.class)
    public class BaseConfig {
    }

注解接收一个类对象数组,传入的类都会被注册成为bean,切记如果需要获取bean则要使用全限定类名,如getBean("com.demo.service.UserService")

依赖注入: @Value,@Autowired,@Resource,setter方法,构造方法

假定一个类中,有两个成员属性,一个为String类型(或者是基本数据类型),一个为引用类型,

  • @Value 可以在属性声明注入简单类型的值,包括String,此时无需编写setter方法,如有setter添加到setter也可以,支持从配置文件读取(配合@PropertySource
  • @Autowired 可以自动注入引用类型,前提是必须为当前容器中被注册的bean对象,默认情况下会根据bean的类型去寻找bean注入,匹配到多个类型时则会按照name注入,有个required属性默认为true,匹配失败则会抛出异常

最基本字段注入

    @Repository
    public class UserDao {
        @Value("testName")
        private String daoName;
        @Autowired
        private Cat daoCat;
    }

使用setter方法注入

    
    @Repository
    public class UserDao {
        private String daoName;
        private Cat daoCat;
    
        @Value("testName")
        public void setDaoName(String daoName) {
            this.daoName = daoName;
        }
    
        @Autowired
        public void setDaoCat(Cat daoCat) {
            this.daoCat = daoCat;
        }
    }

构造器注入(目前Spring推荐的写法),由于当前没有注册过类型为String类型的bean,所以使用@Value注入想要的值,如果有String类型的bean则可不写会自动注入。普通方法或者方法参数使用@Autowired也可以实现注入,但一般不这么做,因为不好管理依赖。

    @Repository
    public class UserDao {
        private String daoName;
        private Cat daoCat;
    
        // @Autowired // 写和不写的效果等价
        public UserDao(@Value("test") String daoName, Cat daoCat) {
            this.daoName = daoName;
            this.daoCat = daoCat;
        }

@Value和@PropertySource 注入值从配置文件中读取

resource目录下新建一个config.properties文件,写入一行jdbc.name=test,语法格式为@Value("${配置文件中的key:默认值}")@PropertySource接受配置文件路径的数组

    @Repository
    @PropertySource("classpath:config.properties")
    public class UserDao {
        @Value("${jdbc.name:默认值}")
        public String daoName;
    }
    // 当key不存在时,字段的值就是配置的默认值

实际开发中,可以用一个配置类加载需要从配置文件中读取的数据,并将配置类注册为bean,在其他需要使用的地方注入配置类即可方便使用配置文件

不太常用的CollectionMap注入,需要使用接口,了解即可

  • 使用CollectionListCollection的子接口),会将所有符合泛型类型的bean收集到list
  • 使用Map时通常为key/value,会将所有符合泛型类型的beanname作为key,实例对象作为value
    // 假定有一个UserDao的接口,以及若干个实现类
    
    @Autowired
    private List<UserDao> interfaceList;
    // 会将所有UserDao的实现类收集
    
    @Autowired
    private Map<String,UserDao> interfaceMap;
    // 会将所有UserDao的实现类的name作为key,实例对象作为value存入map中

@Resource

Spring还支持了Java的原生注解@Resource,效果和@Autowired注解类型相同,但仍有细微差别,使用注意事项

  1. 当使用@Resource注解不传属性时,默认会按照当前注入字段的name去寻找bean
    @Resource
    private UserService userService;
    // 此时会去找名为userService的bean进行注入
  1. 传入了name属性,则会去寻找容器中同名bean进行注入@Resource(name = "userService")
  2. 传入了type属性,则会按照类型去寻找bean进行注入@Resource(name = "userService", type = UserService.class)
  3. 既传了name又传了type,则必须二者都符合规则的bean才会被注入 @Resource和@Autowired对比
  4. 默认情况,@Resource使用name寻找bean装配,@Autowired使用type寻找bean装配
  5. @Resource传入type属性支持按照类型注入,@Autowired配合@Qualifier可以指定name注入
    // 指定了bean的名称,而不是使用类型装配
    @Autowired
    @Qualifier("userService")
    private UserService userService;
  1. @Autowired注解支持设置没有找到可注入bean时跳过注入,不会报错,而@Resource不支持
    // 如果没有对应的bean,此时为null而不会报错
    @Autowired(required = false)
    private UserService userService;
  1. 都可以用在字段和setter方法上
  2. @Resource的使用可以减少和Spring框架的耦合

同源类型的定义

  1. 被注入的类型和注入的类型是完全相同的类型
  2. 被注入的类型是父类型,注入的是子类型
  3. 被注入的类型是接口,注入的是实现类

当容器中存在多同源类型时,bean的装配机制(以下均指使用类型注入方式)

  1. 当注入类型同时存在多个bean时,默认会按照注入的字段name去匹配,如果匹配不到会抛出异常,此时需要指定name去进行装配
  2. 当注入类型为父类型,父类bean和子类bean同时存在时,默认会按照注入的字段name去匹配,如果匹配不到会抛出异常,此时需要指定name去进行装配
  3. 注入类型为接口,有诸多实现类bean实现了同一接口,由于匹配到了多个可注入类型,此时会按照字段name去装配,此时需要指定name去进行装配

@Primary

提高bean的候选优先级,上面说过如果同类型bean存在多个且没有特殊配置的情况下,会抛出异常,解决方案还可以使用@Primary注解,相当于在匹配到多个可注入类型时,会优先注入带有@Primary注解的bean

    // 接口
    public interface UserDao{}
    
    // 实现类1
    @Component
    public class UserDao1 implements UserDao{}
    
    // 实现类2
    @Primary
    @Component
    public class UserDao2 implements UserDao{}
    
    // 实现类3
    @Component
    public class UserDao3 implements UserDao{}
    
    @Component
    public class UserService{
        @Autowired
        private UserDao userDao;
    }
    // 由于匹配到了3个相同类型的bean,此时候选者列表中有3个bean对象
    // spring会寻找是否有bean是带有@Primary注解的,有则注入bean

@Scope 指定bean的作用域

关于bean的作用域在xml的段落讲过了,这个@Scope注解起同等作用,Spring提供了一些常量支持

  1. ConfigurableBeanFactory.SCOPE_PROTOTYPE 多例模式,每次使用都会生成新的实例bean
  2. ConfigurableBeanFactory.SCOPE_SINGLETON 单例模式,默认行为,容器中只有一份实例
  3. org.springframework.web.context.WebApplicationContext.SCOPE_REQUEST 生成bean的时机为request级别
  4. org.springframework.web.context.WebApplicationContext.SCOPE_SESSION 生成bean的时机为session级别 每一次bean被使用时都会new一个新的实例,校验时调用多次context.getBean,即可观察到输出了多次构造方法的内容
    @Component
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public class Cat {
        public Cat() {
            System.out.println("Cat is created");
        }
    }

@DependsOn 指定依赖bean

效果等同于在xmlbean标签中配置depend-on属性,作用为确保在当前bean被创建前,依赖的bean已经被创建好了,因为没有办法保证Spring初始化bean的顺序,这个注解则可以确保在创建当前bean时,被依赖的bean一定会被先创建好。注解接受字符串数组,字符串为beanname(在idea中可能会报红线,忽略即可)

    @Component
    @DependsOn("userDao")
    public class UserService {
        @Autowired
        private UserDao userDao;
        
        public UserService(){
            System.out.println("UserService created");
        }
    }

使用了注解后,可以确保创建UserService实例前,UserDao会被先创建

@ImportResource导入bean的配置文件

可用于使用注解方式的场景下,通过此注解进行配置文件配置,沿用上述xml讲解时编写的xml文件

    @Configuration
    @ImportResource("classpath:Application.xml")
    public class BaseConfig {
    }

效果等同于将xml文件中配置的bean编写在配置类中,classpath:表示检索当前目录下的classes目录

202301012020412107.png

@Lazy 延迟初始化

效果等于同在xmlbean标签中配置lazy-init属性,可以让bean的初始化延迟,意思就是只有用到时再去初始化实例

  1. 配合@Componet使用,则表示这个类延迟加载
  2. 配合@Configuration使用,表示这个配置类中,所有带@Bean注解的bean延迟加载
  3. 配合@Bean使用,表示当前被标注的bean延迟加载 检验方式很简单,给bean的构造方法添加一段log输出,默认情况下容器会初始化实例,log会输出,对比加了@Lazybean,没有被用到的bean的构造方法并没有被执行
    @Lazy
    @Configuration
    public class BaseConfig {
        @Bean
        public String test() {
            return "test";
        }
        
        @Bean
        @Lazy(false)
        public String test2() {
            return "test2";
        }
    }

如果这个配置类下的test bean没有被使用,则不会实例化,在bean上单独使用则优先级比配置类的高,test2 bean没有被使用也会被创建

end :)