03.Spring IoC 注解

Spring IoC 注解

Xml+ 注解

Spring 的 IOC/DI 对应的配置开发就已经讲解完成,但是使用起来相对来说还是比较复杂的,复杂的地方在配置文件

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

步骤

<dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>
  </dependencies>
<?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
       http://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 通过注解的方式注入 -->
    <context:component-scan base-package="me.hacket.spring.model"/>
</beans>

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

package me.hacket.spring.model;
import org.springframework.stereotype.Component;

// 等价于 <bean id="user" class="me.hacket.spring.model.User2"/>
@Component
public class User2 {
    public String name = "哇哈哈";
}
@Test
public void testUser2() {
	ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
	User2 user = (User2) context.getBean("user2", User2.class);
	System.out.println(user.name);
}

@Component

名称 @Component/@Controller/@Service/@Repository
类型 类注解
位置 类定义上方
作用 设置该类为 spring 管理的 bean
属性 value(默认):定义 bean 的 id
### 属性注入: @Value
// 等价于 <bean id="user" class="me.hacket.spring.model.User2"/>
@Component
public class User2 {

    public String name = "哇哈哈";

    // 相当于  <property name="name" value="hacket"/>
    @Value("hacket")
    public String name2;

    public void setName2(String name2) {
        this.name2 = name2;
    }
}

衍生的注解

@Component 有几个衍生注解,我们在 web 开发中,会按照 mvc 三层架构分层

这四个注解功能都是一样的,都是代表将某个类注册到 Spring 中,装配 Bean。

自动装配置

参考 Bean 的自动装配

如果 Autowired 不能唯一自动装配上属性,则需要通过 @Qualifier(value=“xxx”)

作用域:@Scope

// 等价于 <bean id="user" class="me.hacket.spring.model.User2"/>
@Component
//@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) // 单例模式
 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // 原型模式
public class User2 {

    public String name = "哇哈哈";

    // 相当于  <property name="name" value="hacket"/>
    @Value("hacket")
    public String name2;

    public void setName2(String name2) {
        this.name2 = name2;
    }
}

XML 与注解

比较:

xml 与注解整合开发 :

 <!-- 指定要扫描的包,这个包下的注解就会生效! -->
 <context:component-scan base-package="me.hacket.spring.model"/>
<context:annotation-config/>

纯注解开发

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

纯注解开发步骤

public class User3 {
    private String name;

    public String getName() {
        return name;
    }

    @Value("hacket zeng") //属性注入值
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User3{" +
                "name='" + name + '\'' +
                '}';
    }
}
// 这个也会Spring容器托管,注册到容器中,因为他本来就是一个@Component
// @Configuration代表这是一个配置类,就和我们之前看的beans.xml
@Configuration  // 代表这是一个配置类
@ComponentScan("me.hacket.spring.model")
public class SunConfig {

    // 注册一个bean,就相当于我们之前写的一个bean标签;
    // 这个方法的名字,就相当bean标签中的id属性;
    // 这个方法的返回值,就相当bean标签中的cLass属性;
    @Bean
    public User getUser() {
        return new User(); // 就是返回要注入到bean的对象
    }
}
public class MyTest {
    // 如果完全使用了配置类方式去做,我们就只能通过Annotationconfig 上下文来获收容器,通过配置类的class对象加载!
    public static void main(String[] args) {
        ApplicationContext applicationContext =
                new AnnotationConfigApplicationContext(SunConfig.class);
        User user = (User) applicationContext.getBean("getUser");
        System.out.println(user.getName());
    }
}
// 其他的配置类
@Configuration //代表这是一个配置类
public class SunConfig2 {
    @Bean
    public User3 getUser3() {
        return new User3(); // 就是返回要注入到bean的对象
    }
}

// 导入
@Configuration  // 代表这是一个配置类
@ComponentScan("me.hacket.spring.model")
@Import(SunConfig2.class) // 导入合并其他配置类,类似于配置文件中的 inculde 标签
public class SunConfig {

