Spring IoC学习笔记

143 阅读20分钟

基本概念

IoC(Inversion of Control)和DI(Dependency Injection)指的是一回事儿。

业务告诉容器需要哪些依赖。容器在创建bean时注入这些依赖项。怎么告诉容器呢?可以有三个地方:

    • 构造函数参数
    • 工厂方法参数
    • 对象成员变量

最传统的方式就是自己创建依赖像实例,new一个。Spring官方文档还提到了一种模式是Service Locator pattern,我理解实现之一就是SPI。关于DI和Service Locator的比较,可以看下参考资料第一条的链接。

我理解bean就是受spring管理的对象。pojo就是不受spring管理的普通java对象。

Beans以及bean之间的依赖关系,在容器这边,就是配置元数据(the configuration metadata)。

在spring的语境下,容器就是ApplicationContext。实际上DI是一种设计模式,所以不用spring但是有实现DI/IoC的话,容器就是ApplicationContext的conterpart。这个和集合的容器不是一个概念哈。

核心包和类

  • 核心包
    • org.springframework.beans
    • org.springframework.context
  • 核心类
    • BeanFactory - 提供框架配置和基本能力
    • ApplicationContext - BeanFactory的子类,增加了4个功能
      • 与Spring AOP的集成
      • 国际化的支持(Message资源处理)
      • 事件发送
      • 应用层的上下文支持,如WebApplicationContext

容器

ApplicationContext负责实例化、配置和装配bean。那容器也不能自己拍脑袋决定bean要怎么实例化、配置和装配吖,要让容器工作,就得告诉它要怎么干活,所以得有个地方配置,就是配置元数据(configuration metadata)。现在有三种:xml,注解,Java代码写。

ApplicationContext现在有俩实现:

    • ClassPathXmlApplicationContext
    • FileSystemXmlApplicationContext
    • 自己搞一个

XML中相关元素

    • <import/>
    • <beans/>
    • <bean/>
// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml","daos.xml");
// retrieve configured instance
PetStoreService service = context.getBean("petStore", PetStoreService.class);
// use configured instance
List<String> userList = service.getUsernameList();

// 也可以分开,可以混着用,比如扫一些然后再读一些xml的
GenericApplicationContext context = new GenericApplicationContext();
new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml");
context.refresh();

Bean

在容器里,bean都是以BeanDefinition对象的形式被抽象的。BeanDefinition包括这些信息:

    • 全限定名-通常就是实际的实现类
    • 行为配置元素,告诉容器bean要做哪些动作,比如范围(scope)、生命周期回调方法等。
    • bean正常工作所需的其他bean引用,也叫作依赖或者合作者(collaborators)
    • 其他实例化所需的设置项,如池子的大小限制、连接池的连接数量等

除了根据BeanDefinition创建bean,ApplicationContext也支持把已经创建好的对象作为bean注册进来。首先调用getBeanFactory()获取到DefaultListableBeanFactory(BeanFactory实现类),DefaultListableBeanFactory提供了方法

    • registerSingleton(..)
    • registerBeanDefinition(..)

注意的话,如果要这么干,越早注册越好。以免容器初始化auto wiring的时候出问题。

bean的命名

一个bean可以有一到多个坐标名称,但是多个bean不能重名。多个的实现,在spring里,一个作为name,其他作为alias。

XML配置里,有两个标签 id和name,id是只有一个的,name可以指定一个或多个,用逗号,分号,或者空格做分隔符都可以1。如果id和name都不提供,那容器就自己生成一个唯一名称(类名首字母小写,除非前两个字母都算大写,那保留大写)2。如果是大型系统里,要用其他模块提供的bean,但是需要重新取个名字,可以用alias(@Bean也提供):

<alias name="fromName" alias="toName"/>

初始Bean

在xml的bean中提供class标签项,对应的是BeanDefinition的Class字段 。

    • 类提供构造函数,调用构造函数创建bean -- 调用的是无参的构造函数,如果要有状态,等构造完了之后再注入依赖
    • 类提供静态工厂方法,调用工厂方法创建bean,创建的bean的类型由工厂方法决定 -- 还需要提供factory-method表示工厂方法名。Spring提供这个主要还是考虑到要兼容历史遗留库类。
    • 现有bean的非静态工厂方法 -- class不填,factory-bean填现有bean的名字,factory-method填工厂方法名

静态工厂方法示例:

<bean id="clientService"
class="examples.ClientService"
factory-method="createInstance"/>
public class ClientService {
  private static ClientService clientService = new ClientService();
  private ClientService() {}
  public static ClientService createInstance() {
    return clientService;
  }
}

现有bean的非静态工厂方法

<!-- the factory bean, which contains a method called createInstance() -->
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
  <!-- inject any dependencies required by this locator bean -->
</bean>
<!-- the bean to be created via the factory bean -->
<bean id="clientService"
  factory-bean="serviceLocator"
  factory-method="createClientServiceInstance"/>
public class DefaultServiceLocator {
  private static ClientService clientService = new ClientServiceImpl();
  public ClientService createClientServiceInstance() {
    return clientService;
  }
}
  • 嵌套类
    • com.example.外层类名$嵌套类
    • com.example.外层类名.嵌套类

确定bean运行时的类型

bean定义类型和运行时类型不一定一致的。

BeanFactory.getType

依赖

依赖注入

依赖注入有两大变种:基于构造函数的和基于setter的。

基于构造函数的依赖注入

  • 构造函数参数解析

根据参数类型进行,一般和xml定义的顺序一致。如果光靠类型不够,就在constructor-arg加上index(下标从0开始)。或者也可以加上name3。

<bean id="exampleBean" class="examples.ExampleBean">
  <constructor-arg type="int" value="7500000"/>
  <constructor-arg type="java.lang.String" value="42"/>
</bean>
<bean id="exampleBean" class="examples.ExampleBean">
  <constructor-arg index="0" value="7500000"/>
  <constructor-arg index="1" value="42"/>
</bean>

<bean id="exampleBean" class="examples.ExampleBean">
  <constructor-arg name="years" value="7500000"/>
  <constructor-arg name="ultimateAnswer" value="42"/>
</bean>

基于setter的依赖注入

可以使用BeanDefinition和PropertyEditor手写代码注入。但是不推荐

通常做法还是使用 XML或者注解(@Component, @Controller,)或者@Configuration 类里的@Bean 方法提供给容器。

Spring是推荐多用基于构造函数的依赖注入的,对于基于setter的依赖注入,官方推荐在这个依赖是optional的时候使用。

依赖解析的顺序

  1. 实例化ApplicationContext,在这个过程中会加载元配置
  2. 挨个实例化bean,如果bean有依赖项,会在创建的时候提供
  3. 每个属性或者构造函数参数,都是一个需要设置的定义或者引用
  4. 属性或者参数,需要的话,会做一次转化,Spring默认支持所有内置类型的转换,如int, long, String, boolean等。

