LOADING

加载过慢请开启缓存 浏览器默认开启

Spring面试基础题

什么是 Spring 框架?

Spring 是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题。是一个分层的 JavaSE/EE full-stack(一站式)轻量级开源框架。为 Java 应用程序提供了全面的基础性服务支持,通过 IOC 和 AOP 功能,Spring 帮助企业级应用程序在简单的 Java 对象(POJO)上构建简洁、可维护的企业级应用。

为了降低 Java 开发的复杂性,Spring 采用了以下 4 种关键策略:

  • 基于 POJO 的轻量级和最小侵入性编程;
  • 通过 IOC、依赖注入(DI)和面向接口实现松耦合;
  • 基于切面和惯例进行声明式编程;
  • 通过切面和模板减少样式代码;

Spring 的两大核心概念

控制反转(IoC)

控制反转是一种通过描述(XML 或注解)并通过第三方去生产或获取特定对象的方式。在 Spring 中实现控制反转的是 IOC 容器,其实现方法是依赖注入(Dependency Injection,DI)。IOC 容器在创建对象时,会将它所依赖的对象注入进去,而不是由对象主动去获取依赖对象。核心思想是将程序的控制权从程序员转移到了 Spring 容器,通过容器来实现对象的装配和管理。程序员只需要关注业务逻辑本身的实现,而不用去管理对象的创建和组装。

优点:

  • 降低了组件之间的耦合度,提高了代码的可重用性和可维护性;
  • 提高了系统的扩展性和灵活性,通过配置文件或注解的方式来描述对象的依赖关系,可以方便的进行配置和修改,不需要修改代码;
  • 提高了代码的可维护性和可测试性,因为控制反转后,每个组件的职责更加单一,代码更加容易测试,同时也便于功能复用。
  • 降低开发成本,降低了代码的复杂度,提高了开发效率。

面向切面编程(AOP)

面向切面编程是一种新的编程范式,是对传统 OOP 的补充。OOP 通过封装、继承、多态来建立一种对象层次结构,以此来模拟真实世界。而 AOP 则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,这些步骤或阶段通常称为横切关注点,这些横切关注点在业务处理过程中往往散布于各个业务模块中,而核心业务模块往往会有多个对象参与,这些横切关注点在对象中得到分散和实现,它们分散于各个不同的对象中,与业务无关,却为业务所共同调用。AOP 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来,横切关注点被封装为特殊的类,称为切面(Aspect),切面将导致系统的多个模块都产生影响,所以 AOP 被称为横切面向的编程。

Spring AOP 支持以下几种类型的切面
  • 基于方法的切面:通过在方法执行之前,之后或者抛出异常时执行额外的逻辑来切入方法
  • 基于切点的切面:通过定义一个切点,该切点确定在哪些方法上应用切面逻辑
  • 基于注解的切面:通过在切面逻辑上添加注解,Spring 将判断何时应该在运行时应用该切面逻辑
Spring AOP 通知(Advice)的类型
  • 前置通知(Before):在目标方法被调用之前调用通知功能
  • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么
  • 异常通知(AfterThrowing):在目标方法抛出异常后调用通知
  • 最终通知(AfterReturning):在目标方法完成之后调用通知,此时不会关系方法是否执行成功
  • 环绕通知(Around):在目标方法调用前后调用通知,可以控制目标方法的执行与否

Spring 的优点

  • 方便解耦,简化开发:Spring 就是一个大工厂,可以将所有对象创建和依赖关系维护,交给 Spring 管理。
  • AOP 编程的支持:Spring 提供面向切面编程,可以方便的实现对程序进行权限拦截和运行监控等功能。
  • 声明式事务的支持:只需要通过配置就可以完成对事务的管理,而无需手动编程。
  • 方便程序的测试:Spring 对 Junit4 支持,可以通过注解方便的测试 Spring 程序。
  • 方便集成各种优秀框架:Spring 不排斥各种优秀的开源框架,其内部提供了对各种优秀框架(如:Struts、Hibernate、MyBatis、Quartz 等)的直接支持。
  • 降低 JavaEE API 的使用难度:Spring 对 JavaEE 开发中非常难用的一些 API(JDBC、JavaMail、远程调用等),都提供了封装,使这些 API 应用难度大大降低。

Spring 的缺点

  • Spring 的 API 比较难学,它是一个学习成本较高的框架。
  • 依赖反射机制,会影响程序的性能。
  • 没有轻量框架的感觉,配置过于复杂,比如需要配置文件、依赖等。

