Spring IOC详解

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: Spring IOC详解

Spring 笔记

官网:https://spring.io/

核心功能:当你的项目启动的时候,自动的将当前项目的各种 Bean 都自动的注册到 Spring 容器中,然后在项目的其他地方,如果需要用到这些 Bean,直接去 Spring 容器中查找需要的对象即可。

Spring 家族的产品:

  • Spring Framework:Spring 框架的基础,我们一般所说的 Spring、SpringMVC 其实都是 Spring Framework。
  • Spring Boot:简化 Spring 配置,可以一键创建一个带有各种配置的 Spring 环境。
  • Spring Data:简化数据库配置/简化数据库访问。
  • Spring Cloud:微服务。
  • Spring Security:安全管理框架。
  • Spring Session:session 共享。

Spring Framework:

  • Ioc
  • Aop
  • JdbcTemplate
  • 事务

Spring 容器注册 Bean

正常情况

  1. 创建一个 Maven 工程。

  2. 引入 Spring 依赖:

    1. <dependencies>
          <dependency>
              <groupId>org.springframework</groupId>
              <artifactId>spring-context</artifactId>
              <version>5.3.20</version>
          </dependency>
      </dependencies>
      
  3. 加入 Spring 的配置。

  1. 创建一个 Bean 并注册。

    public class User {
         
        public void sayHello() {
         
            System.out.println("hello zhangsan");
        }
    }
    
    <?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">
    
        <!--
        class 指定类的全路径
        id 指定类的名称
        Spring 拿到这两个关键信息,就可以自动的创建一个 User 对象出来了
        -->
        <bean class="com.qfedu.demo.model.User" id="user"/>
    </beans>
    
  2. 启动 Spring 容器,并加载配置文件,当 Spring 容器启动之后,无论你是否跟 Spring 容器去要 User 对象,此时 User 对象都是已经创建好的状态,并保存在 Spring 容器中。

    public class MainDemo01 {
         
        public static void main(String[] args) {
         
            //这里只需要写配置文件的文件名即可,系统会自动去 classpath 下查找配置文件
            //这个就是加载 Spring 容器,只要 Spring 容器启动了,那么配置文件中的所有 Bean 就会完成初始化
            ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
            //根据名字去查找一个 user 对象,这个方法返回一个 Object,需要进行类型转换
            User u1 = (User) ctx.getBean("user");
            u1.sayHello();
            //去查找 User 类型的对象
            //这种方式有一个缺陷:Spring 容器中如果存在多个 User 对象,那么这个方法执行就会报错
            User u2 = ctx.getBean(User.class);
            u2.sayHello();
            //告诉 Spring 容器,查找一个名为 user 的 bean,并且类型是 User
            User u3 = ctx.getBean("user", User.class);
            u3.sayHello();
        }
    }
    

异常情况

NoSuchBeanDefinitionException

没有找到需要的 Bean:

  1. 先检查注册的时候,Bean 的名称是否正确。
  2. 检查跟 Spring 容器要的时候,Bean 的名称是否正确。
  3. 检查一下启动 Spring 容器时,Spring 的配置文件名称是否正确。

NoUniqueBeanDefinitionException:

这表示要查找的目标 Bean 有多个,查找异常。这种时候就不要按照类型去查找,而应该按照名字去查找。

Bean 注册的细节

