首页 sping核心模块
文章
取消

sping核心模块

第一部分的IOC容器主要三大块:

  • bean在IOC中是如何组织的
  • ioc容器的自定义和拓展实现
  • spring的常用注解以及实现

一. IOC容器

1.1 BeanFactory与ApplicationContext

org.springframework.beans和org.springframework.context是构成spring IOC容器的基本包。

Beanfactory是提供所有对象管理的基础接口, ApplicationContext是BeanFactory的子接口, 为beanfactory补充以下内容:

  1. 与AOP集成
  2. 消息资源处理(Message Resource Handler), 用于国际化处理
  3. 消息监听发布机制
  4. 应用的特定上下文,例如WebApplicationContext,用于web应用程序

有关BeanFactory与ApplicationContext的详细解析参考源码解析中的章节。

1.2 容器概述

org.springframework.context.ApplicationContext接口负责实例化IOC容器,负责实例化,组装和配置Bean。容器通过配置的元数据进行实例化容器, 元数据定义的方式有 : java注解,xml和java代码的形式。

通常一个IOC容器不需要显式手动创建, 在web程序中由web容器的监听器去拉起创建IOC容器,在SpringBoot程序中由ApplicationContextRunner和CommandContextRunner接口进行拉起。

1.3 bean概述

spring ioc容器中管理多个bean,这些bean在容器中由BeanDefinition进行定义和管理。

一个BeanDefinition主要包含一下信息:

  1. 包限定的全类名, 为bean的实际实现类。
  2. bean的行为配置元数据,包括 scope(作用域),lifecycle callback(生命周期回调)等。
  3. 这个bean所需的其他bean的引用
  4. 创建新对象的时候其他的配置

BeanDefinition 的属性构成

属性解释
Classbean实例化
NameBean命名
ScopeBean作用域
Constructor arguments依赖注入
Properties依赖注入
Autowiring modeAutowiring Collaborators
Lazy initialization mode延迟加载
Initialization method初始化回调
Destruction method初始化回调

1.3.1 Bean实例化

bean实例化有两种方式:

  1. 指定类名,由IOC容器进行反射调用构造函数构造
  2. 通过工厂类包含的静态工厂方法构建
1
2
3
4
5
6
<!-- 默认发射构建 -->
<bean id="exampleBean" class="examples.ExampleBean"/>
<!-- 静态方法构建 -->
<bean id="clientService"
    class="examples.ClientService"
    factory-method="createInstance"/>
1
2
3
4
5
6
7
8
public class ClientService {
    private static ClientService clientService = new ClientService();
    private ClientService() {}

    public static ClientService createInstance() {
        return clientService;
    }
}

###### 确定Bean运行时类型

bean的运行时类型可能是CGLIB等字节工具生成的包装类, 也可能是工厂方法生成的实现类。要想获取IOC容器中在运行时的实际类型方法是通过BeanFactory.getType(String name)方法进行获取。

1.3.2 Bean命名

每个bean都会有一个或多个标识符。这些标识符都需要在IOC容器中是唯一的, 通常一个bean只有一个标识符,但是如果需要也可以多个, 其他的就会被视为别名。

在基于xml的配置中, 可以通过id和name来标识bean, id是唯一的标识, name作为别名, 通过逗号分隔。

如果不显式指定bean的id和name的话,IOC容器会为其自动生成一个id, 如果你想引用的话可以通过xml的ref元素进行引用。

**在BeanDefinition外给Bean定义别名**

在大型系统协作中, 公共bean定义的地方声明的别名与其他系统的命名往往不是一致的,并且要在公共bean声明定义的地方就声明所有的别名不现实,于是spring还提供了另外一种方式, 在自己的各个子系统中声明别名

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

使用java 配置方式@Bean进行配置的参考 @Bean

1.4 依赖注入

Spring IOC容器中的依赖注入主要有两种方式:

  1. 构造器依赖注入,带非空校验
  2. Setter依赖注入,默认不带非空校验,可以配合@Required实现强非空和强依赖效果

1.4.1 构造器注入

1.4.1.1 构造函数的参数解析

当构造函数的入参类型没有歧义的时候(歧义 : 例如第一个参数和第二个参数类型有依赖关系),构造函数的注入功能只需要知道这个构造函数的依赖入参的顺序和类型就可以了.

1
2
3
4
5
6
7
8
package x.y;

public class ThingOne {

    public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
        // ...
    }
}
1
2
3
4
5
6
7
8
9
  <beans>
      <bean id="beanOne" class="x.y.ThingOne">
          <constructor-arg ref="beanTwo"/>
          <constructor-arg ref="beanThree"/>
      </bean>
  
      <bean id="beanTwo" class="x.y.ThingTwo"/>
      <bean id="beanThree" class="x.y.ThingThree"/>
  </beans>
1.4.1.2 基础类型的参数解析

依赖注入的时候需要引用别的bean的时候, IOC容器是可以知道这个Bean的类型的,但是如果这个参数是个基础类型的话, 例如 int, boolean这些的时候, IOC容器是感知不到, 需要用户手动指定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package examples;

public class ExampleBean {

    // Number of years to calculate the Ultimate Answer
    private final int years;

