【JavaEE】深入理解Spring中的IoC与DI:从依赖注入到Bean管理的详解

目录


DI介绍

学习了IoC后, 什么是DI呢?

IoC是一种思想,DI是一种实现方式

DI: Dependency Injection(依赖注⼊)

容器在运⾏期间, 动态的为应⽤程序提供运⾏时所依赖的资源,称之为依赖注⼊

程序运⾏时需要某个资源,此时容器就为其提供这个资源.

从这点来看, 依赖注⼊(DI)和控制反转(IoC)是从不同的⻆度的描述的同⼀件事情,就是指通过引⼊ IoC 容器,利⽤依赖关系注⼊的⽅式,实现对象之间的解耦。

上述代码中, 是通过构造函数的⽅式, 把依赖对象注⼊到需要使⽤的对象中

IoC 是⼀种思想,也是"⽬标", ⽽思想只是⼀种指导原则,最终还是要有可⾏的落地⽅案,⽽ DI 就属于具体的实现。所以也可以说, DI 是IoC的⼀种实现.

⽐如说我今天⼼情⽐较好,吃⼀顿好的犒劳犒劳⾃⼰,那么"吃⼀顿好的"是思想和⽬标(是IoC),但最后我是吃海底捞还是杨国福?这就是具体的实现,就是 DI。

IoC & DI 使⽤

对IoC和DI有了初步的了解, 我们接下来具体学习Spring IoC和DI的代码实现.

依然是先使⽤, 再学习

既然 Spring 是⼀个 IoC(控制反转)容器,作为容器, 那么它就具备两个最基础的功能

Spring 容器 管理的主要是对象, 这些对象, 我们称之为"Bean". 我们把这些对象交由Spring管理, 由Spring来负责对象的创建和销毁. 我们程序只需要告诉Spring, 哪些需要存, 以及如何从Spring中取出对象

⽬标: 把BookDao, BookService 交给Spring管理, 完成Controller层, Service层, Dao层的解耦

步骤:

  1. Service层及Dao层的实现类,交给Spring管理: 使⽤注解: @Component
  2. 在Controller层和Service层注⼊运⾏时依赖的对象: 使⽤注解 @Autowired

实现:

  1. 把 BookDao 交给Spring管理, 由Spring来管理对象
java 复制代码
@Component
public class BookDao {
    //mock - 虚拟数据,假数据
    public List<BookInfo> mockData() {
        //对于已知的数据量或者大概知道这个集合的数量时,创建List时建议指定初始化容量
        List<BookInfo> bookInfos=new ArrayList<>(15);
        for(int i=0;i<15;i++){
            BookInfo bookInfo=new BookInfo();
            bookInfo.setId(i);
            bookInfo.setBookName("图书"+i);
            bookInfo.setAuthor("作者"+i);
            bookInfo.setCount(new Random().nextInt(200));
            bookInfo.setPrice(new BigDecimal(new Random().nextInt(100)));
            bookInfo.setPublish("出版社"+i);
            bookInfo.setStatus(i%5==0?2:1);
            bookInfos.add(bookInfo);
        }
        return bookInfos;
    }
}
  1. 把 BookService 交给Spring管理, 由Spring来管理对象
java 复制代码
//通过注解告诉Spring帮我们把BookService存入容器中
@Component
public class BookService {
    private BookDao bookDao = new BookDao();
    public List<BookInfo> getBookList(){
        //1.获取图书数据
        List<BookInfo> bookInfos=bookDao.mockData();
        //2.对图书数据进行修改处理
        for(BookInfo bookInfo:bookInfos){
            if(bookInfo.getStatus()==1){
                bookInfo.setStatusCN("可借阅");
            }else{
                bookInfo.setStatusCN("不可借阅");
            }
        }
        return bookInfos;
    }
}
  1. 删除创建BookDao的代码, 从Spring中获取对象
java 复制代码
//通过注解告诉Spring帮我们把BookService存入容器中
@Component
public class BookService {
    @Autowired
    private BookDao bookDao;
    public List<BookInfo> getBookList(){
        //1.获取图书数据
        List<BookInfo> bookInfos=bookDao.mockData();
        //2.对图书数据进行修改处理
        for(BookInfo bookInfo:bookInfos){
            if(bookInfo.getStatus()==1){
                bookInfo.setStatusCN("可借阅");
            }else{
                bookInfo.setStatusCN("不可借阅");
            }
        }
        return bookInfos;
    }
}
  1. 删除创建BookService的代码, 从Spring中获取对象
java 复制代码
@RestController
@RequestMapping("/book")
public class BookController {
    @Autowired//告诉Spring,从容器中取出这个对象,赋值给当前对象的属性
    private BookService bookService;
    //等价于
    //private BookService bookService
    //public BookController() {
    //    this.bookService = new BookService();
    //}

    @RequestMapping("/getBookList")
    public List<BookInfo> getBookList(){
        List<BookInfo> bookInfos = bookService.getBookList();
        //3.返回数据
        return bookInfos;
    }
}
  1. 重新运⾏程序, http://127.0.0.1:8080/book_list.html

结果是一样的

@RestController的源码

java 复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
 @AliasFor(
     annotation = Controller.class
 )
 String value() default "";
}