属性注入方式

  1. 构造器注入

    1. 默认情况下,如果我们向 Spring 容器注入一个 Bean 的时候,不指定构造方法,那么默认使用的构造方法就是无参构造方法,所以如果你的类里边没有无参构造方法,就会出错。

      1. public class Book {
                 
            private Integer id;
            private String name;
            private String author;
        
            public Book(Integer id, String name, String author) {
                 
                this.id = id;
                this.name = name;
                this.author = author;
            }
        }
        
      2. 由于这个 Java 类没有无参构造方法,所以在注入 Bean 的时候,如果按照下面的方式注入,就会出错:

      3. <bean class="com.qfedu.demo.p2.model.Book" id="book"/>
        
      4. 在 bean 标签中,如果没有指定构造方法,默认就使用无参构造方法。

    2. 开发者也可以自己指定要使用哪一个构造方法:

      1. <!--这里没有指定使用哪个构造方法,默认就使用无参构造方法-->
        <bean class="com.qfedu.demo.p2.model.Book" id="book"/>
        <!--也可以指定要用哪个构造方法-->
        <bean class="com.qfedu.demo.p2.model.Book" id="book2">
            <constructor-arg name="id" value="1"/>
            <constructor-arg name="name" value="三国演义"/>
            <constructor-arg name="author" value="罗贯中"/>
        </bean>
        
  2. set 方法注入

    <!--
    这里没有设置构造方法参数,所以默认使用的就是无参构造方法
    -->
    <bean class="com.qfedu.demo.p2.model.Book" id="book3">
        <!--这个配置将来会调用到 setId 这个方法-->
        <property name="id" value="2"/>
        <property name="name" value="红楼梦"/>
        <property name="author" value="曹雪芹"/>
    </bean>
    
  3. p 名称空间注入

    1. p 名称空间注入,本质上其实就是 set 方法注入。

    2. <!--没有指定构造方法,依然是使用默认的构造方法-->
      <bean class="com.qfedu.demo.p2.model.Book" id="book4" p:id="3" p:name="水浒传" p:author="施耐庵"></bean>
      

复杂属性的注入

List、数组、对象、Map、Set、Properties 等。

首先定义 User 类:

public class User {
   
    private Integer id;
    private String name;
    private String address;
    private Cat cat;
    private List<String> favorites;
    private List<Cat> cats;
    private Book[] books;
    private Map<String, Object> info;
    private Properties school;
}

属性注入:

<?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 class="com.qfedu.demo.model.Cat" id="cat">
        <property name="name" value="小白"/>
        <property name="color" value="白色"/>
    </bean>

    <bean class="com.qfedu.demo.model.User" id="user">
        <property name="id" value="1"/>
        <property name="name" value="zhangsan"/>
        <property name="address" value="广州"/>
        <!--
        用 set 方法给 cat 赋值的时候,有两种方式:
        Cat cat = new Cat();
        setCat(cat);

        setCat(new Cat());

        ref 表示这里的值是引用了一个提前定义好的 cat 对象
        -->
        <property name="cat" ref="cat"/>
        <property name="favorites">
            <!--
            list 标签表示数据类型是一个 List
            如果 list 中存放的数据是字符串,那么这里就直接使用 value
            如果 list 中存放的数据库是对象,那么可以使用 ref 去引用外部的 对象,也可以使用 bean 标签现场定义 Bean
            -->
            <list>
                <value>足球</value>
                <value>篮球</value>
            </list>
        </property>
        <property name="cats">
            <list>
                <!--引用外部定义的 cat-->
                <ref bean="cat"/>
                <bean class="com.qfedu.demo.model.Cat">
                    <property name="name" value="小黑"/>
                    <property name="color" value="黑色"/>
                </bean>
            </list>
        </property>
        <property name="books">
            <array>
                <bean class="com.qfedu.demo.model.Book">
                    <property name="id" value="1"/>
                    <property name="name" value="三国演义"/>
                    <property name="author" value="罗贯中"/>
                </bean>
                <bean class="com.qfedu.demo.model.Book">
                    <property name="id" value="2"/>
                    <property name="name" value="红楼梦"/>
                    <property name="author" value="曹雪芹"/>
                </bean>
            </array>
        </property>
        <property name="info">
            <!--
            map 的定义
            -->
            <map>
                <entry key="gender" value=""/>
                <entry key="age" value="99"/>
            </map>
        </property>
        <property name="school">
            <props>
                <prop key="name">广州千锋</prop>
                <prop key="address">广州市白云区</prop>
            </props>
        </property>
    </bean>
</beans>

如果是通过构造方法注入:

