某外包,主要针对简历提问

技术面

1.自我介绍

姓名+学校+专业,实习经历的项目简单介绍+负责工作的业务和技术简单介绍,个人优势(与JD匹配度)

2.实习项目及负责工作介绍

(1)AOP定义,实现过程,原理,用处,几种机制?

定义:AOP(面向切面编程)将哪些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性。

将横切关注点(日志、事务、权限、监控)从业务逻辑中剥离,提高复用性和可维护性。

实现过程:定义切面(@Aspect)→ 定义切点(@Pointcut)→ 定义通知(@Before/@After/@Around等)。

原理:基于动态代理。

  • 被代理类有接口,使用JDK动态代理,生产接口的代理类
  • 被代理类无皆苦,使用CGLIB,生成子类字节码

几种机制(通知类型):@Before@After@AfterReturning@AfterThrowing@Around(最强大,可控制是否执行目标方法)。

Spring AOP的实现过程

(a)容器在创建UserService的Bean时,发现它被切面匹配到了(比如切点表达式execution(* saveUser(..)))。

(b)不直接返回UserService对象,而是用动态代理包装它。

  • 有接口 → JDK动态代理(InvocationHandler + 反射调用目标方法)

  • 无接口 → CGLIB(生成子类,也是用反射调用父类方法)

(c)调用userService.saveUser()时,实际调用的是代理对象的方法,代理内部会在调用目标方法的前后执行增强逻辑(通过反射调用@Before@Around等方法)。

进阶复习:AspectJ(编译时织入) vs Spring AOP(运行时织入,性能稍低但更方便)。

(2)注解的核心机制 。IOC?

注解是一个继承了java.lang.annotation.Annotation的接口,通过反射读取元数据。

注解只是标记,本身没有逻辑。注解需要配合“解析器”才有意义,解析器通常依赖反射来读取注解。

IoC(控制反转):把对象的创建和依赖关系的管理交给Spring容器,而不是由对象自己new。

  • 核心机制:利用反射+注解 实现容器

    • @Component / @Service / @Controller → 扫描后注册到BeanDefinitionMap

    • @Autowired / @Resource → 容器通过反射注入依赖(前者按类型,后者按名称)。

    • 底层:BeanFactory / ApplicationContext 负责实例化、属性填充、初始化、销毁。

Spring IoC容器启动时做的事情:

  • 扫描指定包下所有类,通过反射读取类上的注解(@Component@Service等);
  • 对于每个带注解的类,通过反射实例化对象(clazz.getDeclaredConstructor().newInstance());
  • 再通过反射扫描每个对象内部的字段,如果字段上有@Autowired,就通过反射把对应的依赖对象注入进去

(3)反射。功能增强?

反射:运行时获取类的构造器、方法、字段(即使private也能拿到),并动态创建对象(clazz.newInstance())、调用方法(method.invoke(obj, args))、读写字段。

功能增强的列子:

        AOP——通过反射调用目标方法,并在前后插入增强逻辑。

        注解处理——扫描类上的注解,根据注解值动态生成Bean或改变行为。

        ORM框架——根据实体类字段反射生成SQL并赋值。

对于IoC,AOP,反射,注解之间的关系还是有点混乱。

反射是底层能力(运行时窥探/操作类);

注解是元数据标记(贴在类/方法上的标签);

IoC利用反射+注解实现对象的创建与依赖注入;

AOP在IoC基础上,利用动态代理(底层也是反射)对Bean的方法进行增强。

(4)定时任务的用处,怎么实现的?

定时任务:是让代码在某个具体时间/每隔固定时间/周期性执行。

实现:Spring Boot注解 @Schedule。启动类加 @EnableScheduling,任务方法加 @Scheduled。

import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
 
@EnableScheduling // 开启定时任务
@Component
public class SpringScheduledDemo {

    // 1. 固定间隔:每 5 秒执行一次
    @Scheduled(fixedRate = 5000)
    public void task1() {
        System.out.println("固定间隔任务");
    }

    // 2. Cron 表达式:每天凌晨 0 点执行
    @Scheduled(cron = "0 0 0 * * ?")
    public void task2() {
        System.out.println("每日凌晨执行任务");
    }
}

// Cron 表达式(最常用):秒 分 时 日 月 周 年(可选)
// 优点:简单、无依赖、整合Spring、支持cron
// 缺点:不支持分布式(集群部署会重复执行)

底层:

        线程休眠+轮询:后台开一个独立线程,循环判断时间,到点就执行任务

        时间轮算法(高级方案):用环形队列管理任务,高效调度大量定时任务

项目是单体还是分布式?

单体。

我们是部署了几套,生产环境有好几套,如果有一套有问题,或者说有一套的一个节点能处理的请求是有限的,多几个节点可以处理更多的用户请求。如果多节点,怎么确保定时任务的一致性——分布式定时任务调度?

多节点问题:所有节点同时执行同一任务,导致数据重复处理、资源竞争。

