Spring Boot 自定义 starter 和自动配置的工作原理
如果你正在参与一个共享库的开发,你可能会想为使用方提供自动配置的支持,以帮助对方快速地接入和使用。自动配置机制往往和 starter
联系在一起,本文将介绍如何创建一个自定义的 starter
并从源码角度分析 Spring Boot
自动配置的工作原理。
如果你正在参与一个共享库的开发,你可能会想为使用方提供自动配置的支持,以帮助对方快速地接入和使用。自动配置机制往往和 starter
联系在一起,本文将介绍如何创建一个自定义的 starter
并从源码角度分析 Spring Boot
自动配置的工作原理。
Import
注解是 Spring
基于 Java
注解配置的重要组成部分,处理 Import
注解是处理 Configuration
注解的子过程之一,本文将介绍 Import
注解的 3
种使用方式,然后通过分析源码和处理过程示意图解释它是如何导入(注册) BeanDefinition
的。
Nginx
没有提供开箱即用的日志滚动功能,而是将其交给使用者自己实现。你既可以按照官方文档的建议通过编写脚本实现,也可以使用 logrotate
管理日志。但是和在普通场景下不同,在使用 Docker
运行 Nginx
时,你可能需要额外考虑一点细节。本文记录了在为 Docker
中的 Nginx
的日志文件配置滚动功能过程中遇到的一些问题和思考。
原先在使用 Cloudflare Tunnel
访问家庭网络中的服务时,是直接将域名解析到相应服务。尽管 Cloudflare
已经提供相关的请求统计和安全防护功能,部分服务自身也有访问日志,但是为了更好地监控和跟踪对外服务的使用情况,采集 Cloudlfare
统计中缺少的新,决定使用 Nginx
反向代理内部服务,统一内部服务的访问入口。简而言之就是,又折腾一些有的没的。以上修改带来的一个附加好处是在局域网内访问服务时,通过在 hosts
文件中添加域名映射,可以用更加容易记忆的域名代替 IP + port
的形式去访问。
直接展示一个具体的 Dubbo SPI
自适应拓展是什么样子,是一种非常好的表现其作用的方式。正如官方博客中所说的,它让人对自适应拓展有更加感性的认识,避免读者一开始就陷入复杂的代码生成逻辑。本文在此基础上,从更原始的使用方式上展现“动态加载”技术对“按需加载”的天然倾向,从更普遍的角度解释自适应拓展的本质目的,在介绍 Dubbo
的具体实现是如何约束自身从而规避缺点之后,详细梳理了 Dubbo SPI
自适应拓展的相关源码和工作原理。
SPI
作为一种服务发现机制,允许程序在运行时动态地加载具体实现类。因其强大的可拓展性,SPI
被广泛应用于各类技术框架中,例如 JDBC
驱动、Spring
和 Dubbo
等等。Dubbo
并未使用原生的 Java SPI
,而是重新实现了一套更加强大的 Dubbo SPI
。本文将简单介绍 SPI
的设计理念,通过示例带你体会 SPI
的作用,通过 Dubbo
获取拓展的流程图和源码分析带你理解 Dubbo SPI
的工作原理。深入了解 Dubbo SPI
,你将能更好地利用这一机制为你的程序提供灵活的拓展功能。
Configuration
注解是 Spring
中常用的注解,在一般的应用场景中,它用于标识一个类作为配置类,搭配 Bean
注解将创建的 bean
交给 Spring
容器管理。神奇的是,被 Bean
注解标注的方法,只会被真正调用一次。这种方法调用被拦截的情况很容易让人联想到代理,如果你在 Debug
时注意过配置类的实例,你会发现配置类的 Class
名称中携带 EnhancerBySpringCGLIB
。本文将从源码角度,分析 Configuration
注解是如何工作的。
Spring
中的循环依赖是一个“大名鼎鼎”的问题,本文从原始的问题出发分析应当如何正确地看待和处理循环依赖现象,同时也会回归到源码详细介绍 Spring
的具体处理过程,并在最后给出笔者的个人思考。
Spring AOP 是基于代理实现的,它既支持 JDK 动态代理也支持 CGLib。
1 | <dependency> |
1 | public class MathCalculator { |
1 | @Aspect |
1 | @Configuration |
1 | public class AopTest { |
1 | beanName.equals("mathCalculator") |
其实创建代理 Bean 的过程和创建普通 Bean 的过程直到进行初始化处理(initializeBean)前都是一样的。更具体地说,如很多资料所言,Spring 创建代理对象的工作,是在应用后置处理器阶段完成的。
mathCalculator 以 getBean 方法为起点,开始创建的过程。
1 | @Override |
在正常地实例化 Bean 后,初始化 Bean 时,会对 Bean 实例应用后置处理器。
可是,究竟是哪一个后置处理器做的呢?
1 | protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) { |
在本示例中,创建代理的后置处理器就是 AnnotationAwareAspectJAutoProxyCreator,它继承自 AbstractAutoProxyCreator,AbstractAutoProxyCreator 实现了 BeanPostProcessor 接口。
那么,它是什么时候,怎么加入到 beanFactory 中呢?
PS: 显然,还有其他继承自 AbstractAutoProxyCreator 的后置处理器,暂时不谈。
postProcessBeforeInitialization 和 postProcessAfterInitialization 方法,前者什么都没做,后者在必要时对 Bean 进行包装。
AbstractAutoProxyCreator#postProcessAfterInitialization
就是创建代理对象的入口。1 | public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport |
1 | protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { |
AbstractAutoProxyCreator#createProxy,创建一个 ProxyFactory,将工作交给它处理。
1 | protected Object createProxy( |
ProxyFactory#getProxy,创建一个 AopProxy 并委托它实现 getProxy。
AopProxy 的含义与职责从字面上有点不好理解。
1 | public Object getProxy(ClassLoader classLoader) { |
ProxyFactory#createAopProxy,获取一个 AopProxyFactory 创建 AopProxy。
1 | protected final synchronized AopProxy createAopProxy() { |
AopProxyFactory#createAopProxy。
这里的处理,决定了 Spring AOP 会使用哪一种动态代理实现。比如 Spring AOP 默认使用 JDK 动态代理,如果目标对象实现了接口 Spring 会使用 JDK 动态代理,这些结论的依据就在于此。
1 | public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { |
AopProxy 视角,获取代理。
JdkDynamicAopProxy。
1 | @Override |
根据 Proxy.newProxyInstance(classLoader, proxiedInterfaces, this)
可知,this 也就是 JdkDynamicAopProxy 同时也是一个 InvocationHandler,它必然实现了 invoke 方法,当代理对象调用方法时,就会进入到 invoke 方法中。
1 | @Override |
ObjenesisCglibAopProxy。
1 | @Override |
你可能会注意到 Spring 中并没有直接依赖 CGLib,像 Enhancer 所在的包是 org.springframework.cglib.proxy
。根据文档:
从 spring 3.2 开始,不再需要将 cglib 添加到类路径中,因为 cglib 类在 org.springframework 下重新打包并分布在 spring-core jar 中。 这样做既是为了方便,也是为了避免与使用不同版本 cglib 的其他项目发生潜在冲突。
在前面预留了一些问题,当初我在看网上的资料时就有这些困惑。
Debug 停留在 Spring 上下文刷新方法中的 finishBeanFactoryInitialization。
1 | @Override |
从 beanFatory 的 beanDefinitionMap 可以观察到,配置类 AopConfig 中的 MathCalculator 和 LogAspect 的信息已经就位。
从 beanFactory 的 beanProcessor 可以观察到,AnnotationAwareAspectJAutoProxyCreator 已经就位。
注解 @EnableXXX 往往伴随着注解 @Import,在 invokeBeanFactoryPostProcessors(beanFactory) 中,工厂后置处理器 ConfigurationClassPostProcessor 会处理它。
1 | @Target(ElementType.TYPE) |
在 ConfigurationClassPostProcessor 的处理中,因为 AspectJAutoProxyRegistrar 实现了 ImportBeanDefinitionRegistrar,registerBeanDefinitions 方法会被调用,AnnotationAwareAspectJAutoProxyCreator 的 beanDefinition 随之被注册到 beanFactory,因 AnnotationAwareAspectJAutoProxyCreator 实现了 BeanPostProcessor 被提前创建。
1 | class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar { |
1 | public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, Object source) { |
进入创建 Bean 的方法 createBean 后,除了 doCreateBean,应额外留意 resolveBeforeInstantiation 方法。
Object bean = resolveBeforeInstantiation(beanName, mbdToUse)
,在实例化前进行解析。Object beanInstance = doCreateBean(beanName, mbdToUse, args)
,创建 Bean 的具体过程。1 | @Override |
根据注释,该方法给 BeanPostProcessors 一个机会提前返回一个代理对象。在本示例中,返回 null,但是方法在第一次执行后已经提前解析得到 advisors 并缓存。
1 | protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) { |
应用 InstantiationAwareBeanPostProcessor 的 postProcessBeforeInstantiation。
1 | protected Object applyBeanPostProcessorsBeforeInstantiation(Class<?> beanClass, String beanName) { |
AnnotationAwareAspectJAutoProxyCreator 不仅仅是一个 BeanPostProcessor,它还是一个 InstantiationAwareBeanPostProcessor。
1 | public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException { |
和 wrapIfNecessary 方法对比,容易发现两者有不少相似的处理。
注意:以下方法应注意是否被子类重写。
org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator#shouldSkip
1 | protected boolean shouldSkip(Class<?> beanClass, String beanName) { |
org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean
1 | protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, TargetSource targetSource) { |
org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#findEligibleAdvisors
1 | protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) { |
容易注意到两者在创建代理前,都会调用 findCandidateAdvisors 方法查找候选的 advisors,其实这也是我们想要找的对切面类的解析处理所在。
org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors
1 | protected List<Advisor> findCandidateAdvisors() { |
org.springframework.aop.aspectj.annotation.BeanFactoryAspectJAdvisorsBuilder#buildAspectJAdvisors
1 | public List<Advisor> buildAspectJAdvisors() { |
可以通过 beanFactory->beanPostProcessors->aspectJAdvisorsBuilder->advisorsCache
观察 advisors 的查找情况。
Java 标准库提供了动态代理功能,允许程序在运行期动态创建指定接口的实例。
使用 ASM 框架,加载代理对象的 Class 文件,通过修改其字节码生成子类。
根据 README.md 的提醒,cglib 已经不再维护,且在较新版本的 JDK 尤其是 JDK 17+ 中表现不佳,官方推荐可以考虑迁移到 ByteBuddy。在如今越来越多的项目迁移到 JDK 17 的背景下,值得注意。
代理对象的类和实现的接口:
HelloService.java
1 | public interface HelloService { |
HelloService.java
1 | public class HelloServiceImpl implements HelloService { |
1 | public class UserServiceInvocationHandler implements InvocationHandler { |
1 | public class JdkProxyTest { |
1 | do sth. before invocation |
1 | <dependencies> |
1 | public class UserServiceMethodInterceptor implements MethodInterceptor { |
1 | public class CglibTest { |
1 | do sth. before invocation |
使用以下语句,将在工作目录下生成代理类的 Class 文件。
1 | System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); |
1 | public final class $Proxy0 extends Proxy implements HelloService { |
使用以下语句,将 CGLib 生成的子类的 Class 文件输出到指定目录,会发现出现了 3 个 Class 文件。
1 | System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "C:\\Users\\username\\Class"); |
继承了被代理类。
1 | public class HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31 extends HelloServiceImpl implements Factory { |
1 | static { |
MethodProxy 稍后再做介绍。
构造器方法内,调用了绑定回调(Callbacks)方法。
1 | public HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31() { |
CGLib 会为每一个代理方法生成两个对应的方法,一个直接调用父类方法,一个则调用回调(拦截器)的 intercept 方法。
1 | final void CGLIB$sayHello$0(String var1) { |
CGLib 通过继承实现动态代理的过程,在查看生成的子类的 Class 后,是非常容易理解的。拦截器的参数有代理对象、Method、方法参数和 MethodProxy 对象。
如何在拦截器中调用被代理的方法呢?就是通过 MethodProxy 实现的。
MethodProxy 是 CGLib 为每一个代理方法创建的方法代理,当调用拦截的方法时,它被传递给 MethodInterceptor 对象的 intercept 方法。它可以用于调用原始方法,或对同一类型的不同对象调用相同方法。
1 | CGLIB$sayHello$0$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/String;)V", "sayHello", "CGLIB$sayHello$0"); |
CreateInfo 静态内部类,保存被代理类和代理类以及其他一些信息。
1 | private static class CreateInfo |
MethodProxy 通过 invokeSuper 调用原始方法(父类方法)。
1 | // invoke 方法的代码相似 |
1 | private void init() |
CGLIB$sayHello$0
方法在生成的 FastClass 中的索引。sayHello
方法在生成的 FastClass 中的索引。invoke 方法根据传入的方法索引,快速定位要调用对象 obj 的哪个方法。
CGLib 完全有能力获得
CGLIB$sayHello$0
的 Method 对象,通过反射实现调用,这样处理逻辑更加清楚。但是早期 Java 反射的性能并不好,通过 FastClass 机制避免使用反射从而提升了性能。
1 | private static class FastClassInfo |
以代理类的 FastClass HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31$$FastClassByCGLIB$$c068b511
为例,当传入的方法索引为 16 时,就会调用 CGLIB$sayHello$0
方法。
1 | public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException { |
怎么知道方法的索引呢?在初始化 FastClass 信息时,不仅生成了 FastClass,还通过 getIndex 获取方法的索引。
在 JDK 7 之后,switch 不仅可以支持 int、enum,还能支持 String,CGLib 这样实现是出于兼容性的考虑还是说有什么性能提升?
1 | public int getIndex(Signature var1) { |
两者在使用上是相仿的。
借着梳理 Spring 的机会回头再看,又感觉轻松不少。