Java 框架面试题(MyBatis / Spring / SpringMVC / SpringBoot / SpringCloud)
一、MyBatis
1. MyBatis 的组件和原理
问题: MyBatis 的核心组件有哪些?它的工作原理是什么?
答案:
MyBatis 的核心组件包括:
| 组件 | 作用 |
|---|---|
| SqlSessionFactory | 创建 SqlSession 的工厂,通过配置文件或 Configuration 对象构建 |
| SqlSession | 面向用户的 API 入口,负责发送 SQL、获取映射结果、管理事务 |
| Configuration | MyBatis 的全局配置信息,包含映射器、插件、缓存等所有配置 |
| Mapper 接口 | 数据访问接口,通过动态代理生成实现类 |
| MappedStatement | 一条 SQL 语句的完整封装,包括 SQL、入参映射、结果映射 |
| Executor | SQL 执行器(Simple/Reuse/Batch),负责实际执行 SQL 和缓存管理 |
| StatementHandler | 负责操作 Statement 对象,设置参数、执行 SQL |
| ParameterHandler | 负责将 Java 参数设置为 PreparedStatement 的入参 |
| ResultSetHandler | 负责将 ResultSet 结果集映射为 Java 对象 |
| TypeHandler | Java 类型与 JDBC 类型之间的转换器 |
| Plugin / Interceptor | 插件机制,可拦截四大对象(Executor、StatementHandler、ParameterHandler、ResultSetHandler)的方法调用 |
工作原理:
- 读取
mybatis-config.xml或application.yml配置,构建Configuration对象 - 由
SqlSessionFactoryBuilder创建SqlSessionFactory - 通过
SqlSessionFactory创建SqlSession - 获取 Mapper 接口的代理对象(JDK 动态代理)
- 执行 SQL 时,代理对象调用
Executor Executor依次调用StatementHandler→ParameterHandler→ 执行 SQL →ResultSetHandler映射结果- 整个流程中可通过 插件(Interceptor) 对四大对象进行拦截增强
2. MyBatis 的多级缓存
问题: MyBatis 的多级缓存是怎样的?二级缓存和一级缓存有什么区别?
答案:
MyBatis 内置了两级缓存机制:
一级缓存(Local Cache / Session 级缓存):
- 默认开启,作用域为
SqlSession - 在同一个 SqlSession 中相同的查询会直接走缓存
- 增删改操作会清空该 SqlSession 的一级缓存
- 不同的 SqlSession 之间缓存互不共享
二级缓存(Namespace 级缓存):
- 默认关闭,需要在 mapper XML 中配置
<cache/>或在接口上使用@CacheNamespace注解 - 作用域为 Mapper 的 Namespace,跨 SqlSession 共享
- 数据查询时,SqlSession 关闭或提交后数据才会写入二级缓存
- 增删改操作会清空该 Namespace 下的所有二级缓存
- 实体类必须实现
Serializable接口(因为二级缓存可能存储到磁盘)
注意: MyBatis 没有"三级缓存"的概念。两级的查询顺序:二级缓存 → 一级缓存 → 数据库。
3. Mybatis 和 MybatisPlus 的区别
问题: MyBatis 和 MyBatisPlus 有什么区别?
答案:
| 对比项 | MyBatis | MyBatis-Plus(MP) |
|---|---|---|
| 定位 | ORM 框架 | MyBatis 增强工具 |
| CRUD | 需手写 XML 或注解 SQL | 内置通用 Mapper/Service,零 SQL 实现 CRUD |
| 条件构造器 | 手写 SQL 拼接条件 | 提供 Wrapper(如 QueryWrapper、LambdaQueryWrapper)链式查询 |
| 分页 | 需手动写分页 SQL 或插件 | 内置 PaginationInterceptor 分页插件 |
| 代码生成 | 无 | 内置代码生成器(AutoGenerator),可生成 Entity/Mapper/Service/Controller |
| 逻辑删除 | 需自行实现 | 内置 @TableLogic 注解支持 |
| 自动填充 | 无 | 内置 MetaObjectHandler,创建时间/更新时间自动填充 |
| 乐观锁 | 无 | 内置 @Version 注解 + OptimisticLockerPlugin |
| 性能 | — | 性能分析插件、SQL 执行分析等 |
| 兼容性 | — | 完全兼容 MyBatis,原有代码无需修改 |
总结: MyBatis-Plus 只做增强不做改变,在 MyBatis 基础上提供了大量开箱即用的功能,减少重复的 CRUD 编写。
4. MyBatis 里 $ 和 # 有什么区别?
问题: MyBatis 中 ${} 和 #{} 有什么区别?
答案:
| 对比项 | #{} | ${} |
|---|---|---|
| 处理方式 | 预编译处理,使用 PreparedStatement 的参数占位符 ? | 字符串拼接,直接替换到 SQL 中 |
| 安全性 | ✅ 防 SQL 注入 | ❌ 存在 SQL 注入风险 |
| 参数类型 | 适用于所有类型的参数 | 适用于表名、列名、order by 等不能预编译的场景 |
| 底层实现 | PreparedStatement.setXXX() | 直接拼接字符串 |
| 单引号 | 自动加单引号(字符串类型) | 不会自动加单引号 |
<!-- #{} 示例:预编译参数 -->
SELECT * FROM user WHERE id = #{id}
-- 实际执行:SELECT * FROM user WHERE id = ?
-- ${} 示例:字符串替换(注意 SQL 注入风险)
SELECT * FROM user ORDER BY ${columnName}
-- 实际执行:SELECT * FROM user ORDER BY name
最佳实践: 能用
#{}就用#{},只有在需要动态传入表名、列名、排序字段时才用${}。
5. MyBatis 里 ResultMap 和 ResultType 有什么区别?
问题: MyBatis 中 resultMap 和 resultType 有什么区别?
答案:
| 对比项 | resultType | resultMap |
|---|---|---|
| 映射方式 | 自动映射(字段名与属性名一致) | 手动定义映射规则 |
| 适用场景 | 数据库列名与 Java 属性名一致(或开启驼峰映射) | 列名与属性名不一致、需要复杂映射 |
| 嵌套映射 | 不支持复杂嵌套 | 支持一对一(association)、一对多(collection)嵌套映射 |
| 灵活性 | 简单方便 | 高度灵活可控 |
<!-- resultType:字段名与属性名一致时直接使用 -->
<select id="getUserById" resultType="com.example.User">
SELECT id, name, age FROM user WHERE id = #{id}
</select>
<!-- resultMap:自定义映射规则 -->
<resultMap id="userResultMap" type="com.example.User">
<id property="userId" column="id"/>
<result property="userName" column="name"/>
<result property="userAge" column="age"/>
<!-- 一对一关联 -->
<association property="dept" javaType="com.example.Dept">
<id property="deptId" column="dept_id"/>
<result property="deptName" column="dept_name"/>
</association>
</resultMap>
6. MyBatis 里动态标签有哪些?
问题: MyBatis 的动态 SQL 标签有哪些?
答案:
| 标签 | 作用 |
|---|---|
<if> | 条件判断,满足条件时拼接 SQL 片段 |
<choose>/<when>/<otherwise> | 类似 Java 的 switch-case,多条件分支选择 |
<where> | 智能处理 WHERE 关键字,自动去除多余的前缀 AND/OR |
<set> | 智能处理 UPDATE 的 SET 子句,自动去除多余的逗号 |
<trim> | 通用前后缀处理,可自定义前缀、后缀及去除内容 |
<foreach> | 遍历集合,用于 IN 查询、批量插入等场景 |
<sql>/<include> | 定义可复用的 SQL 片段,通过 include 引入 |
<bind> | 创建变量并绑定到上下文中 |
<!-- if 标签示例 -->
<select id="queryUser" resultType="User">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
<!-- foreach 标签示例 -->
<select id="getByIds" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
7. 为什么要使用 MyBatisPlus?
问题: 为什么要使用 MyBatisPlus?
答案:
- 简化 CRUD:通用 Mapper/Service 封装了单表 CRUD,无需编写 XML,极大减少重复代码
- 条件构造器:
Wrapper提供链式编程风格,代替手写 SQL 条件拼接,代码更优雅 - 内置分页:
PaginationInnerInterceptor一键实现物理分页,无需手写分页 SQL - 代码生成器:AutoGenerator 根据数据库表自动生成 Entity、Mapper、Service、Controller 代码
- 逻辑删除:
@TableLogic注解实现逻辑删除,查询时自动过滤已删除数据 - 自动填充:创建时间、修改时间等公共字段自动填充,避免重复编码
- 乐观锁:
@Version注解轻松实现并发控制 - 多租户插件:通过 TenantLineInnerInterceptor 实现多租户数据隔离
- 完全兼容:在 MyBatis 基础上增强,原有 MyBatis 代码和配置无需任何修改
- Lambda 支持:
LambdaQueryWrapper避免字段名硬编码,编译期检查,减少错误
二、Spring
8. Spring 中 Bean 的注入方式有几种?
问题: Spring 中 Bean 的注入方式有几种?
答案:
| 注入方式 | 说明 | 示例 |
|---|---|---|
| 构造器注入(推荐) | 通过构造函数注入,依赖不可变,适合必需依赖 | @Autowired 放在构造器上 |
| Setter 注入 | 通过 setter 方法注入,适合可选依赖 | @Autowired 放在 setter 方法上 |
| 字段注入 | 直接在属性上注解,使用最简单但 Spring 不推荐 | @Autowired 放在字段上 |
| @Bean 方法参数注入 | 在 @Bean 方法中通过参数注入 | @Bean 方法参数列表自动注入 |
// 1. 构造器注入(Spring 推荐)
@Service
public class UserService {
private final UserDao userDao;
@Autowired
public UserService(UserDao userDao) {
this.userDao = userDao;
}
}
// 2. Setter 注入
@Service
public class UserService {
private UserDao userDao;
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
// 3. 字段注入(简洁但不推荐)
@Service
public class UserService {
@Autowired
private UserDao userDao;
}
最佳实践: 优先使用构造器注入,保证依赖不可变且方便测试。
9. Spring 中 Bean 的作用域有几种?
问题: Spring 中 Bean 的作用域有几种?
答案:
| 作用域 | 说明 |
|---|---|
| singleton(默认) | 整个 IoC 容器中只有一个实例 |
| prototype | 每次请求都创建一个新实例 |
| request | 每个 HTTP 请求创建一个实例(仅 Web 应用) |
| session | 每个 HTTP Session 创建一个实例(仅 Web 应用) |
| application | 每个 ServletContext 创建一个实例(仅 Web 应用) |
| websocket | 每个 WebSocket 创建一个实例(仅 Web 应用) |
注意:
singleton作用域的 Bean 不会在容器启动时就创建(默认延迟加载),除非设置lazy-init=false或使用了@DependsOn。
10. Spring 常用注解有哪些?
问题: Spring 常用注解有哪些?
答案:
Bean 声明注解:
| 注解 | 作用 |
|---|---|
@Component | 通用组件,标注为一个 Spring Bean |
@Controller | Web 层控制器 |
@Service | 业务逻辑层 |
@Repository | 数据访问层(DAO),支持异常转换 |
@Configuration | 配置类,相当于 XML 配置文件 |
@Bean | 在 @Configuration 类中,方法返回的对象注册为 Bean |
依赖注入注解:
| 注解 | 作用 |
|---|---|
@Autowired | Spring 提供的自动注入(按类型) |
@Resource | JSR-250 提供(默认按名称,再按类型) |
@Value | 注入配置文件中的属性值 |
配置注解:
| 注解 | 作用 |
|---|---|
@Configuration | 标记配置类 |
@ComponentScan | 组件扫描路径 |
@PropertySource | 加载外部配置文件 |
@Import | 导入其他配置类 |
@Profile | 根据环境激活不同的配置 |
@Conditional | 条件化装配 Bean |
其他常用注解:
| 注解 | 作用 |
|---|---|
@Lazy | 延迟加载 |
@Scope | 指定 Bean 作用域 |
@Primary | 当多个同类型 Bean 存在时,指定优先注入 |
@PostConstruct | Bean 初始化后执行的方法 |
@PreDestroy | Bean 销毁前执行的方法 |
@Transactional | 声明式事务 |
11. @Component 和 @Bean 注解的区别?
问题: @Component 和 @Bean 注解有什么区别?
答案:
| 对比项 | @Component | @Bean |
|---|---|---|
| 使用位置 | 标注在类上 | 标注在方法上 |
| 适用场景 | 自己编写的类(可以修改源码) | 第三方库的类(无法修改源码) |
| 控制方式 | Spring 自动扫描和实例化 | 开发者手动创建和配置 Bean |
| 自定义程度 | 较低,类本身决定 | 较高,可以在方法中进行复杂的初始化逻辑 |
| 配合使用 | 通常配合 @ComponentScan | 通常放在 @Configuration 类中 |
// @Component:用于自己写的类
@Component
public class MyService { ... }
// @Bean:用于第三方库的类或需要复杂初始化的场景
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
public DataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
ds.setUsername("root");
return ds;
}
}
12. AOP 的实现原理?
问题: AOP 的实现原理是什么?
答案:
AOP(面向切面编程)的底层实现主要依赖动态代理:
1. JDK 动态代理(基于接口):
- 目标类必须实现接口
- 使用
java.lang.reflect.Proxy创建代理对象 - 通过
InvocationHandler的invoke()方法拦截方法调用 - Spring 默认优先使用 JDK 动态代理
2. CGLIB 动态代理(基于继承):
- 目标类不需要实现接口
- 通过生成目标类的子类来实现代理
- 使用
Enhancer类和MethodInterceptor接口 - Spring Boot 2.x 默认使用 CGLIB 代理(
spring.aop.proxy-target-class=true)
AOP 核心概念:
| 概念 | 说明 |
|---|---|
| Aspect(切面) | 横切关注点的模块化,如日志、事务等 |
| JoinPoint(连接点) | 程序执行中的一个点,如方法调用 |
| Pointcut(切入点) | 匹配连接点的表达式,决定哪些方法被增强 |
| Advice(通知) | 在切入点执行的增强逻辑 |
| Target(目标对象) | 被代理的原始对象 |
| Weaving(织入) | 将切面应用到目标对象的过程,Spring 采用运行时织入 |
13. AOP 主要用在哪些场景中?
问题: AOP 主要用在哪些场景?
答案:
| 场景 | 说明 |
|---|---|
| 日志记录 | 统一记录方法调用的入参、出参、耗时 |
| 事务管理 | @Transactional 的底层就是 AOP 实现 |
| 权限校验 | 在方法执行前校验用户权限 |
| 性能监控 | 统计方法执行时间,监控慢接口 |
| 异常处理 | 统一捕获和处理异常 |
| 缓存 | 方法级缓存,查询时先走缓存 |
| 限流/熔断 | 接口级别的限流和熔断 |
| 数据脱敏 | 返回结果中对敏感信息进行脱敏处理 |
| 操作审计 | 记录关键操作的审计日志 |
14. 项目中 AOP 的使用
问题: AOP 在你们项目中哪里有使用到?怎么用的?
答案:
(以下为常见项目实践示例,可根据实际项目替换)
场景:操作日志记录
// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperLog {
String value() default "";
OperType type() default OperType.OTHER;
}
// AOP 切面
@Aspect
@Component
@Slf4j
public class OperLogAspect {
@Pointcut("@annotation(com.example.annotation.OperLog)")
public void operLogPointcut() {}
@Around("operLogPointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
// 记录开始时间
long startTime = System.currentTimeMillis();
// 获取方法信息
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
OperLog operLog = method.getAnnotation(OperLog.class);
// 记录入参
String className = point.getTarget().getClass().getSimpleName();
String methodName = method.getName();
Object[] args = point.getArgs();
log.info("[{}]{}.{} 开始执行,参数:{}",
operLog.value(), className, methodName, args);
// 执行方法
Object result = point.proceed();
// 记录出参和耗时
long cost = System.currentTimeMillis() - startTime;
log.info("[{}]{}.{} 执行完成,耗时:{}ms,结果:{}",
operLog.value(), className, methodName, cost, result);
// 异步保存到数据库
saveLog(className, methodName, operLog, args, result, cost);
return result;
}
}
典型使用场景:
- 通过自定义
@OperLog注解标注需要记录日志的 Controller 方法 - AOP 自动记录操作人、操作模块、请求参数、响应结果、执行耗时
- 日志异步写入数据库,不影响主流程性能
15. Spring 的三级缓存
问题: Spring 的三级缓存是什么?
答案:
Spring 使用三级缓存解决 Bean 的循环依赖问题:
| 缓存级别 | 名称 | 存储内容 | 作用 |
|---|---|---|---|
| 一级缓存 | singletonObjects | 完整的 Bean 实例(已初始化完毕) | 存放完全创建好的单例 Bean |
| 二级缓存 | earlySingletonObjects | 早期 Bean 引用(已实例化,未初始化) | 保存提前暴露的 Bean 引用,解决循环依赖 |
| 三级缓存 | singletonFactories | Bean 工厂(ObjectFactory) | 生成早期 Bean 引用的工厂,支持 AOP 代理 |
获取 Bean 的顺序:
一级缓存 singletonObjects → 二级缓存 earlySingletonObjects → 三级缓存 singletonFactories
创建 Bean 的流程:
- 实例化 Bean(调用构造方法)
- 将三级缓存中放入
ObjectFactory(如果需要 AOP,工厂会返回代理对象) - 属性注入(发现循环依赖时,从三级缓存获取早期引用)
- 获取早期引用后放入二级缓存,删除三级缓存
- 初始化 Bean(调用
@PostConstruct、InitializingBean.afterPropertiesSet()) - 初始化完成后放入一级缓存,删除二级缓存
16. Spring 如何解决循环依赖
问题: Spring 如何解决循环依赖?
答案:
解决范围: Spring 只能解决单例模式下 Setter 注入/字段注入 的循环依赖,无法解决构造器注入的循环依赖。
解决原理(以 A → B → A 为例):
1. 创建 A:实例化 A → 将 A 的工厂放入三级缓存
2. A 属性注入 B → 发现需要 B
3. 创建 B:实例化 B → 将 B 的工厂放入三级缓存
4. B 属性注入 A → 从三级缓存获取 A 的工厂 → 生成 A 的早期引用(代理对象)
5. 将 A 的早期引用放入二级缓存,删除三级缓存
6. B 完成初始化 → B 放入一级缓存
7. A 获取到 B → A 完成初始化 → A 放入一级缓存
无法解决的情况:
- 构造器注入循环依赖:实例化时就互相依赖,无法提前暴露引用,直接抛出
BeanCurrentlyInCreationException - 原型(prototype)作用域的循环依赖:Spring 不缓存原型 Bean,无法解决
解决方案(构造器循环依赖):
- 使用
@Lazy注解延迟加载 - 重构代码,消除循环依赖
17. Spring 事务如何与线程绑定
问题: Spring 事务是如何与线程绑定的?
答案:
Spring 事务通过 ThreadLocal 实现与线程绑定:
-
TransactionSynchronizationManager是核心类,内部使用多个ThreadLocal变量保存事务相关资源:resources:保存数据源连接(DataSource → Connection)transactionSynchronizations:保存事务同步回调currentTransactionName:当前事务名称currentTransactionReadOnly:当前事务是否只读currentTransactionIsolationLevel:当前事务隔离级别
-
绑定流程:
开启事务 → 从数据源获取 Connection → 绑定到当前线程的 ThreadLocal 执行 SQL → 从 ThreadLocal 获取 Connection → 使用同一个 Connection 提交/回滚事务 → 从 ThreadLocal 获取 Connection → 提交/回滚 → 解除绑定 -
关键点:
- 同一线程中的所有数据库操作共用同一个 Connection,保证事务一致性
- 不同线程各自独立的事务上下文,互不影响
- 如果需要跨线程传播事务,需要手动传递(如使用
InheritableThreadLocal或自定义传播机制)
18. Spring 的传播行为
问题: Spring 的事务传播行为有哪些?
答案:
| 传播行为 | 说明 |
|---|---|
| REQUIRED(默认) | 有事务就加入,没有就新建 |
| REQUIRES_NEW | 总是新建事务,挂起当前事务 |
| NESTED | 有事务就嵌套(savepoint),没有就新建 |
| SUPPORTS | 有事务就加入,没有就以非事务方式执行 |
| NOT_SUPPORTED | 以非事务方式执行,挂起当前事务 |
| MANDATORY | 必须在事务中执行,否则抛出异常 |
| NEVER | 必须不在事务中执行,否则抛出异常 |
// 外层方法
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
// ...
methodB(); // B 加入 A 的事务
}
// 内层方法
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// B 总是新建独立事务,A 的事务被挂起
}
常见使用场景:
- 日志记录:
REQUIRES_NEW或NOT_SUPPORTED,保证日志不受业务事务回滚影响- 批量操作:
NESTED,部分失败可以回滚到 savepoint
三、SpringMVC
19. SpringMVC 执行流程(工作原理)
问题: SpringMVC 的执行流程是什么?
答案:
客户端请求
↓
1. DispatcherServlet(前端控制器)
↓
2. HandlerMapping(处理器映射器)→ 根据 URL 找到对应的 Handler
↓
3. HandlerAdapter(处理器适配器)→ 执行 Handler(Controller 方法)
↓
4. Controller 处理业务逻辑,返回 ModelAndView(或数据)
↓
5. (如果有拦截器)执行拦截器的 postHandle
↓
6. ViewResolver(视图解析器)→ 解析视图名称为具体 View
↓
7. View 渲染视图(返回 JSON 则跳过此步)
↓
8. DispatcherServlet 返回响应给客户端
核心组件:
| 组件 | 作用 |
|---|---|
DispatcherServlet | 前端控制器,统一接收和分发请求 |
HandlerMapping | 根据 URL 查找 Handler |
HandlerAdapter | 执行 Handler,统一适配不同类型的 Handler |
Handler | 具体的 Controller 方法 |
ViewResolver | 解析逻辑视图名为具体视图 |
View | 负责页面渲染 |
20. SpringMVC 默认的 JSON 序列化技术是哪个?
问题: SpringMVC 默认的 JSON 序列化技术是哪个?
答案:
- SpringMVC 默认使用 Jackson 作为 JSON 序列化/反序列化库
- 核心类:
com.fasterxml.jackson.databind.ObjectMapper - 当 classpath 中存在 Jackson 的 jar 包时,Spring Boot 会自动配置
MappingJackson2HttpMessageConverter - 如果想替换为其他库:
- Fastjson:配置
FastJsonHttpMessageConverter - Gson:配置
GsonHttpMessageConverter
- Fastjson:配置
21. RequestBody 和 ResponseBody 区别
问题: @RequestBody 和 @ResponseBody 有什么区别?
答案:
| 对比项 | @RequestBody | @ResponseBody |
|---|---|---|
| 作用位置 | 方法参数上 | 方法返回值上 |
| 方向 | 请求 → Controller(接收请求体) | Controller → 响应(写入响应体) |
| 功能 | 将请求体(JSON/XML)反序列化为 Java 对象 | 将 Java 对象序列化为 JSON/XML 写入响应 |
| Content-Type | 需要请求头 Content-Type: application/json | 响应头 Content-Type: application/json |
// @RequestBody:接收前端传来的 JSON 数据
@PostMapping("/user")
public Result createUser(@RequestBody UserDTO userDTO) {
// userDTO 从请求体中反序列化得到
userService.save(userDTO);
return Result.success();
}
// @ResponseBody:返回值直接写入响应体
@GetMapping("/user/{id}")
@ResponseBody
public User getUser(@PathVariable Long id) {
// 返回的 User 对象会被序列化为 JSON
return userService.getById(id);
}
// @RestController = @Controller + @ResponseBody
22. SpringAOP 的通知类型有哪些
问题: SpringAOP 的通知类型有哪些?
答案:
| 通知类型 | 注解 | 说明 |
|---|---|---|
| 前置通知 | @Before | 在目标方法执行前执行 |
| 后置通知 | @AfterReturning | 在目标方法正常返回后执行(有返回值) |
| 异常通知 | @AfterThrowing | 在目标方法抛出异常后执行 |
| 最终通知 | @After | 在目标方法执行后执行(无论正常返回还是异常) |
| 环绕通知 | @Around | 包裹整个方法,可控制是否执行、何时执行、执行几次 |
@Aspect
@Component
public class LogAspect {
@Before("execution(* com.example.service.*.*(..))")
public void before(JoinPoint jp) {
log.info("前置通知:{}", jp.getSignature().getName());
}
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
log.info("环绕通知-前");
Object result = pjp.proceed(); // 执行目标方法
log.info("环绕通知-后");
return result;
}
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void afterReturning(JoinPoint jp, Object result) {
log.info("后置通知,返回值:{}", result);
}
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void afterThrowing(JoinPoint jp, Exception ex) {
log.error("异常通知:{}", ex.getMessage());
}
}
23. @Controller 和 @RestController 有何区别
问题: @Controller 和 @RestController 有什么区别?
答案:
| 对比项 | @Controller | @RestController |
|---|---|---|
| 组成 | @Controller | @Controller + @ResponseBody |
| 返回值处理 | 默认走视图解析器(JSP/Thymeleaf),返回页面 | 直接将返回值序列化为 JSON 写入响应体 |
| 适用场景 | 返回视图页面(前后端不分离) | 返回 JSON 数据(前后端分离 / RESTful API) |
// @Controller:返回页面
@Controller
public class PageController {
@GetMapping("/index")
public String index() {
return "index"; // 解析为 index.html 或 index.jsp
}
}
// @RestController:返回 JSON
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/user")
public User getUser() {
return new User(1L, "张三"); // 序列化为 {"id":1,"name":"张三"}
}
}
24. @Autowired 和 @Resource 的区别
问题: @Autowired 和 @Resource 有什么区别?
答案:
| 对比项 | @Autowired | @Resource |
|---|---|---|
| 来源 | Spring 提供 | JSR-250(Java 标准) |
| 匹配方式 | 默认按类型(byType)匹配 | 默认按名称(byName)匹配 |
| 名称匹配 | 配合 @Qualifier 指定名称 | 通过 name 属性指定 |
| 依赖对象 | 必须存在(required=true),否则启动报错 | 查找不到时不会立即报错(可通过 name 或 type 调整) |
| 适用性 | Spring 生态专用 | 标准注解,可切换为其他 IoC 容器 |
// @Autowired + @Qualifier
@Autowired
@Qualifier("myDataSource")
private DataSource dataSource;
// @Resource 按名称注入
@Resource(name = "myDataSource")
private DataSource dataSource;
// @Resource 按类型注入
@Resource(type = DataSource.class)
private DataSource dataSource;
25. SpringMVC 常用注解有哪些?
问题: SpringMVC 常用注解有哪些?
答案:
| 注解 | 作用 |
|---|---|
@RequestMapping | 映射 HTTP 请求到 Controller 方法 |
@GetMapping | 映射 GET 请求 |
@PostMapping | 映射 POST 请求 |
@PutMapping | 映射 PUT 请求 |
@DeleteMapping | 映射 DELETE 请求 |
@PatchMapping | 映射 PATCH 请求 |
@RestController | @Controller + @ResponseBody,返回 JSON |
@RequestBody | 接收请求体中的 JSON 数据 |
@ResponseBody | 将返回值写入响应体 |
@PathVariable | 获取 URL 路径中的变量 |
@RequestParam | 获取请求参数(URL 参数或表单参数) |
@RequestHeader | 获取请求头信息 |
@CookieValue | 获取 Cookie 值 |
@SessionAttribute | 获取 Session 属性 |
26. SpringMVC 中接收参数的注解有哪些?
问题: SpringMVC 中接收参数的注解有哪些?
答案:
| 注解 | 使用位置 | 说明 | 示例 |
|---|---|---|---|
@RequestParam | 方法参数 | 获取 URL 查询参数或表单参数 | ?name=张三 |
@PathVariable | 方法参数 | 获取 URL 路径中的变量 | /user/{id} |
@RequestBody | 方法参数 | 获取请求体(JSON/XML) | 请求体 JSON |
@RequestHeader | 方法参数 | 获取请求头信息 | Accept: application/json |
@CookieValue | 方法参数 | 获取 Cookie 中的值 | JSESSIONID=xxx |
@SessionAttribute | 方法参数 | 从 Session 中获取属性 | session 中的 user |
@ModelAttribute | 方法参数 | 将请求参数绑定到对象,也可在方法上返回数据到 Model | 表单提交绑定 |
@GetMapping("/users/{id}")
public User getUser(
@PathVariable Long id, // /users/123 → id=123
@RequestParam(defaultValue = "1") int page, // ?page=2 → page=2
@RequestHeader("Authorization") String token, // 请求头 Authorization
@CookieValue("sessionId") String sessionId // Cookie sessionId
) { ... }
四、SpringBoot
27. 项目中 SpringBoot 和 SpringCloud 的版本号
问题: 你们项目中 SpringBoot 和 SpringCloud 的版本号是什么?
答案:
(以下为常见版本组合示例,需根据实际项目填写)
| SpringBoot | SpringCloud | SpringCloud Alibaba |
|---|---|---|
| 2.7.x | 2021.0.x (Jubilee) | 2021.0.x |
| 3.0.x | 2022.0.x (Kilburn) | 2022.0.x |
| 3.2.x | 2023.0.x (Leyton) | 2023.0.x |
注意: SpringBoot 和 SpringCloud 版本有严格的兼容对应关系,不能随意搭配。官方提供版本对应表:Spring Cloud Release Notes
28. 为什么要使用 SpringBoot?
问题: 为什么要使用 SpringBoot?
答案:
- 自动配置(Auto-Configuration):根据 classpath 中的依赖自动配置 Spring 应用,减少大量 XML 配置
- 内嵌容器:内嵌 Tomcat/Jetty/Undertow,无需部署 WAR 包,直接
java -jar启动 - 起步依赖(Starter):提供各种场景的 Starter POM,一键引入所需依赖,解决依赖冲突
- 无需 XML:使用 Java 配置 +
application.yml/properties,告别繁琐的 XML - 生产就绪特性:内置健康检查、指标监控、外部化配置等
- 简化构建:集成 Maven/Gradle 插件,一键打包可执行 jar
- 快速开发:结合 Spring Initializr 可快速搭建项目骨架
29. @SpringBootApplication 注解由哪三个注解组成?
问题: @SpringBootApplication 注解是由哪三个注解组成的?
答案:
@SpringBootConfiguration // 等同于 @Configuration,标记为配置类
@EnableAutoConfiguration // 启用自动配置机制
@ComponentScan // 开启组件扫描(扫描当前包及子包下的组件)
@SpringBootConfiguration:本质就是@Configuration,表示这是一个配置类@EnableAutoConfiguration:根据 classpath 中的依赖自动配置 Spring 应用@ComponentScan:自动扫描主类所在包及其子包下的@Component注解的类
30. SpringBoot 启动时做了什么?
问题: SpringBoot 启动时做了什么?
答案:
1. new SpringApplication() 初始化
├── 推断应用类型(SERVLET / REACTIVE / NONE)
├── 加载初始器(ApplicationContextInitializer)
├── 加载监听器(ApplicationListener)
└── 推断主类(main 方法所在类)
2. run() 方法执行
├── 启动计时器(StopWatch)
├── 获取监听器,广播 ApplicationStartingEvent
├── 准备环境(Environment),广播 ApplicationEnvironmentPreparedEvent
├── 打印 Banner
├── 创建 ApplicationContext(根据类型创建)
├── 准备 ApplicationContext,广播 ApplicationContextInitializedEvent
├── 刷新 ApplicationContext(核心步骤)
│ ├── 注册 BeanDefinition
│ ├── 实例化单例 Bean
│ ├── 处理 @Autowired 注入
│ ├── 执行自动配置(@EnableAutoConfiguration)
│ └── 启动内嵌 Web 容器(Tomcat 等)
├── 刷新后处理,广播 ApplicationStartedEvent
├── 执行 Runner(ApplicationRunner / CommandLineRunner)
└── 广播 ApplicationReadyEvent,启动完成
31. SpringBoot 的 starter 列举至少 5 个常用的
问题: SpringBoot 有哪些常用的 Starter?
答案:
| Starter | 作用 |
|---|---|
spring-boot-starter-web | Web 开发支持(SpringMVC + 内嵌 Tomcat) |
spring-boot-starter-data-redis | Redis 集成 |
spring-boot-starter-data-jpa | JPA / Hibernate 数据库操作 |
spring-boot-starter-jdbc | JDBC + 数据源支持 |
spring-boot-starter-mybatis | MyBatis 集成 |
spring-boot-starter-security | Spring Security 安全框架 |
spring-boot-starter-validation | 参数校验(JSR-380) |
spring-boot-starter-aop | AOP 切面编程 |
spring-boot-starter-actuator | 监控与管理端点 |
spring-boot-starter-test | 测试支持(JUnit + Mockito) |
spring-boot-starter-amqp | RabbitMQ 消息队列 |
spring-boot-starter-mail | 邮件发送 |
32. SpringBoot 读取配置文件的方式有几种?
问题: SpringBoot 读取配置文件的方式有几种?
答案:
| 方式 | 说明 | 示例 |
|---|---|---|
@Value | 注入单个配置值 | @Value("${server.port}") |
@ConfigurationProperties | 批量绑定配置到 Bean | @ConfigurationProperties(prefix = "spring.datasource") |
Environment | 通过环境对象获取 | env.getProperty("server.port") |
@PropertySource | 加载指定的 properties 文件 | @PropertySource("classpath:custom.properties") |
ConfigurableApplicationContext | 通过上下文获取环境 | applicationContext.getEnvironment() |
# application.yml
app:
name: MyApp
config:
host: localhost
port: 8080
// 方式一:@Value
@Value("${app.name}")
private String appName;
// 方式二:@ConfigurationProperties(推荐,支持类型安全和 JSR-303 校验)
@Component
@ConfigurationProperties(prefix = "app.config")
@Validated
public class AppConfig {
private String host;
private int port;
// getter/setter ...
}
// 方式三:Environment
@Autowired
private Environment env;
public void method() {
String name = env.getProperty("app.name");
}
33. SpringBoot 内置的容器和默认日志框架
问题: SpringBoot 内置的容器是哪个?默认的日志框架是哪个?
答案:
内置容器:
- 默认:Tomcat(
spring-boot-starter-web) - 可切换为:Jetty 或 Undertow(排除 Tomcat 依赖,添加对应 Starter)
<!-- 切换为 Undertow -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
默认日志框架:
- SLF4J + Logback
- SpringBoot 通过
spring-boot-starter-logging默认引入 - SLF4J 是日志门面(接口),Logback 是具体实现
- 可通过配置切换为 Log4j2
34. SpringBoot 如何接收客户端请求
问题: SpringBoot 如何接收客户端请求?
答案:
SpringBoot 接收请求的方式与 SpringMVC 一致,核心就是通过 DispatcherServlet 分发请求到 Controller:
@RestController
@RequestMapping("/api")
public class UserController {
// 1. 路径参数
@GetMapping("/user/{id}")
public User getById(@PathVariable Long id) { ... }
// 2. 查询参数
@GetMapping("/user")
public User getByName(@RequestParam String name) { ... }
// 3. 请求体 JSON
@PostMapping("/user")
public Result create(@RequestBody UserDTO dto) { ... }
// 4. 表单数据
@PostMapping("/login")
public Result login(@RequestParam String username,
@RequestParam String password) { ... }
// 5. 文件上传
@PostMapping("/upload")
public Result upload(@RequestParam("file") MultipartFile file) { ... }
// 6. 请求头
@GetMapping("/info")
public Result info(@RequestHeader("Authorization") String token) { ... }
}
请求处理流程:
客户端 → Tomcat → DispatcherServlet → HandlerMapping → Controller → 返回 JSON
35. SpringBoot 如何处理跨域问题
问题: SpringBoot 如何处理跨域问题?
答案:
方式一:全局配置(推荐)
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
方式二:CORS 过滤器
@Configuration
public class CorsFilterConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
方式三:单个接口配置
@CrossOrigin(origins = "*", maxAge = 3600)
@GetMapping("/api/user")
public User getUser() { ... }
五、SpringCloud
36. 为什么用 SpringCloud?优势和缺点?
问题: 为什么用 SpringCloud?优势是什么?缺点是什么?
答案:
优势:
- 生态完善:提供了一整套微服务解决方案(注册中心、配置中心、网关、负载均衡、熔断等)
- 技术统一:基于 Spring Boot 开发,与 Spring 生态无缝集成
- 社区活跃:Spring 官方维护,文档丰富,社区支持好
- 选型灵活:各个组件可按需选择,支持 Netflix OSS、Alibaba 等多种实现
- 开箱即用:通过 Starter 依赖快速集成各组件
- 兼容性强:支持多种注册中心(Eureka、Nacos、Consul 等)和消息队列
缺点:
- 学习曲线陡:组件繁多,概念复杂,需要掌握分布式系统的知识
- 部署运维复杂:微服务数量多,对运维(Docker/K8s、日志收集、链路追踪)要求高
- 调试困难:服务间调用链长,排查问题需要完善的监控体系
- 版本迭代快:版本更新频繁,组件淘汰和替换频繁(如 Netflix 部分组件停更)
- 性能开销:相比单体应用,微服务间通信增加网络开销
- 事务一致性:分布式事务处理复杂(需要 Seata 等额外组件)
37. SpringCloud 有哪些组件?
问题: SpringCloud 有哪些组件?
答案:
| 功能 | Netflix 方案 | Alibaba 方案 | 说明 |
|---|---|---|---|
| 服务注册与发现 | Eureka | Nacos | 微服务注册与发现 |
| 服务调用 | Feign | OpenFeign | 声明式 HTTP 客户端 |
| 负载均衡 | Ribbon | LoadBalancer | 客户端负载均衡 |
| 熔断降级 | Hystrix | Sentinel | 服务熔断、限流、降级 |
| 配置中心 | Spring Cloud Config | Nacos Config | 统一配置管理 |
| 网关 | Zuul | Gateway | API 网关,路由、过滤 |
| 分布式事务 | — | Seata | 分布式事务解决方案 |
| 消息驱动 | — | Spring Cloud Stream | 消息驱动的微服务 |
| 链路追踪 | Sleuth + Zipkin | SkyWalking | 分布式链路追踪 |
| 服务限流 | — | Sentinel | 流量控制 |
38. Eureka 的工作原理?
问题: Eureka 的工作原理是什么?
答案:
核心架构:Eureka Server + Eureka Client
┌──────────────┐
│ Eureka Server│
│ (注册中心) │
└──────┬───────┘
│
┌──────────────┼──────────────┐
│ │ │
┌──────┴──────┐ ┌────┴─────┐ ┌──────┴──────┐
│ Service A │ │ Service B│ │ Service C │
│ (Provider) │ │(Consumer)│ │ (Provider) │
└─────────────┘ └──────────┘ └─────────────┘
工作流程:
- 服务注册:服务启动时,向 Eureka Server 发送注册请求(服务名、IP、端口等)
- 心跳续约:服务每隔 30 秒向 Eureka Server 发送心跳(Renew),证明自己存活
- 服务拉取:客户端从 Eureka Server 获取服务列表,缓存到本地
- 服务调用:客户端通过负载均衡(Ribbon)选择一个服务实例发起远程调用
- 服务下线:服务正常关闭时,向 Eureka Server 发送下线请求(Cancel)
- 自我保护:Eureka Server 如果短时间丢失大量心跳(网络分区),会进入自我保护模式,不再删除注册信息
- 剔除服务:如果服务 90 秒未发送心跳(3 个心跳周期),Eureka Server 将其剔除
注意: Eureka 采用 AP 模型(可用性优先),不保证强一致性。Eureka 2.x 已停止开发。
39. Nacos 组件解决了什么问题?
问题: Nacos 组件解决了什么问题?
答案:
Nacos(Dynamic Naming and Configuration Service)是阿里巴巴开源的,主要解决两大问题:
1. 服务注册与发现:
- 微服务的动态注册和发现
- 支持服务健康检查(临时实例:心跳模式;永久实例:主动探测模式)
- 服务分组管理,支持命名空间隔离
2. 动态配置管理:
- 集中管理所有微服务的配置
- 配置热更新(修改配置后无需重启服务)
- 支持配置的版本管理和灰度发布
- 支持多种配置格式(YAML、Properties、JSON、TEXT)
- 支持配置监听(配置变更实时推送)
其他特性:
- 支持命名空间(Namespace)实现多环境隔离(dev/test/prod)
- 支持服务分组(Group)
- 支持权重路由
- 提供控制台 UI,可视化管理服务和配置
40. Nacos 和 Eureka 有什么区别?
问题: Nacos 和 Eureka 有什么区别?
答案:
| 对比项 | Nacos | Eureka |
|---|---|---|
| CAP 模型 | 支持 AP 和 CP 切换 | 仅 AP |
| 配置中心 | ✅ 内置配置中心 | ❌ 需配合 Spring Cloud Config |
| 健康检查 | 临时实例(心跳)/ 永久实例(主动探测) | 仅心跳模式 |
| 服务分组 | 支持分组和命名空间 | 仅支持 Zone |
| 配置热更新 | ✅ 原生支持 | ❌ 不支持 |
| 权重路由 | ✅ 支持权重调整 | ❌ 不支持 |
| 管理界面 | ✅ 功能丰富的控制台 | ✅ 基础控制台 |
| 社区状态 | 活跃维护(阿里主导) | 2.x 停止维护,1.x 维护中 |
| 一致性协议 | Raft(CP 模式)/ Distro(AP 模式) | 复制协议(最终一致性) |
| 中文文档 | ✅ 完善 | 一般 |
41. 为什么要用 Gateway 组件?
问题: 为什么要用 Gateway 组件?
答案:
- 统一入口:所有微服务的统一访问入口,客户端只需知道网关地址
- 路由转发:根据请求路径、Header、参数等规则将请求路由到对应的微服务
- 统一鉴权:在网关层统一做身份认证和权限校验,避免每个服务重复实现
- 限流:对高并发接口进行流量控制,保护后端服务
- 熔断降级:结合 Sentinel/Hystrix 实现服务熔断降级
- 日志监控:统一记录访问日志、请求耗时等
- 跨域处理:在网关层统一处理跨域问题
- 灰度发布:支持基于权重的路由,实现灰度发布
- 协议转换:支持 WebSocket、gRPC 等多种协议
- 隐藏内部结构:对外只暴露网关地址,不暴露内部微服务拓扑
42. Gateway 组件的核心知识
问题: Gateway 组件的核心知识包括哪些内容?
答案:
三大核心概念:
| 概念 | 说明 |
|---|---|
| Route(路由) | 路由的基本构建块,包含 ID、目标 URI、Predicate 和 Filter |
| Predicate(断言) | 匹配请求的条件(路径、Header、参数、时间等) |
| Filter(过滤器) | 对请求/响应进行修改(添加 Header、限流、重试等) |
内置 Predicate 工厂:
| Predicate | 说明 | 示例 |
|---|---|---|
Path | 路径匹配 | Path=/api/user/** |
Method | HTTP 方法匹配 | Method=GET |
Header | 请求头匹配 | Header=X-Request-Id, \d+ |
Query | 查询参数匹配 | Query=token |
Host | 主机名匹配 | Host=**.example.com |
RemoteAddr | IP 地址匹配 | RemoteAddr=192.168.1.0/24 |
After/Before/Between | 时间匹配 | After=2024-01-01T00:00:00+08:00[Asia/Shanghai] |
内置 Filter:
| Filter | 说明 |
|---|---|
AddRequestHeader | 添加请求头 |
AddResponseHeader | 添加响应头 |
StripPrefix | 去除路径前缀 |
Retry | 重试 |
RequestRateLimiter | 限流(配合 Redis + RedisRateLimiter) |
CircuitBreaker | 熔断器 |
# Gateway 配置示例
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service # lb:// 表示负载均衡
predicates:
- Path=/api/user/**
filters:
- StripPrefix=1 # 去除 /api 前缀
- AddRequestHeader=X-Token, xxx
注意: SpringCloud Gateway 基于 WebFlux + Netty 实现,与 SpringMVC 不兼容,不能在 Web 项目中同时使用。
43. Feign 组件解决了什么问题?
问题: Feign 组件解决了什么问题?
答案:
- 简化 HTTP 调用:将微服务间的 HTTP 调用抽象为 Java 接口,像调用本地方法一样调用远程服务
- 声明式调用:通过注解定义请求方式、URL、参数等,无需手动拼接 URL 和处理响应
- 集成 Ribbon/LoadBalancer:自动实现客户端负载均衡
- 集成熔断器:可配合 Hystrix/Sentinel 实现熔断降级
- 模板化编码:消除 RESTful API 调用中的重复代码(如 RestTemplate 的手动序列化/反序列化)
// 使用 RestTemplate(繁琐)
@RestController
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/order/{id}")
public Order getOrder(@PathVariable Long id) {
User user = restTemplate.getForObject(
"http://user-service/user/" + id, User.class);
return new Order(id, user);
}
}
// 使用 Feign(简洁)
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/user/{id}")
User getUser(@PathVariable Long id);
}
@RestController
public class OrderController {
@Autowired
private UserClient userClient;
@GetMapping("/order/{id}")
public Order getOrder(@PathVariable Long id) {
User user = userClient.getUser(id); // 像调用本地方法一样
return new Order(id, user);
}
}
44. Feign 远程调用异常处理及协议
问题: Feign 远程调用接口出现异常应该怎么处理?Feign 是基于什么协议?
答案:
异常处理方式:
方式一:Feign 的 Fallback(降级处理)
// 定义降级类
@Component
public class UserClientFallback implements UserClient {
@Override
public User getUser(Long id) {
return new User(-1L, "服务降级", 0);
}
}
// 接口指定 fallback
@FeignClient(name = "user-service", fallback = UserClientFallback.class)
public interface UserClient {
@GetMapping("/user/{id}")
User getUser(@PathVariable Long id);
}
方式二:FallbackFactory(带异常信息的降级)
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable cause) {
return new UserClient() {
@Override
public User getUser(Long id) {
log.error("调用用户服务失败,原因:", cause);
return new User(-1L, "服务降级:" + cause.getMessage(), 0);
}
};
}
}
@FeignClient(name = "user-service", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient { ... }
方式三:全局异常处理
@Configuration
public class FeignConfig {
@Bean
public ErrorDecoder errorDecoder() {
return (methodKey, response) -> {
// 自定义异常处理逻辑
String body = Util.toString(response.body().asReader());
return new RemoteServiceException("远程调用失败: " + body);
};
}
}
Feign 基于的协议:
- Feign 默认基于 HTTP 协议(底层使用
HttpURLConnection) - 可替换底层实现:Apache HttpClient、OkHttp
- SpringCloud OpenFeign 默认集成 Spring MVC 注解
45. Feign 的执行流程?使用 Feign 会用到哪些注解?
问题: Feign 的执行流程是怎样的?使用 Feign 会用到哪些注解?
答案:
执行流程:
1. @EnableFeignClients → 扫描标注了 @FeignClient 的接口
2. 为每个 @FeignClient 接口生成 JDK 动态代理对象
3. 调用接口方法 → 代理对象拦截调用
4. 根据 @GetMapping/@PostMapping 等注解构建 HTTP 请求模板
5. 经过 Encoder 将参数编码(如 Jackson 序列化为 JSON)
6. 经过 Interceptor(拦截器)添加 Header 等
7. 通过 LoadBalancer 选择服务实例
8. 通过 Client(HttpURLConnection/OkHttp/HttpClient)发送 HTTP 请求
9. 经过 Decoder 将响应解码为 Java 对象
10. 返回结果(或进入 Fallback 降级逻辑)
常用注解:
| 注解 | 作用 |
|---|---|
@EnableFeignClients | 启动类上开启 Feign 客户端扫描 |
@FeignClient | 声明 Feign 客户端(name=服务名,url=直连地址,fallback=降级类) |
@GetMapping | 映射 GET 请求 |
@PostMapping | 映射 POST 请求 |
@PutMapping | 映射 PUT 请求 |
@DeleteMapping | 映射 DELETE 请求 |
@PathVariable | 路径参数 |
@RequestParam | 查询参数 |
@RequestBody | 请求体参数 |
@RequestHeader | 请求头参数 |
46. Feign 是如何使用的?
问题: Feign 是如何使用的?
答案:
步骤一:引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 负载均衡 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
步骤二:启动类添加注解
@SpringBootApplication
@EnableFeignClients // 开启 Feign 客户端
public class OrderApplication { ... }
步骤三:定义 Feign 接口
@FeignClient(name = "user-service", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
@GetMapping("/user/{id}")
User getUser(@PathVariable("id") Long id);
@PostMapping("/user")
User createUser(@RequestBody UserDTO dto);
@GetMapping("/user/list")
List<User> listUsers(@RequestParam("ids") List<Long> ids);
}
步骤四:调用远程服务
@Service
public class OrderService {
@Autowired
private UserClient userClient;
public OrderDTO getOrder(Long orderId) {
// 像调用本地方法一样调用远程服务
User user = userClient.getUser(userId);
return buildOrderDTO(user);
}
}
步骤五:配置文件(可选)
feign:
client:
config:
default:
connectTimeout: 5000 # 连接超时
readTimeout: 10000 # 读取超时
loggerLevel: full # 日志级别
47. Feign 的实现原理是什么样的?
问题: Feign 的实现原理是什么?
答案:
Feign 的核心原理就是 JDK 动态代理 + 注解解析 + HTTP 客户端:
-
@EnableFeignClients触发FeignClientsRegistrar,扫描所有@FeignClient注解的接口,注册为FactoryBean -
FeignClientFactoryBean.getObject()为每个接口创建代理对象:// 内部调用 Feign.builder() 构建 return Feign.builder() .encoder(encoder) // 参数编码器 .decoder(decoder) // 响应解码器 .contract(contract) // 注解契约(SpringMVCContract) .client(client) // HTTP 客户端 .interceptor(interceptor) // 请求拦截器 .target(FeignClient.class, url); -
动态代理(
FeignInvocationHandler):- 调用接口方法时,代理对象拦截
- 通过
SynchronousMethodHandler处理方法调用
-
构建请求:
Contract解析方法上的 Spring MVC 注解(@GetMapping、@PostMapping、@PathVariable等)- 构建
RequestTemplate(包含 URL、Method、Headers、Body)
-
负载均衡:
- 如果 URI 使用
lb://前缀,通过LoadBalancerClient选择服务实例 - 将逻辑服务名替换为实际的 IP:PORT
- 如果 URI 使用
-
发送请求:
- 通过底层的
Client(默认 JDK HttpURLConnection,可切换 OkHttp/HttpClient)发送 HTTP 请求
- 通过底层的
-
解析响应:
Decoder将响应体反序列化为方法返回值类型
48. 微服务间互相通信可以怎么实现?
问题: 微服务间互相通信可以怎么实现?
答案:
| 方式 | 说明 | 适用场景 |
|---|---|---|
| OpenFeign | 声明式 HTTP 客户端,像调用本地方法一样调用远程服务 | 同步请求,最常用 |
| RestTemplate | 直接发起 HTTP 请求 | 简单场景,代码稍繁琐 |
| Dubbo | 基于 RPC 协议的高性能远程调用 | 高性能、低延迟场景 |
| 消息队列(MQ) | 通过 RabbitMQ/Kafka/RocketMQ 异步通信 | 异步解耦、削峰填谷、事件驱动 |
| gRPC | Google 的 RPC 框架,基于 HTTP/2 + Protocol Buffers | 多语言、高性能场景 |
| WebSocket | 全双工通信 | 实时推送场景 |
// 方式一:OpenFeign(推荐)
@FeignClient("user-service")
public interface UserClient {
@GetMapping("/user/{id}")
User getUser(@PathVariable Long id);
}
// 方式二:RestTemplate + 负载均衡
@Autowired
@LoadBalanced
private RestTemplate restTemplate;
User user = restTemplate.getForObject("http://user-service/user/1", User.class);
// 方式三:消息队列异步通信
@Autowired
private RabbitTemplate rabbitTemplate;
rabbitTemplate.convertAndSend("order.exchange", "order.created", orderDTO);
49. Nacos 是 AP 还是 CP?
问题: Nacos 是 AP 还是 CP?
答案:
Nacos 同时支持 AP 和 CP,可以根据服务类型灵活切换:
| 实例类型 | 模式 | 一致性协议 | 说明 |
|---|---|---|---|
| 临时实例(默认) | AP 模式 | Distro 协议 | 通过心跳保活,牺牲一致性保证可用性 |
| 永久实例 | CP 模式 | Raft 协议 | 通过主动健康检查,保证一致性但可用性稍低 |
# 注册为临时实例(AP 模式)- 默认
spring:
cloud:
nacos:
discovery:
ephemeral: true # 临时实例,AP
# 注册为永久实例(CP 模式)
spring:
cloud:
nacos:
discovery:
ephemeral: false # 永久实例,CP
选择建议:
- AP 模式(临时实例):大多数微服务场景,允许短暂的不一致,优先保证可用性
- CP 模式(永久实例):对一致性要求高的服务,如配置中心的数据
50. Hystrix 熔断降级触发标准?熔断器有几种状态?
问题: Hystrix 熔断降级的触发标准是什么?熔断器有几种状态?
答案:
熔断触发标准:
在设定的时间窗口(默认 10 秒)内,满足以下任一条件时触发熔断:
| 条件 | 默认值 | 说明 |
|---|---|---|
| 请求量阈值 | ≥ 20 次 | 时间窗口内请求数达到阈值才计算错误率 |
| 错误率阈值 | ≥ 50% | 失败请求占总请求的比例 |
| 异常类型 | — | 超时、异常、拒绝、短路 |
注意: 如果请求数不足 20 次,即使全部失败也不会触发熔断。
熔断器三种状态:
失败率 >= 50%
┌──────────────────┐
│ ↓
┌───────┐ ┌─────────┐ ┌──────────┐
│ 关闭 │───────→│ 打开 │───────→│ 半打开 │
│(Closed)│ │ (Open) │ │(Half-Open)│
└───────┘ └─────────┘ └──────────┘
↑ │ │
│ 成功率正常 │ 失败率 ≥ 50% │
└──────────────────┘←──────────────────┘
成功率正常
| 状态 | 说明 |
|---|---|
| 关闭(Closed) | 正常状态,所有请求正常通过。滑动窗口统计失败率 |
| 打开(Open) | 熔断状态,所有请求直接走降级逻辑(Fallback)。持续 5 秒 |
| 半打开(Half-Open) | 熔断超时后进入半打开状态,放行少量请求试探测。如果成功则恢复关闭;如果继续失败则重新打开 |
实际项目中,SpringCloud Alibaba 更推荐使用 Sentinel 替代 Hystrix,Sentinel 提供了更丰富的限流、熔断、降级功能和可视化控制台。