解决方案:

        分布式锁:如Redis的SET NX EX,抢到锁的节点才执行

        分布式调度框架:XXL-JOBElastic-JobSchedulerX。它们通过数据库/ ZK / Raft选举主节点,只有主节点执行定时任务。

        @Scheduled+@RedisLock自定义,配合Redission实现看门狗自动续期。

(5)回收物品是可以退款到微信支付吗,可以讲讲微信支付怎么实现吗?

  • 微信支付基本流程:统一下单--支付--回调--退款

  • 退款实现关键点

    1. 调用微信支付退款APIsecapi/pay/refund),需要商户证书(双向SSL)。

    2. 参数:out_trade_no(商户订单号)、out_refund_no(退款单号,唯一)、refund_feetotal_fee

    3. 签名:构造参数 → 按字典序排序 → MD5/HMAC-SHA256 → 放入请求。

    4. 微信会异步发送退款结果通知(可选),但退款通常不需要等待回调,可先更新本地退款状态为“处理中”,然后通过主动查询或回调确认。

  • 安全注意:退款涉及资金,必须做幂等处理(同一退款单号多次调用只能退一次),用数据库唯一约束或分布式锁。

(6)redis分布式锁保证幂等性,用在哪里,怎么用,锁的时间怎么设置,大概几秒,如果过期业务还没执行完怎么办(Redission看门狗机制自动续期)

是用java自带的锁(有哪些 怎么用)还是用redis锁(怎么用)。单体用自带锁就可以。

  • 用在哪里:如退款接口下单接口MQ消费端,防止重复提交/重复处理。

  • 怎么用

    String lockKey = "lock:refund:" + refundNo;
    Boolean locked = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, "1", Duration.ofSeconds(30));
    if (Boolean.TRUE.equals(locked)) {
        try {
            // 业务逻辑
        } finally {
            redisTemplate.delete(lockKey);
        }
    }
  • 锁时间设置:一般比业务平均执行时间长一点,比如10~30秒。但无法绝对避免业务超时。

  • 过期业务未完成怎么办

    • 错误做法:锁时间设很长(影响性能,死锁风险)。

    • 正确做法:用Redisson的看门狗机制(默认30秒,业务未完成自动续期,每lockWatchdogTimeout/3检查一次)。

    • 手动续期:用while循环判断剩余时间,接近过期时expire延长。

  • 与Java自带锁对比

    • 单体应用:synchronizedReentrantLock(JVM级,无法跨进程)。

    • 分布式应用:必须用Redis锁ZooKeeper锁。(Redis锁不是100%安全的(主从切换可能丢锁),极高要求可用RedLock或ZooKeeper)

(7)如果做的模块发展性能有问题,要优化性能,有什么思路?

a. 排查瓶颈:先压测(JMeter)+ APM(SkyWalking/Pinpoint)+ 日志分析,定位是CPU、内存、IO还是数据库慢。

b. 数据库优化

                慢SQL → 加索引、改写SQL、分页优化。

                热点数据 → 加Redis缓存(注意穿透/击穿/雪崩)。

                读写分离、分库分表。

c. 代码层面:减少不必要循环、批量操作(如批量insert)。使用线程池处理异步任务。避免大对象频繁GC。

d. 架构层面:加消息队列削峰(如RabbitMQ/Kafka)。 静态资源走CDN。  微服务拆分、水平扩容。

e. JVM调优:调整堆内存、GC算法(如G1)、元空间等。

3.个人项目

(1)分表一般怎么分,分表策略,在项目中你怎么分的?

  • 为什么分表:单表数据量过大(超过500万~1000万)时,索引效率下降,写入锁竞争严重。分表后要解决跨表查询、全局ID生成(雪花算法)等问题。

  • 分表策略

    • 垂直分表:将大字段或访问频率低的列 拆分到另一张表。

    • 水平分表:按某种规则 将行分散到多张结构相同的表。

      • 常用策略:取模user_id % 8)、范围(按时间/ID区间)、哈希(一致性哈希)。

      • 选择依据:业务查询多数带哪个字段(如订单按user_id分)。

项目中我用的不太对,不应该叫分表,简历要改。

扩展:

雪花算法(Snowflake)一套分布式全局唯一ID生成算法,用来在分布式系统里生成不重复、趋势递增、纯数字的ID,是Java后端、微服务里最常用的主键ID方案。

在分布式、多节点、高并发场景下,数据库自增ID——集群多台机器会重复;UUID——太长、无序、索引性能差、字符串存储浪费空间;号段模式——需要额外维护发号器(一个专门发编号的服务)。

雪花算法:全局唯一不重复;趋势递增,数据库索引友好;纯数字,长度固定,性能好;不依赖数据库,本地生成,高并发;可反向解析出 时间、机器、序列号。

缺点:强依赖机器时钟——服务器时间回拨会导致ID重复或生成失败,实际项目会拒绝生成,等待时间追上,改用改良算法;不是绝对连续,只是趋势递增。

使用场景:分布式系统订单 ID,用户 ID、商品 ID。日志 ID、消息 ID。高并发表主键。微服务之间唯一标识。Long id = IdWorker.getId();//框架内置了方法

