Administrator
发布于 2026-03-16 / 0 阅读
0
0

Java中级开发核心面试补充

# 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中级开发面试


评论