本文最后更新于70 天前,其中的信息可能已经过时,如有错误请发送邮件到3082654005@qq.com
(Java 开发岗)中等难度面试题及答案
一、Java 基础与面向对象(5 题)
1. 请说明 Java 中接口(Interface)和抽象类(Abstract Class)的核心区别,分别适用于什么场景?
答案:
| 维度 | 接口(Interface) | 抽象类(Abstract Class) |
|---|---|---|
| 方法定义 | Java 8 + 允许默认方法(default)和静态方法,其余必须是抽象方法(无实现) | 可包含抽象方法(无实现)和具体方法(有实现) |
| 成员变量 | 只能是public static final修饰的常量 | 可包含任意修饰符的成员变量(private、protected 等) |
| 继承 / 实现 | 一个类可实现多个接口(多实现) | 一个类只能继承一个抽象类(单继承) |
| 实例化 | 不能直接实例化,需通过实现类实例化 | 不能直接实例化,需通过子类实例化 |
| 适用场景: |
- 接口:需定义多类共用的 “行为规范”(如
Runnable接口定义 “可运行” 行为),或实现 “多继承” 效果(如一个类同时实现Comparable和Serializable); - 抽象类:需抽取多子类的 “共性属性和部分共性方法实现”(如
AbstractList封装集合的共性遍历逻辑,子类仅需实现核心方法)。
2. HashMap 和 HashTable 的核心区别是什么?在并发场景下若需使用类似集合,推荐用什么?
答案:
核心区别如下:
- 线程安全:HashMap 非线程安全;HashTable 通过
synchronized修饰方法实现线程安全,效率低; - null 键值支持:HashMap 允许
key=null和value=null(仅一个key=null);HashTable 不允许key和value为null,否则抛NullPointerException; - 底层实现:JDK 1.8 后,HashMap 当链表长度≥8 时转为红黑树;HashTable 始终是数组 + 链表;
- 父类:HashMap 继承自
AbstractMap;HashTable 继承自Dictionary。
并发场景推荐:使用ConcurrentHashMap,它通过 “分段锁”(JDK 1.7)或 “CAS+synchronized”(JDK 1.8)实现高效并发,性能优于 HashTable。
3. Java 中 try-catch-finally 结构的执行顺序是什么?若 finally 块中包含 return 语句,会对 try 或 catch 中的 return 结果产生什么影响?
答案:
- 基础执行顺序:
- 先执行
try块代码; - 若
try块无异常,执行完try后跳至finally块,再执行try后的代码; - 若
try块抛异常,匹配catch块并执行,执行完catch后跳至finally块,再执行catch后的代码(若未捕获异常则不执行catch,直接执行finally后抛异常)。
- 先执行
- finally 含 return 的影响:
finally块的return会覆盖try或catch中的return结果,且try/catch中后续代码(如return后的打印)不再执行;- 示例:java运行
public static int test() { try { return 1; // 暂存结果1 } finally { return 2; // 直接返回2,覆盖try的1 } } // 调用test()返回2 - 建议:finally 块不建议写 return,仅用于释放资源(如关闭流、连接)。
4. 请解释 Java 中的 “值传递” 和 “引用传递”,并举例说明方法参数为对象时的传递机制?
答案:
- 值传递:方法接收的是参数的 “副本”(基本数据类型如
int、char,或对象引用的地址副本),修改副本不会影响原参数; - 引用传递:方法接收的是参数的 “内存地址”,修改参数会直接影响原对象(Java不支持引用传递,仅支持值传递)。
对象参数传递机制:传递的是 “对象引用的地址副本”—— 方法内通过副本地址操作对象时,会修改原对象的内容;但修改副本地址本身(如指向新对象),不会影响原引用。
示例:
java
运行
public class Test {
static class User { String name; }
public static void changeName(User u) {
u.name = "张三"; // 通过副本地址修改原对象内容,生效
u = new User(); // 修改副本地址指向新对象,不影响原引用
u.name = "李四";
}
public static void main(String[] args) {
User user = new User();
user.name = "王五";
changeName(user);
System.out.println(user.name); // 输出“张三”(原对象内容被改,地址未变)
}
}
5. Java 中的线程池核心参数有哪些?假设你需要创建一个处理电商订单的线程池,核心参数该如何设置(说明理由)?
答案:
- 线程池核心参数(基于
ThreadPoolExecutor):corePoolSize:核心线程数(线程池长期维持的线程数,即使空闲也不销毁);maximumPoolSize:最大线程数(线程池允许的最大线程数,超出核心线程数的为 “非核心线程”);keepAliveTime:非核心线程空闲存活时间(超过该时间则销毁);workQueue:任务阻塞队列(核心线程满时,新任务暂存的队列);threadFactory:线程创建工厂(自定义线程名称、优先级等);handler:拒绝策略(队列满且线程数达最大时,如何处理新任务,如AbortPolicy抛异常、CallerRunsPolicy由调用者执行)。
- 电商订单线程池参数设置(假设日均订单 10 万,峰值 3 万 / 小时):
corePoolSize:8~10(日常订单量稳定,核心线程足够处理,避免频繁创建销毁);maximumPoolSize:20~30(峰值时临时扩容,处理突发订单,避免队列堆积);keepAliveTime:60 秒(非核心线程空闲 1 分钟后销毁,减少资源占用);workQueue:LinkedBlockingQueue(容量 1000,订单任务可暂存,避免瞬间峰值触发拒绝);handler:CallerRunsPolicy(峰值时让主线程协助执行,避免订单丢失,优于直接抛异常)。
二、Spring Boot 与 MyBatis(6 题)
6. Spring Boot 的 “自动配置” 原理是什么?若你需要自定义一个自动配置类(如自定义 Redis 配置),步骤是什么?
答案:
- 自动配置核心原理:
- 核心注解:
@SpringBootApplication包含@EnableAutoConfiguration,该注解会扫描类路径下META-INF/spring.factories文件; spring.factories中配置了大量自动配置类(如RedisAutoConfiguration、DataSourceAutoConfiguration);- 自动配置类通过
@Conditional系列注解(如@ConditionalOnClass:类存在时生效、@ConditionalOnMissingBean:Bean 不存在时生效)判断是否需要生效,最终向容器注入默认 Bean。
- 核心注解:
- 自定义 Redis 自动配置步骤:
- 步骤 1:创建配置类
CustomRedisAutoConfiguration,添加@Configuration和@ConditionalOnClass(RedisTemplate.class)(确保 Redis 依赖存在时生效); - 步骤 2:在配置类中定义 Bean,如自定义
RedisTemplate(设置序列化方式,避免默认 JDK 序列化乱码):java运行@Bean @ConditionalOnMissingBean // 若用户未自定义,才使用此Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 设置JSON序列化器 Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); template.setValueSerializer(serializer); template.setKeySerializer(new StringRedisSerializer()); return template; } - 步骤 3:在
resources/META-INF/spring.factories中注册配置类:org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.xxx.config.CustomRedisAutoConfiguration。
- 步骤 1:创建配置类
7. Spring Boot 中如何实现 RESTful API?请举例说明常用 HTTP 方法(GET、POST、PUT、DELETE)对应的业务场景和注解使用。
答案:
RESTful API 核心是 “资源导向”,通过 HTTP 方法表达对资源的操作,Spring Boot 中通过@RestController(= @Controller+@ResponseBody)和@RequestMapping系列注解实现。
| HTTP 方法 | 业务场景 | 注解 | 示例代码 |
|---|---|---|---|
| GET | 查询资源(如查询电影列表) | @GetMapping | @GetMapping("/movies") public List<Movie> getMovieList() { ... } |
| POST | 创建资源(如创建订单) | @PostMapping | @PostMapping("/orders") public Result createOrder(@RequestBody Order order) { ... } |
| PUT | 更新资源(如修改用户信息) | @PutMapping | @PutMapping("/users/{id}") public Result updateUser(@PathVariable Long id, @RequestBody User user) { ... } |
| DELETE | 删除资源(如取消订单) | @DeleteMapping | @DeleteMapping("/orders/{id}") public Result cancelOrder(@PathVariable Long id) { ... } |
说明:
@PathVariable:获取 URL 路径中的参数(如/users/{id}中的id);@RequestBody:接收请求体中的 JSON 数据(如创建订单时传递的订单信息);- 返回结果建议封装为统一格式(如
Result类包含code、message、data),便于前端处理。
8. MyBatis 中#{}和${}的核心区别是什么?为什么推荐优先使用 `#{}?
答案:
核心区别如下:
| 维度 | #{} | ${} |
|---|---|---|
| 参数替换方式 | 预编译 SQL,将参数替换为?,通过PreparedStatement设置参数(如where id = ?) | 字符串拼接,直接将参数值嵌入 SQL(如where id = 1) |
| SQL 注入风险 | 无风险(预编译机制避免注入) | 高风险(如参数为1 or 1=1,拼接后 SQL 变为where id = 1 or 1=1,查询所有数据) |
| 数据类型转换 | 自动转换(如传入String类型的"123",自动转为int) | 需手动处理(如字符串需加引号:where name = '${name}') |
| 适用场景 | 传递参数(如查询条件、新增数据的值) | 动态拼接 SQL 片段(如动态表名、动态排序字段:order by ${field}) |
** 推荐#{}的原因**:#{} 通过预编译机制避免 SQL 注入,安全性更高,且无需手动处理数据类型和引号,是 MyBatis 传递参数的默认推荐方式;仅当需动态拼接 SQL 片段(如动态表名)时,才谨慎使用${},且需确保参数来源安全(如内部固定值,非用户输入)。
9. MyBatis 的一级缓存和二级缓存有什么区别?如何开启二级缓存?
答案:
- 一级缓存与二级缓存区别:
| 维度 | 一级缓存(本地缓存) | 二级缓存(全局缓存) |
|---|---|---|
| 作用范围 | 单个SqlSession(会话)内有效 | 整个SqlSessionFactory内有效(跨SqlSession) |
| 开启方式 | 默认开启,无需额外配置 | 需手动开启(全局 + 映射文件) |
| 存储介质 | 内存(HashMap) | 内存、磁盘等(可自定义,默认内存) |
| 失效场景 | SqlSession关闭 / 提交 / 回滚,或执行update/delete/insert | 对应Mapper的update/delete/insert执行,或缓存过期 |
- 开启二级缓存步骤:
- 步骤 1:在 MyBatis 配置文件(
mybatis-config.xml)中开启全局二级缓存(默认是true,可显式配置):<setting name="cacheEnabled" value="true"/> - 步骤 2:在需要使用二级缓存的
Mapper.xml中添加<cache/>标签(表示该Mapper的所有查询结果存入二级缓存):xml<mapper namespace="com.xxx.mapper.MovieMapper"> <cache eviction="LRU" flushInterval="60000" size="1024" readOnly="true"/> <!-- eviction:缓存淘汰策略(LRU:最近最少使用);flushInterval:缓存过期时间(60秒);size:缓存最多存储1024个对象;readOnly:是否只读 --> <select id="getMovieById" resultType="Movie"> select * from movie where id = #{id} </select> </mapper> - 注意:若查询结果包含
collection(一对多)或association(一对一)关联对象,关联的实体类需实现Serializable接口(二级缓存可能序列化存储)。
- 步骤 1:在 MyBatis 配置文件(
10. Spring Boot 项目中,如何实现 “事务管理”?若一个方法内调用多个 DAO 层方法(如扣库存 + 创建订单),如何保证这些操作要么全成功,要么全失败?
答案:
Spring Boot 通过Spring Transaction实现事务管理,核心是 “声明式事务”(基于注解,简单高效),底层通过 AOP 代理实现。
- 实现事务管理步骤:
- 步骤 1:确保项目引入
spring-boot-starter-jdbc或spring-boot-starter-data-jpa依赖(自动引入事务相关组件); - 步骤 2:在 Spring Boot 启动类上添加
@EnableTransactionManagement(Spring Boot 2.0 + 可省略,默认自动开启); - 步骤 3:在需要事务管理的Service 层方法上添加
@Transactional注解(DAO 层或 Controller 层不推荐,因事务粒度需控制在业务逻辑层)。
- 步骤 1:确保项目引入
- 保证 “扣库存 + 创建订单” 原子性:
示例代码(Service 层):java运行@Service public class OrderService { @Autowired private InventoryMapper inventoryMapper; // 扣库存DAO @Autowired private OrderMapper orderMapper; // 创建订单DAO // 添加@Transactional,确保两个操作原子性 @Transactional(rollbackFor = Exception.class) // rollbackFor:指定异常类型回滚(默认仅RuntimeException回滚) public void createOrder(Order order) { // 1. 扣库存(假设商品ID为order.getGoodsId(),扣减数量为order.getNum()) int inventoryRows = inventoryMapper.decreaseStock(order.getGoodsId(), order.getNum()); if (inventoryRows == 0) { throw new RuntimeException("库存不足"); // 抛异常触发回滚 } // 2. 创建订单 int orderRows = orderMapper.insertOrder(order); if (orderRows == 0) { throw new RuntimeException("订单创建失败"); // 抛异常触发回滚 } // 若两个操作均成功,事务自动提交 } }
关键说明:rollbackFor = Exception.class必须加,因insert/update等 DAO 操作可能抛SQLException(受检异常),默认不会触发回滚,需显式指定回滚异常类型。
11. 若 Spring Boot 项目中,前端传递的参数与后端实体类属性名不一致(如前端传user_name,后端实体类是userName),如何解决参数映射问题?
答案:
有 3 种常用解决方案,根据场景选择:
- 方案 1:使用
@RequestParam(适用于 URL 参数或表单参数)
若前端通过GET请求传递user_name(如/users?user_name=张三),后端 Controller 方法可通过@RequestParam指定参数名映射:java运行@GetMapping("/users") public Result getUser(@RequestParam("user_name") String userName) { // ... 业务逻辑 } - 方案 2:使用
@JsonProperty(适用于 JSON 请求体参数)
若前端通过POST请求传递 JSON(如{"user_name":"张三","age":20}),后端实体类可通过@JsonProperty指定 JSON 字段名与实体属性的映射:java运行public class User { @JsonProperty("user_name") // 前端JSON的user_name映射到userName private String userName; private Integer age; // getter/setter } // Controller方法 @PostMapping("/users") public Result addUser(@RequestBody User user) { System.out.println(user.getUserName()); // 输出“张三” // ... 业务逻辑 } - 方案 3:配置全局参数映射(适用于统一前缀或下划线转驼峰)
若项目中大量存在 “下划线转驼峰” 场景(如user_name→userName),可在 Spring Boot 配置文件中开启全局下划线转驼峰:- 对于 Spring MVC(接收请求参数):yaml
spring: mvc: format: date-time: yyyy-MM-dd HH:mm:ss hiddenmethod: filter: enabled: true # 开启请求参数下划线转驼峰(需Spring Boot 2.0+) jackson: property-naming-strategy: SNAKE_CASE # SNAKE_CASE表示下划线命名(如user_name),实体类用驼峰(userName) - 对于 MyBatis(数据库字段下划线转实体类驼峰):yaml
mybatis: configuration: map-underscore-to-camel-case: true # 数据库字段user_name映射到实体类userName
- 对于 Spring MVC(接收请求参数):yaml
三、MySQL 与数据库优化(5 题)
12. MySQL 中 InnoDB 和 MyISAM 两种存储引擎的核心区别是什么?你的电影购票系统中,“订单表” 和 “电影信息表” 分别适合用哪种引擎(说明理由)?
答案:
- InnoDB 与 MyISAM 核心区别:
| 维度 | InnoDB | MyISAM |
|---|---|---|
| 事务支持 | 支持 ACID 事务(事务安全) | 不支持事务(非事务安全) |
| 锁机制 | 支持行级锁(更新时仅锁单行,并发高)、表级锁 | 仅支持表级锁(更新时锁整个表,并发低) |
| 外键支持 | 支持外键约束 | 不支持外键 |
| 索引结构 | 聚簇索引(主键索引与数据存储在一起,查询快) | 非聚簇索引(索引与数据分离,查询需二次查找) |
| 崩溃恢复 | 支持崩溃恢复(通过 redo/undo 日志) | 不支持崩溃恢复(崩溃后可能丢失数据) |
| 适用场景 | 事务场景(如订单、支付)、高并发更新 | 只读场景(如新闻、商品列表)、查询频繁 |
- 存储引擎选择:
- 订单表(order):适合 InnoDB;
理由:订单表需支持事务(如创建订单时 “扣库存 + 写订单” 原子性),且存在频繁更新(如订单状态从 “待支付” 改为 “已支付”),InnoDB 的行级锁和事务支持能保证数据一致性和高并发。 - 电影信息表(movie):适合 MyISAM;
理由:电影信息表以 “查询” 为主(用户查看影讯、场次),更新频率低(仅管理员修改电影海报、简介),MyISAM 的表级锁对只读场景影响小,且查询性能略优于 InnoDB(非聚簇索引结构对纯查询更轻量)。
- 订单表(order):适合 InnoDB;
13. MySQL 索引的 “最左前缀原则” 是什么?假设有一个联合索引idx_movie_cinema_time(movie_id, cinema_id, show_time),哪些查询语句能用到该索引,哪些不能?
答案:
- 最左前缀原则:
对于 MySQL 联合索引(多字段索引),查询条件需从索引的 “最左字段” 开始匹配,且中间不能跳过字段,否则索引会失效(仅匹配到最左前缀部分,后续字段不生效)。简单说:“索引像新华字典的目录,先按第一个字查,再按第二个字查,跳过第一个字直接查第二个字,目录无效”。 - 联合索引
idx_movie_cinema_time(movie_id, cinema_id, show_time)的适用情况:查询语句(where 条件)是否用到索引说明where movie_id = 1是(全索引生效)匹配最左前缀第一个字段,索引全生效where movie_id = 1 and cinema_id = 2是(全索引生效)匹配前两个字段,索引全生效where movie_id = 1 and cinema_id = 2 and show_time = '2024-05-01 19:00'是(全索引生效)匹配所有三个字段,索引全生效where cinema_id = 2否(索引失效)跳过最左字段 movie_id,索引完全不生效where movie_id = 1 and show_time = '2024-05-01 19:00'是(仅 movie_id 字段生效)跳过中间字段 cinema_id,仅最左前缀 movie_id 生效,show_time 不生效where movie_id > 1 and cinema_id = 2是(仅 movie_id 字段生效)movie_id 用范围查询(>),后续字段 cinema_id 失效
关键注意点:若最左字段用 “范围查询”(如>、<、between、like %xx),则后续字段的索引会失效,仅最左字段的范围查询生效。
14. MySQL 中如何实现 “读写分离”?你在项目中配置的 MySQL 主从复制,若从库查询延迟超过 1 秒,可能的原因是什么?如何优化?
答案:
- MySQL 读写分离实现方式:
基于 “主从复制”,主库(Master)负责写操作(insert/update/delete),从库(Slave)负责读操作(select),通过中间件或应用层路由实现分离:- 中间件方式(推荐):使用 MyCat、Sharding-JDBC 等中间件,中间件自动将写请求转发到主库,读请求转发到从库(支持多从库负载均衡);
- 应用层方式:在代码中通过注解或配置指定读写路由(如 Spring 的
@Transactional(readOnly = true)指定读从库,写操作默认走主库)。
- 从库查询延迟超 1 秒的可能原因及优化:可能原因优化方案主库写操作频繁(如大量订单插入),binlog 日志生成慢1. 主库开启 binlog 日志刷盘优化:
innodb_flush_log_at_trx_commit=1(确保事务提交时 binlog 同步刷盘)、sync_binlog=1(每提交 1 个事务刷 1 次 binlog);2. 主库分库分表,减少单库写压力从库 SQL 线程执行慢(如从库配置低、大事务)1. 提升从库硬件配置(CPU、内存、磁盘,用 SSD 减少 IO 延迟);2. 拆分大事务(如批量更新拆分为小批量);3. 从库开启并行复制(MySQL 5.7 + 支持slave_parallel_workers>0,并行执行多个 binlog 事务)网络延迟(主从库跨机房)1. 主从库部署在同一机房或使用低延迟网络(如专线);2. 从库使用relay_log_info_repository=TABLE(将中继日志信息存表,减少文件 IO)从库索引缺失,查询慢为从库查询频繁的字段添加索引(如订单表的user_id、create_time),避免全表扫描
你项目中的优化:结合简历中 “调整 MySQL 参数将主从同步延迟优化至 1s 内”,可重点提及:
- 调整主库
innodb_flush_log_at_trx_commit=1和sync_binlog=1,确保 binlog 及时生成; - 从库开启并行复制(
slave_parallel_workers=4),提升 SQL 线程执行效率; - 从库添加查询索引,减少慢查询对同步的影响。
15. 写出一条 MySQL 查询语句:从 “电影购票系统” 的order表(字段:order_id, user_id, movie_id, cinema_id, create_time, pay_amount, order_status)中,查询 2024 年 5 月 1 日至 5 月 7 日期间,“万达影城”(cinema_id=1001)的所有已支付订单(order_status=2),按支付金额降序排列,取前 10 条,并显示用户 ID、电影 ID、支付金额、创建时间。
答案:
sql
SELECT
user_id AS 用户ID,
movie_id AS 电影ID,
pay_amount AS 支付金额,
create_time AS 创建时间
FROM
`order` -- order是MySQL关键字,需用反引号包裹
WHERE
cinema_id = 1001 -- 万达影城ID
AND order_status = 2 -- 已支付状态
AND create_time BETWEEN '2024-05-01 00:00:00' AND '2024-05-07 23:59:59' -- 5月1日至5月7日
ORDER BY
pay_amount DESC -- 按支付金额降序
LIMIT 10; -- 取前10条
关键说明:
order是 MySQL 关键字(如ORDER BY),作为表名时需用反引号(`)包裹,避免语法错误;create_time的范围查询用BETWEEN ... AND ...,需指定完整时间(如2024-05-07 23:59:59),避免漏查 5 月 7 日当天的订单;- 字段别名用
AS(可省略),让结果更易读,符合前端展示需求。
16. MySQL 中 “慢查询日志” 的作用是什么?如何开启慢查询日志?若你的电影购票系统中出现一条慢查询(如查询订单列表耗时 5 秒),如何排查和优化?
答案:
- 慢查询日志作用:
记录 MySQL 中 “执行时间超过指定阈值” 的 SQL 语句(默认阈值是 10 秒),用于定位低效查询,是优化 MySQL 性能的核心工具。 - 开启慢查询日志步骤:
- 步骤 1:查看当前配置(登录 MySQL 终端):sql
show variables like '%slow_query_log%'; -- 查看慢查询日志是否开启 show variables like '%long_query_time%'; -- 查看慢查询阈值(默认10秒) - 步骤 2:临时开启(重启 MySQL 后失效):sql
set global slow_query_log = on; -- 开启慢查询日志 set global long_query_time = 2; -- 设置阈值为2秒(根据业务调整,如电商场景设2秒) set global slow_query_log_file = '/var/lib/mysql/slow.log'; -- 指定日志存储路径 - 步骤 3:永久开启(修改 MySQL 配置文件
my.cnf或my.ini):ini[mysqld] slow_query_log = on slow_query_log_file = /var/lib/mysql/slow.log long_query_time = 2 log_queries_not_using_indexes = on -- 记录未使用索引的查询(可选,便于发现无索引SQL) - 步骤 4:重启 MySQL 服务(使配置生效):
systemctl restart mysqld(Linux)或在服务管理器重启(Windows)。
- 步骤 1:查看当前配置(登录 MySQL 终端):sql
- 慢查询(订单列表耗时 5 秒)排查与优化:
- 排查步骤:
- 查看慢查询日志:
cat /var/lib/mysql/slow.log,找到耗时 5 秒的订单查询 SQL; - 用
EXPLAIN分析 SQL 执行计划:sqlEXPLAIN SELECT * FROM `order` WHERE user_id = 123 ORDER BY create_time DESC; -- 假设慢查询SQL是这个
通过EXPLAIN查看:type列:若为ALL(全表扫描),说明无索引;key列:若为NULL,说明未使用索引;rows列:若数值很大,说明扫描行数过多。
- 查看慢查询日志:
- 优化方案:
- 添加索引:若
user_id是查询条件,create_time是排序字段,添加联合索引idx_user_create(user_id, create_time):sqlCREATE INDEX idx_user_create ON `order`(user_id, create_time);
理由:联合索引覆盖 “查询条件 + 排序字段”,避免全表扫描和文件排序(Using filesort); - 优化 SQL:避免
SELECT *,只查询需要的字段(如user_id, order_id, pay_amount),减少数据传输量; - 分页查询:若订单列表无分页,添加
LIMIT(如LIMIT 0,20),避免一次性查询大量数据; - 数据归档:若订单表数据量过大(如超过 100 万条),将历史订单(如 1 年前)归档到历史表,减少主表数据量。
- 添加索引:若
- 排查步骤:
四、Vue3 与前端(4 题)
17. Vue3 中 Composition API 相比 Options API 的核心优势是什么?你在电影购票系统的前端开发中,如何用 Composition API 实现 “电影列表查询” 功能(举例代码)?
答案:
- Composition API vs Options API 核心优势:
| 维度 | Options API(Vue2) | Composition API(Vue3) |
|---|---|---|
| 代码组织 | 按选项(data、methods、computed)拆分,大组件代码分散(如数据在 data,方法在 methods) | 按 “逻辑功能” 拆分(如 “电影列表查询” 相关的变量、方法、生命周期写在一起),代码聚合度高 |
| 逻辑复用 | 依赖 mixins,但存在 “命名冲突”“来源不明确” 问题 | 用 “组合函数”(如useMovieList())复用逻辑,无命名冲突,来源清晰 |
| 类型支持 | 对 TypeScript 支持差(需额外配置) | 原生支持 TypeScript,类型推导更精准,减少类型错误 |
| 生命周期 | 需在beforeMount、mounted等选项中写代码,分散 | 用onMounted等函数,可在同一逻辑块中写生命周期,更灵活 |
- Composition API 实现 “电影列表查询” 功能:
假设电影购票系统的 “电影列表页” 需实现:页面加载时查询电影列表、支持按电影名称搜索、加载状态显示。
代码示例(Vue3 单文件组件<script setup>语法):vue<template> <div class="movie-list"> <!-- 搜索框 --> <input v-model="searchName" placeholder="输入电影名称搜索" @input="debounceGetMovieList" <!-- 防抖搜索 --> > <!-- 加载状态 --> <div v-if="loading">加载中...</div> <!-- 电影列表 --> <ul v-else> <li v-for="movie in movieList" :key="movie.id"> {{ movie.name }} - {{ movie.duration }}分钟 </li> </ul> </div> </template> <script setup> import { ref, onMounted } from 'vue'; // 引入Composition API核心函数 import axios from 'axios'; // 引入axios请求工具 // 1. 定义响应式变量(替代Options API的data) const movieList = ref([]); // 电影列表 const searchName = ref(''); // 搜索关键词 const loading = ref(false); // 加载状态 // 2. 定义防抖函数(优化搜索,避免输入一次发一次请求) const debounceGetMovieList = debounce(() => { getMovieList(); }, 500); // 500毫秒防抖 // 3. 定义电影列表查询方法(替代Options API的methods) const getMovieList = async () => { loading.value = true; // 开始加载 try { const res = await axios.get('/api/movies', { params: { name: searchName.value } // 传递搜索关键词 }); movieList.value = res.data; // 赋值响应式变量(需加.value) } catch (err) { console.error('查询电影列表失败:', err); } finally { loading.value = false; // 结束加载 } }; // 4. 生命周期钩子(页面加载时查询列表,替代Options API的mounted) onMounted(() => { getMovieList(); }); // 防抖工具函数(可抽离到utils文件复用) function debounce(fn, delay) { let timer = null; return (...args) => { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; } </script>
核心亮点:- 所有 “电影列表查询” 相关的逻辑(变量、方法、生命周期)聚合在一起,代码可读性高;
- 防抖函数可抽离为公共工具,实现逻辑复用;
- 用
async/await处理异步请求,比Promise.then更简洁。
18. Vue3 中 Pinia 的作用是什么?它和 Vue2 中的 Vuex 相比,有哪些简化的地方?你在项目中如何用 Pinia 存储和获取 “用户登录状态”?
答案:
- Pinia 作用:
Pinia 是 Vue3 官方推荐的状态管理库,用于集中管理跨组件共享的状态(如用户登录状态、购物车数据),替代 Vue2 中的 Vuex,解决了 Vuex 的复杂性问题。 - Pinia vs Vuex 简化点:维度Vuex(Vue2)Pinia(Vue3)模块划分需通过
modules划分模块,复杂模块需namespace: true开启命名空间无需modules,每个 Store 实例就是一个模块,天然支持命名空间状态修改需通过mutations修改状态(同步)、actions处理异步,mutations不可省略无mutations,直接在actions中修改状态(同步 / 异步均可),简化代码TypeScript 支持支持差,需额外写类型声明文件(如xxx.d.ts)原生支持 TypeScript,类型推导自动完成,无需额外配置API 复杂度API 多(commit、dispatch、mapState等)API 简洁(defineStore、storeToRefs等),学习成本低调试工具依赖 Vue Devtools 的 Vuex 面板原生集成到 Vue Devtools,调试更直观 - Pinia 存储 / 获取 “用户登录状态” 示例:
- 步骤 1:创建 Pinia 实例(main.js):javascript运行
import { createApp } from 'vue'; import { createPinia } from 'pinia'; import App from './App.vue'; const app = createApp(App); app.use(createPinia()); // 注册Pinia app.mount('#app'); - 步骤 2:创建用户 Store(src/store/userStore.js):javascript运行
import { defineStore } from 'pinia'; import axios from 'axios'; // 定义Store(第一个参数是Store唯一ID,第二个参数是配置对象) export const useUserStore = defineStore('user', { // 状态(替代Vuex的state) state: () => ({ isLogin: false, // 是否登录 userInfo: { // 用户信息(如ID、昵称、头像) id: '', name: '', avatar: '' } }), // 计算属性(替代Vuex的getters) getters: { // 获取用户昵称(若未登录返回“游客”) userNickname: (state) => state.isLogin ? state.userInfo.name : '游客' }, // 方法(替代Vuex的actions,支持同步/异步) actions: { // 登录方法(异步,调用后端登录接口) async login(userData) { try { const res = await axios.post('/api/login', userData); // userData:账号密码 const { id, name, avatar } = res.data; // 直接修改状态(无需mutations) this.isLogin = true; this.userInfo = { id, name, avatar }; // 存储到localStorage,避免页面刷新后状态丢失 localStorage.setItem('userInfo', JSON.stringify({ id, name, avatar })); } catch (err) { console.error('登录失败:', err); throw err; // 抛异常,让组件处理错误(如提示“账号密码错误”) } }, // 退出登录方法(同步) logout() { this.isLogin = false; this.userInfo = { id: '', name: '', avatar: '' }; localStorage.removeItem('userInfo'); // 清除localStorage }, // 页面刷新后加载localStorage中的用户信息 loadUserInfo() { const userInfo = localStorage.getItem('userInfo'); if (userInfo) { this.isLogin = true; this.userInfo = JSON.parse(userInfo); } } } }); - 步骤 3:组件中使用 Store(如 Login.vue、Header.vue):vue
<!-- Login.vue:登录组件 --> <template> <div> <input v-model="username" placeholder="账号"> <input v-model="password" type="password" placeholder="密码"> <button @click="handleLogin">登录</button> </div> </template> <script setup> import { ref } from 'vue'; import { useUserStore } from '@/store/userStore'; import { useRouter } from 'vue-router'; const userStore = useUserStore(); // 获取用户Store const router = useRouter(); const username = ref(''); const password = ref(''); const handleLogin = async () => { try { await userStore.login({ username: username.value, password: password.value }); router.push('/home'); // 登录成功跳首页 } catch (err) { alert('登录失败,请检查账号密码'); // 处理错误 } }; </script> <!-- Header.vue:头部组件(显示用户状态) --> <template> <div class="header"> <div v-if="userStore.isLogin"> <img :src="userStore.userInfo.avatar" alt="头像" class="avatar"> <span>{{ userStore.userNickname }}</span> <!-- 使用getter --> <button @click="userStore.logout">退出登录</button> </div> <div v-else> <router-link to="/login">登录</router-link> </div> </div> </template> <script setup> import { useUserStore } from '@/store/userStore'; import { onMounted } from 'vue'; const userStore = useUserStore(); // 页面刷新后加载用户信息 onMounted(() => { userStore.loadUserInfo(); }); </script>
- 步骤 1:创建 Pinia 实例(main.js):javascript运行
19. Vue3 中v-model的使用方式与 Vue2 有什么区别?如何用v-model实现一个 “自定义搜索框组件”(支持双向绑定搜索关键词)?
答案:
- Vue3 vs Vue2
v-model区别:- Vue2
v-model:
本质是 “v-bind:value+v-on:input” 的语法糖,一个组件只能有一个v-model,若需多字段双向绑定,需用.sync修饰符(如v-bind:title.sync="title")。 - Vue3
v-model:- 本质是 “
v-bind:modelValue+v-on:update:modelValue” 的语法糖,支持自定义modelValue名称(如v-model:keyword="searchKey"); - 一个组件可支持多个
v-model(如v-model:keyword="searchKey"、v-model:date="searchDate"),无需sync修饰符; - 支持修饰符(如
v-model.trim去除空格、v-model.number转为数字),且可自定义修饰符。
- 本质是 “
- Vue2
- 自定义搜索框组件(支持双向绑定)示例:
需求:创建CustomSearch组件,支持双向绑定搜索关键词,且自带 “清除” 功能和 “搜索” 按钮。- 步骤 1:创建自定义组件(src/components/CustomSearch.vue):vue
<template> <div class="custom-search"> <!-- 输入框:绑定modelValue,输入时触发update:modelValue事件 --> <input type="text" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" :placeholder="placeholder" class="search-input" > <!-- 清除按钮:点击清空关键词 --> <button class="clear-btn" @click="$emit('update:modelValue', '')" v-if="modelValue" > × </button> <!-- 搜索按钮:点击触发search事件,通知父组件执行搜索 --> <button class="search-btn" @click="$emit('search')"> 搜索 </button> </div> </template> <script setup> // 定义组件接收的props(modelValue是v-model默认的prop名) const props = defineProps({ modelValue: { // 搜索关键词(v-model绑定的值) type: String, default: '' }, placeholder: { // 输入框提示文字(可选) type: String, default: '请输入搜索内容' } }); // 定义组件触发的事件(update:modelValue是v-model默认的事件名) defineEmits(['update:modelValue', 'search']); </script> <style scoped> .custom-search { display: flex; align-items: center; gap: 8px; } .search-input { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; flex: 1; } .clear-btn { width: 24px; height: 24px; border: none; border-radius: 50%; background: #eee; cursor: pointer; display: flex; align-items: center; justify-content: center; } .search-btn { padding: 8px 16px; border: none; border-radius: 4px; background: #42b983; color: white; cursor: pointer; } </style> - 步骤 2:父组件使用自定义搜索框(如 MovieList.vue):vue
<template> <div class="movie-list-page"> <!-- 使用自定义搜索框:v-model双向绑定searchKey,监听search事件执行搜索 --> <CustomSearch v-model="searchKey" placeholder="输入电影名称搜索" @search="getMovieList" /> <!-- 电影列表展示 --> <ul> <li v-for="movie in movieList" :key="movie.id"> {{ movie.name }} </li> </ul> </div> </template> <script setup> import { ref } from 'vue'; import CustomSearch from '@/components/CustomSearch.vue'; import axios from 'axios'; const searchKey = ref(''); // 搜索关键词(v-model绑定的值) const movieList = ref([]); // 电影列表 // 搜索电影列表方法 const getMovieList = async () => { const res = await axios.get('/api/movies', { params: { name: searchKey.value } }); movieList.value = res.data; }; // 初始加载电影列表 getMovieList(); </script>
- 子组件通过
props.modelValue接收父组件的searchKey,通过$emit('update:modelValue', 新值)通知父组件更新searchKey,实现双向绑定; - 父组件通过
v-model="searchKey"简化绑定,无需手动写:modelValue和@update:modelValue; - 子组件触发
search事件,父组件监听并执行搜索逻辑,实现 “按钮点击搜索” 功能。
- 步骤 1:创建自定义组件(src/components/CustomSearch.vue):vue
20. Vue3 项目中如何实现 “路由跳转” 和 “路由守卫”?你在电影购票系统中,如何用路由守卫实现 “未登录用户禁止访问订单中心”?
答案:
- Vue3 路由跳转方式:
Vue3 中使用vue-router4.x 版本,路由跳转分为 “声明式” 和 “编程式”:- 声明式跳转(模板中使用
router-link):vue<!-- 跳转到首页(to是路由路径) --> <router-link to="/home">首页</router-link> <!-- 跳转到电影详情页(传递参数,如电影ID=123) --> <router-link :to="{ path: '/movie/detail', query: { id: 123 } }">电影详情</router-link> <router-link :to="{ name: 'MovieDetail', params: { id: 123 } }">电影详情</router-link> <!-- 说明:query参数会拼在URL后(如? id=123),params参数需路由配置中定义(如/movie/detail/:id),URL中无参数名 --> - 编程式跳转(脚本中使用
useRouter):javascript运行import { useRouter } from 'vue-router'; const router = useRouter(); // 跳转到首页 const goToHome = () => { router.push('/home'); }; // 跳转到电影详情页(带query参数) const goToMovieDetail = (movieId) => { router.push({ path: '/movie/detail', query: { id: movieId } }); }; // 后退一步 const goBack = () => { router.go(-1); }; // 替换当前路由(不添加历史记录,如登录后替换登录页,避免后退回到登录页) const replaceToHome = () => { router.replace('/home'); };
- 声明式跳转(模板中使用
- Vue3 路由守卫类型:
路由守卫用于控制路由访问权限(如登录验证、权限校验),分为 3 类:- 全局守卫:作用于所有路由(如
router.beforeEach前置守卫、router.afterEach后置守卫); - 路由独享守卫:作用于单个路由(在路由配置中定义
beforeEnter); - 组件内守卫:作用于当前组件(如
onBeforeRouteEnter、onBeforeRouteLeave)。
- 全局守卫:作用于所有路由(如
- 路由守卫实现 “未登录禁止访问订单中心”:
采用 “全局前置守卫”(router.beforeEach),对所有需要登录的路由进行拦截:- 步骤 1:路由配置(src/router/index.js):javascript运行
import { createRouter, createWebHistory } from 'vue-router'; import Home from '@/views/Home.vue'; import Login from '@/views/Login.vue'; import OrderCenter from '@/views/OrderCenter.vue'; // 订单中心(需登录) import MovieDetail from '@/views/MovieDetail.vue'; // 电影详情(无需登录) // 定义路由,给需要登录的路由添加meta.requireAuth: true const routes = [ { path: '/', redirect: '/home' // 重定向到首页 }, { path: '/home', name: 'Home', component: Home, meta: { requireAuth: false } // 无需登录 }, { path: '/login', name: 'Login', component: Login, meta: { requireAuth: false } // 无需登录 }, { path: '/order-center', name: 'OrderCenter', component: OrderCenter, meta: { requireAuth: true } // 需登录 }, { path: '/movie/detail', name: 'MovieDetail', component: MovieDetail, meta: { requireAuth: false } // 无需登录 } ]; const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes }); // 全局前置守卫:拦截所有路由 router.beforeEach((to, from, next) => { // 1. 判断目标路由是否需要登录(meta.requireAuth === true) if (to.meta.requireAuth) { // 2. 从localStorage获取用户信息(或从Pinia获取) const userInfo = localStorage.getItem('userInfo'); // 3. 若有用户信息(已登录),放行 if (userInfo) { next(); } else { // 4. 若未登录,跳转到登录页,并携带“目标路由地址”(登录成功后返回原页面) next({ path: '/login', query: { redirect: to.fullPath } // to.fullPath是目标路由地址(如/order-center) }); } } else { // 目标路由无需登录,直接放行 next(); } }); export default router; - 步骤 2:登录成功后返回原页面(Login.vue):javascript运行
import { useRouter, useRoute } from 'vue-router'; import { useUserStore } from '@/store/userStore'; const router = useRouter(); const route = useRoute(); // 获取当前路由信息(包含query参数) const userStore = useUserStore(); const handleLogin = async () => { try { await userStore.login({ username: 'xxx', password: 'xxx' }); // 登录成功后,判断是否有redirect参数(即是否从需要登录的页面跳转过来) const redirect = route.query.redirect; if (redirect) { router.push(redirect); // 跳回原目标页面(如/order-center) } else { router.push('/home'); // 无redirect,跳首页 } } catch (err) { alert('登录失败'); } };
- 给需要登录的路由添加
meta.requireAuth: true标记; - 全局前置守卫拦截所有路由,判断目标路由是否需要登录;
- 未登录时跳登录页,并携带原目标路由地址,登录成功后返回原页面,提升用户体验。
- 步骤 1:路由配置(src/router/index.js):javascript运行
五、Docker 与云服务(4 题)
21. Docker 中 “镜像(Image)” 和 “容器(Container)” 的区别是什么?如何基于一个 Spring Boot 应用的 JAR 包构建 Docker 镜像(写出 Dockerfile 内容和构建命令)?
答案:
- 镜像与容器核心区别:
| 维度 | 镜像(Image) | 容器(Container) |
|---|---|---|
| 本质 | 只读的 “模板文件”(包含运行应用所需的代码、依赖、环境变量、配置) | 镜像的 “运行实例”(基于镜像创建,可读写,是独立的运行单元) |
| 状态 | 静态(无法运行,仅用于创建容器) | 动态(可启动、停止、重启、删除,运行时占用资源) |
| 生命周期 | 长期存在(除非手动删除) | 短期存在(停止后可保留,删除后数据丢失,除非挂载数据卷) |
| 关系 | 一个镜像可创建多个容器(如基于 tomcat 镜像创建 10 个 tomcat 容器) | 一个容器仅基于一个镜像创建(创建时指定镜像) |
- 基于 Spring Boot JAR 包构建 Docker 镜像:
假设 Spring Boot 应用 JAR 包名为movie-ticket.jar,路径为./target/movie-ticket.jar。- 步骤 1:编写 Dockerfile(与 JAR 包同级目录,文件名
Dockerfile,无后缀):dockerfile# 基础镜像:使用官方OpenJDK 17镜像(轻量级,适合Spring Boot 3.x) FROM openjdk:17-jdk-slim # 维护者信息(可选) MAINTAINER liusiwen <3082654005@qq.com> # 设置工作目录(容器内的目录,后续命令在此目录执行) WORKDIR /app # 将本地JAR包复制到容器的/app目录下,并重命名为app.jar(简化名称) COPY ./target/movie-ticket.jar /app/app.jar # 暴露端口(Spring Boot应用的端口,如8080,需与应用配置一致) EXPOSE 8080 # 容器启动时执行的命令:运行JAR包 # 格式:java -jar 文件名 ENTRYPOINT ["java", "-jar", "app.jar"]
关键指令说明:FROM:指定基础镜像,必须是 Dockerfile 的第一行指令;WORKDIR:设置工作目录,避免文件混乱;COPY:将本地文件复制到容器内(若本地路径是相对路径,需相对于 Dockerfile 所在目录);EXPOSE:声明容器暴露的端口(仅为文档说明,实际需docker run -p映射端口);ENTRYPOINT:容器启动时的固定命令(不可被docker run后的参数覆盖,适合固定启动逻辑)。
- 步骤 2:构建 Docker 镜像(终端执行,需在 Dockerfile 所在目录):bash
# 命令格式:docker build -t 镜像名:版本号 构建上下文路径 docker build -t movie-ticket-app:1.0 .
说明:-t movie-ticket-app:1.0:给镜像命名为movie-ticket-app,版本号为1.0(便于管理);- 最后的
.:表示 “构建上下文路径” 为当前目录(Docker 会读取该目录下的所有文件用于构建)。
- 步骤 3:验证镜像(查看构建的镜像):bash
docker images | grep movie-ticket-app # 输出示例:movie-ticket-app 1.0 abc123456789 2 minutes ago 450MB - 步骤 4:基于镜像创建并启动容器:bash
# 命令格式:docker run -d -p 宿主机端口:容器端口 --name 容器名 镜像名:版本号 docker run -d -p 8080:8080 --name movie-ticket-container movie-ticket-app:1.0
说明:-d:后台运行容器;-p 8080:8080:将宿主机的 8080 端口映射到容器的 8080 端口(外部通过宿主机 IP:8080 访问应用);--name movie-ticket-container:给容器命名,便于后续操作(如停止、删除)。
- 步骤 1:编写 Dockerfile(与 JAR 包同级目录,文件名
22. Docker Compose 的作用是什么?请基于你的 “电影购票系统”(包含 Spring Boot 后端、Vue3 前端、MySQL 数据库),写出一份 docker-compose.yml 文件(说明核心配置项)。
答案:
- Docker Compose 作用:
Docker Compose 是 Docker 官方的 “多容器编排工具”,通过一个docker-compose.yml文件定义多个容器的配置(如镜像、端口映射、网络、数据卷、依赖关系),执行一条命令即可批量创建、启动、停止所有容器,解决了 “手动逐个管理多容器” 的繁琐问题(如后端、前端、数据库需分别docker run)。 - 电影购票系统 docker-compose.yml 文件:
假设系统包含 3 个服务:mysql:MySQL 数据库(主库,版本 8.0);backend:Spring Boot 后端服务(基于movie-ticket-app:1.0镜像);frontend:Vue3 前端服务(基于movie-ticket-front:1.0镜像,通过 Nginx 部署)。
# docker-compose.yml文件版本(需与Docker Compose版本匹配,3.x是主流) version: '3.8' # 定义所有服务(services下的每个子节点是一个容器服务) services: # 1. MySQL数据库服务 mysql: # 使用的镜像(官方MySQL 8.0镜像) image: mysql:8.0 # 容器名称 container_name: movie-mysql # 环境变量(配置MySQL root密码、数据库名、字符集) environment: - MYSQL_ROOT_PASSWORD=123456 # MySQL root用户密码 - MYSQL_DATABASE=movie_ticket # 初始化的数据库名(应用需连接的库) - MYSQL_CHARSET=utf8mb4 # 数据库字符集 - MYSQL_COLLATION=utf8mb4_unicode_ci # 数据库排序规则 # 端口映射(宿主机3306端口 → 容器3306端口) ports: - "3306:3306" # 数据卷挂载(将容器内MySQL数据目录挂载到宿主机,避免容器删除后数据丢失) volumes: - mysql-data:/var/lib/mysql # 数据目录挂载 - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql # 初始化SQL脚本(可选,如创建表、插入初始数据) # 网络配置(加入自定义网络movie-network,确保服务间可通过服务名通信) networks: - movie-network # 健康检查(判断MySQL是否启动成功,避免后端服务启动时MySQL未就绪) healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"] interval: 10s # 每10秒检查一次 timeout: 5s # 检查超时时间 retries: 5 # 重试5次失败则认为服务不健康 # 2. Spring Boot后端服务 backend: # 使用的镜像(之前构建的后端镜像) image: movie-ticket-app:1.0 container_name: movie-backend # 环境变量(配置后端连接MySQL的地址、端口、数据库名、密码) environment: - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/movie_ticket?useSSL=false&serverTimezone=Asia/Shanghai - SPRING_DATASOURCE_USERNAME=root - SPRING_DATASOURCE_PASSWORD=123456 # 端口映射(宿主机8080端口 → 容器8080端口) ports: - "8080:8080" # 依赖关系(指定backend服务在mysql服务启动且健康后才启动) depends_on: mysql: condition: service_healthy # 等待mysql健康检查通过 # 网络配置(加入自定义网络) networks: - movie-network # 3. Vue3前端服务(Nginx部署) frontend: # 使用的镜像(前端打包后构建的Nginx镜像,假设已构建为movie-ticket-front:1.0) image