再看@Controller的源码

java 复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
 @AliasFor(
     annotation = Component.class
 )
 String value() default "";
}

可以看到有@Component

IoC详解

通过上⾯的案例, 我们已经知道了Spring IoC 和DI的基本操作, 接下来我们来系统的学习Spring IoC和DI的操作.

前⾯我们提到IoC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对象。

也就是bean的存储.

Spring是一个容器,存的是对象,对象这个词,在Spring的范围内,称之为bean

Bean的存储

在之前的⼊⻔案例中,要把某个对象交给IOC容器管理,需要在类上添加⼀个注解 @Component

⽽Spring框架为了更好的服务web应⽤程序, 提供了更丰富的注解.

共有两类注解类型可以实现:

  1. 类注解@Controller、@Service、@Repository、@Component、@Configuration

    这五个注解只能加在类上,并且只能加在自己的代码上。如果引入第三方的jar包,也想交给Spring管理,是没有办法加这五个注解的。对于一个类,定义多个对象时,比如数据库操作,定义多个数据源,如果在这五个注解下取对象,取多少次都是同一个对象

  2. ⽅法注解@Bean

接下来我们分别来看

@Controller(控制器存储)

控制层用这个

使⽤ @Controller 存储 bean 的代码如下所⽰:

java 复制代码
@Controller//将对象存储到 Spring 中
public class UserController {
    public void doController(){
        System.out.println("doController...");
    }
}

如何观察这个对象已经存在Spring容器当中了呢?

接下来我们学习如何从Spring容器中获取对象

java 复制代码
@SpringBootApplication
public class DemoApplication {

   public static void main(String[] args) {
      //Spring上下文,返回的就是Spring的运行环境
      ApplicationContext context=SpringApplication.run(DemoApplication.class, args);
      //从Spring上下⽂中获取对象
      UserController bean = context.getBean(UserController.class);
      //使用对象
      bean.doController();
   }

}

ApplicationContext 翻译过来就是: Spring 上下⽂因为对象都交给 Spring 管理了,所以获取对象要从 Spring 中获取,那么就得先得到 Spring 的上下⽂

关于上下⽂的概念

上学时, 阅读理解经常会这样问: 根据上下⽂, 说⼀下你对XX的理解

在计算机领域, 上下⽂这个概念, 在学习线程时了解到过, ⽐如我们应⽤进⾏线程切换的时候,切换前都会把线程的状态信息暂时储存起来,这⾥的上下⽂就包括了当前线程的信息,等下次该线程⼜得到CPU时间的时候, 从上下⽂中拿到线程上次运⾏的信息

这个上下⽂, 就是指当前的运⾏环境, 也可以看作是⼀个容器, 容器⾥存了很多内容, 这些内容是当前运⾏的环境

观察运⾏结果, 发现成功从Spring中获取到Controller对象, 并执⾏Controller的doController⽅法

如果把@Controller删掉, 再观察运⾏结果

报错信息显⽰: 找不到类型是: com.example.demo.controller.UserController 的 bean

获取bean对象的其他⽅式

上述代码是根据类型来查找对象, 如果Spring容器中, 同⼀个类型存在多个bean的话, 怎么来获取呢?

ApplicationContext 也提供了其他获取bean的⽅式, ApplicationContext 获取bean对象的功能, 是⽗类 BeanFactory 提供的功能.

java 复制代码
public interface BeanFactory {

    //以上省略...

    // 1. 根据bean名称获取bean
    Object getBean(String var1) throws BeansException;

    // 2. 根据bean名称和类型获取bean
    <T> T getBean(String var1, Class<T> var2) throws BeansException;

    // 3. 按bean名称和构造函数参数动态创建bean,只适⽤于具有原型(prototype)作⽤域的bean
    Object getBean(String var1, Object... var2) throws BeansException;

    // 4. 根据类型获取bean
    <T> T getBean(Class<T> var1) throws BeansException;

    // 5. 按bean类型和构造函数参数动态创建bean, 只适⽤于具有原型(prototype)作⽤域的bean
    <T> T getBean(Class<T> var1, Object... var2) throws BeansException;

    //以下省略...
}

常⽤的是上述1,2,4种, 这三种⽅式,获取到的bean是⼀样的

其中1,2种都涉及到根据名称来获取对象. bean的名称是什么呢?

Spring bean是Spring框架在运⾏时管理的对象, Spring会给管理的对象起⼀个名字.

⽐如学校管理学⽣, 会给每个学⽣分配⼀个学号, 根据学号, 就可以找到对应的学⽣.

Spring也是如此, 给每个对象起⼀个名字, 根据Bean的名称(BeanId)就可以获取到对应的对象.

Bean 命名约定

程序开发⼈员不需要为bean指定名称(BeanId), 如果没有显式的提供名称(BeanId),Spring容器将为该bean⽣成唯⼀的名称.

命名约定使⽤Java标准约定作为实例字段名. 也就是说,bean名称以⼩写字⺟开头,然后使⽤驼峰式⼤⼩写.

⽐如

类名: UserController, Bean的名称为: userController

类名: AccountManager, Bean的名称为: accountManager

类名: AccountService, Bean的名称为: accountService

