zoukankan      html  css  js  c++  java
  • 全注解下的Spring IoC

    一、IoC容器简介

    ​ IoC容器是Spring的核心,可以说Spring是一种基于IoC容器编程的框架。IoC是一种通过描述来生成或者获取对象的技术。Java初学者更多的时候熟悉的是使用new关键字来创建对象,而Spring是通过描述来创建对象的。

    ​ 在Spring中把每一个需要管理的对象称为Spring Bean(简称Bean),而Spring管理这些Bean的容器,被我们称为Spring IoC容器(或者简称IoC容器)。IoC容器具备两个基本的功能:

    • 通过描述管理Bean,包括发布和获取Bean;
    • 通过描述完成Bean之间的依赖关系。

    ​ Spring IoC容器是一个管理Bean的容器,在Spring的定义中,所有的IoC容器都需要实现接口BeanFactory,它是一个顶级容器接口。我们需要注意接口中的几个方法:

    • 首先是多个getBean方法,这是IoC容器中最重要的方法之一,它的作用是从IoC容器中获取Bean,我们可以通过Bean的类型或者名称来获取Bean。
    • isSingleton方法用来判断Bean是否在Spring IoC中为单例。这里需要记住的是在Spring IoC容器中,默认的情况下Bean都是一单例存在的,也就是说getBean方法返回的都是同一个对象。
    • 与isSingleton方法相反的是isPrototype方法,如果它返回的是true,那么当我们使用getBean方法获取Bean的时候,Spring IoC容器就会创建一个新的Bean返回给调用者。

    ​ 由于BeanFactory的功能还不够强大,因此Spring在BeanFactory的基础上,还设计了一个更为高级的接口ApplicationContext。它是BeanFactory的子接口之一,在Spring的体系中BeanFactoryApplicationContext是最为重要的接口设计,在现实中我们使用的大部分Spring IoC容器都是ApplicationContext接口的实现类。

    ​ 在Spring Boot当中我们主要是通过注解来装配Bean到Spring IoC容器中,为了贴近Spring Boot的需要,这里不再介绍与XML相关的IoC容器,而主要介绍一个基于注解的IoC容器,它就是AnnotationConfigApplicationContext类,从名称就可以看出它是一个基于注解的IoC容器。

    下面来演示一个简单的例子:

    首先定义一个Java简单对象(Plain Ordinary Java Object)简称POJO:

    public class User{
    	private Long id;
    	private String userName;
    	private String note;
    	
    	/**setter and getter **/
    }
    

    接着定义一个Java配置文件:

    //@Configuration代表这是一个Java配置文件,Spring的容器会根据它来生成IoC容器去装配Bean
    @Configuration 
    public class AppConfig{
    	/*
    		@Bean代表将initUser方法返回的POJO装配到IoC容器中,而其属性name定义这个Bean的名称,如果没有指定		 名称,则将方法名initUser作为Bean的名称保存到Spring IoC容器中。
    	/*
    	@Bean(name = "user")
    	public User initUser(){
    		User user = new User();
    		user.setId(1L);
    		user.setUserName("user_name_1");
    		user.setNote("note_1");
    		return user;
    	}
    }
    

    最后使用AnnotationConfigApplicationContext类来构建自己的IoC容器:

    public class IoCTest{
    	public static void main(String[] args){
    		/*
    			将Java配置文件AppConfig传递给AnnotationConfigApplicationContext类的构造方法,这样它就			  可以读取配置了,然后将配置里面的Bean装配到IoC容器中,接着就可以使用getBean方法获取对应的				POJO了。
    		/*
    		ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    		User user = ctx.geBean(User.class);
    		System.out.println(user.getId);
    	}
    }
    

    二、装配你的Bean

    1. 通过扫描装配你的Bean

    ​ 如果Bean使用注解@Bean一个个地注入到IoC容器中,那将是一件很麻烦的事。好在Spring还允许我们通过扫描装配Bean到IoC容器中。对于扫描装配而言需要用到两个注解@Component和@ComponentScan。其中@Component用来标明哪个类被扫描到IoC容器中,而@ComponentScan用来标明采用何种策略去扫描装配Bean。

    这里我们通过对上面例子的修改来演示如何通过扫描的方式来装配Bean:

    /*
    	注解@Component表明这个类将被IoC容器扫描装配,其中配置的"user"作为Bean的名称,当然你也可以不配置这个	 字符串,那么IoC容器就会把类名的第一个字母小写,其他不变作为Bean的名称放入到IoC容器中。
    */
    @Component("user")
    public class User{
    	// 注解@Value则是指定具体的值,使得IoC容器给对应的属性注入相应的值。
    	@Value("1")
    	private Long id;
    	@Value("user_name_1")
    	private String userName;
    	@Value("note_1")
    	private String note;
    	
    	/**setter and getter **/
    }
    

    为了让IoC容器装配这个类,需要改造AppConfig类:

    @Configuration
    // 这里加入了@ComponentScan注解,意味着它会进行扫描,但是它只会扫描AppConfig类所在的包及其子包中的Bean
    @ComponentScan
    public class AppConfig{
    }
    

    测试扫描:

    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    User user = ctx.geBean(User.class);
    System.out.println(user.getId);
    

    这样就能够运行了。然而之前为了使得User类能够被扫描,我们需要把它放到AppConfig类所在的包中,这样显然是不合理的。因此@ComponentScan注解还允许我们自定义扫描的包。

    // 修改AppConfig中的注解,使它扫描com.springboot.chapter3包及其子包
    @ComponentScan("com.springboot.chapter3.*")
    

    过滤器:

    // 这样可以使得com.springboot.chapter3包下的UserService类不被IoC容器扫描注入
    @ComponentScan(basePackages = "com.springboot.chapter3.*",
    	excludeFilters = {@Filter(classes = {UserService.class})})
    

    2. 通过@Bean注解将第三方Bean放入到IoC容器中

    ​ 现实中Java的应用往往需要引入许多来自第三方的包,并且很有可能希望把第三方包中的类也存放到IoC容器中,这时就又可以使用@Bean注解来完成了。

    三、依赖注入(DI)

    这里我们通过一个人类依赖于动物的例子来讲解依赖注入。

    首先定义人类和动物接口:

    public interface Person{
    	// 使用动物服务
    	public void service();
    	
    	// 设置动物
    	public void setAnimal(Animal animal);
    }
    
    public interface Animal{
    	public void use();
    }
    

    接着定义两个实现类:

    @Component
    public class BussinessPerson implements Person{
    	/*
    		@Autowired注解会根据属性的类型(这里是Animal类)找到对应的Bean进行注入。狗是动物的一种,所以IoC		   容器会把Dog的实例注入BussinessPerson实例中。这样当通过IoC容器获取BussinessPerson实例的时候就		   能够使用Dog实例来提供服务了。
    	*/
    	@Autowired
    	private Animal animal = null;
    	
    	@Override
    	public void service(){
    		this.animal.use();
    	}
    	
    	@Override
    	public void setAnimal(Animal animal){
    		this.animal = animal;
    	}
    }
    
    @Component
    public class Dog implements Animal{
    	@Override
    	public void use(){
    		System.out.println("狗是用来看门的。");
    	}
    }
    

    测试代码:

    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    Person person = ctx.getBean(BussinessPerson.class);
    person.service();// 会输出:"狗是用来看门的。"
    

    1. 注解@Autowired

    ​ 此时我们来思考一个问题。假如我们再定义一个Cat类:

    @Component
    public class Cat implements Animal{
    	@Override
    	public void use(){
    		System.out.println("猫是用来抓老鼠的。");
    	}
    }
    

    ​ 好了,如果我们还是使用之前的BussinessPerson类,那么麻烦来了,因为这个类只是定义了一个动物属性(Animal),而我们却有两个动物,一个狗,一个猫,IoC容器该如何注入呢?如果进行测试,我们会发现IoC容器抛出异常。因为IoC容器并不能知道你想要注入什么动物(是狗?是猫?)给BussinessPerson类对象。

    ​ 假设我们目前想要狗来提供服务,此时我们需要修改代码:

    @Autowired
    private Animal animal = null;
    

    ​ 修改为

    @Autowired
    private Animal dog = null;
    

    ​ 注解@Autowired首先会根据类型找到对应的Bean,如果对应类型的Bean不是唯一的,那么它会根据其属性名称和Bean的名称进行匹配。如果匹配得上,就会使用该Bean;如果还无法匹配,就会抛出异常。

    ​ 注解@Autowired除了可以标注属性外,还可以标注方法,如setAnimal方法,如下所示:

    @Override
    @Autowired
    public void setAnimal(Animal animal){
    	this.animal = animal;
    }
    

    ​ 这样它会使用setAnimal方法从IoC容器中找到对应的动物进行注入。

    ​ 我们还可以把注解@Autowired使用在方法的参数上。

    2. 消除歧义性——@Primary和@Qualifier

    ​ 在上面我们发现有猫有狗的时候,为了使@Autowired能够继续使用,我们将BussinessPerson类的属性名称从animal修改为dog。这种做法显然不是很合理。所以我们需要有更好的方式来解决歧义性问题。

    ​ 首先是注解@Primary,它是一个用来修改优先权的注解。当有猫有狗的时候,假设这次需要使用猫,那么只需要在猫类的定义上加上@Primary就可以了,Cat的实例会被优先注入。但是当有多个类都被添加了@Primary注解时,IoC容器还是无法区分应该采用哪个Bean的实例进行注入,看样子我们还需要一种更加灵活的机制来实现注入。

    ​ 注解@Qualifier可以实现这个愿望。它的配置项value需要一个字符串去定义,它将与@Autowired组合在一起,通过类型和名称一起找到Bean。我们知道Bean名称在IoC容器中是唯一的标识,通过这个就可以消除歧义性了。

    ​ 下面我们假设猫已经标注了@Primary,而我们需要的是狗提供服务,因此需要修改BussinessPerson类的属性animal的标注来满足我们的需求,如下所示:

    @Autowired
    @Qualifier("dog")
    private Animal animal = null;
    

    ​ 一旦这样声明,IoC容器就会以类型和名称去寻找对应的Bean进行注入。那么根据类型Animal,名称dog,显然也只能找到狗为我们服务了。

    3. 带有参数的构造方法类的装配

    ​ 在之前,我们都基于一个默认的情况,那就是不带参数的构造方法下实现依赖注入。但事实上,有些类只有带参数的构造方法,于是上述的方法都不能再使用了。为了满足这个功能,我们可以使用@Autowired注解对构造方法的参数进行注入。例如,修改BussinessPerson类来满足这个功能:

    @Component
    public class BussinessPerson implements Person{
    
    	private Animal animal = null;
    	
    	public BussinessPerson(@Autowired @Qualifier("dog") Animal animal){
    		this.animal = animal;
    	}
    	
    	@Override
    	public void service(){
    		this.animal.use();
    	}
    	
    	@Override
    	public void setAnimal(Animal animal){
    		this.animal = animal;
    	}
    }
    

    四、Bean的生命周期

    Bean的生命周期大致可以分为Bean的定义、Bean的初始化、Bean的生存期和Bean的销毁这4个部分。

    1. Bean的定义过程

    • 资源定位:Spring通过我们的配置,如@ComponentScan定义的扫描路径去找到带有@Component的类,这个过程就是一个资源定位的过程。
    • Bean定义:一旦找到了资源,那么它就开始解析,并且将定义的信息保存起来。注意,此时还没有初始化Bean,也就没有Bean的实例,它有的仅仅是Bean的定义。
    • 发布Bean定义:然后就会把Bean的定义发布到IoC容器中。此时IoC容器中也只有Bean的定义,还是没有Bean的实例生成。

    完成了这3步只是一个资源定位并将Bean的定义发布到IoC容器的过程,还没有Bean实例的生成,更没有完成依赖注入。

    2. Bean的初始化过程

    • 实例化:创建Bean的实例对象。
    • 依赖注入(DI):完成依赖注入,例如@Autowired注入的各类资源。

    ​ 假如我们希望只是将Bean的定义发布到IoC容器而不做实例化和依赖注入,只有当我们取出Bean的时候才去完成实例化和依赖注入,即延迟初始化。我们需要用到注解@ComponentScan中的一个配置项lazyInit,我们需要将该配置项的值设置为true,这样就可以实现延迟初始化了。

    五、使用属性文件

    六、条件装配Bean

    七、Bean的作用域

    1. singleton

    ​ 默认值,IoC容器只存在单例。

    2. prototype

    ​ 每当从IoC容器中取出一个Bean,则创建一个新的Bean。

    ​ 如果想要让一个类的作用域为prototype,则需要在类上加上以下注解:

    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    

    八、使用@Profile

    ​ Spring提供的Profile机制使我们可以很方便地实现各个环境(发开环境、测试环境、准生产环境、生产环境)之间的切换。

    九、引入XML配置Bean

    十、使用Spring EL

  • 相关阅读:
    谈谈Android重打包--初语
    Servlet注解
    提高打开Android本地文档的速度
    内置传感器---智能手机(资料)
    数学分析里面的蕴含(⇒)究竟是什么意思
    [Xcode 实际操作]七、文件与数据-(20)CoreML机器学习框架:检测和识别图片中的物体
    [Xcode 实际操作]七、文件与数据-(19)颜色集(Color Set)的使用
    [Xcode 实际操作]七、文件与数据-(18)使用MarkMan与设计师进行心灵沟通
    [Xcode 实际操作]七、文件与数据-(17)解析JSON文档
    [Xcode 实际操作]七、文件与数据-(16)解析XML文档
  • 原文地址:https://www.cnblogs.com/jiajun107/p/13051622.html
Copyright © 2011-2022 走看看