Spring学习之IOC

认识Spring

Spring是一个开源框架,目的是为了简化Java开发。
为了降低Java开发的复杂性,Spring采取了以下4种策略:

  • 基于POJO的轻量级和最小侵入性编程;
  • 通过依赖注入和面向接口实现松耦合;
  • 基于切面和惯例进行声明式编程;
  • 通过切面和模板减少样板式代码;

POJO

POJO 全称是Plain Ordinary Java Object,翻译过来即普通Java类。普通的一个类为什么要用POJO来称呼那?直接说一个类不就完了嘛。POJO主要用来指代那些没有遵从特定的Java对象模型、约定或框架的Java对象,强调的是不受约束。

依赖注入(DI)

当一个类A中需要用到另一个类B的时,如下面所示:

public  class A {
    private B b;

    public A() {
        b = new B();
    }
}

这里类A与类B就存在了耦合,为避免这种耦合,我们不应该在类A中创建B的实例,而是交给第三方,把对B的控制权叫出来,所以称之为控制反转(IOC)。那既然类B不是在类A中创建,那么如何才能把类B的实例交给类A那?要么通过构造,要么通过set方法,而这就是依赖注入(DI)。

    class A {
        private B b;

        public A(B b) {
            this.b = b;
        }
    }

依赖注入实现了控制反转,实现了松耦合。但是也导致要写更多的代码,例如我们要是上面的类A,可能需要这样写:

B b = new B();
A a = new A(b);

这是最简单的情况,如果类A依赖的类很多,则需要一个个实例化被依赖的类,然后注入到类A中。而Spring可以帮我们省下这些代码。通过容器管理bean,也即是类A,类B等。

Spring容器

Spring容器可以归纳为两种:BeanFactory和ApplicationContext。通常我们会选择ApplicationContext,它提供了应用框架级别的服务,例如从属性文件解析文本信息,以及发布应用事件。
ApplicationContext有多种实现:

  • AnnotationConfigApplicationContext:从一个或多个基于Java的配置类中加载Spring应用上下文。
  • AnnotationConfigWebApplicationContext:从一个或多个基于Java的配置类中加载Spring Web应用上下文。
  • ClassPathXmlApplicationContext:从类路径下的一个或多个xml配置文件中加载上下文定义,把应用上下文的定义文件作为类资源。
  • FileSystemXmlApplicationContext:从文件系统下的一个或多个xml配置文件中加载上下文定义。
  • XmlWebApplicationContext:从web应用下的一个或多个xml配置文件中加载上下文定义。

其实区别也就是从不同的地方加载bean的配置文件。

装配bean

Spring容器负责创建应用程序中的bean并通过DI来协调这些对象之间的关系。而我们要做事情则是告诉Spring容器,哪些是bean。我们有三种装配机制可以选择:

  1. 在xml中进行显示配置;
  2. 在Java中进行显示配置;
  3. 隐式的bean发现机制和自动装配;

准备

在装配之前,我们先来准备几个POJO类。
一个cd接口:

public interface CD {
    void play();
}

一个cd接口的实现类:

public class SgtPeppers implements CD {

    @Override
    public void play() {
        System.out.println("Playing Sgt. Pepper's Lonely Heart Club Band by The Beatles");
    }
}

一个cd播放器用于播放cd:

public class CDPlayer {

    private CD cd;

    public CDPlayer(CD cd) {
        this.cd = cd;
    }

    public void play() {
        cd.play();
    }
}

自动装配bean

我们通过@Configuration注解一个类表示这个类为Spring的配置类。并且通过@ComponentScan注解开启自动扫描bean:

@Configuration
@ComponentScan
public class AutoConfig {
}

@ComponentScan默认会扫描与配置类同级以及子级包中所有带有@Component注解的类,自动创建为一个bean。 我们在需要装配的bean上添加注解:

@Component
public class SgtPeppers implements CD {
    ...
}

@Component("player")
public class CDPlayer {
    ...
}

接下来,我们可以AnnotationConfigApplicationContext类加载Spring配置类,看下SgtPeppers是否被自动扫描并创建了bean:

        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AutoConfig.class);
        System.out.println("----------------------------------------------");
        String[] names = applicationContext.getBeanDefinitionNames();
        System.out.println(Arrays.toString(names));
        System.out.println("----------------------------------------------");

        CD cd = (CD) applicationContext.getBean("sgtPeppers");
        System.out.println(cd);

        CDPlayer cdPlayer = (CDPlayer) applicationContext.getBean("player");
        System.out.println(cdPlayer);
        cdPlayer.play();

