51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Java 面试之 Spring 框架

Preface {#preface}

本文中使用的 SpringBoot 版本为2.6.13,对应的Spring版本为5.3.23

IOC 原理 {#ioc-原理}

IOC(Inversion of Control ):控制反转。是 Spring Core 最核心的部分。

它本身并不算为一种技术,而是使我们从繁琐的对象交互中解脱出来的一种思想,使你能够专注于对象本身,更好的使用面向对象。

了解它之前必须弄清楚依赖注入(Dependency Inversion)。

DI举例:设计行李箱

首先需要设计轮子,然后根据轮子大小设计底盘,接着再根据底盘设计箱体,最后再根据箱体设计出整个行李箱。因此行李箱依赖于箱体,箱体依赖于底盘,而底盘依赖于轮子。

image-20240314214305661

这样的设计看似 OK,实际上可维护性非常低,若市场需求反馈需要轮子尺寸需要改大一码,由于此时的依赖关系,轮子改大一码底盘也需重新设计,底盘重新设计了那么箱体也得改,进而整个行李箱都需要重新设计,这就是上层建筑依赖下层建筑的缺点。

上面这个场景转换为代码的形式如下:

PixPin_2024-03-14_21-52-28

若要满足轮子大小可以改动的话需要做如下改动:

PixPin_2024-03-14_22-00-24

仅仅是为了能够调整轮胎的尺寸就需要更改上层建筑的所有构造函数,这在软件工程中这样的设计几乎是不可维护的。

如果使用依赖注入的思想又如何进行设计?

首先设计出行李箱的大致样子,然后根据样子设计箱体,接着再根据箱体设计底盘,最后根据底盘设计出轮子。此时依赖关系相较于之前就完全倒置过来了。

image-20240314221746891

所谓依赖注入的含义:把底层类作为参数传递给上层类,实现上层对下层的"控制"。

转换为代码的形式如下:

PixPin_2024-03-14_22-20-25

此时若要再实现修改轮子尺寸的功能只需更改轮子类Tire即可:

PixPin_2024-03-14_22-24-19

在实际的工程中这种设计模式还有利于协同合作和单元测试。

比如开发这 4 个类分别属于 4 个开发小组,只要定义好了接口,所有小组可以同时进行开发而不相互收到受到限制。

如果要写Luggage类的单元测试,只需传入Framework类对象即可,而不用把FrameworkBottomTire类全部new一遍后再构造Luggage对象。

IOC、DI、DL 的关系:

实现IOC除了DI依赖注入的方式外还有DL依赖查找的方式,它相较于DI是一种更为主动的方法,它通过调用框架提供的方法获取对象,获取时需要提供相关配置文件的路径,key 等信息确定获取对象的状态,由于它需要用户自己使用 API 查找资源和组装对象有侵入性,所以如今已被抛弃不再使用了。

DI是 Spring 实现IOC的方式,负责组件的装配,DI也是当今主流的方式。

image-20240314223931743

依赖注入的方式:

  • Setter
  • Interface
  • Constructor
  • Annotation。如 Spring 中的autowired注解。

依赖倒置原则、IOC、DI、IOC容器的关系:

依赖倒置原则是一种思想,大致含义是高层模块不应该依赖底层模块,两者都应该依赖于其抽象。

在依赖倒置原则的思想的指导下才有了 IOC 的思路,依赖注入DI就是实现 IOC 的一种方式。

Spring 框架基于 IOC 才提出容器的概念,对于 IOC 来说最终要的就是容器了,容器中管理着Bean的生命周期,控制着Bean的依赖注入。

image-20240314224838129

在上面的例子中初始化Luggage类的地方就是 IOC容器所做的事情:

image-20240314225740532

IOC 容器的优势:

  • 避免在各处使用new来创建类,并且可以做到统一维护;
  • 创建实例的时候不需要了解其中的细节。

用上面的例子来说如果我们手动创建Luggage实例就需要了解TireBottomFramework类的构造函数,然后一步一步的new直到创建Luggage实例为止。

image-20240317171842391

而使用IOC 容器时所需的步骤是反过来的,它首先会从上到下查找所需的类依赖关系,寻找完毕后再向上一步一步的new直到创建Luggage实例为止。

image-20240317172510004

因此使用 IOC 容器可以隐藏创建Luggage实例的具体细节。它就像个工厂,我们只需向工厂请求Luggage实例它就会返回给我们,完全不用管Luggage实例是怎么一步一步被创建出来的。

实际项目中有的Service类可能是很久很久之前写的依赖几十上百个类,若我们新写的类需要实例化这个Service总不能去弄慢慢弄清楚它所有的依赖然后一步一步创建,使用 IOC 容器就可以完美解决此问题。

image-20240317172758315

Spring IOC 应用 {#spring-ioc-应用}

Spring 启动时读取容器的Bean配置信息生成对应的Bean定义注册表,根据这个注册表通过 Java 反射功能实例化出Bean对象,装配好Bean之间的依赖关系为上层提供准备就绪的运行环境。

Spring IOC 支持的功能:

  • 依赖注入;

  • 依赖检查;

  • 自动装配;

  • 支持集合;

  • 指定初始化方法和销毁方法;

  • 支持回调方法。

    需要实现 Spring 提供的接口才行,略带有侵入性,谨慎使用。

其中最重要的就是:依赖注入自动装配

SpringIOC 容器的核心接口:

  • BeanFactory
  • ApplicationContext

首先来看下BeanDefinitionBeanDefinitionRegistry

BeanDefinition:主要用来描述Bean的定义。

Spring 容器在启动时会将xml文件或注解中Bean的定义解析成BeanDefinition

image-20240317184307614

BeanDefinitionRegistry:提供向 IOC容器注册BeanDefinition对象的方法。

在 Spring5 源码中BeanDefinitionRegistry.registerBeanDefinition()方法就是将BeanDefinition注册到BeanFactory接口实现类DefaultListableBeanFactory.beanDefinitionMap属性中。

beanDefinitionMapConcurrentHashMap类型,键为beanName,值为BeanDefinition,同时还会将beanName存入至beanDefinitionNames数组中,以便后续Bean的实例化。

image-20240317190149873

image-20240317190633695

BeanFactory:Spring框架最核心的接口。

  • 提供 IOC 的配置机制;
  • 包含Bean的各种定义,便于实例化Bean
  • 建立Bean之间的依赖关系;
  • Bean生命周期的控制。

BeanFactory体系结构:

image-20240317191209512

从上图中可以看出BeanFactory是顶级接口,前面提到的BeanDefinitionRegistryDefaultListableBeanFactory都是从它扩展衍生出来的。

Spring 有个显著的特点,看接口直接的继承关系和它们的名字就能大致得出其作用和含义。

大概过下图中比较重要的接口留个眼熟,不需要记忆:

  • ListableBeanFactory:定义了访问容器中Bean基本信息的若干方法,如:查看Bean的个数、获取某一类型Bean的配置名、查看容器中是否包含某个Bean等方法。

  • HierarchicalBeanFactory:通过此接口可以建立出父子层级关联的容器体系,子容器可以访问父容器中的Bean,但父容器不能访问子容器中的Bean

    Spring 通过父子容器实现出了很多功能,比如 SpringMVC 中展现层的Bean位于子容器中,而业务层和持久层的Bean位于父容器中,这样展现层中的Bean就可以引用业务层中的Bean,但业务层则看不到展现层中的Bean

  • ConfigurableBeanFactory:它增强了 IOC 容器的可定制性,定义了设置类装载器、属性遍历器、属性初始化后置处理器等方法。

  • AutowireCapableBeanFactory:定义了将容器中的Bean按照某种规则自动装配,如:按照名字,按照类型等。

  • SingletonBeanRegistry:允许在运行期间向容器注册单例Bean实例的方法。

BeanFactory接口比较重要的方法:

  • getBean()方法:它是 Spring 容器最重要的方法之一,功能是从 Spring 容器中获取Bean,有 5 个重载方法,比如按名称获取Bean,按类型获取Bean等。

  • isSingleton()方法:判断Bean在 Spring IOC 中是否为单例

    在 Spring IOC 中默认情况下Bean都是单例

  • isPrototype()方法:判断Bean在 Spring IOC 中是否为多例。

    如果返回的是true,在使用getBean()方法时,Spring 容器会创建一个新的Bean对象并返回给调用者。

image-20240317193818088

由于BeanFactory只提供一些基础的功能,所以 Spring 在其基础上又设计了更为高级的ApplicationContext接口,我们在使用 Spring 容器时大部分使用的都是ApplicationContext接口的实现类。

BeanFactoryApplicationContext的比较:

  • BeanFactory是 Spring框架的基础设施,面向 Spring;
  • ApplicationContext面向使用 Spring框架的开发者。

ApplicationContext的功能(继承多个接口):

  • BeanFactory:能够管理、装配Bean
  • ResourcePatternResolver:能够加载资源文件;
  • MessageSource:能够实现国际化等功能;
  • ApplicationEventPublisher:能够注册监听器,实现监听机制。

image-20240317211701884

image-20240317213123996

其中最底部的AnnotationConfigServletWebServerApplicationContext类是 SpringBoot 默认的 IOC 容器类。

在 SpringBoot 源码中层层点击:

image-20240317214545766

image-20240317214645874

image-20240317214729117

image-20240317214835301

image-20240317215107419create()方法在其两个实现类中都有,点入AnnotationConfigServletWebServerApplicationContext.create()方法源码设置断点然后以debug方式启动 SpringBoot。

image-20240317215415948

成功进入了设置的断点,说明 SpringBoot 默认使用的就是**AnnotationConfigServletWebServerApplicationContext**。

关于 Spring 中@Component@ComponentScan@Configuration@Bean注解将Bean注入 IOC 容器以及getBean()方法从容器中获取Bean等基础功能这里就不演示了。

Spring IOC 的 refresh 方法解析 {#spring-ioc-的-refresh-方法解析}

refresh()方法作用:

  • 为 IOC容器以及Bean的生命周期管理提供条件;
  • 刷新 Spring 上下文信息,定义 Spring 上下文加载流程。

下面仅对该方法的源码做简要解析。

首先找到该方法所在的为止,跟着一步一步点所在源码位置。

image-20240318215230816

image-20240318215305253

image-20240318215339964

image-20240318215440345

扩展:上图中afterRefresh()方法的方法体为空。

image-20240318220332692

因为 Spring 框架充分考虑到了扩展性,在框架中留了很多口子,使我们可以继承框架的某些模块重写其中的某些方法能够实现很多自定义的需求。

image-20240318215507682

image-20240318215545846

PixPin_2024-03-18_22-37-59

Spring IOC 的 getBean 方法解析 {#spring-ioc-的-getbean-方法解析}

getBean()方法调用的都是doGetBean()方法。

image-20240319215653162

doGetBean()方法的代码逻辑:

  1. 转换beanName
  2. 从缓存中加载实例;
  3. 实例化Bean
  4. 检测parentBeanFactory
  5. 初始化依赖的Bean
  6. 创建Bean

PixPin_2024-03-19_22-12-07

Spring Bean 的作用域:

  • singleton:Spring 的默认作用域,容器里拥有唯一的Bean实例;

  • prototype:针对每个getBean()请求,容器都会创建一个Bean实例;

  • request:会为每个 Http 请求创建一个Bean实例;

  • session:会为每个 session 创建一个Bean实例;

  • globalSession:会为每个全局 Http Session 创建一个Bean实例该作用域仅对 Portlet 有效。

    Portlet 规范提出的概念,该 Session 对构成某个 Portlet 应用的所有不同的 Portlet 所共享,因此仅在使用 PortletContext 时有用。

Spring Bean 的生命周期:

  • 创建过程

    image-20240319230637087

  • 销毁过程

    • 若实现了DisposableBean接口,则会调用destroy()方法;
    • 若配置了destry-method属性,则会调用其配置的销毁方法。

AOP 的介绍和使用 {#aop-的介绍和使用}

软件工程中有一个基本原则关注点分离:不同的问题交给不同的部分去解决。

  • 面向切面编程 AOP正 是此种技术的体现;
  • 通用化功能代码的实现,对应的就是所谓的切面(Aspect);
  • 业务功能代码和切面代码分开后,架构将变得高内聚低耦合;
  • 确保功能的完整性:切面最终需要被合并到业务中(Weave 织入)。

AOP 的三种织入方式:

  • 编译时织入:需要特殊的 Java 编译器在编译时生成完整的字节码,如 AspectJ;
  • 类加载时织入:需要特殊的 Java 编译器在加载字节码时将切面的字节码融合进来,如 AspectJ 和 AspectWerkz;
  • 运行时织入:Spring 采用的方式,通过动态代理的方式,实现简单。

实现一个简单的例子,在请求中通过 AOP 记录请求的来源地址:

编写 controller。

@RestController
public class HelloController {

    @GetMapping("hello")
    public String hello() {
        return "Hello World!";
    }

}

pom.xml文件中引入 AOP 相关依赖。

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

编写 AOP 类。

@Aspect
@Component
public class RequestLogAspect {
    private static final Logger logger = LoggerFactory.getLogger(RequestLogAspect.class);

    /**
     * 定义切入点
     */
    @Pointcut("execution(public * cool.ldw.framework.conttroller..*.*(..))")
    public void webLog(){}

    /**
     * 切入的目标方法执行前执行此方法
     */
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint){
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 记录下请求内容
        logger.info("URL : " + request.getRequestURL().toString());
        logger.info("IP : " + request.getRemoteAddr());

    }

    /**
     * 切入的目标方法执行完毕后执行此方法
     */
    @AfterReturning(returning = "ret", pointcut = "webLog()")
    public void doAfterReturning(Object ret) {
        // 处理完请求,返回内容
        logger.info("RESPONSE : " + ret);
    }
    
}

这样一个 AOP 类就写好了,它会切入 conttroller 包下所有类的所有public方法。

@Pointcutexecution的用法:

execution 表达式是定义切入点时最常用的一种表达式,用于匹配特定的方法签名。其语法如下:

  • execution(modifiers-pattern? return-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

具体各部分含义如下:

  1. modifiers-pattern : 可选部分,表示方法的访问修饰符,如 public, protected, private, static 等。
  2. return-type-pattern: 必需部分,表示方法的返回类型,可以为任意类型或特定类型。
  3. declaring-type-pattern: 可选部分,表示所在类的名称或者包名。
  4. name-pattern: 必需部分,表示方法名,可使用通配符匹配。
  5. param-pattern : 必需部分,表示方法参数列表,使用两个点 .. 表示零个或多个任意参数,也可以指定具体的参数类型。
  6. throws-pattern: 可选部分,表示方法可能抛出的异常。

下面是一个更详细的示例:

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

@Pointcut("execution(* com.example.repository.*Repository.*(..))")
public void repositoryMethods() {}

@Pointcut("executon(* com.example.controller.*Controller.*(..))")
public void controllerMethods() {}

在这个示例中:

  • 第一个 @Pointcut 匹配 com.example.service 包中所有以 "Service" 结尾的类的公共方法。
  • 第二个 @Pointcut 匹配 com.example.repository 包中所有以 "Repository" 结尾的类的所有方法。
  • 第三个 @Pointcut 匹配 com.example.controller 包中所有以 "Controller" 结尾的类的所有方法。

使用上述语法和示例,你可以根据具体需要编写符合要求的 execution 表达式。

AOP 的主要名词概念:

  • Aspect:通用功能的代码实现;

  • Target:被织入 Aspect 的对象;如HelloController

  • JoinPoint:可以作为切入点的机会,所有方法都可以作为切入点;如hello()方法;

  • Pointcut:Aspect 实际被应用在的 JoinPoint,即定义切入点,支持正则;

  • Advice:类里的方法以及这个方法如何织入到目标方法的方式;

    Advice 的种类:

    • 前置通知(Before)
    • 后置通知(AfterReturning)
    • 异常通知(AfterThrowing)
    • 最终通知(After)
    • 环绕通知(Around)
  • Weaving:Aop的实现过程,将切面应用到实际对象创建一个新的代理对象的过程,对于 Spring 来说就是初始化 Context 中对象时完成织入操作。

Spring AOP 的原理 {#spring-aop-的原理}

Spring AOP 的实现:JdkProxy 和 Cglib

  • AopProxyFactory根据AdvisedSupport对象的配置来决定使用哪种实现方式;

  • 默认策略如果目标类是接口,则用JDKProxy来实现,否则用后者;

  • JDKProxy 的核心:InvocationHandler接口和Proxy类;

    实现原理是 Java 内部的反射机制。

    反射机制在生成类的过程中比较高效。

    通过反射接收被代理的类,并且被代理的类必须要实现InvocationHandler接口。

  • Cglib:以继承的方式动态生成目标类的代理。

    实现原理是借助 ASM 实现。

    ASM 在生成类之后的执行过程中比较高效,另外可通过缓存 ASM 生成的类可解决生成类过程中比较低效的问题。

    如果目标类没有实现InvocationHandler接口,Spring 就会采用 Cglib 动态代理目标类。

    Cglib(Code Generation Library)是一个代码生成的类库,可以在运行时动态的生成某个类的子类,通过修改字节码实现代理。

    Cglib 是通过继承的方式实现动态代理,所以某个类若是被final关键字修饰就无法使用 Cglib 实现动态代理。

代理模式:接口 + 真实实现类 + 代理类。

真实实现类和代理类都需要实现接口,在实例化时需要使用代理类,Spring AOP 需要做的就是生成一个代理类替换掉真实实现类对外提供服务。

来看一个 AOP 原理的简单例子:

支付宝用户只需使用支付功能,而无需关心钱如何从支付宝绑定的银行卡取出以及如何支付给店家的,这些都是委托给支付宝去实现。

首先编写支付接口Payment

public interface Payment {
    void pay();
}

接着编写真实实现类RealPayment

public class RealPayment implements Payment {
    @Override
    public void pay() {
        System.out.println("作为用户,我只关心支付功能。");
    }
}

编写支付宝支付类AliPay,即代理类

public class AliPay implements Payment{
    private Payment payment;

    public AliPay(Payment payment) {
        this.payment = payment;
    }

    public void beforePay() {
        System.out.println("从银行取款");
    }

    @Override
    public void pay() {
        beforePay();
        payment.pay();
        afterPay();
    }

    public void afterPay() {
        System.out.println("支付给商家");
    }
}

最后编写测试方法:

public class ProxyDemo {
    public static void main(String[] args) {
        Payment proxy = new AliPay(new RealPayment());
        proxy.pay();
    }
    /*输出:
    从银行取款
    作为用户,我只关心支付功能
    支付给商家
     */
}

实现对pay()方法织入了beforePay()afterPay()的逻辑。

Spring 里的代理模式的实现:

  • 真实实现类的逻辑包含在了getBean()方法里;
  • getBean()方法返回的实际上是Proxy的实例;
  • Proxy实例是 Spring 采用JDKProxyCGLIB动态生成的。

看看 Spring 中 getBean()方法时如何生成代理类的:

getBean()方法调用的都是doGetBean()方法。

image-20240319215653162

doGetBean()方法中通过一系列的方法调最终会调用到AbstractAutowireCapableBeanFactory.doCreateBean()方法, 可以使用断点调试验证这一点:

doCreateBean()方法也是比较复杂的,其中较为关键的地方是调用了initializeBean()方法。

PixPin_2024-03-24_00-44-54

initializeBean()方法中调用了BeanPostProcessors中的方法,不过在这里我们重点关注调用的applyBeanPostProcessorsAfterInitialization()方法。

applyBeanPostProcessorsAfterInitialization()方法中又调用了postProcessAfterInitialization()方法。

image-20240324010140308

继续点入postProcessAfterInitialization()方法中发现已经来到接口处了。

image-20240324010729234

实际上此处调用的是其实现类AbstractAutoProxyCreator中该方法的重写。

image-20240324011013673

wrapIfNecessary()方法中调用了createProxy()方法。

image-20240324011402585

createProxy()方法中最后返回的是proxyFactory.getProxy()方法的返回值。

PixPin_2024-03-24_01-16-35

继续看getProxy()方法中调用的createAopProxy()方法的源码。

image-20240324012106393

这里就是真正创建 AOP 代理的逻辑了。

image-20240324012843812

它实际上调用的是其实现类DefaultAopProxyFactory.createAopProxy()方法。

image-20240324013048337

DefaultAopProxyFactory.createAopProxy()方法可以看到返回的不是ObjenesisCglibAopProxy,就是JdkDynamicAopProxy,具体根据配置情况返回。

由此证明了 Spring 中实现 AOP 的方式是JdkProxy 和 Cglib。

image-20240324013316334

参考资料 {#参考资料}

赞(3)
未经允许不得转载:工具盒子 » Java 面试之 Spring 框架