    // The Answer to Life, the Universe, and Everything
    private final String ultimateAnswer;

    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 通过类型区分 -->
<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>

<!-- 通过代码中的参数名字进行匹配,前提条件,在编译的时候使用debug标志位进行编译,这样能保留参数入参的名称 --> 
<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg name="years" value="7500000"/>
    <constructor-arg name="ultimateAnswer" value="42"/>
</bean>

上面的这个通过参数名称进行匹配的方式如果不想使用debug进行编译的话,也可以通过使用JDK注解@ConstructorProperties进行编译,这个注解也会保留对应的参数名。

1
2
3
4
5
6
7
8
9
10
11
12
package examples;

public class ExampleBean {

    // Fields omitted

    @ConstructorProperties({"years", "ultimateAnswer"})
    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}

1.4.2 Setter注入

基于setter的注入方式是在IOC容器反射调用无参构造函数或者工厂类方法生成bean以后,在调用bean的setter方法实现的。

**选择构造函数注入还是Setter注入?**

编程原则:对于强依赖的属性使用构造函数注入,对于可选依赖使用setter注入方式

在setter方法上使用@Required注解可以使其变成非空强依赖。

构造函数注入建议优先使用,可以确保依赖项不是null。 如果一个类的构造函数入参太多证明这个类的负责的功能太多了,需要进行重构。

Setter注入建议主要使用于存在默认值的属性中, 否则,在每次的使用中最好都有非空检查。Setter注入的好处是可以执行重注入操作。

1.4.3 依赖解析过程

  1. 根据配置元数据创建ApplicationContext。
  2. 创建bean的时候将依赖关系提供给bean
  3. 对每个属性或者构造函数进行设置值或者另一个bean的引用
  4. 将每个属性或者构造函数参数的值从实际值转换成所需要的实际类型的值, 默认情况下, spring ioc容器可以将String类型转换成所有的内置类型,例如int, long,String, boolean

IOC容器在构建容器的时候会对每个bean的配置进行验证, 但是在创建这个bean之前, 这些属性并不会被依赖注入和设置。

创建容器的时候只会对 作用域为单例且设置为预实例化的bean进行初始化,其他的bean均为在请求使用到的时候才会创建, 创建bean的时候可能会导致创建bean图。因此, 依赖项的不匹配可能会出现得晚一些。

**循环依赖**

循环依赖只会在使用构造函数注入构建的时候才会出现, 当A实例化的时候需要注入B , B实例化的时候又需要注入A的情况。

解决办法:

编辑构造函数入参的方式改成使用setter的方式进行入参, 虽然Setter不是推荐的方式, 但是可以Setter注入的方式解决循环依赖的问题。

底层逻辑就是, A与B循环依赖的时候,迫使其中一个bean在还没有完全实例化的时候就注入到另一个bean中。

1.4.4 依赖配置

  1. 直接值

    1
    2
    3
    4
    5
    6
    7
    
    <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>
    
  2. 内部bean

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    <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>
    

    inner bean不需要定义一个单独的id,且scope也会被忽略,外部不能直接访问inner bean,或者注入封闭bean之外的bean中。

…略, 中间一些xml的依赖配置如何配置, 如各种集合类型的配置,空值null处理

1.4.5 使用depends-on

用于强调弱依赖,不直接依赖,但是此bean需要前一个bean先完成初始化的时候使用。

1.4.6 延迟初始化

默认情况下,创建ApplicationContext的时候会创建单例bean,进行预实例化,可以指定bean配置为延迟加载,也可以指定IOC容器级别为延迟加载。

1.4.7 自动装配

1.4.8 方法注入

单例beanA需要注入prototype的beanB的时候,假如每次beanA的调用都需要一个新的beanB,IOC容器的默认反射注入是不支持的,因为属性输入只有一次机会,就是在创建bean的时候。

解决办法:

  1. 使用ApplicationContextAware接口,显示调用getBean的方式获取B,但是这种做法不推荐,因为业务代码耦合了spring的代码

  2. 查找方法注入(lookup method injection), 这种做法就是在需要用到prototype的bean的方法声明为抽象方法,同时搭配Lookup注解,这样spring就会通过CGLIB字节码技术,在声明lookup注解的方法上进行注入这个bean,如果这个bean是prototype就每次返回一个新的对象,如果这个bean声明是singletone则返回的是同一个对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    package fiona.apple;
    // no more Spring imports!
    public abstract class CommandManager {
       
        public Object process(Object commandState) {
            Command command = createCommand();
            command.setState(commandState);
            return command.execute();
        }
       
      	@Lookup	  //根据类型匹配
        protected abstract Command createCommand();
         
