spring通过注解实现IOC详解

 2023-01-24
原文作者:Jony_zhang 原文地址:https://juejin.cn/post/7073439880998551566

在上篇文章中,我们都是通过xml文件进行bean或者某些属性的赋值,其实还有另外一种注解的方式,在企业开发中使用的很多,在bean上添加注解,可以快速的将bean注册到ioc容器。

一、使用注解的方式注册bean到IOC容器中

1、首先我们先创建一个maven项目(后面熟悉了,可以直接创建spring项目)

1、new 一个project

202301012037033271.png 2、填写项目相关信息

202301012037042642.png 3、修改maven配置信息
3-1、打开maven配置窗口

202301012037053413.png 3-2、修改maven配置信息

202301012037061614.png

2、配置spring maven依赖

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.19.RELEASE</version>
    </dependency>

202301012037072045.png 可以看到spring的核心jar包已经全部导入。

3、我们安装MVC结构创建我们项目的目录结构

202301012037077846.png 我们在 com.jony下分别创建了各个层的package。上篇文章中如果我们需要让spring实现这些类的注入,我们需要把这些类都在spring配置文件中进行bean的注入,那我们通过注解该如何实现呢?

4、配置spring的配置文件

我们在resource中新加spring的配置文件,并在xml中通过使用

    <co4text:component-scan base-package="com.jony"></context:component-scan>

配置spring需要扫码的package路径,这样在这个package下的类就可以自动被spring扫描到了

202301012037088987.png

5、核心注解

上面通过配置扫描的package可以扫描到java类,但是如果要注入的IOC容器中,还需要给类添加注解,平时用到的主要注解如下:

@Controller:控制器,推荐给controller层添加此注解
@Service:业务逻辑,推荐给业务逻辑层添加此注解
@Repository:仓库管理,推荐给数据访问层添加此注解
@Component:给不属于以上基层的组件添加此注解

注意
1、我们虽然人为的给不同的层添加不同的注解,但是在spring看来,可以在任意层添加任意注解 spring底层是不会给具体的层次验证注解,这样写的目的只是为了提高可读性,最偷懒的方式 就是给所有想交由IOC容器管理的bean对象添加component注解
2、注解不可以加到interface上面,因为接口是不允许注册到IOC容器中,spring IOC容器在解析的时候会忽略到接口的注解

5-1、测试注解的效果

5-1-1、不添加注解测试

通过下图我们可以看到,虽然在xml中已经配置了,扫描com.jony的包,但是UserController.java 中并没有注解,这样在通过ioc取类对象的时候就会报错,告诉我们找不到。

202301012037098058.png

5-1-2、添加注解

通过下图我们可以看到,加上@Controller之后,通过IOC我们就可以获取的类对象了。

202301012037109879.png

5-1-3、给所有类添加注解

我们给service实现类,以及dao实现类分别加上@service和@Repository。通常情况下,我们不需要给entity加注解,如果需要可以添加@Component

2023010120371230710.png

注意 :当使用注解注册组件和使用配置文件注册组件是一样的,但是要注意:
1、组件的id默认就是组件的类名首字符小写,如果非要改名字的话,直接在注解中添加即可(如我们的UserController在测试类中使用userController),如果需要自定义可以设置为:@Controller("ab")\

2023010120371316511.png 2、组件默认情况下都是单例的,如果需要配置多例模式的话,可以在注解下添加@Scope注解

二、定义扫描包时要包含的类和不要包含的类

当定义好基础的扫描包后,在某情况下可能要有选择性的配置是否要注册bean到IOC容器中,此时可以通过如下的方式进行配置。

2-1 设置不扫描

2-1-1、spring 配置文件如下

在 context:conponent-san中添加context:exclude-filter就可以进行排除设置,其中

  • type:表示指定过滤的规则 annotation :按照注解进行排除,标注了指定注解的组件不要,expression表示要过滤的注解
    assignable :指定排除某个具体的类,按照类排除,expression表示不注册的具体类名
    aspectj :后面讲aop的时候说明要使用的aspectj表达式,基本不用
    custom :定义一个typeFilter,自己写代码决定哪些类被过滤掉,基本不用
    regex :使用正则表达式过滤,基本不用
    <!--定义扫描的包-->
    <context:component-scan base-package="com.jony">
        <!--排除不需要过滤的注解-->
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

上面的配置了@Controller不被扫描,那expression的值是如何取到的呢?看下面截图

2023010120371401512.png 光标定位到@Controller 右键到 copy Reference即可复制@Controller的完整限定名。

2-1-2、测试如下

