Spring系列(23-28)

烟雨 3年前 (2022-10-20) 阅读数 538 #Spring
文章标签 Spring

二十三、父子容器

  1. 什么是父子容器?

  2. 为什么需要用父子容器?

  3. 父子容器如何使用?

BeanFactory的方式

//创建父容器
parentFactoryDefaultListableBeanFactory parentFactory = new DefaultListableBeanFactory();
//创建一个子容器
childFactoryDefaultListableBeanFactory childFactory = new DefaultListableBeanFactory();
//调用setParentBeanFactory指定父容器
childFactory.setParentBeanFactory(parentFactory);

ApplicationContext的方式

//创建父容器
AnnotationConfigApplicationContext parentContext = new AnnotationConfigApplicationContext();
//启动父容器
parentContext.refresh();
//创建子容器
AnnotationConfigApplicationContext childContext = new AnnotationConfigApplicationContext();
//给子容器设置父容器
childContext.setParent(parentContext);
//启动子容器
childContext.refresh();

父子容器特点

  1. 父容器和子容器是相互隔离的,他们内部可以存在名称相同的bean。

  2. 子容器可以访问父容器中的bean,而父容器不能访问子容器中的bean。

  3. 调用子容器的getBean方法获取bean的时候,会沿着当前容器开始向上面的容器进行查找,直到找到对应的bean为止。

  4. 子容器中可以通过任何注入方式注入父容器中的bean,而父容器中是无法注入子容器中的bean,原因是第2点。

父子容器使用注意点

我们使用容器的过程中,经常会使用到的一些方法,这些方法通常会在下面的两个接口中
org.springframework.beans.factory.BeanFactory
org.springframework.beans.factory.ListableBeanFacto
BeanFactory接口,是Spring容器的顶层接口,这个接口中的方法是支持容器嵌套结构查找的,比如我们常用的getBean方法,就是这个接口中定义的,调用getBean方法的时候,会从沿着当前容器向上查找,直到找到满足条件的bean为止。
而ListableBeanFactory这个接口中的方法是不支持容器嵌套结构查找的,比如下面这个方法
String[] getBeanNamesForType(@Nullable Class<?> type)
获取指定类型的所有bean名称,调用这个方法的时候只会返回当前容器中符合条件的bean,而不会去递归查找其父容器中的bean。

有没有方式解决ListableBeanFactory接口不支持层次查找的问题?

Spring中有个工具类就是解决这个问题的,如下:
org.springframework.beans.factory.BeanFactoryUtils
这个类中提供了很多静态方法,有很多支持层次查找的方法,源码你们可以去细看一下,名称中包含有Ancestors 的都是支持层次查找。

问题1:Springmvc中只使用一个容器是否可以?

只使用一个容器是可以正常运行的。

问题2:那么Springmvc中为什么需要用到父子容器?

通常我们使用Springmvc的时候,采用3层结构,controller层,service层,dao层;父容器中会包含dao层和service层,而子容器中包含的只有controller层;这2个容器组成了父子容器的关系,controller层通常会注入service层的bean。
采用父子容器可以避免有些人在service层去注入controller层的bean,导致整个依赖层次是比较混乱的。
父容器和子容器的需求也是不一样的,比如父容器中需要有事务的支持,会注入一些支持事务的扩展组件,而子容器中controller完全用不到这些,对这些并不关心,子容器中需要注入一下Springmvc相关的bean,而这些bean父容器中同样是不会用到的,也是不关心一些东西,将这些相互不关心的东西隔开,可以有效的避免一些不必要的错误,而父子容器加载的速度也会快一些。

二十四、@PropertySource、@Value注解及动态刷新实现

  1. @Value的用法

  2. @Value数据来源

  3. @Value动态刷新的问题

@Value的用法

系统中需要连接db,连接db有很多配置信息。
系统中需要发送邮件,发送邮件需要配置邮件服务器的信息。
还有其他的一些配置信息。
我们可以将这些配置信息统一放在一个配置文件中,上线的时候由运维统一修改。
那么系统中如何使用这些配置信息呢,Spring中提供了@Value注解来解决这个问题。
通常我们会将配置信息以key=value的形式存储在properties配置文件中。
通过@Value("${配置文件中的key}")来引用指定的key对应的value。

@Value使用步骤

将@PropertySource放在类上面,如下
@PropertySource({"配置文件路径1","配置文件路径2"...})
@PropertySource注解有个value属性,字符串数组类型,可以用来指定多个配置文件的路径。

使用@Value注解引用配置文件的值

通过@Value引用上面配置文件中的值:
@Value("${配置文件中的key:默认值}")
@Value("${配置文件中的key}")

@Value数据来源

通常情况下我们@Value的数据来源于配置文件,不过,还可以用其他方式,比如我们可以将配置文件的内容放在数据库,这样修改起来更容易一些。
我们需要先了解一下@Value中数据来源于spring的什么地方。
spring中有个类
org.springframework.core.env.PropertySource

可以将其理解为一个配置源,里面包含了key->value的配置信息,可以通过这个类中提供的方法获取key对应的value信息,内部有个方法:

public abstract Object getProperty(String name);
通过name获取对应的配置信息。
系统有个比较重要的接口
org.springframework.core.env.Environment

用来表示环境配置信息,这个接口有几个方法比较重要

// 用来解析 ${text} 的,@Value注解最后就是调用这个方法来解析的。
String resolvePlaceholders(String text);
// 返回MutablePropertySources对象,来看一下这个类
MutablePropertySources getPropertySources();

返回MutablePropertySources对象,来看一下这个类

public class MutablePropertySources implements PropertySources {
	private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();
}
内部包含一个 propertySourceList 列表。
spring容器中会有一个 Environment 对象,最后会调用这个对象的 resolvePlaceholders 方法解析@Value。
大家可以捋一下,最终解析@Value的过程:
  1. 将@Value注解的value参数值作为Environment.resolvePlaceholders方法参数进行解析。

  2. Environment内部会访问MutablePropertySources来解析。

  3. MutablePropertySources内部有多个PropertySource,此时会遍历PropertySource列表,调用PropertySource.getProperty方法来解析key对应的值。

