Java后端框架技术点整理讲解——SSM,Springboot,原理

对于后端常用框架的技术整理,其实框架在平时就是会用就行,但面试时多半需要描述实现原理,这个要靠自己理解,不推荐死记硬背。

这篇和另外几篇文章区分开,主要用于规整Java后端各种框架,面试使用及原理相关问题。

一:Spring

Spring是一个轻量级开源的后端框架,是为Java应用程序提供基础性服务的一套框架。核心功能为IOC容器管理和AOP面向切面编程。

早期使用Spring需提供XML配置,Java5引入注解后,使用大量注解替换原有XML配置。但随着Spring注解的数量越来越多,尤其是相同注解会用到各个地方,就导致了繁琐的配置及大量冗余的代码。

Spring是一个生态,包含了SpringMVC、JPA、Security安全框架等,也是SpringBoot的基石,后续在它的基础上扩展的。

1:Spring IOC的理解

总:控制反转,即原来的对象是由使用者控制(手动new创建),有了Spring之后,可以把整个对象交给Spring来帮我们管理。降低代码耦合性。

容器:存储Bean对象,底层使用map结构来存储。singletonObjects存放完整的Bean对象。

(可引出Bean的生命周期,从创建到销毁的过程都是容器管理。生命周期可引出循环依赖问题,进而可引出三级缓存处理及原理。)

容器管理Bean的生命周期,并提供重要的AOP特性,AOP也属于IOC的一部分

BeanFactory:基础容器,提供基础DI功能,延迟加载(使用时才实例化Bean)

 ApplicationContext(更常用):扩展自BeanFactory,支持更多企业级功能(AOP、事件、国际化等)预加载单例Bean(容器启动时实例化所有非懒加载的Bean)。通常说的容器就指这个。

如果直接使用BeanFactory创建Bean,需手动提供IOC加载过程的所需资源,如BeanDifinition。而ApplicationContext则封装了整个IOC的加载过程,更加自动。

1:实例化容器对象

  • ClassPathXmlApplicationContext:基于 XML 配置的应用程序上下文实现。它从类路径中加载 XML 配置文件,这些文件包含了 Spring Bean 的定义和配置信息。开发人员需要在 XML 文件中使用特定的标签(如 <bean>)来定义和配置 Spring Bean。
  • AnnotationConfigApplicationContext:基于注解配置的应用程序上下文实现。它不需要 XML 配置文件,而是使用配置类(例如通过 @Configuration 注解标记的类)来定义和配置 Spring Bean。这种方式将配置信息直接嵌入到 Java 代码中。

通过构造函数初始化容器,触发核心方法 refresh() 创建并启动容器。

二者都会调用核心方法refresh,区别于配置方式不同(xml,注解),前置准备有所不同,但都是读取和扫描文件或配置类,处理一些初始化和环境的设置。之后Spring会统一进行处理。

//Spring容器创建流程
//1: 使用AnnotationConfigApplicationContext(类.class)  ClassPathXmlApplicationContext(管理Bean的XML文件) 创建
AnnotationConfigApplicationContext annotationContext = new AnnotationConfigApplicationContext(Empvo.class);
ClassPathXmlApplicationContext classPathContext = new ClassPathXmlApplicationContext("XXX.xml");

2:DI 依赖注入 

在需要使用Spring容器对象时,可将对应的属性值注入到具体的对象中,依赖注入有三种方式。

1:构造器注入

通过类的构造方法传递依赖对象,将一个配置类的方法添加@Bean注解,在返回的对象中设置一个final对象,通过构造器赋值。

官方推荐使用构造器注入,因为它强制依赖不可变,并且保证完全初始化的对象。符合不可变对象设计原则。好处是有助于保持代码的清晰和可测试性。缺点是不够灵活,不够简洁。

2:Setter注入

通过类的 Setter 方法设置依赖对象,在XML中配置需注入的属性位置及信息,在指定类下创建set方法,并添加 @Autowired 注解。

Setter注入方式,相比构造器更加灵活,依赖对象可以在对象创建后动态设置。但需谨慎使用,仅在依赖可能变化的场景使用。对象可能在未完全初始化时被使用(需注意空指针问题)。

3:字段注入

使用@Autowired(Spring注解) 或 @Resourse(JSR-250规范,Java EE注解),直接通过字段或成员变量注入依赖,无需构造方法或 Setter。

目前使用较多的一种方式,代码简介,适合快速开发。通过反射进行注入,有两种注入方式:

@Autowired 默认按类型注入,可配合 @Qualifier 按名称注入。@Resource 默认按名称注入,找不到名称时按类型注入。

该方式缺点是依赖关系不明确,需避免重复依赖。单元测试不方便。

3:Bean的生命周期

总:总体分为以下几个环节,其中可以通过 BeanPostProcessor 插入扩展逻辑(贯穿全流程)

实例化 → 属性注入 → Aware → 前置处理 → 初始化 → 后置处理 → 使用 → 销毁

Spring容器在启动时会创建好所有单例Bean的实例,并将其存储在单例池中。当客户端请求获取单例Bean时,Spring容器会直接从单例池中获取已经创建好的Bean实例,而不需要每次都重新创建。

1:Bean加载定义

不论通过何种方式(解析XML、Java Config、注解如@ComponentScan等),解析获取的Bean对象,Spring会把bean的配置信息(如BeanCLass、scope等)统一存储在BeanDefinition对象中,该对象是一个接口,封装了所有Bean的定义信息。一个Bean会对应一个BeanDefinition。

Spring维护了一个 beanDefinitionMap,用来存储所有的BeanDefinition,使用ConcurrentHashMap进行存储,用bean的名称作为键,对象作为值。

另外,Spring还维护了一个‌List<String> beanDefinitionNames‌,用于记录所有Bean的名称(保证注册顺序)。

2:BeanFactoryPostProcessor处理

该对象是Spring容器的一个扩展点,允许在Bean实例化之前修改Bean的定义(BeanDefinition),

通常用于在应用程序上下文加载时调整配置,比如修改属性值、添加属性等。

BeanFactoryPostProcessor ‌只会执行一次‌(整个容器生命周期内),且‌作用于所有Bean的定义‌,而非针对单个Bean实例。在容器启动的 refresh() 阶段通过 invokeBeanFactoryPostProcessors() 方法统一触发。

很关键的是 SpringBoot 在该阶段提供了一个实现类,用于配置文件解析和自动配置。

1:解析配置文件占位符

解析配置文件(如 application.properties)中的 ${...} 占位符

<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
    <property name="location" value="classpath:config.properties"/>
</bean>

‌2:动态覆盖 Bean 定义

根据环境变量或条件修改 Bean 的元数据(如类名、作用域、属性值)。

public class DynamicBeanProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        BeanDefinition beanDef = beanFactory.getBeanDefinition("dataSource");
        beanDef.getPropertyValues().add("url", "jdbc:new-url");
    }
}

 3:根据条件注册或移除Bean

根据运行时条件(如系统参数)动态注册或移除 Bean。

public class ConditionalBeanProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        if (!System.getProperty("env").equals("prod")) {
            ((BeanDefinitionRegistry) beanFactory).removeBeanDefinition("prodOnlyBean");
        }
    }
}

其他还有:Profile 激活扩展、配置类增强、自定义注解解析等场景。

3:Bean实例化

总体的实例化可以分为两大步骤,预实例化阶段和单例池检查,在每个阶段有额外的处理。

预实例阶段:

在容器刷新的最后阶段,遍历加载时维护的 beanDefinitionNames 名称集合,确定哪些Bean需要预实例化。仅针对于非抽象、单例、非懒加载的Bean,调用getBean方法进入实例化流程。

多例Bean和懒加载,都是在使用时才创建实例。并且多例Bean每次使用会创建新的实例

  1. 是否懒加载:xml设置:lazy-init="true",注解设置:@Lazy,可加在配置类或@Bean方法。
  2. 是否多例:xml设置:scope="prototype",注解设置:@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)。
public void preInstantiateSingletons() {
    // 获取Bean名称集合并遍历
    List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
    for (String beanName : beanNames) {
        // 合并BeanDefinition(处理父子定义)
        RootBeanDefinition bd = getMergedBeanDefinition(beanName);
        // 仅处理非抽象、单例、非懒加载的Bean
        if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
            getBean(beanName); // 调用getBean()触发实例化
        }
    }
}

单例池检查:

上面判断通过,进入 getBean(beanName) 方法后 ,会进入 doGetBean(beanName) 进一步处理。

会优先检查单例池中是否已存在,如果有则直接使用,没有则调用 createBean、doCreateBean进入创建流程。

查单例池是确保单例Bean全局唯一,直接复用已有实例。这里可以提一下实例化会往三级缓存放,提前暴露对象以及处理AOP代理对象等。

protected <T> T doGetBean(String name, Class<T> requiredType, Object[] args) {
    // 1. 检查单例池
    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null) {
        return (T) sharedInstance;
    }
    // 2. 若未命中,创建Bean
    if (mbd.isSingleton()) {
        sharedInstance = getSingleton(beanName, () -> createBean(beanName, mbd, args));
        return (T) sharedInstance;
    }
}

进入创建流程后,则根据Bean的定义配置判断实例化方式,常用的是构造器实例化方式。其他的几个例如静态或实例工厂方法、FactoryBean自定义都是XML格式的配置方式。

1:构造器实例化

通过反射,使用无参构造器或带参数构造器创建对象,默认使用无参。如果存在多个构造器,需通过@Autowired或<constructor-arg>指定参数。

XML配置为直接指定需创建的类路径,注解方式则是在类上添加注解,例如 @Component,@Configuration + @Bean等。

用的最多的:先根据BeanName获取到BeanDefinition实例,然后通过 getBeanClass() 获取到 Class 类对象,随后使用Class实例的无参构造创建Bean对象实例:getConstractor().newInstance();此时创建的Bean称为纯净Bean(未完成)。

<!-- XML配置 -->
<bean id="userService" class="com.example.UserServiceImpl"/>
2:静态工厂方法

通过类的静态方法返回Bean实例,在XML配置中,指定类路径,以及 factory-method 静态方法返回Bean实例。

<!-- XML配置 -->
<bean id="clientService" class="com.example.ClientServiceFactory" factory-method="createInstance"/>
// 静态工厂类
public class ClientServiceFactory {
    public static ClientService createInstance() {
        return new ClientService();
    }
}
3:实例工厂方法

通过某个已存在的Bean的非静态方法创建对象,在XML中,指定已经存在的Bean,以及其下的非静态方法返回实例对象。

<!-- XML配置 -->
<bean id="serviceFactory" class="com.example.ServiceFactory"/>
<bean id="accountService" factory-bean="serviceFactory" factory-method="createAccountService"/>
// 实例工厂类
public class ServiceFactory {
    public AccountService createAccountService() {
        return new AccountServiceImpl();
    }
}
4:FactoryBean方式

通过实现FactoryBean接口自定义Bean的创建逻辑,在XML中指定自定义的Bean创建类,在重写的getObject方法中返回实例对象。

<bean id="userService" class="com.example.MyFactoryBean"/>

4:属性注入

创建Bean实例后,Spring容器会根据BeanDefinition中的信息来配置Bean,例如设置属性值、注入依赖等。尤其是处理@Autowired,此时会再走到 getBean 方法处,尝试从单例池获取,获取不到就创建。因为之前创建的纯净Bean依赖这个实例。

进入 populateBean() 方法,注入Bean的属性和依赖:

  1. 调用 postProcessProperties() 方法注入依赖,例如@Autowired。
  2. 调用 applyPropertyValues() 方法设置属性值,例如@Value。

这个节点可以引出循环依赖问题,进而可以引出三级缓存处理。

