关于我的简历3.3.4版本豆包给出的面试题
本文最后更新于70 天前,其中的信息可能已经过时,如有错误请发送邮件到3082654005@qq.com

(Java 开发岗)中等难度面试题及答案

一、Java 基础与面向对象(5 题)

1. 请说明 Java 中接口(Interface)和抽象类(Abstract Class)的核心区别,分别适用于什么场景?

答案

维度接口(Interface)抽象类(Abstract Class)
方法定义Java 8 + 允许默认方法(default)和静态方法,其余必须是抽象方法(无实现)可包含抽象方法(无实现)和具体方法(有实现)
成员变量只能是public static final修饰的常量可包含任意修饰符的成员变量(private、protected 等)
继承 / 实现一个类可实现多个接口(多实现)一个类只能继承一个抽象类(单继承)
实例化不能直接实例化,需通过实现类实例化不能直接实例化,需通过子类实例化
适用场景
  • 接口:需定义多类共用的 “行为规范”(如Runnable接口定义 “可运行” 行为),或实现 “多继承” 效果(如一个类同时实现ComparableSerializable);
  • 抽象类:需抽取多子类的 “共性属性和部分共性方法实现”(如AbstractList封装集合的共性遍历逻辑,子类仅需实现核心方法)。

2. HashMap 和 HashTable 的核心区别是什么?在并发场景下若需使用类似集合,推荐用什么?

答案
核心区别如下:

  1. 线程安全:HashMap 非线程安全;HashTable 通过synchronized修饰方法实现线程安全,效率低;
  2. null 键值支持:HashMap 允许key=nullvalue=null(仅一个key=null);HashTable 不允许keyvaluenull,否则抛NullPointerException
  3. 底层实现:JDK 1.8 后,HashMap 当链表长度≥8 时转为红黑树;HashTable 始终是数组 + 链表;
  4. 父类:HashMap 继承自AbstractMap;HashTable 继承自Dictionary

并发场景推荐:使用ConcurrentHashMap,它通过 “分段锁”(JDK 1.7)或 “CAS+synchronized”(JDK 1.8)实现高效并发,性能优于 HashTable。

3. Java 中 try-catch-finally 结构的执行顺序是什么?若 finally 块中包含 return 语句,会对 try 或 catch 中的 return 结果产生什么影响?

答案

  1. 基础执行顺序
    • 先执行try块代码;
    • try块无异常,执行完try后跳至finally块,再执行try后的代码;
    • try块抛异常,匹配catch块并执行,执行完catch后跳至finally块,再执行catch后的代码(若未捕获异常则不执行catch,直接执行finally后抛异常)。
  2. finally 含 return 的影响
    • finally块的return会覆盖trycatch中的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 中的 “值传递” 和 “引用传递”,并举例说明方法参数为对象时的传递机制?