<bean class="com.qfedu.demo.model.User" id="user2">
    <constructor-arg name="books">
        <!--这个标签中的内容和 set 方法注入时的一模一样-->
    </constructor-arg>
</bean>

注意属性的名字

内省。

对于框架而言,并不是看对象定义的属性叫什么名字,而是根据对象的 get/set 方法来推断属性名称,无论是 MyBatis、Spring、SpringMVC,所有框架,只要用到反射,都是这样的。所以,定义 get/set 方法的时候,不要写错,另一方面,变量的命名要符合规范。

Java 代码配置 Spring

用一个 Java 配置类,去代替 XML 配置即可:

/**
 * 这个是 Java 配置类,它的作用类似于 applicationContext.xml
 *
 * @Configuration 表示这是一个配置类
 */
@Configuration
public class JavaConfig {
   

    /**
     * @Bean 就表示将当前方法的返回值注册到 Spring 容器中
     *
     * 默认情况下,方法名称就是 bean 的名字
     *
     * 如果需要自定义 bean 的名称,那么在注解中配置即可
     *
     * @return
     */
    @Bean("u")
    User user() {
   
        User user = new User();
        user.setAddress("广州");
        user.setName("zhangsan");
        return user;
    }

}

启动 Spring 容器时,加载这个配置类即可:

public class Demo01 {
   
    public static void main(String[] args) {
   
        //启动 Spring 容器,加载一个 Java 配置类,构造方法中指定配置类即可
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
        User user = ctx.getBean("u", User.class);
        System.out.println("user = " + user);
    }
}

参数自动注入

  • @Configuration:这个注解表示当前类是一个配置类,那么当前类中,所有添加了 @Bean 注解的方法都会被注册到 Spring 容器中,如果有其他方法调用到一个添加了 @Bean 注解的方法,那么不会立马执行对应的方法,而是先去 Spring 容器中查看是否有对应的对象,如果有,则直接从容器中获取即可,如果容器中没有的话,才回去执行对应的方法。

  • @Component 虽然也可以加在配置类上,但是,如果有其他方法调用到一个添加了 @Bean 注解的方法,那么不会先去 Spring 容器中查看是否有对应的对象,而是直接执行对应的方法。所以一般在配置类中不使用 @Component 注解。如果一定要使用 @Component 注解,可以通过依赖注入来代替方法调用,类似下面这样:

    /**
     * 向 Spring 容器注册一个 Author 对象
     *
     * @return
     */
    @Bean
    Author author() {
         
        Author author = new Author();
        author.setName("鲁迅");
        author.setAge(55);
        return author;
    }
    /**
     * 向 Spring 容器中注册一个 Book 对象
     *
     * book 中有一个 author 对象,book 中的 author 和 spring 容器中的 author 是否是同一个对象?
     * @return
     */
    @Bean
    Book book2(Author author) {
         
        Book book = new Book();
        book.setName("故事新编");
        book.setAuthor(author);
        book.setPrice(18.0);
        return book;
    }
    

    在这里,所有的方法都是 Spring 容器调用的,当 Spring 容器调用 book2 这个方法的时候,就会发现这个方法的执行需要一个 Author 类型的参数,那么此时 Spring 容器就会去查找是否有一个 Author,如果有,则直接作为参数传进来,如果 Spring 容器中没有这个对象,那么直接抛出异常。

条件注解

条件注解是多环境配置的核心,思路就是提前准备好环境,所谓的环境,实际上就是 Condition 接口的实现类,然后在注册 Bean 的时候,通过 @Conditional 注解去指定环境,当满足某种条件的时候,bean 才会注入到 Spring 容器中。

两个非常经典的使用场景:

  • 项目中的多环境配置。
  • SpringBoot 中的自动化配置。

多环境切换

提前准备好生产环境、开发环境等的配置信息,将来通过 Profile 可以一键切换。

通过 Java 代码实现

主要是使用 @Profile 注解,这个注解的本质就是 @Conditional 条件注解,用到的条件实际上就是 ProfileCondition。

