Spring_IOC

依赖注入

Spring的核心之一控制反转,即不需要我们来手动创建对象,而是把创建对象的职责交给Spring来去做,我们通过IOC容器来直接获取这个对象。完成这一操作,我们需要做的仅仅是告诉Spring,它需要帮我们创建哪些对象。而有的对象本身内部聚合/组合了其他对象(成员变量),我们也不需要去为这些成员变量赋值,Spring容器负责创建应用程序中的bean并通过DI(依赖注入)来协调这些对象之间的关系,我们需要告诉Spring要创建那些bean并且如何将它们装配到一起,Spring提供了三种主要的装配机制

  • 在XML中进行显式的配置
  • 在Java中进行显示的配置(JavaConfig)
  • 隐式的bean发现机制和自动装配

几乎所有的课件都是基于XML的方式来讲述spring的知识,但是Spring4.0作者推荐使用自动化配置搭配JavaConfig,JavaConfig我真的爱了爱了,这很Java,包括后面的SpringBoot也可以看出来,XML作用会越来越弱化。

装配Bean

自动装配

Spring从两个角度来实现自动化装配

  • 组件扫描
    Spring会自动发现上下文中创建的bean,即上面说的,我们要告诉Spring,哪些对象是要交给Spring来管理的
  • 自动装配
    Spring会自动满足bean之间的依赖,从IOC容器中取出对象,自动赋值给需要这个对象的类

@Component
这个注解放在类名上,这个注解表明这个类会作为组件类,Spring会为标注了这个注解的类创建bean并放在IOC容器中。这就是控制反转,创建对象的工作由Spring帮我们完成。bean的ID默认是类名首字母小写,我们可以主动设置Value值来给bean指定ID
@Named
这个注解和@Component的功能用法基本相同,是由Java Dependency Injection提供的

@Component(value = "beanID")
public class Human{}
@Named(value = "beanID")
public class Teacher{}

@ComponentScan
这个注解用于开启组件扫描,默认会扫描与配置类相同的包以及这个包下面的子包。可以通过指定value属性的值来扫描指定的包

//通过设置value值来指定要扫描的包及其子包
@ComponentScan("soundsystem")
//也可以通过basePackages属性来设置
@ComponentScan(basePackages="soundsystem")
//basePackages可以设置扫描多个包,这时候传入一个数组
@ComponentScan(basePackages={"soundsystem","video"})
//上述的方式都是将包名以String的方式传入,这种方式是类型不安全的,重构代码会导致包名改变
//可以将其指定为包中所含的类或接口,指定的类所在的包会作为扫描的基础包
//建议在包中创建一个用来进行扫描的空接口,因为重构可能会导致应用代码从想要扫描的包中移除掉
@ComponentScan(basePackageClasses={"CDplayer.class","DVDPlayer.class"})
<!--开启自动扫描-->
<context:component-scan base-package="包名">

自动装配就是让Spring自动满足bean依赖的一种方法,在满足依赖的过程中,会在Spring应用上下文中寻找匹配某个bean的其他bean,我们使用@AutoWired来声明要进行自动装配
@AutoWired

public class CDPlayer implements MediaPlayer{
    //直接放在成员变量上
    @AutoWired
    private CompactDisc cd;
    //构造器注入
    @AutoWired
    public CDPlayer(CopmactDisc cd){
        this.cd=cd;   
    }
    //属性注入
    //将安全检查设为false,允许找不到装配的bean,默认为true,没有匹配的bean会抛出异常
    @AutoWired(required=false)
    public void setCompactDisc(CopmactDisc cd){
        this.cd=cd;
    }
}

@Inject
这个注解来源于Java依赖注入规范,和@AutoWired基本相同

不管构造器,Setter方法还是其他方法,Spring都会尝试满足方法参数上声明的依赖。假如有且仅有一个bean匹配依赖需求的话,那么这个bean将会被装配进来,但如果没有匹配的bean,那么会抛出异常,为了避免异常的出现,可以把@AtuoWired的required的属性设置为false。但如果有多个匹配的bean呢?在后面我们会单独讲述自动装配的歧义性

显式装配

尽管组件扫描和自动装配来实现Spring的自动化配置是推荐方式,但有的时候,我们没办法在一个类上面通过添加@Component和@AutoWired注解(比如第三方组件)来将这些组件装配到工程中,这种情况我们就需要显示装配

