一、Java基础

1、java 的三大特性?

封装、继承、多态

  • 封装:即隐藏对象的属性和实现细节,将公共代码抽取出来,可以进行复用。 一般体现在公共方法的封装。工具类。
  • 继承:子类继承父类的特征和行为,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
    • 1.提升代码的可重用性
      • 子类通过继承可以得到父类有的内容
    • 2.能够扩展功能
      • 通过继承,子类可以扩展父类已有的功能
    • 3.方便维护
      • 多个子类都继承了父类的某个功能,当功能需要修改时,只要修改父类这个功能即可
  • 多态:一个行为具有多种不同表现形式或形态的能力,不同类的的对象可以对同一消息做出不同反应,父类可以根据子类的不同,而使得同一个方法产生不同的结果(向上转型,向下转型)

    2、抽象和接口的区别?

  • 实现:抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。

  • 构造函数:抽象类可以有构造函数;接口不能有。
  • 实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
  • 访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符。

    3、集合有哪些?

    List、Set、Map 的区别主要体现在两个方面:元素是否有序、是否允许元素重复。
    三者之间的区别,如下表:
    面试总结 - 图1

    4、HashMap 和 HashTable 的区别?

  • 存储:HashMap 允许 key 和 value 为 null,而 Hashtable 不允许。

  • 线程安全:Hashtable 是线程安全的,而 HashMap 是非线程安全的。
  • 推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。

    5、HashMap 的原理?扩容怎么实现的?

  • 原理:HashMap 基于 Hash 算法实现的,我们通过 put(key,value)存储,get(key)来获取。当传入 key 时,HashMap 会根据 key.hashCode() 计算出 hash 值,根据 hash 值将 value 保存在 bucket 里。当计算出的 hash 值相同时,我们称之为 hash 冲突,HashMap 的做法是用链表和红黑树存储相同 hash 值的 value。当 hash 冲突的个数比较少时,使用链表否则使用红黑树。当两个不同的键对象的hashcode相同时会发生什么? 它们会储存在同一个bucket位置的链表中。键对象的equals()方法用来找到键值对。

  • 扩容:若new一个HashMap时没有指定容量(数组大小),则HashMap初始为null,即没有大小,在第一次执行put()时会将容量初始化为16,默认加载因子为0.75,扩容阈值 Threadhold 则为容量 capacityload factor(160.75=12),当Map中元素个数超过阈值后,进行扩容机制,容量变为原来的两倍(容量必须为2的幂次,且每次扩容都为2倍)。若new一个HashMap时候若指定了容量,比如 HashMap map = new HashMap<>(33); 指定了数组大小为33,那数组大小就是33了吗? 显然不是,这样new了以后,会根据指定的正整数“33”找到不小于指定容量的2的幂数)(源码内部是以位运算实现的,速度快),将这个数设置赋值给阈值(threshold)。第一次调用put方法时,会将阈值赋值给容量,然后让阈值=0.75*容量。
  • 第三问为什么不一开始就使用红黑树,不是效率很高吗?
    因为红⿊树需要进⾏左旋,右旋,变⾊这些操作来保持平衡,⽽单链表不需要。
    当元素⼩于8个当时候,此时做查询操作,链表结构已经能保证查询性能。
    当元素⼤于8个的时候,此时需要红⿊树来加快查 询速度,但是新增节点的效率变慢了。
    因此,如果⼀开始就⽤红⿊树结构,元素太少,新增效率⼜⽐较慢,⽆疑这是浪费性能的。
    第四问什么时候退化为链表
    为6的时候退转为链表。中间有个差值7可以防⽌链表和树之间频繁的转换。
    假设⼀下,如果设计成链表个数超过8则链表转 换成树结构,链表个数⼩于8则树结构转换成链表,
    如果⼀个HashMap不停的插⼊、删除元素,链表个数在8左右徘徊,就会 频繁的发⽣树转链表、链表转树,效率会很低。

    6、静态变量和实例变量的区别?

  • 所属对象不同:静态变量是为所有对象共享的,即它是属于某个类的,而实例变量是属于某一个对象的,不具有共享性。

  • 内存空间分配的不同:静态变量在类加载内存的时候会对其进行分配空间,且只会分配一次内存空间,以后对该静态变量的操作都是在这一块内存上完成的。与静态变量不同,每创建一个对象,都会为该对象的实例变量进行分配内存空间。
  • 访问方式不同:访问静态变量可以有两种方式:类名.静态变量名 或者 对象名.静态变量名。而访问实例变量只有一种方法,即对象名.实例变量名。

    7、static、super、this关键字各自作用?

  • this:代表对当前所在类的引用。

  • super:代表对父类对象的引用,用于子类虽然重写了父类的某个方法,但是依旧想调用父类的这个方法。
  • static:方便在没有创建对象的情况下,对属性和方法进行调用。

    8、equals 和 hashcode?

    阿里巴巴开发规范有一句话:只要重写 equals,就必须重写 hashCode。

  • hashCode()方法的作用是为了获取哈希码,返回的是一个int整数,而哈希码的作用是确定对象在哈希表的索引下标。

  • equals()方法的作用是判断两个对象是否相等,equals()方法是定义在Object类中,而所有的类的父类都是Object,所以如果不重写equals方法则会调用Object类的equals方法。Object类的equals方法是用“==”号进行比较,在很多时候,因为==号比较的是两个对象的内存地址而不是实际的值,所以不是很符合业务要求。所以很多时候我们需要重写equals方法,去比较对象中每一个成员变量的值是否相等。

