# Java中级开发核心面试补充
> 🎯 针对中级开发:消息队列、分布式、异常处理、Maven依赖、基础易错题
> 涵盖:RabbitMQ、Nacos、Elasticsearch、XXL-Job、异常处理规范、Maven实战
---
## 📚 目录
1. [消息队列RabbitMQ](#一消息队列rabbitmq)
2. [分布式Nacos](#二分布式nacos)
3. [搜索引擎Elasticsearch](#三搜索引擎elasticsearch)
4. [定时任务XXL-Job](#四定时任务xxl-job)
5. [异常处理规范](#五异常处理规范)
6. [Maven依赖管理](#六maven依赖管理)
7. [基础易错题](#七基础易错题)
8. [安全认证JWT](#八安全认证jwt)
---
# 一、消息队列RabbitMQ
## 1.1 为什么要用MQ?
**面试回答模板**:
> 我们项目中用RabbitMQ主要解决3个问题:
>
> 1. **异步解耦**:下单后发送短信、更新积分,不影响主流程
> 2. **流量削峰**:秒杀时10万请求打到MQ,慢慢消费
> 3. **最终一致性**:订单创建后,通过MQ保证库存、优惠券等数据最终一致
## 1.2 核心概念
```
Producer(生产者)→ Exchange(交换机)→ Queue(队列)→ Consumer(消费者)
交换机类型:
- Direct:精确匹配(routingKey完全相同)
- Fanout:广播(不管routingKey,发给所有队列)
- Topic:模糊匹配(支持通配符 * 和 #)
- Headers:根据消息头匹配(很少用)
```
## 1.3 Spring Boot集成
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
```
```yaml
spring:
rabbitmq:
host: 192.168.1.100
port: 5672
username: admin
password: admin
virtual-host: /
# 开启消息确认(重要!)
publisher-confirm-type: correlated # 发送确认
publisher-returns: true # 发送失败回调
listener:
simple:
acknowledge-mode: manual # 手动ACK
retry:
enabled: true
max-attempts: 3 # 最多重试3次
initial-interval: 2000 # 首次重试间隔2秒
```
## 1.4 发送消息(生产者)
```java
/**
* RabbitMQ配置
*/
@Configuration
public class RabbitMQConfig {
/**
* 订单交换机
*/
@Bean
public DirectExchange orderExchange() {
return new DirectExchange("order.exchange", true, false);
}
/**
* 订单队列
*/
@Bean
public Queue orderQueue() {
return QueueBuilder.durable("order.queue")
.ttl(60000) // 消息TTL:60秒
.maxLength(10000) // 队列最大长度
.deadLetterExchange("order.dlx.exchange") // 死信交换机
.deadLetterRoutingKey("order.dlx")
.build();
}
/**
* 绑定
*/
@Bean
public Binding orderBinding() {
return BindingBuilder
.bind(orderQueue())
.to(orderExchange())
.with("order.create"); // routingKey
}
/**
* 死信队列(处理失败的消息)
*/
@Bean
public DirectExchange orderDlxExchange() {
return new DirectExchange("order.dlx.exchange", true, false);
}
@Bean
public Queue orderDlxQueue() {
return new Queue("order.dlx.queue", true);
}
@Bean
public Binding orderDlxBinding() {
return BindingBuilder
.bind(orderDlxQueue())
.to(orderDlxExchange())
.with("order.dlx");
}
}
/**
* 发送消息
*/
@Service
@Slf4j
public class OrderProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送订单创建消息
*/
public void sendOrderMessage(Order order) {
// 设置消息ID(用于幂等处理)
String messageId = UUID.randomUUID().toString();
// 构建消息
OrderMessage message = new OrderMessage();
message.setOrderId(order.getId());
message.setUserId(order.getUserId());
message.setAmount(order.getTotalAmount());
message.setMessageId(messageId);
// 发送消息
rabbitTemplate.convertAndSend(
"order.exchange", // 交换机
"order.create", // routingKey
message, // 消息体
msg -> {
// 设置消息属性
msg.getMessageProperties().setMessageId(messageId);
msg.getMessageProperties().setExpiration("60000"); // 过期时间60秒
return msg;
}
);
log.info("发送订单消息: orderId={}, messageId={}", order.getId(), messageId);
}
/**
* 发送延迟消息(订单超时取消)
*/
public void sendDelayMessage(Long orderId, int delaySeconds) {
rabbitTemplate.convertAndSend(
"order.delay.exchange",
"order.cancel",
orderId,
msg -> {
// 设置延迟时间
msg.getMessageProperties().setDelay(delaySeconds * 1000);
return msg;
}
);
log.info("发送延迟取消消息: orderId={}, delay={}s", orderId, delaySeconds);
}
}
```
## 1.5 消费消息(消费者)
```java
/**
* 订单消息消费者
*/
@Component
@Slf4j
public class OrderConsumer {
@Autowired
private OrderService orderService;
@Autowired
private StockService stockService;
/**
* 消费订单创建消息
*
* 注意:
* 1. 手动ACK
* 2. 幂等处理(消息可能重复消费)
* 3. 异常处理(失败消息进入死信队列)
*/
@RabbitListener(queues = "order.queue")
public void handleOrderMessage(OrderMessage message, Channel channel, Message mqMessage) throws IOException {
long deliveryTag = mqMessage.getMessageProperties().getDeliveryTag();
String messageId = message.getMessageId();
try {
log.info("收到订单消息: orderId={}, messageId={}", message.getOrderId(), messageId);
// 1. 幂等检查(防止重复消费)
if (isMessageProcessed(messageId)) {
log.warn("消息已处理过,跳过: messageId={}", messageId);
channel.basicAck(deliveryTag, false); // 确认消息
return;
}
// 2. 业务处理
// 扣减库存
stockService.deductByOrderId(message.getOrderId());
// 增加积分
int points = message.getAmount().divide(new BigDecimal("100"), 0, RoundingMode.DOWN).intValue();
pointsService.add(message.getUserId(), points, "订单消费赠送");
// 3. 标记消息已处理
markMessageProcessed(messageId);
// 4. 手动ACK(确认消息)
channel.basicAck(deliveryTag, false);
log.info("订单消息处理成功: orderId={}", message.getOrderId());
} catch (BusinessException e) {
// 业务异常:直接拒绝,进入死信队列
log.error("订单消息处理失败(业务异常): orderId={}", message.getOrderId(), e);
channel.basicReject(deliveryTag, false); // 拒绝消息,不重新入队
} catch (Exception e) {
// 系统异常:可能是临时故障,重新入队重试
log.error("订单消息处理失败(系统异常): orderId={}", message.getOrderId(), e);
// 检查重试次数
Integer retryCount = (Integer) mqMessage.getMessageProperties().getHeaders().get("retry-count");
if (retryCount == null) {
retryCount = 0;
}
if (retryCount < 3) {
// 重试次数未达上限,重新入队
channel.basicNack(deliveryTag, false, true); // 重新入队
} else {
// 重试次数达到上限,进入死信队列
channel.basicReject(deliveryTag, false);
}
}
}
/**
* 消费死信队列(人工处理)
*/
@RabbitListener(queues = "order.dlx.queue")
public void handleDlxMessage(OrderMessage message, Channel channel, Message mqMessage) throws IOException {
long deliveryTag = mqMessage.getMessageProperties().getDeliveryTag();
log.error("收到死信消息: orderId={}, messageId={}", message.getOrderId(), message.getMessageId());
// 记录到数据库,人工处理
saveFailedMessage(message);
// 确认消息
channel.basicAck(deliveryTag, false);
}
// 幂等处理(Redis)
private boolean isMessageProcessed(String messageId) {
return redisTemplate.hasKey("mq:processed:" + messageId);
}
private void markMessageProcessed(String messageId) {
redisTemplate.opsForValue().set("mq:processed:" + messageId, "1", 24, TimeUnit.HOURS);
}
}
```
## 1.6 面试常见问题
**Q1:如何保证消息不丢失?**
**答**:三个环节都要保证:
1. **生产者 → MQ**:开启发送确认(publisher-confirm)
```java
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (!ack) {
log.error("消息发送失败: {}", cause);
// 重发或记录到数据库
}
});
```
2. **MQ存储**:队列和消息持久化
```java
new Queue("order.queue", true); // durable=true
```
3. **MQ → 消费者**:手动ACK
```java
channel.basicAck(deliveryTag, false);
```
---
**Q2:如何保证消息不重复消费?**
**答**:幂等处理
```java
// 方式1:Redis
if (redisTemplate.hasKey("mq:processed:" + messageId)) {
return; // 已处理过,跳过
}
// 方式2:数据库唯一索引
// 消息表:message_id设置唯一索引
// 插入失败说明已处理过
```
---
**Q3:如何保证消息顺序?**
**答**:
1. 单队列 + 单消费者(性能差)
2. 同一订单的消息用相同routingKey路由到同一队列
3. 消费者内部用队列或锁保证顺序
---
# 二、分布式Nacos
## 2.1 Nacos是什么?
```
Nacos = 配置中心 + 服务注册中心
替代:
- 配置中心:替代Apollo、Spring Cloud Config
- 服务注册:替代Eureka、Consul
```
## 2.2 配置中心使用
```xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
```
```yaml
# bootstrap.yml(优先级高于application.yml)
spring:
application:
name: order-service
cloud:
nacos:
config:
server-addr: 192.168.1.100:8848
namespace: dev # 开发环境
group: DEFAULT_GROUP
file-extension: yaml
# 共享配置
shared-configs:
- data-id: common.yaml
refresh: true # 支持动态刷新
```
**动态刷新配置**:
```java
@Component
@RefreshScope // 支持配置动态刷新
@ConfigurationProperties(prefix = "app")
@Data
public class AppConfig {
private String name;
private String version;
private Integer timeout;
}
// 使用
@Autowired
private AppConfig appConfig;
public void test() {
log.info("配置: {}", appConfig.getTimeout());
// 在Nacos修改配置后,这里会自动更新
}
```
## 2.3 服务注册与发现
```xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
```
```yaml
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.1.100:8848
namespace: dev
group: DEFAULT_GROUP
# 元数据
metadata:
version: 1.0
region: cn-north
```
**服务调用(OpenFeign)**:
```java
@FeignClient(name = "order-service")
public interface OrderClient {
@GetMapping("/order/{id}")
Order getOrder(@PathVariable Long id);
}
// 使用
@Autowired
private OrderClient orderClient;
public void test() {
Order order = orderClient.getOrder(1L);
// OpenFeign自动从Nacos获取order-service的地址并负载均衡调用
}
```
---
# 三、搜索引擎Elasticsearch
## 3.1 为什么要用ES?
**面试回答**:
> 我们商品数据有500万条,用MySQL的LIKE查询:
> - SELECT * FROM product WHERE name LIKE '%手机%'
> - 不走索引,全表扫描,非常慢
>
> 用了ES后:
> - 全文检索,毫秒级响应
> - 支持分词、高亮、聚合统计
> - 可以按销量、价格等排序
## 3.2 核心概念
```
MySQL → Elasticsearch
数据库(DB) → 索引(Index)
表(Table) → 类型(Type,7.x后废弃)
行(Row) → 文档(Document)
列(Column) → 字段(Field)
```
## 3.3 Spring Boot集成
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
```
```yaml
spring:
elasticsearch:
rest:
uris: http://192.168.1.100:9200
username: elastic
password: elastic123
```
## 3.4 实战:商品搜索
```java
/**
* ES文档定义
*/
@Document(indexName = "product")
@Data
public class ProductDocument {
@Id
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word") // 分词
private String name;
@Field(type = FieldType.Keyword) // 不分词
private String brandName;
@Field(type = FieldType.Double)
private BigDecimal price;
@Field(type = FieldType.Integer)
private Integer sales;
@Field(type = FieldType.Date, format = DateFormat.date_time)
private Date createTime;
}
/**
* Repository
*/
public interface ProductRepository extends ElasticsearchRepository<ProductDocument, Long> {
// 根据名称搜索
List<ProductDocument> findByName(String name);
// 价格区间
List<ProductDocument> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
}
/**
* 搜索服务
*/
@Service
public class ProductSearchService {
@Autowired
private ElasticsearchRestTemplate elasticsearchTemplate;
/**
* 复杂搜索
*/
public Page<ProductDocument> search(String keyword, BigDecimal minPrice, BigDecimal maxPrice, int page, int size) {
// 构建查询
NativeSearchQuery query = new NativeSearchQueryBuilder()
// 1. 全文检索(匹配名称)
.withQuery(QueryBuilders.matchQuery("name", keyword))
// 2. 过滤条件
.withFilter(QueryBuilders.rangeQuery("price").gte(minPrice).lte(maxPrice))
// 3. 排序
.withSort(SortBuilders.fieldSort("sales").order(SortOrder.DESC))
// 4. 分页
.withPageable(PageRequest.of(page, size))
// 5. 高亮
.withHighlightFields(
new HighlightBuilder.Field("name")
.preTags("<em>")
.postTags("</em>")
)
.build();
// 执行查询
SearchHits<ProductDocument> searchHits = elasticsearchTemplate.search(query, ProductDocument.class);
// 处理高亮
List<ProductDocument> products = searchHits.stream()
.map(hit -> {
ProductDocument product = hit.getContent();
// 获取高亮内容
List<String> highlights = hit.getHighlightField("name");
if (!highlights.isEmpty()) {
product.setName(highlights.get(0));
}
return product;
})
.collect(Collectors.toList());
return new PageImpl<>(products, PageRequest.of(page, size), searchHits.getTotalHits());
}
/**
* 聚合统计(按品牌分组统计)
*/
public Map<String, Long> aggregateByBrand() {
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withAggregations(
AggregationBuilders.terms("brand_agg").field("brandName")
)
.build();
SearchHits<ProductDocument> searchHits = elasticsearchTemplate.search(query, ProductDocument.class);
// 解析聚合结果
Aggregations aggregations = searchHits.getAggregations();
Terms brandAgg = aggregations.get("brand_agg");
return brandAgg.getBuckets().stream()
.collect(Collectors.toMap(
Terms.Bucket::getKeyAsString,
Terms.Bucket::getDocCount
));
}
}
```
## 3.5 数据同步(MySQL → ES)
```java
/**
* 同步策略
*
* 方案1:定时全量同步(简单但有延迟)
* 方案2:MQ增量同步(实时性好)
* 方案3:Canal监听binlog(推荐)
*/
@Component
public class ProductSyncTask {
@Autowired
private ProductMapper productMapper;
@Autowired
private ProductRepository productRepository;
/**
* 方案1:定时全量同步
*/
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点
public void syncAll() {
log.info("开始同步商品数据到ES");
int pageSize = 1000;
int pageNum = 1;
while (true) {
Page<Product> page = new Page<>(pageNum, pageSize);
productMapper.selectPage(page, null);
if (page.getRecords().isEmpty()) {
break;
}
// 转换为ES文档
List<ProductDocument> documents = page.getRecords().stream()
.map(this::convertToDocument)
.collect(Collectors.toList());
// 批量保存到ES
productRepository.saveAll(documents);
pageNum++;
}
log.info("商品数据同步完成");
}
/**
* 方案2:MQ增量同步
*/
@RabbitListener(queues = "product.sync.queue")
public void syncFromMQ(ProductSyncMessage message) {
if ("INSERT".equals(message.getType()) || "UPDATE".equals(message.getType())) {
// 查询最新数据
Product product = productMapper.selectById(message.getProductId());
ProductDocument document = convertToDocument(product);
productRepository.save(document);
} else if ("DELETE".equals(message.getType())) {
productRepository.deleteById(message.getProductId());
}
}
}
```
---
# 四、定时任务XXL-Job
## 4.1 为什么用XXL-Job?
**Spring @Scheduled的问题**:
```
1. 单机执行,没有分片能力
2. 没有任务监控、日志
3. 没有失败重试、告警
4. 不支持动态修改cron表达式
```
**XXL-Job优势**:
```
1. 分片执行(大数据量任务)
2. 任务监控、执行日志
3. 失败自动重试、邮件告警
4. Web界面管理
5. 支持多种执行模式(Bean、Shell、Python等)
```
## 4.2 集成使用
```xml
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.3.1</version>
</dependency>
```
```yaml
xxl:
job:
admin:
addresses: http://192.168.1.100:8080/xxl-job-admin # 调度中心地址
executor:
appname: order-service # 执行器名称
ip:
port: 9999
logpath: /data/applogs/xxl-job
logretentiondays: 30
accessToken: default_token
```
```java
@Configuration
public class XxlJobConfig {
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.executor.appname}")
private String appname;
@Value("${xxl.job.executor.port}")
private int port;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
XxlJobSpringExecutor executor = new XxlJobSpringExecutor();
executor.setAdminAddresses(adminAddresses);
executor.setAppname(appname);
executor.setPort(port);
return executor;
}
}
```
## 4.3 任务开发
```java
@Component
public class OrderJobHandler {
/**
* 简单任务
*/
@XxlJob("orderTimeoutCancelJob")
public void orderTimeoutCancel() {
XxlJobHelper.log("开始处理超时订单");
// 查询30分钟未支付的订单
Date timeoutTime = DateUtils.addMinutes(new Date(), -30);
List<Order> orders = orderMapper.selectList(
Wrappers.<Order>lambdaQuery()
.eq(Order::getStatus, 0) // 待支付
.lt(Order::getCreateTime, timeoutTime)
);
// 批量取消
for (Order order : orders) {
orderService.cancel(order.getId(), "超时未支付自动取消");
}
XxlJobHelper.log("处理完成,取消订单数: {}", orders.size());
}
/**
* 分片任务(大数据量)
*
* 场景:每天给100万用户发送营销短信
* 配置:10个执行器实例,每个分片处理10万
*/
@XxlJob("userMarketingSmsJob")
public void sendMarketingSms() {
// 获取分片参数
int shardIndex = XxlJobHelper.getShardIndex(); // 当前分片序号(0-9)
int shardTotal = XxlJobHelper.getShardTotal(); // 总分片数(10)
XxlJobHelper.log("分片参数: index={}, total={}", shardIndex, shardTotal);
// 查询当前分片的用户
// 用户ID % shardTotal == shardIndex
List<User> users = userMapper.selectList(
new LambdaQueryWrapper<User>()
.apply("MOD(id, {0}) = {1}", shardTotal, shardIndex)
);
// 发送短信
for (User user : users) {
smsService.send(user.getPhone(), "营销短信内容");
}
XxlJobHelper.log("分片{}处理完成,发送短信数: {}", shardIndex, users.size());
}
/**
* 带参数的任务
*/
@XxlJob("dataExportJob")
public void dataExport() {
// 从调度中心获取参数
String param = XxlJobHelper.getJobParam();
JSONObject params = JSON.parseObject(param);
Date startDate = params.getDate("startDate");
Date endDate = params.getDate("endDate");
XxlJobHelper.log("导出参数: startDate={}, endDate={}", startDate, endDate);
// 导出数据
List<Order> orders = orderMapper.selectList(
Wrappers.<Order>lambdaQuery()
.between(Order::getCreateTime, startDate, endDate)
);
// 生成Excel
String filePath = exportService.export(orders);
XxlJobHelper.log("导出完成: filePath={}", filePath);
}
}
```
---
# 五、异常处理规范
## 5.1 异常体系设计
```java
/**
* 业务异常基类
*/
public class BusinessException extends RuntimeException {
private String code; // 错误码
private String message; // 错误信息
public BusinessException(String message) {
super(message);
this.code = "BUSINESS_ERROR";
this.message = message;
}
public BusinessException(String code, String message) {
super(message);
this.code = code;
this.message = message;
}
public BusinessException(ErrorCodeEnum errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
this.message = errorCode.getMessage();
}
}
/**
* 错误码枚举
*/
@Getter
@AllArgsConstructor
public enum ErrorCodeEnum {
// 通用错误 1xxxx
PARAM_ERROR("10001", "参数错误"),
NOT_FOUND("10002", "资源不存在"),
SYSTEM_ERROR("10003", "系统异常"),
// 用户相关 2xxxx
USER_NOT_EXIST("20001", "用户不存在"),
USER_DISABLED("20002", "用户已被禁用"),
PASSWORD_ERROR("20003", "密码错误"),
// 订单相关 3xxxx
ORDER_NOT_EXIST("30001", "订单不存在"),
ORDER_STATUS_ERROR("30002", "订单状态异常"),
STOCK_NOT_ENOUGH("30003", "库存不足"),
// 支付相关 4xxxx
PAY_TIMEOUT("40001", "支付超时"),
PAY_FAILED("40002", "支付失败");
private String code;
private String message;
}
```
## 5.2 全局异常处理
```java
/**
* 全局异常处理器
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 业务异常
*/
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e) {
log.error("业务异常: code={}, message={}", e.getCode(), e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
/**
* 参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
String message = bindingResult.getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining(", "));
log.error("参数校验失败: {}", message);
return Result.error("10001", message);
}
/**
* 空指针异常
*/
@ExceptionHandler(NullPointerException.class)
public Result handleNullPointerException(NullPointerException e) {
log.error("空指针异常", e);
return Result.error("10003", "系统异常,请联系管理员");
}
/**
* 数据库异常
*/
@ExceptionHandler(DataAccessException.class)
public Result handleDataAccessException(DataAccessException e) {
log.error("数据库异常", e);
return Result.error("10003", "数据库异常,请稍后重试");
}
/**
* 其他异常
*/
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
log.error("未知异常", e);
return Result.error("10003", "系统异常,请联系管理员");
}
}
```
## 5.3 异常使用规范
```java
@Service
public class OrderService {
/**
* ✅ 正确:业务异常直接抛出
*/
public void createOrder(OrderCreateRequest request) {
// 校验用户
User user = userService.getById(request.getUserId());
if (user == null) {
throw new BusinessException(ErrorCodeEnum.USER_NOT_EXIST);
}
// 校验库存
Integer stock = stockService.getStock(request.getProductId());
if (stock < request.getQuantity()) {
throw new BusinessException(ErrorCodeEnum.STOCK_NOT_ENOUGH);
}
// 创建订单
Order order = new Order();
// ...
orderMapper.insert(order);
}
/**
* ❌ 错误:捕获异常但不处理
*/
public void badExample1() {
try {
// 业务逻辑
} catch (Exception e) {
e.printStackTrace(); // 不要这样!
}
}
/**
* ❌ 错误:吞掉异常
*/
public void badExample2() {
try {
// 业务逻辑
} catch (Exception e) {
// 什么都不做,异常被吞掉了
}
}
/**
* ✅ 正确:捕获后记录日志并抛出
*/
public void goodExample() {
try {
// 调用第三方API
payService.pay(request);
} catch (Exception e) {
log.error("支付失败: orderId={}", orderId, e);
throw new BusinessException(ErrorCodeEnum.PAY_FAILED);
}
}
/**
* ✅ 正确:finally关闭资源
*/
public void closeResource() {
FileInputStream fis = null;
try {
fis = new FileInputStream("file.txt");
// 读取文件
} catch (IOException e) {
log.error("读取文件失败", e);
throw new BusinessException("文件读取失败");
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
log.error("关闭文件流失败", e);
}
}
}
}
/**
* ✅ 更好:try-with-resources
*/
public void closeResourceBetter() {
try (FileInputStream fis = new FileInputStream("file.txt")) {
// 读取文件
} catch (IOException e) {
log.error("读取文件失败", e);
throw new BusinessException("文件读取失败");
}
// 自动关闭资源
}
}
```
---
# 六、Maven依赖管理
## 6.1 依赖冲突问题
**面试常问**:项目启动报NoSuchMethodError或ClassNotFoundException,怎么排查?
**答**:Maven依赖冲突导致的。
### 6.1.1 依赖传递
```
A依赖B,B依赖C → A会自动依赖C(传递依赖)
项目
└── spring-boot-starter-web
└── spring-boot-starter
└── spring-core (5.3.10)
└── spring-webmvc
└── spring-core (5.2.8) ← 冲突!
Maven选择规则:
1. 最短路径优先
2. 同路径长度,先声明优先
```
### 6.1.2 查看依赖树
```bash
# 查看完整依赖树
mvn dependency:tree
# 查看冲突
mvn dependency:tree -Dverbose
# 指定模块
mvn dependency:tree -pl uf-fny-mall-service
# 输出到文件
mvn dependency:tree > dependency.txt
```
### 6.1.3 解决冲突
```xml
<!-- 方式1:排除依赖 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>some-library</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 方式2:显式声明版本(最短路径优先)-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.10</version>
</dependency>
<!-- 方式3:统一版本管理(推荐)-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.10</version>
</dependency>
</dependencies>
</dependencyManagement>
```
## 6.2 多模块项目管理
```xml
<!-- 父POM:fny-business/pom.xml -->
<project>
<groupId>cn.ufood.fny</groupId>
<artifactId>fny-business</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<!-- 子模块 -->
<modules>
<module>uf-fny-mall-api</module>
<module>uf-fny-mall-service</module>
<module>uf-fny-mall-dao</module>
<module>uf-fny-mall-provider</module>
</modules>
<!-- 统一版本管理 -->
<properties>
<spring-boot.version>2.7.5</spring-boot.version>
<mybatis-plus.version>3.5.2</mybatis-plus.version>
<mysql.version>8.0.30</mysql.version>
</properties>
<!-- 依赖管理(只声明,不引入)-->
<dependencyManagement>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
<!-- 子模块:uf-fny-mall-service/pom.xml -->
<project>
<parent>
<groupId>cn.ufood.fny</groupId>
<artifactId>fny-business</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>uf-fny-mall-service</artifactId>
<dependencies>
<!-- 不需要写version,继承父POM -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- 依赖其他模块 -->
<dependency>
<groupId>cn.ufood.fny</groupId>
<artifactId>uf-fny-mall-dao</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>
```
## 6.3 常用Maven命令
```bash
# 编译
mvn clean compile
# 打包
mvn clean package
# 跳过测试打包
mvn clean package -DskipTests
# 安装到本地仓库
mvn clean install
# 只编译指定模块
mvn clean install -pl uf-fny-mall-service -am
# -pl: 指定模块
# -am: 同时构建依赖的模块
# 清理target目录
mvn clean
# 查看有效POM
mvn help:effective-pom
# 更新依赖
mvn dependency:resolve
# 下载源码
mvn dependency:sources
```
---
# 七、基础易错题
## 7.1 Java基础
### Q1:== 和 equals的区别?
**答**:
```java
// ==:比较引用地址
String s1 = new String("hello");
String s2 = new String("hello");
s1 == s2; // false(不同对象,地址不同)
// equals:比较内容(String重写了equals)
s1.equals(s2); // true
// 陷阱:字符串常量池
String s3 = "hello";
String s4 = "hello";
s3 == s4; // true(指向常量池同一个对象)
// 包装类陷阱
Integer i1 = 127;
Integer i2 = 127;
i1 == i2; // true(-128到127有缓存)
Integer i3 = 128;
Integer i4 = 128;
i3 == i4; // false(超出缓存范围)
```
---
### Q2:String、StringBuilder、StringBuffer的区别?
**答**:
```
String:不可变,线程安全,每次拼接都会创建新对象
StringBuilder:可变,线程不安全,性能最好
StringBuffer:可变,线程安全(方法加了synchronized),性能较差
使用场景:
- 少量字符串拼接:String(+ 或 concat)
- 大量字符串拼接(单线程):StringBuilder
- 大量字符串拼接(多线程):StringBuffer
```
```java
// 性能对比(拼接10000次)
// String:~5000ms
String s = "";
for (int i = 0; i < 10000; i++) {
s += "a"; // 每次都创建新对象
}
// StringBuilder:~1ms
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append("a");
}
```
---
### Q3:ArrayList和LinkedList的区别?
**答**:
```
ArrayList:数组实现
- 查询快:O(1)
- 插入/删除慢:O(n)(需要移动元素)
- 内存连续
LinkedList:双向链表实现
- 查询慢:O(n)
- 插入/删除快:O(1)(只需改指针)
- 内存不连续
使用场景:
- 频繁查询:ArrayList
- 频繁插入/删除:LinkedList
- 实际开发:99%用ArrayList
```
---
### Q4:HashMap的底层原理?
**答**:
```
JDK 1.7:数组 + 链表
JDK 1.8:数组 + 链表 + 红黑树
put流程:
1. 计算key的hash值
2. 找到数组索引:index = hash & (length - 1)
3. 如果位置为空,直接放入
4. 如果位置有值:
- key相同,覆盖value
- key不同,挂在链表/红黑树上
5. 链表长度>=8且数组长度>=64,转红黑树
扩容机制:
- 初始容量:16
- 负载因子:0.75
- 扩容时机:size > capacity * 0.75
- 扩容大小:2倍
```
```java
// 面试易错点
Map<String, String> map = new HashMap<>();
map.put(null, "value"); // ✅ 允许null key
map.put("key", null); // ✅ 允许null value
// ConcurrentHashMap不允许null
ConcurrentHashMap<String, String> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put(null, "value"); // ❌ NullPointerException
concurrentMap.put("key", null); // ❌ NullPointerException
```
---
### Q5:final、finally、finalize的区别?
**答**:
```
final:修饰符
- 修饰类:不能被继承
- 修饰方法:不能被重写
- 修饰变量:不能被修改(常量)
finally:异常处理
- try-catch-finally中的代码块
- 无论是否发生异常都会执行
- 常用于关闭资源
finalize:方法
- Object类的方法
- GC回收对象前调用
- 不推荐使用(已过时)
```
```java
// finally陷阱题
public int test() {
try {
return 1;
} finally {
return 2; // finally的return会覆盖try的return
}
}
// 结果:2
public int test2() {
int i = 0;
try {
i = 1;
return i; // 返回1(finally之前保存返回值)
} finally {
i = 2; // 修改不影响返回值
}
}
// 结果:1
```
---
## 7.2 并发编程
### Q6:synchronized和Lock的区别?
**答**:
```
synchronized:
- 关键字,JVM实现
- 自动释放锁
- 不可中断
- 非公平锁
- 适合简单场景
Lock(ReentrantLock):
- 接口,JDK实现
- 手动释放锁(finally)
- 可中断(lockInterruptibly)
- 可设置公平/非公平
- 可尝试获取锁(tryLock)
- 可绑定多个Condition
使用场景:
- 简单同步:synchronized
- 需要高级特性:Lock
```
```java
// synchronized
public synchronized void method() {
// 业务逻辑
} // 自动释放锁
// Lock
private Lock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock(); // 必须手动释放
}
}
// Lock高级特性
// 1. 尝试获取锁
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
// 业务逻辑
} finally {
lock.unlock();
}
} else {
// 获取锁失败的处理
}
// 2. 可中断
try {
lock.lockInterruptibly(); // 可被interrupt()中断
// 业务逻辑
} catch (InterruptedException e) {
// 处理中断
} finally {
lock.unlock();
}
```
---
### Q7:volatile的作用?
**答**:
```
作用1:保证可见性
- 线程修改变量后,立即刷新到主内存
- 其他线程读取时,从主内存读取最新值
作用2:禁止指令重排序
不保证原子性!
```
```java
// 典型应用:双重检查锁(单例模式)
public class Singleton {
// 必须加volatile,防止指令重排序
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // ← 这里可能指令重排序
}
}
}
return instance;
}
}
// 为什么需要volatile?
// instance = new Singleton() 分3步:
// 1. 分配内存空间
// 2. 初始化对象
// 3. 将instance指向内存空间
//
// 可能重排序为:1 → 3 → 2
// 线程A执行到3,instance != null
// 线程B判断instance != null,直接返回
// 但此时对象还未初始化(步骤2未执行)
```
---
### Q8:ThreadLocal的作用和原理?
**答**:
```
作用:线程本地变量,每个线程有独立的副本
原理:
- Thread类有个threadLocals变量(ThreadLocalMap)
- ThreadLocalMap的key是ThreadLocal,value是变量值
- 每个线程操作自己的ThreadLocalMap
注意:使用完要remove(),否则内存泄漏!
```
```java
// 使用示例:保存用户信息
public class UserContext {
private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
public static void setUser(User user) {
userThreadLocal.set(user);
}
public static User getUser() {
return userThreadLocal.get();
}
public static void remove() {
userThreadLocal.remove(); // 必须调用!
}
}
// 拦截器中设置
@Component
public class UserInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, ...) {
String userId = request.getHeader("userId");
User user = userService.getById(userId);
UserContext.setUser(user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, ...) {
UserContext.remove(); // 一定要清理!
}
}
// 业务代码直接获取
public void createOrder() {
User user = UserContext.getUser();
// 不需要传参,直接获取当前用户
}
```
---
# 八、安全认证JWT
## 8.1 JWT是什么?
```
JWT = JSON Web Token
传统Session:
用户登录 → 服务端生成Session → 存储到Redis → 返回SessionId给客户端
JWT:
用户登录 → 服务端生成Token(包含用户信息) → 返回Token → 客户端每次请求带上Token
优点:
1. 无状态,不需要存储(适合分布式)
2. 跨域友好
缺点:
1. Token较大
2. 无法主动让Token失效(需要配合Redis黑名单)
```
## 8.2 JWT结构
```
JWT = Header.Payload.Signature
Header(头部):
{
"alg": "HS256", // 算法
"typ": "JWT" // 类型
}
Payload(载荷):
{
"userId": 123,
"username": "张三",
"exp": 1640000000 // 过期时间
}
Signature(签名):
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
```
## 8.3 实战代码
```xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
```
```java
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret; // 密钥,要保密!
@Value("${jwt.expiration}")
private Long expiration; // 过期时间(秒)
/**
* 生成Token
*/
public String generateToken(User user) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("username", user.getUsername());
return Jwts.builder()
.setClaims(claims)
.setSubject(user.getUsername())
.setIssuedAt(new Date()) // 签发时间
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)) // 过期时间
.signWith(SignatureAlgorithm.HS512, secret) // 签名
.compact();
}
/**
* 解析Token
*/
public Claims parseToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
throw new BusinessException("Token已过期");
} catch (Exception e) {
throw new BusinessException("Token无效");
}
}
/**
* 从Token获取用户ID
*/
public Long getUserId(String token) {
Claims claims = parseToken(token);
return claims.get("userId", Long.class);
}
/**
* 验证Token
*/
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 刷新Token
*/
public String refreshToken(String token) {
Claims claims = parseToken(token);
claims.setIssuedAt(new Date());
claims.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000));
return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
}
/**
* 登录
*/
@RestController
public class AuthController {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserService userService;
@PostMapping("/login")
public Result login(@RequestBody LoginRequest request) {
// 1. 验证用户名密码
User user = userService.getByUsername(request.getUsername());
if (user == null || !user.getPassword().equals(request.getPassword())) {
throw new BusinessException("用户名或密码错误");
}
// 2. 生成Token
String token = jwtUtil.generateToken(user);
// 3. 返回
return Result.success(Map.of("token", token));
}
}
/**
* JWT拦截器
*/
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, ...) {
// 1. 获取Token
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
throw new BusinessException("未登录");
}
token = token.substring(7); // 去掉"Bearer "
// 2. 验证Token
if (!jwtUtil.validateToken(token)) {
throw new BusinessException("Token无效或已过期");
}
// 3. 解析Token,保存用户信息
Long userId = jwtUtil.getUserId(token);
User user = userService.getById(userId);
UserContext.setUser(user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, ...) {
UserContext.remove();
}
}
```
---
## 📝 面试速记卡
```
【RabbitMQ】
三要素:Producer → Exchange → Queue → Consumer
保证消息不丢:发送确认 + 持久化 + 手动ACK
保证不重复:幂等处理(Redis/数据库唯一索引)
死信队列:处理失败消息
【Nacos】
配置中心:@RefreshScope动态刷新
服务发现:OpenFeign + Nacos自动负载均衡
【Elasticsearch】
全文检索:比MySQL LIKE快100倍
分词器:ik_max_word
高亮:preTags + postTags
聚合:AggregationBuilders
【XXL-Job】
解决@Scheduled问题:分片、监控、重试
分片任务:MOD(id, shardTotal) = shardIndex
【异常处理】
业务异常:继承RuntimeException + 错误码
全局处理:@RestControllerAdvice + @ExceptionHandler
规范:不要吞异常、finally关闭资源、try-with-resources
【Maven】
依赖冲突:mvn dependency:tree查看
解决方式:exclusions排除 / 显式声明版本
多模块:父POM统一版本管理(dependencyManagement)
【基础易错】
== vs equals:地址 vs 内容
String vs StringBuilder:不可变 vs 可变
ArrayList vs LinkedList:数组 vs 链表
HashMap:数组+链表+红黑树,JDK8链表长度>=8转红黑树
synchronized vs Lock:关键字 vs 接口,自动 vs 手动
volatile:可见性+禁止重排序,不保证原子性
ThreadLocal:线程本地变量,用完必须remove()
【JWT】
结构:Header.Payload.Signature
生成:Jwts.builder()
解析:Jwts.parser()
拦截器:验证Token + 保存用户到ThreadLocal
```
---
**文档版本**:v1.0
**创建时间**:2026-01-19
**适用场景**:Java中级开发面试