侧边栏壁纸
博主头像
5faith分享栈

憧憬未来

  • 累计撰写 9 篇文章
  • 累计创建 13 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

【面试题】Spring/SpringBoot部分[2025/1/13 ~ 2025/1/19]

faith5
2025-01-18 / 0 评论 / 1 点赞 / 33 阅读 / 0 字

@[TOC](Spring/SpringBoot部分[2025/1/13 ~ 2025/1/19])

8. 什么是 Spring IOC/DI?

  1. 什么是 Spring IOC
    • 定义: Spring IOC(Inversion of Control,控制反转)是 Spring 框架的核心概念之一。它是通过依赖注入(Dependency Injection) 实现的。IOC 让对象的创建与管理职责由容器负责,而不是由对象自身控制。
      • 控制: IOC 容器控制对象的创建,IOC 容器会根据配置文件来创建对象,在对象不同生命周期根据不同配置进行对象的创建和改造。
      • 反转: 创建对象且注入依赖对象的这个动作, 即创建对象A需要对象对象B, 这时候需要程序员手动创建B, 反转之后将这一动作由 IOC 容器触发.
  2. 什么是 DI
    DI(Dependency Injection,依赖注入)普遍认为是 Spring 框架中用于实现控制反转(IOC) 的一种机制。DI 的核心思想是由容器负责对象的依赖注入,而不是由对象自行创建或查找依赖对象。

9. Spring AOP默认用的是什么动态代理,两者的区别?

  1. 默认代理方式
    • Spring Framework 默认使用的动态代理是 JDK 动态代理
    • SpringBoot 2.x 版本的默认动态代理是 CGLIB。
  2. 区别
    1. 代理方式
      • JDK 动态代理:基于接口实现,通过 java.lang.reflect.Proxy 动态生成代理类。
      • CGLIB 动态代理:基于类继承,通过字节码技术生成目标类的子类,来实现对目标方法的代理。
    2. 使用场景:
      • JDK 动态代理:推荐用于代理接口的场景,适合代理的类实现了接口。
      • CGLIB 动态代理:适合没有接口的类,或需要代理具体类中的非接口方法的场景。由于基于继承实现,不能代理 final 类和 final 方法。
  3. 实现
  • JDK 动态代理
  1. 简单 JDK 动态代理示例
// 接口
public interface Service {
    void perform();
}

// 需要被代理的实现类
public class ServiceImpl implements Service {
    @Override
    public void perform() {
        System.out.println("mianshiya.com");
    }
}

  1. JDK 动态代理处理类:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class ServiceInvocationHandler implements InvocationHandler {
    private final Object target;

    public ServiceInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before method invoke");
        Object result = method.invoke(target, args);
        System.out.println("After method invoke");
        return result;
    }
}

  1. 创建并使用动态代理对象:
import java.lang.reflect.Proxy;

public class DynamicProxyDemo {
    public static void main(String[] args) {
        Service target = new ServiceImpl();
        Service proxy = (Service) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new ServiceInvocationHandler(target)
        );

        proxy.perform();
    }
}

  • CGLIB 动态代理
  1. 服务示例:
public class Service {
    public void perform() {
        System.out.println("mianshiya.com");
    }
}

  1. CGLIB 动态代理处理类:
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class ServiceMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("Before method invoke");
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("After method invoke");
        return result;
    }
}

  1. 创建并使用动态代理对象:
import net.sf.cglib.proxy.Enhancer;

public class CglibDynamicProxyDemo {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(Service.class);
        enhancer.setCallback(new ServiceMethodInterceptor());

        Service proxy = (Service) enhancer.create();
        proxy.perform();
    }
}

10. 什么是 AOP?