通过排除@Controller就无法获得类对象了,如下

2023010120371498813.png

2-1-3、通过类名设置不被扫描

2023010120371586914.png

2-2、设置扫描

2-2-1、给UserController的注解去掉,然后我们运行测试代码

这样是无法获得类对象的,上面也测试过

2023010120371726515.png

2-2-2、通过设置spring配置文件,进行指定扫描

铜鼓下图我们可以看到,设置了context:include-filter就可以让spring进行指定类的扫描了,配置中的type和上面context:exclude-filter中的type设置一直,这边就不做具体演示了。

2023010120371824916.png

三、使用@AutoWrite进行自动注入

使用注解的方式实现自动注入需要使用@AutoWired注解。

2023010120371957617.png 通过上图我们就可以看到,通过AutoWrite,就可以把对象注入到另外一个bean中了。

注意 :@AutoWrite默认根据ByType去匹配bean的,在Controller中通过@AutoWrite 注入的是UserService,然后我们的UserServiceImpl有实现了UserService接口,因此UserServiceImpl和UserService被spring认为是一个类型,所以此处实际注入的是UserServiceImpl类。

3-1、@AutoWrite的匹配方式

下图中,我们有两个service的实现类

2023010120372076418.png

3-1-1、UserController中注入service

    package com.jony.controller;
    
    import com.jony.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Scope;
    import org.springframework.stereotype.Controller;
    
    @Controller
    public class UserController {
    
        @Autowired
        private UserService userService;
    
        public void getUser(){
            userService.getUser();
        }
    }

这样实现我们userService的实现类就变成了两个

3-1-2、测试结果

通过下图,我们可以看到通过userService,进行匹配的时候发现了两个bean,user1ServiceImpl,userServiceImpl,又通过userService根据名词查找的时候,没有匹配到,则就会报错

2023010120372178019.png 以上问题,主要就是因为通过类型匹配到了两个,然后通过名字又没找到报错,所以解决方案就是我们修改一下注入的名称即可,如下

3-1-3、通过类型匹配到多个bean,通过名称无法找到ben报错解决

3-1-3-1、修改注入bean的名称

我们把之前userService,改为userServiceImpl,这样在通过类型匹配到user1ServiceImpl,userServiceImpl,然后就可以通过通过名称匹配到userServiceImpl,从而解决报错问题。

2023010120372266220.png

3-1-3-2、修改一开始注入bean的名称

可以在service的实现层,@Service添加一个别名,如下

2023010120372379821.png

3-1-3-3、通过@Qualifier指定bean名称(覆盖原来bean名称)

    @Qualifier("userServiceImpl")
    private UserService userService;

2023010120372622022.png 通过@Qualifier 设置名称之后,就会通过这个名称去找bean,即使这时候通过类型或者userService可以找到也不会起作用!

3-1-3-3-1、先去掉重复类型的类

删掉了之前User1ServiceImpl,这样根据类型查找的时候就可以匹配到UserServiceImpl,执行没问题

2023010120372726323.png

3-1-3-3-1、通过@Qualifier设置存在的bean
    @Qualifier("userServiceImpl")

通过上面的配置就可以通过名称直接匹配到UserServiceImpl了

2023010120372858724.png

3-1-3-3-1、通过@Qualifier设置不存在的bean

设置了不存在的bean程序就会报错

2023010120372993225.png

3-1-3-4、通过@Primary设置主要注入bean

把之前删掉的User1ServiceImpl再次还原,我们既不设置@Service的名称,也不修改Controller注入的名称,同时也不使用@Qualifier的情况下,我们只需要通过@Primary设置通过类型匹配到多个bean中的某一个,将其设置为主要自动注入的类,就可以解决报错问题

2023010120373080426.png

3-1-3、总结

如果@AutoWrite根据类型去匹配的时候,如果匹配到了多个,那么就会通过名字去进行匹配,如果名字没有匹配到则会报错.我们就可以通过两种方式修改bean的 别名 或者添加@Qualifier设置bean 名称 来解决,或者通过设置@Primary设置主要自动注入bean来解决。

∗ ∗ 注意 ∗ ∗\color{red}{**注意**}∗∗注意∗∗:修改bean的名字@Controller @Service @Repository @component都可以修改bean的名字

3-2、AutoWrite的使用

我们进入@AutoWrite注解可以看到如下

2023010120373181527.png @AutoWrite可以标注在构造函数、方法、参数、字段或者其他标注注解的类型。

3-3、@Resource和@AutoWrite的区别

