spring面试总结
spring 篇
spring IOC (控制反转)
📖 定义:
IoC(Inversion of Control,控制反转)是一种 设计思想,指的是对象的创建与依赖关系的管理不再由程序员控制,而是交由容器(如 Spring)来管理。
- 🔧 二、传统写法 vs IoC 思想
👎 传统写法(耦合度高):👍 使用 IoC:1
2
3
4
5
6
7
8
9
10
11
12
13class UserDao {
public void save() {
System.out.println("Saving user");
}
}
class UserService {
private UserDao userDao = new UserDao(); // 手动创建依赖
public void doBusiness() {
userDao.save();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16@Component
class UserDao {
public void save() {
System.out.println("Saving user");
}
}
@Service
class UserService {
@Autowired
private UserDao userDao; // 自动注入
public void doBusiness() {
userDao.save();
}
}
⚙️ 五、Spring IoC 容器的底层流程(重要)
1 | |
🔄 更准确的说法:
IoC 容器启动时,会:
- 扫描或加载所有配置的类(如 @Component、XML
)。 - 将这些类信息 注册为 BeanDefinition。
- 当程序需要某个对象(比如 UserService)时,Spring 会:
- 检查它的依赖(比如 UserRepository)。
- 自动创建依赖(通过反射)并注入(DI)进来。
- 这整个流程,是 IoC 的体现:我们不再手动 new 依赖,而是让容器帮我们创建并注入。
spring事务的传播机制
Spring 提供了 7 种传播行为,定义在 org.springframework.transaction.annotation.Propagation 枚举中。
| 传播行为 | 描述 |
|---|---|
REQUIRED(默认) | 有事务则加入,没有则新建事务。常用。 |
REQUIRES_NEW | 挂起当前事务,新建事务,互不影响。 |
NESTED | 嵌套事务,内层异常只回滚自身,不影响外层。 |
SUPPORTS | 有事务就加入,没事务就以非事务方式运行。 |
NOT_SUPPORTED | 不支持事务,有事务则挂起,用非事务方式运行。 |
NEVER | 不允许有事务,否则抛异常。 |
MANDATORY | 必须在事务中运行,没有事务就抛异常。 |
1️⃣ REQUIRED(默认)
A 有事务,B 就加入;
A 无事务,B 就新建事务。
1 | |
事务失效的场景
- 1️⃣ 方法不是 public 的原因:Spring AOP 只能拦截 public 方法。非 public 方法不会被代理,自然事务不生效。
1
2@Transactional
private void doSomething() { ... } // ❌ 不生效 - 2️⃣ 方法被内部调用(在同一个java类中)原因:Spring AOP 是基于代理对象实现的,this.methodB() 这种内部调用不会走代理,事务不会生效。
1
2
3
4
5
6
7@Transactional
public void methodA() {
methodB(); // 内部方法调用,事务不生效
}
@Transactional
public void methodB() { ... }- ✅ 正确做法:
1
2
3
4
5
6@Autowired
private Service self; // 注入自身代理对象
public void methodA() {
self.methodB(); // 用代理调用才能生效
}
- ✅ 正确做法:
- 3️⃣ 异常被捕获了原因:Spring 默认只在 运行时异常(RuntimeException)或 Error 时才回滚,且异常要能抛出,不能被吞掉。
1
2
3
4
5
6
7
8@Transactional
public void method() {
try {
// 可能抛异常的代码
} catch (Exception e) {
// 异常被吃掉
}
}
✅ 正确做法:
- 异常不要随便吃掉,或者手动 throw
- 或者加 @Transactional(rollbackFor = Exception.class) 明确声明回滚类型
- 4️⃣ 事务方法没有被 Spring 管理(比如 new 出来的类)原因:Spring 只能代理容器内的 Bean,new 出来的对象是普通对象,事务无效。
1
2UserService userService = new UserService(); // ❌ 非 Spring Bean
userService.saveUser(); // 事务不会生效 - 5️⃣多线程导致事务失效原因:Spring 的事务是线程绑定的,开启事务的线程和新线程不是同一个,事务无效。
1
2
3
4
5
6
7@Transactional
public void saveData() {
new Thread(() -> {
// 数据库操作
}).start();
}
bean的生命周期
实例化(new)
↓
属性注入(依赖注入)
↓
BeanPostProcessor(初始化前置处理)
↓
初始化方法(如 @PostConstruct / init-method)
↓
BeanPostProcessor(初始化后置处理)
↓
Bean 开始使用
↓
销毁前处理(如 @PreDestroy / destroy-method)
↓
Bean 被销毁(容器关闭)
springbean的但历史怎么实现的
Spring 容器通过 一个 Map 来缓存单例 Bean 实例,在创建 Bean 时先查缓存,如果已存在就直接返回,不再重新创建。这样有点像CAS一样,保证了bean的单例。
Spring 单例 Bean 的生命周期流程:
Spring 容器启动 → 解析配置 → 实例化 Bean → 放入单例池(Map) → 后续使用都从池中取。
mybatis
mybatis的二级缓存
MyBatis 提供了两级缓存:
| 缓存级别 | 缓存范围 | 生命周期 |
|---|---|---|
| 一级缓存 | SqlSession 级别 | 当前会话内有效 |
| 二级缓存 | Mapper 级别 | 跨 SqlSession 有效(多个会话共享) |
流程:
1 | |
- 开启 MyBatis 二级缓存
1
2
3<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
rpc篇
八股文
什么是grpc
- 定义:
gRPC 是一种高性能、开源、跨语言的远程过程调用(RPC)框架,由 Google 开发,基于 HTTP/2 和 Protobuf 协议。
| 特点 | 说明 |
|---|---|
| 📦 基于 Protobuf 序列化 | 使用 Google 的 Protocol Buffers(比 JSON/XML 更小更快) |
| 🛣️ 使用 HTTP/2 协议 | 支持多路复用、流控制、头压缩,性能远超 HTTP/1.1 |
| 🔁 支持 4 种调用方式 | 普通调用、服务器流、客户端流、双向流 |
| 🧬 跨语言支持 | 支持 Java、Go、Python、C++、Node.js 等十几种语言 |
| 🧱 接口强类型 | 用 .proto 文件定义服务和消息,代码自动生成,接口规范统一 |
| 🔐 支持认证和加密 | 支持 TLS/SSL,支持 token、OAuth2 等安全机制 |
| 🔔 支持流式通信 | 天然支持 Server Push、双向长连接、实时消息等场景 |
| 🚀 高性能、低延迟 | Protobuf + HTTP/2 = 高吞吐 + 低延迟 |
| 📦 支持负载均衡、服务发现(可配合 etcd) | 在大型微服务架构中易于扩展 |
如何使用grpc
- 大致过程:
- 通过 gRPC 代理,把方法和参数序列化为 Protobuf 二进制;
- 用 HTTP/2 发送给远程服务端;
- 服务端解析请求,执行对应方法;
- 返回结果(也是 Protobuf 格式);
- 客户端反序列化,获得最终结果。
- 🔧 示例:gRPC 使用流程(Java / Go 都类似)
- 1️⃣ 定义 proto 文件(接口说明)
1
2
3
4
5
6
7
8
9
10
11
12
13
14syntax = "proto3";
service UserService {
rpc GetUserById (UserRequest) returns (UserResponse);
}
message UserRequest {
int32 id = 1;
}
message UserResponse {
string name = 1;
int32 age = 2;
} - 2️⃣ 使用 protoc 工具生成代码(Stub)
1
protoc --java_out=. user.proto - 3️⃣ 实现服务端逻辑
1
2
3
4
5
6
7
8
9public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
@Override
public void getUserById(UserRequest request, StreamObserver<UserResponse> responseObserver) {
UserResponse resp = UserResponse.newBuilder()
.setName("Tom").setAge(22).build();
responseObserver.onNext(resp);
responseObserver.onCompleted();
}
} - 4️⃣ 客户端调用服务
1
2
3ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051).usePlaintext().build();
UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc.newBlockingStub(channel);
UserResponse resp = stub.getUserById(UserRequest.newBuilder().setId(1).build());
- 1️⃣ 定义 proto 文件(接口说明)
etcd
八股文
什么是etcd
etcd 是一个开源的、强一致性、高可用的分布式键值数据库,广泛用于服务注册、配置中心、选主、分布式协调等场景。
| 等级 | etcd 用于 | Redis 用于 |
|---|---|---|
| 📦 轻量配置 / 注册中心 | ❌ Redis 不推荐做配置中心 | |
| 💽 高频缓存 / 业务数据 | ✅ Redis 优于 etcd | |
| ⚠️ 一致性要求高的协调场景 | ✅ etcd 更安全稳定 |
设计模式
八股文
单例模式
ChatGPT 说:
单例模式(Singleton Pattern)是一种创建型设计模式,它的主要目的是确保一个类只有一个实例,并且提供一个全局访问点来获得该实例。
1 | |
其它篇
什么是幂等性,接口幂等性
✅ 一句话理解幂等性:
一次和多次执行的效果是一样的,不论你执行多少次,结果都是一样的。
| 请求类型 | 示例 URL | 幂等性说明 |
|---|---|---|
GET | /user/1001 | 每次获取数据,不改变状态 → ✅ 幂等 |
DELETE | /user/1001 | 多次删除同一个用户,结果一样 → ✅ 幂等 |
PUT | /user/1001 设置 age=20 | 多次设置同一个值,结果一样 → ✅ 幂等 |
POST | /order 新建订单 | 每次都创建一个新订单 → ❌ 不幂等 |
如果 接口不幂等,就会导致:
- 重复扣款
- 重复创建订单
- 数据不一致
所以设计接口时,最好让它幂等,或者用幂等机制来保护。
JWT 如何使用续约toekn避免用户登录过期的
🔁 JWT 续约机制:
| 类型 | 用途 | 有效期 | 存储方式 |
|---|---|---|---|
accessToken | 访问业务接口用,权限控制 | 短(如15分钟) | Authorization 头 / localStorage |
refreshToken | 用来刷新 accessToken | 长(如7天~30天) | HttpOnly Cookie(推荐)或 localStorage |
- 🧠 为什么需要 refreshToken?
- 如果只用一个长期有效的 accessToken,一旦泄露无法阻止使用;
- 如果 accessToken 很短,又不能刷新,用户就要频繁登录,体验差;
- 因此,需要一个长期有效的 refreshToken,专门用于续签新的 accessToken。
流程图
1
2
3
4
5
6
7
8
9
10
11
12
13登录:
Client ----> Server
<---- 返回 accessToken + refreshToken
访问接口:
Client 携带 accessToken ----> Server 验证通过
accessToken 过期:
Client 携带 refreshToken ----> /auth/refresh
<---- 返回新的 accessToken
refreshToken 也过期:
Client 重新登录当返回token过期时,需要前端去捕捉,捕捉成功后让前端主动调刷新accessToken的接口,再调用一次失败的接口
JWT 流程示意
| 部分 | 说明 | 内容示例 |
|---|---|---|
| Header | 头部,描述令牌类型和签名算法 | {"alg":"HS256","typ":"JWT"} |
| Payload | 载荷,存放声明(claims) | {"sub":"1234567890","name":"Alice","iat":1516239022} |
| Signature | 签名,对前两部分进行签名校验 | HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret) |
- 将 Header 和 Payload 转为 JSON 后,进行 Base64Url 编码;
- 用密钥和算法对编码后的头部和载荷拼接字符串签名,生成 Signature;
- 拼接成 header.payload.signature 格式的 JWT 字符串。
- 面试官: ThreadLocal的内存泄漏的原因
- 我:
- ThreadLocalMap 存储结构
ThreadLocalMap 存储结构
每个线程内部(Thread 类)有一个 ThreadLocalMap,它以 弱引用(WeakReference) 来引用 ThreadLocal 对象(key),而 value 是强引用。1
2
3
4
5
6
7Thread -> ThreadLocalMap
key (弱引用): ThreadLocal对象
value (强引用): 线程绑定的变量值
// 例如
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("Hello, ThreadLocal!"); // 绑定变量值
- key:就是 threadLocal 这个对象(弱引用)
- value:就是 “Hello, ThreadLocal!” 这个字符串
内存泄漏的关键点:
- 当 ThreadLocal 对象被垃圾回收(key 为弱引用,没被外部引用了),
- 但是 ThreadLocalMap 中对应的 value 是强引用,不会被自动回收,
- 导致这个 value 对象一直存在,造成内存泄漏。
必须在使用完 ThreadLocal 后调用 remove()
- ThreadLocalMap 存储结构