也有⼀些特殊情况, 当有多个字符并且第⼀个和第⼆个字符都是⼤写时, 将保留原始的⼤⼩写. 这些规则与java.beans.Introspector.decapitalize (Spring在这⾥使⽤的)定义的规则相同.

⽐如

类名: UController, Bean的名称为: UController

类名: AManager, Bean的名称为: AManager
获取bean对象, 是⽗类BeanFactory提供的功能

ApplicationContext VS BeanFactory(常⻅⾯试题)

  • 继承关系和功能⽅⾯来说:Spring 容器有两个顶级的接⼝ :BeanFactory 和ApplicationContext。其中 BeanFactory 提供了基础的访问容器的能⼒,⽽ApplicationContext 属于 BeanFactory 的⼦类,它除了继承了 BeanFactory 的所有功能之外,它还拥有独特的特性,还添加了对国际化⽀持资源访问⽀持 、以及事件传播等⽅⾯的⽀持.

  • 从性能⽅⾯来说:ApplicationContext 是**⼀次性加载并初始化所有的 Bean 对象** ,⽽BeanFactory需要那个才去加载那个,因此更加轻量. (空间换时间)

@Service(服务存储)

业务逻辑层用这个

使⽤ @Service 存储 bean 的代码如下所⽰:

java 复制代码
@Service
public class UserService {
    public void doService(){
        System.out.println("doService...");
    }
}

读取 bean 的代码:

java 复制代码
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        //Spring上下文,返回的就是Spring的运行环境
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        //从Spring上下⽂中获取对象
        UserController bean = context.getBean(UserController.class);
        //使用对象
        bean.doController();

        UserService userService = context.getBean(UserService.class);
        userService.doService();
    }

}

观察运⾏结果, 发现成功从Spring中获取到UserService对象, 并执⾏UserService的doService⽅法

同样的, 把注解@Service删掉, 再观察运⾏结果,也是会有一样的异常找不到bean

@Repository(仓库存储)

数据访问层用这个

使⽤ @Repository 存储 bean 的代码如下所⽰:

java 复制代码
@Repository
public class UserRepository {
    public void doRepository(){
        System.out.println("doRepository...");
    }
}

读取 bean 的代码:

java 复制代码
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        //Spring上下文,返回的就是Spring的运行环境
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        //从Spring上下⽂中获取对象
        UserController bean = context.getBean(UserController.class);
        //使用对象
        bean.doController();

        UserService userService = context.getBean(UserService.class);
        userService.doService();
        //根据名称获取bean
        UserService userService2 = (UserService)context.getBean("userService");
        userService2.doService();
        //根据名称和类型获取bean
        UserService userService3 = context.getBean("userService", UserService.class);
        userService3.doService();

        UserRepository userRepository = context.getBean(UserRepository.class);
        userRepository.doRepository();
    }

}

观察运⾏结果, 发现成功从Spring中获取到UserRepository 对象, 并执⾏UserRepository 的doRepository⽅法

同样的, 把注解@Repository删掉, 再观察运⾏结果,也是会有一样的异常找不到bean

@Component(组件存储)

用于除了控制层、业务逻辑层、数据访问层的代码

使⽤ @Component 存储 bean 的代码如下所⽰:

java 复制代码
@Component
public class UserComponent {
    public void doComponent(){
        System.out.println("doComponent...");
    }
}

读取 bean 的代码:

java 复制代码
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        //Spring上下文,返回的就是Spring的运行环境
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        //从Spring上下⽂中获取对象
        UserController bean = context.getBean(UserController.class);
        //使用对象
        bean.doController();

        UserService userService = context.getBean(UserService.class);
        userService.doService();
        //根据名称获取bean
        UserService userService2 = (UserService)context.getBean("userService");
        userService2.doService();
        //根据名称和类型获取bean
        UserService userService3 = context.getBean("userService", UserService.class);
        userService3.doService();

        UserRepository userRepository = context.getBean(UserRepository.class);
        userRepository.doRepository();

        UserComponent userComponent=context.getBean(UserComponent.class);
        userComponent.doComponent();
    }

}

观察运⾏结果, 发现成功从Spring中获取到UserComponent 对象, 并执⾏UserComponent 的doComponent⽅法

同样的, 把注解@Component删掉, 再观察运⾏结果,也是会有一样的异常找不到bean

@Configuration(配置存储)

用于大型项目一些Spring以外的配置

使⽤ @Configuration 存储 bean 的代码如下所⽰:

java 复制代码
@Configuration
public class UserConfig {
    public void doConfig(){
        System.out.println("doConfig...");
    }
}

读取 bean 的代码:

java 复制代码
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        //Spring上下文,返回的就是Spring的运行环境
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        //从Spring上下⽂中获取对象
        UserController bean = context.getBean(UserController.class);
        //使用对象
        bean.doController();

        UserService userService = context.getBean(UserService.class);
        userService.doService();
        //根据名称获取bean
        UserService userService2 = (UserService)context.getBean("userService");
        userService2.doService();
        //根据名称和类型获取bean
        UserService userService3 = context.getBean("userService", UserService.class);
        userService3.doService();

        UserRepository userRepository = context.getBean(UserRepository.class);
        userRepository.doRepository();

        UserComponent userComponent=context.getBean(UserComponent.class);
        userComponent.doComponent();

        UserConfig userConfig=context.getBean(UserConfig.class);
        userConfig.doConfig();
    }

}

