7. IOC & DI注解开发

发布时间 2023-06-24 13:41:38作者: 捞起月亮的小北

要想真正简化开发,就需要用到 Spring 的注解开发,Spring 对注解支持的版本历程:

  • 2.0 版开始支持注解
  • 2.5 版注解功能趋于完善
  • 3.0 版支持纯注解开发

关于注解开发,我们会讲解两块内容注解开发定义bean​ 和纯注解开发​。

注解开发定义 bean 用的是 2.5 版提供的注解,纯注解开发用的是 3.0 版提供的注解。

1. 环境准备

  • 创建一个 Maven 项目

  • pom.xml 添加 Spring 的依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
    </dependencies>
    
  • resources 下添加 applicationContext.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"
           xsi:schemaLocation="
                http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
        <bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
    </beans>
    
  • 添加 BookDao、BookDaoImpl、BookService、BookServiceImpl 类

    public interface BookDao {
        public void save();
    }
    public class BookDaoImpl implements BookDao {
        public void save() {
            System.out.println("book dao save ..." );
        }
    }
    public interface BookService {
        public void save();
    }
    
    public class BookServiceImpl implements BookService {
        public void save() {
            System.out.println("book service save ...");
        }
    }
    
    
  • 创建运行类 App

    public class App {
        public static void main(String[] args) {
            ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
            BookDao bookDao = (BookDao) ctx.getBean("bookDao");
            bookDao.save();
        }
    }
    

2. 注解开发定义 bean

步骤 1:删除原 XML 配置

将配置文件中的<bean>​ 标签删除掉

<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>

步骤 2:Dao 上添加注解

在 BookDaoImpl 类上添加@Component​ 注解

@Component("bookDao")
public class BookDaoImpl implements BookDao {
    public void save() {
        System.out.println("book dao save ..." );
    }
}

注意:@Component 注解不可以添加在接口上,因为接口是无法创建对象的。

XML 与注解配置的对应关系:

image

步骤 3:配置 Spring 的注解包扫描

为了让 Spring 框架能够扫描到写在类上的注解,需要在配置文件上进行包扫描

<?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">
    <context:component-scan base-package="com.itheima"/>
</beans>

说明:

component-scan

  • component:组件,Spring 将管理的 bean 视作自己的一个组件
  • scan:扫描

base-package 指定 Spring 框架扫描的包路径,它会扫描指定包及其子包中的所有类上的注解。

  • 包路径越多[如:com.itheima.dao.impl],扫描的范围越小速度越快
  • 包路径越少[如:com.itheima],扫描的范围越大速度越慢
  • 一般扫描到项目的组织名称即 Maven 的 groupId 下[如:com.itheima]即可。

步骤 4:运行程序

运行App​ 类查看打印结果

image

步骤 5:Service 上添加注解

在 BookServiceImpl 类上也添加@Component​ 交给 Spring 框架管理

@Component
public class BookServiceImpl implements BookService {
    private BookDao bookDao;

    public void setBookDao(BookDao bookDao) {
        this.bookDao = bookDao;
    }

    public void save() {
        System.out.println("book service save ...");
        bookDao.save();
    }
}

步骤 6:运行程序

在 App 类中,从 IOC 容器中获取 BookServiceImpl 对应的 bean 对象,打印

public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        BookDao bookDao = (BookDao) ctx.getBean("bookDao");
        System.out.println(bookDao);
        //按类型获取bean
        BookService bookService = ctx.getBean(BookService.class);
        System.out.println(bookService);
    }
}

说明:

  • BookServiceImpl 类没有起名称,所以在 App 中是按照类型来获取 bean 对象

  • @Component 注解如果不起名称,会有一个默认值就是当前类名首字母小写​,所以也可以按照名称获取,如

    BookService bookService = (BookService)ctx.getBean("bookServiceImpl");
    System.out.println(bookService);
    

对于@Component 注解,还衍生出了其他三个注解@Controller​、@Service​、@Repository

通过查看源码会发现:

image

这三个注解和@Component 注解的作用是一样的,为什么要衍生出这三个呢?