通过上面过程,如果我们想改变@Value数据的来源,只需要将配置信息包装为PropertySource对象,丢到Environment中的MutablePropertySources内部就可以了。
@Test
public void test2() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    /*下面这段是关键 start*/
    //模拟从db中获取配置信息
    Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();
    //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是  PropertySource的子类)
    MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
    //将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高
    context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
    /*上面这段是关键 end*/
    context.register(MainConfig2.class);
    context.refresh();
    MailConfig mailConfig = context.getBean(MailConfig.class);
    System.out.println(mailConfig);
}
此时你们可以随意修改 DbUtil.getMailInfoFromDb ,具体数据是从db中来,来时从redis或者其他介质中来,任由大家发挥。

@Value动态刷新

如果我们将配置信息放在db中,可能我们会通过一个界面来修改这些配置信息,然后保存之后,希望系统在不重启的情况下,让这些值在spring容器中立即生效。
@Value动态刷新的问题的问题,springboot中使用@RefreshScope实现了。
来看一下@Scope这个注解的源码,有个参数是:
ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;

这个参数的值是个ScopedProxyMode类型的枚举,值有下面4中

public enum ScopedProxyMode {
    DEFAULT,
    NO,
    INTERFACES,
    TARGET_CLASS;
}
当@Scope中proxyMode为TARGET_CLASS的时候,会给当前创建的bean通过cglib生成一个代理对象,调用代理对象的任何方法,都会调用这个自定义的作用域实现类中get方法来重新来获取这个bean对象。

具体实现

先来自定义一个Scope:RefreshScope
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
// 指定自定义Scope解析类
@Scope(BeanRefreshScope.SCOPE_REFRESH)
@Documented
public @interface RefreshScope {
    // 关键:使用的是ScopedProxyMode.TARGET_CLASS
	ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
要求标注@RefreshScope注解的类支持动态刷新@Value的配置。
这个自定义Scope对应的解析类
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.lang.Nullable;
import java.util.concurrent.ConcurrentHashMa

public class BeanRefreshScope implements Scope {
    public static final String SCOPE_REFRESH = "refresh";
    
    private static final BeanRefreshScope INSTANCE = new BeanRefreshScope();
    
    //来个map用来缓存bean
    private ConcurrentHashMap<String, Object> beanMap = new ConcurrentHashMap<>();
    
    private BeanRefreshScope() {}
    
    public static BeanRefreshScope getInstance() {
        return INSTANCE;
    }
    
    /**
    * 清理当前
    */
    public static void clean() {
    	INSTANCE.beanMap.clear();
    }
    
    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Object bean = beanMap.get(name);
        if (bean == null) {
        	bean = objectFactory.getObject();
        	beanMap.put(name, bean);
        }
        return bean;
    }
}
上面的get方法会先从beanMap中获取,获取不到会调用objectFactory的getObject让spring创建bean的实例,然后丢到beanMap中。
上面的clean方法用来清理beanMap中当前已缓存的所有bean。
来个邮件配置类,使用@Value注解注入配置,这个bean作用域为自定义的@RefreshScope
/**
* 邮件配置信息
*/
@Component
// 使用了自定义的作用域@RefreshScope
@RefreshScope
@Data
public class MailConfig {
    @Value("${mail.username}")
    private String username;
}

再来个普通的bean,内部会注入MailConfig

@Component
@Data
public class MailService {
    @Autowired
    private MailConfig mailConfig;

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
    }
}

来个类,用来从db中获取邮件配置信息

public class DbUtil {
    /**
    * 模拟从db中获取邮件配置信息
    *
    * @return
    */
    public static Map<String, Object> getMailInfoFromDb() {
    	Map<String, Object> result = new HashMap<>();
    	result.put("mail.username", UUID.randomUUID().toString());
    	return result;
    }
}

来个spring配置类,扫描加载上面的组件

@Configuration
@ComponentScan("com.zender.test")
public class MainConfig {
}

来个工具类

public class RefreshConfigUtil {
    /**
    * 模拟改变数据库中都配置信息
    */
    public static void updateDbConfig(AbstractApplicationContext context) {
        //更新context中的mailPropertySource配置信息
        refreshMailPropertySource(context);
        //清空BeanRefreshScope中所有bean的缓存
        BeanRefreshScope.clean();
    }
    
    public static void refreshMailPropertySource(AbstractApplicationContext context) {
        Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();
        //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)
        MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
        context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
    }
}
updateDbConfig方法模拟修改db中配置的时候需要调用的方法,方法中2行代码,第一行代码调用refreshMailPropertySource方法修改容器中邮件的配置信息。
BeanRefreshScope.clean()用来清除BeanRefreshScope中所有已经缓存的bean,那么调用bean的任意方法的时候,会重新出发spring容器来创建bean,spring容器重新创建bean的时候,会重新解析@Value的信息,此时容器中的邮件配置信息是新的,所以@Value注入的信息也是新的。
@Test
public void test4() throws InterruptedException {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.getBeanFactory().registerScope(BeanRefreshScope.SCOPE_REFRESH,  BeanRefreshScope.getInstance());
    context.register(MainConfig.class);
    //刷新mail的配置到Environment
    RefreshConfigUtil.refreshMailPropertySource(context);
    context.refresh();
    MailService mailService = context.getBean(MailService.class);
    System.out.println("配置未更新的情况下,输出3次");
    for (int i = 0; i < 3; i++) { //@1
        System.out.println(mailService);
        TimeUnit.MILLISECONDS.sleep(200);
    }
    System.out.println("模拟3次更新配置效果");
    for (int i = 0; i < 3; i++) { //@2
        RefreshConfigUtil.updateDbConfig(context); //@3
        System.out.println(mailService);
        TimeUnit.MILLISECONDS.sleep(200);
    }
}

image.png

动态@Value实现的关键是@Scope中proxyMode参数,值为ScopedProxyMode.DEFAULT,会生成一个代理,通过这个代理来实现@Value动态刷新的效果,这个地方是关键。

二十五、国际化

java中使用 java.util.Locale 来表示地区语言这个对象,内部包含了国家和语言的信息。Locale中有个比较常用的构造方法
public Locale(String language, String country) {
	this(language, country, "");
}
language:语言
country:国家
语言和国家这两个参数的值不是乱写的,国际上有统一的标准:
Locale类中已经创建好了很多常用的Locale对象,直接可以拿过来用,随便列几个看一下:
static public final Locale SIMPLIFIED_CHINESE = createConstant("zh", "CN");//zh_CN
static public final Locale UK = createConstant("en", "GB"); //en_GB
static public final Locale US = createConstant("en", "US"); //en_US
static public final Locale CANADA = createConstant("en", "CA"); //en_CA

MessageSource接口

spring中国际化是通过MessageSource这个接口来支持的
org.springframework.context.MessageSource

内部有3个常用的方法用来获取国际化信息

public interface MessageSource {
    /**
    * 获取国际化信息
    * @param code 表示国际化资源中的属性名;
    * @param args用于传递格式化串占位符所用的运行期参数;
    * @param defaultMessage 当在资源找不到对应属性名时,返回defaultMessage参数所指定的
    默认信息;
    * @param locale 表示本地化对象
    */
    @Nullable
    String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
    /**
    * 与上面的方法类似,只不过在找不到资源中对应的属性名时,直接抛出NoSuchMessageException
    异常
    */
    String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;
    
    /**
    * @param MessageSourceResolvable 将属性名、参数数组以及默认信息封装起来,它的功能和第一个方法相同
    */
    String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
}

常见3个实现类

ResourceBundleMessageSource
这个是基于Java的ResourceBundle基础类实现,允许仅通过资源名加载国际化资源
ReloadableResourceBundleMessageSource
这个功能和第一个类的功能类似,多了定时刷新功能,允许在不重启系统的情况下,更新资源的信息
StaticMessageSource
它允许通过编程的方式提供国际化信息,一会我们可以通过这个来实现db中存储国际化信息的功能。

Spring中使用国际化的3个步骤

通常我们使用spring的时候,都会使用带有ApplicationContext字样的spring容器,这些容器一般是继承了AbstractApplicationContext接口,而这个接口实现了上面说的国际化接口MessageSource,所以通常我们用到的ApplicationContext类型的容器都自带了国际化的功能。通常我们在ApplicationContext类型的容器中使用国际化3个步骤
步骤一:创建国际化文件
message_zh_CN.properties:中文
name=姓名
personal_introduction=个人介绍:{0},{1},{0}

message_en_GB.properties:英文

name=Full 
namepersonal_introduction=personal_introduction:{0},{1},{0}

步骤二:向容器中注册一个MessageSource类型的bean,bean名称必须为:messageSource

@Configuration
public class MainConfig1 {
    @Bean
    public ResourceBundleMessageSource messageSource() {
        ResourceBundleMessageSource result = new ResourceBundleMessageSource();
        // 可以指定国际化化配置文件的位置,格式:路径/文件名称,注意不包含【语言_国 家.properties】含这部分
        result.setBasenames("com/javacode2018/lesson002/demo19/message"); //@1
        return result;
    }
}

步骤三:调用AbstractApplicationContext中的getMessage来获取国际化信息,其内部将交给第二步中注册的messageSource名称的bean进行处理

@Test
public void test1() {
	AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(MainConfig1.class);
    context.refresh();
    //未指定Locale,此时系统会取默认的locale对象,本地默认的值中文【中国】,即:zh_CN
    System.out.println(context.getMessage("name", null, null));
    System.out.println(context.getMessage("name", null, Locale.CHINA));
    //CHINA对应:zh_CN
    System.out.println(context.getMessage("name", null, Locale.UK)); //UK对应en_GB
    
    //动态参数使用
    System.out.println(context.getMessage("personal_introduction", new String[]{"spring高手", "java高手"}, Locale.CHINA)); //CHINA对应:zh_CN
    System.out.println(context.getMessage("personal_introduction", new String[]{"spring", "java"}, Locale.UK)); //UK对应en_GB
}

监控国际化文件的变化

用 ReloadableResourceBundleMessageSource 这个类,功能和上面案例中的ResourceBundleMessageSource 类似,不过多了个可以监控国际化资源文件变化的功能,有个方法用来设置缓存时间:
public void setCacheMillis(long cacheMillis)
-1:表示永远缓存
0:每次获取国际化信息的时候,都会重新读取国际化文件
大于0:上次读取配置文件的时间距离当前时间超过了这个时间,重新读取国际化文件
还有个按秒设置缓存时间的方法 setCacheSeconds ,和 setCacheMillis 类似
@Bean
public MessageSource messageSource() {
    ReloadableResourceBundleMessageSource result = new
    ReloadableResourceBundleMessageSource();
    //设置缓存时间1000毫秒
    result.setCacheMillis(1000);
    return result;
}
使用注意:线上环境,缓存时间最好设置大一点,性能会好一些。

国际化信息存在db中

上面我们介绍了一个类: StaticMessageSource ,这个类它允许通过编程的方式提供国际化信息,我们通过这个类来实现从db中获取国际化信息的功能。
这个类中有2个方法比较重要:
public void addMessage(String code, Locale locale, String msg);
public void addMessages(Map<String, String> messages, Locale locale);
通过这两个方法来添加国际化配置信息。
下面来看案例
自定义一个StaticMessageSource类
public class MessageSourceFromDb extends StaticMessageSource implements InitializingBean {
    @Override
    public void afterPropertiesSet() throws Exception {
    //此处我们在当前bean初始化之后,模拟从db中获取国际化信息,然后调用addMessage来配置国际化信息
    this.addMessage("desc", Locale.CHINA, "我是从db来的信息");
    this.addMessage("desc", Locale.UK, "MessageSource From Db");
    }
}
上面的类实现了spring的InitializingBean接口,重写了接口中干掉afterPropertiesSet方法,这个方法会在当前bean初始化之后调用,在这个方法中模拟从db中获取国际化信息,然后调用addMessage来配置国际化信息
将MessageSourceFromDb注册到spring容器
@Bean
public MessageSource messageSource(){
	return new MessageSourceFromDb();
}
@Test
public void test4() throws InterruptedException {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(MainConfig3.class);
    context.refresh();
    System.out.println(context.getMessage("desc", null, Locale.CHINA));
    System.out.println(context.getMessage("desc", null, Locale.UK));
}

bean名称为什么必须是messageSource

上面我容器启动的时候会调用 refresh 方法,过程如下:
  1. org.springframework.context.support.AbstractApplicationContext#refresh内部会调用

  2. org.springframework.context.support.AbstractApplicationContext#initMessageSource这个方法用来初始化MessageSource,方法内部会查找当前容器中是否有messageSource名称的bean,如果有就将其作为处理国际化的对象

  3. 如果没有找到,此时会注册一个名称为messageSource的MessageSource

自定义bean中使用国际化

自定义的bean如果想使用国际化,比较简单,只需实现下面这个接口,spring容器会自动调用这个方法,将 MessageSource 注入,然后我们就可以使用 MessageSource 获取国际化信息了。
public interface MessageSourceAware extends Aware {
	void setMessageSource(MessageSource messageSource);
}

二十六、Spring事件详解

  1. 为什么需要使用事件这种模式?

  2. spring中实现事件有几种方式?

  3. spring中事件监听器消费事件是否支持异步模式?

  4. spring中事件监听器消费事件是否支持自定义顺序?

事件模式中的几个概念

事件源:事件的触发者
事件:描述发生了什么事情的对象
事件监听器:监听到事件发生的时候,做一些处理,比如上面的:路人A、路人B

ApplicationContext容器中事件的支持

通常情况下,我们会使用以 ApplicationContext 结尾的类作为spring的容器来启动应用,下面2个是比较常见的
AnnotationConfigApplicationContext
ClassPathXmlApplicationContext

image.png

对这个图我们来解释一下:
  1. AnnotationConfigApplicationContext和ClassPathXmlApplicationContext都继承了AbstractApplicationContext

  2. AbstractApplicationContext实现了ApplicationEventPublisher接口

  3. AbstractApplicationContext内部有个ApplicationEventMulticaster类型的字段

ApplicationEventPublisher接口

上面类图中多了一个新的接口 ApplicationEventPublisher ,来看一下源码
@FunctionalInterface
public interface ApplicationEventPublisher {
    default void publishEvent(ApplicationEvent event) {
        publishEvent((Object) event);
    }

    void publishEvent(Object event);
}
这个接口用来发布事件的,内部定义2个方法都是用来发布事件的。
spring中不是有个 ApplicationEventMulticaster 接口么,此处怎么又来了一个发布事件的接口?
这个接口的实现类中,比如 AnnotationConfigApplicationContext 内部将这2个方法委托给ApplicationEventMulticaster#multicastEvent 进行处理了。
所以调用 AbstractApplicationContext中的publishEvent 方法,也实现广播事件的效果,不过使用AbstractApplicationContext 也只能通过调用 publishEvent 方法来广播事件。

获取ApplicationEventPublisher对象

如果我们想在普通的bean中获取 ApplicationEventPublisher 对象,需要实现ApplicationEventPublisherAware 接口
public interface ApplicationEventPublisherAware extends Aware {
	void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher);
}
spring容器会自动通过上面的 setApplicationEventPublisher 方法将 ApplicationEventPublisher注入进来,此时我们就可以使用这个来发布事了。
Spring为了简化事件的使用,提供了2种使用方式
  1. 面相接口的方式

  2. 面相@EventListener注解的方式

面相接口的方式

用户注册事件
/**
* 用户注册事件
*/
@Data
public class UserRegisterEvent extends ApplicationEvent {
    //用户名
    private String userName;
    
    public UserRegisterEvent(Object source, String userName) {
        super(source);
        this.userName = userName;
    }
}

发送邮件监听器

/**
* 用户注册成功发送邮件
*/
@Component
public class SendEmailListener implements ApplicationListener<UserRegisterEvent>{
    @Override
    public void onApplicationEvent(UserRegisterEvent event) {
    	System.out.println(String.format("给用户【%s】发送注册成功邮件!",
    	event.getUserName()));
    }
}

用户注册服务

/**
* 用户注册服务
*/
@Component
public class UserRegisterService implements ApplicationEventPublisherAware {
    
    private ApplicationEventPublisher applicationEventPublisher;
    /**
    * 负责用户注册及发布事件的功能
    * @param userName 用户名
    */
    public void registerUser(String userName) {
        //用户注册(将用户信息入库等操作)
        System.out.println(String.format("用户【%s】注册成功", userName));
        //发布注册成功事件
        this.applicationEventPublisher.publishEvent(new UserRegisterEvent(this,userName));
    }
    
     @Override
     public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        	this.applicationEventPublisher = applicationEventPublisher;
    }
}

测试用例

@Test
public void test2() throws InterruptedException {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(MainConfig2.class);
    context.refresh();
    //获取用户注册服务
    com.javacode2018.lesson003.demo1.test2.UserRegisterService
    userRegisterService = context.getBean(com.javacode2018.lesson003.demo1.test2.UserRegisterService.class);
    //模拟用户注册
    userRegisterService.registerUser("路人甲Java");
}
spring容器在创建bean的过程中,会判断bean是否为 ApplicationListener 类型,进而会将其作为监听器注册到 AbstractApplicationContext#applicationEventMulticaster 中,这块的源码在下面这个方法中,有兴趣的可以看一下
org.springframework.context.support.ApplicationListenerDetector#postProcessAfterInitialization

面相@EventListener注解方式

上面是通过接口的方式创建一个监听器,spring还提供了通过 @EventListener 注解的方式来创建一个监听器,直接将这个注解标注在一个bean的方法上,那么这个方法就可以用来处理感兴趣的事件,使用更简单
/**
* 用户注册监听器
*/
@Component
public class UserRegisterListener {
    @EventListener
    public void sendMail(UserRegisterEvent event) {
    	System.out.println(String.format("给用户【%s】发送注册成功邮件!",
    	event.getUserName()));
    }
    @EventListener
    public void sendCompon(UserRegisterEvent event) {
    	System.out.println(String.format("给用户【%s】发送优惠券!",
    	event.getUserName()));
    }
}
spring中处理@EventListener注解源码位于下面的方法中
org.springframework.context.event.EventListenerMethodProcessor#afterSingletonsInstantiated
EventListenerMethodProcessor实现了SmartInitializingSingleton接口,SmartInitializingSingleton接口中的 afterSingletonsInstantiated 方法会在所有单例的bean创建完成之后被spring容器调用

监听器支持排序功能

如果某个事件有多个监听器,默认情况下,监听器执行顺序是无序的,不过我们可以为监听器指定顺序。
如果自定义的监听器是通过ApplicationListener接口实现的,那么指定监听器的顺序有三种方式
方式1:实现org.springframework.core.Ordered接口
需要实现一个getOrder方法,返回顺序值,值越小,顺序越高
方式2:实现org.springframework.core.PriorityOrdered接口
PriorityOrdered接口继承了方式一中的Ordered接口,所以如果你实现PriorityOrdered接口,也需要实现getOrder方法。
方式3:类上使用@org.springframework.core.annotation.Order注解

监听器异步模式

监听器最终是通过 ApplicationEventMulticaster 内部的实现来调用的,所以我们关注的重点就是这个类,这个类默认有个实现类 SimpleApplicationEventMulticaster ,这个类是支持监听器异步调用的,里面有个字段:
private Executor taskExecut
高并发比较熟悉的朋友对 Executor 这个接口是比较熟悉的,可以用来异步执行一些任务。
我们常用的线程池类 java.util.concurrent.ThreadPoolExecutor 就实现了 Executor 接口。
再来看一下 SimpleApplicationEventMulticaster 中事件监听器的调用,最终会执行下面这个方法
@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
    ResolvableType type = (eventType != null ? eventType :
    resolveDefaultEventType(event));
    Executor executor = getTaskExecutor();
    for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
        if (executor != null) { // @1
        	executor.execute(() -> invokeListener(listener, event));
        } else {
        	invokeListener(listener, event);
        }
    }
}
上面的 invokeListener 方法内部就是调用监听器,从代码 @1 可以看出,如果当前 executor 不为空,监听器就会被异步调用,所以如果需要异步只需要让 executor 不为空就可以了,但是默认情况下executor 是空的,此时需要我们来给其设置一个值,下面我们需要看容器中是如何创建广播器的,我们在那个地方去干预。
通常我们使用的容器是 AbstractApplicationContext 类型的,需要看一下AbstractApplicationContext 中广播器是怎么初始化的,就是下面这个方法,容器启动的时候会被调用,用来初始化 AbstractApplicationContext 中的事件广播器 applicationEventMulticaster。
public static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "applicationEventMulticaster";

protected void initApplicationEventMulticaster() {
    ConfigurableListableBeanFactory beanFactory = getBeanFactory();
    if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
        this.applicationEventMulticaster = beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
    } else {
        this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
        beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
    }
}
上面逻辑解释一下:判断spring容器中是否有名称为 applicationEventMulticaster 的bean,如果有就将其作为事件广播器,否则创建一个SimpleApplicationEventMulticaster作为广播器,并将其注册到spring容器中。
从上面可以得出结论:我们只需要自定义一个类型为 SimpleApplicationEventMulticaster 名称为applicationEventMulticaster 的bean就可以了,顺便给 executor 设置一个值,就可以实现监听器异步执行了。
@ComponentScan
@Configuration
public class MainConfig5 {
    @Bean
    public ApplicationEventMulticaster applicationEventMulticaster() { //@1
        //创建一个事件广播器
        SimpleApplicationEventMulticaster result = new  SimpleApplicationEventMulticaster();
        //给广播器提供一个线程池,通过这个线程池来调用事件监听器
        Executor executor = this.applicationEventMulticasterThreadPool().getObject();
        //设置异步执行器
        result.setTaskExecutor(executor);//@1
        return result;
    }
    
    @Bean
    public ThreadPoolExecutorFactoryBean applicationEventMulticasterThreadPool() {
        ThreadPoolExecutorFactoryBean result = new ThreadPoolExecutorFactoryBean();
        result.setThreadNamePrefix("applicationEventMulticasterThreadPool-");
        result.setCorePoolSize(5);
        return result;
    }

}

定义了一个名称为 applicationEventMulticaster 的事件广播器,内部设置了一个线程池用来异步调用监听器

@Test
public void test5() throws InterruptedException {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(MainConfig5.class);
    context.refresh();
    //获取用户注册服务
    UserRegisterService userRegisterService = context.getBean(UserRegisterService.class);
    //模拟用户注册
    userRegisterService.registerUser("路人甲Java");
}
关于事件使用建议
  1. spring中事件是使用接口的方式还是使用注解的方式?具体使用哪种方式都可以,不过在公司内部最好大家都统一使用一种方式

  2. 异步事件的模式,通常将一些非主要的业务放在监听器中执行,因为监听器中存在失败的风险,所以使用的时候需要注意。如果只是为了解耦,但是被解耦的次要业务也是必须要成功的,可以使用消息中间件的方式来解决这些问题。

二十七、循环bean详解

什么是循环依赖?

这个很好理解,多个bean之间相互依赖,形成了一个闭环。
比如:A依赖于B、B依赖于C、C依赖于A。
代码中表示:
public class A{
	B b;
}
public class B{
	C c;
}
public class C{
	A a;
}

如何检测是否存在循环依赖?

检测循环依赖比较简单,使用一个列表来记录正在创建中的bean,bean创建之前,先去记录中看一下自己是否已经在列表中了,如果在,说明存在循环依赖,如果不在,则将其加入到这个列表,bean创建完毕之后,将其再从这个列表中移除。
源码方面来看一下,spring创建单例bean时候,会调用下面方法:
protected void beforeSingletonCreation(String beanName) {
    if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
        throw new BeanCurrentlyInCreationException(beanName);
    }
}

singletonsCurrentlyInCreation 就是用来记录目前正在创建中的bean名称列表,this.singletonsCurrentlyInCreation.add(beanName) 返回 false ,说明beanName已经在当前列表中了,此时会抛循环依赖的异常 BeanCurrentlyInCreationException ,这个异常对应的源码:

public BeanCurrentlyInCreationException(String beanName) {
    super(beanName,"Requested bean is currently in creation: Is there anunresolvable circular reference?");
}
上面是单例bean检测循环依赖的源码,再来看看非单例bean的情况。
以prototype情况为例,源码位于org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean 方法中,将主要代码列出来看一下:
//检查正在创建的bean列表中是否存在beanName,如果存在,说明存在循环依赖,抛出循环依赖的异常
if (isPrototypeCurrentlyInCreation(beanName)) {
	throw new BeanCurrentlyInCreationException(beanName);
}

//判断scope是否是prototype
if (mbd.isPrototype()) {
    Object prototypeInstance = null;
    try {
        //将beanName放入正在创建的列表中
        beforePrototypeCreation(beanName);
        prototypeInstance = createBean(beanName, mbd, args);
    }
    finally {
        //将beanName从正在创建的列表中移除
        afterPrototypeCreation(beanName);
    }
}

Spring如何解决循环依赖的问题

spring创建bean主要的几个步骤
  1. 步骤1:实例化bean,即调用构造器创建bean实例。

  2. 步骤2:填充属性,注入依赖的bean,比如通过set方式、@Autowired注解的方式注入依赖的bean。

  3. 步骤3:bean的初始化,比如调用init方法。

从上面3个步骤中可以看出,注入依赖的对象,有2种情况:
  1. 通过步骤1中构造器的方式注入依赖

  2. 通过步骤2注入依赖

先来看构造器的方式注入依赖的bean,下面两个bean循环依赖:
@Component
public class ServiceA {
    private ServiceB serviceB;
    public ServiceA(ServiceB serviceB) {
    	this.serviceB = serviceB;
    }
}

@Component
public class ServiceB {
    private ServiceA serviceA;
    public ServiceB(ServiceA serviceA) {
    	this.serviceA = serviceA;
    }
}
构造器的情况比较容易理解,实例化ServiceA的时候,需要有serviceB,而实例化ServiceB的时候需要有serviceA,构造器循环依赖是无法解决的,大家可以尝试一下使用编码的方式创建上面2个对象,是无法创建成功的!
再来看看非构造器的方式注入相互依赖的bean,以set方式注入为例,下面是2个单例的bean:serviceA和serviceB:
@Component
public class ServiceA {
    private ServiceB serviceB;
    @Autowired
    public void setServiceB(ServiceB serviceB) {
    	this.serviceB = serviceB;
    }
}
@Component
public class ServiceB {
    private ServiceA serviceA;
    @Autowired
    public void setServiceA(ServiceA serviceA) {
    	this.serviceA = serviceA;
    }
}

如果我们采用硬编码的方式创建上面2个对象,过程如下:

//创建serviceA
ServiceA serviceA = new ServiceA();
//创建serviceB
ServiceB serviceB = new ServiceB();
//将serviceA注入到serviceB中
serviceB.setServiceA(serviceA);
//将serviceB注入到serviceA中
serviceA.setServiceB(serviceB);

由于单例bean在spring容器中只存在一个,所以spring容器中肯定是有一个缓存来存放所有已创建好的单例bean;获取单例bean之前,可以先去缓存中找,找到了直接返回,找不到的情况下再去创建,创建完毕之后再将其丢到缓存中,可以使用一个map来存储单例bean,比如下面这个:

Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

下面来看一下spring中set方法创建上面2个bean的过程:

1.spring轮询准备创建2个bean:serviceA和serviceB
2.spring容器发现singletonObjects中没有serviceA
3.调用serviceA的构造器创建serviceA实例
4.serviceA准备注入依赖的对象,发现需要通过setServiceB注入serviceB
5.serviceA向spring容器查找serviceB
6.spring容器发现singletonObjects中没有serviceB
7.调用serviceB的构造器创建serviceB实例
8.serviceB准备注入依赖的对象,发现需要通过setServiceA注入serviceA
9.serviceB向spring容器查找serviceA
10.此时又进入步骤2了

卧槽,上面过程死循环了,怎么才能终结?
可以在第3步后加一个操作:将实例化好的serviceA丢到singletonObjects中,此时问题就解决了。spring中也采用类似的方式,稍微有点区别,上面使用了一个缓存,而spring内部采用了3级缓存来解决
这个问题,我们一起来细看一下。
3级缓存对应的代码:
/** 第一级缓存:单例bean的缓存 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** 第二级缓存:早期暴露的bean的缓存 */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
/** 第三级缓存:单例bean工厂的缓存 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
下面来看spring中具体的过程,我们一起来分析源码
开始的时候,获取serviceA,会调用下面代码 org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType, @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
    //1.查看缓存中是否已经有这个bean了
    Object sharedInstance = getSingleton(beanName); //@1
    if (sharedInstance != null && args == null) {
    	bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    }else {
        //若缓存中不存在,准备创建这个bean
        if (mbd.isSingleton()) {
            //2.下面进入单例bean的创建过程
            sharedInstance = getSingleton(beanName, () -> {
            try {
                return createBean(beanName, mbd, args);
            }
            catch (BeansException ex) {
                throw ex;
            }
        	});
    		bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
        }
    }
    return (T) bean;
}

@1:查看缓存中是否已经有这个bean了,如下:

public Object getSingleton(String beanName) {
	return getSingleton(beanName, true);
}

然后进入下面方法,会依次尝试从3级缓存中查找bean,注意下面的第2个参数,为ture的时候,才会从第3级中查找,否则只会查找1、2级缓存

//allowEarlyReference:是否允许从三级缓存singletonFactories中通过getObject拿到bean
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    //1.先从一级缓存中找
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            //2.从二级缓存中找
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                //3.二级缓存中没找到 && allowEarlyReference为true的情况下,从三级缓存中找
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    //三级缓存返回的是一个工厂,通过工厂来获取创建bean
                    singletonObject = singletonFactory.getObject();
                    //将创建好的bean丢到二级缓存中
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    //从三级缓存移除
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

刚开始,3个缓存中肯定是找不到的,会返回null,接着会执行下面代码准备创建 serviceA

if (mbd.isSingleton()) {
    sharedInstance = getSingleton(beanName, () -> {
        try {
            return createBean(beanName, mbd, args);
        }
        catch (BeansException ex) {
            destroySingleton(beanName);
            throw ex;
        }
    });
}

进入 getSingleton 方法,而 getSingleton 方法代码比较多,为了方便大家理解,无关的代码我给剔除了,如下:

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    synchronized (this.singletonObjects) {
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null) {
            //单例bean创建之前调用,将其加入正在创建的列表中,上面有提到过,主要用来检测循环依赖用的
            beforeSingletonCreation(beanName);
            boolean newSingleton = false;
            try {
                //调用工厂创建bean
                singletonObject = singletonFactory.getObject();//@1
                newSingleton = true;
            } finally {
                //单例bean创建之前调用,主要是将其从正在创建的列表中移除
                afterSingletonCreation(beanName);
            }
            if (newSingleton) {
                //将创建好的单例bean放入缓存中
                addSingleton(beanName, singletonObject);//@2
            }
        }
        return singletonObject;
    }
}

上面@1和@2是关键代码,先来看一下@1,这个是一个ObjectFactory类型的,从外面传入的,如下

image.png

红框中的 createBean 最终会调用下面这个方法org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
其内部主要代码如下:
BeanWrapper instanceWrapper = null;
if (instanceWrapper == null) {
    //通过反射调用构造器实例化serviceA
    instanceWrapper = createBeanInstance(beanName, mbd, args);
}
//变量bean:表示刚刚同构造器创建好的bean示例
final Object bean = instanceWrapper.getWrappedInstance();
//判断是否需要暴露早期的bean,条件为(是否是单例bean && 当前容器允许循环依赖 && bean名称存在于正在创建的bean名称清单中)
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
    //若earlySingletonExposure为true,通过下面代码将早期的bean暴露出去
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));//@1
}
这里需要理解一下什么是早期bean?
刚刚实例化好的bean就是早期的bean,此时bean还未进行属性填充,初始化等操作
@1 :通过 addSingletonFactory 用于将早期的bean暴露出去,主要是将其丢到第3级缓存中,代码如下:
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
        //第1级缓存中不存在bean
        if (!this.singletonObjects.containsKey(beanName)) {
            //将其丢到第3级缓存中
            this.singletonFactories.put(beanName, singletonFactory);
            //后面的2行代码不用关注
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
}
上面的方法执行之后,serviceA就被丢到第3级的缓存中了。
后续的过程serviceA开始注入依赖的对象,发现需要注入serviceB,会从容器中获取serviceB,而serviceB的获取又会走上面同样的过程实例化serviceB,然后将serviceB提前暴露出去,然后serviceB开始注入依赖的对象,serviceB发现自己需要注入serviceA,此时去容器中找serviceA,找serviceA会先去缓存中找,会执行 getSingleton("serviceA", true) ,此时会走下面代码:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    //1.先从一级缓存中找
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            //2.从二级缓存中找
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                //3.二级缓存中没找到 && allowEarlyReference为true的情况下,从三级缓存中找
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    //三级缓存返回的是一个工厂,通过工厂来获取创建bean
                    singletonObject = singletonFactory.getObject();
                    //将创建好的bean丢到二级缓存中
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    //从三级缓存移除
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}
上面的方法走完之后,serviceA会被放入二级缓存 earlySingletonObjects 中,会将serviceA返回,此时serviceB中的serviceA注入成功,serviceB继续完成创建,然后将自己返回给serviceA,此serviceA通过set方法将serviceB注入。
serviceA创建完毕之后,会调用 addSingleton 方法将其加入到缓存中,这块代码如下:
protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        //将bean放入第1级缓存中
        this.singletonObjects.put(beanName, singletonObject);
        //将其从第3级缓存中移除
        this.singletonFactories.remove(beanName);
        //将其从第2级缓存中移除
        this.earlySingletonObjects.remove(beanName);
    }
}
到此,serviceA和serviceB之间的循环依赖注入就完成了。
下面捋一捋整个过程:
1.从容器中获取serviceA
2.容器尝试从3个缓存中找serviceA,找不到
3.准备创建serviceA
4.调用serviceA的构造器创建serviceA,得到serviceA实例,此时serviceA还未填充属性,未进行其他任何初始化的操作
5.将早期的serviceA暴露出去:即将其丢到第3级缓存singletonFactories中
6.serviceA准备填充属性,发现需要注入serviceB,然后向容器获取serviceB
7.容器尝试从3个缓存中找serviceB,找不到
8.准备创建serviceB
9.调用serviceB的构造器创建serviceB,得到serviceB实例,此时serviceB还未填充属性,未进行其他任何初始化的操作
10.将早期的serviceB暴露出去:即将其丢到第3级缓存singletonFactories中
11.serviceB准备填充属性,发现需要注入serviceA,然后向容器获取serviceA
12.容器尝试从3个缓存中找serviceA,发现此时serviceA位于第3级缓存中,经过处理之后,serviceA会从第3级缓存中移除,然后会存到第2级缓存中,然后将其返回给serviceB,此时serviceA通过serviceB中的setServiceA方法被注入到serviceB中
13.serviceB继续执行后续的一些操作,最后完成创建工作,然后会调用addSingleton方法,将自己丢到第1级缓存中,并将自己从第2和第3级缓存中移除
14.serviceB将自己返回给serviceA
15.serviceA通过setServiceB方法将serviceB注入进去
16.serviceB继续执行后续的一些操作,最后完成创建工作,然后会调用addSingleton方法,将自己丢到第1级缓存中,并将自己从第2和第3级缓存中移除

循环依赖无法解决的情况

只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,而非单例的bean,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中。
那就会有下面几种情况需要注意。
还是以2个bean相互依赖为例:serviceA和serviceB

情况1

条件
serviceA:多例
serviceB:多例
结果
此时不管是任何方式都是无法解决循环依赖的问题,最终都会报错,因为每次去获取依赖的bean都会重新创建。

情况2

条件
serviceA:单例
serviceB:多例
结果
若使用构造器的方式相互注入,是无法完成注入操作的,会报错。
若采用set方式注入,所有bean都还未创建的情况下,若去容器中获取serviceB,会报错,为什么?我们来看一下过程:
1.从容器中获取serviceB
2.serviceB由于是多例的,所以缓存中肯定是没有的
3.检查serviceB是在正在创建的bean名称列表中,没有
4.准备创建serviceB
5.将serviceB放入正在创建的bean名称列表中
6.实例化serviceB(由于serviceB是多例的,所以不会提前暴露,必须是单例的才会暴露)
7.准备填充serviceB属性,发现需要注入serviceA
8.从容器中查找serviceA
9.尝试从3级缓存中找serviceA,找不到
10.准备创建serviceA
11.将serviceA放入正在创建的bean名称列表中
12.实例化serviceA
13.由于serviceA是单例的,将早期serviceA暴露出去,丢到第3级缓存中
14.准备填充serviceA的属性,发现需要注入serviceB
15.从容器中获取serviceB
16.先从缓存中找serviceB,找不到
17.检查serviceB是在正在创建的bean名称列表中,发现已经存在了,抛出循环依赖的异常
如果此处不是去获取serviceB,而是先去获取serviceA呢?则不会报错。

探讨:为什么需要用3级缓存

问题

如果只使用2级缓存,直接将刚实例化好的bean暴露给二级缓存出是否可以否?
先下个结论吧:不行

原因

这样做是可以解决:早期暴露给其他依赖者的bean和最终暴露的bean不一致的问题。
若将刚刚实例化好的bean直接丢到二级缓存中暴露出去,如果后期这个bean对象被更改了,比如可能在上面加了一些拦截器,将其包装为一个代理了,那么暴露出去的bean和最终的这个bean就不一样的,将自己暴露出去的时候是一个原始对象,而自己最终却是一个代理对象,最终会导致被暴露出去的和最终的bean不是同一个bean的,将产生意向不到的效果,而三级缓存就可以发现这个问题,会报错。
循环依赖的情况下,由于注入的是早期的bean,此时早期的bean中还未被填充属性,初始化等各种操作,也就是说此时bean并没有被完全初始化完毕,此时若直接拿去使用,可能存在有问题的风险。

二十八、BeanFactory扩展(BeanFactoryPostProcessor、BeanDefinitionRegistryPostProcessor)

Spring容器中主要的4个阶段
  1. 阶段1:Bean注册阶段,此阶段会完成所有bean的注册。

  2. 阶段2:BeanFactory后置处理阶段。

  3. 阶段3:注册BeanPostProcessor。

  4. 阶段4:bean创建阶段,此阶段完成所有单例bean的注册和装载操作。

本文介绍的2个接口主要和前2个阶段有关系,下面我们主要来看前2个阶段。

阶段1:Bean注册阶段

spring中所有bean的注册都会在此阶段完成,按照规范,所有bean的注册必须在此阶段进行,其他阶段不要再进行bean的注册。
这个阶段spring为我们提供1个接口:BeanDefinitionRegistryPostProcessor,spring容器在这个阶段中会获取容器中所有类型为 BeanDefinitionRegistryPostProcessor 的bean,然后会调用他们的postProcessBeanDefinitionRegistry 方法,源码如下,方法参数类型是BeanDefinitionRegistry ,这个类型大家都比较熟悉,即bean定义注册器,内部提供了一些方法可以用来向容器中注册bean。
public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
	void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;
}
这个接口还继承了 BeanFactoryPostProcessor 接口,这个大家先不用关心,一会阶段2中会介绍。
当容器中有多个 BeanDefinitionRegistryPostProcessor 的时候,可以通过下面任意一种方式来指定顺序
  1. 实现 org.springframework.core.PriorityOrdered 接口

  2. 实现 org.springframework.core.Ordered 接口

执行顺序:
PriorityOrdered.getOrder() asc
Ordered.getOrder() asc
BeanDefinitionRegistryPostProcessor 有个非常重要的实现 org.springframework.context.annotation.ConfigurationClassPostProcessor
这个类可能有些人不熟悉,下面这些注解大家应该比较熟悉吧,这些注解都是在上面这个类中实现的,通过这些注解来实现bean的批量注册
@Configuration
@ComponentScan
@Import
@ImportResource
@PropertySource
有兴趣的朋友可以去看一下ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry 研究一下上面这些注解的解析过程

阶段2:BeanFactory后置处理阶段

到这个阶段的时候,spring容器已经完成了所有bean的注册,这个阶段中你可以对BeanFactory中的一些信息进行修改,比如修改阶段1中一些bean的定义信息,修改BeanFactory的一些配置等等,此阶段spring也提供了一个接口来进行扩展: BeanFactoryPostProcessor ,简称 bfpp ,接口中有个方法postProcessBeanFactory ,spring会获取容器中所有BeanFactoryPostProcessor类型的bean,然后调用他们的 postProcessBeanFactory ,来看一下这个接口的源码:
@FunctionalInterface
public interface BeanFactoryPostProcessor {
	void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;
}
当容器中有多个 BeanFactoryPostProcessor 的时候,可以通过下面任意一种方式来指定顺序
  1. 实现 org.springframework.core.PriorityOrdered 接口

  2. 实现 org.springframework.core.Ordered 接口

执行顺序:
PriorityOrdered.getOrder() asc
Ordered.getOrder() asc

这个接口的几个重要实现类

PropertySourcesPlaceholderConfigurer
这个接口做什么的,大家知道么?来看一段代码
<bean class="xxxxx">
    <property name="userName" value="${userName}"/>
    <property name="address" value="${address}"/>
</bean>
这个大家比较熟悉吧,spring就是在PropertySourcesPlaceholderConfigurer#postProcessBeanFactory 中来处理xml中属性中的${xxx} ,会对这种格式的进行解析处理为真正的值。
CustomScopeConfigurer
向容器中注册自定义的Scope对象,即注册自定义的作用域实现类。
EventListenerMethodProcessor
处理 @EventListener 注解的,即spring中事件机制。

使用注意

BeanFactoryPostProcessor 接口的使用有一个需要注意的地方,在其 postProcessBeanFactory 方法中,强烈禁止去通过容器获取其他bean,此时会导致bean的提前初始化,会出现一些意想不到的问题,因为这个阶段中 BeanPostProcessor 还未准备好,本文开头4个阶段中有介绍,BeanPostProcessor 是在第3个阶段中注册到spring容器的,而 BeanPostProcessor 可以对bean的创建过程进行干预,比如spring中的aop就是在 BeanPostProcessor 的一些子类中实现的, @Autowired也是在 BeanPostProcessor 的子类中处理的,此时如果去获取bean,此时bean不会被BeanPostProcessor 处理。

总结

  1. 注意Spring的4个阶段:bean定义阶段、BeanFactory后置处理阶段、BeanPostProcessor注册阶段、单例bean创建组装阶段。

  2. BeanDefinitionRegistryPostProcessor会在第一个阶段被调用,用来实现bean的注册操作,这个阶段会完成所有bean的注册。

  3. BeanFactoryPostProcessor会在第2个阶段被调用,到这个阶段时候,bean此时已经完成了所有bean的注册操作,这个阶段中你可以对BeanFactory中的一些信息进行修改,比如修改阶段1中一些bean的定义信息,修改BeanFactory的一些配置等等。。

  4. 阶段2的时候,2个禁止操作:禁止注册bean、禁止从容器中获取bean。

  5. 本文介绍的2个接口的实现类可以通过 PriorityOrdered 接口或者 Ordered 接口来指定顺序。


版权声明

非特殊说明,本文由Zender原创或收集发布,欢迎转载。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

作者文章
热门