观察运⾏结果, 发现成功从Spring中获取到UserConfiguration 对象, 并执⾏UserConfiguration 的doConfig⽅法

同样的, 把注解@Configuration删掉, 再观察运⾏结果,也是会有一样的异常找不到bean

为什么要这么多类注解?

这个也是和咱们前⾯讲的应⽤分层是呼应的. 让程序员看到类注解之后,就能直接了解当前类的⽤途.

  • @Controller控制层, 接收请求, 对请求进⾏处理, 并进⾏响应.
  • @ServiCE业务逻辑层, 处理具体的业务逻辑.
  • @Repository数据访问层,也称为持久层. 负责数据访问操作
  • @Configuration配置层. 处理项⽬中的⼀些配置信息.

这和每个省/市都有⾃⼰的⻋牌号是⼀样的.

⻋牌号都是唯⼀的, 标识⼀个⻋辆的. 但是为什么还需要设置不同的⻋牌开头呢.

⽐如陕西的⻋牌号就是:陕X:XXXXXX,北京的⻋牌号:京X:XXXXXX,甚⾄⼀个省不同的县区也是不同的,⽐如西安就是,陕A:XXXXX,咸阳:陕B:XXXXXX,宝鸡,陕C:XXXXXX,⼀样.

这样做的好处除了可以节约号码之外,更重要的作⽤是可以直观的标识⼀辆⻋的归属地

程序的应⽤分层,调⽤流程如下:

类注解之间的关系

查看 @Controller / @Service / @Repository / @Configuration 等注解的源码发现:

其实这些注解⾥⾯都有⼀个注解@Component,说明它们本⾝就是属于 @Component 的"⼦类".

@Component 是⼀个元注解,也就是说可以注解其他类注解,如 @Controller , @Service ,@Repository 等. 这些注解被称为 @Component 的衍⽣注解.

Controller是被赋予其他功能的,想被外界访问到,只能用这个Controller,想要接受请求就只能用这个Controller,也就是说这个Controller必须作为程序的第一关

基本大家程序入口都是Controller

@Controller , @Service@Repository ⽤于更具体的⽤例(分别在控制层, 业务逻辑层, 持久化层), 在开发过程中, 如果你要在业务逻辑层使⽤ @Component@Service,显然@Service是更好的选择

⽐如杯⼦有喝⽔杯, 刷⽛杯等, 但是我们更倾向于在⽇常喝⽔时使⽤⽔杯, 洗漱时使⽤刷⽛杯.

更多资料参考:

https://docs.spring.io/spring-framework/reference/core/beans/classpathscanning.html#beans-stereotype-annotations

RequestMapping() 是SpringMVC的一个注解

⽅法注解 @Bean

类注解是添加到某个类上的,但是存在两个问题:

  1. 使⽤外部包⾥的类, 没办法添加类注解
  2. ⼀个类, 需要多个对象, ⽐如多个数据源

这种场景, 我们就需要使⽤⽅法注解 @Bean

我们先来看看⽅法注解如何使⽤:

java 复制代码
@Data
public class UserInfo {
    private Integer id;
    private String name;
    private Integer age;
}
java 复制代码
public class BeanConfig {
    @Bean
    public UserInfo userInfo(){
        UserInfo userInfo=new UserInfo();
        userInfo.setId(1);
        userInfo.setName("zhangsan");
        userInfo.setAge(12);
        return userInfo;
    }
}

然⽽,当我们写完以上代码,尝试获取 bean 对象中的 user 时却发现,根本获取不到:

java 复制代码
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        //Spring上下文,返回的就是Spring的运行环境
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        //@Bean演示
        UserInfo userInfo=(UserInfo) context.getBean(UserInfo.class);
        System.out.println(userInfo);
    }

}

以上程序的执⾏结果如下:

这是为什么呢?

⽅法注解要配合类注解使⽤ & 定义多个对象

在 Spring 框架的设计中,⽅法注解 @Bean 要配合类注解才能将对象正常的存储到 Spring 容器中

对于同⼀个类, 如何定义多个对象呢?

⽐如多数据源的场景, 类是同⼀个, 但是配置不同, 指向不同的数据源.

如下代码所⽰:

java 复制代码
@Configuration
public class BeanConfig {
    @Bean
    public UserInfo userInfo(){
        UserInfo userInfo=new UserInfo();
        userInfo.setId(1);
        userInfo.setName("zhangsan");
        userInfo.setAge(12);
        return userInfo;
    }
    @Bean
    public UserInfo userInfo2(){
        UserInfo userInfo=new UserInfo();
        userInfo.setId(2);
        userInfo.setName("lisi");
        userInfo.setAge(13);
        return userInfo;
    }
}

定义了多个对象的话, 我们根据类型获取对象, 获取的是哪个对象呢?

java 复制代码
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        //Spring上下文,返回的就是Spring的运行环境
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        //@Bean演示
        UserInfo userInfo=(UserInfo) context.getBean(UserInfo.class);
        System.out.println(userInfo);
    }

}