标准的雪花算法生成一个时间+机器+序列号 拼成的 64bit的long型数字

(2)openclaw有没有玩过,项目中这个AI怎么用的

了解过,没玩过。

  • 通过HTTP API,调用本地大模型的接口。

  • 调用方式:RestTemplate 或 WebClient 发送POST请求,携带用户问题,模型返回答案。

  • 如果需要流式输出(打字机效果),使用SseEmitter(Spring)或WebSocket。

4.八股

(1)java内存模型一般分什么区域?

  • 堆(Heap):所有对象实例、数组,线程共享,GC主要区域。分新生代和老年代。

  • 方法区 / 元空间(Metaspace / PermGen):类信息、常量、静态变量、JIT编译后的代码。JDK8后改为元空间(本地内存)。

  • 虚拟机栈(VM Stack):每个方法一个栈帧(局部变量表、操作数栈、动态链接、返回地址),线程私有。

  • 本地方法栈native方法。

  • 程序计数器:当前线程执行的字节码行号,线程私有。

(2)垃圾回收器,垃圾回收机制,jdk不同垃圾回收不同,项目用的什么版本的jdk?

  • 垃圾回收机制:可达性分析(GC Roots)→ 标记不可达对象 → 回收(标记清除/复制/标记整理)。

  • 常见垃圾回收器

    • Serial / ParNew / Parallel Scavenge(年轻代)

    • CMS / Serial Old / Parallel Old(老年代)

    • G1(JDK9+默认):分区+可预测停顿。

    • ZGC(JDK11+):低延迟(<10ms),适合大堆。

    • Shenandoah

  • 项目用的JDK版本:比如JDK 8 → 默认用Parallel Scavenge + Parallel Old;JDK 11 → 默认G1。

(3)线程的创建方式。线程池参数,线程的创建与销毁。线程池的好处,逻辑?

线程池是一种用于管理和复用线程的机制,它允许在应用程序中创建一组可用线程,并在需要执行

任务时将任务分配给这些线程。 优化线程的创建、销毁和管理,以提高多线程应用程序的性能和效率。

  • 创建方式

    1. 继承Thread,重写run()

    2. 实现Runnable,传给Thread

    3. 实现Callable + FutureTask(有返回值,JDK>=1.5)。

    4. 线程池(推荐)。

>>>>>>>>>>>>>>>>>

自定义线程池的核心参数如下:

1. 核心线程数(corePoolSize):线程池中一直保持活动的线程数。可以使用corePoolSize方法

来设置。一般情况下,可以根据系统的资源情况和任务的特性来设置合适的值。

2. 最大线程数(maximumPoolSize):线程池中允许存在的最大线程数。可以使用

maximumPoolSize方法来设置。如果所有线程都处于活动状态,而此时又有新的任务提交,

线程池会创建新的线程,直到达到最大线程数。

3. 空闲线程存活时间(keepAliveTime):当线程池中的线程数量超过核心线程数时,如果这些

线程在一定时间内没有执行任务,则这些线程会被销毁。可以使用keepAliveTime和TimeUnit

方法来设置。

4. 阻塞队列(workQueue):用于存放等待执行的任务的阻塞队列。可以根据任务的特性选择

不同类型的队列,如LinkedBlockingQueue、ArrayBlockingQueue等。默认情况下,使用无

界阻塞队列,即LinkedBlockingQueue,但也可以根据需要设置有界队列。

5. 线程工厂(threadFactory):用于创建线程的工厂。可以通过实现ThreadFactory接口自定义

线程的创建逻辑。

6. 拒绝策略(rejectedExecutionHandler):当线程池无法接受新的任务时,会根据设置的拒绝

策略进行处理。常见的拒绝策略有 AbortPolicy、DiscardPolicy、DiscardOldestPolicy 和

CallerRunsPolicy。

线程池提交流程:

a.提交任务,判断是否达到核心线程数,没有则新建线程执行任务。

b.达到核心线程数,尝试放入任务队列。

入队成功后,如果工作线程数为 0,会再创建一个非核心线程来消费队列任务

c.队列满,判断是否达到最大线程数,没有则新建线程执行任务。

d.达到最大线程数,执行拒绝策略。

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

  • 线程创建与销毁

    • 新任务提交时 → 核心线程数未满 → 新建线程。

    • 队列满了 → 线程数<最大线程数 → 继续新建。

    • 超出最大线程数 → 执行拒绝策略。

    • 空闲线程超过keepAliveTime且大于核心数 → 销毁。

  • 线程池的好处

    • 减少线程创建销毁开销(复用)。

    • 控制资源(限制并发数)。

    • 提供定时、监控等管理能力。

5.反问:

1.想了解公司最小的一个技术团队的规模,负责的业务大概到哪里

3.项目迭代周期

2.对我有什么建议

hr面:

1.个人规划(纯技术还是技术管理),希望可以成长到什么

2.之前实习最大的收获

3.选择工作最注重的点

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