Spring Boot 拦截器:解锁5大实用场景

一、Spring Boot中拦截器是什么

  在Spring Boot中,拦截器(Interceptor)是一种基于AOP(面向切面编程)思想的组件,用于在请求处理前后插入自定义逻辑,实现权限校验、日志记录、性能监控等非业务功能。

  其核心作用是在不修改业务代码的前提下,对请求进行统一处理,类似于Servlet中的Filter,但更贴近Spring MVC的体系。

二、拦截器与过滤器的区别

在这里插入图片描述

三、拦截器主要方法

在实现拦截器时,HandlerInterceptor接口提供了三个主要方法,它们在请求处理的不同阶段发挥着重要作用。

preHandle
  这个方法在请求进入Controller之前被调用 。它的返回值是一个布尔类型,如果返回true,请求将继续被处理,进入Controller。
   如果返回false,请求将被拦截,后续的Controller方法将不会被执行。在这个方法中,我们通常可以进行一些前置的处理操作,比如用户认证、权限校验、日志记录等。​

postHandle
  当Controller方法执行完毕后,视图渲染之前,这个方法会被调用 。

  在这个方法中,我们可以对ModelAndView进行一些操作,比如添加额外的模型数据,修改视图名称等。需要注意的是,如果在preHandle方法中返回了false,这个方法将不会被执行。​

afterCompletion
  在整个请求处理完成,包括视图渲染之后,这个方法会被调用。无论请求处理过程中是否发生异常,只要preHandle方法返回true,这个方法就会被执行。

  我们可以在这个方法中进行一些资源清理、记录日志等收尾工作。如果请求处理过程中发生了异常,异常信息会通过 ex 参数传递进来,我们可以根据这个参数进行相应的异常处理。

@Component
public class MyInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 在请求处理之前执行的逻辑
        System.out.println("preHandle被调用,请求即将进入Controller");
        return true; // 返回true表示放行请求,返回false则拦截请求
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 在请求处理之后,视图渲染之前执行的逻辑
        System.out.println("postHandle被调用,Controller方法已执行完毕,视图即将渲染");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 在整个请求处理完成,包括视图渲染之后执行的逻辑
        System.out.println("afterCompletion被调用,整个请求已处理完毕");
    }
}
@Configuration
public class WebMvcConfig1 implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyInterceptor())
               .addPathPatterns("/**") // 拦截所有请求
                .excludePathPatterns("/static/**"); // 排除/static目录下的静态资源请求
    }
}

四、5 种常见的拦截器使用场景

1.用户认证拦截器

  用户认证拦截器的作用就是在用户请求进入 Controller 之前,验证用户的登录状态。如果用户已经登录,允许请求继续处理。如果用户未登录,则返回错误信息或者重定向到登录页面。

@Component
public class JwtHandlerInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @Autowired
    private UserInfoService userInfoService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        // 取出token
        String token = request.getHeader("token");
        if (!jwtTokenProvider.validateToken(token)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("{\"error\": \"Token已失效,请重新登录\"}");
            return false;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();

        // 判断方法上是否有NoAuth注解,如果有则跳过认证
        if (method.isAnnotationPresent(NoAuth.class)) {
            return true;
        }

        String username = jwtTokenProvider.getUsernameFromJWT(token);

        User user = userInfoService.getUserInfoByUserName(username);

        if (user == null) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("{\"error\": \"用户不存在\"}");
            return false;
        }

        // 判断角色权限
        HasRoles hasRoles = handlerMethod.getMethodAnnotation(HasRoles.class);
        if (!Objects.isNull(hasRoles)) {
            // 检查用户是否有所需角色
            String[] roles = hasRoles.value();
            boolean hasRole = false;

            // 角色校验 ...

            if (!hasRole) {
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                response.getWriter().write("{\"error\": \"权限不足\"}");
                return false;
            }
        }

        // 将用户信息放入请求属性
        request.setAttribute("currentUser", user);

        return true;
    }


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

  在WebMvcConfig配置类中,将JwtHandlerInterceptor注册到Spring的拦截器链中,并设置拦截路径为所有请求,排除了登录和注册接口,因为这两个接口不需要用户登录就可以访问。

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JwtHandlerInterceptor())
                .addPathPatterns("/**")// 拦截所有请求
                .excludePathPatterns("/login","/register");// 排除登录和注册接口
    }

}

  通过这样的配置,就可以实现对用户登录状态的验证,确保只有登录用户才能访问受保护的资源。