总而言之:

  • 哈希码不相等,则两个对象一定不相同。
  • 哈希码相等,两个对象不一定相同。
  • 两个对象相同,则哈希码和值都一定相等。

    9、泛型?反射?

  • 反射:在Java程序运行过程中,ClassLoader会根据需要加载这些class文件。每一个class文件加载完成后,虚拟机都会为其生成一个Class对象,可以通过类名.class 或者 实例应用.getClass() 获取该对象。通过 class 对象,我们就可以获取 class 文件中绝大部分信息,这就是 Java 反射。

  • 泛型:不同的数据结构可以使用同样的操作。

    10、jre 和 jdk 的区别?

  • JDK:Java Development Kit 的简称,java 开发工具包,提供了 java 的开发环境和运行环境。

  • JRE:Java Runtime Environment 的简称,java 运行环境,为 java 的运行提供了所需环境。

具体来说 JDK 其实包含了 JRE,同时还包含了编译 java 源码的编译器 javac,还包含了很多 java 程序调试和分析的工具。简单来说:如果你需要运行 java 程序,只需安装 JRE 就可以了,如果你需要编写 java 程序,需要安装 JDK。

二、多线程和线程池的一些问题

1、多线程创建方式?项目中怎么使用的?

创建线程有三种方式:

  • 继承 Thread 重写 run 方法;
  • 实现 Runnable 接口;
  • 实现 Callable 接口。

使用:https://www.cnblogs.com/three-fighter/p/14491256.html

2、线程池的参数有哪些?怎么进行参数配置?

  1. corePoolSize:表示当前线程池的核心线程数大小,即最小线程数(初始化线程数),线程池会维护当前数据的线程在线程池中,即使这些线程一直处于闲置状态,也不会被销毁;
  2. maximumPoolSize:表示线程池中允许的最大线程数;后文中会详细讲解
  3. keepAliveTime :表示空闲线程的存活时间,当线程池中的线程数量大于核心线程数且线程处于空闲状态,那么在指定时间后,这个空闲线程将会被销毁,从而逐渐恢复到稳定的核心线程数数量;
  4. unit:当前unit表示的是keepAliveTime存活时间的计量单位,通常使用TimeUnit.SECONDS秒级;
  5. workQueue:任务工作队列;后文会结合maximumPoolSize一块来讲
  6. threadFactory:线程工厂,用于创建新线程以及为线程起名字等
  7. handler:拒绝策略,即当任务过多无法及时处理时所需采取的策略;

https://zhuanlan.zhihu.com/p/394094807
配置参数时需要考虑 CPU密集型任务 、 IO密集型任务 、内存使用率 、下游系统抗并发的能力
配置参数:
CPU密集型 CPU的核数+1
IO密集型 一般配置 2*CPU的核数
参考公式(某大厂配置):
CPU核数/(1-阻塞系数) 阻塞系数在0.8~0.9之间
比如8核CPU 8/(1-0.9) = 80个线程数
https://riemann.blog.csdn.net/article/details/104704197?spm=1001.2101.3001.6650.2&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-2-104704197-blog-110132365.pc_relevant_paycolumn_v3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-2-104704197-blog-110132365.pc_relevant_paycolumn_v3&utm_relevant_index=4

