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文件,并且其中以<beans>元素作为根节点:

<?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">

</beans>

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

声明一个bean

我们在xml中声明一个cd:

      <bean id="cd" class="com.hubert.spring.component.SgtPeppers"/>

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

    <bean id="cdPlayer" class="com.hubert.spring.component.CDPlayer">
        <constructor-arg ref="cd"/>
    </bean>

属性注入:

    <bean id="otherPlayer" class="com.hubert.spring.component.OtherPlayer">
        <property name="cd" ref="sgtPeppers"/>
    </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命名空间,来简化构造声明:

<?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:c="http://www.springframework.org/schema/c"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="sgtPeppers" class="com.hubert.spring.component.SgtPeppers"/>

    <bean id="player" class="com.hubert.spring.component.CDPlayer"
          c:cd-ref="sgtPeppers"/>
</beans>

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

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

    <bean id="otherPlayer" class="com.hubert.spring.component.OtherPlayer"
          p:cd-ref="sgtPeppers"/>

同样需要新增一个命名空间: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中同样使用<import>标签导入其他配置:

<?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:c="http://www.springframework.org/schema/c"
       xmlns:p="http://www.springframework.org/schema/p" 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="com.hubert.spring.component"/>
    <!--导入其他配置-->
    <import resource="other-beans.xml"/>
    <!--导入java配置-->
    <bean class="com.hubert.spring.component.JavaConfig"/>
</beans>

注意这里导入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<T>)。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容