在使用自动装配的时候,出了可以使用@AutoWired注解之外,还可以使用@Resource注解,大家需要知道这两个注解的区别。

1、@AutoWired:是spring中提供的注解,@Resource:是jdk中定义的注解,依靠的是java的标准

2、@AutoWired默认是按照类型进行装配,默认情况下要求依赖的对象必须存在,@Resource默认是按照名字进行匹配的,同时可以指定name属性。

3、@AutoWired只适合spring框架,而@Resource扩展性更好

四、通过@Value 设置依赖注入的属性

此处主要理解@Value的使用
@Value("xxx") :设置硬编码值
@Value("${}") :设置读取外部配置文件的值
@Value("#{}") :设置SpEL表达式

4-1、添加一个外部资源文件

2023010120373279928.png

4-2、设置spring配置文件读取资源文件

通过 context:property-placeholder 就可以读取外部资源文件了

2023010120373367729.png

4-3、我们修改User类,并添加@Value注解

    package com.jony.entity;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    @Component
    public class User {
        //通过@Value设置硬编码值
        @Value("张三")
        private String name;
    
        //通过${} 设置读取外部资源数据
        @Value("${user.age}")
        private Integer age;
    
        //通过#{} 设置SpEL表达式
        @Value("#{100*100}")
        private Integer money;
    
        @Value("#{role.name}")
        private String role;
    
        public String getRole() {
            return role;
        }
    
        public void setRole(String role) {
            this.role = role;
        }
    
        public Integer getMoney() {
            return money;
        }
    
        public void setMoney(Integer money) {
            this.money = money;
        }
    
        public Integer getAge() {
            return age;
        }
    
        public void setAge(Integer age) {
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + ''' +
                    ", age=" + age +
                    ", money=" + money +
                    ", role='" + role + ''' +
                    '}';
        }
    }

4-4、添加Role类

一定要添加@Component注解,否则在User中的@Value("#{role.name}")将会报错

    package com.jony.entity;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    @Component
    public class Role {
        @Value("管理员")
        private String name;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }

4-4 测试

2023010120373498830.png 通过上图可以看到,我们使用@Value 的三种方式已经成功将值注入的bean中。

五、泛型依赖注入

平时实际项目中,我们会把一些公共的方法放到一个base接口中,比如实现对数据库的基本CURD抽出去,然后在各自的服务中进行实现,下面对上面的代码就行修改,目录结构如下

2023010120373608631.png

5-1、修改UserService 接口为公共的service,设置公共的CRUD,我这就先只做getBean操作

    package com.jony.service;
    
    public interface BaseService<T> {
        T getBean();
    }

5-2、修改UserServiceImpl

在实现BaseService的时候泛型传入User,最终getBean就是查询User

    package com.jony.service.impl;
    
    import com.jony.dao.UserDao;
    import com.jony.entity.User;
    import com.jony.service.BaseService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    @Service
    public class UserServiceImpl implements BaseService<User> {
    
        @Autowired
        private UserDao userDao;
        @Override
        public User getBean() {
            System.out.println("UserServiceImpl");
            userDao.getBean();
            return null;
        }
    }

5-3、修改RoleServiceImpl

在实现BaseService的时候泛型传入Role,最终getBean就是查询Role

    package com.jony.service.impl;
    
    import com.jony.dao.UserDao;
    import com.jony.entity.Role;
    import com.jony.service.BaseService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    @Service
    public class RoleServiceImpl implements BaseService<Role> {
    
        @Autowired
        private UserDao userDao;
        @Override
        public Role getBean() {
            System.out.println("RoleServiceImpl");
            userDao.getBean();
            return null;
        }
    }

5-4、UserController测试

在注入BaseService的时候泛型指定User,这样执行的service实现就是UserServiceImpl了,这样也可以解决@AutoWrite通过类型查找多个bean报错的问题了

2023010120373755832.png

六、使用@DependOn 设置依赖优先加载顺序

6-1、我们先准备两个类

6-1-1、User.java

    package com.jony.entity;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    @Component
    public class User {
       //通过@Value设置硬编码值
       @Value("张三")
       private String name;
    
       //通过${} 设置读取外部资源数据
       @Value("${user.age}")
       private Integer age;
    
       //通过#{} 设置SpEL表达式
       @Value("#{100*100}")
       private Integer money;
    
       @Value("#{role.name}")
       private String role;
    
       public String getRole() {
           return role;
       }
    
       public void setRole(String role) {
           this.role = role;
       }
    
       public Integer getMoney() {
           return money;
       }
    
       public void setMoney(Integer money) {
           this.money = money;
       }
    
       public Integer getAge() {
           return age;
       }
    
       public void setAge(Integer age) {
           this.age = age;
       }
    
       public String getName() {
           return name;
       }
    
       public void setName(String name) {
           this.name = name;
       }
    
       @Override
       public String toString() {
           return "User{" +
                   "name='" + name + ''' +
                   ", age=" + age +
                   ", money=" + money +
                   ", role='" + role + ''' +
                   '}';
       }
       public User(){
           System.out.println("User 加载了");
       }
    }