3、synchronized的底层原理?有几种实现方式?锁的优化有哪些?

  • 底层原理:synchronized是由一对monitorenter/monitorexit指令实现的,monitor对象是同步的基本实现单元。在Java6之前,monitor的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。但在java6的时候,java虚拟机对此进行了大刀阔斧地改进,提供了三种不同的monitor实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
  • 实现方式:
    • 作用在实例方法
    • 作用在静态方法
    • 修饰代码块

https://zhuanlan.zhihu.com/p/343305760

4、volatile关键字的作用?与synchronized区别?

  • volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。
  • volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
  • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。

    5、synchronized与Lock的区别是什么?

  • synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。

  • synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
  • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

    6、synchronized 和 ReentrantLock 区别是什么?

    synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 java 6 中对 synchronized 进行了非常多的改进。
    主要区别如下:

  • ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;

  • ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
  • ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。
  • volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

image.png

7、ReetrantLock的原理是什么?

ReentrantLock表示重入锁,它是唯一一个实现了Lock接口的类。重入锁指的是 线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次,同时它也是个典型的独占模式AQS,同步状态为0时表示空闲。当有线程获取到空闲的同步状态时,它会将同步状态加1,将同步状态改为非空闲,于是其他线程挂起等待。在修改同步状态的同时,并记录下自己的线程,作为后续重入的依据,即一个线程持有某个对象的锁时,再次去获取这个对象的锁是可以成功的。如果是不可重入的锁的话,就会造成死锁。
https://zhuanlan.zhihu.com/p/232458435#:~:text=%20ReentrantLock%E6%98%AF%E4%B8%AA%E5%85%B8%E5%9E%8B%E7%9A%84%E7%8B%AC%E5%8D%A0%E6%A8%A1%E5%BC%8FAQS%EF%BC%8C%E5%90%8C%E6%AD%A5%E7%8A%B6%E6%80%81%E4%B8%BA0%E6%97%B6%E8%A1%A8%E7%A4%BA%E7%A9%BA%E9%97%B2%E3%80%82,%E5%BD%93%E6%9C%89%E7%BA%BF%E7%A8%8B%E8%8E%B7%E5%8F%96%E5%88%B0%E7%A9%BA%E9%97%B2%E7%9A%84%E5%90%8C%E6%AD%A5%E7%8A%B6%E6%80%81%E6%97%B6%EF%BC%8C%E5%AE%83%E4%BC%9A%E5%B0%86%E5%90%8C%E6%AD%A5%E7%8A%B6%E6%80%81%E5%8A%A01%EF%BC%8C%E5%B0%86%E5%90%8C%E6%AD%A5%E7%8A%B6%E6%80%81%E6%94%B9%E4%B8%BA%E9%9D%9E%E7%A9%BA%E9%97%B2%EF%BC%8C%E4%BA%8E%E6%98%AF%E5%85%B6%E4%BB%96%E7%BA%BF%E7%A8%8B%E6%8C%82%E8%B5%B7%E7%AD%89%E5%BE%85%E3%80%82%20%E5%9C%A8%E4%BF%AE%E6%94%B9%E5%90%8C%E6%AD%A5%E7%8A%B6%E6%80%81%E7%9A%84%E5%90%8C%E6%97%B6%EF%BC%8C%E5%B9%B6%E8%AE%B0%E5%BD%95%E4%B8%8B%E8%87%AA%E5%B7%B1%E7%9A%84%E7%BA%BF%E7%A8%8B%EF%BC%8C%E4%BD%9C%E4%B8%BA%E5%90%8E%E7%BB%AD%E9%87%8D%E5%85%A5%E7%9A%84%E4%BE%9D%E6%8D%AE%EF%BC%8C%E5%8D%B3%E4%B8%80%E4%B8%AA%E7%BA%BF%E7%A8%8B%E6%8C%81%E6%9C%89%E6%9F%90%E4%B8%AA%E5%AF%B9%E8%B1%A1%E7%9A%84%E9%94%81%E6%97%B6%EF%BC%8C%E5%86%8D%E6%AC%A1%E5%8E%BB%E8%8E%B7%E5%8F%96%E8%BF%99%E4%B8%AA%E5%AF%B9%E8%B1%A1%E7%9A%84%E9%94%81%E6%98%AF%E5%8F%AF%E4%BB%A5%E6%88%90%E5%8A%9F%E7%9A%84%E3%80%82

8、cas是什么?会有什么问题?