Spring在初始化容器的时候,会校验bean的配置。如果bean的scope是singleton,并且是提前初始化(默认)的,那会随容器一起创建。否则的话只有在使用的时候才会创建。 创建一个bean有可能会让依赖图上的所有bean都创建(如果还没有创建),这里可能会有类型不匹配的风险。

循环依赖

比如A类的构造函数有对B的依赖,B的构造函数有对A的依赖,Spring碰到这种情况会抛BeanCurrentlyInCreationException。

可以不使用构造函数注入,改成setter注入。

依赖与配置

  • 基本类型属性
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-
  method="close">
  <!-- results in a setDriverClassName(String) call -->
  <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
  <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
  <property name="username" value="root"/>
  <property name="password" value="misterkaoli"/>
</bean>

<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
  https://www.springframework.org/schema/beans/spring-beans.xsd">
  <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource"
    destroy-method="close"
    p:driverClassName="com.mysql.jdbc.Driver"
    p:url="jdbc:mysql://localhost:3306/mydb"
    p:username=
    "
    root
    "
    p:password="misterkaoli"/>
</beans>

<bean id="mappings"
  class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
  <!-- typed as a java.util.Properties -->
  <property name="properties">
    <value>
      jdbc.driver.className=com.mysql.jdbc.Driver
      jdbc.url=jdbc:mysql://localhost:3306/mydb
    </value>
  </property>
</bean>
  • 其他bean

在<constructor-arg/>或者 <property/>里使用ref。

<ref bean="someBean"/>

如果是在父容器里的,那bean要改成parent

<!-- in the parent context -->
<bean id="accountService" class="com.something.SimpleAccountService">
  <!-- insert dependencies as required here -->
</bean>

<!-- in the child (descendant) context -->
<bean id="accountService" <!-- bean name is the same as the parent bean -->
  class="org.springframework.aop.framework.ProxyFactoryBean">
  <property name="target">
    <ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
  </property>
  <!-- insert other configuration and dependencies as required here -->
</bean>
  • 内部的bean
<bean id="outer" class="...">
  <!-- instead of using a reference to a target bean, simply define the target bean
  inline -->
  <property name="target">
    <bean class="com.example.Person"> <!-- this is the inner bean -->
      <property name="name" value="Fiona Apple"/>
      <property name="age" value="25"/>
    </bean>
  </property>
</bean>

作为匿名内部类实例处理。

  • 集合

<list/>, <set/>, <map/>, and <props/> 对应 List, Set, Map, 和 Properties。

<bean id="moreComplexObject" class="example.ComplexObject">
  <!-- results in a setAdminEmails(java.util.Properties) call -->
  <property name="adminEmails">
    <props>
      <prop key="administrator">administrator@example.org</prop>
      <prop key="support">support@example.org</prop>
      <prop key="development">development@example.org</prop>
    </props>
  </property>
  <!-- results in a setSomeList(java.util.List) call -->
  <property name="someList">
    <list>
      <value>a list element followed by a reference</value>
      <ref bean="myDataSource" />
    </list>
  </property>
  <!-- results in a setSomeMap(java.util.Map) call -->
  <property name="someMap">
    <map>
      <entry key="an entry" value="just some string"/>
      <entry key="a ref" value-ref="myDataSource"/>
    </map>
  </property>
  <!-- results in a setSomeSet(java.util.Set) call -->
  <property name="someSet">
    <set>
      <value>just some string</value>
      <ref bean="myDataSource" />
    </set>
  </property>
</bean>

key value支持以下类型:

bean | ref | idref | list | set | map | props | value | null

容器也支持合并集合,使用parent把parent的选项合并到child里,注意要加上merge=true。

<beans>
    <bean id="parent" abstract="true" class="example.ComplexObject">
        <property name="adminEmails">
            <props>
                <prop key="administrator">administrator@example.com</prop>
                <prop key="support">support@example.com</prop>
            </props>
        </property>
    </bean>
    <bean id="child" parent="parent">
        <property name="adminEmails">
            <!-- the merge is specified on the child collection definition -->
            <props merge="true">
                <prop key="sales">sales@example.com</prop>
                <prop key="support">support@example.co.uk</prop>
            </props>
        </property>
    </bean>
    <beans>
  • null值与空值

其他的都按照空字符串处理,除非使用

  • XML简写 p-namespace
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean name="classic" class="com.example.ExampleBean">
        <property name="email" value="someone@somewhere.com"/>
    </bean>
    <bean name="p-namespace" class="com.example.ExampleBean"
        p:email="someone@somewhere.com"/>
</beans>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean name="john-classic" class="com.example.Person">
        <property name="name" value="John Doe"/>
        <property name="spouse" ref="jane"/>
    </bean>
    <bean name="john-modern"
        class="com.example.Person"
        p:name="John Doe"
        p:spouse-ref="jane"/>
    <bean name="jane" class="com.example.Person">
        <property name="name" value="Jane Doe"/>
    </bean>
</beans>
  • XML简写 c-namespace
<!-- c-namespace index declaration -->
<bean id="beanOne" class="x.y.ThingOne" c:_0-ref="beanTwo" c:_1-ref="beanThree"
    c:_2="something@somewhere.com"/>
<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
    https://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="beanTwo" class="x.y.ThingTwo"/>
    <bean id="beanThree" class="x.y.ThingThree"/>
    <!-- traditional declaration with optional argument names -->
    <bean id="beanOne" class="x.y.ThingOne">
        <constructor-arg name="thingTwo" ref="beanTwo"/>
        <constructor-arg name="thingThree" ref="beanThree"/>
        <constructor-arg name="email" value="something@somewhere.com"/>
    </bean>
    <!-- c-namespace declaration with argument names -->
    <bean id="beanOne" class="x.y.ThingOne" c:thingTwo-ref="beanTwo"
        c:thingThree-ref="beanThree" c:email="something@somewhere.com"/>
</beans>
  • 嵌套属性名
<bean id="something" class="things.ThingOne">
    <property name="fred.bob.sammy" value="123" />
</bean>

depends-on

大部分情况下,依赖关系通过ref就能表达出来,但也有更隐晦一些的依赖,比如DB driver的初始化。depends-on可以强制在当前bean初始化之前,初始化一些其他bean。如果bean是singleton,那bean的destruction顺序也会被影响。

<!-- 单个强制依赖场景 -->
<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />

<!-- 多个强制依赖场景 -->
<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
    <property name="manager" ref="manager" />
</bean>
<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />

延迟初始化

<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.something.AnotherBean"/>

<beans default-lazy-init="true">
<!-- no beans will be pre-instantiated... -->
</beans>

Autowiring

使用 的 autowire属性开启。

4种模式

    • no -- 必须使用ref定义依赖关系
    • byName -- 根据名字来
    • byType -- 根据类型,如果容器里有多于一个这个类型的bean,会抛错退出
    • constructor -- 同byType,区别是只对构造函数的参数有效

排除不被autowire

    • 把 的autowire-candidate 属性 设置为 false,这个bean就不会作为autowire的candicate管理了。
    • 在层,设置default-autowire-candidate,限制范围。比如*Repository限制只有名字以Respostiry结尾的才作为autowire选项。如果多个,用逗号分开。

