朱晔和你聊Spring系列S1E3:Spring咖啡罐里的豆子
标题中的咖啡罐指的是Spring容器,容器里装的当然就是被称作Bean的豆子。本文我们会以一个最基本的例子来熟悉Spring的容器管理和扩展点。
阅读PDF版本
为什么要让容器来管理对象?
首先我们来聊聊这个问题,为什么我们要用Spring来管理对象(的生命周期和对象之间的关系)而不是自己new一个对象呢?大家可能会回答是方便,为了解耦。我个人觉得除了这两个原因之外,还有就是给予了我们更多可能性。如果我们以容器为依托来管理所有的框架、业务对象,那么不仅仅我们可以无侵入调整对象的关系,还有可能无侵入随时调整对象的属性甚至悄悄进行对象的替换。这就给了我们无限多的可能性,大大方便了框架的开发者在程序背后实现一些扩展。不仅仅Spring Core本身以及Spring Boot大量依赖Spring这套容器体系,一些外部框架也因为这个原因可以和Spring进行无缝整合。
Spring可以有三种方式来配置Bean,分别是最早期的XML方式、后来的注解方式以及现在最流行的Java代码配置方式。
Bean的回调事件
在前文parent模块(空的一个SpringBoot应用程序)的基础上,我们先来创建一个beans模块:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>me.josephzhu</groupId>
<artifactId>spring101-beans</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring101-beans</name>
<description></description>
<parent>
<groupId>me.josephzhu</groupId>
<artifactId>spring101</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
然后来创建我们的豆子:
package me.josephzhu.spring101beans;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@Component
public class MyService implements InitializingBean, DisposableBean {
public int increaseCounter() {
this.counter++;
return counter;
}
public int getCounter() {
return counter;
}
public void setCounter(int counter) {
this.counter = counter;
}
private int counter=0;
public MyService(){
counter++;
System.out.println(this + "#constructor:" + counter);
}
public String hello(){
return this + "#hello:" + counter;
}
@PreDestroy
public void preDestroy() {
System.out.println(this + "#preDestroy:" + counter);
}
@Override
public void afterPropertiesSet() {
counter++;
System.out.println(this + "#afterPropertiesSet:" + counter);
}
@PostConstruct
public void postConstruct(){
counter++;
System.out.println(this + "#postConstruct:" + counter);
}
@Override
public void destroy() {
System.out.println(this + "#destroy:" + counter);
}
}
这里可以看到,我们的服务中有一个counter字段,默认是0。这个类我们实现了InitializingBean接口和DisposableBean接口,同时还创建了两个方法分别加上了@PostConstruct和@PreDestroy注解。这两套实现方式都可以在对象的额外初始化功能和释放功能,注解的实现不依赖Spring的接口,侵入性弱一点。
接下去,我们创建一个Main类来测试一下:
package me.josephzhu.spring101beans;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import javax.annotation.Resource;
@SpringBootApplication
public class Spring101BeansApplication implements CommandLineRunner {
@Autowired
private ApplicationContext applicationContext;
@Resource
private MyService helloService;
@Autowired
private MyService service;
public static void main(String[] args) {
SpringApplication.run(Spring101BeansApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
System.out.println("====================");
applicationContext.getBeansOfType(MyService.class).forEach((name, service)->{
System.out.println(name + ":" + service);
});
System.out.println("====================");
System.out.println(helloService.hello());
System.out.println(service.hello());
}
}
ApplicationContext直接注入即可,不一定需要用ApplicationContextAware方式来获取。执行程序后可以看到输出如下:
me.josephzhu.spring101beans.MyService@7fb4f2a9#constructor:1
me.josephzhu.spring101beans.MyService@7fb4f2a9#postConstruct:2
me.josephzhu.spring101beans.MyService@7fb4f2a9#afterPropertiesSet:3
====================
myService:me.josephzhu.spring101beans.MyService@7fb4f2a9
====================
me.josephzhu.spring101beans.MyService@7fb4f2a9#hello:3
me.josephzhu.spring101beans.MyService@7fb4f2a9#hello:3
me.josephzhu.spring101beans.MyService@7fb4f2a9#preDestroy:3
me.josephzhu.spring101beans.MyService@7fb4f2a9#destroy:3
这里我们使用@Resource注解和@Autowired注解分别引用了两次对象,可以看到由于Bean默认配置为singleton单例,所以容器中MyService类型的对象只有一份,代码输出也可以证明这点。此外,我们也通过输出看到了构造方法以及两套Bean回调的次序是:
- 类自己的构造方法
- @PostConstruct注释的方法
- InitializingBean接口实现的方法
- @PreDestroy注释的方法
- DisposableBean接口实现的方法
Java 代码方式创建Bean
从刚才的输出中可以看到,在刚才的例子中,我们为Bean打上了@Component注解,容器为我们创建了名为myService的MyService类型的Bean。现在我们再来用Java代码方式来创建相同类型的Bean,创建如下的文件:
package me.josephzhu.spring101beans;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
public class ApplicationConfig {
@Bean(initMethod = "init")
public MyService helloService(){
MyService myService = new MyService();
myService.increaseCounter();
return myService;
}
}
这里可以看到在定义Bean的时候我们关联了一个initMethod,因此我们需要修改Bean加上这个方法:
public void init() {
counter++;
System.out.println(this + "#init:" + counter);
}
现在我们运行代码看看结果,得到了如下错误:
Field service in me.josephzhu.spring101beans.Spring101BeansApplication required a single bean, but 2 were found:
- myService: defined in file [/Users/zyhome/IdeaProjects/spring101/spring101-beans/target/classes/me/josephzhu/spring101beans/MyService.class]
- helloService: defined by method 'helloService' in class path resource [me/josephzhu/spring101beans/ApplicationConfig.class]
出现错误的原因是@Autowired了一个MyService,@Resource注解因为使用Bean的名称来查找Bean,所以并不会出错,而@Autowired因为根据Bean的类型来查抄Bean找到了两个匹配所有出错了,解决方式很简单,我们在多个Bean里选一个作为主Bean。我们修改一下MyService加上注解:
@Component
@Primary
public class MyService implements InitializingBean, DisposableBean
这样,我们的@Resource根据名字匹配到的是我们@Configuration出来的Bean,而@Autowired根据类型+Primary匹配到了@Component注解定义的Bean,重新运行代码来看看是不是这样:
me.josephzhu.spring101beans.MyService@6cd24612#constructor:1
me.josephzhu.spring101beans.MyService@6cd24612#postConstruct:3
me.josephzhu.spring101beans.MyService@6cd24612#afterPropertiesSet:4
me.josephzhu.spring101beans.MyService@6cd24612#init:5
me.josephzhu.spring101beans.MyService@7486b455#constructor:1
me.josephzhu.spring101beans.MyService@7486b455#postConstruct:2
me.josephzhu.spring101beans.MyService@7486b455#afterPropertiesSet:3
====================
myService:me.josephzhu.spring101beans.MyService@7486b455
helloService:me.josephzhu.spring101beans.MyService@6cd24612
====================
me.josephzhu.spring101beans.MyService@6cd24612#hello:5
me.josephzhu.spring101beans.MyService@7486b455#hello:3
me.josephzhu.spring101beans.MyService@7486b455#preDestroy:3
me.josephzhu.spring101beans.MyService@7486b455#destroy:3
me.josephzhu.spring101beans.MyService@6cd24612#preDestroy:5
me.josephzhu.spring101beans.MyService@6cd24612#destroy:5
从输出中我们注意到几点:
- 先输出的的确是helloService,说明@Resource引入的是我们Java代码配置的MyService,helloService由于在我们配置的多调用了一次increaseCounter()以及关联的initMethod,所以counter的值是5
- initMethod执行的顺序在@PostConstruct注释的方法和InitializingBean接口实现的方法之后
- 虽然我们的MySerive的两种Bean的定义都是单例,但是这不代表我们的Bean就是一套,在这里我们通过代码配置和注解方式在容器内创建了两套MyService类型的Bean,它们都经历了自己的初始化过程。通过@Resource和@Autowired引入到了是不同的Bean,当然也就是不同的对象
你还可以试试在使用@Autowired引入MyService的时候直接指定需要的Bean:
@Autowired
@Qualifier("helloService")
private MyService service;
两个重要的扩展点
我们来继续探索Spring容器提供给我们的两个有关Bean的重要扩展点。
- 用于修改Bean定义的BeanFactoryPostProcessor。所谓修改定义就是修改Bean的元数据,元数据有哪些呢?如下图所示,类型、名字、实例化方式、构造参数、属性、Autowire模式、懒初始化模式、初始析构方法。实现了这个接口后,我们就可以修改这些已经定义的元数据,实现真正的动态配置。这里需要注意,我们不应该在这个接口的实现中去实例化Bean,否则这相当于提前进行了实例化会破坏Bean的生命周期。
- 用于修改Bean实例的BeanPostProcessor。在这个阶段其实Bean已经实例化了,我们可以进行一些额外的操作对Bean进行修改。如下图,我们可以清晰的看到Bean的生命周期如下(BeanPostProcessor缩写为BPP):
- Bean定义加载
- BeanFactoryPostProcessor来修改Bean定义
- Bean逐一实例化
- BeanPostProcessor预处理
- Bean初始化
- BeanPostProcessor后处理
好,我们现在来实现这两种类型的处理器,首先是用于修改Bean定义的处理器:
package me.josephzhu.spring101beans;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
BeanDefinition beanDefinition = configurableListableBeanFactory.getBeanDefinition("helloService");
if (beanDefinition != null) {
beanDefinition.setScope("prototype");
beanDefinition.getPropertyValues().add("counter", 10);
}
System.out.println("MyBeanFactoryPostProcessor");
}
}
这里,我们首先找到了我们的helloService(Java代码配置的那个Bean),然后修改了它的属性和Scope(还记得吗,在之前的图中我们可以看到,这两项都是Bean的定义,定义相当于类描述,实例当然就是类实例了)。
然后,我们再来创建一个修改Bean实例的处理器:
package me.josephzhu.spring101beans;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof MyService) {
System.out.println(bean + "#postProcessAfterInitialization:" + ((MyService)bean).increaseCounter());
}
return bean;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof MyService) {
System.out.println(bean + "#postProcessBeforeInitialization:" + ((MyService)bean).increaseCounter());
}
return bean;
}
}
实现比较简单,在这个处理器的两个接口我们都调用了一次增加计数器的操作。我们运行代码来看一下这两个处理器执行的顺序是否符合刚才那个图的预期:
MyBeanFactoryPostProcessor
me.josephzhu.spring101beans.MyService@41330d4f#constructor:1
me.josephzhu.spring101beans.MyService@41330d4f#postProcessBeforeInitialization:11
me.josephzhu.spring101beans.MyService@41330d4f#postConstruct:12
me.josephzhu.spring101beans.MyService@41330d4f#afterPropertiesSet:13
me.josephzhu.spring101beans.MyService@41330d4f#init:14
me.josephzhu.spring101beans.MyService@41330d4f#postProcessAfterInitialization:15
me.josephzhu.spring101beans.MyService@6f36c2f0#constructor:1
me.josephzhu.spring101beans.MyService@6f36c2f0#postProcessBeforeInitialization:11
me.josephzhu.spring101beans.MyService@6f36c2f0#postConstruct:12
me.josephzhu.spring101beans.MyService@6f36c2f0#afterPropertiesSet:13
me.josephzhu.spring101beans.MyService@6f36c2f0#init:14
me.josephzhu.spring101beans.MyService@6f36c2f0#postProcessAfterInitialization:15
me.josephzhu.spring101beans.MyService@3b35a229#constructor:1
me.josephzhu.spring101beans.MyService@3b35a229#postProcessBeforeInitialization:2
me.josephzhu.spring101beans.MyService@3b35a229#postConstruct:3
me.josephzhu.spring101beans.MyService@3b35a229#afterPropertiesSet:4
me.josephzhu.spring101beans.MyService@3b35a229#postProcessAfterInitialization:5
====================
me.josephzhu.spring101beans.MyService@6692b6c6#constructor:1
me.josephzhu.spring101beans.MyService@6692b6c6#postProcessBeforeInitialization:11
me.josephzhu.spring101beans.MyService@6692b6c6#postConstruct:12
me.josephzhu.spring101beans.MyService@6692b6c6#afterPropertiesSet:13
me.josephzhu.spring101beans.MyService@6692b6c6#init:14
me.josephzhu.spring101beans.MyService@6692b6c6#postProcessAfterInitialization:15
myService:me.josephzhu.spring101beans.MyService@3b35a229
helloService:me.josephzhu.spring101beans.MyService@6692b6c6
====================
me.josephzhu.spring101beans.MyService@41330d4f#hello:15
me.josephzhu.spring101beans.MyService@6f36c2f0#hello:15
me.josephzhu.spring101beans.MyService@3b35a229#preDestroy:5
me.josephzhu.spring101beans.MyService@3b35a229#destroy:5
这个输出结果有点长,第一行就输出了MyBeanFactoryPostProcessor这是预料之中,Bean定义的修改肯定是最先发生的。我们看下输出的规律,1、11、12、13、14、15出现了三次,之所以从1跳到了11是因为我们的BeanFactoryPostProcessor修改了其中的counter属性的值为10。这说明了,我们的helloService的初始化进行了三次:
- 第一套指针地址是5a7fe64f,对应输出第一个hello(),这是我们@Resource引入的
- 第二套指针地址是69ee81fc,对应输出第二个hello(),这是我们@Autowird+@Qualifier引入的(刚才一节最后我们指定了helloService)
- 第三套指针地址是29f7cefd,这是我们getBeansOfType的时候创建的,对应下面Key-Value的输出:
这里的输出说明了几点:
- 我们的BeanFactoryPostProcessor生效了,不但修改了helloService的Scope为prototype而且修改了它的counter属性
- 对于Scope=ptototype的Bean,显然在每次使用Bean的时候都会新建一个实例
- BeanPostProcessor两个方法的顺序结合一开始说的Bean事件回调的顺序整体如下:
- 类自己的构造方法
- BeanFactoryPostProcessor接口实现的postProcessBeforeInitialization()方法
- @PostConstruct注释的方法
- InitializingBean接口实现的afterPropertiesSet()方法
- Init-method定义的方法
- BeanFactoryPostProcessor接口实现的postProcessAfterInitialization()方法
- @PreDestroy注释的方法
- DisposableBean接口实现的destroy()方法
最后,我们可以修改BeanFactoryPostProcessor中的代码把prototype修改为singleton看看是否我们的helloService这个Bean恢复为了单例:
MyBeanFactoryPostProcessor
me.josephzhu.spring101beans.MyService@51891008#constructor:1
me.josephzhu.spring101beans.MyService@51891008#postProcessBeforeInitialization:11
me.josephzhu.spring101beans.MyService@51891008#postConstruct:12
me.josephzhu.spring101beans.MyService@51891008#afterPropertiesSet:13
me.josephzhu.spring101beans.MyService@51891008#init:14
me.josephzhu.spring101beans.MyService@51891008#postProcessAfterInitialization:15
me.josephzhu.spring101beans.MyService@49c90a9c#constructor:1
me.josephzhu.spring101beans.MyService@49c90a9c#postProcessBeforeInitialization:2
me.josephzhu.spring101beans.MyService@49c90a9c#postConstruct:3
me.josephzhu.spring101beans.MyService@49c90a9c#afterPropertiesSet:4
me.josephzhu.spring101beans.MyService@49c90a9c#postProcessAfterInitialization:5
====================
myService:me.josephzhu.spring101beans.MyService@49c90a9c
helloService:me.josephzhu.spring101beans.MyService@51891008
====================
me.josephzhu.spring101beans.MyService@51891008#hello:15
me.josephzhu.spring101beans.MyService@51891008#hello:15
me.josephzhu.spring101beans.MyService@49c90a9c#preDestroy:5
me.josephzhu.spring101beans.MyService@49c90a9c#destroy:5
me.josephzhu.spring101beans.MyService@51891008#preDestroy:15
me.josephzhu.spring101beans.MyService@51891008#destroy:15
本次输出结果的hello()方法明显是同一个bean,结果中也没出现三次1、11、12、13、14、15。
总结
本文以探索的形式讨论了下面的一些知识点:
- 容器管理对象的意义是什么
- Bean的生命周期回调事件
- Spring提供的Bean的两个重要扩展点
- @Resource和@Autowired的区别
- 注解方式和代码方式配置Bean
- @Primary和@Qualifier注解的作用
- Bean的不同类型的Scope
朱晔和你聊Spring系列S1E3:Spring咖啡罐里的豆子的更多相关文章
- Spring系列(七) Spring MVC 异常处理
Servlet传统异常处理 Servlet规范规定了当web应用发生异常时必须能够指明, 并确定了该如何处理, 规定了错误信息应该包含的内容和展示页面的方式.(详细可以参考servlet规范文档) 处 ...
- Spring系列之Spring常用注解总结 转载
Spring系列之Spring常用注解总结 传统的Spring做法是使用.xml文件来对bean进行注入或者是配置aop.事物,这么做有两个缺点:1.如果所有的内容都配置在.xml文件中,那么.x ...
- Spring系列(零) Spring Framework 文档中文翻译
Spring 框架文档(核心篇1和2) Version 5.1.3.RELEASE 最新的, 更新的笔记, 支持的版本和其他主题,独立的发布版本等, 是在Github Wiki 项目维护的. 总览 历 ...
- Spring系列之Spring常用注解总结
传统的Spring做法是使用.xml文件来对bean进行注入或者是配置aop.事物,这么做有两个缺点:1.如果所有的内容都配置在.xml文件中,那么.xml文件将会十分庞大:如果按需求分开.xml文件 ...
- 【Spring系列】Spring mvc整合redis(非集群)
一.在pom.xml中增加redis需要的jar包 <!--spring redis相关jar包--> <dependency> <groupId>redis.cl ...
- Spring 系列之Spring常用注解总结
传统的Spring做法是使用.xml文件来对bean进行注入或者是配置aop.实物,这么做有两个缺点: 1.如果所有的内容都配置在.xml文件中,那么.xml文件将会十分庞大:如果按需求分开.xml文 ...
- Spring系列(一) Spring的核心
Spring 简介 Spring 是一个开源轻量级企业应用架构,目的是为了简化企业级应用开发.(1)Spring 框架可以帮我们管理对象的生命周期,帮助我们管理对象间的依赖关系,相互协作:(2)Spr ...
- Spring系列(六) Spring Web MVC 应用构建分析
DispatcherServlet DispatcherServlet 是Spring MVC的前端控制器名称, 用户的请求到达这里进行集中处理, 在Spring MVC中, 它的作用是为不同请求匹配 ...
- 【Spring系列】Spring IoC
前言 IoC其实有两种方式,一种是DI,而另一种是DL,即Dependency Lookup(依赖查找),前者是当前软件实体被动接受其依赖的其他组件被IOc容器注入,而后者是当前软件实体主动去某个服务 ...
随机推荐
- [Python][小知识][NO.1] Python字符串前 加 u、r、b 的含义
1.字符串前加 u 例:u"我是含有中文字符组成的字符串." 作用:后面字符串以 Unicode 格式 进行编码,一般用在中文字符串前面,防止因为源码储存格式问题,导致再次使用时出 ...
- c/c++ 标准顺序容器 容器的访问,删除 操作
c/c++ 标准顺序容器 容器的访问,删除 操作 pop_front:vector,string不支持 pop_back:forward_list不支持 知识点 1,front, back, at 成 ...
- c/c++ 浅拷贝
c/c++ 浅拷贝 编译器合成的拷贝构造函数和=重载函数,只是做如下处理: 对象1.成员变量a = 对象2.成员变量a 如果成员变量a是指针,执行完拷贝构造函数或者*=重载函数**后,对象1和对象2的 ...
- c/c++ 二叉排序树
c/c++ 二叉排序树 概念: 左树的所有节点的值(包括子节点)必须小于中心节点,右树所有节点的值(包括子节点)必须大于中心节点. 不允许有值相同的节点. 二叉排序树的特点: 中序遍历后,就是从小到大 ...
- c/c++叉树的创建与遍历(非递归遍历左右中,不破坏树结构)
二叉树的创建与遍历(非递归遍历左右中,不破坏树结构) 创建 二叉树的递归3种遍历方式: 1,先中心,再左树,再右树 2,先左树,再中心,再右树 3,先左树,再右树,再中心 二叉树的非递归4种遍历方式: ...
- 【2018.05.05 C与C++基础】C++中的自动废料收集:概念与问题引入
在阅读C++语言的设计与演化一书时,作者多次提到希望能设计出一个自动废料收集,然而出于种种考虑,始终未将自动废料收集纳入标准讨论中,而是由Coder自己考虑是否在程序中实现废料收集. 当然了,许多Ja ...
- March 09th, 2018 Week 10th Friday
All good things must come to an end. 好景无常. Love is when the other person's happiness is more importa ...
- ElasticSearch(二):允许外网连接服务配置
上一篇文章的配置,只能在本机使用,但是要想为集群或者其他的机器连接,则需要做以下配置: 一.修改/opt/elasticsearch-6.4.0/config/elasticsearch.yml文件 ...
- I/O 机制的介绍(Linux 中直接 I/O 机制的介绍)
IO连接的建立方式 1.缓存IO.流式IO: 2.映射IO.块式IO: 3.直接IO. IO的方式: 同步.异步.定时刷新: MMAP与内核空间 mmap使用共享用户空间与内核空间实现: 直接 I/O ...
- centos7下安装docker(17.4docker监控----prometheus)
Prometheus是一个非常优秀的监控工具.准确的说,应该是监控方案.Prometheus提供了监控数据搜集,存储,处理,可视化和告警一套完整的解决方案 Prometheus架构如盗图: 官网上的原 ...