Spring 初步的理解

理解IoC container

  1. 什么是IoC Container?
    IoC(Inversion of control)是一种设计思想,可以将设计好的对象交给容器去控制(包括对象的整个生命周期、以及对象之间的组装)。

  2. 它解决了什么问题?
    传统应用程序中,为了实现业务逻辑,肯定需要多个对象来协作完成任务,例如:

    • 有一个用户信息类
    • 有一个用户类,这个用户类会依赖于用户信息类
    • 然后用一个客户端类去创建用户类和用户信息类,并且将用户信息类主动注入到用户类中

    有了IoC Container之后,在客户端类中就不需要再去主动去创建对象和注入依赖了,而是通过配置文件或者注解的形式表现出来,然后IoC Container通过解析配置文件和注解去完成模块的创建和模块间的依赖注入。这样可以使代码之间的耦合度更低

  3. IoC原型(参考JinhaoPlus的博客)

  • 如果不采用IoC,传统程序依赖注入的方式
class MainModule{
  private DependModuleA moduleA;  // 存在依赖关系
  public DependModuleA getModuleA() {
    return moduleA;
  }
  public void setModuleA(DependModuleA moduleA) {
    this.moduleA = moduleA;
  }
}

class DependModuleAImpl {
  public void funcFromModuleA() {
    System.out.println("This is func from Module A");
  }
}

public class SimpleIOCDemo {
  public static void main(String[] args) throws ClassNotFoundException {
    MainModule mainModule = new MainModule();
    // 依赖注入
    mainModule.setModuleA(new DependModuleAImpl()); 
    mainModule.getModuleA().funcFromModuleA();
  }
}
  • 自己定义一个简易的IoC
定义一个properties配置文件(xxx.properties)描述依赖关系
moduleA=DependModuleAImpl
mainModule=MainModule
mainModule.moduleA=moduleA
class SimpleIoCContainer{
  private Properties properties = new Properties();
  // 存储已经加载过的bean,String为bean的名称
  private Map<String, Object> moduleMap = new HashMap<>();
  public SimpleIoCContainer(String configurFile) {
    try { // 加载配置文件
      properties.load(new FileInputStream(configurFile));
    } catch (Exception e) {
      e.printStackTrace();
    }    
  }
  public Object getBean(String moduleName) throws ClassNotFoundException {
    Object instanceObj;
    // 如果不为空,说明这个bean已经创建过,返回已经创建过的bean
    if(moduleMap.get(moduleName)!=null){
      System.out.println("return old bean");
      return moduleMap.get(moduleName);
    }
    System.out.println("create new bean");
    String fullClassName = properties.getProperty(moduleName);
    if(fullClassName == null)
      throw new ClassNotFoundException();
    else{
      // 通过properties的值获取类对象
      Class<? extends Object> clazz = Class.forName(fullClassName);
      try {
        // 通过类对象创建一个实例对象
        instanceObj = clazz.newInstance();
        // 检查每个属性是否有对其他bean的依赖
        instanceObj = buildAttachedModules(moduleName,instanceObj);
        // bean创建完成,put到map中
        moduleMap.put(moduleName, instanceObj);
        return instanceObj;
      } catch (InstantiationException e) {
        e.printStackTrace();
      } catch (IllegalAccessException e) {
        e.printStackTrace();
      }
    }
    return null;
  }
  /*
  * 实现依赖注入
  */
  private Object buildAttachedModules(String modulename , Object instanceObj) {
    Set<String> propertiesKeys = properties.stringPropertyNames();
    Field[] fields = instanceObj.getClass().getDeclaredFields();
    for (String key : propertiesKeys) {
      // 如果存在,说明可能存在对其他的bean对象的依赖
      if(key.contains(modulename)&&!key.equals(modulename)){
        try {
          Class<? extends Object> clazz = Class.forName(properties.getPropert(key));
          // 存在依赖其他bean的字段属性
          for (Field field : fields) {
            if(field.getType().isAssignableFrom(clazz))
              // 依赖注入
              field.set(instanceObj, moduleMap.get(properties.getPropert(key)));
          }
        } catch (Exception e) {
          e.printStackTrace();
        }
        
      }
    }
    return instanceObj;
  }
}

public class SimpleIOCDemo {
  public static void main(String[] args) throws ClassNotFoundException {
    SimpleIOCContainer container = new SimpleIOCContainer();  
    // spring默认是预加载的(容器创建时,就开始执行bean的创建和依赖注)
    // 这里实现是在getBean方法执行时才会加载(懒加载方式)
    DependModuleAImpl moduleA = container.getBean("moduleA");
    MainModule mainModule = container.getBean("mainModule");
  }
}