运⾏结果:

报错信息显⽰: 期望只有⼀个匹配, 结果发现了两个, userInfo,userInfo2

从报错信息中, 可以看出来, @Bean 注解的bean, bean的名称就是它的⽅法名

一个类型,存在多个bean时,我们就不能使用类型(.class)来获取对象了

接下来我们根据名称(也就是方法名)来获取bean对象

java 复制代码
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        //Spring上下文,返回的就是Spring的运行环境
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        //@Bean演示
        UserInfo userInfo=(UserInfo) context.getBean("userInfo");
        System.out.println(userInfo);
        UserInfo userInfo2=(UserInfo) context.getBean("userInfo2");
        System.out.println(userInfo2);
    }

}

运⾏结果:

可以看到, @Bean 可以针对同⼀个类, 定义多个对象。

java 复制代码
UserInfo userInfo=context.getBean("userInfo",UserInfo.class);
UserInfo userInfo2=context.getBean("userInfo2",UserInfo.class);

也可以通过这种方式获取对象,这样就可以不用(UserInfo)强转了

java 复制代码
@Configuration
public class BeanConfig {
 @Bean
 public String name(){
    return "zhangsan"; 
 }
 @Bean
 public UserInfo userInfo(String name){
     UserInfo userInfo=new UserInfo();
     userInfo.setId(1);
     userInfo.setName(name);
     userInfo.setAge(12);
     return userInfo;
 }
}

也可以通过这样传参,注意一定要给name加个@Bean,因为它要求传入的参数是一个bean对象,所以这个name也要加个@Bean变为一个bean对象,对象名称就是name。含义是:定义了一个名叫name的String类型的bean对象

重命名 Bean

可以通过设置 name 属性给 Bean 对象进⾏重命名操作,如下代码所⽰:这两个都是它的别名

java 复制代码
@Bean(name = {"u1","user1"})
public User user1(){
 	User user = new User();
 	user.setName("zhangsan");
 	user.setAge(18);
 	return user;
}

此时我们使⽤ u1 就可以获取到 User 对象了,如下代码所⽰:

java 复制代码
 @SpringBootApplication
 public class SpringIocDemoApplication {

 	public static void main(String[] args) {
 		//获取Spring上下⽂对象
 		ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
 		//从Spring上下⽂中获取对象
 		User u1 = (User) context.getBean("u1");
 		//使⽤对象
 		System.out.println(u1);
 	}
 }

name={} 可以省略,如下代码所⽰:

java 复制代码
@Bean({"u1","user1"})
public User user1(){
	User user = new User();
	user.setName("zhangsan");
	user.setAge(18);
	return user;
}

只有⼀个名称时, {}也可以省略, 如:

java 复制代码
@Bean("u1")
public User user1(){
	User user = new User();
	user.setName("zhangsan");
	user.setAge(18);
	return user;
}
扫描路径

Q: 使⽤前⾯学习的四个注解声明的bean,⼀定会⽣效吗?

A: 不⼀定(原因:bean想要⽣效,还需要被Spring扫描)

下⾯我们通过修改项⽬⼯程的⽬录结构,来测试bean对象是否⽣效:

再运⾏代码:

java 复制代码
 @SpringBootApplication
 public class SpringIocDemoApplication {

 	public static void main(String[] args) {
 		//获取Spring上下⽂对象
 		ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
 		//从Spring上下⽂中获取对象
 		User u1 = (User) context.getBean("u1");
 		//使⽤对象
 		System.out.println(u1);
 	}
                                                           
 }

运⾏结果:

解释: 没有bean的名称为u1

为什么没有找到bean对象呢?

使⽤五⼤注解声明的bean,要想⽣效, 还需要配置扫描路径, 让Spring扫描到这些注解

也就是通过 @ComponentScan 来配置扫描路径.

