SSM框架面试篇
1. Springboot启动流程
回答:
总:SpringBoot启动,其本质就是加载各种配置信息,然后初始化loc容器并返回。
分:在其启动的过程中会做这么几个事情:
首先,当我们在启动类执行SpringApplication.run
这行代码的时候,在它的方法内部其实会做两个事情:
创建SpringApplication对象;
执行run方法。
其次,在创建SpringApplication
对象的时候,在它的构造方法内部主要做3个事情:
确认web应用类型,一般情况下是Servlet类型,这种类型的应用,将来会自动启动一个tomcat
从spring.factories配置文件中,加载默认的
ApplicationContextlnitializer
和ApplicationListener
记录当前应用的主启动类,将来做包扫描使用
最后,对象创建好了以后,再调用该对象的run方法,在run方法的内部主要做4个事情:
准备
Environment
对象,它里面会封装一些当前应用运行环境的参数,比如环境变量等等实例化容器,这里仅仅是创建
ApplicationContext
对象容器创建好了以后,会为容器做一些准备工作,比如为容器设置
Environment
、BeanFactoryPostProcessor
后置处理器,并且加载主类对应的Definition
刷新容器,就是我们常说的refresh,在这里会真正的创建Bean实例
总:总结一下我刚说的,其实SpringBoot启动的时候核心就两步,创建SpringApplication对象以及run方法的调用,在run方法中会真正的实例化容器,并创建容器中需要的Bean实例,最终返回。
2. IOC容器初始化流程
回答:
总:IOC容器的初始化,核心工作是在AbstractApplicationContext.refresh
方法中完成的。
分:在refresh
方法中主要做了以下事情:
准备
BeanFactory
, 在这一块需要给BeanFactory
设置很多属性,比如类加载器、Enviroment
等执行
BeanFactory
后置处理器,这一阶段会扫描要放入到容器的Bean
信息,等到对应的BeanDefinition
(注意,这里只扫描,不创建)注册
BeanPostProcessor
,我们自定义的BeanPostProcessor
就是在这一阶段被加载的,将来Bean
对象实例化好后需要用到启动tomcat
实例化容器中非懒加载的单例
Bean
,这里需要说的是,多例Bean
和懒加载的Bean
不会在这个阶段实例化,将来用到的时候再创建当容器初始化完成后,再做一些扫尾的工作,比如清除缓存等
总:总的来说,在IOC容器初始化的过程中,首先得准备并执行BeanFactory
后置处理器,其次得注册Bean
后置处理器,并启动tomcat,最后需要借助于BeanFactory
完成Bean
的实例化。
3. Bean生命周期
回答:
总:Bean的生命周期总的来说有四个阶段,创建对象、初始化对象、使用对象以及销毁对象,而且这些工作大部分是交给Bean工厂的doCreateBean
方法完成的:
分:
首先,在创建对象阶段,先调用构造方法实例化对象,对象有了后会填充该对象的内容,其实就是处理依赖注入。
其次,对象创建完毕之后,需要做一些初始化的操作,在这里涉及到几个扩展点:执行Aware
感知接口的回调方法。BeanNameAware
BeanClassLoaderAware
BeanFactoryAware
执行Bean后置处理器的
postProcessBeforelnitialization
方法执行
InitializingBean
接口的回调,在这一步如果Bean中有标注了@PostConstruct
注解的方法,会先执行它执行Bean后置处理器的
postProcessAfterInitization
接下来,就是程序员从容器中获取该Bean使用即可
最后,在销毁容器之前,会先销毁对象,此时会执行
DisposableBean
接口的回调,这一步如果Bean中有标注了@PreDestory
接口的函数,会先执行它
总:总的来说,Bean的生命周期共包含四个阶段,其中初始化对象和销毁对象我们程序员可以通过一些扩展点执行自己的代码。
4. Bean的循环依赖
回答:
总:Bean的循环依赖指的是A依赖B,B又依赖A这样的依赖闭环问题,在Spring中,通过三个对象缓存区来解决循环依赖问题,这三个缓存区被定义到了DefaultSingletonBeanRegistry
中,分别是singletonObjects
用来储存创建完成的Bean,earlySingletonObjects
用来储存未完成依赖注入的Bean,还有SingletonFactories
用来存储创建Bean的ObjectFactory。假如说现在A依赖B,B依赖A,整个Bean的创建过程是这样的:
分:
首先,调用A的构造方法实例化A,当前A还没有处理依赖注入,暂且把它称为半成品,此时半成品A封装到一个ObjectFactory
中,并存储到singletonFactories
缓存区。
接下来,要处理A的依赖注入了,由于此时还没有B,所以得先实例化一个B,同样的,半成品B也会被封装到ObjectFactory
中,并存储到singletonFactories
缓存区
紧接着,要处理B的依赖注入了,此时会找到singletonFactories
中A对应的ObjectFactory
,调用它的getObject
方法得到刚才实例化的半成品A(如果需要代理对象,则会自动创建代理对象,将来得到的就是代理对象),把得到的半成品A注入给B,并同时会把半成品A存入到earlySingletonObjects
中,将来如果还有其他的类循环依赖了A,就可以直接从earlySingletonObjects
中找到它,那么此时singletonFactories
中创建A的ObjectFactory
也可以删除了。
至此,B的依赖注入处理完了后,B就创建完毕了,就可以把B的对象存入到singletonObjects
中了,并同时删除掉singletonFactories
中创建B的ObjectFactory
。
B创建完成之后,就可以继续处理A的依赖注入了,把B注入给A,此时A也创建完成了,就可以把A的对象存储到singletonObjects
中,并同时删除掉earlySingletonObjects
中的半成品A。
截此为止,A和B对象全部创建完成,并存储到了singletonObjects
中,将来通过容器获取对象,都是从singletonObjects
中获取。
总:总结起来还是一句话,借助于DefaultSingletonBeanRegistry
的三个缓存区可以解决循环依赖问题。
开启循环依赖,可以在yaml配置文件中设置spring.main.allow-circular-references
属性来实现。
5. 构造方法出现了循环依赖怎么解决?
回答:由于构造函数是bean生命周期中最先执行的,Spring框架无法解决构造方法的循环依赖问题。可以使用@Lazy
懒加载注解,延迟bean的创建直到实际需要时。
6. SpringMVC的执行流程
回答:
总:使用SpringMvc后,所有的请求都需要经过DispatcherServlet
前端控制器,该类中提供了一个doDispatch
方法,有关请求处理和结果响应的所有流程都在该方法中完成。
分:
首先,借助于HandlerMapping
处理器映射器得到处理器执行链,里面封装了HandlerMethod
代表目标Controller
的方法,同时还通过一个集合记录了要执行的拦截器;
接下来,会根据HandlerMethod
获取对应的HandlerAdapter
处理器适配器,里面封装了参数解析器以及结果处理器;
然后,执行拦截器的preHandler
方法;
接下来是核心,通过HandlerAdapter
处理器适配器执行目标Controller
的方法,在这个过程中会通过参数解析器和结果处理器分别解析浏览器提交的数据以及处理Controller
方法返回的结果;
然后,执行拦截器的postHandler
方法;
最后处理响应,在这个过程中如果有异常抛出,会执行异常的逻辑,这里还会执行全局异常处理器的逻辑,并通过视图解析器ViewResolver
解析视图,在渲染视图,最后再执行拦截器的afterCompletion
。
7. Spring框架中的单例bean是线程安全的吗?
回答:不是线程安全的。当多用户同时请求一个服务时,容器会给每个请求分配一个线程,这些线程会并发执行业务的逻辑。如果处理逻辑中包含对单例状态的修改,比如修改单例的成员属性,就必须考虑线程同步的问题。Spring框架本身并不对单例bean进行线程安全封装,线程安全和并发需开发者自行解决。
通常在项目中使用的bean是不可变状态(如Service类和DAO类),因此在某种程度上可以说Spring的单例bean是线程安全的。如果bean有多种状态(如ViewModel对象),就需要自行保证线程安全。
最简单的解决方法是将单例bean的作用域由singleton
变更为prototype
。
8. 什么是AOP?
回答:AOP
,即面向切面编程,在spring中用于将那些与业务无关但对多个对象产生影响的公共行为和逻辑提取出来,实现公共模块的复用,降低耦合。常见的场景包括公共日志保存和事务处理。
9. 在项目中如何使用到AOP?
回答:我们之前在后台管理系统中使用AOP来记录系统操作日志。主要思路是使用AOP的环绕通知和切点表达式,找到需要记录日志的方法,然后通过环绕通知的参数获取请求方法的参数,例如类信息、方法信息、注解、请求方式等,并将这些参数保存到数据库。
10. Spring中的事务是如何实现的?
回答:本质上是利用AOP完成的。它对方法前后进行拦截,在执行方法前开启事务,在执行完目标后根据执行情况提交或回滚事务。
11. Spring中事务失效的场景有哪些?
回答:
如果方法内部捕获并处理了异常却没有将异常抛出,会导致事务失效。因此,处理异常后应该确保异常能够被抛出。
如果方法抛出检查型异常(checked exception),并且没有在
@Transactional
注解上配置rollbackFor
属性为Exception
,那么异常发生时事务不会回滚。如果事务注解的方法不是公开(public)修饰的,也可能导致事务失效。
自调用:在同一个类中,方法A调用本类的带有
@Transactional
的方法B时,事务不生效(绕过代理直接调用目标方法)。解决方法有,在类内部通过 Spring 容器获取当前对象的代理实例,然后通过代理对象调用目标方法,从而让事务生效。
@Service
public class TransactionService {
@Autowired
private ApplicationContext context;
@Transactional
public void publicMethod() {
// 从 Spring 容器中获取代理对象
TransactionService proxy = context.getBean(TransactionService.class);
proxy.internalMethod(); // 通过代理对象调用方法,事务生效
}
@Transactional
public void internalMethod() {
// 事务在这里生效
}
}
12. Springboot自动配置原理?
回答:原理是基于@SpringBootApplication
注解,它封装了@SpringBootConfiguration
、@EnableAutoConfiguration
和@ComponentScan
。其中@EnableAutoConfiguration
是核心,它通过@Import
导入配置选择器,读取 META-INF/spring.factories 文件中的类名,根据条件注解决定是否将配置类中的Bean导入到Spring容器中。
13. Spring 的常见注解有哪些?
回答:
声明Bean的注解:
@Component
、@Service
、@Repository
、@Controller
依赖注入相关的注解:
@Autowired
、@Qualifier
、@Resource
设置作用域的注解:
@Scope
配置相关注解:
@Configuration
、@ComponentScan
、@Bean
AOP相关注解:
@Aspect
、@Before
、@After
、@Around
、@Pointcut
14. SpringMVC常见的注解有哪些?
回答:
@RequestMapping
:映射请求路径。
@RequestBody
:接收HTTP请求的JSON数据。
@RequestParam
:指定请求参数名称。
@PathVariable
:从请求路径中获取参数。
@ResponseBody
:将Controller方法返回的对象转化为JSON。
@RequestHeader
:获取请求头数据。
@PostMapping
、@GetMapping
等。
15. Springboot常见注解有哪些?
回答:
@SpringBootApplication
:由@SpringBootConfiguration
、@EnableAutoConfiguration
和@ComponentScan
组成。其他注解如
@RestController
、@GetMapping
、@PostMapping
等,用于简化Spring MVC的配置。
16. MyBatis执行流程?
回答:
读取MyBatis配置文件
mybatis-config.xml
。加载其中的xml映射文件。
构造会话工厂
SqlSessionFactory
。会话工厂创建
SqlSession
对象。操作数据库的接口,
Executor
执行器。输入参数的映射。
输出结果的映射。
17. Mybatis是否支持延迟加载?
回答:MyBatis支持延迟加载。但仅支持 association 关联对象和 collection 关联集合对象的延迟加载,这可以通过配置文件中的lazyLoadingEnabled
配置启用或禁用延迟加载。
18. Mybatis延迟加载的原理?
回答:底层原理主要是使用CGLIB动态代理
实现:
使用CGLIB创建目标对象的代理对象。
调用目标方法时,如果发现是null值,则执行SQL查询。
获取数据后,设置属性值并继续查询目标方法。
19. batis的一级、二级缓存用过吗?
回答:MyBatis的一级缓存是基于PerpetualCache
的HashMap本地缓存,作用域为Session,默认开启。二级缓存需要单独开启,作用域为Namespace或mapper,默认也是采用PerpetualCache
,HashMap存储。
20. Mybatis的二级缓存什么时候会清理缓存中的数据?
回答:当作用域(一级缓存Session/二级缓存Namespaces)进行了新增、修改、删除操作后,默认该作用域下所有的select中的缓存将被清空。