方法注入

想象一个场景,一个单例bean要调用非单例实例的方法,但是注入只在单例创建的时候,那就不能保证每次调用方法的时候都是新的依赖实例了。

第一个办法是放弃一些IoC功能,每次调用的时候都去容器拿一遍依赖项。但是这就让业务代码与spring深耦合了,不推荐。

// a class that uses a stateful Command-style class to perform some processing
package fiona.apple;
// Spring-API imports
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
public class CommandManager implements ApplicationContextAware {
    private ApplicationContext applicationContext;
    public Object process(Map commandState) {
        // grab a new instance of the appropriate Command
        Command command = createCommand();
        // set the state on the (hopefully brand new) Command instance
        command.setState(commandState);
        return command.execute();
    }
    protected Command createCommand() {
        // notice the Spring API dependency!
        return this.applicationContext.getBean("command", Command.class);
    }
    public void setApplicationContext(
        ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
  • Lookup Method Injection

容器关联的bean的方法,可以被重写,使用容器里另一个bean的方法。另一个bean往往是prototype类型的(非singleton)。这种情况下,Spring框架通过使用CGLIB动态创建子类,在子类里重写该方法实现。

Lookup Method Injection有些限制:

    • bean的类以及需要被重写的方法不能是final的。
    • 单元测试的时候,如果这个方法是abstract的,需要自行stub一个子类和子类方法
    • 不能与工厂方法一起使用,尤其是配置类的@Bean方法,那个时候容器还不能动态创建子类。
package fiona.apple;
// no more Spring imports!
public abstract class CommandManager {
  public Object process(Object commandState) {
    // grab a new instance of the appropriate Command interface
    Command command = createCommand();
    // set the state on the (hopefully brand new) Command instance
    command.setState(commandState);
    return command.execute();
  }
  // okay... but where is the implementation of this method?
  protected abstract Command createCommand();
}
<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
  <!-- inject dependencies here as required -->
</bean>
<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
  <lookup-method name="createCommand" bean="myCommand"/>
</bean>
public abstract class CommandManager {
    public Object process(Object commandState) {
        Command command = createCommand();
        command.setState(commandState);
        return command.execute();
    }
    @Lookup("myCommand")
    protected abstract Command createCommand();
}
public abstract class CommandManager {
  public Object process(Object commandState) {
    Command command = createCommand();
    command.setState(commandState);
    return command.execute();
  }
  @Lookup
  protected abstract Command createCommand();
}
  • Arbitrary Method Replacement

官网说这玩意用不上的,建议别看了。所以我决定听他们的。

就是那些。

Bean Scopes

默认提供以下几个,想的话也可以自定义一个scope。

1. singleton

这里singleton的概念,和GangOfFour的单例模式,有些许区别:

    • 单例模式: 一个ClassLoader只有一个实例。
    • spring:一个容器中的一个bean只能有一个实例(per-container and per-bean)。

无状态的bean设置为singleton。

2. prototype

每次访问这个bean都创建一个对象。适合有状态的bean。

容器并不全程管理prototype类型的bean,只负责初始化、配置和装配,交给使用者(相当于new函数的职责),后续流程就不参与管理了。destruction lifecycle callbacks是不会被调用的。如果有资源需要释放,需要手工处理,可以考虑使自定义bean post-processor。

3. 网络环境的scope

    • request -- 一个 HTTP request 一个 scope="request", @RequestScope
    • session -- 一个 HTTP Session一个 scope="session" @SessionScope
    • application -- 一个 ServletContext 一个 scope="application" @ApplicationScope
    • websocket -- 一个WebSocket一个

要支持这几个,可能需要做一些动作才行。如果是使用的servlet技术栈(Spring Web MVC + DispatcherServlet),DispatcherServlet已经帮忙处理好了。

如果用的是Servlet 2.5 的容器,或者不使用DispatcherServlet处理请求,需要注册RequestContextListener 和ServletRequestListener。如果是Servlet3.0,使用WebApplicationInitializer接口。

4. c.f. application vs singleton

看着差不多,只不过singleton在ApplicationContext里,application的bean在ServletContext里。

5. 大scope依赖小scope的bean

假设想在singleton的bean里注入一个HTTP请求范围的bean,那最好注入那个bean的AOP代理,并将方法调用代理到实际的bean处理。

<?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:aop="http://www.springframework.org/schema/aop"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
  https://www.springframework.org/schema/beans/spring-beans.xsd
  http://www.springframework.org/schema/aop
  https://www.springframework.org/schema/aop/spring-aop.xsd">
  <!-- an HTTP Session-scoped bean exposed as a proxy -->
  <bean id="userPreferences" class="com.something.UserPreferences" scope="session">
    <!-- instructs the container to proxy the surrounding bean -->
    <aop:scoped-proxy/></bean>
  <!-- a singleton-scoped bean injected with a proxy to the above bean -->
  <bean id="userService" class="com.something.SimpleUserService">
    <!-- a reference to the proxied userPreferences bean -->
    <property name="userPreferences" ref="userPreferences"/>
  </bean>
</beans>
<!-- DefaultUserPreferences implements the UserPreferences interface -->
<bean id="userPreferences" class="com.stuff.DefaultUserPreferences" scope="session">
  <aop:scoped-proxy proxy-target-class="false"/>
</bean>
<bean id="userManager" class="com.stuff.UserManager">
  <property name="userPreferences" ref="userPreferences"/>
</bean>
 @Scope(value = WebApplicationContext.SCOPE_SESSION, 
    	proxyMode =ScopedProxyMode.INTERFACES)

@RequestScope(proxyMode = ScopedProxyMode.TARGET_CLASS)

还能一个线程一个的,但是要手动开启。

6. 自定义Scope

实现接口org.springframework.beans.factory.config.Scope

  • 代码方式

调用ConfigurableBeanFactory接口的void registerScope(String scopeName, Scope scope)方法注册Scope实现。ConfigurableBeanFactory在大部分的ApplicationContext实现的BeanFactory属性中都能获得。

Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
<bean id="..." class="..." scope="thread">
  • 声明方式
<?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:aop="http://www.springframework.org/schema/aop"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
  https://www.springframework.org/schema/beans/spring-beans.xsd
  http://www.springframework.org/schema/aop
  https://www.springframework.org/schema/aop/spring-aop.xsd">
  <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
    <property name="scopes">
      <map>
        <entry key="thread">
          <bean class="
            org.springframework.context.support.SimpleThreadScope"/>
        </entry>
      </map>
    </property>
  </bean>
  <bean id="thing2" class="x.y.Thing2" scope="thread">
    <property name="name" value="Rick"/>
    <aop:scoped-proxy/>
  </bean>
  <bean id="thing1" class="x.y.Thing1">
    <property name="thing2" ref="thing2"/>
  </bean>
</beans>

Bean的拓展点

生命周期回调方法

例子:

    • InitializingBean接口的afterPropertiesSet()方法 --初始化
    • DisposableBean接口的destroy()方法 -- 销毁前
    • @PostConstruct (JSR-250)
    • @PreDestroy (JSR-250)
    • init-method bean definition metadata
    • destroy-method bean definition metadata

Spring内部是使用BeanPostProcessor来调用回调方法的。如果不够用,可以自己写一个。

初始化时期的回调

InitializingBean接口的afterPropertiesSet()方法

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
public class AnotherExampleBean implements InitializingBean {
  @Override
  public void afterPropertiesSet() {
    // do some initialization work
  }
}

xml中的init-method

<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
public class ExampleBean {
    public void init() {
        // do some initialization work
    }
}

销毁前的回调

DisposableBean接口的destroy()方法

destroy-method bean definition metadata

默认的初始化和销毁方法

default-init-method & default-destroy-method

执行时机是依赖已经注入,但在aop之前。

执行顺序

容器启动与关闭期间的回调

只要一个bean实现了Lifecycle接口(start,stop,isRunning),当容器(ApplicationContext)接收到start或stop信号的时候,就会调用这些bean的相关方法。实现原理是调用 LifecycleProcessor(onRefresh,onClose)。LifecycleProcessor本身也实现了Lifecycle接口。

Lifecycle是比较粗颗粒度的,如果需要细颗粒度的控制,可以考虑实现SmartLifecycle接口。

启动的时候,phase最小的先启动,关闭的时候,phase最大的先关闭。默认是0。

LifecycleProcessor的默认实现类是DefaultLifecycleProcessor。DefaultLifecycleProcessor会等待stop执行结束,但是等待是有超时时间的,默认每个30秒。也可以自己定义一个名字是lifecycleProcessor的bean替换掉默认的。如果只是想更改超时等待时间,设置字段即可:

<bean id="lifecycleProcessor" class=
  "
  org.springframework.context.support.DefaultLifecycleProcessor">
  <!-- timeout value in milliseconds -->
  <property name="timeoutPerShutdownPhase" value="10000"/>
</bean>
server:
  shutdown: graceful   ## 开启优雅停机
spring:
  lifecycle:
    timeout-per-shutdown-phase: 50s    ## 优雅停机等待时间,默认30s

非网络应用优雅关闭容器

网络应用的话,(WebApplicationContext)已经提供了优雅停机支持。如果是非网络应用,需要优雅关闭,需要业务根据需要自己实现。

注册一个shutdownHook

import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Boot {
    public static void main(final String[] args) throws Exception {
        ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext( "beans.xml");
        // add a shutdown hook for the above context...
        ctx.registerShutdownHook();
        // app runs here...
        // main method exits, hook is called prior to the app shutting down...
    }
}

ApplicationContextAware and BeanNameAware

ApplicationContextAware

如果一个bean实现了ApplicationContextAware接口,在ApplicationContext创建它的时候,会把自己的引用放一个在这个bean里(ApplicationContextAware.setApplicationContext),这个bean就可以拿到创建它的容器,并且做一些事情

    • 通过容器拿其他bean(不推荐)
    • 通过容器拿到文件资源
    • 通过容器发布应用事件
    • 访问MessageSource
    • ......

当然bean获取ApplicationContext也可以把它当做依赖自动注入。

BeanNameAware

如果一个bean实现了BeanNameAware接口,容器会在创建后调用它的setBeanName方法,把bean的name传进去。

其他Aware接口

因为使用这些接口意味着会与Spring深耦合,就不符合IoC风格了,所以Spring官方建议别实现这些接口。

BeanDefinition继承

ApplicationContext里,子BeanDefinition的实现是ChildBeanDefinition。但是实际xml中是用parent标记表达的。

除了以下的属性只拿当前BeanDefinition的,其他的都可以继承

    • depends on
    • autowire mode
    • dependency check
    • singleton
    • lazy init

如果父级的BeanDefinition是abstract,那它就只是作为一个模板存在,不会实例化。

<bean id="inheritedTestBean" abstract="true"
  class="org.springframework.beans.TestBean">
  <property name="name" value="parent"/>
  <property name="age" value="1"/>
</bean>
<bean id="inheritsWithDifferentClass"
  class="org.springframework.beans.DerivedTestBean"
  parent="inheritedTestBean" init-method="initialize"><property name="name" value="override"/>
  <!-- the age property value of 1 will be inherited from parent -->
</bean>

容器的拓展点

一般情况下,业务开发不需要自己实现一个ApplicationContext,可以直接去实现接口提供给容器。

BeanPostProcessor — 拓展bean

可以实现1到多个BeanPostProcessor实例,执行顺序由order属性决定(通过实现Ordered接口来定义)。

容器首先实例化一个bean对象,BeanPostProcessor接着做二次加工。每个BeanPostProcessor实例,都是一个容器一个的,没法跨容器工作的哈(即便是有继承关系的容器)。

如果要有顺序,那就让Spring自己扫描到(需要注意声明的类型不然就是实现类本身,不然就是BeanPostProcessor,否则认不出来是BeanPostProcessor),不要手工代码往容器添加。

因为AOP auto-proxying本身自己就是一个BeanPostProcessor,所以对BeanPostProcessor做切面很可能无效的。

简单的示例代码:

package scripting;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {
  // simply return the instantiated bean as-is
  public Object postProcessBeforeInitialization(Object bean, String beanName) {
    return bean; // we could potentially return any object reference here...
  }
  public Object postProcessAfterInitialization(Object bean, String beanName) {
    System.out.println("Bean '" + beanName + "' created : " + bean.toString());
    return bean;
  }
}

实际的例子:AutowiredAnnotationBeanPostProcessor

BeanFactoryPostProcessor — 拓展元配置

BeanFactoryPostProcessor处理的是BeanDefinition,并且是在Bean初始化之前,也就是说,它允许改变BeanDefinition。

也可以使用Ordered定义多个BeanFactoryPostProcessor的执行顺序。

不能跨容器干活。

Spring自带的BeanFactoryPostProcessor有:

    • PropertyOverrideConfigurer
    • PropertySourcesPlaceholderConfigurer

使用例子可以看官方文档,代码看源码。

FactoryBean — 拓展实例化逻辑

基于注解的容器配置

对注解的扫描先于xml,所以xml的会覆盖注解的。

使用context:annotation-config/开启注解扫描,它会自动注册这些:

    • ConfigurationClassPostProcessor
    • AutowiredAnnotationBeanPostProcessor
    • CommonAnnotationBeanPostProcessor
    • PersistenceAnnotationBeanPostProcessor
    • EventListenerMethodProcessor

@Required

已经标记为Deprecated了。

可以放在setter方法上,表示这个属性应该在配置时赋值,不然就是从BeanDefinition里拿值,不然就autowire过来一个对应的bean。如果没有的话,容器应该抛错。

Spring更建议使用注解校验,以免容器外场景使用的时候没有校验。

注意,要使用@Required必须注册RequiredAnnotationBeanPostProcessor

@Autowired

将由JSR-330的@Inject替换它的职责

可以加在构造函数上。Spring4.3以后,如果只有一个构造函数,则不需要加。如果有多个,还是需要加上以指定使用哪个实例化Bean。

public class MovieRecommender {
    private final CustomerPreferenceDao customerPreferenceDao;
    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }
    // ...
}

也可以加在setter方法上

也可以加在有参数的方法上

也可以加在属性上

注意是根据类型匹配的,除非添加了required = false, 如果找不到对应类型的bean,会抛运行时错no type

match found。

这样会把所有这个类型的Bean都绑进数组里。默认按照注册顺序。如果想保证顺序,可以在目标bean上加@Order或者@Priority。@Order只影响注入顺序,不影响实例化顺序的哈(实例化顺序要考虑依赖关系)。

如果一个bean有多个构造函数,那其中只有一个可以是@Autowired(required = true)的。其他要么不加,要么required应该为false。

Spring最不推荐使用@Required表达非空。建议可以使用@Autowired(required = true)。

但更推荐使用java.util.Optional:

或者@Nullable(spring也提供了, JSR-305的javax.annotation.Nullable 也提供了)

推荐使用这个注解关联Spring的一些bean:

BeanFactory, ApplicationContext, Environment, ResourceLoader, ApplicationEventPublisher, and

MessageSource, ConfigurableApplicationContext ResourcePatternResolver

注意:@Autowired, @Inject, @Value, @Resource是由BeanPostProcessor实现功能的,所以不能应用在BeanPostProcessor 或者 BeanFactoryPostProcessor实现里。

@Primary

针对@Autowired有多个符合条件的bean的场景,用哪个的问题,Spring提供了@Primary来标注这个bean是首选项。

Qualifiers

除了@Primary,Spring还提供了Qualifier帮助缩小范围。

默认是用名称,也可以自己指定id或者名称

如果想根据bean的名字自动注入,建议使用@Resource而不是@Autowired。

if you intend to express annotation-driven injection by name, do not primarily use

@Autowired, even if it is capable of selecting by bean name among type-matching candidates.

Instead, use the JSR-250 @Resource annotation, which is semantically defined to identify a specific

target component by its unique name, with the declared type being irrelevant for the matching

process. @Autowired has rather different semantics: After selecting candidate beans by type, the

specified String qualifier value is considered within those type-selected candidates only (for

example, matching an account qualifier against beans marked with the same qualifier label).

c.f. @Autowired vs @Resource