// 简化版源码逻辑
protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) {
    // 1. 处理 @Autowired/@Value 注解
    if (hasInstAwareBpps) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof InstantiationAwareBeanPostProcessor) {
                // 触发注解注入
                ((InstantiationAwareBeanPostProcessor) bp).postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
            }
        }
    }

    // 2. 处理 XML/Java Config 显式属性定义
    applyPropertyValues(beanName, mbd, bw, pvs);
}

5:Aware接口回调

属性注入完成后,调用invokeAwareMethod()方法,实现接口可设置Bean的上下文信息,完成对象的属性设置。常见的例如有:

BeanNameAware  =>  setBeanName()

BeanClassLoaderAware  =>  setBeanClassLoader()

BeanFactoryAware  =>  setBeanFactory()

ApplicationContextAware  =>  setApplicationContextAware()    需注意此时容器未初始化,只有原始的实例对象。这里可以实现策略模式的实现类统一管理。

6:BeanPostProcessor前置处理

初始化前置处理,执行所有实现了BeanPostProcessor下的 postProcessBeforeInitialization() 方法。

@Component
public class LoggingBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        System.out.println("🟡 前置处理 Bean: " + beanName + " | 类型: " + bean.getClass());
        // 可在此修改 Bean 属性或返回代理对象
        return bean;
    }
}

场景运用场景,日志监控(耗时),属性校验(是否非空)等。

以及做其他个性化操作,但是要注意,避免在其中执行耗时操作(影响启动速度)。

7:初始化方法

前置处理完成后,调用初始化方法,并判断是否实现了InitializingBean,如果有则调用afterPropertiesSet() 方法,如果没有则不调用。

AbstractAutowireCapableBeanFactory#initializeBean() {
    // 1️⃣ BeanPostProcessor 前置处理
    applyBeanPostProcessorsBeforeInitialization();

    // 2️⃣ 执行 InitializingBean 逻辑(核心阶段)
    invokeInitMethods() {
        if (bean instanceof InitializingBean) {
            ((InitializingBean) bean).afterPropertiesSet();
        }
        // 3️⃣ 执行自定义 init-method(XML/Java Config 定义)
        invokeCustomInitMethod();
    }

    // 4️⃣ BeanPostProcessor 后置处理
    applyBeanPostProcessorsAfterInitialization();
}

调用初始化有三种方式,且有先后顺序,推荐使用 @PostConstruct 注解方式。

避免在 afterPropertiesSet() 执行数据库连接等阻塞操作,可结合 @Lazy 注解优化启动速度。

@Service
public class PaymentService implements InitializingBean {
    // 组合使用三种初始化方式
    @PostConstruct
    public void validateConfig() {
        System.out.println("🟢 @PostConstruct 优先执行");
    }

    @Override
    public void afterPropertiesSet() {
        System.out.println("🟠 InitializingBean 次之执行");
    }

    @Bean(initMethod = "initMethod")   //xml方式在Bean标签处添加 init-method 指定方法
    public void initMethod() {
        System.out.println("🔵 init-method 最后执行");
    }
}

8:BeanPostProcessor后置处理

初始化后置处理,执行所有实现了BeanPostProcessor下的 postProcessAfterInitialization() 方法。

Spring的AOP动态代理,就是在此处实现的。

@Component
public class LoggingBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        System.out.println("🟡 后置处理 Bean: " + beanName + " | 类型: " + bean.getClass());
        // 可在此生成代理对象,例如AOP动态代理
        return bean;
    }
}

9:Bean就绪

创建后的Bean会放在单例池中:DefaultSingletonBeanRegistry类下的 singletonObjects 集合。存储结构为ConcurrentHashMap,其中键是Bean的名称,值是对应的Bean实例对象。

可通过依赖注入,或直接 getBean() 的方式来进行实例的获取及使用。

10:销毁阶段

容器关闭或手动调用close方法时,执行销毁流程。如果提供了多个销毁方法,按照顺序执行。

注意在执行销毁方法前,会优先执行:DestructionAwareBeanPostProcessor(接口)下的 postProcessBeforeDestruction 方法。

@Component
public class DemoApplicationTests implements DestructionAwareBeanPostProcessor {
    @Override
    public void postProcessBeforeDestruction(Object o, String s) throws BeansException {
        //销毁前置处理,优先执行
    }
}

随后,按照顺序执行提供的销毁方法。

// 实现销毁逻辑的三种方式
public class DemoBean implements DisposableBean {

    @PreDestroy
    public void preDestroy() {
        System.out.println("1注解方式优先执行,@PreDestroy 方法");
    }

    @Override
    public void destroy() {
        System.out.println("2接口实现次之执行,DisposableBean#destroy()");
    }

    //注解或XML配置的自定义方法,最后执行   XML:destroy-method="customDestroy"
    public void xmlDestroy() {
        System.out.println("XML 配置的 destroy-method");
    }
    
    @Bean(destroyMethod = "customDestroy")
    public ExampleBean exampleBean() { 
        return new ExampleBean(); 
    }

}

// 测试销毁流程
public class App {
    public static void main(String[] args) {
        ConfigurableApplicationContext ctx = SpringApplication.run(App.class);
        ctx.close(); // 触发销毁
    }
}

Spring 不触发Prototype作用域(多例)的 Bean,需手动触发多例Bean的销毁。例如 BeanFactory.destroyBean(beanInstance) 。

若某个销毁方法抛出异常,Spring 会记录错误(WARN 级别)但‌继续执行后续销毁逻辑‌。

若未显式指定 destroyMethod,Spring 会自动检测 close() 或 shutdown() 方法(可通过 @Bean(destroyMethod = "") 禁用)。

4:循环依赖及三级缓存

前文提过,在Bean实例化之后,属性注入阶段,可能发生循环依赖问题。

总:循环依赖问题是:A引用B,B引用A,造成一直实例化并注入依赖的过程。

处理方式:三级缓存,提前暴露对象,AOP

三级缓存其实就是不同的三个Map集合,用于存放不同Bean的相关对象。

‌缓存级别存储内容作用
一级缓存‌(singletonObjects)

Map<BeanName, 完全体Bean>

完整的单例Bean

对外提供可用的Bean
二级缓存‌(earlySingletonObjects)

Map<BeanName, 原始对象/代理对象>

提前暴露的未初始化Bean(半成品)

临时存储,解决循环依赖的核心
‌三级缓存‌(singletonFactories)

Map<BeanName, () -> getEarlyBeanReference()>

Bean工厂(ObjectFactory)

早期引用,处理动态代理逻辑

解决思路:在B引用A时候,此时其实A已经实例化了,但未初始化,所以可以设法先拿到A的实例化对象,将B初始化完成之后,再将A的属性补全。

所以,循环依赖的核心处理逻辑就是:将Bean的‌实例化‌与‌初始化‌分离,三级缓存作为中间态解决依赖注入时序问题。

为什么要三级缓存:三级缓存的设计是为了效率和正确性,避免重复创建和保证单例。三级缓存催在的意义是保证在整个容器的运行过程中同名的Bean对象只能有一个。

若Bean需要AOP代理,三级缓存的ObjectFactory会‌提前生成代理对象‌,确保依赖注入的正确性。

Spring 之所以需要三级缓存而不是简单的二级缓存,主要原因在于AOP代理和Bean的早期引用问题。二级缓存虽然可以解决循环依赖的问题,但在涉及到动态代理(AOP)时,直接使用二级缓存不做任何处理会导致我们拿到的Bean是未代理的原始对象。如果二级缓存内存放的都是代理对象,则违反了Bean的生命周期(正常代理对象的生成是在后置处理器)。


所以,针对循环依赖问题,所有创建的Bean都要优先放到三级缓存中,后续向一二级缓存提升。

针对上述问题,调整后的实例化和属性注入流程为:

1:实例化A

A实例化完毕后,但未进行属性注入时,将创建A的ObjectFactory并存入三级缓存。

// 源码位置:AbstractAutowireCapableBeanFactory
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, bean, beanDefinition));

Lambda表达式即为ObjectFactory,该对象只负责生成Bean的早期引用(非BeanFactory)。

注意需优先判断对象是否需要被代理,如果是代理对象,则覆盖原来对象,重新创建。

// 伪代码:AbstractAutoProxyCreator
protected Object getEarlyBeanReference(String beanName, Object bean) {
    //...
    return wrapIfNecessary(bean, beanName);
}

 随后进行三级缓存的操作,注意有其他的后续操作。

// DefaultSingletonBeanRegistry
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    synchronized (this.singletonObjects) {    //加锁,确保三级缓存-二级缓存的原子性
        if (!this.singletonObjects.containsKey(beanName)) {
            this.singletonFactories.put(beanName, singletonFactory); // 存入三级缓存
            this.earlySingletonObjects.remove(beanName);   //清除二级缓存
            this.registeredSingletons.add(beanName);  //记录所有单例Bean的注册状态
        }
    }
}

上述涉及到几个知识点:

1:加锁,为了保证三级缓存到二级缓存的唯一性,避免并发问题。

2:earlySingletonObjects.remove(beanName),清除二级缓存。是为了保证获取的是最新的早期对象。比如AOP代理对象的唯一性、多个线程的并发安全。

3:registeredSingletons.add(beanName),将当前正在创建的 Bean 名称(beanName)添加到一个‌有序集合‌中,记录所有已注册的单例 Bean。使用LinkedHashSet有序存储,为了保证销毁时逆序执行。

// 源码参考:DefaultSingletonBeanRegistry
public void destroySingletons() {
    String[] singletonNames = this.registeredSingletons.toArray(new String);
    for (int i = singletonNames.length - 1; i >= 0; i--) {
        destroySingleton(singletonNames[i]); // 逆序销毁
    }
}

2:初始化B

A依赖于B,此时按照顺序从1、2、3级缓存查找,如果没有则进入B的创建流程(实例化并放入三级缓存中)

之后进行属性注入,发现依赖于A,此时从1、2、3级缓存查找A,发现此时A在三级缓存。

从三级缓存中获取A的早期对象,getSingleton("a")。

// 源码位置:DefaultSingletonBeanRegistry#getSingleton()
public Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);  // ① 查一级缓存
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            singletonObject = this.earlySingletonObjects.get(beanName);  // ② 查二级缓存
            if (singletonObject == null && allowEarlyReference) {
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);  // ③ 查三级缓存
                if (singletonFactory != null) {
                    // 命中三级缓存,触发后续操作...
                }
            }
        }
    }
    return singletonObject;
}

注意在这之后,还会调用 getEarlyBeanReference() 判断是否代理,确保代理对象的一致性。

随后,将 A 的早期引用存入二级缓存,清空三级缓存。后续都从二级缓存获取A的引用。

// 源码位置:DefaultSingletonBeanRegistry#getSingleton()
if (singletonFactory != null) {
    singletonObject = singletonFactory.getObject();  // 生成早期引用
    this.earlySingletonObjects.put(beanName, singletonObject);  // ⑤ 存入二级缓存
    this.singletonFactories.remove(beanName);         // ⑥ 清空三级缓存
}

最后,B 完成属性注入和初始化,并存入一级缓存。添加锁,避免出现后续A获取不到B的情况。

// 源码位置:DefaultSingletonBeanRegistry#addSingleton()
protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        this.singletonObjects.put(beanName, singletonObject);  // ⑦ 写入一级缓存
        this.singletonFactories.remove(beanName);  //清除该Bean三级缓存
        this.earlySingletonObjects.remove(beanName);  //清除该Bean二级缓存
        this.registeredSingletons.add(beanName);  //实例化Bean记录
    }
}

3:初始化A

