之前写的博客太杂,最近想把后端框架的知识点再系统的过一遍,主要是Spring Boot和Mybatis相关,带着自己的理解使用简短的话把一些问题总结一下,尤其是开发中和面试中的高频问题,基础知识点可以参考之前写java后端专栏,这篇不再赘述。
目录
- Spring
-
- 什么是AOP?底层原理?
- IOC的工作流程;控制反转与依赖注入原理
- 事务底层原理?多线程事务能不能保证一致性?
- 为什么禁用@Transactional?
- 事务失效场景?
- Bean的生命周期及常用自定义方法
- 有两个相同id的Bean会报错吗?
- 循环依赖相关
- Spring MVC是什么,核心是什么,它的执行流程?
- Spring MVC的控制器(bean)是不是单例模式(是不是线程安全)?存在什么问题?如何解决?
- 起步依赖、自动配置原理;yml等配置文件的配置优先级
- 常见注解有哪些? @Autowird 与 @Resource区别;@Component和@Bean区别;@Conditional作用
- 过滤器与拦截器的区别
- 如何解决跨域问题?
- 设计模式
- Mybaits
- Mybaits-Plus
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 版本的升级,这个优势更加明显。
IOC的工作流程;控制反转与依赖注入原理
详细介绍–>SpringBoot Web 分层解耦
Spring IOC的工作流程
回答这个问题需要回答:1、IOC是什么 2、Bean的声明方式 3、IOC的工作流程(面试时间短可以直接回答这个)
1、控制反转(Inversion of Control, IoC),核心思想是把对象的管理权限交给了容器,应用程序如果需要使用某个对象的实例直接从IOC容器里去获取就行。这种设计的好处在于降低了程序里对象与对象之间的耦合性,使得程序的整个体系结构变得更加灵活。
2、Spring里提供了很多方式去声明一个Bean,比如在XML配置文件里通过<bean>标签、通过@Service等注解、通过配置类里@Bean注解去声明等等。Spring在启动时会去解析这些Bean然后保存到IOC容器里。
3、IOC的工作流程大致可以分为两个阶段。第一个阶段是IOC容器初始化阶段,这个阶段主要是根据程序里面定义的XML或者注解等Bean的声明方式,通过解析和加载后生成BeanDefinition,然后把BeanDefinition注册到IOC容器里,通过XML或者注解等声明的Bean都会解析得到一个BeanDefinition实体,里面包含bean的一些定义和基本属性。最后把这个BeanDefinition保存到一个Map集合里从而去完成IOC的初始化。
IOC容器的作用就是对这些注册的Bean的定义信息进行处理和维护,它是IOC容器控制反转的一个核心。第二个阶段是完成Bean的初始化和依赖注入,第二阶段会做两个事情。第一个是通过反射去针对没有设置lazy-init属性的单例bean进行初始化,第二个是完成Bean的依赖注入。最后一个阶段就是Bean的使用,通常我们通过@Autowired注解,或者通过BeanFactory.getBean()从IOC容器里去获取指定bean的实例。另外针对设置了懒加载属性和非单例bean的实例化是在每一次获取bean对象的时候调用bean的初始化方法来完成实例化的,并且Spring IOC容器不会去管理这些Bean。
控制反转与依赖注入原理:
简介
控制反转(Inversion of Control, IoC):是一种将对象的创建和初始化过程从应用程序代码中抽离出来,并将这部分控制移交给 Spring 容器的设计模式。
依赖注入(Dependency Injection, DI):是实现 IoC 的一种方式。 DI 通过注入所需的依赖对象来实现对象之间的松耦合。这意味着类不再直接创建其依赖项,而是通过外部提供这些依赖项。
目的:高内聚低耦合
- 高内聚指的是:一个模块中各个元素之间的联系的紧密程度,如果各个元素(语句、程序段)之间的联系程度越高,则内聚性越高,即 “高内聚”。
- 低耦合指的是:软件中各个层、模块之间的依赖关联程序越低越好。
实现原理
-
IoC 容器:Spring 提供了一个 IoC 容器,通常是 ApplicationContext。这个容器负责管理 beans 的生命周期和配置。
-
Bean:在 Spring 中,每一个被容器管理的对象都称为一个 bean。你可以使用各种注解(如 @Component, @Service, @Repository, @Controller 等)或者 XML 配置文件来声明一个 bean。
-
依赖注入的方式:
-
- 构造器注入:通过类的构造器注入依赖
-
- Setter 注入:通过 setter 方法注入依赖
-
- 字段注入:直接在字段上注入依赖。
-
自动装配(@Autowired):Spring 通过 @Autowired 注解自动寻找和装配需要的依赖项。Spring 容器会在适当的(构造器、setter、字段)上注入所需的 bean 实例。
-
配置类和注解:在 Spring Boot 中,通常使用配置类(使用 @Configuration 注解的类)和 Java 注解来简化配置。例如,@SpringBootApplication 是一个常用的复合注解,包含了@Configuration, @EnableAutoConfiguration, 和 @ComponentScan 等注解
事务底层原理?多线程事务能不能保证一致性?
详细介绍–>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对象。</