Spring 的架构

Spring 总共大约有 20 个模块, 由 1300 多个不同的文件构成。 而这些组件被分别整合在核心容器(Core Container) 、 AOP(Aspect Oriented Programming)和设备支持(Instrmentation) 、数据访问与集成(Data Access/Integeration) 、 Web、 消息(Messaging) 、 Test 等 6 个模块中

  • 核心容器(Core Container):核心容器提供 Spring 框架的基本功能。核心容器的主要组件是 BeanFactory,它是工厂模式的实现。BeanFactory 使用控制反转(IOC) 模式将应用程序的配置和依赖性规范与实际的应用程序代码分开。编程人员可以通过配置文件来指定应用程序需要哪些对象,由 BeanFactory 来将这些对象实例化。
  • AOP(Aspect Oriented Programming):通过配置管理特性,Spring AOP 模块直接将面向方面的编程功能集成到了 Spring 框架中。所以,可以很容易地使 Spring 框架管理的任何对象支持 AOP。Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖 EJB 组件,就可以将声明性事务管理集成到应用程序中。
  • Beans:Beans 模块是构成框架的基础。它提供了 BeanFactory,它是工厂模式的经典实现,以及用于处理控制反转的 BeanFactoryPostProcessor 和 BeanPostProcessor 接口。BeanFactoryPostProcessor 可以在容器实例化任何 bean 之前读取配置元数据,并可以根据需要进行修改。BeanPostProcessor 接口允许 bean 实例在实例化过程中进行自定义修改。Spring 将管理对象称为 bean。
  • Context:Context 模块构建于 Beans 模块之上。它添加了国际化(使用 ContextMessageSource 接口)、资源加载(使用 ContextResourceLoader 接口)和对框架事件的发布(使用 ApplicationEventPublisher 接口,ApplicationContext 接口实现了这个接口)的支持。ApplicationContext 是 Beans 模块的一个子集,它添加了自动化生命周期管理。这些功能包括容器的启动和关闭时自动化的实例化和销毁。它还检测由框架和应用程序抛出的任何 BeanException 类型的异常,并将它们重新抛出为更易于使用的异常类型。
  • jdbc:JDBC 模块提供了一个 JDBC 抽象层,它消除了编写冗长的 JDBC 代码和针对特定数据库供应商的代码的需要。Transaction 模块支持程序式和声明式事务管理,JDBC 模块提供了一个 JDBC 抽象层,它消除了编写冗长的 JDBC 代码和针对特定数据库供应商的代码的需要。Transaction 模块支持程序式和声明式事务管理。
  • Web:Web 模块提供了基本的 Web 集成特性,例如多文件上传功能、使用 Servlet 监听器初始化 IoC 容器、使用 Web 应用程序上下文自动注册 bean 等。
  • Test:Test 模块支持使用 JUnit 和 TestNG 进行测试。它还提供了 Spring 应用程序上下文的 Mock 实现,用于测试代码。

Spring 的设计模式

  • 工厂模式:Spring 使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
  • 单例模式:Spring 中的 Bean 默认都是单例的。
  • 代理模式:Spring AOP 功能的实现。用到了 JDK 动态代理和 CGLIB 字节码生成技术。
  • 模板方法模式:Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。用来解决代码重复的问题,它定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。
  • 观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。如 Spring 中 Listener 的实现 ApplicationListener。

JDK 动态代理

JDK 动态代理是 Java 语言自带的动态代理,它位于 java.lang.reflect 包中的 Proxy 类和 InvocationHandler 接口,一般情况下,动态代理的使用不需要用户去直接操作它的 API,而是通过一些间接的方式来间接使用动态代理的 API。
使用动态代理的步骤:

  1. 创建一个实现接口 InvocationHandler 的类,该类负责实现代理逻辑,在实现类中,需要重写 invoke 方法,在代理对象的方法被调用时 invoke 会被执行。
  2. 通过 Proxy 的静态方法 newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h)创建一个代理对象。三个参数的含义:classLoader 类加载器,class[]被代理的接口,InvocationHandler h 实现类的实例。
  3. 通过代理对象调用目标方法。实际上是调用了 InvocationHandler 的 invoke 方法。
    优点:不需要实现接口,可以在运行期动态的创建某个对象的代理对象。
    缺点:只能代理实现了接口的类,不能代理没有实现接口的类。

Java 字节码

