Loading... ## 1. 重载与重写的区别? * 重载是对象的方法之间,它们方法名相同,但方法的参数列表不同 * 重写是父子类(包括接口与实现类)中两个同名方法,它们方法名相同,且方法的参数列表相同 * 重载在编译阶段,由**编译器** 根据传递给方法的参数来区分方法,例如 ```Java MyObject obj = ... obj.test(123); // 应该是调用 test(int x) 这个方法 obj.test("abc"); // 应该是调用 test(String x) 这个方法 ``` * 而重写是在运行阶段,由虚拟机**解释器** 去获取引用对象的实际类型,根据类型才能确定该调用哪个方法,例如 ```Java Super obj = ... obj.test(); // 到底是调用父类,还是子类的 test 方法,必须检查引用对象的实际类型才能确定 ``` * 有没有发生重写,可以使用 @Override 来检查 > ***P.S.*** > > * 括号内的说明是为了严谨,自己知道就行,回答时不必说出,这样比较简洁 > * 个人觉得,在回答方法重载时,不必去细说什么参数的类型、个数、顺序,就说参数列表不同就完了 > * 个人觉得,重点在于点出:重载是编译时由**编译器** 来区分方法,而重写是运行时由**解释器** 来区分方法 > * 语法细节,问了再说,不问不必说 > * 重写时,子类方法的访问修饰符要 >= 父类方法的访问修饰符 > * 重写时,子类方法抛出的检查异常类型要 <= 父类方法抛出的检查异常类型,或子类不抛异常 > * 重写时,父子类的方法的返回值类型要一样,或子类方法返回值是父类方法返回值的子类 --- ## 2. == 与 equals 的区别? * 对于基本类型,== 是比较两边的值是否相同 * 对于引用类型,== 是比较两边的引用地址是否相同,用来判断是否引用着同一对象 * equals 要看实现 * Object.equals(Object other) 的内部实现就是 ==,即判断当前对象和 other 是否引用着同一对象 * 比如 String,它的内部实现就是去比较两个字符串中每个字符是否相同,比较的是内容 * 比如 ArrayList,它的内部实现就是去比较两个集合中每个元素是否 equals,比较的也是内容 --- ## 3. String,StringBuilder 和 StringBuffer 的区别? * 它们都可以用来表示字符串对象 * String 表示的字符串是不可变的,而后两者表示的字符串是内容可变的(可以增、删、改字符串里的内容) * StringBuilder 不是线程安全的,StringBuffer 是线程安全的,而 String 也算是线程安全的 适用场景 * 大部分场景下使用 String 就足够了 * 如果有大量字符串拼接的需求,建议用后两者,此时 * 此字符串对象需要被多线程同时访问,用 StringBuffer 保证安全 * 此字符串对象只在线程内被使用,用 StringBuilder 足够了 另外针对 String 类是 final 修饰会提一些问题,把握下面几点 * 本质是因为 String 要设计成不可变的,final 只是条件之一 * 不可变的好处有很多:线程安全、可以缓存等 --- ## 4. 说说 Java 中的异常体系?  异常的重要继承关系如图所示,其中 * Throwable 是其它异常类型的顶层父类 * Error 表示无法恢复的错误,例如 OutOfMemoryError 内存溢出、StackOverflowError 栈溢出等 * 这类异常即使捕捉住,通常也无法让程序恢复正常运行 * Exception 表示可恢复的错误,处理方式有两种 * 一是自己处理,用 catch 语句捕捉后,可以进行一些补救(如记录日志、恢复初始状态等) * 二是用 throw 语句将异常继续抛给上一层调用者,由调用者去处理 * Exception 有特殊的子类异常 RuntimeException,它与 Exception 的不同之处在于 * Exception 被称之为**检查** 异常,意思是必须在语法层面对异常进行处理,要么 try-catch,要么 throws * RuntimeException 和它的子类被称为**非检查** 异常(也可以翻译为字面意思:运行时异常),在语法层面对这类异常并不要求强制处理,不加 try-catch 和 throws 编译时也不会提示错误 * 常见的非检查异常有 * 空指针异常 * 算术异常(例如整数除零) * 数组索引越界异常 * 类型转换异常 * ... --- ## 5. 说说 java 中常见的集合类? 重要的集合接口以及实现类参考下图  接口 * 接口四个:Collection、List、Set、Map,它们的关系: * Collection 是父接口,List 和 Set 是它的子接口 * Map 接口与其它接口的关系 * Map 调用 entrySet(),keySet() 方法时,会创建 Set 的实现 * Map 调用 values() 方法时,会用到 Collection 的实现 List 实现(常见三个) * ArrayList 基于数组实现 * 随机访问(即根据索引访问)性能高 * 增、删由于要移动数组元素,性能会受影响 * 【进阶】但如果增、删操作的是数组尾部不牵涉移动元素 * LinkedList 基于链表实现 * 随机访问性能低,因为需要顺着链表一个个才能访问到某索引位置 * 增、删性能高 * 【进阶】说它随机访问性能低是相对的,如果是头尾节点,无论增删改查都快 * 【进阶】说它增删性能高也是有前提的,并没有包含定位到该节点的时间,把这个算上,增删性能并不高 * Vector 基于数组实现 * 相对于前两种 List 实现是线程安全的 * 【进阶】一些说法说 Vector 已经被舍弃,这是不正确的 Set 实现 * HashSet 内部组合了 HashMap,利用 Map key 唯一的特点来实现 Set * 集合中元素唯一,注意需要为元素实现 hashCode 和 equals 方法 * 【进阶】Set 的特性只有元素唯一,有些人说 Set 无序,这得看实现,例如 HashSet 无序,但TreeSet 有序 Map 实现(常见五个) * HashMap 底层是 Hash 表,即数组 + 链表,链表过长时会优化为红黑树 * 集合中 Key 要唯一,并且它需要实现 hashCode 和 equals 方法 * LinkedHashMap 基于 HashMap,只是在它基础上增加了一个链表来记录元素的插入顺序 * 【进阶】这个链表,默认会记录元素插入顺序,这样可以以插入顺序遍历元素 * 【进阶】这个链表,还可以按元素最近访问来调整顺序,这样可以用来做 LRU Cache 的数据结构 * TreeMap 底层是红黑树 * Hashtable 底层是 Hash 表,相对前面三个实现来说,线程安全 * 【进阶】它的线程安全实现方式是在 put,get 等方法上都加了 synchronized,锁住整个对象 * ConcurrentHashMap 底层也是 Hash 表,也是线程安全的 * 【进阶】它的 put 方法执行时仅锁住一个链表,并发度比 Hashtable 高 * 【进阶】它的 get 方法执行不加锁,是通过 volatile 保证数据的可见性 > ***P.S.*** > > * 未标注的是必须记住的部分 > * 标注【进阶】的条目是该集合比较有特色的地方,回答出来就是**加分** 项,不过也根据自己情况来记忆 --- ## 6. HashMap 原理? ### 6.1 HashMap 原理(数据结构) 底层数据结构:数组+链表+红黑树 接下来的回答中要点出**数组的作用** ,**为啥会有冲突** ,**如何解决冲突** * 数组:存取元素时,利用 key 的 hashCode 来计算它在数组中的索引,这样在没有冲突的情况下,能让存取时间复杂度达到 **O(1)** * 冲突:数组大小毕竟有限,就算元素的 hashCode 唯一,数组大小是 n 的情况下要放入 n+1 个元素,根据鸽巢原理,肯定会发生冲突 * 解决冲突:一种办法就是利用链表,将这些冲突的元素链起来,当然在在此链表中存取元素,时间复杂度会提高为 **O(n)** 接下来要能说出为什么在在链表的基础上还要有红黑树 * 树化目的是避免链表过长引起的整个 HashMap 性能下降,红黑树的时间复杂度是 **O(\log{n})** 有一些细节问题可以继续回答,比如树化的时机【进阶】 * 时机:在数组容量达到 >= 64 且链表长度 >= 8 时,链表会转换成红黑树 * 如果树中节点做了删除,节点少到已经没必要维护树,那么红黑树也会退化为链表 --- ### 6.2 HashMap 扩容原理 扩容因子:0.75 也就是 3/4 * 初始容量 16,当放入第 13 个元素时(超过 3/4)时会进行扩容 * 每次扩容,容量翻倍 * 扩容后,会重新计算 key 对应的桶下标(即数组索引)这样,一部分 key 会移动到其它桶中 --- ### 6.3 HashMap 原理(方法执行流程) 以 put 方法为例进行说明 1. 产生 hash 码。 1. 先调用 key.hashCode() 方法 2. 二次哈希就是把 hashCode 的高 16 位与低 16 位做了个异或运算(二次哈希实现) 3. 为了让哈希分布更均匀,还要对它返回结果进行二次哈希,这个结果称为 hash 2. 搞定数组。 1. 如果数组还不存在,会创建默认容量为 16 的数组,容量称为 n 2. 否则使用已有数组 3. 计算桶下标。 1. 利用 (n - 1) & hash 得到 key 对应的桶下标(即数组索引) * 也可以用 hash % n 来计算,但效率比前面的方法低,且有负数问题 * 用 (n - 1) & hash 有前提,就是容量 n 必须是 2 的幂(如 16,32,64 ...) 4. 计算好桶下标后,分三种情况 1. 如果该桶位置还空着,直接根据键值创建新的 Node 对象放入该位置即可 2. 如果该桶是一条链表,沿着链表找,看看是否有值相同的 key,有走更新,没有走新增 * 走新增逻辑的话,是把节点链到尾部(尾插法) * 新增后还要检查链表是否需要树化,如果是,转成红黑树 * 新增的最后要检查元素个数 size,如果超过阈值,要走扩容逻辑 3. 如果该桶是一棵红黑树,走红黑树新增和更新逻辑,同样新增的最后要看是否需要扩容 > ***P.S.*** > > * 以上讲解基于 jdk 1.8 及以上版本的 HashMap 实现 > * 考虑到 jdk 1.7 已经很少使用了,故不再介绍基于 1.7 的 HashMap,有需求可以看 b 站黑马面试视频 --- ## 7. ThreadLocal 的原理? ThreadLocal 的主要目的是用来实现多线程环境下的变量隔离 * 【解释】即每个线程自己用自己的资源,这样就不会出现共享,既然没有共享,就不会有多线程竞争的问题 原理 * 每个线程对象内部有一个 ThreadLocalMap,它用来存储这些需要线程隔离的资源 * 资源的种类有很多,比如说数据库连接对象、比如说用来判断身份的用户对象 ... * 怎么区分它们呢,就是通过 ThreadLocal,它作为 ThreadLocalMap 的 key,而真正要线程隔离的资源作为 ThreadLocalMap 的 value * ThreadLocal.set 就是把 ThreadLocal 自己作为 key,隔离资源作为值,存入当前线程的 ThreadLocalMap * ThreadLocal.get 就是把 ThreadLocal 自己作为 key,到当前线程的 ThreadLocalMap 中去查找隔离资源 * ThreadLocal **一定要记得** 用完之后调用 remove() 清空资源,避免内存泄漏 --- ## 8. 解释悲观锁与乐观锁? 悲观锁 * 像 synchronized,Lock 这些都属于悲观锁 * 如果发生了竞争,失败的线程会进入阻塞 * 【理解】悲观的名字由来:**害怕** 其他线程来同时修改共享资源,因此用互斥锁让同一时刻只能有一个线程来占用共享资源 乐观锁 * 像 AtomicInteger,AtomicReference 等原子类,这些都属于乐观锁 * 如果发生了竞争,失败的线程不会阻塞,仍然会重试 * 【理解】乐观的名字由来:**不怕** 其他线程来同时修改共享资源,事实上它根本不加锁,所有线程都可以去修改共享资源,只不过并发时只有一个线程能成功,其它线程发现自己**失败** 了,就去**重试** ,直至成功 适用场景 * 如果竞争少,能很快占有共享资源,适合使用乐观锁 * 如果竞争多,线程对共享资源的独占时间长,适合使用悲观锁 > ***P.S.*** > > * 这里讨论 Java 中的悲观锁和乐观锁,其它领域如数据库也有这俩概念,当然思想是类似的 --- ## 9. synchronized 原理? 以重量级锁为例,比如 T0、T1 两个线程同时执行加锁代码,已经出现了竞争(代码如下) ```Java synchronized(obj) { // 加锁 ... } // 解锁 ``` 1. 当执行到行1 的代码时,会根据 obj 的对象头**找到** 或**创建** 此对象对应的 Monitor 对象(C++对象) 2. 检查 Monitor 对象的 owner 属性,用 Cas 操作去设置 owner 为当前线程,Cas 是原子操作,只能有一个线程能成功 1. 假设 T0 Cas 成功,那么 T0 就加锁成功,可以继续执行 synchronized 代码块内的部分 2. T1 这边 Cas 失败,会自旋若干次,重新尝试加锁,如果 1. 重试过程中 T0 释放了锁,则 T1 不必阻塞,加锁成功 2. 重试时 T0 仍持有锁,则 T1 会进入 Monitor 的等待队列阻塞,将来 T0 解锁后会唤醒它恢复运行(去重新抢锁) --- ## 10. synchronized 锁升级? synchronized 锁有三个级别:偏向锁、轻量级锁、重量级锁,性能从左到右逐渐降低 * 如果就一个线程对同一对象加锁,此时就用偏向锁 * 又来一个线程,与前一个线程交替为对象加锁,但只是交替,没有竞争,此时要升级为轻量级锁 * 如果多个线程加锁时发生了竞争,必须升级为重量级锁 【说明】 * 自 java 6 开始对 synchronized 提供了锁升级功能,之前只有重量级锁 * 但从 java 15 开始,偏向锁被标记为已废弃,将来会移除(因为实际带来的性能提升不明显,某些情况下反而影响性能) --- ## 11. synchronized 和 Lock? * synchronized 是关键字,Lock 是 Java 接口 * 前者底层是 C++ 代码实现锁,后者是 Java 自己的代码来实现锁 * Lock 功能更多,比如可以选择是公平锁还是非公平锁、可以设置加锁超时时间、可打断等 * Lock 的提供多种扩展实现(例如读写锁),可以根据场景选择更合适的实现 * Lock 释放锁需要调用 unlock 方法,而 synchronzied 在代码块结束无需显式调用就可以释放锁 --- ## 12. 线程池的核心参数? 记忆七个参数 1. 核心线程数 1. 核心线程会常驻线程池 2. 最大线程数 1. 如果同时执行的任务数超过了核心线程数,且队列已满,会创建新的线程来救急 2. 总线程数(新线程+原有的核心线程)不超这个最大线程数 3. 存活时间 1. 超过核心线程数的线程一旦闲下来,会存活一段时间,然后被销毁 4. 存活时间单位 5. 工作队列 1. 如果同时执行的任务数超过了核心线程数,会把暂时无法处理的任务放入此队列 6. 线程工厂 1. 可以控制池中线程的命名规则,是否是守护线程等(不太重要的参数) 7. 拒绝策略,队列放满任务,且所有线程都被占用,再来新任务,就会有问题,此时有四种拒绝策略: 1. AbortPolicy 报错策略,直接抛异常 2. CallerRunsPolicy 推脱策略,线程池不执行任务,推脱给任务提交线程 3. DiscardOldestPolicy 抛弃最老任务策略,把队列中最早的任务抛弃,新任务加入队列等待 4. DiscardPolicy 抛弃策略,直接把新任务抛弃不执行 --- ## 13. JVM 堆内存结构? 堆内存的布局与垃圾回收器有关。 传统的垃圾回收器会把堆内存划分为:老年代和年轻代,年轻代又分为 * 伊甸园 Eden * 幸存区 S0,S1 如果是 G1 垃圾回收器,会把内存划分为一个个的 Region,每个 Region 都可以充当 * 伊甸园 * 幸存区 * 老年代 * 巨型对象区 --- ## 14. 垃圾回收算法? 记忆三种: 1. 标记-清除算法。优点是回收速度快,但会产生内存碎片 2. 标记-整理算法。相对清除算法,不会有内存碎片,当然速度会慢一些 3. 标记-复制算法。将内存划分为大小相等的两个区域 S0 和 S1 1. S0 的职责用来存储对象,S1 始终保持空闲 2. 垃圾回收时,只需要扫描 S0 的存活对象,把它们复制到 S1 区域,然后把 S0 整个清空,最后二者互换职责即可 3. 不会有内存碎片,特别适合存活对象很少时(因为此时复制工作少) --- ## 15. 伊甸园、幸存区、老年代细节? * 对象最初都诞生在伊甸园,这些对象通常寿命都很短,在伊甸园空间不足,会触发年轻代回收,还活着的对象进入幸存区 S0,年轻代回收适合采用标记-复制算法 * 接下来再触发年轻代回收时,会将伊甸园和 S0 仍活着的对象复制到 S1,清空 S0,交换 S0 和 S1 职责 * 经过多次回收仍不死的对象,会**晋升** 至老年代,老年代适合放那些长时间存活的对象 * 老年代回收如果满了,会触发老年代垃圾回收,会采用标记-整理或标记-清除算法。老年代回收时的暂停时间通常比年轻代回收更长 还会常问 晋升条件 * 注意不同垃圾回收器,晋升条件不一样 * 在 parallel 里,经历 15 次(默认值)新生代回收不死的对象,会晋升 * 可以通过 -XX:MaxTenuringThreshold 来调整 * 例外:如果幸存区中的某个年龄对象空间占比已经超过 50%,那么大于等于这个年龄的对象会**提前晋升** 大对象的处理 * 首先大对象不适合存储在年轻代,因为年轻代是复制算法,对象移动成本高 * 注意不同垃圾回收器,大对象处理方式也不一样 * 在 serial 和 cms 里,如果对象大小超过阈值,会直接把大对象晋升到老年代 * 这个阈值通过 -XX:PretenureSizeThreshold 来设置 * 在 g1 里,如果对象被认定为巨型对象(对象大小超过了 region 的一半),会存储在巨型对象区 * Region 大小是堆内存总大小 / 2048(必须取整为2的幂),或者通过 -XX:G1HeapRegionSize 来设置 > ***P.S.*** > > 著名教材《深入理解Java虚拟机》一书关于这些论述,很多观点陈旧过时,需要带批判眼光来学习。例如在它的《内存分配与回收策略》这一章节,提到了这些: > > * 对象优先在Eden分配(OK) > * 大对象直接进入老年代(没有提到 g1 情况) > * 长期存活的对象将进入老年代(即我上面讲的晋升条件,但没强调要区分垃圾回收器) > * 动态对象年龄判定(即提前晋升) > * 空间分配担保(已过时)文中提到的 -XX:+HandlePromotionFailure 参数在 jdk8 之后已经没了 --- ## 16. Lambda表达式? 什么是 Lambda 表达式 * 文献中把 Lambda 表达式一般称作**匿名函数** ,语法为 `(参数部分) -> 表达式部分` * 它本质上是一个**函数对象** * 它可以用在那些需要将**行为参数化** 的场景,例如 Stream API,MyBatisPlus 的 QueryWrapper 等地方 Lambda 与匿名内部类有何异同 * 它们都可以用于需要行为参数化的场景 * Lambda 表达式必须配合函数式接口使用,而匿名内部类不必拘泥于函数式接口,其它接口和抽象类也可以 * Lambda 表达式比匿名内部类语法上更加简洁 * 匿名内部类是在编译阶段由程序员编写提供,而 Lambda 表达式是在运行阶段动态生成它所需的类 * 【进阶】Lambda 中 this 含义与匿名内部类中的 this 不同 --- ## 17. 什么是反射? * 反射是 java 提供的一套 API,通过这套 API 能够在**运行期间** * 根据类名加载类 * 获取类的各种信息,如类有哪些属性、哪些方法、实现了哪些接口 ... * 类型参数化,根据类型创建对象 * 方法、属性参数化,以统一的方式来使用方法和属性 * 反射广泛应用于各种框架实现,例如 * Spring 中的 bean 对象创建、依赖注入 * JUnit 单元测试方法的执行 * MyBatis 映射查询结果到 java 对象 * ... * 反射在带来巨大灵活性的同时也不是没有缺点,那就是反射调用效率会受一定影响 --- ## 18. 什么是 Java 泛型? * 泛型的主要目的是实现**类型参数化** ,java 在定义类、定义接口、定义方法时都支持泛型 * 泛型的好处有 * 提供编译时类型检查,避免运行时类型转换错误,提高代码健壮性 * 设计更通用的类型,提高代码通用性 【例如】想设计 List 集合,里面只放一种类型的元素,如果不用泛型,怎么办呢?你必须写很多实现类 * Impl1 实现类中,只放 String * Impl2 实现类中,只放 Integer * ... * 要支持新的元素类型,实现类型也得不断增加,解决方法需要把元素类型作为参数,允许它可变化:List<T>,其中 T 就是泛型参数,它将来即可以是 String,也可以是 Integer ... > ***P.S.*** > > * 【例如】是为了帮助你理解,不是必须答出来。 > * 关键是答出类型参数化,懂的面试官不必多说,不懂的也没必要跟他继续啰嗦 --- ## 19. 解释对称加密、非对称加密、哈希摘要? * 对称加密 * 加密和解密的密钥使用同一个 * 因为密钥只有一个,所以密钥需要妥善保管 * 加解密速度快 * 非对称加密 * 密钥分成公钥、私钥,其中公钥用来加密、私钥用来解密 * 只需将私钥妥善保管,公钥可以对外公开 * 如果是双向通信保证传输数据安全,需要双方各产生一对密钥 * A 把 A公钥 给 B,B 把 B公钥 给 A,他们各自持有自己的私钥和对方的公钥 * A 要发消息给 B,用 B公钥 加密数据后传输,B 收到后用 B私钥 解密数据 * 类似的 B 要发消息给 A,用 A公钥 加密数据后传输,A 收到后用 A私钥 解密数据 * 相对对称加密、加解密速度慢 * 哈希摘要,摘要就是将原始数据的特征提取出来,它**能够代表原始数据** ,可以用作数据的完整性校验 * 举个例子,张三对应着完整数据 * 描述张三时,会用它的特征来描述:他名叫张三、男性、30多岁、秃顶、从事 java 开发、年薪百万,这些特征就对应着哈希摘要,以后拿到这段描述,就知道是在说张三这个人 * 为什么说摘要能区分不同数据呢,看这段描述:还是名叫张三、男性、30多岁、秃顶、从事 java 开发、月薪八千,有一个特征不符吧,这时可以断定,此张三非彼张三 --- ## 20. 解释签名算法? 电子签名,主要用于**防止数据被篡改** 。 先思考一下单纯用摘要算法能否防篡改?例如  * 发送者想把一段消息发给接收者 * 中途 message 被坏人改成了 massage(摘要没改) * 但由于发送者同时发送了消息的摘要,一旦接收者验证摘要,就可以发现消息被改过了 坏人开始冒坏水  * 但摘要算法都其实都是公开的(例如 SHA-256),坏人也能用相同的摘要算法 * 一旦这回把摘要也一起改了发给接收者,接收者就发现不了 怎么解决?  * 发送者这回把**消息连同一个密钥** 一起做摘要运算,密钥在发送者本地不随网络传输 * 坏人不知道密钥是什么,自然无法伪造摘要 * 密钥也可以是两把:公钥和私钥。私钥留给发送方签名,公钥给接收方验证签名,参考下图 * 注意:验签和加密是用对方公钥,签名和解密是用自己私钥。不要弄混  --- ## 21. 你们项目中密码如何存储? * 首先,明文肯定是不行的 * 第二,单纯使用 MD5、SHA-2 将密码摘要后存储也不行,简单密码很容易被彩虹表攻击,例如 * 攻击者可以把常用密码和它们的 MD5 摘要结果放在被称作【彩虹表】的表里,反查就能查到原始密码 因此 * 要么提示用户输入足够强度的密码,增加破解难度 * 要么我们帮用户增加密码的复杂度,增加破解难度 * 可以在用户简单密码基础上加上一段盐值,让密码变得复杂 * 可以进行多次迭代哈希摘要运算,而不是一次摘要运算 * 还有一种办法就是用 BCrypt,它不仅采用了多次迭代和加盐,还可以控制成本因子来增加哈希运算量,让攻击者知难而退 --- ## 22. 请介绍一下二分查找算法 参考回答: * 二分查找也称之为折半查找,是一种在***有序*** 数组内查找特定元素的搜索算法,非常高效,时间复杂度是**O(\log{n}** * 它具体实现步骤是: * 定义两个指针 i、j,分别指向有序数组的起始和结束位置 * 找到指针范围内中间元素 * 如果目标 < 中间元素,则在左半部分继续搜索 * 如果目标 > 中间元素,则在右半部分继续搜索 * 如果目标 = 中间元素,则找到目标,算法结束 参考代码: ```Java // a 为已排序数组,target 为搜索目标 static int binarySearch(int[] a, int target) { int i = 0, j = a.length - 1; while (i <= j) { // m 为中间索引,无符号右移是避免整数除法溢出 int m = (i + j) >>> 1; // 目标小于中间元素,改动右边界(下次循环在左边搜索) if (target < a[m]) { j = m - 1; } // 目标大于中间元素,改动左边界(下次循环在右边搜索) else if (a[m] < target) { i = m + 1; } // 找到 else { return m; } } // 未找到 return -1; } ``` --- ## 23. 什么是HTTP协议 ? * **必答内容:** HTTP协议就是 "超文本传输协议",规定了客户端与服务器端数据通信的规则。 而HTTP协议,它的底层,是基于TCP协议的,而TCP协议呢,是面向连接、安全且无状态的协议。 那在现在的Web开发中,基本上所有的请求都是基于HTTP协议 或 HTTPS协议的。 * **可能追问的问题:** 1). 那HTTP协议与HTTPS协议的区别是什么 ? * 那HTTP协议与HTTPS协议最大的区别,当然是数据传输的安全性了。 HTTP协议的信息是以明文传输,如果敏感信息被截取了,是可以直接获取传递的信息的。 相对之下,HTTPS协议是基于SSL加密传输的信息,可以确保数据的安全传输。 * 还有呢,就是端口不同。 HTTP协议默认端口 80,而HTTPS协议默认的端口 443。 所以说,HTTP协议的安全性没有HTTPS高,但是HTTPS协议会比HTTP耗费更多的服务器资源。 --- ## 24. HTTP协议中请求方式GET 与 POST 什么区别 ? * **必答内容:** 那两种请求方式,使我们进行项目开发,最为常见的两种请求方式。 两者的区别主要有以下几点: * 传递参数的大小限制不同。GET请求参数在URL中传递,所以参数的大小会收到URL长度的限制。 而POST请求,是在请求体中传递参数,只受到服务器端的配置限制。 * 安全性不同。 GET请求的参数暴露在URL中,安全性较低,不适合传递敏感信息。 而POST请求参数在HTTP消息体中传递,安全性相对较高。 * 应用场景不同。 GET请求一般用于获取数据,而POST请求则用于提交数据。 * **进阶回答:** 那在项目开发中,现在的url风格,基本都是restful风格。所以呢,项目开发中,请求方式除了GET、POST之外,还有像PUT、POST也是非常常用的。 * **可能会继续追问的问题:** 你刚才提到Restful,什么是Restful,谈谈你的理解? Restful其实就是一种软件架构风格,那既然是一种风格,就说明是可以被打破的,项目开发可以不按这套风格来。 但是我之前接触的项目,都是Restful风格的。 按照我的理解,Restful风格的两大特点: * 通过请求url地址,来定位要操作的资源。(如:http://localhost:8080/users/1,通过这个url,我就知道对1号用户资源进行操作) * 通过请求方式,来决定对资源进行什么样的操作。比如,GET 方式,就是用来查询的;POST方式,就是用来新增的;PUT方式,就是用来修改数据的;而DELETE方式就是用来删除数据的。 --- ## 25. HTTP协议中常见的状态码 ? HTTP协议的状态码,大的方面来说,分为5类, 分别是1xx,2xx,3xx,4xx,5xx。而在项目开发中,最为常见的状态码有这么几个: * 101:这个状态码,表示临时状态码,表示请求已经接受,服务器正在处理 (之前项目中,使用websocket时见到这个状态码) * 200:这个状态码,是最常见的,表示请求成功。 * 302:表示重定向。 * 401:表示此次请求需要用户身份认证,未认证就响应401。 * 404:表示服务器无法找到对应的资源(请求路径找不到)。 * 500:服务器内部错误。 --- ## 26. 会话跟踪方案 ### Cookie会话跟踪的原理? 会话跟踪的方案有很多,比如像 Cookie、Session、以及令牌技术,都可以进行会话跟踪。 Cookie是属于客户端会话跟踪方案,是存储在客户端浏览器的。 当我们第一次访问服务器的时候,服务器会创建Cookie,并在响应头 `set-Cookie` 中将Cookie响应给浏览器,浏览器接收到响应头之后,会自动将Cookie的值存储在浏览器中。 然后在后续的每一次请求中,浏览器都会自动的获取浏览器存储的Cookie值,并在请求头 `Cookie` 中将其携带到服务器,服务器就可以获取到Cookie中的数据了,从而完成会话跟踪. 所以,总的来说,Cookie会话跟踪的原理,其实就是HTTP协议中规定的两个头信息:一个是响应头 `Set-Cookie`,一个是请求头 `Cookie`。 但是由于Cookie存储在客户端浏览器,所以这种会话跟踪方案其实并不安全,因为用户是可以操作Cookie的(比如用户可以自己删除、禁用Cookie)。 **帮助理解的图示:**  ### Session会话跟踪的原理? Session是服务端会话跟踪方案,具体的机制是这样的: * 首先,当用户首次访问网站的时候,服务器会为该用户创建一个会话对象Session,而每一个Session对象都有一个唯一标识ID,同一次会话中需要共享的数据,就可以存储在Session中。然后在服务器给客户端浏览器响应的时候,会将会话对象Session的ID在响应头 `Set-Cookie` 中响应给浏览器。(Cookie的名字为JSESSIONID,Cookie的值为服务端会话对象Session的ID值) * 浏览器接收到Cookie之后,就会自动将Cookie的值(JSESSIONID)存储起来,然后在后续访问服务器的时候,再将Cookie的值(JSESSIONID)携带到服务器。 在服务器中,就可以根据 JSESSIONID的值,找到对应的会话对象Session,从而操作会话对象Session中的数据了。 所以,总的来说,Sesssion会话跟踪的底层,其实还是基于Cookie实现的。 在Session会话跟踪的过程中,基于Cookie传递的其实就是Session会话对象的ID。 那这种方案,虽然Session存储在服务器端,用户无法操作,比较安全。 但是,在集群环境下Session的共享却是一个问题。 **帮助理解的图示:**  --- ## 27. MySQL数据库中的 char 与 varchar的区别是什么? MySQL中的 `char` 和 `varchar` 都是用于存储字符串的数据类型,但它们在存储方式和性能上有所不同。以下是它们的主要区别: 第一点呢,就是存储方式不同: * char:定长字符串,长度是固定的,不管实际存储的字符串长度如何,都会占用固定长度的存储空间。如:char(10) 会始终占用10个字符的空间。 * varchar:变长字符串,长度不固定。占用的空间与实际存储的字段长度有关。 如:varchar(10) 表示最多可以存储10个字符,如果存储的字符串长度不足10,假设为5,只会占用5个字符空间。 第二点呢,就是性能不同: * 对于char,由于其固定长度,操作会快些,但是会存储浪费磁盘空间的问题。 * 对于varchar,由于长度可变,操作时会相对慢一点,但是可以节省磁盘空间,尤其是存储的数据长度不固定时。 所以呢,我们在设计表结构的时候,需要根据具体的场景来选择具体的数据类型。 就比如啊,如果是手机号、身份证号这样的字段,由于长度固定,我们就直接选择char类型即可,并指定长度,如:char(11)、char(18)。再比如,像用户名、备注信息这类长度不固定的,我们直接选择varchar类型,长度根据页面原型和需求文档确定。 --- ## 28. 什么是事务以及事务的四大特性? * **必答内容:** 事务是数据库中的基本概念,是指一组操作的集合,而这一组操作要么同时成功,要么同时失败,从而保证数据库中数据的正确性和完整性。 那事务呢,具有四大特性,也就是我们常说的ACID,分别是:原子性、一致性、隔离性、持久性。 那接下来,我就分别来聊聊这四大特性。 1). 原子性指的是事务中的这一组操作,是不可分割的最小操作单元了,操作要么全部成功,要么全部失败。 2). 一致性是指在事务操作的前后,必须使数据处于一致的状态。 3). 隔离性指的是数据库中提供了隔离机制,保证事务在不受外部并发操作的影响的独立环境中运行。 4). 持久性就比较简单了,就是事务一旦提交或回滚了,它对数据库的改变就是永久的。 * **可能继续发问的问题:** 1). 你刚才提到了并发事务,那并发事务回引发哪些问题? 并发事务引发的问题,主要有这么几个: * 脏读:就是一个事务,读取到了另一个事务还没有提交的数据。 * 不可重复读:指的是在同一个事务中,先后读取同一条记录,但两次读取的数据不同。 * 幻读:指的是一个事务按照条件查询数据时,没有对应的行,但是插入时,又发现这行数据已经存在了好像出现了幻觉。 2). 如何解决这些问题呢? 那这些问题,在数据库系统中都已经解决了。在数据库中提供了不同的隔离级别来解决这些问题, 分别有以下几种: * READ UNCOMMITED :读未提交。 这种隔离级别下,会出现脏读、不可重复读、幻读问题。 * READ COMMITED:读已提交。 这种隔离级别,解决了脏读问题,但是会出现不可重复读、幻读问题。 * REPEATABLE READ:可重复读。这种隔离级别,解决了脏读、不可重复读问题,但是会出现幻读问题。 * SERIALIZABLE:串行化。解决了上述所有的并发事务问题。 而在MySQL数据库中,默认的隔离级别是 `REPEATABLE READ`(可重复读)。 3). 那为什么没有用`SERIALIZABLE`(串行化) 这种隔离级别呢? 其实,隔离级别,也不是越高越好。因为隔离级别高了,确实可以解决并发事务引发的问题,但是隔离级别越高,性能也越低。 --- ## 29. 谈谈你对Spring IOC 与 DI的理解 ? * Spring的IOC,翻译过来,叫控制反转。 指的是在Spring中使用工厂模式,为我们创建了对象,并且将这些对象放在了一个容器中,我们在使用的时候,就不用每次都去new对象了,直接让容器为我们提供这些对象就可以了。 这就是控制反转的思想。 * 而DI,翻译过来,叫依赖注入。 那刚才提到,现在对象已经交给容器管理了,那程序运行时,需要用到某个对象,此时就需要让容器给我们提供,这个过程呢,称之为依赖注入。 **可能继续追问的问题:** * 那如何将一个对象,讲给IOC容器管理呢? 那现在项目开发,都是基于Springboot构建的项目,所以呢,声明bean对象,我们只需要在对应的类上加上注解就可以了。 比如: * 如果是controller层,直接在类上加上 `@Controller` 或 `@RestController` 注解。 * 如果是service层,直接在类上加上 `@Service` 注解。 * 如果是dao层,直接在类上加上 `@Repository` 注解。当然现在基本都是Mybatis 或 MybatisPlus,所以这个注解很少用了,都用的是 `@Mapper` 注解。 * 如果是一些其他的工具类、配置类啊,我们可以通过 `@Component` 、`@Configuration` 来声明。 * 那如何完成依赖注入操作呢 ? 依赖注入的方式比较多,我们可以使用构造函数注入 或 成员变量输入,也是使用对应的注解就可以了。常用的注解有两个: * `@Autowired` 和 `@Resource` 注解。 那 `@Autowired` 默认是根据类型注入,而 `@Resource` 注解默认是根据名称注入。 --- ## 30. Spring Bean的作用域如何设置,常见的取值有哪些 ? Spring Bean的作用域可以通过 `@Scope` 注解来设置。常见的取值如下: * singleton :这种bean范围是默认的,这种范围确保不管接受到多少个请求,每个容器中同一个名称的bean只有一个实例,也就是单例的 。 * prototype :这种范围,表示非单例的。也就是说每一次用到的bean都是一个新的 。 * request :同一个请求,使用的是同一个bean。会为每一个来自客户端的请求都创建一个实例,在请求完成以后, bean会失效并被垃圾回收器回收 。 * session:与request 请求范围类似,确保每个session会话范围内,是同一个实例,在session过期后, bean会随之失效 。 虽然,bean作用域可以设置这些值,但是在项目开发中,绝大部分的bean都不会添加这个 `@Scope` 注解,也就是说默认都是用的是单例的bean。 --- ## 31. Spring容器的bean什么时候初始化的? 嗯~ 这个得分情况来看哈。 * 如果是单例的bean,默认是Spring容器启动的时候,就完成bean的初始化操作,那这是默认情况,我们可以通过 @Lazy 注解来延迟bean的初始化,延迟到第一次使用的时候。 * 而如果是非单例的bean(也就是prototype),则是在每次使用这个bean的时候,都会重新实例化一个新的bean。 --- ## 32. 什么是AOP ? aop是面向切面编程,在spring中用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取公共模块复用,降低耦合,一般像系统的公共日志记录,事务处理,权限的控制等都可以通过AOP来实现。 **可能继续追问的问题:** 1). 你们项目中有没有使用到AOP? 这个很多地方都用到了,比如我们当时在后台管理系统中,就是使用aop来记录了系统的操作日志、以及权限控制。那就来说一下,记录日志的操作思路吧。 主要思路是这样的,使用aop中的环绕通知 加上 基于注解`@annotation` 的切点表达式来实现的。 * 首先,自定义了一个注解,比如叫 @Log,然后哪些操作需要记录日志,我们就在哪些方法上加上这个注解。 * 然后再定义一个切面类,通过环绕通知,来获取原始方法在运行的各项信息,比如:类信息、方法信息、注解、请求方式、请求参数、当前操作人、操作时间、返回值等信息,全部记录下来,保存在数据库中。 当时,我们主要记录的是一些核心业务模块的增删改的操作日志,主要便于数据追踪。 2). AOP的底层是如何实现的? SpringAOP的底层主要是通过动态代理技术实现的,主要是两种代理技术。一种是JDK的动态代理,而JDK的动态代理呢,有限制,只能针对于实现了接口的类做代理,所以,在spring中还有一种是Cglib动态代理,那Cglib动态代理呢,就没有这个限制。 3). JDK动态代理 与 Cglib动态代理有什么区别呢? * 限制不同。 JDK动态代理,要求被代理对象必须实现了接口才可以。 而Cglib动态代理,无论是否实现接口都可以(只要类不是final修饰即可)。 * 代理对象不同。 JDK动态代理生成的代理对象,与被代理对象其实是实现了相同的接口,可以说是兄弟关系。 而Cglib动态代理生成的代理对象,其实是继承了被代理类,是基于继承体系的,所以是父子关系。 也正是因为此,所以Cglib不能为被final修饰的类做代理。 --- ## 33. Spring中的事务是如何实现的? spring实现的事务本质就是aop完成,对方法前后进行拦截,在执行方法之前开启事务,在执行完目标方法之后根据执行情况提交或者回滚事务。 而如果我们进行项目开发,不存在分布式事务问题,我们就可以直接使用Spring提供的`@Transactional`注解来控制事务即可。 **可能继续发问的问题:** 1). 在开发中,有没有遇到事务失效的场景 ? 在刚开始上班做项目时,遇到过,但是现在在做项目,写代码时,都会规避这些坑。 事务失效比较典型的场景呢就是: * 第一个,如果业务方法上try、catch处理,自己处理了异常,没有抛出,就会导致事务失效,所以一般处理了异常以后,别忘了抛出去就行。 * 第二个,如果方法抛出检查异常,如果报错也会导致事务失效,因为默认spring事务管理只会针对于RuntimeException进行回滚。那这个呢,就可以在spring事务的注解上,就是@Transactional上配置rollbackFor属性为Exception.class,这样别管是什么异常,都会回滚事务。 * 第三个,是我早期开发中遇到的一个,如果业务方法上不是public修饰的,也会导致事务失效。 嗯,就能想起来那么多 2). 什么是事务的传播行为 ? Spring的事务传播行为,指的是两个被事务控制的方法,相互调用的过程中,到底是加入到已存在的事务,还是创建一个新的事务,控制的是这个事儿。 我们设置事务的传播行为,可以通过 @Transactional 注解的propagation属性来设置,可取值有很多啊,但是常见的就只有两个: * REQUIRED:也是默认值,表示如果没有事务,就新建一个事务,如果有事务,就加入到已存在的事务中。 * REQIRES_NEW:表示需要一个新的事务,无论当前环境是否存在事务,都会开启一个新的事务。 > PS: 下面伪代码,仅为了帮助理解: ```Java @Service public OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private OrderLogService orderLogService; @Transactional public void submitOrder(Order order){ try { //.... 订单数据处理 //.... orderMapper.insert(order); //保存订单数据 } finally { //.... 构造订单日志数据OrderLog orderLogService.insertLog(orderLog); //记录订单日志, 无论是否下单成功, 都需要记录 } } } ``` ```Java @Service public OrderLogServiceImpl implements OrderLogService{ @Autowired private OrderLogMapper orderLogMapper; @Transactional(propagation=Propagation.REQUIRES_NEW) public void insertLog(OrderLogorderLog){ orderLogMapper.insert(orderLog) } } ``` --- ## 34. 聊聊Spring框架中的常用注解 ? 额,这个就很多很多了。 我就分为这么几类,说一下吧。 第一类是:声明bean,有@Component、@Service、@Repository、@Controller 第二类是:依赖注入相关的,有@Autowired、@Qualifier、@Resourse 第三类是:设置作用域 @Scope 第四类是:spring配置相关的,比如@Configuration,@ComponentScan 和 @Bean 第五类是:跟aop相关做增强的注解 @Aspect,@Before,@After,@Around,@AfterReturning,@AfterThrowing,@Pointcut --- ## 35. spring框架中应用了哪些常用设计模式? Spring 框架是一个高度模块化和设计精良的框架,其内部大量使用了经典的设计模式。以下是 Spring 框架中常用的设计模式,结合具体的 Spring 知识点和代码进行详细描述: --- 1. **工厂模式(Factory Pattern)** **1.1 模式说明** 工厂模式用于创建对象,隐藏对象的创建逻辑,客户端只需通过工厂获取对象,而不需要关心对象的创建过程。 **1.2 Spring 中的应用** * **BeanFactory** : Spring 的核心接口 `BeanFactory` 是工厂模式的典型实现。它负责创建和管理 Bean 实例。 ```Java BeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml")); MyBean myBean = (MyBean) factory.getBean("myBean"); ``` * **ApplicationContext** : `ApplicationContext` 是 `BeanFactory` 的扩展,提供了更多企业级功能(如国际化、事件传播等)。 ```Java ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); MyBean myBean = context.getBean(MyBean.class); ``` **1.3 实际作用** * 解耦对象的创建和使用。 * 集中管理对象的生命周期。 --- 2. **单例模式(Singleton Pattern)** **2.1 模式说明** 单例模式确保一个类只有一个实例,并提供一个全局访问点。 **2.2 Spring 中的应用** * **Bean 的作用域** : Spring 默认使用单例模式管理 Bean。通过 `@Scope("singleton")` 或配置文件中的 `scope="singleton"` 指定。 **2.3 实际作用** * 减少资源消耗,避免重复创建对象。 * 适用于无状态的 Bean,如 Service、DAO 等。 --- 3. **代理模式(Proxy Pattern)** **3.1 模式说明** 代理模式为其他对象提供一个代理,以控制对原始对象的访问。 **3.2 Spring 中的应用** * **AOP(面向切面编程)** : Spring AOP 使用动态代理(JDK 动态代理或 CGLIB)实现切面功能。 ```Java @Aspect @Component public class LoggingAspect { @Before("execution(* com.example.service.*.*(..))") public void logBefore(JoinPoint joinPoint) { System.out.println("Before method: " + joinPoint.getSignature().getName()); } } ``` * **事务管理** : Spring 通过代理模式实现声明式事务管理@Transactional。 **3.3 实际作用** * 在不修改原始代码的情况下增强功能(如日志、事务、安全性等)。 * 解耦核心业务逻辑和横切关注点。 --- 4. **模板方法模式(Template Method Pattern)** **4.1 模式说明** 模板方法模式定义算法的骨架,将某些步骤延迟到子类中实现。 **4.2 Spring 中的应用** * **JdbcTemplate** : Spring 的 `JdbcTemplate` 提供了数据库操作的模板方法,开发者只需关注 SQL 和结果处理。 ```Java JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); List<User> users = jdbcTemplate.query("SELECT * FROM users", new UserRowMapper()); ``` * **RestTemplate** : 用于 RESTful 服务的调用。 ```Java RestTemplate restTemplate = new RestTemplate(); String result = restTemplate.getForObject("http://example.com/api", String.class); ``` **4.3 实际作用** * 提供通用的算法骨架,减少重复代码。 * 提高代码的可维护性和扩展性。 --- 5. **观察者模式(Observer Pattern)** **5.1 模式说明** 观察者模式定义对象间的一对多依赖关系,当一个对象状态改变时,所有依赖它的对象都会收到通知。 **5.2 Spring 中的应用** * **事件驱动模型** : Spring 提供了 `ApplicationEvent` 和 `ApplicationListener` 实现观察者模式。 ```Java // 自定义事件 public class MyEvent extends ApplicationEvent { public MyEvent(Object source) { super(source); } } // 事件监听器 @Component public class MyEventListener implements ApplicationListener<MyEvent> { @Override public void onApplicationEvent(MyEvent event) { System.out.println("Event received: " + event.getSource()); } } // 发布事件 @Autowired private ApplicationEventPublisher publisher; public void publishEvent() { publisher.publishEvent(new MyEvent(this)); } ``` **5.3 实际作用** * 实现松耦合的事件处理机制。 * 适用于需要解耦的业务场景,如异步通知、日志记录等。 --- 6. **适配器模式(Adapter Pattern)** **6.1 模式说明** 适配器模式将一个类的接口转换成客户端期望的另一个接口。 **6.2 Spring 中的应用** * **HandlerAdapter** : Spring MVC 使用 `HandlerAdapter` 适配不同的处理器(如 `@Controller`、`HttpRequestHandler` 等)。 ```Java public class MyHandlerAdapter implements HandlerAdapter { @Override public boolean supports(Object handler) { return handler instanceof MyHandler; } @Override public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 处理请求 return new ModelAndView("viewName"); } } ``` **6.3 实际作用** * 兼容不同的处理器类型。 * 提高框架的扩展性和灵活性。 --- 7. **装饰器模式(Decorator Pattern)** **7.1 模式说明** 装饰器模式动态地为对象添加额外的职责。 **7.2 Spring 中的应用** * **Bean 的装饰** : Spring 通过 `BeanPostProcessor` 实现 Bean 的装饰。 ```Java @Component public class MyBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) { // 在初始化前装饰 Bean return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) { // 在初始化后装饰 Bean return bean; } } ``` **7.3 实际作用** * 动态增强 Bean 的功能。 * 适用于 AOP、事务管理等场景。 --- 8. **策略模式(Strategy Pattern)** **8.1 模式说明** 策略模式定义一系列算法,并将每个算法封装起来,使它们可以互换。 **8.2 Spring 中的应用** * **ResourceLoader** : Spring 的 `ResourceLoader` 根据不同的资源路径(如 classpath、file、URL)选择不同的加载策略。 ```Java Resource resource = new ClassPathResource("config.xml"); InputStream inputStream = resource.getInputStream(); ``` **8.3 实际作用** * 提供灵活的算法选择。 * 适用于需要动态切换策略的场景。 --- 9. **组合模式(Composite Pattern)** **9.1 模式说明** 组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构。 **9.2 Spring 中的应用** * **AOP 中的 Advisor** : Spring AOP 将多个 `Advisor` 组合成一个链,依次执行。 ```Java @Aspect @Component public class MyAspect { @Before("execution(* com.example.service.*.*(..))") public void beforeAdvice() { System.out.println("Before advice"); } } ``` **9.3 实际作用** * 统一处理多个对象。 * 适用于需要递归或层次化处理的场景。 --- **总结** Spring 框架通过巧妙地运用多种设计模式,实现了高度的灵活性、扩展性和可维护性。以下是主要设计模式的总结: | 设计模式 | Spring中的应用 | 实际作用 | | ------------ | ------------------------------------- | -------------------------------- | | 工厂模式 | BeanFactory、ApplicationContext | 解耦对象的创建和使用 | | 单例模式 | Bean 的作用域(Singleton) | 减少资源消耗,避免重复创建对象 | | 代理模式 | AOP、事务管理 | 增强功能,解耦核心业务逻辑 | | 模板方法模式 | JdbcTemplate、RestTemplate | 提供通用的算法骨架,减少重复代码 | | 观察者模式 | ApplicationEvent、ApplicationListener | 实现松耦合的事件处理机制 | | 适配器模式 | HandlerAdapter | 兼容不同的处理器类型 | | 装饰器模式 | BeanPostProcessor | 动态增强 Bean 的功能 | | 策略模式 | ResourceLoader | 提供灵活的算法选择 | | 组合模式 | AOP 中的 Advisor | 统一处理多个对象 | --- ## 36. Spring MVC的核心组件有哪些? 好的,SpringMVC中的组件比较多,职责各不相同,那我就说一下核心的几个。 * 那首先第一个就是DispatchServlet,叫核心控制器,这个是SpringMVC中最为核心的组件,其本质就是一个Servlet,用于将请求分发给相应的处理程序,通过DispatchServlet这个组件,就可以降低组件之间的耦合度。 * 那第二个核心组件就是HandlerMapping,叫处理器映射器,这个组件的作用,就是根据请求的url匹配能够处理这次请求的Handler(指Controller中的方法) * 那还有就是HandlerAdapter,叫处理器适配器,其作用呢,就是来执行Handler处理器的,并获取到执行的结果。 * 第四个核心组件,就是Handler,叫处理器,其实可以简单理解为,就是我们开发的Controller中的方法。 * 最后一个就是视图解析器 ViewResolver,其作用是进行视图的解析,根据逻辑视图名解析成真正的视图(View)。当然,在现在前后端分离的开发模式中,基本上也不存在对应的jsp、freemarker这一类的视图解析了。 那刚才所提到的这些个组件呢,只有一个组件,是需要我们开发的,就是Handler,其他的组件,都不需要我们自己开发,框架底层已经提供了这些组件,并且现在我们直接基于SpringBoot进行项目开发,这些组件我们也不需要在做额外的配置了,SpringBoot底层已经自动配置好了。 --- ## 36. SpringMVC的请求执行流程是什么样的?  SpringMVC的请求执行流程如下所示: 1). 用户发送请求到前端控制器DispatcherServlet 。 2). DispatcherServlet接收到请求之后,会调用HandlerMapping(处理器映射器),来查找能够处理本次请求的处理器,生成处理器对象及处理器拦截器(如果有),然后再一起返回给DispatcherServlet 。 3). DispatcherServlet调用HandlerAdapter处理器适配器,让其执行对应的Handler。 4). HandlerAdapter执行对应的Handler(Controller中的方法),并将执行的结果封装在ModelAndView中返回给DispatcherServlet。 5). DispatcherServlet将ModelAndView传给ViewReslover(视图解析器),视图解析器负责对视图进行解析处理,最后返回视图对象View。 6). DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。 7). DispatcherServlet响应用户 。 --- ## 38. SpringMVC的拦截器用过没有? * **必答内容:** 拦截器的应用场景还是很多的,比如在项目中,我们基于拦截器实现登录校验的功能、参数统一转换处理、数据的脱敏、统一编码处理等功能。 在SpringBoot项目拦截器的使用分为两步进行: 第一步呢,需要定义一个类实现HandlerInterceptor接口,然后再实现接口中的方法,比如:preHandle、postHandle、afterCompletion。 第二步呢,就是需要定义一个配置类,然后实现WebMvcConfigure,然后在这个配置类中配置拦截器,指定拦截器的拦截路径、排除哪些路径等信息。 * **可能继续追问的问题:** 你说的这些个功能,过滤器好像也能干,那拦截器Interceptor 与 过滤器Filter有什么区别? * 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。 * 拦截范围不同:过滤器Filter会拦截web服务器中的所有资源,而Interceptor只会拦截Spring环境的资源,主要就是Controller。 * 实现机制不同:过滤器在实现上是基于函数回调的,拦截器是基于java的反射机制的,属于面向切面编程的一种运用。 那其实在开发中,很多的功能,我们既可以通过过滤器Filter来实现,也可以通过拦截器Interceptor来实现。 --- ## 39. SpringMVC怎么处理异常? SpringMVC的异常处理,就比较简单了,可以直接使用Spring MVC中的全局异常处理器对异常进行统一处理,此时在我们的三层架构中,都不需要处理异常了,如果运行过程中出现异常,最终会被全局异常处理器捕获,然后返回统一的错误信息。 开发一个全局异常处理器需要使用到两个注解:@RestControllerAdvice 、@ExceptionHandler,@RestControllerAdvice加在全局异常处理器的这个类上,而@ExceptionHandler加在异常处理的方法上,来指定这个方法捕获什么样的异常。 那在定义异常处理方法的时候,可以也定义多个,根据业务的需求,可以针对不同类型的异常,进行不同的处理。 --- ## 40. 聊聊SpringMVC中的常用注解? SpringMVC中的注解就比较多了,平时项目开发中比较常用的注解有以下几个: 1、@RequestMapping:用于映射请求路径,可以定义在类上和方法上。用于类上,则表示类中的所有的方法都是以该地址作为父路径 。我们也可以基于该注解中的method属性,来限定请求方式,由此也衍生了几个注解,如:@GetMapping、@PostMapping、@PutMapping、@DeleteMapping。 2、@RequestBody:该注解实现接收请求的json数据,将json转换为java对象 。 3、@RequestParam:指定请求参数的名称 ,如果请求参数名与方法形参不一致,可以使用此注解映射绑定 。也可以使用该注解来设置参数的默认值。 4、@PathViriable:从请求路径中获取请求参数(/user/{id}),传递给方法的形式参数 。 5、@ResponseBody:注解实现将controller方法返回值直接作为请求体响应,如果返回值是对象/集合,会转化为json对象响应给客户端 。 6、@RequestHeader:获取指定的请求头数据 。 --- ## 41. 聊聊你对SpringBoot框架的理解 ? SpringBoot是现在Spring家族最为流行的子项目,因为采用原始的SpringFramework框架开发项目,配置起来非常的繁琐,所以在Spring的4.0版本之后,Spring家族推出了SpringBoot框架,而Springboot就是来解决Spring框架开发繁琐的问题的,是用来简化spring框架开发的。 主要提供了这么三大块功能: * starter起步依赖。springboot提供了各种各样的starter,在starter起步依赖中,就封装了常用的依赖配置,大大简化了项目引入依赖坐标的复杂度。 * 自动配置。 这也是springboot中最核心的功能,springboot可以根据特定的条件(当前环境是否引入对应的依赖、配置文件中是否有某个配置项、当前环境是否已经有了某个bean)来创建对象的bean,从而完成bean的自动配置。 * jar包方式运行。 springboot中内嵌了web服务器,所以我们开发的web项目,也可以直接打成一个jar包,直接基于java -jar 执行运行,非常的方便。 当然,这些呢,只是Springboot中提供的核心功能,还有其他的一些小功能,都是非常实用的 。 --- ## 42. Spring Boot配置的优先级? SpringBoot项目中,可以在很多地方来配置项目中的配置项,那这里我主要说两个方面:一个是配置文件,一个是外部配置。 * 在springboot项目中,支持三类配置文件,分别是:application.properties、application.yml、application.yaml。 而这三类配置文件的优先级最高的是 application.properties,其次是 application.yml,最后是 application.yaml。 * 而外部配置呢,常用的有两种配置形式,一种是java系统属性,比如:-Dserver.port=9001;另一种是命令行参数,比如:--server.port=10010。而命令行参数的优先级要高于java系统属性。 * 而整体上,外部配置的优先级要高于项目内部的配置文件中的配置,所以整体来说配置文件的优先级由高到低的顺序为: * 命令行参数 > java系统属性 > application.properties > application.yml > application.yaml --- ## 43. SpringBoot自动配置的原理是什么? 嗯,好的,它是这样的。 其实SpringBoot自动配置的核心,是引导类上加的注解`@SpringBootApplication `底层封装的一个注解,叫`@EnableAutoConfiguration`,这个注解才是实现自动化配置的核心注解。 该注解通过`@Import`注解导入对应的配置选择器,导入了一个ImportSelector接口的实现类。 而在这个类的内部呢,读取了该项目和该项目引用的Jar包中的classpath路径下`META-INF/spring.factories`文件中的所配置的类的全类名。 在这些配置类中所定义的Bean,会根据条件注解@Condition系列注解所**指定的条件来决定** 是否需要将其导入到Spring容器中。 一般条件判断会有像`@ConditionalOnClass`这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的Bean放入spring容器中使用。 但是这里要说明一点哈,就是刚才提到的系统配置类声明的配置文件 `META-INF/spring.factories`, 在springboot3.0版本之后,就已经废除了,不会在这个文件中配置自动配置类了,替换成了一份新的配置文件,配置文件名比较长,记不住,后缀名为:`XXxxxSpringAutoConfiguration.imports`。 --- ## 44. SpringBoot中如何自定义starter? 嗯,这个我知道,之前在项目中,我们封装过的,像阿里云OSS操作的starter。 * 首先,先说模块哈,自定义starter,我们通常会定义两个maven模块。 * 一个是:xxx-spring-boot-starter ,这个模块主要负责管理依赖,最后项目中引入的就是这个模块。 * 另一个是:xxx-spring-boot-autoconfigure,这个模块负责自动配置功能,所有的自动配置的核心代码,都定义在这个模块中。并在在starter模块中,还要引入autoconfigure这个模块的依赖。 * 然后再来说核心的autoconfigure模块的实现。 * 第一步呢,就是要根据具体的需求,编写自动配置类。 基于@Configuration注解定义配置类,然后使用@Bean注解来声明bean,可以通过@Conditional系列的注解,根据条件决定是否声明这个bean。 * 第二步,就是需要在`META-INF/spring.factories`配置文件中,根据规则配置自动配置类的全类名。 如果是SpringBoot3.x版本,则需要在新的配置文件,`META-INF/spring/xxxx.SpringAutoConfiguration.imports`文件中配置自动配置类的全类名。 基本上,就这么两块儿。定义好了之后,在各个项目中,就可以引入对应的starter直接使用了。 --- ## 45. mapper传参? 在 MyBatis 中,Mapper 接口中定义的方法可以接受参数来进行数据库操作。Mapper 方法可以接受多种类型的参数,包括基本数据类型、Java 对象、Map 等。 1、在mapper接口中的方法形参前面使用 `@Param` 指定名称;然后在映射文件或者`@Select` 注解内直接使用 `#{}` 或者 `${}` 获取; 2、不使用 `@Param` ;那么直接在需要使用的sql语句中,参数的名称要写成 param1,param2。。。 3、如果传递的是一个对象或者map类型;那么在sql也可以直接使用对象或map的属性或key名称 --- ## 46. maven 用来做什么? Maven 是一个流行的项目管理工具,主要用于 Java 项目的构建、依赖管理和项目信息管理。以下是 Maven 的主要用途: 1. **项目构建** : 1. Maven 可以帮助开发者对 Java 项目进行自动化构建。通过 Maven 的约定优于配置的原则,开发者只需定义项目的基本结构和依赖关系,Maven 就可以自动完成项目的编译、测试、打包等构建工作。 2. **依赖管理** : 1. Maven 通过中央仓库和本地仓库来管理项目的依赖库。开发者可以在项目的配置文件中声明所需的依赖,Maven 将自动下载并管理这些依赖库。 3. **项目信息管理** : 1. 通过 Maven,开发者可以方便地管理项目的元数据信息,如项目名称、版本号、作者、许可证等。这些信息可以被用于生成项目文档、发布到中央仓库等操作。 4. **项目报告** : 1. Maven 提供了丰富的插件机制,可以生成各种项目报告,如单元测试报告、代码覆盖率报告、静态代码分析报告等,帮助开发者更好地了解项目的状态。 5. **项目部署** : 1. Maven 可以帮助开发者将构建好的项目部署到指定的环境中,如本地、测试、生产环境等。 --- ## 47. Maven的生命周期? Maven 生命周期按照构建过程分为三个部分,即 清理(clean)、构建(build) 和 站点生成(site)。每个部分包含不同的阶段(phase),执行的顺序是固定的,也就是说 Maven 在执行构建时会依次运行每个阶段。 以下是 Maven 的生命周期和各个阶段的简要说明: 1. **清理生命周期** : 1. `pre-clean`:在清理之前执行的动作 2. `clean`:清理上一次构建生成的文件 2. **构建生命周期** : 1. `validate`:验证项目是否正确并且所有必要信息都可用 2. `compile`:编译项目的源代码 3. `test`:测试编译后的代码 4. `package`:将编译后的代码打包成可发布的格式,如 JAR、WAR 等 5. `install`:将打包好的代码安装到本地仓库,方便其他项目进行依赖管理 6. `deploy`:将打包好的代码部署到远程仓库,方便其他人使用 3. **站点生命周期** : 1. `pre-site`:在生成站点之前执行的动作 2. `site`:生成项目的站点文档 3. `post-site`:在生成站点之后执行的动作,如部署站点到服务器上 4. `site-deploy`:将生成的站点部署到远程服务器上 最后修改:2025 年 11 月 19 日 © 来自互联网 赞 如果觉得我的文章对你有用,请随意赞赏