CAS(Compare And Swap 比较并且替换)是乐观锁的一种实现方式,是一种轻量级锁,JUC中很多工具类的实现就是基于CAS的。
线程在读取数据时不进行加锁,在准备写回数据时,先去查询原值,操作的时候比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。
CAS的缺点:

  1. 循环时间长资源占用问题,如果替换失败,线程会自旋,直到替换为止,会占用CPU资源
  2. ABA问题
    1. 因为CAS需要在操作值的时候检查下主内存的值是否发生变化,如果没有发生变化就交换,但是假如一个值原先是A,变成了B,又变成了A,那么使用CAS进行检查的时候是未发现此值的变化的,但实际上它是变化的了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
  3. 只能保证一个共享变量的原子操作

    9、死锁?怎么进行防止?

    当线程A持有独占锁a,并尝试去获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
    防止方法:
  • 尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
  • 尽量使用 java.util.concurrent 并发类代替自己手写锁。
  • 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
  • 尽量减少同步的代码块。

    10、线程池的常见创建方式?拒绝策略有哪些?你们怎么设置拒绝策略的?阻塞队列有哪些?

    线程池创建有七种方式,最核心的是最后一种:

  • newSingleThreadExecutor():它的特点在于工作线程数目被限制为1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目;

  • newCachedThreadPool():它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明的特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过60秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用SynchronousQueue作为工作队列;
  • newFixedThreadsPool(int nThreads):重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有nThreads个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目nThreads;
  • newSingleThreadScheduledExecutor():创建单线程池,返回ScheduledExecutorService,可以进行定时或周期性的工作调度;
  • newScheduledThreadPool(int corePoolSize):和newSingleThreadScheduledExecutor()类似,创建的是多个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;
  • newWorkStealingPool(int parallelism):这是一个经常被人忽视的线程池,Java8才加入这个创建方法,其内部会创建ForkJoinPool,利用Work-Stealing算法,并行的处理任务,不保证处理顺序;
  • ThreadPoolExecutor():是最原始的线程池创建,上面1-3创建方式都是对ThreadPoolExecutor的封装。

拒绝策略:首先我们知道线程池的拒绝策略参数的类型是 RejectedExecutionHandler 类型的,那么我们可以先来了解一下关于这个接口的关系
image.png

  • 在上述类图中我们可以看到 RejectedExecutionHandler 接口有四个实现类,同时都提供了无参构造函数,这四个实现类对应了不同的拒绝策略,都有各自的适用场景
  • AbortPolicy 拒绝策略:终止策略,这是ThreadPoolExecutor线程池默认的拒绝策略,程序将会抛出RejectedExecutionException异常。这种策略在拒绝任务时,会直接抛出一个类型为RejectedExecutionException 的 RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
  • DiscardPolicy 拒绝策略:丢弃策略,什么都不做,即丢弃新提交的任务。这种策略是当任务提交时直接将刚提交的任务丢弃,而且不会给与任何提示通知,所以这种策略使用要慎重,因为有一定的风险,对我们来说根本不知道提交的任务有没有被丢弃
  • DiscardOldestPolicy 拒绝策略:丢弃最早未处理请求策略,丢弃最先进入阻塞队列的任务以腾出空间让新的任务入队列。这种策略和上面相似。不过它丢弃的是队列中的头节点,也就是存活时间最久的
  • CallerRunsPolicy 拒绝策略:调用者运行策略,线程池中没办法运行,那么就由提交任务的这个线程运行(哪儿来的回哪儿儿去~)。这种策略算是最完善的相对于其他三个,当线程池无能力处理当前任务时,会将这个任务的执行权交予提交任务的线程来执行,也就是谁提交谁负责,这样的话提交的任务就不会被丢弃而造成业务损失,同时这种谁提交谁负责的策略必须让提交线程来负责执行,如果任务比较耗时,那么这段时间内提交任务的线程也会处于忙碌状态而无法继续提交任务,这样也就减缓了任务的提交速度,这相当于一个负反馈。也有利于线程池中的线程来消化任务

阻塞队列有哪些:https://blog.csdn.net/xiewenfeng520/article/details/106954169

11、CountDownLatch和CyclicBarrier的区别?项目中怎么使用?为啥会选择CountDownLatch不选CyclicBarrier?

共同点:都是阻塞线程

  • CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
  • CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。比如以下代码执行完之后会返回true。
  • CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。
  • 某线程中断CyclicBarrier会抛出异常,避免了所有线程无限等待。