Spring IoC容器的工作过程

  1. 容器的启动阶段

    • 和自己写的ioc原型类似,首先要找到并加载配置文件或者注解等元信息
    • 容器对加载的配置信息进行解析和分析
    • 将解析处理后的信息装配成BeanDefinition(bean在Spring ioc内部表示的一种数据结构,用来描述一个bean实例所具有的属性值、构造函数参数值、具体实现的信息),相到于一个生成bean的“内部图纸”
    • 将BeanDefinition注册到容器中
    • 对BeanDefinition进行一些后处理
  2. Bean实例化阶段

    • 容器通过对BeanDefinition的解析进行实例化对象
    • 如果有依赖,就装配依赖(注:最后创建的实例是经过spring包装的BeanWrapper实例,而不是简单的bean实例,为了提供统一访问bean属性的接口,其可以方便的进行属性设置,而不用通过反射的方式)
    • 可以通过定义BeanPostProcessor处理器对已经装配好的实例进行一些处理(可选)
    • 可以设置init-method和destroy-method来对bean的初始化和销毁过程设置毁掉函数(可选)
  3. Bean完整的生命周期(下面以ApplicationContext作为容器为例)
    来自于知乎
    1. Spring对Bean进行实例化,相当于new
    2. 为bean设置对象属性(依赖注入)
    3. 注入Aware接口
    • 如果bean实现了BeanNameAware接口,将Spring的id传递给setBeanName方法并执行,目的是提供接口通过Bean来访问Bean的id
    • 如果bean实现了BeanFactory接口,Spring把BeanFactory容器实例传递给setBeanFaoctory方法并执行,目的是通过bean获取Spring容器实例,使用场景-》bean通过spring容器发布事件
    • 如果Bean实现了ApplicationContextAware接口,Spring容器将调用setApplictionContext(Application ctx)方法,把应用上下文作为参数传入。
      它与BeanFactory的区别在于,前者在spring容器调用时会把自己作为参数传入,而不需要程序员去指定参数传入,而且前者提供的功能更多。
    1. 经过上面的步骤后,bean对象就被正确的构造,但是还可以通过一下方法进行自定义处理 ,实现BeanPostProcessor接口
    • postProcessBeforeInitialzation( Object bean, String beanName ) 当前正在初始化的bean对象会被传递进来,我们就可以对这个bean作任何处理。 这个函数会先于InitialzationBean执行,因此称为前置处理。
    • postProcessAfterInitialzation( Object bean, String beanName ) ,Spring会在InitialzationBean完成后执行
    1. 如果bean实现了InitializingBean接口,spring将调用afterPropertiesSet()方法。如果设置了属性init-method,在这之后,还会执行init-method属性指定的自定义方法。
    2. bean将一直驻留在应用上下文中(如果bean的作用域为prototype类型,就会把bean实例交给客户端,不驻留在容器中),直到应用上下文被销毁
    3. 如果bean实现了DispostbleBean接口,销毁前将执行destroy方法
    4. 如果这个Bean配置了destroy-method属性,会自动调用这个自定义的销毁方法
  4. BeanFactoty作为容器的几点不同

  • 不会调用ApplicationContextAware接口的setApplicationContext()方法
  • postProcessBeforeInitialzation()方法和postProcessAfterInitialization()方法不会自动调用,必须自己通过代码手动注册
  • BeanFactory容器启动的时候,不会去实例化所有Bean,包括所有scope为singleton且非懒加载的Bean也是一样,而是在调用的时候去实例化(默认为懒加载)

基于xml配置文件的方式声明bean、依赖注入(主要使用场景:主要用于第三方类库)

  1. 常见的配置方式如下:
<?xml version="1.0" encoding="UTF-8"?>
<!-- xmlns表示默认的命名空间,不使用前缀的标签,都代表是这个命名空间下的标签 -->
<!-- xmlns:xsi表示使用xsi作为前缀的命名空间 -->
<!-- xsi:schemaLocation定义命名空间和对应的XSD文档(可以对文档格式合法性进行验证)的位置关系 -->
<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">

    <!-- 可以导入另外一个xml配置文件,进行组合 -->
    <import resource="another.xml"/>

    <!-- 在使用注解和xml配置文件混合配置的时候使用 -->
    <context:annotation-config />  

    <!-- 最常见声明bean的方式,id为bean的名称,class为bean的classpath -->
    <bean id="a" class="com.wulei.A">

    <!-- 对名为a的bean声明一个别名 -->
    <alias name="a" alias="aAlias" />

    <!-- 这里在声明一个bean的同时,还指定了这个bean的bPropertie属性值依赖于名为a的bean -->
    <!-- spring默认是预加载的,可以设置lazy-init属性为懒加载 -->
    <!-- init-method和destroy-mehod用于指定自定义的初始化和销毁方法 -->
    <bean id="b" class="com.wulei.B" lazy-init="true" init-method="" destroy-method="">
        <property name="bPropertie" ref="a" />
    </bean>
    <bean id="b1" class="com.wulei.B">
        <property name="bPropertie" ref="a" />
    </bean>
    <bean id="b2" class="com.wulei.B">
        <property name="bPropertie" ref="a" />
    </bean>

    <!-- 如果设置bean的作用域为prototype,那么就不再默认是单例模式-->
    <bean id="b3" class="com.wulei.B" scope="prototype">
    </bean>

    <!--
    public class C {
        public Foo(A a, List<B> bList, int i,String name s) {
            // ...
        }
    }
    对构造函数参数的依赖注入声明方式
    -->
    <bean id="c" class="com.wulei.C">
        <constructor-arg ref="a" />
        <constructor-arg>
            <list>
                <ref bean="b1" />
                <ref bean="b2" />
            </list>
        </constructor-arg>
        <constructor-arg type="int" value="1" />
        <constructor-arg name="s" value="oneString" />
    </bean>

</beans>
  1. 如何让这个配置文件被容器加载?
// 还可以使用FileSystemXmlApplicationContext
ApplicationContext context = new ClassPathXmlApplicationContext("xxx.xml");

基于注解的方式声明bean、依赖注入(主要使用场景:Bean的实现类是当前项目开发的)

  1. @Autowired,此注解可以声明依赖注入,使用方式如下
public class A {

    private final B b;

    // 对属性字段使用
    @Autowired
    private C c;

    private D d;

    private E e;

    // 对构造函数使用
    @Autowired
    public MovieRecommender(B b) {
        this.b = b;
    }

    // 对setter方法使用
    @Autowired
    public void setD(D d) {
        this.d = d;
    }

    // 对一个方法使用
    @Autowired
    public void wireE(E e) {
        this.E = e;
    }
}
  1. @Bean,它是一个方法级别的注解,声明当前方法的返回值为一个Bean。同时它必须使用在有@Configuration或@Component注解的类中,@Configuration注解让容器知道这个类相当于一个xml配置文件,@Bean有一些属性可以定义,例如:init-method、destroy-method、name等。使用方式如下,
/*
@Configuration包含了@Component,他们的区别在于如下场景。
如果使用@Component,那么在userInfo中,country()会生成一个新的实例,
而使用@Configuration则不会出现这种情况,会使用同一个Country实例。
@Component
public class MyBeanConfig {

    @Bean
    public Country country(){
        return new Country();
    }

    @Bean
    public UserInfo userInfo(){
        return new UserInfo(country());
    }

}
*/
@Configuration
public class AppContext {
    // 该方法必须返回一个对象
    @Bean
    public Course course() {
        return new Course();
    }
}
  1. @Component、@Repository(用于标注数据访问层组件)、@Service(用于标注业务层组件)、@Controller(用于标注控制层组件),这四个注解表明一个类声明为一个bean。

  2. @Qualifier,在依赖注入时,如果有多个候选的Bean,可以指定具体某一个Bean来作为注入的类,使用方式如下

@Autowired  
public void setOffice(@Qualifier("office")Office office)  
{  
    this.office =office;  
}  
//或则如下
@Autowired   
@Qualifier("office")   
private Office office; 
  1. @ComponentScan,自动扫描包明下所有使用@Component @Service @Repository @Controller 的类,并注册为Bean。需要将@ComponentScan添加在包含有@Configuration的类中,其中basePackages属性
    可以指定扫描的路径。
// 相当于xml文件中的<context:component-scan base-package="org.example"/>
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig  {
   ...
}
  1. 这种情况如何实例化一个容器?
// AppContext类必须是要被@Configuration声明过的
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppContext.class)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容

  • 在简书写完文章,总喜欢往首页投稿,屡屡遭拒,编辑回复说原因首先是字数少,其次是分析的不够深刻。每日的作业只要求写5...
    aimeelala阅读 251评论 0 0
  • 01 大概书虫都爱把书捧在手心的踏实感吧,图书馆才刚上班,母亲就去借了《微型小说选刊》。 昨晚她走进书房,把杂志拿...
    Jessica时间橱窗阅读 315评论 0 1
  • 1 在网上搜索“你为什么不快乐”。会出现很多回答,譬如:房价高,物价高,老婆成本高,就是工资不高;做牛,做马,做羊...
    一jie书生阅读 227评论 2 3