方便我们后期在编写类的时候能很好的区分出这个类是属于表现层​、业务层​ 还是数据层​ 的类。

知识点 1:@Component 等

名称 @Component/@Controller/@Service/@Repository
类型 类注解
位置 类定义上方
作用 设置该类为 spring 管理的 bean
属性 value(默认):定义 bean 的 id

3. 纯注解开发模式

上面已经可以使用注解来配置 bean,但是依然有用到配置文件,在配置文件中对包进行了扫描,Spring 在 3.0 版已经支持纯注解开发

  • Spring3.0 开启了纯注解开发模式,使用 Java 类替代配置文件,开启了 Spring 快速开发赛道

具体如何实现?

3.1 思路分析

实现思路为:

  • 将配置文件 applicationContext.xml 删除掉,使用类来替换。

3.2 实现步骤

步骤 1:创建配置类

创建一个配置类SpringConfig

public class SpringConfig {
}

步骤 2:标识该类为配置类

在配置类上添加@Configuration​ 注解,将其标识为一个配置类,替换applicationContext.xml

@Configuration
public class SpringConfig {
}

步骤 3:用注解替换包扫描配置

在配置类上添加包扫描注解@ComponentScan​ 替换<context:component-scan base-package=""/>

@Configuration
@ComponentScan("com.itheima")
public class SpringConfig {
}

步骤 4:创建运行类并执行

创建一个新的运行类AppForAnnotation

public class AppForAnnotation {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = (BookDao) ctx.getBean("bookDao");
        System.out.println(bookDao);
        BookService bookService = ctx.getBean(BookService.class);
        System.out.println(bookService);
    }
}

运行 AppForAnnotation,可以看到两个对象依然被获取成功

image

至此,纯注解开发的方式就已经完成了,主要内容包括:

  • Java 类替换 Spring 核心配置文件

image

  • @Configuration 注解用于设定当前类为配置类

  • @ComponentScan 注解用于设定扫描路径,此注解只能添加一次,多个数据请用数组格式

    @ComponentScan({com.itheima.service","com.itheima.dao"})
    
  • 读取 Spring 核心配置文件初始化容器对象切换为读取 Java 配置类初始化容器对象

    //加载配置文件初始化容器
    ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    //加载配置类初始化容器
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
    

知识点 1:@Configuration

名称 @Configuration
类型 类注解
位置 类定义上方
作用 设置该类为 spring 配置类
属性 value(默认):定义 bean 的 id

知识点 2:@ComponentScan

名称 @ComponentScan
类型 类注解
位置 类定义上方
作用 设置 spring 配置类扫描路径,用于加载使用注解格式定义的 bean
属性 value(默认):扫描路径,此路径可以逐层向下扫描

小结:

这一节重点掌握的是使用注解完成 Spring 的 bean 管理,需要掌握的内容为:

  • 记住@Component、@Controller、@Service、@Repository 这四个注解
  • applicationContext.xml 中<context:component-san/>​ 的作用是指定扫描包路径,注解为@ComponentScan
  • @Configuration 标识该类为配置类,使用类替换 applicationContext.xml 文件
  • ClassPathXmlApplicationContext 是加载 XML 配置文件
  • AnnotationConfigApplicationContext 是加载配置类

4. 注解开发 bean 作用范围与生命周期管理

使用注解已经完成了 bean 的管理,接下来按照前面所学习的内容,将通过配置实现的内容都换成对应的注解实现,包含两部分内容:bean作用范围​ 和bean生命周期​。

4.1 bean 作用范围

知识点 1:@Scope

名称 @Scope
类型 类注解
位置 类定义上方
作用 设置该类创建对象的作用范围 可用于设置创建出的 bean 是否为单例对象
属性 value(默认):定义 bean 作用范围, 默认值 singleton(单例),可选值 prototype(非单例)

4.2 Bean 的生命周期

(1)在 BookDaoImpl 中添加两个方法,init​ 和destroy​,方法名可以任意

@Repository
public class BookDaoImpl implements BookDao {
    public void save() {
        System.out.println("book dao save ...");
    }
    public void init() {
        System.out.println("init ...");
    }
    public void destroy() {
        System.out.println("destroy ...");
    }
}

(2)如何对方法进行标识,哪个是初始化方法,哪个是销毁方法?

只需要在对应的方法上添加@PostConstruct​ 和@PreDestroy​ 注解即可。

@Repository
public class BookDaoImpl implements BookDao {
    public void save() {
        System.out.println("book dao save ...");
    }
    @PostConstruct //在构造方法之后执行,替换 init-method
    public void init() {
        System.out.println("init ...");
    }
    @PreDestroy //在销毁方法之前执行,替换 destroy-method
    public void destroy() {
        System.out.println("destroy ...");
    }
}

(3)要想看到两个方法执行,需要注意的是destroy​ 只有在容器关闭的时候,才会执行,所以需要修改 App 的类

public class App {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao1 = ctx.getBean(BookDao.class);
        BookDao bookDao2 = ctx.getBean(BookDao.class);
        System.out.println(bookDao1);
        System.out.println(bookDao2);
        ctx.close(); //关闭容器
    }
}

(4)运行 App,类查看打印结果,证明 init 和 destroy 方法都被执行了。

​​image​​

注意:@PostConstruct 和@PreDestroy 注解如果找不到,需要导入下面的 jar 包

<dependency>
  <groupId>javax.annotation</groupId>
  <artifactId>javax.annotation-api</artifactId>
  <version>1.3.2</version>
</dependency>

找不到的原因是,从 JDK9 以后 jdk 中的 javax.annotation 包被移除了,这两个注解刚好就在这个包中。

知识点 1:@PostConstruct

名称 @PostConstruct
类型 方法注解
位置 方法上
作用 设置该方法为初始化方法
属性

知识点 2:@PreDestroy

名称 @PreDestroy
类型 方法注解
位置 方法上
作用 设置该方法为销毁方法
属性

小结

​​image​​

5. 注解开发依赖注入

5.1 注解实现按照类型注入

(1) 在 BookServiceImpl 类的 bookDao 属性上添加@Autowired​ 注解

@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;
    
//    public void setBookDao(BookDao bookDao) {
//        this.bookDao = bookDao;
//    }
    public void save() {
        System.out.println("book service save ...");
        bookDao.save();
    }
}