java 复制代码
 @ComponentScan({"com.example.demo"})
 @SpringBootApplication
 public class SpringIocDemoApplication {

     public static void main(String[] args) {
 		//获取Spring上下⽂对象
 		ApplicationContext context = SpringApplication.run(SpringIocDemoApplicatio
 		//从Spring上下⽂中获取对象
 		User u1 = (User) context.getBean("u1");
 		//使⽤对象
		System.out.println(u1);
 	}
 }

{} ⾥可以配置多个包路径

这种做法仅做了解, 不做推荐使⽤

那为什么前⾯没有配置 @ComponentScan 注解也可以呢?

@ComponentScan 注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解@SpringBootApplication 中了

SpringBoot特点:约定大于配置

其中之一体现:扫描路径

约定:大学生的课表就是一种约定

配置:老师逐个通知每节课上什么,在哪上

如果约定好了就不需要配置这么麻烦了

默认的扫描路径:启动类所在的目录及其子孙目录(这句话和下面这句是一样的)

默认扫描的范围是SpringBoot启动类所在包及其⼦包

在配置类上添加 @ComponentScan 注解, 该注解默认会扫描该类所在的包下所有的配置类

推荐做法:

把启动类放在我们希望扫描的包的路径下, 这样我们定义的bean就都可以被扫描到

这样扫描路径就是com.example.demo,里面这几个包就都可以扫到了

DI详解

上⾯讲了控制反转IoC的细节,接下来我们学习依赖注⼊DI的细节。

依赖注⼊是⼀个过程,是指IoC容器在创建Bean时, 去提供运⾏时所依赖的资源,⽽资源指的就是对象.

在上⾯程序案例中,我们使⽤了 @Autowired 这个注解,完成了依赖注⼊的操作.

简单来说, 就是把对象取出来放到某个类的属性中.

在⼀些⽂章中, 依赖注⼊也被称之为 "对象注⼊", "属性装配", 具体含义需要结合⽂章的上下⽂来理解

关于依赖注⼊, Spring也给我们提供了三种⽅式:

  1. 属性注⼊(Field Injection)
  2. 构造⽅法注⼊(Constructor Injection)
  3. Setter 注⼊(Setter Injection)

接下来,我们分别来看。

下⾯我们按照实际开发中的模式,将 Service 类注⼊到 Controller 类中。

1. 属性注⼊

属性注入是以类型进行匹配,与注入的属性名称无关

但是一个类型如果存在多个对象时,优先名称匹配,如果名称都匹配不上,就报错了

而且@Autowired无法注入一个final修饰的属性

属性注⼊是使⽤ @Autowired 实现的,将 Service 类注⼊到 Controller 类中

Service 类的实现代码如下:

java 复制代码
@Service
public class UserService {
    public void doService(){
        System.out.println("doService...");
    }
}

Controller 类的实现代码如下:

java 复制代码
@Controller//将对象存储到 Spring 中
public class UserController {

    @Autowired
    private UserService userService;
    public void doController(){
        userService.doService();
        System.out.println("doController...");
    }
}

获取 Controller 中的 doController⽅法:

java 复制代码
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        //Spring上下文,返回的就是Spring的运行环境
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        //从Spring上下⽂中获取对象
        UserController bean = context.getBean(UserController.class);
        //使用对象
        bean.doController();
    }

}

最终结果如下:

去掉@Autowired , 再运⾏⼀下程序看看结果

2. 构造⽅法注⼊

构造⽅法注⼊是在类的构造⽅法中实现注⼊,如下代码所⽰:

java 复制代码
@Controller//将对象存储到 Spring 中
public class UserController {

    //属性注入
    //@Autowired
    //private UserService userService;

    //构造方法注入
    private UserService userService;
    private UserInfo userInfo;

    //Spring会默认优先使用无参构造函数,这样就没有userService对象,下面doController()方法就空指针异常了
    //public UserController() {
    //}

    //注释了无参构造,下面两个构造函数Spring就不知道用哪个了,因为Spring是默认优先使用无参的
    //因此我们要显式告诉Spring用哪个,这里就用到@Autowired
    @Autowired
    public UserController(UserService userService) {
        this.userService=userService;
    }
    public UserController(UserService userService, UserInfo userInfo) {
        this.userService = userService;
        this.userInfo = userInfo;
    }

    public void doController(){
        userService.doService();
        System.out.println("doController...");
    }
}

Spring会默认优先使用无参构造函数,这样就没有userService对象,下面doController()方法中的userService.doService()就空指针异常了

注意事项:如果类只有⼀个构造⽅法,那么 @Autowired 注解可以省略;如果类中有多个构造⽅法,那么需要添加上 @Autowired 来明确指定到底使⽤哪个构造⽅法,不然Spring不知道用哪个构造方法。

3. Setter 注⼊

Setter 注⼊和属性的 Setter ⽅法实现类似,只不过在设置 Setter ⽅法的时候需要加上 @Autowired 注解 ,它不像是构造方法只有一个的话会默认帮你加上,如下代码所⽰:

java 复制代码
@Controller//将对象存储到 Spring 中
public class UserController {

    //属性注入
    //@Autowired
    //private UserService userService;

    //构造方法注入
    //private UserService userService;
    //private UserInfo userInfo;

    //Spring会默认优先使用无参构造函数,这样就没有userService对象,下面doController()方法就空指针异常了
    //public UserController() {
    //}

    //注释了无参构造,下面两个构造函数Spring就不知道用哪个了,因为Spring是默认优先使用无参的
    //因此我们要显式告诉Spring用哪个,这里就用到@Autowired
    //@Autowired
    //public UserController(UserService userService) {
    //    this.userService=userService;
    //}
    //public UserController(UserService userService, UserInfo userInfo) {
    //    this.userService = userService;
    //    this.userInfo = userInfo;
    //}

    //Setter方法注入
    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    public void doController(){
        userService.doService();
        System.out.println("doController...");
    }
}

尝试⼀下 Setter ⽅法如果不加 @Autowired 注解能注⼊成功吗?

肯定不行