    • @Autowired根据类型,@Resource根据名字
    • @Autowired应用范围:字段、构造函数、多参方法
    • @Resource应用范围:字段,或者单参setter方法

泛化的Qualifiers

CustomAutowireConfigurer

CustomAutowireConfigurer是Spring提供的BeanFactoryPostProcessor,它允许注册自定义的Qualifier。

@Resource

javax.annotation.Resource是JSR-1504规范里的注解。适用于成员字段和bean的属性setter方法。

有一个name参数,用于定义需要被注入的bean的名字。如果没有定义,就用字段的名字。

对一些内部的常用的依赖如BeanFactory, ApplicationContext, ResourceLoader, ApplicationEventPublisher, 以及 MessageSource。即使名字不一样,也可以根据类型注入进去。

@Value

常用于注入属性值(值来自外部)。

Spring提供了内嵌的值解析器(value resolver),如果找不到对应的值,会把值名称(如${catalog.name})注入。如果想要更严格的控制,可以提供一个自定义的PropertySourcesPlaceholderConfigurer。注意应该是个static方法注入。

@Configuration
public class AppConfig {
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer()
    {
        return new PropertySourcesPlaceholderConfigurer();
    }
}

SpringBoot提供了一个支持解析application.properties和application.yml的PropertySourcesPlaceholderConfigurer。

Spring提供了内嵌的转换器,支持简单的类型转换,如int Integer,以及逗号分割的会被拆成字符串数组。背后实现这个功能的是Spring提供了调用ConversionService来做转换的BeanPostProcessor,支持把字符串转为其他类型。也可以自定义一个ConversionService:

@Value也能接收SpEL表达式的。

@Component
public class MovieRecommender {
    private final String catalog;
    public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }")
                            String catalog) {
        this.catalog = catalog;
    }
}
@Component
public class MovieRecommender {
    private final Map<String, Integer> countOfMoviesPerCatalog;
    public MovieRecommender(
        @Value("#{{'Thriller': 100, 'Comedy': 300}}") Map<String, Integer>
        countOfMoviesPerCatalog) {
        this.countOfMoviesPerCatalog = countOfMoviesPerCatalog;
    }
}