Java 字节码是一种中间代码,它是 Java 源代码编译后的结果。Java 字节码可以在任何支持 Java 虚拟机(JVM)的平台上运行,这使得 Java 成为一种跨平台的编程语言。Java 字节码是一种基于栈的指令集,它包含了一系列的指令,用于执行各种操作,例如加载、存储、运算、跳转等。Java 字节码可以通过反编译工具转换为 Java 源代码,这使得 Java 字节码成为一种保护 Java 代码的方式。在 Java 中,动态代理和 AOP 等技术都是基于 Java 字节码实现的。

CGLIB 字节码生成

CGLIB(Code Generation Library)是一个基于 ASM(Java 字节码操控框架)的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理,它也是 Spring AOP 实现的方式之一。它可以通过继承和重写方法来生成代理类,并在代理类中添加额外的逻辑.CGLIB 通过生成目标类的子类来实现代理,然后在子类中重写目标方法,并在重写的方法中添加额外的逻辑,这种比原生的代理更加灵活,但是也有一些缺点,因为它是通过继承来实现代理的,所以如果目标类是 final 类型的,那么它是无法继承的,也就无法实现代理。
下面是一个简单的 CGLIB 代理的例子:

public class TargetClass {
    public void doSomething() {
        System.out.println("TargetClass: doSomething");
    }
}

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

public class Main {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(TargetClass.class);
        enhancer.setCallback(new MyInterceptor());
        TargetClass proxy = (TargetClass) enhancer.create();
        proxy.doSomething();
    }
}

Spring Context 应用上下文模块

这是基本的 Spring 模块,提供 Spring 框架的基础功能,BeanFactory 是任何以 spring 为基础的应用的核心,Spring 框架建立在此模块之上,它使 Spring 成为一个容器。Bean 工厂是工厂模式的实现,提供了控制反转 IOC 功能,用来把应用的配置和依赖从实际的应用代码中解耦出来。最常用的就是 org.springframework.beans.factory.xml.XmlBeanFactory,它从 XML 文件中读取配置元数据,由这些元数据来生成一个被配置化的系统或者应用。

依赖注入

通过描述配置文件中有哪些组件和哪些服务来创建对象,通过IOC容器来管理对象的生命周期和对象间的依赖关系,而不是由对象主动去获取依赖对象。核心思想是将程序的控制权从程序员转移到了Spring容器,通过容器来实现对象的装配和管理。程序员只需要关注业务逻辑本身的实现,而不用去管理对象的创建和组装。

依赖注入的方式:
构造函数,setter方法,接口注入

BeanFactory 和 ApplicationContext 有什么区别?

BeanFactory优缺点

优点:启动时占用的资源少。对资源要求较高的应用比较有优势
缺点:运行速度慢,而且有可能会出现空指针异常的错误,而且通过bean工厂创建的Bean生命周期会简单一些

ApplicationContext优缺点

优点:所有的Bean在启动的时候都完成了加载,系统运行速度快,在系统启动的时候就可以发现系统中的配置问题
缺点:启动时占用的资源多,对资源要求较高的应用比较吃亏

区分构造函数注入和setter方法注入

Spring配置方法

  1. XML配置
  2. 基于注解配置:
   <bean>
        <context:annotation-config/>
<!-- bean definitions go here -->
   </bean>
  1. 基于Java API配置:通过@Configuration和@Bean注解实现

Spring Bean 的作用域有哪些?

  • singleton:单例模式,一个 BeanFactory 有且仅有一个实例,Bean 默认为单例模式。
  • prototype:原型模式,每次从 BeanFactory 获取 Bean 时,都会生成一个新的 Bean 实例。
  • request:每次 HTTP 请求都会产生一个新的 Bean,该 Bean 仅在当前 HTTP request 内有效。
  • session: 在一个 HTTP Session 中,一个 Bean 对象对应一个实例,该作用域仅在基于web的Spring ApplicationContext 情形下有效。
  • global-session:全局session作用域,仅仅在基于Portlet的web应用中才有意义,Spring5已经没有了。

如何理解Ioc和DI?

Ioc就是控制反转,就是我们不用自己创建对象,这些都交给Spring的bean工厂帮我们创建,通过面向接口编程的方式来实现对业务组件的动态依赖。

DI就是依赖注入,组件之间的依赖关系有容器在运行期决定,也就是容器动态的将某个依赖关系注入到组件之中,依赖注入不是为了创建更多的功能,而是为提高组件的复用性和可维护性。并创建一个灵活,可扩展的平台。