2.日志记录拦截器

  日志记录对于系统的运维和故障排查非常重要。日志记录拦截器可以在请求处理的前后记录请求的相关信息,比如请求的 URL、请求方法、请求参数、客户端 IP 等。这些日志信息可以帮助我们了解系统的运行情况,分析用户行为,在出现问题时快速定位问题。

@Component
public class RequestLoggingInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(RequestLoggingInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        logger.info("Request URL: {}, Method: {}, IP: {}", request.getRequestURI(), request.getMethod(), request.getRemoteAddr());

        // 记录请求参数
        Map<String, String[]> paramMap = request.getParameterMap();
        StringBuilder params = new StringBuilder();

        if (!paramMap.isEmpty()) {
            for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
                params.append(entry.getKey())
                        .append("=")
                        .append(String.join(",", entry.getValue()))
                        .append("&");
            }

            if (params.length() > 0) {
                params.deleteCharAt(params.length() - 1);
            }
        }

        // 记录请求体(仅POST/PUT/PATCH请求)
        String method = request.getMethod();
        String requestBody = "";
        if (HttpMethod.POST.matches(method) ||
                HttpMethod.PUT.matches(method) ||
                HttpMethod.PATCH.matches(method)) {

            // 使用包装请求对象来多次读取请求体
            ContentCachingRequestWrapper wrappedRequest =
                    new ContentCachingRequestWrapper(request);

            // 为了触发内容缓存,我们需要获取一次输入流
            if (wrappedRequest.getContentLength() > 0) {
                wrappedRequest.getInputStream().read();
                requestBody = new String(wrappedRequest.getContentAsByteArray(),
                        wrappedRequest.getCharacterEncoding());
            }
        }

        logger.info("Request URL: {}, Method: {}, IP: {},params: {},requestBody: {}", request.getRequestURI(), request.getMethod(), request.getRemoteAddr(), params, requestBody);

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        logger.info("Request to {} has been completed", request.getRequestURI());
        if (ex != null) {
            logger.error("An exception occurred during request handling", ex);
        }
    }
}

3.性能监控拦截器

  性能监控对于优化系统性能至关重要。性能监控拦截器可以用来计算和记录请求的处理时间,通过分析这些时间数据,我们可以找出系统中的性能瓶颈,进而进行针对性的优化。

@Component
public class PerformanceInterceptor implements HandlerInterceptor {

    private static final ThreadLocal<Long> startTimeThreadLocal = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        long startTime = System.currentTimeMillis();
        startTimeThreadLocal.set(startTime);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 计算请求处理时间
        long endTime = System.currentTimeMillis();
        long startTime = startTimeThreadLocal.get();
        long executeTime = endTime - startTime;
        System.out.println("Request URL: " + request.getRequestURL() + ", Execution Time: " + executeTime + "ms");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        startTimeThreadLocal.remove();
    }
}

4.接口限流拦截器
  在高并发场景下,接口限流是保护系统的重要手段 。接口限流拦截器可以限制单位时间内对某个接口的访问次数,防止因大量请求导致系统资源耗尽,从而保证系统的稳定性和可用性 。

@Component
public class AccessLimitInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        AccessLimit accessLimit = handlerMethod.getMethodAnnotation(AccessLimit.class);
        if (accessLimit == null) {
            return true;
        }
        int seconds = accessLimit.seconds();
        int maxCount = accessLimit.maxCount();
        boolean needLogin = accessLimit.needLogin();

        if (needLogin) {
            // 检查用户登录状态,这里省略具体实现
            // 如果未登录,返回错误信息
            return false;
        }

        String key = request.getRemoteAddr() + request.getRequestURI();
        Integer count = redisTemplate.opsForValue().get(key);
        if (count == null) {
            redisTemplate.opsForValue().set(key, 1, seconds, TimeUnit.SECONDS);
        } else if (count < maxCount) {
            redisTemplate.opsForValue().increment(key, 1);
        } else {
            render(response, "请求过于频繁,请稍后再试");
            return false;
        }
        return true;
    }
            
    private void render(HttpServletResponse response, String msg) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.write(msg);
        out.flush();
        out.close();
    }
            
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }
            
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }

}

5.数据加密解密拦截器
  在数据传输和存储过程中,保护敏感数据的安全至关重要。数据加密解密拦截器可以在请求到达Controller之前对敏感数据进行加密,在响应返回给客户端之前对加密数据进行解密,确保数据在传输和处理过程中的安全性。

  比如在用户登录时,对用户输入的密码进行加密后再传输到服务器。在从数据库中查询用户的身份证号、手机号等敏感信息时,对查询结果进行解密后再返回给前端。