三种注⼊优缺点分析
  • 属性注⼊

    • 优点: 简洁,使⽤⽅便;

    • 缺点:

      • 只能⽤于 IoC 容器,如果是⾮ IoC 容器不可⽤,并且只有在使⽤的时候才会出现 NPE(空指针异常)

      • 不能注⼊⼀个Final修饰的属性

  • 构造函数注⼊(Spring 4.X推荐)

    • 优点:

      • 可以注⼊final修饰的属性

      • 注⼊的对象不会被修改

      • 依赖对象在使⽤前⼀定会被完全初始化,因为依赖是在类的构造⽅法中执⾏的,⽽构造⽅法是在类加载阶段就会执⾏的⽅法.

      • 通⽤性好, 构造⽅法是JDK⽀持的, 所以更换任何框架,他都是适⽤的

    • 缺点:

      • 注⼊多个对象时, 代码会⽐较繁琐
  • Setter注⼊(Spring 3.X推荐)

  • 优点: ⽅便在类实例之后, 重新对该对象进⾏配置或者注⼊

  • 缺点:

    • 不能注⼊⼀个Final修饰的属性

    • 注⼊对象可能会被改变, 因为setter⽅法可能会被多次调⽤, 就有被修改的⻛险.

更多DI相关内容参考:https://docs.spring.io/spring-framework/reference/core/beans/dependencies.html

@Autowired存在问题

当同⼀类型存在多个bean时 , 使⽤@Autowired会存在问题

java 复制代码
@Configuration
public class BeanConfig {
    @Bean
    public String name(){
       return "zhangsan";
    }
    @Bean
    public UserInfo userInfo1(String name){
        UserInfo userInfo=new UserInfo();
        userInfo.setId(1);
        userInfo.setName(name);
        userInfo.setAge(12);
        return userInfo;
    }
    @Bean   
    public UserInfo userInfo2(){
        UserInfo userInfo=new UserInfo();
        userInfo.setId(2);
        userInfo.setName("lisi");
        userInfo.setAge(13);
        return userInfo;
    }
}
java 复制代码
@Controller//将对象存储到 Spring 中
public class UserController {

    //属性注入
    @Autowired
    private UserService userService;
    @Autowired
    private UserInfo userInfo;

    public void doController(){
        userService.doService();
        System.out.println(userInfo);
        System.out.println("doController...");
    }
}

报错的原因是,⾮唯⼀的 Bean 对象。

如何解决上述问题呢?Spring提供了以下⼏种解决⽅案:(解决方法思想:给bean重命名或者指定名称)

  • 属性名和你需要使用的对象名保持一致(不这样做就以下三种方法)

  • @Primary

  • @Qualifier

  • @Resource

**(不推荐)**使⽤@Primary注解:当存在多个相同类型的Bean注⼊时,加上@Primary注解,来确定默认的实现,标识默认的对象

java 复制代码
@Configuration
public class BeanConfig {
    @Bean
    public String name(){
       return "zhangsan";
    }
    @Primary
    @Bean
    public UserInfo userInfo1(){
        UserInfo userInfo=new UserInfo();
        userInfo.setId(1);
        userInfo.setName("zhangsan");
        userInfo.setAge(12);
        return userInfo;
    }
    @Bean
    public UserInfo userInfo2(){
        UserInfo userInfo=new UserInfo();
        userInfo.setId(2);
        userInfo.setName("lisi");
        userInfo.setAge(13);
        return userInfo;
    }
}

**(较常用)**使⽤@Qualifier注解:指定当前要注⼊的bean对象。@Qualifier的value属性中,指定注⼊的bean的名称。

  • @Qualifier注解不能单独使⽤,必须配合@Autowired使⽤
java 复制代码
@Controller//将对象存储到 Spring 中
public class UserController {

    //属性注入
    @Autowired
    private UserService userService;
    @Qualifier("userInfo2")
    @Autowired
    private UserInfo userInfo;

    public void doController(){
        userService.doService();
        System.out.println(userInfo);
        System.out.println("doController...");
    }
}

(较常用)使⽤@Resource注解:是按照bean的名称进⾏注⼊。通过name属性指定要注⼊的bean的名称。

java 复制代码
@Controller//将对象存储到 Spring 中
public class UserController {

    //属性注入
    @Autowired
    private UserService userService;
    @Resource(name="userInfo2")
    private UserInfo userInfo;

    public void doController(){
        userService.doService();
        System.out.println(userInfo);
        System.out.println("doController...");
    }
}

常⻅⾯试题:

@Autowird@Resource的区别

  • @Autowired 是 Spring框架提供的注解,⽽@Resource是 JDK 提供的注解
  • @Autowired 默认是按照类型注⼊,⽽ @Resource 是按照名称注⼊ . 相⽐于 @Autowired 来说,@Resource ⽀持更多的参数设置,例如 name 设置,根据名称获取 Bean。
  • @Autowired默认,如果同个类型存在多个对象,按名称匹配,如果名称匹配不上,就会报错
练习

通过上⾯的学习, 我们把前⾯的图书管理系统代码进⾏调整

Service层的注解, 改成 @Service

Dao层的注解, 改成 @Repository

重新运⾏代码, 验证程序访问正常

一般我们默认是8080的端口号,但也可以修改,这里简单提一下


总结
Spring, Spring Boot 和 Spring MVC 的关系以及区别

Spring : 简单来说, Spring 是⼀个开发应⽤框架,什么样的框架呢,有这么⼏个标签:轻量级、⼀站式、模块化,其⽬的是⽤于简化企业级应⽤程序开发

Spring的主要功能:管理对象,以及对象之间的依赖关系, ⾯向切⾯编程, 数据库事务管理, 数据访问, web框架⽀持等.