答案

  • 值传递:方法接收的是参数的 “副本”(基本数据类型如intchar,或对象引用的地址副本),修改副本不会影响原参数;
  • 引用传递:方法接收的是参数的 “内存地址”,修改参数会直接影响原对象(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 中的线程池核心参数有哪些?假设你需要创建一个处理电商订单的线程池,核心参数该如何设置(说明理由)?

答案

  1. 线程池核心参数(基于ThreadPoolExecutor):
    • corePoolSize:核心线程数(线程池长期维持的线程数,即使空闲也不销毁);
    • maximumPoolSize:最大线程数(线程池允许的最大线程数,超出核心线程数的为 “非核心线程”);
    • keepAliveTime:非核心线程空闲存活时间(超过该时间则销毁);
    • workQueue:任务阻塞队列(核心线程满时,新任务暂存的队列);
    • threadFactory:线程创建工厂(自定义线程名称、优先级等);
    • handler:拒绝策略(队列满且线程数达最大时,如何处理新任务,如AbortPolicy抛异常、CallerRunsPolicy由调用者执行)。
  2. 电商订单线程池参数设置(假设日均订单 10 万,峰值 3 万 / 小时):
    • corePoolSize:8~10(日常订单量稳定,核心线程足够处理,避免频繁创建销毁);
    • maximumPoolSize:20~30(峰值时临时扩容,处理突发订单,避免队列堆积);
    • keepAliveTime:60 秒(非核心线程空闲 1 分钟后销毁,减少资源占用);
    • workQueueLinkedBlockingQueue(容量 1000,订单任务可暂存,避免瞬间峰值触发拒绝);
    • handlerCallerRunsPolicy(峰值时让主线程协助执行,避免订单丢失,优于直接抛异常)。

二、Spring Boot 与 MyBatis(6 题)

6. Spring Boot 的 “自动配置” 原理是什么?若你需要自定义一个自动配置类(如自定义 Redis 配置),步骤是什么?

答案

  1. 自动配置核心原理
    • 核心注解:@SpringBootApplication包含@EnableAutoConfiguration,该注解会扫描类路径下META-INF/spring.factories文件;
    • spring.factories中配置了大量自动配置类(如RedisAutoConfigurationDataSourceAutoConfiguration);
    • 自动配置类通过@Conditional系列注解(如@ConditionalOnClass:类存在时生效、@ConditionalOnMissingBean:Bean 不存在时生效)判断是否需要生效,最终向容器注入默认 Bean。
  2. 自定义 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

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类包含codemessagedata),便于前端处理。

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 的一级缓存和二级缓存有什么区别?如何开启二级缓存?

答案

  1. 一级缓存与二级缓存区别