@PostConstruct and @PreDestroy

这俩也是JSR-250规范里的。Spring2.5提供了CommonAnnotationBeanPostProcessor支持他们。

@Resource,@PostConstruct和@PreDestroy在JDK 6 to 8都是直接提供的。但是自从JDK9开始,整个javax.annotation都被剥离开,并且在JDK11里直接移除了。需要的话,从maven里加上javax.annotation-api依赖就好了。

Classpath Scanning and Managed Components

前面说的都是通过xml加载元配置,生成BeanDefinition。前面虽然也介绍了一些含有元配置含义的注解,但都是在依赖注入的范围里。至于基础的BeanDefinition还是在xml文件里的。事实上我们也可以使用注解、AspectJ type expressions,或者自定义的筛选条件来确定哪些类需要生成对应的BeanDefinition。

Spring3.0及以后,Spring Framework提供 @Configuration, @Bean,@Import, @DependsOn 来支持注解定义元配置。

模式注解

模式注解用于表达组件在应用中的角色。

    • @Component 泛化的概念,表示需要被Spring管理的组件,另外仨都是它的细化
    • @Repository 持久化,如数据库(Data Access Object or DAO)
    • @Service 服务层
    • @Controller 展示层

虽然@Component更像万金油,但是把含义分清楚,在需要引入切面的时候,会更好些。

元注解与复合注解

Spring提供的注解里,有很多都是元注解。可以自有组合和封装自己的注解。

自动识别Class并注册BeanDefinition

Spring容器可以自动识别标记了模式注解的类,并且创建对应的BeanDefinition。只需要在标记了@Configuration的类上加上@ComponentScan即可,basePackages是扫描范围,多个的话可以用逗号,分号或者空格分割。

如果开启了component scan的话,会自动开启AutowiredAnnotationBeanPostProcessor 和 CommonAnnotationBeanPostProcessor。如果不想开启的话,把annotation-config设置为false即可。

自定义扫描规则

Spring默认扫描@Component, @Repository, @Service, @Controller, @Configuration,以及标记了其他含有@Component元注解的注解。如果想在这个事情上做手脚,可以自定义筛选器,然后注册到 @ComponentScanincludeFilters或者excludeFilters属性。也可以XML:<context:component-

scan>的<context:include-filter /> 或者 <context:exclude-filter /> 子标签。

每个筛选器,有俩属性,type和expression。

例子:

如果设置useDefaultFilters=false,那所有的默认的都会被关闭。

在Components里定义Bean元数据

之前介绍的都是在@Configuration类里使用@Bean定义bean的元数据。在@Component类里也可以定义bean的元数据,同样是使用@Bean。