输出的是一个内存地址值,SgtPeppers已经被自动扫描发现,并创建。可以看到我们是通过sgtPeppers来找到类SgtPeppers的bean,这也是默认的id——类名(首字母小写),我们也可以通过@Component("key1") 来手动指定该bean的id为key1 。
你可能注意到CDPlayer的构造需要一个CD类型的参数,Spring会自动查找装配的bean是否有符合该参数的类型,如果发现有则自动传入,如果没有查找到符合类型的bean则会抛出NoSuchBeanDefinitionException异常。

设置组件扫描的基础包

有时候bean和config类可能并不在同级包中的话,那就需要设置扫描的基础包:

@Configuration
@ComponentScan("com.hubert")
public class AutoConfig { }

如果有多个地方需要扫描也可以这样定义:

@Configuration
@ComponentScan(basePackages = {"com.hubert", "music"})
public class AutoConfig { }

除了用String这种硬编码的声明,也可以传入class对象,即将class对象所在的包作为基础包。

@Configuration
@ComponentScan(basePackageClasses = ComponentPackageMaker.class)//扫描自动装载
public class AutoConfig {

这里的ComponentPackageMaker类是一个空接口,用来标识基础包的位置。

目前我们实现了将bean放入Spring容器,除了bean之间构造参数的强制依赖关系会自动注入bean之外,我们也可以通过@Autowired 注解在方法或属性上实现bean的自动注入。例如这里有另一个cd播放器,它通过set方法实现注入:

@Component
public class OtherPlayer {
    private CD cd;

    @Autowired
    public void setCd(CD cd) {
        this.cd = cd;
    }

    public void play() {
        cd.play();
    }
}

我们可以验证下否正确注入:

        OtherPlayer otherPlayer = (OtherPlayer) applicationContext.getBean("otherPlayer");
        System.out.println(otherPlayer);
        otherPlayer.play();

注意这里与构造参数注入不同,构造是强制的,就算没有添加@Autowired 注解,也必须依赖相对应的bean,而set方法注入如果没有添加@Autowired 注解则不会调用该方法注入,因此不要忘记添加注解。

如果没有匹配到bean,在创建Context的时候Spring会抛出异常。为了避免异常,可以修改为@Autowired(required = false) 表示不是必须的bean,当然这样做之后你就得考虑为null的情况了。

通过Java代码装配bean

这里我们还是一个@Configuration 注解的配置类:

@Configuration
public class JavaConfig {
}

接着可以在配置类中声明bean了:

    @Bean
    public CD cd() {
        return new SgtPeppers();
    }

这种方式声明的bean默认id是方法名,这里就是"cd",也可以通过@Bean注解的name属性指定id:

    @Bean(name = "myCd")
    public CD cd() {
        return new SgtPeppers();
    }

CDPlayer的构造需要一个CD作为参数,这个时候我们可以把需要依赖的bean设置为方法参数,这样在创建cdPlayer这个bean的时候,容器会去自动查找匹配参数的bean自动装配。

    @Bean
    public CDPlayer cdPlayer(CD cd) {
        return new CDPlayer(cd);
    }

同样的,我们可以通过AnnotationConfigApplicationContext加载配置类验证bean的装载情况。

通过xml装配bean

最初的时候xml是Spring配置的主要方式,虽然相比于JavaConfig显得过于繁琐。但在无法在代码中添加@bean等Spirng注解的时候(如第三方库中),使用xml也是不错的选择。

首先我们需要一个xml文件,并且其中以元素作为根节点:





xmlns 是命名空间,表示声明xml中使用的标签来源,也方便IDE提示验证xml文件的正确性。

声明一个bean

我们在xml中声明一个cd:

      

对于有构造参数的bean需要这样声明:

    
        
    

属性注入:

    
        
    

我们使用ClassPathXmlApplicationContext来加载xml配置文件装载bean:

        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("my-beans.xml");
        System.out.println("----------------------------------------------");
        String[] names = applicationContext.getBeanDefinitionNames();
        System.out.println(Arrays.toString(names));
        System.out.println("----------------------------------------------");

        CD cd = (CD) applicationContext.getBean("sgtPeppers");
        System.out.println(cd);

        CDPlayer cdPlayer = (CDPlayer) applicationContext.getBean("player");
        System.out.println(cdPlayer);
        cdPlayer.play();