@Component
public class EncryptionInterceptor implements HandlerInterceptor {

    /**
     * 密钥
     */
    private static final String KEY = "secret_key";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 假设请求参数中有一个名为"sensitiveData"的敏感数据需要加密
        String sensitiveData = request.getParameter("sensitiveData");
        if (sensitiveData != null) {
            SecretKey secretKey = new SecretKeySpec(KEY.getBytes(), "AES");
            byte[] encryptedData = AESUtil.encrypt(sensitiveData, secretKey);
            String encryptedDataStr = Base64.getEncoder().encodeToString(encryptedData);
            // 将加密后的数据重新放回请求参数中
            request.setAttribute("sensitiveData", encryptedDataStr);
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 假设响应中有一个名为"sensitiveResponseData"的敏感数据需要解密
        String sensitiveResponseData = (String) request.getAttribute("sensitiveResponseData");
        if (sensitiveResponseData != null) {
            SecretKey secretKey = new SecretKeySpec(KEY.getBytes(), "AES");
            byte[] decodedData = Base64.getDecoder().decode(sensitiveResponseData);
            String decryptedData = AESUtil.decrypt(decodedData, secretKey);
            // 将解密后的数据重新放回请求属性中,方便后续处理
            request.setAttribute("sensitiveResponseData", decryptedData);
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
}

五、总结

  在 Spring Boot 开发中,拦截器就像是一把 “万能钥匙”,为我们提供了丰富的功能扩展和业务处理的可能性。

  希望大家在实际项目中,能够根据业务需求,灵活地运用这些拦截器,让我们的 Spring Boot 应用更加健壮、高效、安全。

  同时,拦截器还有很多值得深入探讨的地方,比如如何在拦截器中优雅地处理事务、如何实现更加复杂的动态拦截规则等 ,后续我们可以一起继续探索。

  如果大家在使用拦截器的过程中有任何问题或心得,欢迎在留言区分享交流。

<think>嗯,用户现在想了解Spring Boot如何保证数据一致性相关的面试问题。首先,我需要确定用户的需求是什么。他们可能正在准备面试,需要整理这方面的知识点,或者在实际项目中遇到了数据一致性的问题。 接下来,我得回想一下数据一致性在Spring Boot中的常见处理方式。比如事务管理、分布式事务、乐观锁、幂等性设计、消息队列等等。这些都是面试中常被问到的点。 然后,用户可能想知道具体的实现方法和常见问题。比如,本地事务如何使用@Transactional注解,分布式事务框架如Seata的应用,还有如何通过乐观锁机制避免并发问题。此外,幂等性设计也是保证数据一致性的重要部分,特别是在接口重复提交的情况下。 还需要考虑用户是否有分布式系统的经验,或者只是在单体应用中的问题。比如,如果系统是微服务架构,可能需要用到分布式事务解决方案,而单体应用可能更关注数据库事务和锁机制。 另外,用户提到的面试问题,应该涵盖理论知识和实际案例。比如,如何处理跨服务的数据一致性问题,或者如何在并发场景下保证数据正确性。这些都需要结合Spring Boot的特性和相关技术栈来回答。 还要注意用户可能对某些术语不太熟悉,比如CAP定理、BASE理论,或者具体的框架如Seata、RocketMQ的事务消息。需要用简洁明了的语言解释这些概念,并给出实际应用的例子。 最后,应该整理出一个结构化的回答,分点说明不同的保证数据一致性的方法,每个方法的原理、实现步骤以及适用场景。同时,给出一些常见面试问题及其回答示例,帮助用户更好地准备面试。</think>以下是关于 Spring Boot 数据一致性保障的常见面试问题及核心知识点整理,涵盖单机事务、分布式事务、锁机制等高频考点: --- ### **一、单机事务** #### **1. `@Transactional` 注解原理** ```text - 底层依赖数据库事务(如MySQL的InnoDB引擎) - 基于AOP实现,通过TransactionInterceptor拦截方法 - 事务传播行为(默认PROPAGATION_REQUIRED) - 常见坑点:非public方法失效、异常类型不匹配(默认只回滚RuntimeException) ``` **示例代码**: ```java @Transactional(rollbackFor = Exception.class) public void transferMoney(Long from, Long to, BigDecimal amount) { accountDao.deduct(from, amount); // 模拟业务异常 if (amount.compareTo(BigDecimal.ZERO) < 0) { throw new RuntimeException("金额非法"); } accountDao.add(to, amount); } ``` --- ### **二、分布式事务** #### **1. CAP与BASE理论** ```text - CAP只能满足其中两个:一致性(C)、可用性(A)、分区容错性(P) - BASE理论:基本可用(Basically Available)、软状态(Soft State)、最终一致(Eventually Consistent) ``` #### **2. 常见解决方案** | **方案** | **实现方式** | **适用场景** | |-------------------|----------------------------------------|---------------------------| | **2PC** | 协调者+两阶段提交(prepare/commit) | 强一致、低并发场景 | | **TCC** | Try-Confirm-Cancel三阶段补偿 | 高一致性要求的金融交易 | | **本地消息表** | 数据库+消息队列(事务提交后异步通知) | 最终一致、高吞吐量场景 | | **Seata AT模式** | 全局锁+undo_log(自动反向补偿SQL) | 简单业务逻辑的分布式事务 | **Seata配置示例**: ```properties # seata-server配置 seata.tx-service-group=my_tx_group seata.service.vgroup-mapping.my_tx_group=default ``` --- ### **三、高并发场景下的数据一致性** #### **1. 乐观锁 vs 悲观锁** | **对比项** | 乐观锁 | 悲观锁 | |------------------|---------------------------------------|---------------------------| | 实现方式 | 版本号机制(如`@Version`) | `SELECT ... FOR UPDATE` | | 冲突检测 | 提交时校验版本 | 操作前加锁 | | 适用场景 | 读多写少 | 写多读少 | **JPA乐观锁实现**: ```java @Entity public class Product { @Id private Long id; @Version private Integer version; // 版本号字段 private Integer stock; } ``` #### **2. Redis分布式锁** ```java public boolean tryLock(String key, String value, long expireTime) { return redisTemplate.opsForValue() .setIfAbsent(key, value, expireTime, TimeUnit.SECONDS); } // 使用Lua脚本保证原子性解锁 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList(key), value); ``` --- ### **四、最终一致性保障** #### **1. 消息队列事务消息** ```text 实现步骤: 1. 发送半消息(RocketMQ事务消息) 2. 执行本地事务 3. 根据本地事务结果提交/回滚消息 4. 消费者幂等消费 ``` **RocketMQ事务消息示例**: ```java TransactionMQProducer producer = new TransactionMQProducer("group"); producer.setTransactionListener(new TransactionListener() { @Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { try { // 执行本地数据库操作 return LocalTransactionState.COMMIT_MESSAGE; } catch (Exception e) { return LocalTransactionState.ROLLBACK_MESSAGE; } } }); ``` #### **2. 幂等性设计** ```text 实现方法: - 唯一索引:防止重复数据插入 - 令牌机制:请求携带唯一token,服务端校验后删除 - 状态机:业务状态流转校验(如订单状态不可逆) ``` --- ### **五、高频面试问题** #### **Q1:`@Transactional`注解在哪些情况下会失效?** ```text 1. 方法非public修饰 2. 异常类型非RuntimeException且未指定rollbackFor 3. 同一类内非事务方法调用事务方法(未通过代理对象) 4. 多数据源未指定事务管理器 ``` #### **Q2:如何避免超卖问题?** ```text 解决方案: 1. 数据库层面:UPDATE库存时校验stock > 0 UPDATE product SET stock = stock - 1 WHERE id = ? AND stock > 0 2. Redis原子操作:DECR命令或Lua脚本 3. 队列削峰:下单请求进入MQ顺序处理 ``` #### **Q3:跨服务数据一致性如何保障?** ```text 典型回答: - 强一致:Seata AT模式(适合简单业务) - 最终一致:本地消息表+RocketMQ事务消息(适合高并发场景) - 补偿机制:TCC手工回滚(适合资金类敏感业务) ``` --- ### **六、实践案例** **案例**:电商系统扣减库存与生成订单的一致性 ```text 技术选型: 1. 下单服务本地事务: - 预扣库存(Redis扣减) - 写入订单表(状态为"待支付") 2. 支付回调: - 实际扣减数据库库存 - 更新订单状态为"已支付" 3. 超时未支付: - 定时任务回滚Redis预扣库存 - 订单状态改为"已取消" ``` --- 掌握以上内容后,建议结合项目经验准备**具体数字**(如:"通过Seata将分布式事务成功率从92%提升至99.8%")和**异常处理细节**(如:如何处理网络抖动导致的事务悬挂问题)。面试时重点展现**技术深度**与**系统设计思维**。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

奋斗的狍子007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值