@Target({
   ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
   

    /**
     * The set of profiles for which the annotated component should be registered.
     */
    String[] value();

}

条件注解:

class ProfileCondition implements Condition {
   

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
   
        //获取 Profile 注解的所有属性,其实这个注解只有一个 value 属性,属性的值是一个数组
        MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
        if (attrs != null) {
   
            //将属性中的 value 读取出来,这个 value 的值实际上是一个 String 数组,遍历 String 数组
            for (Object value : attrs.get("value")) {
   
                //判断当前环境中,有没有 value 中的值
                if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
   
                    return true;
                }
            }
            return false;
        }
        return true;
    }

}

具体配置:

@Configuration
public class JavaConfig {
   
    /**
     * 开发环境的数据源
     *
     * 通过 @Profile("dev") 注解可以指定当前的环境是开发环境
     * @return
     */
    @Profile("dev")
    @Bean("ds")
    DataSource devDataSource() {
   
        DataSource ds = new DataSource();
        ds.setUsername("root");
        ds.setPassword("123");
        ds.setUrl("jdbc:mysql:///test01");
        return ds;
    }

    /**
     * 配置生产环境的数据源
     * @return
     */
    @Profile("prod")
    @Bean("ds")
    DataSource prodDataSource() {
   
        DataSource ds = new DataSource();
        ds.setUsername("zhangsan");
        ds.setPassword("jdfkslajfl890324");
        ds.setUrl("jdbc:mysql://114.132.43.22/prod01");
        return ds;
    }
}

注册 Bean 的时候,通过 @Profile("prod") 注解来指定当前 Bean 在哪个环境下生效。

当启动 Spring 容器的时候,要为 Spring 容器指定当前的环境信息:

public class Demo01 {
   
    public static void main(String[] args) {
   
        //注意先不要写配置类,要先设置环境信息
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        //设置当前的环境信息
        ctx.getEnvironment().addActiveProfile("prod");
        ctx.register(JavaConfig.class);
        ctx.refresh();
        DataSource ds = ctx.getBean(DataSource.class);
        System.out.println("ds = " + ds);
    }
}

通过 XML 配置实现

首先在 xml 文件中,通过 beans 标签来指定环境:

<?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 profile="dev">
        <!--将来写在这个标签中的 Bean,都是在 dev 环境下才会生效的 Bean-->
        <bean class="com.qfedu.demo.p1.model.DataSource" id="dataSource">
            <property name="username" value="root"/>
            <property name="password" value="123"/>
            <property name="url" value="jdbc:mysql:///test01"/>
        </bean>
    </beans>
    <beans profile="prod">
        <!--将来写在这个标签中的 Bean,都是在 prod 环境下才会生效的 Bean-->
        <bean class="com.qfedu.demo.p1.model.DataSource" id="dataSource">
            <property name="username" value="root"/>
            <property name="password" value="jkld3u$%^"/>
            <property name="url" value="jdbc:mysql://11.22.11.22/test01"/>
        </bean>
    </beans>
</beans>

将来 Spring 容器启动的时候,会根据当前的环境信息去注册不同的 beans 标签中的 Bean。

启动容器的时候,设置一下当前环境即可:

public class Demo02 {
   
    public static void main(String[] args) {
   
        //先不要加载配置文件
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext();
        ctx.getEnvironment().addActiveProfile("dev");
        //设置好当前环境之后,再去设置配置文件的位置
        ctx.setConfigLocation("applicationContext.xml");
        ctx.refresh();
        DataSource ds = ctx.getBean(DataSource.class);
        System.out.println("ds = " + ds);
    }
}

配置文件的注入

主要是指 properties 配置文件的注入。

Java 代码配置

主要就是两个配置:

  • @PropertySource:项目启动的时候,将配置文件中的内容注册到 Spring 容器中。
  • @Value:从 Spring 容器要一个字符串回来。
@Configuration
@PropertySource("classpath:db.properties")
public class DsConfig {
   