通过Java代码装配

JavaConfig是我很喜欢的装配方案,相对于自动装配,显示装配显得更有据可循和信任,类型安全且易于重构,而跟XML相比,JavaConfig显得更加易于理解和可读,并且JavaConfig也是Java代码,这种一致性我是非常钟意的。JavaConfig是配置代码,不包因那个该包含任何的业务逻辑,我们通常会将JavaConfig放到单独的包中,与其他应用的逻辑分离开。

//这个注解用来表明这个类是一个配置类
@Configuration
public class CDPlayerConfig{
    //@Bean注解会告诉Spring这个方法将返回一个对象,该对象要注册为Spring应用上下文的bean。
    //方法体重包含了最终产生bean的实例逻辑,只要最后可以返回一个bean就可以,所以我们可以在这个方法里面干很多事情
    //默认情况bean的ID与带有@Bean的注解的方法名是一样的,可以通过name属性来指定ID
    @Bean(name="sgtPaperBean")
    public CompactDisc sgtPeppers(){
        return new SgtPeppers();
    }
    
    /**
     *方法请求一个CompactDisc作为参数,当Spring调用这个方法来创建对象的时候,它会自动装配一个ComoactDisc对象到配置中去
     *无论这个对象是谁创建的,只要它存在在ioc容器中就可以
    */
    @Bean
    public CDPlayer cdPlayer(CompactDisc compactDisc){
        return new CDPlayer(compactDisc);/*
    我们还可以这样进行配置,因为sgtPeppers方法被@Bean标记了,所以所有访问这个方法都会被拦截,并返回一个IOC容器中已有的对象,并不会调用一次就生成一个对象
    */
    @Bean
    public CDPlayer cdPlayer(){
        return new CDPlayer(sgtPeppers());}

@Configuration标记的类必须符合下面的要求

  • 配置类必须以类的形式提供(不能是工厂方法返回的实例),允许通过生成子类在运行时增强(CGLIB动态代理)
  • 配置类不能是final类(没法动态代理)
  • 配置注解通常为了通过@Bean注解生成Spring容器管理的类
  • 配置类必须是非私有的(即不能在方法中声明,不能是 private)
  • 任何嵌套配置类都必须声明为static
  • @Bean 方法不可能会反过来创建进一步的配置类,也就是返回的bean如果带有@Configuration,也不会被特殊处理,只会作为普通的 bean

关于JavaConfig日后再来填坑

通过XML装配Bean

麻烦的很,尤其是依赖注入这块。DI还是推荐使用注解吧!

        <!-- 配置一个 bean -->
        <!-- id就是IOC容器中bean的ID,class为bean的全路径名(反射创建对象) -->
	<bean id="helloWorld" class="com.HelllowSpring.helloworld.HelloWorld">
		<!-- 为属性赋值 -->
		<!-- 通过属性注入: 通过 setter 方法注入属性值 -->
		<property name="user" value="Tom"></property>
	</bean>
	
	<!-- 通过构造器注入属性值 -->
	<bean id="helloWorld1" class="com.HelllowSpring.helloworld.HelloWorld">
		<!-- 要求:在Bean中必须有对应的构造器.  -->
		<constructor-arg value="Mike"></constructor-arg>
	</bean>		
	
	<!-- 通过 ref 属性值指定当前属性指向哪一个bean -->
	<bean id="daoRef" class="com.HelllowSpring.helloworld.Dao"></bean>
	<bean id="service" class="com.HelllowSpring.helloworld.Service">		
		<property name="dao" ref="daoRef"></property>
	</bean>
	
	 <!-- 声明使用内部 bean -->
	 <!-- 内部 bean, 类似于匿名内部类对象。不能被外部的bean来引用,,所以没有必要设置id属性 -->
	<bean id="service2" class="com.HelllowSpring.helloworld.Service">
		<property name="dao">			
			<bean class="com.HelllowSpring.helloworld.Dao">
				<property name="dataSource" value="c3p0"></property>
			</bean>
		</property>
	</bean>	
	
	<!-- 装配集合属性 -->
	<bean id="user" class="com.HelllowSpring.helloworld.User">
		<property name="userName" value="Jack"></property>
		<property name="cars">
			<!-- 使用 list 元素来装配集合属性 -->
			<list>
				<ref bean="car"/>
				<ref bean="car1"/>
			</list>
		</property>
	</bean>
	
	<!-- 声明集合类型的 bean -->
	<util:list id="cars">
		<ref bean="car"/>
		<ref bean="car1"/>
	</util:list>
	<!-- 引用外部声明的 list -->	
	<bean id="user2" class="com.HelllowSpring.helloworld.User">
		<property name="userName" value="Rose"></property>
		<property name="cars" ref="cars"></property>
	</bean>
混合配置

Spring支持混合配置,即自动装配、XML和javaConfig可以混合在一起使用。在自动装配的时候,它并不在意要装配的bean来自哪里,自动装配会考虑到Spring容器中的所用bean,不管它是在JavaConfig或XML中声明的还是通过组件扫描获取到的

  • 在JavaConfig中引入其他配置
    @import 引入配置类
    @importResource引入XML文件
  • 在XML中引入其他配置
    JavaConfig本质上也是一个Java类,所以我们直接在XML文件中以标签引入这个类就可以了

环境与Profile

@Profile
指定某个bean属于哪个profile,只有指定的profile处于激活状态,那么这个bean才会被创建。

  • 这个注解可以添加在配置类上。如果对应的配置文件没有被激活,那么这个配置类中所有带有@Bean注解的方法都会被忽略掉。
  • 如果注解添加到方法上,那么这个方法只有规定的profile被激活了才会被创建。同类下其它没有被@Profile标记的bean方法始终都会创建,与激活哪个profile没有关系
@Configuration
public class CDPlayerConfig{
    //只有名为dev的配置文件(配置类)处于激活状态这个bean才会被Spring创建
    @Profile("dev")
    @Bean(name="sgtPaperBean")
    public CompactDisc sgtPeppers(){
        return new SgtPeppers();
    }
    //因为@Profile注解只是加在了方法上,所以这个类无论如何都会被Spring创建
    @Bean
    public CDPlayer cdPlayer(CompactDisc compactDisc){
        return new CDPlayer(compactDisc);}

在XML中我们通过beans标签的profile属性来声明这个beans属于哪个profile

<beans profile="QA">
    <bean></bean>
</beans>

Spring在确定哪个profile处于激活状态时,需要依赖两个独立属性:

  • srping.profiles.active
  • spring.profiles.default

如果设置了active属性的话,那么它的值就用来确定哪个proflie是激活的。如果没有设置的话,Spring会查找default的值,如果都没有设置的话,那就没有激活proflie,就只会创建没有定义在profile中的bean。可以同时激活多个profile,以逗号分隔。

有多种方式来设置这两个属性

  • 作为DispatcherServlet的初始化参数
  • 作为Web应用的上下文参数
  • 作为JNDI条目
  • 作为环境变量
  • 作为JVM的系统属性
  • 在集成测试类上,使用@ActiveProfiles注解

条件化的Bean

@Conditional
这个注解用在带有@Bean注解的方法上,如果给定条件为TRUE,就会创建这个bean,否则的话这个bean会被忽略,不会被创建

@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean(){
    return new MagicBean();
}

设置给@Conditional的类可以是任意实现了Condition接口的类

public interface Condition{
    boolean matches(ConditionContext ctxt,AnnotatedTypeMetadata metadata);
}
public class MagicExistsCondition implements Condition{
    public boolean matches(ConditionContext context,AnnotatedTypeMetadata metadata){
        Environment env = context.getEnvironment();
        return env.containsProperty("magic");
    }
}

在上面的程序中,方法通过给定的ConditionContext对象得到了Environment对象,并用这个对象来检查环境中是否存在名为magic的环境属性。通过ConditionContext我们可以做到如下几点

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

处理自动装配的歧义性

  • 解决方案一 @primary
    我们可以把多个符合匹配条件的bean中通过设置首选bean
@Bean
@Primary
public Dessert iceCream(){
    return new IceCream();
}
<bean id="iceCream" class="com.desserteater.IceCream" primary="true" />
  • 解决方案二 @Qualifier
    但如果有多个bean被设置了首选bean,我们可以使用限定符的方式

@Qualifier注解是使用限定符的主要方式

@Autowired
//这里只会注入同样标注了@Qualifier("iceCream")的bean
@Qualifier("iceCream")的bean
public void setDessert(Dessert dessert){
    this.dessert = dessert;
}

这是使用限定符最简单的例子,@Qualifier的参数就是想要注入的bean的ID,所有使用@Component注解声明的类都会创建为bean,并且bean的ID为首字母小写的类名,实际上@Qualifier(“iceCream”)所指向的bean要有一个String 的“iceCream”作为限定符,所有的bean都有一个默认的限定符,这个限定符与bean的ID相同,但这种情况限定符与bean的ID紧紧耦合在一起,如果我们修改了bean的名称就会导致限定符的失败,所以我们可以限定符设置自定义值

@Component
//当使用自定义值的时候,建议为bean选择特征性活描述性的词语,而不是随意的名字
@Qualifier("cold")
public class IceCream implements Dessert{}

有一种情况,当不同的类都采用了同样的描述性的注解,这时候也会有歧义性,我们可能会想到使用多个@Qualifier注解来将bean限定到一个,但遗憾的是,@Qualifier注解并没有在定义时添加@Repeatable注解,所以Java不允许在同一个位置上重复出现多个@Qualifier注解
,但是我们可以创建自定义的限定符注解,这个注解本身使用@Qualifier注解来标注

@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.METHOD,ElementType.ANNOTATION_TYPE.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold {}
@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.METHOD,ElementType.ANNOTATION_TYPE.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Creamy {}

//这样,我们在声明和注入bean的地方就可以同时加上这两个注解,从而将可选范围缩小到只有一个bean满足
@Component
@Cold
@Creamy
public class IceCream implements Dessert{}

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

bean的作用域

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

单例是默认作用域,使用@Scope注解来声明bean的作用域,这个注解可以和@Component和@Bean搭配使用。也可以在xml中,使用bean的scope属性来设置

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

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad{
    return new Notepad();
}
<bean id="notepad" class="com.myapp.Notepad" scope="prototype" />
使用请求和会话作用域

一个生活场景,购物车bean,这个bean应该是作为会话作用域,在整个会话和请求范围内共享数据。

@Component
@Scope(value=WebApolicationContext.SCOPE_SESSION,
            proxyMode=ScopedProxyMode.INTERFACES
)
public ShoppingCart cart(){
}
@Component
public class StoreService{
    @AutoWired
    public void setShoppingCart(ShoppingCart shoppingCart){
        this.shoppingCart=shoppingCary;
    }   
}

我们创建了两个对象,一个是会话作用域的ShoppingCart,一个是单例的StoreService,StoreService中聚合了ShoppingCart对象。但这样会有一个问题,单例对象会在加载时创建,而这时候成员变量的对象还没有生成,只有创建了会话之后才会出现这个实例,并且我们希望StoreService对象里面的这个ShoppingCart是针对当前会话的,不同会话中的这个对象是不一样的。为了解决这个问题,我们在ShoppingCart中设置了proxyMode属性,这个属性会为ShoppingCart对象生成一个代理对象,当调用的时候,代理对象会将请求转发给会话作用域里真正的ShoppingCart

proxy的属性有两种

  • ScopedProxyMode.INTERFACES
    这个是基于JDK实现的
  • ScopedProxyMode.TARGET_CLASS
    这个是基于CGLIB实现的

运行时值注入

Spring提供了两种运行时求值的方式

  • 属性占位符
  • Spring表达式语言
注入外部的值
@Configuration
@PropertySource("classpath:/com/soundsystem/app.properties")
public class ExpressiveConfig{
    @Autowired
    Environment env
    @Bean
    public BlankDisc disc(){
        return new BlankDisc(env.getProperty("disc.title"),env.getProperty("disc.artist"));
    }
}

上面的例子中,@PropertySource引用了类路径中一个名为app.properties的配置文件,这个文件会被加载到Environment中,通过Environment的getProperty方法,我们可以获取到配置文件中对应的值

Environment
  • Environment对象的getProperty方法有四个重载
    • String getProperty(String key)
    • String getProperty(String key, String defaultValue)
    • T getProperty(String key, Class type)
    • T getProperty(String key, Class type, T defaultValue)
      前两个方法的返回值都是String值,不同的是,如果在指定的属性不存在的时候,我们可以通过设置defaultValue给他返回一个默认值。后两个方法我们可以把配置文件里面的值装换成我们需要的类型
//返回值是一个int类型,并且如果配置文件中没有对应的属性,设置默认值30
int connectionCount=env.getProperty("db.connection.count",Integer.class,30)
  • getRequiredProperty()
    如果使用getProperty()方法的时候没有指定默认值,并且这个属性没有定义的话,获取到的值是null,如果这个属性必须定义的话,个可以使用getRequiredProperty()方法,如果这个方法获取的属性没有定义会抛出异常。
  • containsProperty()
    使用containsProperty()方法来检查某个属性是否存在,使用getPropertyAsClass()方法将属性解析为类
  • 检查哪些profile处于激活状态
    • String [] getActiveProfiles() 返回激活profile名称的数组
    • String [] getDefaultProfiles() 返回默认profile名称的数组
    • boolean acceptsProfiles(String…profiles) 如果environment支持给定profile的话就返回true
属性占位符

Spring支持将属性定义到外部的属性的文件中,并使用占位符将其插入到SpringBean中,占位符的形式为使用"${…}"包装的属性名称

public BlankDisc(
        @Value("${disc.title}") String title ,
        @Value("${disc.artist}") String artist) {
    this.title=title;
    this.artist=artist;
}

为了使用占位符,我们需要配置一个PropertyPlaceholderConfigurerbean 或 PropertySourcesPlaceholderConfigurerbean,推荐使用后面的,以为它可以基于SpringEnvironment及其属性源来解析占位符

@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer(){
    return new PropertySourcesPlaceholderConfigurer()
}
<context : property-placeholder />
Spring表达式语言

使用Spring表达式语言进行装配,SpEL表达式要放到"#{…}"

SpEL的常见特性
  • 使用bean的ID来引用bean
  • 调用方法和访问对象的属性
  • 对值进行算数、关系和逻辑运算
  • 正则表达式匹配
  • 集合操作

  • #{1}
    固定值
  • #{T(System).currentTimeMillis()}
    调用静态方法获取返回值,SpEL中访问类作作用域的方法(静态方法)和常量的话,要依赖 T() 这个运算符
  • #{sgtPeppers.artist}
    获取ID为sgtPeppers的bean的artist属性
  • #{sgtPeppers.selectArtist()}
    获取ID为sgtPeppers的bean的selectArtist()方法的返回值,对于被调用方法的返回值来说,我们同样可以调用它的方法,比如这样
    #{sgtPeppers.selectArtist().toUpprerCase()},这样写可能会出现空指针异常,我们可以在这里面写Java代码来规避它,但SpEL提供了更简单的方式#{sgtPeppers.selectArtist()?.toUpprerCase()},"?." 会先判断它左边的对象是不是null,如果不是则继续调用右边的方法,否则返回一个null
  • #{systemProperties[‘disc.title’]}
    引用配置文件中的属性
SpEL运算符
运算符类型运算符
算术运算+,-,*,/,%,^
比较运算<,>,==,<=,>=,lt,gt,eq,le,ge
逻辑运算and,or,not,*
条件运算?:(ternary),?:(Elvis)
正则表达式matches

条件运算符与Java中的三元运算符很相似
一种用法是?前面是一个boolean值,ture的时候整个表达式的值是:之前的值,否则是:之后的值,这与Java中完全一致。还有一种用法是用来判断null,#{disc.title ? : ‘JackSon’} 整个表达式会判断disc.title的值是不是null,如果是的话,则表达式的结果就是JackSon

正则表达式example
#{admin.email matches ‘[a-zA-Z0-9._%±]+@[a-zA-Z0-9.-]+//.com’}

计算集合
  • #{jukebox.songs[4].title}
    获取集合songs中的第五个(从零开始)元素的title属性,整个集合来自ID为jukebox的bean
  • #{‘abcd’[2]}
    还可以从String中获取一个字符,上面的表达式的值就是"c"

SpEL还提供了

  • (.?[])
    查询运算符 (.?[]) 用来对集合进行过滤,[]里面的是一个另一个表达式,值为boolean值,用于过滤逻辑。相当于Stream里面的filter
  • (.1)
    返回满足过滤条件的第一个匹配项
  • (.$[])
    返回满足过滤条件的最后一个匹配项
  • (.![])
    投影运算符,它会从集合的每一个成员中选择特定的属性放到另外一个集合中,相当于Stream里面的map

  1. ↩︎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值