        @Lookup("myCommand")   //根据bean名字进行匹配	
        protected abstract Command createCommand();
    }
    

1.5 Bean作用域

Spring Framework中,总共定义了6种bean 的作用域,其中有4种作用域只有当应用为web应用的时候才有效,并且Spring还支持自定义作用域。

ScopeDescription
singleton(默认的)在每个Spring IoC容器中,一个bean定义对应只会有唯一的一个bean实例。
prototype一个bean定义可以有多个bean实例。每次的getBean()方法注入的都是一个新的实例
request一个bean定义对应于单个HTTP 请求的生命周期。也就是说,每个HTTP 请求都有一个bean实例,且该实例仅在这个HTTP 请求的生命周期里有效。该作用域仅适用于WebApplicationContext环境。
session一个bean 定义对应于单个HTTP Session 的生命周期,也就是说,每个HTTP Session 都有一个bean实例,且该实例仅在这个HTTP Session 的生命周期里有效。该作用域仅适用于WebApplicationContext环境。
application一个bean 定义对应于单个ServletContext 的生命周期。该作用域仅适用于WebApplicationContext环境。
websocket一个bean 定义对应于单个websocket 的生命周期。该作用域仅适用于WebApplicationContext环境。

当一个singleton的bean依赖一个prototype的bean的时候,如果想要在运行时每次调用的时候注入的都是一个new的prototype bean, 这是不行的, 如果想要实现这种结果,可以参考方法注入

1.6 定制Bean特性

spring提供一系列的接口定制bean的特性,主要有

  1. lifecycle回调接口
  2. ApplicationContextAware接口
  3. 其他的Aware接口

1.6.1 生命周期回调

Bean 生命周期接口

  1. InitializingBean 接口 afterPropertiesSet() 方法
  2. DisposableBean 接口 destroy() 方法

关于JSR 250

JSR 250规范主要是关于”资源”的一些预定义注解。这里的资源指的是一个java类的实例,也就是spring中的bean。JSR 250的所有的注解都在javax.annotation包中,和javax.annotation.security包中,分成两个部分,资源定义和安全控制。

javax.annotation中包含一下几个注解:

@Generated:标记产生的实例是一个资源。类似spring中的@Bean注解

@PreDestroy:销毁资源前的回调处理

@PostConstruct:创建资源后的回调处理

@Resource:标记使用资源的位置

@Resources:标记使用多项资源的位置

spring中实现了@PostConstruct、@PreDestroy和@Resource。

接口与注解取舍

建议使用@PostConstruct代替InitializingBean 接口的afterPropertiesSet()

Spring应用的生命周期接口,bean可以时间这个接口达到在spring生命周期中干一些事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface Lifecycle {

    void start();

    void stop();

    boolean isRunning();
}

public interface LifecycleProcessor extends Lifecycle {

    void onRefresh();

    void onClose();
}

1.6.2 启动顺序控制

上面的控制只针对声明周期进行了控制, 但是除了bean中的生命周期的各个阶段外, 有些场景对启动循序还会有要求,spring中对这种场景提供了smartlifecycle接口进行控制。

  • 实现了SmartLifecycle接口的对象, getPhase()方法返回的数值越小对象越先启动。默认在IOC容器中没有实现任何的lifecycle接口的对象默认的phase是0, 任何负相位的bean都会在正常的bean启动之前启动, 正常bean销毁之后销毁。
  • autoStartup发生在当bean进入IOC容器以后,onRefresh完成以后自动触发,而不是等待被主动调用strat方法。
1
2
3
4
5
6
7
8
9
10
11
12
public interface Phased {

    int getPhase();
}


public interface SmartLifecycle extends Lifecycle, Phased {

    boolean isAutoStartup();

    void stop(Runnable callback);
}

1.6.3 IOC容器优雅停止

提供jvm关闭钩子注册api context.registerShutdownHook()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
	@Override
	public void registerShutdownHook() {
		if (this.shutdownHook == null) {
			// No shutdown hook registered yet.
			this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) {
				@Override
				public void run() {
					synchronized (startupShutdownMonitor) {
						doClose();
					}
				}
			};
			Runtime.getRuntime().addShutdownHook(this.shutdownHook);
		}
	}

1.6.4 各种aware

感知spring架构中的特定内容。

1.7 IOC容器拓展点

通常Spring不推荐对ApplicationContext进行子类定制化,spring中通过提供接口来实现拓展spring的作用。

1.7.1 beans定制接口 BeanPostProcessor

在IOC容器实例化bean以后对bean做定制操作。

注意,此接口不可以改变bean的定义对象,如需修改bean的定义对象需使用接口BeanFactoryPostProcessor

–> Spring IOC容器实例化Bean –> 调用BeanPostProcessor的postProcessBeforeInitialization方法 –> 调用bean实例的初始化方法 –> 调用BeanPostProcessor的postProcessAfterInitialization方法

1.7.2 BeanPostProcessor注册

  1. 注解式声明,在@Configuration中使用@Bean注解标明。
  2. 编程式声明,ConfigurableBeanFactory.addBeanPostProcessor进行添加。

1.7.3 BeanPostProcessor与AOP代理

BeanPostProcess的生命周期的初始化时发生在ApplicationContext初始化的时候, 作为ApplicationContext的一部分。 AOP的实现也是借助BeanPostProcessor接口进行实现的,

常用场景:

将自定义注解与自定义的BeanPostProcessor结合起来使用,是拓展spring ioc容器的常用方式。

1.8 spring注解

xml与注解结合使用的方式。在注解配置bean元数据的方式中,各个提供bean的地方称作“配置”,与spring中的注解@Configuration相对应,就是指示获取bean的地方。

1.8.1 @Require

强制校验避免NPE,用于Bean的setter方法中。

注意:必须要将RequiredAnnotationBeanPostProcessor作为bean注册到IOC容器中先才可以正确识别处理@Required注解。

1.8.2 @Autowired

  • 可以用于造方法中,属性中,settter方法中,还有任意名称和多个参数的方法中。
  • 类型匹配注入的时候, 注入的对象类型应该尽量具体。
  • 可以使用在数组或者集合的属性或者setter方法上,表示让spring提供同一个类型的所有的bean对象。
    • 这个集合是没有顺序的, 如果需要有序的话可以实现org.springframework.core.Ordered接口或者使用注解@Order,@Priority

@Autowired@Inject@Value,和@Resource注释由Spring提供的BeanPostProcessor实现,自定义的BeanPostProcessor和BeanFactoryPostProcessor

1.8.3 @Primary(提供者)

单值bean属性,适配到一个bean有多个实例的时候, 需要通过指定优先级的方式进行装配。Primary注解便是通过指定提供者的优先级的时候使用。通常配合Bean注解一起使用

1.8.4 @Qualfier(消费者端)

特定装配

除此外, 还支持对qualfiler进行特定的装配, 对qualfiler进行特定的定制,定制的qualfiler与原生的关系就像Component与service, controller类似。

1.8.5 泛型自动装配

使用autowrie进行自动装配的时候可以通过泛型进行适配。泛型自动装配在运行时的自动校验。

1.8.6 @Resource

除了autowrie, primary, qualfier以外, spring还支持使用jsr-250中的resource注解进行注入。

1.8.7 @PostCustruct与PreDestroy

生命周期控制注解。

1.9 类路径扫描与组件扫描

本节主要关于如何使用注解定义注册进容器的组件类。

在spring的IOC容器中一切对于容器都是对象实例。但是容器中的实例又包含了很多中。 通用的配置javabean通过Bean注解进行标注注册进入IOC容器中。

1.9.1 @Component

IOC容器中有特定用途的实例我们称之为组件, component, 这些组件细化出来包括服务service, 仓库repository, 控制器controller。

spring注解定制与编程

1.9.2 元注解生成新注解

元注解就是定义注解的注解。spring原有的注解可以作为元注解组成一个新的注解。例如@Component就作为@Service的元注解。

什么时候解析?

1.9.2 扫描与自定义过滤器

@ComponentScan 注解定义扫描路径, 通过可以配合

1.9.3 在Component中定义bean

在component注解的类中提供bean定义,功能与configuration注解的类定义bean一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class FactoryMethodComponent {

    @Bean
    @Qualifier("public")
    public TestBean publicInstance() {
        return new TestBean("publicInstance");
    }

    public void doWork() {
        // Component method implementation omitted
    }
}

1.9.10 组件索引

在启动过程中动态扫描classpath的过程虽然也很快, 但是在大应用启动的时候依然会很费事,可以通过生成组件缩影的方式加快启动速度。

1
2
3
4
5
6
7
8
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-indexer</artifactId>
        <version>5.1.4.RELEASE</version>
        <optional>true</optional>
    </dependency>
</dependencies>

1.10 JSR-330

jsr330反射注入规范。

1.11 spring配置文件与环境

Spring中的Environment接口是应用的两个关键属性的抽象:profile配置文件和property属性

1.11.1 @Profile

定义一个配置文件bean的时候可以通过这个注解指定此配置文件bean的环境。例如, 生产,测试等@Profile(“dev”),@Profile(“product”)

多个环境的配置文件生效方式,

api:ctx.getEnvironment().setActiveProfiles(“profile1”, “profile2”);

启动命令:-Dspring.profiles.active=”profile1,profile2”

默认的配置文件生效方式@Profile(“default”)

使用范例 一

1
2
3
4
5
6
7
8
9
10
@Configuration
@Profile("production")
public class JndiDataConfig {

    @Bean(destroyMethod="")
    public DataSource dataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

使用范例 二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class AppConfig {

    @Bean("dataSource")
    @Profile("development") 
    public DataSource standaloneDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }

    @Bean("dataSource")
    @Profile("production") 
    public DataSource jndiDataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

1.11.2 @PropertySource

什么是属性?

key-value键值对

什么是属性源?

提供key-value键值对的地方。

如何定义属性?

  1. -D参数传入定义
  2. 环境变量定义
  3. properties配置文件中定义

如何获取属性?

  1. 通过自动装配Environment进行编程式获取
  2. 通过${}占位符进行获取

Spring的Environment抽象接口提供层级式查找属性的方法。

Environment的实现与应用上下文的实现相关,初始化ApplicationContext的时候会初始化这个,对应的有StandardEnvironment, StandardServletEnvironment。

StandardEnvironment的属性源由 JVM系统属性参数 (System.getProperties()) 和 系统环境变量(System.getenv())两者组成。

**PropertySource定义 : **那么jvm系统属性环境变量 就可以称作是属性源(PropertySource)

除了这些自带的属性源以外还可以通过@PropertySource注解进行指定自定义的属性源。

  1. 编程API设置属性源
1
2
3
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
  1. @PropertySource定义属性源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@PropertySource("classpath:/com/myco/app.properties")//两种都可以
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")//如果my.placeholder已经设置则使用已经设置的, 否则就是用默认的default/path作为值
public class AppConfig {

    @Autowired
    Environment env;

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}

1.12 ApplicationContext的附加功能

1.12.1 MessageSource国际化支持

1.12.2 ResourceLoader资源加载

1.12.3 事件发布监听机制

ApplicationContext提供ApplicationEvent接口和ApplicationListener接口完成事件监听,典型的观察者模式。

内置事件:ContextRefreshedEvent, ContextStartedEvent, ContextStoppedEvent, ContextClosedEvent, ContextClosedEvent

自定义事件

  1. 创建自定义事件类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    public class BlackListEvent extends ApplicationEvent { //继承ApplicationEvent
       
        private final String address;
        private final String content;
       
        public BlackListEvent(Object source, String address, String content) {
            super(source);
            this.address = address;
            this.content = content;
        }
       
        // accessor and other methods...
    }
    
  2. 发布自定义事件

    通常通过Aware机制注册ApplicationEventPublisher,再通过publishEvent方法进行发布

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    public class EmailService implements ApplicationEventPublisherAware {
       
        private List<String> blackList;
        private ApplicationEventPublisher publisher;
       
        public void setBlackList(List<String> blackList) {
            this.blackList = blackList;
        }
       
        public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
            this.publisher = publisher;
        }
       
        public void sendEmail(String address, String content) {
            if (blackList.contains(address)) {
                publisher.publishEvent(new BlackListEvent(this, address, content));
                return;
            }
            // send email...
        }
    }
    
    1. 创建事件监听器

      从spring 4.2 开始支持注解创建时间监听器。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      
      public class BlackListNotifier {
            
          private String notificationAddress;
            
          public void setNotificationAddress(String notificationAddress) {
              this.notificationAddress = notificationAddress;
          }
            
          @EventListener
          public void processBlackListEvent(BlackListEvent event) {
              // notify appropriate parties via notificationAddress...
          }
      }
      
      1
      2
      3
      4
      
      @EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
      public void handleContextStart() {
          ...
      }
      
      1
      2
      3
      4
      
      @EventListener(condition = "#blEvent.content == 'my-event'")
      public void processBlackListEvent(BlackListEvent blEvent) {
          // notify appropriate parties via notificationAddress...
      }
      
异步事件监听器支持

使用@Async注解支持

1
2
3
4
5
@EventListener
@Async
public void processBlackListEvent(BlackListEvent event) {
    // BlackListEvent is processed in a separate thread
}
事件监听器排序
1
2
3
4
5
@EventListener
@Order(42)
public void processBlackListEvent(BlackListEvent event) {
    // notify appropriate parties via notificationAddress...
}
泛型事件监听器支持
1
2
3
4
@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
    ...
}

由于泛型擦除,所以上面这个监听器只有在触发监听器的那个事件是明确参数类型的时候才会触发。

于是可以通过下面的方式,为每个泛型类添加一个明确的实现类

class PersonCreatedEvent extends EntityCreatedEvent<Person> { … }

也就是说publish的那个事件必须是类似PersonCreatedEvent这种事件。

但是为每个类型事件创建一个明确的实现类并不好看, 所以spring还支持通过实现ResolvableTypeProvider接口的方式,在运行时告诉spring参数的类型。

1
2
3
4
5
6
7
8
9
10
11
12
public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {

    public EntityCreatedEvent(T entity) {
        super(entity);//传给ResolvableTypeProvider
    }

    @Override
    public ResolvableType getResolvableType() {
      //通过父类的getResource方法在运行时明确参数类型。
        return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
    }
}

二. Resources资源

2.1 简介

java标准的java.uet.URL访问接口支持多种类型的URL资源访问, 但是对于访问一些更底层的资源来说还不够

spring便提供了一个Resource接口,访问日常开发的时候需要用到的一些底层的接口

2.2 Resource 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface Resource extends InputStreamSource {

    boolean exists();

    boolean isOpen();

    URL getURL() throws IOException;

    File getFile() throws IOException;

    Resource createRelative(String relativePath) throws IOException;

    String getFilename();

    String getDescription();

}

public interface InputStreamSource {

    InputStream getInputStream() throws IOException;

}

spring对Resources的接口使用是很频繁的, 例如需要确定一个方法的参数类型的时候就需要用到,

2.3 内置Resource

2.4 ResourceLoader

1
2
3
4
5
public interface ResourceLoader {

    Resource getResource(String location);

}

spring的资源加载器,ResourceLoader也是和ApplicationContext的具体实现有关,每个具体的ApplicationContext都有一个自己的默认的ResourceLoader。

1
Resource template = ctx.getResource("some/resource/path/myTemplate.txt");

上面这个语句加载resource的url没有指定前缀,所以使用的是默认的ResourceLoader, 具体使用的是哪个类型的ResourceLoader,返回的是什么的Resource实现取决于ApplicationContext的具体实现。

ClassPathXmlApplicationContext返回的是ClassPathResource

` FileSystemXmlApplicationContext返回的是FileSystemResource`

WebApplicationContext返回的是ServletContextResource

除此以外,可以通过指定url前缀的方式进行指定资源加载器。

1
2
3
Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt");
Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt");
Resource template = ctx.getResource("http://myhost.com/resource/path/myTemplate.txt");

2.5 ResourceLoaderAware接口

2.6 作为属性注入

当一个属性的类型是Resource的时候可以通过配置一个url进行加载对应的资源。

1
2
3
<bean id="myBean" class="...">
    <property name="template" value="some/resource/path/myTemplate.txt"/>
</bean>

三. 验证、数据绑定与类型转换

Spring提供一种专有设计用于数据验证和数据转换绑定。数据绑定是用于动态将用户数据转换成应用的领域模型。spring提供DataBinder接口去做这件事情。ValidatorDataBinder组成了validation包。

BeanWrapper是spring中的一个基础概念, 一般你不需要接出到这个概念, 但是当你需要将用户数据绑定到应用的模型中的时候便需要了解它。

spring中的DataBinderBeanWrapper都是用到了PropertyEditorSupport实现解析和格式化属性值。

3.1 使用Validator接口进行校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class PersonValidator implements Validator {

    public boolean supports(Class clazz) {
        return Person.class.equals(clazz);
    }

    public void validate(Object obj, Errors e) {
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
        Person p = (Person) obj;
        if (p.getAge() < 0) {
            e.rejectValue("age", "negativevalue");
        } else if (p.getAge() > 110) {
            e.rejectValue("age", "too.darn.old");
        }
    }
}

3.2 使用BeanWrapper操作Bean

BeanWrapper提供对javabean的属性的get set操作。默认实现类是BeanWrapperImpl。

BeanWrapper通过定义一套字符串规范进行操作bean。

下表为操作规范的示例

表达式含义
name指代与属性相关的操作。如getName, setName,isName,
account.name指代嵌套属性的操作。getAccount().getName / getAccount().setName
account[2]指代此属性的第三个元素。此属性可能是个数组,也可能是个list等有序集合
account[COMPANYNAME]指代此属性的键映射的条目值,此属性是个map。
1
2
3
4
5
6
public class Company {

    private String name;
    private Employee managingDirector;
		//...get、set方法
}
1
2
3
4
5
6
7
8
public class Employee {

    private String name;

    private float salary;

    //...get、set方法
}
1
2
3
4
5
6
7
8
9
10
11
12
13
BeanWrapper company = new BeanWrapperImpl(new Company()); // 使用beanwrapper进行操作此bean。
// 通过属性表达式的方式设置name属性
company.setPropertyValue("name", "Some Company Inc."); 
// 常规做法
PropertyValue value = new PropertyValue("name", "Some Company Inc.");
company.setPropertyValue(value);

BeanWrapper jim = new BeanWrapperImpl(new Employee());
jim.setPropertyValue("name", "Jim Stravinsky");
company.setPropertyValue("managingDirector", jim.getWrappedInstance());

// 通过属性表达式进行检索嵌套属性
Float salary = (Float) company.getPropertyValue("managingDirector.salary");

3.3 属性编辑器PropertyEditor实现

Spring使用PropertyEditor这个概念来实现String和对象之间的转换。例如人类可读的日期, 2021-09-09转换成Date日期类。这个行为可以通过注册特定的java.beans.PropertyEditorBeanWrapper中。

spring中内置了一些PropertyEditor实现。

3.4 spring类型转换

3.4.1 convert spi

1
2
3
4
5
6
package org.springframework.core.convert.converter;

public interface Converter<S, T> {

    T convert(S source);
}

3.5 spring属性格式化

3.6 spring验证

spring支持jsr303检验标准api。

四. Spring 表达式语言SpEL

springel是spring操作ioc容器查询和操作对象图的sql

五. Spring 切面编程

AOP(Aspect Oriented Programming)切面编程. OOP(Object-Oriented Programming)面向对象编程的基本单位是类, AOP的切面编程则是对面向对象编程的补充, 提供另一种编程结构的思维-对类进行切面。通过切面的思维对一些对象编程的概念进行模块化(例如事务管理)。

Spring AOP的局限: Spring AOP仅支持方法执行作为连接点,对bean执行的方法作为进行拦截,而不支持对某个类字段的拦截,或者对构造函数的拦截。

关键定义:

@Aspect:面,每一个面都可以是一个 操作概念,编程概念的模块化。例如事务管理的这一个概念, 对所有的特定类进行统一使用切面编程实现对事务的管理, 这就可以说使用过切面实现了事务管理的这个概念。

常见的通过 这种切面统一实现的概念还有日志埋点, 监控,异常,入参的业务合法性校验

5.1 AOP概念

Aspect(切面): 使用切面编程的方式将一个概念进行模块化。

Join point(连接点): 切入的时机,连接在方法的什么时候(方法的调用前,调用后,抛出异常后,方法体环绕增强)。

Point cut (切入点): 连接点准入的条件判断,判定的是哪个方法。

Advice (在哪儿增强): 在某一个切入点执行的一系列增强操作链。这些操作链的点包括“around”, “before” 和 “after”。很多的AOP框架, 包括spring,使用Interceptor作为Advice的实现,内部维护的也是一条interceptor链条。

Introduction:为切面的目标类声明一个新的没实现的接口。具体的逻辑:声明一个新接口和对应的实现类,将这个实现类和切面的目标类关联起来,在运行时便可以将这个切面的目标类强转成这个新的接口类,然后调用这个接口方法。

Target object:切面目标类。

AOP proxy:由AOP框架生成的一个代理类,是实现上面的这些切面概念的实例。在spring框架中,代理类使用的是JDK动态代理或者CGBLIB代理进行实现。

Weaving:织入(植入)是将切面代码应用到目标对象的过程。织入分为编译期织入, 类加载期织入和运行期织入。

概念深入理解:

joinpoint、pointcut、advice的关系和区别。

  • joinpoint,是代码中可以插入切面代码的候选点, 这个点可以是 方法调用前中后,方法抛出异常,还可以是某个字段的值的修改。连接点不需要进行代码声明, 这是一个抽象的概念, 可以把代码执行流想象成由一个一个的连接点串起来执行的流,这一个个的连接点可以是方法执行权限等, 方法抛出异常。
  • advice,是这些切入这些连接点以后,对应执行的织入的切面增强代码,需要代码编写,入参可以拿到切入的连接点的信息 如连接点的入参,返回值和执行的方法块等,around的增强权限最大, 不仅可以获取连接点的信息,还可以决定连接点的调用。
  • pointcut,是匹配连接点的pattern

六. Spring AOP api

6.1 声明切面 @Aspect

作用是把当前类标识为一个切面供容器读取,意义排除自己在自动代理之外。

**6.2 切面增强(Advice) **

6.2.1 @Before

标识一个前置增强方法,相当于BeforeAdvice的功能,相似功能的还有

6.2.2 @AfterReturning

后置增强,相当于AfterReturningAdvice,方法正常退出时执行

6.2.3 @AfterThrowing

异常抛出增强,相当于ThrowsAdvice

6.2.4 @After

final增强,不管是抛出异常或者正常退出都会执行

6.2.5 @Around

环绕增强,相当于MethodInterceptor

6.2.6 @DeclareParents

引介增强,相当于IntroductionInterceptor

6.3 切点函数execution

execution函数用于匹配方法执行的连接点,语法为:

execution(方法修饰符(可选) 返回类型 方法名 参数 异常模式(可选))

6.8.1 参数部分允许使用通配符

  • * 匹配任意字符,但只能匹配一个元素

  • .. 匹配任意字符,可以匹配任意多个元素,表示类时,必须和*联合使用

  • + 必须跟在类名后面,如Horseman+,表示类本身和继承或扩展指定类的所有类

6.8.2 示例中的* chop(..)解读为

方法修饰符 无

返回类型 *匹配任意数量字符,表示返回类型不限

方法名 chop表示匹配名称为chop的方法

参数 (..)表示匹配任意数量和类型的输入参数

异常模式 不限

6.4 更多切点函数

除了execution(),Spring中还支持其他多个函数,这里列出名称和简单介绍,以方便根据需要进行更详细的查询

6.4.1 @annotation()

表示标注了指定注解的目标类方法

例如 @annotation(org.springframework.transaction.annotation.Transactional) 表示标注了@Transactional的方法

6.4.2 args()

通过目标类方法的参数类型指定切点

例如 args(String) 表示有且仅有一个String型参数的方法

6.4.3 @args()

通过目标类参数的对象类型是否标注了指定注解指定切点

如 @args(org.springframework.stereotype.Service) 表示有且仅有一个标注了@Service的类参数的方法

6.4.4 within()

通过类名指定切点

6.4.5 target()

通过类名指定,同时包含所有子类

如 target(examples.chap03.Horseman) 且Elephantman extends Horseman,则两个类的所有方法都匹配

6.4.6 @within()

匹配标注了指定注解的类及其所有子类

如 @within(org.springframework.stereotype.Service) 给Horseman加上@Service标注,则Horseman和Elephantman 的所有方法都匹配

6.4.7 @target()

所有标注了指定注解的类

如 @target(org.springframework.stereotype.Service) 表示所有标注了@Service的类的所有方法

6.4.8 this()

大部分时候和target()相同,区别是this是在运行时生成代理类后,才判断代理类与指定的对象类型是否匹配

关于this()和target()的区别

this(atype) 切面的目标是代理对象, 当一个bean是spring的aop代理对象且这个代理对象代理了atype,植入切面增强的只有属于atype的那些方法。应为一个 代理对象可以代理很多的接口。

target(atype) 切面的目标是目标对象,当一个bean是atype的实例化对象

为什么需要这两个呢?

6.5 切点 @PointCut

切点声明就是将公共的切点函数提取出来,放在有一个单独的地方, 增强在使用的时候只需要引用这个切点的方法即可。

切点单独出来的目的是为了减少重复的切点匹配,优化性能。

advice增强+直接声明的切点函数 和 advice + 预先声明的切点函数效果是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Aspect
@Commponent
public class UserviceAspect1{
  
  @Before("execution(* login(..))")  //增强上直接配合切点函数进行织入增强代码
  public void beforeLogin(){
    System.out.println("logging...");
  }
  
  @Before("UservicePointcut.login()") //增强上引入已声明的切点的
  public void initConn(){
    // init
  }
  
  @After("UservicePointcut.login()") //复用已经声明的切点,假如有多个切面增强这里的切点复用的优势便显现出来了
  public void applyLogging(){
    System.out.println("login success");
  }
}

@Aspect
@Commponent
public class UservicePointcut{
  
  @Pointcut("execution(* login(..))")
  public void login(){}
}

6.6 切面增强入参

任何的增强advice都可以声明第一个入参是org.aspectj.lang.JoinPoint,来获取连接点的信息(包括方法名,入参等)。

除了将连接点作为入参外,还可以将 连接点的入参作为入参传给 切面增强方法,要实现这个动作,可以使用args()表达式, 将args表达式中的参数换成增强方法入参的名称,便可将这个args表达式的入参类型限定为切面增强方法中声明的入参类型,同时将参数传给切面增强。

6.6.1 args()表达式

参数为类型名称(全类名)的时候:仅作为切点判定条件

参数为增强的参数名称(参数名)的时候:将参数名对应的参数类型作为判定连接点的入参判定条件,同时将此参数传给切面增强

1
2
3
4
5
6
7
8
9
10
11
@Aspect
@Component
public class SampleAspect {
    
    
    @Before("execution(* sampleGenericMethod(..)) && args(param)") //将args的入参
    public void beforeSampleMethod(String param) { //对应上面的param,将args的入参类型限定为java.lang.String,同时还将这个切面的这个string入参传进来
        System.out.println("before string param:"+param);
    }
    
}

6.6.2 argsName属性

上面的args中填写参数名的时候, 参数名要和方法的参数名对应一致,但是如果在编译的时候没有指定特定的参数的话, 编译后的class文件是不会保留方法中的参数名称的,这个时候就可以通过argsName进行指定参数名。

当没有指定argsName的时候, spring通过放射的方式获取方法的入参名字,如果指定了argsName的参数以后, spring就直接通过argsName获取参数的名称,argsName中的顺序对应的就是切面增强中的方法入参。

1
2
3
4
5
@RequestMapping("/helloworld")
public String helloWorld(String id, String age){
    System.out.println("被代理方法正在执行");
    return null;
}
1
2
3
4
5
6
7
@After(value = "execution(* com.bxp.controller.TestController.*(..)) && args(userId, userAge)", 
       argNames = "userId,userAge") //argsName参数名称和args一致进行匹配,argsName参数顺序和下面的增强方法的对应,便可以确定argsName和args的参数类型, 进行匹配连接点。
public void after(JoinPoint point, String userAge, String userId){ 
  	//这里实际上的传参第一个是userId,第二个是UserAge
    System.out.println("userId===========" + userId);
    System.out.println("userAge===========" + userAge);
}
1
2
3
4
5
6
7
8
9
10
请求连接
http://localhost:8088/testAop/helloworld?age=24&id=bian1996
 
输出结果
被代理方法正在执行
userId===========24
userAge===========bian1996
 
注意:这一次两个参数的类型都给成String类型了

总结:

  • 目标方法和args()通过参数类型和顺序一一进行匹配
  • argNames参数名称和args()参数名称一致进行匹配
  • argNames和增强方法通过参数顺序一一对应,并借此明确args()中的参数类型

总结spring如何获取方法中的参数名称:

  1. 编译的时候保留了调试信息(通过制定-g:vars),spring会尝试从class文件的的局部变量表中进行获取参数名称。
  2. 没有添加调试参数保留调试信息,但是使用了AspectJ编译器(ajc)进行编译@AspectJ,那AspectJ编译器会保留这些信息。
  3. 如果没有保留调试信息,同时也没有使用AspectJ编译器进行编译切面, 那spring aop就会尝试推断, 例如只有一个入参变量的时候就很明显,但是如果绑定不明确的时候在运行时就会抛出AmbiguousBindingException。

6.7 切面增强排序

当一个连接点中,匹配了多个切面增强的时候, 如何确定执行的顺序呢?

aop中也是通过@Ordered注解进行确定执行的优先级, Ordered.getValue()中的放回的值越小则优先级越高。

同一个连接点,两个 @Before 增强,优先级越高越先执行。

同一个连接点,两个@After增强,优先级越高的越后执行。

七. 空指针安全控制

八. 数据缓冲与编解码

九. 日志相关

十. 附录

参考文档:https://docs.spring.io/spring-framework/docs/5.1.4.RELEASE/spring-framework-reference/core.html

spring4.1.8扩展实战之四:感知spring容器变化(SmartLifecycle接口)

Spring探秘|妙用BeanPostProcessor

SpringBoot把配置文件中的值映射到实体类中

第二十四节 SpringBoot使用spring.factories

databinder使用场景

本文由作者按照 CC BY 4.0 进行授权

TSDB part(2) - WAL && Checkpoint

Reactor系列- Reactive Stream api规范初识