    //跟 Spring 容器要一个字符串回来
    @Value("${db.username}")
    String username;
    @Value("${db.password}")
    String password;
    @Value("${db.url}")
    String url;

    @Bean
    DataSource dataSource() {
   
        DataSource ds = new DataSource();
        ds.setPassword(password);
        ds.setUsername(username);
        ds.setUrl(url);
        return ds;
    }
}

要能够从 Spring 容器中要到字符串,或者对象,有一个前提,当前 Bean 必须处于 Spring 容器中,不可以自己手动 new 一个 Bean。

XML 配置

<?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:context="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">

    <!--
    这个配置的作用类似于 @PropertySource,就是将配置文件注册到 Spring 容器中
    -->
    <context:property-placeholder location="classpath:db.properties"/>

    <!--
    ${db.url} 表示引用 Spring 容器中,key 为 db.url 的变量
    -->
    <bean class="com.qfedu.demo.p1.model.DataSource" id="dataSource">
        <property name="url" value="${db.url}"/>
        <property name="password" value="${db.password}"/>
        <property name="username" value="${db.username}"/>
    </bean>
</beans>

包扫描

前面的 Bean 的注册方式,都是一个一个 Bean 去注册的。

Java 配置包扫描

官方推荐写法

整体上来说,三个方面:

  1. 将 Bean 注册到 Spring 容器中:@Repository、@Service、@Controller 以及 @Component。

    /**
     * 将 UserDao 注册到 Spring 容器中,将来谁需要使用 UserDao,就直接跟 Spring 容器去查找即可
     *
     *
     * @Repository:一般是加在 Dao 层
     * @Service:一般是加在 Service 层
     * @Controller:一般是加在控制层,也就是 servlet
     * @Component:身份不明的 Bean 注册到 Spring 容器中时,使用这个注解
     *
     * 技术上来说,这四个注解并没有差异,即实际上除了 @Controller 之外,其他几个注解是可以混用的。
     *
     * 实际开发中,不要混用
     *
     */
    @Repository
    public class UserDao {
         
        public User getUserByUsername(String username) {
         
            User user = new User();
            user.setUsername(username);
            user.setAddress("广州");
            return user;
        }
    }
    
  2. 从 Spring 容器中要一个 Bean 回来,官方推荐的方式,是通过构造器来注入。

    /**
     * UserService 想要使用 UserDao
     * 1. UserService 自己需要在 Spring 容器
     * 2. 跟 Spring 容器去要一个 UserDao
     */
    @Service
    public class UserService {
         
        UserDao userDao;
    
        /**
         * 官方推荐使用构造器注入。因为 UserService 要注册到 Spring 容器,要生成一个对象,必然就要调用它的构造方法,现在它只有这一个构造法方法,那么 Spring 容器只能通过这个构造方法来获取一个 UserService 的实例,那么 Spring 容器会自动去查找容器中是否有一个 UserDao 的实例,如果有,就作为这个方法的参数传入进来
         * @param userDao
         */
        public UserService(UserDao userDao) {
         
            this.userDao = userDao;
        }
    
        public User getUserById(String username) {
         
            return userDao.getUserByUsername(username);
        }
    
    }
    

    这里又涉及到一个问题:如果有多个构造器怎么办?@Autowired 注解可解决。

    @Service
    public class UserService {
         
        UserDao userDao;
    
        /**
         * 官方推荐使用构造器注入。因为 UserService 要注册到 Spring 容器,要生成一个对象,必然就要调用它的构造方法,现在它只有这一个构造法方法,那么 Spring 容器只能通过这个构造方法来获取一个 UserService 的实例,那么 Spring 容器会自动去查找容器中是否有一个 UserDao 的实例,如果有,就作为这个方法的参数传入进来
         * 
         * 如果有多个构造方法,那么可以通过 @Autowired 注解来告诉 Spring 容器,应该调用哪一个构造方法去初始化当前对象
         * @param userDao
         */
        @Autowired
        public UserService(UserDao userDao) {
         
            this.userDao = userDao;
        }
    
        public User getUserById(String username) {
         
            return userDao.getUserByUsername(username);
        }
    
    }
    
  3. 配置包扫描。

    @ComponentScan(basePackages = "com.qfedu.demo")
    @Configuration
    public class JavaConfig {
         
    }
    

    通过 @ComponentScan 注解指定组件的路径即可。

