Spring
- Spring
- 1、介绍一下Spring 的 IOC
- 2、将一个类声明为 Bean 的注解有哪些
- 3、Bean 的作用域有哪些
- 4、Spring 框架中的 Bean 是线程安全的吗
- 5、Spring 容器对象的懒加载
- 6、Spring 容器中的 bean 生命周期
- 7、谈谈自己对于 Spring DI 的了解
- 8、注入 Bean 的注解有哪些
- Spring Boot如何让你的bean在其他bean之前加载
- 9、介绍一下 Spring 的 AOP
- 为什么不建议直接使用Springl的@Async
- 10、Spring 通知有哪些类型
- 11、多个切面的执行顺序如何控制
- 12、Spring 事务中哪几种事务传播行为
- 13、BeanFactory 和 FactroyBean 的关系
- 14、Spring 如何解决循环依赖问题
- 15、Spring 中的设计模式
- Spring MVC
- MyBatis
- 1、什么是 MyBatis
- 2、为什么说 MyBatis 是半ORM框架,与 Hibernate 有哪些不同
- 3、#{ } 和 ${ } 的区别是什么
- 4、MyBatis 是怎么解决实体类中的属性名和表中的字段名不一样的问题
- 5、如何在 Mapper 中传递多个参数
- 6、MyBatis 的接口绑定是什么,有哪些绑定方式
- 7、在MyBatis 中使用 Mapper 接口开发时有哪些要求
- 8、MyBatis 中 Mapper 接口中的方法支持重载么
- 9、MyBatis 的动态SQL是什么,主要标签有哪些
- 10、MyBatis 映射文件中,A 标签通过 include 引用了 B 标签的内容,B 标签能否定义在 A 标签的后面
- 11、MyBatis 的 Mapper 接口工作原理是什么
- 12、MyBatis 的工作原理是什么
- 13、MyBatis 中一对一查询、一对多查询是怎么实现的
- 14、MyBatis 的分页方法有哪些
- 15、MyBatis 中都有哪些Executor执行器,它们之间的区别是什么
- 16、SpringBoot + Mybatis 一级缓存和二级缓存详解
- 17、MyBatis 是否支持延迟加载,如果支持,它的实现原理是什么
- 18、MyBatis 是如何防止sql注入的
Spring
1、介绍一下Spring 的 IOC
所谓的 IOC,就是控制反转的意思。何为控制反转?我们可以根据字面意思理解,就是对于某个东西A,原来的控制权在使用方B,B想用就能用,不想用就不用。现在把控制权交还给了A,只有A给了才能用,这样就是控制反转了。
可能说的有点抽象,更具体一点呢,我们拿代码来说话:
class A {}
class B {
// B需要将A的实例new出来,也就是我们说的控制
private A a = new A();
public void use() {
System.out.print(a);
}
}
当有了IOC后
@Component // 说明A自己控制自己,把自己初始化出来,注入给了容器
class A {}
class B {
// B不需要控制a,直接使用。如果A没有把自己注入给容器,B就不能使用
@Resource
private A a;
public void use() {
System.out.print(a);
}
}
也就是说,没有Spring的话,我们要使用的对象,需要我们自己创建,而有了Spring的IOC之后,对象由IOC容
器创建并管理,我们只需要在想要使用的时候从容器中获取就行了。
2、将一个类声明为 Bean 的注解有哪些
- @Controller : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。
- @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
- @Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。
- @Component :通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层使用。
3、Bean 的作用域有哪些
Spring 中 Bean 的作用域通常有下面几种:
- singleton : 只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
- prototype : 每次获取都会创建一个新的 bean 实例,连续 getBean() 两次,得到的是不同的 Bean 实例。
- request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
- session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
- application/global-session (仅 Web 应用可用): 每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
- websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。
如何配置 bean 的作用域呢
xml 方式:
<bean id="..." class="..." scope="singleton"></bean>
注解方式:
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Person personPrototype() {
return new Person();
}
单例和多例优缺点
- 单例: 所有请求用同一个对象来处理,通过单例模式,可以保证系统中一个类只有一个实例。
- 多例:每一个请求用一个新的对象来处理。
- 单例优点降低了实例创建和销毁所占用的资源,缺点线程共享一个实体,会发生线程安全问题。
- 多例线程之间数据隔离,所以不会有线程安全问题,但是频繁的实例创建和销毁会带来资源的大量浪费。
4、Spring 框架中的 Bean 是线程安全的吗
平常的bean它分为单例bean和多例bean。单例bean创建一个全局共享的实例,多例bean使用时候创建一个新的对象,线程之间不存在bean共享的问题,单例bean是所有线程共享一个实例,这样单例bean就可能会产生线程安全问题。但是也不是绝对的。单例bean分为有状态有无状态两种。
- 有状态Bean:多线程操作中如果需要对bean中的成员变量进行数据更新操作,是非线程安全。
- 无状态Bean:多线程操作中没有成员变量或者只会对bean成员变量进行查询操作,不会修改操作,是线程安全(比如 Controller、Dao、Service等)。
处理有状态的单例bean线程安全问题,常见的有两种解决办法:
- 改为多例bean
- 在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)
5、Spring 容器对象的懒加载
意思为控制对象的创建的时机,如果Spring容器创建,对象立即创建,则称为立即加载。如果 Spring 容器创建,对象在被使用的时候创建,则称为懒加载。
注解:@Lazy 表示为懒加载
懒加载XML写法
<!-- 懒加载的局部的写法 -->
<bean id="hello" class="com.ioc.Hello" scope="singleton" lazy-init="true"></bean>
<!-- 懒加载的全局的写法 -->
<beans default-lazy-init="true"></beans>
lazy-init 是否懒加载 | scope 单例多例 | 对象的创建结果 |
---|---|---|
true | singleton | 懒加载 |
true | prototype | 懒加载 |
default/flase | singleton | 立即加载 |
default/flase | prototype | 懒加载 |
只要对象是多例模式 则都是懒加载! 在单例模式中控制懒加载才有效
6、Spring 容器中的 bean 生命周期
- 获取元数据信息:Spring容器首先会读取XML配置文件或注解配置,得到指定bean的元数据信息(BeanDefinition),包括bean的名称、类名、作用域等等。
- 创建对象:Spring容器使用反射机制创建指定bean的实例。这通常通过调用类的构造函数来完成。
- 属性赋值:Spring容器使用populateBean方法将创建的对象的属性赋值。这包括使用setter方法设置属性值、调用任何实现了InitializingBean接口的afterPropertiesSet方法以及自定义的init-method方法。
- Aware接口:如果bean实现了相应的Aware接口,Spring容器会在创建bean时自动调用相关的方法,例如BeanNameAware、BeanFactoryAware和ApplicationContextAware等接口。
- 前置处理:如果在Spring容器中定义了任何实现了BeanPostProcessor接口的类,它们就会在bean初始化之前或之后进行前置或后置处理。
- AOP代理:如果该bean已经与AOP相关联,则Spring容器将为该bean创建代理对象,以实现AOP功能。
- 初始化方法:在bean完成所有其他配置和属性设置后,执行任何自定义的初始化方法。
- 后置处理:与前置处理类似,如果在Spring容器中定义了任何实现了BeanPostProcessor接口的类,则它们将在bean初始化之前或之后进行后置处理。
- 销毁方法:在bean不再需要时,Spring容器将执行任何自定义的销毁方法以及实现了DisposableBean接口的destroy方法。
7、谈谈自己对于 Spring DI 的了解
从spring容器中取出容器中的对象,然后把对象注入到需要的地方。
Spring支持哪些注入方式
- 字段注入
@Autowired
private Bean bean;
- 构造器注入
@Component
class Test {
private final Bean bean;
@Autowired
public Test(Bean bean){
this.bean bean;
}
}
- setteri注入
@Component
class Test {
private Bean bean;
@Autowired
public void setBean(Bean bean){
this.bean bean;
}
}
使用构造器注入可能有哪些问题
如果我们两个bean循环依赖的话,构造器注入就会抛出异常:
@Component
public class BeanTwo implements Bean{
Bean beanone;
public BeanTwo(Bean beanone){
this.beanOne beanone;
}
}
@Component
public class Beanone implements Bean{
Bean beanTwo;
public BeanOne(Bean beanTwo){
this.beanTwo beanTwo;
}
}
如果两个类彼此循环引用,那说明代码的设计一定是有问题的。如果临时解决不了,我们可以在某一个构造器中加
入@Lazy注解,让一个类延迟初始化即可。
@Component
public class Beanone implements Bean{
Bean beanTwo;
@Lazy
public Beanone(Bean beanTwo){
this.beanTwo beanTwo;
}
}
8、注入 Bean 的注解有哪些
Spring 内置的 @Autowired 以及 JDK 内置的 @Resource 和 @Inject 都可以用于注入 Bean。
Annotaion | Package | Source |
---|---|---|
@Autowired | org.springframework.bean.factory | Spring 2.5+ |
@Resource | javax.annotation | Java JSR-250 |
@Inject | javax.inject | Java JSR-250 |
@Autowired 和@Resource使用的比较多一些。
@Resource( name = “value” )
1. 按照名字装配Bean,即会按照name属性的值来找到具有相同id的Bean并注入。如果没有指定name属性(@Resource 样式),则会根据这个将要被注入的变量名进行注入(value)。如果变量名在容器中也不存在,就按照变量类型注入,如果类型不存在或者存在多个实现类情况下抛出异常。
@Resource
private UserMapper userMapper;
@Autowired @Qualifier( “value” )
1.默认属性required= true(属性必须存储对象,不能为null,false可以),按照名字装配Bean,即会按照value值来找到具有相同id的Bean并注入。如果没有指定vaule值(@Autowired 样式),按照类型注入,类型不存在抛出异常,类型存在,如果类型只有一个实现类就按照类型注入,如果类型有多个实现类先按类型匹配再按变量名称匹配,再匹配不到抛出异常。
@Autowired @Qualifier("yang")
private UserMapper userMapper;
Spring Boot如何让你的bean在其他bean之前加载
- 直接依赖某Bean
@Component
public class A {
@Autowired
private B b;
}
如上,在加载Bean A的时候,一定会先初始化Bean B
- DependsOn
对于应用之外的二方或者三方库来说,因为我们不能修改外部库的代码,如果想要二方库的Ben在初始化之前就
初始化我们内部的某个bean,就不能用第一种直接依赖的方式,可以使用@Dependst0n注解来完成。
@Configuration
public class BeanorderConfiguration {
@Bean
@Dependson("beanB")
public BeanA beanA(){
return new BeanA();
}
}
9、介绍一下 Spring 的 AOP
首先,在面向切面编程的思想里面,把功能分为核心业务功能和周边功能。
- 所谓的核心业务,工作中做的最多的就是增删改查,增删改查都叫核心业务。
- 所谓的周边功能,比如性能统计,日志记录,事务管理等等。
核心业务功能和切面功能分别独立进行开发,在程序运行期间,在不修改核心业务的情况下,然后把切面功能和核心业务功能 “编织” 在一起,这就叫AOP。
Spring AOP 有如下概念
术语 | 翻译 | 释义 |
---|---|---|
Aspect | 切面 | 切面由切入点和通知组成,它既包含了横切逻辑的定义,也包括了切入点的定义,比如说事务处理和日志处理可以理解为两个切面 |
PointCut | 切入点 | 切入点是对连接点进行拦截的条件定义,决定通知应该作用于截哪些方法 |
Advice | 通知 | 通知定义了通过切入点拦截后,应该在连接点做什么,是切面的具体行为 |
Target | 目标对象 | 目标对象指将要被增强的对象,即包含主业务逻辑的类对象。或者说是被一个或者多个切面所通知的对象 |
JoinPoint | 连接点 | 连接点是程序在运行时的执行点,这个点可以是正在执行的方法,或者是正在抛出的异常。因为Spring.只支持方法类型的连接点,所以在Spring中连接点就是运行时刻被拦截到的方法。 |
Weaving | 织入 | 织入是将切面和业务逻辑对象连接起来,并创建通知代理的过程。在编时进行织入就是静态代理,而在运行时进行织入则是动态代理 |
AOP的动态代理技术
- JDK代理:基于接口的动态代理技术(默认,有接口时用)
- CGLIB代理:基于父类的动态代理技术(没接口时用)
jdk动态代理 和cglib动态代理的区别
- jdk动态代理目标业务类必须有接口,cglib动态代理业务类有无接口皆可。
- jdk动态代理必须实现InvocationHandler接口,cglib动态代理必须实现MethodInterceptor接口。
- jdk动态代理代理类和目标业务类是兄弟关系,因为隶属于同一个接口,cglib动态代理代理类和目标业务类是父子关系,业务类是父类,业务类不能是final类,代理类是子类。
- jdk动态代理创建代理类快,执行代理类慢,cglib动态代理创建代理类慢,执行代理类快。
Spring的AOP在什么场景下会失效
为什么不建议直接使用Springl的@Async
10、Spring 通知有哪些类型
- 前置通知(Before Advice):在连接点(Join point)之前执行的通知。
- 后置通知(After Advice):当连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
- 环绕通知(Around Advice):包围一个连接点的通知,这是最强大的一种通知类型。 环绕通知可以在方法调用前后完成自定义的行为。它也可以选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。
- 后置返回(AfterReturning Advice):在连接点正常完成后执行的通知(如果连接点抛出异常,则不执行)。
- 后置异常通知(AfterThrowing advice):在方法抛出异常退出时执行的通知。
正常情况:环绕前置 ==> 前置通知@Before ==> 目标方法执行 ==> 后置返回通知@AfterReturning ==> 后置通知@After ==> 环绕返回 ==> 环绕最终
异常情况:环绕通知 ==> 前置通知@Before ==> 目标方法执行 ==> 后置异常通知@AfterThrowing ==> 后置通知@After ==> 环绕异常 ==> 环绕最终
11、多个切面的执行顺序如何控制
- 通常使用@Order 注解直接定义切面顺序
// 值越小优先级越高
@Order(3)
@Component
@Aspect
public class LoggingAspect implements Ordered {
- 实现Ordered 接口重写 getOrder 方法。
@Component
@Aspect
public class LoggingAspect implements Ordered {
// ....
@Override
public int getOrder() {
// 返回值越小优先级越高
return 1;
}
}
12、Spring 事务中哪几种事务传播行为
7种传播机制的约束条件
约束条件 | 说明 |
---|---|
REQUIRED | 如果当前没有事务,则新建事务,如果当前存在事务,则加入当前事务,合并成一个事务 |
REQUIRES_NEW | 新建事务,如果当前存在事务,则把当前事务挂起,新建事务执行完后再恢复当前事务 |
NESTED | 如果当前没有事务,则新建事务,如果当前存在事务,则创建一个当前事务的子事务(嵌套事务),子事务不能单独提交,只能和父事务一起提交 |
SUPPORTS | 支持当前事务,如果当前没有事务,以非事务的方式执行 |
NOT_SUPPORTED | 以非事务方式执行,如果存在当前事务就把当前事务挂起 |
NEVER | 以非事务方式执行,如果当前存在事务就抛异常 |
MANDATORY | 使用当前事务,如果当前没有事务,就抛异常 |
@Transactional的几种失效场景
- 访问权限问题 (只有public方法会生效)
spring要求被代理方法必须得是public的,在AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。
- 方法用 final 修饰,不会生效。
有时候,某个方法不想被子类重新,这时可以将该方法定义成final的。普通方法这样定义是没问题的,但如果将事务方法定义成final会失效,spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。注意:如果某个方法是static的,同样无法通过动态代理,变成事务方法。
- 同一个类中的方法直接内部调用,会导致事务失效。
事务的底层是Spring AOP来实现的,这种自调用的方式是不满足AOP的动态代理的,如果你想要让这个事务生效,你在A类A方法中调用的时候不能采用 addMoney1(yang, i) 的方式,应该用A类的对象调用才行。
- 类本身未被spring管理。
在我们平时开发过程中,有个细节很容易被忽略。即使用spring事务的前提是:对象要被spring管理,需要创建bean实例。
- 多线程调用。
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
new Thread(() -> {
roleService.doOtherThing();
}).start();
}
}
@Service
public class RoleService {
@Transactional
public void doOtherThing() {
System.out.println("保存role表数据");
}
}
从上面的例子中,我们可以看到事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。
- 存储引擎不支持事务。
myisam好用,但有个很致命的问题是:不支持事务。如果只是单表操作还好,不会出现太大的问题。但如果需要跨多张表操作,由于其不支持事务,数据极有可能会出现不完整的情况。此外,myisam还不支持行锁和外键。所以在实际业务场景中,myisam使用的并不多。在mysql5以后,myisam已经逐渐退出了历史的舞台,取而代之的是innodb。
- 自己吞了异常。
spring的事务是在调用业务方法之前开始的,业务方法执行完毕之后才执行commit or rollback,事务是否执行取决于是否抛出runtime异常。如果抛出runtime exception 并在你的业务方法中没有catch到的话,事务会回滚。尽量不要写 try-catch 如果要写的同时还要保证事务回滚可以尝试在catch最后一行throw一个runtimeException或者手动回滚。
声明式事务与编程式事务
① 声明式事务
通常情况下,我们会在方法上@Transactional注解,填加事务功能,比如:
@Service
public class UserService {
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
query1();
query2();
query3();
roleService.save(userModel);
update(userModel);
}
}
@Service
public class RoleService {
@Autowired
private RoleService roleService;
@Transactional
public void save(UserModel userModel) throws Exception {
query4();
query5();
query6();
saveData(userModel);
}
}
但@Transactional注解,如果被加到方法上,有个缺点就是整个方法都包含在事务当中了。
上面的这个例子中,在UserService类中,其实只有这两行才需要事务:
roleService.save(userModel);
update(userModel);
在RoleService类中,只有这一行需要事务:
saveData(userModel);
现在的这种写法,会导致所有的query方法也被包含在同一个事务当中。如果query方法非常多,调用层级很深,而且有部分查询方法比较耗时的话,会造成整个事务非常耗时,而从造成大事务问题。
② 编程式事务
- @Transactional注解是通过Spring的AOP起作用的,但是如果使用不当,事务功能可能会失效。
- @Transactional注解一般加在某个业务方法上,会导致整个业务方法都在这个事务中,粒度太大,不好控制事务范围。
上面的这些内容都是基于@Transactional注解的,主要讲的是它的事务问题,我们把这种事务叫做:声明式事务。
其实,spring还提供了另外一种创建事务的方式,即通过手动编写代码实现的事务,我们把这种事务叫做:编程式事务。例如:
@Autowired
private TransactionTemplate transactionTemplate;
public void save(final User user) {
queryData1();
queryData2();
transactionTemplate.execute(transactionStatus -> {
addData1();
updateData2();
return Boolean.TRUE;
});
}
在spring中为了支持编程式事务,专门提供了一个类:TransactionTemplate,在它的execute方法中,就实现了事务的功能。
相较于@Transactional注解声明式事务,更建议大家使用,基于TransactionTemplate的编程式事务。主要原因如下:
- 避免由于spring aop问题,导致事务失效的问题。
- 能够更小粒度的控制事务的范围,更直观。
建议在项目中少使用@Transactional注解开启事务。但并不是说一定不能用它,如果项目中有些业务逻辑比较简单,而且不经常变动,使用@Transactional注解开启事务开启事务也无妨,因为它更简单,开发效率更高,但是千万要小心事务失效的问题。
事务中避免远程调用
我们在接口中调用其他系统的接口是不能避免的,由于网络不稳定,这种远程调的响应时间可能比较长,如果远程调用的代码放在某个事物中,这个事物就可能是大事务。当然,远程调用不仅仅是指调用接口,还有包括:发MQ消息,或者连接redis、mongodb保存数据等。
13、BeanFactory 和 FactroyBean 的关系
从类名后缀就可以体现这两个类的作用,BeanFactory 是一个Bean工厂,FactoryBean 是一个java bean 明白了这两点我们再来详细介绍下 BeanFactory 和 FactoryBean。
BeanFactory
BeanFactory 是 IOC 容器的底层实现接口,Spring 不允许我们直接操作 BeanFactory 工厂,所以 BeanFactory 接口又衍生很多接口,其中我们经常用到的是 ApplicationContext 接口,这个接口此接口继承 BeanFactory 接口,包含 BeanFactory 的所有功能,同时还进行更多的扩展。
FileSystemXmlApplicationContext 和 ClassPathXmlApplicationContext:是用来读取xml文件创建bean对象
ClassPathXmlApplicationContext:读取类路径下xml 创建bean
FileSystemXmlApplicationContext:读取文件系统下xml创建bean
AnnotationConfigApplicationContext:主要是注解开发获取ioc中的bean实例
使用
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(TestApplication.class);
applicationContext.getBean("yang");
FactoryBean
工厂 Bean 跟普通 Bean 不同,其返回的对象不是指定类的一个实例,,其返回的是该工厂 Bean 的 getObject 方法所返回的对象。
- T getObject():返回由FactoryBean创建的bean实例,如果isSingleton()返回true,那么该实例会放到Spring容器中单实例缓存池中
- Class getObjectType():返回FactoryBean创建的bean实例的类型
- boolean isSingleton():返回由FactoryBean创建的bean实例的作用域是singleton还是prototype
演示
/**
* @Description: 创建一个Spring定义的FactoryBean,T(泛型):指定我们要创建什么类型的对象
* @Author: yangjj_tc
* @Date: 2023/5/9 0:23
*/
public class Yang implements FactoryBean<TopCharts> {
// 返回一个Color对象,这个对象会添加到容器中
@Override
public TopCharts getObject() throws Exception {
System.out.println("TopCharts...getObject...");
return new TopCharts();
}
// 返回这个对象的类型
@Override
public Class<?> getObjectType() {
return TopCharts.class;
}
// 是单例吗?
// 如果返回true,那么代表这个bean是单实例,在容器中只会保存一份;
// 如果返回false,那么代表这个bean是多实例,每次获取都会创建一个新的bean
@Override
public boolean isSingleton() {
return FactoryBean.super.isSingleton();
}
}
使用 getBean 返回 getObject 中的对象
@Configuration
public class TestYang {
@Bean
public Yang yang() {
return new Yang();
}
@Test
public void testImport() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(TestApplication.class);
// 工厂bean获取的是调用getObject方法创建的对象
System.out.println(applicationContext.getBean("yang").getClass());
System.out.println(applicationContext.getBean("&yang"));
}
}
TopCharts...getObject...
class com.example.test.entity.TopCharts
com.example.test.controller.Yang@6fca5907
14、Spring 如何解决循环依赖问题
一个或多个对象之间存在直接或间接的依赖关系,这种依赖关系构成一个环形调用,有下面 3 种方式。
我们看一个简单的 Demo,对标“情况 2”。
@Service
public class Louzai1 {
@Autowired
private Louzai2 louzai2;
public void test1() {
}
}
@Service
public class Louzai2 {
@Autowired
private Louzai1 louzai1;
public void test2() {
}
}
这是一个经典的循环依赖,它能正常运行,后面我们会通过源码的角度,解读整体的执行流程。
解读源码流程之前,spring 内部的三级缓存逻辑必须了解,要不然后面看代码会蒙圈。
- 第一级缓存:singletonObjects,存储完整的Bean实例,这些实例是可以直接被使用的。
- 第二级缓存:earlySingletonObjects,存储的是实例化以后,但是还没有设置属性值的Bean实例,也就是Bean里面的依赖注入还没有做。·
- 第三级缓存:singletonFactories,用于保存 bean 创建工厂,以便后面有机会创建代理对象。用来存放Bean工厂,它主要用来生成原始Bean对象并且放到第二级缓存里面。
这是最核心,我们直接上源码:
执行逻辑:
- 先从“第一级缓存”找对象,有就返回,没有就找“二级缓存”
- 找“二级缓存”,有就返回,没有就找“三级缓存”
- 找“三级缓存”,找到了,就获取对象,放到“二级缓存”,从“三级缓存”移除。
原理执行流程
我把“情况 2”执行的流程分解为下面 3 步
整个执行逻辑如下:
- 在第一层中,先去获取 A 的 Bean,发现没有就准备去创建一个,然后将 A 的代理工厂放入“三级缓存”(这个 A 其实是一个半成品,还没有对里面的属性进行注入),但是 A 依赖 B 的创建,就必须先去创建 B;
- 在第二层中,准备创建 B,发现 B 又依赖 A,需要先去创建 A;
- 在第三层中,去创建 A,因为第一层已经创建了 A 的代理工厂,直接从“三级缓存”中拿到 A 的代理工厂,获取 A 的代理对象,放入“二级缓存”,并清除“三级缓存”;
- 回到第二层,现在有了 A 的代理对象,对 A 的依赖完美解决(这里的 A 仍然是个半成品),B 初始化成功;
- 回到第一层,现在 B 初始化成功,完成 A 对象的属性注入,然后再填充 A 的其它属性,以及 A 的其它步骤(包括 AOP),完成对 A 完整的初始化功能(这里的 A 才是完整的 Bean)。
- 将 A 放入“一级缓存”。
15、Spring 中的设计模式
(1)工厂模式:Spring使用工厂模式,通过BeanFactory和ApplicationContext来创建对象
(2)单例模式:Bean默认为单例模式
(3)策略模式:例如Resource的实现类,针对不同的资源文件,实现了不同方式的资源获取策略
(4)代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术
(5)模板方法:可以将相同部分的代码放在父类中,而将不同的代码放入不同的子类中,用来解决代码重复的问题。比如RestTemplate, JmsTemplate, JpaTemplate
(6)适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式,Spring MVC中也是用到了适配器模式适配Controller
(7)观察者模式:Spring事件驱动模型就是观察者模式的一个经典应用。
(8)桥接模式:可以根据客户的需求能够动态切换不同的数据源。比如我们的项目需要连接多个数据库,客户在每次访问中根据需要会去访问不同的数据库
Spring MVC
1、说说自己对于 Spring MVC 了解
MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。
MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring MVC 可以帮助我们进行更简洁的 Web 层的开发,并且它天生与 Spring 框架集成。Spring MVC 下我们一般把后端项目分为Controller 层(控制层,返回数据给前台页面)、 Service 层(处理业务)、Dao 层(数据库操作)、Entity 层(实体类)。
2、Spring MVC 执行流程
- 用户点击某个请求路径,发起一个 HTTP request 请求,该请求会被提交到 Dispatcher Servlet(前端控制器)
- 由 Dispatcher Servlet 调用 Handler Mapping(处理器映射器),并返回一个执行链(Handler Execution Chain)
- Dispatcher Servlet 将 Handler Mapping 返回的执行链中的 Handler 信息发送给 Handler Adapter(处理器适配器)
- HandlerAdapter 根据 Handler 信息找到并执行相应的 Handler(常称为 Controller
- Handler 执行完毕后会返回给 HandlerAdapter 一个 ModelAndView 对象
- HandlerAdapter 接收到 ModelAndView 对象后,将其返回给 DispatcherServlet
- DispatcherServlet 接收到 ModelAndView 对象后,会请求 ViewResolver(视图解析器)对视图进行解析
- ViewResolver 根据 View 信息匹配到相应的视图结果,并返回给 DispatcherServlet
- DispatcherServlet 接收到具体的 View 视图后,进行视图渲染,将 Model 中的模型数据填充到 View 视图中的 request 域,生成最终的 View(视图)
- 视图负责将结果显示到浏览器(客户端)
3、 Spring MVC 常用的注解有哪些
@RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。
@RequestBody:注解实现接收http请求的json数据,将json转换为java对象。
@ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。
Sping MVC 中的控制器的注解一般用@Controller注解,也可以使用@RestController,@RestController注解相当于@ResponseBody + @Controller,表示是表现层,除此之外,一般不用别的注解代替。
4、SpringMVC是如何将不同的Request路由到不同Controller中的
4、Spring MVC 里面拦截器是怎么写的
有两种写法,一种是实现HandlerInterceptor接口,另外一种是继承适配器类,接着在接口方法当中,实现处理逻辑;然后在SpringMvc的配置文件中配置拦截器即可
https://blog.csdn.net/yy139926/article/details/127916974
Springboot是如何实现自动配置的
MyBatis
1、什么是 MyBatis
MyBatis是一个半ORM框架(模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术,将Java中的对象和数据库中的表关联对应起来,理解为:Java对象与数据库表的映射管理框架),它内部封装了JDBC,开发的时候只需要关注SQL语句本身就可以了,我们不需要花太多精力去处理原生的JDBC那一套流程,比如 加载驱动、创建connection连接、创建statement等。
优点
很明显,我们能看到使用ybatis的代码,结构更清晰,代码量也比较少,这就是ybatisi最直观的优点:
1.将数据库的操作逻辑和业务操作解耦合,使得开发人员可以专心业务逻辑的处理。
2.开发人员只写Sql就可以访问数据库,不需要关心各种数据库连接等额外的操作。各种Connection和
Statementi堵都交给了Mybatis来管理
3.可以将数据库表的字段按照业务规则直接映射到DO层,不用再像DBC一样需要业务代码来转换
除此之外,还有其他优点:
4.支持多种数据源,如POOLED,UNPOOLED,JNDl。同时,还可以整合其他数据库连接池如HikariCP,
Druid,C3p0等
5.支持动态SQL,大大减少了代码的开发量,如if/foreach等常用的动态标签
6.支持事务性的一级缓存,二级缓存和自定义缓存,其中,一级缓存是以session为生命周期,默认开启;二级
缓存则是根据配置的算法来计算过期时间(「O,LRU等),二级缓存如果操作不当容易产生脏数据,不建
议使用
2、为什么说 MyBatis 是半ORM框架,与 Hibernate 有哪些不同
Hibernate是全自动ORM框架,而Mybatis是半自动的。hibernate完全可以通过对象关系模型实现对数据库的操作,拥有完整的JavaBean对象与数据库的映射结构来自动生成sql。而mybatis仅有基本的字段映射,对象数据以及对象实际关系仍然需要通过手写sql来实现和管理。
3、#{ } 和 ${ } 的区别是什么
- #{ } 是预编译处理当中的一个占位符。MyBatis在处理 #{ } 的时候,会将SQL中的 #{ } 替换成 ? ,然后,再调用PreparedStatement对象的set方法进行赋值,由此来防止SQL注入的问题。底层如下:
// 实例化
PreparedStatement pstmt = con.prepareStatement(sql);
// 装载占位符
pstmt.setString(1, "6");
pstmt.setString(2, "bb");
// 执行sql语句
ResultSet rs = pstmt.executeQuery();
- ${ } 是单纯的字符串文本替换。MyBatis在处理 ${ } 的时候,只是简单的把 ${ } 替换为变量的值而已,由此会造成SQL注入,带来不必要的风险。
- 大部分情况下,我们都是使用 #{ } 来处理业务。但是,针对一些特殊的情况,比如 通过一个“变化的字段”做排序等,也可以使用 ${ } 。
${param} 传入的参数会被当成SQL语句中的一部分,举例:order by ${param},则解析成的sql为:order by id
4、MyBatis 是怎么解决实体类中的属性名和表中的字段名不一样的问题
(1)第一种是使用 <resultMap>
标签,逐一定义列名和实体类对象属性名之间的映射关系。
<resultMap id="orderResultMap" type="com.wind.OrderEntity">
<!–用id标签来映射主键字段,用result属性来映射非主键字段–>
<!–其中,用property为实体类属性名,column为数据表中的属性–>
<id property="id" column="user_id">
<result property="no" column="user_no"/>
</reslutMap>
(2)第二种是使用在SQL中定义列的别名,将列的别名与实体类对象的属性名一一对应起来
<select id="selectUserById" parameterType="java.lang.Integer" resultetype="com.wind.UserEntity">
select user_id as id, user_no as no from Test where user_id = #{id}
</select>
5、如何在 Mapper 中传递多个参数
(1)第一种是使用 @param 注解的方式。比如:
user selectUser(@param("username") string username, @param("password") string password);
(2)第二种是使用Java对象的方式。此时,在Java对象中可以有多个属性,每一个属性其实都是一个参数,这样也可以实现在Mapper中传递多个参数。
(3)第三种是使用map集合的方式。此时,需要使Mapper接口方法的输入参数类型和mapper.xml中定义的每个SQL的parameterType的类型都相同。
6、MyBatis 的接口绑定是什么,有哪些绑定方式
接口绑定,就是在MyBatis中定义接口,然后把接口里面的方法和SQL语句绑定,我们直接调用接口中的方法就可以操作数据库了。这样比起直接使用SqlSession对象提供的原生态的方法,更加灵活与简单。
(1)第一种是通过注解绑定,也就是说 在接口的方法上面加上 @Select、@Update 等注解,注解里面包含SQL语句来进行绑定。这种方式可以省去SQL的 xml 映射文件,对于简单的SQL来说比较适用,后期维护比较困难,平时在业务中基本不怎么使用。
(2)第二种是通过在SQL的xml映射文件里面写SQL来进行绑定, 在这种情况下,要指定 xml 映射文件里面的 namespace 参数必须为接口的全类名。不管是SQL简单还是复杂,xml 文件的方式 都比较简单高效,也是最常用的。
7、在MyBatis 中使用 Mapper 接口开发时有哪些要求
一般情况下,在日常开发的时候,会遵循一个mapper.xml映射文件对应于一张表的增删改查。
- mapper.xml映射文件中的namespace属性,必须要定义为对应的Mapper接口的全类名,以此来标识一个mapper级别的二级缓存。
- Mapper接口中的方法名要和mapper.xml中定义的每个SQL语句的id属性相同。
- Mapper接口中的方法的输入参数类型要和mapper.xml中定义的每个SQL语句的parameterType的类型相同。
- Mapper接口中的方法的输出参数类型要和mapper.xml中定义的每个SQL语句的resultType的类型相同,或者使用resultMap也行。
8、MyBatis 中 Mapper 接口中的方法支持重载么
通常一个 xml 映射文件,都会写一个 Dao 接口与之对应。Dao 接口就是人们常说的 Mapper 接口,接口的全限名,就是映射文件中的 namespace 的值,接口的方法名,就是映射文件中 MappedStatement 的 id 值,接口方法内的参数,就是传递给 sql 的参数。
Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个 MappedStatement
举例: com.mybatis3.mappers. StudentDao.findStudentById,可以唯一找到 namespace 为 com.mybatis3.mappers. StudentDao 下面 id = findStudentById 的 MappedStatement,在 MyBatis 中,每一个 <select>
、 <insert>
、 <update>
、 <delete>
标签,都会被解析为一个 MappedStatement 对象。
Dao 接口里的方法可以重载,但是 Mybatis 的 xml 里面的 ID 不允许重复。
/**
* Mapper接口里面方法重载
*/
public interface StuMapper {
List<Student> getAllStu();
List<Student> getAllStu(@Param("id") Integer id);
}
然后在 StuMapper.xml 中利用 Mybatis 的动态 sql 就可以实现。
<select id="getAllStu" resultType="com.pojo.Student">
select * from student
<where>
<if test="id != null">
id = #{id}
</if>
</where>
</select>
能正常运行,并能得到相应的结果,这样就实现了在 Dao 接口中写重载方法。Mybatis 的 Dao 接口可以有多个重载方法,但是多个接口对应的映射必须只有一个,否则启动会报错。
9、MyBatis 的动态SQL是什么,主要标签有哪些
MyBatis的动态SQL标签,主要有以下几类:(1) if 标签,配合 test 标签用来做简单的条件判断。(2)choose 标签,配合 when、otherwise 标签,相当于Java语言中的switch…case语句实现分支选择功能。(3)where 标签,主要是用来简化SQL语句中where条件判断的,能智能的处理 and、or,不用担心有多余的 and 或者 or 导致语法错误。(4)set 标签,主要用来做数据update的时候。(5)trim 标签,对包含的内容加上前缀 prefix、或者后缀 suffix。 (6)foreach 标签,主要用在 Mybatis in 语句中。
(1)if 标签
<select id="findUsersByIf" parameterType="User" resultType="User">
<include refid="select"></include> where age=23
<if test="name!=null"> and username like #{name}</if>
<if test="address!=null"> and address like #{address}</if>
</select>
name->Name->getName 用getName去parameterType指定的类中寻找是否有此方法,如果有就反射调用,调用完反射结果不为null就拼装sql语句 null就不拼装,address同理。
如果两个条件都不为null
select * from user where age=20 username like ? and address like ?
(2)choose when otherwise 标签
<select id="findUsersByChoose" parameterType="java.util.Map" resultType="User">
<include refid="select"></include> where age=23
<choose>
<when test="uname !=null">and username like #{uname}</when>
<when test="uaddress !=null">and address like #{uaddress}</when>
<otherwise>
and username like '%a%'
and address like '%b%'
</otherwise>
</choose>
</select>
choose when otherwise 标签 多个when条件同时成立,就取第一个条件成立的when。
(3)where 标签
<select id="findUsersByWhere" parameterType="java.util.Map" resultType="User">
<include refid="select"></include>
<where>
<if test="uname != null">username like #{uname}</if>
<if test="uaddress !=null">and address like #{uaddress}</if>
</where>
</select>
where标签是为了给sql语句添加where关键字,where标签中的条件都不成立,where关键字就不添加了,如果两个条件都成立
select * from t_user where username like ? and address like ?
如果第一个不成立,第二个条件成立
select * from t_user where and address like ?
他会自动去掉and关键字
(4)set 标签
<update id="updateUserBySet" parameterType="java.util.Map"> update user
<set>
<if test="uname != null">username=#{uname},</if>
<if test="uaddress !=null">address=#{uaddress}</if>
</set>
where id=#{uid}
</update>
set 标签只能用于更新语句,第一个条件成立,第二条件不成立,则自动取消逗号。
(5)trim 标签
trim 替换where标签
<select id="findUsersByTrim" parameterType="java.util.Map" resultType="User">
<include refid="select"></include>
<trim prefix="where" prefixOverrides="and|or">
<if test="uname != null">username like #{uname}</if>
<if test="uaddress !=null">and address like #{uaddress}</if>
</trim>
</select>
trim标签替换 set标签
<update id="updateUserByTrim" parameterType="java.util.Map"> update t_user
<trim prefix="set" suffixOverrides=",">
<if test="uname != null">username=#{uname},</if>
<if test="uaddress !=null">address=#{uaddress}</if>
</trim>
where id=#{uid}
</update>
可以替换where标签和set标签。
(6)foreach 标签
<select id="findUsersByForeach" parameterType="list" resultType="User">
<include refid="select"></include>where id in
<foreach collection="list"
item="id"
index="index"
open="("
close=")"
separator=",">
#{id}
</foreach>
</select>
10、MyBatis 映射文件中,A 标签通过 include 引用了 B 标签的内容,B 标签能否定义在 A 标签的后面
虽然 MyBatis 解析 xml 映射文件是按照顺序解析的,但是,被引用的 B 标签依然可以定义在任何地方,MyBatis 都可以正确识别。
原理是,MyBatis 解析 A 标签,发现 A 标签引用了 B 标签,但是 B 标签尚未解析到,尚不存在,此时,MyBatis 会将 A 标签标记为未解析状态,然后继续解析余下的标签,包含 B 标签,待所有标签解析完毕,MyBatis 会重新解析那些被标记为未解析的标签,此时再解析 A 标签时,B 标签已经存在,A 标签也就可以正常解析完成了。
11、MyBatis 的 Mapper 接口工作原理是什么
Mapper接口,它是没有实现类的。当调用接口方法的时候,它是采用了JDK的动态代理的方式。
UserMapper userMapper=session.getMapper(UserMapper.class);
- Mapper接口的Class对象会被包装成MapperProxyFactory对象,通过MapperProxyFactory对象调用newInstance创建Mapper接口动态代理对象MapperProxy。
- 执行Mapper接口方法时候,其实执行的就是用代理对象执行接口方法,本质执行的是MapperProxy代理类的invoke方法,invoke方法中使用MapperMethod对象执行execute方法。
- 在execute方法中根据MapperMethod对象中的操作类型选择调用的原生方法(接口名,参数)。
每次通过调用接口方法操作数据库的时候,Mybatis都会利用MapperProxyFactory创建当前Mapper接口对应的MapperProxy代理实现类,在此代理类定义的增强中,会利用sqlSession、接口、方法等信息构造MapperMethod。MapperMethod是Mybatis对Mapper的接口方法生成的对应封装类,此封装类定义了真正的操作数据库的代码实现,最终对数据库的操作就是依赖他实现的。
12、MyBatis 的工作原理是什么
- 系统启动时候会读取全局配置文件mybatis-config.xml和加载映射文件Mapper.xml,加载的相关信息都会保存在Configuration对象中。
- MyBstis的环境配置信息构建会话工厂SqlSessionFactory(单例模式)
- SqlSessionFactory会话工厂创建SqlSession对象,这个对象中包含执行SQL语句的所有方法
- MyBatis底层定义了一个Executer执行器来操作数据库,它会根据SqlSession传递的参数生成需要执行的语句,同时负责查询缓存维护。
- 在Executer中MappedStatement对象,该参数是对映射信息的封装,用于储存要映射的SQL语句的id,参数等信息
13、MyBatis 中一对一查询、一对多查询是怎么实现的
- 在MyBatis中,使用association标签来解决一对一的关联查询。association标签可用的属性有:(1)property:对象属性的名称(2)javaType:对象属性的类型(3) column:对应的外键字段名称(4)select:使用另一个查询封装的结果。
- 在MyBatis中,使用collection标签来解决一对多的关联查询。collection标签可用的属性有:(1)property:指的是集合属性的值(2)ofType:指的是集合中元素的类型(3)column:所对应的外键字段名称(4)select:使用另一个查询封装的结果。
一对一关联查询举例如下
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wind.repository.StudentRepository">
<resultMap id="studentMap" type="com.wind.entity.StudentEntity">
<result column="Id" property="id"/>
<result column="Name" property="name"/>
<result column="ClassId" property="classId"/>
<result column="Status" property="status"/>
<result column="AddTime" property="addTime"/>
<result column="UpdateTime" property="updateTime"/>
</resultMap>
<resultMap id="studentMap2" type="com.wind.entity.StudentEntity">
<result column="Id" property="id"/>
<result column="Name" property="name"/>
<result column="ClassId" property="classId"/>
<result column="Status" property="status"/>
<result column="AddTime" property="addTime"/>
<result column="UpdateTime" property="updateTime"/>
<association property="classEntity" javaType="classEntity">
<id column="cId" property="id"/>
<result column="cClassName" property="className"/>
<result column="cStatus" property="status"/>
</association>
</resultMap>
<sql id="sql_select">
select Id, Name, ClassId, Status, AddTime, UpdateTime from RUN_Student
</sql>
<select id="queryStudent" parameterType="int" resultMap="studentMap">
<include refid="sql_select"/>
where id = #{id} and status = 1
</select>
<select id="queryStudentWithClass" parameterType="int" resultMap="studentMap2">
select r.Id, r.Name, r.ClassId, r.Status, r.AddTime, r.UpdateTime, c.id as cid, c.ClassName as cClassName, c.Status as cStatus
from RUN_Student r join RUN_Class c on r.classId = c.id
where r.id = #{id}
</select>
</mapper>
一对多关联查询举例如下
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wind.repository.ClassRepository">
<resultMap id="classStudentMap" type="com.wind.entity.ClassEntity">
<result column="Id" property="id"/>
<result column="ClassName" property="className"/>
<result column="Status" property="status"/>
<result column="AddTime" property="addTime"/>
<result column="UpdateTime" property="updateTime"/>
<collection property="studentEntities" ofType="com.wind.entity.StudentEntity">
<result column="sId" property="id"/>
<result column="sName" property="name"/>
<result column="sClassId" property="classId"/>
<result column="sStatus" property="status"/>
<result column="sAddTime" property="addTime"/>
<result column="sUpdateTime" property="updateTime"/>
</collection>
</resultMap>
<sql id="sql_select_join_student">
select c.Id, c.ClassName, c.Status, c.AddTime, c.UpdateTime,
s.Id as sId, s.Name as sName, s.ClassId as sClassId, s.Status as sStatus, s.AddTime as sAddTime, s.UpdateTime as sUpdateTime
from RUN_Class c join RUN_Student s on c.Id = s.classId
</sql>
<select id="queryClassByClassId" parameterType="int" resultMap="classStudentMap">
<include refid="sql_select_join_student"/>
where c.id = #{id} and c.status = 1 and s.status =1
</select>
</mapper>
14、MyBatis 的分页方法有哪些
(1)在MyBatis中,是使用RowBounds对象进行分页的,它是针对ResultSet结果集执行的逻辑分页,而不是物理分页。
(2)另外,我们可以在SQL内,直接书写带有物理分页的参数来完成物理分页功能,也可以使用第三方的分页插件PageHelper来完成物理分页。
<!-- SQL物理分页 -->
<select id="queryStudentsBySql" parameterType="map" resultMap="studentmapper">
select * from student limit #{start} , #{end}
</select>
int perPage=3; 每页现实的记录数
int page=1; 页数
int begin=(page-1)*perPage; 计算起点
(3)分页插件的原理(物理分页):就是使用MyBatis提供的插件接口,来实现自定义插件。在自定义插件的拦截方法内,拦截待执行的SQL,然后根据设置的分页参数 重写SQL ,生成带有分页语句的SQL,最终执行的是重写之后的SQL,从而实现分页。 举例:
select * from student,分页插件拦截SQL后 重写SQL为:select t.* from (select * from student)t limit 0,10;
15、MyBatis 中都有哪些Executor执行器,它们之间的区别是什么
在 MyBatis 配置文件中,可以指定默认的 ExecutorType 执行器类型,也可以手动给 SqlSessionFactory 的创建 SqlSession 的方法传递 ExecutorType 类型参数。
BaseExecutor:基础抽象类,实现了Executor接口的大部分方法,主要提供了缓存管理和事务管理的能力,使用了模板模式,doUpdate、doQuery、doQueryCursor 等方法的具体实现交给不同的子类去实现。
-
SimpleExecutor:BaseExecutor的具体子类实现,且为默认配置,在doQuery方法中使用PrepareStatement对象访问数据库,每次访问都要创建新的PrepareStatement对象,用完立刻关闭PrepareStatement。
-
ReuseExecutor:BaseExecutor的具体子类实现,与SimpleExecutor不同的是,在doQuery方法中,以 sql 作为 key 查找 Statement 对象,存在就使用,不存在就创建,用完后,不关闭 Statement 对象,而是放置于 Map<String, Statement>内,供下一次使用。简言之,会重用缓存中的statement对象,而不是每次都创建新的PrepareStatement。
-
BatchExecutor:BaseExecutor的具体子类实现,在doUpdate方法中,提供批量执行多条SQL语句的能力。将所有 sql 都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个 Statement 对象,每个 Statement 对象都是 addBatch()完毕后,等待逐一执行 executeBatch()批处理。与 JDBC 批处理相同。
-
CachingExecutor:直接实现Executor接口,使用装饰器模式提供二级缓存能力。先从二级缓存中查询,缓存没有命中再从数据库中查询,最后将结果添加到缓存中再返回给用户。如果在xml文件中配置了节点,则会创建 CachingExecutor。
16、SpringBoot + Mybatis 一级缓存和二级缓存详解
一级缓存
一级缓存在 mybatis 中默认是开启的并且是 session 级别,它的作用域为一次 sqlSession 会话。 一个SqlSession对象中创建一个本地缓存(local cache),在同一个SqlSession中,执行相同的SQL查询时,会尝试去本地缓存中查找是否在缓存,如果在缓存中,就直接从缓存中取出,然后返回给用户,否则,从数据库读取数据,将查询结果存入缓存并返回给用户。
代码演示:
@Test
@Transactional(rollbackFor = Throwable.class)
public void testFistCache(){
// 第一次查询,缓存到一级缓存
userMapper.selectById(1);
// 第二次查询,直接读取一级缓存
userMapper.selectById(1);
}
console 2023-03-15 14:53:58.084 DEBUG [BaseJdbcLogger.java:137] : ==> Preparing: select * from user where id = ?
console 2023-03-15 14:53:58.084 DEBUG [BaseJdbcLogger.java:137] : ==> Parameters: 12(Integer)
console 2023-03-15 14:53:58.103 DEBUG [BaseJdbcLogger.java:137] : <== Total: 1
User{userId=12, userName='Endo Riku', userSex='M', userAge=238}
User{userId=12, userName='Endo Riku', userSex='M', userAge=238}
可以看到,虽然进行了两次查询,但最终只请求了一次数据库,第二次查询命中了一级缓存,直接返回了数据。
这里有两点需要说明一下:
- 为什么开启事务
使用了数据库连接池,默认每次查询完之后自动 commite,这就导致两次查询使用的不是同一个sqlSessioin,根据一级缓存的原理,它将永远不会生效。当我们开启了事务,两次查询都在同一个 sqlSession 中,从而让第二次查询命中了一级缓存。
- 两种一级缓存模式
一级缓存的作用域有两种:session(默认)和 statment,可通过设置 local-cache-scope 的值来切换,默认为 session。二者的区别在于 session 会将缓存作用于同一个 sqlSesson,而 statment 仅针对一次查询,所以,local-cache-scope: statment 可以理解为关闭一级缓存。
- 一级缓存总结
mybatis 默认的 session 级别一级缓存,由于 springboot 中默认使用了 hikariCP,所以基本没用,需要开启事务才有用。但一级缓存作用域仅限同一 sqlSession 内,无法感知到其他 sqlSession 的增删改,所以极易产生脏数据。
二级缓存
默认情况下,mybatis 打开了二级缓存,但它并未生效,因为二级缓存的作用域是 namespace,所以还需要在 Mapper.xml 文件中配置一下才能使二级缓存生效
<cache></cache>
测试 单表二级缓存:
/**
* 测试二级缓存效果
* 需要*Mapper.xml开启二级缓存
**/
@Test
public void testSecondCache(){
userMapper.selectById(1);
userMapper.selectById(1);
}
console 2023-03-15 14:52:54.318 DEBUG [LoggingCache.java:60] : Cache Hit Ratio [com.example.canal.mybatis.mapper.UserMapper]: 0.5
console 2023-03-15 14:52:54.319 DEBUG [LoggingCache.java:60] : Cache Hit Ratio [com.example.canal.mybatis.mapper.UserMapper]: 0.6
User{userId=12, userName='Endo Riku', userSex='M', userAge=238}
User{userId=12, userName='Endo Riku', userSex='M', userAge=238}
这里可以看到,第二次查询直接命中了缓存,日志还打印了该缓存的命中率。读者可以自行关闭二级缓存查看效果,通过注掉对应 mapper.xml 的 cache 标签,或者 cache-enabled: false 均可
第一次调用mapper下的SQL去查询用户的信息,查询到的信息会存放代该mapper对应的二级缓存区域。 第二次调用namespace下的mapper映射文件中,相同的sql去查询用户信息,会去对应的二级缓存内取结果。
缓存的优先级
通过 mybatis 发起的查询,作用顺序为:二级缓存 -> 一级缓存 -> 数据库 ,其中任何一个环节查到不为空的数据,都将直接返回结果。
如果多表联查的二级缓存,user 表 left join user_order 表 on user.id = user_order.user_id
我们考虑这样一种情况,该联查执行两次,第二次联查前更新 user_order 表,如果只使用 cache 配置,将会查不到更新的 user_orderxi,因为两个 mapper.xml 的作用域不同,要想合到一个作用域,就需要用到 cache-ref
userOrderMapper.xml
<cache></cache>
userMapper.xml
<cache-ref namespace="com.zhengxl.mybatiscache.mapper.UserOrderMapper"/>
二级缓存可通过 cache-ref 让多个 mapper.xml 共享同一 namespace,从而实现缓存共享,但多表联查时配置略微繁琐。所以生产环境建议将一级缓存设置为 statment 级别(即关闭一级缓存),如果有必要,可以开启二级缓存
17、MyBatis 是否支持延迟加载,如果支持,它的实现原理是什么
MyBatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 MyBatis 配置文件中,可以配置是否启用延迟加载 。
什么是延迟加载
就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载。
局部延迟加载
在association和collection标签中都有⼀个fetchType属性,通过修改它的值,可以修改局部的加载策略。
<!-- 开启⼀对多 延迟加载 -->
<resultMap id="userMap" type="user">
<id column="id" property="id"></id>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
<!--
fetchType="lazy" 懒加载策略
fetchType="eager" ⽴即加载策略
-->
<collection property="orderList" ofType="order" column="id"
select="com.lagou.dao.OrderMapper.findByUid" fetchType="lazy">
</collection>
</resultMap>
<select id="findAll" resultMap="userMap">
SELECT * FROM `user`
</select>
全局延迟加载
在Mybatis的核⼼配置⽂件中可以使⽤setting标签修改全局的加载策略。
<settings>
<!--开启全局延迟加载功能-->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
局部的加载策略的优先级高于全局的加载策略
延迟加载原理实现
它的原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName() ,拦截器 invoke() 方法发现 a.getB() 是 null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName() 方法的调用。这就是延迟加载的基本原理。
18、MyBatis 是如何防止sql注入的
Mybatis也是用的预编译进行防护SQL注入。其实在框架底层,是JDBC中的PreparedStatement类在起作用。
@Test
public void test02() throws SQLException {
Connection con = JdbcUtil.getConn();
String sql = "SELECT * FROM test WHERE ID = ? AND NAME = ?";
// 实例化
PreparedStatement pstmt = con.prepareStatement(sql);
// 装载占位符
pstmt.setString(1, "6");
pstmt.setString(2, "bb");
// 执行sql语句
ResultSet rs = pstmt.executeQuery();
System.out.println(rs);
while (rs.next()) {
System.out.println(rs.getInt("id") + " " + rs.getString("name"));
}
JdbcUtil.close(con);
}