6-1-2、Role.java

    package com.jony.entity;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    @Component
    public class Role {
        @Value("管理员")
        private String name;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
        public Role(){
            System.out.println("Role 加载了");
        }
    }

6-2、测试加载

通过给User和Role 两个类添加@Component,让spring 进行管理,然后启动的时候,默认加载顺序如下,那如果我们要让User先加载呢?

2023010120373866833.png

6-3、设置优先加载顺序

我们可以在Role中设置User先加载,可以通过如下来进行设置

    @DependsOn("user")

最终Role代码为:

    package com.jony.entity;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.DependsOn;
    import org.springframework.stereotype.Component;
    
    @Component
    @DependsOn("user")
    public class Role {
        @Value("管理员")
        private String name;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
        public Role(){
            System.out.println("Role 加载了");
        }
    }

6-3-1、通过设置@DependOn查看结果

可以看到这次User先加载了

2023010120373994134.png

七、使用@Lazy懒加载

顾明思议,在bean上加上@Lazy,在一开始加载的时候,spring就不会把类加载到IOC中,当使用的时候再进行注入,下面就不做演示了。

八、使用@Scope 设置bean的作用域

默认是单例模式@Scope("singleton"),同一个类只会创建一次。如下图

2023010120374123635.png

8-1、通过设置@Scope("prototype")多例注入

    package com.jony.entity;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.DependsOn;
    import org.springframework.context.annotation.Scope;
    import org.springframework.stereotype.Component;
    
    @Component
    @DependsOn("user")
    @Scope("prototype")
    public class Role {
        @Value("管理员")
        private String name;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
        public Role(){
            System.out.println("Role 加载了");
        }
    }

8-2、测试结果

2023010120374233336.png

九、监控Bean的生命周期

9-1、通过@PostConstruct 监控bean的初始化

    @PostConstruct
    public void init(){
        System.out.println("User 初始化");
    }

9-2、通过@PreDestroy 监控bean的销毁

    @PreDestroy
    public void desstroy(){
        System.out.println("user 销毁");
    }

9-3 测试

通过下图结果,我们就可以看到bean的初始化和注销结果了

2023010120374323337.png

总结

1 、本篇文章主要介绍了通过配置spring.xml,利用context:component-scan进行包扫描,达到我们要注入bean的第一步
2 、使用@Controller/@Service/@Repository/@Component添加注解,告诉spring被注解的对象需要添加到IOC中。
3 、通过context:exclude-filter排除不需要注入IOC容器的对象,以及使用context:include-filter添加需要注入IOC容器的对象,这两个配置中都有type属性,需要记住常用的annotation、assignable

  • type:表示指定过滤的规则 annotation :按照注解进行排除,标注了指定注解的组件不要,expression表示要过滤的注解
    assignable :指定排除某个具体的类,按照类排除,expression表示不注册的具体类名
    aspectj :后面讲aop的时候说明要使用的aspectj表达式,基本不用
    custom :定义一个typeFilter,自己写代码决定哪些类被过滤掉,基本不用
    regex :使用正则表达式过滤,基本不用
    4 、掌握@AutoWrite注解的方式,先通过byType查找,如果没找到或者找到多个,再通过byName查找对象。如果通过byType找到多个bean,可以通过修改bean的名称,精准注入,也可以通过修改bean的别名如@Service("userService"),同时还可以通过@Qualifier指定bean的名称,或者通过设置@primart设置有限加载的bean来解决报错问题。
    5 、知道@AutoWrite和@Resource的区别。两者一个是spring的一个是jdk的,然后一个是先通过byType一个是通过byName。
    6 、@Value的使用,可以通过直接赋值,或者${}读取外部资料文件,或者使用#{}使用SpEL表达式。
    7 、通过@AutoWrite可以注入泛型,解决底层重复的代码。
    8 、使用@DependsOn设置依赖优先加载的类。
    9 、使用@Lazy 设置懒加载
    10 、使用@scope设置bean的作用域,分别有singleton和prototype。
    11 、使用@PostConstruct监控bean的初始化,以及使用@PreDestroy监控bean的注销。