        OtherPlayer otherPlayer = (OtherPlayer) applicationContext.getBean("otherPlayer");
        System.out.println(otherPlayer);
        otherPlayer.play();

Spring3之后引入了c命名空间,来简化构造声明:




    

    

启动需要新增了一个命名空间:xmlns:c="http://www.springframework.org/schema/c"
我们通过c:cd-ref="cd"声明CdPlayer的构造参数,其中c:是命名空间前缀,cd是构造参数名,-ref表示引用类型,="sgtPeppers"指向id为sgtPeppers的bean。

与c命名空间类似的还有p命名空间,用于简化属性声明:

    

同样需要新增一个命名空间:xmlns:p="http://www.springframework.org/schema/p"

混合使用

假设我们有两个以上的Spring配置,其中有JavaConfig也有xml配置。我们可以使用import将所有的config归并到一起。
在JavaConfig中可以使用@Import注解来导入其他配置:

@Configuration
@Import(AutoConfig.class)//导入其他JavaConfig
@ImportResource("my-beans.xml")//导入xml配置
public class JavaConfig {

在xml中同样使用标签导入其他配置:




    
    
    
    
    
    

注意这里导入Java配置的方式并不是用import标签,而是用bean表示。

不管是使用JavaConfig还是xml进行装配,通常都会创建一个根配置,根配置不装配具体的bean,而是用于组合多个其他配置。

处理自动装配的歧义性

前面我们讲到可以通过@Autowired 注解自动注入对应的bean,但有时候,可能注册了多个相同类型的bean,这时候就会发生歧义,因为Spring容器不知道应该使用哪个bean进行注入,例如下面这种情况:

@Autowired
public void setDessert(Dessert dessert) {
    this.dessert = dessert;
}

Dessert是一个接口,并且我们有三个类实现了这个接口:

@Component
public class Cake implements Dessert {...}

@Component
public class Cookies implements Dessert {...}

@Component
public class IceCream implements Dessert {...}

因为这个三个实现类都使用了@Component 注解,组件扫描的时候能够发现并创建为bean。但是在试图自动装配setDessert 时无法选择唯一的值,会抛出NoUniqueBeanDefinitationException 。
Spring提供了多种可选方案来解决这样的问题:

  • 将某一个bean设置为首选(primary);
  • 使用限定符(qualifier)缩小bean的范围到只有一个bean;

Primary

我们可以使用@Primary 注解来标记首选bean:

@Component
@Primary
public class Cake implements Dessert {...}

首选消除了歧义性,使得自动装配能够正确执行。需要注意首选标记的唯一性,如果存在有个Dessert实现类的bean都标记了@Primary ,那首选也就失去了作用。

Qualifier

我们也可以使用@Qualifier来限定注入的bean,下面是直接限定了bean的id:

@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert) {
    this.dessert = dessert;
}

环境与Profile

在开发中通常都会存在不同的环境使用不同的配置,如Database。Spring提供了Profile来指定bean所属的环境,只有相应的环境才会装配该bean。
在Java配置中,可以使用@Profile 注解指定bean所属的环境:

@Configuration
public class DataSourceConfig {

    @Bean(destroyMethod = "shutdown")
    @Profile("dev")
    public DataSource embeddedDataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("classpath:scheme.sql")
                .addScript("classpath:test-data.sql")
                .build();
    }

    @Bean
    @Profile("prod")
    public DataSource jndiDataSource() {
        JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
        jndiObjectFactoryBean.setJndiName("jdbc/myDS");
        jndiObjectFactoryBean.setResourceRef(true);
        jndiObjectFactoryBean.setProxyInterface(DataSource.class);
        return (DataSource) jndiObjectFactoryBean.getObject();
    }
}

@Profile 也可以与@Configuration 同时注解Config类,表示该配置类中所有bean都属于该环境。

标明bean所属的环境,接下来就是激活profile。Spring首先读取spring.profiles.active 属性获取指定激活profile,如果没有指定,则使用spring.profiles.default属性指定的默认profile。如果spring.profiles.default属性也没有指定,则只装配没有被profile标记的bean。

条件化的bean

假设你希望一个或多个bean只有在应用的类路径下包含特定的库时才创建。这种依赖于某种条件的情况下才装配bean的情形在Spring4之后得到了支持。我们可以使用@Conditional 注解设置条件,如果给定的条件满足则会创建这个bean,否则不会装配。

    @Bean
    @Conditional(MyCondition.class)
    public CD cd() {
        return new SgtPeppers();
    }

@Conditional 注解需要一个Condition接口的实现类作为参数:

package org.springframework.context.annotation;

import org.springframework.core.type.AnnotatedTypeMetadata;

