之前写的博客太杂,最近想把后端框架的知识点再系统的过一遍,主要是Spring Boot和Mybatis相关,带着自己的理解使用简短的话把一些问题总结一下,尤其是开发中和面试中的高频问题,基础知识点可以参考之前写java后端专栏,这篇不再赘述。
目录
- Spring
- 什么是AOP?底层原理?
- 事务底层原理?多线程事务能不能保证一致性?
- 为什么禁用@Transactional?
- 事务失效场景?
- Bean的生命周期及常用自定义方法
- 有两个相同id的Bean会报错吗?
- 循环依赖相关
- Spring MVC是什么,核心是什么,它的执行流程?
- Spring MVC的控制器(bean)是不是单例模式(是不是线程安全)?存在什么问题?如何解决?
- 起步依赖、自动配置原理;yml等配置文件的配置优先级
- IOC的工作流程;控制反转与依赖注入原理
- 常见注解有哪些? @Autowird 与 @Resource区别;@Component和@Bean区别;@Conditional作用
- 过滤器与拦截器的区别
- 如何解决跨域问题?
- 设计模式
- Mybaits
- MyBatis执行流程
- Mybatis是否支持延迟加载?底层原理是什么?
- 一级、二级缓存;二级缓存什么时候会清理缓存中的数据
- Mybatis实现分页查询的几种方式
Spring
什么是AOP?底层原理?
详细介绍–>SpringBoot 事务与AOP
什么是AOP?
aop是面向切面编程,在spring中用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取公共模块复用,降低耦合,一般比如可以做为公共日志保存,事务处理等
我们当时在后台系统中,就是使用aop来记录了系统的操作日志。主要思路是这样的,使用aop中的环绕通知+切点表达式,这个表达式就是要找到要记录日志的方法,然后通过环绕通知的参数获取请求方法的参数,比如类信息、方法信息、注解、请求方式等,获取到这些参数以后,保存到数
据库。
AOP的底层原理?
Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。
- JDK动态代理只提供接口的代理,不支持类的代理。
-
- JDK会在运行时为目标类生成一个动态代理类$proxy*.class .
-
- 该代理类是实现了接目标类接口,并且代理类会实现接口所有的方法增强代码
-
- 调用时先去调用处理类进行增强,再通过反射的方式进行调用目标方法。从而实现AOP
- 如果代理类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。
-
- CGLIB的底层是通过ASM(一种用于直接生成或修改字节码的框架,全称为 “Abstract Syntax Notation One”)在运行时动态的生成目标类的一个子类。(还有其他相关类)会生成多个。并且会重写父类所有的方法增强代码,
-
- 调用时先通过代理类进行增强,再直接调用父类对应的方法进行调用目标方法。从而实现AOP。
-
-
- CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
-
-
-
- CGLIB除了生成目标子类代理类,还有一个FastClass(路由类),可以(但不是必须))让本类方法调用进行增强,而不会像jdk代理那样本类方法调用增强会失效
-
- jck动态代理生成类速度快,调用慢,cglib生成类速度慢,但后续调用快。就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。
事务底层原理?多线程事务能不能保证一致性?
详细介绍–>SpringBoot 事务与AOP
spring实现的事务本质就是AOP完成,对方法前后进行拦截,在执行方法之前开启事务,在执行完目标方法之后根据执行情况提交或者回滚事务。其中AOP底层是基于JDK动态代理和cglib动态代理实现的,这个上面有介绍。
Spring多线程事务能不能保证一致性
假设A方法被声明了@Transactional,然后在A方法里new一个线程,去执行B方法,那么这种情况下A和B能保证原子性或者数据一致性么?A失败了B会回滚吗?或者B失败了A会回滚吗?
答案:不能。因为Spring底层是使用ThreadLocal来保存事务信息的比如数据库连接Connection,所以一个线程永远只能有一个事务,Spring的事务是无法实现事务一致性的。
解决办法:可以自己解决,比如通过编程式的事务,自己控制提交和回滚,或者说通过分布式事务的思路,2PC,3PC,SAGA,MQ的消息最终一致性都可以解决。
编程式事务:这种方式是通过编程的方式来控制事务的提交和回滚。在Spring中,可以使用TransactionTemplate或者PlatformTransactionManager来实现。例如,你可以在A方法中创建一个新的事务,然后在B方法中使用相同的事务。如果A或B中有任何异常,你可以捕获这个异常并决定是否回滚事务。这种方式需要你手动管理事务,包括开始事务、提交事务、回滚事务等。
分布式事务:这是一种更复杂的解决方案,通常用于处理跨多个数据库或服务的事务。这种方法包括以下几种策略:
2PC(两阶段提交):这是一种原子性协议,它保证了所有参与者要么都提交事务,要么都不提交。它分为两个阶段:准备阶段和提交阶段。在准备阶段,所有参与者都会被询问是否可以提交事务,只有当所有参与者都同意提交事务时,才会进入提交阶段。否则,事务将被回滚。
3PC(三阶段提交):这是两阶段提交的改进版,它添加了一个超时机制,以防止在等待其他参与者响应时发生阻塞。
SAGA:这是一种长寿命事务的解决方案,它将一个大事务分解为多个小事务,每个小事务都可以独立地提交或回滚。如果某个小事务失败,SAGA会执行一系列的补偿操作来保证数据的一致性。
MQ(消息队列):这是一种最终一致性的解决方案,它使用消息队列来保证事务的一致性。如果A方法执行成功,它会发送一个消息到消息队列,然后B方法会监听这个消息队列,当它收到消息时,就会执行相应的操作。
为什么禁用@Transactional?
为什么有些公司禁止使用@Transactional声明式事务?
有几个方面的考虑:
1、在方法上面增加@Transactional的声明式事务,如果一个方法中存在较多耗时的操作,很容易引发长事务的问题,而长事务会带来锁的竞争,影响性能,同时也会导致数据库的连接池被消耗尽,影响程序正常执行
2、如果方法存在嵌套调用,而被嵌套调用的方法也声明了@Transactional事务,那么这个时候就会出现事务嵌套调用的行为,容易引起事务混乱,程序运行结果出现异常等问题
3、@Transactional的声明式事务是将事务控制逻辑放在注解里,如果项目复杂度增加,事务控制会变得更加复杂,导致代码的可读性和维护性下降,所以为了避免这类问题,有些公司会推荐使用编程式事务,这样可以更加灵活的去控制事务的范围,减少事务的锁定时间,提高系统性能
事务失效场景?
详细介绍–>SpringBoot 事务与AOP
Spirng通过@transactional注解来进行事务控制,但很多场景会导致事务失效。
第一个,如果方法上异常捕获处理,自己try catch处理了异常,没有抛出,就会导致事务失效,所以一般处理了异常以后,别忘了抛出去就行了
第二个,如果报RuntimeException以外的错也会导致事务失效,若在@Transactional上配置rollbackFor属性为Exception,这样别管是什么异常,都会回滚事务
默认情况下,只有出现RuntimeException(运行时异常)才会回滚事务。假如我们想让所有的异常都回滚,需要来配置@Transactional注解当中的rollbackFor属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务
第三,如果方法是private修饰的,也会导致事务失效,因为AOP底层是基于cglib动态代理实现的,子类是不能重载父类的private方法的,所以无法很好的利用代理,会导致@Transactional失效。
ps:因为Spring事务是基于代理来实现的,所以某个加了@Transactional的方法只有是代理对象调用时,那么这个注解才会生效,所以如果是被代理对象来调用这个方法,那么@Transactional是不会生效的。
第四,@Transactional注解在了final或static修饰的方法上。因为被final修饰的方法是无法被重写的,所以代理对象是无法调用的。而static修饰的方法属于类对象不属于对象实例,所以代理对象也是无法调用的。
第五,当前类没有被Spring容器所管理,没有配置成bean。如果当前类没有被Spring 容器所管理,那么Spring就无法为该类生成代理对象,从而Spring 的事务会失效。
第六,同一个类中方法的调用,导致的 AOP失效,从而导致@Transactional注解失效。这也是我们开发中最容易犯错的一种场景。
同一个类中的方法调用,属于this 调用,并不会使用代理对象,所以 AOP会失效。没有办法通过动态代理给你进行增强。不过,该问题可以使用如下方式来解决,因为我们从spring容器中自动装配的bean假如实现了声明式事务或者AOP,那么就会为你创建动态代理。
除了上面的自动装配,还可以这样:((UserService)AopContext.currentProxy()).methodB()
也可以拿到当前的动态代理对象,这个方法的原理是当我们调用了一个动态代理的方法(methodA()),会把当前的动态代理对象存入到currentProxy的ThreadLocal中,那么此时再通过AopContext获得currentProxy,就可以拿到当前正在调用的动态代理对象,这种写法呢比自己注入进来更加优雅,且不会存在循环依赖问题。
第七,多个事务方法不在同一个线程内执行。如下代码片段中,事务方法addUser()中调用了另外一个事务方法roleService.addRole(),注意,这两个方法不是在同一个线程中执行的!这会导致什么问题呢?我们上面刚刚说过了,spring不能保证多线程下的事务一致性,这两个事务方法获取到的数据库连接是2个不同的数据库连接!不同的数据库连接会导致事务失效!解决办法在上面的Spring多线程事务能不能保证一致性中有介绍。
Bean的生命周期及常用自定义方法
详细介绍–>SpingBoot原理
简单来说分为五步:
- 1.实例化:当容器需要初始化一个尚未初始化的Bean,或者初始化Bean时需要注入另一个尚未初始化的依赖时,容器会调用doCreateBean()方法进行实例化。实际上,这是通过反射的方式创建出一个Bean对象。
- 2.属性赋值:Bean实例创建完成后,接下来是为这个Bean对象进行属性填充,也就是注入这个Bean依赖的其他Bean对象。
- 3.初始化 :在属性填充完成后,进行Bean的初始化操作。初始化阶段包括以下几个步骤:
-
- 3.1.执行Aware接口的方法:Spring会检测该对象是否实现了xxx Aware接口,通过Aware类型的接口,可以让我们获取Spring容器的一些资源。例如,实现BeanNameAware接口可以获取到Bean的名称,实现BeanFactoryAware接口可以获取到工厂对象BeanFactory等。
-
- 3.2.判断Bean是否实现了InitializingBean接口,如果实现了,将会执行InitializingBean的afterPropertiesSet()初始化方法。
-
- 3.3.执行用户自定义的初始化方法(如果有指定的话),如init-method等。
-
- 3.4.执行BeanPostProcessor的后置处理方法postProcessAfterInitialization()。
- 4.使用:初始化完成后,Bean就成功创建了,之后就可以使用这个Bean。
- 5.销毁:当Bean不再需要时,会进行销毁操作。销毁阶段包括以下步骤:
-
- 5.1.判断Bean是否实现了DestructionAwareBeanPostProcessor接口,如果实现了,则会执行DestructionAwareBeanPostProcessor后置处理器的销毁回调方法。
-
- 5.2.判断Bean是否实现了DisposableBean接口,如果实现了,将会调用其实现的destroy()方法。
-
- 5.3后判断这个Bean是否配置了destroy-method等自定义的销毁方法,如果有配置,则会自动调用其配置的销毁方法。
如果详细一点介绍的话可以分为五个阶段:
创建前准备:Bean在开始加载之前要从上下文和一些配置中去解析并且查找Bean有关