注意:

  • @Autowired 可以写在属性上,也可也写在 setter 方法上,最简单的处理方式是写在属性上并将setter方法删除掉

  • 为什么 setter 方法可以删除呢?

    • 自动装配基于反射设计创建对象并通过暴力反射为私有属性进行设值
    • 普通反射只能获取 public 修饰的内容
    • 暴力反射除了获取 public 修饰的内容还可以获取 private 修改的内容
    • 所以此处无需提供 setter 方法

(2)@Autowired 是按照类型注入,那么对应 BookDao 接口如果有多个实现类,比如添加 BookDaoImpl2

@Repository
public class BookDaoImpl2 implements BookDao {
    public void save() {
        System.out.println("book dao save ...2");
    }
}

这个时候再次运行 App,就会报错

image

此时,按照类型注入就无法区分到底注入哪个对象,解决方案:按照名称注入

  • 先给两个 Dao 类分别起个名称

    @Repository("bookDao")
    public class BookDaoImpl implements BookDao {
        public void save() {
            System.out.println("book dao save ..." );
        }
    }
    @Repository("bookDao2")
    public class BookDaoImpl2 implements BookDao {
        public void save() {
            System.out.println("book dao save ...2" );
        }
    }
    

    此时就可以注入成功,但是得思考个问题:

    • @Autowired 是按照类型注入的,给 BookDao 的两个实现起了名称,它还是有两个 bean 对象,为什么不报错?
    • @Autowired 默认按照类型自动装配,如果 IOC 容器中同类的 Bean 找到多个,就按照变量名和 Bean 的名称匹配。因为变量名叫bookDao​ 而容器中也有一个booDao​,所以可以成功注入。
    • 分析下面这种情况是否能完成注入呢?

image

  • 不行,因为按照类型会找到多个 bean 对象,此时会按照bookDao​ 名称去找,因为 IOC 容器只有名称叫bookDao1​ 和bookDao2​,所以找不到,会报NoUniqueBeanDefinitionException