但是Spring具备⾼度可开放性, 并不强制依赖Spring, 开发者可以⾃由选择Spring的部分或者全部, Spring可以⽆缝继承第三⽅框架, ⽐如数据访问框架(Hibernate 、JPA), web框架(如Struts、JSF)

Spring MVC : Spring MVC 是 Spring 的⼀个⼦框架, Spring诞⽣之后, ⼤家觉得很好⽤, 于是按照MVC模式设计了⼀个 MVC框架(⼀些⽤Spring 解耦的组件), 主要⽤于开发WEB应⽤和⽹络接⼝,所以,Spring MVC 是⼀个Web框架

Spring MVC 基于 Spring 进⾏开发的, 天⽣的与 Spring 框架集成. 可以让我们更简洁的进⾏Web层开发, ⽀持灵活的 URL 到⻚⾯控制器的映射, 提供了强⼤的约定⼤于配置的契约式编程⽀持, ⾮常容易与其他视图框架集成,如 Velocity、FreeMarker等

Spring Boot : Spring Boot 是对 Spring 的⼀个封装, 为了简化Spring应⽤的开发⽽出现的,中⼩型企业,没有成本研究⾃⼰的框架, 使⽤Spring Boot 可以更加快速的搭建框架, 降级开发成本, 让开发⼈员更加专注于Spring应⽤的开发,⽽⽆需过多关注XML的配置和⼀些底层的实现

Spring Boot 是个脚⼿架, 插拔式搭建项⽬, 可以快速的集成其他框架进来.

⽐如想使⽤SpringBoot开发Web项⽬, 只需要引⼊Spring MVC框架即可, Web开发的⼯作是SpringMVC完成的, ⽽不是SpringBoot, 想完成数据访问, 只需要引⼊Mybatis框架即可.

Spring Boot只是辅助简化项⽬开发的, 让开发变得更加简单, 甚⾄不需要额外的web服务器, 直接⽣成jar包执⾏即可.

最后⼀句话总结: Spring MVC 和 Spring Boot 都属于 Spring,Spring MVC 是基于 Spring 的⼀个MVC 框架,⽽Spring Boot 是基于 Spring 的⼀套快速开发整合包

⽐如我们的图书系统代码中

整体框架是通过 SpringBoot 搭建的

IoC, DI 功能是 Spring 的提供的,

web 相关功能是 Spring MVC 提供的

这三者专注的领域不同,解决的问题也不⼀样, 总的来说,Spring 就像⼀个⼤家族,有众多衍⽣产品, 但他们的基础都是Spring, ⽤⼀张图来表⽰他们三个的关系:

bean的存是五大注解和@Bean,使用这六个的时候,Spring会给一个默认的名称,五大注解的名称是类名的小驼峰表示法,特殊情况,前两位字母都为大写,名称就是类名;@Bean的名称就是方法名

bean 的命名

添加之后之前的名称就不再有了,Spring会使用程序员定义的bean名称

  1. 五⼤注解存储的bean

​ ① 前两位字⺟均为⼤写, bean名称为类名

​ ② 其他的为类名⾸字⺟⼩写

​ ③ 通过 value属性设置,比如 @Controller(value = "user")

  1. @Bean 注解存储的bean

​ ① bean名称为⽅法名

​ ②通过name属性设置 @Bean(name = {"u1","user1"})

​ ③一个Bean可以有多个名称,但一个名称只能对应一个Bean

常⻅⾯试题
  1. 三种注⼊⽅式的优缺点
  2. 常⻅注解有哪些? 分别是什么作⽤?

​ web url映射: @RequestMapping

​ 参数接收和接⼝响应: @RequestParam, @RequestBody, @ResponseBody

bean的存储 : @Controller, @Service, @Repository, @Component, @Configuration, @Bean

bean的获取: 属性注入、构造方法注入、Setter方法注入

  1. @Autowired@Resource 区别
  2. 说下你对Spring, SpringMVC, Springboot的理解
相关推荐
曾令胜1 小时前
excel导出使用arthas动态追踪方法调用耗时后性能优化的过程
spring·性能优化·excel
.格子衫.1 小时前
Spring Boot 原理篇
java·spring boot·后端
多云几多1 小时前
Yudao单体项目 springboot Admin安全验证开启
java·spring boot·spring·springbootadmin
摇滚侠3 小时前
Spring Boot 3零基础教程,Spring Intializer,笔记05
spring boot·笔记·spring
Jabes.yang3 小时前
Java求职面试实战:从Spring Boot到微服务架构的技术探讨
java·数据库·spring boot·微服务·面试·消息队列·互联网大厂
聪明的笨猪猪3 小时前
Java Redis “高可用 — 主从复制”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
兮动人4 小时前
Spring Bean耗时分析工具
java·后端·spring·bean耗时分析工具
MESSIR224 小时前
Spring IOC(控制反转)中常用注解
java·spring
摇滚侠4 小时前
Spring Boot 3零基础教程,Demo小结,笔记04
java·spring boot·笔记
笨手笨脚の5 小时前
设计模式-迭代器模式
java·设计模式·迭代器模式·行为型设计模式