当 B 通过 addSingleton 完成初始化后,返回到 A 的 populateBean 方法继续执行。

B已初始化成功并存入一级缓存,还需补充A的属性注入及初始化。(注意此时A的引用在二级缓存中,在上述B的属性注入过程中,从三级缓存提升)

之后,A注入B,完成自身初始化。并存入一级缓存,清空二三级缓存。添加锁。

// DefaultSingletonBeanRegistry#getSingleton
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    synchronized (this.singletonObjects) {
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null) {
            // 标记Bean正在创建(防止重复进入)
            beforeSingletonCreation(beanName);
            try {
                // 关键恢复点:B完成初始化后返回此处
                singletonObject = singletonFactory.getObject();
            }
            finally {
                afterSingletonCreation(beanName);
            }
            // 存入一级缓存
            addSingleton(beanName, singletonObject);
        }
        return singletonObject;
    }
}

至此,A和B都完成了初始化,且都存入一级缓存中。后续其他Bean注入AB也会直接查询获取。

补充:构造器注入‌无法解决循环依赖(需使用Setter/字段注入),或添加@Lazy注解延迟加载。

非单例Bean(如prototype)无法解决循环依赖。

注意清空二级、三级缓存,都指的是清空当前Bean的实例,并非整个缓存数据。

缓存操作的代码,获取实例,或缓存创建删除,全部都会加同步锁,避免不一致情况。

5:Spring AOP

AOP是一种强大的编程范式,和OOP一样是一种思想。Spring AOP只是其的一种实现

Spring框架中的AOP称为面向切面编程。在传统的OOP面向对象的编程中,程序关注的是竖向扩展,属性复用。而AOP则是用于横向公共模块的抽取解耦,实现横切关注点的模块化,提升代码的可维护性和复用性。

Spring AOP就是位于Bean生命周期的初始化后置处理阶段,算是其的一个扩展点。

常见场景有:日志切面,权限切面,耗时切面,业务数据切面(分片)等。

1:核心概念

基本术语描述及使用
切面(Aspect)封装横切逻辑的模块,通过注解@Aspect定义在类上,才可使用AOP功能。
连接点(Join Point)

概念术语,指程序执行时的某个具体位置。

说白了就是我们的代码类或方法,理论上所有可能被增强的位置都是连接点。

切点(Pointcut)

实际处理,从上方筛选出需要被增强的特定连接点‌。

使用时可通过表达式(如AspectJ语法)‌,或注解切点指示符方式定义拦截方法。

@Pointcut本身是一个注解,定义切点后可以其他可以调用方法复用。

通知(Advice)

AOP的核心处理,切面在特定连接点执行的操作,共有五种。

使用时可以调用定义好的切点方法,或直接内联表达式。

切点可以被多个通知(Advice)共享。

  • ‌@Before‌:方法执行前
  • ‌@After‌(后置通知):方法执行后(无论是否异常)
  • ‌@AfterReturning‌:方法正常返回后
  • ‌@AfterThrowing‌:方法抛出异常后
  • ‌@Around‌:包裹目标方法,需手动调用ProceedingJoinPoint.proceed()

其他的术语:目标对象即指被代理的原始对象,代理即为Spring底层生成的代理对象,织入则是Spring在运行时将切面应用到目标对象的过程。

2:切点表达式

使用AspectJ语法定义拦截规则,或使用切点指示符匹配注解。

‌execution‌是匹配指定路径下的方法执行,比较常用。

@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}

切点指示符:

‌@within‌:拦截‌被指定注解标注的类中的所有方法‌,粒度粗,适用处理整个类方法的场景。

‌@annotation‌:拦截‌带有指定注解的方法,粒度细,适用于需要针对具体方法做拦截的场景。

这两个指示符在代码中点击不进去,因为它们并不是实际定义的注解,而是类似于语法糖,告诉AOP框架如何匹配目标或类

这两个指示符在我的项目中用到了,搭配自定义注解简化AOP切面流程复用,并且他们还可以一起用,中间添加逻辑关系符进一步指定。

3:实现原理

使用Spring AOP的流程:首先确认自己的业务连接点,需要在哪里切入。确认后需在启动类开启AOP支持,然后创建切面类(需放入容器),设置切点和通知类型即可。

Spring AOP的底层是动态代理实现,动态代理选择优先级是:如果目标类实现了接口,则默认使用JDK动态代理,否则使用CGLib动态代理。可通过注解强制使用CGLIB动态代理。

4:注意事项

Spring AOP仅支持方法级别的连接点。切面不能应用于final类或方法(CGLIB无法代理)。

同类内部方法调用(如A方法调用B方法)不会触发AOP(需通过代理对象调用)。

Spring AOP 和 AspectJ 对比:

总结:两者都是AOP思想的具体实现,提供对目标对象的代理增强处理。不同点:

1:不同框架

Spring AOP依赖于容器,只能代理由Spring容器创建的Bean;AspectJ是另外一个框架,由 Eclipse 基金会维护,可拦截任意对象(如通过new创建的对象)。

2:实现方式不同

Spring AOP基于动态代理实现(运行时增强),会有额外开销;AspectJ是直接修改字节码,性能更高。

3:控制粒度不同

Spring AOP只支持方法级的连接点;AspectJ支持方法、字段、构造器等更细粒度的控制。

4:织入阶段不同

Spring AOP是在运行时织入,将切面应用到目标对象;AspectJ支持‌编译时、编译后、加载时‌三种织入方式,能够处理更复杂的横切逻辑。

5:适用场景不同

Spring AOP与Spring生态无缝集成,适合简单场景;AspectJ功能更强大,但需要额外学习成本。

具体可以根据项目中的业务复杂度、拦截粒度等选择使用,或结合使用。

6:Spring 事务

Spring 事务管理是 Spring 框架中用于保证数据库操作ACID的核心功能。提供了声明式事务和编程式事务两种方式,简化了事务管理代码,提高了开发效率。

优化:事务应尽可能短,避免长时间占用数据库连接。根据业务场景选择具体传播行为。设置回滚方式并确保异常场景下事务能正确回滚。

1:为什么需要事务

1:首先,需要先弄清除代码为什么需要事务?

如果代码不加事务,在同一个方法内插入多条数据,一部分成功,一部分失败,违反了数据库的A原子性(要么全成功要么全失败),后续重新执行会导致数据错误(重复插入)。所以同一个代码事务中如果失败则需要整体回滚。

2:针对上述问题,如何在代码中处理事务相关操作?

传统的事务处理是通过 Connection 数据库连接对象,设置自动提交为false,成功时调用commit,失败时调用rollback,最后还需要关闭资源。

3:总结,需要使用Spring事务的原因

Spring 事务管理不是替代数据库事务‌,而是在应用层对数据库事务的增强和抽象。

1:简化事务代码

手动设置的方式,代码可能会在每个事务操作(方法)中反复出现,导致代码冗余且忘记关闭资源;Spring的声明式事务‌通过 AOP 自动管理事务生命周期,开发者只需关注业务逻辑。编程式事务‌通过回调机制封装事务代码,减少重复。

2:多数据源或分布式事务

如果涉及多个数据库,手动协调多个资源的事务极其复杂;Spring声明式事务对业务代码透明,并且提供了分布式事务,支持多资源事务协调。

3:灵活控制事务传播行为

之前,事务的传播逻辑需要手动通过代码控制;Spring通过注解定义事务传播行为,灵活定义事务的边界,支持按方法定义隔离级别和超时,并将业务异常与事务回滚绑定,实现一致性。

4:支持其他功能扩展

直接操作数据库的方式,事务逻辑与业务代码高度耦合,难以复用或动态调整;Spring通过通过 AOP 代理将事务逻辑与业务代码解耦,并可结合其他Spring功能增强事务行为。

2:事务管理方式

Spring事务管理中,分为编程式事务和声明式事务两种方式,都可用来实现事务控制。

1:编程式事务

相当于传统操作数据库的优化,通过 TransactionTemplate 或 PlatformTransactionManager 手动管理事务,减少重复代码。优点是可以精准控制事务边界,缺点是代码侵入性强,不利于扩展维护。

PlatformTransactionManager 是核心接口,常用实现类有JpaTransactionManager(JPA/Hibernate 事务),JtaTransactionManager(JTA分布式事务)。

2:声明式事务

通过 @Transactional 注解或 XML 配置实现事务管理,优点是代码简洁,不会侵入代码。

缺点是粒度较粗,不能灵活控制边界。默认基于AOP代理实现,会有性能消耗。需注意自调用问题。

底层:对 @Transactional 注解的方法,Spring 通过动态代理(JDK 或 CGLIB)生成代理对象。代理对象在方法执行前后通过 TransactionInterceptor 管理事务(开启、提交、回滚)。

自调用问题:同一类中的方法调用 @Transactional 方法时,事务不生效。

原因:Spring 基于代理实现事务,自调用绕过代理,同一个类中无法获取代理对象。

解决:将方法移到另一个 Bean。或使用 AopContext.currentProxy() 获取当前代理对象。

3:事务注解

@Transactional 注解是使用最多的,简洁有效,并且包含一些关键属性。

propagation‌事务传播行为,提供了枚举类。定义多个事务方法相互调用时的事务边界。
‌isolation

事务隔离级别(如 Isolation.READ_COMMITTED)。

默认使用数据库的,当出现了隔离级别无法控制的问题时,可以手动提高隔离级别。

timeout‌超时时间(秒),默认 -1 表示使用数据库默认。
‌readOnly是否只读事务(优化数据库操作)。
rollbackFor / noRollbackFor‌指定触发回滚的异常类型,开发中经常用到,保证业务的一致性。

使用就是直接在方法或类上添加注解即可,一般指定异常回滚参数,加在类上表示所有方法事务。

@Transactional
public void transferMoney(Account from, Account to, double amount) {
    from.withdraw(amount);
    to.deposit(amount);
}

事务注解失效的常见原因:

  1. 方法非 public。
  2. 自调用问题。
  3.  异常类型非 RuntimeException 且未配置 rollbackFor。
  4. 多线程环境下事务上下文丢失。

4:事务传播行为

事务传播行为定义了多个事务方法相互调用时,事务的边界如何划分。Spring提供了7种传播行为,核心解决问题是如果调用者已经有事务,该如何处理

注意,注解的传播行为是针对于被调用时处理,而不是调用其他方法时控制。

事务传播行为规则及场景
REQUIRED(默认)

如果当前调用方存在事务,则加入该事务;如果不存在事务,则新建一个事务。

适用于大多数业务方法,保证操作在同一个事务中(如订单创建和库存扣减)。

REQUIRES_NEW

无论怎样都新建一个独立事务,如果当前调用方存在事务,则挂起当前事务。

适用于需要独立提交的操作(如日志记录、审计),即使主事务回滚,子事务仍提交。

SUPPORTS

如果当前存在事务,则加入。如果不存在,则以非事务方式执行。

适用于查询方法,需要在事务中执行以保证一致性,但不需要强制事务。

NOT_SUPPORTED

以非事务方式执行,如果当前存在事务,则挂起该事务。

适用于需要避免事务影响的操作(如发送消息、调用外部 API)。

MANDATORY

必须在一个已有的事务中执行,否则抛出 IllegalTransactionStateException。

确保方法必须由事务调用(如关键数据更新)。

NEVER

必须在非事务状态下执行,如果当前存在事务,则抛出异常。

可用来防止事务中的某些操作(如性能监控)。

NESTED

如果当前存在事务,则在嵌套事务(基于保存点)中执行;否则新建事务。

适用于部分操作需要回滚,但整体事务可继续(如订单中部分商品库存不足)。

MANDATORY:作用只是校验,不仅保证自己需在事务内,还确保调用方必须在事务内,可以用来保证多个操作强关联一致。