5.2 注解实现按照名称注入

当根据类型在容器中找到多个 bean,注入参数的属性名又和容器中 bean 的名称不一致,这个时候该如何解决,就需要使用到@Qualifier​ 来指定注入哪个名称的 bean 对象。

@Service
public class BookServiceImpl implements BookService {
    @Autowired
    @Qualifier("bookDao1")
    private BookDao bookDao;
    
    public void save() {
        System.out.println("book service save ...");
        bookDao.save();
    }
}

@Qualifier 注解后的值就是需要注入的 bean 的名称。

注意:@Qualifier 不能独立使用,必须和@Autowired 一起使用

5.3 简单数据类型注入

引用类型看完,简单类型注入就比较容易懂了。简单类型注入的是基本数据类型或者字符串类型,下面在BookDaoImpl​ 类中添加一个name​ 属性,用其进行简单类型注入

@Repository("bookDao")
public class BookDaoImpl implements BookDao {
    private String name;
    public void save() {
        System.out.println("book dao save ..." + name);
    }
}

数据类型换了,对应的注解也要跟着换,这次使用@Value​ 注解,将值写入注解的参数中就行了

@Repository("bookDao")
public class BookDaoImpl implements BookDao {
    @Value("itheima")
    private String name;
    public void save() {
        System.out.println("book dao save ..." + name);
    }
}

注意数据格式要匹配,如将"abc"注入给 int 值,这样程序就会报错。

介绍完后,会有一种感觉就是这个注解好像没什么用,跟直接赋值是一个效果,还没有直接赋值简单,所以这个注解存在的意义是什么?

注解读取 properties 配置文件

@Value​ 一般会被用在从 properties 配置文件中读取内容进行使用,具体如何实现?

步骤 1:resource 下准备 properties 文件

jdbc.properties

name=itheima888

步骤 2: 使用注解加载 properties 配置文件

在配置类上添加@PropertySource​ 注解

@Configuration
@ComponentScan("com.itheima")
@PropertySource("jdbc.properties")
public class SpringConfig {
}

步骤 3:使用@Value 读取配置文件中的内容

@Repository("bookDao")
public class BookDaoImpl implements BookDao {
    @Value("${name}")
    private String name;
    public void save() {
        System.out.println("book dao save ..." + name);
    }
}

步骤 4:运行程序

运行 App 类,查看运行结果,说明配置文件中的内容已经被加载到

image

注意:

  • 如果读取的 properties 配置文件有多个,可以使用@PropertySource​ 的属性来指定多个

    @PropertySource({"jdbc.properties","xxx.properties"})
    
  • @PropertySource​ 注解属性中不支持使用通配符*​,运行会报错

    @PropertySource({"*.properties"})
    
  • @PropertySource​ 注解属性中可以把classpath:​ 加上,代表从当前项目的根路径找文件

    @PropertySource({"classpath:jdbc.properties"})
    

知识点 1:@Autowired

名称 @Autowired
类型 属性注解 或 方法注解(了解) 或 方法形参注解(了解)
位置 属性定义上方 或 标准 set 方法上方 或 类 set 方法上方 或 方法形参前面
作用 为引用类型属性设置值
属性 required:true/false,定义该属性是否允许为 null

知识点 2:@Qualifier

名称 @Qualifier
类型 属性注解 或 方法注解(了解)
位置 属性定义上方 或 标准 set 方法上方 或 类 set 方法上方
作用 为引用类型属性指定注入的 beanId
属性 value(默认):设置注入的 beanId

知识点 3:@Value

名称 @Value
类型 属性注解 或 方法注解(了解)
位置 属性定义上方 或 标准 set 方法上方 或 类 set 方法上方
作用 为 基本数据类型 或 字符串类型 属性设置值
属性 value(默认):要注入的属性值

知识点 4:@PropertySource

名称 @PropertySource
类型 类注解
位置 类定义上方
作用 加载 properties 文件中的属性值
属性 value(默认):设置加载的 properties 文件对应的文件名或文件名组成的数组