这是根据个人面试经历总结出来的一些经验希望可以帮助到有需要的人。
面试的时候,会先让你进行自我介绍,这个大家准备一两分钟的面试稿就可以。然后就是正式面试,面试官一般是两个人以上,开始,面试官会先提问一些基本知识,很基础,基本这个大家是没什么问题的,都可以回答应对。最重要的一点来了,就是中间提问,这个提问会根据你的简历上面的掌握技能来进行,所以简历上面一定要写自己掌握的,如果写上自己没掌握的知识,被面试官提问到如果回答不上来,会很拉分。最后就是面试官会问一些你开发过程中遇到的问题,然后怎么解决的。
一、Java
1、Java是一种什么语言?有哪些特点?
Java是一种面向对象的编程语言,它的设计理念是“一次编写,到处运行”,意思是你写好一段 Java 代码后,可以在不同的平台上运行,比如 Windows、Linux 和 macOS,而不需要对代码做特别的修改。这是因为 Java 使用了一个叫做“Java 虚拟机(JVM)”的东西,它帮助代码在不同的操作系统上运行。
特点:
简单易学:它的语法结构类似于C和C++,容易上手。
跨平台性:得益于JVM,Java程序可以在不同的操作系统上运行。
面向对象:Java鼓励把问题分解为多个“对象”,每个对象都有属性和行为,这样代码更好理解和维护。
安全性高:Java内置了很多机制,减少了程序漏洞和安全风险。
广泛应用:Java被广泛用于Web开发、移动应用(特别是Android开发)、企业级应用和大数据处理等领域。
2、Java的三大特性?
Java 的三大特性是 封装、继承 和 多态。这三大特性是面向对象编程的核心思想,也是 Java 语言的基础。下面分别解释每个特性,并用通俗易懂的方式说明它们的作用。
封装(Encapsulation)
封装是指将对象的属性和方法隐藏在类的内部,不允许外部直接访问,而是通过提供的公有方法(getter、setter)来控制对这些属性的访问和修改。这种做法保护了数据的完整性。
继承(Inheritance)
继承允许一个类(子类)从另一个类(父类)继承属性和方法。这样,子类可以复用父类的代码,也可以根据需要重写或扩展父类的方法。
多态(Polymorphism)
多态允许对象在不同情况下表现出不同的行为。在 Java 中,多态有两种形式:方法重载(同一个类中方法名称相同,但参数不同)和方法重写(子类重写父类的方法)。
3、Java有哪些数据类型?
Java有8种基本数据类型,它们主要用于存储简单的数值或字符。可以根据类型进行分类,按照它们的存储大小和用途进行介绍。
基本数据类型
整数类型
byte
:占 1字节,用于表示范围为 -128 到 127 的整数。
short
:占 2字节,用于表示范围为 -32,768 到 32,767 的整数。
int
:占 4字节,这是最常用的整数类型,表示范围为 -2^31 到 2^31-1。
long
:占 8字节,用于表示范围更大的整数,范围为 -2^63 到 2^63-1。
浮点类型
float
:占 4字节,用于单精度浮点数,适合存储小数。
double
:占 8字节,用于双精度浮点数,精度更高,适合处理更精确的计算。
字符类型
char
:占 2字节,用于表示单个字符,存储Unicode字符(如 'A' 或 '汉')。
布尔类型
boolean
:占用大小未明确规定(具体实现依赖虚拟机),通常表示 true
或 false
两个值。
引用数据类型
引用数据类型主要包括类(class)、接口(interface)、数组(array)。这些类型的变量存储的是对象的引用,而不是对象的实际值。引用类型的内存大小依赖于具体的实现(即对象的结构和虚拟机的实现)。
4、& 与 &&的区别?
&(按位与 & 逻辑与):
按位运算:当 &
作为按位运算符使用时,它会对两个数的二进制位进行逐位比较,并返回一个新的值。只有当两个位都是 1
时,结果才为 1
,否则为 0
。例如:
int a = 5; // 5 的二进制是 0101
int b = 3; // 3 的二进制是 0011
int result = a & b; // 结果为 0001,也就是 1
逻辑运算:当 &
用于逻辑运算时,它的行为类似于 &&
,用于比较两个布尔表达式的值,但不同的是**&
不会短路**。也就是说,无论第一个条件是否为 false
,都会继续计算第二个条件。例如:
boolean result = (5 > 3) & (2 < 1); // result 为 false,尽管第一个条件为 true,第二个条件仍然被计算
&&(短路与):
短路逻辑与,用于布尔表达式的比较。当第一个条件为 false
时,整个表达式直接返回 false
,不会再计算第二个条件。这样可以提高效率,特别是在涉及方法调用时。例如:
boolean result = (5 > 3) && (2 < 1); // result 为 false,因为第二个条件不会被计算
5、权限修饰符
6、重载和重写有什么区别?
重载定义:重载指的是在同一个类中,允许多个方法名字相同,但它们的参数列表不同(参数的个数或类型不同)。重载方法可以有不同的返回类型,但返回类型不能作为区分重载的依据。
特点:
方法名相同,参数不同(可以是参数个数、类型或顺序不同)。
发生在同一个类中。
重载方法可以有不同的返回类型和访问修饰符。
用途:重载提高了代码的灵活性,允许同一个方法名处理不同类型或数量的输入。
示例:
public class Example {
public void print(int a) {
System.out.println(a);
}
public void print(String b) {
System.out.println(b);
}
}
在这个例子中,print 方法被重载了两次,一个接受 int 参数,一个接受 String 参数。
重写定义:重写指的是在子类中定义一个与父类方法相同名称、相同参数列表的方法,以便修改或扩展父类的行为。重写的目的是为子类提供特定的实现。
特点:
方法名、参数列表、返回类型必须相同。
发生在子类与父类之间。
重写方法不能降低父类方法的访问级别(例如,父类的 public
方法不能在子类中被重写为 private
)。
用途:重写用于在继承中修改或扩展父类的方法,提供特定子类的实现。
示例:
class Animal {
public void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("Dog barks");
}
}
在这个例子中,Dog 类重写了 Animal 类的 sound 方法,提供了子类特定的行为。
重载 vs 重写 的区别总结:
7、==和equals的区别?
==
的作用和特点:
用途:==
用于比较两个变量的内存地址,即它们是否引用的是同一个对象。
基本数据类型:当 ==
用于比较基本数据类型(如 int
、char
、float
)时,比较的是它们的值是否相等。
int a = 5;
int b = 5;
System.out.println(a == b); // 输出 true,因为 a 和 b 的值相同
引用数据类型:当 ==
用于比较引用类型(如 String
、Object
)时,比较的是两个对象在内存中的地址是否相同,即它们是否指向同一个对象。
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1 == str2); // 输出 false,因为 str1 和 str2 是不同的对象,地址不同
equals()
的作用和特点:
用途:equals()
方法用于比较两个对象的内容是否相同,而不是内存地址。equals()
在 Object
类中是定义过的,默认情况下它和 ==
的作用一样,比较的是内存地址。但是,大多数类(如 String
、Integer
)都重写了 equals()
方法,使其用于比较对象的内容。
equals()
:如果类没有重写 equals()
方法,那么 equals()
和 ==
的比较结果是一样的。如果重写了 equals()
方法(如 String
类),则比较的是对象的内容。
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1.equals(str2)); // 输出 true,因为 String 重写了 equals() 方法,比较的是内容
8、String、StringBuffer、StringBuilder的区别?
String
不可变性:String
类的对象是不可变的,一旦创建后,它的值就不能改变。每次对 String
进行修改(如拼接、替换)时,都会创建一个新的 String
对象,并且将原始对象废弃。
示例:
String str = "Hello";
str = str + " World"; // 创建了一个新的字符串对象
StringBuffer
可变性:StringBuffer
是可变的,可以对同一个对象进行多次修改(如拼接、插入、删除),不会创建新的对象。
线程安全:StringBuffer
是线程安全的,因为它的所有方法都被 synchronized
修饰,确保多线程访问时不会发生数据不一致的问题。
性能:由于 StringBuffer
是线程安全的,它在多线程环境下使用是安全的,但由于引入了同步机制,其性能相对较低,特别是在单线程场景下。
示例:
StringBuffer sb = new StringBuffer("Hello");
sb.append(" World"); // 修改的是同一个对象
StringBuilder
可变性:和 StringBuffer
一样,StringBuilder
也是可变的,允许在同一个对象上进行多次修改。
非线程安全:StringBuilder
不是线程安全的,它没有同步机制,因此在多线程环境下使用 StringBuilder
可能导致数据不一致。
性能:StringBuilder
的性能比 StringBuffer
高,尤其是在单线程环境下,因为它没有同步的开销。
示例:
9、抽象类和接口的区别
10、jdk1.8的新特性
Lambda 表达式
定义:Lambda 表达式是 Java 8 中引入的一种更简洁的函数表达方式。它允许你使用更简短的代码来定义匿名函数,特别适合简化一些只需要一小段代码的场景,比如集合的操作或回调函数
示例:
// 传统写法
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello World");
}
}).start();
// Lambda 表达式写法
new Thread(() -> System.out.println("Hello World")).start();
函数式接口(Functional Interface)
定义:Java 8 中引入了函数式接口的概念,它是指只包含一个抽象方法的接口,并可以和 Lambda 表达式一起使用。@FunctionalInterface
注解用于标记函数式接口。
示例:
@FunctionalInterface
interface MyFunction {
void apply();
}
MyFunction myFunction = () -> System.out.println("Hello from Functional Interface");
Stream API
定义:Stream 是 Java 8 中为处理集合数据提供的全新 API,它允许你使用声明式编程风格来进行复杂的数据处理操作,如筛选、映射、聚合等。Stream 可以极大简化集合处理的代码,支持并行操作,提高性能。
示例:
List<String> names = Arrays.asList("Tom", "Jerry", "Mike");
names.stream()
.filter(name -> name.startsWith("T"))
.forEach(System.out::println);
默认方法
定义:Java 8 允许在接口中定义默认方法(default
methods),这些方法在接口中提供了默认实现,不强制子类进行重写。这样做可以避免因为接口的扩展而导致的现有类的编译错误。
示例:
interface Animal {
default void run() {
System.out.println("Running");
}
}
class Dog implements Animal {
// 可以选择不重写 run() 方法,使用默认实现
}
Optional 类
定义:Optional
是一个容器类,用于防止 null
值引发的空指针异常(NullPointerException)。通过 Optional
,开发者可以显式地处理可能为 null
的值,而不是依赖于 null
检查。
示例:
Optional<String> optionalStr = Optional.ofNullable(null);
System.out.println(optionalStr.orElse("默认值")); // 输出 "默认值"
方法引用
定义:方法引用是 Lambda 表达式的简化形式,允许你直接引用已有的方法或构造函数,可以通过 ::
符号来调用。
示例:
List<String> names = Arrays.asList("Tom", "Jerry", "Mike");
names.forEach(System.out::println); // 使用方法引用简化代码
新的日期和时间 API
定义:Java 8 引入了全新的日期和时间 API(java.time
包),如 LocalDate
、LocalTime
和 LocalDateTime
,它们比原来的 java.util.Date
和 Calendar
更易用、线程安全,并且提供了更多功能。
示例:
LocalDate today = LocalDate.now();
System.out.println(today); // 输出当前日期
Nashorn JavaScript 引擎
定义:Java 8 引入了一个新的 JavaScript 引擎——Nashorn,允许开发者在 Java 应用中直接嵌入和执行 JavaScript 代码。
11、Java中的数据结构?
数组(Array)
简介:数组是最基本的数据结构,存储固定大小的同类型元素。
特点:
大小固定:数组在声明时需要确定大小,无法动态改变。
连续存储:元素在内存中是连续的,可以通过索引快速访问。
不支持动态扩展:无法自动调整大小,适合存储已知数量的元素。
示例:
int[] arr = new int[5];
arr[0] = 1;
List(列表)
简介:List
是有序的集合,允许元素重复。常用实现包括 ArrayList
和 LinkedList
。
特点:
ArrayList
:底层基于动态数组,支持快速随机访问,适合读操作较多的场景,但插入和删除效率相对较低。
LinkedList
:基于双向链表,适合频繁插入、删除操作的场景,但随机访问效率较低。
示例:
List<String> list = new ArrayList<>();
list.add("A");
Set(集合)
简介:Set
是无序集合,不允许元素重复。常见实现类有 HashSet
和 TreeSet
。
特点:
HashSet
:基于哈希表实现,插入和查找速度快,但不保证顺序。
TreeSet
:基于红黑树实现,元素按自然顺序或自定义顺序排列。
示例:
Set<String> set = new HashSet<>();
set.add("A");
Map(映射)
简介:Map
用于存储键值对,允许根据键快速查找对应的值。常用实现包括 HashMap
和 TreeMap
。
特点:
HashMap
:基于哈希表,查找和插入效率高,但不保证顺序。
TreeMap
:基于红黑树,按键的自然顺序或自定义顺序排列。
示例:
Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
栈(Stack)
简介:栈是后进先出(LIFO)的数据结构,元素只能从栈顶插入和取出。
特点:适合需要回退操作的场景,如函数调用栈。
示例:
Stack<Integer> stack = new Stack<>();
stack.push(1);
队列(Queue)
简介:队列是先进先出(FIFO)的数据结构,常用实现有 LinkedList
和 PriorityQueue
。
特点:适合任务排队处理,PriorityQueue
支持按优先级处理元素。
示例:
Queue<Integer> queue = new LinkedList<>();
queue.offer(1);
链表(LinkedList)
简介:链表是一种每个节点包含数据和指向下一个节点指针的数据结构。
特点:插入和删除操作效率高,尤其是双向链表(LinkedList
),但随机访问效率较低。
示例:
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("A");
12、集合
集合框架(单列集合)
集合框架(双列集合)
13、ArrayList和LinkedList区别?
ArrayList
基于动态数组实现,支持高效的随机访问(O(1)),但在中间位置插入和删除元素时,需要移动大量元素,效率较低,适合读操作较多的场景。它的内存开销较小,因为除了存储元素外,只有未使用的数组空间作为额外开销。
LinkedList
基于双向链表实现,适合频繁的插入和删除操作,尤其是在中间或头部位置时效率较高(O(1)),但随机访问效率较低(O(n)),因为需要遍历链表。它的内存开销较大,因为每个节点都需要存储前后指针。
14、Java中的工厂模式?
工厂模式(Factory Pattern):通过定义一个接口或抽象类来创建对象,并将具体对象的创建延迟到子类或具体实现中。使用场景:常用于如图形绘制系统中创建不同形状的对象(如圆形、矩形),或创建不同类型的交通工具(如汽车、摩托车)等。
单例模式(Singleton Pattern):确保一个类只有一个实例,并提供一个全局访问点。使用场景:需要一个唯一的数据库连接对象或线程池管理类时。
观察者模式(Observer Pattern):定义对象间的一对多依赖关系,当一个对象状态发生改变时,所有依赖它的对象都会收到通知并自动更新。使用场景:事件监听机制,如 GUI 中的按钮点击事件。
装饰者模式(Decorator Pattern):通过将对象放入包含行为的装饰类中,实现动态扩展对象功能,而不改变其本身的类定义。使用场景:为现有的类动态添加新功能,如给窗口添加滚动条功能。
策略模式(Strategy Pattern):定义一系列算法,并将它们封装到独立的类中,使得它们可以互相替换。使用场景:在支付系统中,根据不同支付方式选择不同的策略类(如微信支付、支付宝支付)。
代理模式(Proxy Pattern):为其他对象提供一种代理,以控制对该对象的访问。使用场景:远程代理(调用远程服务)、虚拟代理(延迟对象创建)等。
15、List、Set、Map区别?
List(列表)
定义:List
是一个有序的集合,允许元素重复,并且可以通过索引来访问元素。
特点:
有序:元素的插入顺序保留。
允许重复元素:同一个元素可以多次添加到列表中。
可以通过索引访问:支持快速的随机访问。
适用场景:适合存储需要按顺序排列的、可能包含重复元素的数据,比如用户输入的多项选择答案列表
常见实现类:ArrayList
:基于动态数组,随机访问快,但插入和删除效率相对较低。LinkedList
:基于双向链表,适合频繁插入和删除操作,但随机访问效率较低。
Set(集合)
定义:Set
是一个无序的集合,不允许元素重复。
特点:
无序:元素没有固定的顺序(TreeSet
例外)。
不允许重复元素:集合中的每个元素都是唯一的。
查找效率高:通过哈希表或树结构实现,通常用于快速查找。
适用场景:适合需要确保元素唯一的场景,比如存储唯一的用户 ID 或者处理去重操作。
常见实现类:HashSet
:基于哈希表实现,元素无序,插入、删除和查找效率高。TreeSet
:基于红黑树实现,元素按自然顺序或自定义顺序排列。
Map(映射)
定义:Map
用于存储键值对,每个键(key)是唯一的,对应的值(value)可以重复。
特点:
存储键值对:通过键来查找对应的值。
键必须唯一:不能有相同的键,但值可以重复。
高效查找:可以通过键快速找到对应的值。
适用场景:适合存储需要根据唯一标识符(键)查找对应值的场景,比如根据用户名查找用户信息。
常见实现类:HashMap
:基于哈希表实现,键无序,查找和插入效率高。TreeMap
:基于红黑树实现,键按自然顺序或自定义顺序排列。
16、HashMap和HashTable有什么区别?
线程安全性:
HashMap:非线程安全的,不保证多个线程同时访问时的一致性。如果有多线程并发访问,需要手动同步(如使用 Collections.synchronizedMap()
)。
Hashtable:线程安全的,内部方法是同步的(使用 synchronized
关键字),因此适合多线程环境。
性能:
HashMap:因为没有同步开销,所以在单线程环境下性能比 Hashtable 更高。
Hashtable:由于方法是同步的,会有额外的锁机制开销,因此性能比 HashMap 慢。
允许空值:
HashMap:允许一个 null
键和多个 null
值。
Hashtable:不允许键或值为 null
,插入 null
会抛出 NullPointerException
。
继承关系:
HashMap:继承自 AbstractMap,并实现了 Map 接口。
Hashtable:继承自 Dictionary 类,是更早期的类,同时也实现了 Map 接口。
扩展机制:
HashMap:默认初始容量为 16,负载因子为 0.75,当哈希表中元素数量超过容量的 75% 时会自动扩容。
Hashtable:默认初始容量为 11,负载因子为 0.75,扩容时容量增长为原来的 2 倍加 1。
17、为什么用线程池,解释下线程池参数?
降低资源消耗:线程池通过复用已经创建的线程,避免了频繁创建和销毁线程所带来的资源消耗,尤其在高并发环境下能显著提升性能。
提高响应速度:线程池中的线程是提前创建好的,当有任务时可以直接利用现有的线程执行,从而减少了等待线程创建的时间,提升系统的响应速度。
更好的管理线程:线程池允许通过设置参数来管理线程的数量和执行规则,避免了系统中线程数过多或线程资源浪费的问题。它通过有效的调度机制,防止系统因过多线程导致内存溢出或线程争用问题。
常用的线程池实现是 ThreadPoolExecutor
,它的几个关键参数如下:
corePoolSize(核心线程数):
线程池中保持存活的最小线程数。即使线程处于空闲状态,也不会被销毁,除非设置了 allowCoreThreadTimeOut
为 true
当新任务提交时,如果当前线程数小于 corePoolSize
,则创建新的线程处理任务。
maximumPoolSize(最大线程数):
线程池允许创建的最大线程数。当线程数达到 corePoolSize
,但任务队列已满时,线程池可以继续创建线程,直到线程总数达到 maximumPoolSize
。 如果任务数量持续增加并超出此限制,线程池将拒绝新的任务。
keepAliveTime(线程存活时间):
当线程数大于 corePoolSize
时,线程的最大空闲时间。超过这个时间,空闲线程将被终止并回收。 如果希望线程池中的空闲线程能够及时回收,可以缩短 keepAliveTime
,减少系统资源消耗。
unit(时间单位):
keepAliveTime
参数的时间单位,可以是 TimeUnit.SECONDS
、TimeUnit.MILLISECONDS
等,指定线程空闲时间的精度。
workQueue(任务队列):
用于保存等待执行的任务的队列。当所有核心线程都在执行任务时,新的任务将被放入这个队列中。常用的队列类型包括:
LinkedBlockingQueue
:一个无界队列,任务量很大时可以使用。 ArrayBlockingQueue
:一个有界队列,限制队列长度,防止任务过多时内存溢出。
threadFactory(线程工厂):
用于创建新线程的工厂,可以通过自定义 ThreadFactory
为线程设置名字、优先级等。
handler(拒绝策略):
当线程池无法处理新的任务时(队列已满且线程数达到 maximumPoolSize
),会触发拒绝策略,常见的拒绝策略包括:
AbortPolicy
:抛出 RejectedExecutionException
,拒绝任务。
CallerRunsPolicy
:由调用者线程执行该任务,避免丢弃任务。
DiscardPolicy
:丢弃任务,不抛异常。
DiscardOldestPolicy
:丢弃队列中最早的任务,执行新任务。
18、什么是序列化,反序列化?
序列化(Serialization):将对象的状态转换为字节流的过程,便于存储或传输。序列化后,可以将对象保存到文件、数据库,或通过网络发送到其他系统。
反序列化(Deserialization):将字节流重新转换为对象的过程。通过反序列化,可以从存储或网络中读取数据并恢复为内存中的对象
二、Spring
1、什么是Spring、核心特性?
定义:Spring 是一个开源的 Java 企业级应用开发框架,它提供了丰富的功能,帮助开发者构建高效、可扩展、易维护的 Java 应用。Spring 的核心是依赖注入(DI)和面向切面编程(AOP),这些机制大大简化了应用的开发和测试过程。
特性:
依赖注入(DI):Spring 通过依赖注入实现了对象之间的松耦合,开发者无需手动创建依赖对象,Spring 容器会自动为类注入所需的依赖。
面向切面编程(AOP):Spring 支持面向切面编程,用于分离系统中的横切关注点,比如日志记录、事务管理、安全控制等功能,从而简化核心业务逻辑。
Spring 容器:Spring 提供了一个 IOC(控制反转)容器,用于管理应用程序中的对象生命周期和依赖关系。
2、Spring优缺点?
优点:
Spring 通过**依赖注入(DI)和控制反转(IoC)**降低了类之间的耦合,使得代码更易于维护和测试。不同模块可以根据需求灵活组合使用,避免使用整个框架带来的复杂性。
Spring 支持 AOP,用于分离业务逻辑和横切关注点(如日志、事务管理、权限控制等),从而简化代码,实现代码的关注点分离。
Spring 可以轻松集成各种第三方框架和工具,如 Hibernate、JPA、MyBatis 等,极大简化了数据库和事务处理的开发。
Spring Boot 是 Spring 的扩展,简化了应用配置,提供开箱即用的开发环境,特别适合微服务架构和快速开发原型。
Spring 拥有活跃的社区和丰富的文档资源,开发者遇到问题时可以很快找到解决方案,快速上手或排除故障。
缺点:
Spring 的功能非常丰富,模块众多,对于新手来说,初次接触时可能会觉得难以掌握,需要花费时间学习其核心概念(如 IoC、AOP 等)以及各种配置方式。
尽管 Spring Boot 简化了配置,但在更复杂的应用中,Spring 的配置可能会非常繁琐,特别是涉及到多个模块时,可能需要大量的配置文件。
由于 Spring 提供了大量的功能和灵活性,这会在一定程度上增加应用的性能开销。过度使用依赖注入和 AOP 可能会导致应用启动时间延长或运行性能下降。
Spring 为了降低耦合度,使用了大量的抽象层次,有时会让代码变得不易理解,排查问题时需要深入了解其内部机制。
3、什么是Spring IOC ?
定义:Spring 的 IOC(Inversion of Control,控制反转)是一种设计模式,它将对象的创建和依赖管理的责任交给 Spring 容器,而不是由程序中的代码手动控制。简单来说,IOC 意味着将对象之间的依赖关系交由 Spring 框架来处理,实现松耦合的设计。
核心概念:
控制反转:传统的程序设计中,类内部通常通过自己创建或查找其他类的实例,而在 Spring 中,由容器负责对象的创建及依赖的注入,这个过程被称为“控制反转”。换句话说,对象不再自己控制依赖,而是由外部容器来控制。
依赖注入(DI):IOC 的实现方式之一就是依赖注入,即将对象所依赖的其他对象通过构造函数、Setter 方法或接口注入到该对象中。通过 DI,可以让对象专注于业务逻辑,而不需要关心依赖的创建。
作用:
解耦:通过将对象创建和依赖管理职责交给 Spring 容器,类之间的耦合性降低,代码更加灵活易于维护和测试。
提升可测试性:由于依赖关系由容器管理,测试时可以轻松替换或模拟对象,使得单元测试更加方便。
集中管理依赖:所有对象和依赖关系由 Spring 容器统一管理,便于维护复杂应用中的依赖关系。
4、什么是Spring AOP?
定义:Spring AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,它将与业务逻辑无关的功能(如日志、事务管理、权限控制等)从核心业务逻辑中分离出来。这些功能称为“切面”,可以在不修改原有代码的情况下动态地加入到程序的指定位置。
核心概念:
切面(Aspect):切面是一个模块,包含可以影响程序多个部分的功能,例如日志记录、事务管理等。
连接点(Join Point):程序执行过程中,可以插入切面的具体点,通常是方法的调用或执行。
通知(Advice):切面在某个连接点上执行的具体操作。通知有不同类型,比如在方法前执行、后执行、或抛出异常时执行等。
切入点(Pointcut):用于定义在哪些连接点上应用切面逻辑。通过表达式选择特定的方法或类。
目标对象(Target Object):AOP增强的对象,Spring AOP 是基于代理的,所以目标对象是被代理的对象。
织入(Weaving):将切面应用到目标对象并创建新的代理对象的过程。
作用:
关注点分离:将系统中某些“横切关注点”(如日志、安全、事务等)从业务逻辑中分离出来,使代码更加简洁、可维护,提升代码的可读性。
减少代码冗余:避免在多处重复编写相同的代码,例如在多个方法中手动添加日志记录或异常处理。
动态增强功能:可以动态地向现有代码中添加功能而不需要修改原有代码,增强代码的灵活性。
5、Spring常用注解?
@Controller:用于标注控制器层组件。标识一个类为 Spring MVC 控制器,用于处理 HTTP 请求。
@Service:用于标注业务层组件。标识一个类为业务逻辑处理类,通常是服务层组件。
@Repository:用于标注数据访问层组件。标识一个类为数据访问对象(DAO),专门用于与数据库交互。
@Component:通用组件注解。表示一个类是一个 Spring 容器管理的普通 Bean,其他注解如 @Controller
、@Service
、@Repository
都是 @Component
的特化版本。
@Autowired:用于自动注入依赖。自动注入 Spring 容器中的 Bean,可以应用于构造函数、Setter 方法或字段。
@Qualifier:用于指定注入的 Bean。与 @Autowired
一起使用,当有多个候选 Bean 时,通过 @Qualifier
指定具体注入哪一个。
@RequestMapping:用于映射 HTTP 请求。用于映射 HTTP 请求到控制器的方法或类上,可以指定请求路径、方法类型等。
@PathVariable:用于获取 URL 路径中的参数。绑定请求路径中的占位符到方法参数上。
@RequestBody:用于将请求体中的 JSON 数据转换为 Java 对象。用于将 HTTP 请求体中的 JSON 数据直接映射为方法参数中的 Java 对象。
@ResponseBody:用于将方法的返回值直接写入 HTTP 响应体。将控制器方法的返回值直接写入 HTTP 响应体,而不是解析为视图名称。
三、Mybatis
1、MyBatis是什么?
定义:MyBatis 是一个轻量级的持久化框架,通过XML 或 注解 配置将 SQL 语句映射到 Java 对象。它帮助开发者简化与数据库的交互,避免了手写大量 JDBC 代码,同时保持了对 SQL 的灵活控制。
核心功能:
SQL 映射:MyBatis 通过配置文件或注解将 SQL 语句与 Java 对象映射,开发者可以直接编写 SQL,并让 MyBatis 处理结果集的映射工作。
动态 SQL:MyBatis 提供动态 SQL 功能,可以在 XML 中使用 <if>
、<choose>
等标签生成动态 SQL 语句,满足复杂的查询需求。
支持存储过程:MyBatis 支持调用数据库的存储过程,简化复杂业务逻辑的处理。
与 Spring 集成:MyBatis 可以轻松与 Spring 框架集成,使得数据库访问更高效且易于管理。
优势:
控制 SQL 细节:相比于其他 ORM 框架(如 Hibernate),MyBatis 更加灵活,开发者可以直接编写 SQL,从而对查询优化、复杂查询逻辑有更精细的控制。
轻量级:MyBatis 轻量灵活,不需要像全功能 ORM 那样映射复杂的实体关系,非常适合需要大量自定义 SQL 的项目。
易于使用:通过简单的配置文件或注解即可实现数据的持久化,代码量较少。
使用场景:
复杂 SQL 查询场景:适用于对 SQL 语句有高度定制需求的应用,比如报表系统或有复杂查询逻辑的场景。
对 SQL 性能敏感的项目:需要开发者手动编写优化 SQL,且希望对 SQL 有绝对控制权的项目。
轻量级应用:适用于不需要复杂实体关系映射的项目,如简单的 CRUD 应用。
2、ORM是什么?
ORM(Object-Relational Mapping,对象关系映射)是一种用于将面向对象编程语言中的对象与关系型数据库中的表结构对应起来的技术。它通过自动将数据库表中的数据转换为编程语言中的对象,简化了与数据库的交互,使开发者不需要直接编写 SQL 语句。
3、#{}和${}的区别?
#{}: 是 MyBatis 中的占位符,它会将传入的参数作为预编译语句的参数处理,将其自动转换为 SQL 语句中的参数。适合传递查询条件、插入或更新的数据等,几乎所有的动态参数都可以用#{}来安全传递。
${}: 是一种拼接符,会直接将传入的参数值拼接到 SQL 语句中。适合动态构建 SQL 语句中的不可变部分,如表名、列名,但需要确保这些动态值是可信任的,避免引入 SQL 注入风险。
四、MySQL
1、什么是MySQL?
MySQL 是一个开源的关系型数据库管理系统(RDBMS),使用**结构化查询语言(SQL)**进行数据管理和查询。它最早由瑞典的 MySQL AB 公司开发,后来被 Oracle 公司收购。MySQL 是互联网应用开发中最常用的数据库之一,广泛应用于网站、移动应用和企业系统。
2、什么是数据库事务,四大事务特性?
数据库事务(Transaction)是一组逻辑操作单元,这些操作要么全部成功,要么全部失败。事务的典型应用场景是在执行多条操作时,确保数据库的完整性和一致性。例如,在银行转账的场景中,涉及到两个账户的余额变动,这两步操作要么同时成功,要么都不执行,确保数据一致。
原子性(Atomicity):
原子性保证事务中的所有操作要么全部成功,要么全部失败。如果事务在执行过程中发生错误,已经执行的操作会被回滚(撤销),数据库会恢复到事务开始之前的状态。
一致性(Consistency):
一致性确保事务执行前后,数据库的状态是一致的,不会破坏数据的完整性规则。事务必须将数据库从一种一致的状态转换到另一种一致的状态。
隔离性(Isolation):
隔离性指的是多个事务并发执行时,事务之间相互独立,互不干扰。一个事务的中间状态对其他事务是不可见的,必须等到该事务提交后,其他事务才能看到其对数据的更改。
持久性(Durability):
持久性确保事务一旦提交,数据库的变更将被永久保存,即使系统发生崩溃,也不会丢失数据。
3、数据库三大范式是什么?
第一范式:第一范式要求数据库表的每一列都是不可再分的原子数据。换句话说,表中的每个字段都应该是最小的单元,不能包含多个值。
第二范式:在满足 1NF 的前提下,第二范式要求表中的每个非主键字段都完全依赖于主键。换句话说,表中的非主键字段不能依赖于主键的一部分,而必须依赖整个主键。
第三范式:在满足 2NF 的前提下,第三范式要求非主键字段之间没有传递依赖关系。即,非主键字段不能依赖于其他非主键字段,而只能直接依赖主键。
4、什么是索引?
定义:数据库索引(Index)是一种用于加速数据查询的数据结构,它在数据库表中的一个或多个列上创建,类似于书本的目录,能够显著提高查询的效率。当我们通过索引查找数据时,数据库可以通过索引快速定位到对应的记录,而不必扫描整个表。
作用:索引的主要作用是提高数据库的查询速度,尤其是当表中的数据量较大时。没有索引的情况下,数据库必须进行全表扫描,逐行读取并匹配数据。而使用索引后,数据库可以通过索引树结构(如 B-树、哈希表)快速定位所需数据,从而大幅减少 I/O 操作。
索引的类型:
普通索引(Normal Index):最基本的索引类型,没有任何约束,适用于频繁查询的列。
唯一索引(Unique Index):保证索引列中的值是唯一的,适用于需要唯一标识的字段,例如邮箱、身份证号等。
主键索引(Primary Key Index):主键索引是一种特殊的唯一索引,要求列不能有空值,通常用于标识表中每条记录的唯一性。
全文索引(Full-text Index):用于快速查找文本数据,特别是在大文本字段中查找关键词时有效。
组合索引(Composite Index):在多个列上建立的索引,适用于多个列一起使用的查询条件。
优点:
提高查询速度:索引可以大幅提高 SELECT 查询语句的执行效率,尤其是针对 WHERE 条件的查询、JOIN 操作或 ORDER BY 排序。
加速表连接:索引有助于在多个表之间进行高效连接,减少查询时间。
缺点:
占用存储空间:每个索引都会占用额外的存储空间,特别是在大表上,过多的索引可能会增加存储负担。
影响写性能:虽然索引能提高查询效率,但在执行 INSERT、UPDATE 或 DELETE 操作时,索引需要同步更新,可能会影响写入性能。
维护开销:创建、更新和删除索引都有维护开销,尤其是当表中的数据频繁更新时,索引的维护代价较高。
5、B树和B+树的区别,为什么MySQL使用B+树?
B树:B树是一种自平衡的多路搜索树,可以保持数据有序,并允许在对数时间内插入、删除和查找操作。B树的节点不仅存储数据,还存储指向子节点的指针。
B+树:B+树是 B树的变体,与 B树类似,它也是一棵自平衡的多路搜索树,具有相同的插入、删除和查找时间复杂度。但在 B+树中,所有数据都存储在叶子节点上,内部节点仅用于存储索引值和指向子节点的指针。
B树和B+树的主要区别:
数据存储位置不同:
B树:数据存储在所有节点(即根节点、内部节点和叶子节点)上。
B+树:数据只存储在叶子节点上,内部节点只作为索引使用,不存储数据。
链表结构:
B树:叶子节点之间没有链接,遍历 B树时只能通过树的结构逐级访问。
B+树:叶子节点之间通过链表相互连接,方便进行范围查询。可以顺序遍历叶子节点,访问数据更加高效。
查询效率:
B树:由于数据存储在每个节点上,查找时可以直接在内部节点上找到数据,但每次查找都要逐层遍历。
B+树:由于所有数据都在叶子节点上,查找数据时必须走到叶子节点,但由于数据的密集存储和叶子节点的链表结构,范围查询更高效。
树的层数:
B树:相对来说,由于数据存储在每个节点上,可能需要更高的树层数。
B+树:由于数据仅存储在叶子节点,内部节点只存储索引,因此树的层数更低,访问数据时磁盘 I/O 更少。
MySQL 使用 B+树作为其索引结构,尤其是 InnoDB 存储引擎中默认使用 B+树来构建聚簇索引和二级索引。其原因主要包括以下几个方面:
磁盘 I/O 效率高:在数据库中,磁盘 I/O 操作是性能瓶颈。B+树的内部节点不存储数据,这意味着每个节点能存储更多的索引信息,导致树的高度更低,减少了磁盘 I/O 次数。因此,B+树在大规模数据下的查询性能更好。
顺序访问性能优越:由于 B+树的叶子节点通过链表链接,MySQL 可以非常高效地执行范围查询(例如 BETWEEN
操作)。可以顺序遍历所有叶子节点,减少了随机 I/O 操作。
更加稳定的查询性能:在 B+树中,所有的查询最终都到达叶子节点,无论是直接查找某个值还是范围查询,B+树的查找路径长度一致,这确保了稳定的查询性能。
适合数据库场景:数据库系统中,大多数操作是读取大量数据或按顺序读取记录,B+树的叶子节点链表结构非常适合这种场景。同时,B+树能够有效支持高并发的插入、删除和更新操作。
6、什么是隔离级别?
数据库隔离级别(Transaction Isolation Levels)是指多个事务并发执行时,数据库如何处理它们之间的相互影响。隔离级别的设置决定了一个事务在执行过程中能否看到其他事务的中间结果,从而保证数据的一致性。
读未提交(Read Uncommitted):在该隔离级别下,一个事务可以读取未提交的其他事务的数据。也就是说,事务之间没有隔离,可能会出现脏读。
读已提交(Read Committed):一个事务只能读取已经提交的其他事务的数据。这样可以避免脏读,但仍然可能发生不可重复读。
可重复读(Repeatable Read):在该隔离级别下,一个事务在读取数据时,整个事务期间内读取的数据是一致的,即使其他事务已经修改了数据,也不会影响到当前事务。这避免了不可重复读。
串行化(Serializable):这是隔离级别最高的一种,它强制所有事务串行执行,即一个事务完成后,另一个事务才能执行。这样可以避免所有并发问题(脏读、不可重复读和幻读)。
7、什么是视图,以及优势?
定义:视图是数据库中的一种虚拟表,它并不实际存储数据,而是基于现有的表(或多个表)的查询结果动态生成的。视图通过一个 SQL 查询定义,它的内容是查询的结果集。换句话说,视图是对查询的封装,类似于存储的查询。
视图的优势:
简化复杂查询:视图可以将复杂的多表连接或嵌套查询封装为一个简单的表格,使查询过程更加简洁,提高代码的可读性和维护性。
提高安全性:视图可以用来控制用户访问数据的粒度。通过视图,开发者可以限制用户只访问某些字段或某些行,而不是直接访问基础表的全部数据。
数据抽象:视图提供了数据的抽象层,允许数据库结构的更改而不影响用户。比如,如果底层表的结构发生变化,可以通过修改视图来适应,而无需修改用户的查询逻辑。
简化权限管理:视图可以用来为不同用户提供定制化的数据访问权限。数据库管理员可以根据需要授予用户访问视图的权限,而无需给他们底层表的访问权限。
适用场景:
复杂查询重用:当某些查询逻辑重复使用,且较为复杂时,可以通过视图简化和重用。
数据隔离:对敏感数据进行保护,确保用户只能访问视图中的部分信息。
层次化数据展示:例如,将多表连接的结果以简单表的形式呈现,方便上层应用直接访问。
8、数据库索引失效?
使用函数或表达式:如果在查询条件中对索引列使用了函数或运算(如 WHERE UPPER(name)
),索引将不会被利用。
隐式类型转换:当列的数据类型与查询中的参数类型不匹配时,数据库可能会进行隐式类型转换,导致索引失效。例如,字符串和数字之间的比较。
LIKE 模式不当:LIKE
操作符在以 %
开头的情况下(如 LIKE '%abc'
),无法使用索引进行查询。
不等运算符:使用 <>
、NOT IN
、NOT EXISTS
等不等式条件时,通常无法利用索引。
范围查询后跟其他条件:当使用范围查询(如 BETWEEN
、>
, <
)后,后续的条件不会使用复合索引。
OR 条件不当:在没有对 OR
条件两边的列都建立索引的情况下,索引可能不会被使用。
9、数据库优化?
数据库设计
范式设计与反范式设计:合理设计表结构,应用数据库范式来减少数据冗余,提高数据一致性。同时,在性能需求较高的场景中,可以适度反范式化(如使用冗余字段或分表)来减少关联查询的开销。
数据类型选择:为字段选择合适的数据类型,确保字段占用最小的存储空间。例如,使用 INT
而不是 BIGINT
,或者使用 VARCHAR
而不是 TEXT
。
规范化命名与主键设计:采用规范的表和字段命名,使用自增主键或 UUID 作为主键,确保数据访问的高效性。
索引优化
合理创建索引:根据查询条件为常用的字段(尤其是 WHERE、ORDER BY、GROUP BY 中涉及的字段)创建索引。常用的索引类型包括单列索引、复合索引和唯一索引。
避免冗余索引和过多索引:过多或重复的索引会占用大量存储,并影响写操作的性能,因此应谨慎创建和维护索引。
避免索引失效:确保查询语句不使用函数、表达式或隐式类型转换,否则会导致索引失效,数据库不得不进行全表扫描。
分析索引使用情况:通过工具(如 MySQL 的 EXPLAIN
命令)分析查询的执行计划,判断是否有效利用了索引。
查询优化
优化 SQL 语句:避免使用 SELECT *
,只选择需要的字段,减少数据传输量;避免在 WHERE 子句中使用 OR
,如果可能,将其拆分为多条查询或使用 UNION。
避免嵌套子查询:将嵌套的子查询转化为 JOIN,减少数据库扫描次数,提高查询效率。
利用分页查询:对于大数据量的分页查询,可以利用索引和 ID 范围限制来优化分页,如通过 WHERE id > last_id LIMIT 10
代替 OFFSET
。
缓存查询结果:对于频繁执行的复杂查询,可以利用数据库内置的缓存机制(如 Query Cache)或外部缓存(如 Redis)存储查询结果,减少数据库压力。
硬件和配置优化
升级硬件:提升数据库服务器的硬件配置,包括 CPU 性能、内存大小、磁盘速度(如 SSD),以提高数据库的读写性能。
优化数据库配置:根据业务需求优化数据库的参数配置,如调整 innodb_buffer_pool_size
、max_connections
和 query_cache_size
,确保数据库性能最大化。
调整存储引擎:选择合适的存储引擎(如 MySQL 的 InnoDB 或 MyISAM),根据具体的应用场景优化数据读写性能。
缓存策略和分片
使用缓存:在数据库前加入缓存层(如 Redis、Memcached)来缓存频繁访问的数据,减少数据库的读负载,提高响应速度。
分库分表:对于大数据量或高并发的应用,可以进行水平分片(将数据按某种规则拆分到不同的库或表中),或者垂直分片(按业务模块拆分表),以减轻单一数据库的压力。
读写分离:通过配置主从数据库复制,主库负责写操作,从库负责读操作,借助负载均衡器分配请求,进一步提高读写性能。
10、缓存穿透、缓存击穿、缓存雪崩分别是什么?
缓存穿透:当请求查询的数据在缓存和数据库中都不存在时,每次查询都会直接落到数据库,绕过缓存,导致数据库压力过大。为避免这种情况,可以使用布隆过滤器来拦截非法请求,或将查询结果为空的数据也缓存起来(例如设置一个短期的空值缓存)。
缓存击穿:当某个热点数据的缓存正好失效,在这失效的瞬间大量并发请求涌入查询该数据,因为缓存未命中,所有请求都会直接落到数据库,可能导致数据库瞬间压力过大。解决方法包括对热点数据加锁,让只有一个请求可以加载数据并更新缓存,或者在缓存即将到期时主动提前刷新。
缓存雪崩:当缓存集中在某个时间段大规模失效或出现系统故障,导致所有请求绕过缓存直接访问数据库,容易造成数据库服务器崩溃。解决方案包括为不同缓存设置随机的过期时间,避免缓存同时失效,或者采用多级缓存策略(如本地缓存+分布式缓存)来减轻数据库压力。
五、Redis
1、什么是Redis?
Redis(Remote Dictionary Server)是一个开源的内存数据结构存储,用作数据库、缓存和消息中间件。它以键值对的形式存储数据,所有数据都存放在内存中,具有极高的读写性能。Redis 支持多种数据结构,并且提供持久化选项,可以将数据写入磁盘以确保数据不会因为内存丢失。
2、Redis有哪些数据类型?
Redis 提供了多种数据类型,能够适应不同的应用场景。以下是 Redis 支持的主要数据类型及其特点和应用场景:
字符串(String):字符串是 Redis 最基本的类型,每个键都对应一个字符串值,字符串可以是文本或二进制数据(如图片)。常用于缓存简单的键值对,例如存储用户信息、页面数据、计数器等。
列表(List):列表是一个有序的字符串集合,按插入顺序排序。列表允许从两端(左或右)进行操作。适合消息队列、任务队列、时间排序的数据存储等场景。
集合(Set):集合是无序的字符串集合,集合中的元素是唯一的。它支持一些常见的集合操作,如交集、并集和差集。适合用户标签管理、共同好友、去重操作等场景。
有序集合(Sorted Set,ZSet):有序集合类似于集合,但每个元素关联一个分数,根据分数进行排序。元素仍然是唯一的,但分数可以重复。非常适合排行榜、积分系统、优先级队列等场景。
哈希表(Hash):哈希表是一个键值对集合,类似于 Python 的字典或 Java 中的 HashMap。每个哈希表可以存储多个字段,每个字段都有一个对应的值。适合存储对象类型的数据,比如用户信息(用户名、年龄、邮箱等)。
3、Redis的应用场景?
“Redis 常用于缓存、分布式锁、会话管理、消息队列、排行榜和实时统计系统等多种场景。它的高性能、多数据结构支持以及主从复制、持久化等特性使其成为构建高效、实时系统的理想选择。”
4、Redis的持久化?
Redis 是一个内存数据库,但为了避免数据因意外宕机而丢失,提供了两种持久化机制:RDB(Redis Database Backup) 和 AOF(Append Only File)。这些机制用于将数据保存在磁盘中,确保数据可以在重启后恢复。
RDB(Redis Database Backup)
工作原理:RDB 是 Redis 在指定时间间隔内将内存中的数据快照保存到磁盘的机制。生成的 RDB 文件是一个紧凑的二进制文件,包含了某一时刻的全部数据。
优势:
恢复速度快:RDB 文件是一个紧凑的二进制文件,Redis 在启动时可以快速加载该文件,恢复内存中的数据。
减少性能影响:RDB 以指定的间隔(比如每隔 5 分钟)进行快照,因此对 Redis 的性能影响较小,适合高性能、高吞吐量的场景。
适用于备份:RDB 文件非常适合用于数据的备份,可以方便地将某一时刻的数据复制到其他环境中。
劣势:
数据可能丢失:因为 RDB 是在固定的时间间隔进行快照,如果 Redis 在两次快照之间崩溃,所有未保存的数据将丢失
生成 RDB 文件时可能耗时:在数据量非常大的情况下,生成 RDB 文件会占用较多的 I/O 资源。
适用场景:RDB 适用于对数据一致性要求不高、需要高性能和快速恢复的场景,比如缓存、离线数据分析等。
AOF(Append Only File)
工作原理:AOF 记录每一个对 Redis 的写操作,将其以日志的形式追加到文件中(append only
模式)。Redis 可以根据 AOF 文件中的日志指令来重放所有操作,从而恢复数据。
优势:
数据更持久:AOF 提供了三种同步策略:always
(每次写入都同步到磁盘)、everysec
(每秒同步一次)、no
(由操作系统决定何时同步)。通常,everysec
能够在性能和持久性之间取得平衡,最多只会丢失 1 秒的数据。
可读性:AOF 文件是一个文本文件,记录了所有的 Redis 操作指令,因此具有可读性,方便对数据进行分析或手动修改。
追加写入:AOF 通过追加写入日志文件,性能较好,且文件损坏的几率较低。
劣势:
文件体积较大:AOF 文件比 RDB 文件更大,恢复数据时需要逐条重放命令,速度较慢。
性能影响:由于每次写操作都会记录到 AOF 文件中,尤其是设置 always
策略时,会对 Redis 性能产生一定影响。
适用场景:AOF 适用于对数据持久化要求较高、希望最小化数据丢失的场景,如需要实时保存用户会话、订单信息等。
5、Redis和MySQL如何保证数据一致?
先更新Mysql,再更新Redis,如果更新Redis失败,可能仍然不一致
先删除Redis缓存数据,再更新Mysql,再次查询的时候在将数据添加到缓存中,这种方案能解决1方案的问题,但是在高并发下性能较低,而且仍然会出现数据不一致的问题,比如线程1删除了Redis缓存数据,正在更新Mysql,此时另外一个查询再查询,那么就会把Mysql中老数据又查到Redis中
延时双删,步骤是:先删除Redis缓存数据,再更新Mysql,延迟几百毫秒再删除Redis缓存数据,这样就算在更新Mysql时,有其他线程读了Mysql,把老数据读到了Redis中,那么也会被删除掉,从而把数据保持一致
六、SpringBoot
1、什么是 Spring Boot?
Spring Boot 是 Spring 框架的一个快速开发框架,用于简化基于 Spring 应用的创建和配置。它通过提供一套默认配置和自动化的依赖管理,帮助开发者快速构建独立的、生产级别的 Spring 应用程序,而无需编写大量的配置文件。
Spring Boot 的目标是让开发者更快地启动项目,并减少手动配置的工作量,使 Spring 应用更加简单和高效。
2、Spring Boot 有哪些优点?
自动配置(Auto-Configuration):Spring Boot 的自动配置功能能够根据项目中的依赖关系自动配置 Spring 组件,开发者无需手动配置复杂的 XML 或 Java 配置。这大大减少了设置项目的时间,使应用程序能够快速启动并运行。
嵌入式服务器:Spring Boot 内置了 Tomcat、Jetty 和 Undertow 等嵌入式 Web 服务器,这意味着应用程序可以直接打包为一个可执行的 JAR 文件,独立运行,无需单独配置外部服务器。这使得应用的部署更加简单,尤其是在云环境或容器化场景中非常有用。
简化的依赖管理:Spring Boot 提供了starter 模块,这些模块预定义了常用功能的依赖集,例如 spring-boot-starter-web
,可以快速配置 Web 应用。开发者不需要手动选择和配置各个 Spring 组件,避免了依赖冲突问题。
生产级的监控和管理:Spring Boot 提供了 Actuator 模块,允许开发者通过 HTTP、JMX 等方式监控和管理应用程序的运行状态。Actuator 提供了一些默认的端点,可以查看应用的健康状况、度量数据、日志信息、数据库连接池状态等,方便了生产环境的监控和调试。
快速开发:Spring Boot 的“开箱即用”特性非常适合快速原型开发和项目启动。开发者可以快速搭建项目,进行功能验证和快速迭代,极大地提高了开发效率。
微服务架构支持:Spring Boot 与 Spring Cloud 无缝集成,为构建微服务架构提供了强大的支持。Spring Boot 简化了微服务的创建、配置和部署过程,结合 Spring Cloud 可以实现分布式系统中的服务注册、配置管理、负载均衡等功能,是构建微服务的主流技术选型之一。
易于测试:Spring Boot 提供了强大的测试支持,可以方便地对应用的各个层次进行单元测试、集成测试等。通过 @SpringBootTest
注解,开发者可以轻松加载应用程序的上下文环境,测试 Web 层、数据层等各个组件。
3、Spring Boot的核心注解
@SpringBootApplication:这是 Spring Boot 应用的核心注解,通常标记在主应用类上。它是一个复合注解,结合了 @Configuration
、@EnableAutoConfiguration
和 @ComponentScan
三个注解。
@EnableAutoConfiguration:启用 Spring Boot 的自动配置机制。Spring Boot 会根据项目中的类路径(classpath)中的依赖和配置,自动配置应用程序的各种组件。这一注解常与 @SpringBootApplication
一起使用,不需要单独显式声明。
@RestController:这是 Spring MVC 中常用的注解,表示该类是一个 RESTful Web 服务的控制器。它是 @Controller
和 @ResponseBody
的组合注解。
@RequestMapping:用于映射 HTTP 请求到控制器的方法或类上。可以用来定义访问 URL、请求方法(GET、POST 等)、请求参数等。
@Configuration:表明该类是一个配置类,用于替代传统的 XML 配置文件。@Configuration
类通常包含 @Bean
注解方法,用于声明和配置 Bean。
@Bean:用于定义和注册一个 Bean 到 Spring 容器中,通常与 @Configuration
注解一起使用。相当于传统 XML 中的 <bean>
元素。
@Service:标记在业务逻辑类上,表明该类是一个服务组件,属于 Spring 组件的一部分。它的作用与 @Component
类似,但语义更明确。
@Repository:用于标记数据访问层(DAO)组件。它的功能与 @Component
相同,但它被特别用于持久层,与数据库操作相关的类使用。
@Component:是一个通用的 Spring 组件注解,表示一个类可以被 Spring IoC 容器扫描并实例化为一个 Bean。
@Conditional:用于有条件地加载配置类或 Bean。Spring Boot 提供了多种条件注解,例如 @ConditionalOnProperty
、@ConditionalOnMissingBean
、@ConditionalOnClass
,这些注解可以根据环境、依赖等条件决定是否加载某个 Bean。
@EnableScheduling:用于启用 Spring 的任务调度功能,允许开发者使用 @Scheduled
注解来定义定时任务。
@EnableAsync:启用 Spring 的异步方法执行功能,允许使用 @Async
注解异步调用方法。
4、简述一下,如何启动一个Spring Boot应用?
启动一个 Spring Boot 应用非常简单,只需:
创建一个带 @SpringBootApplication
注解的主类。
配置项目依赖。
编写必要的业务代码(如控制器)。
通过 IDE 或命令行启动应用。
5、Spring与Spring Boot区别?
设计理念:
Spring:Spring 是一个完整的 Java 应用开发框架,提供了大量灵活的功能模块(如依赖注入、AOP、数据访问等),但需要大量的手动配置,尤其是 XML 配置。
Spring Boot:Spring Boot 是基于 Spring 的一个快速开发框架,旨在简化 Spring 应用的配置和开发。它提供开箱即用的功能,通过自动配置、简化依赖管理,使开发者可以快速启动项目。
配置方式:
Spring:Spring 的配置通常依赖于 XML 或 Java 类配置,开发者需要显式配置所有的 Bean 和依赖,项目初期设置工作量较大。
Spring Boot:Spring Boot 通过自动配置(@EnableAutoConfiguration
)和Spring Boot Starter 来减少配置,开发者只需很少的配置文件,就可以启动应用程序,几乎不需要手动定义大量的 Bean。
内置服务器:
Spring:传统的 Spring 应用需要在外部服务器(如 Tomcat、Jetty)上部署,无法直接运行。
Spring Boot:Spring Boot 内置了 Tomcat、Jetty 等嵌入式服务器,允许将应用打包成一个可执行的 JAR 文件,可以直接运行而无需额外配置服务器。
依赖管理:
Spring:开发者需要手动管理所有的库依赖,并处理可能的版本冲突,依赖管理较为复杂。
Spring Boot:Spring Boot 提供了一套Starter 模块,简化了依赖管理,自动选择合适的库版本,开发者无需手动管理复杂的依赖。
适用场景:
Spring:适合需要高度自定义的大型应用,灵活性强,但初期设置繁琐。
Spring Boot:适合需要快速启动的微服务或小型应用,特别强调开发效率和简便性。
6、简要介绍下GET和POST请求区别?
目的
GET:用于从服务器请求数据,通常用于获取资源或数据,不会对服务器上的数据产生修改。
POST:用于向服务器发送数据,通常用于提交表单、上传文件或执行数据库操作,会对服务器上的数据产生修改。
参数传递方式
GET:请求参数附加在 URL 的查询字符串中,形式为 http://example.com?name=value
,因此参数在 URL 中可见,有长度限制(通常约为 2048 字符)。
POST:请求参数包含在请求的请求体中,参数不会显示在 URL 中,适合传输大数据量或敏感数据,如密码。
安全性
GET:因为参数暴露在 URL 中,敏感信息(如密码)不应通过 GET 传输,安全性较低。
POST:参数在请求体中传输,虽然看不见,但并不是完全安全。POST 更适合传输敏感信息,尤其是配合 HTTPS 使用。
幂等性
GET:是幂等的,多次请求不会改变服务器上的数据,执行多次结果相同。
POST:非幂等,每次请求都可能修改服务器上的数据,执行多次可能产生不同结果。
缓存
GET:浏览器可以缓存 GET 请求的结果,因此适合用于获取静态资源或数据。
POST:POST 请求一般不会被浏览器缓存,适合用于处理动态数据。
7、Cookie和Session区别?
存储位置
Cookie:存储在客户端(浏览器)中,随着每次 HTTP 请求一起发送到服务器,存在安全风险。
Session:存储在服务器端,通过服务器进行管理,客户端只保存一个 SessionID
作为标识,安全性较高。
存储容量和数据类型
Cookie:容量有限(通常最大 4KB),只能存储字符串,且在客户端浏览器中可被查看或篡改。
Session:服务器端存储,容量理论上不受限制,可以存储任何类型的数据(如对象、列表等)。
生命周期
Cookie:可以通过设置有效期(Expires
)实现持久化,或者为临时会话 Cookie,会在浏览器关闭后删除。
Session:一般是基于会话的,默认在用户关闭浏览器或 session 超时后失效;也可以手动设置超时时间。
安全性
Cookie:容易被客户端修改或截获(如果不加密),因此在传输敏感信息时需要配合 HTTPS 使用。
Session:由于数据存储在服务器端,相对更安全,客户端无法直接访问数据,只通过 SessionID
进行操作。
七、开发过程中遇到的问题?
1. 数据响应慢,接口延迟高
问题:有时我们会遇到接口的响应时间过长,导致用户体验差。经过排查,发现是数据库查询效率低,特别是在没有优化索引的情况下,导致某些复杂查询耗时很长。
解决方法:通过分析 SQL 查询,添加必要的索引,并优化复杂的查询逻辑。同时引入**缓存(Redis)**来存储一些频繁访问的数据,减少数据库的压力,从而提升响应速度。
2. 高并发下服务崩溃
问题:在高并发请求场景下,服务器出现了崩溃,原因是服务器资源被大量请求耗尽,未能合理处理并发流量。
解决方法:为了解决这个问题,我引入了限流和熔断机制,限制单一用户或 IP 的请求频率。还使用了负载均衡,将请求分配到不同的服务器上,避免单个服务器压力过大。此外,还通过异步处理和消息队列来应对突发的大量请求,提升系统的稳定性。
3. 分布式系统中的数据不一致
问题:在分布式系统中,不同节点间的数据同步偶尔会发生延迟,导致用户在不同节点上看到的数据不一致。
解决方法:我使用了最终一致性的策略,通过消息队列进行异步处理,确保每个节点的数据最终会一致。同时,还为某些关键业务引入了分布式锁,确保在并发写操作时不会出现数据冲突。
4. 接口没有返回预期的数据
问题:在某些情况下,API 请求没有返回预期的数据,经过分析后发现是由于代码中出现了逻辑错误或边界情况未被处理,导致返回数据不正确。
解决方法:首先,通过完善的单元测试和接口测试来捕捉这些异常情况,并在开发过程中加入更多的异常处理机制,确保即使出现边界情况,系统也能优雅地处理。其次,建立详细的日志记录,可以快速定位问题所在。
5. 数据库连接池耗尽
问题:在一次项目中,数据库连接池配置不合理,导致请求高峰期连接池中的连接耗尽,新的请求无法创建连接,系统响应出现卡顿。
解决方法:调整了数据库连接池的大小,并且优化了数据库连接的使用,减少了连接的长时间占用。同时引入了连接池监控,可以提前发现连接耗尽的潜在风险,并根据流量情况动态调整连接池的配置。