NESTED:可理解为在调用NESTED事务标注的方法时,记录保存点。发生异常时,外层事务可继续。

5:事务回滚方式

默认是仅在抛出 RuntimeException 或 Error 时回滚,检查型异常(如 IOException)不触发回滚。

生产环境使用时,一般是指定发生异常Exception时回滚。

@Transactional(rollbackFor = Exception.class)

7:Spring常用注解

如果问到Spring注解的问题,回想下Spring大致有四大方面注解:IOC、DI、AOP和事务相关注解,每个注解简单介绍然后等提问。

主要是针对于Spring容器和功能相关的注解,并记录每个注解的功能和区别。AOP和事务注解上面已经整理,相关问题可以找到答案。

1:容器注解

@Component通用组件注解,声明为 Spring 管理的 Bean。
@Service声明Bean,语义化标识业务逻辑组件,无额外功能,在Service类上使用。
‌@Repository声明Bean,语义化标识数据访问组件,有异常统一的特殊功能,在DAO 类上使用。
@ComponentScan配置Spring框架自动扫描并注册组件到容器,默认当前类所在包及子包,可指定配置。
@Import

用于显式导入一个或多个配置类,动态加载 Bean 定义。SpringBoot的自动装配依赖于该注解。

@Configuration声明一个配置类,替代 XML 配置。内部有Component注解,会注入容器。
@Bean声明方法返回一个 Bean,并将对象交由Spring容器管理,只会创建一次。常用于配置类,默认Bean的id是方法名。
@Scope定义 Bean 的作用域(如 singleton、prototype),控制Bean的生命周期。

问题描述:

@Component和@Service、‌@Repository的区别及为什么使用?

都是声明Bean交由Spring管理。可以明确代码分层角色,提高可读性和可维护性。其中@Service无任何特殊处理,@Repository还会将数据库异常转换为统一数据访问异常。

@Component 和 @Bean 的区别?

都可以声明Bean实例,但作用对象和使用场景不同。@Component 作用于类,自动扫描注册 Bean。@Bean 作用于方法,显式声明 Bean,常用于第三方库或复杂配置。

@ComponentScan 自定义扫描路径?

用于配置组件扫描路径,自动发现并注册【添加了容器注解的类】。

默认扫描范围为‌主类所在包及其子包‌。如果自定义了扫描路径,默认路径将会失效,如果需要保留,可以设置多个扫描路径。

2:DI注解

@Autowired按类型自动注入 Bean。
@Qualifier按名称指定注入的 Bean,一般配合 @Autowired 使用。
@Primary标记优先注入的 Bean,可解决多个同类型 Bean 冲突。
@Value动态注入外部配置值,在运行时获取配置信息。

常见问题:

@Autowired和@Resource的区别?

二者都可用来依赖注入Bean,来源和方式不同。@Autowired是Spring注解,默认按类型注入。@Resource是JSR-250规范注解,属于Java社区,默认按名称注入。

二:Spring MVC

‌Spring MVC‌ 是基于 Java 的 Web 开发框架,是Struts2加上Spring的整合。遵循 ‌MVC(Model-View-Controller)‌ 设计模式,用于构建灵活、松耦合的 Web 应用程序。

在之前,web开发使用 @WebServlet 方式,接口交互使用 HttpServletRequest 和 HttpServletResponse,每个接口都需要配置,代码冗余且不好维护,web页面和后端代码高度耦合(获取字段),增加沟通成本。

SpringMVC是Spring生态的一部分,就是Spring的一个子模块,自然拥有Spring的容器和AOP功能,使用注解和视图处理接口的请求和响应,封装了完整的运行流程,优化了接口交互,提高了开发效率。

早期使用SpringMVC时,需要同Spring手动通过XML方式配置视图解析器、静态资源等。Java引入注解后简化了配置流程,但是造成了代码冗余和配置类繁琐。

MVC设计模式:注意不是传统的Java23种设计模式,而是Web页面的框架设计模式。

M-Model 模型,执行具体的业务逻辑代码,执行后返回给控制器。

V-View 视图,前端展示业务,渲染视图后返回给控制器。

C-Controller 控制器,用于接收请求和响应请求,流程的中转站。

1:核心注解

MVC的常用注解包括控制器、接口交互、全局异常相关。这些注解就构成了SpringMVC的使用。

1:控制器

@Controller添加在类上,标记该类为 MVC 控制器,用于Web 层处理 HTTP 请求。
@RestController同样标记该类为 MVC 控制器,遵循RESTful API 开发规范。

二者的区别可能会问到,在生产环境中,使用后者较多。

@Controller 返回视图,需配合视图解析器(默认返回视图到页面),返回数据时,需手动加@ResponseBody。

@RestController 直接返回 JSON/XML(相当于 @Controller + @ResponseBody)。

2:交口交互

@RequestMapping

定义请求路径和方法,映射 HTTP 请求到控制器方法。

可通过 method 属性指定请求类型,例如:method=RequestMethod.GET。

@GetMapping

@PostMapping

简化特定 HTTP 方法的映射,替代 @RequestMapping 的属性设置,精简划分层级,提高代码可读性。
@RequestBody将请求体反序列化为对象,接收 JSON/XML 数据。
@ResponseBody将返回值序列化为响应体,直接返回数据而非默认视图
@RequestParam

从请求参数中获取值,可不加,默认获取同参数名的数据。

一般搭配@RequestBody获取前端页面传递的Json报文转对象,可设置必填和默认值等属性。

@PathVariable

从 URL 路径中获取变量,将其绑定到参数上,常用于RESTful 风格的接口设计。

一般在 get 请求中使用,参数名一致时,可省略name和value,但是不能省略注解

@ModelAttribute绑定请求参数到模型对象,实现表单数据绑定。还可用于初始化模型对象

1:@PathVariable使用

 一般在get请求中使用的原因是,需要在路径后显式拼接参数,页面可以看到,数据不安全。并且get请求较小,可以避免拼接大批量参数。生产中使用较少,除非有参数传递在路径的特殊需求

@GetMapping("/user/{id}")
public String getUserById(@PathVariable Long id) {
    return "UserID: " + id;
}

3:全局处理

Spring提供了一个重要的全局处理的注解@ControllerAdvice,可用于全局异常处理、全局数据绑定和全局数据预处理。

@ControllerAdvice注解内部使用了 @Component 元注解,因此使用时无需额外添加容器注解。

核心原理是AOP思想的实现,当在一个类上定义该注解时,表示需要使用全局的拦截功能。提供拦截规则,然后设置@ExceptionHandler、@InitBinder 或 @ModelAttribute这三个注解以及被其注解的方法来自定义拦截后的处理。

提供了多种拦截规则的定义,默认是拦截所有Controller,可自定义为拦截包、注解等。

1:@ExceptionHandler

单个Controller接口使用时,表示处理 Controller 控制器内的异常,当发生异常时,Spring会调用该注解标注的方法处理异常。

默认处理方法所有异常,可接收一个 Throwable 数组作为参数,传入指定的一个或多个异常进行处理。使用时,可以指定Execption或自定义异常,丰富提示信息,便于排查。

在 @ControllerAdvice 修饰的全局控制器处理类中,添加 @ExceptionHandler 注解修饰的方法,可实现全局异常处理,返回自定义异常或错误页面。

2:@ModelAttribute

添加在参数上时,和RequestParam用处一样,绑定值到参数上,并且可以设置别名。

这个注解的特殊地方时,可添加在方法上,可以用来初始化模型数据,为视图提供初始数据。说白了往model中注入属性,可以供Controller中注有 @RequestMapping 的方法获取使用。

@ModelAttribute("user")    //默认属性名,可以自定义名称
public User getUser() {
    return new User();
}

这样表示,这个方法会在每个请求处理方法执行前被调用,初始化一个User对象并将其添加到模型中‌。在接口中就可以获取使用,直接将其赋值给参数或者手动获取。

@GetMapping("methodTwo")    //直接获取并赋值到参数上
public String methodTwo(@ModelAttribute("user") String globalAttr){
    return globalAttr;
}

@GetMapping("methodThree")
public String methodThree(ModelMap modelMap) {
    return (String) modelMap.get("user");    //手动获取
}

使用时需注意,被@ModelAttribute注解的方法会在每个请求处理方法执行前被调用。因此,在一个控制器映射多个URL时,要谨慎使用,以避免不必要的重复执行‌。

在 @ControllerAdvice 修饰的全局控制器处理类中,添加 @ModelAttribute 注解修饰的方法,可以实现全局预设变量数据,达到默认视图及参数默认值的效果。

3:@InitBinder

可以在Controller中定义一个该注解修饰的方法,该方法会在控制器处理请求之前被调用,用于自定义数据绑定逻辑。

使用:注解中默认处理所有参数,可传入对象名指定处理对象。方法中必须包含一个 WebDataBinder 类型的参数,且方法的返回类型为 void。

@Controller
public class MyController {
    @InitBinder    //@InitBinder("user") 只针对user对象
    public void customizeBinding(WebDataBinder binder) {
        // 指定前缀
        binder.setFieldDefaultPrefix("p.");
        // 自定义绑定逻辑
    }
}

在 @ControllerAdvice 修饰的全局控制器处理类中,添加 @InitBinder 注解修饰的方法,可以实现全局初始数据处理,字段赋值转换等操作。

实际使用场景有对参数进行格式化转换(日期、字符串等),简化代码,避免在每个接口重复处理。以及多个参数包含相同字段时,可以在方法中指定前缀区分。

2:运行流程

面试一定会问到的运行流程,包括目前使用的SpringBoot,SpringCloud都是基于这个原理。

把核心组件记下来,然后在回答时,整体串起来描述。

1:DispatcherServlet

前端控制器,dispatcherServlet是整个流程控制的中心。

作用是接收请求,响应结果,相当于转发器或中央处理器,属于mvc模式中的c。

所有HTTP请求都首先由 DispatcherServlet 处理,由它调用统一调度其他组件执行,降低组件之间的耦合性,提高每个组件的扩展性。

2:HandlerMapping

处理器映射器,负责根据用户请求找到对应的控制器(Controller),注意只是查找,不执行。

SpringMVC提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。

HandlerMapping 将会把请求映射为 HandlerExecutionChain 对象,包含一个Handler处理器对象,和多个 HandlerInterceptor 拦截器对象。


映射的这个操作,用到了策略模式。新增映射策略时,只需实现 HandlerMapping 接口,便于扩展,并且达到了解耦的效果。

HandlerMapping作为策略接口,定义了统一的映射方法。其下实现类提供不同的映射算法。

  1. RequestMappingHandlerMapping:基于注解(如 @RequestMapping)的映射策略,处理控制器方法的动态匹配。
  2. ‌BeanNameUrlHandlerMapping:根据Bean名称与URL路径匹配的简单策略。
  3. ‌SimpleUrlHandlerMapping:通过显式配置URL路径与处理器的映射关系。

DispatcherServlet 作为上下文,负责‌按顺序选择并使用具体的 HandlerMapping 策略‌。

  1. 在初始化时,加载所有 HandlerMapping 实现类的实例。
  2. 处理请求时,遍历这些实例,直到找到第一个能返回 HandlerExecutionChain 的策略。
  3. 最终将请求委托给选中的策略处理,结束循环。
// DispatcherServlet 中简化后的逻辑
for (HandlerMapping hm : handlerMappings) {
    HandlerExecutionChain handler = hm.getHandler(request);
    if (handler != null) {
        return handler;
    }
}

3:HandlerAdapter

处理器适配器,执行具体的业务逻辑,返回 ModelAndView 或数据。