@Component
public class FactoryMethodComponent {
    @Bean
    @Qualifier("public")
    public TestBean publicInstance() {
        return new TestBean("publicInstance");
    }
    public void doWork() {
        // Component method implementation omitted
    }
}

Bean的元数据信息很多。@Bean只能告诉我们构造是通过这个工厂方法构造的,bean的名字是方法名(或者@Bean注解的值),其他的信息,如Scope,延迟加载,也可以通过注解提供,如@Scope, @Lazy。

@Component
public class FactoryMethodComponent {
    private static int i;
    @Bean
    @Qualifier("public")
    public TestBean publicInstance() {
        return new TestBean("publicInstance");
    }
    // use of a custom qualifier and autowiring of method parameters
    @Bean
    protected TestBean protectedInstance(
        @Qualifier("public") TestBean spouse,
        @Value("#{privateInstance.age}") String country) {
        TestBean tb = new TestBean("protectedInstance", 1);
        tb.setSpouse(spouse);
        tb.setCountry(country);
        return tb;
    }
    @Bean
    private TestBean privateInstance() {
        return new TestBean("privateInstance", i++);
    }
    @Bean
    @RequestScope
    public TestBean requestScopedInstance() {
        return new TestBean("requestScopedInstance", 3);
    }
}

@Lazy除了标记延迟初始化,也可以用在依赖注入的场景@Autowired或者@Inject上,等真正需要调用的是才初始化。不过依赖注入的场景,@Lazy是有限制的,尤其复杂交互和可空依赖(optional dependencies),这俩场景建议使用 ObjectProvider,更强大一点。

Spring4.3开始,可以使用类型为InjectionPoint或者DependencyDescriptor(前者的子类)来获取触发bean实例化的依赖注入点信息。注意它触发时机是实例化的时候,而不是依赖注入的时候,所以对Prototype Scope的bean才有意义。

@Component
public class FactoryMethodComponent {
    @Bean @Scope("prototype")
    public TestBean prototypeInstance(InjectionPoint injectionPoint) {
        return new TestBean("prototypeInstance for " + injectionPoint.getMember());
    }
}

Configuration BeanDefinition vs Component BeanDefinition

在@Configuration里的@Bean工厂方法,如果需要其他对象的字段或者方法,会创建那个对象的BeanDefinition的引用,使用的时候会通过容器。这样就能享受Spring提供的生命周期管理和代理功能。但如果是在@Component下的@Bean工厂方法,就是普通的Java语义了。

可以把@Bean工厂方法声明成静态的,这样就不必把它所在的类实例化了。尤其在提供post-processor beans的时候(如BeanFactoryPostProcessor or BeanPostProcessor)。因为这些bean要在容器生命周期的较早时期初始化的,应该要避免它们的初始化触发其他的配置过早执行。

对静态@Bean方法的调用,是不能被AOP增强的,因为CGLIB不能增强静态方法。另外,直接调用@Bean方法的话,就会创建一个全新的对象,也不会纳入Spring管理。

@Bean方法不建议声明为private或者final,其他的随意。

@Bean方法也可以声明在component类的父类或者配置类上,甚至可以是component类实现的接口的default方法上。

一个类可以有多个@Bean方法,多个@Bean方法可以提供同一个bean,根据运行时的依赖就位情况选择。符合条件的依赖参数越多的那个,是最终被选中的。

被扫描Bean的命名

被扫描的bean的命名,由BeanNameGenerator负责。

自定义命名规则

实现接口BeanNameGenerator

@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
    // ...
}

如果两个包有同样名字的Component定义了,默认的会抛错命名冲突。可以使用FullyQualifiedAnnotationBeanNameGenerator改成用全限定名。

但还是更推荐在注解里制定好名字。

被扫描Bean的生命周期Scope

@Scope("prototype"),@RequestScope ...

自定义Scope:ScopeMetadataResolver

@Configuration
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
public class AppConfig {
    // ...
}

注解提供Qualifier元信息

可以在bean定义的地方使用注解提供qualifier元信息(xml的话是使用qualifier或者meta属性):

注意和xml的区别是,注解是作用在类上的,所以如果这个类有多个bean,那都受影响。如果是在xml里配置,可以每个bean提供不同的元信息。

为目标组件创建索引

虽然classpath扫描也不慢,但仍然可以提升,提升方式是在编译期创建一个静态的组件列表。容器如果检测到索引,会直接使用,不再重新扫描。

要创建索引,需要引入maven依赖:

<dependencies>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-indexer</artifactId>
    <version>{spring-version}</version>
    <optional>true</optional>
  </dependency>
</dependencies>
dependencies {
    compileOnly "org.springframework:spring-context-indexer:{spring-version}"
}
dependencies {
    annotationProcessor "org.springframework:spring-context-indexer:{spring-version}"
}

spring-context-indexer会创建一个META-INF/spring.components 文件,放进jar包。

如果项目只是一部分有索引,一部分没有,那设置spring.index.ignore为true,没有索引部分还是会走扫描。

JSR-330标准的注解

Spring从3.0版本起开始支持JSR-330标准的注解(主要就是依赖注入相关的)。Spring的支持方式是和Spring自己的注解一样一样的。想要使用这些注解的话,首先得加入依赖

<dependency>
  <groupId>javax.inject</groupId>
  <artifactId>javax.inject</artifactId>
  <version>1</version>
</dependency>x

@Inject和@Named--依赖注入

@Inject和@Autowired用法差不多。@Autowired可以用在字段上、方法上,构造函数的参数上,@Inject也都可以。另外还可以把注入点声明成Provider,通过Provider.get()在需要时才获取(适合声明周期scope更小的bena或者懒加载场景)。

import javax.inject.Inject;
public class SimpleMovieLister {
    private MovieFinder movieFinder;
    @Inject
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
    public void listMovies() {
        this.movieFinder.findMovies(...);
        // ...
    }
}

@Named相当于@Qualifier的作用,用来根据名字确定用注入哪个bean。

java.util.Optional 和 @Nullable也可以和@Inject搭配使用,并且更合适,因为@Inject并没有required参数。

@Named 和 @ManagedBean -- 标准语义下的@Component

语义是完全相同的,区别是这俩不是元注解,所以不能拿他们搭自己的注解哈。

JSR-330标准注解的局限

Java-based Container Configuration

AnnotationConfigApplicationContext

@Configuration类以及它里面的@Bean方法,各自都是单独的BeanDefiniiton。@Component类、@Named类,以及@ManagedBean类也是单独的BeanDifinition。

如果走XML,由ClassPathXmlApplicationContext处理。

走注解, 由AnnotationConfigApplicationContext处理。

  • 使用构造函数创建容器
public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}
public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class);
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}
  • 使用register配置容器
public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.register(AppConfig.class, OtherConfig.class);
    ctx.register(AdditionalConfig.class);
    ctx.refresh();
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}
  • 开启Component Scanning

public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.scan("com.acme");//扫出来
    ctx.refresh();
    MyService myService = ctx.getBean(MyService.class);
}

AnnotationConfigWebApplicationContext

针对网络应用的变种。用它来配置Spring的ContextLoaderListener来监听Servlet, Spring MVC DispatcherServlet, 等等。

  • Spring MVC web application等价的web.xml