AOP(Aspect-Oriented Programming,面向切面编程) 是一种编程范式, 通过将通用代码模块化, 并使用切面应用到用到业务多个地方, 避免代码重复, 从而达到不同业务分离的目的
主要组成部分:AOP 包括几个关键概念:切面(Aspect)、连接点(Join Point)、通知(Advice)、切入点(Pointcut)和织入(Weaving)。

  1. 切面(Aspect):切面是一个模块,包含跨领域的关注点,比如日志、事务等。它可以包含多个通知(Advice)来定义在何时何地应用特定的逻辑。
  2. 连接点(Join Point):连接点是程序执行中的一个特定位置,例如方法调用或异常抛出。AOP 允许在这些点上插入切面逻辑。
  3. 通知(Advice):通知是定义在连接点执行的操作。
    • 前置通知(Before):在方法执行之前执行的操作。
    • 后置通知(After):在方法执行之后执行的操作。
    • 环绕通知(Around):在方法执行前后都可以执行的操作,可以控制方法是否执行。
    • 异常通知(AfterThrowing):在方法抛出异常后执行的操作。
    • 返回通知(AfterReturning):在方法成功返回后执行的操作。
  4. 切入点(Pointcut):切入点定义了在何处应用通知,通常是通过表达式来匹配方法或类。例如,可以定义某个包下所有方法为切入点。
  5. 织入(Weaving):织入是将切面应用到目标对象的过程。可以在编译时、类加载时或运行时进行织入。

11. 看过源码吗?说下 Spring 由哪些重要的模块组成?

  1. Core Container(核心容器):
    • Spring Core:提供了依赖注入(Dependency Injection, DI)和控制反转(Inversion of Control, IoC)的实现,所有其他Spring模块的基础,别的模块都会依赖此模块。
    • Spring Beans:负责管理Bean的定义和生命周期。通过IoC容器完成Bean的创建、依赖注入、初始化、销毁等操作。
    • Spring Context:基于Core和Beans的高级容器,提供了类似JNDI的上下文功能,还包含了国际化、事件传播、资源访问等功能。
    • Spring Expression Language(SpEL):一个强大的表达式语言,用于在运行时查询和操作对象的值。
  2. AOP(面向切面编程):
    • Spring AOP:提供面向切面编程的功能,可以在方法执行前后或抛出异常时动态插入额外的逻辑,比如日志记录、权限验证、事务管理等。
  3. Data Access(数据访问):
    • Spring JDBC:简化了原生JDBC的操作,提供模板方法来管理连接、资源的释放和异常处理。
    • Spring ORM:支持与主流ORM框架(如Hibernate、JPA、MyBatis等)集成,简化持久层开发。
    • Spring Transaction(事务管理):提供声明式和编程式的事务管理机制,与数据库操作密切结合。
  4. Web层
    • Spring Web:提供基础的Web开发支持,包括Servlet API的集成,适用于构建MVC架构。
    • Spring MVC:实现了Model-View-Controller(MVC)模式的框架,用于构建基于HTTP请求的Web应用。它是一个常用的模块,支持注解驱动的Web开发。
    • Spring WebFlux:提供基于Reactive Streams的响应式编程模型,专为高并发的异步非阻塞请求设计。
  5. 除了核心模块外
    • Spring Batch:用于批处理的框架,支持大规模数据的处理与分块执行。
    • Spring Integration:提供消息驱动的应用程序集成方案,适用于构建企业集成架构(EAI)。
    • Spring Cloud:用于构建微服务架构的模块集合,支持分布式系统中的服务注册、配置管理、服务调用等功能。

12. 什么是循环依赖(常问)?

循环依赖(Circular Dependency)是指两个或多个模块、类、组件之间相互依赖,形成一个闭环。简而言之,模块A依赖于模块B,而模块B又依赖于模块A,这会导致依赖链的循环,无法确定加载或初始化的顺序。

  1. spring循环依赖的判决办法: 使用三级缓存来解决了循环依赖(提前暴露未完全创建完毕的 Bean)

    • 一级缓存(singletonObjects):这是一个ConcurrentHashMap,用于存储完全初始化完成的单例bean。当bean创建完成,并且其属性都填充完毕、初始化方法也执行完成后,就会被放入这个缓存中。
    • 二级缓存(earlySingletonObjects):这是一个ConcurrentHashMap,用于存储早期暴露的单例bean。在bean的创建过程中,如果需要提前暴露这个bean(例如,解决循环依赖),就会先将其放入这个缓存中。此时,bean没有初始化完成, 但是已经实例的bean(bean可能还没有完全初始化完成,属性可能还没有全部填充)。
    • 三级缓存(Singleton Factories Map): 这是一个ConcurrentHashMap,用于存储bean的ObjectFactory(特别是为了支持AOP代理对象的创建)。ObjectFactory是一个工厂接口,可以用于创建bean。当bean开始创建时,会先将一个ObjectFactory放入这个缓存中。这个工厂可以用于后续获取bean的实例,无论bean是否已经完全创建完成。
  2. 解决循环依赖的步骤(即三级缓存的工作流程)
    三级缓存的引入使得 Spring 能够更灵活地处理循环依赖和 AOP 代理:
    a. 创建 Bean 实例后,将其对应的工厂放入三级缓存(singletonFactories)。
    b. 在注入依赖时,如果需要该 Bean 的引用,先从一级缓存查找,若找不到,再从二级缓存查找,仍找不到时,从三级缓存通过工厂获取 Bean。
    c. 如果三级缓存的工厂生成了代理对象,则可以将代理对象存入二级缓存供后续使用。
    d. 最终,当 Bean 完成初始化后,将完整的 Bean 放入一级缓存,并移除二级和三级缓存。