维度一级缓存(本地缓存)二级缓存(全局缓存)
作用范围单个SqlSession(会话)内有效整个SqlSessionFactory内有效(跨SqlSession
开启方式默认开启,无需额外配置需手动开启(全局 + 映射文件)
存储介质内存(HashMap内存、磁盘等(可自定义,默认内存)
失效场景SqlSession关闭 / 提交 / 回滚,或执行update/delete/insert对应Mapperupdate/delete/insert执行,或缓存过期
  1. 开启二级缓存步骤
    • 步骤 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接口(二级缓存可能序列化存储)。

10. Spring Boot 项目中,如何实现 “事务管理”?若一个方法内调用多个 DAO 层方法(如扣库存 + 创建订单),如何保证这些操作要么全成功,要么全失败?

答案
Spring Boot 通过Spring Transaction实现事务管理,核心是 “声明式事务”(基于注解,简单高效),底层通过 AOP 代理实现。

  1. 实现事务管理步骤
    • 步骤 1:确保项目引入spring-boot-starter-jdbcspring-boot-starter-data-jpa依赖(自动引入事务相关组件);
    • 步骤 2:在 Spring Boot 启动类上添加@EnableTransactionManagement(Spring Boot 2.0 + 可省略,默认自动开启);
    • 步骤 3:在需要事务管理的Service 层方法上添加@Transactional注解(DAO 层或 Controller 层不推荐,因事务粒度需控制在业务逻辑层)。
  2. 保证 “扣库存 + 创建订单” 原子性
    示例代码(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. 方案 1:使用@RequestParam(适用于 URL 参数或表单参数)
    若前端通过GET请求传递user_name(如/users?user_name=张三),后端 Controller 方法可通过@RequestParam指定参数名映射:java运行@GetMapping("/users") public Result getUser(@RequestParam("user_name") String userName) { // ... 业务逻辑 }
  2. 方案 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. 方案 3:配置全局参数映射(适用于统一前缀或下划线转驼峰)
    若项目中大量存在 “下划线转驼峰” 场景(如user_nameuserName),可在 Spring Boot 配置文件中开启全局下划线转驼峰:
    • 对于 Spring MVC(接收请求参数):yamlspring: 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(数据库字段下划线转实体类驼峰):yamlmybatis: configuration: map-underscore-to-camel-case: true # 数据库字段user_name映射到实体类userName

三、MySQL 与数据库优化(5 题)

12. MySQL 中 InnoDB 和 MyISAM 两种存储引擎的核心区别是什么?你的电影购票系统中,“订单表” 和 “电影信息表” 分别适合用哪种引擎(说明理由)?

答案

  1. InnoDB 与 MyISAM 核心区别
维度InnoDBMyISAM
事务支持支持 ACID 事务(事务安全)不支持事务(非事务安全)
锁机制支持行级锁(更新时仅锁单行,并发高)、表级锁仅支持表级锁(更新时锁整个表,并发低)
外键支持支持外键约束不支持外键
索引结构聚簇索引(主键索引与数据存储在一起,查询快)非聚簇索引(索引与数据分离,查询需二次查找)
崩溃恢复支持崩溃恢复(通过 redo/undo 日志)不支持崩溃恢复(崩溃后可能丢失数据)
适用场景事务场景(如订单、支付)、高并发更新只读场景(如新闻、商品列表)、查询频繁
  1. 存储引擎选择
    • 订单表(order):适合 InnoDB;
      理由:订单表需支持事务(如创建订单时 “扣库存 + 写订单” 原子性),且存在频繁更新(如订单状态从 “待支付” 改为 “已支付”),InnoDB 的行级锁和事务支持能保证数据一致性和高并发。
    • 电影信息表(movie):适合 MyISAM;
      理由:电影信息表以 “查询” 为主(用户查看影讯、场次),更新频率低(仅管理员修改电影海报、简介),MyISAM 的表级锁对只读场景影响小,且查询性能略优于 InnoDB(非聚簇索引结构对纯查询更轻量)。

13. MySQL 索引的 “最左前缀原则” 是什么?假设有一个联合索引idx_movie_cinema_time(movie_id, cinema_id, show_time),哪些查询语句能用到该索引,哪些不能?

答案

  1. 最左前缀原则
    对于 MySQL 联合索引(多字段索引),查询条件需从索引的 “最左字段” 开始匹配,且中间不能跳过字段,否则索引会失效(仅匹配到最左前缀部分,后续字段不生效)。简单说:“索引像新华字典的目录,先按第一个字查,再按第二个字查,跳过第一个字直接查第二个字,目录无效”。
  2. 联合索引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 秒,可能的原因是什么?如何优化?

答案

  1. MySQL 读写分离实现方式
    基于 “主从复制”,主库(Master)负责写操作(insert/update/delete),从库(Slave)负责读操作(select),通过中间件或应用层路由实现分离:
    • 中间件方式(推荐):使用 MyCat、Sharding-JDBC 等中间件,中间件自动将写请求转发到主库,读请求转发到从库(支持多从库负载均衡);
    • 应用层方式:在代码中通过注解或配置指定读写路由(如 Spring 的@Transactional(readOnly = true)指定读从库,写操作默认走主库)。
  2. 从库查询延迟超 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_idcreate_time),避免全表扫描

你项目中的优化:结合简历中 “调整 MySQL 参数将主从同步延迟优化至 1s 内”,可重点提及:

  • 调整主库innodb_flush_log_at_trx_commit=1sync_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条

关键说明

  1. order是 MySQL 关键字(如ORDER BY),作为表名时需用反引号(`)包裹,避免语法错误;
  2. create_time的范围查询用BETWEEN ... AND ...,需指定完整时间(如2024-05-07 23:59:59),避免漏查 5 月 7 日当天的订单;
  3. 字段别名用AS(可省略),让结果更易读,符合前端展示需求。

16. MySQL 中 “慢查询日志” 的作用是什么?如何开启慢查询日志?若你的电影购票系统中出现一条慢查询(如查询订单列表耗时 5 秒),如何排查和优化?

答案

  1. 慢查询日志作用
    记录 MySQL 中 “执行时间超过指定阈值” 的 SQL 语句(默认阈值是 10 秒),用于定位低效查询,是优化 MySQL 性能的核心工具。
  2. 开启慢查询日志步骤
    • 步骤 1:查看当前配置(登录 MySQL 终端):sqlshow variables like '%slow_query_log%'; -- 查看慢查询日志是否开启 show variables like '%long_query_time%'; -- 查看慢查询阈值(默认10秒)
    • 步骤 2:临时开启(重启 MySQL 后失效):sqlset 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.cnfmy.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)。
  3. 慢查询(订单列表耗时 5 秒)排查与优化
    • 排查步骤
      1. 查看慢查询日志:cat /var/lib/mysql/slow.log,找到耗时 5 秒的订单查询 SQL;
      2. EXPLAIN分析 SQL 执行计划:sqlEXPLAIN SELECT * FROM `order` WHERE user_id = 123 ORDER BY create_time DESC; -- 假设慢查询SQL是这个
        通过EXPLAIN查看:
        • type列:若为ALL(全表扫描),说明无索引;
        • key列:若为NULL,说明未使用索引;
        • rows列:若数值很大,说明扫描行数过多。
    • 优化方案
      1. 添加索引:若user_id是查询条件,create_time是排序字段,添加联合索引idx_user_create(user_id, create_time):sqlCREATE INDEX idx_user_create ON `order`(user_id, create_time);
        理由:联合索引覆盖 “查询条件 + 排序字段”,避免全表扫描和文件排序(Using filesort);
      2. 优化 SQL:避免SELECT *,只查询需要的字段(如user_id, order_id, pay_amount),减少数据传输量;
      3. 分页查询:若订单列表无分页,添加LIMIT(如LIMIT 0,20),避免一次性查询大量数据;
      4. 数据归档:若订单表数据量过大(如超过 100 万条),将历史订单(如 1 年前)归档到历史表,减少主表数据量。

四、Vue3 与前端(4 题)

17. Vue3 中 Composition API 相比 Options API 的核心优势是什么?你在电影购票系统的前端开发中,如何用 Composition API 实现 “电影列表查询” 功能(举例代码)?

答案

  1. Composition API vs Options API 核心优势
维度Options API(Vue2)Composition API(Vue3)
代码组织按选项(data、methods、computed)拆分,大组件代码分散(如数据在 data,方法在 methods)按 “逻辑功能” 拆分(如 “电影列表查询” 相关的变量、方法、生命周期写在一起),代码聚合度高
逻辑复用依赖 mixins,但存在 “命名冲突”“来源不明确” 问题用 “组合函数”(如useMovieList())复用逻辑,无命名冲突,来源清晰
类型支持对 TypeScript 支持差(需额外配置)原生支持 TypeScript,类型推导更精准,减少类型错误
生命周期需在beforeMountmounted等选项中写代码,分散onMounted等函数,可在同一逻辑块中写生命周期,更灵活
  1. 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 存储和获取 “用户登录状态”?

答案

  1. Pinia 作用
    Pinia 是 Vue3 官方推荐的状态管理库,用于集中管理跨组件共享的状态(如用户登录状态、购物车数据),替代 Vue2 中的 Vuex,解决了 Vuex 的复杂性问题。
  2. Pinia vs Vuex 简化点:维度Vuex(Vue2)Pinia(Vue3)模块划分需通过modules划分模块,复杂模块需namespace: true开启命名空间无需modules,每个 Store 实例就是一个模块,天然支持命名空间状态修改需通过mutations修改状态(同步)、actions处理异步,mutations不可省略无mutations,直接在actions中修改状态(同步 / 异步均可),简化代码TypeScript 支持支持差,需额外写类型声明文件(如xxx.d.ts)原生支持 TypeScript,类型推导自动完成,无需额外配置API 复杂度API 多(commitdispatchmapState等)API 简洁(defineStorestoreToRefs等),学习成本低调试工具依赖 Vue Devtools 的 Vuex 面板原生集成到 Vue Devtools,调试更直观
  3. 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>

19. Vue3 中v-model的使用方式与 Vue2 有什么区别?如何用v-model实现一个 “自定义搜索框组件”(支持双向绑定搜索关键词)?

答案

  1. 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
      1. 本质是 “v-bind:modelValue + v-on:update:modelValue” 的语法糖,支持自定义modelValue名称(如v-model:keyword="searchKey");
      2. 一个组件可支持多个v-model(如v-model:keyword="searchKey"v-model:date="searchDate"),无需sync修饰符;
      3. 支持修饰符(如v-model.trim去除空格、v-model.number转为数字),且可自定义修饰符。
  2. 自定义搜索框组件(支持双向绑定)示例
    需求:创建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事件,父组件监听并执行搜索逻辑,实现 “按钮点击搜索” 功能。

20. Vue3 项目中如何实现 “路由跳转” 和 “路由守卫”?你在电影购票系统中,如何用路由守卫实现 “未登录用户禁止访问订单中心”?

答案

  1. Vue3 路由跳转方式
    Vue3 中使用vue-router 4.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'); };
  2. Vue3 路由守卫类型
    路由守卫用于控制路由访问权限(如登录验证、权限校验),分为 3 类:
    • 全局守卫:作用于所有路由(如router.beforeEach前置守卫、router.afterEach后置守卫);
    • 路由独享守卫:作用于单个路由(在路由配置中定义beforeEnter);
    • 组件内守卫:作用于当前组件(如onBeforeRouteEnteronBeforeRouteLeave)。
  3. 路由守卫实现 “未登录禁止访问订单中心”
    采用 “全局前置守卫”(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标记;
    • 全局前置守卫拦截所有路由,判断目标路由是否需要登录;
    • 未登录时跳登录页,并携带原目标路由地址,登录成功后返回原页面,提升用户体验。

五、Docker 与云服务(4 题)

21. Docker 中 “镜像(Image)” 和 “容器(Container)” 的区别是什么?如何基于一个 Spring Boot 应用的 JAR 包构建 Docker 镜像(写出 Dockerfile 内容和构建命令)?

答案

  1. 镜像与容器核心区别
维度镜像(Image)容器(Container)
本质只读的 “模板文件”(包含运行应用所需的代码、依赖、环境变量、配置)镜像的 “运行实例”(基于镜像创建,可读写,是独立的运行单元)
状态静态(无法运行,仅用于创建容器)动态(可启动、停止、重启、删除,运行时占用资源)
生命周期长期存在(除非手动删除)短期存在(停止后可保留,删除后数据丢失,除非挂载数据卷)
关系一个镜像可创建多个容器(如基于 tomcat 镜像创建 10 个 tomcat 容器)一个容器仅基于一个镜像创建(创建时指定镜像)
  1. 基于 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:验证镜像(查看构建的镜像):bashdocker 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:给容器命名,便于后续操作(如停止、删除)。

22. Docker Compose 的作用是什么?请基于你的 “电影购票系统”(包含 Spring Boot 后端、Vue3 前端、MySQL 数据库),写出一份 docker-compose.yml 文件(说明核心配置项)。

答案

  1. Docker Compose 作用
    Docker Compose 是 Docker 官方的 “多容器编排工具”,通过一个docker-compose.yml文件定义多个容器的配置(如镜像、端口映射、网络、数据卷、依赖关系),执行一条命令即可批量创建、启动、停止所有容器,解决了 “手动逐个管理多容器” 的繁琐问题(如后端、前端、数据库需分别docker run)。
  2. 电影购票系统 docker-compose.yml 文件
    假设系统包含 3 个服务:
    • mysql:MySQL 数据库(主库,版本 8.0);
    • backend:Spring Boot 后端服务(基于movie-ticket-app:1.0镜像);
    • frontend:Vue3 前端服务(基于movie-ticket-front:1.0镜像,通过 Nginx 部署)。
    yaml# 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
文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