在Spring MVC中,HandlerAdapter 的作用是将不同类型的处理器,统一适配到 DispatcherServlet 可调用的接口‌,随后执行具体的处理器。

用到了适配器模式,新增处理器类型时,只需添加新的 HandlerAdapter 实现类,并在适配器做对应处理即可。无需与具体处理器耦合,提高了框架的灵活性和可维护性。


HandlerAdapter 作为目标接口定义了统一的处理器调用方法(handle)和判断处理器是否支持方法(supports)。

Spring MVC提供了多种 HandlerAdapter 实现类,每种适配一种处理器类型。

  1. RequestMappingHandlerAdapter:适配带有 @RequestMapping 注解的控制器方法(现代Spring MVC的默认方式)。
  2. ‌SimpleControllerHandlerAdapter:适配实现了旧版 Controller 接口的处理器(如 AbstractController)。
  3. ‌HttpRequestHandlerAdapter:适配实现了 HttpRequestHandler 接口的处理器(如静态资源处理)。
  4. ‌SimpleServletHandlerAdapter:适配 Servlet 类型的处理器。

在获取到上一步的处理器映射器之后,Spring会遍历所有 HandlerAdapter,通过 supports() 方法找到支持该处理器的适配器。

最后通过 handle() 方法统一调用处理器,屏蔽具体处理器的差异。

// DispatcherServlet 中的简化逻辑
HandlerAdapter ha = getHandlerAdapter(handler);
ModelAndView mv = ha.handle(request, response, handler);

4:Handler

后端控制器,就是我们编写的具体Controller接口,内部执行真正业务逻辑代码。

调用 handle 方法后,会返回 ModelAndView 对象,包含模型数据、逻辑视图名。

但是这里需要区分两个场景:接口默认返回ModelAndView,但添加@ResponseBody表示返回数据。

底层实现:

进入handle执行方法后,SpringMVC会通过 ‌HandlerMethodReturnValueHandler‌ 组件机制,‌判断是否走视图渲染或消息转换逻辑。

1:模型生成

若方法返回 ModelAndView 或视图名称,其会最终生成 ModelAndView 对象,并分情况交给 DispatcherServlet 进行视图渲染。

这里有一个重要的概念,当接口返回字符串时,SpringMVC会隐式创建一个ModelAndView,将返回的字符串解析为视图名称,并将参数合并到模型中。通过该方式创建的模型对象,和代码显式创建的是一致的。

然后可以引出另一个重要的概念,当返回的字符串以 redirect: 重定向或 forward: 转发开头时,Spring MVC会直接创建视图对象(view实现类)!redirect 会创建 RedirectView 对象,forward会创建 InternalResourceView 对象。并将其放入ModelAndView中的视图部分,同时处理模型数据。

转发:通过服务器端请求转发跳转页面,属于内部资源访问‌。URL不变,共享作用域,模型数据可以自动传递。在服务端内部完成,客户端仅收到一次响应。效率高,适用于大多数页面切换及数据传递。

重定向:触发客户端发起新的HTTP请求‌,URL会变为目标URL并显式,模型数据默认不传递到重定向后的请求(除非通过 RedirectAttributes 显式传递),客户端会发起第二次请求到新URL。适用于防止重复提交。

随后,会在下方代码检查视图是否是重定向或转发视图,如果是,则不设置视图解析标记。只有为普通字符串时,才会设置解析标记供后续解析。

public class ViewNameMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType,
            ModelAndViewContainer mavContainer, NativeWebRequest webRequest) {
        // 将字符串作为视图名称设置到 ModelAndViewContainer
        String viewName = (String) returnValue;
        mavContainer.setViewName(viewName);
        // 标记需要后续处理视图解析
        if (isRedirectViewName(viewName)) {
            mavContainer.setRedirectModelScenario(true);
        }
    }

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        // 支持返回类型为 String 且非 @ResponseBody 注解的方法
        return CharSequence.class.isAssignableFrom(returnType.getParameterType());
    }
}

2:数据转换

内部会检查方法是否有 @ResponseBody 或类是否有 @RestController,如果有,则会触发消息转换逻辑。使用 HttpMessageConverter‌ 将Java对象转换为HTTP响应体的内容(如JSON、XML),并写入响应。

将 ModelAndViewContainer 的属性 requestHandled 标记为true,告知 DispatcherServlet 无需后续视图渲染。

所以,这里可以看出来,其实最终都会返回ModelAndView,但是返回数据的场景下,全局控制器会根据标记直接跳过后续流程,结束请求处理。

5:ViewResolver

返回 ModelAndView‌ 后,流程进入视图解析和渲染阶段,依赖 ViewResolver 和 View 实现。

如果是注解标注的返回数据,则不再调用 ViewResolver 和 View.render(),直接结束请求处理。

1:视图解析

视图解析的目的就是得到一个View视图,已经有视图或数据的,就会根据标记跳过该步骤,只有返回字符串且不为指定前缀时,才需要解析获取视图。

通过 ViewResolver (视图解析器)对象将逻辑视图名称(如 "index")映射为具体的 View 实现类(如 JstlView、ThymeleafView),SpringMVC框架提供了很多的View视图类型。

根据我们的视图解析器配置,拼接前缀后缀,组成完整路径,例如:

根据上图配置,最终路径会解析为:/WEB-INF/views/index.jsp


2:视图渲染

这一步是对View进行渲染将处理结果,通过 DispatcherServlet 返回响应给用户。

除了包含对字符串解析得到的 View 视图渲染,还包含转发或重定向的 View 实现类视图,只是调用方法不一样。

解析得到的 View 视图:DispatcherServlet 会调用 View.render() 方法,将模型数据(Model)填充到视图模板中。

RedirectView 重定向视图,触发 response.sendRedirect()。

InternalResourceView 转发视图,触发 RequestDispatcher.forward()。

对于重定向视图,模型数据也会被保留,只是默认无法传递,只有两种场景下会传递数据。

路径参数‌:通过 RedirectAttributes.addAttribute() 添加,拼接到URL。

‌Flash属性‌:通过 RedirectAttributes.addFlashAttribute() 添加,临时存储于Session。

转发视图的模型数据‌完全保留‌,所有属性通过请求作用域传递。

6:完整流程

梳理完整运行流程,细节可抛出锚点,全局控制器在讲解时描述,对象都会返回给它统一调度。

1:用户发起请求,前端控制器收到请求后,通过映射器的策略模式,将请求转换为包含处理器映射器的对象。

2:得到映射器后,通过处理器适配器的装饰器模式,获取到支持该处理器的适配器,调用统一执行方法,执行Handler后端控制器。

3:执行具体的业务逻辑代码,最终根据是否含有@ResponseBody注解区分视图和数据响应,隐式创建 ModelAndValue 模型对象并设置视图和模型数据。

4:如果是视图响应,进入视图解析渲染过程。这一步会根据是否重定向或转发,跳过视图解析操作,随后所有View进行视图渲染,处理模型数据返回结果。如果是数据响应,则这两步都跳过,直接返回响应数据。

5:解析渲染后的视图通过 DispatcherServlet 返回响应给用户。

3:拦截器

HandlerInterceptor‌ 是 ‌SpringMVC 框架原生提供‌的接口,用于拦截 HTTP 请求的处理流程。它允许你在请求到达控制器(Controller)‌之前‌、‌之后‌,以及‌整个请求完成之后‌插入自定义逻辑(如权限校验、日志记录等)。

底层是基于 ‌Servlet 规范‌ 的拦截机制,由SpringMVC管理,拦截逻辑嵌入到 DispatcherServlet 的处理流程中。

接收到请求获取处理器映射器时,会生成一个 HandlerExecutionChain 对象,该对象包含:

  1. Handler:具体处理器对象。
  2. Interceptors:拦截器列表,所有注册的拦截器实现类。

随后,DispatcherServlet 直接遍历 HandlerExecutionChain 中的拦截器,依次调用它们的 preHandle、postHandle 和 afterCompletion 方法。

属于是责任链的调用,正常代码流转。不涉及代理或字节码

1:定义拦截器

实现 HandlerInterceptor 接口‌,定义具体拦截逻辑。

定义好拦截器之后,还需要将拦截器添加到 Spring MVC 的配置中。有XML配置和注解两种方式。

仅拦截SpringMVC处理的请求,经常会和 Filter 过滤器做比较,下面描述。

2:定义过滤器

Filter 是 ‌Java Servlet 规范‌的核心组件,不依赖 Spring 框架,属于 Servlet 容器的原生功能。

过滤器用于在 HTTP 请求到达 Servlet(如 Spring MVC 的 DispatcherServlet)‌之前‌执行预处理,或响应返回客户端‌之后‌执行后处理逻辑。

生命周期‌由 Servlet 容器(如 Tomcat)管理,随 Web 应用启动而初始化,销毁时调用 destroy()。执行顺序‌是通过 web.xml 或注解定义的顺序依次执行,遵循“先进后出”的链式调用。

Filter比较好的一点是拦截面较广,兼容新老代码,可用来处理跨域问题。

跨域处理还可使用 Spring 的 @CrossOrigin 或专用 Filter,但不要同时使用两者。

创建过滤器有三种方式,分别为XML、基于 Servlet 的注解、以及基于 Spring 中配置类方式。

上面是规范的 SpringBoot 的过滤器显式注册,但直接使用 @Component 注解并实现Filter接口也可以实现过滤器功能,原因是因为SpringBoot的自动装配机制。

启动时扫描,如果类实现了 Filter 接口且标注了 @Component,SpringBoot 会通过 FilterRegistrationBean ‌自动注册该 Filter‌。