不同点:CountDownLatch是主线程被多线程阻塞,直到多线程执行完成才被唤醒继续执行,所以更关注主线程等待多线程执行完成再继续执行的场景;CyclicBarrier是多线程各自被阻塞在栅栏前,是多线程之间的相互等待,直到全部的多线程全部执行完成,然后并发的去共同做某件事
https://zhuanlan.zhihu.com/p/409112151

三、MyBatis 的常见问题

1、一级缓存和二级缓存?

  • 一级缓存:

    • 1:一级缓存是默认开启的;
    • 2:底层其实是基于hashmap的本地内存缓存;
    • 3:作用域是session(其实就相当于一个方法);
    • 4:当session关闭或者刷新的时候缓存清空;
    • 5:不通sqlsession之间缓存互不影响;

    • 问题一:其实一级缓存也有数据一致性问题:

    • 比如:我有一个更新操作对同一条数据,
    • 如果是sqlsessionA进行了更新操作,则sqlsessionA对应的一级缓存被清空;
    • 如果是sqlsessionB进行了更新操作,则此更新操作对改sqlsessionA不可见;
    • 那么其实这个时候sqlsessionA再查的数据就是过期失效数据了;
    • 就出现了数据不一致现象;

    • 建议:
    • 1:单个sqlsession的生命周期不能过长;
    • 2:如果是对同一个语句更新尽量使用同一个sql,也就是同一个sqlsession;
    • 3:建议关闭一级缓存,
    • 怎么关闭呢?
    • 在mybatis的全局配置文件中增加
      1. <settiog name="localCacheScope" value="STATEMENT" />
  • 二级缓存:

    • 1:首先mybatis默认是没有开启二级缓存的,
    • 2:二级缓存需要我们手动开启,它是mapper级别的缓存;
    • 3:同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。
    • 那怎么开启二级缓存呢?

      1. <setting name="cacheEnabled" value="true"/>
    • 使用二级缓存?

      1. 在标签<mapper>下面添加<cache/>
    • 使用二级缓存的pojo类实现序列化接口;

    • 当然二级缓存也不建议使用,mysql都默认关闭了,更何况我们呢;
    • 因为二级缓存是建立在同一个namespace下的,如果对某一个表的操作查询可能有多个namespace,那么得到的数据就是有问题的;

    • 建议:
    • 1:对某个表的操作和查询都写在同一个namespace下,其他的namespace如果有操作就会有问题,脏数据;
    • 2:对表的关联查询,关联的所有表的操作都必须在同一个namespace下;

开启二级缓存数据查询流程:二级缓存 -> 一级缓存 -> 数据库。
缓存更新机制:当某一个作用域(一级缓存 Session/二级缓存 Mapper)进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。

2、枚举怎么通过mybatis实现?

  • MyBatis 内置枚举转换器:

org.apache.ibatis.type.EnumTypeHandler 和 org.apache.ibatis.type.EnumOrdinalTypeHandler
EnumTypeHandler
Mybatis 中默认的枚举转换器,获取枚举中的 name 属性
EnumOrdinalTypeHandler
获取枚举中 ordinal 属性,相当于索引,从1开始

  • 自定义类型转换器:

    • BaseTypeHandler抽象类,实现了TypeHandler
    • 自定义枚举转换器实现
      • 枚举接口
      • 自定义类型转换器

        3、mybatis 中 #{}和 ${}的区别是什么?

        {} 是预编译处理,${} 是字符替换。
        在使用 #{} 时,mybatis会将 sql 中的 #{} 替换成“?”,配合 PreparedStatement 的 set 方法赋值,这样可以有效的防止 sql 注入,保证程序的运行安全。

        4、MyBatis 怎么进行传参?

  • 顺序传参

  • 使用 @Param 传参
  • 使用 Map 传参
  • 使用 @Param 和 Map 组合传参
  • 使用实体类传参
  • 使用 List 传参

    5、想要拿到主键的方式有哪些?

  • 自动主键回填:使用 useGeneratedKeys + keyProperty 组合的方式

面试总结 - 图4

  • 插入之后,主键回填(适用于自增主键):

面试总结 - 图5面试总结 - 图6

  • 插入之前,主键回填(适用于没有自增的主键,主键需要自己指定(Oracle)):

面试总结 - 图7

6、mybatis没有接口实现类,mapper的工作原理是什么?

MyBatis 使用了 动态代理 来生成了实现类。
1、加载mybatis全局配置文件(数据源、mapper映射文件等),解析配置文件,MyBatis基于XML配置文件生成Configuration,和一个个MappedStatement(包括了参数映射配置、动态SQL语句、结果映射配置),其对应着