public interface Condition {
    boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

实现Condition接口并实现matches方法,返回true表示满足条件,返回false表示不满足条件。
我们可以借助ConditionContext判断各种情况:

  • 借助getRegistry() 返回的BeanDefinitionRegistry检查bean定义;
  • 借助getBeanFactory() 返回的ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性;
  • 借助getEnvironment() 返回的Environment检查环境变量是否存在以及它的值是什么;
  • 读取并探查getResourceLoader() 返回的ResourceLoader所加载的资源;
  • 借助getClassLoader() 返回的ClassLoader加载并检查类是否存在;

AnnotatedTypeMetadata则能够让我们检查带有@Bean 注解的方法上还有什么其他的注解。

bean的作用域

Spring定义了多种作用域:

  • 单例(Singleton):在整个应用中,只创建bean的一个实例。
  • 原型(Prototype):每次注入或者通过Spring应用上下文获取的实例,都会创建一个新的bean实例。
  • 会话(Session):在Web应用中,为每个会话创建一个bean实例。
  • 请求(Request):在Web应用中,为每个请求创建一个bean实例。

默认情况下,Spring应用中所有的bean都是以单例(singleton)的形式创建的。我们可以使用@Scope 注解改变默认作用域:

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad {...}

ConfigurableBeanFactory.SCOPE_PROTOTYPE的值是字符串"prototype" , 你也可以直接使用这个字符串,但用常量不容易出现拼写错误。

在Web应用中通常会使用会话和请求范围内共享的bean,例如购物车bean:

@Bean
@Score(value=WebApplicationContext.SCOPE_SESSION, 
              proxyMode=ScopedProxyMode.INTERFACES)
public ShoppingCart cart() { ... }

这里我们将ShoppingCart的声明周期设置为session,对于同一个会话只会创建一个ShoppingCart实例。要注意这里还有另一个proxyMode属性,这个属性解决的是一个短生命周期的bean注入到长生命周期bean中的问题。
假设我们要将ShoppingCart的bean注入到单例StoreService中:

@Component
public class StoreService {

    @Autowired
    public void setShoppingCart(ShoppingCart shoppingCart) {
        this.shoppingCart = shoppingCart;
    }
}

StoreService是一个单例的bean,当它创建的时候,Spring会试图将ShoppingCart注入到setShoppingCart()方法中。但是ShoppingCart是会话作用域的,此时并不存在,直到某个用户进入系统,创建了会话之后,才会出现ShoppingCart实例。另外,系统中将会存在多个ShoppingCart实例,我们不想让Spring注入某个固定的ShoppingCart实例到StoreService中。我们希望的是当StoreService处理购物车功能时,它所使用的ShoppingCart实例恰好是当前会话所对应的那一个。
所以Spring并不会将实际的ShoppingCart bean注入到StoreService中,Spring会注入到一个ShoppingCart bean的代理。这个代理会暴露与ShoppingCart相同的方法,所以StoreService会认为它就是一个购物车。当StoreService调用ShoppingCart的方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的ShoppingCart bean。
proxyMode属性声明了代理的方式,ScopedProxyMode.INTERFACES 表明这个代理要实现ShoppingCart接口。但如果注入的bean是一个类不是接口,Spring就没有办法创建基于接口的代理了。这时候则需要设置proxyMode属性为ScopedProxyMode.TARGET_CLASS ,以此表明要以生成目标类扩展的方式创建代理。

运行时注入

在Spring中处理外部值的最简单方式就是声明属性源并通过Spring的Environment来检索属性。
我们先在resource文件夹中创建一个app.properties声明属性值,内容是=链接的键值对。

cd.title=this is cd title
cd.author=hubert

然后在config中通过@PropertySource 注解引入app.properties

@Configuration
@PropertySource("app.properties")
public class PropertiesConfig {

    private Environment env;

    @Autowired
    public PropertiesConfig(Environment env) {
        this.env = env;
    }

    @Bean
    public BlackDisc disc() {
        return new BlackDisc(
                env.getProperty("cd.title"),
                env.getProperty("cd.author"));
    }
}

BlackDisc的构造需要两个String类型的title和author,这里通过Environment的getProperty 方法获取我们在外部声明的属性。getProperty 方法还有几个重载方法,可以传入默认值或者转换目标类型(Class)。

getProperty 方法在没有传入默认值的情况下,如果属性没有定义,则获取到null。如果你希望该属性是必须的,可以使用getRequiredProperty()方法。使用该方法获取属性,如果属性没有定义,则会抛出IllegalStateException异常。

你可能感兴趣的:(Spring学习之IOC)