13. 为什么 Spring 循环依赖需要三级缓存,二级不够吗?

  1. 二级缓存的局限性
    二级缓存(earlySingletonObjects)用于存储未完全初始化的 Bean 实例,可以解决简单的循环依赖问题。然而,如果仅使用二级缓存,会有以下问题:
    • 无法支持动态代理:在 AOP 场景下,Bean 的代理对象需要在完全初始化后生成。如果提前将原始 Bean 放入二级缓存,后续需要代理对象时,可能从二级缓存中获取到原始对象导致出现问题。
    • 代理对象过早生成:如果直接在二级缓存中放入代理对象,这与 Spring 的生命周期管理原则冲突,因为代理对象通常需要在所有属性注入和生命周期方法(如 @PostConstruct)执行完毕后才生成。
  2. 三级缓存的灵活性
    三级缓存(singletonFactories)存储的是一个可以动态生成 Bean 实例的工厂(ObjectFactory),提供了更大的灵活性:
    • 延迟生成代理对象:三级缓存允许在需要时通过工厂动态生成代理对象,而不是提前暴露代理对象。
    • 按需暴露对象:如果需要原始对象,可以通过工厂获取原始 Bean;如果需要代理对象,可以通过工厂动态创建并返回代理对象。
  3. 总结
    虽然二级缓存可以解决部分循环依赖问题,但在涉及动态代理和 AOP 的复杂场景下,它无法满足所有需求。三级缓存通过存储 ObjectFactory,提供了动态生成和按需暴露的能力,从而使 Spring 能够更全面地解决循环依赖问题。

14. 说下 Spring Bean 的生命周期?

  1. 实例化:Spring 容器根据配置文件或注解实例化 Bean 对象。
  2. 属性注入:Spring 将依赖(通过构造器、setter 方法或字段注入)注入到 Bean 实例中。
  3. 执行aware:如果 Bean 实现了 BeanNameAware 等 aware 接口,则执行 aware 注入。
  4. 初始化前(BeanPostProcessor):在 Bean 初始化之前,可以通过 BeanPostProcessor 接口对 Bean 进行一些额外的处理。
  5. 初始化:调用 InitializingBean 接口的 afterPropertiesSet() 方法或通过 init-method 属性指定的初始化方法。
  6. 初始化后(BeanPostProcessor):在 Bean 初始化后,可以通过 BeanPostProcessor 进行进一步的处理。
  7. 使用 Bean:Bean 已经初始化完成,可以被容器中的其他 Bean 使用。
  8. 销毁:当容器关闭时,Spring 调用 DisposableBean 接口的 destroy() 方法或通过 destroy-method 属性指定的销毁方法。

15. Spring MVC 具体的工作原理?

  1. 客户端请求:用户通过浏览器发送 HTTP 请求,所有请求都被 DispatcherServlet 接收。
  2. 请求映射:DispatcherServlet 根据配置的处理器映射器(HandlerMapping)查找与请求 URL 对应的控制器(Controller)。
  3. 调用控制器方法:找到控制器后,DispatcherServlet 将请求转发给对应的控制器方法进行处理。控制器方法处理业务逻辑后,通常返回一个 ModelAndView 对象,包含数据模型和视图名称。
  4. 视图解析:DispatcherServlet 根据控制器返回的视图名称,使用视图解析器(ViewResolver)将逻辑视图名称解析为实际的视图(如 JSP、Thymeleaf 等)。
  5. 视图渲染返回:视图渲染引擎根据数据模型渲染视图,并将生成的 HTML 响应返回给客户端

备注: Spring MVC 还支持返回 JSON 数据响应,用于构建 RESTful Web 服务,因此上述视图解析部分, 如果是 JSON 响应,则 Spring MVC 会将对象序列化为 JSON,后续返回给客户端。

1

评论区