面试模拟-202506

面试模拟-202506
mengnankkzhou6.11模拟面试
1.什么是 Java 的面向对象编程?它的核心特性有哪些?
面向对象编程是OOP是说就是用“对象”来组织代码,java就是一门面向对象编程的语言。
java的面向对象的三大特征是封装继承多态
封装:反射,修饰符
继承:构造方法调用,final
多态:方法重载,方法重写
抽象:抽象和接口
静态:
2.说说 Java 中 ArrayList 和 LinkedList 的区别,以及各自的适用场景
底层:
遍历和插入:
arrayList扩容:1.5,数组的复制
内存占比:
使用场景:
线程安全:
3.HashMap 的底层原理
特点:
底层实现,1.7/1.7之后,头插和尾插
put过程
转换红黑树/扩容:当链表长度≥8 且数组容量≥64 时转红黑树 为什么用红黑树
线程安全:
concurrenthashmap,底层,put过程
4.Spring 框架中,Bean 的作用域有哪些?请分别说明它们的生命周期和应用场景
5 大标准作用域:
- Singleton:全局唯一实例,Spring 容器加载时创建,适用于无状态服务;
- Prototype:每次获取新实例,适用于有状态对象(如 DAO 层);
- Request:每次 HTTP 请求一个实例,需 Web 环境,适用于请求上下文数据;
- Session:每个用户会话一个实例,需 Web 环境,适用于会话级数据;
- Application:全局 ServletContext 共享实例,需 Web 环境。
生命周期:实例化→属性注入→@PostConstruct→初始化完成→使用→@PreDestroy→销毁。
循环依赖:setter单例注入,三级缓存,
- 一级缓存(singletonObjects):存储完全初始化的单例 Bean;
- 二级缓存(earlySingletonObjects):存储早期曝光的 Bean(解决 A→B→A 的循环依赖);
- 三级缓存(singletonFactories):存储 Bean 的工厂,用于生成代理对象(解决 A→B(代理)→A 的循环依赖)。
5.AOP 的实现原理
概念:适用场景
作用点:
- 切面(Aspect):封装横切逻辑的类,用
@Aspect注解标识; - 切点(Pointcut):定义横切逻辑作用的目标方法(如
@Pointcut("execution(* com.service.*.*(..))")); - 通知(Advice):横切逻辑的具体实现,包括 5 种类型
- 连接点(Joinpoint):程序执行中的具体点(如方法调用、字段修改),AOP 中主要指方法调用
动态代理:
- JDK 动态代理:基于接口实现,生成
InvocationHandler的代理对象,适用于目标类有接口的情况; - CGLIB 代理:基于子类继承,生成目标类的子类代理对象,适用于无接口的类(需引入
cglib依赖); - Spring 默认策略:有接口用 JDK 代理,无接口用 CGLIB 代理(可通过
proxy-target-class属性强制使用 CGLIB)。 - 流程:
- Spring 容器扫描到
@Aspect注解的切面类,解析切点表达式和通知类型; - 对目标类判断是否适用 JDK/CGLIB 代理,生成代理对象;
- 当调用代理对象的方法时,实际执行的是通知逻辑 + 目标方法的组合逻辑。
细节:
开启 AOP 支持:在配置类中添加
@EnableAspectJAutoProxy注解,或在 XML 中配置<aop:aspectj-autoproxy/>;代理模式选择
:
- 默认
proxyTargetClass=false(优先 JDK 代理); - 若需强制 CGLIB 代理,设置
proxyTargetClass=true或@EnableAspectJAutoProxy(proxyTargetClass = true);
- 默认
循环依赖与 AOP:若目标类被代理,注入的是代理对象,需注意
this.方法()调用不会触发 AOP(因this指向原始对象)。
场景:
日志记录:在方法执行前后记录入参 / 结果,避免业务代码污染;
事务管理:
@Transactional本质是 AOP 实现,在方法调用时开启 / 提交 / 回滚事务;- 权限校验:在接口调用前校验用户权限,拒绝非法请求;
- 性能监控:统计方法执行耗时,用于性能优化。
流程:
- 调用代理方法:客户端调用代理对象的方法(如
userService.save()); - 匹配切点:Spring 判断该方法是否匹配切面的切点表达式;
- 执行通知逻辑
@Around中先执行前置逻辑(如日志记录开始);- 调用
proceed()触发目标方法执行; - 执行后置逻辑(如日志记录结束、统计耗时);
- 返回结果:通知逻辑执行完毕后,将结果返回给客户端。
实例:
1 | // 1. 定义切面类 |
6.MySQL 中,索引是什么?常见的索引类型有哪些?请说明它们的适用场景和优缺点。
数据库中用于加快数据查询速度的一种数据结构,可以类比为书本的目录。通过索引,数据库可以更快地定位到目标数据,而无需全表扫描。
为什么使用b+树,层高,叶子节点/非叶子节点
聚簇索引,非聚簇索引,索引下推
联合索引,覆盖索引
普通索引,唯一索引,前缀索引,全文索引。
索引失效:
explain:
7.MyBatis 的一级缓存和二级缓存机制?如何禁用缓存?
sqlsession:
在一次 SqlSession 生命周期内,相同的查询语句(SQL + 参数)会被缓存,第二次执行时直接从缓存中取值,不会发起数据库请求。当前会话有效,执行 insert/update/delete:任何更新操作会清空缓存。执行sqlSession.clearCache()。不同参数的时候也会失效
mapper:
二级缓存是 Mapper 级别(namespace)共享缓存。
多个 SqlSession 之间共享该 Mapper 的缓存数据。
启用条件:
在 MyBatis 配置中启用全局缓存:
1 | <settings> |
在对应 Mapper 文件中配置 <cache> 标签:
查询的 POJO 实体类必须实现 Serializable。
查询语句不能使用 flushCache="true"(默认查询是 false,更新是 true)。
SqlSession 必须关闭后,一级缓存的数据才会被写入二级缓存。
默认缓存实现是 PerpetualCache + LRU。
可自定义缓存策略(如 EhCache、Redis)。
8.MyBatis Plus 的 Wrapper 和原生 XML 写 SQL 的区别?什么时候该用 Wrapper?
Wrapper 是 MyBatis Plus 提供的条件构造器,用于构建 SQL 的 WHERE、ORDER BY 等子句,简化代码书写。
QueryWrapper:用于普通查询。
LambdaQueryWrapper:使用 lambda 表达式避免写字段名字符串。
UpdateWrapper / LambdaUpdateWrapper:用于更新条件构造。
XML 方式是传统 MyBatis 写 SQL 的方式,通过 Mapper.xml 文件自定义 SQL 语句,更加灵活和强大,支持复杂的多表连接、子查询等。
| 对比项 | Wrapper(构造器) | XML 原生 SQL |
|---|---|---|
| 语法风格 | Java 代码风格,链式调用 | SQL 语法,放在 XML 中 |
| 可读性 | 简洁、类型安全(特别是 Lambda) | 接近原生 SQL,清晰直观 |
| 编写速度 | 快速开发,尤其适合单表 CRUD | 编写略慢,需额外维护 Mapper.xml 文件 |
| SQL 灵活性 | 支持简单查询(单表、分页、排序) | 支持复杂 SQL(多表连接、子查询、聚合) |
| 维护性 | 逻辑分散在代码中,不易集中查看 | 逻辑集中在 XML,更适合团队协作维护 |
| 运行效率 | 两者本质上都由 MyBatis 执行,性能差异不大 | 性能主要看 SQL 写得是否合理 |
| 调试与日志 | SQL 日志可查看 | 也可通过日志查看 |
✅ 使用 Wrapper 的场景:
- 快速开发、原型项目。
- 简单的单表查询、分页、筛选。
- Controller/Service 中构造简单业务逻辑。
- 需要链式调用构造条件,代码更加优雅。
- 使用 Lambda 避免字段拼写错误风险。
推荐:日常开发中能用 Wrapper 就用 Wrapper,提高开发效率。
✅ 使用 XML 写 SQL 的场景:
- 涉及复杂 SQL(多表关联、聚合函数、子查询、动态 SQL)。
- 查询语句过长、不适合写在 Java 代码中。
- 项目追求清晰的逻辑分层、SQL 可维护性。
- 性能调优场景,需要手写精细 SQL。
- 团队需要 DBA 审查 SQL。
推荐:复杂业务、线上稳定项目,用 XML 更清晰、更可控。
6.12模拟面试
1.单例模式有哪些实现方式?各自的优缺点是什么?(比如饿汉式、懒汉式、双重检查锁,还有静态内部类和枚举方式,有没有线程安全的坑呀?)
饿汉式:
同步模式
volatile+synchronized
静态内部类
枚举
2.说下Spring 的ApplicationContext是单例模式”“策略模式在排序算法中的应用”
ApplicationContext 本质上采用了单例模式来保证 Spring 容器中 Bean 的唯一性和全局访问能力。
Spring Boot 启动时会初始化一个 ApplicationContext(如 AnnotationConfigApplicationContext),作为IoC 容器的核心上下文。
这个容器对象在整个应用中只创建一次(单例),所有组件(Controller、Service、Repository)都从这个容器中获取 Bean 实例。
避免了重复初始化 Bean 的性能开销,也方便了依赖管理、统一配置、事件发布等功能的实现。
1 | ApplicationContext context = SpringApplication.run(MyApp.class, args); |
开发中我们只需注入一次上下文或通过 @Autowired 注解获取 Bean,即可全局复用。
Spring 默认的 Bean 是单例的(@Scope("singleton")),这与 ApplicationContext 单例模型相辅相成,进一步保证了资源一致性与管理效率。
策略模式允许在运行时选择算法逻辑,在排序算法中可用于根据用户选择动态切换不同排序策略。
定义统一的排序接口(策略抽象),不同的排序方式(快排、归并、冒泡等)实现这个接口。
运行时根据条件动态切换策略,无需修改原有代码(遵循开闭原则)。
1 | public interface SortStrategy { |
1 | SortContext ctx = new SortContext(new QuickSort()); |
- 电商平台商品排序(按价格、销量、上架时间)
- 数据可视化工具的排序规则切换
3.Java 线程池的核心参数有哪些?各自的作用是什么?比如corePoolSize和maximumPoolSize,如何根据业务场景设置这些参数呀?
核心线程
最大线程
最大线程存活事件
单位
阻塞队列
拒绝策略
工厂
4.你项目里用了JWT加双 Token,具体是怎么设计的?两个 Token 各自的作用是什么?拦截器又分别处理什么逻辑?比如有没有考虑过 Token 过期、刷新机制,或者防重放攻击的问题?
token:
- access token(7 天有效期):放在请求头,用于日常接口认证,用 HS256 加密(密钥服务端持有);放在前端的localStorage/jwt
- refresh token(15 天有效期):存在 Redis,值是随机字符串,且绑定用户 ID ,防止盗用。放在redis中
| 安全措施 | 说明 |
|---|---|
| 使用 HTTPS | 防止中间人劫持 token |
| access token 只读 | JWT 使用 HS256 加密,防篡改 |
| refresh token 存 Redis | 结合用户 ID 存储,支持设置 TTL,有效控制生命周期 |
| 黑名单机制(登出) | 用户退出登录时可删除 Redis 中 refresh token,立即失效 |
| 防止多端同时登录 | Redis 可使用 userId 为 key,限制 refresh token 单个登录会话 |
| Token 刷新接口限流 | 防止 refresh token 被滥用(加防重放、限频) |
| Token 刷新重发保护 | Redis 设置短期 refresh token 使用标记,防止并发重复刷新 |
退出策略:
用户点击退出:
- 删除 Redis 中的 refresh token(或设置为无效标记);
- access token 因为是 JWT,不可被撤销,可考虑实现黑名单机制(例如 Redis 中维护一张 token 黑名单);
黑名单适用于高安全场景(如后台管理系统),但会略增加接口访问时的 Redis 查询压力。
jwt拦截器:
- 认证拦截器:先检查请求头是否有 access token,没有就返回 401;有则解析 token,校验用户信息是否存在。
- 第一个拦截器先校验请求头是否有 access token,没有的话直接返回 401;第二个拦截器在 access token 有效时,额外检查是否快过期(比如剩余时间 < 10 分钟),如果是就用 refresh token 去 Redis 换全新的 access token 和 refresh token(这里要注意刷新时的原子性,避免并发问题)。
- 刷新拦截器:如果 access token 剩余有效期 < 10 分钟,就用请求头中的 refresh token 去 Redis 校验:
- 校验通过的话,生成新的 access token 和 refresh token(新 refresh token 会覆盖 Redis 中的旧值,保证单设备登录);
- 校验失败的话,直接让用户重新登录。
危险:
前端或攻击者拿到密钥(如部署泄露、浏览器调试泄露),就可以伪造合法 token。
token 内容是可解密的,攻击者可猜测或修改 payload,然后重新签名。
如果服务端未验证签名(有些误用场景会只解析不校验),更容易被利用。
但还是sh256更快,计算算开销低;对称密钥管理更简单,适用于内部系统或中小型项目;
开发成本更低,不涉及证书管理、公私钥分发;
| 措施 | 说明 |
|---|---|
| ✅ 使用强密钥 | 至少 256 bit 的复杂密钥,不可硬编码进前端或代码中 |
| ✅ 启用 HTTPS | 防止 token 被中间人劫持 |
| ✅ token 签名校验 | 每次都校验 JWT 的签名,防止伪造 |
| ✅ 缩短 access token 有效期 | 缩小攻击窗口 |
| ✅ refresh token 存 Redis | 结合用户 ID 防重放、防伪造 |
| ✅ 黑名单机制 + 登出清除 | 登出时使 refresh token 无效 |
| ✅ 检查 UA/IP 等指纹 | 防止 token 被别人拿去复用 |
5.看你在优惠劵的发放的时候使用限流,怎么设计的
我们在优惠券领取接口中用 AOP + 令牌桶实现限流
- 注解定义:
@RateLimit注解标注需要限流的方法,参数包括limitCount=5(每分钟 5 次)、time=1(时间单位分钟)、keyType="USER_IP"(按用户 IP+ID 限流)。 - 切面实现:通过
@Around切面拦截注解方法,获取参数后生成唯一限流 key(例如rate_limit:user_123:192.168.1.1),然后调用 Redis 令牌桶服务校验。 - Redis 令牌桶:用 Lua 脚本实现原子性校验,核心是根据时间戳计算可生成的新令牌数,不足时拒绝请求。比如用户抢优惠券时,同一 IP+ID 每分钟最多 5 次请求,防止恶意刷接口。
- 防重复领取:避免用户短时间内多次点击接口,导致优惠券超发;
- 保护服务端:峰值流量时限制请求频率,防止 Redis 或数据库被击穿。线上遇到过同一 WiFi 下多个用户被误限的情况,后来把 key 改为`IP+用户ID,减少了误判率。”
切面类如何获取注解:
1 |
|
为什么选令牌桶而不是漏桶?
令牌桶适合突发流量,漏桶适合平滑流量
使用redis+lua原子的存储令牌桶,使用时间戳来校验。
1 | -- 令牌桶Lua脚本(简化版) |
6.那么优惠劵的发放呢?一些列的问题呢
✅ 一、关于优惠券类型的常见问题
❓1. 如何区分和应用不同类型的优惠券逻辑?
面试点:策略模式、优惠金额计算方式。
答法:
我们用策略模式来封装每种优惠券的优惠逻辑(如满减、折扣、无门槛)。每种类型对应一个策略类,实现统一的接口方法
calculateDiscount(OrderInfo order),这样能在运行时动态选择策略,便于扩展和维护。
✅ 二、关于接口设计的追问
❓2. 为什么接口是这样的?有没有考虑幂等性和安全性?
POST /use接口可能会被重复调用,是否幂等?- 有没有做权限控制,防止其他用户伪造请求?
答法:
在使用优惠券时,为防止重复使用,我们使用 Redis + Lua 实现原子操作,同时标记优惠券为“已使用”。此外接口采用登录态校验,验证当前操作用户是否拥有该优惠券。敏感操作都要求用户登录,并记录操作日志。
✅ 三、关于并发处理和限领逻辑
❓3. 如果多用户并发领取优惠券,如何防止超发?
答法:
发放流程中,我们使用 Redis 的
DECR或 Lua 脚本实现优惠券库存的原子扣减,保证不会发放超过设定数量。同时加锁防止并发条件下库存扣减不一致。
❓4. 如何限制每个用户只能领取一次?
答法:
Redis 中设置一个标识键:
user:coupon:received:{couponId}:{userId},发放前先判断这个键是否存在,避免重复发放。也可以结合布隆过滤器提前过滤无效请求。
✅ 四、关于时间与状态校验
❓5. 限时券怎么判断是否有效?服务端怎么处理时间逻辑?
答法:
每张优惠券记录中保存有效时间范围。使用时,服务端比对当前时间是否在有效期内。同时定时任务每天扫描过期优惠券,标记为“已过期”。
✅ 五、关于优惠券使用过程的逻辑判断
❓6. 使用时如果有多个优惠券,怎么选最优?
答法:
前端可以请求一个“推荐最优券”的接口,我们在服务端遍历用户可用券,调用各个策略类计算预期优惠,返回最高优惠金额对应的券。
✅ 六、关于优惠券与订单系统结合问题
❓7. 如果下单失败了,优惠券是否要恢复?
答法:
下单失败(如支付失败、库存不足)时,我们会在事务回滚后将优惠券状态重置为“未使用”。为了避免并发问题,使用分布式事务(如消息队列、TCC)或 Redis 回滚标记。
✅ 七、进阶设计问题
❓8. 优惠券支持“异步发放”和“定时生效”吗?
答法:
是的,可以结合定时任务(如 Quartz 或 Spring Schedule)实现定时发放;发放时间和有效时间字段分离,发放时写入 Redis 延时队列,当生效时间到达时插入到用户优惠券表中。
✅ 八、可能让你设计代码结构的问题
❓9. 优惠券模块的结构划分是怎样的?
答法(示意):
1 | - entity(Coupon, UserCoupon) |
✅ 九、安全性问题
❓10. 用户伪造优惠券 ID,试图套用别人的优惠券怎么办?
答法:
每次使用时不仅要校验优惠券 ID 是否存在,还要校验当前登录用户是否是该优惠券持有人,只有归属校验通过才能使用。
7.AOP记录日志
“我们用 AOP 实现了登录日志和业务日志的分类记录,核心设计是:
- 注解与策略:定义
@Log注解标注需要记录的方法,通过type()参数区分LOGIN和BUSINESS类型。 切面逻辑:拦截方法执行前后,获取用户信息、操作参数、接口耗时等数据,封装成日志对象。例如登录日志会记录 IP、设备信息,业务日志会记录操作类型(如下单、领券)和参数(如商品 ID、优惠券 ID)。
存储优化:用 Kafka 异步写入日志,避免影响接口性能,再通过 Logstash 同步到 Elasticsearch,方便用 Kibana 按时间、用户 ID 等维度检索。
举个实际场景:用户领取优惠券时,业务日志会记录{user_id:123, operation:"领取满100减20", params:{coupon_id:567}, cost:87ms}。有次线上发现某个接口耗时突然增加,通过 ELK 检索该接口的日志,很快定位到是参数校验逻辑异常导致的。
我们对敏感信息做了脱敏处理,比如用户手机号会存成138****5678,既满足日志追溯需求,又符合数据安全规范。”
存储的信息,使用json进行存储
- 登录日志:用户 ID、登录 IP、设备信息、登录时间、状态(成功 / 失败)、失败原因(如密码错误);
- 业务日志:用户 ID、操作类型(如 “领取优惠券”“下单”)、操作内容、操作时间、接口耗时、请求参数(脱敏处理)。
1 |
|
Method获取主机额上的方法,然后建立实体类,设置参数,然后利用DTO传参,然后执行service方法,然后返回,service就是调用log,info
DTO层负责service和controller直接传输数据
6.14模拟面试
1.Spring 事务的实现原理是什么?@Transactional 注解在什么情况下会失效?
spring事务的实现原理是基于AOP的动态代理和TransacationInterceptorh还有底层依赖
AOP:Spring 通过 ProxyFactoryBean 生成代理对象,默认对接口用 JDK 动态代理,对类用 CGLIB 代理;是采用cglib继承目标类的方式去创建代理类,非pulic的方法不能能继承。
拦截器:核心拦截器,在方法调用前后开启 / 提交 / 回滚事务,基于 ThreadLocal 存储事务状态;基于运行时异常来回滚的,所以把运行时异常给catch或者返回没指定的异常
底层依赖:通过 PlatformTransactionManager 接口适配不同事务管理器(如 JDBC、JPA)。
使用ThreadLocal存储事务的状态,(如连接、隔离级别)通过TransactionSynchronizationManager存在线程本地变量,保证线程安全。
所以基于这个情况,spring事务失效的场景有:
1.吃掉运行时异常没抛出:
2.未配置回滚规则,要配置rollbackFor=Exception.class指定类型
3.调用this
4.非public方法
5.事务的传播属性设置为never,not_support这种不支持事务的
6.调用了不支持事务的数据库
7.事务嵌套:
REQUIRES_NEW 会挂起外部事务,提前提交
嵌套事务用 NESTED 会创建 savepoint,支持回滚子事务(但数据库需支持)
1 | // 外部事务 |
Transactional 属性详解:
propagation:传播行为(如 REQUIRED:当前有事务则加入,无则新建);isolation:隔离级别(如 READ_COMMITTED 避免脏读);timeout:事务超时时间(如timeout=30秒);rollbackFor/rollbackForClassName:指定回滚的异常类型;noRollbackFor/noRollbackForClassName:指定不回滚的异常类型。
2.Java 中 HashMap 在 JDK1.8 的底层实现是什么?相对于 JDK1.7 做了哪些优化?
1.8:数组+链表+红黑树,1.7数组+链表
尾插 头插
转换条件,<64先扩容
为什么不用平衡二叉树,
尾插解决环的问题(线程 A 和线程 B 同时扩容 HashMap,A 处理链表时被挂起,B 完成扩容后,A 继续处理时按头插法插入元素,导致链表成环;)。
扩容机制 <64先扩容
哈希算法优化:
- 1.7:通过
hashCode() ^ (hashCode() >>> 16)扰动函数打散高 16 位,避免低位冲突; - 1.8:直接使用
hashCode(),但计算下标时用(n-1) & hash(n 为数组长度,需是 2 的幂),例:
实战:
- 性能提升:存储 1000 个哈希冲突的键时,1.7 查询需遍历链表(O (1000)),1.8 用红黑树只需 O (log2 (1000))≈10 次查询;
- 内存占用:红黑树节点比链表节点多存储父节点、左右子节点指针,但若哈希冲突少,链表长度 <8 时仍用链表,节省内存。”
红黑树特点:
- 节点非红即黑;
- 根节点和叶节点(null)是黑色;
- 红色节点不能相邻;
- 任意节点到叶节点的路径上黑节点数相同;
插入时最多旋转 2 次即可平衡,适合 HashMap 的高频插入场景。
场景:
“线上曾遇到 HashMap 性能问题,通过 JProfiler 发现某接口频繁操作哈希冲突严重的 Map,将 JDK1.7 升级到 1.8 后,接口响应时间从 500ms 降至 50ms,主要得益于红黑树对高冲突场景的优化。”
3.Spring Bean 的生命周期是怎样的?@PostConstruct 和 @PreDestroy 的作用是什么?
实例化->初始化->使用->销毁
实例化:通过构造器创建 Bean 实例(无参构造或工厂方法);
属性赋值:依赖注入(@Autowired、setter 方法等);
初始化前:执行 BeanPostProcessor 的postProcessBeforeInitialization;
初始化:
- 执行 @PostConstruct 标注的方法;
- 实现 InitializingBean 接口的
afterPropertiesSet; - 自定义 init-method(XML 配置或 @Bean 的 initMethod 属性);
初始化后:执行 BeanPostProcessor 的postProcessAfterInitialization;
使用:Bean 放入容器,供应用获取使用;
销毁前:执行 @PreDestroy 标注的方法;
销毁:
- 实现 DisposableBean 接口的
destroy; - 自定义 destroy-method。
三级缓存解决循环依赖:
@PostConstruct 和 @PreDestroy 的执行时机:
@PostConstruct:在依赖注入完成后,初始化方法(如 init-method)之前执行;
@PreDestroy:在 Bean 销毁前执行,先于 destroy-method;
应用场景:
- 初始化:数据库连接池初始化、缓存预热;
- 销毁:释放资源(关闭文件流、释放锁)。
关键点:
- BeanPostProcessor:初始化前后的拦截器(如 AOP 代理在此阶段创建);
- InstantiationAwareBeanPostProcessor:属性赋值前的拦截器(可修改属性值);
- SmartInitializingSingleton:所有单例 Bean 初始化完成后执行(适合初始化需要依赖其他 Bean 的场景)。
问题解决:
- 问题:ServiceA 依赖 ServiceB,ServiceB 的 @PostConstruct 方法未完成时,ServiceA 已使用 ServiceB;
- 解决:通过
@DependsOn("serviceB")强制 ServiceA 在 ServiceB 之后初始化,或实现Ordered接口指定初始化顺序。”或者是使用懒加载加载一个,或者是重新构建方法
4.Java 中 synchronized 和 volatile 的区别是什么?各自的应用场景有哪些?
synchronized:
- 原子性:通过 Monitor 锁确保代码块同一时间只能被一个线程执行;
- 可见性:解锁时将工作内存变量刷新到主内存;
- 有序性:通过 happens-before 原则,禁止指令重排(锁的获取 / 释放形成 happens-before 关系)。
volatile:
- 可见性:写操作时强制刷新主内存,读操作时强制从主内存读取;
- 有序性:通过内存屏障(Memory Barrier)禁止指令重排,确保 happens-before 关系;
- 不保证原子性:仅保证单次读 / 写操作的原子性,如
i++(实际是读 - 改 - 写三步,非原子)。
内存屏障使用的时间:读写屏障,写写屏障,写读屏障
使用场景:
s:原子操作,线程安全比如hashtable
v:状态标记,interput
联合使用:单例的DCL模式,concurrenthashmap,
问题:
- 问题:多线程读取配置开关不生效;
- 原因:配置开关未用 volatile 修饰,线程读取的是本地缓存值;
- 解决:
volatile boolean configSwitch = false;,修改后立即通知所有线程。
5.Java 中为什么要使用线程池?线程池如何处理异常?
线程池的核心优势系统化梳理:
- 资源复用:避免频繁创建销毁线程(创建线程耗时约 3ms,复用可降低开销);
- 控制并发数:防止并发过高导致的 OOM(如秒杀场景限制线程数保护数据库);
- 统一管理:统一设置线程名称、优先级,方便日志追踪和故障排查;
- 异步处理:将耗时任务放入线程池,避免主线程阻塞(如日志异步写入)。
比较单线程,多线程,和线程池
异常处理:
使用try-catch包裹任务
1 | executor.execute(() -> { |
通过Future获取异常
1 | Future<?> future = executor.submit(() -> { |
拒绝策略AbortPolicy会抛出RejectedExecutionException,需在提交任务时捕获:
1 | try { |
场景:
- 电商下单场景:
用线程池异步处理库存扣减、积分计算等耗时任务,主线程快速返回订单创建结果,提升用户体验; - 日志系统:
用线程池异步写入日志到 Kafka,避免 IO 阻塞影响业务接口响应时间。
6.19模拟面试
1.在 “校园餐饮系统” 项目里,你提到用了 Redis 存储 token 缓解压力,能讲讲具体怎么设计 key 的结构,以及如何保障 token 存储和读取的高效性与安全性不?
我们使用 Redis 存储用户的 refresh token,以提升系统性能并减少数据库压力,整体设计分为三部分:Key 设计、存储结构、安全机制。
结构设计:
1 | token:refresh:{userId} |
这种格式能明确标识 token 类型和所属用户,保证唯一性和可读性,避免 key 冲突。
Value 的存储结构:
我们使用 Redis 的 字符串结构(String) 或 哈希结构(Hash) 存储 refresh token,取决于业务是否需要额外附加字段(如生成时间、IP、设备信息)。
1 | HSET token:refresh:123 userId 123 token abc123 createdAt 2025-06-19 |
设置合理的过期时间(如 7 天),使用 EXPIRE 或 SETEX 命令控制 token 生命周期。
Token 的生成与安全性保障:
Token 使用 JWT 生成,签名采用 HS256 算法(HMAC + SHA256),密钥保存在服务端配置中心或安全的 KMS(Key Management Service)中。
JWT 本身包含用户信息、过期时间等,生成后只将 access token 下发到前端,refresh token 则存于 Redis 中服务端管理,防止伪造。
认证流程:
登录时生成 access + refresh token:
- access token 返回给前端;
- refresh token 存入 Redis,key 为
token:refresh:{userId}。
前端 token 过期后,携带 refresh token 请求刷新接口;
后端校验 Redis 中的 refresh token,校验通过后重新生成新 token 对。
提前防止问题:
防缓存穿透:使用 布隆过滤器 记录有效用户 ID 或常见请求 Key,防止恶意请求频繁查询 Redis 失败并打爆数据库。
防击穿:为热点用户的 token 设置合理 TTL,避免同一时间大批用户同时失效。
防雪崩:TTL 设置加入随机因子,避免大批 key 同时过期。
权限校验:请求时验证 token 是否属于当前 userId,防止越权操作。
2.在 “MinaDB - 自研轻量级数据库系统” 项目里,你实现了基于 Java NIO 的数据页读写管理模块,提升了 3 倍访问效率,讲讲 Java NIO 是咋在这个模块里发挥作用的,和传统 I/O 相比,具体优化点在哪呀?
BIO 与 NIO 的区别:
BIO(Blocking I/O):每个请求需要一个线程处理,线程阻塞在 I/O 操作上,连接数一多就容易造成线程资源耗尽,系统响应慢。
NIO(Non-blocking I/O):通过 Selector + Channel + Buffer 实现多路复用,单线程可监听多个 Channel,避免大量线程阻塞等待,提高了资源利用率。
NIO 工作机制:
- 客户端连接通过
ServerSocketChannel接收; - 每个
Channel注册到Selector上,监听感兴趣的事件(如READ、WRITE); - Selector 使用
select()非阻塞轮询就绪事件; - 当某个 Channel 有事件到达,就通过
Buffer读写数据,由工作线程处理请求。
这种模型的优势是:少量线程可处理高并发请求,适合 I/O 密集型场景。
数据库应用:
数据库系统天然是多并发场景,传统 BIO 每个连接对应一个线程,会造成线程浪费甚至上下文切换频繁;
使用 NIO 模型后,我们采用 Reactor 模式,用一个主线程监听所有连接事件,用线程池异步处理真正的读写请求;
每个客户端连接通过 Channel 注册到 Selector 上,提升了连接并发处理能力;
底层页式读写通过 Buffer 显著减少了系统调用次数和数据拷贝成本,提高了磁盘 I/O 效率。
并发控制说明:
因为是数据库系统,多用户可能同时对同一数据页进行访问;
在读写路径上,我们结合了读写锁机制和自实现的MVCC(多版本并发控制),确保事务隔离的一致性;
NIO 负责连接层的高效事件处理,MVCC 负责数据访问层的并发安全。
3.在 “校园餐饮系统” 项目里,你用 AOP 实现了操作日志自动记录,讲讲 AOP 切点是怎么定义的,怎么确保只拦截到需要记录日志的关键业务方法,又不会过度拦截影响系统性能呀?
我们系统中使用了 Spring AOP 来统一记录用户的操作日志,包括登录日志和业务行为日志。整体设计思路是基于 自定义注解 + 切面编程 + 策略模式 来实现灵活、可扩展的日志记录体系。
我们定义了一个自定义注解 @LogRecord,用于标记需要记录的操作方法,注解中可以配置以下元信息:
1 |
|
切面实现日志拦截逻辑:
1 |
|
日志记录策略设计(策略模式):
1 | public interface LogStrategy { |
策略选择通过 LogStrategyFactory 实现,支持按日志类型动态扩展。
如何保证只拦截关键方法、避免性能问题:
只对使用 @LogRecord 注解的方法进行拦截,确保业务无关方法不被扫描,避免性能浪费;
切面逻辑尽量精简,只做元信息解析和日志上下文构建,真正的日志落盘交给异步任务处理或消息队列;
对不需要记录的查询类接口,不加注解,完全绕过切面执行,保障系统整体响应效率。
日志可选择输出到文件(使用 SLF4J + Logback),或存入数据库 / Elasticsearch,供后期审计;
特殊场景下(如操作异常),支持记录异常栈与执行耗时。
追问:::
- 问:@Around 和 @Before 有什么区别,为什么你选用 Around?
- 答:Around 能拿到方法执行前后的控制权,包括返回值和异常,更适合记录执行结果和耗时。
- 问:怎么处理日志失败、落盘慢的问题?
- 答:日志落盘可异步执行或通过 MQ 异步解耦,避免阻塞主流程。
4.Mysql的两段提交的三个步骤
1.prepare
InnoDB 写入 redo log 的 prepare 状态并刷盘:
InnoDB 引擎将事务修改的数据写入内存(Buffer Pool);
然后生成一条 redo log,标记为 "prepare" 状态;
调用 fsync 将 redo log 刷入磁盘,保证崩溃后能恢复;
2.binlogcommit:
Server 层将整个事务的逻辑操作记录为一条 binlog;
将 binlog 持久化刷盘,fsync() 确保落地。
3.redocommit:
Server 层写 binlog 成功后,通知 InnoDB;
InnoDB 将之前 prepare 状态的 redo log 更新为 "commit" 状态;
这一步是关键确认步骤,表示事务已完全提交。
如果 先写 binlog 再写 redo log:InnoDB 崩溃了但 binlog 有记录,主从同步出错;
如果 只写 redo log,不写 binlog:主库恢复没问题,但从库根本不会同步这条事务,数据不一致;
所以必须保证 redo log 与 binlog 一致性落盘,2PC 正是为了解决这一问题。
5.springboot启动类注解
@SpringBootApplication
这个注解是 Spring Boot 应用的入口,是一个组合注解,等价于:
1 | @Configuration |
@Configuration
- 表示这是一个配置类,等同于传统的
applicationContext.xml; - 可在该类中使用
@Bean定义 Bean。
@EnableAutoConfiguration
- 开启 Spring Boot 的自动配置机制;
- 会根据项目依赖的 jar 自动配置 Spring Bean(例如依赖了 spring-boot-starter-web,就会自动配置 Tomcat、Spring MVC 等);
- 可通过
exclude属性排除某些自动配置类:
1 | @EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class}) |
它通过 SpringFactoriesLoader + SPI机制 + 条件注解,在启动时动态加载并生效配置类。
@Import(AutoConfigurationImportSelector.class)
这个 Import 会触发 Spring 的自动导入机制;AutoConfigurationImportSelector 会加载所有自动配置类
内部使用
SpringFactoriesLoader加载spring.factories文件中所有自动配置类每个自动配置类都配合有
@ConditionalOn...注解,例如:Spring 会根据当前 classpath、已有 Bean、配置文件内容等条件判断是否真正加载配置类;
从而实现了“按需加载”的自动配置。
在springboot中约定大于配置,
约定大于配置是Spring Boot的核心设计理念,它通过预设合理的默认行为和项目规范,大幅减少开发者需要手动配置的步骤,从而提升开发效率和项目标准化程度。
理解 Spring Boot 中的“约定大于配置”原则,可以从以下几个方面来解释:
- 自动化配置:Spring Boot 提供了大量的自动化配置,通过分析项目的依赖和环境,自动配置应用程序的行为。开发者无需显式地配置每个细节,大部分常用的配置都已经预设好了。例如,引入
spring-boot-starter-web后,Spring Boot会自动配置内嵌Tomcat和Spring MVC,无需手动编写XML。 - 默认配置:Spring Boot 为诸多方面提供大量默认配置,如连接数据库、设置 Web 服务器、处理日志等。开发人员无需手动配置这些常见内容,框架已做好决策。例如,默认的日志配置可让应用程序快速输出日志信息,无需开发者额外繁琐配置日志级别、输出格式与位置等。
- 约定的项目结构:Spring Boot 提倡特定项目结构,通常主应用程序类(含 main 方法)置于根包,控制器类、服务类、数据访问类等分别放在相应子包,如
com.example.demo.controller放控制器类,com.example.demo.service放服务类等。此约定使团队成员更易理解项目结构与组织,新成员加入项目时能快速定位各功能代码位置,提升协作效率。
@ComponentScan
- 扫描当前类所在包及其子包下的所有类,识别注解如
@Component、@Service、@Repository、@Controller并注册到 Spring 容器; - 可指定自定义扫描路径:
1 | @ComponentScan(basePackages = {"com.example.service", "com.example.controller"}) |
扫描这个下面的包和他的子类
常见注解:
@EnableScheduling
- 开启定时任务支持(配合
@Scheduled使用)
@EnableAsync
- 开启异步任务支持(配合
@Async使用)
@EnableTransactionManagement
- 开启事务注解支持(如
@Transactional)
6.BeanFactory 和 FactoryBean 的区别
BeanFactory 是 Spring 容器本身,负责管理所有 Bean;
FactoryBean 是你自己定义的 Bean,用来创建其他 Bean。
| 对比点 | BeanFactory |
FactoryBean |
|---|---|---|
| 类型 | 接口(Spring 顶层 IOC 容器) | 接口(自定义 Bean 的工厂) |
| 位置 | Spring 框架提供 | 用户自定义实现 |
| 作用 | 提供获取 Bean 的基础功能,如 getBean() |
用于控制某个复杂 Bean 的创建逻辑 |
| 返回对象 | 返回注册的原始 Bean 实例 | 返回由 getObject() 方法创建的对象 |
| 示例用途 | XML 加载、懒加载场景用到 | MyBatis 中用于创建 Mapper 接口代理 |
| 典型实现类 | DefaultListableBeanFactory |
用户自定义,如 MyCarFactoryBean |
1️⃣ BeanFactory 示例(Spring 提供)
1 | BeanFactory factory = new ClassPathXmlApplicationContext("beans.xml"); |
BeanFactory就是容器,负责“拿 Bean”。- 常用于早期的 XML 配置方式。
FactoryBean 示例(用户自定义)
1 |
|
- Spring 启动时会识别
MyCarFactoryBean,但注入时拿的是getObject()的返回值; - 如果你想拿到
FactoryBean本身,可以用:
1 | CarFactoryBean factoryBean = (CarFactoryBean) context.getBean("&carFactoryBean"); |
beanFactory 是 Spring 的底层 IOC 容器接口,提供了 Bean 的获取、懒加载等功能。
FactoryBean 是一个用于创建 Bean 的“工厂类”,由我们自定义创建逻辑,它本身是一个 Bean,但它返回的对象是 getObject() 产生的。
例如:MyBatis 就是通过 FactoryBean 动态创建 Mapper 接口代理对象。
7.MyBatis Mapper 注册过程
动态代理、FactoryBean、Configuration 注册流程的理解
MyBatis 使用 MapperFactoryBean 将接口注册为代理对象,启动时会把接口方法解析成 MappedStatement 存入 Configuration 中,运行时由 MapperProxy 通过反射动态执行 SQL。
1 | XML or |
1.@MapperScan 启动时扫描 Mapper 接口
@MapperScan 会注册 MapperScannerConfigurer 或 MapperScannerRegistrar;
它们会扫描包下所有接口,对每个接口注册一个 MapperFactoryBean 到 Spring 容器中。
2.每个 Mapper 接口注册成一个 MapperFactoryBean
MapperFactoryBean 实现了 Spring 的 FactoryBean 接口;
它不会直接注入自身,而是调用 getObject() 返回 Mapper 接口的代理类。
3.MapperProxyFactory 创建代理类
返回的是一个 MapperProxy 的动态代理对象,底层通过 Proxy.newProxyInstance(...) 实现;
所以你注入的 UserMapper 实际是 MapperProxy 代理类。
4.代理对象调用方法时执行 SQL
MapperMethod 会查找 Configuration 中注册的 MappedStatement;
调用 sqlSession.selectOne(...) 或 insert(...) 等执行 SQL;
SQL 语句和参数映射都来源于 XML 或注解注册的 MappedStatement。
5.在 SqlSessionFactoryBuilder.build() 构建过程中,MyBatis 会:
- 加载 XML 映射文件或注解 Mapper;
- 把每个
<mapper>里的 SQL 语句解析为MappedStatement; - 调用
Configuration.addMappedStatement()注册到全局配置里; - 最终保存在
configuration.getMappedStatements()的 Map 中。