<web-app>
  <!-- Configure ContextLoaderListener to use AnnotationConfigWebApplicationContext
  instead of the default XmlWebApplicationContext -->
  <context-param>
    <param-name>contextClass</param-name>
    <param-value>
      org.springframework.web.context.support.AnnotationConfigWebApplicationContext
    </param-value>
  </context-param>
  <!-- Configuration locations must consist of one or more comma- or space-delimited
  fully-qualified @Configuration classes. Fully-qualified packages may also be
  specified for component-scanning -->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>com.acme.AppConfig</param-value>
  </context-param>
  <!-- Bootstrap the root application context as usual using ContextLoaderListener
  -->
  <listener>
    <listener-class>
      org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  <!-- Declare a Spring MVC DispatcherServlet as usual -->
  <servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-
      class>
      <!-- Configure DispatcherServlet to use AnnotationConfigWebApplicationContext
      instead of the default XmlWebApplicationContext -->
      <init-param>
        <param-name>contextClass</param-name>
        <param-value>
          org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
      </init-param>
      <!-- Again, config locations must consist of one or more comma- or space-
      delimited
      158
      and fully-qualified @Configuration classes -->
      <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.acme.web.MvcConfig</param-value>
      </init-param>
  </servlet>
  <!-- map all requests for /app/* to the dispatcher servlet -->
  <servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/app/*</url-pattern>
  </servlet-mapping>
</web-app>

@Bean

使用@Bean告诉容器需要注册一个bean definition,类型是方法的返回值类型,bean名字默认是方法名。方法必须放在受Spring管理的Bean的类里(@Configuraiton,@Component ... ),或者它实现的接口上(默认方法),或者它的父类里。但是如果放接口,那只有它真正实例化了之后,才有可能通过它实际的类型找到它。

Bean之间的依赖

工厂方法的入参表示当前bean创建所需的依赖。

依赖解析的逻辑,和构造函数依赖,是一样的。

生命周期回调

JSR-250的 @PostConstruct , @PreDestroy

Spring生命周期接口:InitializingBean, DisposableBean, or Lifecycle

支持标准的*Aware接口 (如BeanFactoryAware, BeanNameAware, MessageSourceAware, ApplicationContextAware)

XML的 init-method , destroy-method属性

bean的类里的public的close和shutdown方法会被自动注册为销毁回调。如果有这样的方法,但实际不是,注意要加@Bean(destroyMethod="")

  • 声明bean别名

  • bean的描述

@Configuration

只能作用于类,标注它是bean definitions的来源之一

Lookup Method注入

如果 singleton-scoped bean 依赖了 prototype-scoped bean,可以使用lookup method injection。

其他

两个bean依赖同一个bean,用的几个?

启动时,由@Bean方法提供的bean,都会用CGLIB创建子类实例代理,在代理类里,会先看下容器里是不是已经有对应scope的实例了,没有才会去创建。前提是@Configuration类没有被声明为final,工厂方法没有被声明成final。

复合注解

Spring支持业务开发创建自己的复合注解,以降低配置的复杂度。

@Import

@Configuration
public class ServiceConfig {
    @Bean
    public TransferService transferService(AccountRepository accountRepository) {
        return new TransferServiceImpl(accountRepository);
    }
}
@Configuration
public class RepositoryConfig {
    @Bean
    public AccountRepository accountRepository(DataSource dataSource) {
        return new JdbcAccountRepository(dataSource);
    }
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
    @Bean
    public DataSource dataSource() {
        // return new DataSource
    }
}
public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig
                                                                    .
                                                                    class);
    // everything wires up across configuration classes...
    TransferService transferService = ctx.getBean(TransferService.class);
    transferService.transfer(100.00, "A123", "C456");
}

@Configuration @Bean 加条件

@Profile的底层实现是@Conditional。@Conditional表示在注册当前的bean之前,先要用对应的

org.springframework.context.annotation.Condition判断一下要不要注册。Condition的实现类应该提供一个matches方法,返回boolean。

@Profile的Condition实现类是这样的:

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    // Read the @Profile annotation attributes
    MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
    if (attrs != null) {
        for (Object value : attrs.get("value")) {
            if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
                return true;
            }
        }
        return false;
    }
    return true;
}

注解与XML的混合使用

  • 以XML为主:

  • 以注解为主:

环境抽象

Spring中对环境的抽象是Environment接口,包括两大概念:profiles,proerties。

profile的概念是一堆bean definition的组合,如果这个Profile被激活,就会加载这堆bean definition。反之不加载。

properties指的就是大家理解的属性。来源可以是JVM系统参数,系统环境变量,JNDI,servlet上下文参数,额外的Properties对象,Map对象等。

Bean Definition Profiles

现有测试环境:

生产环境:

注意如果需要使用JNDI的话,要么用JndiLocatorDelegate,要么用InitialContext。尽量不用 JndiObjectFactoryBean,因为它会强制把返回类型转成FactoryBean。

profile里传的可以是简单的名字,也可以是表达式。!,&,|。注意与和或要一起用的话必须加括号,比如production & (us-east | eu-central)。不允许production & us-east | eu-central。

@Profile也可以作为元注解用的:

一个类里可以有俩。

注意一点,如果有多个重载工厂方法,那@Profile只对第一个被加载的有效。

用XML的话长这样:

激活Profile

注意可以同时激活多个Profile的。

手工改Environment

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

修改属性spring.profiles.active

可以通过修改system environment variables, JVM system properties,

servlet context parameters in web.xml, or even as an entry in JNDI (see PropertySource Abstraction).

如:

-Dspring.profiles.active="profile1,profile2"

默认profile

要修改不用default用其他名字做缺省项的话,可以调用Environment的setDefaultProfiles() 方法或者设置

spring.profiles.default属性。

PropertySource抽象

这段代码里,我们问了Spring一个问题,环境里有没有“my-property”属性呀?Environment要回答这个问题,就需要在一组PropertySource对象里搜索一遍。PropertySource是一个KV对形式的对属性的抽象。而StandardEnvironment是Spring提供的,包含了两个PropertySource对象,一个是JVM系统参数(System.getProperties()),另一个是系统的环境变量(System.getenv())。

StandardServletEnvironment在StandardEnvironment的基础上,还包含了其他额外属性源如servlet配置、servlet上下文属性,也可以开启JndiPropertySource。

搜索的过程是有序的。默认的话,系统参数高于环境变量。

对于普通的StandardServletEnvironment,优先级是这样的:

  1. ServletConfig parameters

  2. ServletContext parameters (web.xml context-param entries)

  3. JNDI environment variables (java:comp/env/ entries)

  4. JVM system properties (-D command-line arguments)

  5. JVM system environment (operating system environment variables)

想要调整顺序的话,手工调用MutablePropertySources提供的方法。

如何自己定义一个PropertySource

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());

@PropertySource

@PropertySource允许往Environment添加PropertySource 。

@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {
    @Autowired
    Environment env;
    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}

@PropertySource里的占位符${…}是其他参数里拿的。

@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {
    @Autowired
    Environment env;
    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}

注册LoadTimeWeaver

Spring用LoadTimeWeaver在类被加载进JVM的时候去改变类。要开启的话,需要在@Configuration类上加@EnableLoadTimeWeaving注解:

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
<beans>
  <context:load-time-weaver/>
</beans>

在JPA场景和Load-time Weaving with AspectJ 场景有用。

ApplicationContext的能力

国际化-MessageSource

applicationContext实现了接口MessageSource,所以能够提供i18n能力。Spring还挺了接口HierarchicalMessageSource,来实现层级化的多语言。

    • String getMessage(String code, Object[] args, String default, Locale loc)
    • String getMessage(String code, Object[] args, Locale loc)
    • String getMessage(MessaveSourceResolvable resolvable, Locale locale)

在加载容器的时候,它会自动去搜索MessageSource这个bean(名字必须是messageSource)。如果找不到的话,会去父容器里找,如果真的完全找不到,就创建一个空的DelegatingMessageSource。

Spring提供了3个MessageSource实现类:ResourceBundleMessageSource,

ReloadableResourceBundleMessageSource 和 StaticMessageSource。 StaticMessageSource是提供手工添加多语言配置的,所以基本上不会使用。

ResourceBundleMessageSource配置示例:

可以使用MessageSourceAware接口获取MessageSource,所有被容器管理的bean,如果实现了接口MessageSource,都会被注入容器的MessageSource。

Spring的MessageSource是基于Java的ResourceBundle的。所以如果有多个重名的bundle,只会拿第一个,忽略其他的。

ReloadableResourceBundleMessageSource[5]是Spring提供的,允许从任何Spring资源位置(不仅限于根路径),支持热更新多语言资源文件(并且也支持缓存的噢)。

标准事件和自定义事件

标准事件

容器对事件的支持,主要靠ApplicationEvent类和AppliationListener接口,实现是标准的观察者模式。容器里的实现了ApplicationListener的bean,在每次有ApplicationEvent发布到容器的时候,都能被通知到。

从Spring 4.2开始,事件不一定非得是ApplicationEvent的子类,容器会把它包装一下送出来。

Spring提供的标准事件:

    • ContextRefreshedEvent -- 容器调用refresh()方法的时候。此时所有bean都被加载,post-processors被激活,singleton scoped beans被实例化,容器可以使用。有些容器支持热更新,那refresh可能会有多次(如XmlWebApplicationContext)。
    • ContextStartedEvent

    • ContextStoppedEvent,stop了也可以再次start

    • ContextClosedEvent -- 触发点,手工close,或者JVM的shutdown钩子。close表示所有bean都会被销毁,生命周期结束了,是不能再次启动的。

    • RequestHandledEvent 和ServletRequestHandledEvent(实际发出的是ServletRequestHandledEvent) 只在使用了DispatcherServlet的场景下适用,告诉大家请求已经处理完啦。

自定义事件

定义事件

public class BlockedListEvent extends ApplicationEvent {
    private final String address;
    private final String content;
    public BlockedListEvent(Object source, String address, String content) {
        super(source);
        this.address = address;
        this.content = content;
    }
    // accessor and other methods...
}

发布事件

调用ApplicationEventPublisher的publishEvent()发布事件。通常是通过实现接口ApplicationEventPublisherAware并且把实现类注册为bean。

public class EmailService implements ApplicationEventPublisherAware {
    private List<String> blockedList;
    private ApplicationEventPublisher publisher;
    public void setBlockedList(List<String> blockedList) {
        this.blockedList = blockedList;
    }
    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }
    public void sendEmail(String address, String content) {
        if (blockedList.contains(address)) {
            publisher.publishEvent(new BlockedListEvent(this, address, content));
            return;
        }
        // send email...
    }
}

接收事件

public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {
    private String notificationAddress;
    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }
    public void onApplicationEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

注意一下,默认的话,listener的处理是同步的哈。也就是说publishEvent()是一个一个通知的,通知了一个,一个处理完了,再下一个。如果不希望这样,可以看下ApplicationEventMulticaster和SimpleApplicationEventMulticaster。

Spring自带的事件实现是一个简单的,给同一个容器内的bean使用的模型。如果更复杂的场景,可以看下Spring Integration[6]。

事件注解

@EventListener 接收事件

如果要发布多个事件,返回类型就改成事件集合。

异步监听

局限:

  1. 如果监听器抛错,那发出方是无感知的
  2. 异步监听不能通过声明返回值类型来发出后续事件,需要注入ApplicationEventPublisher手工处理。

监听顺序

使用@Order

泛型事件

比如发布的是EntityCreatedEvent,接收只接收EntityCreatedEvent。

访问底层资源

容器都是ResourceLoader,可以用来加载Resource对象。Resource是对JDK URL类的增强。Resource可以是任何底层资源,包括类的根目录,文件系统,任何可以用标准URL表示的位置。

实现了ResourceLoaderAware的bean,在创建的时候,会传入ResourceLoader(实际就是容器自己),这个bean就可以获取到静态资源了。

应用启动的追踪

容器内置了ApplicationStartup,会搜集StartupStep数据(tag, end),涉及这几个阶段:

    • 容器声明周期(包扫描,配置文件管理)
    • bean的声明周期(初始化,smart initializaiton,post processing)
    • 事件处理

Spring还提供了FlightRecorderApplicationStartup,要启用的话,就在Spring刚启动的时候注入它的实例。

BeanFactory

其他

boilerplate 重复但又必须要的代码,模块化代码

【1】 官方文档里,空格用的是white space,理论上,white space是有很多种的,不只是空格,所有不可见字符都算,包括tab、回车等。但是还没有去试过,也还没仔细看源码……就先认为是空格吧~~~~~

【2】规则在java.beans.Introspector.decapitalize里

【3】要求编译时候加上debug flag,或者在构造函数上加注解@ConstructorProperties

package examples;
public class ExampleBean {
    // Fields omitted
    @ConstructorProperties({"years", "ultimateAnswer"})
    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}

【4】Java EE的概念,如JSF,JAX-WS,都是SOA时期架构的遗留了,我理解Spring刚出现的时候可能并没有想着完全干掉SOA,只是希望作为补充或者第二选项。所以Spring文档的全篇都挺谦虚的,并且鼓励一定要和Spring解耦的写代码。

【5】看了下ReloadableResourceBundleMessageSource的API,说是可以从其他路径拿,但是似乎指的也是网络服务容器(Tomcat等)的资源路径下。如果走SpringBoot的话,可能也没啥用,因为都被打包进去了。

【6】Spring自己这么想的,我是觉得为啥不用消息队列。

参考资料

www.baeldung.com/cs/dependen…

github.com/spring-proj…

en.wikipedia.org/wiki/Whites…

www.zhihu.com/question/25…

huangyijie.com/2019/10/22/…

github.com/spring-proj…

blog.csdn.net/song8546011…

docs.spring.io/spring-fram…

juejin.cn/post/702672…

www.baeldung.com/java-profil…