    // 注册一个bean,就相当于我们之前写的一个bean标签;
    // 这个方法的名字,就相当bean标签中的id属性;
    // 这个方法的返回值,就相当bean标签中的cLass属性;
    @Bean
    public User getUser() {
        return new User(); // 就是返回要注入到bean的对象
    }
}

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

@Repository  
//@Scope设置bean的作用范围  
@Scope("prototype")  
public class BookDaoImpl implements BookDao {  
  
    public void save() {  
        System.out.println("book dao save ...");    
        }
	}  
}

@Scope

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

Bean 的生命周期

@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 ...");
    }
}

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

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

只需要在对应的方法上添加 @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 ...");
    }
}

要想看到两个方法执行,需要注意的是 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(); //关闭容器
    }
}

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

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

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

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

注解开发依赖注入

Spring 为了使用注解简化开发,并没有提供 构造函数注入setter注入 对应的注解,只提供了自动装配的注解实现。

注解实现按照类型注入 @Autowired

单个实现

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 是按照类型注入,那么对应 BookDao 接口如果有多个实现类,比如添加 BookDaoImpl2

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

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

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

@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" );
	}
}

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

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

注解实现按照名称注入 @Qualifier

当根据类型在容器中找到多个 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 一起使用

简单数据类型注入 @Value

引用类型看完,简单类型注入就比较容易懂了。简单类型注入的是基本数据类型或者字符串类型。

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

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

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

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

注解读取 properties 配置文件

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

步骤 1:resource 下准备 properties 文件

jdbc.properties

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

在配置类上添加 @PropertySource 注解

@Configuration
@ComponentScan("com.example")
@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);
    }
}

注意:

IOC/DI 注解开发管理第三方 bean

前面定义 bean 的时候都是在自己开发的类上面写个注解就完成了,但如果是第三方的类,这些类都是在 jar 包中,我们没有办法在类上面添加注解,这个时候该怎么办?

遇到上述问题,我们就需要有一种更加灵活的方式来定义 bean,这种方式不能在原始代码上面书写注解,一样能定义 bean,这就用到了一个全新的注解==@Bean==。

注解开发管理第三方 bean @Bean

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.16</version>
</dependency>
@Configuration
public class SpringConfig {
	@Bean
    public DataSource dataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/spring_db");
        ds.setUsername("root");
        ds.setPassword("root");
        return ds;
    }
}

注意: 不能使用 DataSource ds = new DruidDataSource(),因为 DataSource 接口中没有对应的 setter 方法来设置属性。

如果有多个 bean 要被 Spring 管理,直接在配置类中多些几个方法,方法上添加@Bean 注解即可。

引入外部配置类

如果把所有的第三方 bean 都配置到 Spring 的配置类 SpringConfig 中,虽然可以,但是不利于代码阅读和分类管理,所有我们就想能不能按照类别将这些 bean 配置到不同的配置类中?

对于数据源的 bean,我们新建一个 JdbcConfig 配置类,并把数据源配置到该类下。

public class JdbcConfig {
	@Bean
    public DataSource dataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/spring_db");
        ds.setUsername("root");
        ds.setPassword("root");
        return ds;
    }
}

现在的问题是,这个配置类如何能被 Spring 配置类加载到,并创建 DataSource 对象在 IOC 容器中?

针对这个问题,有两个解决方案:

使用包扫描引入

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

JdbcConfig 类要放入到 com.example.config 包下,需要被 Spring 的配置类扫描到即可

@Configuration
public class JdbcConfig {
	@Bean
    public DataSource dataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/spring_db");
        ds.setUsername("root");
        ds.setPassword("root");
        return ds;
    }
}

使用@Import 引入

方案一实现起来有点小复杂,Spring 早就想到了这一点,于是又给我们提供了第二种方案。

这种方案可以不用加 @Configuration 注解,但是必须在 Spring 配置类上使用 @Import 注解手动引入需要加载的配置类

@Configuration
//@ComponentScan("com.example.config")
@Import({JdbcConfig.class})
public class SpringConfig {
	
}

注意:

@Configuration
//@ComponentScan("com.example.config")
@Import(JdbcConfig.class)
@Import(Xxx.class)
public class SpringConfig {
	
}

涉及到的注解

@Bean

名称 @Bean
类型 方法注解
位置 方法定义上方
作用 设置该方法的返回值作为 spring 管理的 bean
属性 value(默认):定义 bean 的 id

@Import

名称 @Import
类型 类注解
位置 类定义上方
作用 导入配置类
属性 value(默认):定义导入的配置类类名,
当配置类有多个时使用数组格式一次性导入多个配置类

注解开发实现为第三方 bean 注入资源

在使用 @Bean 创建 bean 对象的时候,如果方法在创建的过程中需要其他资源该怎么办?

这些资源会有两大类,分别是 简单数据类型 和 引用数据类型

简单数据类型

现状:对于下面代码关于数据库的四要素不应该写死在代码中,应该是从 properties 配置文件中读取。如何来优化下面的代码?

public class JdbcConfig {
	@Bean
    public DataSource dataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/spring_db");
        ds.setUsername("root");
        ds.setPassword("root");
        return ds;
    }
}

步骤:

  1. resources 目录下添加 jdbc.properties
  2. 配置文件中提供四个键值对分别是数据库的四要素
  3. 使用 @PropertySource 加载 jdbc.properties 配置文件
  4. 修改 @Value 注解属性的值,将其修改为 ${key},key 就是键值对中的键的值
public class JdbcConfig {
    @Value("com.mysql.jdbc.Driver")
    private String driver;
    @Value("jdbc:mysql://localhost:3306/spring_db")
    private String url;
    @Value("root")
    private String userName;
    @Value("password")
    private String password;
	@Bean
    public DataSource dataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(userName);
        ds.setPassword(password);
        return ds;
    }
}

引用数据类型

假设在构建 DataSource 对象的时候,需要用到 BookDao 对象,该如何把 BookDao 对象注入进方法内让其使用呢?

public class JdbcConfig {
	@Bean
    public DataSource dataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/spring_db");
        ds.setUsername("root");
        ds.setPassword("root");
        return ds;
    }
}

引用类型注入只需要为 bean 定义方法设置形参即可,容器会根据类型自动装配对象。

@Bean
public DataSource dataSource(BookDao bookDao){
    System.out.println(bookDao);
    DruidDataSource ds = new DruidDataSource();
    ds.setDriverClassName(driver);
    ds.setUrl(url);
    ds.setUsername(userName);
    ds.setPassword(password);
    return ds;
}

纯注解开发小结

  @ComponentScan({com.example.service","com.example.dao"})  

涉及到的注解

@Configuration
名称 @Configuration
类型 类注解
位置 类定义上方
作用 设置该类为 spring 配置类
属性 value(默认):定义 bean 的 id
@ComponentScan
名称 @ComponentScan
类型 类注解
位置 类定义上方
作用 设置 spring 配置类扫描路径,用于加载使用注解格式定义的 bean
属性 value(默认):扫描路径,此路径可以逐层向下扫描
@Autowired
名称 @Autowired
类型 属性注解或方法注解(了解) 或方法形参注解(了解)
位置 属性定义上方或标准 set 方法上方或类 set 方法上方或方法形参前面
作用 为引用类型属性设置值
属性 required:true/false,定义该属性是否允许为 null
@Qualifier
名称 @Qualifier
类型 属性注解或方法注解(了解)
位置 属性定义上方或标准 set 方法上方或类 set 方法上方
作用 为引用类型属性指定注入的 beanId
属性 value(默认):设置注入的 beanId
@Value
名称 @Value
类型 属性注解或方法注解(了解)
位置 属性定义上方或标准 set 方法上方或类 set 方法上方
作用 为基本数据类型或字符串类型属性设置值
属性 value(默认):要注入的属性值
@PropertySource
名称 @PropertySource
类型 类注解
位置 类定义上方
作用 加载 properties 文件中的属性值
属性 value(默认):设置加载的 properties 文件对应的文件名或文件名组成的数组

bean.xml 和注解对应关系

XML 配置和注解差异: