521我发誓读完本文,再也不会担心Spring配置类问题了(下)

简介: 521我发誓读完本文,再也不会担心Spring配置类问题了(下)

FactoryBean模式剖析


FactoryBean也是向容器提供Bean的一种方式,如最常见的SqlSessionFactoryBean就是这么一个大代表,因为它比较常用,并且这里也作为此拦截器一个单独的执行分支,所以很有必要研究一番。


执行此分支逻辑的条件是:容器内已经存在&beanName和beanName两个Bean。执行的方式是:使用enhanceFactoryBean()方法对FactoryBean进行增强。


ConfigurationClassEnhancer:
// 创建一个子类代理,拦截对getObject()的调用,委托给当前的BeanFactory
// 而不是创建一个新的实例。这些代理仅在调用FactoryBean时创建
// factoryBean:从容器内拿出来的那个已经存在的工厂Bean实例(是工厂Bean实例)
// exposedType:@Bean标注的方法的返回值类型
private Object enhanceFactoryBean(Object factoryBean, Class<?> exposedType,
    ConfigurableBeanFactory beanFactory, String beanName) {
  try {
    // 看看Spring容器内已经存在的这个工厂Bean的情况,看看是否有final
    Class<?> clazz = factoryBean.getClass();
    boolean finalClass = Modifier.isFinal(clazz.getModifiers());
    boolean finalMethod = Modifier.isFinal(clazz.getMethod("getObject").getModifiers());
    // 类和方法其中有一个是final,那就只能看看能不能走接口代理喽
    if (finalClass || finalMethod) {
      // @Bean标注的方法返回值若是接口类型 尝试走基于接口的JDK动态代理
      if (exposedType.isInterface()) {
        // 基于JDK的动态代理
        return createInterfaceProxyForFactoryBean(factoryBean, exposedType, beanFactory, beanName);
      } else {
        // 类或方法存在final情况,但是呢返回类型又不是
        return factoryBean;
      }
    }
  }
  catch (NoSuchMethodException ex) {
    // 没有getObject()方法  很明显,一般不会走到这里
  }
  // 到这,说明以上条件不满足:存在final且还不是接口类型
  // 类和方法都不是final,生成一个CGLIB的动态代理
  return createCglibProxyForFactoryBean(factoryBean, beanFactory, beanName);
}



步骤总结:


1.拿到容器内已经存在的这个工厂Bean的类型,看看类上、getObject()方法是否用final修饰了


2.但凡只需有一个被final修饰了,那注定不能使用CGLIB代理了喽,那么就尝试使用基于接口的JDK动态代理:

  1. 若你标注的@Bean返回的是接口类型(也就是FactoryBean类型),那就ok,使用JDK创建个代理对象返回
  2. 若不是接口(有final又还不是接口),那老衲无能为力了:原样return返回


3.若以上条件不满足,表示一个final都木有,那就统一使用CGLIB去生成一个代理子类。大多数情况下,都会走到这个分支上,代理是通过CGLIB生成的



说明:无论是JDK动态代理还是CGLIB的代理实现均非常简单,就是把getObject()方法代理为使用beanFactory.getBean(beanName)去获取实例(要不代理掉的话,每次不就执行你getObject()里面的逻辑了麽,就又会创建新实例啦~)


需要明确,此拦截器对FactoryBean逻辑处理分支的目的是:确保你通过方法调用拿到FactoryBean后,再调用其getObject()方法(哪怕调用多次)得到的都是同一个示例(容器内的单例)。因此需要对getObject()方法做拦截嘛,让该方法指向到getBean(),永远从容器里面拿即可。


这个拦截处理逻辑只有在@Bean方法调用时才有意义,比如parent()里调用了son()这样子才会起到作用,否则你就忽略它吧~


针对于此,下面给出不同case下的代码示例,加强理解。


代码示例(重要)


准备一个SonFactoryBean用于产生Son实例:


public class SonFactoryBean implements FactoryBean<Son> {
    @Override
    public Son getObject() throws Exception {
        return new Son();
    }
    @Override
    public Class<?> getObjectType() {
        return Son.class;
    }
}


并且在配置类里把它放好:


@Configuration
public class AppConfig {
    @Bean
    public FactoryBean<Son> son() {
        SonFactoryBean sonFactoryBean = new SonFactoryBean();
        System.out.println("我使用@Bean定义sonFactoryBean:" + sonFactoryBean.hashCode());
        System.out.println("我使用@Bean定义sonFactoryBean identityHashCode:" + System.identityHashCode(sonFactoryBean));
        return sonFactoryBean;
    }
    @Bean
    public Parent parent(Son son) throws Exception {
        // 根据前面所学,sonFactoryBean肯定是去容器拿
        FactoryBean<Son> sonFactoryBean = son();
        System.out.println("parent流程使用的sonFactoryBean:" + sonFactoryBean.hashCode());
        System.out.println("parent流程使用的sonFactoryBean identityHashCode:" + System.identityHashCode(sonFactoryBean));
        System.out.println("parent流程使用的sonFactoryBean:" + sonFactoryBean.getClass());
        // 虽然sonFactoryBean是从容器拿的,但是getObject()你可不能保证每次都返回单例哦~
        Son sonFromFactory1 = sonFactoryBean.getObject();
        Son sonFromFactory2 = sonFactoryBean.getObject();
        System.out.println("parent流程使用的sonFromFactory1:" + sonFromFactory1.hashCode());
        System.out.println("parent流程使用的sonFromFactory1:" + sonFromFactory2.hashCode());
        System.out.println("parent流程使用的son和容器内的son是否相等:" + (son == sonFromFactory1));
        return new Parent(sonFromFactory1);
    }
}


运行程序:

@Bean
public static void main(String[] args) {
    ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
    SonFactoryBean sonFactoryBean = context.getBean("&son", SonFactoryBean.class);
    System.out.println("Spring容器内的SonFactoryBean:" + sonFactoryBean.hashCode());
    System.out.println("Spring容器内的SonFactoryBean:" + System.identityHashCode(sonFactoryBean));
    System.out.println("Spring容器内的SonFactoryBean:" + sonFactoryBean.getClass());
    System.out.println("Spring容器内的Son:" + context.getBean("son").hashCode());
}


输出结果:

我使用@Bean定义sonFactoryBean:313540687
我使用@Bean定义sonFactoryBean identityHashCode:313540687
parent流程使用的sonFactoryBean:313540687
parent流程使用的sonFactoryBean identityHashCode:70807318
parent流程使用的sonFactoryBean:class com.yourbatman.fullliteconfig.config.SonFactoryBean$$EnhancerBySpringCGLIB$$1ccec41d
parent流程使用的sonFromFactory1:910091170
parent流程使用的sonFromFactory1:910091170
parent流程使用的son和容器内的son是否相等:true
Spring容器内的SonFactoryBean:313540687
Spring容器内的SonFactoryBean:313540687
Spring容器内的SonFactoryBean:class com.yourbatman.fullliteconfig.config.SonFactoryBean
Spring容器内的Son:910091170


结果分析:


image.png


达到了预期的效果:parent在调用son()方法时,得到的是在容器内已经存在的SonFactoryBean基础上CGLIB字节码提升过的实例,拦截成功,从而getObject()也就实际是去容器里拿对象的。


通过本例有如下小细节需要指出:


  1. 原始对象和代理/增强后(不管是CGLIB还是JDK动态代理)的实例的.hashCode()以及.equals()方法是一毛一样的,但是identityHashCode()值(实际内存值)不一样哦,因为是不同类型、不同实例,这点请务必注意
  2. 最终存在于容器内的仍旧是原生工厂Bean对象,而非代理后的工厂Bean实例。毕竟拦截器只是拦截了@Bean方法的调用来了个“偷天换日”而已~
  3. 若SonFactoryBean上加个final关键字修饰,根据上面讲述的逻辑,那代理对象会使用JDK动态代理生成喽,形如这样(本处仅作为示例,实际使用中请别这么干):


/

public final class SonFactoryBean implements FactoryBean<Son> { ... }


再次运行程序,结果输出为:执行的结果一样,只是代理方式不一样而已。从这个小细节你也能看出来Spring对代理实现上的偏向:优先选择CGLIB代理方式,JDK动态代理方式用于兜底。


...
// 使用了JDK的动态代理
parent流程使用的sonFactoryBean:class com.sun.proxy.$Proxy11
...


提示:若你标注了final关键字了,那么请保证@Bean方法返回的是FactoryBean接口,而不能是SonFactoryBean实现类,否则最终无法代理了,原样输出。因为JDK动态代理和CGLIB都搞不定了嘛~


在以上例子的基础上,我给它“加点料”,再看看效果呢:


使用BeanDefinitionRegistryPostProcessor提前就放进去一个名为son的实例:


// 这两种方式向容器扔bd or singleton bean都行  我就选择第二种喽
// 注意:此处放进去的是BeanFactory工厂,名称是son哦~~~  不要写成了&son
@Component
public class SonBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        // registry.registerBeanDefinition("son", BeanDefinitionBuilder.rootBeanDefinition(SonFactoryBean.class).getBeanDefinition());
    }
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        SonFactoryBean sonFactoryBean = new SonFactoryBean();
        System.out.println("初始化时,注册进容器的sonFactoryBean:" + sonFactoryBean);
        beanFactory.registerSingleton("son", sonFactoryBean);
    }
}


再次运行程序,输出结果:


初始化时最早进容器的sonFactoryBean:2027775614
初始化时最早进容器的sonFactoryBean identityHashCode:2027775614
parent流程使用的sonFactoryBean:2027775614
parent流程使用的sonFactoryBean identityHashCode:1183888521
parent流程使用的sonFactoryBean:class com.yourbatman.fullliteconfig.config.SonFactoryBean$$EnhancerBySpringCGLIB$$1ccec41d
parent流程使用的sonFromFactory1:2041605291
parent流程使用的sonFromFactory1:2041605291
parent流程使用的son和容器内的son是否相等:true
Spring容器内的SonFactoryBean:2027775614
Spring容器内的SonFactoryBean:2027775614
Spring容器内的SonFactoryBean:class com.yourbatman.fullliteconfig.config.SonFactoryBean
Spring容器内的Son:2041605291


效果上并不差异,从日志上可以看到:你配置类上使用@Bean标注的son()方法体并没执行了,而是使用的最开始注册进去的实例,差异仅此而已。


为何是这样的现象?这就不属于本文的内容了,是Spring容器对Bean的实例化、初始化逻辑,本公众号后面依旧会采用专栏式讲解,让你彻底弄懂它。当前有兴趣的可以先自行参考DefaultListableBeanFactory#preInstantiateSingletons的内容~


Lite模式下表现如何?

Lite模式下可没这些“加强特性”,所以在Lite模式下(拿掉@Configuration这个注解便可)运行以上程序,结果输出为:


我使用@Bean定义sonFactoryBean:477289012
我使用@Bean定义sonFactoryBean identityHashCode:477289012
我使用@Bean定义sonFactoryBean:2008966511
我使用@Bean定义sonFactoryBean identityHashCode:2008966511
parent流程使用的sonFactoryBean:2008966511
parent流程使用的sonFactoryBean identityHashCode:2008966511
parent流程使用的sonFactoryBean:class com.yourbatman.fullliteconfig.config.SonFactoryBean
parent流程使用的sonFromFactory1:433874882
parent流程使用的sonFromFactory1:572191680
parent流程使用的son和容器内的son是否相等:false
Spring容器内的SonFactoryBean:477289012
Spring容器内的SonFactoryBean:477289012
Spring容器内的SonFactoryBean:class com.yourbatman.fullliteconfig.config.SonFactoryBean
Spring容器内的Son:211968962


结果解释我就不再啰嗦,有了前面的基础就太容易理解了。

为何是@Scope域代理就不用处理?


要解释好这个原因,和@Scope代理方式的原理知识强相关。限于篇幅,本文就先卖个关子~


关于@Scope我个人觉得足够用5篇以上文章专题讲解,虽然在Spring Framework里使用得比较少,但是在理解Spirng Cloud的自定义扩展实现上显得非常非常有必要,所以你可关注我公众号,会近期推出相关专栏的。


总结


关于Spring配置类这个专栏内容,讲解到这就完成99%了,毫不客气的说关于此部分知识真正可以实现“横扫千军”,据我了解没有解决不了的问题了。


当然还剩下1%,那自然是缺少一篇总结篇喽:在下一篇总结篇里,我会用图文并茂的方式对Spring配置类相关内容的执行流程进行总结,目的是让你快速掌握,应付面试嘛。

相关文章
|
4月前
|
负载均衡 监控 Java
Spring Cloud Gateway 全解析:路由配置、断言规则与过滤器实战指南
本文详细介绍了 Spring Cloud Gateway 的核心功能与实践配置。首先讲解了网关模块的创建流程,包括依赖引入(gateway、nacos 服务发现、负载均衡)、端口与服务发现配置,以及路由规则的设置(需注意路径前缀重复与优先级 order)。接着深入解析路由断言,涵盖 After、Before、Path 等 12 种内置断言的参数、作用及配置示例,并说明了自定义断言的实现方法。随后重点阐述过滤器机制,区分路由过滤器(如 AddRequestHeader、RewritePath、RequestRateLimiter 等)与全局过滤器的作用范围与配置方式,提
Spring Cloud Gateway 全解析:路由配置、断言规则与过滤器实战指南
|
4月前
|
Java 关系型数据库 MySQL
Spring Boot自动配置:魔法背后的秘密
Spring Boot 自动配置揭秘:只需简单配置即可启动项目,背后依赖“约定大于配置”与条件化装配。核心在于 `@EnableAutoConfiguration` 注解与 `@Conditional` 系列条件判断,通过 `spring.factories` 或 `AutoConfiguration.imports` 加载配置类,实现按需自动装配 Bean。
|
4月前
|
人工智能 Java 开发者
【Spring】原理解析:Spring Boot 自动配置
Spring Boot通过“约定优于配置”的设计理念,自动检测项目依赖并根据这些依赖自动装配相应的Bean,从而解放开发者从繁琐的配置工作中解脱出来,专注于业务逻辑实现。
1679 0
|
6月前
|
Java Spring
Spring Boot配置的优先级?
在Spring Boot项目中,配置可通过配置文件和外部配置实现。支持的配置文件包括application.properties、application.yml和application.yaml,优先级依次降低。外部配置常用方式有Java系统属性(如-Dserver.port=9001)和命令行参数(如--server.port=10010),其中命令行参数优先级高于系统属性。整体优先级顺序为:命令行参数 &gt; Java系统属性 &gt; application.properties &gt; application.yml &gt; application.yaml。
1084 0
|
3月前
|
前端开发 Java 应用服务中间件
《深入理解Spring》 Spring Boot——约定优于配置的革命者
Spring Boot基于“约定优于配置”理念,通过自动配置、起步依赖、嵌入式容器和Actuator四大特性,简化Spring应用的开发与部署,提升效率,降低门槛,成为现代Java开发的事实标准。
|
4月前
|
缓存 Java 应用服务中间件
Spring Boot配置优化:Tomcat+数据库+缓存+日志,全场景教程
本文详解Spring Boot十大核心配置优化技巧,涵盖Tomcat连接池、数据库连接池、Jackson时区、日志管理、缓存策略、异步线程池等关键配置,结合代码示例与通俗解释,助你轻松掌握高并发场景下的性能调优方法,适用于实际项目落地。
713 5
|
4月前
|
传感器 Java 数据库
探索Spring Boot的@Conditional注解的上下文配置
Spring Boot 的 `@Conditional` 注解可根据不同条件动态控制 Bean 的加载,提升应用的灵活性与可配置性。本文深入解析其用法与优势,并结合实例展示如何通过自定义条件类实现环境适配的智能配置。
236 0
探索Spring Boot的@Conditional注解的上下文配置
|
5月前
|
安全 算法 Java
在Spring Boot中应用Jasypt以加密配置信息。
通过以上步骤,可以在Spring Boot应用中有效地利用Jasypt对配置信息进行加密,这样即使配置文件被泄露,其中的敏感信息也不会直接暴露给攻击者。这是一种在不牺牲操作复杂度的情况下提升应用安全性的简便方法。
1145 10
|
6月前
|
人工智能 安全 Java
Spring Boot yml 配置敏感信息加密
本文介绍了如何在 Spring Boot 项目中使用 Jasypt 实现配置文件加密,包含添加依赖、配置密钥、生成加密值、在配置中使用加密值及验证步骤,并提供了注意事项,确保敏感信息的安全管理。
1315 1
|
6月前
|
SQL XML Java
配置Spring框架以连接SQL Server数据库
最后,需要集成Spring配置到应用中,这通常在 `main`方法或者Spring Boot的应用配置类中通过加载XML配置或使用注解来实现。
534 0

热门文章

最新文章