一般更推荐使用显式注册,因为自动注册的默认为/*拦截所有,并且顺序不可控,无法配置初始化参数,不利于扩展维护。

3:拦截器 VS 过滤器

总:二者都可以对请求进行拦截处理,但是所属和粒度有区别。

1:功能所属

拦截器属于SpringMVC管理,依赖Spring容器支持;过滤器不依赖Spring,是Servlet 容器原生支持的功能。

2:作用范围

拦截器只会拦截SpringMVC处理的请求(Controller层面);过滤器作用范围大,可控制所有请求(Servlet 容器层面)。

3:执行时机

拦截器作用在 DispatcherServlet 内部执行(控制器前后),不会拦截原生Servlet接口;过滤器作用在 DispatcherServlet 之前/之后执行(容器初始关闭),提供了每次请求的过滤方法。

4:访问Bean

拦截器可以直接注入Bean进行使用;过滤器无法直接注入,需通过原始方式手动获取Bean(例如WebApplicationContext)。

5:读取参数

拦截器更加灵活,可访问控制器对象、ModelAndView视图信息等;过滤器只能操作 Servlet 请求和响应对象。


常见场景:

拦截器:权限控制、Token校验、参数记录、详细请求参数设置等。

过滤器:兼容历史代码、跨域处理、全局日志记录、字符编码统一、过滤请求参数等。

开发中,可将拦截器与 Servlet 原生的 Filter 结合使用,能够实现更全面的请求控制逻辑,各司其职。

三:SpringBoot

Spring Boot 是一个基于 Spring 框架的开源 Java 开发框架,也属于Spring生态的一部分。并不是要取代Spring,而是简化 Spring 应用的初始搭建和开发流程‌

通过‌约定大于配置‌的理念和一系列自动化工具,显著减少了传统 Spring 开发中的复杂配置,使开发者能够快速构建独立、生产级的应用程序。

核心特性包括:自动配置、Starter依赖、嵌入式服务器、多环境配置、安全控制五大特性。

换用 SpringBoot 之后,直接创建项目,引入依赖添加配置,编写主启动类就可以直接运行!

1:核心注解

对于原始的Spring、SpringMVC,注解很繁琐且冗余,所以SpringBoot在其基础上做了二次封装,定义了一些新的组合注解来实现一些Spring注解的组合,从而极大地简化了Spring框架本身的繁琐配置,实现快速的集成和开发。

Spring的很多注解都可以作为元注解,允许定义在其他注解上,其自身也有很多组合注解。这也是SpringBoot实现的基础。

1:条件注解

@Conditional,Spring提供的基于条件控制的注解,条件满足时,被注解标注的组件才会注册到容器中,可以标注在类或方法上。

可以用来控制 Bean 的创建行为,这一点是 SpringBoot 实现自动配置的核心基础能力。

Spring Boot中以@Conditional为元注解又重新定义了一组针对不同场景的组合条件注解,这里只列举一些常用的,除了这些还有Web、资源等条件的判断注解。

@ConditionalOnBean当容器中有指定Bean的条件下进行实例化,保证Bean的创建顺序。
@ConditionalOnMissingBean当容器里没有指定Bean的条件下进行实例化,避免重复配置。
@ConditionalOnClass当classpath类路径下有指定类的条件下进行实例化。
@ConditionalOnMissingClass当类路径下没有指定类的条件下进行实例化。

SpringBoot的核心功能就是基于@Conditional为元注解的组合注解实现的,在SpringBoot的源码中,可以经常看到条件注解的使用,这就是 SpringBoot 实现高度自动化配置的原因。

2:启动注解

核心的启动类注解,其实也就是组合注解,后续的启动流程描述就会从这里作为入口。

@SpringBootApplication

启动类注解,作为Spring Boot 入口类。

组合注解:@Configuration + @EnableAutoConfiguration + @ComponentScan。

@EnableAutoConfiguration核心注解,开启自动配置功能,自动加载 Spring 和第三方库的配置。
@ConfigurationProperties

将外部配置绑定到 Java Bean,将配置文件中的属性批量绑定到Java对象,不用单个@Value。

这个正式项目中用到了,可搭配自定义配置,在启动时绑定值到全局Java对象类中。

@EnableConfigurationProperties

启用配置绑定功能,可添加在启动类或配置类上。

另外,在配置类上单独添加 @Component 注入容器也可生效。

@SpringBootTest

标记 Spring Boot 集成测试类。

访问外部依赖时,需注入容器并添加@RunWith(SpringRunner.class)。

2:启动流程

Spring Boot 的启动流程包括初始化、环境准备、上下文创建、自动配置加载等步骤。

自动配置只是启动流程中的一个关键部分。启动流程更广泛,涵盖整体过程,自动配置则专注于条件化配置Bean的机制。

其实这里内部描述的是 Spring 的启动流程,SpringBoot只是整合并且加了自动配置、容器等功能。

核心问题:当我们创建一个 SpringBoot 项目,通过主启动类启动,启动流程是什么?

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

首先,会进入 SpringApplication 的 run 方法,分为两个操作:实例化对象和调用实际运行方法

1:推断应用类型

进入实例化构造方法,核心步骤之一是推断应用类型。

根据类路径是否存在指定核心类,判断是 Servlet 应用(如 Tomcat)、Reactive 应用(如 Netty)还是普通 Java 应用。

原理是通过依赖自动推导,并做不同的处理:

  1. 引入 spring-boot-starter-web,自动推断为 SERVLET。需启动嵌入式 Servlet 容器(如 Tomcat)并注册 DispatcherServlet。
  2. 引入 spring-boot-starter-webflux,自动推断为 REACTIVE。需初始化 Netty 服务器和响应式编程模型。
  3. 无 Web 相关依赖,推断为 NONE。应用不加载任何 Web 相关配置。

SpringBoot默认选择 Servlet 类型,同时指定上述依赖时,也会优先选择 Servlet,但可通过配置显式指定为 REACTIVE(配置文件指定,或在启动类方法中设置 setWebApplicationType)。

为什么需要推断应用类型?

1:无需用户手动配置,指定应用容器类型等,符合约定大于配置思想。

2:避免冲突,确保不加载冲突的配置,配置多个时,按默认或指定配置选择一个。

3:性能优化,精准加载‌仅适用于当前场景的配置‌,避免冗余资源消耗。

 我这里本地是2.3的版本,比较低一点,后续2.7的代码做了优化,可读性更高一点,可以对比下。

 

2:加载核心扩展类

推断应用类型之后,返回到初始化对象的构造方法,紧接着读取 META-INF/spring.factories 中定义的两个核心组件。

这里只是从工厂中尽早获取组件,跟自动装配没有关系,因为SpringBoot是使用工厂统一管理。

尽早加载这两个组件,是为了允许开发者在 Spring Boot 启动的‌最早阶段‌介入,自定义上下文或监听关键事件,避免部分事件在启动流程的极早期触发。

1:ApplicationContextInitializer

上下文初始化器:在 ApplicationContext ‌创建完成后但未刷新(refresh)前‌,对上下文进行‌定制化初始化‌(如添加属性源、注册 Bean 等)。

使用:实现接口并重写方法,并在META-INF/spring.factories中声明。其源码会循环执行所有初始化方法。

org.springframework.context.ApplicationContextInitializer=com.example.CustomContextInitializer

2:ApplicationListener

应用事件监听器:核心作用是监听 Spring Boot 的‌生命周期事件‌(如启动、刷新、失败等),并在事件发生时执行处理逻辑,提供了多种事件类型(如应用启动前、应用就绪等)。

使用方式同样为实现接口,并在spring.factories中声明。

ApplicationListener用到了观察者模式,通过事件驱动机制实现模块间解耦,‌监听器‌只需关注事件内容,无需知道事件如何触发。提高了可维护性和扩展性。

其实这个监听器类似于工作流的监听,覆盖整个启动环节,监听调度。可以通过实现某个事件接口,重写方法添加个性化处理。

3:触发启动事件

进入运行阶段,调用 run 方法这是 SpringBoot 的核心方法,后续的所有操作都在这个方法中。

首先会调用 getRunListeners 方法,触发启动事件,且返回一个监听器汇总对象。

内部有一个从容器中获取的监听器集合。然后使用事件监听器发布状态(观察者思想)。

4:准备环境

随后调用 prepareEnvironment() 方法,准备运行环境,这是启动的核心步骤之一,这一阶段负责加载和整合所有配置信息(如命令行参数、配置文件、系统变量等),为后续创建 ApplicationContext 和自动配置提供基础数据支持。

1:加载配置源‌

包括命令行参数、application.properties/application.yml、系统变量等。

2:激活配置文件

激活指定的 Profiles(通过 spring.profiles.active 配置),一般表示不同环境的配置文件。

3:发布事件

激活配置之后发布事件,监听器会监听此事件并加载所有配置文件,越靠前的配置源优先级越高‌。

优先级顺序:命令行参数 → 系统变量 → 指定配置文件 → 默认配置文件 → 默认属性。

针对于配置文件的优先级,由两个维度决定:文件路径‌和‌文件格式‌。

1:‌不同路径下

路径优先级高于文件格式优先级(项目根目录大于类根目录),随后再根据文件格式判断。

2:相同路径下

不同格式的配置优先级为:指定前缀配置文件 > properties > .yml > .yaml‌。

生产中,避免混合使用 .properties 和 .yml,减少优先级冲突。‌可利用 Profile 隔离不同环境的配置。

5:创建上下文

根据应用类型创建不同的上下文,实现原理是根据初始化推断的应用类型,反射获取处理类。

6:准备上下文

该阶段也称为准备阶段、配置类处理阶段。负责完成上下文的初始配置、注册关键 Bean 定义,并触发扩展点。

根据前面创建的具体的上下文处理对象,调用 prepareContext()。

准备阶段:仅收集和注册 Bean 定义,将其放入 BeanDefinitionMap,不触发任何实例化逻辑。

原理是:Bean 的实例化逻辑被封装在 BeanFactory 中,只有在调用 getBean() 时才会触发实例化。在准备阶段,Spring 仅操作 BeanDefinitionRegistry,不主动调用 getBean()。

准备阶段和刷新阶段都会处理注解,这是Spring设计上的一种分层处理机制。

记住该阶段只处理显式注册的类和初步扫描,例如@Configuration、@Bean、@Component等。生成基础的Bean定义,其他注解不会被处理。

首尾的这五个方法为关键步骤,这一块还涉及Bean的处理,是核心代码逻辑。

1:环境变量

将之前准备的Environment对象,绑定到上一步创建的上下文对象中。

2:后置处理

对上下文进行后置处理,例如设置 Bean 名称生成器、资源加载器、类加载器等。Bean名称生成器用于生成 @Configuration 类中 @Bean 方法的名称。

3:自定义和监听

使用之前提前注册的上下文初始化器,执行自定义上下文初始化逻辑,可以允许开发者手动定义Bean。

通知监听器上下文准备状态,发布初始化创建完成的事件,此时尚未加载Bean定义。

4:load 方法

核心的方法,解析注解并生成Bean定义。注意这里名词是只Bean定义,不实例化!

  1. 将主类解析为 @Configuration 类,并注册其 Bean 定义,将其作为一个入口。
  2. 先执行@ComponentScan,扫描用户定义的Bean(如@Component、@Service等),将其放入 BeanDefinitionMap 中。
  3. 处理@EnableAutoConfiguration 中的 @Import 注解,将其直接引用的配置类注册为 Bean 定义

 这里最关键的是只定义 @Import 引入的类,例如将引入的 MyConfig.class 注册为Bean定义。

1:我们在平时创建项目,会习惯把主启动类放在根目录,因为其默认扫描主类的包及子包,就会把我们的添加容器注解的类扫描注入,无需配置即可简单生效。

2:注解的顺序执行,为了确保用户自定义的 Bean 优先于自动配置的Bean被注册,使得自动配置可以基于用户已有的Bean进行条件化调整(如覆盖默认配置)。

5:监听事件

Bean定义加载完成后,执行 listeners.contextLoaded(context),通知监听器上下文已加载完成,但尚未刷新(refresh)。

该阶段对应的事件为 ApplicationPreparedEvent,可以实现该接口,并重写其方法。

实现在 Bean 定义加载完成后、上下文刷新前执行逻辑(如修改 Bean 定义)。

7:刷新上下文

该阶段也称为刷新阶段,SpringBoot的最核心处理节点,通过 refreshContext(context) 触发,最终会执行到 refresh 核心方法。

刷新环节是 Spring 中 Bean 的生命周期最核心的处理。

这一阶段负责初始化并配置应用上下文,加载 Bean 定义,执行自动配置,最终启动内嵌服务器。​​​​​

刷新阶段也会解析注解,目的是扫描更多组件并注册Bean定义。

1:这是Spring的设计理念,并不会重复解析,而是职责分离,可简单理解为处理不同注解。

2:通过扩展机制全面处理配置类、自动配置和条件逻辑注解,例如@ComponentScan、@Import 等,并将其注册Bean定义。

3:会先执行@ComponentScan扫描包,然后才处理自动配置。如果是自定义自动配置类,需避免组件扫描冲突,不在@ComponentScan扫描路径下,条件注解才可生效。

1:获取BeanFactory

在进入方法时,会执行初始化上下文,校验环境变量等。

随后获取 BeanFactory 对象,并对其进行准备,供后续使用,提供了供子类扩展的后处理方法。

2:Bean工厂后置处理

最关键的处理,调用 invokeBeanFactoryPostProcessors 方法,执行所有 BeanFactoryPostProcessor 处理。

这块是Spring的逻辑,该节点是Bean生命周期的一部分,允许在实例化之前修改或扩展 Bean 定义(自定义)。


1:调用方法时,需要传递 BeanFactory 参数。

内部从Bean工厂中获取所有 BeanFactoryPostProcessor 的Bean名称‌,并通过getBean实例化这些处理器,最后汇总到集合中。包括系统或依赖内部定义的,还有我们自定义扩展的。

2:循环后置处理器,由Bean工厂统一执行每个处理器的方法,优先执行 Registry 级别的方法。

// PostProcessorRegistrationDelegate.java
public static void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactorybeanFactory, List<BeanFactoryPostProcessor> postProcessors) {
    
    // 处理 BeanDefinitionRegistryPostProcessor(优先级最高)
    for (BeanFactoryPostProcessor postProcessor : postProcessors) {
        if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) {
            ((BeanDefinitionRegistryPostProcessor) postProcessor).postProcessBeanDefinitionRegistry(registry);
        }
    }

    // 处理普通 BeanFactoryPostProcessor
    for (BeanFactoryPostProcessor postProcessor : postProcessors) {
        postProcessor.postProcessBeanFactory(beanFactory);
    }
}
3:自动配置

上述的 BeanFactoryPostProcessor 处理是 Spring 提供的,SpringBoot的自动配置就依赖于此。

注意 SpringBoot 就只是新增了一个实现类:ConfigurationClassPostProcessor,内部实现了解析配置类、处理动态 @Import、自动加载配置类、注册添加 Bean 定义。

每一个Bean工厂后置处理器,在容器生命周期中只会执行一次,且‌作用于所有Bean的定义‌。

排除特定自动配置类:@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

流程:

1:@SpringBootApplication 组合了 @EnableAutoConfiguration,其中 @Import(AutoConfigurationImportSelector.class) 引入自动配置逻辑。

2:SpringBoot内部维护了一个处理器实现 ConfigurationClassPostProcessor,从 @SpringBootApplication 标注的主类开始解析‌,解析所有 ‌@Configuration 配置类。

3:当发现主类上的 @EnableAutoConfiguration 注解后,进而解析其 @Import(AutoConfigurationImportSelector.class)。

4:然后通过 AutoConfigurationImportSelector.selectImports() 加载所有 META-INF/spring.factories 中的自动配置类‌。

5:解析完成后,通过 @ConditionalOnClass 等条件注解,过滤无效或按指定规则配置对应类,将其注册进容器中,在后续阶段实例化。

// ConfigurationClassPostProcessor.java	实现类
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    // 解析配置类,处理 @Import、@ComponentScan 等
    processConfigBeanDefinitions(registry);
}

// ConfigurationClassParser.java   解析配置类,处理 @Import
protected void processImports(ConfigurationClass configClass, SourceClass currentSourceClass) {
    // 处理 AutoConfigurationImportSelector
    if (selector instanceof AutoConfigurationImportSelector) {
        // 加载自动配置类
        String[] imports = selector.selectImports(currentSourceClass.getMetadata());
        // 注册自动配置类的 Bean 定义
        for (String importClassName : imports) {
            this.importStack.registerImport(importClassName);
        }
    }
}
4:BeanPostProcessor

在Spring的刷新阶段,调用 registerBeanPostProcessors 注册 BeanPostProcessor。

BeanPostProcessor 是一个接口,提供了两个方法,供实现类重写处理。作用是在初始化前后对Bean进行扩展,例如AOP。

在类内部,会调用 getBean() 方法创建 BeanPostProcessor 实例,此时会触发它们的依赖注入和初始化。

但是在这个阶段所有的Bean还是定义状态(用不上),为什么要提前实例初始化?

原因:其本身也是一个Bean,需要自己先被实例化为 Bean,才能对其他 Bean 生效。所以提前实例化。

最主要是实例化流程依赖于它(如依赖注入),需确保后续 Bean 的创建过程可以应用这些处理器。

BeanPostProcessor 针对于每个Bean实例,每个Bean初始化前后都会执行所有处理器。

5:初始化容器

嵌入式服务器的初始化发生在刷新阶段的 ‌onRefresh()‌ 方法中,是一个空方法,由具体子类实现。

实现原理:Spring Boot 根据类路径中的依赖自动配置对应的工厂类,然后会调用核心方法 factory.getWebServer()。

  1. 根据上述步骤设置的配置参数初始化服务器。
  2. 注册 Servlet 和 Filter‌。
  3. 启动服务器‌,调用服务器的 start() 方法,开始监听端口。

onRefresh 在设置环境和加载配置之后执行,是为了确保参数已加载,可以安全读取 server.port 等配置。

嵌入式服务器的配置参数,读取优先级为:命令行参数 => 应用配置文件 => 默认值。

选择在 Bean 初始化‌之前执行的原因是:

1:服务器需要在单例 Bean 初始化‌之前‌启动,以便在应用完全就绪时,服务器已可处理请求。

2:若服务器启动失败(如端口被占用),应用会在初始化阶段快速失败,避免后续无效操作。

6:创建Bean对象

在处理其他初始化和注册监听之后,会调用 finishBeanFactoryInitialization 方法,创建所有单例Bean对象。

这里就到达了之前Spring描述的 Bean 的整个创建过程了,通过遍历BeanName集合,然后判断,最后调用 getBean 进入Bean的创建过程。

7:完成刷新

所有Bean创建完成之后,会调用完成刷新方法 finishRefresh()。

内部会发布 ContextRefreshedEvent 事件,标记容器初始化完成,通知监听器。

8:调用 Runner 接口

刷新完成后,调用 callRunners 方法,用于在应用启动后触发自定义初始化逻辑。此时所有Bean和上下文都已准备就绪。

Runner 接口提供了两个组件供自定义实现,ApplicationRunner 和 CommandLineRunner。两个组件接收参数不一致,App接收 ApplicationArguments 对象(封装了命令行参数),Com则是接收原始的 args 数组。

流程:

1:从应用上下文中获取所有实现这两个接口的Bean,分开各自整理,但最终合并为一个统一列表,按 Order 排序(@Order或实现Ordered 接口)。

2:优先执行所有 ApplicationRunner‌,再执行 CommandLineRunner。同一类型的Runner按Order值升序排列(值越小优先级越高)。

3:若某个Runner抛出异常,会中断后续Runner的执行,并导致应用启动失败。


注意事项:

同一个Bean不应该同时实现两种Runner接口,否则会重复执行(各自整理)。

需避免长时间阻塞,长时间运行的逻辑可考虑异步处理(如结合 @Async),以免拖慢启动速度。


方法执行完之后,返回到启动run方法中,最后通过监听器发布启动完成事件,项目成功启动。

3:Starter 依赖

‌Starter‌ 是 Spring Boot 的核心特性之一,用于简化项目的依赖管理和自动配置。

早期使用 Spring 和 SpringMVC 时,需要手动添加所需Jar包,很麻烦而且效率不高,还容易出问题。

而 Starter 将某功能相关的依赖打包,开发者无需手动添加多个依赖或编写冗长配置,即可快速集成特定功能,统一管理便于后续扩展维护。

1:使用规范

我们一般会称其为项目管理工具,就是统一管理Jar包和工具。

支持两种格式 Maven/Gradle,创建简单 SpringBoot 项目时,默认为 Maven 格式,即 pom.xml 文件。如果是Gradle,则需手动创建 build.gradle 文件。

1:Maven 使用

Maven的生态比较好,用的比较多,有一个Maven的查询网站,可以很容易的找到所需依赖及版本。

使用:添加所需依赖的 Gav(Maven坐标),即可简单声明依赖。刷新Maven将其引入到项目中。

排除:在Maven中可使用 <exclusions> <exclusion> 指定gav坐标,排除指定依赖。

‌官方 Starter 命名规范‌:spring-boot-starter-{功能}

‌第三方 Starter 命名规范‌:{框架}-spring-boot-starter

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>2.7.18</version>
</dependency>

微服务架构中,支持多项目构建管理子类。以及定义规范和版本,供子类使用。

 <modules>
    <module>cloud-common-business</module>
</modules>

<!-- 统一管理jar包版本 -->
<properties>
    <mysql.version>8.0.23</mysql.version>
</properties>

<!-- 子模块继承之后,不声明版本号使用父类的,声明则使用自己的 -->
<dependencyManagement>
    <dependencies>
        <!--springboot依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.3.10.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

子类中只需要指定父类,生命依赖即可使用,默认用父类规定的版本号。

<parent>
    <artifactId>springcloud_alibaba</artifactId>
    <groupId>com.aaa</groupId>
    <version>1.0-SNAPSHOT</version>
</parent>
2:Gradle 使用

Gradle在 Android 语言开发中普遍使用,大型项目和微服务架构也选择。我的公司就用的这个。

‌Gradle‌ 是一款基于 ‌Groovy/Kotlin DSL‌ 的现代构建工具,结合了 Maven 的约定优于配置,提供了更高效、动态、兼容的特性。

核心特性:支持多语言开发‌(Java、Kotlin、Android、C++ 等),兼容Maven生态,以及增量构建等启动时的优化。


优点就是启动速度优化、动态灵活、可扩展性强等。

  1. ‌增量构建‌:仅处理变化的代码和资源。
  2. ‌构建缓存‌:重用历史构建结果(如已编译的类)。
  3. 支持动态依赖版本和条件化引入,例如指定最小版本3.+

缺点是生态不如Maven,Java中用的比较少,有学习成本,问题不好排查等。

3:Maven 和Gradle 的区别

1:配置文件

Maven使用 XML 格式的pom文件,SpringBoot默认;Gradle需手动创建build.gradle文件,基于Groovy/Kotlin DSL‌。

2:依赖管理

Maven是固定声明式添加依赖,版本号需精确指定或默认;Gradle可动态控制依赖,指定最小版本号控制等,更加灵活。

3:构建速度

Maven每次都会重新构建所有,速度较慢;Gradle有增量构建和缓存机制,仅重新编译改动部分,加快构建速度,会减少JVM启动消耗

4:场景不同

Maven适用于传统 Java 项目、简单构建需求,生态好,没有大的学习成本可快速使用;Gradle适用于大型项目、负责构建流程、依赖拖慢启动速度等场景,学习成本较高。

2:自定义 Starter

自定义实现自动配置类,首先就需要用到项目管理工具,使用工具的打包功能。

1:新建项目

新建Maven或Gradle项目,因为需要改动 spring.factories 文件。

添加 spring-boot-autoconfigure 依赖,因为需要使用注入容器、条件属性等注解。

2:创建自动配置类

 注入容器,并添加条件注解或代码逻辑。

@Configuration
@ConditionalOnClass(MyService.class)
public class MyAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public MyService myService() {
        return new DefaultMyService();
    }
}

3:注册配置类

在新建的这个项目中,在 src/main/resources/META-INF/spring.factories 文件中,添加 EnableAutoConfiguration 并指定我们自动配置类的全路径

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.MyAutoConfiguration

4:使用

Spring Boot 在启动时扫描所有 jar 包中的 META-INF/spring.factories 文件。

加载文件中 org.springframework.boot.autoconfigure.EnableAutoConfiguration 键对应的配置类。

根据条件注解判断是否应用配置,最后将所有Bean注册,包含我们的自动配置类。

4:嵌入式服务器

SpringBoot 支持Tomcat(默认)、Jetty、Undertow。默认内置Tomcat服务器,默认使用Servlet应用类型,可以根据业务进行更换服务器。

实现方式就是排除默认的服务器依赖,然后引入其他容器的 Starter(依赖)。

5:多环境和安全控制

多环境就很简单,指定不同的配置文件,然后每个环境激活不同端口等配置文件。

安全控制就是提供了健康检查、指标监控等配置。

四:MyBatis

MyBatis是一个开源的,半自动的ORM持久层框架,通过 XML 或注解配置 SQL 映射,将 Java 对象与数据库操作解耦。支持普通sql,关联查询,嵌套查询等。

它的核心思想是让开发者通过更简单的方式操作数据库,同时保留对 SQL 语句的完全控制权,避免了传统 JDBC 代码的复杂性。

1:核心架构

Mybatis提供的核心架构组件,熟悉各个组件才能明白其运行流程。

‌SqlSessionFactory通过 SqlSessionFactoryBuilder 解析配置文件生成的,用于创建 SqlSession。
‌SqlSession‌表示一次数据库会话,通过 Executor 执行 SQL,线程不安全。
Mapper(接口)通过动态代理生成实现类,将接口方法与映射文件中的 SQL 绑定。
StatementHandler‌处理 SQL 预编译、参数设置和结果集映射。
ExecutorSQL 执行器,分为 SimpleExecutor(默认)、ReuseExecutor(重用预处理语句)、BatchExecutor(批处理)。

在项目资源目录下,创建XML配置文件,并添加注解 @MapperScan 扫描XML文件。

这些组件在原生MyBatis中需要手动编写使用的,与Spring集成之后,不需要再手动调用。

2:运行流程

Mybatis的使用规范是创建Dao层接口,提供Sql调用方法,编写对应XML语句实现Sql。

底层是使用 JDK 动态代理‌技术为 Mapper 接口生成代理对象,因为接口才能使用JDK的动态代理,而CGLIB适用于类。

注意:Mybatis分为原生和Spring集成,这两种方式对代理对象的处理不一致

代理的作用:将接口方法调用转发给 MyBatis 核心组件(如 Executor),‌与 SQL 执行解耦‌。 

1:解析文件

原生 MyBatis 在初始化时会解析所有 Mapper XML 文件,将每个 SQL 语句封装为 MappedStatement 对象,并注册到全局 Configuration 对象中。

Mapper XML 文件的 namespace 必须与接口的全限定名一致,XML 中的 SQL 语句的 id 必须与接口方法名一致。否则会抛出绑定错误异常。

MappedStatement 的键为:接口全路径+方法名,例如com.aaa.xxx.selectUser。

所以,Mybatis不支持方法重载。如果接口中存在同名方法(即使参数不同),MyBatis 会因无法区分 MappedStatement 的键而报错。

1:原生框架代理

当通过 SqlSession 调用 getMapper 方法时,MyBatis 会从 Configuration 中获取对应的 MapperProxyFactory(维护了所有Mapper接口的代理工厂),‌动态生成代理对象‌。

所以原生的Mybatis框架,代理对象在getMapper调用时创建,且通常只需创建一次。

2:Spring集成代理

而Spring通过 MapperScannerConfigurer 扫描 Mapper 接口,‌在 Spring 容器初始化时‌,为每个 Mapper 接口生成代理对象,并注册为 Spring Bean。此时代理对象已创建完毕

在 Service 层通过 @Autowired 注入 Mapper 接口时,实际注入的是‌启动时已创建的代理对象‌。

3:Sql执行

上述获取到代理对象后,当调用接口方法时,代理对象会触发 MapperProxy.invoke() 拦截方法。

注意:如果接口方法有默认实现,动态代理会直接调用默认方法,而不会执行 SQL。

1:根据(接口全路径+方法名)从 Configuration 中获取对应的 MappedStatement。

2:将方法参数转换为 SQL 参数。

3:通过 SqlSession 内部的 Executor 执行 SQL,处理参数映射和结果集转换。

3:Sql参数

MyBatis比较关注于Sql的执行,提供了动态Sql、参数映射、以及负责映射等功能。

1:动态Sql

开发使用中,尽量避免在动态 SQL 中过度嵌套,可能有性能问题且代码可读性差。可以将判断逻辑放在业务层。

2:参数映射

#{} 和 ${},两者都是Mybatis框架提供的,用于动态 SQL 参数替换的实现。

#{}用于处理‌数据值‌(如字符串);${}用于处理‌SQL 结构‌(如动态表名、列名、排序字段)。

不同点:

#符是预编译占位符,在执行时,会将sql中的 #{} 替换为 ? 号,之后调用 PreparedStatement 的set方法进行赋值。可以有效防止sql注入。操作数据优先使用该方式。

是在编译期替换为 ? 号,变量的替换是在DBMS数据库中,并且加双引号。

适用场景:普通参数操作,如where id = #{id}。


$符是字符串替换,在处理时,就只是把 $() 替换为变量的值,不防Sql注入,语句不会加双引号。

sql语句变量的替换是在动态sql解析阶段,存在sql注入问题,在预编译之前变量可被替换。

适用场景:动态表名、列名、排序字段(如 ORDER BY ${sortField})。

<!-- 高危操作:用户输入直接拼接 -->
<select id="login" resultType="User">
    SELECT * FROM user 
    WHERE username = '${username}' AND password = '${password}'
</select>
<!-- 若 username = "admin' --",会绕过密码验证! -->

3:结果映射

可使用 resultMap 处理字段不一致情况,将实体类字段和数据库字段做映射,在查询方法上指向该 ResultMap 即可。

并且提供了高级嵌套查询:嵌套 association(一对一)、collection(一对多)。

4:缓存机制

MyBatis的缓存分为‌一级缓存‌和‌二级缓存‌,目的是通过减少数据库查询次数提升性能。两者的作用域、配置方式及失效策略不同。

所以它的缓存机制就是针对于查询的Sql语句,避免查询数据库。

1:一级缓存

一级缓存是 SqlSession 级别,同一个会话中有效,其生命周期随 SqlSession 的创建而创建,关闭或提交事务(commit、rollback)时清空。

无需配置,默认开启,存在 BaseExecutor 中的 localCache 属性,PerpetualCache 实例,该对象内部为 HashMap 结构,每个SqlSession有自己独立的缓存。

SqlSession非线程安全,需确保每个线程使用独立的 SqlSession(例如ThreadLocal)。

脏读问题(缓存一致性问题):若其他会话修改数据,当前会话无法感知,需通过更新操作或关闭会话强制刷新。

流程:

1:执行查询时,MyBatis 根据 MappedStatement、参数、分页等信息生成缓存键(CacheKey)。然后通过键查询一级缓存是否有数据,如果有则直接返回结果,如果没有就查询数据库,并将结果存入一级缓存。

2:执行 INSERT/UPDATE/DELETE 或调用 SqlSession.commit()/rollback() 时,清空一级缓存。这里的清空是指清空当前会话下的所有缓存,不会对其他会话造成影响

2:二级缓存

二级缓存是 Mapper 级别,跨 SqlSession 共享(如多个会话访问同一命名空间)。

需要手动开启,在Mapper中添加XML标签指定缓存信息,生命周期跟随容器。

默认使用 PerpetualCache(内存缓存),可集成第三方缓存(如 Ehcache、Redis),引入依赖和配置,在添加 cache 标签时指定 type 即可,

若缓存对象需要跨会话传输,实体类需实现 Serializable 接口。

<!-- UserMapper.xml -->
<mapper namespace="com.example.dao.UserMapper">
    <cache/> <!-- 默认配置 -->  <!--集成:type="org.mybatis.caches.ehcache.EhcacheCache"-->
    <!-- 或自定义参数 -->
    <cache 
        eviction="LRU" 
        flushInterval="60000" 
        size="1024" 
        readOnly="true"/>
</mapper>
  1. eviction:淘汰策略(LRU、FIFO、SOFT、WEAK)。
  2. flushInterval:自动刷新时间(毫秒)。
  3. size:最大缓存对象数量。
  4. readOnly:是否只读(若为 false,缓存返回对象的拷贝)。

流程:

1:执行查询时,根据缓存键先查询一级缓存,再查询二级缓存,如果都没有,再查询数据库。查询结果按照SqlSession(会话)、一级缓存、二级缓存的顺序填充保存。

2:当执行 INSERT/UPDATE/DELETE 时,清空‌当前 Mapper‌ 的二级缓存。

3:多表关联可能有脏数据问题,导致Mapper缓存不会自动失效。可以通过禁用关联Mapper的缓存,或使用cache-ref建立缓存引用,确保关联Mapper的更新触发当前缓存清空。

3:注意事项

如果发现查询速度过慢,优先考虑数据库索引优化,缓存应作为补充手段,不可过度依赖。

缓存数据不一致问题:分布式架构下,可能会有多个线程同时要修改数据,最终可能导致每台机器数据不一致,此时就无法使用二级缓存处理。

但是如果用第三方插件(例如Redis)来实现二级缓存,又没有必要。
因为 Redis 一般是在Mybatis之前做缓存的,而不是嵌套在Mybatis里面使用的,
所以在分布式架构下,应优先考虑在数据查询前缓存控制,Mapper层只用来专注查询数据。

一级缓存适用场景‌:

  1. ‌短会话操作‌:如单个请求中多次读取同一数据。
  2. ‌事务内操作‌:确保事务内数据一致性。

二级缓存适用场景‌:

  1. ‌读多写少数据‌:如配置表、静态数据。‌
  2. 跨会话共享数据‌:如热门查询结果。

二级缓存应根据场景慎用,高频更新查询不要用二级缓存。另外Mybatis缓存也会有缓存穿透、缓存雪崩问题,处理方式同Redis一样。

1:缓存穿透,频繁查询不存在的数据,绕过缓存直接访问数据库。
处理:对空结果进行短期缓存,或使用布隆过滤器。

2:缓存雪崩,大量缓存同时失效,导致数据库压力激增。
解决:给不同Mapper设置随机过期时间,或使用集群化缓存。

5:扩展插件

Mybatis提供了扩展插件,用于简化开发以及个性化处理。

1:拦截器插件

基于 JDK 动态代理,可以按照 @Intercepts 注解指定的规则拦截方法,按照 @Signature 指定类、方法等详细信息。

提供了接口 Interceptor,供开发人员实现接口并重写方法,提供了三个核心方法。

  1. Interceptor:拦截之后的主要处理实现,从参数中获取MappedStatement对象(Sql语句)。
  2. plugin:创建目标对象的代理,如果要拦截,就生成代理对象,不拦截就返回原生对象。
  3. setProperties:读取配置文件中的参数。

当我们实现接口后,底层会创建一个该接口的代理实现类,在指定Sql语句执行前,对其进行拦截,通过代理对象执行拦截器方法。

使用场景:可根据 SqlCommandType 语句类型,自动生成主键id、创建人、创建时间等功能,简化调用和自动填充。

2:分页插件

PageHelper 是 MyBatis 的一个分页插件,其核心实现依赖于 ‌MyBatis 的拦截器机制,通过动态代理拦截 SQL 执行过程,自动添加分页逻辑。

原理:PageHelper 实现 MyBatis 的 Interceptor 接口,拦截 Executor 的 query() 方法,在执行 SQL 前动态修改 SQL 语句并添加分页参数。

流程:

1:在代码中直接使用 PageHelper.startPage() 方法时,会通过 ThreadLocal 将分页参数(页码、每页条数)绑定到当前线程,确保同一线程内的后续查询自动分页。

2:代理对象拦截方法后,会检查当前线程是否有分页参数,如果有,就改写 SQL,执行分页查询和 COUNT 查询。如果没有则直接通过。

3:改写Sql时,会根据数据库类型(如 MySQL、Oracle)的方言(Dialect),将原始 SQL 改写为分页 SQL(例如Oracle不支持Limit)。

4:维护了一个总条数字段,自动生成并执行 COUNT(*) 查询,获取总记录数,用于计算总页数。

5:改写Sql和总条数查询完毕之后,清除 ThreadLocal 中的分页参数,避免污染后续查询,以及防止内存泄漏。

五:Spring Cloud

六:JPA

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值