【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的理解
相关推荐
hummhumm1 小时前
第 12 章 - Go语言 方法
java·开发语言·javascript·后端·python·sql·golang
hummhumm1 小时前
第 8 章 - Go语言 数组与切片
java·开发语言·javascript·python·sql·golang·database
尼克_张1 小时前
tomcat配合geoserver安装及使用
java·tomcat
wywcool1 小时前
JVM学习之路(5)垃圾回收
java·jvm·后端·学习
-seventy-2 小时前
Java Web 工程全貌
java
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ2 小时前
idea 删除本地分支后,弹窗 delete tracked brank
java·ide·intellij-idea
言慢行善2 小时前
idea出现的问题
java·ide·intellij-idea
杨荧2 小时前
【JAVA毕业设计】基于Vue和SpringBoot的宠物咖啡馆平台
java·开发语言·jvm·vue.js·spring boot·spring cloud·开源
Ling_suu2 小时前
Spring——单元测试
java·spring·单元测试
ModelBulider2 小时前
十三、注解配置SpringMVC
java·开发语言·数据库·sql·mysql