日常使用较多的写法

和前面的步骤相比,主要是第二步不一样,其他都是一样的。

@Controller
public class UserServlet {
   
    /**
     * @Autowired 表示根据类型去 Spring 容器中查找到相应的 Bean,并赋值给 userService 变量
     */
    @Autowired
    UserService userService;

    public User getUserById(String username) {
   
        return userService.getUserById(username);
    }
}

直接在属性上添加 @Autowired 注解就可以完成属性的注入了。

这种写法是我们在实际开发中使用较多的一种方式。

构造方法注入:

  • 官方考虑的是,这个类将来使用的时候,不一定就是跟 Spring 容器去要的,可能就是用户自己 new 出来的,例如用户如果直接 new 一个 UserServlet,那么此时 userService 变量就为 null,并且用户没有办法给 userService 赋值,导致 userService 为 null。但是如果用户是 new 一个 UserService 实例的话,那么就必须传递一个 UserDao 的实例。核心思想就是:这个 Bean 既可以跟 Spring 去要,也可以直接 new。
  • 大部分情况下,一个类的属性都特别多,导致类的构造方法特别长,所以这种方式在实际项目中用的少。

注解注入:

  • 简单便捷。
  • 如果用户使用不规范,自己 new 了一个该对象,可能会导致对象中的很多属性没有赋值,为 null。

XML 配置包扫描

XML 配置包扫描跟 Java 配置包扫描,区别在于第三步。即包扫描用 XML 文件来配置:

<?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:context="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">

    <context:component-scan base-package="com.qfedu.demo"/>
</beans>

工厂 Bean

这个主要是解决一些第三方的 Bean,一些无法通过构造方法正常初始化、或者无法通过 set 方法正常为属性的赋值的 Bean,可以通过工厂 Bean 的方式将之注册到 Spring 容器中。

静态工厂

工厂方法是一个静态方法。

public class OkHttpClientFactory {
   
    private static OkHttpClient okHttpClient;
    public static OkHttpClient getInstance() {
   
        if (okHttpClient == null) {
   
            okHttpClient = new OkHttpClient.Builder()
                    //设置服务端的读取超时时间
                    .readTimeout(5000, TimeUnit.SECONDS)
                    //连接超时
                    .connectTimeout(5000, TimeUnit.SECONDS)
                    .build();
        }
        return okHttpClient;
    }
}

在 XML 文件中,直接配置工厂 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,将来会自动调用 getInstance 方法,并将该方法的返回值注入到 Spring 容器中
    -->
    <bean class="com.qfedu.demo.OkHttpClientFactory" factory-method="getInstance" id="client01"/>
</beans>

需要注意的是,这里将来注册到 Spring 容器中的 Bean,不是 class 对应的类的对象,而是 getInstance 方法返回的对象。

实例工厂

工厂方法变为实例方法,执行的时候,必须要现有工厂对象,然后才能调用对应的方法。

public class OkHttpClientFactory2 {
   
    private OkHttpClient okHttpClient;
    public OkHttpClient getInstance() {
   
        if (okHttpClient == null) {
   
            okHttpClient = new OkHttpClient.Builder()
                    //设置服务端的读取超时时间
                    .readTimeout(5000, TimeUnit.SECONDS)
                    //连接超时
                    .connectTimeout(5000, TimeUnit.SECONDS)
                    .build();
        }
        return okHttpClient;
    }
}

Bean 的注册:

<!--
对于实例工厂方法,必须得先有一个工厂的实例,将来要通过这个实例才能调用工厂方法
-->
<bean class="com.qfedu.demo.OkHttpClientFactory2" id="clientFactory2"/>
<!--此时配置工厂的时候,既需要指定工厂的 Bean,也需要指定工厂方法-->
<bean class="okhttp3.OkHttpClient" factory-bean="clientFactory2" factory-method="getInstance" id="client02"/>

FactoryBean

这是 Spring 官方推荐的工厂 Bean 的实现方式。

public class OkHttpClientFactoryBean implements FactoryBean<OkHttpClient> {
   
    /**
     * 返回真正的对象
     * @return
     * @throws Exception
     */
    @Override
    public OkHttpClient getObject() throws Exception {
   
        return new OkHttpClient.Builder()
                //设置服务端的读取超时时间
                .readTimeout(5000, TimeUnit.SECONDS)
                //连接超时
                .connectTimeout(5000, TimeUnit.SECONDS)
                .build();
    }

    /**
     * 返回对象的类型
     * @return
     */
    @Override
    public Class<?> getObjectType() {
   
        return OkHttpClient.class;
    }

    /**
     * 这个对象是否是单例的
     * @return
     */
    @Override
    public boolean isSingleton() {
   
        return true;
    }
}

然后在 XML 文件中注册即可:

<bean class="com.qfedu.demo.OkHttpClientFactoryBean" id="client03"/>

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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean class="com.qfedu.demo.User" id="user"/>
</beans>

如果不希望这个 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 class="com.qfedu.demo.User" id="user" scope="prototype"/>
</beans>

scope 有五种取值:

  • singleton:表示这个 bean 是单例的,从 Spring 容器中多次获取,拿到的是同一个 Bean。
  • prototype:每一次去 Spring 容器中,都可以获取到一个全新的 Bean。
  • request:在同一个请求中,如果多次获取同一个 Bean,获取到的是同一个(web 环境下生效)
  • session:在同一个 session 中,如果多次获取同一个 Bean,获取到的是同一个(web 环境下生效)
  • application:在同一个 web 应用中,如果多次获取同一个 Bean,获取到的是同一个(web 环境下生效)(web 环境下生效)

也可以通过 Java 代码配置 scope,通过 @Scope 注解,可以指定一个 Bean 的作用域:

@Configuration
public class JavaConfig {
   

    @Bean
    @Scope("prototype")
    User user() {
   
        return new User();
    }
}

Bean 的初始化和销毁

主要是两个方法:

public class User {
   
    private String username;
    private String address;

    public User() {
   
        System.out.println("构造方法。。。");
    }

    /**
     * 假设这个方法用来初始化当前 Bean
     */
    public void init() {
   
        System.out.println("初始化方法。。。");
    }

    /**
     * 当当前 Bean 销毁的时候,可以在当前方法中做一些资源回收操作
     */
    public void destroy() {
   
        System.out.println("销毁方法。。。");
    }
}

如果在 Spring 容器完成注册之后,还想要做一些配置,那么可以在 init 方法中完成,当 Spring 容器销毁的时候,可以在 destroy 方法中完成一些资源回收操作。

<bean class="com.qfedu.demo.User" init-method="init" destroy-method="destroy" id="user"/>

IoC 小结