Spring Bean 的生命周期

SpringMVC的核心组件

  • DispatcherServlet:前端控制器,负责接收请求并将请求分发给其他的控制器,请求的入口
  • MultipatViewResolver:视图解析器,负责将逻辑视图名解析为具体的视图实现,如jsp、freemarker、velocity等
  • HandlerMapping:处理器映射器,负责根据请求找到对应的处理器,包含处理器和拦截器
  • HandlerAdapter:处理器适配器,负责将请求分发给处理器
  • HandlerExceptionResolver:处理器异常解析器,负责处理处理器执行过程中的异常,将异常映射为错误页面
  • RequestToViewNameTranslator:请求到视图名的转换器,负责将请求解析为视图名,用于解析出请求的默认视图名
  • LocaleResolver:本地化解析器,负责解析请求的区域信息,用于国际化
  • ThemeResolver:主题解析器,负责解析请求的主题信息,用于主题的切换
  • ViewResolver:视图解析器,负责将逻辑视图名解析为具体的视图实现
  • FlashMapManager:FlashMap管理器,负责重定向时保存参数至临时存储(默认是session)

Spring通知(Advice)的类型

  1. 前置通知(Before):在目标方法被调用之前调用通知功能
  2. 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么
  3. 返回通知(AfterReturning):在目标方法成功执行之后调用通知
  4. 异常通知(AfterThrowing):在目标方法抛出异常后调用通知
  5. 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为

IOC容器初始化过程

  1. 从xml文件中读取配置信息
  2. 将bean标签解析成BeanDefinition对象,如解析property标签,将其封装成PropertyValue对象,再将PropertyValue对象封装到BeanDefinition对象中
  3. 将BeanDefinition对象注册到BeanDefinitionMap中
  4. BeanFactory根据BeanDefinitionMap中的信息创建Bean实例和初始化Bean

bean的生命周期

  1. 实例化bean:通过构造器或工厂方法创建bean实例
  2. 设置对象属性:通过反射调用setter方法设置属性
  3. 调用BeanNameAware的setBeanName()方法
  4. 调用BeanFactoryAware的setBeanFactory()方法
  5. 调用BeanPostProcessor的预初始化方法
  6. 调用InitializingBean的afterPropertiesSet()方法
  7. 调用init-method属性配置的初始化方法
  8. 调用BeanPostProcessor的后初始化方法
  9. 如果是单例模式,将bean注册到单例缓存池中,如果是多例模式,返回Bean给用户,剩下来的生命周期由用户控制
  10. 调用DisposableBean的destroy()方法
  11. 调用destroy-method属性配置的销毁方法

Beanfactory和ApplicationContext的区别

  1. 功能上:BeanFactory是Spring里面最底层的接口,包含了各种Bean的定义,读取bean配置文档,管理bean的加载、实例化、依赖、生命周期,维护Bean之间的依赖关系等。ApplicationContext是BeanFactory的子接口,除了包含BeanFactory的所有功能外,还提供了更完整的框架功能,如AOP、事件传播、资源加载、国际化处理等。
  2. 加载方式上:BeanFactory是延迟加载,只有在使用到某个Bean时才对该Bean进行加载实例化。ApplicationContext是预加载,当ApplicationContext容器启动时,容器中管理的所有单例Bean都被初始化。
  3. 创建方式上:BeanFactory是通过编程的方式创建的,ApplicationContext是通过声明的方式创建的。如使用ContextLoader
  4. 注册方式上: BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor等注册方式,但是BeanFactory需要手动注册,而ApplicationContext则是自动注册。

BeanFactory和FactoryBean的区别

BeanFactory:管理Bean的容器,Spring中生成的Bean都是由BeanFactory来进行管理的,BeanFactory是Spring中最底层的接口,提供了最简单的容器的功能,只提供了实例化对象和拿对象的功能,不提供注解、AOP等高级功能。

FactoryBean:是一个接口,是Spring中比较高级的工厂类,可以生成复杂的Bean,FactoryBean是一个Bean,其本身也是一个Bean,FactoryBean可以生成Bean,也可以生成FactoryBean,FactoryBean可以生成复杂的Bean,而BeanFactory只能生成简单的Bean。

Bean的作用域

  1. singleton:单例模式,一个BeanFactory有且仅有一个实例,Bean默认为单例模式。
  2. prototype:原型模式,每次从BeanFactory获取Bean时,都会生成一个新的Bean实例。
  3. request:每次HTTP请求都会产生一个新的Bean,该Bean仅在当前HTTP request内有效。
  4. session: 在一个HTTP Session中,一个Bean对象对应一个实例,该作用域仅在基于web的Spring ApplicationContext情形下有效。
  5. global-session:全局session作用域,仅仅在基于Portlet的web应用中才有意义,Spring5已经没有了。

spring的自动装配方式

  1. byName:根据属性名自动装配,Spring容器中查找和属性名相同的Bean,如果有则注入,没有则报错。
  2. byType:根据属性类型自动装配,Spring容器中查找和属性类型相同的Bean,如果有且只有一个则注入,如果有多个则报错。
  3. byConstructor:根据构造器自动装配,Spring容器中查找和构造器参数类型相同的Bean,如果有且只有一个则注入,如果有多个则报错。

@Autowired和@Resource的区别

  1. @Autowired:默认按照byType方式进行bean匹配,如果匹配到多个bean,则按照byName方式进行匹配,如果匹配到多个bean,则报错。@Autowired是Spring的注解,需要导入包org.springframework.beans.factory.annotation.Autowired。
  2. @Resource:默认按照byName方式进行bean匹配,如果匹配到多个bean,则按照byType方式进行匹配,如果匹配到多个bean,则报错。@Resource是J2EE的注解,需要导入包javax.annotation.Resource。

Spring的事务实现方式

  1. 编程式事务管理:通过TransactionTemplate或者直接使用底层的PlatformTransactionManager。编程式事务管理的优点在于可以很精确的控制事务,缺点在于繁琐,不易维护。
  2. 声明式事务管理方式:将事务管理代码从业务方法中分离出来,通过aop进行封装,Spring声明式事务使我们无需要去处理获得连接,关闭连接,事务提交和回滚等这些操作。

事务的传播行为

  1. PROPAGATION_REQUIRED:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
  2. Propagation_SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。
  3. PROPAGATION_MANDATORY:使用当前的事务,如果当前没有事务,就抛出异常。
  4. PROPAGATION_REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起。
  5. PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  6. PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
  7. PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

循环依赖解决

三级缓存解决循环依赖问题

  1. singletonObjects:一级缓存,存放完全初始化好的Bean
  2. earlySingletonObjects:二级缓存,存放早期暴露的Bean
  3. singletonFactories:三级缓存,存放Bean工厂

首先A完成实例化,并将自己添加到singletonFactories中,然后接着进行依赖注入,发现自己依赖对象B,此时就尝试去get(B),然后发现B还没有被实例化,然后对B进行实例化,在B实例化的过程中发现自己依赖了对象A,于是尝试get(A),尝试一级缓存和二级缓存中都没有找到,然后再第三级缓存中找到,因为A初始化时将自己添加到了三级缓存中,所以B可以拿到A,然后将A从三级缓存中移到二级缓存中,然后完成B的实例化,将B添加到一级缓存中,然后将A从二级缓存中移到一级缓存中,然后完成A的实例化。

Spring的单例Bean是否有并发安全问题

当多用用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该请求对应的业务逻辑,如果业务逻辑有对单例状态的修改,则必须考虑线程安全问题。
有实例变量的Ben,可以保存数据,但是是非线程安全
无实例变量的Bean,不可以保存数据,是线程安全的
在Spring中无状态的Bean适合用单例模式,这样可以共享实例提高性能,有状态的bean在多线程环境下不安全,一般用Prototype模式或者使用ThreadLocal保存状态。

Spring bean如何保证并发安全

  1. 单例变原型:对web项目,可以Controller类上加@Scope(“prototype”)注解,对于非web项目,可以在配置文件中配置。但这种方式会导致每次请求都会创建一个新的Bean,会影响性能。
  2. 尽量避免使用成员变量,在业务方法中使用局部变量,这样就不会有线程安全问题。
  3. 使用并发安全的类,如Vector、Hashtable、Collections.synchronizedList、Collections.synchronizedMap等。
  4. 分布式或微服务的情况下,可以使用分布式锁,如Redis分布式锁。

XML方式和注解方式的优缺点

  1. XML方式:优点是配置灵活,缺点是配置繁琐,不易维护。
  2. 注解方式:优点是配置简单,缺点是不够灵活,不易维护。
本文作者:GWB
当前时间:2023-11-09 11:11:10
版权声明:本文由gwb原创,本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 国际许可协议。
转载请注明出处!