控制反转,指的是对象的控制权反转。本来,UserService 需要一个 UserDao,那么直接在 UserSerivce 中 new 一个 UserDao,那么此时 UserDao 的对象的控制权就在 UserService 中;有了 Spring 之后,系统启动的时候,UserDao 会将自己的各种信息告诉 Spring 容器,Spring 利用这些信息,就可以结合反射创建一个 UserDao 对象,此时 UserDao 对象处于 Spring 容器中,这个对象的控制权在 Spring 容器中,对象的控制权从 UserService 中转移到了 Spring 容器中,就是控制(权)反正。利用 IoC 可以实现对象之间的解耦。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
3月前
|
XML Java 数据格式
Spring5入门到实战------7、IOC容器-Bean管理XML方式(外部属性文件)
这篇文章是Spring5框架的实战教程,主要介绍了如何在Spring的IOC容器中通过XML配置方式使用外部属性文件来管理Bean,特别是数据库连接池的配置。文章详细讲解了创建属性文件、引入属性文件到Spring配置、以及如何使用属性占位符来引用属性文件中的值。
Spring5入门到实战------7、IOC容器-Bean管理XML方式(外部属性文件)
|
16天前
|
XML 缓存 Java
搞透 IOC、Spring IOC ,看这篇就够了!
本文详细解析了Spring框架的核心内容——IOC(控制反转)及其依赖注入(DI)的实现原理,帮助读者理解如何通过IOC实现组件解耦,提高程序的灵活性和可维护性。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
|
2月前
|
XML Java 数据格式
Spring IOC—基于XML配置Bean的更多内容和细节(通俗易懂)
Spring 第二节内容补充 关于Bean配置的更多内容和细节 万字详解!
206 18
Spring IOC—基于XML配置Bean的更多内容和细节(通俗易懂)
|
8天前
|
安全 Java 测试技术
Java开发必读,谈谈对Spring IOC与AOP的理解
Spring的IOC和AOP机制通过依赖注入和横切关注点的分离,大大提高了代码的模块化和可维护性。IOC使得对象的创建和管理变得灵活可控,降低了对象之间的耦合度;AOP则通过动态代理机制实现了横切关注点的集中管理,减少了重复代码。理解和掌握这两个核心概念,是高效使用Spring框架的关键。希望本文对你深入理解Spring的IOC和AOP有所帮助。
14 0
|
2月前
|
XML Java 测试技术
spring复习01,IOC的思想和第一个spring程序helloWorld
Spring框架中IOC(控制反转)的思想和实现,通过一个简单的例子展示了如何通过IOC容器管理对象依赖,从而提高代码的灵活性和可维护性。
spring复习01,IOC的思想和第一个spring程序helloWorld
|
1月前
|
Java Spring 容器
Spring IOC、AOP与事务管理底层原理及源码解析
【10月更文挑战第1天】Spring框架以其强大的控制反转(IOC)和面向切面编程(AOP)功能,成为Java企业级开发中的首选框架。本文将深入探讨Spring IOC和AOP的底层原理,并通过源码解析来揭示其实现机制。同时,我们还将探讨Spring事务管理的核心原理,并给出相应的源码示例。
123 9
|
1月前
|
存储 开发框架 Java
什么是Spring?什么是IOC?什么是DI?IOC和DI的关系? —— 零基础可无压力学习,带源码
文章详细介绍了Spring、IOC、DI的概念和关系,解释了控制反转(IOC)和依赖注入(DI)的原理,并提供了IOC的代码示例,阐述了Spring框架作为IOC容器的应用。
26 0
什么是Spring?什么是IOC?什么是DI?IOC和DI的关系? —— 零基础可无压力学习,带源码
|
2月前
|
缓存 Java Spring
手写Spring Ioc 循环依赖底层源码剖析
在Spring框架中,IoC(控制反转)是一个核心特性,它通过依赖注入(DI)实现了对象间的解耦。然而,在实际开发中,循环依赖是一个常见的问题。
40 4
|
28天前
|
XML Java 数据格式
Spring IOC容器的深度解析及实战应用
【10月更文挑战第14天】在软件工程中,随着系统规模的扩大,对象间的依赖关系变得越来越复杂,这导致了系统的高耦合度,增加了开发和维护的难度。为解决这一问题,Michael Mattson在1996年提出了IOC(Inversion of Control,控制反转)理论,旨在降低对象间的耦合度,提高系统的灵活性和可维护性。Spring框架正是基于这一理论,通过IOC容器实现了对象间的依赖注入和生命周期管理。
62 0
|
2月前
|
XML Java 开发者
经典面试---spring IOC容器的核心实现原理
作为一名拥有十年研发经验的工程师,对Spring框架尤其是其IOC(Inversion of Control,控制反转)容器的核心实现原理有着深入的理解。
116 3